feat: v2.0.0 邮件接收架构改造 —— 移除 Node,Email Worker 直连 Console(docs/11)

- 两阶段接收: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 更新
This commit is contained in:
bigcode
2026-09-30 13:10:26 +08:00
parent 421cfb2f04
commit fec96e49cc
56 changed files with 2584 additions and 1416 deletions
+676
View File
@@ -0,0 +1,676 @@
# 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": "<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 必须:
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 邮件架构中移除