feat(email): delivery you can see: each alert's email in history, outgoing email on Settings -> System (#5509)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m10s
CI / unit (push) Successful in 1m25s
CI / publish (push) Successful in 1m1s

#5509 "Delivery you can see: each alert's email state in history, and
outgoing email on Settings -> System" (milestone 501).

- Alerts -> History: an alert's email reads from the outbox (#5506):
  "gathering" with when it sends, "sending", "retrying" with the attempt,
  next try and the server's last answer, "sent", or "failed" naming each
  reader it didn't reach and why (outbox.delivery / email_states). Events
  from before the outbox keep what they recorded. The Notified column
  shows once an alert has a queued email.
- Settings -> System gets "Outgoing email": every email of the last 7
  days not yet sent, failures first, then the longest retrying, with
  recipient, kind, state and last error, and a count of what was sent.
  Failures and anything still retrying after 5 minutes lead the page's
  verdict (outbox.outgoing).
- get_alert_history (MCP) adds `email`: the same delivery state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 16:12:40 -04:00
co-authored by Claude Opus 5.5
parent 249d22e41f
commit defad2e98b
14 changed files with 375 additions and 26 deletions
+20
View File
@@ -223,3 +223,23 @@ def test_the_smtp_guard_can_fail():
assert _smtp_openers({"x.py": " with smtplib.SMTP(host) as s:"}) == {"x.py"}
assert _smtp_openers({"y.py": " smtp = notifications.smtp_open(cfg)"}) == {"y.py"}
assert _smtp_openers({"z.py": "# smtp_open(cfg) is the outbox's\ndef smtp_open(cfg):"}) == set()
# ── How an alert's email reads in history (#5509) ────────────────────────────
def test_an_alerts_email_reads_by_its_least_finished_copy():
later, sent_at = NOW + timedelta(minutes=2), NOW + timedelta(seconds=5)
held = _row(state="held", send_after=later, created_at=NOW)
retrying = _row(recipient="b@x", state="pending", attempts=3, next_attempt_at=later,
last_error="451 greylisted", created_at=NOW)
sent = _row(recipient="c@x", state="sent", attempts=1, sent_at=sent_at, created_at=NOW)
failed = _row(recipient="d@x", state="failed", attempts=1, first_attempt_at=NOW,
last_error="550 no such user", created_at=NOW)
first = _row(recipient="e@x", state="pending", attempts=0, next_attempt_at=NOW, created_at=NOW)
assert outbox.delivery([sent, held, failed]) == {"state": "held", "at": later, "attempts": 0, "error": None}
assert outbox.delivery([sent, retrying]) == {"state": "retrying", "at": later, "attempts": 3,
"error": "451 greylisted"}
assert outbox.delivery([first])["state"] == "sending"
assert outbox.delivery([sent, failed]) == {"state": "failed", "at": NOW, "attempts": 1,
"error": "d@x: 550 no such user"}
assert outbox.delivery([sent]) == {"state": "sent", "at": sent_at, "attempts": 1, "error": None}