| 1 | /** |
| 2 | * Inbound email worker: drive it locally by POSTing a raw MIME message to |
| 3 | * `/cdn-cgi/handler/email?from=...&to=...`. The `email()` handler records |
| 4 | * every message into the INBOX KV namespace (read back via |
| 5 | * `GET /email/received`); a subject starting with `reject-me` is rejected |
| 6 | * via `setReject`, which the sender observes as a 400. |
| 7 | * |
| 8 | * NOTE: this handler lives on its own worker (not MediaWorker) because in |
| 9 | * local dev the images/stream simulators insert a fetch-only middleware |
| 10 | * between the dev entry and the user worker, which currently swallows the |
| 11 | * entry's RPC dispatch of non-fetch events (`email()`, `queue()`, |
| 12 | * `scheduled()`). Keep inbound email handlers on workers without images or |
| 13 | * stream bindings until that is fixed in the local runtime. |
| 14 | */ |
| 15 | interface InboxKV { |
| 16 | get(key: string): Promise<string | null>; |
| 17 | put(key: string, value: string): Promise<void>; |
| 18 | } |
| 19 | |
| 20 | interface Env { |
| 21 | INBOX: InboxKV; |
| 22 | } |
| 23 | |
| 24 | interface InboundEmailLike { |
| 25 | from: string; |
| 26 | to: string; |
| 27 | raw: ReadableStream<Uint8Array>; |
| 28 | rawSize: number; |
| 29 | headers: Headers; |
| 30 | // `void` in production; locally the message crosses JSRPC, so the call |
| 31 | // returns a promise that must be awaited for the rejection to register. |
| 32 | setReject(reason: string): void | Promise<void>; |
| 33 | reply(message: unknown): Promise<{ messageId: string }>; |
| 34 | } |
| 35 | |
| 36 | export default { |
| 37 | async fetch(request: Request, env: Env): Promise<Response> { |
| 38 | const url = new URL(request.url); |
| 39 | if (url.pathname === "/email/received") { |
| 40 | const raw = await env.INBOX.get("received"); |
| 41 | return Response.json(raw ? JSON.parse(raw) : []); |
| 42 | } |
| 43 | return new Response("ok"); |
| 44 | }, |
| 45 | |
| 46 | async email(message: InboundEmailLike, env: Env): Promise<void> { |
| 47 | const subject = message.headers.get("subject") ?? ""; |
| 48 | const record = { |
| 49 | from: message.from, |
| 50 | to: message.to, |
| 51 | rawSize: message.rawSize, |
| 52 | subject, |
| 53 | messageIdHeader: message.headers.get("message-id"), |
| 54 | }; |
| 55 | const prior = await env.INBOX.get("received"); |
| 56 | const received: unknown[] = prior ? JSON.parse(prior) : []; |
| 57 | received.push(record); |
| 58 | await env.INBOX.put("received", JSON.stringify(received)); |
| 59 | |
| 60 | if (subject.startsWith("reject-me")) { |
| 61 | await message.setReject("I don't like this email"); |
| 62 | } |
| 63 | }, |
| 64 | }; |