- 设置新增「Cloudflare 接入」tab:Client ID / Client Secret(Secret AES-GCM 加密入库,
只回显末 4 位),并展示可复制的授权回调地址;settings 优先、env 变量兜底,
保存后立即生效无需重新部署;DELETE /api/settings/cf-oauth 清除
- 四个资源列表(账号/域名/邮箱/收件箱)加复选框批量选择与批量删除
- 新增 POST /api/{accounts,domains,mailboxes,emails}/batch-delete:
账号软删+撤销凭据;域名/邮箱级联删除其下邮件;邮件连 R2 原文与附件一并清理
- 抽出 deleteEmailById / deleteEmailsByMailbox / revokeAccount 供单条与批量共用
- 点击复选框不再触发行点击(避免误开详情抽屉)
- 新增 tests/batch.test.ts(6 例)与 settings OAuth 凭据用例(2 例);ui-smoke 扩到 17 步
146 lines
9.5 KiB
Markdown
146 lines
9.5 KiB
Markdown
# WmailHub
|
|
|
|
基于 **Cloudflare Email Workers** 的轻量邮件接收与转发平台。
|
|
|
|
```text
|
|
Internet → Cloudflare Email Routing → Email Worker → Console → D1/R2
|
|
(catch-all) (authorize/ingest) ├→ Webhook
|
|
├→ API v1
|
|
└→ MCP
|
|
```
|
|
|
|
- **Console**(唯一业务控制中心)= 前端(Vite + React + antd v6)+ 管理 API + 内部邮件 API(`/internal/email/authorize` / `/internal/email/ingest`),部署在 Cloudflare Workers,负责 Cloudflare OAuth、账号/域名/邮箱/邮件管理、数据存储与 TTL 清理。
|
|
- **Email Worker** = 邮件入口 + SMTP reject 执行者:先向 Console authorize 询问是否接收,明确拒绝时 `message.setReject()`,允许时把 raw MIME 提交 ingest;超时/5xx 一律按临时错误,不伪装成邮箱不存在。
|
|
|
|
> v2.0.0 起 Node 已从邮件架构中移除(旧 `Email Worker → Node → Console` 链路废弃,docs/11)。
|
|
> Email Worker 直连 Console;「账号」= Connect Cloudflare Account(OAuth 自动接入,
|
|
> 自动部署账号级 Email Worker 并签发 Worker 凭据)。
|
|
|
|
## 本地开发(无需 Cloudflare 账号)
|
|
|
|
```sh
|
|
bun install
|
|
bun run dev # 一键启动:console + email worker + 自动 OAuth(sim)接入
|
|
bun run dev -- --reset # 清空本地数据重新接入
|
|
bun test # 测试套件(协议安全/authorize 决策/管线/TTL/Webhook/MCP/OAuth)
|
|
bun tests/ui-smoke.ts # UI 冒烟(需本机 Playwright chromium)
|
|
```
|
|
|
|
启动后:
|
|
|
|
| 服务 | 地址 |
|
|
| --- | --- |
|
|
| Console(前端 + API) | http://127.0.0.1:18787 |
|
|
| Email Worker(本地联调) | http://127.0.0.1:18789/__health |
|
|
|
|
**登录**:默认 `admin / admin`。
|
|
|
|
**发测试邮件**(走完整两阶段链路 Worker→authorize→ingest):
|
|
|
|
```sh
|
|
COOKIE=$(curl -si -X POST http://127.0.0.1:18787/api/auth/login \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"username":"admin","password":"admin"}' \
|
|
| grep -i '^set-cookie' | sed 's/^.*wmh_session=\([^;]*\).*/wmh_session=\1/')
|
|
ACC=$(curl -s http://127.0.0.1:18787/api/accounts -H "Cookie: $COOKIE" \
|
|
| python3 -c "import sys,json;print(json.load(sys.stdin)['items'][0]['id'])")
|
|
SECRET=$(curl -s http://127.0.0.1:18787/api/accounts/$ACC/worker-secret \
|
|
-H "Cookie: $COOKIE" | python3 -c "import sys,json;print(json.load(sys.stdin)['secret'])")
|
|
|
|
curl -X POST http://127.0.0.1:18789/__email_test \
|
|
-H "X-WmailHub-Test-Secret: $SECRET" \
|
|
-H "X-WmailHub-Test-From: alice@example.org" \
|
|
-H "X-WmailHub-Test-To: hello@example.test" \
|
|
--data-binary @tests/fixtures/test-mail.eml
|
|
# {"action":"accepted","email_id":"msg_xxx","mailbox_id":"mbx_xxx"}
|
|
# 不存在的邮箱:{"action":"rejected","reason":"Mailbox does not exist","code":"MAILBOX_NOT_FOUND"}
|
|
```
|
|
|
|
`bun run dev` 启动 Console(:18787)与 Email Worker(:18789)。Cloudflare OAuth 没有模拟通道:账号必须在 Console 页面里人工授权接入。因此首次运行若本地还没有账号,脚本只拉起 Console 并提示下一步;接入成功后再次执行 `bun run dev`,就会自动为未激活域名配置 Email Workers(部署账号级 Worker + Email Routing + catch-all 规则)、签发 WorkerCredential 并拉起本地 Email Worker。
|
|
|
|
接入前先配置 Cloudflare OAuth 客户端(本地同样必需):
|
|
|
|
```sh
|
|
# .env 或 shell 环境
|
|
export CF_OAUTH_CLIENT_ID=...
|
|
export CF_OAUTH_CLIENT_SECRET=...
|
|
export CF_OAUTH_REDIRECT_URI=http://127.0.0.1:18787/api/oauth/cloudflare/callback
|
|
```
|
|
|
|
未配置时账号页「添加账号」返回 503 并提示原因,不会生成任何假账号或假资源。
|
|
|
|
## 功能
|
|
|
|
- **用户登录**:用户名密码(PBKDF2),默认 admin/admin,HMAC 签名会话 cookie;密码在「设置」中修改。
|
|
- **账号管理**:「添加账号」直达 Cloudflare OAuth → 自动创建账号并同步该账号下全部域名、部署 Email Worker、签发 Worker 凭据;支持凭据轮换(轮换后已配置域名自动重新部署)。
|
|
- **Cloudflare 接入配置**:设置 →「Cloudflare 接入」填写 OAuth Client ID / Secret(Secret AES-GCM 加密入库,只回显末 4 位),保存后立即生效、无需重新部署;未配置时「添加账号」返回 503。
|
|
- **批量操作**:账号 / 域名 / 邮箱 / 收件箱四个列表均支持复选框选择与批量删除。账号为软删(撤销 Worker 凭据);域名与邮箱级联删除其下邮件;邮件连同 R2 原文与附件一并清理。
|
|
- **域名**:由 OAuth 自动同步(无手工添加);点击域名「设置 Email Workers」= 确保账号级 Worker 部署 + 开启 Email Routing + catch-all 规则;支持按账号筛选。
|
|
- **两阶段接收**:authorize(SMTP 决策:accept/reject/temporary_error,拒绝码 DOMAIN_NOT_FOUND / DOMAIN_DISABLED / MAILBOX_NOT_FOUND / MAILBOX_DISABLED / RECEIVING_DISABLED)+ ingest(raw MIME 入库);动态邮箱创建/删除/禁用下一封邮件立即生效。
|
|
- **邮件管线**:MIME 解析(postal-mime)、附件、catch-all/精确匹配、大小限额、malformed 降级入库。
|
|
- **TTL**:邮件默认保留 1 天(`expires_at`),Cron 定期清理 R2 + DB + orphan,幂等。
|
|
- **Webhook(三级)**:配置落在 邮箱 → 域名 → 全局(设置)三级,收信时取第一个命中者转发 `email.received` 事件;支持自定义请求头与 key(key 默认以 `Authorization: Bearer <key>` 携带,并用于 `X-WmailHub-Signature` 签名);指数退避重试,SSRF 防护。
|
|
- **API v1**:`Authorization: Bearer wmh_...` 访问 `/api/v1/*`(/accounts /domains /mailboxes /emails)。
|
|
- **MCP**:`/api/v1/mcp`(JSON-RPC over HTTP),工具:`list_mailboxes` / `list_emails` / `get_email` / `create_mailbox` / `delete_mailbox`。
|
|
- **Key 管理**:API Key 与 MCP Key 在「设置」模态窗口内生成/撤销/删除,`wmh_` 前缀 + 随机串,总长 32 位;库中只存 SHA-256,明文仅创建时展示一次。
|
|
|
|
## Worker <-> Console 协议(docs/04)
|
|
|
|
```text
|
|
body_hash = SHA256(raw_body)
|
|
canonical = METHOD \n PATH \n TIMESTAMP \n NONCE \n body_hash
|
|
signature = HMAC-SHA256(worker_credential_secret, canonical)
|
|
```
|
|
|
|
请求头:`Authorization: WmailHub-Worker <worker_id>`、`X-WmailHub-Timestamp`(±5 分钟)、`X-WmailHub-Nonce`(防重放)、`X-WmailHub-Body-SHA256`、`X-WmailHub-Signature`、`X-WmailHub-Protocol: 1`。
|
|
|
|
内部 API:
|
|
|
|
```text
|
|
POST /internal/email/authorize { "recipient": "...", "sender": "..." } → accept | reject | temporary_error
|
|
POST /internal/email/ingest raw MIME(message/rfc822)+ X-WmailHub-Recipient / X-WmailHub-Sender
|
|
```
|
|
|
|
Console 校验:Worker ID、timestamp 窗口、nonce 防重放、body hash、HMAC、credential 状态(撤销立即生效)、Worker 所属账号与收件域名一致。
|
|
|
|
## 目录结构
|
|
|
|
```text
|
|
WmailHub/
|
|
console/ Console(Workers 入口 src/worker.ts + 本地运行时 src/local/main.ts + 前端 frontend/)
|
|
worker/ Email Worker(Cloudflare 部署模板 shared/src/email-worker-template.ts + 本地联调 local.ts)
|
|
shared/ 共享协议:HMAC 签名 / Key 生成 / 加密 / SSRF 防护 / Email Worker 脚本模板
|
|
migrations/ D1 schema(0001 初始 + 0002 三级 Webhook + 0003 邮件链路 v2)
|
|
tests/ bun test 测试 + UI 冒烟 + fixtures
|
|
scripts/ dev 启动(前端构建走 vite)
|
|
docs/ 设计文档(00~11;11 为邮件架构改造方案)
|
|
```
|
|
|
|
## 生产部署(Cloudflare Workers)
|
|
|
|
线上实例:**https://wmailhub.adx9.com**(Worker `wmailhub-console`,自定义域名由 wrangler `routes.custom_domain` 自动建 DNS + 签证书)。
|
|
|
|
1. `bunx wrangler d1 create wmailhub` → 将 `database_id` 填入 `console/wrangler.jsonc`(线上:`cfc47e65-c71e-470f-96db-5bc23e3e31a4`)
|
|
2. `bunx wrangler r2 bucket create wmailhub`
|
|
3. 依次执行 `bunx wrangler d1 execute wmailhub --remote --file=./migrations/0001_init.sql` … `0004_single_key_markdown.sql`(0001~0004 按序号全部执行,0004 依赖 0001 的 api_credentials)
|
|
4. `cd console && bunx wrangler deploy`(前端先 `bun run build`);`PUBLIC_BASE_URL` 即 Email Worker 注入的 CONSOLE_URL
|
|
5. `bunx wrangler secret put SESSION_SECRET --config console/wrangler.jsonc`(≥32 位随机串)
|
|
6. Cloudflare Dashboard 创建 OAuth App(client id/secret),回调填 `https://wmailhub.adx9.com/api/oauth/cloudflare/callback`(页面里会显示这个地址,直接复制)。Client ID / Secret 有两个配置位置,任一即可:
|
|
- **推荐**:部署后登录 Console → 设置 →「Cloudflare 接入」填写,Secret 加密入库,改完立即生效,以后换 client 也不用重新部署;
|
|
- 或部署时用 wrangler 配成 vars/secret(`CF_OAUTH_CLIENT_ID` / `CF_OAUTH_CLIENT_SECRET`),作为后台未配置时的兜底。
|
|
|
|
两者都没有时,`/api/oauth/cloudflare/start` 与 `/cloudflare/callback` 直接返回 503,系统不会降级到任何模拟通道
|
|
|
|
> 提示:`wrangler.jsonc` 里的 `database_id` 必须先填真实值,否则 `d1 execute` / `deploy` 都会报 `Invalid uuid`。
|
|
> 部署后自检:`curl https://wmailhub.adx9.com/api/health` → `{"ok":true,"name":"WmailHub","version":"<package.json 版本>"}`。
|
|
|
|
Cron(`*/1 * * * *`)驱动 TTL 清理与 Webhook 重试;Email Worker 由 Console provisioning 自动上传(无需手工部署),catch-all 规则在「域名 → 设置 Email Workers」时自动配置。
|
|
|
|
## 安全设计(docs/08)
|
|
|
|
- OAuth token、Worker credential、Webhook key 全部 AES-256-GCM 加密存储;凭据撤销/轮换即时生效。
|
|
- Worker→Console 强制 HTTPS + HMAC 签名 + timestamp 窗口 + nonce 防重放 + body hash 防篡改 + 账号归属校验。
|
|
- Worker 只持有自身凭据,不持有 Cloudflare OAuth 凭据;Console 拒绝文案不泄露内部信息。
|
|
- Webhook endpoint 防 SSRF(私网/loopback/metadata/非法 scheme;本地联调需显式 `WMAILHUB_ALLOW_PRIVATE_WEBHOOKS=1`)。
|
|
- 会话 cookie `SameSite=Strict` + 跨域 Origin 校验;日志不落 secret 与邮件正文。
|