diff --git a/outlook-bulk-sender/BulkArchiveMailer.AddIn/SignatureReader.cs b/outlook-bulk-sender/BulkArchiveMailer.AddIn/SignatureReader.cs
index 1a5915f..6aa6adc 100644
--- a/outlook-bulk-sender/BulkArchiveMailer.AddIn/SignatureReader.cs
+++ b/outlook-bulk-sender/BulkArchiveMailer.AddIn/SignatureReader.cs
@@ -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;
}
+
+ ///
+ /// Reads a signature .htm file with the encoding it was actually
+ /// written in, instead of assuming UTF-8 the way
+ /// 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, .
+ ///
+ 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(
+ @"]*charset\s*=\s*[""']?\s*([A-Za-z0-9_\-]+)",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
+ private static Encoding TryDetectMetaCharsetEncoding(byte[] bytes)
+ {
+ try
+ {
+ // 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;
+ }
+ }
}
}
diff --git a/outlook-bulk-sender/BulkArchiveMailer.AddIn/UI/PreviewForm.cs b/outlook-bulk-sender/BulkArchiveMailer.AddIn/UI/PreviewForm.cs
index a53af63..b115008 100644
--- a/outlook-bulk-sender/BulkArchiveMailer.AddIn/UI/PreviewForm.cs
+++ b/outlook-bulk-sender/BulkArchiveMailer.AddIn/UI/PreviewForm.cs
@@ -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);