inbound journal: one row per recipient, and every row counts to the cap (audit M3)

Security audit 2026-10-17, finding M3, stage 1. inboundSession.RcptTo
appended every RCPT (the outbound session de-duplicates), and the end of
the message inserted a row per recipient one statement at a time inside
Postfix's callback; the cut to 10 000 rows every 500 counted only the log
tailer's rows. Postfix accepts a repeated recipient up to
smtpd_recipient_limit (1 000) and sends each to the milter, so one message
was a thousand rows, uncut until the six-hourly prune.

Now: the inbound milter folds a repeated recipient (case-insensitive, as
cleanup's been_here() with BH_FLAG_FOLD does, and as session.RcptTo does);
a message's rows go in one transaction (store.InsertInboundSpamMessage,
which replaces InsertInboundSpam in milter.InboundStore); the count and the
cut moved from the tailer into the store (countInboundWrites, an atomic
counter on Store) so the milter's rows, the tailer's and the tailer's
start-up re-reads all count. store.InboundSpamLogMaxRows is the one cap.

Tests: TestInboundRecipientRepeatedIsOneRow (1 000 RCPT in two cases, one
row); TestInboundJournalIsCutWhicheverWriterWrites (a small cap: a message's
rows and refusals together cross the mark, the table is cut); the tailer's
own counting test went with its counter. go test ./internal/store
./internal/milter ./internal/logtail (Windows-only Follow failures aside).
Also gofmt of a test line from the M2 commit.

Docs: security.md § Mail path, architecture.md; CHANGELOG; plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-17 21:44:28 +03:00
co-authored by Claude Opus 5.5
parent d254a9952d
commit c50314fb4c
11 changed files with 216 additions and 53 deletions
+5 -2
View File
@@ -727,13 +727,16 @@ the argument.
that an unauthenticated session reserves nothing. — **Opus**, 2 h.
*Done 2026-10-17:* TestRateLimitIsNotReservedByAnotherSendersMailFrom;
e2e `domain_limit_not_reserved_by_a_stranger`.
- [ ] **M3.** De-duplicate recipients in `inboundSession.RcptTo` (case-folded,
- [x] **M3.** De-duplicate recipients in `inboundSession.RcptTo` (case-folded,
as the outbound session does); the milter's inserts count toward the prune
trigger (a counter in the store beside `InsertInboundSpam`, the prune
called from whichever side crosses 500); one transaction per message.
Done when a unit test sends one message with the same recipient 1 000
times and sees one row, and 600 milter rows with no refusal trigger the
cut. — **Opus**, 2 h.
cut. — **Opus**, 2 h. *Done 2026-10-17:* the counter and the cut moved
into the store (`countInboundWrites`, both writers),
`InsertInboundSpamMessage`; TestInboundRecipientRepeatedIsOneRow,
TestInboundJournalIsCutWhicheverWriterWrites.
- [ ] **W1.** Refuse a custom report address on the hostname; exact hosted
address only in `AllowedRecipients`; a tag required for every recipient on
the hostname in `checkReportDomain`; the existing test inverted; the