在 HTML 封电子邮件中显示嵌入图像

Display embed images in HTML emails

我通过 Mailkit IMAP 将电子邮件存储为 .eml 文件,当用户想要打开邮件时,我打开该 eml 文件并显示电子邮件的内容和附件

var file = System.IO.File.ReadAllBytes("E\sampleEmail.eml"));
var msg = new MsgReader.Mime.Message(file);
ViewBag.Body = System.Text.Encoding.UTF8.GetString(msg.HtmlBody == null ? msg.TextBody.Body : msg.HtmlBody.Body);

电子邮件的 HtmlBody 包含所有内容,但如何在 html 上显示嵌入图像?

img 标签显示如下并且有 cid:

<img src="cid:image001.png@01d4330b.ed93f2e0" id="_x0000_i1025">

image001 是 eml 文件的附件,它嵌入在电子邮件正文的 html 内容中,直接在原处显示图像的最佳方式是什么?!

MailMessage mailWithImg = getMailWithImg();
MySMTPClient.Send(mailWithImg); //* Set up your SMTPClient before!

private MailMessage getMailWithImg() {
    MailMessage mail = new MailMessage();
    mail.IsBodyHtml = true;
    mail.AlternateViews.Add(getEmbeddedImage("c:/image.png"));
    mail.From = new MailAddress("yourAddress@yourDomain");
    mail.To.Add("recipient@hisDomain");
    mail.Subject = "yourSubject";
    return mail;
}
private AlternateView getEmbeddedImage(String filePath) {
    LinkedResource res = new LinkedResource(filePath);
    res.ContentId = Guid.NewGuid().ToString();
    string htmlBody = @"<img src='cid:" + res.ContentId + @"'/>";
    AlternateView alternateView = AlternateView.CreateAlternateViewFromString(htmlBody, null, MediaTypeNames.Text.Html);
    alternateView.LinkedResources.Add(res);
    return alternateView;
}

我使用了一个名为 MSGReader 的库。

Git: https://github.com/Sicos1977/MSGReader

一般规则: 这个id:"cid:image001.png@01d4330b.ed93f2e0"是一个link到文件的末尾,图片是像base64一样存储的。我在 MSGReader 中做了一些更改,我会尽快提出拉取请求,对我来说效果很好。

如果您使用 MailKit 保存从 IMAP 下载的邮件,则可以像这样重新加载邮件:

var message = MimeMessage.Load (fileName);

然后,要获取消息正文,您可以这样做:

string body = message.HtmlBody ?? message.TextBody;

现在,要回答有关如何使用 MimeKit/MailKit 呈现图像的问题,您可以在此处查看示例消息查看器:https://github.com/jstedfast/MimeKit/tree/master/samples/MessageReader/MessageReader

重要的一点是 HtmlPreviewVisitor:

using System;
using System.IO;
using System.Collections.Generic;

using MimeKit;
using MimeKit.Text;
using MimeKit.Tnef;

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

        /// <summary>
        /// Creates a new HtmlPreviewVisitor.
        /// </summary>
        public HtmlPreviewVisitor ()
        {
        }

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

        /// <summary>
        /// Gets the attachent content as a data URI.
        /// </summary>
        /// <returns>The data URI.</returns>
        /// <param name="attachment">The attachment.</param>
        string GetDataUri (MimePart attachment)
        {
            using (var memory = new MemoryStream ()) {
                attachment.Content.DecodeTo (memory);
                var buffer = memory.GetBuffer ();
                var length = (int) memory.Length;
                var base64 = Convert.ToBase64String (buffer, 0, length);

                return string.Format ("data:{0};base64,{1}", attachment.ContentType.MimeType, base64);
            }
        }

        // 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 = GetDataUri (image);

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

将上述代码与 System.Windows.Forms.WebBrowser 一起使用的方法是这样的:

void Render ()
{
    var visitor = new HtmlPreviewVisitor ();
    message.Accept (visitor);

    webBrowser.DocumentText = visitor.HtmlBody;
}

HtmlPreviewVisitor 所做的是使用 <img> 标签中的 "data:" URL 方案将图像附件嵌入到 HTML 中。