Cloudflare Email Worker 踩坑实录:三个你一定会遇到的问题
前些天(其实好几个月了,我都忘了这篇文章的存在……)我在用 Cloudflare Email Worker 处理公司的邮件自动化流程,踩了几个文档里没有记录的坑。写篇博客把整理分享出来,希望能帮后来人少走弯路。
背景
我们的需求很简单:用 Email Worker 接收发到 `service@example.com` 的邮件,解析内容,做一些自动化处理,同时把原始邮件备份一份以便日后追溯。
听起来很符合直觉,应该不难。但实际跑起来,三个坑一个接一个。
坑一:message.forward() 的 “same worker” 限制
最初的方案是用 message.forward() 把邮件转发到管理员邮箱做备份:
export async function handleServiceEmail(
message: ForwardableEmailMessage,
env: Cloudflare.Env,
) {
// 备份邮件到管理员邮箱
try {
await message.forward('admin@staff.example.com');
} catch (error) {
console.error('Failed to forward:', error);
}
// 继续处理邮件...
}
部署后,日志里出现了这个错误:
Error: cannot forward email to same worker
原因是我在 Cloudflare Email Routing 里配置了“Catch-all 规则,全部转发到当前 Worker”,Cloudflare 认为转发到管理员邮箱的邮件,会进入同一个 Worker,便直接拒绝了。我猜这是为了防止死循环,但是实际上我的管理员邮箱其实单独设置过转发规则,所以这里按说不该有问题。无奈。
关键点:这个检查发生在 Email Routing 层面,不是你代码层面能控制的。只要目标地址在 Email Routing 里最终会路由回同一个 Worker,forward() 就会报错。
坑二:message.forward() 的目标地址验证
好,那换一个不会路由回 Worker 的地址:
await message.forward('admin@example.com');
结果又报错了:
Error: destination address not verified
Cloudflare 要求 message.forward() 的目标地址必须是在 Email Routing 的 Destination addresses 里验证过的地址。你不能随意转发到任何邮箱——即使那个地址是你自己的。
去哪里验证? Cloudflare Dashboard → 对应域名 → Email Routing → Destination addresses → Add destination address,然后去邮箱里点确认链接。
到这里,我其实对 message.forward() 已经不太满意了。它的限制太多:
-
不能转发到路由回同一个 Worker 的地址
-
目标地址必须提前验证
-
转发后的邮件可能因为 SPF/DKIM 不对齐而进垃圾箱
对于 “备份邮件” 这个需求来说,forward() 不是最佳方案。
坑三:message.raw 只能读一次
放弃 forward() 后,我改用 R2 存储原始邮件:
export async function handleServiceEmail(
message: ForwardableEmailMessage,
env: Cloudflare.Env,
) {
// 先解析邮件内容
const parser = new PostalMime();
const rawEmail = new Response(message.raw);
const email = await parser.parse(await rawEmail.arrayBuffer());
// 再备份原始邮件到 R2
const rawForBackup = new Response(message.raw);
const rawBuffer = await rawForBackup.arrayBuffer();
await env.R2_BUCKET.put(`emails/${Date.now()}.eml`, rawBuffer);
// 用解析好的 email 对象继续处理...
}
看起来没问题?但日志告诉你不是:
TypeError: This ReadableStream is disturbed (has already been read from),
and cannot be used as a body.
message.raw 是一个 ReadableStream,只能被消费一次。第一次 new Response(message.raw) 读完之后,流就耗尽了。第二次再读,就会报 “disturbed” 错误。
这个问题在邮件处理场景里特别容易遇到,因为你通常需要:
-
解析邮件(提取标题、正文、附件)
-
保存原始邮件(备份、审计)
两个操作都需要读取 message.raw,但它只给你一次机会。
最终方案:先缓存,再复用
解决思路很简单——先把 ReadableStream 读到 ArrayBuffer 里缓存起来,后续所有操作都用这个缓存:
import PostalMime from 'postal-mime';
// 入口函数
export default async function email(
message: ForwardableEmailMessage,
env: Cloudflare.Env,
) {
// 第一步:读取并缓存原始邮件(只读一次 stream)
const rawBuffer = await new Response(message.raw).arrayBuffer();
// 第二步:用缓存的 buffer 解析邮件
const parser = new PostalMime();
const email = await parser.parse(rawBuffer);
// 第三步:用同一个 buffer 备份到 R2
const timestamp = Date.now();
const r2Key = `service-emails/${timestamp}-${message.from}.eml`;
await env.R2_BUCKET.put(r2Key, rawBuffer, {
customMetadata: {
from: message.from || '',
to: message.to || '',
subject: email.subject || '',
receivedAt: new Date(timestamp).toISOString(),
},
});
// 后续用 email 对象处理业务逻辑
console.log(`Subject: ${email.subject}`);
console.log(`Attachments: ${email.attachments?.length ?? 0}`);
}
几个要点:
-
rawBuffer是普通的ArrayBuffer,可以无限次读取,不存在 “consumed” 的问题。 -
R2 的
customMetadata非常实用。.eml文件本身包含完整的邮件头(Date、From、To、Subject、Message-ID等),但通过 metadata 你可以直接在 R2 控制台或 API 里浏览关键信息,不需要下载并解析.eml文件。 -
如果你的处理函数拆分在多个模块里,把
rawBuffer作为参数传递:
// index.ts
const rawBuffer = await new Response(message.raw).arrayBuffer();
const email = await parseEmail(rawBuffer);
await handleServiceEmail(message, env, email, rawBuffer);
// parse.ts
export async function parseEmail(rawBuffer: ArrayBuffer) {
const parser = new PostalMime();
return parser.parse(rawBuffer);
}
// service-email.ts
export async function handleServiceEmail(
message: ForwardableEmailMessage,
env: Cloudflare.Env,
email: Email,
rawBuffer: ArrayBuffer,
) {
// 备份到 R2
await env.R2_BUCKET.put(`emails/${Date.now()}.eml`, rawBuffer);
// 处理业务...
}
总结
| 坑 |
原因 |
解决方案 |
|---|---|---|
|
|
目标地址的 Email Routing 规则指向当前 Worker |
不用 |
|
|
目标地址未在 Email Routing 中验证 |
不用 |
|
|
|
先缓存到 |
核心建议:如果你的需求是”备份邮件”而不是”让人收到转发的邮件”,直接用 R2 存 .eml 文件,比 message.forward() 可靠得多。 没有地址验证的限制,没有同 Worker 的限制,没有 SPF/DKIM 导致进垃圾箱的风险,而且 .eml 文件包含完整的邮件原始信息,随时可以用任何邮件客户端打开查看。
话说回来,其实 Email Routing 在 AI 时代非常好用,我推荐大家好好开发一下。
有任何问题和意见,欢迎留言讨论分享。
相关文章
独立开发周记 · 2026-05-04 → 2026-05-10
一周九个项目并行,共189个 commit。muicv 快速迭代完善语音输入与本地/云同步,free-ai-api 批量接入多家模型并扩展到6语,多个新项目上线 Cloudflare。
Next.js 图片怎么这么贵?!——Cloudflare 原生 Image Resizing 避坑与最佳实践
Why Next.js default image optimization on Cloudflare can spike your bill and how to use a custom loa
在 Cloudflare 上 Vibe Coding 一个轻量 KMS:用 Workers 管理密钥(2026)
用 Cloudflare Workers 打造轻量级 KMS:基于 Web Crypto 与 Secrets Store/KV 实现密钥加密、轮换与访问控制的思路与实践,适合小团队的低成本密钥管理方案


