Skip to content

feat(email): let administrators add outbound-only custom domains - #551

Merged
Brooooooklyn merged 1 commit into
mainfrom
feat/email-outbound-only
Oct 22, 2026
Merged

Brooooooklyn merged 1 commit into
mainfrom
feat/email-outbound-only

Conversation

@Brooooooklyn

Copy link
Copy Markdown
Member

Fixes #510

What it does

An admin can now register an apex whose mail lives with Microsoft 365 or Google Workspace, for sending only:

void platform email domain-add brand.example --project hr-portal --outbound-only
                 managed (today)                 outbound-only (new)
                 ───────────────                 ───────────────────
 apex MX         → Cloudflare Email Routing      untouched (M365 / Workspace)
 catch-all       → Void ingress Worker           untouched
 ingress Worker  deployed                        never deployed
 steps           routing_enable … publish        sending_onboard → publish
 receive mail    yes                             no  (inbound_status = disabled)
 send mail       yes                             yes
 Sending refused → verified_only fallback        → blocked, with the reason

Answers to the 4 questions

  1. Shape. A column, email_domain_routes.inbound_mode (managed | none, default managed), plus a check that none is only allowed with addressing = 'custom'. addressing already plays the "route kind" role, so a second kind would duplicate it.
  2. Who. Admin only. Owners get a 403 from the service. The owner CLI points them at the admin command when their apex already has foreign MX.
  3. DMARC/SPF. They live together; Void never edits DMARC or SPF. Before onboarding, the step calls Cloudflare's read-only POST /email/sending/subdomains/preview. If it reports any real conflict (not just *.missing), the operation is blocked with a list (dmarc.multiple at _dmarc.brand.example; …). Once the admin fixes those records, domain-sync resumes. A domain that is already onboarded skips the preview.
  4. Subdomains. Not in this PR; apex only. Subdomain senders need several routes per connection, so that is a follow-up.
domain-add --outbound-only
   │
   ├─ admin? apex? not the shared zone? ── no ──▶ 4xx, no Cloudflare writes
   │
   ▼
sending_onboard
   ├─ already onboarded? ─ yes ─▶ onboard (idempotent)
   ├─ preview conflicts? ─ yes ─▶ blocked + checklist
   ├─ onboard 4xx       ─────────▶ blocked ("needs Workers Paid")
   ├─ onboard 5xx/408   ─────────▶ unknown (operator recovery, as today)
   ▼
publish ─▶ outbound ready, inbound disabled

Safety details

  • Removal keeps the usual step list, but catchall_detach and ingress_delete finish locally for outbound-only routes, so no Email Routing or Workers calls are made. The stored credential is still cleared, so a Sending-only token can finish removal.
  • Mode switch on an active route returns 409 ("remove it first"). The add dry-run says so (plan: 'refuse').
  • Race guard. If the MX preflight was skipped because the route was active, and the route is no longer active by the time the transaction runs, the add returns 409 "retry" instead of reactivating it without an MX check.
  • Status refresh no longer overwrites a newer Sending result recorded by a setup step. Otherwise, on an outbound-only route, a stale blocked would stop all sending.
  • Older servers. The CLI aborts unless the dry-run echoes target.inboundMode === 'none', so an old platform that ignores the flag cannot run full managed setup.
  • Rollback. compatibility.json lists 1.1.11 through 1.2.1 as rollback-safe (the column is additive and defaults to managed). platform/meta/operations.md warns not to run domain-add or domain-sync on outbound-only domains after a rollback below this release, because an older runtime would treat them as managed and take over the MX.
  • domain-rotate-secret returns 409 for these domains, in both the preview and the real request.

Not verified live

  • That Cloudflare onboarding keeps a single existing _dmarc record instead of replacing it. The preview is Cloudflare's own check, but this still needs one run against a real test zone.
  • Removal still does not offboard Email Sending in Cloudflare. That was already true for managed domains and is unchanged here.

Tests

  • admin-upgrade-compatibility.test.ts: 0061 upgrade from the 0060 schema, defaults, and the check constraint.
  • email-connections.test.ts: foreign-MX apex runs 2 steps with no inbound calls; preview conflict blocks, then resumes; 403 blocks (not verified_only), then resumes; managed still falls back; 504 stays unknown; refusals; removal makes no inbound calls.
  • operator-email-domains.test.ts: HTTP flag validation, dry-run text, blocked → sync → ready, status, previews, rotate refused, audit, remove; status-refresh race (confirmed to fail without the fix).
  • email-admission.test.ts: sending admitted, receipt refused.
  • operator-cli.test.ts, email-cli-domain.test.ts: flag parsing, old-server guard, owner hint.

🤖 Generated with Claude Code

An apex whose MX belongs to Microsoft 365 or Google Workspace can now be
registered with `void platform email domain-add <apex> --outbound-only`.
The platform onboards only Cloudflare Email Sending and never touches the
apex MX, Email Routing, catch-all or ingress Worker.

- New `email_domain_routes.inbound_mode` column (managed | none), with
  rollback-safe compatibility entries for 1.1.11 through 1.2.1.
- Outbound-only routes run sending_onboard + publish only, store inbound
  as disabled, and block (not verified_only) when Email Sending is refused.
- A read-only Email Sending preview blocks setup with a checklist when
  existing DNS records conflict.
- Admin only, zone apex only, never the shared mail zone.
- The CLI refuses to continue when an older platform ignores the mode.

Fixes #510

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Brooooooklyn

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-22T11:11:15.661890Z db40c63 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://feat-email-outbound-only-void-cloud.voidzero-docs.workers.dev (commit db40c63)

This URL reflects your latest Preview deployment

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://4dafcb91-void-cloud.voidzero-docs.workers.dev db40c63 2026-10-22T11:08:22.939Z Visit the dashboard ↗

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: db40c637e1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Brooooooklyn
Brooooooklyn merged commit 36a3697 into main Oct 22, 2026
20 checks passed
@Brooooooklyn
Brooooooklyn deleted the feat/email-outbound-only branch October 22, 2026 11:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Platform email: outbound-only custom domain for an apex whose MX belongs to another provider

1 participant