v2.4.1 回读校验收紧到 status:SPF 冲突(misconfigured)不再谎报已配置
实测 adx9.com 全链路时发现的新缺口:/enable 成功后 enabled=true,但 zone 的
Email Routing status 可能仍是 misconfigured(SPF 指向旧邮件服务)——
照 v2.4.0 的校验(enabled===true)照样会把域名标成「已配置」,而邮件根本不会投递,
与 MX 假成功是同一类问题。
- cfapi: 回读校验增加 status 判定:
- ready / unlocked(过渡态,实测邮件照常投递)视为成功
- misconfigured* 判失败,并把状态对象里的 errors 翻译成中文
(spf.foreign = 现有 SPF 指向其它邮件服务;spf.missing = 给出应替换的 SPF 内容)
- cf-mock: 读回的状态随 emailRoutingStatusAfterEnable 可配置
- oauth.test 新增 2 例:misconfigured 判失败(不标 active)、unlocked 视为成功
线上排查记录(2026-09-30,已手工修复):
adx9.com 删除自建 MX(10 mx.adx9.com)→ POST /email/routing/enable 成功但
status=misconfigured/locked(errors: spf.foreign/spf.missing)→ PATCH
/email/routing/dns(unlock)→ 替换 SPF TXT 为 v=spf1 include:_spf.mx.cloudflare.net
~all → Graph API 发测试邮件 → Console 收到(msg_7eb6afeba6179293),全链路打通。
This commit is contained in:
+10
-3
@@ -37,6 +37,10 @@ export interface CfMockOptions {
|
||||
emailRoutingBlocked?: boolean;
|
||||
/** emailRoutingBlocked 时 /dns_records 返回的 MX 列表(用于验证中文提示点名了冲突记录) **/
|
||||
foreignMx?: { id?: string; name: string; content: string; priority?: number }[];
|
||||
/** 开启成功后回读到的状态(默认 ready;可传 misconfigured 模拟 SPF 等配置冲突) **/
|
||||
emailRoutingStatusAfterEnable?: string;
|
||||
/** emailRoutingStatusAfterEnable=misconfigured 时状态对象里的 errors **/
|
||||
routingErrors?: { code?: string; existing?: { content?: string }; missing?: { content?: string } }[];
|
||||
}
|
||||
|
||||
/** # 成员变量: Cloudflare API 桩句柄 **/
|
||||
@@ -180,13 +184,16 @@ export function installCloudflareMock(opts: CfMockOptions = {}): CfMock {
|
||||
}
|
||||
if (url.endsWith("/email/routing")) {
|
||||
// 回显型接口:无论 PUT 还是 PATCH,一律 success 但绝不改 enabled
|
||||
const enabled = state.emailRoutingEnabled;
|
||||
const misconfigured = Boolean(opts.emailRoutingStatusAfterEnable?.startsWith("misconfigured"));
|
||||
return json({
|
||||
id: url.split("/").at(-2),
|
||||
name: zones[0]?.name ?? "example.com",
|
||||
enabled: state.emailRoutingEnabled,
|
||||
enabled,
|
||||
skip_wizard: true,
|
||||
status: state.emailRoutingEnabled ? "ready" : "unconfigured",
|
||||
});
|
||||
status: enabled ? (opts.emailRoutingStatusAfterEnable ?? "ready") : "unconfigured",
|
||||
...(misconfigured ? { errors: opts.routingErrors ?? [] } : {}),
|
||||
} as unknown);
|
||||
}
|
||||
// 开启被 2008 挡下时,补一次 MX 查询,验证中文提示里确实点名了冲突记录
|
||||
if (url.includes("/dns_records")) return json(opts.foreignMx ?? []);
|
||||
|
||||
@@ -615,6 +615,50 @@ describe("Email Routing 开启失败与回读校验(2026-09-30 线上 bug 的回
|
||||
expect(setup.ok).toBe(false);
|
||||
expect(setup.error).toContain("enabled=false");
|
||||
});
|
||||
|
||||
test("开启成功但 status=misconfigured(SPF 冲突):判失败并翻译 errors,不谎报已配置", async () => {
|
||||
const t = await makeTestEnv();
|
||||
const cookie = await loginCookie(t);
|
||||
await connectAccount(t, cookie, {
|
||||
emailRoutingStatusAfterEnable: "misconfigured",
|
||||
routingErrors: [
|
||||
{ code: "spf.foreign", existing: { content: "v=spf1 mx -all" } },
|
||||
{ code: "spf.missing", missing: { content: "v=spf1 include:_spf.mx.cloudflare.net ~all" } },
|
||||
],
|
||||
});
|
||||
const domList = (await (
|
||||
await authedFetch(t, cookie, "http://localhost/api/domains")
|
||||
).json()) as { items: { id: string; name: string }[] };
|
||||
const example = domList.items.find((d) => d.name === "example.com")!;
|
||||
|
||||
const setup = (await (
|
||||
await authedFetch(t, cookie, `http://localhost/api/domains/${example.id}/setup`, { method: "POST" })
|
||||
).json()) as { ok: boolean; error?: string };
|
||||
// enabled=true 但 misconfigured 照样不能标已配置 —— 这也是"显示配好了却收不到邮件"的一种
|
||||
expect(setup.ok).toBe(false);
|
||||
expect(setup.error).toContain("misconfigured");
|
||||
expect(setup.error).toContain("SPF");
|
||||
expect(setup.error).toContain("v=spf1 include:_spf.mx.cloudflare.net ~all");
|
||||
|
||||
const after = (await (
|
||||
await authedFetch(t, cookie, "http://localhost/api/domains")
|
||||
).json()) as { items: { id: string; status: string }[] };
|
||||
expect(after.items.find((d) => d.id === example.id)!.status).toBe("error");
|
||||
});
|
||||
|
||||
test("开启成功但 status=unlocked(过渡态):视为成功,邮件照常投递", async () => {
|
||||
const t = await makeTestEnv();
|
||||
const cookie = await loginCookie(t);
|
||||
await connectAccount(t, cookie, { emailRoutingStatusAfterEnable: "unlocked" });
|
||||
const domList = (await (
|
||||
await authedFetch(t, cookie, "http://localhost/api/domains")
|
||||
).json()) as { items: { id: string; name: string }[] };
|
||||
const example = domList.items.find((d) => d.name === "example.com")!;
|
||||
const setup = (await (
|
||||
await authedFetch(t, cookie, `http://localhost/api/domains/${example.id}/setup`, { method: "POST" })
|
||||
).json()) as { ok: boolean };
|
||||
expect(setup.ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("API Token 方式添加账号(与 OAuth 并列的第二种接入方式)", () => {
|
||||
|
||||
Reference in New Issue
Block a user