Postfix Sender Relay Maps Need a Warning-Safe Preflight
Last edited on August 14, 2026

A malformed sender-relay row produced an uncomfortable result in the lab: postmap exited with status zero, created a database, and still warned that the row did not have a value. A deployment gate that checks only $? would have admitted an incomplete sender_dependent_relayhost_maps table.

The safer standard is stronger. Compile a candidate outside /etc/postfix, require empty standard error and a nonempty database, then query exact-sender, @domain, DUNNO, and absent-key behavior before any reload. This guide reproduces that sequence with Postfix 3.10.13 from Debian packages, without installing or reloading a host mail service.

The result matters to agencies and operators who route separate customer, application, or transactional senders through different upstream relays. A syntactically acceptable file is not yet a proven routing policy. The release artifact must show which relay every important sender class will select and where the global fallback still applies.

Exit Zero Can Still Hide a Broken Relay Row

Postfix lookup tables are simple enough to invite casual automation: write a text file, run postmap, reload, and watch the queue. That sequence moves discovery too late. A skipped line may send traffic through the wrong global relay, while a live-delivery test may expose customer mail to an unintended upstream.

The current Postfix postmap manual documents how indexed tables are built and queried. The broader Postfix database README explains lookup-table forms and operational expectations. Neither turns the process exit status into a promise that every source row was accepted without a diagnostic. Standard error is part of the compilation result.

This negative control creates one key with no value, captures both channels, and proves the observed trap. It belongs in a disposable scope, not beside the active map.

set -Eeuo pipefail
printf '%s\n' "$alerts_sender" >"$lab_root/malformed"
set +e
"$postmap_bin" -c "$lab_root" "hash:$lab_root/malformed" \
  >"$lab_root/malformed.stdout" 2>"$lab_root/malformed.stderr"
malformed_rc=$?
set -e
test "$malformed_rc" -eq 0
grep -F 'expected format: key whitespace value' \
  "$lab_root/malformed.stderr" >/dev/null

The point is not that every warning has identical consequences. It is that a scheduled change cannot silently decide which warnings are harmless. Fail the candidate, preserve the diagnostic, and let an operator correct or explicitly assess the row before activation.

Write the Lookup Contract Before the Map

According to the current sender_dependent_relayhost_maps reference, Postfix searches by envelope sender address and then by @domain. That envelope identity is not necessarily the visible From: header. A sender-dependent result overrides the global relayhost; DUNNO terminates this search without overriding the global value.

Write those branches down before editing data:

  1. An exact sender can select a dedicated relay.
  2. Another sender in the same domain can inherit the domain relay.
  3. DUNNO can deliberately retain the global relay for a domain.
  4. A sender absent from the table also falls through to the global relay.

transport_maps is an earlier routing decision and may supply a different nexthop. The Postfix transport table manual is therefore part of the preflight whenever recipient-dependent transport rules already exist. Do not promise sender routing without inventorying that precedence.

Forwarding is a useful counterexample: SPF, SRS, and ARC follow different SMTP identities across the next hop. This guide tests the envelope sender key used by Postfix, not alignment of a message’s visible author.

Freeze Active State Without Exposing Passwords

First collect the active routing contract. postconf -n shows non-default settings, but the narrower commands below produce a safer receipt. They do not dump SASL password maps or reveal their contents.

set -Eeuo pipefail
postconf -h mail_version
postconf -h relayhost
postconf -h sender_dependent_relayhost_maps
postconf -h smtp_sender_dependent_authentication
postconf -h smtp_sasl_auth_enable
postconf -h transport_maps
postconf -M smtp/unix

Interpret the values together. When different senders also require different credentials, the smtp_sender_dependent_authentication documentation says SMTP SASL must be enabled. It also notes that sender-dependent authentication disables SMTP connection caching so credentials remain correct for the sender. That is a real capacity and connection-rate change, not a cosmetic companion flag.

Do not copy secrets into an article, ticket, or preflight artifact. Record map paths, permissions, ownership, hashes, relay endpoints, and boolean settings. Validate credential-map keys locally through an approved secret-aware procedure, then test authentication in a staged delivery after route selection is proven.

Compile a Candidate Outside /etc/postfix

Use a marker-owned directory and a copied configuration context. The compact lab below builds only the parameters needed by extracted Postfix binaries; on a real server, copy the active main.cf and master.cf into a protected candidate directory and preserve their ownership and modes. Never run a candidate against the production queue directory.

