域名邮箱只能收不能发?Worker+Resend 自建发信通道

🔥 个人主页:weixin_41961749 | 📚 本文已收录进专栏「后端手册」,更多后端实战持续更新

前言

用 Cloudflare 的 Email Routing(邮箱转发) 给域名挂了个 admin@example.com,别人发邮件过来能自动转发到自己的 QQ/Gmail,零成本很香。但真要用它回信就露馅了:从自己邮箱点回复,对方看到的发件人是你的私人邮箱,域名邮箱形同虚设。

原因一句话:Email Routing 只管收、不管发,它根本没有 SMTP 发信能力。

本文给出一条不依赖 Gmail 的完整解法——Cloudflare Worker 调 Resend API 自建发信通道,外加一个带密码保护的网页写信页:浏览器打开就能以 admin@example.com 人工回信,curl/程序也能调 API 自动发,全程免费额度内跑。

看完你能拿到:收发分离架构图、Resend 域名验证的 4 条 DNS 记录、可直接部署的 Worker 源码、以及实操中踩到的 4 个真实坑(受限 API Key、region 参数变更、verify 接口 405、本机代理)。

一、原理:收和发是两条独立通道

很多人以为"配了转发=有了域名邮箱",其实收信和发信在邮件体系里是两套完全独立的机制:

  • 收信:靠 MX 记录。全世界的邮件服务器投递邮件时查 MX,把信投给你指定的服务器。Cloudflare Email Routing 的本质是"MX 指向 Cloudflare + 转发规则",只做入站。
  • 发信:靠 SPF / DKIM / DMARC 三件套 TXT 记录 + 一个真正能发信的服务端(SMTP 或 HTTP API)。Email Routing 这三样一样都不提供。

所以补齐发信能力,动的是 TXT 记录和新增一个发信服务,MX 一条都不用动,原有转发完全不受影响。架构如下:

收发分离双通道架构

还有个关键细节:SPF 一条域名只能有一条 TXT 记录。如果之前为了 Email Routing 配过 v=spf1 include:_spf.mx.cloudflare.net ~all,后面加发信服务时必须把两个 include 合并进同一条,而不是再加一条新记录——两条 SPF 并存会导致 SPF 永久 fail(permerror)。

二、三条路怎么选

补发信能力常见三条路,先说结论:个人邮箱是 Gmail 选 A 最省事;QQ/163 用户或需要程序化发信选 C。

方案做法优点缺点
A. Gmail 别名Gmail「以此地址发送」+ Resend SMTP在 Gmail 里一键切换发件人QQ/163 不支持;Gmail 强制要求填 SMTP 凭据
B. Zoho Mail 免费版MX 切到 Zoho,独立网页收发真·独立邮箱要接管 MX(和 CF 转发二选一);免费版不开 SMTP/IMAP
C. Worker + 发信 API(本文)Worker 调 Resend API 发信不依赖任何邮箱客户端;网页写信页人工回信;API 可程序化调用;收信转发原样不动手动回信要打开写信页(不是在自己邮箱里点回复)

选 C 的理由很实际:

  1. 国内用户主力是 QQ/163 邮箱,它们根本没有 Send-as 别名功能,方案 A 直接出局;
  2. 网页写信页补齐了"人工回信"场景——收到转发来的邮件后,打开写信页照着对方地址回一封,发件人就是域名邮箱;
  3. Worker 天然适合程序化场景:网站表单自动答复、订单通知、机器人通知,一个 HTTP 接口全搞定。

三、准备:Resend 的两个 API Key(权限分级)

Resend 免费档 100 封/天、3000 封/月,对个人回信场景绰绰有余。注册支持 GitHub/Google 登录,创建 API Key 时会让你选权限。

这里是我踩的第一个坑:我一开始顺手建了个默认 Key,结果调域名管理接口直接报错:

{"statusCode":401,"message":"This API key is restricted to only send emails","name":"restricted_api_key"}

原来 Resend 创建 Key 时可以勾选 "Full access" 或 "Sending access"(发信专用)。正确姿势是建两个 Key 各司其职:

Key权限用途
管理用 KeyFull access仅配置期用:创建域名、加 DNS、触发验证;配完可删
发信 KeySending access部署进 Worker,日常发信;即使泄露也只能发信,不能动你的域名

