Archived
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:
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user