v2.4.0 修 Email Routing 假成功 + 加 API Token 接入与邮箱/域名页交互改进
问题 6(收不到邮件、Dashboard 邮件路由里看不到域名)的根因:
上一版把「开启 Email Routing」改成了 PUT /zones/{id}/email/routing,
但实测该接口(以及 PATCH)是**纯回显** —— 返回 success:true 但 enabled 纹丝不动,
域名在 Cloudflare 侧仍是 unconfigured、连 MX 记录都没有。Console 却照它的返回值
把域名标成「已配置 Email Workers」,于是「显示配好了却收不到邮件」。
实测只有 POST /zones/{id}/email/routing/enable 真的开启(线上对 adx9.com 与
galaxyemail.com 逐一验证)。
后端
- cfapi: 改用 /enable,并在开启后回读 GET 校验 enabled===true,不满足即判失败;
新增 CFApiError(带 Cloudflare 错误码)与 getEmailRoutingSettings
- cfapi: 错误码 2008(Non-Cloudflare MX records exist)翻译成中文并点名冲突的 MX 记录
- provision: Email Worker 统一命名 wmailhub-email(原先按账号 id 拼名),
部署走 PUT,同名即覆盖更新
- db: 新增 decryptAccountToken —— 按账号取 token,不再取「最近一条连接」,
并跳过已过期的 OAuth token;oauth_connections.account_id 真正写入
- resources: 新增 POST /api/accounts/token(API Token 方式添加账号):
先校验 token 与账户 ID,确认可用才建账号并自动接入
前端
- 账号:「添加账号」改为模态窗口,OAuth / API Token 两个 tab,各自带说明
- 域名: 删除「Email Worker」列;「设置 Email Workers / 重新配置」先弹确认框
(说明会改 MX 与路由),执行期间按钮 loading 且互相禁用;error 状态改成可读的 Alert + 重试
- 邮箱: 删除「状态」列(已由「启用」开关承担);地址列加复制按钮(带弹跳+变绿动画);
「添加邮箱」窗口的域名只列已装 Email Workers 的、支持搜索、默认选中最近使用的域名;
地址框可一键从 namedb 取随机用户名;勾选 catch-all 后地址框禁用
- 收件箱: 域名筛选只显示已装 Email Workers 的域名
测试
- cf-mock 复刻「PUT/PATCH 回 success 但不改 enabled」与 2008,锁住本次修复
- oauth.test 新增 4 例:2008 冲突、回读校验、API Token 接入、Token/账户 ID 校验失败
- ui-smoke 覆盖新增的添加账号/添加邮箱/确认框等交互(28/28 通过)
文档: docs/02 补「三种开启接口只有 /enable 有效」的实测表、2008 处理、
两种接入方式与按账号取 token 的说明
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
|
||||
import type { Env } from "../env.ts";
|
||||
import { getCFApi, type CFApi } from "./cfapi.ts";
|
||||
import { encryptSecret, decryptSecret, decryptConnectionToken } from "../db.ts";
|
||||
import { encryptSecret, decryptSecret, decryptAccountToken } from "../db.ts";
|
||||
import { ids, sha256Hex, DEFAULT_RETENTION_SECONDS } from "../../../shared/src/index.ts";
|
||||
import { nowIso } from "../util.ts";
|
||||
|
||||
@@ -35,28 +35,46 @@ export function initialSteps(): Record<string, { status: StepStatus; error?: str
|
||||
return out;
|
||||
}
|
||||
|
||||
/** # 成员变量: Email Worker 的统一脚本名
|
||||
*
|
||||
* 所有接入的 Cloudflare 账号共用这一个名字(Worker 本身是 **账号级** 资源,
|
||||
* 同账号下只有一个,靠名字区分毫无意义)。好处是在 Cloudflare Dashboard 的
|
||||
* Workers 列表里一眼就能找到,不必在一堆 wmailhub-email-acct-xxxx 里猜。
|
||||
* 部署走 PUT,同名即覆盖更新,重复执行不会产生第二个 Worker。
|
||||
* (历史上曾按账号 id 拼名,改名后旧脚本会残留在 Cloudflare,可手动删除。)
|
||||
**/
|
||||
export const EMAIL_WORKER_NAME = "wmailhub-email";
|
||||
|
||||
/** # 函数
|
||||
* 账号的 Email Worker 脚本名(幂等:同一账号固定同名;同时作为 Worker -> Console 的身份 ID)
|
||||
* 账号的 Email Worker 脚本名(统一名;保留函数形式以兼容既有调用点)
|
||||
*/
|
||||
export function emailWorkerNameFor(accountId: string): string {
|
||||
return `wmailhub-email-${accountId.replace(/[^a-z0-9-]/gi, "-").toLowerCase()}`;
|
||||
export function emailWorkerNameFor(_accountId: string): string {
|
||||
return EMAIL_WORKER_NAME;
|
||||
}
|
||||
|
||||
/** # 函数
|
||||
* 添加账号:创建 cloudflare_accounts 记录(等待 OAuth 授权)。
|
||||
* 添加账号:创建 cloudflare_accounts 记录(等待接入)。
|
||||
* 参数: opts.name - 展示名;opts.cloudflareAccountId - 已知的 Cloudflare 账户 ID(API Token 方式可先填)
|
||||
*/
|
||||
export async function createAccount(
|
||||
env: Env,
|
||||
opts: { name: string },
|
||||
opts: { name: string; cloudflareAccountId?: string | null },
|
||||
): Promise<{ accountId: string }> {
|
||||
const accountId = ids.account();
|
||||
const now = nowIso();
|
||||
await env.DB.prepare(
|
||||
`INSERT INTO cloudflare_accounts (id, name, cloudflare_account_id, worker_id, status,
|
||||
provision_steps, created_at, updated_at)
|
||||
VALUES (?, ?, NULL, NULL, 'pending_oauth', ?, ?, ?)`,
|
||||
VALUES (?, ?, ?, NULL, 'pending_oauth', ?, ?, ?)`,
|
||||
)
|
||||
.bind(accountId, opts.name, JSON.stringify(initialSteps()), now, now)
|
||||
.bind(
|
||||
accountId,
|
||||
opts.name,
|
||||
opts.cloudflareAccountId ?? null,
|
||||
JSON.stringify(initialSteps()),
|
||||
now,
|
||||
now,
|
||||
)
|
||||
.run();
|
||||
return { accountId };
|
||||
}
|
||||
@@ -272,12 +290,14 @@ export async function setupDomainEmail(env: Env, domainId: string): Promise<{ ok
|
||||
.first<{ id: string; cloudflare_account_id: string | null }>();
|
||||
if (!account?.cloudflare_account_id) return { ok: false, error: "account not connected via OAuth" };
|
||||
|
||||
const accessToken = await decryptConnectionToken(env);
|
||||
if (!accessToken) return { ok: false, error: "no active Cloudflare OAuth connection" };
|
||||
const accessToken = await decryptAccountToken(env, account.id);
|
||||
if (!accessToken) return { ok: false, error: "no active Cloudflare connection for this account" };
|
||||
|
||||
const cf: CFApi = await getCFApi(env);
|
||||
try {
|
||||
const workerId = await deployAccountWorker(env, account.id, accessToken);
|
||||
// 开启 Email Routing 会失败于「已存在非 Cloudflare MX 记录」等真实原因,
|
||||
// 失败信息已翻译成中文写进 domains.setup_error,不会静默标成已配置。
|
||||
await cf.enableEmailRouting({ accessToken, zoneId: domain.zone_id });
|
||||
await cf.setCatchAllRule({ accessToken, zoneId: domain.zone_id, workerName: workerId });
|
||||
await env.DB.prepare(
|
||||
|
||||
Reference in New Issue
Block a user