PR #784 was stacked on the #783 branch and its base was never retargeted to
master, so it merged into fix/mailsuite-2.2.1-empty-address instead of master.
master therefore has 10.0.2 (#783's squash) but is missing the 10.0.3 changes.
This re-lands exactly that delta — the Reply-To/Delivered-To parser fix, the
ES/OS Reply-To header flattening, and the Splunk/OpenSearch/Grafana failure
dashboard fixes, with the version bumped to 10.0.3. No mailsuite re-bump (the
>=2.2.1 floor is already on master from 10.0.2).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a PostgreSQL output backend as a lighter-weight alternative to
Elasticsearch/OpenSearch, configured via a [postgresql] section
(host/port/user/password/database or a libpq connection_string). Tables
are created automatically on first run; a Grafana dashboard is included.
- psycopg is an optional extra (pip install parsedmarc[postgresql]); the
import is guarded so `import parsedmarc` works without it, and
PostgreSQLClient raises a clear install hint when constructed without
the driver. Binary wheels aren't available for every platform.
- Schema captures the RFC 9990 / DMARCbis aggregate fields: np, testing,
discovery_method, generator, xml_namespace, and per-result human_result
on the DKIM/SPF auth-result tables.
- forensic -> failure naming throughout (table dmarc_failure_report,
save_failure_report_to_postgresql, dashboard, docs) to match #659.
- Failure-report de-duplication mirrors the Elasticsearch backend exactly:
arrival date + From + To + Subject (NULL-safe via IS NOT DISTINCT FROM;
semantic JSONB equality). Aggregate and SMTP-TLS use ON CONFLICT.
- PostgreSQLClient.close() for clean CLI shutdown; comment documents why
the two timestamp helpers must stay distinct (report dates are local,
record/SMTP-TLS dates are UTC).
- CLI: config parse raises ConfigurationError on missing
host/connection_string; wired into _init_output_clients + save loops.
- Tests in tests/test_postgres.py (helpers, mocked-DB save assertions,
create_tables, connect/error wrapping, dedup, real-sample round trip)
and tests/test_cli.py (config parse + end-to-end save wiring incl.
AlreadySaved/PostgreSQLError handling). postgres.py at 99% line
coverage; only _main's output-client-init retry path is left.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>