Receive DMARC aggregate reports on port 25 and show parsed summaries in the panel. Close Unreleased; pin compose and docs to 1.7.0. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+1
-1
@@ -102,7 +102,7 @@ than the default «meaningful step» rule in [§ Commits and release build](#com
|
||||
2. Git tag `vX.Y.Z` and publish the GitHub Release only on explicit request (see
|
||||
[§ Release image](#release-image)).
|
||||
|
||||
**Planned version cuts** (from pin `1.6.0`; adjust if semver changes mid-track):
|
||||
**Planned version cuts** (from pin `1.7.0`; adjust if semver changes mid-track):
|
||||
|
||||
| Stage | ID | Cut |
|
||||
|---|---|---|
|
||||
|
||||
+47
-5
@@ -33,6 +33,7 @@ domains hosted on that instance — DNS, deliveries, rate limits, applications).
|
||||
- [Full backup and restore](#full-backup-and-restore)
|
||||
- [Encrypting a backup or export](#encrypting-a-backup-or-export)
|
||||
- [Inbound relay](#inbound-relay)
|
||||
- [DMARC reports](#dmarc-reports)
|
||||
- [Domain administration](#domain-administration)
|
||||
- [Domains page](#domains-page)
|
||||
- [Domain-level DNS (SPF, DKIM, DMARC)](#domain-level-dns-spf-dkim-dmarc)
|
||||
@@ -169,7 +170,7 @@ cat ./data/setup-token
|
||||
#### Fixed image tag
|
||||
|
||||
`deploy/docker-compose.yml` pins an explicit version (`ghcr.io/mixeme/selfpost:X.Y.Z`),
|
||||
deliberately never `:latest`. The current pin is `1.6.0`. Intermediate
|
||||
deliberately never `:latest`. The current pin is `1.7.0`. Intermediate
|
||||
CHANGELOG sections (`0.2.0`…`0.6.0`) record development cuts from before that
|
||||
image was published. Pinning matters because of the backup version check (see
|
||||
[Full backup and restore](#full-backup-and-restore)): the panel binary's
|
||||
@@ -190,6 +191,9 @@ expected to set; defaults match the code exactly.
|
||||
| `SELFPOST_HOSTNAME` | Mail-server identity: Postfix HELO/EHLO, SASL realm, certificate CN/SAN, and the hostname the PTR check expects. Bare FQDN only — no scheme or port. | *(required)* | `.env` |
|
||||
| `SUBMISSION_ENABLE` | When `true`, also listen on port 587 with STARTTLS (RFC 6409 submission) alongside the primary 465/smtps listener. | `false` | `.env` |
|
||||
| `INBOUND_RELAY_ENABLE` | When `true`, accept mail on port 25 for domains configured under *Inbound* in the panel and forward them to the upstream you set. Off by default — the outbound path is unchanged. See [Inbound relay](#inbound-relay). | `false` | `.env` |
|
||||
| `DMARC_REPORTS_ENABLE` | When `true`, accept DMARC aggregate reports on port 25 only for report addresses configured in the panel, parse gzip/XML, and show summaries under *DMARC*. Off by default. See [DMARC reports](#dmarc-reports). | `false` | `.env` |
|
||||
| `DMARC_RATE_LIMIT_MESSAGES_PER_IP` | Per-client-IP cap on port 25 when DMARC ingest is on (shared listener with inbound relay if both are enabled). | `20` | `.env` |
|
||||
| `DMARC_MESSAGE_SIZE_LIMIT` | Maximum report message size in bytes when DMARC ingest is on. | `5242880` (5 MiB) | `.env` |
|
||||
| `INBOUND_ANTISPAM_MILTER` | Optional milter on the inbound listener only (not 465/587). Empty = off. Format `inet:host:port` or `unix:/path`. Example with [deploy/antispam/docker-compose.antispam.yml](../deploy/antispam/docker-compose.antispam.yml): `inet:antispam:11332`. | *(empty)* | `.env` |
|
||||
| `INBOUND_ANTISPAM_MILTER_ACTION` | What Postfix does if that milter is down: `accept` (fail-open) or `tempfail` (defer). | `accept` | `.env` |
|
||||
| `INBOUND_RATE_LIMIT_MESSAGES_PER_IP` | Coarse per-client-IP cap on inbound smtpd (`smtpd_client_message_rate_limit`). Uses the same window as `RATE_LIMIT_WINDOW_SECONDS`. | `20` | `.env` |
|
||||
@@ -640,6 +644,44 @@ Inbound configuration lives in SQLite and `/data/postfix/` map files, so it
|
||||
is included in a [full backup](#full-backup-and-restore). Single-domain
|
||||
export/import is sending domains only.
|
||||
|
||||
### DMARC reports
|
||||
|
||||
Optional ingest of DMARC **aggregate** reports (`rua=`): Postfix accepts mail on
|
||||
port **25** only for addresses you configure, pipes each message to the panel
|
||||
ingest worker, and stores parsed summaries in SQLite. Forensic reports (`ruf=`)
|
||||
are not stored. This is separate from [Inbound relay](#inbound-relay) — no
|
||||
backup-MX, no forwarding upstream.
|
||||
|
||||
**Off by default.** Set `DMARC_REPORTS_ENABLE=true` in `.env` and recreate the
|
||||
container. Until then there is no report ingest, no *DMARC* item in the nav,
|
||||
and `/dmarc` is 404. Outbound 465/587 is unchanged.
|
||||
|
||||
**Addresses.** In *Settings* (global administrator), set the default
|
||||
`rua=` mailbox to an address on `SELFPOST_HOSTNAME`, e.g.
|
||||
`dmarc-reports@mail.example.com`. Per domain you can choose **SelfPost hosted**
|
||||
(`dmarc-reports+<domain>@<hostname>`) under *Domain settings → DMARC reports*.
|
||||
Only those allow-listed addresses are accepted on port 25.
|
||||
|
||||
**DNS.** Publish the usual `_dmarc` TXT on each sending domain with
|
||||
`rua=mailto:…` pointing at your hosted address. Receivers deliver to the
|
||||
address domain — publish **MX** for `SELFPOST_HOSTNAME` (or the report
|
||||
address domain if different) so reports reach this server. If a hub domain
|
||||
authorises external destinations, publish `_report._dmarc` there too; the panel
|
||||
checks it on the domain page.
|
||||
|
||||
**Panel.** *DMARC* (global administrator) lists recent reports and ingest
|
||||
health. Open a domain's roll-up from the list or from *View DMARC reports* on
|
||||
the domain page. Domain administrators see reports only for domains assigned to
|
||||
them. Summaries are pruned (500 kept, 90 days max).
|
||||
|
||||
**Not an open relay.** The inbound smtpd offers no SASL. With DMARC ingest
|
||||
alone, `check_recipient_access` permits only configured report addresses;
|
||||
everything else is rejected. With inbound relay enabled too, both allow-lists
|
||||
apply.
|
||||
|
||||
Parsed report data lives in SQLite and is included in a
|
||||
[full backup](#full-backup-and-restore).
|
||||
|
||||
## Domain administration
|
||||
|
||||
### Domains page
|
||||
@@ -665,10 +707,10 @@ records; see [Inbound relay](#inbound-relay).
|
||||
- **DKIM** — a TXT record with the exact value the panel shows on that
|
||||
domain's page (`domain page → DKIM TXT record`), one selector per domain.
|
||||
- **DMARC** — a `_dmarc` TXT record. The panel suggests `p=none` (monitoring
|
||||
only, safe to publish immediately). On a send-only relay the sending domain
|
||||
often has no inbox, so `rua=` is optional — configure a default report address
|
||||
in *Settings* (see [Settings](#settings)) or per domain when you have a
|
||||
mailbox that receives inbound mail elsewhere. If `rua=` points at another
|
||||
only, safe to publish immediately). Set `rua=` to receive aggregate reports:
|
||||
with [DMARC reports](#dmarc-reports) enabled, use a SelfPost-hosted address
|
||||
from *Settings* or per-domain *Domain settings*; otherwise point `rua=` at a
|
||||
mailbox elsewhere that receives inbound mail. If `rua=` points at another
|
||||
domain, publish `_report._dmarc` on that hub domain too; the panel checks
|
||||
it. Public mail hosts (Gmail, Outlook, …) cannot be used as external
|
||||
report destinations.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Plan: dmarc-reports
|
||||
|
||||
**Status:** candidate
|
||||
**Status:** done — shipped in `[1.7.0]` (2026-08-18); security review (Fable)
|
||||
of the ingest path pending.
|
||||
|
||||
---
|
||||
|
||||
@@ -51,15 +52,28 @@ remain a separate, opt-in feature that does not forward mail upstream.
|
||||
|
||||
## Implementation checklist
|
||||
|
||||
**Ingest path.** `DMARC_REPORTS_ENABLE=true` enables `smtp/inet` on 25 (shared
|
||||
with inbound relay when both are on). Postfix `relay_domains` +
|
||||
`transport_maps` route allow-listed recipients to a `dmarc-ingest` pipe
|
||||
(`panel -dmarc-ingest`). `check_recipient_access` on `dmarc_recipients` is the
|
||||
allow-list; no SASL, no local mailboxes.
|
||||
|
||||
**Schema.** Migration `0008_dmarc_reports.sql`: `dmarc_reports` (summary per
|
||||
aggregate) + `dmarc_report_records` (per-source rows). Dedup on
|
||||
`(reporter, report_id, domain)`.
|
||||
|
||||
**Retention.** Max 500 reports; drop older than 90 days; prune after each
|
||||
ingest.
|
||||
|
||||
Target version cut: **`1.7.0`** (MINOR). One commit per step; code only after
|
||||
roadmap status is **agreed**. Expand the sketch sections above before step 1
|
||||
if still thin. See [development.md](../development.md) § Plan checklists.
|
||||
|
||||
- [ ] Expand plan: ingest path, `dmarc_reports` schema, retention caps — **Sonnet**
|
||||
- [ ] Opt-in inbound SMTP for report addresses only (allow-list) — **Opus**
|
||||
- [ ] Worker: gzip/XML parse → SQLite — **Opus**
|
||||
- [ ] Panel: domain roll-up + parsed report (panel-ui mockups) — **Sonnet**
|
||||
- [ ] Tie-in `dmarc_report_email` / `domains.dmarc_rua` — **Sonnet**
|
||||
- [ ] Tests and [guide.md](../guide.md) — **Sonnet**
|
||||
- [x] Expand plan: ingest path, `dmarc_reports` schema, retention caps — **Sonnet**
|
||||
- [x] Opt-in inbound SMTP for report addresses only (allow-list) — **Opus**
|
||||
- [x] Worker: gzip/XML parse → SQLite — **Opus**
|
||||
- [x] Panel: domain roll-up + parsed report (panel-ui mockups) — **Sonnet**
|
||||
- [x] Tie-in `dmarc_report_email` / `domains.dmarc_rua` — **Sonnet**
|
||||
- [x] Tests and [guide.md](../guide.md) — **Sonnet**
|
||||
- [ ] Security review ingest path — **Fable**
|
||||
- [ ] `go vet`, `go test` on touched packages — **Haiku**
|
||||
- [x] `go vet`, `go test` on touched packages — **Haiku**
|
||||
|
||||
+3
-1
@@ -60,7 +60,9 @@ domain administrators with assigned domains). The optional **inbound relay**
|
||||
(backup-MX / forwarder on port 25) ships in `[1.4.0]`, off by default behind
|
||||
`INBOUND_RELAY_ENABLE`; it is relay/forward, not IMAP/webmail. **Send-log
|
||||
retention in Settings** ships in `[1.5.0]`. **30-day send statistics** and
|
||||
**auto level-2 rate limits** ship in `[1.6.0]`. Items marked *candidate* in the
|
||||
**auto level-2 rate limits** ship in `[1.6.0]`. **DMARC aggregate report
|
||||
ingest** ships in `[1.7.0]`, off by default behind `DMARC_REPORTS_ENABLE`.
|
||||
Items marked *candidate* in the
|
||||
[roadmap](roadmap.md) require explicit approval before coding.
|
||||
|
||||
---
|
||||
|
||||
+7
-34
@@ -29,16 +29,16 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md).
|
||||
| ID | Topic | Status | Progress | Plan |
|
||||
|---|---|---|---|---|
|
||||
| contributing | `CONTRIBUTING.md` | candidate | — | — |
|
||||
| dmarc-reports | DMARC aggregate report ingestion and panel UI | candidate | 0/8 | [plans/dmarc-reports.md](plans/dmarc-reports.md) |
|
||||
| panel-docs | In-panel operator documentation | candidate | 0/6 | [plans/panel-docs.md](plans/panel-docs.md) |
|
||||
| schema-squash | Squash SQLite migrations into a 2.x baseline | **2.x** | — | — |
|
||||
|
||||
**Recommended order** (not binding): the next feature is **dmarc-reports** once
|
||||
agreed. domain-stats-auto-ratelimit shipped in
|
||||
[CHANGELOG.md](../CHANGELOG.md) `[1.6.0]`; send-log-retention in `[1.5.0]`;
|
||||
inbound-relay in `[1.4.0]`; queue-retries in `[1.3.1]`; the 2026-08-13
|
||||
full-tree review follow-ups are in `[1.3.0]`. Candidates need explicit
|
||||
agreement before they join the queue.
|
||||
**Recommended order** (not binding): the next feature is **panel-docs** once
|
||||
agreed. dmarc-reports shipped in
|
||||
[CHANGELOG.md](../CHANGELOG.md) `[1.7.0]` (security review of the ingest path
|
||||
pending); domain-stats-auto-ratelimit in `[1.6.0]`; send-log-retention in
|
||||
`[1.5.0]`; inbound-relay in `[1.4.0]`; queue-retries in `[1.3.1]`; the
|
||||
2026-08-13 full-tree review follow-ups are in `[1.3.0]`. Candidates need
|
||||
explicit agreement before they join the queue.
|
||||
|
||||
After a context reset, pick an item marked `agreed` or `in progress`, then work
|
||||
the **Implementation checklist** in its linked plan. The `Progress` column above
|
||||
@@ -65,33 +65,6 @@ priority.
|
||||
|
||||
---
|
||||
|
||||
## dmarc-reports
|
||||
|
||||
**Goal:** SelfPost receives DMARC aggregate reports (RFC 7489) on SMTP,
|
||||
parses the gzip/XML payloads, and shows pass/fail summaries in the panel — so
|
||||
the operator does not need an external DMARC service or a separate mailbox
|
||||
workflow.
|
||||
|
||||
**Boundary:** an extension of v1.0 — not IMAP/webmail and not a general
|
||||
inbound relay. A dedicated inbound path for report messages only; forensic
|
||||
reports (`ruf=`) out of scope for v1.
|
||||
|
||||
**Done when:** see [plans/dmarc-reports.md](plans/dmarc-reports.md).
|
||||
|
||||
**Dependencies / risks:** inbound SMTP in the image (may share infrastructure
|
||||
with [inbound-relay](plans/inbound-relay.md) but must not require backup-MX);
|
||||
storage and retention of parsed summaries; the `dmarc_report_email` setting
|
||||
(migration `0005` moved it off the old `admin` table into `settings`) and
|
||||
`domains.dmarc_rua` added in the DMARC template work must stay the source of
|
||||
truth for `rua=` in DNS guidance.
|
||||
|
||||
**Order:** after the DMARC `rua=` settings ship; may follow or overlap with
|
||||
inbound-relay depending on how port 25 acceptance is structured.
|
||||
|
||||
**Version:** `1.x` MINOR.
|
||||
|
||||
---
|
||||
|
||||
## panel-docs
|
||||
|
||||
**Goal:** built-in operator documentation in the panel — short pages (or a
|
||||
|
||||
Reference in New Issue
Block a user