Back to article index
TRANSMISSION / CYAN22 min read

Sending and Receiving Email with Cloudflare Email Service—from Routing to Workers, API, and SMTP

A practical July 2026 guide to receiving, forwarding, parsing, replying to, and sending transactional email with Cloudflare Email Service, including Nuxt, Workers, D1, security, and rate limiting.

#Cloudflare#Email#Workers#Serverless

Video explainer / 16:9

Cloudflare Email Service in Practice—an 11-minute Configuration Guide

From DNS, Email Routing, Email Workers, and the send_email binding to REST, SMTP, Nuxt, D1, five-minute verification codes, and production checks.

11:01Chinese narration12 chapters

Press play for the full explainer. Seeking, picture-in-picture, and fullscreen are supported.

For a long time, Cloudflare’s relationship with email was mostly about two things: managing DNS and using Email Routing to forward support@example.com to a real mailbox.

In 2026, that boundary moved considerably.

In April, Cloudflare moved Email Sending into public beta, allowing Workers to call env.EMAIL.send() directly for transactional email. Authenticated SMTP followed in June. Combined with Email Routing and Email Workers, Cloudflare Email Service can now:

  • Receive mail sent to your own domain.
  • Forward it to a verified mailbox.
  • Hand incoming mail to a Worker for parsing, filtering, storage, or automation.
  • Reply within the original email thread.
  • Send transactional mail from a Worker, REST API, or SMTP client.
  • Expose sending logs, bounces, suppressions, and delivery metrics.

This article reflects the official platform as of July 28, 2026. It explains the complete sending and receiving model and proposes an implementation for projects built on Nuxt, Workers, and D1.

The short conclusion:

Cloudflare Email Service is not a conventional IMAP / POP3 mailbox, and it is not a webmail-based enterprise suite. It is better understood as a set of email entry points, sending channels, and programmable events for verification codes, password resets, order notifications, system alerts, support tickets, and email-driven automation.

Separate receiving from sending first

Sending and receiving look like one feature, but they are two independent paths inside Cloudflare.

DirectionCloudflare productEntry pointWhat a Worker can do
ReceiveEmail Routing / Email WorkersAn external mail server connects to Cloudflare MXForward, reply, reject, parse, write to D1/R2, or enqueue work
SendEmail SendingWorkers binding, REST API, or authenticated SMTPSend text/HTML, attachments, and custom headers, then receive a messageId

The receiving path is roughly:

Sender SMTP
  → Cloudflare MX
  → SPF / DKIM / DMARC and reputation checks
  → address rule or Catch-all match
  → mailbox forwarding / Worker / drop

The sending path is:

Nuxt API / Worker / external application
  → EMAIL binding / REST API / SMTP
  → sender-domain and parameter validation
  → Cloudflare delivery pipeline
  → recipient mail server

Email is checked at the edge and then routed to a real mailbox, Worker processing node, or rejection path

This is a conceptual diagram. Real decisions also include the SMTP session, email authentication, rule matching, and Worker execution limits.

Product boundaries and pricing in 2026

Cloudflare currently groups receiving and sending under Email Service, but the two parts have different plan availability.

Email Routing: receiving and forwarding

Email Routing is available on both Workers Free and Workers Paid. The official pricing page lists unlimited inbound email, although messages processed by a Worker still consume the corresponding Worker requests and compute.

It works well for:

  • Forwarding support@yourdomain.com to an existing personal mailbox.
  • Creating a separate contact address for each site.
  • Using sub-addresses such as orders+shop-a@yourdomain.com to identify a source.
  • Turning incoming mail into a ticket, webhook, or database record with a Worker.

Email Sending: transactional email

Email Sending remains in public beta.

Sending to arbitrary recipients requires the Workers Paid plan. According to the official July 2026 pricing:

  • Each account includes 3,000 outbound messages per month.
  • Additional messages cost $0.35 per 1,000.
  • Messages sent to “verified destination addresses” in the account are always free and do not use the included allowance.
  • A message accepted by Email Service normally counts even if it later hard-bounces; a request rejected at the API boundary does not.

