feat(email): the report, invites, resets and test sends go through the outbox; only it talks SMTP (#5508)
#5508 "Every other email through the outbox: weekly report, invites, password resets, and the test buttons" (milestone 501). - outbox.send_each queues one email per reader (claimed, so the sender doesn't take it too) and tries them at once, for a page that answers with what happened; outbox.said words the result. attempt takes a cfg for a test of the form's unsaved settings and returns the settled rows. - Weekly report (scheduled and Send now) and invites: queued, tried at once; one the server couldn't take yet is retried for an hour. An invite that didn't go shows its link with the reason, as before. - Password reset: queued in the reset link's own transaction; the sender delivers it on its next tick. - Settings -> Notifications test email and a rule's Send test: queued and recorded with retry=False, tried once (System 156 ruling). - Removed mail.send, notifications._send_email and _smtp_send_sync; dispatch_notifications is webhook only. A guard asserts only the outbox (and smtp_open itself) opens an SMTP connection, with a self-test. Also fixes run 8941 (#5507's integration test): its Also-send-to addresses were "@test", which is_real_address rightly drops; they are example.com now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -266,9 +266,9 @@ async def active_count(db) -> int:
|
||||
|
||||
def notify_targets(smtp: dict, webhook: dict, accounts: int = 0) -> list[str]:
|
||||
"""Where an alert's notifications go, in words, by the same tests
|
||||
core.notifications.dispatch_notifications sends by: email when a server
|
||||
is set and `accounts` (that get alerts) or Also send to addresses exist,
|
||||
a webhook when its URL is. Empty: nobody (#5315, milestone 495)."""
|
||||
alerts are sent by: email when a server is set and `accounts` (that get
|
||||
alerts) or Also send to addresses exist (core.outbox.hold_alert), a
|
||||
webhook when its URL is (core.notifications.dispatch_notifications). Empty: nobody (#5315, milestone 495)."""
|
||||
from urllib.parse import urlsplit
|
||||
out = []
|
||||
recipients = smtp.get("recipients") or []
|
||||
@@ -752,7 +752,8 @@ async def send_test(app, rule_id: str) -> dict:
|
||||
failed; lines say what happened to each channel. LookupError for no rule.
|
||||
"""
|
||||
from urllib.parse import urlsplit
|
||||
from steward.core.mail import recipients
|
||||
from steward.core import outbox
|
||||
from steward.core.mail import alert_email, recipients
|
||||
from steward.core.notifications import dispatch_notifications
|
||||
smtp, webhook = app.config.get("SMTP", {}), app.config.get("WEBHOOK", {})
|
||||
async with app.db_sessionmaker() as db:
|
||||
@@ -761,12 +762,14 @@ async def send_test(app, rule_id: str) -> dict:
|
||||
raise LookupError(rule_id)
|
||||
alert = await test_alert(db, rule)
|
||||
to = await recipients(db, "alerts", smtp) if smtp.get("host") else []
|
||||
results = await dispatch_notifications(smtp, webhook, alert, to)
|
||||
results = await dispatch_notifications(webhook, alert)
|
||||
lines = []
|
||||
if email := results.get("email"):
|
||||
who = ", ".join(r.address for r in to)
|
||||
lines.append(f"Email sent to {who}." if email["sent"]
|
||||
else f"Email to {who} failed: {email['error']}")
|
||||
if to:
|
||||
# Queued and recorded like any email, tried once, not retried.
|
||||
ok, said = outbox.said(await outbox.send_each(
|
||||
app, "test", to, lambda r: alert_email(smtp, alert, r), retry=False))
|
||||
results["email"] = {"sent": ok, "error": None if ok else said}
|
||||
lines.append(f"Email: {said}")
|
||||
if hook := results.get("webhook"):
|
||||
host = urlsplit(webhook["url"]).hostname or webhook["url"]
|
||||
lines.append(f"Webhook to {host} delivered." if hook["sent"]
|
||||
|
||||
Reference in New Issue
Block a user