RedwoodSDK integrates with Cloudflare Email Workers so your application can send transactional messages, receive inbound mail, and reply in the same Worker runtime.
Production deliveries currently require recipients to be verified through Cloudflare Email Routing, but Cloudflare’s forthcoming Email Service beta expands reach to general addresses.
This guide walks through the configuration steps, highlights important sending considerations, and demonstrates common patterns for end-to-end email workflows.
Implementing Email Handling
Section titled “Implementing Email Handling”Update your wrangler.jsonc to include the EMAIL binding:
{ "send_email": [ { "name": "EMAIL", }, ],}Next, run pnpm generate to update the generated type definitions.
Once you have a zone with Email Routing enabled, follow the Enable Email Workers documentation to deploy your Worker in production.
Outbound email must target a destination address that you have verified in Email Routing.
When calling env.EMAIL.send(), pass either a verified address or leave the recipient undefined when you use a binding that specifies destination_address or allowed_destination_addresses.
ℹ️ For broader transactional delivery to arbitrary recipients, see Cloudflare’s Email Service beta or an external provider such as Resend.
Example Worker with Email Handling
Section titled “Example Worker with Email Handling”The example below demonstrates how to integrate the worker with email sending and receiving.
To send and email, you can
simply call the env.EMAIL.send() method with the email message as
shown in the example below route("/email", async () => { ... }).
ℹ️ Note: In production, this must be a verified address in Email Routing.
However, to receive an email, you need to implement the email handler and
export it in the default export of the worker.
This is a change to how defineApp is often used in the worker.
The default export of the worker is the DefaultWorker class that extends the WorkerEntrypoint class.
This tells Cloudflare that the worker is also email worker and can be selected to route inbound emails to.
Note: If you are just sending emails, you can still use
defineAppas usual and just callenv.EMAIL.send()in the route handler or in a server function.
import * as PostalMime from "postal-mime";import { EmailMessage } from "cloudflare:email";import { createMimeMessage } from "mimetext";import { render, route } from "rwsdk/router";import { defineApp } from "rwsdk/worker";import { Document } from "@/app/Document";import { setCommonHeaders } from "@/app/headers";import { env, WorkerEntrypoint } from "cloudflare:workers";
const app = defineApp([ setCommonHeaders(),
/** * This route is used to send an email from the worker * First, we create a MIME message with the sender, recipient, * and the content of the email. * Then, we create a new EmailMessage object with the * sender, recipient, and the raw content of the email. * Finally, we send the email using the `env.EMAIL.send()` method. * Ensure the `[email protected]` address is verified in Cloudflare Email Routing, or adjust the binding configuration accordingly. */ route("/email", async () => { const msg = createMimeMessage(); msg.setSender({ name: "Sending email test", addr: "[email protected]" }); msg.setRecipient("[email protected]"); msg.setSubject("An email generated in a worker"); msg.addMessage({ contentType: "text/plain", data: `Congratulations, you just sent an email from a worker.`, });
const message = new EmailMessage( msg.asRaw() ); await env.EMAIL.send(message); return Response.json({ ok: true }); }),]);
/** * This is the default worker entrypoint for the Worker. * It extends the WorkerEntrypoint class and implements the email and fetch handlers. */// It extends the WorkerEntrypoint class and implements the email and fetch handlers.export default class DefaultWorker extends WorkerEntrypoint<Env> { /** * Email handler for the Worker. * The `message` parameter is an ForwardableEmailMessage object * * You can call `message.reply()` to respond directly to the * inbound sender without additional verification steps. */ async email(message: ForwardableEmailMessage) {
const parser = new PostalMime.default(); const rawEmail = new Response((message as any).raw); const email = await parser.parse(await rawEmail.arrayBuffer()); console.log(email); }
/** * Fetch handler for the Worker. * Needed so that the worker can handle the request and pass it to the app. */ override async fetch(request: Request) { return await app.fetch(request, this.env, this.ctx); }}Replying to inbound email
Section titled “Replying to inbound email”You can reply directly to an inbound message without pre-verifying the recipient.
The Worker runtime preserves threading headers and delivers the response through the original route. Construct your response with mimetext and pass the raw payload to message.reply(), as shown in the Reply from Workers guide.
It is important to note that the In-Reply-To header is required to reply to the inbound email.
async email(message: ForwardableEmailMessage) { console.log("📧 Email received");
// Parse the inbound email const parser = new PostalMime.default(); const rawEmail = new Response((message as any).raw);
const receivedEmail = await parser.parse(await rawEmail.arrayBuffer()); console.log("📧 Email received and parsed", receivedEmail);
// Create a new message to reply to the inbound email const replyToMessage = createMimeMessage();
// ❗️ Important:This In-Reply-To header is required to reply to the inbound email replyToMessage.setHeader( "In-Reply-To", message.headers.get("Message-ID") ?? "" );
replyToMessage.setSender({ name: "Contact Person", addr: "<SENDER>@example.com" }); replyToMessage.setRecipient(receivedEmail.from); replyToMessage.setSubject(`Re: ${receivedEmail.subject}`); replyToMessage.addMessage({ contentType: "text/plain", data: "Thanks for contacting us. We'll get back to you shortly.", });
console.log("📧 New message created", replyToMessage.asRaw());
const replyMessage = new EmailMessage( "<SENDER>@example.com", message.from, replyToMessage.asRaw() );
console.log("📧 Sending reply email");
await message.reply(replyMessage);
console.log("📧 Reply email sent");}Key points
Section titled “Key points”- By default, the worker is not an email worker. You need to extend the
WorkerEntrypointclass and implement theemailhandler to make it an email worker. - By extending the
WorkerEntrypointclass, you are telling Cloudflare that the worker is also email worker and can be selected to route inbound emails to. - The
messageparameter is anForwardableEmailMessageobject that contains the inbound email message. - The
In-Reply-Toheader is required to reply to the inbound email. - Use
PostalMimeto parse inbound messages for headers, text, HTML, and attachments. - Construct outbound MIME content with
mimetextto control subject, sender, and body. - Call
env.EMAIL.send()to deliver new messages, ormessage.reply()/message.forward()inside the email handler. - A fetch handler is needed so that the worker can handle all requests and pass it to the app.
Testing Locally
Section titled “Testing Locally”RedwoodSDK can emulate both inbound and outbound email interactions locally.
Sending Email Locally
Section titled “Sending Email Locally”To test the sending of an email by the email handler locally, you can use the following command:
pnpm devThis will start the local development server and you can send emails to the [email protected] address.
Now, visit http://localhost:5173/email to see the email in the console output of the local development server.
For this example, you’ll see the following response:
{ "ok": true}and in the console output, you’ll see something like the following log:
send_email binding called with the following message: /var/folders/ft/8320mthj6gbdd2pmc42x13480000gn/T/miniflare-288e7109e15f898bd9877d7857386f8b/files/email/2dad29db-0a7d-498d-89ab-e961746835c4.emlYou can also see the email in the .eml file in the temporary directory.
cat /var/folders/ft/8320mthj6gbdd2pmc42x13480000gn/T/miniflare-288e7109e15f898bd9877d7857386f8b/files/email/2dad29db-0a7d-498d-89ab-e961746835c4.eml
288e7109e15f898bd9877d7857386f8b/files/email/2dad29db-0a7d-498d-89ab-e961746835c4.eml
Date: Sun, 09 Nov 2025 01:54:06 +0000Subject: =?utf-8?B?QW4gZW1haWwgZ2VuZXJhdGVkIGluIGEgd29ya2Vy?=MIME-Version: 1.0Content-Type: text/plain; charset=UTF-8Content-Transfer-Encoding: 7bit
Congratulations, you just sent an email from a worker.%Note: The path to the
.emlfile is different for each operating system.
Receiving Email Locally
Section titled “Receiving Email Locally”To test the receiving of an email by the email handler locally, you can use the following command:
pnpm devThis will start the local development server and you can send emails to the [email protected] address.
Then, you can send an email to the [email protected] address using the following command:
curl --request POST 'http://localhost:5173/cdn-cgi/handler/email' \ --header 'Content-Type: application/json' \ --data-raw 'Received: from smtp.example.com (127.0.0.1) by cloudflare-email.com (unknown) id 4fwwffRXOpyR for <[email protected]>; Tue, 27 Aug 2024 15:50:20 +0000From: "John" <[email protected]>Reply-To: [email protected]Subject: Testing Email Workers Local DevContent-Type: text/html; charset="windows-1252"X-Mailer: CurlDate: Tue, 27 Aug 2024 08:49:44 -0700Message-ID: <6114391943504294873000@ZSH-GHOSTTY>
Hi there'You should see the content of the simulated email in the console output of the local development server.
{ headers: [ { key: 'received', value: 'from smtp.example.com (127.0.0.1) by cloudflare-email.com (unknown) id 4fwwffRXOpyR for <[email protected]>; Tue, 27 Aug 2024 15:50:20 +0000' }, { key: 'subject', value: 'Testing Email Workers Local Dev' }, { key: 'content-type', value: 'text/html; charset="windows-1252"' }, { key: 'x-mailer', value: 'Curl' }, { key: 'date', value: 'Tue, 27 Aug 2024 08:49:44 -0700' }, { key: 'message-id', value: '<6114391943504294873000@ZSH-GHOSTTY>' } ], subject: 'Testing Email Workers Local Dev', messageId: '<6114391943504294873000@ZSH-GHOSTTY>', date: '2024-08-27T15:49:44.000Z', html: 'Hi there\n', attachments: []}Production Deployment
Section titled “Production Deployment”To enable email handling in production, you need to have a Cloudflare zone with Email Routing enabled and at least one verified destination address.
You can refer to the following documentation:
- Configure Email Routing Rules and Addresses
- Enable Email Workers
- Send Email from Workers
- Reply to Email from Workers
- Cloudflare Email Service Beta
- Sending email with Resend
Further Reading
Section titled “Further Reading”Future Improvements
Section titled “Future Improvements”- Demonstrate how to compose emails with React Email.