milter, postfix, app, domain: cite the source of what they rely on (assumptions part B)

Comments only, no behaviour changed. go-milter v0.4.1 (session.go: one
session per connection, replaced after a non-continue reply; macros per
stage), Postfix 3.7.11 (milter8.c, smtpd_check.c
reject_auth_sender_login_mismatch(), mail_addr_find.c, showq_compat.c,
mail_conf_time.c, mail_params.h), Cyrus SASL 2.1.28 (saslpasswd.c,
plugins/sasldb.c: deleting a missing login is not an error), OpenDKIM
2.11.0~beta2 (opendkim-db.c, opendkim-genkey), Berkeley DB 5.3 db_dump,
rspamd 3.11.0. Comments that claimed what the source denies are corrected
(sasldb keeps the password as given, not hashed; an embedded newline is
kept, not truncated; rspamd sorts symbols by absolute weight); fixtures say
seen or composed. The sender check's From: parsing gets no citations: it is
replaced by the port of OpenDKIM's dkim_mail_parse next.

Contradictions left for their own commits: B-G5 cleanAddress, B-PS1 first
key wins, B-PQ1 the forced-expire flag, B-PQ10/PQ11 Postfix's time units.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-14 19:36:12 +03:00
co-authored by Claude Opus 5.5
parent ce363f48dd
commit b113e72c71
23 changed files with 307 additions and 26 deletions
+4
View File
@@ -41,6 +41,10 @@ func (p *Postfix) RebuildDMARCMaps(cfg DMARCMapsConfig) error {
return p.reload()
}
// Postfix 3.7.11, man/man5/access.5: "OK" accepts the address the key
// matches; man/man5/transport.5: "user@domain transport:nexthop", and "a
// non-null transport field with a null nexthop field resets the nexthop
// information to the recipient domain"; relay_domains as in renderInboundMaps.
func renderDMARCMaps(cfg DMARCMapsConfig) (recipients, transport, relayDomains []byte, err error) {
recipients = []byte{}
transport = []byte{}
+12
View File
@@ -55,6 +55,13 @@ func (p *Postfix) RebuildInboundMaps(routes []InboundRoute) error {
return p.reload()
}
// Postfix 3.7.11, man/man5/postconf.5: a relay_domains table "is matched when
// a (parent) domain appears as lookup key", the value unused; relay_recipient_maps
// is used as a list, "@domain" its wild-card (src/global/mail_addr_find.c,
// MA_FIND_AT_DOMAIN, the exact domain only). man/man5/transport.5: "domain
// transport:nexthop" for that domain. README_FILES/TLS_README: the
// smtp_tls_policy_maps key is the verbatim next-hop; src/tls/tls_level.c names
// the levels none, may and encrypt.
func renderInboundMaps(routes []InboundRoute) (relay, transport, recipients, tlsPolicy []byte, err error) {
sort.Slice(routes, func(i, j int) bool { return routes[i].Domain < routes[j].Domain })
@@ -87,6 +94,11 @@ func renderInboundMaps(routes []InboundRoute) (relay, transport, recipients, tls
// inboundNexthop is the Postfix next-hop [host]:port form. The brackets are
// what disable the MX lookup, so the operator's explicit upstream is used as
// given; they also delimit a bare IPv6 address.
//
// Postfix 3.7.11, src/smtp/smtp_connect.c: no MX lookup for a destination that
// starts with "["; src/util/host_port.c, host_port(): "[host]:port", the host
// a name or an address ("ipv6:" in front is optional), checked by
// valid_hostaddr().
func inboundNexthop(host string, port int) string {
return "[" + host + "]:" + strconv.Itoa(port)
}
+18
View File
@@ -59,6 +59,9 @@ type Binding struct {
// architecture.md § Mail path) — their logins are merged onto a single line as
// a comma-separated list, which is how Postfix expects multiple owners of one
// sender.
//
// Postfix 3.7.11, src/smtpd/smtpd_check.c, reject_auth_sender_login_mismatch():
// the value is split into owners with mystrtok(&cp, CHARS_COMMA_SP).
func (p *Postfix) RebuildSenderLoginMaps(bindings []Binding) error {
content, err := renderSenderLoginMaps(bindings)
if err != nil {
@@ -128,6 +131,12 @@ func appendUnique(list []string, v string) []string {
// end it, the comma that separates values, the colon a lookup key cannot carry,
// and the backslash. A login additionally may not carry "@" — that would make it
// look like an address.
//
// Postfix 3.7.11, src/util/dict_thash.c, dict_thash_open(): the key ends at
// the first blank outside quotes, a backslash escapes the next character;
// src/util/readlline.c: a newline ends the line (one that starts with a blank
// continues the one before); the owners split on comma and blank (see
// RebuildSenderLoginMaps).
const (
senderMapAddressForbidden = " \t\r\n,:\\"
senderMapLoginForbidden = " \t\r\n,:@\\"
@@ -161,6 +170,15 @@ func assertMapSafe(address, login string) error {
//
// Arguments are fixed literals — no user input is interpolated into the command,
// and it never goes through a shell (security.md).
//
// Postfix 3.7.11, conf/postfix-script, start-fg: a process that is not PID 1
// runs "$daemon_directory/master -s" as its child, and src/master/master.c
// puts that master in a session of its own (setsid()). "reload" sends the
// master SIGHUP; src/master/master.c calls master_refresh(), and
// master_conf.c, master_config(), lets the running children "commit suicide
// whenever it is convenient" while new ones get the new configuration; a
// texthash table is read into memory once per process
// (README_FILES/DATABASE_README).
func reloadViaSupervisor() error {
return supervisor.Start("postfix-reload")
}
+8
View File
@@ -50,6 +50,11 @@ var postqueueP = func(ctx context.Context) *exec.Cmd {
// listQueue runs `postqueue -p` and keeps at most maxQueueOutput bytes of it;
// truncated says whether there was more.
//
// Postfix 3.7.11, src/postqueue/postqueue.c, show_queue(): any user passes
// authorized_mailq_users (DEF_SHOWQ_ACL static:anyone), and a failure is
// msg_fatal_status() - a non-zero exit with the reason on stderr;
// src/postqueue/showq_compat.c, showq_compat(): "Mail queue is empty".
func listQueue() (text string, truncated bool, err error) {
ctx, cancel := context.WithTimeout(context.Background(), postqueueTimeout)
defer cancel()
@@ -101,6 +106,9 @@ func Queue() (string, error) {
// queue. A listing cut at the size cap is such an error: the messages past the
// cut are still in the queue, and a backlog that large is the outage the sweep
// exists for (audit 2026-10-12, H3). QueueMessages treats its cap the same way.
//
// Postfix 3.7.11, src/showq/showq.c, showq_service(): maildrop, active,
// incoming, deferred and hold are the queues it lists.
func QueueIDs() (map[string]struct{}, error) {
out, truncated, err := listQueue()
if err != nil {
+8 -5
View File
@@ -12,7 +12,8 @@ import (
// TestFakePostqueue is not a test: it is the fake `postqueue -p` the tests
// below run in place of the real one, writing SP_FAKE_POSTQUEUE_BYTES bytes of
// queue entries.
// queue entries composed by hand after the layout of Postfix 3.7.11's
// src/postqueue/showq_compat.c.
func TestFakePostqueue(t *testing.T) {
if os.Getenv("SP_FAKE_POSTQUEUE") != "1" {
t.Skip("helper process")
@@ -73,10 +74,12 @@ func TestQueueIDsReadsAListingUnderTheCap(t *testing.T) {
}
// The reconcile sweep decides whether a message is still Postfix's problem, so
// the parser must pick queue ids out of a real listing and nothing else out of
// it: not the header, not the byte-count trailer, and above all not a deferred
// entry's reason line, which — unlike the recipient lines — starts at the left
// margin just as an entry does.
// the parser must pick queue ids out of a listing and nothing else out of it:
// not the header, not the byte-count trailer, and above all not a deferred
// entry's reason line. The listing is composed by hand. Postfix 3.7.11
// (src/postqueue/showq_compat.c, showq_message()) right-aligns a reason to
// column 76, so it reaches the left margin only when it is 76 characters or
// longer; the one here is put at the margin as the hard case.
func TestParseQueueIDs(t *testing.T) {
listing := `-Queue ID- --Size-- ----Arrival Time---- -Sender/Recipient-------
3C5B04E6C1* 446 Fri Aug 8 10:12:31 app@example.com
+6
View File
@@ -12,6 +12,12 @@ import (
// QueuedMessage is one message Postfix is holding, as `postqueue -j` reports
// it: where it is, and since when.
//
// Postfix 3.7.11, src/postqueue/showq_json.c, format_json(): one object and a
// newline per message - "queue_name", "queue_id", "arrival_time" (seconds,
// %ld), "message_size", "forced_expire", "sender", "recipients" [{"address",
// "delay_reason"}]; the queue names are those of src/global/mail_queue.h that
// src/showq/showq.c, showq_service(), scans.
type QueuedMessage struct {
ID string
Queue string // "deferred", "active", "hold", "incoming", "maildrop"
+3
View File
@@ -6,6 +6,9 @@ import (
"time"
)
// The listing is composed by hand: its first line in the field order of
// Postfix 3.7.11's src/postqueue/showq_json.c, format_json(); the others
// short of fields or broken on purpose.
func TestParseQueueJSON(t *testing.T) {
out := []byte(`{"queue_name": "deferred", "queue_id": "41E862C00D9E", "arrival_time": 1791000000, "message_size": 446, "forced_expire": false, "sender": "app@example.com", "recipients": [{"address": "a@example.net", "delay_reason": "connect to mx.example.net[203.0.113.9]:25: Connection timed out"}]}
{"queue_name": "active", "queue_id": "5F00AA12", "arrival_time": 1791250000, "message_size": 900, "sender": "", "recipients": [{"address": "b@example.net"}]}
+11
View File
@@ -28,6 +28,10 @@ type RetryPolicy struct {
// DefaultRetryPolicy is Postfix 3.x compiled-in values for the six parameters
// build/postfix-config.sh does not set. Used when postconf is missing (unit
// tests, a binary outside the container) so the panel still starts.
//
// Postfix 3.7.11, src/global/mail_params.h: DEF_QUEUE_RUN_DELAY "300s",
// DEF_MIN_BACKOFF_TIME DEF_QUEUE_RUN_DELAY, DEF_MAX_BACKOFF_TIME "4000s",
// DEF_MAX_QUEUE_TIME "5d", DEF_DSN_QUEUE_TIME "5d", DEF_DELAY_WARN_TIME "0h".
func DefaultRetryPolicy() RetryPolicy {
return RetryPolicy{
QueueRunDelay: 300 * time.Second,
@@ -57,6 +61,9 @@ var readRetryConf = postconfRetryValues
func postconfRetryValues() ([]string, error) {
// Fixed argv, no user input — same pattern as Queue (security.md).
// Postfix 3.7.11, src/postconf/postconf_main.c, pcf_show_parameters():
// named parameters are printed in argument order, with -h one "%s\n"
// value each, as written in main.cf (not expanded without -x).
cmd := exec.Command("postconf", "-h",
"queue_run_delay",
"minimal_backoff_time",
@@ -133,6 +140,10 @@ func (p RetryPolicy) BackoffCap() string {
// larger of the queue lifetime for ordinary mail and for mail from the null
// sender, which an application may submit too. It bounds which stored log
// lines can be about a send-log row (store.SendLogLines).
//
// Postfix 3.7.11, src/qmgr/qmgr_active.c, qmgr_active_done(): a message
// expires after var_max_queue_time, or var_dsn_queue_time when its sender is
// null - whoever submitted it (qmgr.c caps the second at the first).
func (p RetryPolicy) LongestLifetime() time.Duration {
if p.BounceQueueLifetime > p.MaximalQueueLifetime {
return p.BounceQueueLifetime
+3
View File
@@ -6,6 +6,9 @@ import (
"time"
)
// The postconf values in these tests are composed by hand: values main.cf
// could hold. Stock Postfix 3.7.11 prints "0h" for delay_warning_time
// (src/global/mail_params.h, DEF_DELAY_WARN_TIME), not "0".
func TestLoadRetryPolicyUsesPostconfValues(t *testing.T) {
old := readRetryConf
readRetryConf = func() ([]string, error) {