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() 已经不太满意了。它的限制太多:

  1. 不能转发到路由回同一个 Worker 的地址

  2. 目标地址必须提前验证

  3. 转发后的邮件可能因为 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” 错误。

这个问题在邮件处理场景里特别容易遇到,因为你通常需要:

  1. 解析邮件(提取标题、正文、附件)

  2. 保存原始邮件(备份、审计)

两个操作都需要读取 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}`);
}

几个要点:

  1. rawBuffer 是普通的 ArrayBuffer,可以无限次读取,不存在 “consumed” 的问题。

  2. R2 的 customMetadata 非常实用.eml 文件本身包含完整的邮件头(DateFromToSubjectMessage-ID 等),但通过 metadata 你可以直接在 R2 控制台或 API 里浏览关键信息,不需要下载并解析 .eml 文件。

  3. 如果你的处理函数拆分在多个模块里,把 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);
  // 处理业务...
}

总结

原因

解决方案

cannot forward to same worker

目标地址的 Email Routing 规则指向当前 Worker

不用 forward(),改用 R2 备份

destination address not verified

目标地址未在 Email Routing 中验证

不用 forward(),改用 R2 备份

ReadableStream is disturbed

message.raw 只能读一次

先缓存到 ArrayBuffer,再复用

核心建议:如果你的需求是”备份邮件”而不是”让人收到转发的邮件”,直接用 R2 存 .eml 文件,比 message.forward() 可靠得多。 没有地址验证的限制,没有同 Worker 的限制,没有 SPF/DKIM 导致进垃圾箱的风险,而且 .eml 文件包含完整的邮件原始信息,随时可以用任何邮件客户端打开查看。

话说回来,其实 Email Routing 在 AI 时代非常好用,我推荐大家好好开发一下。

有任何问题和意见,欢迎留言讨论分享。

相关文章

觉得文章有帮助?

如果我的分享对你有所启发,欢迎通过赞助来支持我持续创作。

❤️ 赞助我

评论