Files
wmailhub/docs/11_REFACTOR_PLAN.md
T
bigcode fec96e49cc 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 更新
2026-09-30 13:10:26 +08:00

677 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 邮件架构中移除