使用 mailkit 在 .net Framework 4.6.1 中支持多种内容编码

Supporting multiple content encoding in .net Framework 4.6.1 using mailkit

我正在使用 .net framework 4.6.1 构建一个电子邮件客户端,它将从邮箱中提取电子邮件并显示在电子邮件客户端上。目前我正在使用 s22.Imap 的 ImapClient.GetMessage() 方法来检索电子邮件内容。它适用于附件和具有大多数默认编码的 bodyContent。

但是我的一些邮件是 CodePage = 932EncodingName = "Japanese (Shift-JIS)" 类型的。我无法获取这些电子邮件,因为它们为大多数 BodyEncoding 属性/属性抛出 System.NotSupportedException

在 github 问题中搜索 s22.Imap 时,有一个 issue 建议使用 mailkit 而不是 s22.Imap。我想知道更多关于这个编码部分在 mailkit 中是如何处理的。还想知道是否有默认方法来处理未知代码页类型的编码。

您可以阅读此博客 post,它解释了大多数 C# MIME 解析器的错误以及为什么 MimeKit 可以处理多个字符集编码。

https://jeffreystedfast.blogspot.com/2013/09/time-for-rant-on-mime-parsers.html https://jeffreystedfast.blogspot.com/2013/08/why-decoding-rfc2047-encoded-headers-is.html

是时候对 mime 解析器大喊大叫了...

警告:建议观众自行斟酌。

我应该从哪里开始?

我想我应该首先说我痴迷于 MIME,尤其是 MIME 解析器。不完全是。我很着迷。不相信我?我已经写了 and/or 在这一点上工作的几个 MIME 解析器。它开始于我大学时代在 Spruce 上工作,它有一个非常糟糕的 MIME 解析器,所以当你进一步阅读我对糟糕的 MIME 解析器的咆哮时,请记住:我去过那里,我写了一个糟糕的 MIME解析器。

正如少数人所知,我最近开始实施一个名为 MimeKit. As I work on this, I've been searching around on GitHub and Google to see what other MIME parsers exist out there to find out what sort of APIs they provide. I thought perhaps I'll find one that offers a well-designed API that will inspire me. Perhaps, by some miracle, I'd find one that was actually pretty good that I could just contribute to instead of writing my own from scratch (yea, wishful thinking). Instead, all I have found are poorly designed and implemented MIME parsers, many probably belong on the front page of the Daily WTF.

的 C# MIME 解析器

我想我会从一些垒球开始。

首先,事实是它们中的每一个都是作为 System.String 解析器编写的。不要被那些自称是“流解析器”的人所愚弄,因为所有这些人所做的只是在字节流之上添加一个 TextReader 并开始使用 reader.ReadLine()。你问这有什么不好?对于那些不熟悉 MIME 的人,我希望您查看收件箱中的原始电子邮件来源,尤其是当您与美国以外的任何人有通信往来时。希望您的大多数朋友和同事都在使用 more-or-less 兼容 MIME 的电子邮件客户端,但我保证您至少会发现一些带有原始 8 位文本的电子邮件。

现在,如果他们使用的语言是 C 或 C++,他们可能会这样做,因为从技术上讲,他们会在字节数组上运行,但使用 Java 和 C#, 'string' 是一个 unicode 字符串。告诉我:如何从原始字节数组中获取 unicode 字符串?

宾果游戏。在将这些字节转换为 unicode 字符之前,您需要知道字符集。

公平地说,确实没有处理消息 headers 中原始 8 位文本的好方法,但是通过使用 TextReader 方法,您确实限制了可能性。

接下来是 ReadLine() 方法。 GMime (pan-mime-parser.c 版本 0.7 天后的 2 个早期解析器之一)使用了 ReadLine() 方法,所以我理解这背后的想法。实际上,就正确性而言,这种方法并没有错,它更像是一种“这永远不可能很快”的抱怨。在 GMime 的两个早期解析器中,pan-mime-parser.c 后端与 in-memory 解析器相比慢得可怕。当然,这并不奇怪。当时更令我惊讶的是,当我编写 GMime 当前一代的解析器(介于 v0.7 和 v1.0 之间的某个时间)时,它的速度与 in-memory 解析器一样快在任何给定时间在读取缓冲区中达到 4k。我的观点是,如果您希望您的解析器具有合理的性能,那么有比 ReadLine() 更好的方法……您为什么不想要那样呢?您的用户肯定想要这样。

好的,现在是我在我发现的几乎所有 MIME 解析器库中遇到的更严重的问题。

我认为到目前为止我发现的每个 mime 解析器都使用“String.Split()”方法来解析地址 headers and/or 来解析 [= 上的参数列表101=] 例如 Content-Type 和 Content-Disposition.

这是来自一个 C# MIME 解析器的示例:

string[] emails = addressHeader.Split(',');

下面是同一个解析器如何解码 encoded-word 个标记:

