返回文章索引
TRANSMISSION / CYAN22 MIN READ

Cloudflare Email Service 收发实战:从 Email Routing 到 Workers、API 与 SMTP

截至 2026 年 7 月,系统介绍 Cloudflare Email Service 如何接收、转发、解析、回复和发送事务邮件,并给出 Nuxt、Workers、D1 与安全限流实践。

#Cloudflare#Email#Workers#Serverless
邮件经过全球边缘网络中的 Worker 节点完成接收、认证与发送
Cloudflare Email Service 正在把邮件从一台需要维护的服务器,变成一种可以由代码处理的事件

很长一段时间里,Cloudflare 与邮件的关系主要停留在两件事上:管理 DNS,以及用 Email Routing 把 support@example.com 转发到一个真实邮箱。

2026 年,这个边界明显向前移动了。

Cloudflare 在 4 月把 Email Sending 推进公开 Beta,让 Workers 可以直接调用 env.EMAIL.send() 发送事务邮件;6 月又加入了认证 SMTP。加上原有的 Email Routing 与 Email Workers,现在一套 Cloudflare Email Service 已经可以覆盖:

  • 接收发往自有域名的邮件;
  • 转发到经过验证的真实邮箱;
  • 把来信交给 Worker 解析、过滤、入库或触发自动化;
  • 在原邮件线程中回复;
  • 从 Worker、REST API 或 SMTP 发送事务邮件;
  • 查看发送日志、退信、抑制与投递指标。

这篇文章以 2026 年 7 月 28 日的官方能力为准,梳理 Cloudflare 邮件收发的完整模型,并给出一套适合 Nuxt、Workers 与 D1 项目的实现思路。

先说结论:

Cloudflare Email Service 不是传统的 IMAP / POP3 邮箱,也不是让你在网页里收信的企业邮箱套件。它更像一组“邮件入口、发送通道和可编程事件”,适合验证码、密码重置、订单通知、系统告警、支持工单和邮件驱动的自动化。

先把“接收”和“发送”分开

邮件收发看起来是一件事,在 Cloudflare 里却是两条独立链路。

方向Cloudflare 产品入口Worker 能做什么
接收Email Routing / Email Workers外部邮件服务器连接 Cloudflare MX转发、回复、拒收、解析、写入 D1/R2、投递 Queue
发送Email SendingWorkers binding、REST API、认证 SMTP发送文本/HTML、附件、自定义头,获得 messageId

接收链路大致是:

发送方 SMTP
  → Cloudflare MX
  → SPF / DKIM / DMARC 与信誉检查
  → 匹配地址规则或 Catch-all
  → 转发到邮箱 / 交给 Worker / 丢弃

发送链路则是:

Nuxt API / Worker / 外部应用
  → EMAIL binding / REST API / SMTP
  → 发件域与参数校验
  → Cloudflare 投递管线
  → 收件方邮件服务器

邮件在边缘入口完成检查后,被分发到真实邮箱、Worker 处理节点或拒绝路径

这是一张概念图。真实判断还包含 SMTP 会话、邮件认证、规则匹配和 Worker 执行限制。

2026 年的产品边界与价格

Cloudflare 当前把接收与发送放在同一个 Email Service 下,但两部分的可用计划不同。

Email Routing:接收与转发

Email Routing 在 Workers Free 和 Workers Paid 都可以使用。官方价格页把入站邮件数量标为不限量,但如果邮件被交给 Worker,仍然会消耗对应的 Worker 请求与计算资源。

它适合:

  • support@yourdomain.com 转发到现有个人邮箱;
  • 为不同网站创建独立的联系地址;
  • orders+shop-a@yourdomain.com 这样的子地址区分来源;
  • 把来信交给 Worker 转换成工单、Webhook 或数据库记录。

Email Sending:事务邮件

Email Sending 当前仍是公开 Beta。

向任意收件人发送需要 Workers Paid 计划。按照 2026 年 7 月的官方价格:

  • 每个账户每月包含 3,000 封出站邮件;
  • 超出后每 1,000 封为 0.35 美元;
  • 发往账户内“已验证目标地址”的邮件始终免费,不计入包含额度;
  • 邮件被 Email Service 接受后,即使随后发生硬退信,通常也会计入额度;在 API 边界直接拒绝的请求不会计入。

价格和 Beta 状态都可能继续调整,真正上线前应重新检查官方价格页,而不是把本文数字长期写死在预算模型里。

第一步:正确接入域名

Email Service 要求域名使用 Cloudflare DNS。发送与接收会创建两组用途不同的 DNS 记录。

接收邮件使用根域记录

