using System.Globalization;
using System.Text;
namespace WpywMail.Native;
/// 一个解析出来的附件。
public sealed record ParsedAttachment(string FileName, string ContentType, byte[] Data, string ContentId, bool Inline);
/// 解析结果。Text / Html 已经解码成可直接展示的字符串。
public sealed record ParsedMime(
string From,
string To,
string Cc,
string Subject,
string MessageId,
string InReplyTo,
string References,
DateTimeOffset? Date,
string Text,
string Html,
IReadOnlyList Attachments);
/// 待发送的附件。
public sealed record OutgoingAttachment(string FileName, string ContentType, byte[] Data, string ContentId = "", bool Inline = false);
/// 组装一封待发送邮件的入参。
public sealed record ComposeRequest(
string From,
string? FromDisplay,
string[] To,
string[] Cc,
string Subject,
string Text,
string? Html = null,
IReadOnlyList? Attachments = null,
string? MessageId = null,
string InReplyTo = "",
string References = "");
///
/// MIME 组装与解析。
///
/// 相比 v1 的关键修正:
/// 1. 组装:Message-ID 的域来自配置(不再写死),正文与附件一律 base64,
/// 非 ASCII 头一律 RFC 2047 编码并按 75 字符上限切分。
/// 2. 解析:正确解码 RFC 2047 编码字、Content-Transfer-Encoding(base64/QP)、
/// charset(含 GBK/GB18030),并支持 multipart 与附件。
///
public static class Mime
{
private const string Crlf = "\r\n";
// ---------------------------------------------------------------- 解析
public static ParsedMime Parse(byte[] raw)
{
var (headers, body) = SplitMessage(raw);
return ParseEntity(headers, body);
}
private static ParsedMime ParseEntity(List> headers, byte[] body)
{
string Header(string name) => headers.FirstOrDefault(h => h.Key.Equals(name, StringComparison.OrdinalIgnoreCase)).Value ?? "";
var from = AddressText(DecodeHeader(Header("From")));
var to = AddressText(DecodeHeader(Header("To")));
var cc = AddressText(DecodeHeader(Header("Cc")));
var subject = DecodeHeader(Header("Subject"));
if (string.IsNullOrWhiteSpace(subject)) subject = "(无主题)";
var contentType = Header("Content-Type");
var (mediaType, parameters) = ParseContentType(contentType);
var transferEncoding = Header("Content-Transfer-Encoding").Trim().ToLowerInvariant();
var text = new StringBuilder();
var html = new StringBuilder();
var attachments = new List();
if (mediaType.StartsWith("multipart/", StringComparison.OrdinalIgnoreCase) &&
parameters.TryGetValue("boundary", out var boundary) && !string.IsNullOrEmpty(boundary))
{
foreach (var part in SplitMultipart(body, boundary))
{
var (partHeaders, partBody) = SplitMessage(part);
var nested = ParseEntity(partHeaders, partBody);
// 嵌套的多部分(例如 mixed 里套 alternative)
if (nested.Text.Length > 0) text.Append(nested.Text);
if (nested.Html.Length > 0) html.Append(nested.Html);
attachments.AddRange(nested.Attachments);
}
}
else
{
var decoded = DecodeTransfer(body, transferEncoding);
var charset = parameters.TryGetValue("charset", out var cs) ? cs : null;
var content = DecodeCharset(decoded, charset);
var isAttachment = parameters.TryGetValue("name", out var name) && !string.IsNullOrWhiteSpace(name);
var disposition = Header("Content-Disposition");
var (dispType, dispParams) = ParseContentType(disposition);
if (dispType.Equals("attachment", StringComparison.OrdinalIgnoreCase) ||
(dispType.Equals("inline", StringComparison.OrdinalIgnoreCase) && !mediaType.StartsWith("text/", StringComparison.OrdinalIgnoreCase)))
isAttachment = true;
if (isAttachment)
{
var fileName = DecodeParameter(dispParams.GetValueOrDefault("filename*"))
?? DecodeHeader(dispParams.GetValueOrDefault("filename") ?? "")
?? DecodeParameter(parameters.GetValueOrDefault("name*"))
?? DecodeHeader(parameters.GetValueOrDefault("name") ?? "")
?? "attachment.bin";
if (string.IsNullOrWhiteSpace(fileName)) fileName = "attachment.bin";
attachments.Add(new ParsedAttachment(
fileName,
mediaType,
decoded,
Header("Content-ID").Trim('<', '>'),
dispType.Equals("inline", StringComparison.OrdinalIgnoreCase)));
}
else if (mediaType.Equals("text/html", StringComparison.OrdinalIgnoreCase))
{
html.Append(content);
}
else
{
text.Append(content);
}
}
return new ParsedMime(
from, to, cc, subject,
Header("Message-ID").Trim(),
Header("In-Reply-To").Trim(),
Header("References").Trim(),
ParseDate(Header("Date")),
text.ToString().TrimEnd(),
html.ToString().TrimEnd(),
attachments);
}
private static DateTimeOffset? ParseDate(string value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
if (DateTimeOffset.TryParse(value.Trim(), CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out var parsed)) return parsed;
// 常见变体:缺秒、单数字日等
var cleaned = value.Replace("GMT", "+0000").Replace("UT", "+0000").Trim();
if (DateTimeOffset.TryParse(cleaned, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out parsed)) return parsed;
return null;
}
/// 把原始字节切成「头(已展开)」与「正文」。
internal static (List> Headers, byte[] Body) SplitMessage(byte[] raw)
{
var separator = IndexOf(raw, Crlf + Crlf);
var separatorLength = 4;
if (separator < 0)
{
separator = IndexOf(raw, "\n\n");
separatorLength = 2;
}
var headerBytes = separator >= 0 ? raw[..separator] : raw;
var body = separator >= 0 ? raw[(separator + separatorLength)..] : [];
var headerText = DecodeHeaderBlock(headerBytes);
var headers = new List>();
string? currentName = null;
var currentValue = new StringBuilder();
void Flush()
{
if (currentName is not null) headers.Add(new KeyValuePair(currentName, currentValue.ToString().Trim()));
currentName = null;
currentValue.Clear();
}
foreach (var line in headerText.Split('\n'))
{
if (line.Length == 0) continue;
if ((line[0] == ' ' || line[0] == '\t') && currentName is not null)
{
currentValue.Append(' ').Append(line.Trim());
continue;
}
Flush();
var colon = line.IndexOf(':');
if (colon <= 0) continue;
currentName = line[..colon].Trim();
currentValue.Append(line[(colon + 1)..].Trim());
}
Flush();
return (headers, body);
}
///
/// 解码头部块。
///
/// 头部按标准应当是 ASCII(非 ASCII 必须用 RFC 2047 编码字),但现实中不少客户端
/// 直接把裸 UTF-8 写进头里(8bit 头)。这里优先按 UTF-8 严格解码,失败再回退
/// Latin-1(保证字节不丢);否则裸 UTF-8 的中文主题会变成 «æµè¯» 这种乱码。
///
private static string DecodeHeaderBlock(byte[] bytes)
{
try
{
return new UTF8Encoding(false, throwOnInvalidBytes: true).GetString(bytes).Replace("\r\n", "\n").Replace('\r', '\n');
}
catch (DecoderFallbackException)
{
return Encoding.Latin1.GetString(bytes).Replace("\r\n", "\n").Replace('\r', '\n');
}
}
/// 按 boundary 切分 multipart 正文。返回每个子部分的原始字节。
private static List SplitMultipart(byte[] body, string boundary)
{
var parts = new List();
var delimiter = Encoding.ASCII.GetBytes("--" + boundary);
var positions = new List();
for (var i = 0; i + delimiter.Length <= body.Length; i++)
{
if (body[i] != (byte)'-') continue;
var match = true;
for (var j = 0; j < delimiter.Length; j++)
{
if (body[i + j] != delimiter[j]) { match = false; break; }
}
if (match) { positions.Add(i); i += delimiter.Length - 1; }
}
if (positions.Count < 2) return parts;
for (var index = 0; index < positions.Count - 1; index++)
{
var start = positions[index];
// 跳过边界行自身
var lineEnd = IndexOf(body, start, "\n");
if (lineEnd < 0) continue;
start = lineEnd + 1;
var end = positions[index + 1];
// 去掉结尾的 CRLF
while (end > start && (body[end - 1] == (byte)'\n' || body[end - 1] == (byte)'\r')) end--;
if (end > start) parts.Add(body[start..end]);
}
return parts;
}
/// 解码 Content-Transfer-Encoding。
private static byte[] DecodeTransfer(byte[] body, string encoding) => encoding switch
{
"base64" => TryBase64(body),
"quoted-printable" => DecodeQuotedPrintable(body),
_ => body, // 7bit / 8bit / binary / 空
};
private static byte[] TryBase64(byte[] body)
{
// 去掉所有空白字符后解码;容错处理不完整的 base64
var buffer = new StringBuilder(body.Length);
foreach (var b in body)
{
if (b is (byte)' ' or (byte)'\r' or (byte)'\n' or (byte)'\t') continue;
if (b == (byte)'=') { buffer.Append('='); continue; }
buffer.Append((char)b);
}
var text = buffer.ToString();
var padding = text.Length % 4;
if (padding == 1) text = text[..^1];
else if (padding is 2 or 3) text += new string('=', 4 - padding);
try { return Convert.FromBase64String(text); }
catch (FormatException) { return []; }
}
private static byte[] DecodeQuotedPrintable(byte[] body)
{
using var output = new MemoryStream(body.Length);
for (var i = 0; i < body.Length; i++)
{
var b = body[i];
if (b != (byte)'=') { output.WriteByte(b); continue; }
// 软换行 =CRLF / =LF
if (i + 1 < body.Length && body[i + 1] == (byte)'\n') { i += 1; continue; }
if (i + 2 < body.Length && body[i + 1] == (byte)'\r' && body[i + 2] == (byte)'\n') { i += 2; continue; }
if (i + 2 < body.Length && IsHex(body[i + 1]) && IsHex(body[i + 2]))
{
output.WriteByte((byte)((HexValue(body[i + 1]) << 4) | HexValue(body[i + 2])));
i += 2;
continue;
}
output.WriteByte(b);
}
return output.ToArray();
static bool IsHex(byte b) => (b >= '0' && b <= '9') || (b >= 'A' && b <= 'F') || (b >= 'a' && b <= 'f');
static int HexValue(byte b) => b switch
{
>= (byte)'0' and <= (byte)'9' => b - '0',
>= (byte)'A' and <= (byte)'F' => b - 'A' + 10,
_ => b - 'a' + 10,
};
}
// ---------------------------------------------------------------- 头编码
/// 解码 RFC 2047 编码字;相邻编码字之间的空白会被丢弃。
public static string DecodeHeader(string value)
{
if (string.IsNullOrEmpty(value) || !value.Contains("=?")) return value ?? "";
var result = new StringBuilder();
var index = 0;
var lastWasEncoded = false;
while (index < value.Length)
{
var start = value.IndexOf("=?", index, StringComparison.Ordinal);
if (start < 0) { result.Append(value, index, value.Length - index); break; }
// 编码字之间的空格应被忽略
var gap = value[index..start];
if (!(lastWasEncoded && gap.Trim().Length == 0)) result.Append(gap);
else if (lastWasEncoded) { /* 丢弃 */ }
var firstQuestion = value.IndexOf('?', start + 2);
var secondQuestion = firstQuestion < 0 ? -1 : value.IndexOf('?', firstQuestion + 1);
var end = secondQuestion < 0 ? -1 : value.IndexOf("?=", secondQuestion + 1, StringComparison.Ordinal);
if (firstQuestion < 0 || secondQuestion < 0 || end < 0)
{
result.Append(value, start, value.Length - start);
break;
}
var charset = value[(start + 2)..firstQuestion];
var encoding = value[(firstQuestion + 1)..secondQuestion];
var payload = value[(secondQuestion + 1)..end];
try
{
byte[] bytes;
if (encoding.Equals("B", StringComparison.OrdinalIgnoreCase))
bytes = Convert.FromBase64String(payload);
else if (encoding.Equals("Q", StringComparison.OrdinalIgnoreCase))
bytes = DecodeQ(payload);
else { result.Append(value, start, end + 2 - start); index = end + 2; lastWasEncoded = false; continue; }
result.Append(DecodeCharset(bytes, charset));
lastWasEncoded = true;
}
catch
{
result.Append(value, start, end + 2 - start);
lastWasEncoded = false;
}
index = end + 2;
}
return result.ToString();
static byte[] DecodeQ(string payload)
{
using var output = new MemoryStream();
for (var i = 0; i < payload.Length; i++)
{
var c = payload[i];
if (c == '_') { output.WriteByte((byte)' '); continue; }
if (c == '=' && i + 2 < payload.Length)
{
try { output.WriteByte(Convert.ToByte(payload.Substring(i + 1, 2), 16)); i += 2; continue; }
catch { }
}
output.WriteByte((byte)c);
}
return output.ToArray();
}
}
/// 解码 RFC 2231 参数值(形如 UTF-8''%E4%B8%AD%文)。
private static string? DecodeParameter(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
var text = value.Trim();
var separator = text.IndexOf("''", StringComparison.Ordinal);
if (separator < 0) return null;
var charset = text[..separator];
var encoded = text[(separator + 2)..];
try
{
using var buffer = new MemoryStream(encoded.Length);
for (var i = 0; i < encoded.Length; i++)
{
if (encoded[i] == '%' && i + 2 < encoded.Length)
{
buffer.WriteByte(Convert.ToByte(encoded.Substring(i + 1, 2), 16));
i += 2;
}
else buffer.WriteByte((byte)encoded[i]);
}
return DecodeCharset(buffer.ToArray(), charset);
}
catch { return null; }
}
/// 对外暴露的 RFC 2047 编码入口(IMAP ENVELOPE 等需要把非 ASCII 头值变成 ASCII)。
public static string EncodeHeaderValue(string value) =>
EncodeHeader(value ?? "").Replace("\r\n", " ").Replace("\n", " ");
/// 需要时把文本编码成 RFC 2047 编码字(按 75 字符上限切分)。
private static string EncodeHeader(string value)
{
if (string.IsNullOrEmpty(value)) return "";
if (value.All(c => c is >= ' ' and <= '~')) return value;
var bytes = Encoding.UTF8.GetBytes(value);
var chunks = new List();
var offset = 0;
while (offset < bytes.Length)
{
// 每个编码字最多 75 字符;"=?UTF-8?B?" + "?=" 占 12 个字符 → base64 最多 63
var take = Math.Min(45, bytes.Length - offset);
// 不要把多字节字符切开
while (take > 1 && offset + take < bytes.Length && (bytes[offset + take] & 0xC0) == 0x80) take--;
chunks.Add("=?UTF-8?B?" + Convert.ToBase64String(bytes, offset, take) + "?=");
offset += take;
}
return chunks.Count == 1 ? chunks[0] : string.Join(Crlf + " ", chunks);
}
/// 解析地址列表,抽出纯地址。
public static string[] Addresses(string value)
{
if (string.IsNullOrWhiteSpace(value)) return [];
var result = new List();
var depth = 0;
var current = new StringBuilder();
foreach (var c in value)
{
switch (c)
{
case '<': depth++; current.Append(c); break;
case '>': depth = Math.Max(0, depth - 1); current.Append(c); break;
case ',' or ';' when depth == 0: Add(); break;
default: current.Append(c); break;
}
}
Add();
return result.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
void Add()
{
var item = current.ToString().Trim();
current.Clear();
if (item.Length == 0) return;
var start = item.IndexOf('<');
var end = item.IndexOf('>', start + 1);
var address = start >= 0 && end > start ? item[(start + 1)..end].Trim() : item;
if (address.Contains('@') && address.IndexOf('@') > 0 && address.IndexOf('@') < address.Length - 1) result.Add(address);
}
}
private static string AddressText(string value) => value ?? "";
internal static (string MediaType, Dictionary Parameters) ParseContentType(string value)
{
var parameters = new Dictionary(StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(value)) return ("text/plain", parameters);
var segments = value.Split(';');
var mediaType = segments[0].Trim().ToLowerInvariant();
for (var i = 1; i < segments.Length; i++)
{
var part = segments[i].Trim();
var equals = part.IndexOf('=');
if (equals <= 0) continue;
var name = part[..equals].Trim().ToLowerInvariant();
var val = part[(equals + 1)..].Trim().Trim('"');
parameters[name] = val;
}
return (mediaType, parameters);
}
/// 按 charset 解码字节;未知字符集回退 UTF-8。
public static string DecodeCharset(byte[] bytes, string? charset)
{
if (bytes.Length == 0) return "";
var encoding = ResolveEncoding(charset);
try { return encoding.GetString(bytes); }
catch { return Encoding.UTF8.GetString(bytes); }
}
private static Encoding ResolveEncoding(string? charset)
{
if (string.IsNullOrWhiteSpace(charset)) return Encoding.UTF8;
var name = charset.Trim().Trim('"').ToLowerInvariant();
if (name is "utf8" or "utf-8" or "unicode-1-1-utf-8") return new UTF8Encoding(false);
if (name is "us-ascii" or "ascii") return Encoding.ASCII;
if (name is "latin1" or "iso-8859-1" or "iso8859-1" or "windows-1252") return Encoding.Latin1;
try { return Encoding.GetEncoding(name); }
catch
{
if (name is "gb2312" or "gbk" or "gb18030" or "csgb2312" or "x-gbk")
{
var provider = CodePagesShim.Provider;
if (provider is not null)
{
try
{
Encoding.RegisterProvider(provider);
var codePage = name == "gb18030" ? 54936 : 936;
return Encoding.GetEncoding(codePage);
}
catch { }
}
}
AppLog.Warn($"[MIME] 不支持的字符集 {charset},按 UTF-8 处理。");
return Encoding.UTF8;
}
}
// ---------------------------------------------------------------- 组装
public static byte[] Build(ComposeRequest request, AppConfig config)
{
var messageId = string.IsNullOrWhiteSpace(request.MessageId)
? $"<{Guid.NewGuid():N}@{config.Domain}>"
: (request.MessageId.StartsWith('<') ? request.MessageId : $"<{request.MessageId}>");
var attachments = request.Attachments ?? [];
var hasHtml = !string.IsNullOrWhiteSpace(request.Html);
var textPartBytes = Encoding.UTF8.GetBytes(request.Text ?? "");
var htmlPartBytes = hasHtml ? Encoding.UTF8.GetBytes(request.Html!) : [];
var headers = new StringBuilder();
headers.Append("Date: ").Append(FormatDate(DateTimeOffset.UtcNow)).Append(Crlf);
headers.Append("From: ").Append(FormatAddress(request.From, request.FromDisplay)).Append(Crlf);
headers.Append("To: ").Append(string.Join(", ", request.To.Select(a => FormatAddress(a, null)))).Append(Crlf);
if (request.Cc.Length > 0) headers.Append("Cc: ").Append(string.Join(", ", request.Cc.Select(a => FormatAddress(a, null)))).Append(Crlf);
headers.Append("Subject: ").Append(EncodeHeader(request.Subject ?? "")).Append(Crlf);
headers.Append("Message-ID: ").Append(messageId).Append(Crlf);
if (!string.IsNullOrWhiteSpace(request.InReplyTo)) headers.Append("In-Reply-To: ").Append(request.InReplyTo.Trim()).Append(Crlf);
if (!string.IsNullOrWhiteSpace(request.References)) headers.Append("References: ").Append(request.References.Trim()).Append(Crlf);
headers.Append("MIME-Version: 1.0").Append(Crlf);
var body = new MemoryStream();
if (attachments.Count > 0)
{
var mixedBoundary = "mix-" + Guid.NewGuid().ToString("N")[..16];
headers.Append("Content-Type: multipart/mixed; boundary=\"").Append(mixedBoundary).Append('"').Append(Crlf);
headers.Append(Crlf);
WriteBoundary(body, mixedBoundary, false);
body.Write(hasHtml ? BuildAlternativePart(textPartBytes, htmlPartBytes) : BuildTextPart(textPartBytes));
foreach (var attachment in attachments)
{
WriteBoundary(body, mixedBoundary, false);
var part = new StringBuilder();
part.Append("Content-Type: ").Append(attachment.ContentType).Append("; ")
.Append(FileNameParameters(attachment.FileName, "name")).Append(Crlf);
part.Append("Content-Transfer-Encoding: base64").Append(Crlf);
part.Append("Content-Disposition: ").Append(attachment.Inline ? "inline" : "attachment").Append("; ")
.Append(FileNameParameters(attachment.FileName, "filename")).Append(Crlf);
if (!string.IsNullOrWhiteSpace(attachment.ContentId))
part.Append("Content-ID: <").Append(attachment.ContentId.Trim('<', '>')).Append('>').Append(Crlf);
part.Append(Crlf);
part.Append(WrapBase64(attachment.Data));
body.Write(Encoding.ASCII.GetBytes(part.ToString()));
}
WriteBoundary(body, mixedBoundary, true);
}
else if (hasHtml)
{
var boundary = "alt-" + Guid.NewGuid().ToString("N")[..16];
headers.Append("Content-Type: multipart/alternative; boundary=\"").Append(boundary).Append('"').Append(Crlf);
headers.Append(Crlf);
body.Write(BuildAlternativeWithBoundary(textPartBytes, htmlPartBytes, boundary));
}
else
{
headers.Append("Content-Type: text/plain; charset=UTF-8").Append(Crlf);
headers.Append("Content-Transfer-Encoding: base64").Append(Crlf);
headers.Append(Crlf);
body.Write(Encoding.ASCII.GetBytes(WrapBase64(textPartBytes)));
}
var output = new MemoryStream();
output.Write(Encoding.ASCII.GetBytes(headers.ToString()));
body.Position = 0;
body.CopyTo(output);
return output.ToArray();
}
private static byte[] BuildTextPart(byte[] textBytes)
{
var part = new StringBuilder();
part.Append("Content-Type: text/plain; charset=UTF-8").Append(Crlf);
part.Append("Content-Transfer-Encoding: base64").Append(Crlf);
part.Append(Crlf);
part.Append(WrapBase64(textBytes));
return Encoding.ASCII.GetBytes(part.ToString());
}
/// 作为 multipart/mixed 的子部分时,必须带上自己的 Content-Type 头。
private static byte[] BuildAlternativePart(byte[] textBytes, byte[] htmlBytes)
{
var boundary = "alt-" + Guid.NewGuid().ToString("N")[..16];
var output = new MemoryStream();
output.Write(Encoding.ASCII.GetBytes("Content-Type: multipart/alternative; boundary=\"" + boundary + "\"" + Crlf + Crlf));
output.Write(BuildAlternativeWithBoundary(textBytes, htmlBytes, boundary));
return output.ToArray();
}
/// 附件名参数:ASCII 回退 + RFC 2231 扩展写法,保证中文文件名不乱码。
private static string FileNameParameters(string fileName, string parameterName)
{
var ascii = new string(fileName.Select(c => c is >= ' ' and <= '~' && c != '"' && c != '\\' ? c : '_').ToArray());
if (ascii.Length == 0) ascii = "attachment";
return $"{parameterName}=\"{ascii}\"; {parameterName}*=UTF-8''{Uri.EscapeDataString(fileName)}";
}
private static byte[] BuildAlternativeWithBoundary(byte[] textBytes, byte[] htmlBytes, string boundary)
{
var output = new MemoryStream();
WriteBoundary(output, boundary, false);
output.Write(BuildTextPart(textBytes));
WriteBoundary(output, boundary, false);
var html = new StringBuilder();
html.Append("Content-Type: text/html; charset=UTF-8").Append(Crlf);
html.Append("Content-Transfer-Encoding: base64").Append(Crlf);
html.Append(Crlf);
html.Append(WrapBase64(htmlBytes));
output.Write(Encoding.ASCII.GetBytes(html.ToString()));
WriteBoundary(output, boundary, true);
return output.ToArray();
}
private static void WriteBoundary(Stream stream, string boundary, bool closing)
{
var line = "--" + boundary + (closing ? "--" : "") + Crlf;
stream.Write(Encoding.ASCII.GetBytes(line));
}
private static string FormatAddress(string address, string? display)
{
address = address.Trim().Trim('<', '>');
if (string.IsNullOrWhiteSpace(display)) return address;
return $"{EncodeHeader(display)} <{address}>";
}
/// RFC 5322 日期(必须是 ±HHMM 形式的时区)。
public static string FormatDate(DateTimeOffset value)
{
var offset = value.Offset;
var sign = offset < TimeSpan.Zero ? "-" : "+";
var absolute = offset.Duration();
return value.ToString("ddd, dd MMM yyyy HH:mm:ss ", CultureInfo.InvariantCulture) +
$"{sign}{absolute.Hours:D2}{absolute.Minutes:D2}";
}
private static string WrapBase64(byte[] bytes)
{
var encoded = Convert.ToBase64String(bytes);
var builder = new StringBuilder(encoded.Length + encoded.Length / 76 * 2 + 2);
for (var index = 0; index < encoded.Length; index += 76)
{
builder.Append(encoded, index, Math.Min(76, encoded.Length - index)).Append(Crlf);
}
return builder.ToString();
}
// ---------------------------------------------------------------- 工具
private static int IndexOf(byte[] data, string text) => IndexOf(data, 0, text);
private static int IndexOf(byte[] data, int start, string text)
{
var pattern = Encoding.ASCII.GetBytes(text);
for (var i = start; i + pattern.Length <= data.Length; i++)
{
var match = true;
for (var j = 0; j < pattern.Length; j++)
{
if (data[i + j] != pattern[j]) { match = false; break; }
}
if (match) return i;
}
return -1;
}
}
///
/// 可选注册 .NET 的代码页编码提供程序(用于 GBK/GB18030)。
/// 未引用 System.Text.Encoding.CodePages 包时静默降级为 UTF-8。
///
internal static class CodePagesShim
{
private static EncodingProvider? provider;
private static bool resolved;
public static EncodingProvider? Provider
{
get
{
if (!resolved)
{
resolved = true;
try
{
var type = Type.GetType("System.Text.CodePagesEncodingProvider, System.Text.Encoding.CodePages", throwOnError: false);
provider = type?.GetProperty("Instance", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)?.GetValue(null) as EncodingProvider;
if (provider is not null) AppLog.Info("[MIME] 已启用代码页编码支持(GBK/GB18030 可正常解码)。");
else AppLog.Warn("[MIME] 未找到代码页编码支持;GBK/GB2312 编码的中文邮件可能解码异常。");
}
catch { provider = null; }
}
return provider;
}
}
}