Pricing and beta status can change. Check the official pricing page again before production instead of permanently encoding these numbers into a budget model.

Step one: connect the domain correctly

Email Service requires the domain to use Cloudflare DNS. Sending and receiving create two sets of records for different purposes.

Receiving uses root-domain records

When Email Routing is enabled, Cloudflare configures the domain root with:

  • MX records that deliver incoming mail to Cloudflare.
  • SPF authorization for Cloudflare forwarding.
  • DKIM signatures for the forwarding path.

These records cover “someone sends mail to your domain.”

Sending uses cf-bounce records

When Email Sending is enabled, Cloudflare configures cf-bounce.yourdomain.com with:

  • MX records for bounce processing.
  • SPF authorization for Cloudflare to send on behalf of the domain.
  • DKIM verification that the message was not modified in transit.
  • _dmarc.yourdomain.com, which declares how authentication failures should be handled.

Sending and receiving use different DKIM selectors. Seeing two DKIM records does not mean the setup is duplicated.

If the domain already uses Google Workspace, Microsoft 365, or another mail service, check MX and SPF conflicts carefully. A domain can have only one valid SPF policy. To authorize several providers, merge their include values instead of creating a second v=spf1 record.

I recommend separating reputation-sensitive purposes across subdomains:

noreply@example.com          account verification and password resets
notify@mail.example.com      product notifications
support@example.com          human support and incoming mail

Transactional notifications, human correspondence, and possible future bulk mail will not damage one another’s reputation, and failure and migration boundaries become clearer.

Receiving mail: from forwarding rules to an Email Worker

The simplest way to receive mail requires no code:

  1. Add a verified destination mailbox in Email Routing.
  2. Create support@yourdomain.com.
  3. Select “Send to an email.”
  4. Cloudflare forwards incoming messages to the destination.

If you need to classify by subject, extract attachments, create tickets, or send automatic replies, change the action to “Send to a Worker.”

email() is the inbound entry point

An Email Worker enters through email(), not fetch():

interface Env {
  SUPPORT_FORWARD_TO: string
}

export default {
  async email(message, env): Promise<void> {
    const recipient = message.to.toLowerCase()
    const subject = message.headers.get('subject') ?? ''

    if (message.rawSize > 10 * 1024 * 1024) {
      message.setReject('Message is too large for this mailbox')
      return
    }

    if (recipient === 'support@example.com') {
      console.log('Support email accepted', {
        fromDomain: message.from.split('@').at(-1),
        subjectLength: subject.length,
        rawSize: message.rawSize,
      })

      await message.forward(env.SUPPORT_FORWARD_TO)
      return
    }

    message.setReject('Unknown recipient')
  },
} satisfies ExportedHandler<Env>

Here, message is a ForwardableEmailMessage. Common fields and operations include:

CapabilityPurpose
message.fromSender in the SMTP envelope
message.toRecipient in the SMTP envelope
message.headersRead Subject, Message-ID, and other headers
message.rawRaw MIME stream
message.rawSizeSize of the raw message
message.forward(address)Forward to a verified destination address
message.reply(email)Reply to the original sender within platform constraints
message.setReject(reason)Return a permanent SMTP rejection

There is an easy-to-miss security detail: message.from and message.to come from the SMTP envelope, while From: and To: inside the message headers can contain different values. For authorization, routing, and auditing, do not mistake a user-visible header address for a trusted identity.

Parse MIME only when you need the body

There is no reason to fully parse a message if you only forward it or route it by address. To extract the body and attachments, use postal-mime:

import PostalMime from 'postal-mime'

export default {
  async email(message): Promise<void> {
    const parsed = await PostalMime.parse(message.raw)

    console.log({
      subject: parsed.subject,
      textLength: parsed.text?.length ?? 0,
      attachmentCount: parsed.attachments.length,
    })
  },
} satisfies ExportedHandler