启用 Email Routing 后,Cloudflare 会在域名根节点配置:

  • MX:把来信送到 Cloudflare 的邮件入口;
  • SPF:授权 Cloudflare 转发邮件;
  • DKIM:为转发链路提供签名。

这些记录负责的是“别人发到你的域名”。

发送邮件使用 cf-bounce 记录

启用 Email Sending 时,Cloudflare 会为 cf-bounce.yourdomain.com 配置:

  • MX:处理退信;
  • SPF:授权 Cloudflare 代域名发送;
  • DKIM:验证邮件内容未在途中被修改;
  • _dmarc.yourdomain.com:声明认证失败时的处理策略。

发送和接收使用不同的 DKIM selector。看到两条 DKIM 记录并不意味着重复配置。

如果域名已有 Google Workspace、Microsoft 365 或其他邮件服务,尤其要检查 MX 和 SPF 是否冲突。一个域名只能有一条有效 SPF 策略;需要同时授权多个服务时,应合并 include,而不是再创建第二条 v=spf1

我的建议是把不同信誉用途拆到子域:

noreply@example.com          账户验证码与密码重置
notify@mail.example.com      产品通知
support@example.com          人工支持与来信处理

事务通知、人工往来与未来可能出现的批量邮件互不拖累,故障和迁移边界也更清晰。

接收邮件:从转发规则到 Email Worker

最简单的接收方式完全不用代码:

  1. 在 Email Routing 中添加一个经过验证的目标邮箱;
  2. 创建 support@yourdomain.com
  3. 选择“发送到邮箱”;
  4. 来信会被 Cloudflare 转发到目标地址。

如果需要按主题分类、提取附件、建立工单或自动回复,就把动作改为“发送到 Worker”。

email() 是入站邮件入口

Email Worker 的入口不是 fetch(),而是 email()

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>

其中 messageForwardableEmailMessage,常用字段和操作包括:

能力用途
message.fromSMTP envelope 的发件人
message.toSMTP envelope 的收件人
message.headers读取 Subject、Message-ID 等邮件头
message.raw原始 MIME 数据流
message.rawSize原始邮件大小
message.forward(address)转发到已验证目标地址
message.reply(email)在约束范围内回复原发件人
message.setReject(reason)返回永久 SMTP 拒收

这里有一个容易忽略的安全点:message.from / message.to 是 SMTP envelope,邮件正文头部中的 From: / To: 则可能是另一组值。做授权、路由与审计时,不要把显示给用户看的头部地址误当成可信身份。

需要正文时再解析 MIME

只做转发或按地址路由时,没有必要完整解析邮件。要提取正文和附件,可以使用 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

解析后的 HTML、文件名、Content-Type 与附件都来自外部输入。正确做法是:

  • 不在管理页面直接插入来信 HTML;
  • 限制正文、附件数量和解压后大小;
  • 文件写入 R2 前重新生成对象键,不使用原始文件名作为路径;
  • 不让来信内容直接组成 SQL、Shell、模板或 AI 工具参数;
  • 大任务交给 Queue,不要把杀毒、OCR、模型调用全塞进入站事件的同步 CPU 时间。

转发、回复、拒绝与静默丢弃的区别

  • 转发:适合人工邮箱继续处理,目标地址必须提前验证;
  • 回复:适合自动确认或邮件机器人,但有额外限制;
  • 拒绝:在 SMTP 阶段明确告诉发送方没有接收;
  • 丢弃:规则匹配后不继续处理,也不给发送方业务层反馈。

自动回复不能被当成任意发送接口。官方当前要求:

  • 来信必须有有效 DMARC 结果;
  • 每个入站事件只能调用一次回复;
  • 回复收件人必须是原始来信的发件人;
  • 回复使用的发件域必须与接收邮件的域一致;
  • References 超过 100 项时会拒绝回复,以避免邮件环路。

这组限制反而是一件好事:它让“回复这封邮件”和“向任意地址发一封新邮件”成为两种不同权限。

发送邮件:优先使用 Workers binding

如果应用本身运行在 Cloudflare Workers,Workers binding 是最自然的选择。它不需要在业务代码里保存 Cloudflare API Token。

先在 wrangler.jsonc 中声明绑定:

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

allowed_sender_addresses 不只是文档,它是运行时权限边界。即使某个接口被误用,这个绑定也不能伪造其他发件地址。

还可以用以下配置进一步约束收件人:

  • destination_address:固定为一个目标地址;
  • allowed_destination_addresses:只允许一组目标地址;
  • allowed_sender_addresses:限制可使用的发件地址。

用于系统告警的 Worker 最适合固定 destination_address;用于用户验证码的服务则通常限制发件人,并在应用层校验收件人和频率。

