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
+35 -40
View File
@@ -1,18 +1,19 @@
# WmailHub 开发设计总览
# WmailHub 开发设计总览(v2,docs/11)
## 1. 项目定位
WmailHub 是一个基于 Cloudflare Email Workers 的轻量邮件接收与转发平台。
核心原则:
核心原则:
- Cloudflare 负责邮件基础设施。
- Console 是控制平面,包含前端与管理 Node。
- Node 是邮件数据转发平面,无前端、无业务判断。
- Console 通过 Cloudflare OAuth 自动完成 Node、Domain、Mailbox、Email Worker 的配置。
- Node 只能处理被授权 Cloudflare Account 下的域名邮箱。
- Node 与 Console 的通信必须使用 HTTPS + 应用层签名。
- 邮件默认短期保存,例如 1 天,过期自动删除。
- 不自建 SMTP、IMAP、POP3。
- 不部署邮件转发 Node(v1 的 Node 已从邮件链路移除)。
- Email Worker 是邮件入口 + SMTP reject 执行者。
- Console 是唯一业务控制中心,也是"是否允许接收邮件"的唯一事实来源。
- Worker 不保存邮箱业务数据;邮箱、域名、启用状态全部由 Console 管理。
- Console 明确允许时,Worker 接收邮件;明确拒绝时执行 `message.setReject()`;
Console 超时或 5xx 不能被解释成邮箱不存在。
- 邮件默认短期保存,例如 1 天,过期自动删除。
- 邮件进入 Console 后才能进入 Webhook、API、MCP 等业务能力。
## 2. 总体架构
@@ -25,14 +26,12 @@ Cloudflare Email Routing
|
v
Cloudflare Email Worker
|
| HTTPS + HMAC(authorize / ingest)
v
Node
| HTTPS + Request Signature
v
Console Management Node
Console
|
+--> Database
+--> D1
+--> R2
+--> Webhook
+--> API
+--> MCP
@@ -41,60 +40,56 @@ Console Management Node
## 3. Console
Console = Frontend + Management Node。
Console = Frontend + Management API。
负责:Cloudflare OAuth、Cloudflare Account、Node、Domain、Mailbox、Email、Webhook、API、MCP、Node 安装与配置、数据存储、TTL 清理调度。
负责:Cloudflare OAuth、Cloudflare Account、Domain、Mailbox、邮箱启用/禁用、Email Storage、
Webhook、REST API、MCP、TTL Cleanup、Cloudflare Email Routing 配置、Email Worker 部署、
Worker Credential。
用户不执行 Node 安装命令。点击 Add Node 后进入 Cloudflare OAuth,授权后由 Cloudflare API 自动完成安装和配置。
用户不执行安装命令。点击「添加账号」进入 Cloudflare OAuth(Connect Cloudflare Account),
授权后由 Cloudflare API 自动完成域名同步、Worker 部署与凭据配置。
## 4. Node
## 4. Email Worker
Node 是纯后端组件,只做:接收 Cloudflare Email Worker 的邮件、将邮件安全转发到 Console。
Worker 只做:接收 Cloudflare Email Routing 邮件 -> authorize(询问 Console)->
reject 时 `message.setReject()` -> accept 时把 raw MIME 提交 ingest。
Node 不做 Domain 判断、Mailbox 判断、邮件业务路由、Webhook、MCP、API、用户权限、邮件长期存储。
Node 收到什么就转发什么。
不做邮箱/域名管理、用户权限、Webhook、API、MCP、D1/R2 业务查询、TTL、邮箱业务规则。
## 5. 权限边界
一个 Node 与一个 Cloudflare Account 建立绑定。Node A 只能处理 Account A 下配置的 Email Worker,不允许处理其他 Account。
一个账号绑定一个 Cloudflare Account 与一个 Email Worker;Worker credential 只能处理
所属账号下的域名邮箱(docs/04 归属校验)。
Domain 与 Mailbox 的具体业务信息由 Console 管理,Node 不需要理解这些业务字段。
## 6. 核心邮件流程
## 6. 核心邮件流程(两阶段)
```text
Email
|
v
Cloudflare Email Routing
Cloudflare Email Routing(catch-all)
|
v
Email Worker
|
v
Node
|
v
Console
|
+--> Store
+--> Webhook
+--> API
+--> MCP
Console /internal/email/authorize
+-- accept -> /internal/email/ingest -> D1 + R2 + Webhook
+-- reject -> message.setReject()
+-- temporary_error -> 交由 Cloudflare 邮件机制重试
```
## 7. 生命周期
默认邮件 TTL 为 1 天。数据库记录 expires_at,Cron 定期删除过期记录及对应 R2 对象。
默认邮件 TTL 为 1 天。数据库记录 expires_at,Cron 定期删除过期记录及对应 R2 对象。
## 8. 第一版明确不做
- 自建 SMTP Server
- IMAP / POP3
- 发件服务器
- 发件服务器 / NDR(反站)
- 完整邮箱客户端
- 复杂邮件规则引擎
- AI 邮件分析
- 多租户复杂组织系统
- Node 端业务路由
- KV/DO 邮箱缓存与同步(性能需要时再评估,须接受传播延迟)
+41 -21
View File
@@ -1,31 +1,57 @@
# WmailHub 系统架构
# WmailHub 系统架构(v2,docs/11)
最终架构是两层邮件接收:
```text
Internet
|
v
Cloudflare Email Routing
|
v
Email Worker
|
| HTTPS + HMAC(authorize / ingest)
v
WmailHub Console
|
+-- D1
+-- R2
+-- Webhook
+-- API
+-- MCP
```
Node 已从邮件架构中移除(v1 的 `Email Worker -> Node -> Console` 链路废弃)。
## Console
Console = Frontend + Management Node,是系统控制中心。
Console = Frontend + Management API,是系统唯一业务控制中心。
负责 Cloudflare OAuth、Node、Domain、Mailbox、Email、Webhook、API、MCP 以及数据存储。
负责 Cloudflare OAuth、Cloudflare Account、Domain、Mailbox、邮箱启用/禁用、Email Storage(D1 + R2)、Webhook、REST API、MCP、TTL Cleanup、Email Routing 配置、Email Worker 部署与 Worker Credential。
## Node
Console 是"是否允许接收邮件"的唯一事实来源。
Node 是纯后端、无前端的邮件转发节点。
## Email Worker
唯一核心流程:Cloudflare Email Worker -> Node -> Console。
Email Worker 是邮件入口 + SMTP reject 执行者,只负责:
Node 不负责 Domain、Mailbox、Webhook、MCP、API、用户权限或业务路由。
1. 接收 Cloudflare Email Routing 邮件,读取 `message.from` / `message.to`;
2. 调用 Console `POST /internal/email/authorize` 询问是否接收;
3. 明确拒绝时执行 `message.setReject()`;
4. 明确允许时把 raw MIME 提交到 Console `POST /internal/email/ingest`;
5. 处理内部异常(超时/5xx 一律按临时错误,不伪装成邮箱不存在)。
## Cloudflare Worker
Email Worker 是邮件入口,只负责接收 Cloudflare Email Event 并安全转发给 Node。
Worker 不创建邮箱、不做域名管理、不存业务数据、不实现 Webhook/API/MCP。
## 控制平面
```text
Cloudflare Account
+-- Node
+-- Domain
+-- Mailbox
+-- Email Worker
+-- Email Worker(账号级一个)
+-- WorkerCredential
```
这些资源关系由 Console 管理。
@@ -33,15 +59,9 @@ Cloudflare Account
## 数据平面
```text
Email Worker -> Node -> Console
Email Worker -> Console(authorize + ingest)-> D1 / R2 / Webhook
```
Node 不参与业务决策。
## 归属范围
## Node Scope
第一版一个 Node 绑定一个 Cloudflare Account。Node A 不允许处理 Account B 的资源。
## 扩展
未来可以有多个 Node,例如 Node JP、Node US、Node EU。每个 Node 默认仍绑定一个 Cloudflare Account。
一个账号绑定一个 Cloudflare Account 与一个 Email Worker;Worker credential 不允许处理其它账号的域名。
+29 -6
View File
@@ -1,11 +1,34 @@
# Cloudflare OAuth 与自动安装
# Cloudflare OAuth 与自动接入(docs/11 §14)
Console 的 Add Node 入口必须打开 Cloudflare OAuth 授权页面。用户授权后,Console 通过 Cloudflare API 自动完成 Node、Email Worker、Domain、Mailbox 的配置。
旧「Add Node」概念已删除,改为 **Connect Cloudflare Account**。
用户不执行 shell 安装命令,也不手工创建 API Token。
## 流程
流程:Console -> Add Node -> Cloudflare OAuth -> 授权回调 -> 获取 Account/Zone -> 创建 Node Identity -> 创建 Node Credential -> 创建或更新 Email Worker -> 配置 Email Routing -> 创建 Domain/Mailbox -> Node Ready。
```text
Console
|
v
Cloudflare OAuth
|
v
用户授权 Account
|
v
Console
+-- 保存 Account
+-- Account / Zone 发现
+-- 配置 Domain(同步账号下全部域名,pending)
+-- 创建 catch-all Mailbox
+-- 部署账号级 Email Worker
+-- 签发 WorkerCredential(secret 注入 Worker 脚本)
```
Cloudflare credential 与 Node credential 必须分离。Cloudflare credential 加密存储,不进入前端和日志。
用户不需要执行安装命令,也不手工创建 API Token。
安装过程必须幂等,重复执行不能创建重复资源。OAuth state、授权回调和 Account ownership 必须验证。
## 安全
- Cloudflare credential(OAuth access token)AES-GCM 加密存储,不进入前端和日志。
- OAuth state 一次性(用后即删)、5 分钟过期、绑定账号(CSRF 防护)。
- Account ownership 以 token 实际可访问的 accounts 为准。
- 安装过程幂等,重复执行不产生重复资源。
- Worker credential 与 Cloudflare credential 分离;轮换 credential 后已配置域名自动重新部署。
+19 -37
View File
@@ -1,44 +1,26 @@
# Node 设计
# Node(已移除,docs/11 §4)
Node 是无前端、纯后端的邮件转发节点。
v2 起 WmailHub 邮件架构中**不存在 Node**。旧方案 `Cloudflare -> Email Worker -> Node -> Console`
废弃,新方案为 `Cloudflare -> Email Worker -> Console`。
核心流程:
## 为什么删除 Node
```text
Email Worker -> Node -> Console
```
Node 原来的职责只有「接收邮件 -> 转发邮件」,却额外引入:
## Node 职责
- 一次网络跳转
- 一套部署
- 一套认证
- 一个故障点
- 一套重试
- 一套日志
- 一套版本管理
- 接收 Email Worker 请求
- 验证 Worker 请求
- 获取邮件
- 安全转发到 Console
- 有限重试临时网络错误
- 心跳
- 上报版本和状态
Email Worker 已经能够接收邮件、读取 envelope sender/recipient、执行业务前置判断(authorize)、
调用 HTTPS API(ingest)、执行 `message.setReject()` 并把原始邮件交给 Console,因此 Node 没有独立价值。
## Node 不负责
## 现状
- Domain 路由
- Mailbox 路由
- Webhook
- MCP
- API
- 用户权限
- 邮件业务存储
- 邮件规则
Node 收到什么就转发什么。
## Node Scope
Node 只能服务创建它的 Cloudflare Account。不得通过普通业务请求把 Node 任意绑定到其他 Account。
## 配置
Node 至少需要 node_id、cloudflare_account_id、console_endpoint、credential。
credential secret 必须安全保存。
Node 可以有限重试 Console 网络错误,但不承担长期邮件队列职责。
- Console 的 `/api/internal/nodes/*` 内部协议与 `/internal/enroll` 端点已删除。
- 数据模型不再有 nodes/node_credentials;账号实体为 cloudflare_accounts,
Email Worker 与 Console 的认证凭据为 worker_credentials(docs/05)。
- 账号概念保留:「账号」= 已连接的 Cloudflare Account(Connect Cloudflare Account 流程,docs/02)。
+25 -32
View File
@@ -1,30 +1,12 @@
# Node 与 Console 通信协议
# Email Worker <-> Console 内部协议(docs/11 §8,协议版本 1)
Node -> Console 必须使用 HTTPS + HMAC 请求签名 + timestamp + nonce + body hash。
`/internal/email/authorize` 与 `/internal/email/ingest` 都是内部 API,不能匿名访问。
每个请求必须满足:HTTPS + Worker ID + timestamp + nonce + body SHA256 + HMAC-SHA256。
## Credential
## 请求头
```text
node_id
credential_id
credential_secret
cloudflare_account_id
```
Console 保存 secret 的安全摘要;Node 保存实际 secret。
## 签名
```text
body_hash = SHA256(raw_body)
canonical = METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + body_hash
signature = HMAC-SHA256(secret, canonical)
```
请求头:
```text
Authorization: WmailHub-Node <node_id>
Authorization: WmailHub-Worker <worker_id>
X-WmailHub-Timestamp: <unix_seconds>
X-WmailHub-Nonce: <random>
X-WmailHub-Body-SHA256: <hex>
@@ -32,18 +14,29 @@ X-WmailHub-Signature: <hex>
X-WmailHub-Protocol: 1
```
Console 拒绝 timestamp 超窗、重复 nonce、错误 signature、错误 body hash、已撤销 credential。默认 timestamp window 建议 5 分钟。
## 签名
```text
body_hash = SHA256(raw_body)
canonical = METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + body_hash
signature = HMAC-SHA256(worker_credential_secret, canonical)
```
## Console 必须校验
1. Worker ID(存在且 credential active);
2. timestamp(默认窗口 5 分钟);
3. nonce 不重复(唯一索引入库,主键冲突即 replay);
4. body hash;
5. HMAC;
6. credential 状态(撤销立即生效);
7. Worker 所属 Cloudflare 账号与收件域名一致。
## API
```text
POST /api/internal/nodes/{node_id}/emails
POST /api/internal/nodes/{node_id}/heartbeat
GET /api/internal/nodes/{node_id}/config
POST /internal/email/authorize { "recipient": "...", "sender": "...", "message_id": "..." }
POST /internal/email/ingest raw MIME(message/rfc822),收件人经 X-WmailHub-Recipient/Sender 头传递
```
邮件请求推荐 `Content-Type: message/rfc822`,body 直接使用 raw MIME。
Console 可以立即撤销 credential。Node 收到 401/403 后不得无限重试。
协议必须版本化。
Console 收到 401/403 的 Worker 应视为凭据失效,不得无限重试。协议必须版本化。
+40 -16
View File
@@ -1,34 +1,44 @@
# 数据模型
# 数据模型(v2,docs/11 §13)
核心实体:CloudflareAccount、Node、Domain、Mailbox、Email、EmailAttachment、Webhook、WebhookDelivery、OAuthConnection。
核心实体:CloudflareAccount、Domain、Mailbox、Email、EmailAttachment、Webhook(三级配置)、WebhookDelivery、OAuthConnection、WorkerCredential。
## nodes
Node 作为邮件链路核心模型已删除(旧 nodes / node_credentials 表由迁移 0003 移除,
账号实体更名为 cloudflare_accounts,凭据更换为 worker_credentials)。
## cloudflare_accounts
```text
id, name, cloudflare_account_id, credential_id, status, version, last_seen_at, created_at, updated_at
id, name, cloudflare_account_id, worker_id, status, provision_steps, created_at, updated_at
```
status:pending_oauth | provisioning | active | error | revoked。
worker_id 同时是 Email Worker 脚本名与 Worker -> Console 的身份 ID。
## domains
```text
id, cloudflare_account_id, node_id, name, status, worker_id, created_at, updated_at
id, cloudflare_account_id, account_id, name, status, worker_id, zone_id,
webhook_url, webhook_headers, webhook_key_enc, setup_error, created_at, updated_at
```
status:pending(未配置 Email Workers)| active | error。
## mailboxes
```text
id, domain_id, address, status, retention_seconds, created_at, updated_at
id, domain_id, address, status, retention_seconds, is_catch_all,
webhook_url, webhook_headers, webhook_key_enc, created_at, updated_at
```
## emails
```text
id, node_id, mailbox_id nullable, message_id, from_address, to_addresses,
id, account_id, mailbox_id nullable, message_id, from_address, to_addresses,
cc_addresses, subject, text_body, html_body, raw_storage_key, size,
received_at, expires_at, created_at
```
Node 可以不知道 mailbox_id。Console 收到邮件后,根据邮件内容和自身配置关联。
Console 收到邮件后根据收件人/内容与自身配置关联 mailbox。
## email_attachments
@@ -36,17 +46,17 @@ Node 可以不知道 mailbox_id。Console 收到邮件后,根据邮件内容
id, email_id, filename, content_type, size, storage_key, content_id
```
## webhooks
## Webhook(三级:邮箱 -> 域名 -> 全局)
域名/邮箱行内配置(webhook_url / webhook_headers / webhook_key_enc),
全局配置存 settings.global_webhook;独立 webhooks 实体已移除。
```text
id, name, endpoint, secret, enabled, created_at, updated_at
webhook_deliveries: id, email_id, target_type(mailbox|domain|global),
url, headers, key_enc, status, attempt_count, next_retry_at, last_error, delivered_at, created_at
```
## webhook_deliveries
```text
id, webhook_id, email_id, status, attempt_count, next_retry_at, last_error, delivered_at, created_at
```
投递行在入队时冻结解析出的配置,重试不受后续配置变更影响。
## oauth_connections
@@ -54,4 +64,18 @@ id, webhook_id, email_id, status, attempt_count, next_retry_at, last_error, deli
id, provider, account_id, encrypted_access_token, refresh_token, expires_at, created_at, updated_at
```
OAuth token 必须加密存储。所有 Email 必须有 expires_at,默认 retention 为 86400 秒。
OAuth token 必须加密存储。所有 Email 必须有 expires_at,默认 retention 为 86400 秒。
## worker_credentials
```text
id, cloudflare_account_id, worker_id, secret_hash, secret_enc, secret_last4,
status(active | revoked), created_at, revoked_at
```
用于 Email Worker 与 Console 内部认证:secret_hash 为摘要,secret_enc 为 AES-GCM 密文
(HMAC 验签需要原文,加密存储防拖库泄露);明文 secret 仅在签发/轮换时返回一次。
## 其它
users、settings、api_credentials(wmh_ Key)、nonces、oauth_states、cf_sim_resources(本地 sim)。
+29 -11
View File
@@ -1,42 +1,60 @@
# Email 生命周期与 TTL
# Email 生命周期与 TTL(v2,docs/11 §3/§10)
## 接收
## 接收(两阶段)
第一阶段只回答"这封邮件是否应该由 WmailHub 接收?":
```text
Cloudflare -> Email Worker -> Node -> Console
Cloudflare -> Email Worker
-> POST /internal/email/authorize { recipient, sender }
+-- accept 继续第二阶段
+-- reject message.setReject()(SMTP 永久拒绝)
+-- temporary_error 由 Cloudflare 邮件机制处理后续重试
```
Console 验证 Node 身份和请求完整性,然后解析 MIME、提取 header、text/html 和附件,保存元数据,raw MIME/大附件写入 R2,计算 expires_at,并创建 Webhook Delivery。
Console 拒绝码:DOMAIN_NOT_FOUND / DOMAIN_DISABLED / MAILBOX_NOT_FOUND / MAILBOX_DISABLED / RECEIVING_DISABLED。
Console 超时、网络失败、5xx 都不能被解释成邮箱不存在,也不能伪造永久拒绝。
第二阶段(accept 后)存储邮件:
```text
Email Worker -> POST /internal/email/ingest(raw MIME)
-> Console:验证 Worker 身份与 recipient -> 解析 MIME -> D1 元数据
-> R2 raw MIME / 附件 -> 计算 expires_at -> 创建 Webhook Delivery
```
SMTP 判断与邮件存储完全分离。动态邮箱:创建/删除/禁用邮箱后,下一封邮件的 authorize 立即生效,
Worker 不需要保存 mailbox 列表。
## TTL
默认保留 1 天:
默认保留 1 天,可按 Mailbox 配置:
```text
received_at = 2026-09-30T10:00:00Z
expires_at = 2026-10-01T10:00:00Z
```
Retention 可按 Mailbox 配置。
## Cron
使用 Cloudflare Cron Trigger,建议每 15 分钟执行一次清理。实际频率根据当前 Cloudflare 限制调整。
使用 Cloudflare Cron Trigger(本地每 60s 模拟):
```text
Cron -> 查询 expires_at <= now -> 删除 R2 -> 删除附件 -> 删除 Email
+ 过期 nonce / OAuth state 清理 + 过期 webhook delivery 重试
```
删除必须幂等。
## Webhook
邮件保存成功后再执行 Webhook。Webhook 失败不能导致邮件重新进入 Node。
邮件保存成功后投递(三级解析:邮箱 -> 域名 -> 全局)。Webhook 失败不能导致邮件重新进入接收流程。
## 异常
R2 删除失败时保留数据库记录并记录 cleanup error,下一次 Cron 重试。数据库已删除但 R2 删除失败时,需要 orphan cleanup。
R2 删除失败时保留数据库记录并记录 cleanup error,下一次 Cron 重试;
数据库已删除但 R2 删除失败时,由 orphan cleanup 兜底。ingest 失败不伪造业务拒绝。
## 数据最小化
邮件、附件、raw MIME、Webhook Delivery 和临时日志都应有生命周期,不做无限期保存。
邮件、附件、raw MIME、Webhook Delivery 和临时日志都应有生命周期,不做无限期保存。
+22 -11
View File
@@ -1,10 +1,11 @@
# Webhook、API 与 MCP
Webhook、API、MCP 全部属于 Console,Node 不实现这些能力。
Webhook、API、MCP 全部属于 Console,Email Worker 不实现这些能力。
## Webhook
## Webhook(三级:邮箱 -> 域名 -> 全局)
邮件保存成功后创建 delivery。Payload:
收到邮件后按 邮箱 -> 域名 -> 全局 的顺序取第一个已配置的 Webhook(无配置则逐级回退),
入队 delivery(冻结当时的 url/headers/key 配置)。Payload(docs/07):
```json
{
@@ -19,27 +20,37 @@ Webhook、API、MCP 全部属于 Console,Node 不实现这些能力。
}
```
Webhook 必须签名,建议提供 X-WmailHub-Event、X-WmailHub-Delivery、X-WmailHub-Timestamp、X-WmailHub-Signature。
请求头:X-WmailHub-Event、X-WmailHub-Delivery、X-WmailHub-Timestamp、X-WmailHub-Signature
(签名 = HMAC-SHA256(key, `${timestamp}.${body}`));配置了 key 时默认携带
`Authorization: Bearer <key>`,自定义 Authorization 头优先;无 key 时不携带签名头。
2xx 为成功;5xx 和 timeout 重试;4xx 按策略处理。使用指数退避和最大尝试次数。
2xx 为成功;5xx 和 timeout 按指数退避重试,达到最大尝试次数后置 exhausted。
## API
```text
/api/v1/nodes
/api/v1/accounts
/api/v1/domains
/api/v1/mailboxes
/api/v1/emails
/api/v1/webhooks
/api/v1/mcp
```
API 属于 Console。
API 属于 Console,Bearer API Key(wmh_)认证。
## MCP
MCP 允许 Agent 查询邮箱、获取邮件、查询邮件状态、管理邮箱和 Webhook。Node 不暴露 MCP。
MCP 允许 Agent 查询邮箱、获取邮件、创建/删除邮箱(list_mailboxes、list_emails、get_email、
create_mailbox、delete_mailbox),Bearer MCP Key(wmh_)认证。
MCP/API 获取邮件时必须检查用户权限、邮件是否过期、mailbox scope 和 API credential scope。
MCP/API 获取邮件时必须检查邮件是否过期、mailbox scope 和 API credential scope。
Webhook endpoint 必须防 SSRF,至少阻止 localhost、private IPv4/IPv6、link-local、metadata endpoint 和 DNS rebinding。
## 内部 API
`/internal/email/authorize` 与 `/internal/email/ingest` 为 Email Worker 专用(docs/04),
与 Webhook/API/MCP 无关。
## SSRF
Webhook endpoint 必须防 SSRF,至少阻止 localhost、private IPv4/IPv6、link-local、
metadata endpoint 和 DNS rebinding。
+25 -25
View File
@@ -1,47 +1,47 @@
# 安全设计
# 安全设计(v2,docs/11 §18)
## 安全边界
```text
Cloudflare Email Worker
|
| authenticated request
v
Node
|
| TLS + HMAC
v
Console
Internet -> Cloudflare Email Routing -> Email Worker
|
| HTTPS + HMAC + timestamp + nonce + body hash
v
Console(/internal/email/*)
```
公网不得直接调用 /internal/email/authorize 与 /internal/email/ingest:
所有请求必须携带有效的 Worker credential 签名(docs/04)。
## Cloudflare OAuth
OAuth Token 加密存储,不进入前端和日志,也不作为 Node credential。
OAuth Token 加密存储,不进入前端和日志,也不作为 Worker credential。
## Node Credential
## Worker Credential
由 Console 自动生成。用户不手工配置。支持撤销和重新 enrollment。
由 Console 在接入/部署时自动生成并注入 Worker 脚本,用户不手工配置。
支持撤销(立即生效)与轮换(轮换后已配置域名自动重新部署)。
## 请求签名
所有 Node -> Console 请求必须签名,签名覆盖 method、path、timestamp、nonce、body hash,防止篡改和 replay。
所有 Worker -> Console 请求必须签名,签名覆盖 method、path、timestamp、nonce、body hash,
Console 校验 Worker ID、timestamp 窗口、nonce 防重放、body hash、HMAC、credential 状态与
账号归属,防止篡改、重放与跨账号访问。
## Worker -> Node
## 隐私与日志
Worker 使用 Node 专用 ingress secret。Worker 不应拥有 Console credential。
- 禁止记录 Cloudflare OAuth token、Worker secret、Webhook key、Authorization header 和完整 MIME;
- reject reason 不泄露内部信息(只返回固定文案);
- 不向客户端泄露数据库内部错误。
## 最小权限
## 附件与大小限制
Cloudflare OAuth 只申请业务所需权限。
限制单文件大小、单邮件总大小、文件数量和 MIME header 大小,避免资源耗尽。
## 日志
## SSRF
禁止记录 Cloudflare OAuth token、Node secret、Webhook secret、Authorization header 和完整邮件正文。
## 附件限制
限制单文件大小、单邮件总大小、文件数量和 MIME header 大小,避免资源耗尽。
Webhook endpoint 校验禁止 localhost、私网、link-local、metadata endpoint 与 DNS rebinding。
## 数据保留
默认 Email TTL 为 1 天,过期自动删除。
默认 Email TTL 为 1 天,过期自动删除;nonce/oauth state/delivery 均有清理路径。
+34 -10
View File
@@ -1,33 +1,54 @@
# Cloudflare 部署设计
# Cloudflare 部署设计(v2,docs/11 §12)
## 服务
第一版目标使用 Cloudflare Workers、Email Workers / Email Routing、D1、R2、Cron Triggers、OAuth;Queues 在需要可靠异步投递时再加入,并以当前 Cloudflare 限制为准。
使用 Cloudflare Workers、Email Workers / Email Routing、D1、R2、Cron Triggers、OAuth;
Queues 在需要可靠异步投递时再加入,并以当前 Cloudflare 限制为准。
## Console
Console 本身部署在 Cloudflare Workers:
Console 本身部署在 Cloudflare Workers:
```text
Worker
+-- Frontend Assets
+-- Management API
+-- /internal/email/authorize + /internal/email/ingest(Email Worker 专用)
+-- OAuth Callback
+-- Cron Handler
```
## Node
部署步骤:`bunx wrangler d1 create wmailhub` -> `wrangler r2 bucket create` ->
`wrangler d1 execute wmailhub --remote --file=./migrations/0001_init.sql`(以及 0002/0003)->
`wrangler secret put SESSION_SECRET` -> 配置 CF_OAUTH_CLIENT_ID/SECRET/REDIRECT_URI vars。
PUBLIC_BASE_URL 即 Email Worker 里的 CONSOLE_URL。
Node 是独立纯后端程序,无前端。可以部署在用户服务器、云服务器或其他可访问 Console 的环境,但 Cloudflare Email Worker 必须能访问 Node ingress。
## Email Routing 与 catch-all Worker(动态邮箱)
动态邮箱场景把域名邮件通过 catch-all 规则交给 WmailHub Email Worker:
```text
*@example.com
|
v
WmailHub Email Worker -> Console(authorize)
+-- accept -> ingest
+-- reject -> message.setReject()
```
这样即使 unknown@example.com 在 Console 中不存在,也会进入 Worker,
由 Console 判断后执行 `message.setReject()`。如果只为现有邮箱创建 literal routing,
不存在的地址可能根本不会进入 Worker,WmailHub 无法完成 SMTP 阶段拒绝。
Console 的「域名 -> 设置 Email Workers」会自动完成:开启 Email Routing + catch-all 指向 Worker。
## Email Worker
Email Worker 只接收 Email Event 并安全转发到 Node,不保存业务数据库。
账号级一个 Worker(名字 wmailhub-email-<account_id>),脚本由 Console 自动上传,
内含 CONSOLE_URL / WORKER_ID / WORKER_SECRET;只做 authorize + ingest + setReject,
不保存业务数据。
## R2
用于 raw MIME 和 attachments:
```text
emails/{yyyy}/{mm}/{dd}/{email_id}/raw.eml
emails/{yyyy}/{mm}/{dd}/{email_id}/attachments/{attachment_id}
@@ -35,8 +56,11 @@ emails/{yyyy}/{mm}/{dd}/{email_id}/attachments/{attachment_id}
## D1
保存 nodes、domains、mailboxes、emails、webhooks、webhook deliveries、oauth connections。
保存 cloudflare_accounts、domains、mailboxes、emails、email_attachments、
webhook_deliveries、oauth_connections、worker_credentials、users、settings、api_credentials、
nonces、oauth_states。
## Cron
至少实现 cleanup job,建议每 15 分钟一次,具体以当前 Cloudflare 限制调整。
至少实现 cleanup job(过期邮件 + orphan + nonce/state 清理),建议每 15 分钟一次;
webhook 重试每 1 分钟,具体以当前 Cloudflare 限制调整。
+38 -43
View File
@@ -1,61 +1,56 @@
# ZCODE 实施计划
# WmailHub 实施计划(v2,docs/11 §20)
## Phase 1 项目骨架
## Phase 1 Console 重构(数据模型)
```text
WmailHub/
console/
node/
worker/
docs/
```
Cloudflare Account(cloudflare_accounts)、Domain、Mailbox、WorkerCredential;
迁移 0003 移除 nodes / node_credentials,emails/domains 归属改为 account_id。
Console、Node、Worker 必须边界清晰,共享协议独立。
## Phase 2 Authorization API
## Phase 2 Console
POST /internal/email/authorize:recipient/sender -> accept | reject(code+reason) | temporary_error;
拒绝码 DOMAIN_NOT_FOUND / DOMAIN_DISABLED / MAILBOX_NOT_FOUND / MAILBOX_DISABLED / RECEIVING_DISABLED。
实现 Console Web、Management API、数据库、认证、Node CRUD、Domain CRUD、Mailbox CRUD、Email CRUD。
## Phase 3 Email Worker
## Phase 3 Cloudflare OAuth
email handler + HMAC 签名 + authorize + setReject + ingest(共享逻辑见
shared/src/email-worker-template.ts 与 worker/src)。
实现 OAuth start、callback、state、token exchange、Account/Zone discovery、加密 token storage。
## Phase 4 Mail Ingest API
## Phase 4 自动 Node 安装
POST /internal/email/ingest:message/rfc822 原文直传(不转 JSON 字符串),
复验 recipient(动态邮箱),只允许 accept 后调用。
Add Node 后完成:OAuth、Node identity、Node credential、Email Worker、Worker -> Node、Email Routing、Domain、Mailbox、Node heartbeat。正常流程禁止要求用户复制安装命令。
## Phase 5 Cloudflare 自动配置
## Phase 5 Node
Connect Cloudflare Account:OAuth -> Account/Zone 发现 -> 域名同步 ->
Email Worker 部署(注入 CONSOLE_URL/WORKER_ID/WORKER_SECRET)-> WorkerCredential 签发;
逐域名:Email Routing + catch-all 规则。
实现 bootstrap、secure config、Email Worker ingress、Console egress、HMAC signing、heartbeat、有限 retry、graceful shutdown、structured logging。
## Phase 6 邮件存储
Node 不实现业务路由。
D1 元数据 + R2 raw MIME/附件 + MIME 解析(降级入库)+ 大小/数量限额。
## Phase 6 Email Pipeline
## Phase 7 Webhook / REST API / MCP / TTL
```text
Cloudflare -> Worker -> Node -> Console -> D1/R2
```
三级 Webhook(邮箱 -> 域名 -> 全局,冻结投递配置)、Bearer API/MCP Key、MCP 工具、
Cron 清理(过期邮件 + orphan + nonce/state)+ Webhook 重试。
测试 plain text、HTML、MIME、attachment、多收件人、大正文和 malformed mail。
## Phase 8 完整测试
## Phase 7 Webhook
1. 存在邮箱 -> 接收成功;
2. 不存在邮箱 -> message.setReject()(MAILBOX_NOT_FOUND);
3. 禁用邮箱 -> setReject(MAILBOX_DISABLED);
4. 禁用域名 -> setReject(DOMAIN_DISABLED);
5. Console 超时 -> temporary_error,不能变成 mailbox not found;
6. Console 5xx -> temporary_error;
7. ingest 失败 -> 不伪造业务拒绝;
8. Worker credential revoke/rotate -> Worker 无法访问 Console;
9. 新建邮箱后测试立即接收;
10. 删除邮箱后测试立即拒绝;
11. 多域名并发测试;
12. catch-all 地址测试。
实现 CRUD、secret、signing、delivery、retry、timeout、失败状态和日志。
## 最终验收
## Phase 8 TTL
实现 retention configuration、expires_at、Cron cleanup、R2 cleanup 和 orphan cleanup。默认 1 day。
## Phase 9 API / MCP
实现 Console API,并提供 list_mailboxes、list_emails、get_email、create_mailbox、delete_mailbox、list_webhooks 等 MCP 工具。权限必须基于 Console credential。
## Phase 10 安全测试
测试 OAuth CSRF、credential revoke、replay attack、伪造 Node 请求、body 篡改、过期 timestamp、Webhook SSRF、未授权 mailbox、未授权 Cloudflare Account、超大邮件和超大附件。
## Phase 11 验收
必须完成:Cloudflare OAuth -> 自动 Node 安装 -> Domain 配置 -> Mailbox 创建 -> 真实 Email -> Email Worker -> Node -> Console -> Database/Webhook/API/MCP -> TTL 自动删除。
如果正常流程要求用户手工登录服务器执行安装命令,则不符合设计目标。
运行架构必须是 `Internet -> Cloudflare Email Routing -> Email Worker -> Console(D1/R2/Webhook/API/MCP)`;
不得存在 `Email Worker -> Node -> Console`。
+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 邮件架构中移除