bigcode 7a1625d575 fix: v2.3.2 用探测确定真实 scope 词表,接入已实测可用
- 上一版默认 scope 名是猜的(account.read / email_routing.write),全部被拒。
  正确词表是点号分隔的 <group>.<verb>,且:
  - 账号是 account-settings.read(选择器里叫 Account Settings Read),不是 account.read
  - Workers 脚本是 workers-scripts.write,不是 workers_scripts.write
  - Email Routing 在选择器里是四个独立权限,改 catch-all 对应 email-routing-rule.write
  - account:read 这种冒号写法是 wrangler 的第一方简写,第三方 client 一律被拒
  - client 注册的 scope 是上限不是默认:没勾的权限即使账号有也不生效
- preflight 扩展 scope 探测:授权端点在同意页之前就校验并点名拒绝的 scope,
  逐轮「请求剩余候选 -> 剔除被点名项」即可收敛出 App 已授权集合(未认证请求,
  不登录/不产生授权),结果按 allowed/denied 返回并在设置页用绿/橙标签展示
- 缺少账号级读权限时不再失败:discover() 先试 /accounts,403 则改为列 token 可见的 /zones,
  用 zone 自带的 account_id 反推账户(展示名用第一个域名)
- 默认 scope 收敛为接入必需的三项(zone.read / workers-scripts.write /
  email-routing-rule.write),account-settings.read 改为可选:勾了才显示真实账号名
- 实测:线上 preflight 已 ok=true(Cloudflare 跳登录页),无需再改 App 即可「添加账号」
2026-09-30 16:26:43 +08:00

WmailHub

基于 Cloudflare Email Workers 的轻量邮件接收与转发平台。

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 账号)

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):

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 客户端(本地同样必需):

# .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 位)与申请的 scope,保存后立即生效、无需重新部署;「检测授权配置」按钮可在不开浏览器的情况下验证 OAuth App 是否配置正确(回调地址、Client URI 校验、是否 promote to public、scope 是否已授权)。未配置时「添加账号」返回 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)

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:

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 所属账号与收件域名一致。

目录结构

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,需填齐四项(详见 docs/02):

    • Redirect URL:https://wmailhub.adx9.com/api/oauth/cloudflare/callback
    • Client URI:https://wmailhub.adx9.com(必须是 Console 站点域名),并按提示加 TXT 记录完成域名校验
    • Promote to public:私有 App 无法被授权
    • Scopes(zone.read / workers-scripts.write / email-routing-rule.write / account-settings.read):点号分隔,冒号写法(account:read)是 wrangler 的第一方简写,第三方 client 会被拒;缺 account-settings.read 也能接入(自动从域名反推账号)

    配好后到设置页点「检测授权配置」确认,再点账号页「添加账号」。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 与邮件正文。
S
Description
WmailHub - 基于 Cloudflare Email Workers 的轻量邮件接收与转发平台
Readme
1.2 MiB
Languages
TypeScript 99.1%
CSS 0.8%