From 36db4223f2ec7d5d74ff517b0688580de0a76ce8 Mon Sep 17 00:00:00 2001 From: Mix Date: Sun, 11 Oct 2026 00:05:40 +0300 Subject: [PATCH] postfix-config: report addresses are relay recipients when the relay is on Found by talking SMTP to port 25 of the test server once it answered again (2026-10-10): RCPT TO: was refused with "550 5.1.1 User unknown in relay recipient table". With INBOUND_RELAY_ENABLE and DMARC_REPORTS_ENABLE both true the server's own name is a relay domain (for the reports) and relay_recipient_maps lists the relay's recipients only. smtpd_reject_unlisted_recipient applies after the restriction list, so the OK that check_recipient_access gives a report address does not help: every report was refused. The table of report addresses is added to relay_recipient_maps in that combination. Checked on the server by setting the parameter in the running container and reloading Postfix: the domain's report address is answered 250, the former server-wide mailbox and an unknown local part 550, a foreign domain and an inbound domain without recipients 554. e2e: the combined case requires relay_recipient_maps to name both tables. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 ++++++ build/postfix-config.sh | 13 ++++++++++++- test/e2e/port25_config_check.go | 19 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0675280..4020088 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -227,6 +227,12 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version value with whitespace, and the e2e check asks Postfix what it reads. Present since DMARC ingest was fitted into the 1.9.1 fix of the same stanza. +- **DMARC reports are accepted with the inbound relay on.** With both + features enabled every report address was refused as "User unknown in + relay recipient table": the relay's unlisted-recipient check runs after the + restriction list, and the report addresses - on the server's own name, a + relay domain - were not in `relay_recipient_maps`. Their table is a + recipient table now. Hidden until now by the port-25 failure above. - Overview's Processes card counts the programs that are meant to stay up ("6 of 6 running"); the idle one-shot `postfix-reload` was counted as running. The Milter sockets card no longer clips its second line with three diff --git a/build/postfix-config.sh b/build/postfix-config.sh index 48783ae..dd9026d 100644 --- a/build/postfix-config.sh +++ b/build/postfix-config.sh @@ -306,8 +306,19 @@ if [ "${INBOUND_RELAY_ENABLE}" = "true" ] || [ "${DMARC_REPORTS_ENABLE}" = "true "transport_maps=${TRANSPORT_SETTING}" \ "smtpd_reject_unlisted_recipient=yes" if [ "${INBOUND_RELAY_ENABLE}" = "true" ]; then + # With the relay on, Postfix refuses every recipient of a relay domain + # that relay_recipient_maps does not list (smtpd_reject_unlisted_recipient + # above) - and it does so after the restriction list, whatever + # check_recipient_access answered. The DMARC report addresses live on a + # relay domain too (the server's own name), so their table is a recipient + # table as well; without it they are "User unknown in relay recipient + # table" and no report is ever accepted while both features are on. + RELAY_RECIPIENT_SETTING="texthash:${RELAY_RECIPIENTS_MAP}" + if [ "${DMARC_REPORTS_ENABLE}" = "true" ]; then + RELAY_RECIPIENT_SETTING="${RELAY_RECIPIENT_SETTING} texthash:${DMARC_RECIPIENTS_MAP}" + fi postconf -e \ - "relay_recipient_maps=texthash:${RELAY_RECIPIENTS_MAP}" \ + "relay_recipient_maps=${RELAY_RECIPIENT_SETTING}" \ "smtp_tls_policy_maps=texthash:${TLS_POLICY_MAP}" else postconf -e \ diff --git a/test/e2e/port25_config_check.go b/test/e2e/port25_config_check.go index faf0d03..12f2954 100644 --- a/test/e2e/port25_config_check.go +++ b/test/e2e/port25_config_check.go @@ -22,6 +22,9 @@ type port25Case struct { name string env []string check []optionCheck + // main are main.cf parameters the combination must end up with, read with + // postconf -h. + main []optionCheck } var port25Cases = []port25Case{ @@ -61,6 +64,13 @@ var port25Cases = []port25Case{ {param: "smtpd_recipient_restrictions", contains: "check_recipient_access,texthash:/data/postfix/dmarc_recipients,reject_unauth_destination,reject_unlisted_recipient", wantExact: true}, {param: "smtpd_milters", contains: "", wantExact: true}, }, + // Both on: the report addresses sit on a relay domain, and the relay's + // unlisted-recipient check runs after the restriction list. Unless their + // table is a recipient table too, every report is refused as "User + // unknown in relay recipient table". + main: []optionCheck{ + {param: "relay_recipient_maps", contains: "texthash:/data/postfix/relay_recipients texthash:/data/postfix/dmarc_recipients", wantExact: true}, + }, }, { name: "inbound relay with antispam milter", @@ -129,6 +139,15 @@ func checkPort25ConfigCombinations(s *stack) error { return fmt.Errorf("%s: master.cf has -o %s = %q but Postfix reads %q - the value contains whitespace", tc.name, c.param, got, strings.TrimSpace(seen)) } } + for _, c := range tc.main { + got, err := s.execIn("selfpost", "postconf", "-h", c.param) + if err != nil { + return fmt.Errorf("%s: postconf -h %s: %v\n%s", tc.name, c.param, err, got) + } + if got = strings.TrimSpace(got); got != c.contains { + return fmt.Errorf("%s: %s = %q, want %q", tc.name, c.param, got, c.contains) + } + } } // Back to the stand's own configuration: flags off, no port-25 listener.