1import * as Cloudflare from "alchemy/Cloudflare";
2import * as Effect from "effect/Effect";
3import { HttpServerRequest } from "effect/http/HttpServerRequest";
4import * as HttpServerResponse from "effect/http/HttpServerResponse";
5import { DESTINATION, INBOX, SendEmail, SENDER, ZONE } from "./Email.ts";
6
7interface ReceivedMessage {
8 from: string;
9 to: string;
10 subject: string | null;
11 bodySize: number;
12 receivedAt: number;
13}
14
15/**
16 * Durable Object that records every message the Worker's `email` handler
17 * sees so the integ test can confirm an inbound message round-tripped
18 * through Cloudflare's email routing.
19 */
20export class Inbox extends Cloudflare.DurableObject<Inbox>()(
21 "Inbox",
22 Effect.gen(function* () {
23 return Effect.gen(function* () {
24 const state = yield* Cloudflare.DurableObjectState;
25 let received =
26 (yield* state.storage.get<ReceivedMessage[]>("received")) ?? [];
27 return {
28 record: Effect.fn(function* (msg: ReceivedMessage) {
29 received = [...received, msg];
30 yield* state.storage.put("received", received);
31 }),
32 snapshot: () => Effect.succeed({ received }),
33 reset: Effect.fn(function* () {
34 received = [];
35 yield* state.storage.put("received", received);
36 }),
37 };
38 });
39 }),
40) {}
41
42/**
43 * Email service Worker. Demonstrates both halves of a Cloudflare Email
44 * Worker:
45 *
46 * - **Inbound** — `Cloudflare.email({ zone, matchers }).subscribe(...)`
47 * auto-creates an `EmailRouting` toggle on the zone and an `EmailRule`
48 * whose `actions: [{ type: "worker", … }]` targets this Worker. The
49 * handler records each message on the `Inbox` DO.
50 * - **Outbound** — the `send_email` binding from `Email.ts` lets the
51 * Worker emit mail from the configured sender to the verified
52 * destination.
53 *
54 * Routes:
55 *
56 * - `GET /healthz` — liveness probe; reports the configured addresses.
57 * - `POST /send` body `{ subject, text }` — sends mail from sender to
58 * destination via the `send_email` binding.
59 * - `GET /received` — snapshot of messages the `email` handler has seen.
60 * - `POST /reset` — clear the recorded inbox (used by the integ test).
61 */
62export default class Api extends Cloudflare.Worker<Api>()(
63 "Api",
64 {
65 main: import.meta.filename,
66 },
67 Effect.gen(function* () {
68 const email = yield* Cloudflare.Email.Send(SendEmail);
69 const inboxes = yield* Inbox;
70
71 // Subscribe to inbound mail addressed to INBOX on ZONE. The event
72 // source yields a sibling `Email.Routing` + `Email.Rule` at deploy
73 // time so no hand-rolled routing wiring is needed in `alchemy.run.ts`.
74 yield* Cloudflare.email({
75 zone: ZONE,
76 matchers: [{ type: "literal", field: "to", value: INBOX }],
77 }).subscribe((message) =>
78 inboxes.getByName("default").record({
79 from: message.from,
80 to: message.to,
81 subject: message.headers.get("subject"),
82 bodySize: message.bodySize,
83 receivedAt: Date.now(),
84 }),
85 );
86
87 return {
88 fetch: Effect.gen(function* () {
89 const request = yield* HttpServerRequest;
90 const url = new URL(request.url, "http://x");
91
92 if (url.pathname === "/healthz") {
93 return yield* HttpServerResponse.json({
94 ok: true,
95 from: SENDER,
96 to: DESTINATION,
97 inbox: INBOX,
98 });
99 }
100
101 if (url.pathname === "/received" && request.method === "GET") {
102 const snapshot = yield* inboxes.getByName("default").snapshot();
103 return yield* HttpServerResponse.json(snapshot);
104 }
105
106 if (url.pathname === "/reset" && request.method === "POST") {
107 yield* inboxes.getByName("default").reset();
108 return yield* HttpServerResponse.json({ ok: true });
109 }
110
111 if (url.pathname === "/send" && request.method === "POST") {
112 const body = (yield* request.json) as {
113 subject?: string;
114 text?: string;
115 to?: string;
116 };
117 const result = yield* email
118 .send({
119 from: SENDER,
120 to: body.to ?? DESTINATION,
121 subject: body.subject ?? "alchemy email example",
122 text: body.text ?? `sent at ${new Date().toISOString()}`,
123 })
124 .pipe(
125 Effect.match({
126 onSuccess: () => ({ ok: true as const }),
127 onFailure: (err) => ({
128 ok: false as const,
129 message: err.message,
130 }),
131 }),
132 );
133 return yield* HttpServerResponse.json(result);
134 }
135
136 return HttpServerResponse.text("not found", { status: 404 });
137 }),
138 };
139 }).pipe(
140 Effect.provide(Cloudflare.EmailEventSourceLive),
141 Effect.provide(Cloudflare.Email.SendBinding),
142 ),
143) {}