All skills
cloudflare avatar

/cloudflare-email-service

@41e0d19 official
by cloudflarecloudflare/skills3k stars
298

Implement or troubleshoot Cloudflare Email Sending and Email Routing integrations and their delivery configuration.

Use this Skill: https://skilld.dev/gh/cloudflare/skills/cloudflare-email-service

This session only. Nothing lands on disk.

referencesrouting.md

≈1.9k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Receiving & Routing Inbound Email

Handle incoming emails sent to your domain via a Worker's email() handler. Forward, reply, reject, or parse emails programmatically.

For full API details, see the Email Routing docs.

Email Handler

Export an email() function from your Worker. No special wrangler binding needed — a routing rule connects incoming addresses to your Worker.

export default {
  async email(message, env, ctx): Promise<void> {
    console.log(`Email from ${message.from} to ${message.to}`);
    await message.forward("team@company.com");
  },
} satisfies ExportedHandler<Env>;

Set up routing rules in Dashboard > Compute & AI > Email Service > Email Routing > Routing Rules, or via wrangler email routing rules create.

ForwardableEmailMessage

The message parameter is a ForwardableEmailMessage. Run npx wrangler types to get the full type definition. Key properties and methods:

  • message.from / message.to — envelope addresses (SMTP MAIL FROM / RCPT TO). message.from is trustworthy; header addresses can be spoofed.
  • message.headers — Headers object (use .get("subject"), .get("message-id"), etc.)
  • message.raw — ReadableStream<Uint8Array> of raw MIME content. Single use — buffer before accessing.
  • message.rawSize — size in bytes
  • message.setReject(reason) — reject with a permanent SMTP error
  • message.forward(rcptTo, headers?) — forward to a verified destination
  • message.reply(emailMessage) — reply with an EmailMessage object

Core Operations

Forward

await message.forward("team@company.com");

// With custom headers
await message.forward("team@company.com", new Headers({
  "X-Original-Recipient": message.to,
}));

Destination must be verified first (Dashboard or wrangler email routing addresses create).

Reject

message.setReject("Your message was blocked");

Reply

Using env.EMAIL.send() (recommended — no extra dependencies):

async email(message, env, ctx) {
  const subject = message.headers.get("subject") || "";
  await env.EMAIL.send({
    to: message.from,
    from: message.to,
    subject: `Re: ${subject}`,
    html: "<p>Thanks! We'll respond shortly.</p>",
    text: "Thanks! We'll respond shortly.",
  });
  await message.forward("team@company.com");
}

Using message.reply() with MIME (more control, requires mimetext + nodejs_compat):

import { EmailMessage } from "cloudflare:email";
import { createMimeMessage } from "mimetext";

async email(message, env, ctx) {
  const msg = createMimeMessage();
  const messageId = message.headers.get("Message-ID");
  if (messageId) msg.setHeader("In-Reply-To", messageId);
  msg.setSender({ name: "Support", addr: "support@yourdomain.com" });
  msg.setRecipient(message.from);
  msg.setSubject("Re: " + (message.headers.get("subject") || ""));
  msg.addMessage({ contentType: "text/plain", data: "Thanks for reaching out!" });

  await message.reply(new EmailMessage("support@yourdomain.com", message.from, msg.asRaw()));
}

Parsing Emails

Use postal-mime to parse raw MIME content:

import PostalMime from "postal-mime";

async email(message, env, ctx) {
  const rawBuffer = await new Response(message.raw).arrayBuffer();
  const parsed = await PostalMime.parse(rawBuffer);

  console.log("Subject:", parsed.subject);
  console.log("Text:", parsed.text);
  console.log("Attachments:", parsed.attachments.length);
}

Store and Reply Later (Human-in-the-Loop)

A common pattern is to store incoming emails in a Durable Object (SQLite) so a human or AI agent can review and reply later — rather than replying immediately in the email() handler. This enables support inboxes, approval workflows, and AI-drafted replies.

Architecture

Email Routing → email() handler → parse + store in DO → user reviews later → reply via send_email binding

The email() handler stores the email and returns immediately. Replies happen later via a separate HTTP request or agent action.

Receive and Store

import PostalMime from "postal-mime";

export class MailboxDO extends DurableObject {
  async storeEmail(from: string, to: string, subject: string, body: string,
                   messageId: string, inReplyTo: string | null) {
    this.ctx.storage.sql.exec(
      `INSERT INTO emails (sender, recipient, subject, body, message_id, in_reply_to, date, read)
       VALUES (?, ?, ?, ?, ?, ?, datetime('now'), 0)`,
      from, to, subject, body, messageId, inReplyTo
    );
  }
}