这个分级不是洁癖:发信 Key 会长期躺在 Worker 的环境变量里,给它最小权限,泄露时的爆炸半径小得多。

四、Resend 域名验证:4 条 DNS 记录

发信域名必须先在 Resend 验证,否则只能用它官方的测试发件地址发给自己。流程:Resend 后台 Add Domain → 它生成 DNS 记录 → 去 Cloudflare DNS 添加 → 触发验证。

创建域名也可以走 API(管理 Key):

curl -X POST https://api.resend.com/domains \
  -H "Authorization: Bearer re_管理Key" \
  -H "Content-Type: application/json" \
  -d '{"name":"example.com","region":"ap-northeast-1"}'

坑 2:region 参数取值已经变了。网上老教程里写 "sin"(新加坡),实测直接 422:

{"statusCode":422,"message":"The `region` field must be one of \"us-east-1\", \"eu-west-1\", \"sa-east-1\", or \"ap-northeast-1\".","name":"validation_error"}

现在可选值只有这四个,国内就近选 ap-northeast-1(东京)。

创建成功后 Resend 会给出 4 条记录,全部加到 Cloudflare DNS:

#类型名称内容作用
1TXTresend._domainkeyp=MIGfMA0GCSq...(Resend 生成)DKIM 签名,发信时给邮件签名
2MXsendfeedback-smtp.ap-northeast-1.amazonses.com(优先级 10)退信通道
3TXTsendv=spf1 include:amazonses.com ~allsend 子域的 SPF
4CNAMErsendsend.forge.rmta.net(仅 DNS,不开橙云)打开率追踪

注意第 4 条 CNAME 必须灰云(DNS only),开了橙云代理追踪域名会被代理拦截。

逐条写入的脚本(Zone ID 在 Cloudflare 域名概览页右下角能看到):

CFT="你的_CF_API_Token"
Z="你的_ZONE_ID"
API="https://api.cloudflare.com/client/v4/zones/$Z/dns_records"

# 1. DKIM(value 用 Resend 后台生成的完整串)
curl -s -X POST $API -H "Authorization: Bearer $CFT" -H "Content-Type: application/json" \
  -d '{"type":"TXT","name":"resend._domainkey.example.com","content":"p=MIGfMA0...(完整粘贴)","ttl":1}'

# 2. 退信 MX
curl -s -X POST $API -H "Authorization: Bearer $CFT" -H "Content-Type: application/json" \
  -d '{"type":"MX","name":"send.example.com","content":"feedback-smtp.ap-northeast-1.amazonses.com","priority":10,"ttl":1}'

# 3. send 子域 SPF
curl -s -X POST $API -H "Authorization: Bearer $CFT" -H "Content-Type: application/json" \
  -d '{"type":"TXT","name":"send.example.com","content":"v=spf1 include:amazonses.com ~all","ttl":1}'

# 4. 追踪 CNAME(灰云)
curl -s -X POST $API -H "Authorization: Bearer $CFT" -H "Content-Type: application/json" \
  -d '{"type":"CNAME","name":"rsend.example.com","content":"send.forge.rmta.net","proxied":false,"ttl":1}'

加完触发验证。坑 3:验证接口是 POST,不是 PATCH,用错方法会收到 405:

# ✅ 正确
curl -s -X POST https://api.resend.com/domains/<域名ID>/verify -H "Authorization: Bearer re_管理Key"
# ❌ PATCH 会报 {"statusCode":405,"message":"Method PATCH is not allowed..."}

然后轮询状态(Cloudflare DNS 全球生效很快,实测一轮 12 秒就 verified):

curl -s https://api.resend.com/domains/<域名ID> -H "Authorization: Bearer re_管理Key"
# {"object":"domain","id":"...","status":"verified"}

五、Worker 源码:鉴权、限速、转发三件事

Worker 的职责很克制:鉴权 → 校验 → 转发 Resend API,不做任何邮件逻辑。完整源码约 150 行,这里贴核心部分。

令牌鉴权(写信页密码和 API Bearer 用同一个 token,SHA-256 后恒时比较,防时序侧信道):

