Files
wpywmail-client/tools/mime-test.js
T

133 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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: '[email protected]' },
to: [{ name: '收件人', address: '[email protected]' }],
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, '[email protected]');
// 解析出的正文统一用 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: '[email protected]' },
to: [{ name: '', address: '[email protected]' }],
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('"张三, 三" <[email protected]>, 李四 <[email protected]>, [email protected]');
check('地址解析数量', String(list.length), '3');
check('地址解析:引号内含逗号', list[0].name, '张三, 三');
check('地址解析:裸地址', list[2].address, '[email protected]');
}
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);