Technical review (Opus) and security review (Fable) of 9efc9c7..a59e3d4;
reports in the session's scratchpad. Fixed:
- B1 (verified bug): store.UpdateStatus read before writing in its
transaction; a commit by another connection in between (the dmarc-ingest
process opens the same file) failed it at once with "database is locked"
and the delivery line was lost. The first statement is now a write.
- R3: a delivery line updated every row of its queue id and recipient; it
now updates only the newest (Postfix reuses short ids). The sweep closes
by row id (QueuedDelivery.ID, CloseUnreported(id)).
- R4: a contradicted sweep keeps a mark, reason "reconcile-contradicted",
instead of losing which row it was.
- R1: contradicted cannot see a lost line (never read late). New counter
reconcile_missed: at close, the sweep looks for the row's delivery line
in mail.log and mail.log.1 (QueueLines). 0007 edited - it has run on no
database but throwaway stands.
- R2: queued_over_lifetime cannot measure "what removing the sweep would
leave" while the sweep runs; its text now says rows stuck despite the
sweep, and the rule uses closed - missed - contradicted.
- cert-reload (R5, R6, R7, security medium/low): both files copied to .new
and the copies checked as a pair before both are renamed; acts on the copy
only when postconf says Postfix reads it; a failed reload is retried next
pass; umask 077; says so when the mounted files cannot be read. The e2e
case asserts Postfix reads the copy, the copy equals the mount, the key is
600 root.
- Security low: a recipient with a quoted local part - the journal cut
"a b"@x at the space, deliveryRe cut "a>b"@x at the ">" - matched no
delivery line and was closed by the sweep. cleanAddress ends a bracketed
address at the first ">" outside quotes; deliveryRe takes the recipient
up to the ">, " before relay= (test fixtures given the relay= every real
delivery line has).
- Security low: postqueue -p's error carries at most 512 bytes of output.
- Nits: addDeliveryLogDay refuses a column not on its list; the truncation
test checks its message.
- To stage 2b (page wording, mockup-first): the history step of a bounced
row says the receiving server refused it, false for a row the sweep
closed. To the owner when stage 2b is reached: since M13 the "deferred"
figure counts whole messages too, which "Connections turned away" would
not name.
Found while verifying: the e2e's main positive path failed once with "553
not owned by user app1" - an smtpd idle from before the panel's reload took
the connection in the reload's second. Measured on the stand (one-off
probe, not committed): sending 0, 10, 40, 110 s after the save (two seconds
at the soonest) always met an smtpd started after the reload. The e2e now
waits for the sender map after each application it creates
(waitForSenderMap: AUTH, MAIL, RCPT, RSET - no message, no row), and the
level-1 case uses it instead of its own loop.
Docs: architecture § Queue reconcile, schema-migrations (rows 7 and 8,
chain head 8), CHANGELOG, audit plan (stage 1 note, the measured window,
a new stage 2b line: 62 lines), roadmap 15/62.
Evidence on the test server for this tree: go vet clean, go test ./... 23
packages ok, make e2e exit 0 (renewed certificate, hung panel, level 1 at
49/60).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
postfix-config.sh copies the mounted certificate and key into
/etc/postfix/tls-internal at start (root-owned, key 0600) and Postfix reads
the copy; cert-reload only ran `postfix reload` daily, which re-read the
stale copy, so a certificate renewed while the container ran expired on
465/587 with the old one.
postfix-cert-reload.sh now, every TLS_RELOAD_INTERVAL_SECONDS: compares the
mount with the copy (cmp); when they differ and the certificate and key
belong together (openssl public keys equal - a renewer writes them one
after the other), writes each file beside its target, root-owned with the
start's modes, renames it over the target, and reloads Postfix. Nothing
changed - no reload. No copy (files missing at start) - the old periodic
reload. Never exits non-zero.
e2e renewed_certificate_picked_up: writes a new certificate into the
stand's mount and waits (30 s) for port 465 to present its serial;
compose.override.yml sets TLS_RELOAD_INTERVAL_SECONDS=2.
supervisord.conf comment, architecture.md (process table, § Postfix TLS
certs, the env knob), guide (after the proxies: picked up within a day).
CHANGELOG [Unreleased], audit plan line ticked, roadmap 12/61.
Evidence: GOOS=linux go vet in test/e2e clean; the server run of the stage
follows before the push.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Initial feature-complete implementation of the v1.0 specification (phases 0-11 of `docs/implementation-plan.md`).
Added:
- Panel (Go, single static binary) with SQLite persistence, one-time
- Domain management with per-domain DKIM (RSA-2048, generated in pure Go) and
- Application (sender identity) management: SASL credentials via `sasldb2`,
- Full Postfix relay config generated from env at container start: SMTPS 465,
- Journal milter (pure Go, `go-milter`) recording every send to `send_log`;
… (6 more in CHANGELOG)
Full detail: CHANGELOG.md (0.1.0).