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
Post a Comment