# /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_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.""" 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) # 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"""

{greeting}

{body}

{button_text}

{link_fallback_text}
{link or '#'}


{footer}

""" @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: # Check if emails are disabled via DB config 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 {"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) # Get email provider from environment email_provider = os.getenv("EMAIL_PROVIDER", "smtp").lower() 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: server.starttls() 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()