SMTP: implicit TLS (port 465) support + fix quarantined mail
Build and push server image / build-and-push (push) Successful in 38s

Two real fixes to app/mail.py, both found by testing against an actual
mail server rather than just a fake stub:

- Replaces the STARTTLS-only smtp_use_tls boolean with a three-way
  smtp_encryption ("none"/"starttls"/"ssl"). Implicit TLS (port 465,
  what Purelymail and most providers offer alongside 587/STARTTLS) is a
  different handshake entirely -- TLS from the first byte, not a
  plaintext connection that gets upgraded -- so it needs its own
  smtplib.SMTP_SSL code path, not just a skipped starttls() call.
  Schema migration v3 adds the column, backfills it from the old
  boolean, and drops the boolean (safe on a live, populated DB).

- Outgoing mail was missing Date and Message-ID headers -- email.mime
  doesn't set either automatically, and a missing Message-ID in
  particular is enough for a strict content filter (confirmed via a
  real Postfix+Amavis mail server's logs: SPF/DKIM/DMARC all passed
  cleanly, but Amavis quarantined the message as "BAD-HEADER-0" purely
  for the missing id) to silently swallow an otherwise-legitimate
  email, even though smtplib reports success -- the send genuinely
  succeeds to the relay, it just never survives the recipient's own
  filtering. Both headers are now set, with the Message-ID's domain
  matching the From address.

Verified: SMTP_SSL path against a hand-rolled implicit-TLS fake server
(self-signed cert, client-side verification relaxed only in the test
harness -- production code keeps ssl.create_default_context()'s real
verification), the v2->v3 migration against live data, the full admin
SMTP-save + test-email round trip over HTTP, and the standing legacy-
device curl suite.
This commit is contained in:
2026-07-22 01:07:50 -04:00
parent 8e10ca540e
commit c1c803b497
5 changed files with 58 additions and 14 deletions
+14
View File
@@ -39,9 +39,23 @@ def _migration_2(conn) -> None:
Base.metadata.create_all(bind=conn) # creates the two new tables only; existing ones untouched
def _migration_3(conn) -> None:
"""Replaces the STARTTLS-or-nothing smtp_use_tls boolean with a
three-way smtp_encryption ("none"/"starttls"/"ssl") -- implicit TLS
(port 465 typically) is a different handshake entirely, not just a
skipped starttls() call, so it needs its own connection path in
app/mail.py."""
conn.execute(text("ALTER TABLE server_settings ADD COLUMN smtp_encryption TEXT NOT NULL DEFAULT 'starttls'"))
conn.execute(text(
"UPDATE server_settings SET smtp_encryption = CASE WHEN smtp_use_tls THEN 'starttls' ELSE 'none' END"
))
conn.execute(text("ALTER TABLE server_settings DROP COLUMN smtp_use_tls"))
MIGRATIONS = [
(1, _migration_1),
(2, _migration_2),
(3, _migration_3),
]