async function authorized(request: Request, env: Env): Promise<boolean> {
  if (!env.MAIL_TOKEN) return false;
  const auth = request.headers.get("authorization") ?? "";
  const token = auth.startsWith("Bearer ") ? auth.slice(7).trim() : "";
  if (!token) return false;
  return bytesEqual(await sha256(token), await sha256(env.MAIL_TOKEN));
}

参数校验与 Resend payload 组装(in_reply_to 是串邮件话题线程的关键,带上它对方邮箱里你的回信才会挂进原邮件的会话):

const payload: Record<string, unknown> = { from: FROM, to: [to], subject };
if (text) payload.text = text;
if (html) payload.html = html;
payload.reply_to = "admin@example.com"; // 对方直接"回复"时,回信经 CF 转发回到你手里
if (inReplyTo) {
  payload.headers = { "In-Reply-To": inReplyTo, References: inReplyTo };
}

转发与错误透传:直接把 Resend 的状态码和错误消息透传给调用方,排查时不用猜:

const resp = await fetch("https://api.resend.com/emails", {
  method: "POST",
  headers: {
    authorization: `Bearer ${env.RESEND_API_KEY}`,
    "content-type": "application/json",
  },
  body: JSON.stringify(payload),
});
const data = await resp.json().catch(() => ({}));
if (resp.ok) return json({ ok: true, id: data.id });
return json({ ok: false, status: resp.status, error: data.message }, resp.status);

另外加了个进程内限速(30 封/小时),防 token 泄露后被刷免费额度。

部署配置(wrangler.jsonc),自定义域和 Worker 同账号时部署即自动绑定:

{
  "name": "example-mailer",
  "main": "src/index.ts",
  "compatibility_date": "2025-10-01",
  "routes": [{ "pattern": "mail.example.com", "custom_domain": true }]
}

坑 4:本机到 CF API 网络不稳,代理环境变量不生效。直连 api.cloudflare.com 经常 fetch failed,挂了 HTTPS_PROXY 后 cf/wrangler 又不吃(Node fetch 默认不读代理变量)。解法分两层:

# curl 加 -x 参数,稳定可用
curl -s -x http://127.0.0.1:7890 https://api.cloudflare.com/...

# wrangler 依赖 Node 24+ 的内置代理开关(实测 wrangler 4.x 识别并提示 "use your proxy")
export NODE_USE_ENV_PROXY=1 HTTPS_PROXY=http://127.0.0.1:7890
npx wrangler deploy

部署完写两个 secrets(写信页密码自拟随机串):

npx wrangler secret put RESEND_API_KEY   # 发信 Key(re_ 开头)
npx wrangler secret put MAIL_TOKEN       # 写信页密码 + API Bearer

六、网页写信页:补齐人工回信场景

Worker 的 GET / 返回一个零依赖的写信页(单文件 HTML 直接内嵌在代码里,不用静态资源):

  • 访问密码 = MAIL_TOKEN,存 localStorage,输一次就行;
  • 表单四个字段:收件人 / 主题 / 正文 / In-Reply-To(收起在"高级"里);
  • 发送就是 fetch("/send", { headers: { authorization: "Bearer " + token } }),成功显示 Resend 返回的邮件 id;
  • Ctrl+Enter 快捷发送,深色模式自适应,noindex 防搜索引擎收录。

日常回信姿势:QQ 邮箱收到 CF 转发来的邮件 → 复制对方地址和要回的内容 → 打开 mail.example.com 粘贴发送。比 Gmail 方案多一步复制粘贴,换来的是完全不绑定某个邮箱客户端。

程序化调用更简单:

MT="你的_MAIL_TOKEN"
curl -X POST https://mail.example.com/send \
  -H "Authorization: Bearer $MT" \
  -H "Content-Type: application/json" \
  -d '{"to":"someone@foo.com","subject":"回复:合作事宜","text":"正文...","in_reply_to":"<原邮件Message-ID>"}'
# {"ok":true,"id":"01axxxx-xxxx"}

七、全链路验证

三步验收,缺一不可:

  1. 存活:GET /health 返回 {"ok":true};
  2. 鉴权:拿错误 token 调 /send,应返回 401;
  3. 真实发信:发一封到自己邮箱,确认发件人显示 admin@example.com、在收件箱而非垃圾箱。

