- 登录: QQ/163/126/189/搜狐/Gmail/Outlook/Yahoo/iCloud/企业邮/自定义, 自动识别+测试连接 - 收信: IMAP 多文件夹同步/未读/星标/搜索(本地+服务器) - 读信: HTML渲染/CID内嵌图/原文查看/附件下载 - 写信: 收件人/抄送/密送/附件/富文本/草稿/回复转发 - 存储: SQLite + 系统安全存储(DPAPI/Keychain/Keystore) - 测试: 单元测试 + Mock IMAP/SMTP 服务器集成测试(23项全过) - 可迁移: Windows/Android/iOS/macOS/Linux 一套代码
115 lines
3.5 KiB
Dart
115 lines
3.5 KiB
Dart
/// 地址(发件人/收件人)与邮件相关模型
|
|
library;
|
|
|
|
class MailAddress {
|
|
final String name;
|
|
final String email;
|
|
|
|
const MailAddress(this.name, this.email);
|
|
|
|
bool get isEmpty => email.trim().isEmpty;
|
|
String get display => name.trim().isNotEmpty ? '$name <$email>' : email;
|
|
|
|
factory MailAddress.parse(String raw) {
|
|
final s = raw.trim();
|
|
if (s.isEmpty) return const MailAddress('', '');
|
|
// "Name" <email@x.com>
|
|
final m1 = RegExp(r'"([^"]*)"\s*<([^>]+)>').firstMatch(s);
|
|
if (m1 != null) return MailAddress(m1.group(1)!, m1.group(2)!.trim());
|
|
// Name <email@x.com>
|
|
final m2 = RegExp(r'([^<]*?)\s*<([^>]+)>').firstMatch(s);
|
|
if (m2 != null) {
|
|
return MailAddress(m2.group(1)!.trim(), m2.group(2)!.trim());
|
|
}
|
|
// 纯 email
|
|
return MailAddress('', s);
|
|
}
|
|
|
|
static List<MailAddress> parseList(String? raw) {
|
|
if (raw == null || raw.trim().isEmpty) return const [];
|
|
// 按逗号分割但跳过引号内的逗号
|
|
final result = <MailAddress>[];
|
|
final buf = StringBuffer();
|
|
var inQuote = false;
|
|
for (var i = 0; i < raw.length; i++) {
|
|
final c = raw[i];
|
|
if (c == '"') inQuote = !inQuote;
|
|
if (c == ',' && !inQuote) {
|
|
final a = MailAddress.parse(buf.toString());
|
|
if (!a.isEmpty) result.add(a);
|
|
buf.clear();
|
|
} else {
|
|
buf.write(c);
|
|
}
|
|
}
|
|
final last = MailAddress.parse(buf.toString());
|
|
if (!last.isEmpty) result.add(last);
|
|
return result;
|
|
}
|
|
|
|
Map<String, Object?> toJson() => {'name': name, 'email': email};
|
|
|
|
factory MailAddress.fromJson(Map<String, Object?> j) =>
|
|
MailAddress((j['name'] as String?) ?? '', (j['email'] as String?) ?? '');
|
|
}
|
|
|
|
class MailAttachment {
|
|
final String fileName;
|
|
final String contentType;
|
|
final String? contentId; // 内嵌图片 CID
|
|
final bool inline;
|
|
final int size;
|
|
final String? localPath; // 已下载到本地的路径
|
|
final String? encoding; // base64 / quoted-printable / 7bit / 8bit / binary
|
|
final Uint8ListBytes? data; // 内存中的原始数据(可选, 大附件不常驻)
|
|
|
|
const MailAttachment({
|
|
required this.fileName,
|
|
this.contentType = 'application/octet-stream',
|
|
this.contentId,
|
|
this.inline = false,
|
|
this.size = 0,
|
|
this.localPath,
|
|
this.encoding,
|
|
this.data,
|
|
});
|
|
|
|
MailAttachment copyWith({String? localPath, Uint8ListBytes? data}) =>
|
|
MailAttachment(
|
|
fileName: fileName,
|
|
contentType: contentType,
|
|
contentId: contentId,
|
|
inline: inline,
|
|
size: size,
|
|
localPath: localPath ?? this.localPath,
|
|
encoding: encoding,
|
|
data: data ?? this.data,
|
|
);
|
|
|
|
Map<String, Object?> toJson() => {
|
|
'fileName': fileName,
|
|
'contentType': contentType,
|
|
'contentId': contentId,
|
|
'inline': inline ? 1 : 0,
|
|
'size': size,
|
|
'localPath': localPath,
|
|
'encoding': encoding,
|
|
};
|
|
|
|
factory MailAttachment.fromJson(Map<String, Object?> j) => MailAttachment(
|
|
fileName: (j['fileName'] as String?) ?? '',
|
|
contentType: (j['contentType'] as String?) ?? 'application/octet-stream',
|
|
contentId: j['contentId'] as String?,
|
|
inline: (j['inline'] as int?) == 1,
|
|
size: (j['size'] as int?) ?? 0,
|
|
localPath: j['localPath'] as String?,
|
|
encoding: j['encoding'] as String?,
|
|
);
|
|
}
|
|
|
|
/// 用于避免直接引入 dart:typed_data 到模型层的轻量包装(可序列化)
|
|
class Uint8ListBytes {
|
|
final List<int> bytes;
|
|
const Uint8ListBytes(this.bytes);
|
|
}
|