Ingest and display DMARC aggregate reports
## Original request (verbatim)
> Migrate the emails system from GMail API to AWS SES. Use all the best practices. The quality of communication is of highest priority. Spare no time or expense.
>
> [jaah.ai](http://jaah.ai) MX is currently at GMail and will remain so, but we can carve out whatever we need
Epic context (BA PRD Task 9352, Architecture Task 9353) — settled: all system mail (send + receive) moves to SES on `mail.jaah.ai` (dev: `mail.dev.jaah.ai`, dev account, sends/receives real mail) with suppression, operator-only delivery log, RFC 8058 unsubscribe, DMARC monitoring, operator-approved template redesign; tenant Gmail (agent send, automation action, receive) is built in this epic behind one plan feature. All system AWS calls use the hand-signed SigV4 seam (`apps/api/app/aws/common.py` `signed_call`), no boto3. SES events and inbound mail are PULLED from SQS by system crons (precedent `apps/api/app/alarm_relay.py`, `apps/api/app/aws/sqs.py`); the only new public path is unsubscribe on the hooks edge.
## Scope of this child
C4 DMARC monitoring: the `dmarc-reports` inbound handler, `dmarc_reports` + `dmarc_report_rows` tables, and the operator DMARC view. Delivers PRD R8.
## Requirements (verbatim from the PRD)
### R8 — DMARC report monitoring (A2, Q8)
DMARC aggregate (`rua`) reports for `mail.jaah.ai` and `mail.dev.jaah.ai` are received, parsed and summarised for the operator (per source IP / org: volume, DKIM/SPF/DMARC pass rates), with failing sources visible.
## Approach (from the settled architecture)
- `apps/api/app/dmarc.py`, registered via `ses_inbound.register("dmarc-reports", handler)` (S5). Attachments `.zip`, `.gz`, `.xml`, size-capped, zip-bomb-guarded (max members 5, max uncompressed 10 MiB).
- Trust (§9.13): accept a report only when the carrying message's `dkimVerdict` or `dmarcVerdict` is PASS; refuse XML containing `<!DOCTYPE` or `<!ENTITY` before parsing with `xml.etree.ElementTree`.
- Tables (org-zero): `dmarc_reports` (reporter org, report id unique per reporter, date range, policy domain) + `dmarc_report_rows` (source IP, count, disposition, dkim/spf/dmarc results, header_from). Retention 365 d registered in `app/cron_retention.py`.
- Operator view *Settings > Integrations > Email > DMARC*: per source/reporter volume and pass rates, failing sources first; per `docs/standards/ui-style-guide.md`.
Pinned seams (build against these exactly; other children build against them too):
- S5 inbound dispatch (C3 → C4/C10): `ses_inbound.register(local_part: str, handler)` for reserved names and `ses_inbound.register_resolver(fn: (session, recipient) -> RouteTarget | None)` for tenant addresses; `RouteTarget(org_id, trigger_id | None, tenant_address_id)`; handler signature `(session, InboundEmailRow, raw: bytes) -> None`.
Credentials & reach:
| Oracle | Credential / identity / environment | Status |
|---|---|---|
| AC4 (local), AC7, AC9, AC10, AC13 (after sandbox exit), AC17(b) in the dev console | `me/jaah/debug/main/operator-login` (operator, `https://app.dev.jaah.ai`) | **verified** — `credential_get` ok; `https://app.dev.jaah.ai` answered 200 from this node |
| AC10 (non-operator 403), AC17 tenant side | `org/console/dev-orgb-login` (org B admin) | **verified** — `credential_get` ok (its recorded base_url is legacy; use `https://app.dev.jaah.ai`) |
| SES production access (sandbox exit) — gates AC2, AC3, AC11, AC13 and C11 | AWS approval of `PutAccountDetails` in jaah-dev and jaah-main (filed by C1) | **deferred** — reason: AWS reviews it; both accounts probed `ProductionAccessEnabled=false`. Owner C1 files on day one; dependent oracles run after approval |
| AC5, AC18, unit/integration suites | worktree only | **verified** — nothing beyond the worktree |
Out of scope:
- Do NOT change apex `jaah.ai` DMARC or add apex `rua` monitoring (deferred follow-up).
- Do NOT change `ses_inbound` beyond registering the handler (C3 owns it).
- Operator only — not visible to tenant admins.
Epic out-of-scope (PRD §7, verbatim):
- MUST NOT change the apex `jaah.ai` MX, SPF or `_dmarc.jaah.ai` record, nor any Workspace mailbox, alias or routing.
- MUST NOT add SES Virtual Deliverability Manager, BIMI/VMC, or a dedicated IP / dedicated IP pool.
- MUST NOT request Google's restricted `gmail.readonly` (or any restricted Gmail scope) via OAuth; no CASA assessment.
- MUST NOT add open/click tracking pixels or link rewriting to system mail.
- MUST NOT let tenant Gmail replace system mail for an org's members (Q9 option not chosen).
- MUST NOT expose the delivery log or suppression list to tenant admins or users (operator only, Q7).
- MUST NOT relax the verified-address rule or invitation budgets for system mail (AS6).
- MUST NOT add boto3/botocore to `apps/api/requirements.txt` unless the Architect records why the hand-signed SigV4 seam (`apps/api/app/aws/`) cannot serve SES (the repo guards its absence).
- MUST NOT put a tenant Gmail credential, OAuth token or service-account key on any node or in a runner's environment.
- MUST NOT change Slack/Telegram delivery, the support chat, or site enquiries (no email today).
Done means:
- **AC13 (R8) DMARC monitoring.** → verify: after 48h of sending, the operator's DMARC view on dev lists ≥1 aggregate report from Google with SES source IPs showing DKIM/SPF/DMARC pass. (Live part runs once dev has sent to Google-hosted recipients, i.e. after SES production access in jaah-dev — deferred until then. Now: parser tests over real-shaped fixture reports (zip, gz, xml) incl. a failing source, zip-bomb, DOCTYPE/ENTITY refusal, and an untrusted (DKIM/DMARC FAIL) carrier refused.)
- The view is readable at 390 px and desktop and shows failing sources first (Playwright screenshot with seeded data in the report); `org/console/dev-orgb-login` gets 403 / no nav entry.
- Quick gate `./dc check quick` green and changed-area tests green; `python3 tools/check_context.py` green (any doc this child adds/edits is routed and single-homed).
Activity
- Draft
- Ready