281a844513
Per the v0.18.0 email + webhook hygiene proposal §3:
* `backend/migrations/020_outbound_emails.sql` — new table
capturing every send attempt: to_address, from_address,
subject, kind, sent_at, status ('sent' | 'failed' | 'deferred'
| 'bounced'), error, notification_id (FK), message_id (for
Slice 5 bounce correlation).
* `email.record_outbound()` — best-effort write helper every
send path calls. Status='sent' on SMTP success, 'failed' on
exception (with class + message in `error`), 'deferred' on
the dev-fallback path where SMTP_HOST is unset (the send
didn't happen but the row records the attempt).
* `email._deliver` (watcher notifications + bundles), `digest.py`,
`email_otc.py`, `email_invite.py` — every send path now records.
* `GET /api/admin/outbound-emails` — admin-only listing,
filterable by kind / status / to_address. Answers "did this
person ever get their invite?" without grepping VM logs. No
admin UI in v0.18.0; operator queries via curl + jq for now.
7 new integration tests covering: OTC / invite / notification all
write rows with matching Message-ID; admin endpoint lists,
filters by kind, filters by to_address (case-insensitive),
refuses non-admins.
Full suite: 291 passed.
31 lines
1.6 KiB
SQL
31 lines
1.6 KiB
SQL
-- v0.18.0 Slice 4: outbound_emails audit table.
|
|
--
|
|
-- Per the v0.18.0 email + webhook hygiene proposal §3, every send
|
|
-- helper writes a row to this table before returning, regardless
|
|
-- of outcome. status='sent' on success, 'failed' on exception,
|
|
-- 'deferred' on the dev-fallback path (no SMTP_HOST configured).
|
|
--
|
|
-- The table is queried by `GET /api/admin/outbound-emails` to
|
|
-- answer "did this person ever get their invite?" without having
|
|
-- to grep VM logs, and by the v0.18.0 Slice 5 bounce-correlation
|
|
-- hook (which looks up message_id when a POST lands at
|
|
-- /api/webhooks/email-bounce and marks the matching row
|
|
-- status='bounced').
|
|
|
|
CREATE TABLE IF NOT EXISTS outbound_emails (
|
|
id INTEGER PRIMARY KEY,
|
|
to_address TEXT NOT NULL,
|
|
from_address TEXT NOT NULL,
|
|
subject TEXT NOT NULL,
|
|
kind TEXT NOT NULL, -- 'otc' | 'invite' | 'notification' | 'bundle' | 'digest' | 'rfc-invite'
|
|
sent_at TEXT NOT NULL, -- ISO 8601, time the send was attempted
|
|
status TEXT NOT NULL, -- 'sent' | 'failed' | 'deferred' | 'bounced'
|
|
error TEXT, -- exception class + message if status='failed'
|
|
notification_id INTEGER, -- nullable FK to notifications.id for the watcher path
|
|
message_id TEXT -- the Message-ID header value, for bounce correlation
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_outbound_emails_to ON outbound_emails(to_address);
|
|
CREATE INDEX IF NOT EXISTS idx_outbound_emails_sent_at ON outbound_emails(sent_at);
|
|
CREATE INDEX IF NOT EXISTS idx_outbound_emails_message ON outbound_emails(message_id);
|