4

我们的一位员工丢失了他的邮箱,但幸运的是他的电子邮件以 mbox 格式转储。我需要以某种方式获取 mbox 文件中的所有消息并将它们喷射到我们的技术支持数据库中(因为它是一个自定义工具,所以没有可用的导入工具)。

我发现SharpMimeTools可以分解消息,但不允许您遍历 mbox 文件中的一堆消息。

有谁知道无需学习 RFC 就可以打开的体面解析器?

4

4 回答 4

19

我正在使用 C# 开发一个名为MimeKit的 MIME 和 mbox 解析器。

它基于我编写的早期 MIME 和 mbox 解析器(例如GMime),速度非常快(可以在大约 1 秒内解析 1.2GB mbox 文件中的每条消息)。

我还没有测试 MimeKit 的性能,但是我在 C# 中使用了许多与我在 C 中使用的相同的技术。我怀疑它会比我的 C 实现慢,但是因为瓶颈是 I/O 而 MimeKit 是像 GMime 那样写来做最佳(4k)读取,它们应该非常接近。

您发现当前方法很慢(StreamReader.ReadLine(),组合文本,然后将其传递给 SharpMimeTools)的原因是由于以下原因:

  1. StreamReader.ReadLine() 不是从文件中读取数据的最佳方式。虽然我确定 StreamReader() 会进行内部缓冲,但它需要执行以下步骤:

    A) 将从文件中读取的字节块转换为 unicode(这需要遍历从磁盘读取的 byte[] 中的字节,以将从流中读取的字节转换为 unicode char[])。

    B) 然后它需要遍历其内部的 char[],将每个 char 复制到 StringBuilder 中,直到找到一个 '\n'。

    所以就在那里,只需读取行,您就可以通过 mbox 输入流至少 2 次。更不用说正在进行的所有内存分配......

  2. 然后你把你读过的所有行合并成一个大字符串。这需要对您的输入进行另一次传递(大概将从 ReadLine() 读取的每个字符串中的每个字符复制到 StringBuilder 中?)。

    我们现在对输入文本进行了多达 3 次迭代,甚至还没有进行任何解析。

  3. 现在你将你的巨型字符串交给 SharpMimeTools,它使用一个 SharpMimeMessageStream... (/facepalm) 是一个基于 ReadLine() 的解析器,它位于另一个进行字符集转换的 StreamReader 之上。在解析任何内容之前进行 5 次迭代。如果 SharpMimeMessageStream 发现它读得太远,它还有一种方法可以“撤消” ReadLine()。因此可以合理地假设他至少扫描了其中一些行两次。更不用说正在进行的所有字符串分配......呃。

  4. 对于每个标题,一旦 SharpMimeTools 拥有其行缓冲区,它就会拆分为字段和值。那是另一个关卡。到目前为止,我们最多可以通过 6 次。

  5. 然后 SharpMimeTools 使用 string.Split()(这很好地表明这个 mime 解析器不符合标准)通过拆分来标记地址头,并通过拆分参数化头(例如 Content-Type 和 Content-Disposition)在 ';'。那是另一个关卡。(我们现在最多 7 次传球。)

  6. 一旦它拆分它们,它就会对从 string.Split() 返回的每个字符串运行正则表达式匹配,然后每个 rfc2047 编码词令牌传递更多的正则表达式,然后最终对编码词字符集和有效负载组件进行另一次传递。到目前为止,我们正在谈论至少 9 或 10 次通过大部分输入。

我放弃了进一步的考试,因为它已经是 GMime 和 MimeKit 所需的 2 倍多,而且我知道我的解析器可以优化为至少比它们少 1 次通过。

此外,作为旁注,任何解析字符串而不是 byte[](或 sbyte[])的 MIME 解析器都不会很好。电子邮件的问题在于,如此多的邮件客户端/脚本/等会在标题和消息正文中发送未声明的 8 位文本。unicode 字符串解析器如何处理这个问题?提示:不能。