set -Eeuo pipefail
lab_root=$(mktemp -d /tmp/voxfor-postfix-route-176.XXXXXX)
marker="$lab_root/.voxfor-owned"
printf '%s\n' voxfor-postfix-route-176 >"$marker"
mkdir -m 700 "$lab_root/queue" "$lab_root/data"
alpha_domain='alpha.example'
beta_domain='beta.example'
gamma_domain='gamma.example'
alerts_sender="alerts@${alpha_domain}"
billing_sender="billing@${alpha_domain}"
beta_sender="user@${beta_domain}"
gamma_sender="user@${gamma_domain}"
printf '%s\t%s\n' \
  "$alerts_sender" '[relay-alerts.example]:587' \
  "@$alpha_domain" '[relay-alpha.example]:2525' \
  "@$beta_domain" 'DUNNO' >"$lab_root/sender_relay"
chmod 600 "$lab_root/sender_relay"

Using reserved .example names keeps the reproduction non-delivering. The keys and endpoints are intentionally fake, while the lookup behavior is real. FHR’s sender-domain relay example and Tapoueh’s sender-dependent relay notes demonstrate the core feature; the extra admission controls here exist to make a scheduled production change reversible.

Compile the candidate while treating standard error as a failure channel. The generated database must exist and must not be empty.

set -Eeuo pipefail
compile_stderr="$lab_root/compile.stderr"
if ! "$postmap_bin" -c "$lab_root" \
  "hash:$lab_root/sender_relay" 2>"$compile_stderr"; then
  sed -n '1,20p' "$compile_stderr" >&2
  exit 71
fi
if [[ -s "$compile_stderr" ]]; then
  sed -n '1,20p' "$compile_stderr" >&2
  exit 72
fi
test -s "$lab_root/sender_relay.db"

LinuxBabe’s Postfix transport and relay map guide is the strongest ranking page in this benchmark because it covers practical table creation, compilation, reload, and mail-log testing. The stricter addition is sequencing: a candidate must pass compilation diagnostics and deterministic queries before a reload becomes eligible.

Make Four Senders Prove Every Branch

postmap -q returns the raw lookup result for one key. The selector below mirrors the documented sender-address and @domain order, respects DUNNO, and reads the candidate’s global relayhost. It is a preflight oracle, not a replacement for Postfix.

set -Eeuo pipefail
lookup_raw() {
  "$postmap_bin" -c "$lab_root" -q "$1" \
    "hash:$lab_root/sender_relay" || true
}
select_relay() {
  local sender="$1" result domain_key
  result="$(lookup_raw "$sender")"
  if [[ -n "$result" && "$result" != DUNNO ]]; then
    printf '%s\n' "$result"
    return
  fi
  if [[ "$result" != DUNNO ]]; then
    domain_key="@${sender##*@}"
    result="$(lookup_raw "$domain_key")"
    if [[ -n "$result" && "$result" != DUNNO ]]; then
      printf '%s\n' "$result"
      return
    fi
  fi
  "$postconf_bin" -c "$lab_root" -h relayhost
}

Now define expected policy as data and test every semantic branch. Labels keep the receipt readable without publishing sender addresses.

set -Eeuo pipefail
printf '%s\t%s\t%s\n' \
  exact "$alerts_sender" '[relay-alerts.example]:587' \
  domain "$billing_sender" '[relay-alpha.example]:2525' \
  dunno "$beta_sender" '[relay-default.example]:587' \
  absent "$gamma_sender" '[relay-default.example]:587' \
  >"$lab_root/expected.tsv"
selection_lines=()
while IFS=$'\t' read -r route_case sender expected; do
  actual="$(select_relay "$sender")"
  if [[ "$actual" != "$expected" ]]; then
    printf 'selection mismatch case=%s expected=%s actual=%s\n' \
      "$route_case" "$expected" "$actual" >&2
    exit 74
  fi
  selection_lines+=("case=$route_case selected=$actual")
done <"$lab_root/expected.tsv"
printf '%s\n' "${selection_lines[@]}"

Representative output from the isolated Debian package lab follows. The malformed control returned zero, which is precisely why the empty-stderr gate is mandatory.

postfix_version=3.10.13
map_type=hash
case=exact selected=[relay-alerts.example]:587
case=domain selected=[relay-alpha.example]:2525
case=dunno selected=[relay-default.example]:587
case=absent selected=[relay-default.example]:587
malformed_map_exit=0 warning_detected=yes
compile_stderr_empty=yes compiled_db_present=yes
cleanup=complete owned_path_absent=yes

ITMatrix’s sender-or-recipient relay walkthrough and the referenced multi-relay configuration example cover adjacent operational patterns. The four-case receipt above makes the policy decision explicit: an operator can compare expected and selected nexthops before a message is placed in a real queue.

