# /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 from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.core.i18n import locale_manager from app.services.config_service import config from app.db.session import AsyncSessionLocal logger = logging.getLogger("Email-Manager-2.0") class EmailManager: @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. The caller MUST provide a 'link' key in variables. No fallback guessing. """ greeting = locale_manager.get(f"email.{template_key}_greeting", lang=lang, **variables) body = locale_manager.get(f"email.{template_key}_body", lang=lang, **variables) button_text = locale_manager.get(f"email.{template_key}_button", lang=lang) footer = locale_manager.get(f"email.{template_key}_footer", lang=lang) link_fallback_text = locale_manager.get("email.link_fallback", lang=lang) # Strict: only use variables.get('link'), no guessing link = variables.get('link') if not link: logger.error(f"No 'link' variable provided for template '{template_key}' (lang={lang})") return f"""
""" @staticmethod async def send_email(recipient: str, template_key: str, variables: dict, lang: str = "hu", db: Optional[AsyncSession] = None): """ E-mail küldése Brevo API-n vagy SMTP-n keresztül. """ session_internal = False if db is None: db = AsyncSessionLocal() session_internal = True try: # --- FIX: Wrap DB config lookup in try-except with env fallback --- # If the DB query fails (e.g. enum mismatch), fall back to environment variable try: provider = await config.get_setting(db, "email_provider", default=None) except Exception as cfg_err: logger.warning(f"DB config error reading email_provider: {cfg_err}. Falling back to ENV variables...") provider = None if provider == "disabled": logger.info(f"Email küldés letiltva (Admin config). Cél: {recipient}") 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) # Determine email provider: DB config > ENV > fallback to 'smtp' email_provider_raw = provider if provider else os.getenv("EMAIL_PROVIDER", "smtp") email_provider = str(email_provider_raw).lower().strip() 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_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 if app_env == "test" or is_example_domain: logger.info(f"Mock mode: Skipping SMTP for {recipient} (APP_ENV={app_env}, is_example_domain={is_example_domain})") 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 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 {smtp_host}:{smtp_port}") with smtplib.SMTP(smtp_host, smtp_port, timeout=15) as server: # Try STARTTLS, but fall back to plain if server doesn't support it # (e.g. local Mailpit for testing) try: server.starttls() except smtplib.SMTPNotSupportedError: logger.warning(f"STARTTLS not supported by {smtp_host}:{smtp_port}, sending in plain text") except Exception: logger.warning(f"STARTTLS failed on {smtp_host}:{smtp_port}, sending in plain text") if smtp_user and smtp_pass: server.login(smtp_user, smtp_pass) server.send_message(msg) logger.info(f"SMTP email sent successfully to {recipient}") return {"status": "success", "provider": "smtp"} except Exception as e: logger.error(f"SMTP exception: {str(e)}") return {"status": "error", "provider": "smtp", "message": str(e)} email_manager = EmailManager()