From e7c758ea75e6fef941d6d3c502e5199d5e6e8747 Mon Sep 17 00:00:00 2001 From: alpapad Date: Sat, 30 May 2026 16:49:59 +0300 Subject: [PATCH] fix: support utf-7 and legacy encodings in email parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register CodePagesEncodingProvider to enable legacy code page encodings (ISO-8859-8, Windows-1252, etc.) which are commonly used in older email clients and are not available by default in .NET 5+. Add Utf7EncodingProvider and Utf7WrapperEncoding to re-enable UTF-7 decoding for legacy email messages. .NET 9+ blocks Encoding.GetEncoding(65000) and Encoding.GetEncoding("utf-7") before consulting registered providers. The workaround registers a custom encoding that reports CodePage=65002 (a private-use value not blocked by .NET). MimeKit caches utf-7→65002 and subsequent codepage lookups are intercepted by the provider, delegating actual encoding/decoding to the still-functional Encoding.UTF7 instance. --- Rnwood.Smtp4dev/Program.cs | 81 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/Rnwood.Smtp4dev/Program.cs b/Rnwood.Smtp4dev/Program.cs index 79dabe6a8..5e2085161 100644 --- a/Rnwood.Smtp4dev/Program.cs +++ b/Rnwood.Smtp4dev/Program.cs @@ -56,6 +56,16 @@ private static bool IsDeliverToStdoutEnabled(IEnumerable args) public static async Task Main(string[] args) { + // Register encoding provider for legacy code pages (ISO-8859-8-i, ISO-8859-1, cp1252, etc.) + // This must be done before any encoding operations occur + System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); + + // Re-enable UTF-7 encoding lookup by name for legacy email messages. + // .NET 5+ marks UTF-7 as obsolete but the encoding is still accessible via Encoding.UTF7. + // Some email clients encode headers/bodies as utf-7 and MimeKit resolves charsets by name, + // so we register a provider that maps the utf-7 name to the built-in instance. + System.Text.Encoding.RegisterProvider(new Utf7EncodingProvider()); + bool isHelpRequest = args.Any(a => a == "-h" || a == "--help" || a == "-?"); if (!isHelpRequest && args.Contains("--install-service")) @@ -508,4 +518,75 @@ internal partial class SettingsDebugInfoSerializationContext : JsonSerializerCon { } + + /// + /// Re-enables UTF-7 encoding for legacy email parsing on .NET 9+. + /// + /// Background: .NET 9+ blocks Encoding.GetEncoding("utf-7") and + /// Encoding.GetEncoding(65000) with a + /// before registered providers are consulted for the codepage path. + /// However, the NAME-based path (Encoding.GetEncoding(string)) DOES + /// consult providers first. + /// + /// MimeKit resolves charset names by converting "utf-7" → codepage 65000 and + /// calling Encoding.GetEncoding(65000) — which throws. To work around this + /// we return a whose CodePage is set to + /// a private-use value (65002) that is NOT blocked by .NET. MimeKit then caches + /// "utf-7" → 65002 and calls Encoding.GetEncoding(65002), which our + /// provider intercepts and returns the working wrapper. + /// + internal sealed class Utf7EncodingProvider : System.Text.EncodingProvider + { + private static readonly Utf7WrapperEncoding _instance = new(); + + public override System.Text.Encoding GetEncoding(string name) + { + if (name is null) return null; + return name.Equals("utf-7", StringComparison.OrdinalIgnoreCase) + || name.Equals("utf7", StringComparison.OrdinalIgnoreCase) + || name.Equals("unicode-1-1-utf-7", StringComparison.OrdinalIgnoreCase) + || name.Equals("csunicode11utf7", StringComparison.OrdinalIgnoreCase) + ? _instance : null; + } + + // 65002 is our private-use stand-in for UTF-7; it is not in .NET's internal + // encoding table so providers ARE consulted for it. + public override System.Text.Encoding GetEncoding(int codepage) + => codepage == 65002 ? _instance : null; + } + + /// + /// A thin wrapper around that reports + /// codepage 65002 instead of 65000, allowing it to be retrieved via + /// Encoding.GetEncoding(65002) without hitting .NET 9+'s UTF-7 block. + /// All actual encoding/decoding is delegated to the real UTF-7 encoding. + /// + internal sealed class Utf7WrapperEncoding : System.Text.Encoding + { +#pragma warning disable SYSLIB0001 + private static readonly System.Text.Encoding Inner = System.Text.Encoding.UTF7; +#pragma warning restore SYSLIB0001 + + // Report 65002 so MimeKit caches this codepage instead of the blocked 65000. + public override int CodePage => 65002; + public override string WebName => "utf-7"; + public override string EncodingName => "Unicode (UTF-7)"; + public override string HeaderName => "utf-7"; + public override string BodyName => "utf-7"; + public override bool IsMailNewsDisplay => true; + public override bool IsMailNewsSave => true; + + public override int GetByteCount(char[] chars, int index, int count) + => Inner.GetByteCount(chars, index, count); + public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) + => Inner.GetBytes(chars, charIndex, charCount, bytes, byteIndex); + public override int GetCharCount(byte[] bytes, int index, int count) + => Inner.GetCharCount(bytes, index, count); + public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) + => Inner.GetChars(bytes, byteIndex, byteCount, chars, charIndex); + public override int GetMaxByteCount(int charCount) => Inner.GetMaxByteCount(charCount); + public override int GetMaxCharCount(int byteCount) => Inner.GetMaxCharCount(byteCount); + public override System.Text.Decoder GetDecoder() => Inner.GetDecoder(); + public override System.Text.Encoder GetEncoder() => Inner.GetEncoder(); + } }