Move the Receipt Into a Controlled Reload

Copying a tested map into place is still a production change. Preserve the active text map, compiled database, relevant main.cf settings, modes, owners, and checksums in a root-only backup. Re-run postmap on the final path, then use postfix check before postfix reload. A reload is preferable to a restart only when the deployed Postfix version and local operating procedure support the intended change.

The final tested input cleans only the marked lab directory. Its guard rejects broad or unowned targets; production backups remain outside this cleanup and follow the site’s retention policy.

set -Eeuo pipefail
test -f "$marker"
grep -Fqx voxfor-postfix-route-176 "$marker"
case "$lab_root" in
  /tmp/voxfor-postfix-route-176.*) ;;
  *) printf 'refusing cleanup: %s\n' "$lab_root" >&2; exit 76 ;;
esac
find "$lab_root" -depth -mindepth 1 -delete
rmdir "$lab_root"
test ! -e "$lab_root"
printf 'cleanup=complete owned_path_absent=yes\n'

After the controlled reload, submit one staged message per policy branch and retain the queue ID. Confirm the selected relay in Postfix logs, validate SMTP authentication without exposing credentials, and test TLS identity for every upstream. Run STARTTLS certificate trust and hostname checks against each selected relay. Direct port-25 delivery under DANE is a separate trust model; live TLSA-to-certificate verification belongs on that path.

If a staged message defers, preserve one queue ID and follow Postfix queue evidence before retrying rather than flushing all mail. Route selection also ends before later body mutation; use the post-signing body-change test when DKIM fails only on one delivery path.

Approve the routing change when the final map compiles with empty standard error, its indexed database is nonempty, exact-sender and domain keys select their intended nexthops, DUNNO and absent keys retain the documented global relay, active transport precedence has been reviewed, and staged deliveries confirm the same relays with successful authentication and TLS checks.

If any compile diagnostic, lookup mismatch, unexpected transport override, authentication failure, TLS failure, or deferred staged delivery appears, stop new test submissions, preserve the candidate receipt and queue IDs, restore the backed-up map files and configuration with their original ownership and modes, rebuild the restored map, run postfix check, reload once, and verify that the known-good sender again selects its previous relay before releasing queued mail.

Teams that do not own Postfix change control can hand this work to mail server customization and automation support, whose current service scope explicitly includes mail-server, DNS, and PTR configuration. The technical acceptance criteria above still apply; support changes who performs the work, not what counts as proof.

Seven Sender-Relay Preflight Questions

Does the sender map use the visible From header?

No. sender_dependent_relayhost_maps uses the SMTP envelope sender address. The visible From: header can differ, especially with forwarding, mailing lists, bounce handling, and application-generated mail.

Which wins, an exact sender or an @domain entry?

Postfix searches the full envelope sender address before the @domain key. Use an exact row for a sender that needs a distinct relay and a domain row for the remaining senders in that domain.

What does DUNNO do in this map?

DUNNO terminates the sender-dependent lookup without overriding the global relayhost. It is useful when a broader pattern exists but one domain must deliberately retain the default route.

Does postmap exit zero mean every row was accepted?

No. The reproduced malformed-row control exited zero and printed an “expected format” warning. A safe gate evaluates the exit status, standard error, and resulting database before it queries policy outcomes.

When should sender-dependent SMTP authentication be enabled?

Enable it only when different envelope senders need different upstream credentials, and only with SMTP SASL already enabled. Account for the documented loss of SMTP connection caching and test the resulting connection rate.

Can transport_maps override the sender relay?

Yes. Recipient-dependent transport lookup has earlier routing precedence in relevant delivery paths. Inventory and test existing transport rules before claiming that a sender map owns the nexthop.

Is the preflight enough without a live delivery?

No. It proves candidate compilation and relay selection without risking mail. A staged delivery is still required to verify the active configuration, transport precedence, credentials, TLS identity, provider policy, and actual queue outcome.

Stop at Selection Proof, Then Test Delivery

This preflight deliberately stops before it can send mail. That boundary makes it safe to run repeatedly and useful in CI: the candidate either compiles cleanly and produces the four expected selections, or it never becomes eligible for a reload.

Production release evidence begins where the lab ends. Preserve the candidate hash and receipt, deploy through a backed-up change window, and attach staged queue IDs plus relay-log lines to the same record. Selection proof prevents an avoidable routing mistake; live delivery proof confirms the upstream will actually accept and protect the message.

Leave a Reply

Your email address will not be published. Required fields are marked *