193 lines
7.0 KiB
Dart
193 lines
7.0 KiB
Dart
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);
|