如何从 Cloudflare Workers 发送邮件
Worker 不能运行邮件服务器,但可以和邮件服务器通信。下面是我们在构建 MakiSend(一个完全运行在 Workers 上的邮件 API)时总结的经验:哪些可行,哪些会失败,以及原因。
最后更新 · Markdown 版本(英文)
三种发送方式
Worker 有三条出路。按你想依赖谁来选,而不是按代码选:每种方式的代码都很短。
| 方式 | Worker 如何发送 | 你需要什么 | 注意事项 |
|---|---|---|---|
| HTTP 邮件 API | 用 fetch() 调用服务商的 API | 该服务商的账户和 API key | 只适用于一家服务商的请求格式,以及它按封计费的价格 |
| 基于 TCP 的 SMTP | nodemailer(开启 nodejs_compat)运行在 Cloudflare 的 TCP socket 上 | 任意服务商或邮箱的 SMTP 凭据 | 允许的端口、TLS 模式,以及每条 SMTP 命令一次往返 |
| Cloudflare Email Service | send_email 绑定 | 一个已接入 Email Sending 的发件域名 | 向任意收件人发送需要 Workers Paid |
SMTP 让你不被绑定:同一段代码可以通过 Google Workspace、Zoho、Microsoft 365、Amazon SES、SendGrid、Mailgun、Postmark、Brevo 或 Resend 自己的 SMTP 发送。它也是坑最多的一种。
Worker 能用哪些 SMTP 端口
Cloudflare 封锁了 25 端口的出站连接,所以 Worker 无法直接投递到收件人的 MX 服务器。它必须把邮件交给提交服务器,使用以下端口之一:
| 端口 | 安全方式 | 含义 |
|---|---|---|
| 465 | 隐式 TLS(tls) | 从第一个字节起就是 TLS |
| 587 | STARTTLS(starttls) | 先以明文开始,认证前升级为 TLS |
| 2465 | 隐式 TLS(tls) | 部分服务商提供的 465 替代端口 |
| 2525, 2587 | STARTTLS(starttls) | 部分服务商提供的 587 替代端口 |
| 25 | 无 | 在 Workers 上被封锁 |
端口要配对正确的模式。465 配 STARTTLS,或 587 配隐式 TLS,都会在 TLS 握手阶段失败(wrong version number),此时还没有发送任何凭据。MakiSend 在保存配置时拒绝 25 端口,并根据端口推断模式。
在 Workers 上运行 nodemailer
只要打开 nodejs_compat 兼容性标志,nodemailer 就能在 Workers 上运行:node:net 和 node:tls 映射到 Cloudflare 的 TCP socket。但仍有三个问题让我们踩了坑。
- nodemailer 会先解析主机名,再连接 IP 地址。workerd 拒绝在 465 端口上用隐式 TLS 连接 IP 地址(
proxy request failed, cannot connect to the specified address),而同一台服务器用主机名就能正常连接。这个错误完全没有说明真正的原因。 - 本地开发时
dns.resolve4和dns.resolve6没有实现,所以默认的 transport 在wrangler dev下可能会卡住。 - 如果没有
family: 4,workerd 每次发送都会记录网络警告。
前两个问题的解决办法是 nodemailer 的 getSocket 选项:自己按主机名打开 socket,再交给 nodemailer。需要时 nodemailer 仍会执行 STARTTLS,也仍会按主机名校验证书。
MakiSend 的版本增加了超时,并被连接检查、测试发送和正式发送共用:smtp-connection.ts。
Cloudflare Email Service 也在 smtp.mx.cloudflare.net 提供 SMTP,但 Worker 无法向该主机打开 socket。在 Worker 中请改用 send_email 绑定发送。
不要在请求中发送
SMTP 的每条命令都要等待回复:TCP、TLS、EHLO、AUTH、MAIL FROM、RCPT TO、DATA、QUIT。在 Workers 上,一次发送最快需要 300 到 400 ms,最慢约 2 秒,而且 Worker 每封邮件都要新建连接。用户不应该等这么久,发送失败的邮件也不应该丢失。
- 邮件一保存就返回
202 Accepted,然后把它的 id 放进 Cloudflare Queue。 - 从 Cloudflare Workflow 中发送:每个步骤都是持久化的,重试会从中断处继续。
- 只重试以后可能成功的情况。MakiSend 首次尝试一次,再间隔 30 秒重试三次;认证、TLS、DNS、配置错误和 5xx 会立即停止,用完重试次数的邮件进入死信队列。
- 接受
Idempotency-Key请求头,这样客户端重试自己的请求时不会重复发送。
等待网络的时间不计入 CPU 限制(Workers Free 上每次调用 10 ms),所以发送本身在免费计划内就能完成。架构页面跟随一封邮件走完这些步骤。
固定 IP 与收件箱
- Workers 从共享且会变化的 Cloudflare 地址发起连接。只接受白名单 IP 的服务商会拒绝连接:请关闭白名单、依靠 SMTP 凭据,或者从有固定 IP 的服务器发送。
- 邮件能否进入收件箱取决于发件域名(SPF、DKIM 和 DMARC)和服务商的信誉,与 Worker 无关。在我们的测试中,设置好这三条记录后,MakiSend 的邮件都进入了收件箱。
- Gmail 和 Google Workspace 限制单个邮箱每天的发送量。大量发送请使用事务邮件服务商。
或者交给 MakiSend
MakiSend 把本页的内容全部打包好了:带认证的 POST /send-email、队列、带重试的持久化 Workflow、通过主机名打开 socket 的 nodemailer、Cloudflare Email Service 绑定,以及带日志的 dashboard。它是开源的,部署在你自己的 Cloudflare 账户上,Workers Free 账户也可以。
- 使用编码代理:复制安装提示词,粘贴到 Claude Code、Codex、Cursor 或其他代理中。
- 不使用代理:先生成它要求的两个密钥,再点击 Deploy to Cloudflare。
- 先读代码:github.com/lixalistudio-oss/makisend。
常见问题
Cloudflare Worker 能通过 SMTP 发送邮件吗?
能。开启 nodejs_compat 标志后,nodemailer 运行在 Cloudflare 的 TCP socket 上,可以通过 465、587、2465、2525 或 2587 端口连接任何 SMTP 服务商。25 端口被封锁。
为什么 Worker 无法使用 25 端口?
Cloudflare 封锁了 25 端口的出站连接。请改用服务商的提交端口:465 配隐式 TLS,或 587 配 STARTTLS。
为什么 nodemailer 在 465 端口报错“proxy request failed”?
nodemailer 连接的是解析后的 IP 地址,而 workerd 拒绝对裸 IP 使用隐式 TLS。用 nodemailer 的 getSocket 选项按主机名打开 socket 即可。
免费计划能用 Cloudflare Email Service 吗?
不能向任意收件人发送:这需要 Workers Paid(每月 $5)。在 Workers Free 上,请通过其他 SMTP 服务商发送。