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:
@@ -150,3 +150,34 @@ print("" if r is None else r[0]+" "+r[1])`, token)
|
||||
t.Fatalf("no expiry line in mail.log: %v\n%s", err, out)
|
||||
}
|
||||
}
|
||||
|
||||
// testRecipientsAsPostfixLogsThem: each recipient's row reaches "sent". The
|
||||
// row holds the recipient as Postfix logs it - "a b" stays quoted, "ab" loses
|
||||
// its quotes - and a recipient given again in other capitals is one row, as
|
||||
// cleanup keeps one (Postfix 3.7.11, src/global/quote_822_local.c,
|
||||
// src/cleanup/cleanup_out_recipient.c; assumptions B-G5, A-31). Before, the
|
||||
// first was cut at its blank and its row closed as bounced by the sweep.
|
||||
func testRecipientsAsPostfixLogsThem(t *testing.T, sc *scenario) {
|
||||
token := uniqueToken("rcpt")
|
||||
if res := attemptSend(sendAttempt{
|
||||
authLogin: sc.appLogin, authPassword: sc.appPassword,
|
||||
from: "alerts@" + senderDomain, headerFrom: "alerts@" + senderDomain,
|
||||
to: `"x y"@sink.e2e.test`,
|
||||
moreTo: []string{`"ab"@sink.e2e.test`, `"X Y"@sink.e2e.test`},
|
||||
subject: "e2e " + token, body: "three RCPT TO, two recipients",
|
||||
}); !res.ok() {
|
||||
t.Fatalf("send: %v", res.firstErr())
|
||||
}
|
||||
want := `"x y"@sink.e2e.test sent; ab@sink.e2e.test sent`
|
||||
if err := waitFor("both rows to be sent", 30*time.Second, 500*time.Millisecond, func() (bool, error) {
|
||||
out, err := asPanel(`import sqlite3,sys
|
||||
rows=sqlite3.connect("/data/selfpost.db").execute("select to_addr, status from send_log where subject like ? order by id",("%"+sys.argv[1]+"%",)).fetchall()
|
||||
print("; ".join(r[0]+" "+r[1] for r in rows))`, token)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return out == want, fmt.Errorf("rows %q, want %q", out, want)
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,6 +345,9 @@ func TestE2E(t *testing.T) {
|
||||
|
||||
// From here on the installation is no longer the one the run started: it
|
||||
// is restored from its own backup, then switched to accept mail on port 25.
|
||||
run("recipients_as_postfix_logs_them", func(t *testing.T) {
|
||||
testRecipientsAsPostfixLogsThem(t, sc)
|
||||
})
|
||||
run("expired_message_is_closed", func(t *testing.T) {
|
||||
testExpiredMessageIsClosed(t, sc)
|
||||
})
|
||||
|
||||
@@ -23,6 +23,7 @@ const smtpsAddr = "127.0.0.1:20465"
|
||||
type sendAttempt struct {
|
||||
authLogin, authPassword string
|
||||
from, to string
|
||||
moreTo []string // further RCPT TO, after to
|
||||
headerFrom string
|
||||
subject, body string
|
||||
}
|
||||
@@ -133,10 +134,12 @@ func attemptSend(a sendAttempt) sendResult {
|
||||
_ = c.Quit()
|
||||
return res
|
||||
}
|
||||
if err := c.Rcpt(a.to); err != nil {
|
||||
res.rcptErr = err
|
||||
_ = c.Quit()
|
||||
return res
|
||||
for _, to := range append([]string{a.to}, a.moreTo...) {
|
||||
if err := c.Rcpt(to); err != nil {
|
||||
res.rcptErr = err
|
||||
_ = c.Quit()
|
||||
return res
|
||||
}
|
||||
}
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user