Fix mojibake in signature: detect real encoding instead of assuming UTF-8
Outlook writes signature .htm files in the system ANSI code page (windows-1251 on Russian Windows) without a BOM, but File.ReadAllText defaults to UTF-8 for BOM-less files - mangling Cyrillic into replacement characters. ReadSignatureHtml now checks BOM, then a <meta charset> tag, then falls back to Encoding.Default (the actual system code page) before ever assuming UTF-8. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f04de700d9
commit
cb3b273741
2 files changed
+140
-2
No files matched your search
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Win32;
|
||||
using Outlook = Microsoft.Office.Interop.Outlook;
|
||||
|
||||
@@ -38,7 +40,7 @@ namespace BulkArchiveMailer.AddIn
|
||||
}
|
||||
|
||||
var path = ResolveSignaturePath(signatureName);
|
||||
return File.Exists(path) ? File.ReadAllText(path) : null;
|
||||
return File.Exists(path) ? ReadSignatureHtml(path) : null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -119,5 +121,141 @@ namespace BulkArchiveMailer.AddIn
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a signature .htm file with the encoding it was actually
|
||||
/// written in, instead of assuming UTF-8 the way
|
||||
/// <see cref="File.ReadAllText(string)"/> does for BOM-less files.
|
||||
///
|
||||
/// Outlook writes signature .htm files in the system ANSI code
|
||||
/// page (windows-1251 on a Russian Windows) without a BOM and
|
||||
/// usually without a reliable <meta charset> tag, so blindly
|
||||
/// reading as UTF-8 mangles Cyrillic into replacement characters.
|
||||
///
|
||||
/// Detection order:
|
||||
/// 1. BOM (UTF-8 / UTF-16 LE / UTF-16 BE / UTF-32 LE / UTF-32 BE) - trusted if present.
|
||||
/// 2. <meta charset=...> or <meta http-equiv="Content-Type" content="...charset=...">
|
||||
/// found in the first ~1KB, if the named encoding is recognized.
|
||||
/// 3. Encoding.Default (the system ANSI code page) - matches
|
||||
/// Outlook's legacy signature file format.
|
||||
///
|
||||
/// Never throws: any failure along the way falls back to the
|
||||
/// previous behavior, <see cref="File.ReadAllText(string)"/>.
|
||||
/// </summary>
|
||||
internal static string ReadSignatureHtml(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytes = File.ReadAllBytes(path);
|
||||
|
||||
var bomEncoding = DetectBomEncoding(bytes, out var preambleLength);
|
||||
if (bomEncoding != null)
|
||||
{
|
||||
return bomEncoding.GetString(bytes, preambleLength, bytes.Length - preambleLength);
|
||||
}
|
||||
|
||||
var metaEncoding = TryDetectMetaCharsetEncoding(bytes);
|
||||
if (metaEncoding != null)
|
||||
{
|
||||
return metaEncoding.GetString(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
return Encoding.Default.GetString(bytes, 0, bytes.Length);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Encoding DetectBomEncoding(byte[] bytes, out int preambleLength)
|
||||
{
|
||||
// UTF-32 BOMs must be checked before UTF-16 ones: the UTF-32 LE
|
||||
// BOM (FF FE 00 00) starts with the same two bytes as the
|
||||
// UTF-16 LE BOM (FF FE).
|
||||
if (bytes.Length >= 4 && bytes[0] == 0x00 && bytes[1] == 0x00 && bytes[2] == 0xFE && bytes[3] == 0xFF)
|
||||
{
|
||||
preambleLength = 4;
|
||||
return new UTF32Encoding(bigEndian: true, byteOrderMark: true);
|
||||
}
|
||||
|
||||
if (bytes.Length >= 4 && bytes[0] == 0xFF && bytes[1] == 0xFE && bytes[2] == 0x00 && bytes[3] == 0x00)
|
||||
{
|
||||
preambleLength = 4;
|
||||
return new UTF32Encoding(bigEndian: false, byteOrderMark: true);
|
||||
}
|
||||
|
||||
if (bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF)
|
||||
{
|
||||
preambleLength = 3;
|
||||
return new UTF8Encoding(encoderShouldEmitUTF8Identifier: true);
|
||||
}
|
||||
|
||||
if (bytes.Length >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF)
|
||||
{
|
||||
preambleLength = 2;
|
||||
return new UnicodeEncoding(bigEndian: true, byteOrderMark: true);
|
||||
}
|
||||
|
||||
if (bytes.Length >= 2 && bytes[0] == 0xFF && bytes[1] == 0xFE)
|
||||
{
|
||||
preambleLength = 2;
|
||||
return new UnicodeEncoding(bigEndian: false, byteOrderMark: true);
|
||||
}
|
||||
|
||||
preambleLength = 0;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static readonly Regex MetaCharsetRegex = new Regex(
|
||||
@"<meta\b[^>]*charset\s*=\s*[""']?\s*([A-Za-z0-9_\-]+)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static Encoding TryDetectMetaCharsetEncoding(byte[] bytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
// <meta charset=...> tags are always plain ASCII, so
|
||||
// Latin1 (single byte, 1:1 with the raw bytes) is a safe,
|
||||
// lossless way to look for them regardless of the file's
|
||||
// real encoding.
|
||||
var headLength = Math.Min(bytes.Length, 1024);
|
||||
var head = Encoding.GetEncoding("ISO-8859-1").GetString(bytes, 0, headLength);
|
||||
|
||||
var match = MetaCharsetRegex.Match(head);
|
||||
if (!match.Success)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var charsetName = match.Groups[1].Value.Trim();
|
||||
if (string.IsNullOrEmpty(charsetName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Encoding.GetEncoding(charsetName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Unrecognized charset name - let the caller fall back
|
||||
// to Encoding.Default instead.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(settings.SignatureHtmlPath))
|
||||
{
|
||||
return File.Exists(settings.SignatureHtmlPath) ? File.ReadAllText(settings.SignatureHtmlPath) : null;
|
||||
return File.Exists(settings.SignatureHtmlPath) ? SignatureReader.ReadSignatureHtml(settings.SignatureHtmlPath) : null;
|
||||
}
|
||||
|
||||
return SignatureReader.TryAutoDetectSignatureHtml(outlookApplication);
|
||||
|
||||
Reference in new issue
Block a user