private static void DecodeHeaders(NameValueCollection headers)
{
    ArrayList tmpKeys = new ArrayList(headers.Keys);

    foreach (string key in headers.AllKeys)
    {
        //strip qp encoding information from the header if present
        headers[key] = Regex.Replace(headers[key].ToString(), @"=\?.*?\?Q\?(.*?)\?=",
            new MatchEvaluator(MyMatchEvaluator), RegexOptions.IgnoreCase | RegexOptions.Multiline);
        headers[key] = Regex.Replace(headers[key].ToString(), @"=\?.*?\?B\?(.*?)\?=",
            new MatchEvaluator(MyMatchEvaluatorBase64), RegexOptions.IgnoreCase | RegexOptions.Multiline);
    }
}

private static string MyMatchEvaluator(Match m)
{
    return DecodeQP(m.Groups[1].Value);
}

private static string MyMatchEvaluatorBase64(Match m)
{
    System.Text.Encoding enc = System.Text.Encoding.UTF7;
    return enc.GetString(Convert.FromBase64String(m.Groups[1].Value));
}

什么?!它完全丢弃了每个 encoded-word 标记中的字符集。在 quoted-printable 标记的情况下,它假设它们都是 ASCII(实际上,latin1 也可以工作?)而在 base64 encoded-word 标记的情况下,它假设它们都是 UTF-7! ?!?他到底从哪里得到这个想法的?我无法想象他的代码可以在现实世界中的任何 base64 encoded-word 令牌上运行。 ‍♂️

我想指出的是这个项目的描述是这样的:

一个用 c# 编写的小型、高效、有效的 mime 解析器库。 ... 我以前用过几个 open-source mime 解析器,但它们要么 一种或另一种编码失败,或错过一些关键的 信息。这就是为什么我决定最终解决这个问题 我。 我承认他的 MIME 解析器很小,但我不得不对“高效”和“工作”形容词提出异议。由于大量使用字符串分配和正则表达式匹配,它很难被认为是“有效的”。正如上面指出的代码所示,“工作”有点言过其实。

伙计们...这就是您选择“轻量级”MIME 解析器时得到的结果,因为您认为像 GMime 这样的解析器“臃肿”。

关于解析器 #2...我喜欢称其为“Humpty Dumpty”方法:

public static StringDictionary parseHeaderFieldBody ( String field, String fieldbody ) {
    if ( fieldbody==null )
        return null;
    // FIXME: rewrite parseHeaderFieldBody to being regexp based.
    fieldbody = SharpMimeTools.uncommentString (fieldbody);
    StringDictionary fieldbodycol = new StringDictionary ();
    String[] words = fieldbody.Split(new Char[]{';'});
    if ( words.Length>0 ) {
        fieldbodycol.Add (field.ToLower(), words[0].ToLower().Trim());
        for (int i=1; i<words.Length; i++ ) {
            String[] param = words[i].Trim(new Char[]{' ', '\t'}).Split(new Char[]{'='}, 2);
            if ( param.Length==2 ) {
                param[0] = param[0].Trim(new Char[]{' ', '\t'});
                param[1] = param[1].Trim(new Char[]{' ', '\t'});
                if ( param[1].StartsWith("\"") && !param[1].EndsWith("\"")) {
                    do {
                        param[1] += ";" + words[++i];
                    } while ( !words[i].EndsWith("\"") && i<words.Length);
                }
                fieldbodycol.Add ( param[0], SharpMimeTools.parserfc2047Header (param[1].TrimEnd(';').Trim('\"', ' ')) );
            }
        }
    }
    return fieldbodycol;
}

我会给这个人一些荣誉,至少他看到他的 String.Split() 方法有缺陷,所以试图补偿 b将 Humpty Dumpty 重新拼接在一起。当然,凭借他的 String.Trim()ing,他无法确定地将他重新组合起来。这些引用标记中的白色 space 可能具有重要意义。

许多 C# MIME 解析器都喜欢到处使用 Regex。这是一个完全用正则表达式编写的解析器的片段(是的,维护它很有趣......):

if (m_EncodedWordPattern.RegularExpression.IsMatch(field.Body))
{
    string charset = m_CharsetPattern.RegularExpression.Match(field.Body).Value;
    string text = m_EncodedTextPattern.RegularExpression.Match(field.Body).Value;
    string encoding = m_EncodingPattern.RegularExpression.Match(field.Body).Value;

    Encoding enc = Encoding.GetEncoding(charset);

    byte[] bar;

    if (encoding.ToLower().Equals("q"))
    {
        bar = m_QPDecoder.Decode(ref text);
    }
    else
    {
        bar = m_B64decoder.Decode(ref text);
    }                    
    text = enc.GetString(bar);

    field.Body = Regex.Replace(field.Body,
        m_EncodedWordPattern.TextPattern, text);
    field.Body = field.Body.Replace('_', ' ');
}

让我们假装正则表达式模式字符串的定义是正确的(因为它们 god-awful 可读,我懒得 double-check 它们),替换 '_'使用 space 是错误的(它应该只在 "q" 情况下完成)并且 Regex.Replace() 只是邪恶的。更不用说每个 field.Body 可能有多个编码字,此代码完全无法处理。

伙计们。我知道您喜欢正则表达式并且它们非常有用,但它们不能替代编写真正的分词器。如果您想对接受的内容宽容(对于 MIME,you really need to be),则尤其如此。