c# - How to obtain the WYSIWYG body of an email message using MimeKit -


i using library called eagetmail retrieve body of specified email , working well, using mailkit. problem eagetmail equivalent of message.body returns body user sees in email clients, in mailkit returns lot of different data.

this 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 find"));     }     else     {         query = searchquery.subjectcontains("subject 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 tried message.body.tostring() , searching through message parts plain text, neither worked. question how replicate effect of eagetmail's .body property using mailkit (to return body contents in plain text, user sees)?

a common misunderstanding email there well-defined message body , list of attachments. not case. reality mime tree structure of content, file system.

luckily, mime define set of general rules how mail clients should interpret tree structure of mime parts. content-disposition header meant provide hints receiving client parts meant displayed part of message body , meant interpreted attachments.

the content-disposition header have 1 of 2 values: inline or attachment.

the meaning of these values should obvious. if value attachment, content of said mime part meant presented file attachment separate core message. however, if value inline, content of mime part meant displayed inline within mail client's rendering of core message body. if content-disposition header not exist, should treated if value inline.

technically, every part lacks content-disposition header or marked inline, then, part of core message body.

there's bit more that, though.

modern mime messages contain multipart/alternative mime container contain text/plain , text/html version of text sender wrote. text/html version typically formatted closer sender saw in or wysiwyg editor text/plain version.

the reason sending message text in both formats not mail clients capable of displaying html.

the receiving client should display 1 of alternative views contained within multipart/alternative container. since alternative views listed in order of least faithful faithful sender saw in or wysiwyg editor, receiving client should walk on list of alternative views starting @ end , working backwards until finds part capable of displaying.

example:

multipart/alternative   text/plain   text/html 

as seen in example above, text/html part listed last because faithful sender saw in or wysiwyg editor when writing message.

to make matters more complicated, modern mail clients use multipart/related mime container instead of simple text/html part in order embed images , other multimedia content within html.

example:

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

in example above, 1 of alternative views multipart/related container contains html version of message body references sibling video , images.

now have rough idea of how message structured , how interpret various mime entities, can start figuring out how render message intended.

using mimevisitor (the accurate way of rendering message)

mimekit includes mimevisitor class visiting each node in mime tree structure. example, following mimevisitor subclass used generate html rendered browser control (such webbrowser):

/// <summary> /// visits mimemessage , generates html suitable rendered 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 new htmlpreviewvisitor.     /// </summary>     /// <param name="tempdirectory">a temporary directory used storing image files.</param>     public htmlpreviewvisitor (string tempdirectory)     {         tempdir = tempdirectory;     }      /// <summary>     /// list of attachments in mimemessage.     /// </summary>     public ilist<mimeentity> attachments {         { return attachments; }     }      /// <summary>     /// html string can set on browsercontrol.     /// </summary>     public string htmlbody {         { return body ?? string.empty; }     }      protected override void visitmultipartalternative (multipartalternative alternative)     {         // walk multipart/alternative children backwards greatest level of faithfulness least faithful         (int = alternative.count - 1; >= 0 && body == null; i--)             alternative[i].accept (this);     }      protected override void visitmultipartrelated (multipartrelated related)     {         var root = related.root;          // push multipart/related onto our stack         stack.add (related);          // visit root document         root.accept (this);          // pop multipart/related off our stack         stack.removeat (stack.count - 1);     }      // image based on 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;         }          (int = stack.count - 1; >= 0; i--) {             if ((index = stack[i].indexof (uri)) == -1)                 continue;              image = stack[i][index] mimepart;             return image != null;         }          image = null;          return false;     }      // save image our temp directory , return "file://" url suitable     // browser control load.     // note: if you'd rather embed image data html, can construct     // "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 refer images embedded within message     // "file://" urls browser control able load.     void htmltagcallback (htmltagcontext ctx, htmlwriter htmlwriter)     {         if (ctx.tagid == htmltagid.image && !ctx.isendtag && stack.count > 0) {             ctx.writetag (htmlwriter, false);              // replace src attribute 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 tag through output             ctx.writetag (htmlwriter, true);         }     }      protected override void visittextpart (textpart entity)     {         textconverter converter;          if (body != null) {             // since we've found body, treat 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 attachments in ms-tnef part         attachments.addrange (entity.extractattachments ());     }      protected override void visitmessagepart (messagepart entity)     {         // treat message/rfc822 parts attachments         attachments.add (entity);     }      protected override void visitmimepart (mimepart entity)     {         // realistically, if we've gotten far, can treat attachment         // if isattachment property false.         attachments.add (entity);     } } 

and way you'd use visitor might 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 textbody , htmlbody properties (the easiest way)

to simplify common task of getting text of message, mimemessage includes 2 properties can text/plain or text/html version of message body. these textbody , htmlbody, respectively.

keep in mind, however, @ least htmlbody property, may html part child of multipart/related, allowing refer images , other types of media contained within multipart/related entity. property convenience property , not substitute traversing mime structure may interpret related content.


Comments