Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
148 commits
Select commit Hold shift + click to select a range
3724728
feat(email): inbound routing, sendEmail, dev inbox and test harness
Brooooooklyn Aug 30, 2026
3345169
feat(email): cf-native outbound and inbound email
Brooooooklyn Aug 30, 2026
f8d8b1c
fix(email): dev inbox capture, harness casing, and reply-sender docs
Brooooooklyn Aug 31, 2026
4ccb016
fix(email): RFC 2047 word splitting, harness reject parity, PARSE_FAILED
Brooooooklyn Aug 31, 2026
6c5da08
fix(email): address preflight, docs result narrowing, and the logs --…
Brooooooklyn Sep 1, 2026
9cfd3ba
fix(email): client stub, header line limit, filename escaping, prompt…
Brooooooklyn Sep 2, 2026
89b9d47
fix(email): treat inbound handlers as a backend, and pin the framewor…
Brooooooklyn Sep 10, 2026
af10ad3
fix(email): make the default reply subject best-effort, and qualify t…
Brooooooklyn Sep 11, 2026
814423a
docs(email): design the lifecycle and UX across the three hosting modes
Brooooooklyn Sep 11, 2026
62d3f38
docs(email): merge the lifecycle and UX design into 0078
Brooooooklyn Sep 11, 2026
3687547
docs(email): bring custom domains on Cloudflare into scope
Brooooooklyn Sep 11, 2026
f1a2ee0
docs(email): custom-domain token in three clicks via the dashboard te…
Brooooooklyn Sep 11, 2026
34efbdb
docs(email): take the custom-domain token from the clipboard; no loca…
Brooooooklyn Sep 11, 2026
e002e67
docs(email): full token grant diagram with branches; token_id + expir…
Brooooooklyn Sep 11, 2026
98c8d42
docs(email): what a Void Cloudflare OAuth client would change, and wh…
Brooooooklyn Sep 11, 2026
6834700
docs(email): Mode B is one keystroke — verified flow from the deep re…
Brooooooklyn Sep 11, 2026
3b90aeb
docs(email): say plainly that email is platform-only today
Brooooooklyn Sep 11, 2026
edd899b
docs(email): carry the Mode B research into progress, operations, les…
Brooooooklyn Sep 11, 2026
0272bb4
chore(deps): bump wrangler to 4.131 and @cloudflare/vite-plugin to 1.…
Brooooooklyn Sep 11, 2026
3877f80
feat(email): add email.from to void.json, keep send_email out of dev
Brooooooklyn Sep 11, 2026
3f31f97
feat(email): sendEmail / addresses / vars in ProvisionedWranglerPatch
Brooooooklyn Sep 11, 2026
276697e
feat(email): own-account runtime — SEND_EMAIL transport, sender defau…
Brooooooklyn Sep 11, 2026
12985d9
feat(email): Mode B module — preflight, derivation, checklist, apply,…
Brooooooklyn Sep 11, 2026
d0f4039
Merge branch 'task/runtime' into feat/email-reland
Brooooooklyn Sep 11, 2026
1667090
Merge branch 'task/config' into feat/email-reland
Brooooooklyn Sep 11, 2026
4289c10
Merge branch 'task/wrangler-patch' into feat/email-reland
Brooooooklyn Sep 11, 2026
b503001
Merge branch 'task/email-cloudflare' into feat/email-reland
Brooooooklyn Sep 11, 2026
60aa301
docs(email): describe email on the developer's own Cloudflare account
Brooooooklyn Sep 11, 2026
4766738
feat(email): void email setup / status for --backend cloudflare
Brooooooklyn Sep 11, 2026
fec3e1f
chore(deps): add ci-info to the void CLI
Brooooooklyn Sep 11, 2026
31b87f4
feat(email): wire the Mode B email step into `void deploy --backend c…
Brooooooklyn Sep 11, 2026
c063413
Merge branch 'task/deploy' into feat/email-reland
Brooooooklyn Sep 11, 2026
7595c56
Merge branch 'task/email-cli' into feat/email-reland
Brooooooklyn Sep 11, 2026
fa9178d
Merge branch 'task/docs' into feat/email-reland
Brooooooklyn Sep 11, 2026
d053c8b
fix(email): review round — 17 findings
Brooooooklyn Sep 11, 2026
f3638c4
Merge origin/main into feat/email-reland (platform monorepo)
Brooooooklyn Sep 11, 2026
a970c38
Merge feat/email-reland (void-platform) under platform/ — subtree
Brooooooklyn Sep 11, 2026
cd35afe
docs(email): monorepo paths in 0078 and the runbook Email section; tw…
Brooooooklyn Sep 11, 2026
42d5e46
feat(email): --platform cloudflare on void email setup/status (--back…
Brooooooklyn Sep 11, 2026
70f9067
fix(ci): import test helpers from vite-plus/test, lint the platform a…
Brooooooklyn Sep 11, 2026
75845aa
fix(ci): typecheck only the platform packages in platform-ci
Brooooooklyn Sep 11, 2026
9d300cd
fix(email): bound a hand-built reply at MAX_MESSAGE_BYTES
Brooooooklyn Sep 11, 2026
a96eb3d
fix(api): keep the email-slug backfill out of the routing-parse try b…
Brooooooklyn Sep 11, 2026
6ee214a
fix(api): resend the platform confirmation link on a repeat destinati…
Brooooooklyn Sep 11, 2026
5e4020e
fix(email): export the error classes, guard type-only imports, bound …
Brooooooklyn Sep 13, 2026
b34e350
docs(email): say plainly that `void dev` binds no send transport
Brooooooklyn Sep 13, 2026
1090b35
fix(deploy): gate --require-email on real readiness, not binding pres…
Brooooooklyn Sep 13, 2026
060e28f
fix(proxy): cap the recipient list before it is normalized
Brooooooklyn Sep 13, 2026
72ebb63
fix(api): say that suspension stops mail in both directions
Brooooooklyn Sep 13, 2026
a94e151
fix(api): per-project consent, mirror repair, and the teardown race
Brooooooklyn Sep 13, 2026
dca91bf
fix(api): retire owner-inherited recipients when the provider email c…
Brooooooklyn Sep 13, 2026
363d859
fix(email): do not spread a hostile header into Math.max
Brooooooklyn Sep 13, 2026
91e29fb
fix(api): roll back the owner destination when teardown wins the race
Brooooooklyn Sep 13, 2026
b4a3280
fix(email): bound the inbound action queue at the proxy, and in the h…
Brooooooklyn Sep 13, 2026
aa09501
fix(email): surface a stream read failure, and an unusable project slug
Brooooooklyn Sep 13, 2026
fc09ca4
fix(email): bound attachments before they are buffered
Brooooooklyn Sep 13, 2026
5bc03c7
Merge origin/main into feat/email-reland
Brooooooklyn Sep 13, 2026
ef0bf52
fix(proxy): check the forward header shape before enumerating it
Brooooooklyn Sep 13, 2026
38cac50
fix(proxy): build the envelope header bag with Object.fromEntries
Brooooooklyn Sep 13, 2026
b3f91fe
fix(email): name the error when reply() gets a native EmailMessage
Brooooooklyn Sep 13, 2026
004defd
fix(email): bound the text and html bodies before they are encoded
Brooooooklyn Sep 13, 2026
10f0939
fix(email): reject an email/ directory on node, bun and deno targets
Brooooooklyn Sep 13, 2026
bf0fb56
fix(proxy): refuse a non-string forward header value
Brooooooklyn Sep 14, 2026
dd00bd4
fix(email): bound recipients before any address is parsed or folded
Brooooooklyn Sep 14, 2026
ed94185
Merge origin/main into feat/email-reland
Brooooooklyn Sep 14, 2026
0228266
fix(cli): fail email setup when the inbound rows are not ready
Brooooooklyn Sep 14, 2026
eee1100
fix(proxy): pin the MIME From address to the envelope sender
Brooooooklyn Sep 14, 2026
df2cf2f
fix(email): count headers in the size projection, before any header i…
Brooooooklyn Sep 14, 2026
9a56594
fix(email): fold a header in one pass that keeps only finished lines
Brooooooklyn Sep 14, 2026
64f2f80
fix(email): project threading and part headers, and refuse an unfolda…
Brooooooklyn Sep 14, 2026
9a517aa
fix(email): scan a threading header without materializing its tokens
Brooooooklyn Sep 14, 2026
ae747ba
Merge origin/main into feat/email-reland
Brooooooklyn Sep 14, 2026
e948170
fix(api): commit the owner email change and the destination demotion …
Brooooooklyn Sep 14, 2026
c65eb3e
fix(api): cap a destination address before normalizing or matching it
Brooooooklyn Sep 14, 2026
7d76da5
fix(email): walk a body's lines without splitting or re-joining them
Brooooooklyn Sep 14, 2026
3c7e1eb
fix(email): charge each attachment part's framing and filename in the…
Brooooooklyn Sep 14, 2026
b68b770
fix(email): allow-list the attachment disposition at runtime
Brooooooklyn Sep 14, 2026
36fd47a
fix(api): re-derive the allowlist mirror after a destination row is d…
Brooooooklyn Sep 14, 2026
1c49ad9
test(email): keep the ~10 MB boundary renders inside CI's clock
Brooooooklyn Sep 14, 2026
e57dc4e
docs(email): pin why a deleted handler's `addresses` entry is a block…
Brooooooklyn Sep 14, 2026
cbe5c57
fix(proxy): bound the envelope sender before anything copies it
Brooooooklyn Sep 14, 2026
d238217
fix(email): remember a refused sending onboarding by the committed bi…
Brooooooklyn Sep 14, 2026
edd2022
fix(cli): reject a missing value for `void email --project` / `--limit`
Brooooooklyn Sep 14, 2026
4035761
fix(plugin): strip only Void's own `SEND_EMAIL` entry under serve
Brooooooklyn Sep 14, 2026
ec394c7
fix(email): say when an add mailed nothing because Cloudflare already…
Brooooooklyn Sep 14, 2026
53ed178
fix(proxy): bound four more tenant-sized inputs before anything alloc…
Brooooooklyn Sep 14, 2026
1e0950a
fix(email): reject a second email/_default.* handler
Brooooooklyn Sep 14, 2026
8f1f50e
fix(email): decode Q-words into a typed array; capture one id per uni…
Brooooooklyn Sep 14, 2026
d1a4911
fix(proxy): bound the platform token, request ids, and every untruste…
Brooooooklyn Sep 14, 2026
38365e8
fix(api,cli): cap project slugs at 56 and tell the CLI which owner ad…
Brooooooklyn Sep 14, 2026
559a967
fix(dev): require the dev-trigger token on every /__void/inbox route
Brooooooklyn Sep 14, 2026
1b14fa7
fix(email): bound the inbound subject the default reply subject is bu…
Brooooooklyn Sep 14, 2026
f1823b3
docs(api): name the delete-vs-register window on a shared destination…
Brooooooklyn Sep 14, 2026
6495539
fix(email): charge custom header names in the MIME projection, refuse…
Brooooooklyn Sep 14, 2026
b9b90cf
fix(api): roll the owner auto-add's Cloudflare tag back when the row …
Brooooooklyn Sep 14, 2026
3e0e919
fix(api): promote a CF-pending destination only off the tag it regist…
Brooooooklyn Sep 14, 2026
6c34c78
fix(api): fail project teardown closed when the email send mirrors ca…
Brooooooklyn Sep 14, 2026
f9670ec
fix(api): inherit the owner's Cloudflare consent only while the addre…
Brooooooklyn Sep 14, 2026
22a0bc3
fix(void): store the dev trigger token owner-only
Brooooooklyn Sep 14, 2026
3f17d5b
fix(void): read a disabled routing rule as absent; validate forward h…
Brooooooklyn Sep 14, 2026
b2d854d
fix(deploy-cloudflare): carry `addresses` through the trigger-config …
Brooooooklyn Sep 14, 2026
0f55976
fix(email): cap the local part at 64 octets; bound a reply's Referenc…
Brooooooklyn Sep 14, 2026
478dae3
fix(email): cap inbound header count; run the test harness on the pro…
Brooooooklyn Sep 14, 2026
4bd437e
fix(email): refuse reserved email slugs on every mail path; validate …
Brooooooklyn Sep 14, 2026
de6791b
fix(email): validate a hand-built reply's addresses at the call
Brooooooklyn Sep 14, 2026
c3a104d
fix(email): cap an address at 254 octets; document that own-account s…
Brooooooklyn Sep 14, 2026
eee1908
docs(email): record the RPC result materialization limit of inbound d…
Brooooooklyn Sep 15, 2026
c1b39c3
fix(email): mirror the router's forward-header bounds and reject-reas…
Brooooooklyn Sep 15, 2026
fee38c6
fix(email): refuse C0 controls and DEL in a bare address at the call
Brooooooklyn Sep 15, 2026
b48be39
fix(email): bound the attachment count before any entry is normalized
Brooooooklyn Sep 15, 2026
318de55
fix(email): refuse ':' in a bare address, and pin the framework addre…
Brooooooklyn Sep 15, 2026
6ab857e
refactor(email): validate addresses with email-validator on both sides
Brooooooklyn Sep 15, 2026
ed1e56d
fix(email): refuse control characters in attachment parameters, drop …
Brooooooklyn Sep 15, 2026
c4b34f0
fix(email): size the dev inbox capture cap for the serialized capture
Brooooooklyn Sep 15, 2026
b11b9dd
fix(email): mirror the router's inbound admission limits in the test …
Brooooooklyn Sep 15, 2026
942689e
fix(email): validate an attachment's media type before rendering it
Brooooooklyn Sep 15, 2026
4ca7b1d
fix(email): validate an attachment's Content-ID before rendering it
Brooooooklyn Sep 15, 2026
760e856
fix(email): refuse a non-string workerName as corrupt routing data
Brooooooklyn Sep 15, 2026
ced5f4d
fix(email): refuse a malformed proxy delivery list instead of droppin…
Brooooooklyn Sep 15, 2026
19e1be4
fix(email): quote a display name that contains a period
Brooooooklyn Sep 15, 2026
b92054c
fix(api): tighten the allowlist mirror inside the owner-email change …
Brooooooklyn Sep 15, 2026
81bcc55
Merge origin/main into feat/email-reland
Brooooooklyn Sep 15, 2026
2b36022
Merge remote-tracking branch 'origin/main' into feat/email-reland
Brooooooklyn Sep 15, 2026
eff0e68
docs(email): record the RPC result materialization decision in 0078
Brooooooklyn Sep 15, 2026
cd17b77
fix(email): refuse an incomplete or malformed mid-batch delivery list
Brooooooklyn Sep 15, 2026
8666af5
fix(email): route login guidance through Void, refuse a mistyped proj…
Brooooooklyn Sep 15, 2026
bb6f925
fix(email): gate the api email routes on the platform capability, say…
Brooooooklyn Sep 15, 2026
6ef17b1
fix(email): say when a revoked recipient is refused, pin parse-then-f…
Brooooooklyn Sep 15, 2026
2be1ec2
fix(email): refuse a malformed suspension marker on send, gate the pr…
Brooooooklyn Sep 15, 2026
bd07cbc
Merge origin/main into feat/email-mono (operator API, CLI docs rewrite)
Brooooooklyn Sep 15, 2026
d60939a
fix(email): classify the platform send binding's error code before it…
Brooooooklyn Sep 15, 2026
990d116
docs(0078): record the Mode B setup-by-direction question as open dec…
Brooooooklyn Sep 15, 2026
5d8ec49
Merge origin/main into feat/email-mono (Worker handoff, self-hosted c…
Brooooooklyn Sep 15, 2026
fb1509b
fix(email): let an exact tag beat a captured tag, refuse a repeated o…
Brooooooklyn Sep 15, 2026
8a243a4
fix(prerender): open the same local D1 the dev server and migration r…
Brooooooklyn Sep 15, 2026
7f31424
feat(email): custom domains on the customer's own Cloudflare zone
Brooooooklyn Sep 17, 2026
2d86633
Merge origin/main into feat/email-reland
Brooooooklyn Sep 17, 2026
25671f1
fix(email): close relay leaks and propagate grant revocation failures
Brooooooklyn Sep 17, 2026
35dd85b
docs(email): rewrite 0078's custom-domains chapter as design, not fix…
Brooooooklyn Sep 17, 2026
ee14c13
fix(email): revoke live grant chain heads and serialize secret rotation
Brooooooklyn Sep 17, 2026
2fcb996
fix(email): fence probes against credential replace; complete inbound…
Brooooooklyn Sep 17, 2026
7995893
fix(email): fence the status self-clear; sync reads credentials and p…
Brooooooklyn Sep 17, 2026
d3bc5e4
fix(email): serialize domain removal under the row lock; fence sync a…
Brooooooklyn Sep 17, 2026
5998831
fix(email): serialize adds and retries under the project lock; revoke…
Brooooooklyn Sep 17, 2026
f046bc7
fix(email): reconcile lost add answers in the CLI; serialize admin re…
Brooooooklyn Sep 17, 2026
250e9e2
fix(email): relay MIME through FixedLengthStream; attempt-identity ad…
Brooooooklyn Sep 17, 2026
4c6d6e9
fix(email): skip the shared allowlist on the relay lane; fence the le…
Brooooooklyn Sep 17, 2026
16850e7
fix(email): claim relay names before the upload; reconcile 403 add an…
Brooooooklyn Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const referenceItems = [
{ text: 'void secret', link: '/reference/cli#secrets' },
{ text: 'void github', link: '/reference/cli#github' },
{ text: 'void domain', link: '/reference/cli#custom-domains' },
{ text: 'void email', link: '/reference/cli#email' },
{ text: 'void init --agents', link: '/reference/cli#agent' },
],
},
Expand Down
37 changes: 34 additions & 3 deletions docs/guide/email.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,40 @@ The project owner's email (the GitHub address you signed up with) is added autom
::: warning When this is the right fit
The shared sender is great for: ops alerts to the team, notifications to the project owner, reply-by-email flows on top of inbound, internal/app-internal mail.

For SaaS sending to arbitrary end-users (every signup gets a welcome email), the per-recipient verification model doesn't fit. On [your own Cloudflare account](#your-own-cloudflare-account) with Workers Paid, Void onboards your mail domain for Email Sending, which lifts the verified-recipient gate. Otherwise use [Resend](https://resend.com), [Postmark](https://postmarkapp.com), or [SES](https://aws.amazon.com/ses/) directly — install their SDK and call it from your handler. We may formalize this with a provider abstraction later if there is demand; until then, calling the SDK directly is simple enough that the wrapper would not earn its keep.
For SaaS sending to arbitrary end-users (every signup gets a welcome email), the per-recipient verification model doesn't fit. On the platform, [registering your own domain](#your-own-domain-on-the-platform) lifts that gate for sends from that domain. On [your own Cloudflare account](#your-own-cloudflare-account) with Workers Paid, Void onboards your mail domain for Email Sending, which lifts the verified-recipient gate. Otherwise use [Resend](https://resend.com), [Postmark](https://postmarkapp.com), or [SES](https://aws.amazon.com/ses/) directly — install their SDK and call it from your handler. We may formalize this with a provider abstraction later if there is demand; until then, calling the SDK directly is simple enough that the wrapper would not earn its keep.
:::

## Your own domain on the platform

The shared sender lives on the platform's `mail.void.cloud` zone. To send — and receive — at a domain you own, register its Cloudflare zone with the project:

```sh
void email domain add acme.com
```

`add` needs one credential for the zone and offers two ways to grant it. The default opens Cloudflare's hosted consent page for an OAuth grant — one Allow click; the page names Wrangler, whose OAuth client Void borrows for it. Pressing Enter at any point, or any failure of the consent flow, switches to the fallback: a three-click template link that creates a scoped API token, which the CLI watches for on the clipboard or takes as a masked paste. Either credential is POSTed to the platform once and stored there, encrypted for the project — nothing is kept on your machine.

**Where the mail lives.** Void never enables routing over live mail: when `acme.com` already carries MX records, the CLI proposes `mail.acme.com` (`[change with --subdomain]`); the apex is used only when it carries no MX records. `--subdomain <label|host>` overrides the proposal outright.

**The pending state.** Enabling Email Routing on a subdomain is the one step with no API path, so when it is the step that remains, `add` prints it — Cloudflare dashboard → Email Routing → acme.com → Settings → Subdomains → add the subdomain — and the row sits at `pending`. Poll with:

```sh
void email domain status acme.com
```

Once public MX on the domain names Cloudflare, the row flips itself to `active` — no second command. `status` also re-probes the stored credential and the relay worker on every call, so it doubles as the drift report.

**When a row is not active.** `failed` names the step that failed; fix it and re-run `void email domain add acme.com` — adding again replaces the credential and retries, and the routing rules stay. `token_revoked` / `token_expired` mean the stored credential died at Cloudflare; re-run `add` with a fresh grant or token — rules and the relay stay.

Two upkeep verbs:

- `void email domain sync acme.com` redeploys the relay at the current version and rotates its secret — the answer when `status` reports the relay missing or drifted.
- `void email domain remove acme.com` deletes the platform row, the stored credential, and the relay secret. Cloudflare-side cleanup is best-effort; anything that could not be finished is named so you can delete it by hand.

Two rules bound registrations: one live email domain per Cloudflare zone (a second `add` on the same zone is refused until you `remove` the first), and one project per domain (a domain registered to another project is refused).

Sends from a registered domain skip the verified-recipient allowlist — the domain's own Email Sending onboarding replaces it — while platform quota and abuse controls still apply. Inbound needs nothing further: once the domain is `active`, mail to any address on it reaches your `email/` handlers.

## Options

| Option | Type | Notes |
Expand All @@ -87,7 +118,7 @@ At most 100 recipients across `to`, `cc` and `bcc` per call. Each address is che

`Address` accepts either a string (`"hello@acme.dev"` or `"Name <hello@acme.dev>"`) or an object (`{ email, name? }`). Display names with non-ASCII characters are RFC 2047 encoded automatically.

On the platform the sender is pinned to your project. `from` must be your project's own platform address — `<project-slug>@mail.void.cloud` or `<project-slug>+<tag>@mail.void.cloud`, optionally with a display name (`Acme <acme+noreply@mail.void.cloud>`). Anything else is rejected with `INVALID_FROM`. Omit `from` and Void fills in `<project-slug>+noreply@mail.void.cloud` for you. Sending from your own domain on the platform is not supported yet.
On the platform the sender is pinned to your project. `from` must be your project's own platform address — `<project-slug>@mail.void.cloud` or `<project-slug>+<tag>@mail.void.cloud`, optionally with a display name (`Acme <acme+noreply@mail.void.cloud>`) — or any address on a domain registered with `void email domain add` (see [Your own domain on the platform](#your-own-domain-on-the-platform)). Anything else is rejected with `INVALID_FROM`. Omit `from` and Void fills in `<project-slug>+noreply@mail.void.cloud` for you.

On your own Cloudflare account, `from` defaults to `email.from` from `void.json` and must be on a domain your account can send from; Cloudflare rejects any other sender and `sendEmail` reports it as `INVALID_FROM`.

Expand Down Expand Up @@ -383,7 +414,7 @@ never throws, while `replyEmail` throws when it cannot determine a sender.

### Configuring inbound delivery

In production, inbound runs on one shared mail facility. The platform routes `<slug>+anything@<mail domain>` to your worker's `email()` export — **there is nothing to configure in the Cloudflare dashboard and no per-project DNS work**. Your handlers are live as soon as the deploy lands.
In production, inbound runs on one shared mail facility. The platform routes `<slug>+anything@<mail domain>` to your worker's `email()` export — **there is nothing to configure in the Cloudflare dashboard and no per-project DNS work**. Your handlers are live as soon as the deploy lands. A [registered custom domain](#your-own-domain-on-the-platform) lands on the same facility: once `void email domain add` reports it `active`, mail to any address on the domain reaches your `email/` handlers, with nothing further to configure.

On your own Cloudflare account there is no shared facility: the deploy derives one Email Routing rule per handler and writes it into `wrangler.jsonc` for you — see [Your own Cloudflare account](#your-own-cloudflare-account).

Expand Down
49 changes: 49 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Use this page as a command reference. If you are setting up a project for the fi
| `void email destinations` | List verified recipient addresses |
| `void email allow <address>` | Add a recipient and send a verification email |
| `void email disallow <address>` | Remove a recipient from the allowlist |
| `void email domain` | Send and receive at your own domain on a Cloudflare zone |
| `void init` | Setup wizard for new or existing projects |

## Binary Invocation
Expand Down Expand Up @@ -1313,6 +1314,54 @@ void email disallow <address> [--project <name>]

Remove one recipient from the project's destination list. Sends to that address are refused within about a minute: the platform updates the project's allowlist as part of the command, and the proxy re-reads it every 60 seconds. No deploy is involved.

### `void email domain`

```
void email domain <add|status|list|sync|remove> [<domain>] [--project <name>]
```

Send and receive at your own domain on a Cloudflare zone you own, registered to the project. Void platform only — on your own Cloudflare account the mail domain comes from `email.from` instead (see `void email setup`). The walkthrough is [Your own domain on the platform](../guide/email.md#your-own-domain-on-the-platform).

#### `void email domain add`

```
void email domain add <domain> [--subdomain <label|host>] [--project <name>]
```

Register the domain with the project. One credential is required, granted two ways: an OAuth grant from Cloudflare's hosted consent page (the page names Wrangler — Void borrows its OAuth client), or, as the fallback Enter switches to at any point, a scoped API token created from a three-click template link and picked up from the clipboard or a masked paste. The credential is POSTed once and stored on the platform, encrypted for the project — nothing is kept locally. Needs an interactive terminal. The CLI proposes `mail.<domain>` when the apex already carries MX records and allows the apex only when it carries none; `--subdomain` overrides the proposal. One live email domain per zone and one project per domain are enforced — a conflict is refused with a 409. Re-running `add` on a `failed`, `token_revoked`, or `token_expired` row replaces the credential and retries; the routing rules stay.

#### `void email domain status`

```
void email domain status <domain> [--project <name>]
```

Show one registered domain. The platform re-probes the stored credential and the relay worker on every call, so this doubles as the drift report. A `pending` row whose only remaining step is the dashboard's subdomain form (printed by `add`) self-clears to `active` once public MX on the domain names Cloudflare — which makes this command the poll for that one human step.

#### `void email domain list`

```
void email domain list [--project <name>]
```

List the project's registered email domains with their status and mode.

#### `void email domain sync`

```
void email domain sync <domain> [--project <name>]
```

Redeploy the domain's relay worker at the current version and rotate its secret — the fix when `status` reports the relay missing or drifted.

#### `void email domain remove`

```
void email domain remove <domain> [--project <name>]
```

Delete the registration: the platform row, the stored credential, and the relay secret. Cloudflare-side cleanup is best-effort; any step that fails is named (`failed_steps`) so you can finish it in the Cloudflare dashboard.

### `void email status`

```
Expand Down
Loading
Loading