using (var stream = File.OpenRead ("Inbox.mbox")) {
    var parser = new MimeParser (stream, MimeFormat.Mbox);
    while (!parser.IsEndOfStream) {
        var message = parser.ParseMessage ();

        // At this point, you can do whatever you want with the message.
        // As an example, you could save it to a separate file based on
        // the message subject:
        message.WriteTo (message.Subject + ".eml");

        // You also have the ability to get access to the mbox marker:
        var marker = parser.MboxMarker;

        // You can also get the exact byte offset in the stream where the
        // mbox marker was found:
        var offset = parser.MboxMarkerOffset;
    }
}

2013-09-18 更新:我已经让 MimeKit 可以用于解析 mbox 文件并成功地解决了问题,但它的速度不如我的 C 库快。这是在 iMac 上测试的,因此 I/O 性能不如我的旧 Linux 机器(这是 GMime 能够在 ~1s 内解析类似大小的 mbox 文件的地方):

[fejj@localhost MimeKit]$ mono ./mbox-parser.exe larger.mbox 
Parsed 14896 messages in 6.16 seconds.
[fejj@localhost MimeKit]$ ./gmime-mbox-parser larger.mbox 
Parsed 14896 messages in 3.78 seconds.
[fejj@localhost MimeKit]$ ls -l larger.mbox 
-rw-r--r--  1 fejj  staff  1032555628 Sep 18 12:43 larger.mbox

正如你所看到的,GMime 仍然相当快,但我对如何提高 MimeKit 解析器的性能有一些想法。事实证明,C# 的fixed语句非常昂贵,所以我需要重新使用它们。例如,我昨天做的一个简单优化将总时间缩短了大约 2-3 秒(如果我没记错的话)。

优化更新:通过替换将性能提高了 20%:

while (*inptr != (byte) '\n')
    inptr++;

和:

do {
    mask = *dword++ ^ 0x0A0A0A0A;
    mask = ((mask - 0x01010101) & (~mask & 0x80808080));
} while (mask == 0);

inptr = (byte*) (dword - 1);
while (*inptr != (byte) '\n')
    inptr++;

优化更新:通过放弃使用 Enum.HasFlag() 并改用直接位掩码,我终于能够使 MimeKit 与 GMime 一样快。

MimeKit 现在可以在 3.78 秒内解析相同的 mbox 流。

作为比较,SharpMimeTools 需要 20 多分钟(为了测试这一点,我不得不将电子邮件分成单独的文件,因为 SharpMimeTools 无法解析 mbox 文件)。

另一个更新:通过整个代码中的各种其他调整,我已经把它降到了 3.00 秒。

于 2013-09-13T13:11:07.393 回答
2

我不知道任何解析器,但 mbox 确实是一种非常简单的格式。一封新电子邮件以“发件人”(发件人+空格)开头的行开始,每封邮件的末尾都附有一个空行。如果在电子邮件本身的行首出现任何“From”,则将其引用(通过在前面添加“>”)。

另请参阅Wikipedia 关于该主题的条目

于 2009-05-24T13:05:38.317 回答
0

如果您可以使用 Python,标准库中有一个。遗憾的是,我无法为 .NET 找到任何内容。

于 2009-05-24T13:05:40.160 回答
0

要阅读 .mbox 文件,您可以使用第三方库Aspose.Email。该库是一套完整的电子邮件处理 API,用于构建跨平台应用程序,能够在不使用 Microsoft Outlook 的情况下创建、操作、转换和传输电子邮件。

请看一下我在下面提供的示例。

using(FileStream stream = new FileStream("ExampleMbox.mbox", FileMode.Open, FileAccess.Read))
{
    using(MboxrdStorageReader reader = new MboxrdStorageReader(stream, false))
    {
        // Start reading messages
        MailMessage message = reader.ReadNextMessage();

        // Read all messages in a loop
        while (message != null)
        {
            // Manipulate message - show contents
            Console.WriteLine("Subject: " + message.Subject);
            // Save this message in EML or MSG format
            message.Save(message.Subject + ".eml", SaveOptions.DefaultEml);
            message.Save(message.Subject + ".msg", SaveOptions.DefaultMsgUnicode);

            // Get the next message
            message = reader.ReadNextMessage();
        }
    }
}

它很容易使用。我希望这种方法能让您和其他搜索者满意。

我在 Aspose 担任开发人员布道师。

于 2019-11-01T17:08:42.640 回答