2 Commits
Author SHA1 Message Date
tfaour c1c803b497 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.
2026-07-22 01:07:50 -04:00
tfaour 8e10ca540e Add SMTP email: password reset + per-frame battery-threshold alerts
Build and push server image / build-and-push (push) Successful in 40s
Admin-configured SMTP (server/port/username/password/from address/
STARTTLS, a singleton server_settings row set from /admin -- not env
vars, since it's operator infrastructure a household admin sets up
once through the UI) powers two features, both requiring the relevant
user to have an email set in their own Settings:

- "Forgot password?" on /login emails a one-hour single-use reset link
  (password_reset_tokens table). The endpoint always returns the same
  generic "check your email" response regardless of whether the address
  matched an account, so it can't be used to enumerate registered users.
- A frame's Configuration tab can set a battery-alert threshold
  (Frame.battery_alert_threshold_pct, -1 = disabled); POST /frame/battery
  emails the owner the first time a report drops to or below it, then
  stays quiet for the rest of that discharge cycle (battery_alert_sent,
  reset alongside battery_history whenever the existing recharge-jump
  detection fires) -- not once per wake.

New app/mail.py wraps stdlib smtplib (no new dependency); send_email()
never raises, so a broken mail server can't 500 a battery report or a
password-reset request. Schema migration v2 adds users.email and the
two frame columns via ALTER TABLE (safe against the live, already-
populated database) plus the two new tables via the existing
create_all-based migration runner.

Verified against a real (already-migrated, real user/frame data)
database: the v1->v2 migration, admin SMTP config + test-email button,
full forgot/reset-password roundtrip (including single-use token
invalidation and the no-enumeration response), and the battery alert
firing exactly once per crossing against a hand-rolled fake SMTP
server -- all via curl end-to-end, plus the standing legacy-device
curl suite to confirm the device protocol is untouched.
2026-07-22 00:51:54 -04:00