简体   繁体   English

如何使用 MimeKit 获取电子邮件消息的 WYSIWYG 正文

[英]How to obtain the WYSIWYG body of an email message using MimeKit

I was using a library called EAgetmail to retrieve the body of a specified email and it was working well, however I am now using Mailkit.我正在使用一个名为 EAgetmail 的库来检索指定电子邮件的正文并且它运行良好,但是我现在正在使用 Mailkit。 The problem is with EAgetmail the equivalent of message.body returns the body as the user sees it in email clients, but in mailkit it returns a lot of different data.问题在于 EAgetmail 相当于 message.body 返回用户在电子邮件客户端中看到的正文,但在 mailkit 中它返回很多不同的数据。

This is the relevant code:这是相关代码:

using (var client = new ImapClient())
{
    client.Connect(emailServer, 993, true);
    client.AuthenticationMechanisms.Remove("XOAUTH2");
    client.Authenticate(username, password);
    var inbox = client.Inbox;
    inbox.Open(FolderAccess.ReadOnly);
    SearchQuery query;
    if (checkBox.IsChecked == false)
    {
        query = SearchQuery.DeliveredBefore((DateTime)dateEnd).And(
            SearchQuery.DeliveredAfter((DateTime)dateStart)).And(
            SearchQuery.SubjectContains("Subject to find"));
    }
    else
    {
        query = SearchQuery.SubjectContains("Subject to find");
    }
    foreach (var uid in inbox.Search(query))
    {
        var message = inbox.GetMessage(uid);
        formEmails.Add(message.TextBody);
        messageDate.Add(message.Date.LocalDateTime);
    }
    client.Disconnect(true);
}

I also tried message.Body.ToString() and searching through the message parts for plain text, but neither worked.我还尝试了 message.Body.ToString() 并在消息部分中搜索纯文本,但都没有用。 My question is how do I replicate the effect of EAgetmail's.body property using Mailkit (to return only the body contents in plain text, as the user sees)?我的问题是如何使用 Mailkit 复制 EAgetmail 的.body 属性的效果(以纯文本形式仅返回正文内容,如用户所见)?

A common misunderstanding about email is that there is a well-defined message body and then a list of attachments.关于电子邮件的一个常见误解是有一个定义明确的邮件正文,然后是一个附件列表。 This is not really the case.事实并非如此。 The reality is that MIME is a tree structure of content, much like a file system.事实上,MIME 是内容的树形结构,很像文件系统。

Luckily, MIME does define a set of general rules for how mail clients should interpret this tree structure of MIME parts.幸运的是,MIME 确实为邮件客户端应如何解释 MIME 部分的树结构定义了一组通用规则。 The Content-Disposition header is meant to provide hints to the receiving client as to which parts are meant to be displayed as part of the message body and which are meant to be interpreted as attachments. Content-Disposition标头旨在向接收客户端提供提示,说明哪些部分应显示为消息正文的一部分,哪些部分应解释为附件。

The Content-Disposition header will generally have one of two values: inline or attachment . Content-Disposition标头通常具有以下两个值之一: inlineattachment

The meaning of these values should be fairly obvious.这些值的含义应该是相当明显的。 If the value is attachment , then the content of said MIME part is meant to be presented as a file attachment separate from the core message.如果值为attachment ,则所述 MIME 部分的内容将作为与核心消息分开的文件附件呈现。 However, if the value is inline , then the content of that MIME part is meant to be displayed inline within the mail client's rendering of the core message body.但是,如果该值为inline ,则该 MIME 部分的内容将在邮件客户端呈现的核心邮件正文中内联显示。 If the Content-Disposition header does not exist, then it should be treated as if the value were inline .如果Content-Disposition标头不存在,则应将其视为值inline

Technically, every part that lacks a Content-Disposition header or that is marked as inline , then, is part of the core message body.从技术上讲,每个缺少Content-Disposition标头或标记为inline的部分都是核心消息正文的一部分。

There's a bit more to it than that, though.不过,还有更多的东西。

Modern MIME messages will often contain a multipart/alternative MIME container which will generally contain a text/plain and text/html version of the text that the sender wrote.现代 MIME 消息通常包含一个multipart/alternative MIME 容器,该容器通常包含发件人所写文本的text/plain文本和text/html版本。 The text/html version is typically formatted much closer to what the sender saw in his or her WYSIWYG editor than the text/plain version. text/html版本的格式通常比text/plain版本更接近发件人在他或她的所见即所得编辑器中看到的内容。

The reason for sending the message text in both formats is that not all mail clients are capable of displaying HTML.以两种格式发送消息文本的原因是并非所有邮件客户端都能够显示 HTML。

The receiving client should only display one of the alternative views contained within the multipart/alternative container.接收客户端应该只显示包含在multipart/alternative容器中的替代视图之一。 Since alternative views are listed in order of least faithful to most faithful with what the sender saw in his or her WYSIWYG editor, the receiving client should walk over the list of alternative views starting at the end and working backwards until it finds a part that it is capable of displaying.由于备选视图按照发送者在他或她的所见即所得编辑器中看到的内容从最不忠实到最忠实的顺序列出,因此接收客户端应该从末尾开始遍历备选视图列表并向后工作,直到找到它认为的部分能够显示。

Example:例子:

multipart/alternative
  text/plain
  text/html

As seen in the example above, the text/html part is listed last because it is the most faithful to what the sender saw in his or her WYSIWYG editor when writing the message.如上例所示, text/html部分列在最后,因为它最忠实于发件人在编写消息时在他或她的所见即所得编辑器中看到的内容。

To make matters even more complicated, sometimes modern mail clients will use a multipart/related MIME container instead of a simple text/html part in order to embed images and other multimedia content within the HTML.使事情变得更加复杂的是,有时现代邮件客户端会使用multipart/related的 MIME 容器而不是简单的text/html部分,以便在 HTML 中嵌入图像和其他多媒体内容。

Example:例子:

multipart/alternative
  text/plain
  multipart/related
    text/html
    image/jpeg
    video/mp4
    image/png

In the example above, one of the alternative views is a multipart/related container which contains an HTML version of the message body that references the sibling video and images.在上面的示例中,其中一个替代视图是一个multipart/related容器,其中包含引用同级视频和图像的消息正文的 HTML 版本。

Now that you have a rough idea of how a message is structured and how to interpret various MIME entities, we can start figuring out how to actually render the message as intended.既然您已经大致了解消息的结构以及如何解释各种 MIME 实体,我们就可以开始弄清楚如何按预期实际呈现消息。

Using a MimeVisitor (the most accurate way of rendering a message)使用 MimeVisitor(呈现消息的最准确方法)

MimeKit includes a MimeVisitor class for visiting each node in the MIME tree structure. MimeKit 包含一个MimeVisitor类,用于访问 MIME 树结构中的每个节点。 For example, the following MimeVisitor subclass could be used to generate HTML to be rendered by a browser control (such as WebBrowser ):例如,以下MimeVisitor子类可用于生成由浏览器控件(例如WebBrowser )呈现的 HTML:

/// <summary>
/// Visits a MimeMessage and generates HTML suitable to be rendered by a browser control.
/// </summary>
class HtmlPreviewVisitor : MimeVisitor
{
    List<MultipartRelated> stack = new List<MultipartRelated> ();
    List<MimeEntity> attachments = new List<MimeEntity> ();
    readonly string tempDir;
    string body;

    /// <summary>
    /// Creates a new HtmlPreviewVisitor.
    /// </summary>
    /// <param name="tempDirectory">A temporary directory used for storing image files.</param>
    public HtmlPreviewVisitor (string tempDirectory)
    {
        tempDir = tempDirectory;
    }

    /// <summary>
    /// The list of attachments that were in the MimeMessage.
    /// </summary>
    public IList<MimeEntity> Attachments {
        get { return attachments; }
    }

    /// <summary>
    /// The HTML string that can be set on the BrowserControl.
    /// </summary>
    public string HtmlBody {
        get { return body ?? string.Empty; }
    }

    protected override void VisitMultipartAlternative (MultipartAlternative alternative)
    {
        // walk the multipart/alternative children backwards from greatest level of faithfulness to the least faithful
        for (int i = alternative.Count - 1; i >= 0 && body == null; i--)
            alternative[i].Accept (this);
    }

    protected override void VisitMultipartRelated (MultipartRelated related)
    {
        var root = related.Root;

        // push this multipart/related onto our stack
        stack.Add (related);

        // visit the root document
        root.Accept (this);

        // pop this multipart/related off our stack
        stack.RemoveAt (stack.Count - 1);
    }

    // look up the image based on the img src url within our multipart/related stack
    bool TryGetImage (string url, out MimePart image)
    {
        UriKind kind;
        int index;
        Uri uri;

        if (Uri.IsWellFormedUriString (url, UriKind.Absolute))
            kind = UriKind.Absolute;
        else if (Uri.IsWellFormedUriString (url, UriKind.Relative))
            kind = UriKind.Relative;
        else
            kind = UriKind.RelativeOrAbsolute;

        try {
            uri = new Uri (url, kind);
        } catch {
            image = null;
            return false;
        }

        for (int i = stack.Count - 1; i >= 0; i--) {
            if ((index = stack[i].IndexOf (uri)) == -1)
                continue;

            image = stack[i][index] as MimePart;
            return image != null;
        }

        image = null;

        return false;
    }

    // Save the image to our temp directory and return a "file://" url suitable for
    // the browser control to load.
    // Note: if you'd rather embed the image data into the HTML, you can construct a
    // "data:" url instead.
    string SaveImage (MimePart image, string url)
    {
        string fileName = url.Replace (':', '_').Replace ('\\', '_').Replace ('/', '_');

        string path = Path.Combine (tempDir, fileName);

        if (!File.Exists (path)) {
            using (var output = File.Create (path))
                image.ContentObject.DecodeTo (output);
        }

        return "file://" + path.Replace ('\\', '/');
    }

    // Replaces <img src=...> urls that refer to images embedded within the message with
    // "file://" urls that the browser control will actually be able to load.
    void HtmlTagCallback (HtmlTagContext ctx, HtmlWriter htmlWriter)
    {
        if (ctx.TagId == HtmlTagId.Image && !ctx.IsEndTag && stack.Count > 0) {
            ctx.WriteTag (htmlWriter, false);

            // replace the src attribute with a file:// URL
            foreach (var attribute in ctx.Attributes) {
                if (attribute.Id == HtmlAttributeId.Src) {
                    MimePart image;
                    string url;

                    if (!TryGetImage (attribute.Value, out image)) {
                        htmlWriter.WriteAttribute (attribute);
                        continue;
                    }

                    url = SaveImage (image, attribute.Value);

                    htmlWriter.WriteAttributeName (attribute.Name);
                    htmlWriter.WriteAttributeValue (url);
                } else {
                    htmlWriter.WriteAttribute (attribute);
                }
            }
        } else if (ctx.TagId == HtmlTagId.Body && !ctx.IsEndTag) {
            ctx.WriteTag (htmlWriter, false);

            // add and/or replace oncontextmenu="return false;"
            foreach (var attribute in ctx.Attributes) {
                if (attribute.Name.ToLowerInvariant () == "oncontextmenu")
                    continue;

                htmlWriter.WriteAttribute (attribute);
            }

            htmlWriter.WriteAttribute ("oncontextmenu", "return false;");
        } else {
            // pass the tag through to the output
            ctx.WriteTag (htmlWriter, true);
        }
    }

    protected override void VisitTextPart (TextPart entity)
    {
        TextConverter converter;

        if (body != null) {
            // since we've already found the body, treat this as an attachment
            attachments.Add (entity);
            return;
        }

        if (entity.IsHtml) {
            converter = new HtmlToHtml {
                HtmlTagCallback = HtmlTagCallback
            };
        } else if (entity.IsFlowed) {
            var flowed = new FlowedToHtml ();
            string delsp;

            if (entity.ContentType.Parameters.TryGetValue ("delsp", out delsp))
                flowed.DeleteSpace = delsp.ToLowerInvariant () == "yes";

            converter = flowed;
        } else {
            converter = new TextToHtml ();
        }

        body = converter.Convert (entity.Text);
    }

    protected override void VisitTnefPart (TnefPart entity)
    {
        // extract any attachments in the MS-TNEF part
        attachments.AddRange (entity.ExtractAttachments ());
    }

    protected override void VisitMessagePart (MessagePart entity)
    {
        // treat message/rfc822 parts as attachments
        attachments.Add (entity);
    }

    protected override void VisitMimePart (MimePart entity)
    {
        // realistically, if we've gotten this far, then we can treat this as an attachment
        // even if the IsAttachment property is false.
        attachments.Add (entity);
    }
}

And the way you'd use this visitor might look something like this:您使用此访问者的方式可能如下所示:

void Render (MimeMessage message)
{
    var tmpDir = Path.Combine (Path.GetTempPath (), message.MessageId);
    var visitor = new HtmlPreviewVisitor (tmpDir);

    Directory.CreateDirectory (tmpDir);

    message.Accept (visitor);

    DisplayHtml (visitor.HtmlBody);
    DisplayAttachments (visitor.Attachments);
}

Using the TextBody and HtmlBody Properties (the easiest way)使用TextBodyHtmlBody属性(最简单的方法)

To simplify the common task of getting the text of a message, MimeMessage includes two properties that can help you get the text/plain or text/html version of the message body.为了简化获取消息文本的常见任务, MimeMessage包含两个属性,可帮助您获取消息正文的text/plain文本或text/html版本。 These are TextBody and HtmlBody , respectively.它们分别是TextBodyHtmlBody

Keep in mind, however, that at least with the HtmlBody property, it may be that the HTML part is a child of a multipart/related , allowing it to refer to images and other types of media that are also contained within that multipart/related entity.但是请记住,至少对于HtmlBody属性,HTML 部分可能是multipart/related的子项,允许它引用图像和其他类型的媒体,这些媒体也包含在该multipart/related中实体。 This property is really only a convenience property and is not a really good substitute for traversing the MIME structure yourself so that you may properly interpret related content.该属性实际上只是一个方便的属性,并不能很好地替代您自己遍历 MIME 结构以便您可以正确解释相关内容。

Old post, but relevant, can use inbuilt MimeKit to get body as text:旧帖子,但相关,可以使用内置的 MimeKit 将正文作为文本获取:

string body = mimeMessage.GetTextBody(MimeKit.Text.TextFormat.Plain);

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM