Skip to content

Email

Email Routing turns a zone you own into a mail endpoint: rules match inbound mail and forward it, drop it, or hand it to a Worker — and once routing is enabled, Workers can also send mail from the domain through a send_email binding.

You need a zone first — see Domains & DNS. Receiving needs a verified destination address (or a Worker); sending needs routing enabled on the sender’s domain. The category also covers sending subdomains and the enterprise Email Security suite, both below.

Email.Routing is the prerequisite for both directions:

const routing = yield* Cloudflare.Email.Routing("Routing", {
zone: "example.com",
});
// routing.zoneId, routing.enabled, routing.status

zone accepts a zone id, a zone name, or a { zoneId } object; moving zones is a replacement, and destroy disables Email Routing on the zone.

Rules can only forward to addresses Cloudflare has verified:

const inbox = yield* Cloudflare.Email.Address("Inbox", {
email: "you@personal.example",
});
// inbox.verified, inbox.verifiedAt

Cloudflare emails a verification link on first create — until it’s clicked, forwards don’t deliver; addresses are account-scoped, and changing email is a replacement.

Email.Rule matches inbound mail on the zone and applies actions:

const rule = yield* Cloudflare.Email.Rule("InboxRule", {
zone: "example.com",
name: "Forward inbox to destination",
matchers: [{ type: "literal", field: "to", value: "inbox@example.com" }],
actions: [{ type: "forward", value: ["you@personal.example"] }],
});

Mail that matches no rule falls through to the zone’s catch-all; lower priority numbers run first (default 0).

The catch-all is a per-zone singleton — Email.CatchAll puts the desired configuration and restores the prior state on destroy:

yield* Cloudflare.Email.CatchAll("CatchAll", {
zone: routing.zoneId,
actions: [{ type: "forward", value: ["you@personal.example"] }],
});

To silently discard unmatched mail instead:

yield* Cloudflare.Email.CatchAll("DropTheRest", {
zone: routing.zoneId,
actions: [{ type: "drop" }],
});

Both Rule and CatchAll accept a worker action naming a deployed Worker script (its workerName attribute):

yield* Cloudflare.Email.CatchAll("CatchAllWorker", {
zone: routing.zoneId,
actions: [{ type: "worker", value: ["my-email-worker"] }],
});

Matched messages are delivered to that Worker’s email handler instead of a mailbox.

Email.SendEmail is a binding descriptor, not a cloud resource — it names the binding and pins sender/destination restrictions Cloudflare enforces at send time:

src/email.ts
import * as Cloudflare from "alchemy/Cloudflare";
export const Email = Cloudflare.Email.SendEmail("Email", {
allowedSenderAddresses: ["bot@example.com"],
destinationAddress: "you@personal.example",
});

destinationAddress pins one verified destination, allowedDestinationAddresses allows a list (the two are mutually exclusive); omit all three to send to any verified destination on the account.

Bind the descriptor with Email.Send in the Worker’s init phase and provide Email.SendBinding as the implementation layer:

src/notifier.ts
import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
import { Email } from "./email.ts";
export default Cloudflare.Worker(
"Notifier",
{ main: import.meta.url },
Effect.gen(function* () {
const email = yield* Cloudflare.Email.Send(Email);
return {
fetch: Effect.gen(function* () {
yield* email.send({
from: "bot@example.com",
to: "you@personal.example",
subject: "Hello from a Worker",
text: "Sent via the send_email binding.",
});
return yield* HttpServerResponse.json({ ok: true });
}).pipe(
Effect.catchTag("SendEmailError", (error) =>
Effect.succeed(
HttpServerResponse.text(error.message, { status: 502 }),
),
),
),
};
}).pipe(Effect.provide(Cloudflare.Email.SendBinding)),
);

Rejections (an unverified destination, a sender outside the allow list) surface as a typed SendEmailError; the client also exposes sendRaw for a pre-built EmailMessage and raw for the underlying runtime binding. The from: address must be on a domain with routing enabled, and the to: address must be a verified destination.

Email.SendingSubdomain registers e.g. mail.example.com as a sender domain without touching the apex:

const sending = yield* Cloudflare.Email.SendingSubdomain("Mail", {
zoneId: zone.zoneId,
name: "mail.example.com",
});
// sending.enabled — true once DNS records validated

For zones not on Cloudflare DNS, fetch the expected records via emailSending.getSubdomainDns and create them at your host — enabled flips to true once they validate.

The Email Security add-on filters inbound mail before delivery; accounts without the entitlement get a typed EmailSecurityNotEntitled error:

yield* Cloudflare.Email.BlockSender("KnownPhisher", {
pattern: "phisher@malicious.example.com",
patternType: "EMAIL",
});

The rest of the suite:

The full walkthrough:

Related:

Reference: