MimeKit.MimeMessage to Browser-Renderable HTML

后端 未结 1 1605
温柔的废话
温柔的废话 2021-02-15 11:51

Is there a way to convert a MimeKit.MimeMessage to HTML that can be rendered in a web browser? I\'m not concerned with message attachments, but would like to be able to display

相关标签:
1条回答
  • 2021-02-15 12:45

    Your solution is similar to the logic I used to use in MimeKit's MessageReader sample, but now MimeKit provides a better solution:

    /// <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 ();
            }
    
            string text = entity.Text;
    
            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 then to use this custom HtmlPreviewVisitor class, you'd have a method something like this:

    void Render (WebBrowser browser, MimeMessage message)
    {
        var tmpDir = Path.Combine (Path.GetTempPath (), message.MessageId);
        var visitor = new HtmlPreviewVisitor (tmpDir);
    
        Directory.CreateDirectory (tmpDir);
    
        message.Accept (visitor);
    
        browser.DocumentText = visitor.HtmlBody;
    }
    

    I know that this seems like a lot of code, but it's covering a lot more than just the simple cases. You'll notice that it also handles rendering text/plain as well as text/plain; format=flowed bodies if the HTML is not available. It also correctly only uses images that are part of the encapsulating multipart/related tree.

    One way you could modify this code is to embed the images into the img tags instead of using a temp directory. To do that, you'd modify the SaveImage method to be something like this (be warned, this next segment of code is untested):

    string SaveImage (MimePart image, string url)
    {
        using (var output = new MemoryStream ()) {
            image.ContentObject.DecodeTo (output);
    
            var buffer = output.GetBuffer ();
            int length = (int) output.Length;
    
            return string.Format ("data:{0};base64,{1}", image.ContentType.MimeType, Convert.ToBase64String (buffer, 0, length));
        }
    }
    
    0 讨论(0)
提交回复
热议问题