This commit is contained in:
2026-01-23 22:00:12 +01:00
parent 97e17854b9
commit f01de7a8e6

View File

@@ -12,10 +12,18 @@ def _truthy(v: str | None) -> bool:
def send_email(*, to_email: str, subject: str, body_text: str): def send_email(*, to_email: str, subject: str, body_text: str):
"""Send a plain-text email using SMTP settings from: """Send a plain-text email using SMTP settings from:
1) Environment variables (highest priority) 1) Admin-configured settings stored in the database (AppSettings) (highest priority)
2) Admin-configured settings stored in the database (AppSettings) 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_HOST
- SMTP_PORT - SMTP_PORT
- SMTP_USERNAME - 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 - SMTP_DEBUG (default: "0") - set to 1 to print SMTP conversation to console
""" """
# Pull optional defaults from DB (if available). This keeps backwards compatibility: # Load DB settings (best-effort). If the DB has a complete SMTP config, we will use
# existing deployments that only use env vars keep working unchanged. # it even if environment variables are present.
db_defaults = {} #
# 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: try:
# Local import to avoid import cycles and to keep this module lightweight. # Local import to avoid import cycles and to keep this module lightweight.
from flask import current_app from flask import current_app
@@ -39,7 +50,8 @@ def send_email(*, to_email: str, subject: str, body_text: str):
from .models import AppSettings from .models import AppSettings
# Only try if app context exists (send_email is called inside requests/CLI normally). # 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) s = db.session.get(AppSettings, 1)
if s: if s:
db_defaults = { 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. # Best-effort; if DB isn't ready we fall back to env vars only.
db_defaults = {} db_defaults = {}
host = os.environ.get("SMTP_HOST") or db_defaults.get("host") # DB-first selection rules:
port = int(os.environ.get("SMTP_PORT") or db_defaults.get("port") or 587) # - If *any* SMTP field is configured in DB, treat DB as authoritative (and do NOT
username = os.environ.get("SMTP_USERNAME") or db_defaults.get("username") # silently mix in env vars). This avoids confusion where partial DB config still
password = os.environ.get("SMTP_PASSWORD") or db_defaults.get("password") # results in env vars being used.
from_email = os.environ.get("SMTP_FROM") or db_defaults.get("from_email") or username # - Env override is only possible with SMTP_OVERRIDE_DB=1.
starttls = _truthy(os.environ.get("SMTP_STARTTLS")) if os.environ.get("SMTP_STARTTLS") is not None else bool( override_db = _truthy(os.environ.get("SMTP_OVERRIDE_DB"))
db_defaults.get("starttls") if db_defaults.get("starttls") is not None else True db_config_present = bool(
) db_defaults.get("host")
timeout = float(os.environ.get("SMTP_TIMEOUT_SECONDS") or db_defaults.get("timeout") or 10) or db_defaults.get("username")
debug = _truthy(os.environ.get("SMTP_DEBUG")) if os.environ.get("SMTP_DEBUG") is not None else bool( or db_defaults.get("password")
db_defaults.get("debug") or False 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 = [] missing = []
if not host: if not host:
missing.append("SMTP_HOST") missing.append("host")
if not username: if not username:
missing.append("SMTP_USERNAME") missing.append("username")
if not password: if not password:
missing.append("SMTP_PASSWORD") missing.append("password")
if not from_email: if not from_email:
missing.append("SMTP_FROM") missing.append("from_email")
if missing: if missing:
raise RuntimeError( raise RuntimeError(
"Missing SMTP configuration: " f"Missing SMTP configuration ({config_source}): "
+ ", ".join(missing) + ", ".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() 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: with smtplib.SMTP(host, port, timeout=timeout) as smtp:
if debug: if debug:
smtp.set_debuglevel(1) smtp.set_debuglevel(1)
print(f"[email_utils] Using SMTP config from: {config_source}")
smtp.ehlo() smtp.ehlo()
if starttls: if starttls:
smtp.starttls() smtp.starttls()