audit 6: documents, comments, dead code and staticcheck; the test gaps closed
test / test (push) Canceled after 0s
test / test (push) Canceled after 0s
The last stage of the 2026-10-12 audit; no cut. Doc drift: architecture.md (the sender check in § OpenDKIM, the limit: row's 48 hours, the inbound log and filter-list routes and who may open them, migrations to 0011 and every table, KitPage as production data, Reload configuration as a restore writes, the panel's roles counted), schema-migrations.md (tables and columns since v2), guide.md (a delivery's lines go at the next prune; INBOUND_JOURNAL_MILTER_SOCKET moved to architecture.md's internal list), development.md (the env guard, the deferral the e2e proves, its fixed sleeps), roadmap.md. Comments say what the code does now; TestBaselineIsTheOnlyMigration is TestChainStartsAtTheBaseline; the milter package doc names its three milters. Dead code gone: view.Facts, dmarc HostedSuggestion and ValidateHostedAddress, app UpdateMode (each with the test that alone kept it), the method checks the method-bound routes made dead; Settings.WithResult calls resultFlash. staticcheck 2026.1 in test.yml on both modules, clean: the e2e package builds without its tests now (a test and two constants moved out of non-test files), three findings fixed. The test moved, TestImageBuildPreservesLogrotateMode, had sat in a non-test file and never run; run, it failed on the entrypoint (docker run without a hostname) and now asks with --entrypoint stat. The test gaps no step named, each closed: follow() with mail.log gone past blindMin and made again; CheckRestore with a manifest that is not JSON; the origin check with a port; every /server/ pattern in web.go through server() and in the routes table (it found GET /server/health/fragment missing from the table - guarded, not tested); L9 as the owner decided it on 2026-10-12, the comment on senderSession.Headers and the test passing an h that holds the earlier messages' From:. Plan stage 6 ticked; roadmap audit row done, 81/81. Tested on the test server: go vet + go test of 27 packages, make e2e (all pass, 513 s) on this tree; staticcheck locally on both modules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+37
-19
@@ -207,10 +207,10 @@ The server's own name (`SELFPOST_HOSTNAME`) has a key and a line in both
|
||||
tables too, without being a row of the registry
|
||||
(`domain.Service.SetServerName`): the panel's notifications are sent as
|
||||
`postmaster@` that name and signed with it. No application can use it as an
|
||||
envelope sender - the sender map is built from the registry - but the
|
||||
`From:` header is not held against the login, so an application can still
|
||||
have a message signed as the server ([security.md](security.md) § Accepted
|
||||
risks). A sending domain of the same name shares its key and selector with
|
||||
envelope sender - the sender map is built from the registry - and since
|
||||
`2.4.0` none can have a message signed as the server by writing it into
|
||||
`From:` either: the sender check holds the header against the login (§ Sender
|
||||
check). A sending domain of the same name shares its key and selector with
|
||||
the server; deleting that domain leaves the server signed under the default
|
||||
selector, which is the same key only if that was the domain's. At its start
|
||||
the notifier makes sure the tables have the line and name the selector the
|
||||
@@ -219,7 +219,8 @@ domain is added or deleted.
|
||||
|
||||
### Panel binary ([cmd/panel](../cmd/panel))
|
||||
|
||||
One process, five roles (six with the inbound relay on):
|
||||
One process, six roles (seven with the inbound relay on: its inbound journal
|
||||
milter, § Postfix above, port 25):
|
||||
|
||||
1. **HTTP server** — `:8080` (`PANEL_HTTP_ADDR`); HTTPS terminated by reverse
|
||||
proxy only. On start it runs `postconf -h -x` once for the deferred-mail retry
|
||||
@@ -398,7 +399,8 @@ is sent; the operator's view is guide § Notifications.
|
||||
minutes after it - a count alone would let the last check before a restart
|
||||
and the first after it stand for "an hour apart"; back the same way. The
|
||||
certificate's row remembers which threshold was said for which expiry. A
|
||||
deleted domain's row goes with it.
|
||||
deleted domain's `dns:` row goes at the next check; its `limit:` row stays
|
||||
until it was last said more than 48 hours ago (`CheckLimits`).
|
||||
- **Said means sent.** A problem that arises while no user can be told, or
|
||||
while Postfix does not take the message, stays unsaid and is told at a
|
||||
later check. Its ending, with nobody left to tell, is simply over.
|
||||
@@ -675,6 +677,7 @@ only reads (the delete confirmation at `GET …/delete` renders a form).
|
||||
| Outbound › Log, Queue | `GET /outbound/log` · `/outbound/log/fragment` · `/outbound/log/{id}` · `GET /outbound/queue` · `/outbound/queue/fragment` |
|
||||
| Outbound › DMARC reports | `GET /outbound/dmarc` · `…/dmarc/domains/{id}` · `…/dmarc/reports/{id}` — registered only with `DMARC_REPORTS_ENABLE=true` |
|
||||
| Inbound › Domains | `GET, POST /inbound/domains` · `/inbound/domains/{id}` · `POST …/dns-recheck`, `…/upstream`, `…/recipients` · `GET, POST …/delete` — registered only with `INBOUND_RELAY_ENABLE=true` |
|
||||
| Inbound › Log, Filter lists | `GET /inbound/log` · `GET, POST /inbound/filter-lists` · `POST /inbound/filter-lists/{id}/delete` — registered only with `INBOUND_RELAY_ENABLE=true` |
|
||||
| Server | `GET /server/health` (+ `/fragment`) · `POST /server/health/recheck`, `/server/health/reload` · `GET /server/log` (+ `/fragment`) · `GET, POST /server/backup` · `POST /server/backup/import` · `/server/users`, `…/new`, `…/{uid}`, `…/{uid}/delete` · `GET, POST /server/settings` · `GET /server/components` |
|
||||
| Account | `GET /account` · `POST /account/profile`, `/account/password` |
|
||||
| User menu | `GET /help` · `POST /logout` (public mux) |
|
||||
@@ -718,7 +721,11 @@ is not.
|
||||
([handlers/handlers_inbound.go](../internal/web/handlers/handlers_inbound.go)):
|
||||
`requireInbound` (the list), `requireInboundDomain` (one domain — page,
|
||||
upstream, recipients, DNS re-check) and `requireInboundGlobal` (add, delete).
|
||||
All three answer 404 alike.
|
||||
All three answer 404 alike. The **Inbound log** (`/inbound/log`) is open to
|
||||
anyone who reaches an inbound domain (`requireInbound`) and shows the rows
|
||||
of their domains only; **Filter lists** (`/inbound/filter-lists`) are the
|
||||
instance's and the global role's (`requireInboundGlobal`)
|
||||
([handlers/handlers_inbound_log.go](../internal/web/handlers/handlers_inbound_log.go)).
|
||||
- **The DMARC hub** is open to anyone with outbound reach for the reports of
|
||||
their own domains; the *Ingest* box (server-wide statistics) is rendered for
|
||||
the global role only.
|
||||
@@ -796,8 +803,9 @@ is one layout and one way to render.
|
||||
`panel.css` (the mockups' stylesheet, rule for rule), `htmx.min.js`, `panel.js`,
|
||||
the IBM Plex subset and the logo files. Served with a content-derived `ETag`,
|
||||
and `/static/` does not list its files. The kit page
|
||||
`GET /server/components` renders every partial in every state from fixtures
|
||||
(global role only).
|
||||
`GET /server/components` renders every partial in every state from the data
|
||||
of `KitPage` ([kit.go](../internal/web/view/kit.go), production code that the
|
||||
guard tests render too; global role only).
|
||||
|
||||
**Guard tests** hold the design in place; they run in `go test ./...` and CI.
|
||||
They read their rules from the accepted mockups
|
||||
@@ -958,9 +966,9 @@ flowchart TB
|
||||
dmarcSvc --> postfix
|
||||
```
|
||||
|
||||
The four roles inside the `panel` process (HTTP server, journal-milter,
|
||||
log-tailer, rate-limit recalc) share one binary and one SQLite handle on
|
||||
purpose — see
|
||||
The roles inside the `panel` process (HTTP server, sender check,
|
||||
journal-milter, log-tailer, rate-limit recalc, notifier) share one binary and
|
||||
one SQLite handle on purpose — see
|
||||
[Panel binary](#panel-binary-cmdpanel) for why, and *Persistence* below for the
|
||||
single-connection trade-off that follows from it.
|
||||
|
||||
@@ -993,8 +1001,9 @@ full backups also archive the operator's `./certs` PEM files when present.
|
||||
### Database
|
||||
|
||||
The schema is an embedded baseline, `internal/store/migrations/0001_init.sql`,
|
||||
and the migrations after it (`0002_inbound_spam_log.sql`), applied by
|
||||
`store.Open` in file order; `PRAGMA user_version` counts the files applied and
|
||||
and the migrations after it (`0002` to `0011` today; the next is `0012`),
|
||||
applied by `store.Open` in file order; `PRAGMA user_version` counts the files
|
||||
applied and
|
||||
`PRAGMA application_id` is `0x53503230` ("SP20"). There is no 1.x migration
|
||||
chain. **2.0 starts from an empty data directory:** a database with a schema
|
||||
version but without the 2.x `application_id` — what every 1.x file looks like —
|
||||
@@ -1029,7 +1038,12 @@ The tables that carry the panel's model:
|
||||
`application_addresses`, `rate_limits` (no `allowed_ips` column), `send_log`,
|
||||
`send_log_lines` (what Postfix logged under a queue id — § Log tailer),
|
||||
`logtail_state`, `inbound_domains`, `inbound_transports`,
|
||||
`inbound_recipients`, `dmarc_reports`, `dmarc_report_records`.
|
||||
`inbound_recipients`, `inbound_spam_log` (port 25's decisions, § Postfix),
|
||||
`inbound_spam_list` (the filter lists), `dmarc_reports`,
|
||||
`dmarc_report_records`, `notify_state` and `notify_addressees` (what the
|
||||
notifier has said and to whom, § Notifications), `sender_check_days` (the
|
||||
sender check's downtime and the mail it deferred, per UTC day) and
|
||||
`delivery_log_days` (the queue sweep's measurements, § Log tailer).
|
||||
|
||||
Two processes write: the panel, on one connection, and `dmarc-ingest`, which
|
||||
Postfix starts per report. SQLite runs in WAL mode (see
|
||||
@@ -1051,13 +1065,14 @@ under `/data`), `docker-compose.yml`, `.env`, and `certs/` when present;
|
||||
version check on restore. Requires the project directory mounted read-only at
|
||||
`SELFPOST_DEPLOY_ROOT` (`/selfpost-deploy` in the default compose file). On the
|
||||
first successful boot after restore, the panel runs one **Resync** — OpenDKIM's
|
||||
tables, Postfix's sender map, inbound relay maps (when
|
||||
tables, Postfix's sender map, inbound relay maps and the filter lists (when
|
||||
`INBOUND_RELAY_ENABLE=true`), and DMARC maps (when `DMARC_REPORTS_ENABLE=true`)
|
||||
are re-derived from SQLite and both daemons are reloaded, so drift between the
|
||||
extracted archive and the database is healed before mail flows. Manual
|
||||
`POST /server/health/reload` (the *Reload configuration* button on Server ›
|
||||
Health) runs the same Resync, the filter lists with it, since `2.5.4`.
|
||||
Stopped-container
|
||||
Health) writes the same files in the same order (`HandleReload`; before
|
||||
`2.5.4` it left out the filter lists and the DMARC maps), and on a failure
|
||||
stops at that step and names it on the page. Stopped-container
|
||||
`tar` of `./data` alone remains possible for state-only copies (see guide).
|
||||
|
||||
**Optional encryption** of the two secret-bearing downloads
|
||||
@@ -1095,7 +1110,10 @@ unsupported rather than as a missing doc:
|
||||
(`/data/selfpost.db`), `SELFPOST_SETUP_TOKEN_FILE`
|
||||
(`/data/setup-token`), `PANEL_HTTP_ADDR` (`:8080`),
|
||||
`JOURNAL_MILTER_SOCKET` (`/run/selfpost/journal.sock`),
|
||||
`SENDER_CHECK_SOCKET` (`/run/selfpost/sender.sock`), `MAIL_LOG`
|
||||
`SENDER_CHECK_SOCKET` (`/run/selfpost/sender.sock`),
|
||||
`INBOUND_JOURNAL_MILTER_SOCKET` (`/run/selfpost/inbound-journal.sock` — the
|
||||
inbound journal milter's socket, read by the panel, `build/postfix-config.sh`
|
||||
and `build/postfix-wrapper.sh`, which must agree), `MAIL_LOG`
|
||||
(`/data/log/mail.log` — read by the panel and written by Postfix, so a change
|
||||
here has to be matched in `build/postfix-config.sh`),
|
||||
`PANEL_COOKIE_SECURE` (`true`), `OPENDKIM_SOCKET`
|
||||
|
||||
@@ -496,6 +496,49 @@ tests; `TestRenderInboundMaps` has no two-routes-one-next-hop case (L5);
|
||||
rows of `review-2026-08-followups` (apps/domains/users/dmarc handlers
|
||||
directly, `internal/dmarc` service) are still open.
|
||||
|
||||
*Checked in stage 6 (2026-10-15):* **closed** since, each by the test that
|
||||
now holds it: the H1 stamp (e2e `image_stamped_with_version`); cert re-copy
|
||||
(e2e `renewed_certificate_picked_up`); a recreated log and lines written to
|
||||
the old inode after the switch (`TestFollowTailsAndRotates`,
|
||||
`TestFollowReadsTheRotatedFileAfterTheSwitch`,
|
||||
`TestALogRotatedWhileDownIsReadToItsEnd`); a truncated `postqueue -p`
|
||||
(`TestQueueIDsRefusesATruncatedListing`); a `sent` row left alone by the
|
||||
sweep and by a reused id (`TestCloseUnreportedClosesOnlyQueuedRows`,
|
||||
`TestUpdateStatusRewritesOnlyTheNewestRow`); H4
|
||||
(`TestAddApplicationRefusesALoginDifferingOnlyInCase`,
|
||||
`TestServiceCreateFoldsTheLogin`); M1's ordering
|
||||
(`TestARestoreStartThatFailsIsARestoreAgain`; the real reload by e2e
|
||||
`full_backup_restore`); M3 (`TestSnapshotRefusesAMissingDatabase`,
|
||||
`TestBackupOfAMissingDatabaseIsRefused`); M5 (`TestEncryptionPasswordFloor`);
|
||||
L1 (`TestDecryptLeavesTheOutputAloneOnFailure`); L2
|
||||
(`TestCertsLinksAreFollowed`, `TestCheckRestoreReadsOnlyRegularFiles`); L3
|
||||
(`TestBytesAfterTheLastChunkRejected`); M13 (e2e
|
||||
`panel_hung_mid_message_is_counted`, and the unit fixture now holds the
|
||||
`cleanup` lines as the stand wrote them); M15
|
||||
(`TestServerPTRTimeoutIsUnknown`,
|
||||
`TestServerAddressTimeoutIsUnknownAndKeepsTheAddresses`);
|
||||
`TestLimitRefusalsSince` (the real `limitRefusalsQuery` is planned in
|
||||
`TestSendLogQueriesUseTheirIndexes`); `TestOpenMigratesA20Database` (migrates
|
||||
to the head, 11); M10 (`TestDomainImportIsCheckedLikeTheForms`); M11
|
||||
(`TestPanelJS`: `poll_test.js`, `unsaved_test.js`); M8
|
||||
(`TestLoginTakesAsLongForAnUnknownName`); M7
|
||||
(`TestTheLastGlobalSurvivesTwoAtOnce`); `internal/atomicfile` and
|
||||
`internal/supervisor` (tests of their own); L5
|
||||
(`TestRenderInboundMapsOneTLSLinePerNextHop`); the handler rows of
|
||||
`review-2026-08-followups` (struck in the roadmap with their reasons; users,
|
||||
DMARC and apps have handler tests). The five then **still open**, named by
|
||||
no step, were closed in stage 6 the same day: `follow()` with a log gone
|
||||
past `blindMin` and made again
|
||||
(`TestFollowCountsALogItCannotReadAndReadsItAgain`); `CheckRestore` with a
|
||||
manifest that is not JSON (`TestCheckRestoreRefusesAManifestThatIsNotJSON`);
|
||||
a Host with a port against an Origin (`TestOriginCheckComparesThePort`);
|
||||
every `/server/` pattern in web.go registered through `server()` and listed
|
||||
in the routes table (`TestEveryServerRouteIsGuardedAndListed` - it found
|
||||
`GET /server/health/fragment` missing from the table; the route was guarded,
|
||||
not tested); L9 as the owner decided it, the comment on
|
||||
`senderSession.Headers` and `TestSenderCheckResetsBetweenMessages` passing
|
||||
an `h` that holds the earlier messages' `From:`.
|
||||
|
||||
### Checked and sound
|
||||
|
||||
Worth writing down so the next review does not re-read it: the sender check's
|
||||
@@ -1512,20 +1555,55 @@ log it owns anyway; envdoc_test reads `${VAR:-}` and the Go helpers, not
|
||||
bare `$VAR` or python (none today); the 600 s pruning in crashexit is not
|
||||
tested (the count and the spared are).
|
||||
|
||||
### Stage 6 — documentation and roadmap sweep (no product code, no cut)
|
||||
### Stage 6 — documentation and roadmap sweep (no product code, no cut) (done 2026-10-15)
|
||||
|
||||
- [ ] Every item under *Doc drift* above, file by file. — **Sonnet**
|
||||
- [x] Every item under *Doc drift* above, file by file. — **Sonnet**
|
||||
*Done 2026-10-15:* architecture.md (the sender check in § OpenDKIM, the
|
||||
`limit:` row's 48 hours, the inbound log and filter-list routes and who
|
||||
may open them, migrations to `0011` and every table, `KitPage` as
|
||||
production data, Reload configuration as a restore writes, the panel's
|
||||
roles counted again); schema-migrations.md (the tables and columns since
|
||||
v2); guide.md (a delivery's lines go at the next prune;
|
||||
`INBOUND_JOURNAL_MILTER_SOCKET` out of the operator's table, into
|
||||
architecture.md's internal list); development.md (the env guard as
|
||||
envdoc_test reads now, the deferral the e2e proves, the e2e's fixed
|
||||
sleeps said where they are); roadmap.md's closing paragraph. Already right
|
||||
by then: the `.alive` mark, `dmarc-ingest` as a second writer, the chain
|
||||
head, M1's wording, the version stamp.
|
||||
- [ ] roadmap.md: done ahead on 2026-10-13/14 with the owner -
|
||||
`template-data-typing` struck, `structured-logging` dropped,
|
||||
`review-2026-08-followups` closed with each row placed in a stage or
|
||||
dropped with its reason. What is left: check that every *Test gaps* entry
|
||||
above is named by the step that touches its code, and add it to that step
|
||||
where it is not. — **Sonnet**
|
||||
- [ ] Comments named above (`milter.go`, `sendlog.go`, `store.go`, `schema_test.go`,
|
||||
*Done 2026-10-15:* see *Checked in stage 6* under *Test gaps*: five were
|
||||
open and named by no step; each got its test in this stage (L9 as the
|
||||
owner decided it).
|
||||
- [x] Comments named above (`milter.go`, `sendlog.go`, `store.go`, `schema_test.go`,
|
||||
`machine.go`, `inboundspam.go`, `logtail.go`, supervisord.conf,
|
||||
entrypoint.sh, docker-compose.yml). — **Haiku**
|
||||
- [ ] Dead code removal from § Nits, with `staticcheck` added to `test.yml` so it
|
||||
*Done 2026-10-15:* each said what its code does now (the journal-milter
|
||||
tempfails at MAIL FROM; the rejected status covers the sender check; the
|
||||
two roles; the log is read whole for a filter list's hits; logrotate
|
||||
uses `create`; the guide's headings as they are). `TestBaselineIsTheOnlyMigration`
|
||||
renamed `TestChainStartsAtTheBaseline`. supervisord.conf and
|
||||
entrypoint.sh were already right.
|
||||
- [x] Dead code removal from § Nits, with `staticcheck` added to `test.yml` so it
|
||||
stays gone. — **Haiku**
|
||||
*Done 2026-10-15:* gone: `view.Facts`, `dmarc.Service.HostedSuggestion`
|
||||
and `ValidateHostedAddress`, `app.Service.UpdateMode` (each with the
|
||||
test that alone kept it), the method checks of `HandleDomainDMARC` and the
|
||||
`default:` branches of the user and settings handlers;
|
||||
`Settings.WithResult` calls `resultFlash`. Kept: `domainIDSet` (used by
|
||||
the send log's authorization), `KitPage()`'s `User` (the fixtures render
|
||||
`KitPage()` without the handler); the `set-permissions` branch was gone
|
||||
already. staticcheck 2026.1 (the release for Go 1.26) runs in test.yml
|
||||
on both modules, clean; for the e2e module the package now builds without
|
||||
its tests (a test and two constants moved out of non-test files), and
|
||||
three findings there were fixed (ST1005, SA4006). The test moved,
|
||||
`TestImageBuildPreservesLogrotateMode`, had sat in a non-test file since
|
||||
it was written, and go test had never run it; run, it failed on the
|
||||
entrypoint (`docker run` without a hostname) and now asks past it.
|
||||
|
||||
### Decided with the owner on 2026-10-12
|
||||
|
||||
|
||||
+17
-7
@@ -420,9 +420,14 @@ go test ./...
|
||||
|
||||
### Env documentation regression
|
||||
|
||||
`go test ./cmd/panel -run 'TestLoadConfigKeysDocumented|TestBuildScriptKeysDocumented|TestDocumentedKeysAreRead'`
|
||||
— every new `loadConfig` key must appear in the env lists in [guide.md](guide.md)
|
||||
([cmd/panel/envdoc_test.go](../cmd/panel/envdoc_test.go)).
|
||||
`go test ./cmd/panel -run 'TestEnvironment'` compares what the code reads with
|
||||
what the documents name ([cmd/panel/envdoc_test.go](../cmd/panel/envdoc_test.go)).
|
||||
Read: the `envDefault` / `envInt` / `envBool` / `envSeconds` / `envDuration` /
|
||||
`os.Getenv` / `os.LookupEnv` calls in `cmd/panel` and `internal` (tests aside),
|
||||
and `${VAR:-…}` in `build/*.sh`. Named: [guide.md](guide.md) § Environment
|
||||
variables (the table and the paragraph under it) and the internal list in
|
||||
[architecture.md](architecture.md) § Configuration. A variable read and named
|
||||
nowhere fails the first test; one named and read nowhere fails the second.
|
||||
|
||||
### Panel pages: the component kit and its guards
|
||||
|
||||
@@ -470,12 +475,16 @@ operator would actually use.
|
||||
|
||||
**Coverage (summary):** bootstrap → SMTP AUTH → delivery → DKIM verify →
|
||||
send-log `queued → sent`; negatives (no AUTH, relay, sender/login mismatch,
|
||||
L1/L2 limits, milter fail-open, bad `SELFPOST_HOSTNAME`, session survives
|
||||
`docker restart`); startup checks that supervisord actually brought up
|
||||
L1/L2 limits, the panel down or hung mid-message (Postfix defers the mail
|
||||
rather than letting it through: `panel_down_defers_mail`,
|
||||
`panel_hung_mid_message_is_counted`), bad `SELFPOST_HOSTNAME`, session
|
||||
survives `docker restart`); startup checks that supervisord actually brought up
|
||||
OpenDKIM, the panel, and Postfix (`checkSupervisorProcesses`), plus logrotate
|
||||
config-mode and forced-rotation checks (`checkLogrotateConfigMode`,
|
||||
`checkLogrotateRotation` — [test/e2e/logrotate_check.go](../test/e2e/logrotate_check.go)).
|
||||
Polling with timeouts only — no fixed `sleep`.
|
||||
Waits poll with a timeout (`waitFor`); a fixed sleep is the exception and says
|
||||
why where it stands (`sender_check_test.go`: a downtime worth a second or two,
|
||||
and 35 s to show a row is not written again).
|
||||
|
||||
Requires **Docker + Compose v2** on the machine running the suite.
|
||||
|
||||
@@ -573,7 +582,8 @@ Out of scope for v1.x: `CONTRIBUTING.md`, man pages, a separate docs site
|
||||
behaviour ships together with [guide.md](guide.md) / `.env.example` and a
|
||||
CHANGELOG entry (see [§ Commits and release build](#commits-and-release-build)).
|
||||
2. **Env regression:** [cmd/panel/envdoc_test.go](../cmd/panel/envdoc_test.go)
|
||||
fails on an undocumented `loadConfig` or build-script key.
|
||||
fails on a variable the code or a build script reads that no document names,
|
||||
and on one a document names that nothing reads.
|
||||
3. **New gaps** go into [roadmap.md](roadmap.md) (or the active plan file), not
|
||||
silent drive-by edits.
|
||||
4. **Schema changes:** update [schema-migrations.md](schema-migrations.md) in the
|
||||
|
||||
+3
-2
@@ -228,7 +228,6 @@ expected to set; defaults match the code exactly.
|
||||
| `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`. The panel reads it to report on the *Spam filter* box of the inbound pages and to check that the filter answers (Server › Health). | *(empty)* | `.env` |
|
||||
| `INBOUND_ANTISPAM_MILTER_ACTION` | What Postfix does if that milter is down: `accept` (fail-open) or `tempfail` (refuse with a temporary error). The *Spam filter* box states which. | `accept` | `.env` |
|
||||
| `INBOUND_SPAM_LOG_RETENTION_DAYS` | How long the inbound journal keeps a row, 7-365 days; it also never holds more than 10 000 rows. Seeds the setting on the first start with the inbound relay on. | `90` | `.env` |
|
||||
| `INBOUND_JOURNAL_MILTER_SOCKET` | Unix socket of the panel's inbound journal milter, the last milter on port 25 while the inbound relay is on. It records accepted inbound mail and never refuses any; there is no reason to change it. | `/run/selfpost/inbound-journal.sock` | compose |
|
||||
| `INBOUND_RATE_LIMIT_MESSAGES_PER_IP` | Coarse per-client-IP cap on port 25: how many times one client IP may start a message (`MAIL FROM`) per window, accepted or not (`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`). When both inbound relay and DMARC ingest are on, the shared listener uses the **greater** of this and `DMARC_MESSAGE_SIZE_LIMIT`. This is port 25's whole limit; 465 and 587 keep Postfix's own, 10 240 000 bytes. | `26214400` (25 MiB) | `.env` |
|
||||
| `RATE_LIMIT_MESSAGES_PER_IP` | Level-1 backstop: how many times one client IP may start a message (`MAIL FROM`) on one listener per window, accepted or not; 465 and 587 count separately (Postfix `smtpd_client_message_rate_limit`). See [Rate limiting — level 1](#rate-limiting--level-1-ip-backstop). | `100` | `.env` |
|
||||
@@ -1428,7 +1427,9 @@ with the *domain* role see them here because they cannot open the queue. Under
|
||||
both sit the `mail.log` lines for its queue id: the connection to the
|
||||
receiving server, the server's reply, and the status that reply was filed
|
||||
as. The lines are kept in the database with the row, so they stay on the
|
||||
page after `mail.log` has rotated and are deleted together with the row; a
|
||||
page after `mail.log` has rotated. They go at the next six-hourly prune after
|
||||
the row is deleted by retention, and not while another row carries the same
|
||||
queue id (Postfix reuses ids); a
|
||||
message accepted before `2.2.0` shows them only while they are still in the
|
||||
current file, and the page says so when there are none. Retention is set under **Server › Settings** (global
|
||||
role); `SEND_LOG_RETENTION_DAYS` in `.env` is only the initial default until it
|
||||
|
||||
+14
-15
@@ -46,7 +46,7 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md).
|
||||
| from-header-binding | Hold a message's `From:` header against the application that sends it | shipped `2.4.0` | 9/9 | [plans/from-header-binding.md](plans/from-header-binding.md) |
|
||||
| panel-redesign | Panel redesign (accepted mockups, design contract) | shipped `2.0.0` | 27/27 | [plans/panel-redesign.md](plans/panel-redesign.md) |
|
||||
| schema-squash | Squash SQLite migrations into a 2.x baseline | built in panel-redesign (stage 1) | — | [plans/panel-redesign.md](plans/panel-redesign.md) |
|
||||
| audit-2026-10-12 | Follow-ups of the 2026-10-12 full-tree code audit, seven stages | in progress (stage 0 shipped `2.4.2`, stages 1+2 `2.5.0`, 2b `2.5.1`, 2c `2.5.2`, 3 `2.5.3`, 4 `2.5.4`, 5 `2.6.0`; next stage 6, docs only) | 77/81 | [audit-2026-10-12.md](audit-2026-10-12.md) |
|
||||
| audit-2026-10-12 | Follow-ups of the 2026-10-12 full-tree code audit, seven stages | done 2026-10-15 (stage 0 shipped `2.4.2`, stages 1+2 `2.5.0`, 2b `2.5.1`, 2c `2.5.2`, 3 `2.5.3`, 4 `2.5.4`, 5 `2.6.0`; stage 6 docs and tests, no cut) | 81/81 | [audit-2026-10-12.md](audit-2026-10-12.md) |
|
||||
|
||||
**Recommended order** (not binding; owner, 2026-10-09: feature order follows
|
||||
development convenience): **panel-redesign** shipped as `2.0.0` (2026-10-11) and
|
||||
@@ -57,9 +57,9 @@ shipped as `2.2.0` the same day, **panel-notifications** as `2.3.0` and
|
||||
[audit-2026-10-12](audit-2026-10-12.md) shipped as `2.4.2` (2026-10-14), its
|
||||
stages 1 and 2 as `2.5.0` the same day, stage 2b as `2.5.1`, stage 2c as
|
||||
`2.5.2` (2026-10-15), and stages 3, 4 and 5 as `2.5.3`, `2.5.4` and
|
||||
`2.6.0` the same day.
|
||||
Nothing is agreed after it: every remaining
|
||||
item is a candidate. panel-docs shipped in
|
||||
`2.6.0` the same day; stage 6 (documents and tests, no cut) closed the
|
||||
audit. What is agreed after it is marked so in the index; every other
|
||||
remaining item is a candidate. panel-docs shipped in
|
||||
[CHANGELOG.md](../CHANGELOG.md) `[1.8.0]`; dmarc-reports in `[1.7.0]`
|
||||
(its ingest path first ran in `2.0.0` and was reviewed on 2026-10-11, see
|
||||
[security.md](security.md)); domain-stats-auto-ratelimit in
|
||||
@@ -67,17 +67,16 @@ item is a candidate. panel-docs shipped in
|
||||
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.
|
||||
|
||||
The 2026-08-19 full-tree code review is closed except for four items, all
|
||||
listed above and none of them urgent: [origin-strict](#origin-strict) (its
|
||||
"add CSRF tokens", decided otherwise on 2026-10-13),
|
||||
template-data-typing (done by `2.0.0`; its last map is a step of
|
||||
[audit-2026-10-12](audit-2026-10-12.md) stage 4),
|
||||
structured-logging ([dropped](#dropped-with-the-condition-for-return) on
|
||||
2026-10-13) and
|
||||
[review-2026-08-followups](#review-2026-08-followups). What that review
|
||||
produced is in [CHANGELOG.md](../CHANGELOG.md) `[1.9.2]`; its own document
|
||||
was removed once these four items carried everything still open, and stays in
|
||||
git history.
|
||||
The 2026-08-19 full-tree code review is closed except for one item, listed
|
||||
above and not urgent: [origin-strict](#origin-strict) (its "add CSRF tokens",
|
||||
decided otherwise on 2026-10-13). Of its other three,
|
||||
template-data-typing is done (the last untyped map, the health sampler's, went
|
||||
in [audit-2026-10-12](audit-2026-10-12.md) stage 4, `2.5.4`),
|
||||
structured-logging is [dropped](#dropped-with-the-condition-for-return)
|
||||
(2026-10-13) and [review-2026-08-followups](#review-2026-08-followups) is
|
||||
closed (2026-10-14). What that review produced is in
|
||||
[CHANGELOG.md](../CHANGELOG.md) `[1.9.2]`; its own document was removed once
|
||||
these four items carried everything still open, and stays in git history.
|
||||
|
||||
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
|
||||
|
||||
@@ -94,7 +94,7 @@ for operators upgrading from older 2.x images.
|
||||
|
||||
---
|
||||
|
||||
## Schema at head (v2)
|
||||
## Schema at head
|
||||
|
||||
| Table | Role |
|
||||
|-------|------|
|
||||
@@ -106,7 +106,7 @@ for operators upgrading from older 2.x images.
|
||||
| `applications` | SASL applications per domain, with the client-IP restriction (`auth_ip_restrict`, `auth_allowed_ips`) |
|
||||
| `application_addresses` | Explicit From addresses (`list` mode) |
|
||||
| `rate_limits` | Level-2 limits per domain / application, manual or auto |
|
||||
| `send_log` | Delivery journal |
|
||||
| `send_log` | Delivery journal. `reason` says why a row was refused (`limit`, `client`, `from`) or that the sweep closed it (`reconcile`, `reconcile-contradicted`), `header_from` what the From: line of a message refused for it said (v6), `refusal_line`, `client_ip` and `recovered` (v9), and `domain_id` (v8: the row belongs to a domain by id, `ON DELETE SET NULL`) |
|
||||
| `logtail_state` | Log-tailer read position |
|
||||
| `inbound_domains` | Inbound relay domains |
|
||||
| `inbound_transports` | Upstream host / port / TLS per inbound domain |
|
||||
@@ -114,9 +114,12 @@ for operators upgrading from older 2.x images.
|
||||
| `user_inbound_domains` | Inbound domains assigned to a `domain` user — granted separately from the outbound ones |
|
||||
| `dmarc_reports` | Parsed aggregate report summaries |
|
||||
| `dmarc_report_records` | Per-source rows inside a report |
|
||||
| `inbound_spam_log` | Inbound antispam journal: one row per recipient of a message offered on port 25, with the decision (`accept`, `reject`, `tempfail`, `quarantine`) and who made it (`rspamd`, `postfix`, `selfpost`). Metadata only (v2) |
|
||||
| `inbound_spam_log` | Inbound antispam journal: one row per recipient of a message offered on port 25, with the decision (`accept`, `reject`, `tempfail`, `quarantine`) and who made it (`rspamd`, `postfix`, `selfpost`). Metadata only. `inbound_domain_id` (v8) ties a row to its domain by id (v2) |
|
||||
| `inbound_spam_list` | Allow / deny entries for the external filter (`address`, `domain`, `ip`), instance-wide; unique per list (v2) |
|
||||
| `notify_state` | What the panel has already written to its users about, one row per subject (`dns:<domain>`, `cert`, `queue`, `limit:<domain>`), so that each thing is said once and a restart says nothing again (v4) |
|
||||
| `sender_check_days` | What the sender check costs, a row per UTC day that has something to say: seconds the panel did not answer while Postfix ran, connections turned away (`deferred`), messages deferred mid-message (`messages`), and whether the day's downtime is an upper bound (`estimated`). Kept a year (v6, v9) |
|
||||
| `delivery_log_days` | What the reconcile sweep and the log tailer did, a row per UTC day, kept a year. Read off a copy of the database; no page shows it (v7) |
|
||||
| `notify_addressees` | Who a subject's notification is owed to and whether each was handed it; goes with the subject's `notify_state` row (v11) |
|
||||
| `send_log_lines` | The `mail.log` lines Postfix wrote under a queue id, with the line's own time; what a message's page shows once the log has rotated. No reference to `send_log`: a line belongs to a row by queue id and time, and goes when no row carries its id (v3) |
|
||||
|
||||
`TestBaselineSchema` (`internal/store/schema_test.go`) pins every table, column
|
||||
@@ -150,7 +153,7 @@ for the domain. Nothing refers to a user: deleting one changes no domain.
|
||||
|
||||
1. Add `internal/store/migrations/00NN_short_name.sql` (next number).
|
||||
2. Update the table in [Migration chain](#migration-chain), the snapshot at the
|
||||
top, and the affected rows of [Schema at head](#schema-at-head-v2).
|
||||
top, and the affected rows of [Schema at head](#schema-at-head).
|
||||
3. Update `TestBaselineSchema` if tables, columns or indexes change, and add a
|
||||
test that opens a database at the previous version and migrates it.
|
||||
4. A *data* migration states in a comment which 2.x versions it is written for.
|
||||
|
||||
Reference in New Issue
Block a user