- 两阶段接收:POST /internal/email/authorize(accept/reject/temporary_error)+ POST /internal/email/ingest(raw MIME 直传) - Worker-Console 协议:Authorization: WmailHub-Worker + timestamp/nonce/body-hash/HMAC,nonce 防重放,凭据撤销即时生效 - 数据模型:nodes/node_credentials -> cloudflare_accounts/worker_credentials(迁移 0003),emails/domains 归属 account_id - 接入流程 Connect Cloudflare Account:OAuth 自动同步域名 + 部署账号级 Email Worker + 签发 Worker 凭据;rotate 自动重部署 - 动态邮箱:创建/删除/禁用下一封邮件立即生效;拒绝码 DOMAIN_*/MAILBOX_*;超时/5xx 不伪装成邮箱不存在 - 删除 node/ 目录、旧 /api/internal/nodes 协议、ingress/enrollment 概念;dev.ts 简化为 console + email worker 双进程 - 前端:账号页 Email Worker/凭据管理,设置窗口移除默认 Ingress,仪表盘/文案同步;版本 2.0.0 - 测试 62 项全过(新增 authorize 决策矩阵 + Worker 两阶段临时错误语义);本机虚拟 E2E 12 场景全通 - docs 00-10 同步改写;README 更新
12 KiB
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. 核心原则
- 不自建 SMTP、IMAP、POP3。
- 不部署邮件转发 Node。
- Email Worker 是 SMTP 入口。
- Console 是唯一业务控制中心。
- Worker 不保存邮箱业务数据。
- Worker 在 SMTP 接收阶段查询 Console。
- Console 明确允许时,Worker 接收邮件。
- Console 明确拒绝时,Worker 调用 message.setReject()。
- Console 超时或 5xx 不能被解释成邮箱不存在。
- 邮箱、域名、启用状态全部由 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 只负责:
- 接收 Cloudflare Email Routing 邮件。
- 读取 message.from。
- 读取 message.to。
- 调用 Console authorize API。
- 明确拒绝时执行 message.setReject()。
- 明确允许时调用 Console ingest API。
- 处理内部异常。
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": "<abc@example.net>"
}
响应只能使用明确 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 <worker_id>
X-WmailHub-Timestamp: <unix_seconds>
X-WmailHub-Nonce: <random>
X-WmailHub-Body-SHA256: <sha256>
X-WmailHub-Signature: <hmac>
X-WmailHub-Protocol: 1
Canonical String:
METHOD + "\n" +
PATH + "\n" +
TIMESTAMP + "\n" +
NONCE + "\n" +
BODY_SHA256
Console 必须:
- 校验 Worker ID。
- 校验 timestamp。
- 防止 nonce 重放。
- 校验 body hash。
- 校验 HMAC。
- 校验 Worker 状态。
- 校验 Worker 所属 Cloudflare Account。
9. Mail Ingest API
新增:
POST /internal/email/ingest
只有 authorize 返回 accept 后才能调用。
建议:
Content-Type: message/rfc822
HTTP body 直接传递原始 MIME,不要把整个 MIME 转成 JSON 字符串。
Console 收到后:
- 验证 Worker 身份。
- 验证 recipient。
- 创建 Email metadata。
- 将 raw MIME 写入 R2。
- 保存附件信息。
- 触发 Webhook。
- 返回成功。
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
- 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
完整测试:
- 存在邮箱 -> 接收成功。
- 不存在邮箱 -> message.setReject()。
- 禁用邮箱 -> message.setReject()。
- 禁用域名 -> message.setReject()。
- Console timeout -> 不能变成 mailbox not found。
- Console 5xx -> 不能变成 mailbox not found。
- ingest 失败 -> 不伪造业务拒绝。
- Worker credential revoke -> Worker 无法访问 Console。
- 新建邮箱后测试立即接收。
- 删除邮箱后测试立即拒绝。
- 多域名并发测试。
- 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 邮件架构中移除