返回文章索引
TRANSMISSION / CYAN17 分钟阅读

Cloudflare SMTP 发信实战:从控制台配置到 Nodemailer 上线

一篇独立、可落地的 Cloudflare Email Service SMTP 教程,覆盖域名接入、API Token、cURL、Nodemailer、Python、限额、错误码与生产安全。

#Cloudflare#SMTP#Email#Nodemailer
一封事务邮件通过加密 SMTP 连接进入全球邮件投递网络
把现有应用接到 Cloudflare Email Service,不一定要重写发送层;标准 SMTP 就是一条兼容路径

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 PLAINAUTH LOGIN
发件人已在 Email Sending 中启用的域名地址

这里有三个不能互换的概念:

  1. 用户名不是 Cloudflare 邮箱,也不是 Account ID,而是固定字符串 api_token
  2. 密码不是登录 Cloudflare 的密码,而是专用 API Token;
  3. 端口 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。

Cloudflare Email Service 的 SMTP 连接页,显示主机、端口、用户名与 cURL 示例

截图采集于 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,
})

Cloudflare 控制台中的 Nodemailer 示例,密码从环境变量读取

控制台示例使用 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')
  }
}

不要提供一个接收任意 fromtosubject 和 HTML 的公开 API。否则应用很容易变成开放邮件中继,或者被用来发送钓鱼邮件。更安全的接口只接收业务参数,例如 userIdtemplateId;服务端再从数据库取已验证地址,并从白名单模板生成标题与正文。

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 WorkersWorkers send_email binding
外部服务支持 HTTPS API,且便于管理 TokenREST 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 是一座兼容旧有生态的桥,而不是唯一入口。

参考资料

文章结束
READER CHANNEL

留言

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