/** * 脱敏 —— 凡要落库 / 上接口的厂商原话都先过这里(设计稿 §9.4)。 * 明文、base64(SMTP AUTH 的形)、URL 编码三形都换掉。 */ const MIN_SECRET_CHARS = 4; export function secretVariants(secret: string): string[] { if (secret.length < MIN_SECRET_CHARS) return []; const variants = [secret, Buffer.from(secret, 'utf8').toString('base64'), encodeURIComponent(secret)]; return [...new Set(variants.filter((v) => v.length >= MIN_SECRET_CHARS))]; } function escapeRegex(s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } export function redactSecrets(text: string, secrets: readonly string[]): string { let out = text; for (const secret of secrets) { for (const variant of secretVariants(secret)) { out = out.replace(new RegExp(escapeRegex(variant), 'g'), '***'); } } return out; } /** 错误 → 一句话;nodemailer / imapflow 把服务器回话挂在 `response` / `responseText` 上,一并带上 */ export function errorText(error: unknown): string { if (error instanceof Error) { const bag = error as Error & { response?: unknown; responseText?: unknown }; const response = typeof bag.response === 'string' ? bag.response : typeof bag.responseText === 'string' ? bag.responseText : ''; return response === '' || response === error.message ? error.message : `${error.message} —— ${response}`; } if (error === undefined || error === null) return '未知错误'; return String(error); }