Claude Cursor Skill

cloudflare-email-service

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

LLM Mart · 0 points · 4 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download fcakyon-claude-codex-settings-plugins_cloudflare-skills_skills_cloudflare-email-service-4632eb3.zip · 13 KB
Part of fcakyon/claude-codex-settings — 83 skills

Install

skills CLI npx skills add https://github.com/fcakyon/claude-codex-settings/tree/main/plugins/cloudflare-skills/skills/cloudflare-email-service
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install fcakyon-claude-codex-settings@llmmart
Git git clone https://github.com/fcakyon/claude-codex-settings.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole fcakyon/claude-codex-settings collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Cloudflare Email Service

Your knowledge of the Cloudflare Email Service, Email Routing or Email Sending may be outdated. Prefer retrieval over pre-training for any Cloudflare Email Service task.

Cloudflare Email Service lets you send transactional emails and route incoming emails, all within the Cloudflare platform. Your knowledge of this product may be outdated — it launched in 2025 and is evolving rapidly. Prefer retrieval over pre-training for any Email Service task.

If there is any discrepancy between this skill and the sources below, always trust the original source. The Cloudflare docs, REST API spec, @cloudflare/workers-types, and Agents SDK repo are the source of truth. This skill is a convenience guide — it may lag behind the latest changes. When in doubt, retrieve from the sources below and use what they say.

Retrieval Sources

Source How to retrieve Use for
Cloudflare docs Cloudflare MCP docs tool or URL https://developers.cloudflare.com/email-service/ API reference, limits, pricing, latest features
REST API spec https://developers.cloudflare.com/api/resources/email_sending OpenAPI spec for the Email Sending REST API
Workers types https://www.npmjs.com/package/@cloudflare/workers-types Type signatures, binding shapes
Agents SDK docs Email agent walkthrough Email handling in Agents SDK

FIRST: Check Prerequisites

Before writing any email code, verify the basics are in place:

  1. Domain onboarded? Run npx wrangler email sending list to see which domains have email sending enabled. If the domain isn't listed, run npx wrangler email sending enable userdomain.com or see cli-and-mcp.md for full setup instructions.
  2. Binding configured? Look for send_email in wrangler.jsonc (for Workers)
  3. postal-mime installed? Run npm ls postal-mime (only needed for receiving/parsing emails)

What Do You Need?

Start here. Find your situation, then follow the link for full details.

I want to... Path Reference
Send emails from a Cloudflare Worker Workers binding (no API keys needed) sending.md
Send emails from an AI agent built with Cloudflare Agents SDK onEmail() + replyToEmail() in Agent class sending.md
Send emails from an external app or agent (Node.js, Go, Python, etc.) REST API with Bearer token rest-api.md
Send emails from a coding agent (Claude Code, Cursor, Copilot, etc.) MCP tools, wrangler CLI, or REST API cli-and-mcp.md
Receive and process incoming emails (Email Routing) Workers email() handler routing.md
Set up Email Sending or Email Routing wrangler email sending enable / wrangler email routing enable, or Dashboard cli-and-mcp.md
Improve deliverability, avoid spam folders Authentication, content, compliance deliverability.md

Sending Workflow

Prefer the binding for Workers; use REST for external apps or when explicitly requested. Read sending.md or rest-api.md to retrieve the documentation for the selected task before writing code. These guides cover setup, recipients, attachments, headers, limits, response handling, and errors; the Workers guide also covers Agents SDK integration and types matched to the project configuration.

Common Mistakes

Mistake Why It Happens Fix
Forgetting send_email binding in wrangler config Email Service uses a binding, not an API key Add "send_email": [{ "name": "EMAIL" }] to wrangler.jsonc
Sending from an unverified domain Domain must be onboarded onto Email Sending before first send Run wrangler email sending enable yourdomain.com or onboard in Dashboard
Reading message.raw twice in email handler The raw stream is single-use — second read returns empty Buffer first: const raw = await new Response(message.raw).arrayBuffer()
Missing text field (HTML only) Some email clients only show plain text; also helps spam scores Always include both html and text versions
Using email for marketing/bulk sends Email Service is for transactional email only Use a dedicated marketing email platform for newsletters and campaigns
Forwarding to unverified destinations message.forward() only works with verified addresses Run wrangler email routing addresses create user@gmail.com or add in Dashboard
Testing with fake addresses Bounces from non-existent addresses hurt sender reputation Use real addresses you control during development
Hardcoding API tokens in source code Tokens in code get committed and leaked Use environment variables or Cloudflare secrets
Ignoring the from domain requirement The from address must use a domain onboarded to Email Service Verify the domain first, then send from anything@that-domain.com
Using email key in REST API from object REST API uses address not email for from object Use { "address": "...", "name": "..." } for REST, { "email": "...", "name": "..." } for Workers
Using replyTo in REST API REST API uses snake_case field names Use reply_to for REST API, replyTo for Workers binding

