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:
@@ -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{}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"}]}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user