在 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: '验证你的邮箱',
      text: '你的验证码是 482913,5 分钟内有效。',
      html: '<p>你的验证码是 <strong>482913</strong>,5 分钟内有效。</p>',
    })

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

Workers binding 成功时返回一个 messageId。这个结果代表 Cloudflare 接受了发送请求,不应该被解释为用户一定已经在收件箱里看到邮件。最终投递、延迟、退信和抑制状态还要结合 Email Service 日志与指标。

在 Nuxt 服务端 API 中发送

Nuxt 部署到 Workers 后,同样可以从服务端路由读取 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
  }

  // 实际项目还应在这里执行同源校验、IP/邮箱限流,
  // 并在 D1 中原子领取一次发送资格。
  const result = await env.EMAIL.send({
    from: {
      email: env.EMAIL_FROM,
      name: 'Example App',
    },
    to: email,
    subject: '验证邮箱',
    text: '验证码将在 5 分钟后失效。',
    html: '<p>验证码将在 <strong>5 分钟</strong>后失效。</p>',
  })

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

不要把这个示例直接变成一个匿名、无限频率、可自由填写主题和正文的公开 API。那会把自己的域名变成垃圾邮件跳板。

事务邮件从 Worker 生成,经过多层身份认证后投递到不同收件系统

SPF、DKIM 与 DMARC 解决的是“谁有权发送、内容是否被修改、失败时如何处置”,并不保证每封邮件都进入主收件箱。

REST API 与认证 SMTP 什么时候更合适

Cloudflare 目前提供三种发送入口:

方式最适合凭证与边界
Workers binding已运行在 Workers 的 Nuxt、Hono、Agent不在代码中保存 API Token,可通过 binding 限制发件人与收件人
REST API运行在其他云、CI 或后端平台的程序使用最小权限 API Token,响应会按收件人区分 delivered、queued、permanent bounce
认证 SMTP已经使用 Nodemailer、PHPMailer、JavaMail 等 SMTP 客户端的旧系统smtp.mx.cloudflare.net:465,仅隐式 TLS,密码是有 Email Sending 权限的 API Token

认证 SMTP 是 2026 年 6 月加入的 Beta 能力。当前只支持 465 端口的 SMTPS,不提供 587 端口 STARTTLS,也不是可以匿名使用的 25 端口 relay。

SMTP Token 一旦泄漏,持有者可以使用账户中已接入的发件域发送邮件。因此:

  • 优先使用 account-owned token;
  • 权限只给 Email Sending: Edit
  • 不写进仓库、镜像或前端环境变量;
  • 不同系统使用不同 Token,便于单独吊销;
  • 能用 Worker binding 的场景,不要为了“熟悉 SMTP”额外引入长期凭证。

必须知道的限制

截至 2026 年 7 月,官方平台限制中最值得在代码里提前防守的是:

限制当前值实现影响
每域 Email Routing 规则200大量租户不要为每个人创建独立静态规则
每账户已验证目标地址200转发到多邮箱前先规划账户级目标
入站邮件大小25 MiB更大邮件在 SMTP 阶段被拒绝
单封出站收件人数50To、Cc、Bcc 合计
普通出站邮件总大小5 MiB包含附件与 MIME 编码开销
发往已验证目标的总大小25 MiB仅适用于已验证目标地址
自定义头总大小16 KB不要把业务数据塞进 Header
单个 Zone 的发送/路由域名总数30根域与子域都计入
回复的 References 项数100超过后 reply() 会失败

附件会经过 Base64 和 MIME 封装。3 MiB 的原始附件不等于最终邮件仍是 3 MiB,所以应用限制应给编码、正文和邮件头预留空间。

平台还会根据账户信誉、投递表现和使用历史调整每日发送额度。不要假设“每月额度未用完”就等于某一秒可以无限并发发送。

验证码与密码重置应该怎样做

本博客的注册和密码重置邮件采用的是下面这条链路:

用户提交邮箱
  → 同源检查
  → IP 与邮箱限流
  → 生成 6 位验证码
  → D1 只保存加盐哈希、用途、过期时间和尝试次数
  → EMAIL binding 发送
  → 验证成功后原子标记已使用

关键策略包括:

  • 同一邮箱 5 分钟只能发送一封;
  • 验证码 5 分钟后失效;
  • 验证码只能使用一次;
  • D1 不保存验证码明文;
  • 连续输错达到阈值后停止验证;
  • 邮件发送失败时释放本次占位,避免用户被一个未送达的验证码锁住;
  • 重置密码后撤销已有登录会话,而不是只替换密码哈希。

