1---
2title: Receive email in a Worker
3description: Receive inbound mail in a Cloudflare Worker with Cloudflare.email({ zone }).subscribe(...) — auto-creates routing and forwarding rules.
4sidebar:
5 order: 20
6---
7
8`Cloudflare.email({ zone }).subscribe(handler)` is the
9Effect-native API for Cloudflare Email Workers. One call wires
10both halves of the consumer:
11
12- **Runtime**: registers the `email` event listener on the Worker.
13- **Deploy-time**: yields an `Email.Routing` toggle on the zone and
14 the routing resource whose `actions: [{ type: "worker", … }]`
15 targets this Worker — `Email.CatchAll` for a catch-all
16 subscription, `Email.Rule` for anything more specific.
17
18Unlike queues there is no batch or stream — the handler runs once
19per message and returns `Effect<void, _, _>`.
20
21For the rest of the Email surface — verifying destination
22addresses, forwarding rules, the catch-all, sending from a Worker —
23see [Send & receive email](/cloudflare/email/send-and-receive).
24
25## Subscribe to inbound mail
26
27The handler receives a `ForwardableEmailMessage` whose action
28methods (`forward`, `reply`, `setReject`) return `Effect`s rather
29than `Promise`s, so they compose with the rest of your effect
30program.
31
32```typescript
33// src/Inbox.ts
34import * as Cloudflare from "alchemy/Cloudflare";
35import * as Effect from "effect/Effect";
36
37export default Cloudflare.Worker(
38 "Inbox",
39 { main: import.meta.url },
40 Effect.gen(function* () {
41 yield* Cloudflare.email({ zone: "example.com" }).subscribe((message) =>
42 message.forward("ops@example.com"),
43 );
44 return {};
45 }).pipe(Effect.provide(Cloudflare.EmailEventSourceLive)),
46);
47```
48
49That's it — `alchemy.run.ts` doesn't need any extra `Email.Routing`,
50`Email.Rule` or `Email.CatchAll` calls; the event source yields them
51as siblings of the Worker.
52
53## Provide the runtime layer
54
55`subscribe(...)` is a `Context.Service` call —
56`EmailEventSourceLive` is the layer that wires the listener into
57the Worker's runtime dispatch.
58
59```diff lang="typescript"
60 Effect.gen(function* () {
61 yield* Cloudflare.email({ zone: "example.com" }).subscribe(...);
62 return {};
63 }).pipe(
64+ Effect.provide(Cloudflare.EmailEventSourceLive),
65 ),
66```
67
68Without the live layer, the subscribe call fails at deploy with
69`Service not found: Cloudflare.Workers.EmailEventSource`.
70
71## Forward a message
72
73`message.forward(address)` hands the message off to a verified
74destination. The destination address must already exist on the
75account — declare it with `Email.Address` so Cloudflare sends the
76verification mail during deploy.
77
78```typescript
79const ops = yield* Cloudflare.Email.Address("Ops", {
80 email: "ops@example.com",
81});
82
83yield* Cloudflare.email({ zone: "example.com" }).subscribe((message) =>
84 message.forward(ops.email),
85);
86```
87
88`forward` (and `reply`) fail with `EmailError` if Cloudflare
89rejects the action (e.g. an unverified destination); `setReject`
90never fails.
91
92## Reject a message
93
94`message.setReject(reason)` bounces the message back to the
95sender. Use it for closed mailboxes or anti-abuse checks.
96
97```typescript
98yield* Cloudflare.email({ zone: "example.com" }).subscribe((message) =>
99 message.headers.get("x-spam-score") === "high"
100 ? message.setReject("Rejected: spam")
101 : message.forward("ops@example.com"),
102);
103```
104
105## Match a specific address
106
107By default `email({ zone })` subscribes to the zone's catch-all —
108every envelope no other rule claimed. Pass `matchers` to scope the
109subscription to specific recipients instead:
110
111```typescript
112yield* Cloudflare.email({
113 zone: "example.com",
114 matchers: [{ type: "literal", field: "to", value: "hello@example.com" }],
115}).subscribe((message) => message.forward("ops@example.com"));
116```
117
118The two forms provision different resources, because Cloudflare
119models them differently. A catch-all is a per-zone **singleton**
120behind its own endpoint (`/rules/catch_all`), so the event source
121yields `Email.CatchAll` and destroy restores whatever the zone had
122before. Specific matchers yield an ordinary `Email.Rule`.
123
124There is exactly one catch-all per zone, so a second Worker
125subscribing to it takes the zone's mail from the first. Use a
126dedicated zone, or `matchers`, if you need more than one inbound
127Worker.
128
129## Use the raw Cloudflare message
130
131The wrapped message exposes `raw` as an escape hatch to the
132underlying `cf.ForwardableEmailMessage`. Useful for fields not yet
133surfaced on the wrapper, or for SDKs that want the native type.
134
135```typescript
136yield* Cloudflare.email({ zone: "example.com" }).subscribe((message) =>
137 Effect.log(`raw size: ${message.raw.rawSize}`),
138);
139```
140
141## Bring your own routing
142
143If you're already managing `Email.Routing` / `Email.Rule` resources
144in `alchemy.run.ts` and just want the runtime listener, call
145`email()` with no `zone` — the deploy-time half becomes a no-op.
146
147```typescript
148yield* Cloudflare.email().subscribe((message) =>
149 Effect.log(`from ${message.from}`),
150);
151```
152
153## What's the difference vs. a native `email()` handler?
154
155Cloudflare's runtime delivers email events to an
156`email(message, env, ctx)` export on the Worker module. You can
157write that directly:
158
159```typescript
160export default {
161 async email(message, env, ctx) {
162 await message.forward("ops@example.com");
163 },
164};
165```
166
167`Cloudflare.email({ zone }).subscribe(...)` is the same primitive
168on the Effect side — the Worker bundle's runtime dispatch routes
169the `email` event to the registered listener — but you also get
170`Effect.gen` composition, typed errors, the auto-created
171`Email.Routing` + `Email.Rule`, and the same shape as the other
172Cloudflare event sources
173([`Cloudflare.Workers.cron`](/cloudflare/messaging/cron),
174[`Cloudflare.Queues.consumeQueueMessages`](/cloudflare/messaging/queues)).