export default {
  async email(message, env, ctx) {
    const raw = await new Response(message.raw).arrayBuffer();
    const parsed = await PostalMime.parse(raw);

    const id = env.MAILBOX.idFromName(message.to);
    const stub = env.MAILBOX.get(id);

    await stub.storeEmail(
      message.from,
      message.to,
      parsed.subject || "(no subject)",
      parsed.text || parsed.html || "",
      message.headers.get("message-id") || "",
      message.headers.get("in-reply-to") || null,
    );

    // Optionally trigger an AI agent to draft a reply (non-blocking)
    // ctx.waitUntil(notifyAgent(env, message.to, emailId));
  },
} satisfies ExportedHandler<Env>;

Reply Later

When a user (or agent) decides to reply, build proper threading headers and send via the send_email binding:

// In an HTTP handler or agent tool — not in the email() handler
async function replyToStoredEmail(env: Env, original: StoredEmail, replyBody: string) {
  // Build threading headers (In-Reply-To + References per RFC 2822)
  const headers: Record<string, string> = {};
  if (original.messageId) {
    headers["In-Reply-To"] = original.messageId;
    headers["References"] = original.messageId;
  }

  await env.EMAIL.send({
    to: original.sender,
    from: original.recipient,
    subject: `Re: ${original.subject}`,
    text: replyBody,
    html: `<p>${replyBody}</p>`,
    headers,
  });
}

Key Design Points

  • Buffer message.raw once, parse with postal-mime, then store structured fields. Don't store the raw stream.
  • Extract Message-ID, In-Reply-To, and References headers during ingest for threading. Fall back to subject-based matching for emails without threading headers.
  • Use Durable Object SQLite for per-mailbox storage — each mailbox gets its own DO instance keyed by email address, providing natural isolation.
  • Store attachments separately in R2 (binary blobs), with metadata in SQLite.
  • Defer heavy work (AI drafting, notifications) via ctx.waitUntil() so the email() handler returns quickly.
  • Never auto-send from the email() handler in a human-in-the-loop flow. Store a draft, let the user review, then send via a separate action.

Gotchas

  • message.raw is single-use. Buffer first: const raw = await new Response(message.raw).arrayBuffer()
  • Destinations must be verified. Forwarding to unverified addresses fails silently.
  • Handler must act. If your handler returns without consuming raw, forwarding, or rejecting, the email is dropped.
  • DMARC/SPF for replies. If sending replies, ensure your domain has proper SPF/DKIM records (auto-configured on domain onboarding).

Source: SKILL.md on GitHub

1 warning16d4 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    This skill provides documentation and implementation guides for the Cloudflare Email Service. It includes security considerations regarding the handling of untrusted email data and the use of external development tools, all of which are consistent with the skill's intended purpose.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: MEDIUM · 1 issue

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 41e0d19. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 7 hours ago.

Activeupdated 8 hours ago
  • Python
  • cloudflare
  • email-sending
  • email-routing
  • workers
  • transactional-email
  • rest-api
  • nodejs
  • golang

README badge

README badge for cloudflare/skills/cloudflare-email-service

Send transactional emails and route incoming messages using Cloudflare Email Service, with Workers bindings, REST API, or the Agents SDK. Use this skill for email setup, authentication (SPF/DKIM/DMARC), deliverability tuning, and integrating email into Workers, Node.js, Python, Go, or any AI coding agent.

Generated from the current SKILL.md.

Does this skill cover both sending and receiving emails?
Yes. It covers Email Sending (transactional emails via Workers binding or REST API) and Email Routing (inbound email handling with the `email()` handler). The references are split by use case.
Can I use this skill outside of Cloudflare Workers?
Yes. The REST API works from Node.js, Python, Go, or any external app. You'll need a Cloudflare API token and account ID instead of a binding.
Do I need to set up my domain before sending?
Yes. The domain must be onboarded to Email Sending first via `wrangler email sending enable yourdomain.com` or the Cloudflare Dashboard. The `from` address must use an onboarded domain.
What's the difference between the Workers binding and the REST API?
The Workers binding requires no API keys and uses camelCase field names (`email`, `replyTo`). The REST API uses snake_case (`address`, `reply_to`) and requires a Bearer token, but works outside Workers.
Can I use this for marketing emails or newsletters?
No. Email Service is for transactional emails only. Use a dedicated marketing platform for bulk sends and campaigns.

Generated from the current SKILL.md. These answers refresh after source changes.