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; } } }