mail domain:发信校验与选账号(from 不静默回落)、分发状态机、账号记录与视图(密文进不了视图)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
This commit is contained in:
xcs
2026-08-26 00:28:02 +08:00
co-authored by Claude Fable 5
parent 6748ef01b7
commit 8d7e56aa92
7 changed files with 286 additions and 0 deletions
@@ -0,0 +1,78 @@
/** 账号记录 ↔ 入参 / 视图(设计稿 §5.3);视图经 schema 过一遍,密文进不来 */
import type { MailAccountInput, MailAccountView } from '@contracts';
import { mailAccountViewSchema } from '@contracts';
import { cursorSummary } from './cursor.ts';
import { isPolling } from './due.ts';
import type { MailAccountRecord } from './records.ts';
export function viewOf(record: MailAccountRecord, now: Date): MailAccountView {
return mailAccountViewSchema.parse({
id: record.id,
kind: record.kind,
name: record.name,
address: record.address,
displayName: record.displayName,
enabled: record.enabled,
isDefault: record.isDefault,
pollIntervalSec: record.pollIntervalSec,
channelId: record.channelId,
backfillFrom: record.backfillFrom === null ? null : record.backfillFrom.toISOString(),
settings: record.settings,
secretBound: record.secretCipher !== null && record.secretCipher !== '',
status: {
lastPolledAt: record.lastPolledAt === null ? null : record.lastPolledAt.toISOString(),
lastPollError: record.lastPollError,
lastInboundAt: record.lastInboundAt === null ? null : record.lastInboundAt.toISOString(),
cursor: cursorSummary(record.cursor),
polling: isPolling(record.pollingUntil, now),
},
createdAt: record.createdAt.toISOString(),
updatedAt: record.updatedAt.toISOString(),
});
}
/** 入参里的凭证 → 待密封的 JSON;没给就是 undefined(= 不改) */
export function secretOf(input: MailAccountInput): string | undefined {
return input.secret === undefined ? undefined : JSON.stringify(input.secret);
}
function sameSettings(a: Record<string, unknown>, b: Record<string, unknown>): boolean {
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
return [...keys].every((k) => JSON.stringify(a[k] ?? null) === JSON.stringify(b[k] ?? null));
}
/** kind 或 settings 变了,旧游标指的就不是同一个信箱 / 同一个文件夹 —— 清空重锚 */
export function cursorReset(existing: MailAccountRecord | undefined, input: MailAccountInput): boolean {
if (existing === undefined) return false;
return existing.kind !== input.kind || !sameSettings(existing.settings, input.settings);
}
export function mergeAccount(
input: MailAccountInput,
base: { readonly id: string; readonly existing: MailAccountRecord | undefined; readonly secretCipher: string | null; readonly now: Date },
): MailAccountRecord {
const reset = cursorReset(base.existing, input);
const existing = base.existing;
return {
id: base.id,
kind: input.kind,
name: input.name,
// wecom 的地址由探测回填,人不填;换成 imap-smtp 时以填的为准
address: input.kind === 'imap-smtp' ? input.address : existing?.kind === 'wecom' ? existing.address : '',
displayName: input.displayName,
enabled: input.enabled,
isDefault: input.isDefault,
pollIntervalSec: input.pollIntervalSec,
channelId: input.channelId,
backfillFrom: input.backfillFrom === null ? null : new Date(input.backfillFrom),
settings: { ...input.settings },
secretCipher: base.secretCipher,
cursor: reset ? null : (existing?.cursor ?? null),
lastPolledAt: existing?.lastPolledAt ?? null,
lastPollError: reset ? null : (existing?.lastPollError ?? null),
lastInboundAt: existing?.lastInboundAt ?? null,
pollingUntil: existing?.pollingUntil ?? null,
createdAt: existing?.createdAt ?? base.now,
updatedAt: base.now,
};
}