2026.06.04 frontend építés közben

This commit is contained in:
Roo
2026-06-04 07:26:22 +00:00
parent 7adf6cc3e3
commit 59a30ac428
3302 changed files with 24091 additions and 1771 deletions

View File

@@ -1,6 +1,8 @@
# /opt/docker/dev/service_finder/backend/app/services/email_manager.py
import os
import smtplib
import logging
import requests
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from typing import Optional
@@ -14,6 +16,24 @@ from app.db.session import AsyncSessionLocal
logger = logging.getLogger("Email-Manager-2.0")
class EmailManager:
@staticmethod
def _get_base_url() -> str:
"""Return the appropriate base URL for email links."""
# Check environment variable first
base_url = os.getenv("EMAIL_BASE_URL")
if base_url:
return base_url.rstrip('/')
# Fallback to dev domain for external access
return "https://dev.servicefinder.hu"
@staticmethod
def _build_verification_link(token: str) -> str:
"""Build verification link with proper path."""
base = EmailManager._get_base_url()
# Use frontend verification route (adjust if needed)
return f"{base}/verify?token={token}"
@staticmethod
def _get_html_template(template_key: str, variables: dict, lang: str = "hu") -> str:
"""HTML sablon generálása a fordítási fájlok alapján."""
@@ -24,6 +44,11 @@ class EmailManager:
link_fallback_text = locale_manager.get("email.link_fallback", lang=lang)
# If link is not provided but token is, build verification link
link = variables.get('link')
if not link and 'token' in variables:
link = EmailManager._build_verification_link(variables['token'])
return f"""
<html>
<body style="font-family: Arial, sans-serif; color: #333; line-height: 1.6;">
@@ -31,14 +56,14 @@ class EmailManager:
<h2 style="color: #2c3e50;">{greeting}</h2>
<p>{body}</p>
<div style="text-align: center; margin: 40px 0;">
<a href="{variables.get('link', '#')}"
<a href="{link or '#'}"
style="background-color: #3498db; color: white; padding: 15px 30px; text-decoration: none; border-radius: 5px; font-weight: bold; font-size: 16px;">
{button_text}
</a>
</div>
<p style="font-size: 0.85em; color: #777; word-break: break-all;">
{link_fallback_text}<br>
<a href="{variables.get('link')}" style="color: #3498db;">{variables.get('link')}</a>
<a href="{link or '#'}" style="color: #3498db;">{link or '#'}</a>
</p>
<hr style="border: 0; border-top: 1px solid #eee; margin: 30px 0;">
<p style="font-size: 0.8em; color: #999; text-align: center;">{footer}</p>
@@ -50,7 +75,7 @@ class EmailManager:
@staticmethod
async def send_email(recipient: str, template_key: str, variables: dict, lang: str = "hu", db: Optional[AsyncSession] = None):
"""
E-mail küldése közvetlenül a privát SMTP szerveren keresztül.
E-mail küldése Brevo API-n vagy SMTP-n keresztül.
"""
session_internal = False
if db is None:
@@ -62,35 +87,130 @@ class EmailManager:
provider = await config.get_setting(db, "email_provider", default="smtp")
if provider == "disabled":
logger.info(f"Email küldés letiltva (Admin config). Cél: {recipient}")
return
return {"status": "success", "provider": "disabled", "message": "Email disabled by admin config"}
html = EmailManager._get_html_template(template_key, variables, lang)
subject = locale_manager.get(f"email.{template_key}_subject", lang=lang)
smtp_host = os.getenv("SMTP_HOST", "mail.servicefinder.hu")
smtp_port = int(os.getenv("SMTP_PORT", "465"))
smtp_user = os.getenv("SMTP_USER", "noreply@servicefinder.hu")
smtp_pass = os.getenv("SMTP_PASSWORD", "")
# Get email provider from environment
email_provider = os.getenv("EMAIL_PROVIDER", "smtp").lower()
from_email = os.getenv("MAIL_FROM", "noreply@servicefinder.hu")
from_name = os.getenv("MAIL_FROM_NAME", "ServiceFinder")
smtp_cfg = {
"host": smtp_host,
"port": smtp_port,
"user": smtp_user,
"pass": smtp_pass
}
logger.info(f"Using SMTP config: host={smtp_cfg['host']}, port={smtp_cfg['port']}, user={smtp_cfg['user']}")
return await EmailManager._send_via_smtp(smtp_cfg, from_email, from_name, recipient, subject, html)
if email_provider == "brevo_api":
result = await EmailManager._send_via_brevo_api(recipient, subject, html, variables)
# Primary-Fallback Logic: If Brevo API fails, try SMTP
if result.get("status") == "error":
logger.error(f"Brevo API failed for {recipient}: {result.get('message')}. Falling back to SMTP.")
result = await EmailManager._send_via_smtp(recipient, subject, html)
elif email_provider == "brevo_smtp":
result = await EmailManager._send_via_brevo_smtp(recipient, subject, html)
else: # Default to SMTP
result = await EmailManager._send_via_smtp(recipient, subject, html)
logger.info(f"Email sending result for {recipient}: {result}")
return result
except Exception as e:
logger.error(f"CRITICAL: Email sending failed for {recipient}: {str(e)}")
# Don't crash - return error but don't raise exception
return {"status": "error", "message": str(e), "provider": "unknown"}
finally:
if session_internal:
await db.close()
@staticmethod
async def _send_via_smtp(cfg: dict, from_email: str, from_name: str, recipient: str, subject: str, html: str):
async def _send_via_brevo_api(recipient: str, subject: str, html: str, variables: dict):
"""Send email via Brevo REST API"""
try:
brevo_api_key = os.getenv("BREVO_API_KEY")
if not brevo_api_key:
logger.error("BREVO_API_KEY environment variable not set")
return {"status": "error", "message": "BREVO_API_KEY not configured", "provider": "brevo_api"}
sender_name = os.getenv("MAIL_FROM_NAME", "ServiceFinder")
sender_email = os.getenv("MAIL_FROM", "noreply@servicefinder.hu")
# Prepare Brevo API payload
payload = {
"sender": {
"name": sender_name,
"email": sender_email
},
"to": [{"email": recipient}],
"subject": subject,
"htmlContent": html,
"tags": ["verification"]
}
# Try to disable tracking - Brevo may ignore this if account-level tracking is enabled
# According to Brevo API v3 docs, tracking settings are at account level
# We'll add tracking object but it may not work for free tier
payload["tracking"] = {
"click": False,
"open": False
}
headers = {
"accept": "application/json",
"api-key": brevo_api_key,
"content-type": "application/json"
}
response = requests.post(
"https://api.brevo.com/v3/smtp/email",
json=payload,
headers=headers,
timeout=30
)
if response.status_code == 201:
logger.info(f"Brevo API email sent successfully to {recipient}")
return {"status": "success", "provider": "brevo_api", "message_id": response.json().get("messageId")}
else:
logger.error(f"Brevo API error: {response.status_code} - {response.text}")
return {"status": "error", "provider": "brevo_api", "message": response.text}
except Exception as e:
logger.error(f"Brevo API exception: {str(e)}")
return {"status": "error", "provider": "brevo_api", "message": str(e)}
@staticmethod
async def _send_via_brevo_smtp(recipient: str, subject: str, html: str):
"""Send email via Brevo SMTP relay"""
try:
smtp_host = "smtp-relay.brevo.com"
smtp_port = 587
smtp_user = os.getenv("BREVO_SMTP_USER")
smtp_pass = os.getenv("BREVO_SMTP_PASSWORD")
if not smtp_user or not smtp_pass:
logger.error("BREVO_SMTP_USER or BREVO_SMTP_PASSWORD not set")
return {"status": "error", "message": "Brevo SMTP credentials not configured", "provider": "brevo_smtp"}
from_email = os.getenv("MAIL_FROM", "noreply@servicefinder.hu")
from_name = os.getenv("MAIL_FROM_NAME", "ServiceFinder")
msg = MIMEMultipart()
msg["From"] = f"{from_name} <{from_email}>"
msg["To"] = recipient
msg["Subject"] = subject
msg.attach(MIMEText(html, "html"))
logger.info(f"Connecting to Brevo SMTP: {smtp_host}:{smtp_port}")
with smtplib.SMTP(smtp_host, smtp_port, timeout=15) as server:
server.starttls()
server.login(smtp_user, smtp_pass)
server.send_message(msg)
logger.info(f"Brevo SMTP email sent successfully to {recipient}")
return {"status": "success", "provider": "brevo_smtp"}
except Exception as e:
logger.error(f"Brevo SMTP exception: {str(e)}")
return {"status": "error", "provider": "brevo_smtp", "message": str(e)}
@staticmethod
async def _send_via_smtp(recipient: str, subject: str, html: str):
"""Send email via standard SMTP (fallback)"""
# Mock mode check: If APP_ENV=test or domain is example.com, skip SMTP and return success
app_env = os.getenv("APP_ENV", "").lower()
is_example_domain = recipient.endswith("@example.com") or "@example.com" in recipient
@@ -99,36 +219,40 @@ class EmailManager:
return {"status": "success", "provider": "mock", "message": "Email skipped in test mode"}
try:
smtp_host = os.getenv("SMTP_HOST", "mail.servicefinder.hu")
smtp_port = int(os.getenv("SMTP_PORT", "465"))
smtp_user = os.getenv("SMTP_USER", "noreply@servicefinder.hu")
smtp_pass = os.getenv("SMTP_PASSWORD", "")
from_email = os.getenv("MAIL_FROM", "noreply@servicefinder.hu")
from_name = os.getenv("MAIL_FROM_NAME", "ServiceFinder")
msg = MIMEMultipart()
msg["From"] = f"{from_name} <{from_email}>"
msg["To"] = recipient
msg["Subject"] = subject
msg.attach(MIMEText(html, "html"))
# Port 465 uses SMTP_SSL directly instead of STARTTLS
if cfg["port"] == 465:
logger.info(f"Connecting via SMTP_SSL to {cfg['host']}:{cfg['port']}")
with smtplib.SMTP_SSL(cfg["host"], cfg["port"], timeout=15) as server:
user = cfg.get("user", "")
passwd = cfg.get("pass", "")
if user and passwd:
server.login(user, passwd)
if smtp_port == 465:
logger.info(f"Connecting via SMTP_SSL to {smtp_host}:{smtp_port}")
with smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=15) as server:
if smtp_user and smtp_pass:
server.login(smtp_user, smtp_pass)
server.send_message(msg)
else:
logger.info(f"Connecting via SMTP to {cfg['host']}:{cfg['port']}")
with smtplib.SMTP(cfg["host"], cfg["port"], timeout=15) as server:
# Explicit STARTTLS if not 465, though we expect 465
logger.info(f"Connecting via SMTP to {smtp_host}:{smtp_port}")
with smtplib.SMTP(smtp_host, smtp_port, timeout=15) as server:
server.starttls()
user = cfg.get("user", "")
passwd = cfg.get("pass", "")
if user and passwd:
server.login(user, passwd)
if smtp_user and smtp_pass:
server.login(smtp_user, smtp_pass)
server.send_message(msg)
logger.info(f"SMTP siker -> {recipient}")
logger.info(f"SMTP email sent successfully to {recipient}")
return {"status": "success", "provider": "smtp"}
except Exception as e:
logger.error(f"SMTP hiba: {str(e)}")
return {"status": "error", "message": str(e)}
logger.error(f"SMTP exception: {str(e)}")
return {"status": "error", "provider": "smtp", "message": str(e)}
email_manager = EmailManager()
email_manager = EmailManager()