Cloudflare SMTP 发信实战:从控制台配置到 Nodemailer 上线
一篇独立、可落地的 Cloudflare Email Service SMTP 教程,覆盖域名接入、API Token、cURL、Nodemailer、Python、限额、错误码与生产安全。

Cloudflare Email Service 在 2026 年加入了认证 SMTP。对于已经使用 Nodemailer、Python smtplib、PHPMailer,或者某个只会配置 SMTP 的后台系统来说,这意味着不必为了更换邮件服务商重写整个发送层。
本文是一篇独立的 SMTP 教程。目标不是重复介绍邮件接收、Email Routing 或 Email Workers,而是完成一件具体的事:
使用
smtp.mx.cloudflare.net:465,通过 Cloudflare API Token 完成 SMTPS 认证,并从自己的域名发送一封事务邮件。
本文依据的是 2026 年 7 月 31 日可见的 Cloudflare Email Service Beta 能力。Beta 阶段的配额和控制台界面仍可能变化,上线前应重新核对官方发送指南与SMTP API 参考。
一分钟配置表
| 项目 | 值 |
|---|---|
| SMTP 主机 | smtp.mx.cloudflare.net |
| 端口 | 465 |
| 加密 | 隐式 TLS,也就是 SMTPS |
| 用户名 | 固定为字面量 api_token |
| 密码 | 具有 Email Sending: Edit 权限的 Cloudflare API Token |
| 认证方式 | AUTH PLAIN 或 AUTH LOGIN |
| 发件人 | 已在 Email Sending 中启用的域名地址 |
这里有三个不能互换的概念:
- 用户名不是 Cloudflare 邮箱,也不是 Account ID,而是固定字符串
api_token; - 密码不是登录 Cloudflare 的密码,而是专用 API Token;
- 端口 465 使用连接建立时立即开始的 TLS,不能照搬端口 587 的 STARTTLS 配置。
Cloudflare 当前不提供 587 STARTTLS,也不把 25 端口作为出站中继;25 端口用于入站 Email Routing。详细差异可查阅官方 SMTP 文档。
开始前需要什么
1. 域名使用 Cloudflare DNS
Email Sending 要求发件域由 Cloudflare DNS 托管。进入:
Cloudflare Dashboard
→ Compute
→ Email Service
→ Email Sending
选择域名并完成启用。Cloudflare 会配置发件验证、DKIM 与退信处理需要的 DNS 记录。控制台显示为“已配置”以后,再继续调 SMTP;否则认证成功也可能因为发件域未获授权而被拒绝。
2. 创建最小权限的 API Token
进入域名详情的“连接”页面,切换到 SMTP。页面会提示创建具有 Email Sending: Edit 权限的 API Token。

