Send and receive emails with Cloudflare Email
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
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
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 `recipient@example.com` 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: "sender@example.com" });
msg.setRecipient("recipient@example.com");
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(
"sender@example.com",
"recipient@example.com",
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.