2015-07-14 1 views
4

Existe-t-il un moyen de convertir un MimeKit.MimeMessage en HTML pouvant être rendu dans un navigateur Web? Je ne suis pas concerné par les pièces jointes aux messages, mais j'aimerais pouvoir afficher le corps du message, avec les images intégrées, dans un navigateur. Je suis nouveau sur MimeKit et je n'ai rien trouvé dans les documents API pour cela. Toute information est appréciée.MimeKit.MimeMessage au HTML rendu par le navigateur

EDIT: Je n'ai pas trouvé un moyen de le faire nativement avec MimeKit, mais je l'ai combiné avec le HtmlAgilityPack pour analyser le MimeMessage.HtmBody et corriger les images en ligne. Cela semble fonctionner et je vais y aller à moins que quelqu'un ait une meilleure idée. Pour référence, voici le code:

////////////////////////////////////////////////////////////////////////////////////////// 
// use MimeKit to parse the message 
////////////////////////////////////////////////////////////////////////////////////////// 
MimeKit.MimeMessage msg = MimeKit.MimeMessage.Load(stream); 

////////////////////////////////////////////////////////////////////////////////////////// 
// use HtmlAgilityPack to parse the resulting html in order to fix inline images 
////////////////////////////////////////////////////////////////////////////////////////// 
HtmlAgilityPack.HtmlDocument hdoc = new HtmlAgilityPack.HtmlDocument(); 
hdoc.LoadHtml(msg.HtmlBody); 
// find all image nodes 
var images = hdoc.DocumentNode.Descendants("img"); 
foreach (var img in images) 
{       
    // check that this is an inline image 
    string cid = img.Attributes["src"].Value; 
    if (cid.StartsWith("cid:")) 
    { 
     // remove the cid part of the attribute 
     cid = cid.Remove(0, 4); 
     // find image object in MimeMessage 
     MimeKit.MimePart part = msg.BodyParts.First(x => x.ContentId == cid) as MimeKit.MimePart; 
     if (part != null) 
     { 
      using (MemoryStream mstream = new MemoryStream()) 
      { 
       // get the raw image content 
       part.ContentObject.WriteTo(mstream); 
       mstream.Flush(); 
       byte[] imgbytes = mstream.ToArray(); 
       // fix the image source by making it an embedded image 
       img.Attributes["src"].Value = "data:" + part.ContentType.MimeType + ";" + part.ContentTransferEncoding.ToString().ToLower() + "," + 
        System.Text.ASCIIEncoding.ASCII.GetString(imgbytes); 
      } 
     } 
    } 
} 

// write the resulting html to the output stream 
hdoc.Save(outputStream); 

Répondre

4

Votre solution est similaire à la logique que je l'habitude d'utiliser dans MimeKit's MessageReader sample, mais maintenant MimeKit fournit une meilleure 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); 
    } 
} 

Et puis d'utiliser cette coutume HtmlPreviewVisitor classe, vous auriez une méthode quelque chose comme ceci:

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

Je sais que cela semble beaucoup de code, mais il couvre beaucoup plus que simple cas. Vous remarquerez qu'il gère également le rendu text/plain ainsi que les corps text/plain; format=flowed si le code HTML n'est pas disponible. Il utilise également correctement les images qui font partie de l'arborescence multipart/related d'encapsulation. Une façon de modifier ce code consiste à incorporer les images dans les balises img au lieu d'utiliser un répertoire temporaire. Pour ce faire, vous souhaitez modifier la méthode SaveImage être quelque chose comme ça (attention, ce segment de code est non testé):

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

On dirait que j'étais essentiellement sur la bonne voie, mais votre solution est beaucoup plus complet que vous avez indiqué. Cela aide beaucoup. Merci. – John

+0

Pas de problème, et oui, vous étiez définitivement sur la bonne voie :-) – jstedfast

+0

Excellente solution! L'expérimental SaveImage travaille dans un environnement web avec Angular en utilisant .Net 4.5.2. – Jamison