'use strict'; /** * MIME 层单元测试(纯本地,不联网)。 * 这一层是「中文对不对」的关键,所以要有可回归的测试。 * 真机踩过的坑(都已固化为用例): * - RFC 2231 的 filename* 若先 decodeURIComponent 再 Buffer.from(str,'binary'), * 多字节字符会被截断成 U+FFFD → 中文附件名变乱码 * - ENVELOPE / 头部的编码字必须解 RFC 2047,否则界面显示 =?UTF-8?B?...?= * - 裸 UTF-8 头部要按 UTF-8 优先解,否则变 [æµè¯] */ const mime = require('../server/mime'); const results = []; function check(name, actual, expected) { const ok = actual === expected; results.push({ name, ok, actual, expected }); process.stdout.write(`${ok ? 'PASS' : 'FAIL'} ${name}\n`); if (!ok) process.stdout.write(` 期望 ${JSON.stringify(expected)}\n 实际 ${JSON.stringify(actual)}\n`); } /** 生成 RFC 2047 的 B 编码字(测试里按需现算,避免手写 base64 写错) */ function b64word(text) { return `=?UTF-8?B?${Buffer.from(text, 'utf8').toString('base64')}?=`; } // ── 1. 附件文件名 // encoding 用来模拟报文里的真实字节:正常头部是 ASCII/Latin-1,而「裸 UTF-8」头部要按 UTF-8 装字节 function fname(headerLine, encoding = 'latin1') { const headers = mime.parseHeaders(Buffer.from(headerLine, encoding)); return mime.filenameFrom(headers); } check('filename* RFC2231 中文(真机报文原样)', fname('Content-Disposition: attachment; filename="____.txt"; filename*=UTF-8\'\'%E9%AA%8C%E6%94%B6%E9%99%84%E4%BB%B6.txt'), '验收附件.txt'); check('filename* 在 Content-Type 的 name*', fname('Content-Type: application/octet-stream; name*=UTF-8\'\'%E4%B8%AD%E6%96%87.zip'), '中文.zip'); check('普通 ASCII 文件名', fname('Content-Disposition: attachment; filename="report.pdf"'), 'report.pdf'); check('裸 UTF-8 中文文件名(未做编码的历史客户端)', fname('Content-Disposition: attachment; filename="原始中文名.txt"', 'utf8'), '原始中文名.txt'); check('RFC 2231 分段续行 filename*0* / filename*1*', fname("Content-Disposition: attachment; filename*0*=UTF-8''%E9%AA%8C%E6%94%B6; filename*1*=%E9%99%84%E4%BB%B6.txt"), '验收附件.txt'); // ── 2. RFC 2047 编码字 check('B 编码字(中文主题)', mime.decodeWords('=?UTF-8?B?5a6i5oi356uv6Ieq5rWL?='), '客户端自测'); check('Q 编码字(下划线代表空格)', mime.decodeWords('=?utf-8?Q?=E4=BD=A0=E5=A5=BD_=E4=B8=96=E7=95=8C?='), '你好 世界'); check('相邻编码字之间的空白被丢弃(RFC 2047)', mime.decodeWords('=?UTF-8?B?5a6i5oi3?= =?UTF-8?B?56uv6Ieq5rWL?='), '客户端自测'); check('编码字与纯文本混排', mime.decodeWords('Report for ' + b64word('客户') + ' end'), 'Report for 客户 end'); // ── 3. 裸 UTF-8 头部(历史客户端常见) { const raw = Buffer.from('Subject: 中文主题', 'utf8').toString('latin1'); check('裸 UTF-8 头部按 UTF-8 优先解', mime.smartDecodeHeader(raw.slice(9)), '中文主题'); } // ── 4. 组装 → 解析 往返(含中文与全角标点) { const text = '你好,世界。()《》——、;:!?\r\n第二行中文。'; const raw = mime.buildMessage({ from: { name: '测试 发件人', address: 'a@wpy.email' }, to: [{ name: '收件人', address: 'b@wpy.email' }], subject: '中文主题 with ASCII · 标点测试', text, domain: 'wpy.email', }); const parsed = mime.parseMessage(raw); check('往返:主题', parsed.subject, '中文主题 with ASCII · 标点测试'); check('往返:发件人显示名', parsed.from[0].name, '测试 发件人'); check('往返:收件人地址', parsed.to[0].address, 'b@wpy.email'); // 解析出的正文统一用 LF(界面以 pre-wrap 渲染,LF 才是 DOM 的自然换行) check('往返:正文(含全角标点)', parsed.text, text.replace(/\r\n/g, '\n')); check('往返:头部不含裸非 ASCII(可安全过 SMTP)', /^[\x00-\x7f]*$/.test(raw.subarray(0, raw.indexOf('\r\n\r\n')).toString('latin1')), true); } // ── 5. 带附件往返 { const content = Buffer.from('中文附件内容 123', 'utf8'); const raw = mime.buildMessage({ from: { name: 'W', address: 'a@wpy.email' }, to: [{ name: '', address: 'b@wpy.email' }], subject: '带附件', text: '正文', domain: 'wpy.email', attachments: [{ filename: '验收附件.txt', contentType: 'text/plain', content }], }); const parsed = mime.parseMessage(raw); check('往返:附件数量', String(parsed.attachments.length), '1'); check('往返:附件名(中文)', parsed.attachments[0].filename, '验收附件.txt'); check('往返:附件内容逐字节一致', String(parsed.attachments[0].content.equals(content)), 'true'); check('往返:正文仍可读', parsed.text, '正文'); } // ── 6. 字符集 check('GBK 正文解码', mime.decodeCharset(Buffer.from([0xC4, 0xE3, 0xBA, 0xC3]), 'gbk'), '你好'); check('quoted-printable 解码', mime.decodeCharset(mime.decodeQuotedPrintable(Buffer.from('=E4=BD=A0=E5=A5=BD', 'latin1')), 'utf-8'), '你好'); check('base64 传输解码', mime.decodeCharset(mime.decodeTransfer(Buffer.from(Buffer.from('你好', 'utf8').toString('base64'), 'latin1'), 'base64'), 'utf-8'), '你好'); // ── 7. 地址解析 { const list = mime.parseAddressList('"张三, 三" , 李四 , e@f.com'); check('地址解析数量', String(list.length), '3'); check('地址解析:引号内含逗号', list[0].name, '张三, 三'); check('地址解析:裸地址', list[2].address, 'e@f.com'); } const pass = results.filter((r) => r.ok).length; const fail = results.length - pass; process.stdout.write(`\n汇总:通过 ${pass} / ${results.length},失败 ${fail}\n`); process.exit(fail ? 1 : 0);