Parsed HTML, filenames, Content-Type values, and attachments all come from external input. Safe handling means:

  • Do not inject incoming HTML directly into an administration page.
  • Limit body size, attachment count, and expanded size.
  • Generate a new object key before writing a file to R2; do not use the original filename as the path.
  • Never let message content directly construct SQL, shell commands, templates, or AI tool parameters.
  • Send expensive work to a Queue instead of putting malware scanning, OCR, and model calls into the synchronous CPU time of an inbound event.

Forwarding, replying, rejecting, and silently dropping are different

  • Forward: continue handling in a human mailbox; the destination must already be verified.
  • Reply: useful for confirmations or mail bots, with additional restrictions.
  • Reject: explicitly tell the sender during SMTP that the message was not accepted.
  • Drop: stop processing after a rule match without business-level feedback to the sender.

An automatic reply is not an arbitrary sending interface. The current platform requires:

  • A valid DMARC result on the incoming message.
  • Only one reply call per inbound event.
  • The reply recipient to be the original sender.
  • The reply sender domain to match the domain that received the message.
  • Fewer than 100 entries in References, preventing mail loops.

These restrictions are helpful: “reply to this message” and “send a new message to any address” become two different permissions.

Sending mail: prefer a Workers binding

If the application already runs on Cloudflare Workers, the Workers binding is the most natural option. It does not require a Cloudflare API token in business code.

Declare the binding in wrangler.jsonc:

{
  "send_email": [
    {
      "name": "EMAIL",
      "allowed_sender_addresses": [
        "noreply@example.com"
      ]
    }
  ],
  "vars": {
    "EMAIL_FROM": "noreply@example.com"
  }
}

allowed_sender_addresses is not merely documentation; it is a runtime permission boundary. Even if an endpoint is misused, the binding cannot impersonate another sender address.

The following settings can constrain recipients further:

  • destination_address: fix the binding to one destination.
  • allowed_destination_addresses: allow only a set of destinations.
  • allowed_sender_addresses: restrict available sender addresses.

A Worker used for system alerts is a good fit for a fixed destination_address. A user-verification service normally restricts the sender and validates recipient and frequency in the application.

Sending inside a Worker

interface Env {
  EMAIL: SendEmail
}

export default {
  async fetch(_request, env): Promise<Response> {
    const result = await env.EMAIL.send({
      from: {
        email: 'noreply@example.com',
        name: 'Example App',
      },
      to: 'reader@example.net',
      subject: 'Verify your email',
      text: 'Your verification code is 482913. It is valid for 5 minutes.',
      html: '<p>Your verification code is <strong>482913</strong>. It is valid for 5 minutes.</p>',
    })

    return Response.json({
      messageId: result.messageId,
    })
  },
} satisfies ExportedHandler<Env>

A successful Workers binding call returns a messageId. It means Cloudflare accepted the sending request; it does not prove the message is already visible in the user’s inbox. Final delivery, delay, bounce, and suppression status belong in Email Service logs and metrics.

Sending from a Nuxt server API

Once Nuxt is deployed to Workers, a server route can read the same binding:

import { z } from 'zod'

const schema = z.object({
  email: z.string().trim().email().max(254),
})

export default defineEventHandler(async (event) => {
  const { email } = schema.parse(await readBody(event))
  const env = event.context.cloudflare.env as {
    EMAIL: SendEmail
    EMAIL_FROM: string
  }

  // A real project should also validate same-origin requests,
  // rate-limit IPs and email addresses, and atomically claim
  // one sending opportunity in D1.
  const result = await env.EMAIL.send({
    from: {
      email: env.EMAIL_FROM,
      name: 'Example App',
    },
    to: email,
    subject: 'Verify your email',
    text: 'The verification code will expire in 5 minutes.',
    html: '<p>The verification code will expire in <strong>5 minutes</strong>.</p>',
  })

  return {
    ok: true,
    messageId: result.messageId,
  }
})

