"""SMTP email sending -- password resets and battery-threshold alerts. Config lives in the server_settings singleton row (admin-configured via /admin, see routers/pages.py), not env vars -- it's operator infrastructure a household admin sets up once through the UI, same spirit as the rest of this project's "no separate config file" stance post-redesign. Uses stdlib smtplib; no new dependency. send_email() never raises -- a broken mail server shouldn't 500 a password-reset request or a battery report; callers get a bool and log a warning on failure.""" from __future__ import annotations import email.utils import logging import smtplib import ssl from email.mime.text import MIMEText from .models import ServerSettings logger = logging.getLogger(__name__) SMTP_TIMEOUT_S = 10 def send_email(settings: ServerSettings, to_address: str, subject: str, body: str) -> bool: if not settings.smtp_host or not to_address: return False msg = MIMEText(body) msg["Subject"] = subject msg["From"] = settings.smtp_from_address or settings.smtp_username or "noreply@localhost" msg["To"] = to_address # email.mime doesn't set either of these on its own -- and a missing # Message-ID in particular is enough for a strict content filter # (e.g. Amavis's header-sanity check) to quarantine an otherwise # cleanly SPF/DKIM/DMARC-passing message outright. Domain in the # generated id matches the From address so it's traceable back here. msg["Date"] = email.utils.formatdate(localtime=True) msg["Message-ID"] = email.utils.make_msgid(domain=msg["From"].rsplit("@", 1)[-1]) try: # "ssl" (implicit TLS, port 465 typically) needs a TLS socket from # the very first byte -- SMTP_SSL, not SMTP+starttls(). Connecting # a plaintext SMTP() to a TLS-only port fails outright (garbled # banner/timeout), it doesn't degrade gracefully, so this has to # be a real branch rather than "starttls() or not". if settings.smtp_encryption == "ssl": with smtplib.SMTP_SSL( settings.smtp_host, settings.smtp_port, timeout=SMTP_TIMEOUT_S, context=ssl.create_default_context(), ) as smtp: if settings.smtp_username: smtp.login(settings.smtp_username, settings.smtp_password) smtp.send_message(msg) else: with smtplib.SMTP(settings.smtp_host, settings.smtp_port, timeout=SMTP_TIMEOUT_S) as smtp: if settings.smtp_encryption == "starttls": smtp.starttls(context=ssl.create_default_context()) if settings.smtp_username: smtp.login(settings.smtp_username, settings.smtp_password) smtp.send_message(msg) return True except (OSError, smtplib.SMTPException, ssl.SSLError) as e: logger.warning("Failed to send email to %s: %s", to_address, e) return False