From f01de7a8e69451cc3228071812cf0da9473cbd8a Mon Sep 17 00:00:00 2001 From: bramval Date: Fri, 23 Jan 2026 22:00:12 +0100 Subject: [PATCH] mail fix --- app/email_utils.py | 83 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 59 insertions(+), 24 deletions(-) diff --git a/app/email_utils.py b/app/email_utils.py index 6266d84..c5ac7c9 100644 --- a/app/email_utils.py +++ b/app/email_utils.py @@ -12,10 +12,18 @@ def _truthy(v: str | None) -> bool: 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) + 1) Admin-configured settings stored in the database (AppSettings) (highest priority) + 2) Environment variables (fallback) - Required env vars: + If you *do* want environment variables to override DB settings (e.g. in production), + set SMTP_OVERRIDE_DB=1. + + Required configuration (either from DB or env): + - host + - username + - password + + When using env vars, the names are: - SMTP_HOST - SMTP_PORT - SMTP_USERNAME @@ -28,9 +36,12 @@ def send_email(*, to_email: str, subject: str, body_text: str): - 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 = {} + # Load DB settings (best-effort). If the DB has a complete SMTP config, we will use + # it even if environment variables are present. + # + # This fixes the common deployment situation where a .env/.flaskenv provides SMTP_* + # values that unintentionally override admin-configured settings. + db_defaults: dict[str, object] = {} try: # Local import to avoid import cycles and to keep this module lightweight. from flask import current_app @@ -39,7 +50,8 @@ def send_email(*, to_email: str, subject: str, body_text: str): from .models import AppSettings # Only try if app context exists (send_email is called inside requests/CLI normally). - _ = current_app # noqa: F841 + # Accessing the LocalProxy will raise if there is no active app context. + current_app._get_current_object() s = db.session.get(AppSettings, 1) if s: db_defaults = { @@ -56,33 +68,55 @@ def send_email(*, to_email: str, subject: str, body_text: str): # 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 + # DB-first selection rules: + # - If *any* SMTP field is configured in DB, treat DB as authoritative (and do NOT + # silently mix in env vars). This avoids confusion where partial DB config still + # results in env vars being used. + # - Env override is only possible with SMTP_OVERRIDE_DB=1. + override_db = _truthy(os.environ.get("SMTP_OVERRIDE_DB")) + db_config_present = bool( + db_defaults.get("host") + or db_defaults.get("username") + or db_defaults.get("password") + or db_defaults.get("from_email") + or (db_defaults.get("port") is not None) ) + if db_config_present and not override_db: + config_source = "db" + host = str(db_defaults.get("host") or "") or None + port = int(db_defaults.get("port") or 587) + username = str(db_defaults.get("username") or "") or None + password = str(db_defaults.get("password") or "") or None + from_email = (str(db_defaults.get("from_email")) if db_defaults.get("from_email") else None) or username + starttls = bool(db_defaults.get("starttls") if db_defaults.get("starttls") is not None else True) + timeout = float(db_defaults.get("timeout") or 10) + debug = bool(db_defaults.get("debug") or False) + else: + config_source = "env" + host = os.environ.get("SMTP_HOST") + port = int(os.environ.get("SMTP_PORT") or 587) + username = os.environ.get("SMTP_USERNAME") + password = os.environ.get("SMTP_PASSWORD") + from_email = os.environ.get("SMTP_FROM") or username + starttls = _truthy(os.environ.get("SMTP_STARTTLS")) if os.environ.get("SMTP_STARTTLS") is not None else True + timeout = float(os.environ.get("SMTP_TIMEOUT_SECONDS") or 10) + debug = _truthy(os.environ.get("SMTP_DEBUG")) if os.environ.get("SMTP_DEBUG") is not None else False + missing = [] if not host: - missing.append("SMTP_HOST") + missing.append("host") if not username: - missing.append("SMTP_USERNAME") + missing.append("username") if not password: - missing.append("SMTP_PASSWORD") + missing.append("password") if not from_email: - missing.append("SMTP_FROM") + missing.append("from_email") if missing: raise RuntimeError( - "Missing SMTP configuration: " + f"Missing SMTP configuration ({config_source}): " + ", ".join(missing) - + ". Set them as environment variables (or configure them in Admin → Settings)." + + ". Configure it in Admin → Settings (or set SMTP_* environment variables)." ) msg = EmailMessage() @@ -98,6 +132,7 @@ def send_email(*, to_email: str, subject: str, body_text: str): with smtplib.SMTP(host, port, timeout=timeout) as smtp: if debug: smtp.set_debuglevel(1) + print(f"[email_utils] Using SMTP config from: {config_source}") smtp.ehlo() if starttls: smtp.starttls()