Files

193 lines
7.0 KiB
Dart
Raw Permalink Normal View History

import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:mail_hub/core/models/address.dart';
import 'package:mail_hub/core/protocol/mime/mime_builder.dart';
import 'package:mail_hub/core/protocol/mime/mime_codec.dart';
import 'package:mail_hub/core/protocol/mime/mime_parser.dart';
void main() {
group('MIME 解析', () {
test('解析基本头 + 主题 RFC2047', () {
final raw = [
'Subject: =?UTF-8?B?5L2g55qE6YKu5Lu2?=',
'From: "张三" <zhangsan@example.com>',
'To: lisi@example.com, "Wang Wu" <wangwu@example.com>',
'Date: Tue, 18 Aug 2026 08:30:00 +0800',
'Content-Type: text/plain; charset="utf-8"',
'Message-ID: <abc123@example.com>',
'',
'你好世界!',
].join('\r\n');
final parsed = MimeParser.parse(ascii(raw));
expect(parsed.subject, '你的邮件');
expect(parsed.from!.name, '张三');
expect(parsed.from!.email, 'zhangsan@example.com');
expect(parsed.to.length, 2);
expect(parsed.to[1].name, 'Wang Wu');
expect(parsed.textBody, '你好世界!');
expect(parsed.date!.year, 2026);
});
test('multipart/alternative 取 HTML 与纯文本', () {
final boundary = '----=_test_1234567890abcdef';
final raw = [
'Subject: test',
'Content-Type: multipart/alternative; boundary="$boundary"',
'',
'--$boundary',
'Content-Type: text/plain; charset="utf-8"',
'Content-Transfer-Encoding: base64',
'',
base64.encode(utf8.encode('纯文本内容')),
'--$boundary',
'Content-Type: text/html; charset="utf-8"',
'Content-Transfer-Encoding: quoted-printable',
'',
'<b>=E7=BA=AF=E6=96=87=E6=9C=AC</b>',
'--$boundary--',
'',
].join('\r\n');
final parsed = MimeParser.parse(ascii(raw));
expect(parsed.textBody, '纯文本内容');
expect(parsed.htmlBody, '<b>纯文本</b>');
});
test('multipart/mixed 带附件', () {
final boundary = '----=_test_mixed_1234567890abcdef';
final raw = [
'Subject: with attachment',
'Content-Type: multipart/mixed; boundary="$boundary"',
'',
'--$boundary',
'Content-Type: text/plain; charset="utf-8"',
'',
'正文',
'--$boundary',
'Content-Type: application/pdf; name="=?UTF-8?B?5oql5ZGK5paH5Lu2LnBkZg==?="',
'Content-Transfer-Encoding: base64',
'Content-Disposition: attachment; filename="=?UTF-8?B?5oql5ZGK5paH5Lu2LnBkZg==?="',
'',
'JVBERi0xLjQ=',
'--$boundary--',
'',
].join('\r\n');
final parsed = MimeParser.parse(ascii(raw));
expect(parsed.textBody, '正文');
expect(parsed.attachments.length, 1);
expect(parsed.attachments[0].fileName, '报告文件.pdf');
expect(parsed.attachments[0].contentType, 'application/pdf');
expect(parsed.attachments[0].size, greaterThan(0));
});
test('GBK 编码中文邮件', () {
// GBK: 测试 = B2 E2 CA D4 ; 主题 "测试" base64 = suLK1A==
final raw = 'Subject: =?GBK?B?suLK1A==?=\r\n'
'Content-Type: text/plain; charset="gbk"\r\n\r\n';
final body = <int>[0xB2, 0xE2, 0xCA, 0xD4]; // 测试 (GBK)
final parsed = MimeParser.parse([...ascii(raw), ...body]);
expect(parsed.subject, '测试');
expect(parsed.textBody, '测试');
});
test('内嵌图片 CID', () {
final boundary = '----=_test_cid_1234567890abcdef';
final raw = [
'Subject: inline',
'Content-Type: multipart/related; boundary="$boundary"',
'',
'--$boundary',
'Content-Type: text/html; charset="utf-8"',
'',
'<img src="cid:img1@example.com">',
'--$boundary',
'Content-Type: image/png',
'Content-Transfer-Encoding: base64',
'Content-ID: <img1@example.com>',
'Content-Disposition: inline',
'',
'iVBORw0KGgo=',
'--$boundary--',
'',
].join('\r\n');
final parsed = MimeParser.parse(ascii(raw));
expect(parsed.htmlBody, contains('cid:img1@example.com'));
expect(parsed.inlineImages.containsKey('img1@example.com'), true);
expect(parsed.inlineImages['img1@example.com']!.length, 8);
});
test('RFC2231 文件名', () {
final raw = [
'Content-Type: application/octet-stream; name*=UTF-8\'\'%E6%B5%8B%E8%AF%95.txt',
'Content-Disposition: attachment; filename*=UTF-8\'\'%E6%B5%8B%E8%AF%95.txt',
'',
'data',
].join('\r\n');
final parsed = MimeParser.parse(ascii(raw));
expect(parsed.attachments.length, 1);
expect(parsed.attachments[0].fileName, '测试.txt');
});
});
group('编码解码', () {
test('RFC2047 编码词解码', () {
expect(CharsetDecoder.decodeEncodedWords('=?UTF-8?B?5L2g5aW9?='), '你好');
expect(CharsetDecoder.decodeEncodedWords('=?utf-8?q?hello=20world?='),
'hello world');
expect(CharsetDecoder.decodeEncodedWords('plain text'), 'plain text');
expect(
CharsetDecoder.decodeEncodedWords(
'=?UTF-8?B?5L2g5aW9?= =?UTF-8?B?5LiW55WM?='),
'你好世界');
});
test('Quoted-Printable 解码', () {
final decoded = TransferDecoder.decode(
ascii('a=20b=0D=0Ac=E4=B8=AD'), 'quoted-printable');
expect(latin1.decode(decoded), contains('a b'));
expect(utf8.decode(decoded), contains('中'));
});
});
group('MimeBuilder 往返', () {
test('构建 + 解析 multipart/alternative', () {
final raw = MimeBuilder.build(
from: const MailAddress('张三', 'zhangsan@example.com'),
to: const [MailAddress('李四', 'lisi@example.com')],
subject: '测试主题',
textBody: '纯文本部分',
htmlBody: '<b>HTML部分</b>',
);
final parsed = MimeParser.parse(raw);
expect(parsed.subject, '测试主题');
expect(parsed.textBody, '纯文本部分');
expect(parsed.htmlBody, contains('HTML部分'));
});
test('构建 + 解析带附件邮件', () {
final raw = MimeBuilder.build(
from: const MailAddress('张三', 'zhangsan@example.com'),
to: const [MailAddress('李四', 'lisi@example.com')],
subject: '附件邮件',
textBody: '请查收附件',
attachments: [
(
name: '数据.txt',
bytes: utf8.encode('hello 附件'),
contentType: 'text/plain',
),
],
);
final parsed = MimeParser.parse(raw);
expect(parsed.textBody, '请查收附件');
expect(parsed.attachments.length, 1);
expect(parsed.attachments[0].fileName, '数据.txt');
expect(utf8.decode(parsed.attachments[0].data!.bytes), 'hello 附件');
});
});
}
// ---- 测试辅助 ----
// 真实邮件原始字节通常为 ASCII(中文走编码词/base64),
// 但为方便测试直接用 UTF-8 编码原始串
List<int> ascii(String s) => utf8.encode(s);