| 1 | --- |
| 2 | title: Email |
| 3 | description: Cloudflare Email Routing turns a zone into a mail endpoint — enable routing, verify destinations, forward with rules and a catch-all, hand mail to a Worker, and send from Workers with the send_email binding. |
| 4 | --- |
| 5 | |
| 6 | Email Routing turns a zone you own into a mail endpoint: rules match |
| 7 | inbound mail and forward it, drop it, or hand it to a Worker — and |
| 8 | once routing is enabled, Workers can also *send* mail from the domain |
| 9 | through a `send_email` binding. |
| 10 | |
| 11 | You need a zone first — see [Domains & DNS](/cloudflare/networking/domains). |
| 12 | Receiving needs a verified destination address (or a Worker); sending |
| 13 | needs routing enabled on the sender's domain. The category also |
| 14 | covers sending subdomains and the enterprise Email Security suite, |
| 15 | both below. |
| 16 | |
| 17 | ## Enable routing on the zone |
| 18 | |
| 19 | `Email.Routing` is the prerequisite for both directions: |
| 20 | |
| 21 | ```typescript |
| 22 | const routing = yield* Cloudflare.Email.Routing("Routing", { |
| 23 | zone: "example.com", |
| 24 | }); |
| 25 | // routing.zoneId, routing.enabled, routing.status |
| 26 | ``` |
| 27 | |
| 28 | `zone` accepts a zone id, a zone name, or a `{ zoneId }` object; |
| 29 | moving zones is a replacement, and destroy disables Email Routing on |
| 30 | the zone. |
| 31 | |
| 32 | ## Verify a destination address |
| 33 | |
| 34 | Rules can only forward to addresses Cloudflare has verified: |
| 35 | |
| 36 | ```typescript |
| 37 | const inbox = yield* Cloudflare.Email.Address("Inbox", { |
| 38 | email: "you@personal.example", |
| 39 | }); |
| 40 | // inbox.verified, inbox.verifiedAt |
| 41 | ``` |
| 42 | |
| 43 | Cloudflare emails a verification link on first create — until it's |
| 44 | clicked, forwards don't deliver; addresses are account-scoped, and |
| 45 | changing `email` is a replacement. |
| 46 | |
| 47 | ## Forward an address |
| 48 | |
| 49 | `Email.Rule` matches inbound mail on the zone and applies actions: |
| 50 | |
| 51 | ```typescript |
| 52 | const rule = yield* Cloudflare.Email.Rule("InboxRule", { |
| 53 | zone: "example.com", |
| 54 | name: "Forward inbox to destination", |
| 55 | matchers: [{ type: "literal", field: "to", value: "inbox@example.com" }], |
| 56 | actions: [{ type: "forward", value: ["you@personal.example"] }], |
| 57 | }); |
| 58 | ``` |
| 59 | |
| 60 | Mail that matches no rule falls through to the zone's catch-all; |
| 61 | lower `priority` numbers run first (default `0`). |
| 62 | |
| 63 | ## Catch everything else |
| 64 | |
| 65 | The catch-all is a per-zone singleton — `Email.CatchAll` puts the |
| 66 | desired configuration and restores the prior state on destroy: |
| 67 | |
| 68 | ```typescript |
| 69 | yield* Cloudflare.Email.CatchAll("CatchAll", { |
| 70 | zone: routing.zoneId, |
| 71 | actions: [{ type: "forward", value: ["you@personal.example"] }], |
| 72 | }); |
| 73 | ``` |
| 74 | |
| 75 | To silently discard unmatched mail instead: |
| 76 | |
| 77 | ```typescript |
| 78 | yield* Cloudflare.Email.CatchAll("DropTheRest", { |
| 79 | zone: routing.zoneId, |
| 80 | actions: [{ type: "drop" }], |
| 81 | }); |
| 82 | ``` |
| 83 | |
| 84 | ## Hand mail to a Worker |
| 85 | |
| 86 | Both `Rule` and `CatchAll` accept a `worker` action naming a deployed |
| 87 | Worker script (its `workerName` attribute): |
| 88 | |
| 89 | ```typescript |
| 90 | yield* Cloudflare.Email.CatchAll("CatchAllWorker", { |
| 91 | zone: routing.zoneId, |
| 92 | actions: [{ type: "worker", value: ["my-email-worker"] }], |
| 93 | }); |
| 94 | ``` |
| 95 | |
| 96 | Matched messages are delivered to that Worker's `email` handler |
| 97 | instead of a mailbox. |
| 98 | |
| 99 | ## Declare the send_email binding |
| 100 | |
| 101 | `Email.SendEmail` is a binding descriptor, not a cloud resource — it |
| 102 | names the binding and pins sender/destination restrictions Cloudflare |
| 103 | enforces at send time: |
| 104 | |
| 105 | ```typescript |
| 106 | // src/email.ts |
| 107 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 108 | |
| 109 | export const Email = Cloudflare.Email.SendEmail("Email", { |
| 110 | allowedSenderAddresses: ["bot@example.com"], |
| 111 | destinationAddress: "you@personal.example", |
| 112 | }); |
| 113 | ``` |
| 114 | |
| 115 | `destinationAddress` pins one verified destination, |
| 116 | `allowedDestinationAddresses` allows a list (the two are mutually |
| 117 | exclusive); omit all three to send to any verified destination on the |
| 118 | account. |
| 119 | |
| 120 | ## Send from a Worker |
| 121 | |
| 122 | Bind the descriptor with `Email.Send` in the Worker's Construction phase and |
| 123 | provide `Email.SendBinding` as the implementation layer: |
| 124 | |
| 125 | ```typescript |
| 126 | // src/notifier.ts |
| 127 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 128 | import * as Effect from "effect/Effect"; |
| 129 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 130 | import { Email } from "./email.ts"; |
| 131 | |
| 132 | export default Cloudflare.Worker( |
| 133 | "Notifier", |
| 134 | { main: import.meta.url }, |
| 135 | Effect.gen(function* () { |
| 136 | const email = yield* Cloudflare.Email.Send(Email); |
| 137 | |
| 138 | return { |
| 139 | fetch: Effect.gen(function* () { |
| 140 | yield* email.send({ |
| 141 | from: "bot@example.com", |
| 142 | to: "you@personal.example", |
| 143 | subject: "Hello from a Worker", |
| 144 | text: "Sent via the send_email binding.", |
| 145 | }); |
| 146 | return yield* HttpServerResponse.json({ ok: true }); |
| 147 | }).pipe( |
| 148 | Effect.catchTag("SendEmailError", (error) => |
| 149 | Effect.succeed( |
| 150 | HttpServerResponse.text(error.message, { status: 502 }), |
| 151 | ), |
| 152 | ), |
| 153 | ), |
| 154 | }; |
| 155 | }).pipe(Effect.provide(Cloudflare.Email.SendBinding)), |
| 156 | ); |
| 157 | ``` |
| 158 | |
| 159 | Rejections (an unverified destination, a sender outside the allow |
| 160 | list) surface as a typed `SendEmailError`; the client also exposes |
| 161 | `sendRaw` for a pre-built `EmailMessage` and `raw` for the underlying |
| 162 | runtime binding. The `from:` address must be on a domain with routing |
| 163 | enabled, and the `to:` address must be a verified destination. |
| 164 | |
| 165 | ## Send from a subdomain |
| 166 | |
| 167 | `Email.SendingSubdomain` registers e.g. `mail.example.com` as a |
| 168 | sender domain without touching the apex: |
| 169 | |
| 170 | ```typescript |
| 171 | const sending = yield* Cloudflare.Email.SendingSubdomain("Mail", { |
| 172 | zoneId: zone.zoneId, |
| 173 | name: "mail.example.com", |
| 174 | }); |
| 175 | // sending.enabled — true once DNS records validated |
| 176 | ``` |
| 177 | |
| 178 | For zones not on Cloudflare DNS, fetch the expected records via |
| 179 | `emailSending.getSubdomainDns` and create them at your host — |
| 180 | `enabled` flips to `true` once they validate. |
| 181 | |
| 182 | ## Email Security (enterprise) |
| 183 | |
| 184 | The Email Security add-on filters inbound mail before delivery; |
| 185 | accounts without the entitlement get a typed |
| 186 | `EmailSecurityNotEntitled` error: |
| 187 | |
| 188 | ```typescript |
| 189 | yield* Cloudflare.Email.BlockSender("KnownPhisher", { |
| 190 | pattern: "phisher@malicious.example.com", |
| 191 | patternType: "EMAIL", |
| 192 | }); |
| 193 | ``` |
| 194 | |
| 195 | The rest of the suite: |
| 196 | |
| 197 | - [Domain](/providers/cloudflare/email/domain) — per-domain |
| 198 | dispositions and TLS enforcement. |
| 199 | - [AllowPolicy](/providers/cloudflare/email/allowpolicy) — |
| 200 | acceptable, trusted, and exempt senders. |
| 201 | - [TrustedDomain](/providers/cloudflare/email/trusteddomain) — |
| 202 | suppress lookalike and new-domain detections. |
| 203 | - [ImpersonationRegistryEntry](/providers/cloudflare/email/impersonationregistryentry) |
| 204 | — protect display names from BEC. |
| 205 | |
| 206 | ## Where next |
| 207 | |
| 208 | The full walkthrough: |
| 209 | |
| 210 | - [Send & receive email](/cloudflare/email/send-and-receive) — wire both |
| 211 | directions on one zone, end to end. |
| 212 | |
| 213 | Related: |
| 214 | |
| 215 | - [Workers](/cloudflare/compute/workers) — the runtime that sends and |
| 216 | receives. |
| 217 | - [Domains & DNS](/cloudflare/networking/domains) — the zone mail routes |
| 218 | through. |
| 219 | - [Custom domains & routes](/cloudflare/networking/custom-domains) — get |
| 220 | or adopt the zone first. |
| 221 | |
| 222 | Reference: |
| 223 | |
| 224 | - [Routing API reference](/providers/cloudflare/email/routing) |
| 225 | - [Address API reference](/providers/cloudflare/email/address) |
| 226 | - [Rule API reference](/providers/cloudflare/email/rule) |
| 227 | - [CatchAll API reference](/providers/cloudflare/email/catchall) |
| 228 | - [Send API reference](/providers/cloudflare/email/send) |
| 229 | - [SendingSubdomain API reference](/providers/cloudflare/email/sendingsubdomain) |
| 230 | - [Domain API reference](/providers/cloudflare/email/domain) |
| 231 | - [BlockSender API reference](/providers/cloudflare/email/blocksender) |
| 232 | - [AllowPolicy API reference](/providers/cloudflare/email/allowpolicy) |
| 233 | - [TrustedDomain API reference](/providers/cloudflare/email/trusteddomain) |
| 234 | - [ImpersonationRegistryEntry API reference](/providers/cloudflare/email/impersonationregistryentry) |