【问题标题】:How to obtain the WYSIWYG body of an email message using MimeKit如何使用 MimeKit 获取电子邮件的 WYSIWYG 正文
【发布时间】:2016-01-24 19:09:39
【问题描述】:

我正在使用一个名为 EAgetmail 的库来检索指定电子邮件的正文,它运行良好,但是我现在正在使用 Mailkit。问题在于 EAgetmail 相当于 message.body 会返回用户在电子邮件客户端中看到的正文,但在 mailkit 中它会返回许多不同的数据。

这是相关代码:

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);
}

我还尝试了 message.Body.ToString() 并在消息部分中搜索纯文本,但都没有成功。 我的问题是如何使用 Mailkit 复制 EAgetmail 的 .body 属性的效果(仅以纯文本形式返回正文内容,如用户所见)?

【问题讨论】:

    标签: c# email mailkit mimekit


    【解决方案1】:

    关于电子邮件的一个常见误解是有一个明确定义的邮件正文,然后是一个附件列表。事实并非如此。事实上,MIME 是一种内容树结构,很像文件系统。

    幸运的是,MIME 确实为邮件客户端应如何解释这种 MIME 部分的树形结构定义了一组通用规则。 Content-Disposition 标头旨在向接收客户端提供有关哪些部分应作为邮件正文的一部分显示以及哪些应被解释为附件的提示。

    Content-Disposition 标头通常具有以下两个值之一:inlineattachment

    这些值的含义应该是相当明显的。如果值为attachment,则所述MIME 部分的内容将作为与核心消息分开的文件附件呈现。但是,如果值为inline,则该MIME 部分的内容将在邮件客户端呈现的核心消息正文中内联显示。如果Content-Disposition 标头不存在,则应将其视为值为inline

    从技术上讲,缺少Content-Disposition 标头或标记为inline 的每个部分都是核心消息正文的一部分。

    不过,还有更多的东西。

    现代 MIME 邮件通常包含一个multipart/alternative MIME 容器,该容器通常包含发件人所写文本的text/plaintext/html 版本。 text/html 版本的格式通常比text/plain 版本更接近发件人在其所见即所得编辑器中看到的内容。

    以两种格式发送消息文本的原因是并非所有邮件客户端都能够显示 HTML。

    接收客户端应该只显示包含在multipart/alternative 容器中的替代视图之一。由于替代视图是按照发送者在其所见即所得编辑器中看到的内容按照最不忠实到最忠实的顺序列出的,因此接收客户端应从末尾开始遍历替代视图列表并向后工作,直到找到它的部分可以显示。

    例子:

    multipart/alternative
      text/plain
      text/html
    

    如上例所示,text/html 部分列在最后,因为它最忠实于发件人在编写邮件时在其所见即所得编辑器中看到的内容。

    为了让事情变得更加复杂,有时现代邮件客户端会使用multipart/related MIME 容器而不是简单的text/html 部分,以便在HTML 中嵌入图像和其他多媒体内容。

    例子:

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

    在上面的示例中,备选视图之一是 multipart/related 容器,其中包含引用同级视频和图像的消息正文的 HTML 版本。

    现在您已经大致了解了消息的结构以及如何解释各种 MIME 实体,我们可以开始弄清楚如何按预期实际呈现消息。

    使用 MimeVisitor(呈现消息的最准确方式)

    MimeKit 包含一个MimeVisitor 类,用于访问 MIME 树结构中的每个节点。例如,以下MimeVisitor 子类可用于生成要由浏览器控件呈现的 HTML(例如 WebBrowser):

    /// <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);
        }
    }
    

    您使用此访问者的方式可能如下所示:

    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);
    }
    

    使用TextBodyHtmlBody 属性(最简单的方法)

    为了简化获取消息文本的常见任务,MimeMessage 包含两个属性,可帮助您获取 text/plaintext/html 版本的消息正文。它们分别是TextBodyHtmlBody

    但是请记住,至少对于 HtmlBody 属性,HTML 部分可能是 multipart/related 的子元素,允许它引用图像和其他类型的媒体也包含在那个multipart/related 实体中。该属性实际上只是一个方便属性,并不能很好地替代您自己遍历 MIME 结构以便正确解释相关内容。

    【讨论】:

    • 感谢您的回答。如果我不关心格式并且只想将原始文本保存为字符串,考虑到我正在使用的某些消息不包含文本/纯文本部分,我将如何做到这一点。我是否只需要从 text/html 中删除标签或以某种方式对其进行转换?
    • 我会首先检查MimeMessage 上的TextBody 属性,但如果消息不包含text/plain 部分,它将为空。鉴于此,是的,下一步将是去除 HTML 标签。 MimeKit 有一个 HtmlTokenizer,您可能会发现它对此很有用。
    • 谢谢,我会尝试使用 HtmlTokenizer 让它工作。
    • 这可能有助于作为一个起点:gist.github.com/jstedfast/5c2189db1397aa776b3e
    • 非常感谢,这对于删除 HTML 标记非常有效,尽管有什么简单的方法可以扩展此代码以删除 CSS 吗?我还剩下link 这样的代码。如果没有,你已经帮了我很多了,我可能会想办法的。
    【解决方案2】:

    旧帖,但相关,可以使用内置的 MimeKit 获取正文作为文本:

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

    【讨论】:

      猜你喜欢
      • 2017-12-15
      • 2020-07-04
      • 2015-02-12
      • 2014-10-03
      • 2013-04-30
      • 2017-06-27
      • 1970-01-01
      • 2022-08-24
      • 2021-03-15
      相关资源
      最近更新 更多