Files
service-finder/backend/app/services/email_manager.py
2026-06-04 07:26:22 +00:00

258 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_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"""
<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:
# 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()