限流要在数据库中“原子领取资格”,不能写成“先查询上次发送时间,再插入一行”。两个并发请求可能同时通过查询。使用唯一键配合 INSERT ... ON CONFLICT ... WHERE,才能让数据库决定谁获得发送资格。

邮件接口的响应也不应暴露“这个邮箱是否已经注册”。否则攻击者可以批量枚举用户。

一个更完整的生产架构

当邮件从 Demo 进入生产,推荐把职责拆开:

HTTP 请求 / 业务事件
  → 鉴权、同源、参数校验、限流、幂等
  → Queue
  → 邮件模板渲染
  → EMAIL binding
  → messageId / 错误码写入 D1
  → Email Service 日志与告警

入站链路则可以是:

support@yourdomain.com
  → Email Routing
  → Email Worker
  → 认证与大小检查
  → MIME 解析
  → R2 保存附件 / D1 建立工单
  → Queue 做耗时处理
  → 转发人工邮箱或发送受约束回复

D1 建议记录什么

可以记录:

  • 业务事件 ID 与幂等键;
  • 模板 ID 和模板版本;
  • messageId
  • 收件人数,不一定要保存完整地址;
  • accepted / failed 等应用侧状态;
  • Cloudflare 错误码;
  • 创建、发送和最后更新时间。

不建议默认记录:

  • 验证码明文;
  • 密码重置 Token;
  • 完整邮件正文;
  • 附件内容;
  • SMTP/API Token;
  • 为调试方便长期保留的原始来信。

日志本身也可能成为新的隐私数据源。

不要把 Email Routing 的 “Dropped” 当成发送失败

官方特别说明:通过 send_email binding 发出的邮件,在 Email Routing 汇总页面可能显示为 dropped,即使邮件实际已经成功投递。

出站结果应该查看 Email Sending 的 Metrics 与 Logs,并用 messageId 关联应用日志。Routing 页面描述的是入站路由处理,不是完整的出站投递状态机。

本地怎么测试

Wrangler 已支持本地触发 Email Worker。运行:

pnpm exec wrangler dev

然后向本地入口发送一封原始邮件:

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.'

本地发送 binding 会把文本和 HTML 保存成可检查的文件,适合验证模板。二进制附件的本地模拟仍有单独限制,涉及附件时应补一轮远程测试。

至少覆盖这些用例:

  • 正常文本和 HTML;
  • 非 ASCII 主题与发件人显示名;
  • 重复请求与幂等;
  • 超过发送冷却时间前的重复验证码;
  • 大正文与附件边界;
  • Header 换行注入;
  • 未验证发件域或受限收件人;
  • 退信、抑制、额度和频率错误;
  • 入站无匹配地址、Catch-all 和拒绝路径;
  • HTML 邮件中的脚本、远程资源与危险附件名。

它适合什么,不适合什么

适合:

  • 注册验证、魔法链接、密码重置;
  • 订单、账单、部署、监控通知;
  • 个人域名邮件转发;
  • 联系表单与支持工单;
  • 邮件触发的 Agent 或自动化流程;
  • 希望少维护一台 SMTP 服务的 Cloudflare 应用。

不应直接拿来做:

  • 未经同意的营销群发;
  • 没有退订、投诉和抑制管理的 Newsletter;
  • 匿名开放的“帮我发邮件”接口;
  • 需要 IMAP 文件夹、日历、通讯录和多人邮箱协作的企业邮箱;
  • 把任何来信正文直接交给有高权限工具的 Agent。

Cloudflare 替你维护了邮件入口与投递基础设施,但发件人信誉、用户授权、内容安全、频率控制和数据最小化仍然是应用自己的责任。

最后的判断

Cloudflare Email Service 最有价值的地方,不是“又多了一个 SMTP 厂商”,而是它让邮件和 Worker、D1、R2、Queue、Agents 处于同一运行边界。

接收邮件不再只能转发到人类邮箱,它可以成为一个受约束的事件入口;发送邮件也不再需要为验证码单独接入一个外部 SDK 和密钥体系。

如果项目本来就部署在 Workers,优先从这条最小链路开始:

  1. Email Routing 接收 support@
  2. Worker binding 发送验证码和密码重置;
  3. D1 实现冷却、一次性验证和最小日志;
  4. sender allowlist 限制 binding 权限;
  5. 用 Email Sending 日志核对投递,而不是只看接口返回成功。

等确实需要从外部平台发送时,再引入 REST API;只有现有系统已经深度依赖 SMTP 客户端时,才使用认证 SMTP。

参考资料

END OF SIGNAL
READER CHANNEL

留言

00
还没有留言。成为第一个发出回应的人。