Do not turn this sample into an anonymous public API with unlimited frequency and freely supplied subjects and bodies. That would turn your domain into a spam relay.

A Worker creates transactional email that passes through several layers of authentication before delivery to different recipient systems

SPF, DKIM, and DMARC answer who may send, whether content was modified, and what to do on failure. They do not guarantee that every message reaches the primary inbox.

When are REST API and authenticated SMTP a better fit?

Cloudflare currently offers three sending entry points:

MethodBest fitCredentials and boundaries
Workers bindingNuxt, Hono, or an agent already running on WorkersNo API token in code; sender and recipient restrictions can be enforced by the binding
REST APIPrograms running in another cloud, CI system, or backend platformUses a least-privilege API token; responses distinguish delivered, queued, and permanent bounce per recipient
Authenticated SMTPLegacy systems already using Nodemailer, PHPMailer, JavaMail, or another SMTP clientsmtp.mx.cloudflare.net:465, implicit TLS only; the password is an API token with Email Sending permission

Authenticated SMTP was added as a beta feature in June 2026. It currently supports only SMTPS on port 465. There is no STARTTLS on port 587 and no anonymous relay on port 25.

If an SMTP token leaks, its holder can send using the sender domains connected to the account. Therefore:

  • Prefer an account-owned token.
  • Grant only Email Sending: Edit.
  • Never put it in a repository, image, or frontend environment variable.
  • Give each system a separate token so it can be revoked independently.
  • If a Worker binding is available, do not introduce a long-lived credential merely because SMTP feels familiar.

Limits you need to know

As of July 2026, these platform limits deserve explicit application-side defenses:

LimitCurrent valueImplementation effect
Email Routing rules per domain200A multi-tenant system should not create one static rule per user
Verified destination addresses per account200Plan account-level destinations before forwarding to many mailboxes
Inbound message size25 MiBLarger messages are rejected during SMTP
Recipients per outbound message50Combined To, Cc, and Bcc
Standard outbound message size5 MiBIncludes attachments and MIME encoding overhead
Message size to a verified destination25 MiBOnly for verified destination addresses
Total custom-header size16 KBDo not put business data into headers
Sending/routing domains per Zone30Root domains and subdomains both count
References entries in a reply100reply() fails above the limit

Attachments are wrapped in Base64 and MIME. A raw 3 MiB attachment does not mean the final email is still 3 MiB, so leave room for encoding, body content, and headers.

The platform can also adjust daily sending allowances based on account reputation, delivery performance, and usage history. Do not assume that unused monthly allowance means unlimited concurrency at any given second.

How verification codes and password resets should work

This blog sends registration and password-reset messages through the following path:

User submits email
  → same-origin check
  → IP and email rate limits
  → generate a six-digit code
  → D1 stores only salted hash, purpose, expiry, and attempt count
  → EMAIL binding sends the message
  → successful verification atomically marks the code as used

The important rules are:

  • The same email address can receive only one message every five minutes.
  • A verification code expires after five minutes.
  • A code can be used only once.
  • D1 never stores a plaintext code.
  • Verification stops after a threshold of consecutive incorrect attempts.
  • If sending fails, release the claimed slot so an undelivered code does not lock the user out.
  • Resetting a password revokes existing login sessions instead of only replacing the password hash.

The rate limit must atomically claim eligibility in the database. Do not query the previous sending time and then insert a row. Two concurrent requests can both pass that check. A unique key combined with INSERT ... ON CONFLICT ... WHERE lets the database decide which request receives the sending slot.

The email API response should not reveal whether an address is registered, or an attacker can enumerate users in bulk.

A more complete production architecture

Once email moves from a demo into production, separate the responsibilities:

HTTP request / business event
  → authentication, origin check, validation, rate limit, idempotency
  → Queue
  → email template rendering
  → EMAIL binding
  → write messageId / error code to D1
  → Email Service logs and alerts

The inbound path can be:

