postfix config: port 25's size limit for cleanup too, no local delivery
Port 25 says it takes INBOUND_MESSAGE_SIZE_LIMIT (25 MiB by default) with smtpd -o message_size_limit, but cleanup writes the queue file under its own limit, main.cf's - never set, so DEF_MESSAGE_LIMIT, 10240000 (Postfix 3.7.11, src/cleanup/cleanup_init.c, cleanup_post_jail(), set_file_limit(); src/global/mail_params.h). A larger message went through DATA and was refused 552 (assumptions C-P31). cleanup now gets port 25's limit as a master.cf -o when it is larger than main.cf's; 465 and 587 keep main.cf's in smtpd's own DATA/BDAT checks. /etc/postfix outlives a restart, so the override is removed with postconf -PX first on every start. An empty mydestination left one local destination: an address literal of an interface the container listens on (src/global/resolve_local.c, resolve_local(), own_inet_addr()). local_transport is now "error:local mail delivery is disabled", as STANDARD_CONFIGURATION_README's null client has it (assumptions C-P8). Stand (2026-10-14, make e2e, exit 0): inbound_relay_delivers_and_refuses sends a 12 MB message through port 25 and finds it at the upstream, and counts it among the Inbound log's rows; new address_literal_is_not_local - probe@[127.0.0.1] from an application is refused at RCPT "550 5.1.1 ... local mail delivery is disabled" (smtpd's check_rcpt_maps() rejects what resolves to the error transport; before, local(8) took it). go vet and go test ./... pass on the server (23 packages). Tested together with the next commit (the log tailer); the two touch no common file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -68,6 +68,18 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
|||||||
as `bounced`. The row now holds the recipient as Postfix logs it, once,
|
as `bounced`. The row now holds the recipient as Postfix logs it, once,
|
||||||
and is found under `orig_to=` when Postfix rewrote it (assumptions B-G5,
|
and is found under `orig_to=` when Postfix rewrote it (assumptions B-G5,
|
||||||
A-9, A-31, D-S20).
|
A-9, A-31, D-S20).
|
||||||
|
- **Inbound mail between 10 MB and the port's limit is accepted.** Port 25
|
||||||
|
said it took messages up to `INBOUND_MESSAGE_SIZE_LIMIT` (25 MiB by
|
||||||
|
default), but Postfix writes the message to its queue under a limit of
|
||||||
|
its own, main.cf's, never set and so 10240000 bytes: a larger message was
|
||||||
|
taken through `DATA` and then refused with 552. That limit now follows
|
||||||
|
port 25's when it is larger; 465 and 587 keep theirs (assumptions
|
||||||
|
C-P31).
|
||||||
|
- **Mail to an address literal of the container is refused, not delivered
|
||||||
|
locally.** An empty `mydestination` left one way to local delivery:
|
||||||
|
`user@[127.0.0.1]` and the container's other addresses. Local delivery is
|
||||||
|
now Postfix's refusal, and Postfix refuses such a recipient at `RCPT TO`:
|
||||||
|
`550 5.1.1 ... local mail delivery is disabled` (assumptions C-P8).
|
||||||
- **A bounce is judged by the filter lists as the panel counts it.**
|
- **A bounce is judged by the filter lists as the panel counts it.**
|
||||||
rspamd's sender rules took a bounce's `From:` header when the envelope
|
rspamd's sender rules took a bounce's `From:` header when the envelope
|
||||||
sender was empty, so a bounce could be refused or allowed by an entry the
|
sender was empty, so a bounce could be refused or allowed by an entry the
|
||||||
|
|||||||
+25
-2
@@ -170,6 +170,10 @@ DMARC_RELAY_DOMAINS_MAP="${POSTFIX_DMARC_RELAY_DOMAINS:-/data/postfix/dmarc_rela
|
|||||||
# directory and the file with the ownership postlogd writes and the panel reads.
|
# directory and the file with the ownership postlogd writes and the panel reads.
|
||||||
MAIL_LOG_PATH="${MAIL_LOG:-/data/log/mail.log}"
|
MAIL_LOG_PATH="${MAIL_LOG:-/data/log/mail.log}"
|
||||||
|
|
||||||
|
# /etc/postfix outlives a restart of the container, so a cleanup limit set for
|
||||||
|
# port 25 before is taken back here and set again below when it still applies.
|
||||||
|
postconf -PX "cleanup/unix/message_size_limit" 2>/dev/null || true
|
||||||
|
|
||||||
# --- main.cf -----------------------------------------------------------------
|
# --- main.cf -----------------------------------------------------------------
|
||||||
# maillog_file lives under the persistent /data bind mount (architecture.md §
|
# maillog_file lives under the persistent /data bind mount (architecture.md §
|
||||||
# Log tailer). Postfix's default maillog_file_prefixes are only /var and
|
# Log tailer). Postfix's default maillog_file_prefixes are only /var and
|
||||||
@@ -199,11 +203,18 @@ postconf -e \
|
|||||||
postconf -M "postlog/unix-dgram=postlog unix-dgram n - n - 1 postlogd"
|
postconf -M "postlog/unix-dgram=postlog unix-dgram n - n - 1 postlogd"
|
||||||
|
|
||||||
# This is an outbound relay: no local delivery, no per-user aliases. Empty
|
# This is an outbound relay: no local delivery, no per-user aliases. Empty
|
||||||
# these so a misfiled recipient never gets delivered locally.
|
# these so a misfiled recipient never gets delivered locally. An empty
|
||||||
|
# mydestination is not enough: an address literal of an interface the
|
||||||
|
# container listens on - x@[127.0.0.1] - is local too (Postfix 3.7.11,
|
||||||
|
# src/global/resolve_local.c, resolve_local(), own_inet_addr()), so local(8)
|
||||||
|
# itself is replaced by a refusal, as Postfix's null-client example does
|
||||||
|
# (README_FILES/STANDARD_CONFIGURATION_README, "local_transport = error:local
|
||||||
|
# mail delivery is disabled"; assumptions C-P8).
|
||||||
postconf -e \
|
postconf -e \
|
||||||
"local_recipient_maps=" \
|
"local_recipient_maps=" \
|
||||||
"alias_maps=" \
|
"alias_maps=" \
|
||||||
"alias_database="
|
"alias_database=" \
|
||||||
|
"local_transport=error:local mail delivery is disabled"
|
||||||
|
|
||||||
# Outbound delivery: straight to the recipient MX, opportunistic TLS.
|
# Outbound delivery: straight to the recipient MX, opportunistic TLS.
|
||||||
postconf -e \
|
postconf -e \
|
||||||
@@ -563,6 +574,18 @@ if [ "${INBOUND_RELAY_ENABLE}" = "true" ] || [ "${DMARC_REPORTS_ENABLE}" = "true
|
|||||||
"smtp/inet/message_size_limit=${PORT25_SIZE}"
|
"smtp/inet/message_size_limit=${PORT25_SIZE}"
|
||||||
master_cf_set_smtp_inet_o smtpd_recipient_restrictions "${RECIPIENT_RESTRICTIONS}"
|
master_cf_set_smtp_inet_o smtpd_recipient_restrictions "${RECIPIENT_RESTRICTIONS}"
|
||||||
master_cf_set_smtp_inet_o smtpd_milters "${INBOUND_MILTERS}"
|
master_cf_set_smtp_inet_o smtpd_milters "${INBOUND_MILTERS}"
|
||||||
|
|
||||||
|
# smtpd's -o limit is not the whole of it: cleanup writes the queue file
|
||||||
|
# under its own message_size_limit, main.cf's, which is never set here and
|
||||||
|
# so is 10240000 (Postfix 3.7.11, src/cleanup/cleanup_init.c,
|
||||||
|
# cleanup_post_jail(), set_file_limit(); src/global/mail_params.h,
|
||||||
|
# DEF_MESSAGE_LIMIT), and a larger message is refused 552 after DATA
|
||||||
|
# (assumptions C-P31). cleanup gets port 25's limit when it is larger;
|
||||||
|
# smtpd on 465 and 587 still holds its own, main.cf's (src/smtpd/smtpd.c,
|
||||||
|
# the DATA and BDAT size checks).
|
||||||
|
if [ "${PORT25_SIZE}" -gt "$(postconf -h message_size_limit)" ]; then
|
||||||
|
postconf -P "cleanup/unix/message_size_limit=${PORT25_SIZE}"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
postconf -MX "smtp/inet" 2>/dev/null || true
|
postconf -MX "smtp/inet" 2>/dev/null || true
|
||||||
postconf -MX "dmarc-ingest/unix" 2>/dev/null || true
|
postconf -MX "dmarc-ingest/unix" 2>/dev/null || true
|
||||||
|
|||||||
@@ -181,3 +181,23 @@ print("; ".join(r[0]+" "+r[1] for r in rows))`, token)
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testAddressLiteralIsNotLocal: an address literal of an interface the
|
||||||
|
// container listens on is a local destination to Postfix even with an empty
|
||||||
|
// mydestination (Postfix 3.7.11, src/global/resolve_local.c,
|
||||||
|
// resolve_local()). Local delivery is Postfix's refusal now
|
||||||
|
// (local_transport=error:..., assumptions C-P8), and smtpd refuses such a
|
||||||
|
// recipient at RCPT with its text: smtpd_reject_unlisted_recipient (on by
|
||||||
|
// default) runs check_rcpt_maps(), which rejects whatever resolves to the
|
||||||
|
// error transport (src/smtpd/smtpd_check.c). Before, the message was
|
||||||
|
// accepted and handed to local(8).
|
||||||
|
func testAddressLiteralIsNotLocal(t *testing.T, sc *scenario) {
|
||||||
|
res := attemptSend(sendAttempt{
|
||||||
|
authLogin: sc.appLogin, authPassword: sc.appPassword,
|
||||||
|
from: "alerts@" + senderDomain, headerFrom: "alerts@" + senderDomain,
|
||||||
|
to: "probe@[127.0.0.1]", subject: "e2e " + uniqueToken("literal"), body: "to an address literal of the container",
|
||||||
|
})
|
||||||
|
if err := res.refusedAt("RCPT", 550, "local mail delivery is disabled"); err != nil {
|
||||||
|
t.Fatalf("to probe@[127.0.0.1]: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -348,6 +348,9 @@ func TestE2E(t *testing.T) {
|
|||||||
run("recipients_as_postfix_logs_them", func(t *testing.T) {
|
run("recipients_as_postfix_logs_them", func(t *testing.T) {
|
||||||
testRecipientsAsPostfixLogsThem(t, sc)
|
testRecipientsAsPostfixLogsThem(t, sc)
|
||||||
})
|
})
|
||||||
|
run("address_literal_is_not_local", func(t *testing.T) {
|
||||||
|
testAddressLiteralIsNotLocal(t, sc)
|
||||||
|
})
|
||||||
run("expired_message_is_closed", func(t *testing.T) {
|
run("expired_message_is_closed", func(t *testing.T) {
|
||||||
testExpiredMessageIsClosed(t, sc)
|
testExpiredMessageIsClosed(t, sc)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -268,6 +268,21 @@ func testInboundRelay(t *testing.T, sc *scenario) {
|
|||||||
t.Fatalf("mail for the inbound domain never reached its upstream: %v\n%s", err, mailLogTail(h, "postfix/"))
|
t.Fatalf("mail for the inbound domain never reached its upstream: %v\n%s", err, mailLogTail(h, "postfix/"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Port 25's limit is the whole limit: a 12 MB message - over the
|
||||||
|
// 10240000 bytes cleanup would hold it to on its own, under the 25 MiB the
|
||||||
|
// listener says - is accepted and passed on (Postfix 3.7.11,
|
||||||
|
// src/cleanup/cleanup_init.c, cleanup_post_jail(); assumptions C-P31).
|
||||||
|
big := uniqueToken("inbound-12mb")
|
||||||
|
line := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ab\r\n"
|
||||||
|
bigBody := strings.Repeat(line, 12*1000*1000/len(line))
|
||||||
|
if res := deliverPort25("someone@stranger.e2e.test", inboundRecipient,
|
||||||
|
strings.Replace(message(inboundRecipient, big), "mail for the inbound domain\r\n", bigBody, 1)); !res.ok() {
|
||||||
|
t.Fatalf("port 25 refused a 12 MB message: %v\n%s", res.firstErr(), mailLogTail(h, "postfix/"))
|
||||||
|
}
|
||||||
|
if _, err := findSinkMessage(h.stageDir, big, 60*time.Second); err != nil {
|
||||||
|
t.Fatalf("the 12 MB message never reached the upstream: %v\n%s", err, mailLogTail(h, "postfix/"))
|
||||||
|
}
|
||||||
|
|
||||||
// The filter lists exist as files from the first start with the relay on,
|
// The filter lists exist as files from the first start with the relay on,
|
||||||
// readable by a filter that is another user in another container. Nothing
|
// readable by a filter that is another user in another container. Nothing
|
||||||
// is on them until someone adds an entry.
|
// is on them until someone adds an entry.
|
||||||
@@ -335,7 +350,8 @@ func testInboundRelay(t *testing.T, sc *scenario) {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
rows := logRows("e2e " + token)
|
rows := logRows("e2e " + big)
|
||||||
|
rowWith(rows, "user@<wbr>"+inboundDomain, "e2e "+big, `class="tag is-success is-light" popovertarget="why-`)
|
||||||
rowWith(rows,
|
rowWith(rows,
|
||||||
"someone@<wbr>stranger.e2e.test", "user@<wbr>"+inboundDomain, "e2e "+token,
|
"someone@<wbr>stranger.e2e.test", "user@<wbr>"+inboundDomain, "e2e "+token,
|
||||||
`class="tag is-success is-light" popovertarget="why-`, `<th>Decided by</th><td>selfpost</td>`,
|
`class="tag is-success is-light" popovertarget="why-`, `<th>Decided by</th><td>selfpost</td>`,
|
||||||
@@ -343,8 +359,8 @@ func testInboundRelay(t *testing.T, sc *scenario) {
|
|||||||
`href="/inbound/filter-lists?list=deny&kind=address&value=someone%40stranger.e2e.test">Deny sender</a>`)
|
`href="/inbound/filter-lists?list=deny&kind=address&value=someone%40stranger.e2e.test">Deny sender</a>`)
|
||||||
// A report mailed to the server's own name earlier in the run came through
|
// A report mailed to the server's own name earlier in the run came through
|
||||||
// the same listener and is not the relay's mail.
|
// the same listener and is not the relay's mail.
|
||||||
if len(rows) != 1 {
|
if len(rows) != 2 {
|
||||||
t.Fatalf("the Inbound log holds %d rows, want the relayed message alone:\n%s", len(rows), strings.Join(rows, "\n"))
|
t.Fatalf("the Inbound log holds %d rows, want the two relayed messages alone:\n%s", len(rows), strings.Join(rows, "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Postfix 3.7.11, src/smtpd/smtpd_check.c: "User unknown in relay recipient
|
// Postfix 3.7.11, src/smtpd/smtpd_check.c: "User unknown in relay recipient
|
||||||
@@ -377,8 +393,8 @@ func testInboundRelay(t *testing.T, sc *scenario) {
|
|||||||
if strings.Contains(refused, "sender</a>") {
|
if strings.Contains(refused, "sender</a>") {
|
||||||
t.Fatalf("the refused row offers a way into the filter lists:\n%s", refused)
|
t.Fatalf("the refused row offers a way into the filter lists:\n%s", refused)
|
||||||
}
|
}
|
||||||
if len(rows) != 2 {
|
if len(rows) != 3 {
|
||||||
t.Fatalf("the Inbound log holds %d rows, want the relayed message and one refusal:\n%s", len(rows), strings.Join(rows, "\n"))
|
t.Fatalf("the Inbound log holds %d rows, want the two relayed messages and one refusal:\n%s", len(rows), strings.Join(rows, "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// The filter lists: an entry added in the panel is in the map file the
|
// The filter lists: an entry added in the panel is in the map file the
|
||||||
|
|||||||
Reference in New Issue
Block a user