[follow-up] Stalwart local delivery + relay split verification on smoke server with full DNS

closed
#9bec37e opened by agent Sep 16

Goal

Verify on a fresh smoke-test server (new hosted domain with full DNS credentials) that Stalwart delivers mail locally for same-domain user-to-user mail while routing all other recipients through the configured outbound SMTP relay (sovrn-relay). Clear the stale NOTE in internal/relay/stalwart.go:35-36 (“NOT yet confirmed against a live Stalwart”).

Blocked until: new smoke test server is stood up + a hosted domain with full DNS credentials is available.

Background / investigation result

Investigation (2026-09-16) concluded YES — this split is the documented Stalwart pattern and is already coded:

  • internal/relay/stalwart.go:51-87 (ApplyOutbound): creates MtaRoute sovrn-relay (@type: Relay, smtp, authSecret: {"@type":"Value"}), then sets MtaOutboundStrategy.singleton.route = IF is_local_domain(rcpt_domain) THEN 'local' ELSE 'sovrn-relay' (index-keyed match: {"0":...}, single-quoted literals), disables cell DKIM (SenderAuth.dkimSignDomain: {"else":"false"}, sign-at-relay-only), then Action ReloadSettings.
  • Upstream docs (stalw.art/docs/mta/outbound/routing, /strategy; default route: {"else":"'mx'","match":[{"if":"is_local_domain(rcpt_domain)","then":"'local'"}]}): route is evaluated per-recipient; Local writes to mailbox store (JMAP/IMAP/POP3, no params); Relay forwards to smarthost; replacing default else: 'mx' with 'sovrn-relay' is the supported smarthost-with-local-delivery shape. Migration guide shows the same, plus is_local_address(rcpt)-first variant for split-delivery.
  • Other strategies untouched is correct: default schedule already returns 'local' for local rcpts.
  • Wire-shape tests: internal/relay/stalwart_test.go (TestApplyOutboundWireShape, RouteExistsTolerated, Abort, EmptySecret, TransportError).

Risks to clear live (from support threads on silent fallback to direct-MX): 1. local route must exist (fresh bootstrap ships local+mx; we only add sovrn-relay, never delete — confirm via stalwart-cli get MtaRoute). 2. is_local_domain arity: code uses 1-arg is_local_domain(rcpt_domain), matching current default; 2-arg ('', rcpt_domain) examples in docs are stale — do not “fix” without live evidence. 3. Name-miss fallback: if route returns a name with no matching MtaRoute.name, Stalwart logs smtp.id-not-found / Gateway not found and falls back to direct-MX (looks like “relay ignored”). Requires exact sovrn-relay match + reload. Community ed. has no Trace objects — diagnose via raised log level + journal. 4. implicitTls per provider: 587/STARTTLS → false; 465 → true (router.go:applyRelayRoute overlay — verify Lettermint vs SMTP2GO defaults).

Scope / verification plan

On the new smoke server, with relay configured (Lettermint or SMTP2GO) and a fully-DNS’d hosted domain (MX + SPF + relay CNAMEs verified):

  1. Config dump: stalwart-cli get MtaOutboundStrategy, query MtaRoute; assert route.match[0] == is_local_domain(rcpt_domain) → 'local', else == 'sovrn-relay'; relay object is Relay with correct host/port/auth; no duplicate routes after restart (re-apply tolerates primaryKeyViolation).
  2. Local test: alice@<hosted> → bob@<hosted> via submission. Assert arrival via IMAP/JMAP, no connection to relay host (relay stub counter / relay dashboard / packet capture), queue shows local schedule.
  3. External test: alice@<hosted> → external. Assert relay receives AUTH+message with single relay DKIM-Signature, SPF/DMARC pass, correct return-path; Stalwart does not attempt direct MX.
  4. Mixed recipients: one message to local + external → split: local copy + one relayed copy.
  5. Unknown local recipient: alice → nosuch@<hosted> — currently routes local then bounces locally. Confirm this is desired (vs split-delivery relay via is_local_address).
  6. Log check: no Gateway not found / id-not-found around deliveries; on any direct-MX sighting, treat as name-resolution failure, not relay failure.
  7. Docs cleanup: on green, delete stale NOTE in internal/relay/stalwart.go:35-36, record verified wire shape + 1-arg is_local_domain in docs/05-stalwart-integration.md.

Acceptance

  • [ ] Smoke server + full-DNS hosted domain provisioned; relay configured via sovrnd config.
  • [ ] Config dump matches expected strategy/route objects.
  • [ ] Local user-to-user mail delivered locally with zero relay hits.
  • [ ] External mail goes via sovrn-relay only (no direct-MX fallback).
  • [ ] Mixed-recipient split verified; unknown-local-recipient behavior decided.
  • [ ] Stale live-verification NOTE removed; docs updated.

Non-goals

No routing-expression redesign (unless live results force is_local_address split-delivery); no DKIM re-enablement on cell (sign-at-relay-only stands); no warmup/deliverability study.

Open questions (from investigation)

  • Unknown recipient in hosted domain: local bounce (current) or upstream relay?
  • Mixed-recipient policy: per-recipient split (Stalwart default) vs force-whole-message-via-relay if any external rcpt (DKIM/SPF alignment)?
  • DKIM: relay signs for customer domains, or per-domain keys + SenderAuth follow-up needed?

1 Comment

agent 9abae8c Oct 8

Superseded by e9d14c1 (2026-10-08): restated from current status on mx99 (routes and strategy verified live, external path via SMTP2GO verified; local, mixed and unknown-recipient cases and the docs cleanup remain).