Skip to content

feat: add wrangler email routing commands#12932

Open
thomasgauvin wants to merge 34 commits intocloudflare:mainfrom
thomasgauvin:tgauvin/wrangler-email-routing-commands
Open

feat: add wrangler email routing commands#12932
thomasgauvin wants to merge 34 commits intocloudflare:mainfrom
thomasgauvin:tgauvin/wrangler-email-routing-commands

Conversation

@thomasgauvin
Copy link
Copy Markdown
Contributor

@thomasgauvin thomasgauvin commented Mar 16, 2026

Summary

Adds wrangler email routing and wrangler email sending commands wrapping the Cloudflare Email Routing and Email Sending REST APIs.

Commands

wrangler email routing
├── list                                            # List zones with email routing
├── settings       <domain>                         # Get settings for a zone
├── enable         <domain>                         # Enable email routing
├── disable        <domain>                         # Disable email routing
├── dns get        <domain>                         # Show required DNS records
├── dns unlock     <domain>                         # Unlock MX records
├── rules list/get/create/update/delete <domain>    # Routing rules CRUD (use 'catch-all' as rule ID for catch-all)
└── addresses list/get/create/delete                # Destination addresses (account-scoped)

wrangler email sending
├── list                                            # List zones with email sending
├── settings       <domain>                         # Get sending settings
├── enable         <domain>                         # Enable email sending
├── disable        <domain>                         # Disable email sending
├── dns get        <domain>                         # Show sending DNS records
├── send           --from --to --subject --text     # Send email (builder)
└── send-raw       --from --to --mime/--mime-file   # Send raw MIME email

Key design decisions

  • Positional domain: All zone-scoped commands take <domain> as a positional arg (e.g. wrangler email routing settings example.com), with optional --zone-id to skip zone lookup
  • Subdomain detection: resolveDomain walks up domain labels to find the zone; when --zone-id is provided, fetches GET /zones/{id} to correctly handle multi-label TLDs (.co.uk, .com.br)
  • Follows existing patterns: createCommand(), fetchResult()/fetchPagedListResult()
  • OAuth scopes: Adds email_routing:write and email_sending:write to DefaultScopes. Bach MRs: staging / production

Testing

  • 74 unit tests, all passing (3401 total wrangler tests pass)
  • Build passes

Documentation

PR: cloudflare/cloudflare-docs#27805


  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 16, 2026

🦋 Changeset detected

Latest commit: 0abedff

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown
Contributor

@LuisDuarte1 LuisDuarte1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from the EMAIL team:

├── rules list/get/create/update/delete             # Routing rules CRUD
├── rules catch-all get/update                      # Catch-all rule

why do we have to have 2 seperate comands for rules that are not catch-alls and rules that are a catch-all? couldn't rules * also handle this?

@thomasgauvin thomasgauvin marked this pull request as ready for review March 26, 2026 14:17
@thomasgauvin thomasgauvin requested a review from a team as a code owner March 26, 2026 14:17
@thomasgauvin thomasgauvin requested a review from vicb March 26, 2026 14:17
@workers-devprod
Copy link
Copy Markdown
Contributor

workers-devprod commented Mar 26, 2026

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .vscode/launch.json: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/email-routing.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/core/teams.d.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/addresses/create.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/addresses/delete.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/addresses/get.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/addresses/list.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/client.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/disable.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/dns-get.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/dns-unlock.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/enable.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/index.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/list.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/rules/create.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/rules/delete.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/rules/get.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/rules/list.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/rules/update.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/sending/dns-get.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/sending/send-raw.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/sending/send.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/sending/subdomains/create.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/sending/subdomains/delete.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/sending/subdomains/get.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/sending/subdomains/list.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/settings.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/email-routing/utils.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/index.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/user/user.ts: [@cloudflare/wrangler]

devin-ai-integration[bot]

This comment was marked as resolved.

Add CLI commands wrapping the Cloudflare Email Routing REST API:

- wrangler email routing list - list zones with email routing status
- wrangler email routing settings/enable/disable - manage zone settings
- wrangler email routing dns get/unlock - DNS record management
- wrangler email routing rules list/get/create/update/delete - routing rules CRUD
- wrangler email routing rules catch-all get/update - catch-all rule management
- wrangler email routing addresses list/get/create/delete - destination addresses

Zone-scoped commands support --zone (domain) and --zone-id flags.
Address commands are account-scoped.

Also adds email_routing:write OAuth scope to DefaultScopes so wrangler login
grants the necessary permissions.
- Merge catch-all into rules commands (rules get/update catch-all)
- Remove separate catch-all sub-namespace per EMAIL team feedback
- Add email sending control plane: subdomains list/get/create/delete, dns get
- Add email sending data plane: send (builder) and send-raw (MIME)
- Add 19 new tests for email sending commands (52 total)
- Update changeset to include sending commands
@thomasgauvin thomasgauvin force-pushed the tgauvin/wrangler-email-routing-commands branch from ee37d72 to 135bbce Compare March 26, 2026 14:26
@github-project-automation github-project-automation bot moved this to Untriaged in workers-sdk Mar 26, 2026
@github-project-automation github-project-automation bot moved this from Untriaged to In Review in workers-sdk Mar 26, 2026
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 26, 2026

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@12932

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@12932

