""" Email service for sending transactional emails via SMTP. Supports both sync (smtplib) and async (aiosmtplib) sending. Configuration is resolved from settings JSON file, then env vars. """ import os import logging import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from typing import Optional logger = logging.getLogger(__name__) def _get_smtp_config() -> dict: """Resolve SMTP config from admin settings file then env vars.""" config = {} # Read from admin provider settings (same file as /admin/settings page) try: import json from pathlib import Path settings_path = Path("data/provider_settings.json") if settings_path.exists(): with open(settings_path, "r") as f: settings = json.load(f) smtp = settings.get("smtp", {}) if smtp and smtp.get("host"): config["host"] = smtp.get("host", "") config["port"] = smtp.get("port", 587) config["user"] = smtp.get("username", "") config["password"] = smtp.get("password", "") config["from_email"] = smtp.get("from_email", "") or smtp.get("username", "") config["use_tls"] = smtp.get("use_tls", True) except Exception: pass # Env vars fill gaps config.setdefault("host", os.getenv("SMTP_HOST", "")) config.setdefault("port", int(os.getenv("SMTP_PORT", "587"))) config.setdefault("user", os.getenv("SMTP_USER", os.getenv("SMTP_USERNAME", ""))) config.setdefault("password", os.getenv("SMTP_PASSWORD", "")) config.setdefault("from_email", os.getenv("SMTP_FROM_EMAIL", os.getenv("SMTP_USER", os.getenv("SMTP_USERNAME", "")))) config.setdefault("use_tls", os.getenv("SMTP_USE_TLS", "true").lower() == "true") return config def is_smtp_configured() -> bool: """Check if SMTP is properly configured.""" cfg = _get_smtp_config() return bool(cfg.get("host") and cfg.get("user")) def send_email(to: str, subject: str, body: str) -> bool: """Send an email via SMTP (sync). Returns True if sent successfully.""" cfg = _get_smtp_config() if not cfg.get("host"): logger.warning("SMTP not configured, email not sent") return False try: msg = MIMEMultipart("alternative") msg["Subject"] = subject msg["From"] = cfg["from_email"] msg["To"] = to msg.attach(MIMEText(body, "html")) port = int(cfg.get("port", 587)) use_tls = cfg.get("use_tls", True) if use_tls: server = smtplib.SMTP(cfg["host"], port) server.starttls() else: server = smtplib.SMTP(cfg["host"], port) if cfg.get("user") and cfg.get("password"): server.login(cfg["user"], cfg["password"]) server.sendmail(cfg["from_email"], [to], msg.as_string()) server.quit() logger.info(f"Email sent to {to}: {subject}") return True except Exception as e: logger.error(f"Failed to send email to {to}: {e}") return False async def send_email_async(to: str, subject: str, body: str) -> bool: """Send an email via SMTP (async). Returns True if sent successfully.""" cfg = _get_smtp_config() if not cfg.get("host"): logger.warning("SMTP not configured, email not sent") return False try: from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart msg = MIMEMultipart("alternative") msg["Subject"] = subject msg["From"] = cfg["from_email"] msg["To"] = to msg.attach(MIMEText(body, "html")) port = int(cfg.get("port", 587)) # Use sync smtplib inside a thread for reliability (aiosmtplib has quirks with raw messages) import asyncio return await asyncio.get_event_loop().run_in_executor( None, send_email, to, subject, body ) except Exception as e: logger.error(f"Failed to send email (async) to {to}: {e}") return False