import os import smtplib from email.message import EmailMessage def _truthy(v: str | None) -> bool: if v is None: return False return v.strip().lower() in ("1", "true", "yes", "on") def send_email(*, to_email: str, subject: str, body_text: str): """Send a plain-text email using SMTP settings from: 1) Environment variables (highest priority) 2) Admin-configured settings stored in the database (AppSettings) Required env vars: - SMTP_HOST - SMTP_PORT - SMTP_USERNAME - SMTP_PASSWORD - SMTP_FROM (defaults to SMTP_USERNAME) Optional: - SMTP_STARTTLS (default: "1") - SMTP_TIMEOUT_SECONDS (default: "10") - SMTP_DEBUG (default: "0") - set to 1 to print SMTP conversation to console """ # Pull optional defaults from DB (if available). This keeps backwards compatibility: # existing deployments that only use env vars keep working unchanged. db_defaults = {} try: # Local import to avoid import cycles and to keep this module lightweight. from flask import current_app from .extensions import db from .models import AppSettings # Only try if app context exists (send_email is called inside requests/CLI normally). _ = current_app # noqa: F841 s = db.session.get(AppSettings, 1) if s: db_defaults = { "host": s.smtp_host, "port": s.smtp_port, "username": s.smtp_username, "password": s.smtp_password, "from_email": s.smtp_from, "starttls": s.smtp_starttls, "timeout": s.smtp_timeout_seconds, "debug": s.smtp_debug, } except Exception: # Best-effort; if DB isn't ready we fall back to env vars only. db_defaults = {} host = os.environ.get("SMTP_HOST") or db_defaults.get("host") port = int(os.environ.get("SMTP_PORT") or db_defaults.get("port") or 587) username = os.environ.get("SMTP_USERNAME") or db_defaults.get("username") password = os.environ.get("SMTP_PASSWORD") or db_defaults.get("password") from_email = os.environ.get("SMTP_FROM") or db_defaults.get("from_email") or username starttls = _truthy(os.environ.get("SMTP_STARTTLS")) if os.environ.get("SMTP_STARTTLS") is not None else bool( db_defaults.get("starttls") if db_defaults.get("starttls") is not None else True ) timeout = float(os.environ.get("SMTP_TIMEOUT_SECONDS") or db_defaults.get("timeout") or 10) debug = _truthy(os.environ.get("SMTP_DEBUG")) if os.environ.get("SMTP_DEBUG") is not None else bool( db_defaults.get("debug") or False ) missing = [] if not host: missing.append("SMTP_HOST") if not username: missing.append("SMTP_USERNAME") if not password: missing.append("SMTP_PASSWORD") if not from_email: missing.append("SMTP_FROM") if missing: raise RuntimeError( "Missing SMTP configuration: " + ", ".join(missing) + ". Set them as environment variables (or configure them in Admin → Settings)." ) msg = EmailMessage() msg["From"] = from_email msg["To"] = to_email msg["Subject"] = subject # Helps when SMTP providers force the authenticated mailbox as envelope sender, # but still allow replies to go to the desired address. if from_email and username and from_email != username: msg["Reply-To"] = from_email msg.set_content(body_text) with smtplib.SMTP(host, port, timeout=timeout) as smtp: if debug: smtp.set_debuglevel(1) smtp.ehlo() if starttls: smtp.starttls() smtp.ehlo() smtp.login(username, password) # Pass explicit envelope-from to avoid falling back to the authenticated user. # Note: some SMTP providers will still override this for anti-spoofing. smtp.send_message(msg, from_addr=from_email, to_addrs=[to_email])