- 两阶段接收: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 更新
677 lines
12 KiB
Markdown
677 lines
12 KiB
Markdown
|
||
# 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 邮件架构中移除
|