ReviewOS

also looking at this

mail-os/mail

feat(config): add hosted_domains for multi-domain hosting

#1
Open glennmichael123 wants to merge feat/hosted-domains into main
7 files +153 -42
9 days ago

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 in hosted_domains is also local.
  • Case-sensitivity unified. The local-delivery check at protocol.zig:1362 and the auto-forward check at protocol.zig:1504 were case-sensitive (std.mem.eql); the relay-denial check at :681 was already case-insensitive. The new helper is case-insensitive everywhere, matching RFC 5321 §2.3.5. In practice this only affects pathological clients sending RCPT 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.org

Empty 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:

  1. User lookup is localpart-only. protocol.zig:1375-1378 strips the domain and uses only the localpart as the maildir / database username. Once two hosted domains share a localpart (e.g. hello@stacksjs.com and hello@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.

  2. DKIM signing is not wired into outbound delivery. antispam/dkim.zig has a DKIMSigner, but delivery/outbound.zig never 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 by from domain).

Test plan

  • zig build passes
  • zig build test --summary all → 84/84 tests pass (including 6 new isLocalDomain cases)
  • zig fmt --check clean
  • Manual smoke on production: deploy, set SMTP_HOSTED_DOMAINS=paweldregan.com, restart mail.service, send test mail to hello@paweldregan.com from gmail, confirm delivery to maildir (will require DNS + SES verification first per the linked issue's checklist)

🤖 Generated with Claude Code

7 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.