journal milter: recipients as Postfix logs them, once each
test / test (push) Canceled after 0s

The row's recipient has to be the address Postfix writes in to=<..> or
orig_to=<..>, or the delivery line finds no row and the sweep closes it as
bounced. Postfix 3.7.11 hands the milter the RCPT argument as the client
wrote it (smtpd.c, milter_argv(); extract_addr() leaves it), go-milter
v0.4.1 trims "<>" and drops the ESMTP parameters (session.go, case
CodeRcpt) - so cleanAddress's cut at the first blank broke a quoted local
part (B-G5). Postfix keeps the address in internal form (tok822: quotes
off, backslash escapes taken) and logs it external
(info_log_addr_form.c, quote_822_local(): quoted again only when not a
dot-string); recipientAsLogged does the same, "ab"@x -> ab@x, "a b"@x
stays. A recipient given twice in any case is one row: cleanup drops the
duplicate, comparing folded (cleanup_out_recipient.c, been_here(),
BH_FLAG_FOLD; A-31). A rewritten recipient is matched by orig_to=, which
log_adhoc() writes only when it differs (A-9). The store's
case-insensitive match stays, now with its reason from the source (D-S20).

Stand (2026-10-14, make e2e, exit 0): new recipients_as_postfix_logs_them
- RCPT TO "x y"@, "ab"@ and "X Y"@ sink.e2e.test give two rows, "x
y"@sink.e2e.test and ab@sink.e2e.test, both sent. go vet and go test ./...
pass on the server; a unit test drives the milter through go-milter's own
client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-14 20:30:47 +03:00
co-authored by Claude Opus 5.5
parent 0ed03b2094
commit c8f669714e
11 changed files with 302 additions and 58 deletions
+11 -4
View File
@@ -116,17 +116,24 @@ type RetentionDays func() int
// controls. A bounce whose reply quoted "status=sent" would then be filed as a
// success. The real field is always the first one after to=<…>: in
// log_adhoc() the reply is the "(%s)" after status=.
var deliveryRe = regexp.MustCompile(`^\S+ [^\s\[]+\[\d+\]: ([0-9A-Za-z]+): to=<(.*?)>, (?:orig_to=<.*?>, )?relay=.*?\bstatus=(\w+)`)
var deliveryRe = regexp.MustCompile(`^\S+ [^\s\[]+\[\d+\]: ([0-9A-Za-z]+): to=<(.*?)>, (?:orig_to=<(.*?)>, )?relay=.*?\bstatus=(\w+)`)
// parseDelivery extracts (queue-id, recipient, status) from a mail.log line.
// ok is false for lines that are not recognised delivery results.
// ok is false for lines that are not recognised delivery results. The
// recipient is the one the application gave: orig_to= when Postfix rewrote
// the address (log_adhoc() writes it only then), to= otherwise - the row
// holds the RCPT TO address (assumptions A-9).
func parseDelivery(line string) (queueID, recipient, status string, ok bool) {
_, rest := SplitTimestamp(line)
m := deliveryRe.FindStringSubmatch(rest)
if m == nil {
return "", "", "", false
}
switch m[3] {
recipient = m[2]
if m[3] != "" {
recipient = m[3]
}
switch m[4] {
case "sent":
status = store.StatusSent
case "deferred":
@@ -136,7 +143,7 @@ func parseDelivery(line string) (queueID, recipient, status string, ok bool) {
default:
return "", "", "", false
}
return m[1], m[2], status, true
return m[1], recipient, status, true
}
// expiryRe matches qmgr's line for a message it gave up on:
+5 -4
View File
@@ -153,13 +153,14 @@ func TestParseDelivery(t *testing.T) {
status: store.StatusSent,
},
{
// An alias/virtual expansion carries orig_to= as well; the address
// the message was actually delivered to is the one in to=.
name: "orig_to is ignored in favour of to",
// A rewritten recipient carries orig_to= as well, the address the
// application gave - the one its row holds (log_adhoc() writes
// orig_to= only when it differs; assumptions A-9).
name: "orig_to is the row's recipient",
line: "host postfix/lmtp[26]: 4Xk9tS1abcz: to=<real@example.net>, orig_to=<alias@example.net>, relay=x, dsn=2.0.0, status=sent (ok)",
wantOK: true,
queueID: "4Xk9tS1abcz",
recipient: "real@example.net",
recipient: "alias@example.net",
status: store.StatusSent,
},
{