Summary
Adds Config.hosted_domains so the SMTP server can accept inbound mail for domains beyond hostname and its parent. Configurable via TOML (server.hosted_domains as a comma-separated string) or env (SMTP_HOSTED_DOMAINS).
Replaces three duplicated is_local blocks in core/protocol.zig (relay-denial check, local delivery routing, auto-forward target classification) with a single Config.isLocalDomain helper. Adds 6 unit tests covering exact match, parent match, hosted-domains match, case-insensitive match, no-match, and single-label hostname edge case.
Motivation: unblocks adding new hosted domains (e.g. paweldregan.com) on the existing single-IP server without splitting reputation across hosts. See stacksjs/paweldregan#7.
Behavior changes
- Multi-domain inbound delivery. Previously only
hostname(e.g.mail.stacksjs.com) and its parent (stacksjs.com) were accepted as local recipients. Now any entry inhosted_domainsis also local. - Case-sensitivity unified. The local-delivery check at
protocol.zig:1362and the auto-forward check atprotocol.zig:1504were case-sensitive (std.mem.eql); the relay-denial check at:681was already case-insensitive. The new helper is case-insensitive everywhere, matching RFC 5321 §2.3.5. In practice this only affects pathological clients sendingRCPT TO:<x@MAIL.STACKSJS.COM>— which were silently relayed instead of delivered locally before.
Configuration
# packages/zig configuration TOML
[server]
hostname = "mail.stacksjs.com"
hosted_domains = "paweldregan.com, example.org"Or via env:
SMTP_HOSTED_DOMAINS=paweldregan.com,example.orgEmpty list (default) preserves existing behavior exactly.
Out of scope — follow-ups required before this is end-to-end useful
These two gaps surfaced during the audit but are intentionally not in this PR. Both should get their own issues:
User lookup is localpart-only.
protocol.zig:1375-1378strips the domain and uses only the localpart as the maildir / database username. Once two hosted domains share a localpart (e.g.hello@stacksjs.comandhello@paweldregan.com), they'd silently collide on the same mailbox. Safe today because the domains in question don't share any localparts, but a real bug for future provisioning.DKIM signing is not wired into outbound delivery.
antispam/dkim.zighas aDKIMSigner, butdelivery/outbound.zignever calls it — outbound mail is currently unsigned regardless of domain. The mail-tester 10/10 target on the linked issue won't be hit until this is fixed, and the fix needs to be per-domain (key + selector map keyed byfromdomain).
Test plan
zig buildpasseszig build test --summary all→ 84/84 tests pass (including 6 newisLocalDomaincases)zig fmt --checkclean- Manual smoke on production: deploy, set
SMTP_HOSTED_DOMAINS=paweldregan.com, restartmail.service, send test mail tohello@paweldregan.comfrom gmail, confirm delivery to maildir (will require DNS + SES verification first per the linked issue's checklist)
🤖 Generated with Claude Code