
Cloudflare Email Workers 常用模式实战从白名单过滤到多租户路由的 10 个可落地方案【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills本篇文章以 Cloudflare Email Routing / Email Workers 的常用模式patterns参考文档为核心系统梳理了 10 个可直接复制运行的 TypeScript 处理模式白名单/黑名单、正文解析、垃圾邮件过滤、R2 归档、KV 元数据、主题路由、自动回复、附件提取、D1 日志与多租户路由。你将掌握ForwardableEmailMessage的完整用法、postal-mime 的解析技巧、各存储绑定R2/KV/D1的接入方式以及message.raw流只能消费一次等关键避坑点可直接用于构建自己的邮件网关。前置认知Email Routing 与 Email Worker 的协作架构在进入模式之前先明确两种处理方式的边界。Cloudflare Email Routing 允许你的自定义域名接收邮件并转发到已验证的目的地址免费且不存储邮件而 Email Worker 则是运行在 Workers 运行时上的自定义 TypeScript 处理器拥有对邮件的完整访问权适合复杂逻辑。整体数据流如下摘自 email-routing READMEInternet → MX Records → Cloudflare Email Routing ├─ Routing Rules (dashboard) └─ Email Worker (your code) ├─ Forward to destination ├─ Reject with reason ├─ Store in R2/KV/D1 └─ Send outbound (SendEmail)简单转发需求用 Dashboard 规则即可涉及过滤、解析、存储、拒绝等复杂逻辑时就要写 Email Worker。Worker 的核心入口是一个email处理器接收ForwardableEmailMessage对象interface ExportedHandlerEnv unknown { email?(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext): void | Promisevoid; }ForwardableEmailMessage的关键成员完整定义见 api.md成员类型说明fromstring信封发送者MAIL FROM注意不是 Header 里的 Fromtostring信封收件人RCPT TO注意不是 Header 里的 ToheadersHeaders标准 Web API Headers 对象Subject、From、Message-ID 等rawReadableStream原始 MIME 消息流只能消费一次setReject(reason)void以 SMTP 5xx 拒绝邮件发件人可能收到退信forward(rcptTo, headers?)Promisevoid转发到已验证的目的地址可附加自定义头仅允许X-*一个需要始终牢记的区分message.from/message.to是 SMTP 信封地址可信、用于路由和安全判断而message.headers.get(from)等 Header 地址是展示地址可能被伪造。下文的所有模式都将遵循这一原则。模式 1白名单 / 黑名单Allowlist/Blocklist最基础的安全模式只有白名单内的发件人才能投递其余直接拒绝。注意此处用message.from信封地址做判断而不是可伪造的 Header From// Allowlist const allowed [userexample.com, trustedcorp.com]; if (!allowed.includes(message.from)) { message.setReject(Not allowed); return; } await message.forward(inboxcorp.com);setReject会返回永久的 SMTP 5xx 错误邮件不会被投递。黑名单模式只需把条件反转blocked.includes(...)时拒绝。更灵活的做法是从 KV 读取名单动态维护见模式 5 的 KV 用法。模式 2解析邮件正文Parse Email Body要读取邮件的主题、正文、HTML 与附件必须使用postal-mime库npm 安装npm install postal-mime解析 MIME。关键约束message.raw是单次使用的流必须在任何异步操作之前立刻消费为 ArrayBufferimport PostalMime from postal-mime; export default { async email(message, env, ctx) { // CRITICAL: Consume stream immediately const raw await message.raw.arrayBuffer(); const parser new PostalMime(); const email await parser.parse(raw); console.log({ subject: email.subject, text: email.text, html: email.html, from: email.from.address, attachments: email.attachments.length }); await message.forward(inboxcorp.com); } } satisfies ExportedHandler;postal-mime 解析结果的结构ParsedEmail包含subject、from含name/address、to、cc、bcc、text、html、messageId、references、attachments每项含filename、mimeType、content等字段。这个解析结果是后续所有读内容类模式的基础。模式 3垃圾邮件过滤Spam FilterCloudflare 会在x-cf-spamh-score头中给出垃圾邮件评分读取并设置阈值即可const score parseFloat(message.headers.get(x-cf-spamh-score) || 0); if (score 5) { message.setReject(Spam detected); return; } await message.forward(inboxcorp.com);注意headers.get()在头缺失时返回null因此务必用|| 0兜底否则parseFloat(null)会得到NaN比较结果恒为 false过滤逻辑失效。除评分外还可读取authentication-results头检查 SPF/DKIM/DMARC 是否通过见 gotchas.md 的 Auth Troubleshooting 章节。模式 4归档到 R2Archive to R2把原始邮件以.eml格式存入 R2 对象存储实现零成本归档与合规留存。需要在wrangler.jsonc中声明r2_buckets绑定示例见 configuration.md{ name: email-processor, r2_buckets: [{ binding: R2, bucket_name: emails }] }interface Env { R2: R2Bucket; } export default { async email(message, env, ctx) { const raw await message.raw.arrayBuffer(); const key ${new Date().toISOString()}-${message.from}.eml; await env.R2.put(key, raw, { httpMetadata: { contentType: message/rfc822 } }); await message.forward(inboxcorp.com); } } satisfies ExportedHandlerEnv;contentType: message/rfc822是标准电子邮件 MIME 类型确保.eml文件可被主流邮件客户端直接打开。以时间戳发件人作为 key 可保证唯一性并便于检索。模式 5KV 存储元数据Store Metadata in KV归档完整邮件用 R2但若只想保留轻量元数据用于统计、检索索引、审计KV 是更合适的选择。KV 是键值存储适合写入高频、读取低频的场景import PostalMime from postal-mime; interface Env { KV: KVNamespace; } export default { async email(message, env, ctx) { const raw await message.raw.arrayBuffer(); const parser new PostalMime(); const email await parser.parse(raw); const metadata { from: email.from.address, subject: email.subject, timestamp: new Date().toISOString(), size: raw.byteLength }; await env.KV.put(email:${Date.now()}, JSON.stringify(metadata)); await message.forward(inboxcorp.com); } } satisfies ExportedHandlerEnv;KV.put前先将原始流缓冲为raw再解析正是为了避免重复消费流见模式 2 的 CRITICAL 注释。模式 6基于主题的路由Subject-Based Routing按邮件主题关键词将工单分流到不同团队例如[urgent]走 oncall、[billing]走财务、[support]走客服。主题来自 Header读取时要注意可能缺失用?.toLowerCase() || 兜底export default { async email(message, env, ctx) { const subject message.headers.get(subject)?.toLowerCase() || ; if (subject.includes([urgent])) { await message.forward(oncallcorp.com); } else if (subject.includes([billing])) { await message.forward(billingcorp.com); } else if (subject.includes([support])) { await message.forward(supportcorp.com); } else { await message.forward(generalcorp.com); } } } satisfies ExportedHandler;所有forward()的目标地址都必须是已在 Cloudflare Dashboard 验证过的地址否则转发会失败。模式 7自动回复Auto-Reply对每位发件人自动回复确认邮件同时利用 KV 的expirationTtl实现去重同一message-id只回复一次。回复通过SendEmail绑定send_email配置发送事务性邮件// wrangler.jsonc 中的 SendEmail 绑定 { send_email: [{ name: EMAIL }] }interface Env { EMAIL: SendEmail; REPLIED: KVNamespace; } export default { async email(message, env, ctx) { const msgId message.headers.get(message-id); if (msgId await env.REPLIED.get(msgId)) { await message.forward(archivecorp.com); return; } ctx.waitUntil((async () { await env.EMAIL.send({ from: noreplyyourdomain.com, to: message.from, subject: Re: (message.headers.get(subject) || ), text: Thank you. Well respond within 24h. }); if (msgId) await env.REPLIED.put(msgId, 1, { expirationTtl: 604800 }); })()); await message.forward(supportcorp.com); } } satisfies ExportedHandlerEnv;这里有两个关键设计其一回复动作放入ctx.waitUntil(...)后台执行不阻塞主流程的转发其二expirationTtl: 6048007 天让去重键自动过期避免 KV 无限膨胀。注意 SendEmail 有约束发件地址必须是已验证域名、仅限事务性邮件不支持营销/群发、不支持附件、由 Cloudflare 自动签名 DKIM。模式 8提取附件Extract Attachments利用 postal-mime 解析出的attachments数组把每个附件单独存入 R2实现文档管理系统import PostalMime from postal-mime; interface Env { ATTACHMENTS: R2Bucket; } export default { async email(message, env, ctx) { const parser new PostalMime(); const email await parser.parse(await message.raw.arrayBuffer()); for (const att of email.attachments) { const key ${Date.now()}-${att.filename}; await env.ATTACHMENTS.put(key, att.content, { httpMetadata: { contentType: att.mimeType } }); } await message.forward(inboxcorp.com); } } satisfies ExportedHandlerEnv;att.content是Uint8Array可直接作为R2.put的数据体att.mimeType如application/pdf原样透传给httpMetadata.contentType保证下载时 Content-Type 正确。邮件最大 25 MiB免费与付费计划相同超大附件的防护手段见下文常见坑。模式 9写入 D1 日志Log to D1把每封邮件的元数据写入 D1Cloudflare 的 SQLite 数据库形成可查询的审计日志。同样要在wrangler.jsonc中声明d1_databases绑定{ d1_databases: [{ binding: DB, database_id: def456 }] }import PostalMime from postal-mime; interface Env { DB: D1Database; } export default { async email(message, env, ctx) { const parser new PostalMime(); const email await parser.parse(await message.raw.arrayBuffer()); ctx.waitUntil( env.DB.prepare(INSERT INTO log (ts, from_addr, subj) VALUES (?, ?, ?)) .bind(new Date().toISOString(), email.from.address, email.subject || ) .run() ); await message.forward(inboxcorp.com); } } satisfies ExportedHandlerEnv;数据库写入同样放在ctx.waitUntil中异步执行prepare().bind().run()是 D1 标准参数化查询写法可避免 SQL 注入。日志写入失败不应阻塞邮件转发主流程。模式 10多租户路由Multi-Tenant面向 SaaS 场景根据收件人地址中的子域名如tenant1support.example.com中的tenant1从 KV 查询租户配置动态决定转发目标。message.to是信封收件人地址interface Env { TENANTS: KVNamespace; } export default { async email(message, env, ctx) { const subdomain message.to.split()[1].split(.)[0]; const config await env.TENANTS.get(subdomain, json) as { forward: string } | null; if (!config) { message.setReject(Unknown tenant); return; } await message.forward(config.forward); } } satisfies ExportedHandlerEnv;KV.get(key, json)会自动反序列化 JSON配合类型断言和null检查未知租户直接拒绝。这个模式把路由规则从 Dashboard 静态配置中解放出来变成可动态写入 KV 的业务数据。模式汇总对比PatternUse CaseStorageAllowlistSecurityNoneParseBody/attachmentsNoneSpam FilterReduce spamNoneR2 ArchiveEmail storageR2KV MetaAnalyticsKVSubject RouteDept routingNoneAuto-ReplySupportKVAttachmentsDoc mgmtR2D1 LogAudit trailD1Multi-TenantSaaSKV选择存储的核心原则完整邮件/二进制附件 → R2轻量元数据/配置/去重标记 → KV需要 SQL 查询的审计轨迹 → D1。不需要存储的纯路由/过滤场景则完全零存储依赖。关键避坑与调试Gotchas原模式文档强调的部分约束在此结合 gotchas.md 汇总为三个高频问题1. 流只能消费一次最常见message.raw是ReadableStreamarrayBuffer()一次后即耗尽// ❌ 错误第二次读取会抛 stream already consumed const email1 await parser.parse(await message.raw.arrayBuffer()); const email2 await parser.parse(await message.raw.arrayBuffer()); // ✅ 正确先缓冲再复用 const raw await message.raw.arrayBuffer(); const email await parser.parse(raw);务必在任何异步操作如读 KV、调用外部 API之前完成消费。2. 信封地址 vs Header 地址// 路由/认证判断用信封地址 if (message.from trustedexample.com) { } // 展示给用户用 Header 地址 const display message.headers.get(from);3. 大邮件导致 CPU 超时免费计划 CPU 时间 10ms、付费计划 50ms邮件大小上限均为 25 MiB。解析大邮件前先按大小拦截重活放后台const size parseInt(message.headers.get(content-length) || 0) / 1024 / 1024; if (size 20) { message.setReject(Too large); return; } ctx.waitUntil(expensiveWork()); await message.forward(destexample.com);本地与线上调试本地运行npx wrangler dev后用 curl 向http://localhost:8787/__email发送message/rfc822格式的模拟邮件生产环境用npx wrangler tail查看实时日志。完整排查流程与 SPF/DKIM/DMARC 认证检查代码见 gotchas.md。扩展阅读与配置衔接email-routing README产品概览、阅读顺序与决策树需要收件走哪个方案、需要发件怎么选、遇到问题查哪里api.mdForwardableEmailMessage完整类型、SendEmail 绑定与 REST API 端点含创建路由规则示例configuration.mdwrangler.jsonc全量配置、本地开发、部署接入 Dashboard/API、DNS 记录与 TypeScript 环境搭建gotchas.md限流限额表、常见错误与认证排障。需要发送带回复线程的邮件或使用message.reply()时可进一步参考 email-workers/api.md 中关于mimetext构造 MIME 消息与In-Reply-To/References线程头最多 100 条 References的说明。上述 10 个模式既可独立使用也能组合成完整的邮件处理流水线先过滤垃圾模式 3→ 白名单校验模式 1→ 解析正文/附件模式 2、8→ 落库归档模式 4、5、9→ 按规则转发或自动回复模式 6、7覆盖从安全到合规的典型生产需求。【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考