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:
@@ -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 邮件架构中移除
|
||||
Reference in New Issue
Block a user