References

Read the reference that matches your situation. You don't need all of them.

Files (claude-codex-settings)
  • references
    • cli-and-mcp.md 3.9 KB
      # CLI, MCP, and Project Setup
      
      Manage Cloudflare Email Service from the command line and coding agents.
      
      For full CLI reference, run `npx wrangler email --help`. For Dashboard setup, see the [getting started docs](https://developers.cloudflare.com/email-service/get-started/).
      
      ## Wrangler Email Commands
      
      ```
      wrangler email routing
      ├── enable/disable   <domain>          # Toggle email routing
      ├── dns get          <domain>          # Show required DNS records
      ├── rules list/create/update/delete    # Manage routing rules
      └── addresses list/create/delete       # Destination addresses (account-scoped)
      
      wrangler email sending
      ├── enable/disable   <domain>          # Toggle email sending
      ├── dns get          <domain>          # Show sending DNS records (SPF, DKIM)
      ├── send             --from --to ...   # Send an email (builder flags)
      └── send-raw         --from --to ...   # Send a raw MIME email
      ```
      
      ## Domain Setup
      
      ### Via Dashboard
      
      1. Navigate to **Compute & AI** > **Email Service** > **Email Sending** (or **Email Routing**)
      2. Select **Onboard Domain** > choose domain > **Add records and onboard**
      
      This auto-adds SPF (TXT) and DKIM (CNAME/TXT) records. DNS usually propagates within 5-15 minutes.
      
      ### Via CLI
      
      ```bash
      npx wrangler email sending enable yourdomain.com
      npx wrangler email sending dns get yourdomain.com   # Verify records
      ```
      
      ## Local Development
      
      Add `"remote": true` to send real emails during `wrangler dev`:
      
      ```jsonc
      { "send_email": [{ "name": "EMAIL", "remote": true }] }
      ```
      
      ```bash
      npx wrangler dev
      ```
      
      Emails are actually sent — use test addresses you control. Remove `"remote": true` before deploying.
      
      ## Cloudflare MCP Server
      
      If you have the [Cloudflare MCP server](https://github.com/cloudflare/mcp) (`https://mcp.cloudflare.com/mcp`) configured, you can manage Email Service through its `search` and `execute` tools.
      
      Use `search` to find email sending endpoints:
      
      ```javascript
      // search tool — find all email sending API endpoints
      async () => {
        const results = [];
        for (const [path, methods] of Object.entries(spec.paths)) {
          if (path.includes('email/sending')) {
            for (const [method, op] of Object.entries(methods)) {
              results.push({ method: method.toUpperCase(), path, summary: op.summary });
            }
          }
        }
        return results;
      }
      ```
      
      Then use `execute` to call them — for example, checking sending limits or sending an email:
      
      ```javascript
      // execute tool — check sending quota
      async () => {
        return cloudflare.request({
          method: "GET",
          path: `/accounts/${accountId}/email/sending/limits`
        });
      }
      
      // execute tool — send an email
      async () => {
        return cloudflare.request({
          method: "POST",
          path: `/accounts/${accountId}/email/sending/send`,
          body: {
            to: "user@example.com",
            from: { address: "notifications@yourdomain.com", name: "My App" },
            subject: "Deployment Complete",
            html: "<h1>Deployed!</h1>",
            text: "Deployed!"
          }
        });
      }
      ```
      
      GraphQL analytics queries also work through `execute` — see [deliverability.md](deliverability.md#graphql-analytics-api) for query examples. Note that email analytics are **zone-level** datasets (`emailSendingAdaptiveGroups`, `emailSendingAdaptive`) queried under `viewer > zones`, and require the **Analytics Read** token permission.
      
      ## Sending from CLI / Agents
      
      ```bash
      npx wrangler email sending send \
        --from "agent@yourdomain.com" \
        --to "developer@company.com" \
        --subject "Deployment Complete" \
        --text "Your Worker was deployed successfully."
      ```
      
      Or via REST API:
      
      ```bash
      curl "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/email/sending/send" \
        --header "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
        --header "Content-Type: application/json" \
        --data '{
          "to": "developer@company.com",
          "from": {"address": "agent@yourdomain.com", "name": "Build Agent"},
          "subject": "Deployment Complete",
          "text": "Your Worker was deployed successfully."
        }'
      ```
      
    • deliverability.md 8.9 KB
      # Email Deliverability & Best Practices
      
      For full details, see the [deliverability docs](https://developers.cloudflare.com/email-service/concepts/deliverability/) and [email authentication docs](https://developers.cloudflare.com/email-service/concepts/email-authentication/). All the monitoring endpoints below can be called via the [REST API](rest-api.md), [Wrangler CLI, or the Cloudflare MCP server](cli-and-mcp.md).
      
      ## What Cloudflare Handles
      
      When you onboard a domain, Cloudflare auto-configures:
      
      - **SPF** — TXT records authorizing Cloudflare's sending infrastructure
      - **DKIM** — Records for cryptographic signing of outbound emails
      - **IP reputation** — Managed sending infrastructure optimized for deliverability
      - **Soft bounce retries** — Automatic exponential backoff for temporary failures
      - **Suppression lists** — Hard-bounced addresses automatically blocked
      - **Feedback loops** — ISP complaint signals processed and acted on
      
      Consider adding a **DMARC** record if you don't have one: `v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@yourdomain.com`
      
      ## Bounce Handling
      
      **Hard bounces** — permanent failures (address doesn't exist, domain doesn't exist). Never retried. Address auto-added to suppression list. Sending to suppressed address returns `E_RECIPIENT_SUPPRESSED`.
      
      **Soft bounces** — temporary failures (mailbox full, server down, greylisting). Cloudflare auto-retries with exponential backoff.
      
      ## Suppression Lists
      
      **Account list** (your account) — spam complaints from recipients. Cloudflare integrates with Postmasters to auto-suppress. You can manually add/remove addresses in the Dashboard.
      
      See the [suppressions docs](https://developers.cloudflare.com/email-service/concepts/suppressions/) for details.
      
      ## Your Responsibilities
      
      ### Content
      - Include both HTML and plain text versions
      - Use a recognizable sender name: `{ email: "noreply@app.com", name: "My App" }`
      - Write honest subject lines — avoid ALL CAPS, excessive punctuation
      - Include `List-Unsubscribe` headers for recurring emails
      - Use full URLs from your domain — avoid URL shorteners
      
      ### List Quality
      - Validate email addresses before sending
      - Implement double opt-in for subscriptions
      - Honor unsubscribe requests promptly
      
      ### Transactional Only
      Email Service is for **transactional email** (triggered by user actions: signups, password resets, order confirmations). Marketing/bulk campaigns are not permitted — use a dedicated marketing platform.
      
      ## Monitoring Deliverability
      
      ### Dashboard
      
      Per-domain and account-wide analytics are available in the Cloudflare dashboard:
      
      1. Log in to the [Cloudflare dashboard](https://dash.cloudflare.com) and select your account.
      2. Go to **Compute & AI** > **Email Service**.
      3. Select a domain or view account-wide metrics.
      4. Select the **Analytics** tab.
      
      ### Send Response
      
      Every send (REST API or Workers binding) returns immediate delivery feedback. Check the response to track per-send outcomes:
      
      ```json
      {
        "result": {
          "delivered": ["user@example.com"],
          "permanent_bounces": ["bad@nonexistent.com"],
          "queued": ["slow@recipient.com"]
        }
      }
      ```
      
      Log these to build your own delivery rate metrics.
      
      ### Sending Limits
      
      Check your account's daily sending quota:
      
      ```bash
      curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/limits" \
        --header "Authorization: Bearer <API_TOKEN>"
      ```
      
      Returns:
      
      ```json
      {
        "result": {
          "quota": { "value": 5000, "unit": "day" }
        }
      }
      ```
      
      ### Suppression List
      
      Addresses that hard-bounced or received spam complaints are auto-suppressed. You can query and manage suppressions via the API.
      
      **List suppressions (account-wide):**
      
      ```bash
      curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/suppression?page=1&per_page=100&order=created_at&direction=desc" \
        --header "Authorization: Bearer <API_TOKEN>"
      ```
      
      Returns:
      
      ```json
      {
        "page": 1,
        "per_page": 100,
        "total": 2,
        "result": [
          {
            "id": "396a5436-d4b0-42a6-b3fc-48e8fa522321",
            "email": "bounced@example.com",
            "reason": "hard_bounce",
            "created_at": "2026-03-15T10:00:00Z",
            "expires_at": null
          }
        ]
      }
      ```
      
      Query params: `page`, `per_page` (max 1000), `order` (`email` | `expires_at` | `created_at`), `direction` (`asc` | `desc`).
      
      **Manually suppress an address:**
      
      ```bash
      curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/suppression" \
        --header "Authorization: Bearer <API_TOKEN>" \
        --header "Content-Type: application/json" \
        --data '{ "email": "user@example.com", "expires_at": "2026-06-01T00:00:00Z" }'
      ```
      
      `expires_at` is optional — omit for permanent suppression.
      
      **Remove a suppression:**
      
      ```bash
      curl -X DELETE "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/suppression/{suppression_id}" \
        --header "Authorization: Bearer <API_TOKEN>"
      ```
      
      Zone-level suppressions are also available at `/zones/{zone_id}/email/sending/suppression` with the same interface.
      
      ### GraphQL Analytics API
      
      Email Service exposes two zone-level datasets via the [GraphQL Analytics API](https://developers.cloudflare.com/analytics/graphql-api/). You can explore the schema interactively at [graphql.cloudflare.com/explorer](https://graphql.cloudflare.com/explorer). Metrics are retained for 31 days.
      
      | Dataset | Description |
      |---------|-------------|
      | `emailSendingAdaptiveGroups` | Aggregated counts grouped by dimensions (status, date, domain, auth results, etc.) |
      | `emailSendingAdaptive` | Individual email events with full detail (from, to, subject, messageId, errors, etc.) |
      
      These are **zone-level** datasets — query under `viewer > zones`, not `accounts`.
      
      **Aggregated dimensions** (`emailSendingAdaptiveGroups`):
      
      | Dimension | Type | Description |
      |-----------|------|-------------|
      | `date` | Date | Day-level grouping |
      | `datetime` | Time | Exact timestamp (also: `datetimeMinute`, `datetimeFiveMinutes`, `datetimeFifteenMinutes`, `datetimeHour`) |
      | `status` | string | Delivery status |
      | `eventType` | string | Event type |
      | `sendingDomain` | string | The sending domain |
      | `envelopeTo` | string | Recipient address |
      | `errorCause` | string | Error cause for failed sends |
      | `arc`, `dkim`, `dmarc`, `spf` | string | Email authentication results |
      | `isSpam`, `isNDR`, `isLastEvent` | uint8 | Boolean flags |
      | `spamScore`, `spamThreshold` | uint32 | Spam scoring |
      
      **Individual event fields** (`emailSendingAdaptive`) additionally include: `from`, `to`, `subject`, `messageId`, `sessionId`, `errorDetail`.
      
      **Email counts by status and date:**
      
      ```graphql
      query EmailSendingByStatus(
        $zoneTag: string!
        $start: Date!
        $end: Date!
      ) {
        viewer {
          zones(filter: { zoneTag: $zoneTag }) {
            emailSendingAdaptiveGroups(
              filter: { date_geq: $start, date_leq: $end }
              limit: 10000
              orderBy: [date_DESC]
            ) {
              count
              dimensions {
                date
                status
              }
            }
          }
        }
      }
      ```
      
      **Filter by status (e.g. only failures):**
      
      ```graphql
      query EmailFailures(
        $zoneTag: string!
        $start: Date!
        $end: Date!
      ) {
        viewer {
          zones(filter: { zoneTag: $zoneTag }) {
            emailSendingAdaptiveGroups(
              filter: { date_geq: $start, date_leq: $end, status: "deliveryFailed" }
              limit: 10000
              orderBy: [date_DESC]
            ) {
              count
              dimensions {
                date
                errorCause
                sendingDomain
              }
            }
          }
        }
      }
      ```
      
      **Individual email events (troubleshooting):**
      
      ```graphql
      query RecentEmailEvents(
        $zoneTag: string!
        $start: Time!
        $end: Time!
      ) {
        viewer {
          zones(filter: { zoneTag: $zoneTag }) {
            emailSendingAdaptive(
              filter: { datetime_geq: $start, datetime_leq: $end }
              limit: 50
              orderBy: [datetime_DESC]
            ) {
              datetime
              from
              to
              subject
              status
              eventType
              sendingDomain
              messageId
              errorCause
              errorDetail
              dkim
              dmarc
              spf
              isSpam
            }
          }
        }
      }
      ```
      
      Note: `emailSendingAdaptive` filters use `datetime_geq`/`datetime_leq` (Time type, e.g. `"2026-04-01T00:00:00Z"`), while `emailSendingAdaptiveGroups` uses `date_geq`/`date_leq` (Date type, e.g. `"2026-04-01"`).
      
      **curl example:**
      
      ```bash
      curl "https://api.cloudflare.com/client/v4/graphql" \
        --header "Authorization: Bearer <API_TOKEN>" \
        --header "Content-Type: application/json" \
        --data '{
          "query": "query($zoneTag:string!,$start:Date!,$end:Date!){viewer{zones(filter:{zoneTag:$zoneTag}){emailSendingAdaptiveGroups(filter:{date_geq:$start,date_leq:$end},limit:10000,orderBy:[date_DESC]){count,dimensions{date,status}}}}}",
          "variables": {
            "zoneTag": "<ZONE_ID>",
            "start": "2026-03-15",
            "end": "2026-04-15"
          }
        }'
      ```
      
      ## Metrics to Watch
      
      | Metric | Target | If Out of Range |
      |--------|--------|-----------------|
      | Delivery rate | > 95% | Check for invalid addresses; verify DNS records |
      | Hard bounce rate | < 2% | Clean your email list |
      | Complaint rate | < 0.1% | Make unsubscribe easier; stop unwanted emails |
      
    • rest-api.md 1.8 KB
      # Sending Emails — REST API
      
      Use the REST API for HTTP integrations from external applications, or when the user explicitly requests it inside a Worker. Otherwise prefer the [Workers binding](sending.md).
      
      Read the relevant page before building the request. Keep credentials in the project's existing secret or environment-variable mechanism, and inspect the installed client SDK version if one is used.
      
      | Task | Read |
      |------|------|
      | Authenticate, send an email, or find the complete request schema | [REST API](https://developers.cloudflare.com/email-service/api/send-emails/rest-api/) and its linked Email Sending API reference |
      | Set multiple recipients, CC/BCC, or named addresses | [Specify recipients](https://developers.cloudflare.com/email-service/examples/email-sending/recipients/) (REST examples) |
      | Encode attachments | [REST attachments](https://developers.cloudflare.com/email-service/api/send-emails/rest-api/#attachments) |
      | Add custom headers | [Email headers](https://developers.cloudflare.com/email-service/reference/headers/) |
      | Check recipient, message-size, or sending quotas | [Limits](https://developers.cloudflare.com/email-service/platform/limits/) |
      | Interpret delivery outcomes | [REST response](https://developers.cloudflare.com/email-service/api/send-emails/rest-api/#response) |
      | Diagnose errors and decide whether to retry | [REST error handling](https://developers.cloudflare.com/email-service/api/send-emails/rest-api/#error-handling) |
      
      Do not reuse a Workers binding payload or response parser unchanged: verify field names, attachment encoding, response shape, and error handling for the chosen API. Validate both successful responses and failures through the project's existing checks; distinguish retryable service failures from requests that need correction.
      
    • routing.md 7.3 KB
      # 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](https://developers.cloudflare.com/email-service/api/route-emails/email-handler/).
      
      ## Email Handler
      
      Export an `email()` function from your Worker. No special wrangler binding needed — a routing rule connects incoming addresses to your Worker.
      
      ```typescript
      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
      
      ```typescript
      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
      
      ```typescript
      message.setReject("Your message was blocked");
      ```
      
      ### Reply
      
      Using `env.EMAIL.send()` (recommended — no extra dependencies):
      
      ```typescript
      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`):
      
      ```typescript
      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](https://www.npmjs.com/package/postal-mime) to parse raw MIME content:
      
      ```typescript
      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
      
      ```typescript
      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:
      
      ```typescript
      // 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).
      
    • sending.md 2.5 KB
      # Sending Emails — Workers Binding & Agents SDK
      
      Prefer the native binding for Workers. For an external application, or when the user explicitly requests HTTP integration, use the [REST API guide](rest-api.md).
      
      Read the documentation for the selected task before implementing. Inspect the project's installed Wrangler and Agents SDK versions, configuration, and existing conventions first. Run `wrangler types` through the project's package manager after changing bindings; use its generated types instead of handwritten email interfaces. See [Workers TypeScript](https://developers.cloudflare.com/workers/languages/typescript/) for matching types to the project's compatibility date and flags. Do not upgrade dependencies just to match an example.
      
      | Task | Read |
      |------|------|
      | Configure the binding, send a message, or maintain existing MIME-based sending | [Workers API](https://developers.cloudflare.com/email-service/api/send-emails/workers-api/) |
      | Restrict sender or destination addresses | [Configure send bindings](https://developers.cloudflare.com/email-service/configuration/send-bindings/) |
      | Set multiple recipients, CC/BCC, or named addresses | [Specify recipients](https://developers.cloudflare.com/email-service/examples/email-sending/recipients/) |
      | Add files, inline images, or uploaded attachments | [Email attachments](https://developers.cloudflare.com/email-service/examples/email-sending/email-attachments/) |
      | Set custom headers or diagnose header validation | [Email headers](https://developers.cloudflare.com/email-service/reference/headers/) |
      | Check recipient, message-size, or sending quotas | [Limits](https://developers.cloudflare.com/email-service/platform/limits/) |
      | Choose local simulation or remote delivery | [Local email sending](https://developers.cloudflare.com/email-service/local-development/sending/) |
      | Interpret send results and binding errors | [Workers API response and error handling](https://developers.cloudflare.com/email-service/api/send-emails/workers-api/#error-handling) |
      | Send, receive, route, and securely reply from an Agent | [Email agent walkthrough](https://developers.cloudflare.com/agents/examples/email-agent/) |
      
      When adapting REST code to a binding, verify address fields, attachment representation, response shape, and errors against the binding docs and generated types. Exercise the relevant success and failure paths using the project's existing checks. Confirm whether the chosen local configuration simulates delivery or sends real mail before testing.
      
  • SKILL.md 6.5 KB
    ---
    name: cloudflare-email-service
    description: Implement or troubleshoot Cloudflare Email Sending and Email Routing integrations and their delivery configuration.
    license: Apache-2.0
    ---
    
    # Cloudflare Email Service
    
    Your knowledge of the Cloudflare Email Service, Email Routing or Email Sending may be outdated. **Prefer retrieval over pre-training** for any Cloudflare Email Service task.
    
    Cloudflare Email Service lets you send transactional emails and route incoming emails, all within the Cloudflare platform. Your knowledge of this product may be outdated — it launched in 2025 and is evolving rapidly. **Prefer retrieval over pre-training** for any Email Service task.
    
    **If there is any discrepancy between this skill and the sources below, always trust the original source.** The Cloudflare docs, REST API spec, `@cloudflare/workers-types`, and Agents SDK repo are the source of truth. This skill is a convenience guide — it may lag behind the latest changes. When in doubt, retrieve from the sources below and use what they say.
    
    ## Retrieval Sources
    
    | Source | How to retrieve | Use for |
    |--------|----------------|---------|
    | Cloudflare docs | Cloudflare MCP `docs` tool or URL `https://developers.cloudflare.com/email-service/` | API reference, limits, pricing, latest features |
    | REST API spec | `https://developers.cloudflare.com/api/resources/email_sending` | OpenAPI spec for the Email Sending REST API |
    | Workers types | `https://www.npmjs.com/package/@cloudflare/workers-types` | Type signatures, binding shapes |
    | Agents SDK docs | [Email agent walkthrough](https://developers.cloudflare.com/agents/examples/email-agent/) | Email handling in Agents SDK |
    
    ## FIRST: Check Prerequisites
    
    Before writing any email code, verify the basics are in place:
    
    1. **Domain onboarded?** Run `npx wrangler email sending list` to see which domains have email sending enabled. If the domain isn't listed, run `npx wrangler email sending enable userdomain.com` or see [cli-and-mcp.md](references/cli-and-mcp.md) for full setup instructions.
    2. **Binding configured?** Look for `send_email` in `wrangler.jsonc` (for Workers)
    3. **postal-mime installed?** Run `npm ls postal-mime` (only needed for receiving/parsing emails)
    
    ## What Do You Need?
    
    Start here. Find your situation, then follow the link for full details.
    
    | I want to... | Path | Reference |
    |--------------|------|-----------|
    | **Send emails from a Cloudflare Worker** | Workers binding (no API keys needed) | [sending.md](references/sending.md) |
    | **Send emails from an AI agent built with [Cloudflare Agents SDK](https://developers.cloudflare.com/agents/)** | `onEmail()` + `replyToEmail()` in Agent class | [sending.md](references/sending.md) |
    | **Send emails from an external app or agent** (Node.js, Go, Python, etc.) | REST API with Bearer token | [rest-api.md](references/rest-api.md) |
    | **Send emails from a coding agent** (Claude Code, Cursor, Copilot, etc.) | MCP tools, wrangler CLI, or REST API | [cli-and-mcp.md](references/cli-and-mcp.md) |
    | **Receive and process incoming emails** (Email Routing) | Workers `email()` handler | [routing.md](references/routing.md) |
    | **Set up Email Sending or Email Routing** | `wrangler email sending enable` / `wrangler email routing enable`, or Dashboard | [cli-and-mcp.md](references/cli-and-mcp.md) |
    | **Improve deliverability, avoid spam folders** | Authentication, content, compliance | [deliverability.md](references/deliverability.md) |
    
    ## Sending Workflow
    
    Prefer the binding for Workers; use REST for external apps or when explicitly requested. Read [sending.md](references/sending.md) or [rest-api.md](references/rest-api.md) to retrieve the documentation for the selected task before writing code. These guides cover setup, recipients, attachments, headers, limits, response handling, and errors; the Workers guide also covers Agents SDK integration and types matched to the project configuration.
    
    ## Common Mistakes
    
    | Mistake | Why It Happens | Fix |
    |---------|---------------|-----|
    | Forgetting `send_email` binding in wrangler config | Email Service uses a binding, not an API key | Add `"send_email": [{ "name": "EMAIL" }]` to wrangler.jsonc |
    | Sending from an unverified domain | Domain must be onboarded onto Email Sending before first send | Run `wrangler email sending enable yourdomain.com` or onboard in Dashboard |
    | Reading `message.raw` twice in email handler | The raw stream is single-use — second read returns empty | Buffer first: `const raw = await new Response(message.raw).arrayBuffer()` |
    | Missing `text` field (HTML only) | Some email clients only show plain text; also helps spam scores | Always include both `html` and `text` versions |
    | Using email for marketing/bulk sends | Email Service is for transactional email only | Use a dedicated marketing email platform for newsletters and campaigns |
    | Forwarding to unverified destinations | `message.forward()` only works with verified addresses | Run `wrangler email routing addresses create user@gmail.com` or add in Dashboard |
    | Testing with fake addresses | Bounces from non-existent addresses hurt sender reputation | Use real addresses you control during development |
    | Hardcoding API tokens in source code | Tokens in code get committed and leaked | Use environment variables or Cloudflare secrets |
    | Ignoring the `from` domain requirement | The `from` address must use a domain onboarded to Email Service | Verify the domain first, then send from `anything@that-domain.com` |
    | Using `email` key in REST API `from` object | REST API uses `address` not `email` for `from` object | Use `{ "address": "...", "name": "..." }` for REST, `{ "email": "...", "name": "..." }` for Workers |
    | Using `replyTo` in REST API | REST API uses snake_case field names | Use `reply_to` for REST API, `replyTo` for Workers binding |
    
    ## References
    
    Read the reference that matches your situation. You don't need all of them.
    
    - **[references/sending.md](references/sending.md)** — Documentation map for Workers binding, attachments, and Agents SDK email.
    - **[references/rest-api.md](references/rest-api.md)** — Documentation map for HTTP sending, request schemas, responses, and errors.
    - **[references/routing.md](references/routing.md)** — Inbound `email()` handler, forwarding, replying, parsing. For receiving emails.
    - **[references/cli-and-mcp.md](references/cli-and-mcp.md)** — Domain setup, wrangler commands, MCP tools. For first-time setup.
    - **[references/deliverability.md](references/deliverability.md)** — SPF/DKIM/DMARC, bounces, suppressions, best practices.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related