257 lines
12 KiB
Python
Executable File
257 lines
12 KiB
Python
Executable File
# /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"""
|
|
<html>
|
|
<body style="font-family: Arial, sans-serif; color: #333; line-height: 1.6;">
|
|
<div style="max-width: 600px; margin: 0 auto; border: 1px solid #ddd; padding: 30px; border-radius: 10px;">
|
|
<h2 style="color: #2c3e50;">{greeting}</h2>
|
|
<p>{body}</p>
|
|
<div style="text-align: center; margin: 40px 0;">
|
|
<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="{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>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
@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() |