# WmailHub 邮件接收架构改造方案 > 版本:v2 > 更新时间:2026-09-30 > 项目:/home/bigcode/data/dev/bun/WmailHub ## 1. 最终架构 WmailHub 第一版正式采用两层邮件接收架构: Internet | v Cloudflare Email Routing | v Email Worker | | HTTPS + HMAC v WmailHub Console 删除旧方案中的 Node。 旧方案: Cloudflare -> Email Worker -> Node -> Console 新方案: Cloudflare -> Email Worker -> Console Cloudflare Email Workers 官方 API 支持 email handler、message.from、message.to、message.raw 和 message.setReject();Workers 也可以通过 fetch() 调用外部 HTTPS API。因此 Node 在邮件链路中没有必要。 ## 2. 核心原则 1. 不自建 SMTP、IMAP、POP3。 2. 不部署邮件转发 Node。 3. Email Worker 是 SMTP 入口。 4. Console 是唯一业务控制中心。 5. Worker 不保存邮箱业务数据。 6. Worker 在 SMTP 接收阶段查询 Console。 7. Console 明确允许时,Worker 接收邮件。 8. Console 明确拒绝时,Worker 调用 message.setReject()。 9. Console 超时或 5xx 不能被解释成邮箱不存在。 10. 邮箱、域名、启用状态全部由 Console 管理。 ## 3. 邮件接收流程 邮件进入 Email Worker 后,第一步不是保存邮件,而是询问 Console: POST /internal/email/authorize 请求: { "recipient": "test@example.com", "sender": "sender@example.net" } Console 返回三种明确状态。 ### ACCEPT { "decision": "accept" } Worker 继续接收邮件,然后提交: POST /internal/email/ingest ### REJECT 例如邮箱不存在: { "decision": "reject", "code": "MAILBOX_NOT_FOUND", "reason": "Mailbox does not exist" } Worker: message.setReject("Mailbox does not exist"); return; 这是 SMTP 阶段的永久拒绝。 ### TEMPORARY_ERROR 例如 Console 超时、网络失败、5xx: { "decision": "temporary_error" } 不能转换成 MAILBOX_NOT_FOUND,也不能伪造永久拒绝。 第一版采用安全失败策略:让 Worker 处理异常,由 Cloudflare 的邮件处理机制负责后续失败行为;后续根据实际测试补充重试策略。 ## 4. 为什么删除 Node Node 原来的职责只是: 接收邮件 -> 转发邮件 这会额外产生: - 一次网络跳转 - 一套部署 - 一套认证 - 一个故障点 - 一套重试 - 一套日志 - 一套版本管理 Email Worker 已经能够: - 接收邮件 - 读取 envelope sender / recipient - 执行业务前置判断 - 调用 HTTPS API - reject - 将原始邮件交给 Console 因此 Node 没有独立价值。 ## 5. Email Worker 职责 Worker 只负责: 1. 接收 Cloudflare Email Routing 邮件。 2. 读取 message.from。 3. 读取 message.to。 4. 调用 Console authorize API。 5. 明确拒绝时执行 message.setReject()。 6. 明确允许时调用 Console ingest API。 7. 处理内部异常。 Worker 不负责: - 创建邮箱 - 删除邮箱 - 域名管理 - 用户权限 - Webhook - API - MCP - D1 业务查询 - R2 业务存储 - TTL - 邮箱业务规则 ## 6. Console 职责 Console 是唯一业务控制中心。 负责: - Cloudflare OAuth - Cloudflare Account - Domain - Mailbox - 邮箱启用/禁用 - Email Storage - Webhook - REST API - MCP - TTL Cleanup - Cloudflare Email Routing 配置 - Email Worker 部署 - Worker Credential Console 是“是否允许接收邮件”的唯一事实来源。 ## 7. Authorization API 新增: POST /internal/email/authorize 请求: { "recipient": "test@example.com", "sender": "sender@example.net" } 可选: { "recipient": "test@example.com", "sender": "sender@example.net", "message_id": "" } 响应只能使用明确 decision: accept reject temporary_error reject 的 code 至少包括: DOMAIN_NOT_FOUND DOMAIN_DISABLED MAILBOX_NOT_FOUND MAILBOX_DISABLED RECEIVING_DISABLED 不要只返回 exists=true/false,因为未来拒绝条件不只是邮箱是否存在。 ## 8. Worker -> Console 安全协议 Authorization 和 Ingest 都必须是内部 API,不能匿名访问。 使用: HTTPS + Worker ID + timestamp + nonce + body SHA256 + HMAC-SHA256 请求头: Authorization: WmailHub-Worker X-WmailHub-Timestamp: X-WmailHub-Nonce: X-WmailHub-Body-SHA256: X-WmailHub-Signature: X-WmailHub-Protocol: 1 Canonical String: METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + BODY_SHA256 Console 必须: 1. 校验 Worker ID。 2. 校验 timestamp。 3. 防止 nonce 重放。 4. 校验 body hash。 5. 校验 HMAC。 6. 校验 Worker 状态。 7. 校验 Worker 所属 Cloudflare Account。 ## 9. Mail Ingest API 新增: POST /internal/email/ingest 只有 authorize 返回 accept 后才能调用。 建议: Content-Type: message/rfc822 HTTP body 直接传递原始 MIME,不要把整个 MIME 转成 JSON 字符串。 Console 收到后: 1. 验证 Worker 身份。 2. 验证 recipient。 3. 创建 Email metadata。 4. 将 raw MIME 写入 R2。 5. 保存附件信息。 6. 触发 Webhook。 7. 返回成功。 ## 10. 两阶段模型 第一阶段: Email Worker | v /authorize | +-- accept | +-- reject | +-- temporary_error 这一阶段只回答: “这封邮件是否应该由 WmailHub 接收?” 第二阶段: Email Worker | v /ingest | +-- D1 +-- R2 +-- Webhook 这样 SMTP 判断和邮件存储完全分离。 ## 11. 动态邮箱 Console 创建: support@example.com 下一封邮件: Worker -> authorize -> accept -> ingest 删除或禁用后: Worker -> authorize -> reject -> message.setReject() Worker 不需要保存 mailbox list,也不需要 Node 同步配置。 ## 12. Cloudflare Email Routing 动态邮箱场景推荐把域名邮件通过 catch-all Worker 交给 WmailHub。 例如: *@example.com | v WmailHub Email Worker 这样即使: unknown@example.com 在 Console 中不存在,也能进入 Worker,由 Console 判断后执行 message.setReject()。 如果只为现有邮箱创建 literal routing,那么不存在的地址可能根本不会进入 Worker,因此无法由 WmailHub 自己完成 SMTP reject。 ## 13. 数据模型 删除 Node 作为邮件链路中的核心模型。 保留: ### CloudflareAccount - id - account_id - oauth_connection_id - status ### Domain - id - cloudflare_account_id - name - status ### Mailbox - id - domain_id - address - status - created_at - updated_at ### Email - id - mailbox_id - message_id - sender - recipient - subject - received_at - expires_at - raw_object_key ### EmailAttachment 保留。 ### Webhook 保留。 ### WebhookDelivery 保留。 ### WorkerCredential 用于 Worker 与 Console 内部认证: - id - cloudflare_account_id - worker_id - secret - status - created_at - revoked_at ## 14. Cloudflare OAuth 旧的“Add Node”概念删除。 改为: Connect Cloudflare Account 流程: Console | v Cloudflare OAuth | v 用户授权 Account | v Console | +-- 保存 Account +-- 配置 Email Routing +-- 部署 Email Worker +-- 配置 Worker Secret +-- 配置 Domain 用户不需要执行安装命令。 ## 15. 故障策略 ### Console 正常 authorize -> accept/reject ### Console 明确 reject message.setReject() ### Console 5xx 不能当作邮箱不存在。 ### Console timeout 不能当作邮箱不存在。 ### Ingest 失败 不能伪造业务 reject。 必须记录处理失败,并按照 Cloudflare Email Worker 的实际投递/重试行为处理。 ## 16. 不实现 NDR 第一版明确不采用: 250 OK | v 异步发现邮箱不存在 | v WmailHub 自己生成 NDR 原因: - 增加 SMTP outbound - 增加 DSN/NDR 实现 - 增加 SPF/DKIM/DMARC 发送问题 - 增加 bounce loop 风险 - 增加 backscatter 风险 - 增加系统复杂度 本版本直接使用 Cloudflare Email Worker 的 message.setReject()。 ## 17. 性能 第一版每封邮件: 1 次 /authorize + 1 次 /ingest 这是有意选择的。 暂不加入: - KV mailbox cache - Durable Object - mailbox synchronization - Node queue - 本地 mailbox database 未来如果 authorize 成为瓶颈,再考虑缓存。 但缓存必须接受邮箱创建、删除和停用存在传播延迟的可能性。 ## 18. 安全 必须防止公网直接调用: /internal/email/authorize /internal/email/ingest 必须: - HTTPS - HMAC - timestamp - nonce - body hash - Worker credential revoke - 防重放 - 不记录完整 MIME - 不泄露数据库内部错误 - reject reason 不暴露敏感信息 ## 19. 文档同步修改 以下旧文档必须同步修改: 01_ARCHITECTURE.md 02_CLOUDFLARE_OAUTH.md 03_NODE.md 04_NODE_PROTOCOL.md 05_DATA_MODEL.md 06_EMAIL_LIFECYCLE.md 07_WEBHOOK_API_MCP.md 08_SECURITY.md 09_CLOUDFLARE_DEPLOYMENT.md 10_IMPLEMENTATION_PLAN.md 具体: - 03_NODE.md:删除 Node 邮件转发设计。 - 04_NODE_PROTOCOL.md:改成 Worker-Console Protocol。 - 02_CLOUDFLARE_OAUTH.md:Add Node 改成 Connect Cloudflare Account + 自动部署 Worker。 - 05_DATA_MODEL.md:删除 Node 邮件转发相关模型。 - 06_EMAIL_LIFECYCLE.md:改成 Worker -> Console。 - 09_CLOUDFLARE_DEPLOYMENT.md:增加 catch-all Worker routing。 - 10_IMPLEMENTATION_PLAN.md:重新排列实施阶段。 ## 20. 实施顺序 ### Phase 1 重构 Console: - Cloudflare Account - Domain - Mailbox - WorkerCredential ### Phase 2 实现: POST /internal/email/authorize ### Phase 3 实现 Email Worker: - email handler - HMAC - authorize - setReject - ingest ### Phase 4 实现: POST /internal/email/ingest ### Phase 5 Cloudflare 自动配置: - Email Routing - catch-all Worker - Worker deployment - Worker secrets ### Phase 6 邮件存储: - D1 - R2 - MIME - Attachment ### Phase 7 - Webhook - REST API - MCP - TTL cleanup ### Phase 8 完整测试: 1. 存在邮箱 -> 接收成功。 2. 不存在邮箱 -> message.setReject()。 3. 禁用邮箱 -> message.setReject()。 4. 禁用域名 -> message.setReject()。 5. Console timeout -> 不能变成 mailbox not found。 6. Console 5xx -> 不能变成 mailbox not found。 7. ingest 失败 -> 不伪造业务拒绝。 8. Worker credential revoke -> Worker 无法访问 Console。 9. 新建邮箱后测试立即接收。 10. 删除邮箱后测试立即拒绝。 11. 多域名并发测试。 12. catch-all 地址测试。 ## 21. 最终验收标准 最终运行架构必须是: Internet | v Cloudflare Email Routing | v Email Worker | v WmailHub Console | +-- D1 +-- R2 +-- Webhook +-- API +-- MCP 不得存在: Email Worker -> Node -> Console WmailHub 第一版的邮件入口、SMTP 决策和业务控制关系正式确定为: Cloudflare -> Email Worker -> Console 其中: - Worker = 邮件入口 + SMTP reject 执行者 - Console = 邮箱规则 + 邮件业务控制中心 - D1/R2 = Console 的数据层 - Node = 从 WmailHub 邮件架构中移除