截图采集于 2026 年 7 月 31 日。画面只包含 Cloudflare 提供的配置值和环境变量占位符;真实 Token 没有被创建、显示或写入文章。
建议使用账户级 Token,并尽可能限制:
- 只授权需要发信的 Cloudflare 账户;
- 只授予
Email Sending: Edit; - 为 Token 设置合理有效期;
- 如果部署出口 IP 稳定,再增加客户端 IP 过滤;
- 每个应用使用独立 Token,方便吊销和审计。
Token 只在创建时完整显示一次。将它写入服务端 Secret 管理或运行环境变量,绝不能:
- 写进 Git 仓库、Markdown 或构建日志;
- 放进
NUXT_PUBLIC_*; - 交给浏览器 JavaScript;
- 在异常日志中打印 SMTP 配置对象;
- 与其他项目共享同一个长期有效 Token。
先用 cURL 验证链路
在接入框架以前,先用 cURL 发送最小测试邮件,可以把“Cloudflare 配置问题”和“应用代码问题”拆开排查。
创建 mail.txt:
From: Jackie Moon <noreply@sparkles-editor.com>
To: recipient@example.com
Subject: Cloudflare SMTP test
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Hello,
This message was delivered through Cloudflare Email Service SMTP.
把 Token 放入当前终端进程的环境变量。不要把真实值写进 Shell 历史:
read -s CF_EMAIL_SMTP_TOKEN
export CF_EMAIL_SMTP_TOKEN
然后发送:
curl --url "smtps://smtp.mx.cloudflare.net:465" \
--ssl-reqd \
--user "api_token:${CF_EMAIL_SMTP_TOKEN}" \
--mail-from "noreply@sparkles-editor.com" \
--mail-rcpt "recipient@example.com" \
--upload-file mail.txt
成功时,SMTP 会返回 250。响应中包含的 Message-ID 可以用来关联 Email Service 控制台里的发送日志。完整格式可对照Cloudflare 的 cURL 示例。
如果这里失败,先不要开始改 Nodemailer:
535:Token 无效、过期或缺少Email Sending: Edit;550:发件地址或发件域没有被允许;552:邮件超过大小限制;421/451:临时故障或限流,等待后重试。
在 Node.js 中使用 Nodemailer
下面的方案适用于 Node.js、Docker、VPS、传统 Serverless Function,或运行在 Node 兼容环境的 Nitro 服务端。安装依赖:
pnpm add nodemailer
pnpm add -D @types/nodemailer
服务端环境变量:
CF_EMAIL_SMTP_TOKEN=replace-with-a-server-side-secret
SMTP_FROM="Jackie Moon <noreply@sparkles-editor.com>"
创建 Transport:
import nodemailer from 'nodemailer'
const transporter = nodemailer.createTransport({
host: 'smtp.mx.cloudflare.net',
port: 465,
secure: true,
auth: {
user: 'api_token',
pass: process.env.CF_EMAIL_SMTP_TOKEN,
},
connectionTimeout: 10_000,
greetingTimeout: 10_000,
socketTimeout: 30_000,
})
const info = await transporter.sendMail({
from: process.env.SMTP_FROM,
to: 'recipient@example.com',
subject: 'Welcome to Signal Log',
text: 'Your account is ready.',
html: '<p>Your account is ready.</p>',
})
console.info('Email accepted', {
messageId: info.messageId,
accepted: info.accepted.length,
rejected: info.rejected.length,
})