miniflare

npm i https://pkg.pr.new/miniflare@12932

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@12932

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@12932

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@12932

@cloudflare/vitest-pool-workers

npm i https://pkg.pr.new/@cloudflare/vitest-pool-workers@12932

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@12932

wrangler

npm i https://pkg.pr.new/wrangler@12932

commit: 0abedff

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

@LuisDuarte1 LuisDuarte1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm from Email Eng

thomasgauvin and others added 5 commits March 26, 2026 13:02
- Rename owner from 'Product: Email Routing' to 'Product: Email Service' per EMAIL team feedback
- Use path.basename() instead of split('/') for cross-platform filename extraction
- Use fetchPagedListResult for listEmailSendingSubdomains to handle pagination
- Replace non-null assertion in rules/update.ts with type narrowing guard
- Replace non-null assertion in send-raw.ts with ?? fallback
- Use node: prefix for fs imports per repo conventions
The email sending commands use a separate OAuth scope (email_sending:write)
from the routing commands (email_routing:write). Without this scope,
wrangler login tokens get a 10000 auth error when calling email sending
API endpoints.

Bach staging MR: !4176 (merged)
- Change "open-beta" to "open beta" (correct status literal type)
- Fix ComplianceConfig import to use @cloudflare/workers-utils instead of
  non-existent ../environment-variables/misc-variables module
- Import describe/it/beforeEach/afterEach/vi from vitest in test file
- Use ({ expect }) => test context pattern for all it() callbacks
  per repo conventions (no global expect)
- Cast request.json() as Record<string, unknown> to fix spread type errors
- Use /accounts/{accountId}/email/routing/zones instead of fetching
  /zones then N separate /zones/{id}/email/routing calls. This matches
  the dashboard behavior and is more efficient (1 API call vs N+1).
- Add order/direction query params to rules list and addresses list
  to match dashboard API calls.
- Add VS Code debug launch configs for email routing commands.
devin-ai-integration[bot]

This comment was marked as resolved.

- Enable: use POST /zones/{id}/email/routing/enable (was POST /dns)
- Disable: use POST /zones/{id}/email/routing/disable (was DELETE /dns)
- Unlock: use POST /zones/{id}/email/routing/unlock (was PATCH /dns)
- Update tests to mock the correct endpoints
- Add mockListEmailRoutingZones helper for list tests
- Replace "not configured" and "error" per-zone tests with "disabled" test
  matching the single-endpoint list implementation
- Align with Stratus dashboard API calls per code review
- Guard against undefined response.errors in throwFetchError (fixes crash
  on APIs returning {code, error} instead of {errors: [...]})
- Handle messages as objects, not just strings (fixes esbuild formatMessages
  crash when API returns {code, message} objects in messages array)
- Surface non-standard error details (e.g. 'Unauthorized [code: 2036]')
  when standard errors array is empty
- Fix dns-unlock showing 'status: undefined' by using 'enabled' field
  which the PATCH /email/routing/dns endpoint actually returns
@workers-devprod
Copy link
Copy Markdown
Contributor

workers-devprod commented Mar 31, 2026

Codeowners approval required for this PR:

  • ✅ @cloudflare/wrangler
Show detailed file reviewers

