mail-address-semantics/integrations/exim/NOTES.md
tegwick d766d61e7f MAS-WP-0004: Postfix integration profile, corpus probe in pytest, Exim notes
Profile in integrations/postfix (install.sh + README), now used by the
live deployment. Probe runs every conformance-corpus local-part and the
relay negatives against it; tests/test_postfix_profile.py runs it where
Docker is available.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5787@bnt-lap001
Assistant-Session: 5db26b82-138b-40cc-9856-4d5eb0f752fd
2026-10-19 09:25:03 +02:00

2 KiB

Exim notes (MAS-WP-0004 T03)

No Exim profile is shipped, because there is no demand yet. These notes say what one would need. The ACL finding was checked against the stock config of Debian bookworm exim4-daemon-light (Exim 4.96, 2026-10-19). The router and transport points come from the Exim documentation and are untested.

Pitfalls

  • Stock ACL rejects every P2 address. acl_check_rcpt denies local domains' local-parts that match CHECK_RCPT_LOCAL_LOCALPARTS, which defaults to ^[.] : ^.*[@%!/|#&?]. So ticket!callback@Dandticket=PR%2B42@Dget550 restricted characters in address. This is the same failure class as Postfix's reject_unauth_destination. Upstream Exim's example configure has the same rule (^[.] : ^.*[@%!/|]`).
  • Percent hack is off by default (percent_hack_domains unset), so % is not rewritten once the ACL lets it through. Exim has no bang-path rewriting.
  • Case. Routers lower-case $local_part unless caseful_local_part = true, which would hide Ticket=PR42 (invalid in MAS) as ticket=PR42.
  • Suffixes. A router with local_part_suffix = +* strips facets before the transport sees them. Do not set it on the MAS router.

Settings a profile would need

# main section: drop % and ! from the local-domain restriction
CHECK_RCPT_LOCAL_LOCALPARTS = ^[.] : ^.*[@/|`#&?]

# router, first for the MAS domain
mas:
  driver = accept
  domains = D
  caseful_local_part = true
  transport = mas_pipe

# transport: one recipient per call, envelope recipient as received
mas_pipe:
  driver = pipe
  batch_max = 1
  user = mas
  command = /usr/local/bin/mas-gateway --statute <statute> \
    --trusted-authserv-id D pipe --app-url <url> \
    --recipient ${quote:$local_part}@$domain

Keep CHECK_RCPT_REMOTE_LOCALPARTS as it is: relaying stays closed, and only D is a local domain. Sender authentication would use Exim's built-in dkim/spf/dmarc ACL conditions with add_header = Authentication-Results: D; .... Incoming A-R headers for D must be removed first (headers_remove).