support@yourdomain.com
  → Email Routing
  → Email Worker
  → authentication and size checks
  → MIME parsing
  → save attachments to R2 / create ticket in D1
  → Queue for expensive processing
  → forward to a human mailbox or send a constrained reply

What should D1 record?

Useful fields include:

  • Business event ID and idempotency key.
  • Template ID and template version.
  • messageId.
  • Recipient count, without necessarily storing every complete address.
  • Application-side status such as accepted or failed.
  • Cloudflare error code.
  • Creation, sending, and last-updated timestamps.

Avoid storing by default:

  • Plaintext verification codes.
  • Password-reset tokens.
  • Complete email bodies.
  • Attachment contents.
  • SMTP/API tokens.
  • Raw inbound messages retained indefinitely for convenient debugging.

Logs can become a new source of private data themselves.

Do not treat “Dropped” in Email Routing as an outbound failure

The official documentation specifically notes that messages sent through the send_email binding can appear as dropped on the Email Routing summary page even when they were delivered successfully.

Inspect Email Sending Metrics and Logs for outbound results and correlate application logs with the messageId. The Routing page describes inbound routing behavior, not the complete outbound delivery state machine.

Testing locally

Wrangler supports local Email Worker events. Start it with:

pnpm exec wrangler dev

Then send a raw message to the local handler:

curl -X POST \
  'http://localhost:8787/cdn-cgi/handler/email?from=sender@example.net&to=support@example.com' \
  -H 'Content-Type: text/plain' \
  --data-binary $'From: sender@example.net\nTo: support@example.com\nSubject: Local test\n\nHello from local development.'

The local sending binding stores text and HTML as inspectable files, which is useful for template validation. Local simulation still has separate limitations for binary attachments, so attachment flows need an additional remote test.

Cover at least:

  • Normal text and HTML.
  • Non-ASCII subjects and sender display names.
  • Duplicate requests and idempotency.
  • Repeated verification-code requests before the cooldown expires.
  • Large body and attachment boundaries.
  • Header newline injection.
  • Unverified sender domains or restricted recipients.
  • Bounce, suppression, allowance, and frequency errors.
  • Unmatched inbound addresses, Catch-all, and rejection paths.
  • Scripts, remote resources, and dangerous attachment names in HTML mail.

What is it suitable for—and what is it not?

Good fits:

  • Registration verification, magic links, and password resets.
  • Order, billing, deployment, and monitoring notifications.
  • Personal-domain email forwarding.
  • Contact forms and support tickets.
  • Email-triggered agents or automation.
  • Cloudflare applications that want to avoid maintaining an SMTP server.

Poor direct fits:

  • Marketing blasts without consent.
  • Newsletters without unsubscribe, complaint, and suppression management.
  • An anonymous public “send email for me” endpoint.
  • Enterprise mail that needs IMAP folders, calendars, contacts, and shared-mailbox collaboration.
  • Passing arbitrary incoming message bodies directly to an agent with high-privilege tools.

Cloudflare maintains the email entry and delivery infrastructure, but sender reputation, user consent, content safety, frequency controls, and data minimization remain the application’s responsibility.

Final assessment

The most valuable part of Cloudflare Email Service is not that it is “another SMTP provider.” It puts email inside the same runtime boundary as Workers, D1, R2, Queues, and Agents.

Incoming mail no longer has to be forwarded only to a human mailbox; it can become a constrained event entry point. Outbound mail no longer requires a separate third-party SDK and credential system just to send a verification code.

If the project already runs on Workers, begin with this minimal path:

  1. Receive support@ through Email Routing.
  2. Send verification and password-reset messages through a Worker binding.
  3. Use D1 for cooldowns, one-time verification, and minimal logs.
  4. Restrict binding permissions with a sender allowlist.
  5. Verify delivery in Email Sending logs instead of trusting only a successful API response.

Introduce the REST API only when an external platform truly needs to send mail. Use authenticated SMTP only when an existing system already depends deeply on an SMTP client.

References

End of signal
READER CHANNEL

Comments

00
No comments yet. Be the first to respond.