控制台示例使用 process.env.CLOUDFLARE_API_TOKEN。实际项目可以使用更明确的变量名,但必须保证它只存在于服务端。
生产代码还应补上输入边界:
const allowedFrom = new Set([
'noreply@sparkles-editor.com',
'support@sparkles-editor.com',
])
function assertEmailEnvelope(from: string, to: string) {
if (!allowedFrom.has(from)) {
throw new Error('Sender is not allowed')
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(to)) {
throw new Error('Recipient is invalid')
}
}
不要提供一个接收任意 from、to、subject 和 HTML 的公开 API。否则应用很容易变成开放邮件中继,或者被用来发送钓鱼邮件。更安全的接口只接收业务参数,例如 userId 和 templateId;服务端再从数据库取已验证地址,并从白名单模板生成标题与正文。
Nuxt 部署在 Cloudflare Workers 时怎么办
这里需要特别区分运行时。
Nodemailer 是面向传统 Node SMTP Socket 的库。Cloudflare Workers 上的 Nuxt 应用已经运行在 Cloudflare 网络内,通常更适合使用 send_email binding:
interface Env {
EMAIL: SendEmail
}
await env.EMAIL.send({
from: 'noreply@sparkles-editor.com',
to: 'recipient@example.com',
subject: 'Verification code',
text: 'Your code is 123456',
})
这样不需要在 Worker 中保存 SMTP API Token,权限也由 binding 管理。
选择原则很简单:
| 场景 | 推荐入口 |
|---|---|
| Nuxt / API 已经部署在 Cloudflare Workers | Workers send_email binding |
| 外部服务支持 HTTPS API,且便于管理 Token | REST API |
| 现有 Node、Python、PHP 应用已经支持 SMTP | 认证 SMTP |
| 打印机、旧式 CMS 或第三方工具只能填 SMTP | 认证 SMTP,但先确认能使用 465 SMTPS |
| 浏览器前端直接发送 | 不允许;必须经过受控服务端 |
三个入口最后进入同一套 Cloudflare 投递管线、日志和域名认证系统。SMTP 的价值是兼容性,不是要求所有新服务都退回 Socket 协议。
Python 最小示例
Python 标准库无需额外依赖:
import os
import smtplib
from email.message import EmailMessage
token = os.environ["CF_EMAIL_SMTP_TOKEN"]
message = EmailMessage()
message["From"] = "Jackie Moon <noreply@sparkles-editor.com>"
message["To"] = "recipient@example.com"
message["Subject"] = "Cloudflare SMTP test"
message.set_content("This email was sent over implicit TLS.")
with smtplib.SMTP_SSL("smtp.mx.cloudflare.net", 465, timeout=30) as smtp:
smtp.login("api_token", token)
smtp.send_message(message)
关键是 SMTP_SSL,而不是先明文连接再调用 starttls()。
限额与附件
根据当前Email Service 平台限制:
- 每封邮件最多合计 50 个收件人;
- SMTP 会话最多接受 50 次
RCPT TO; - 普通邮件总大小上限为 5 MiB;
- 发往“已验证目标地址”时,总大小上限可到 25 MiB;
- SMTP
AUTH超时为 30 秒; - SMTP
DATA超时为 300 秒; - 自定义邮件头合计上限为 16 KiB;
- Subject 上限为 998 个字符。
邮件总大小包含 MIME 编码后的正文和附件。二进制附件经过 Base64 后通常会膨胀约三分之一,因此不要把一个 4.9 MiB 文件误认为一定能塞进 5 MiB 邮件。
控制台显示的每日配额可能因账户、Beta 阶段和使用情况而变化。不要把某次看到的数字写死在业务逻辑里;应读取当前控制台配额,并在自己的发送队列中保留余量。
错误处理和重试
SMTP 的返回码已经表达了“要不要重试”:
| 返回码 | 含义 | 建议 |
|---|---|---|
250 | 已被 Cloudflare 接受 | 记录 Message-ID,不再重复发送 |
421 / 451 | 临时故障或限流 | 指数退避并加入随机抖动 |
535 | 认证失败 | 停止自动重试,检查 Token |
550 | 发件域、地址或收件策略被拒绝 | 修正配置或数据后再发 |
552 | 邮件过大 | 缩小正文或附件 |
推荐的重试节奏可以是 30 秒、2 分钟、10 分钟、30 分钟,最多尝试固定次数。每次业务发送必须有幂等键,例如:
password-reset:{userId}:{resetRequestId}
invoice:{invoiceId}:issued
临时错误进入队列,永久错误进入人工检查或失败状态。不要把所有非 250 都立即重试,也不要让同一个验证码因为网络抖动发送十次。
投递率不是“返回 250 就结束”
250 代表 Cloudflare 接受了消息,不代表最终收件箱一定出现了邮件。生产环境还要观察:
- Delivery、Bounce、Deferred 与 Suppression;
- Gmail、Outlook、QQ 邮箱等主要目的地的结果;
- From、Return-Path、DKIM 和 DMARC 对齐;
- 硬退信地址是否立即停止发送;
- 投诉率与无效地址增长;
- 验证码和密码重置是否设置明确有效期。
事务邮件应该使用稳定、可识别的发件人,并同时提供纯文本与 HTML。不要伪造 Reply-To,不要用验证码通道发送营销群发,也不要让用户输入直接进入邮件头,避免 Header Injection。
上线检查清单
- 发件域在 Email Sending 中为“已配置”
- 使用 465 + 隐式 TLS
- 用户名严格为
api_token - Token 只有
Email Sending: Edit最小权限 - Token 位于服务端 Secret,不进入 Git、日志和前端
- From 地址来自服务端白名单
- 收件地址经过验证、去重和长度限制
- HTML 使用固定模板并保留纯文本版本
-
421/451使用带抖动的指数退避 -
535/550/552不做盲目重试 - 记录 Message-ID、模板名与业务幂等键,不记录 Token
- 监控退信、抑制、投诉和每日配额
最后的选择
如果你有一个已经稳定运行的 Node、Python 或 PHP 应用,Cloudflare SMTP 的优势很直接:只换主机、端口和认证信息,就能接入 Cloudflare 的投递与日志体系。
如果应用原生运行在 Workers,则没有必要绕一圈模拟传统服务器,优先使用 Email binding。SMTP 是一座兼容旧有生态的桥,而不是唯一入口。
留言
登录后加入讨论
你的邮箱不会公开,留言只显示昵称。