- Switch DNS record display from table to vertical format (DKIM keys made tables unreadable)
- Remove empty status column from zone list (API doesn't return it)
- Separate catch-all from regular rules in rules list with usage hint
- Fallback to catch-all endpoint when rules get receives error 2020
devin-ai-integration[bot]

This comment was marked as resolved.

When the API returns empty delivered/queued/bounced arrays, the commands
previously printed nothing. Now shows 'Email sent successfully.' as
feedback. Also adds emoji prefixes to delivery status output.
- Remove dead listZones function and CloudflareZone type
- Remove unnecessary JSDoc comments in utils.ts
- Remove section separator comments from client.ts and index.ts
- Remove redundant null guard in rules/update.ts (already in validateArgs)
- Deduplicate EmailRoutingCatchAllAction/Matcher types (reuse base types)
- Extract logSendResult helper to avoid duplication in send/send-raw
Add zone-level email sending management commands:
- wrangler email sending settings --zone <domain>
- wrangler email sending enable --zone <domain>
- wrangler email sending disable --zone <domain>

These mirror the email routing enable/disable pattern and use the
/zones/{id}/email/sending, /enable, and /disable API endpoints.
Add wrangler email sending list to show all zones with email sending
status, mirroring wrangler email routing list. Uses the
/accounts/{id}/email/sending/zones API endpoint.
…ng DNS endpoints

The documented email routing enable/disable endpoints (POST/DELETE
/zones/{id}/email/routing/dns) require Zone Settings Write permission.
Add zone:write to DefaultScopes so wrangler login requests it.
@thomasgauvin
Copy link
Copy Markdown
Contributor Author

Docs will be done in here: cloudflare/cloudflare-docs#27805

…s namespace

Replace the nested subdomains CRUD with flat domain-aware commands:
- `wrangler email sending enable <domain>` — auto-detects zone vs subdomain
- `wrangler email sending disable <domain>` — same
- `wrangler email sending settings <domain>` — shows zone + subdomains
- `wrangler email sending dns get <domain>` — resolves subdomain tag automatically

The CLI walks up domain labels to find the zone (e.g. sub.example.com
tries sub.example.com, then example.com), so users never need to specify
--zone separately.

Removes subdomains/ directory, EmailSendingSubdomain type, and 4
subdomain CRUD client functions.
- Add confirmation prompts to destructive commands (disable, dns-unlock,
  rules delete, addresses delete) with --force/-y bypass
- Add EmailSendingSettings type to replace unsafe casts for subdomains
- Fix catch-all fallback in rules get to match both tag and id
- Fix rules update to include --name in catch-all payload
- Fix rules list contradictory output when only catch-all exists
- Fix sending dns-get to handle zone-level domains, not just subdomains
- Remove dead code (mockListZones, null guard in sending/disable)
- Remove emojis from CLI output
- Clean up test mock signatures and formatting nits
- Reject empty header names in --header parsing (e.g. ':value')
- Extract shared logSendResult() into sending/utils.ts to deduplicate
  inline emoji-laden logging in send.ts and send-raw.ts
- Add tests for malformed header input
- Remove emojis from send result output
…le, zone-level DNS

- Add optional --zone-id flag to all sending commands (enable, disable,
  settings, dns get) to skip zone lookup for tokens without zone:read
- Add confirmation prompt to email sending disable (matches routing disable)
- Add zone-level /email/sending/dns endpoint for apex domain DNS records,
  use subdomain endpoint only for actual subdomains
- Update resolveDomain() to accept optional zoneId override
devin-ai-integration[bot]

This comment was marked as resolved.

Replace --zone/--zone-id flags with a positional domain argument for all
email routing zone-scoped commands, matching the pattern already used by
email sending commands.

Before: wrangler email routing settings --zone example.com
After:  wrangler email routing settings example.com

--zone-id is kept as an optional flag to skip zone lookup.
Rules commands with rule-id use two positionals: domain then rule-id
(e.g. wrangler email routing rules get example.com rule-id-1).
Add 17 new tests covering:
- --force flag for disable, dns-unlock, rules delete
- User declining confirmation for disable, dns-unlock, rules delete
- --mime-file and missing file error for send-raw
- --attachment and missing file error for send
- Error 2020 catch-all fallback for rules get
- Catch-all rule in rules list output
- Empty DNS records for routing dns get
- Help text for email routing dns namespace
- Sending list (happy + empty) and settings commands

Also removes spurious status column assertion from routing list test
(enabled yes/no already conveys the same info).
…main

When --zone-id is provided, resolveDomain previously used labels.slice(-2)
to guess the zone name, which broke multi-label TLDs like .co.uk, .com.br.
For example, example.co.uk was misclassified as a subdomain of co.uk.

Now fetches GET /zones/{zoneId} to get the actual zone name, so subdomain
detection is correct for all TLDs.

Adds 7 tests covering domain + --zone-id combinations:
- Zone-level with --zone-id (body has no name)
- Subdomain with --zone-id (body has name)
- Multi-label TLD zone-level with --zone-id (example.co.uk)
- Multi-label TLD subdomain with --zone-id (notifications.example.co.uk)
- Disable zone-level and subdomain with --zone-id
- DNS get zone-level and subdomain paths with --zone-id
@thomasgauvin
Copy link
Copy Markdown
Contributor Author

thomasgauvin commented Apr 2, 2026

Manually reviewed all files. Mostly straightforward CRUD with approvals, some lookups of domains -> zones

@thomasgauvin
Copy link
Copy Markdown
Contributor Author

Tested manually, summary:

routing list PASS
routing settings PASS
routing enable PASS
routing disable PASS
routing dns get PASS
routing dns unlock PASS
routing rules list PASS
routing rules get PASS
routing rules get catch-all PASS
routing rules create PASS
routing rules update PASS
routing rules delete PASS
routing addresses list PASS
routing addresses get PASS
routing addresses create PASS
routing addresses delete PASS
sending list PASS
sending settings PASS
sending enable PASS
sending disable PASS
sending dns get PASS
sending send PASS
sending send-raw --mime-file PASS
sending send-raw --mime PASS
Unit tests (74 tests) PASS

Copy link
Copy Markdown
Contributor

@ascorbic ascorbic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving with one small comment

Copy link
Copy Markdown
Contributor

@workers-devprod workers-devprod left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codeowners reviews satisfied

@github-project-automation github-project-automation bot moved this from In Review to Approved in workers-sdk Apr 2, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Approved

Development

Successfully merging this pull request may close these issues.

4 participants