想量化送达质量,把收到的邮件原文投给 mail-tester.com,它会打出 SPF/DKIM/DMARC/黑名单的综合分。关键机制:DMARC 验证的是"DKIM/SPF 至少一个与 From 域名对齐"——Resend 发信走 SES,SPF 是 amazonses.com 不对齐,但 DKIM 签名域是你的域名(d=example.com),对齐成立,DMARC 通过。所以哪怕你的 DMARC 是严格的 p=quarantine,只要 DKIM 那条 TXT 配对,进的就是收件箱。

八、避坑清单速查

#坑现象解法
1API Key 权限restricted_api_key 401域名配置用 Full access Key(用完删),发信用 Sending Key
2region 参数422 提示值非法只接受 us-east-1 / eu-west-1 / sa-east-1 / ap-northeast-1,老教程的 sin 已废弃
3验证接口Method PATCH is not allowed 405用 POST /domains/{id}/verify
4本机代理wrangler/cf fetch failed,设了 HTTPS_PROXY 没用curl 用 -x;wrangler 用 Node 24 的 NODE_USE_ENV_PROXY=1
5SPF 重复两条 SPF 并存 → permerror合并进一条:include:amazonses.com include:_spf.mx.cloudflare.net
6追踪域名CNAME 开了橙云rsend 子域必须灰云 DNS only
7进垃圾箱DKIM 未对齐或没配确认 resend._domainkey TXT 生效,mail-tester 复测

九、FAQ

Q:写信页的 token 泄露了怎么办? wrangler secret put MAIL_TOKEN 换一个新值即时生效,老 token 立刻失效;Worker 侧还有 30 封/小时的限速兜底。

Q:免费额度够用吗? Resend 100 封/天、3000 封/月;Cloudflare Worker 免费档 10 万请求/天。个人回信+站点通知场景基本不可能触顶。

Q:能发附件吗? 能,Resend API 支持 attachments 字段(base64)。写信页目前没做上传,API 调用直接传即可。

Q:为什么不用 Cloudflare 自带的 Email Workers 发信? CF 原生的 send_email 绑定对发件人有限制,且无法完成自定义域名的 DKIM 签名体系;用 Resend 这类专业发信服务,DNS 层的可信度(DKIM/SPF/DMARC)和送达率都更有保障。

Q:收信方向要动吗? 完全不动。MX 继续指向 route*.mx.cloudflare.net,Email Routing 转发规则照常工作,发信侧新加的 TXT/CNAME 记录对它零影响。

十、总结

  • Email Routing 只进不出:收信靠 MX,发信靠 SPF/DKIM/DMARC + 发信服务端,两者完全独立;
  • 三步补齐发信:Resend 验证域名(4 条 DNS)→ Worker 做鉴权限速转发 → secrets 注入 Key;
  • Key 权限分级:管理 Key(Full access,用完删)与发信 Key(Sending access)分离;
  • DMARC 靠 DKIM 对齐通过,与 SPF 对齐无关,这是进收件箱的核心;
  • 网页写信页 + HTTP API 双形态:人工回信、程序化发信一个 Worker 全包,且不依赖 Gmail。

参考资料

  • Cloudflare Email Routing 官方文档
  • Cloudflare Email Workers
  • Resend Domains 文档
  • Resend Send Email API
  • DMARC 协议 RFC 7489

写在最后

整套方案零服务器、零付费,一个下午能全部跑通。如果你的场景就是"想要一个能收能回的域名邮箱,但主力邮箱是 QQ/163",这条 Worker + Resend 的路子是目前最省心的免费解。

你在域名邮箱上还踩过哪些坑?评论区聊聊。更多后端实战会持续更新在专栏「后端手册」里,感兴趣可以收藏关注。

相关链接

名称链接
weixin_41961749https://blog.csdn.net/weixin_41961749
Resendhttps://resend.com
mail-tester.comhttps://www.mail-tester.com
Cloudflare Email Routing 官方文档https://developers.cloudflare.com/email-routing/
Cloudflare Email Workershttps://developers.cloudflare.com/email-routing/email-workers/
Resend Domains 文档https://resend.com/docs/dashboard/domains/introduction
Resend Send Email APIhttps://resend.com/docs/api-reference/emails/send-email
DMARC 协议 RFC 7489https://datatracker.ietf.org/doc/html/rfc7489
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