Archived
mail domain:解析结果 → 入站记录 / 视图,附件与原件按上限分存留注,厂商原话脱敏三形
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,126 @@
|
||||
/** 解析结果 → 入站记录:缺 Message-ID 顶上、线程取 In-Reply-To、纯 html 剥标签、附件按上限分「存」与「留注」 */
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { ParsedMail } from '../internal/domain/records.ts';
|
||||
import {
|
||||
ATTACHMENT_CAP,
|
||||
bodyTextOf,
|
||||
buildInboundRecord,
|
||||
detailOf,
|
||||
fallbackMessageId,
|
||||
planAttachments,
|
||||
rawOf,
|
||||
stripHtml,
|
||||
summaryOf,
|
||||
textPreviewOf,
|
||||
threadIdOf,
|
||||
} from '../internal/domain/inbound-record.ts';
|
||||
|
||||
const parsed: ParsedMail = {
|
||||
messageId: '<m1@acme.com>',
|
||||
inReplyTo: '<q1@we.com>',
|
||||
references: ['<root@we.com>', '<q1@we.com>'],
|
||||
from: { address: 'buyer@acme.com', name: '张三' },
|
||||
to: [{ address: 'sales@we.com', name: '' }],
|
||||
cc: [],
|
||||
subject: '询价 RFQ-1',
|
||||
text: '你好,请报价',
|
||||
html: '<p>你好,请报价</p>',
|
||||
date: new Date('2026-08-25T09:00:00Z'),
|
||||
attachments: [{ name: 'bom.xlsx', contentType: 'application/vnd.ms-excel', bytes: new Uint8Array(10) }],
|
||||
};
|
||||
const now = new Date('2026-08-25T10:00:00Z');
|
||||
|
||||
describe('线程与正文', () => {
|
||||
it('线程 = In-Reply-To;没有就取 References 末项;都没有就是 undefined', () => {
|
||||
expect(threadIdOf(parsed)).toBe('<q1@we.com>');
|
||||
expect(threadIdOf({ references: ['<a>', '<b>'] })).toBe('<b>');
|
||||
expect(threadIdOf({ references: [] })).toBeUndefined();
|
||||
});
|
||||
it('缺 Message-ID 用 providerRef@域顶上,不抛', () => {
|
||||
expect(fallbackMessageId('uid:7:12', 'sales@we.com')).toBe('<uid:7:12@we.com>');
|
||||
expect(fallbackMessageId('x', 'no-at')).toBe('<x@local>');
|
||||
});
|
||||
it('纯 html 的信剥标签当正文;script / style 整段丢;实体解回来;空白收成一个', () => {
|
||||
expect(stripHtml('<style>p{}</style><p>你好&<b>世界</b></p>\n<script>x()</script> 再见')).toBe('你好&世界 再见');
|
||||
expect(bodyTextOf({ html: '<div>只有 html</div>' })).toBe('只有 html');
|
||||
expect(bodyTextOf({ text: '有 text', html: '<i>忽略</i>' })).toBe('有 text');
|
||||
expect(bodyTextOf({})).toBe('');
|
||||
});
|
||||
it('预览 200 字', () => {
|
||||
expect(textPreviewOf('a'.repeat(300), null)).toHaveLength(200);
|
||||
expect(textPreviewOf(null, '<p>来自 html</p>')).toBe('来自 html');
|
||||
});
|
||||
});
|
||||
|
||||
describe('附件与原件', () => {
|
||||
it('超上限的附件不存,留注;下标按原顺序', () => {
|
||||
const plan = planAttachments(
|
||||
[
|
||||
{ name: 'a.pdf', contentType: 'application/pdf', bytes: new Uint8Array(3) },
|
||||
{ name: 'big.zip', contentType: 'application/zip', bytes: new Uint8Array(ATTACHMENT_CAP + 1) },
|
||||
],
|
||||
ATTACHMENT_CAP,
|
||||
);
|
||||
expect(plan.map((p) => [p.index, p.store])).toEqual([
|
||||
[0, true],
|
||||
[1, false],
|
||||
]);
|
||||
expect(plan[1]!.note).toContain('8MB');
|
||||
});
|
||||
it('原件超上限留空 + 注', () => {
|
||||
expect(rawOf(new Uint8Array(4), 8)).toEqual({ raw: new Uint8Array(4), note: '' });
|
||||
const big = rawOf(new Uint8Array(9), 8);
|
||||
expect(big.raw).toBeNull();
|
||||
expect(big.note).not.toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildInboundRecord / 视图', () => {
|
||||
const record = buildInboundRecord({
|
||||
id: 'in-1',
|
||||
accountId: 'acc-1',
|
||||
address: 'sales@we.com',
|
||||
providerRef: 'uid:7:12',
|
||||
parsed,
|
||||
eml: new Uint8Array([1, 2, 3]),
|
||||
receivedAt: now,
|
||||
attachments: [{ index: 0, name: 'bom.xlsx', contentType: 'application/vnd.ms-excel', size: 10, ref: 'up-1', note: '' }],
|
||||
now,
|
||||
});
|
||||
|
||||
it('记录照抄解析结果,raw 存下来,size 是原件字节数', () => {
|
||||
expect(record.messageId).toBe('<m1@acme.com>');
|
||||
expect(record.inReplyTo).toBe('<q1@we.com>');
|
||||
expect(record.from).toEqual({ address: 'buyer@acme.com', name: '张三' });
|
||||
expect(record.size).toBe(3);
|
||||
expect(record.raw).toEqual(new Uint8Array([1, 2, 3]));
|
||||
expect(record.sentAt?.toISOString()).toBe('2026-08-25T09:00:00.000Z');
|
||||
});
|
||||
it('缺 from / 缺 Message-ID 也能落记录', () => {
|
||||
const bare = buildInboundRecord({
|
||||
id: 'in-2',
|
||||
accountId: 'acc-1',
|
||||
address: 'sales@we.com',
|
||||
providerRef: 'uid:7:13',
|
||||
parsed: { references: [], to: [], cc: [], subject: '', attachments: [] },
|
||||
eml: new Uint8Array(),
|
||||
receivedAt: now,
|
||||
attachments: [],
|
||||
now,
|
||||
});
|
||||
expect(bare.messageId).toBe('<uid:7:13@we.com>');
|
||||
expect(bare.from).toEqual({ address: '', name: '' });
|
||||
});
|
||||
it('摘要不带正文只带预览;详情带正文、rawAvailable、投递', () => {
|
||||
const deliveries = [{ mailId: 'in-1', subscriber: 'channel', state: 'done' as const, error: null, startedAt: now, finishedAt: now }];
|
||||
const summary = summaryOf(record, deliveries);
|
||||
expect(summary.textPreview).toBe('你好,请报价');
|
||||
expect(summary.hasHtml).toBe(true);
|
||||
expect(summary.deliveries[0]).toEqual({ subscriber: 'channel', state: 'done', error: null, startedAt: now.toISOString(), finishedAt: now.toISOString() });
|
||||
expect('text' in summary).toBe(false);
|
||||
const detail = detailOf(record, deliveries);
|
||||
expect(detail.text).toBe('你好,请报价');
|
||||
expect(detail.rawAvailable).toBe(true);
|
||||
expect(detail.references).toEqual(['<root@we.com>', '<q1@we.com>']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
/** 脱敏三形:明文、base64、URL 编码 —— nodemailer 的 response 行会把 AUTH 明文 / 密文都带出来 */
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { errorText, redactSecrets, secretVariants } from '../internal/domain/redact.ts';
|
||||
|
||||
describe('redactSecrets', () => {
|
||||
it('三形都换成 ***', () => {
|
||||
const secret = 'p@ss w0rd';
|
||||
const text = `login p@ss w0rd failed; AUTH ${Buffer.from(secret).toString('base64')}; url ${encodeURIComponent(secret)}`;
|
||||
expect(redactSecrets(text, [secret])).toBe('login *** failed; AUTH ***; url ***');
|
||||
});
|
||||
it('短于 4 字或空的密钥不脱(否则一封信的每个字母都成了 ***)', () => {
|
||||
expect(secretVariants('')).toEqual([]);
|
||||
expect(secretVariants('abc')).toEqual([]);
|
||||
expect(redactSecrets('abc abc', ['abc'])).toBe('abc abc');
|
||||
});
|
||||
it('没命中原样返回;正则元字符当字面', () => {
|
||||
expect(redactSecrets('nothing here', ['s3cret!'])).toBe('nothing here');
|
||||
expect(redactSecrets('key=a.b*c', ['a.b*c'])).toBe('key=***');
|
||||
});
|
||||
});
|
||||
|
||||
describe('errorText', () => {
|
||||
it('Error 取 message;nodemailer 式的 response 一并带上;别的 String()', () => {
|
||||
expect(errorText(new Error('boom'))).toBe('boom');
|
||||
const withResponse = Object.assign(new Error('Invalid login'), { response: '535 5.7.8 Error: authentication failed' });
|
||||
expect(errorText(withResponse)).toBe('Invalid login —— 535 5.7.8 Error: authentication failed');
|
||||
expect(errorText('raw')).toBe('raw');
|
||||
expect(errorText(undefined)).toBe('未知错误');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user