Port 25, DMARC routes, restore Resync vs reload, image pin, and roadmap index were stale relative to the running panel and Postfix config. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,6 +5,19 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Operator and as-built docs aligned with the code after a full pass —
|
||||||
|
[architecture.md](docs/architecture.md) (port 25 opens for inbound relay
|
||||||
|
and/or DMARC ingest; antispam requires inbound relay; DMARC pipe transport
|
||||||
|
and map paths; `/help` and `/dmarc*` routes; four panel roles including
|
||||||
|
rate-limit recalc; restore Resync vs manual reload); [guide.md](docs/guide.md)
|
||||||
|
and [README.md](README.md) (image pin `1.9.1`, port 25 conditions, combined
|
||||||
|
listener limits, reload vs Resync); [roadmap.md](docs/roadmap.md) (`panel-docs`
|
||||||
|
closed at `[1.8.0]`, next item inbound-antispam-panel); [development.md](docs/development.md)
|
||||||
|
version-cut table through `1.9.1`; [schema-migrations.md](docs/schema-migrations.md)
|
||||||
|
last-updated note. No behaviour change.
|
||||||
|
|
||||||
## [1.9.1] - 2026-08-19
|
## [1.9.1] - 2026-08-19
|
||||||
|
|
||||||
Port-25 Postfix startup when inbound relay, DMARC ingest, or the inbound antispam
|
Port-25 Postfix startup when inbound relay, DMARC ingest, or the inbound antispam
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ send log and DNS checks in the panel, encrypted backups.
|
|||||||
|
|
||||||
- Outbound SMTP (465/smtps; optional 587 submission) with per-domain DKIM signing
|
- Outbound SMTP (465/smtps; optional 587 submission) with per-domain DKIM signing
|
||||||
- Optional inbound relay on port 25 (backup-MX / forwarder; off by default)
|
- Optional inbound relay on port 25 (backup-MX / forwarder; off by default)
|
||||||
|
- Optional DMARC aggregate report ingest on port 25 (off by default)
|
||||||
|
- In-panel Help drawer and `/help` page for Status and domain controls
|
||||||
|
- Per-application client IP allow-list for SMTP AUTH (optional)
|
||||||
- Web panel — domains, applications, deliveries, mail queue, system log, backup
|
- Web panel — domains, applications, deliveries, mail queue, system log, backup
|
||||||
- Multi-domain relay — each SASL application is bound to one sending domain
|
- Multi-domain relay — each SASL application is bound to one sending domain
|
||||||
- DNS status checks (PTR, SPF, DKIM, DMARC) with in-panel re-check
|
- DNS status checks (PTR, SPF, DKIM, DMARC) with in-panel re-check
|
||||||
@@ -108,7 +111,7 @@ docker run --rm -d --name selfpost-try \
|
|||||||
-e SELFPOST_HOSTNAME=mail.local.test \
|
-e SELFPOST_HOSTNAME=mail.local.test \
|
||||||
-e PANEL_COOKIE_SECURE=false \
|
-e PANEL_COOKIE_SECURE=false \
|
||||||
-v selfpost-try-data:/data \
|
-v selfpost-try-data:/data \
|
||||||
ghcr.io/mixeme/selfpost:1.7.0
|
ghcr.io/mixeme/selfpost:1.9.1
|
||||||
```
|
```
|
||||||
|
|
||||||
**Get the setup URL** (pick one):
|
**Get the setup URL** (pick one):
|
||||||
@@ -149,8 +152,9 @@ deployment](docs/guide.md#full-deployment) section, with proxy-specific
|
|||||||
commands under [Reverse proxy](docs/guide.md#reverse-proxy-mandatory).
|
commands under [Reverse proxy](docs/guide.md#reverse-proxy-mandatory).
|
||||||
|
|
||||||
The compose file always publishes **465**, **587**, and **25**; Postfix listens
|
The compose file always publishes **465**, **587**, and **25**; Postfix listens
|
||||||
on 587 only when `SUBMISSION_ENABLE=true`, and on 25 only when
|
on 587 only when `SUBMISSION_ENABLE=true`, and on 25 when
|
||||||
`INBOUND_RELAY_ENABLE=true` (see [Ports](docs/guide.md#ports)). Bump the
|
`INBOUND_RELAY_ENABLE=true` and/or `DMARC_REPORTS_ENABLE=true` (see
|
||||||
|
[Ports](docs/guide.md#ports)). Bump the
|
||||||
pinned image tag deliberately when upgrading, never `:latest` ([why](docs/guide.md#fixed-image-tag)). Optional
|
pinned image tag deliberately when upgrading, never `:latest` ([why](docs/guide.md#fixed-image-tag)). Optional
|
||||||
variables (`TRUSTED_PROXY_CIDR`, rate limits, retention): see [Environment
|
variables (`TRUSTED_PROXY_CIDR`, rate limits, retention): see [Environment
|
||||||
variables](docs/guide.md#environment-variables).
|
variables](docs/guide.md#environment-variables).
|
||||||
|
|||||||
+51
-24
@@ -27,7 +27,7 @@ Managed programs ([build/supervisord.conf](../build/supervisord.conf)):
|
|||||||
| Program | User | Priority | Role |
|
| Program | User | Priority | Role |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `opendkim` | root → `opendkim` (`UserID` in opendkim.conf) | 100 | DKIM signing milter |
|
| `opendkim` | root → `opendkim` (`UserID` in opendkim.conf) | 100 | DKIM signing milter |
|
||||||
| `panel` | panel | 200 | HTTP UI + journal-milter + log-tailer goroutine |
|
| `panel` | panel | 200 | HTTP UI + journal-milter + log-tailer + rate-limit recalc |
|
||||||
| `postfix` | root (wrapper) | 300 | MTA — started only after both milter sockets exist |
|
| `postfix` | root (wrapper) | 300 | MTA — started only after both milter sockets exist |
|
||||||
| `postfix-reload` | root | — | On-demand `postfix reload` (autostart off) |
|
| `postfix-reload` | root | — | On-demand `postfix reload` (autostart off) |
|
||||||
| `cert-reload` | root | 400 | Daily `postfix reload` for renewed TLS certs |
|
| `cert-reload` | root | 400 | Daily `postfix reload` for renewed TLS certs |
|
||||||
@@ -54,30 +54,40 @@ Client ──TLS+SASL──► Postfix (465 smtps, optional 587 submission)
|
|||||||
├─► journal-milter (send log + L2 rate limits, fail-open)
|
├─► journal-milter (send log + L2 rate limits, fail-open)
|
||||||
└─► outbound MX delivery (port 25 client)
|
└─► outbound MX delivery (port 25 client)
|
||||||
|
|
||||||
Internet ──► Postfix smtp inet :25 (only when INBOUND_RELAY_ENABLE=true)
|
Internet ──► Postfix smtp inet :25 (when INBOUND_RELAY_ENABLE=true
|
||||||
|
│ and/or DMARC_REPORTS_ENABLE=true)
|
||||||
│
|
│
|
||||||
├─► optional antispam milter (inbound only)
|
├─► optional antispam milter (inbound relay only)
|
||||||
└─► smtp:[upstream]:port (transport_maps; no local delivery)
|
├─► DMARC aggregate ingest (pipe → dmarc-ingest)
|
||||||
|
└─► smtp:[upstream]:port (inbound relay only;
|
||||||
|
transport_maps; no local delivery)
|
||||||
```
|
```
|
||||||
|
|
||||||
The inbound listener is **absent** when the flag is off (`postconf -MX smtp/inet`
|
The smtp/inet listener is **absent** when both flags are off (`postconf -MX
|
||||||
removes Debian's stock smtpd). Outbound delivery still uses the `smtp unix`
|
smtp/inet` removes Debian's stock smtpd). Outbound delivery still uses the
|
||||||
client; it is not the same service.
|
`smtp unix` client; it is not the same service.
|
||||||
|
|
||||||
### Postfix ([build/postfix-config.sh](../build/postfix-config.sh))
|
### Postfix ([build/postfix-config.sh](../build/postfix-config.sh))
|
||||||
|
|
||||||
- **465/smtps** — implicit TLS, SASL required; primary listener.
|
- **465/smtps** — implicit TLS, SASL required; primary listener.
|
||||||
- **587/submission** — only when `SUBMISSION_ENABLE=true`; STARTTLS with
|
- **587/submission** — only when `SUBMISSION_ENABLE=true`; STARTTLS with
|
||||||
`smtpd_tls_security_level=encrypt`.
|
`smtpd_tls_security_level=encrypt`.
|
||||||
- **25/smtp inet** — only when `INBOUND_RELAY_ENABLE=true`. No SASL, no
|
- **25/smtp inet** — when `INBOUND_RELAY_ENABLE=true` and/or
|
||||||
OpenDKIM, no journal-milter. Accepts only `relay_domains` +
|
`DMARC_REPORTS_ENABLE=true`. No SASL, no OpenDKIM, no journal-milter.
|
||||||
`relay_recipient_maps` (`reject_unauth_destination`,
|
**Inbound relay** (`INBOUND_RELAY_ENABLE=true`): accepts only
|
||||||
`reject_unlisted_recipient`). Maps under `/data/postfix/`
|
`relay_domains` + `relay_recipient_maps` (`reject_unauth_destination`,
|
||||||
(`relay_domains`, `transport`, `relay_recipients`, `tls_policy`), written
|
`reject_unlisted_recipient`). Maps under `/data/postfix/` (`relay_domains`,
|
||||||
atomically by [internal/postfix/inbound.go](../internal/postfix/inbound.go).
|
`transport`, `relay_recipients`, `tls_policy`), written atomically by
|
||||||
Domains with an empty upstream host are omitted from the maps. Optional
|
[internal/postfix/inbound.go](../internal/postfix/inbound.go). Domains with
|
||||||
`INBOUND_ANTISPAM_MILTER` on this listener only; default
|
an empty upstream host are omitted from the maps. Optional
|
||||||
`milter_default_action` is fail-open (`accept`).
|
`INBOUND_ANTISPAM_MILTER` only when inbound relay is enabled (not on
|
||||||
|
DMARC-only port 25); default `milter_default_action` is fail-open
|
||||||
|
(`accept`). **DMARC ingest** (`DMARC_REPORTS_ENABLE=true`): allow-listed
|
||||||
|
report addresses via `check_recipient_access` on `postfix/dmarc_recipients`;
|
||||||
|
messages pipe to `dmarc-ingest` (symlink to `panel`). When both flags are
|
||||||
|
on, one listener serves both paths; per-IP rate uses
|
||||||
|
`INBOUND_RATE_LIMIT_MESSAGES_PER_IP` and message size uses the greater of
|
||||||
|
`INBOUND_MESSAGE_SIZE_LIMIT` and `DMARC_MESSAGE_SIZE_LIMIT`.
|
||||||
- **No open relay** — `permit_sasl_authenticated`, `reject_unauth_destination`;
|
- **No open relay** — `permit_sasl_authenticated`, `reject_unauth_destination`;
|
||||||
`smtpd_sender_login_maps` + `reject_sender_login_mismatch`.
|
`smtpd_sender_login_maps` + `reject_sender_login_mismatch`.
|
||||||
- **Level-1 rate limit** — `smtpd_client_message_rate_limit` /
|
- **Level-1 rate limit** — `smtpd_client_message_rate_limit` /
|
||||||
@@ -93,7 +103,7 @@ by the panel. Socket `/run/opendkim/opendkim.sock`.
|
|||||||
|
|
||||||
### Panel binary ([cmd/panel](../cmd/panel))
|
### Panel binary ([cmd/panel](../cmd/panel))
|
||||||
|
|
||||||
One process, three roles:
|
One process, four roles:
|
||||||
|
|
||||||
1. **HTTP server** — `:8080` (`PANEL_HTTP_ADDR`); HTTPS terminated by reverse
|
1. **HTTP server** — `:8080` (`PANEL_HTTP_ADDR`); HTTPS terminated by reverse
|
||||||
proxy only. On start it runs `postconf -h` once for the deferred-mail retry
|
proxy only. On start it runs `postconf -h` once for the deferred-mail retry
|
||||||
@@ -116,6 +126,8 @@ One process, three roles:
|
|||||||
3. **log-tailer** — follows `MAIL_LOG`, updates send-log delivery status by
|
3. **log-tailer** — follows `MAIL_LOG`, updates send-log delivery status by
|
||||||
queue-id. Send-log `queued → sent` transitions depend on this goroutine alone
|
queue-id. Send-log `queued → sent` transitions depend on this goroutine alone
|
||||||
(`UpdateStatus` is only called from [internal/logtail](../internal/logtail/logtail.go)).
|
(`UpdateStatus` is only called from [internal/logtail](../internal/logtail/logtail.go)).
|
||||||
|
4. **rate-limit recalc** — every six hours (and on demand from the domain
|
||||||
|
page), recomputes level-2 **Auto** rate limits from send-log statistics.
|
||||||
|
|
||||||
Milter chain in Postfix: OpenDKIM (tempfail) then journal (accept on failure).
|
Milter chain in Postfix: OpenDKIM (tempfail) then journal (accept on failure).
|
||||||
|
|
||||||
@@ -200,11 +212,13 @@ below is a summary — HTMX fragment endpoints
|
|||||||
| `/deliveries`, `/deliveries/{id}` | Send log with filters; scoped to assigned domains for domain-admins |
|
| `/deliveries`, `/deliveries/{id}` | Send log with filters; scoped to assigned domains for domain-admins |
|
||||||
| `/mail-queue`, `/mail-queue/*` | **Global.** Postfix queue view; retry-policy card on the page (not the HTMX fragment) |
|
| `/mail-queue`, `/mail-queue/*` | **Global.** Postfix queue view; retry-policy card on the page (not the HTMX fragment) |
|
||||||
| `/system-log`, `/system-log/*` | **Global.** `mail.log` tail |
|
| `/system-log`, `/system-log/*` | **Global.** `mail.log` tail |
|
||||||
| `/reload` | **Global.** `POST` — reload OpenDKIM + Postfix maps |
|
| `/reload` | **Global.** `POST` — reload OpenDKIM tables, Postfix sender map, and inbound relay maps when enabled (not DMARC maps — see § Persistence restore) |
|
||||||
| `/backup`, `/backup/*` | **Global.** Full backup download (page also hosts the import form) |
|
| `/backup`, `/backup/*` | **Global.** Full backup download (page also hosts the import form) |
|
||||||
|
| `/help` | In-panel operator help (any authenticated user) |
|
||||||
| `/settings` | Username/password for any user; DMARC report default is **global** only |
|
| `/settings` | Username/password for any user; DMARC report default is **global** only |
|
||||||
| `/users`, `/users/*` | **Global.** Panel user CRUD |
|
| `/users`, `/users/*` | **Global.** Panel user CRUD |
|
||||||
| `/inbound`, `/inbound/{id}`, `/inbound/{id}/*` | **Global.** Inbound relay domains. Registered only when `INBOUND_RELAY_ENABLE=true`; otherwise 404. |
|
| `/inbound`, `/inbound/{id}`, `/inbound/{id}/*` | **Global.** Inbound relay domains. Registered only when `INBOUND_RELAY_ENABLE=true`; otherwise 404. |
|
||||||
|
| `/dmarc`, `/dmarc/reports/{id}`, `/dmarc/domains/{id}` | **Global** list and report detail; domain roll-up scoped like deliveries. Registered only when `DMARC_REPORTS_ENABLE=true`; otherwise 404. |
|
||||||
|
|
||||||
HTMX polling refreshes monitoring fragments (5 s while the operator is active on
|
HTMX polling refreshes monitoring fragments (5 s while the operator is active on
|
||||||
the page, 30 s when the tab is visible but idle, none when hidden — scheduled in
|
the page, 30 s when the tab is visible but idle, none when hidden — scheduled in
|
||||||
@@ -271,6 +285,7 @@ flowchart TB
|
|||||||
domainSvc["internal/domain"]
|
domainSvc["internal/domain"]
|
||||||
appSvc["internal/app"]
|
appSvc["internal/app"]
|
||||||
inboundSvc["internal/inbound"]
|
inboundSvc["internal/inbound"]
|
||||||
|
dmarcSvc["internal/dmarc"]
|
||||||
end
|
end
|
||||||
subgraph persistence ["Persistence"]
|
subgraph persistence ["Persistence"]
|
||||||
store["internal/store — SQLite, embedded migrations"]
|
store["internal/store — SQLite, embedded migrations"]
|
||||||
@@ -293,6 +308,7 @@ flowchart TB
|
|||||||
web --> domainSvc
|
web --> domainSvc
|
||||||
web --> appSvc
|
web --> appSvc
|
||||||
web --> inboundSvc
|
web --> inboundSvc
|
||||||
|
web --> dmarcSvc
|
||||||
web --> backupPkg
|
web --> backupPkg
|
||||||
web --> dnscheck
|
web --> dnscheck
|
||||||
web --> health
|
web --> health
|
||||||
@@ -300,15 +316,18 @@ flowchart TB
|
|||||||
domainSvc --> store
|
domainSvc --> store
|
||||||
appSvc --> store
|
appSvc --> store
|
||||||
inboundSvc --> store
|
inboundSvc --> store
|
||||||
|
dmarcSvc --> store
|
||||||
milterPkg --> store
|
milterPkg --> store
|
||||||
logtail --> store
|
logtail --> store
|
||||||
domainSvc --> postfix
|
domainSvc --> postfix
|
||||||
appSvc --> postfix
|
appSvc --> postfix
|
||||||
inboundSvc --> postfix
|
inboundSvc --> postfix
|
||||||
|
dmarcSvc --> postfix
|
||||||
```
|
```
|
||||||
|
|
||||||
The three roles inside the `panel` process (HTTP server, journal-milter,
|
The four roles inside the `panel` process (HTTP server, journal-milter,
|
||||||
log-tailer goroutine) share one binary and one SQLite handle on purpose — see
|
log-tailer, rate-limit recalc) share one binary and one SQLite handle on
|
||||||
|
purpose — see
|
||||||
[Panel binary](#panel-binary-cmdpanel) for why, and *Persistence* below for the
|
[Panel binary](#panel-binary-cmdpanel) for why, and *Persistence* below for the
|
||||||
single-connection trade-off that follows from it.
|
single-connection trade-off that follows from it.
|
||||||
|
|
||||||
@@ -327,6 +346,9 @@ single-connection trade-off that follows from it.
|
|||||||
| `postfix/transport` | Inbound next-hop `smtp:[host]:port` |
|
| `postfix/transport` | Inbound next-hop `smtp:[host]:port` |
|
||||||
| `postfix/relay_recipients` | Inbound recipient allow-list or `@domain` catch-all |
|
| `postfix/relay_recipients` | Inbound recipient allow-list or `@domain` catch-all |
|
||||||
| `postfix/tls_policy` | TLS policy for inbound next hops |
|
| `postfix/tls_policy` | TLS policy for inbound next hops |
|
||||||
|
| `postfix/dmarc_recipients` | Allow-listed DMARC aggregate report addresses |
|
||||||
|
| `postfix/dmarc_transport` | Pipe transport for report ingest |
|
||||||
|
| `postfix/dmarc_relay_domains` | Domains accepted for DMARC report delivery |
|
||||||
| `postfix/queue/` | Postfix transit mail (deferred/active); survives container recreate |
|
| `postfix/queue/` | Postfix transit mail (deferred/active); survives container recreate |
|
||||||
| `log/mail.log` | Postfix delivery log + rotated copies (excluded from backups) |
|
| `log/mail.log` | Postfix delivery log + rotated copies (excluded from backups) |
|
||||||
| `manifest.json` | Backup version stamp (consumed on restore) |
|
| `manifest.json` | Backup version stamp (consumed on restore) |
|
||||||
@@ -344,10 +366,12 @@ under `/data`), `docker-compose.yml`, `.env`, and `certs/` when present;
|
|||||||
version check on restore. Requires the project directory mounted read-only at
|
version check on restore. Requires the project directory mounted read-only at
|
||||||
`SELFPOST_DEPLOY_ROOT` (`/selfpost-deploy` in the default compose file). On the
|
`SELFPOST_DEPLOY_ROOT` (`/selfpost-deploy` in the default compose file). On the
|
||||||
first successful boot after restore, the panel runs one **Resync** — OpenDKIM's
|
first successful boot after restore, the panel runs one **Resync** — OpenDKIM's
|
||||||
tables, Postfix's sender map, and (when `INBOUND_RELAY_ENABLE=true`) inbound
|
tables, Postfix's sender map, inbound relay maps (when
|
||||||
relay maps are re-derived from SQLite and both daemons are reloaded, so drift
|
`INBOUND_RELAY_ENABLE=true`), and DMARC maps (when `DMARC_REPORTS_ENABLE=true`)
|
||||||
between the extracted archive and the database is healed before mail flows
|
are re-derived from SQLite and both daemons are reloaded, so drift between the
|
||||||
(same step as `POST /reload` on demand). Stopped-container
|
extracted archive and the database is healed before mail flows. Manual
|
||||||
|
`POST /reload` on the Status page resyncs OpenDKIM, the sender map, and
|
||||||
|
inbound maps only — not DMARC maps. Stopped-container
|
||||||
`tar` of `./data` alone remains possible for state-only copies (see guide).
|
`tar` of `./data` alone remains possible for state-only copies (see guide).
|
||||||
|
|
||||||
**Optional encryption** of the two secret-bearing downloads
|
**Optional encryption** of the two secret-bearing downloads
|
||||||
@@ -398,6 +422,9 @@ unsupported rather than as a missing doc:
|
|||||||
`POSTFIX_TLS_POLICY_MAPS` (`/data/postfix/tls_policy`) — same desync if
|
`POSTFIX_TLS_POLICY_MAPS` (`/data/postfix/tls_policy`) — same desync if
|
||||||
overridden without matching the panel writer in
|
overridden without matching the panel writer in
|
||||||
[internal/postfix/inbound.go](../internal/postfix/inbound.go),
|
[internal/postfix/inbound.go](../internal/postfix/inbound.go),
|
||||||
|
`POSTFIX_DMARC_RECIPIENTS` (`/data/postfix/dmarc_recipients`),
|
||||||
|
`POSTFIX_DMARC_TRANSPORT` (`/data/postfix/dmarc_transport`),
|
||||||
|
`POSTFIX_DMARC_RELAY_DOMAINS` (`/data/postfix/dmarc_relay_domains`),
|
||||||
`POSTFIX_QUEUE_DIR` (`/data/postfix/queue` — set in `build/postfix-config.sh`),
|
`POSTFIX_QUEUE_DIR` (`/data/postfix/queue` — set in `build/postfix-config.sh`),
|
||||||
`SELFPOST_DEPLOY_ROOT` (`/selfpost-deploy` — operator project directory for
|
`SELFPOST_DEPLOY_ROOT` (`/selfpost-deploy` — operator project directory for
|
||||||
full backups; mount `.:/selfpost-deploy:ro` in compose).
|
full backups; mount `.:/selfpost-deploy:ro` in compose).
|
||||||
|
|||||||
+4
-1
@@ -107,7 +107,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
|
2. Git tag `vX.Y.Z` and publish the GitHub Release only on explicit request (see
|
||||||
[§ Release image](#release-image)).
|
[§ Release image](#release-image)).
|
||||||
|
|
||||||
**Planned version cuts** (from pin `1.7.0`; adjust if semver changes mid-track):
|
**Planned version cuts** (from pin `1.9.1`; adjust if semver changes mid-track):
|
||||||
|
|
||||||
| Stage | ID | Cut |
|
| Stage | ID | Cut |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -117,6 +117,9 @@ than the default «meaningful step» rule in [§ Commits and release build](#com
|
|||||||
| 4 | domain-stats-auto-ratelimit | `1.6.0` |
|
| 4 | domain-stats-auto-ratelimit | `1.6.0` |
|
||||||
| 5 | dmarc-reports | `1.7.0` |
|
| 5 | dmarc-reports | `1.7.0` |
|
||||||
| 6 | panel-docs | `1.8.0` |
|
| 6 | panel-docs | `1.8.0` |
|
||||||
|
| 7 | application auth IPs | `1.9.0` |
|
||||||
|
| 8 | port-25 Postfix startup fix | `1.9.1` (PATCH) |
|
||||||
|
| 9 | inbound-antispam-panel | `1.10.0` (planned) |
|
||||||
|
|
||||||
Docs-only prep (checklists in plans, no product code) uses the same per-step
|
Docs-only prep (checklists in plans, no product code) uses the same per-step
|
||||||
commit rule but **no** version cut until the next product stage ships.
|
commit rule but **no** version cut until the next product stage ships.
|
||||||
|
|||||||
+23
-17
@@ -52,9 +52,10 @@ domains hosted on that instance — DNS, deliveries, rate limits, applications).
|
|||||||
465 (smtps) is always active. Port **587** is published even when
|
465 (smtps) is always active. Port **587** is published even when
|
||||||
`SUBMISSION_ENABLE=false`; nothing listens until you set it to `true`. Port
|
`SUBMISSION_ENABLE=false`; nothing listens until you set it to `true`. Port
|
||||||
**25** is published even when `INBOUND_RELAY_ENABLE=false`; Postfix does not
|
**25** is published even when `INBOUND_RELAY_ENABLE=false`; Postfix does not
|
||||||
accept inbound mail until you set it to `true` (see [Inbound
|
listen on port 25 until you set **`INBOUND_RELAY_ENABLE=true`** and/or
|
||||||
relay](#inbound-relay)). Harmless extra publishes can look like open ports in
|
**`DMARC_REPORTS_ENABLE=true`** (see [Inbound relay](#inbound-relay) and
|
||||||
external scans.
|
[DMARC reports](#dmarc-reports)). Harmless extra publishes can look like open
|
||||||
|
ports in external scans.
|
||||||
|
|
||||||
### Local trial
|
### Local trial
|
||||||
|
|
||||||
@@ -172,7 +173,7 @@ cat ./data/setup-token
|
|||||||
#### Fixed image tag
|
#### Fixed image tag
|
||||||
|
|
||||||
`deploy/docker-compose.yml` pins an explicit version (`ghcr.io/mixeme/selfpost:X.Y.Z`),
|
`deploy/docker-compose.yml` pins an explicit version (`ghcr.io/mixeme/selfpost:X.Y.Z`),
|
||||||
deliberately never `:latest`. The current pin is `1.7.0`. Intermediate
|
deliberately never `:latest`. The current pin is `1.9.1`. Intermediate
|
||||||
CHANGELOG sections (`0.2.0`…`0.6.0`) record development cuts from before that
|
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
|
image was published. Pinning matters because of the backup version check (see
|
||||||
[Full backup and restore](#full-backup-and-restore)): the panel binary's
|
[Full backup and restore](#full-backup-and-restore)): the panel binary's
|
||||||
@@ -194,12 +195,12 @@ expected to set; defaults match the code exactly.
|
|||||||
| `SUBMISSION_ENABLE` | When `true`, also listen on port 587 with STARTTLS (RFC 6409 submission) alongside the primary 465/smtps listener. | `false` | `.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` |
|
| `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_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_RATE_LIMIT_MESSAGES_PER_IP` | Per-client-IP cap on port 25 when DMARC ingest is on. When inbound relay is also enabled, **`INBOUND_RATE_LIMIT_MESSAGES_PER_IP`** applies to the shared listener instead. | `20` | `.env` |
|
||||||
| `DMARC_MESSAGE_SIZE_LIMIT` | Maximum report message size in bytes when DMARC ingest is on. | `5242880` (5 MiB) | `.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` | Optional milter on the inbound relay listener only (requires `INBOUND_RELAY_ENABLE=true`; not 465/587 or DMARC-only port 25). 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_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` |
|
| `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` |
|
||||||
| `INBOUND_MESSAGE_SIZE_LIMIT` | Maximum message size in bytes on inbound smtpd (`message_size_limit`). | `26214400` (25 MiB) | `.env` |
|
| `INBOUND_MESSAGE_SIZE_LIMIT` | Maximum message size in bytes on inbound smtpd (`message_size_limit`). When both inbound relay and DMARC ingest are on, the shared listener uses the **greater** of this and `DMARC_MESSAGE_SIZE_LIMIT`. | `26214400` (25 MiB) | `.env` |
|
||||||
| `RATE_LIMIT_MESSAGES_PER_IP` | Level-1 backstop: maximum messages one client IP may submit per window (Postfix `smtpd_client_message_rate_limit`). See [Rate limiting — level 1](#rate-limiting--level-1-ip-backstop). | `100` | `.env` |
|
| `RATE_LIMIT_MESSAGES_PER_IP` | Level-1 backstop: maximum messages one client IP may submit per window (Postfix `smtpd_client_message_rate_limit`). See [Rate limiting — level 1](#rate-limiting--level-1-ip-backstop). | `100` | `.env` |
|
||||||
| `RATE_LIMIT_WINDOW_SECONDS` | Level-1 window length in seconds (Postfix `anvil_rate_time_unit`). | `3600` | `.env` |
|
| `RATE_LIMIT_WINDOW_SECONDS` | Level-1 window length in seconds (Postfix `anvil_rate_time_unit`). | `3600` | `.env` |
|
||||||
| `SEND_LOG_RETENTION_DAYS` | Initial default for how many days of send-log history are kept before the background sweep deletes rows — the main driver of `/data` growth over time. After the first panel start, change retention on **Settings** (global administrator); the env value is only used to seed SQLite when the setting has never been saved. | `90` | `.env` |
|
| `SEND_LOG_RETENTION_DAYS` | Initial default for how many days of send-log history are kept before the background sweep deletes rows — the main driver of `/data` growth over time. After the first panel start, change retention on **Settings** (global administrator); the env value is only used to seed SQLite when the setting has never been saved. | `90` | `.env` |
|
||||||
@@ -342,7 +343,8 @@ against the PTR record the internet publishes for this server's IP
|
|||||||
DNS. The **Reload configuration** button re-applies OpenDKIM tables and the
|
DNS. The **Reload configuration** button re-applies OpenDKIM tables and the
|
||||||
Postfix sender map from the database (and inbound relay maps when
|
Postfix sender map from the database (and inbound relay maps when
|
||||||
`INBOUND_RELAY_ENABLE=true`) — use it if daemons drifted from what the panel
|
`INBOUND_RELAY_ENABLE=true`) — use it if daemons drifted from what the panel
|
||||||
shows after manual edits under `/data`.
|
shows after manual edits under `/data`. It does **not** rebuild DMARC Postfix
|
||||||
|
maps; a full restore Resync does (see [Restore](#restore)).
|
||||||
|
|
||||||
### Mail queue and System log
|
### Mail queue and System log
|
||||||
|
|
||||||
@@ -376,8 +378,8 @@ restart). Application SASL logins are separate and are not changed here.
|
|||||||
There are two roles:
|
There are two roles:
|
||||||
|
|
||||||
- **Global administrator** — full access to every page and every domain,
|
- **Global administrator** — full access to every page and every domain,
|
||||||
including Users, Backup, Status, Mail queue, System log, and Inbound (when
|
including Users, Backup, Status, Mail queue, System log, Help, Inbound (when
|
||||||
the inbound relay flag is on).
|
the inbound relay flag is on), and DMARC (when `DMARC_REPORTS_ENABLE=true`).
|
||||||
- **Domain-admin** — scoped to one or more domains assigned by a global
|
- **Domain-admin** — scoped to one or more domains assigned by a global
|
||||||
administrator. Sees only those domains' pages, applications, and
|
administrator. Sees only those domains' pages, applications, and
|
||||||
Deliveries rows; cannot add or delete domains. `/users`, `/backup`,
|
Deliveries rows; cannot add or delete domains. `/users`, `/backup`,
|
||||||
@@ -547,10 +549,11 @@ refuses to start otherwise and tells you which tag to use. On the first
|
|||||||
successful start after restore, `data/manifest.json` from the archive is
|
successful start after restore, `data/manifest.json` from the archive is
|
||||||
**deleted** — it guards only that one boot, so a later in-place upgrade is not
|
**deleted** — it guards only that one boot, so a later in-place upgrade is not
|
||||||
blocked. On that same first boot the panel also runs one **Resync** — OpenDKIM's
|
blocked. On that same first boot the panel also runs one **Resync** — OpenDKIM's
|
||||||
tables and Postfix's sender map are re-derived from SQLite (and inbound relay
|
tables and Postfix's sender map are re-derived from SQLite (inbound relay maps
|
||||||
maps when `INBOUND_RELAY_ENABLE=true`) and both daemons are reloaded, healing
|
when `INBOUND_RELAY_ENABLE=true`; DMARC maps when `DMARC_REPORTS_ENABLE=true`)
|
||||||
any drift between the extracted files and the database (the Status page's
|
and both daemons are reloaded, healing any drift between the extracted files and
|
||||||
*Reload configuration* button runs the same step on demand). This is why the
|
the database. The Status page's *Reload configuration* button resyncs OpenDKIM,
|
||||||
|
the sender map, and inbound maps only — not DMARC maps. This is why the
|
||||||
compose file pins a fixed tag rather than `:latest`: without a known version,
|
compose file pins a fixed tag rather than `:latest`: without a known version,
|
||||||
there'd be no way to tell which image restoring a given backup actually requires
|
there'd be no way to tell which image restoring a given backup actually requires
|
||||||
(see [Fixed image tag](#fixed-image-tag)).
|
(see [Fixed image tag](#fixed-image-tag)).
|
||||||
@@ -688,8 +691,10 @@ you configure. It is **not** mailboxes, IMAP, or webmail — SelfPost never
|
|||||||
stores the message locally.
|
stores the message locally.
|
||||||
|
|
||||||
**Off by default.** Set `INBOUND_RELAY_ENABLE=true` in `.env` and recreate the
|
**Off by default.** Set `INBOUND_RELAY_ENABLE=true` in `.env` and recreate the
|
||||||
container. Until then there is no `smtp inet` listener, no Inbound item in
|
container. Until then there is no inbound-relay `smtp inet` listener, no
|
||||||
the nav, and `/inbound` is 404. Outbound 465/587 is unchanged.
|
*Inbound* item in the nav, and `/inbound` is 404. (Port 25 can still listen
|
||||||
|
when only `DMARC_REPORTS_ENABLE=true` — see [DMARC reports](#dmarc-reports).)
|
||||||
|
Outbound 465/587 is unchanged.
|
||||||
|
|
||||||
**Panel** (`/inbound`, global administrator only): add a domain, set the
|
**Panel** (`/inbound`, global administrator only): add a domain, set the
|
||||||
upstream host/port and TLS to that hop (opportunistic / required / off), and
|
upstream host/port and TLS to that hop (opportunistic / required / off), and
|
||||||
@@ -710,7 +715,8 @@ Prefer an explicit recipient list so unknown addresses are refused at RCPT
|
|||||||
and never generate a bounce (backscatter).
|
and never generate a bounce (backscatter).
|
||||||
|
|
||||||
**Anti-spam.** SelfPost does not ship a filter. To attach one, set
|
**Anti-spam.** SelfPost does not ship a filter. To attach one, set
|
||||||
`INBOUND_ANTISPAM_MILTER` (inbound listener only) and merge
|
`INBOUND_ANTISPAM_MILTER` (requires inbound relay enabled; not on DMARC-only
|
||||||
|
port 25) and merge
|
||||||
[deploy/antispam/docker-compose.antispam.yml](../deploy/antispam/docker-compose.antispam.yml)
|
[deploy/antispam/docker-compose.antispam.yml](../deploy/antispam/docker-compose.antispam.yml)
|
||||||
the same way as the nginx/Caddy fragments:
|
the same way as the nginx/Caddy fragments:
|
||||||
|
|
||||||
|
|||||||
+11
-9
@@ -29,18 +29,17 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md).
|
|||||||
| ID | Topic | Status | Progress | Plan |
|
| ID | Topic | Status | Progress | Plan |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| contributing | `CONTRIBUTING.md` | candidate | — | — |
|
| contributing | `CONTRIBUTING.md` | candidate | — | — |
|
||||||
| panel-docs | In-panel operator documentation | agreed | 6/6 | [plans/panel-docs.md](plans/panel-docs.md) |
|
|
||||||
| inbound-antispam-panel | Inbound antispam journal + allow/deny lists | agreed | 1/12 | [plans/inbound-antispam-panel.md](plans/inbound-antispam-panel.md) |
|
| inbound-antispam-panel | Inbound antispam journal + allow/deny lists | agreed | 1/12 | [plans/inbound-antispam-panel.md](plans/inbound-antispam-panel.md) |
|
||||||
| inbound-quarantine | Inbound spam quarantine (hold / review / release) | candidate | — | [plans/inbound-quarantine.md](plans/inbound-quarantine.md) |
|
| inbound-quarantine | Inbound spam quarantine (hold / review / release) | candidate | — | [plans/inbound-quarantine.md](plans/inbound-quarantine.md) |
|
||||||
| schema-squash | Squash SQLite migrations into a 2.x baseline | **2.x** | — | — |
|
| schema-squash | Squash SQLite migrations into a 2.x baseline | **2.x** | — | — |
|
||||||
|
|
||||||
**Recommended order** (not binding): the next feature is **panel-docs** once
|
**Recommended order** (not binding): the next feature is
|
||||||
agreed. dmarc-reports shipped in
|
**inbound-antispam-panel** (`1.10.0`). panel-docs shipped in
|
||||||
[CHANGELOG.md](../CHANGELOG.md) `[1.7.0]` (security review of the ingest path
|
[CHANGELOG.md](../CHANGELOG.md) `[1.8.0]`; dmarc-reports in `[1.7.0]`
|
||||||
pending); domain-stats-auto-ratelimit in `[1.6.0]`; send-log-retention in
|
(security review of the ingest path pending); domain-stats-auto-ratelimit in
|
||||||
`[1.5.0]`; inbound-relay in `[1.4.0]`; queue-retries in `[1.3.1]`; the
|
`[1.6.0]`; send-log-retention in `[1.5.0]`; inbound-relay in `[1.4.0]`;
|
||||||
2026-08-13 full-tree review follow-ups are in `[1.3.0]`. Candidates need
|
queue-retries in `[1.3.1]`; the 2026-08-13 full-tree review follow-ups are in
|
||||||
explicit agreement before they join the queue.
|
`[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
|
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
|
the **Implementation checklist** in its linked plan. The `Progress` column above
|
||||||
@@ -69,6 +68,9 @@ priority.
|
|||||||
|
|
||||||
## panel-docs
|
## panel-docs
|
||||||
|
|
||||||
|
**Status:** done — shipped in `[1.8.0]` (2026-08-18). Plan:
|
||||||
|
[plans/panel-docs.md](plans/panel-docs.md) (checklist complete).
|
||||||
|
|
||||||
**Goal:** built-in operator documentation in the panel — short pages (or a
|
**Goal:** built-in operator documentation in the panel — short pages (or a
|
||||||
help drawer) that explain what each Status check and other controls mean,
|
help drawer) that explain what each Status check and other controls mean,
|
||||||
without sending the operator out to `docs/guide.md`.
|
without sending the operator out to `docs/guide.md`.
|
||||||
@@ -87,7 +89,7 @@ read the git tree for day-to-day meaning of a card.
|
|||||||
**Dependencies / risks:** copy ownership and translation; keeping help in sync
|
**Dependencies / risks:** copy ownership and translation; keeping help in sync
|
||||||
when checks change; not bloating every page with a second column of prose.
|
when checks change; not bloating every page with a second column of prose.
|
||||||
|
|
||||||
**Version:** `1.x` MINOR; `candidate` until explicitly agreed.
|
**Version:** shipped `1.8.0`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ Documentation.
|
|||||||
| Database file | `/data/selfpost.db` (bind mount) |
|
| Database file | `/data/selfpost.db` (bind mount) |
|
||||||
| Compatibility | 1.x MINOR releases must boot a `1.0.0` data directory |
|
| Compatibility | 1.x MINOR releases must boot a `1.0.0` data directory |
|
||||||
|
|
||||||
Last updated with release **1.9.0** (`0009_application_auth_ips`).
|
Last updated with release **1.9.0** (`0009_application_auth_ips`); **1.9.1**
|
||||||
|
had no schema change.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user