static bool TryGetMultipartAlternativeBody(BodyPartMultipart multipart, bool html, out BodyPartText body) { // walk the multipart/alternative children backwards from greatest level of faithfulness to the least faithful for (int i = multipart.BodyParts.Count - 1; i >= 0; i--) { var multi = multipart.BodyParts[i] as BodyPartMultipart; BodyPartText text = null; if (multi != null) { if (multi.ContentType.IsMimeType("multipart", "related")) { text = GetMultipartRelatedRoot(multi) as BodyPartText; } else if (multi.ContentType.IsMimeType("multipart", "alternative")) { // Note: nested multipart/alternatives make no sense... yet here we are. if (TryGetMultipartAlternativeBody(multi, html, out body)) { return(true); } } } else { text = multipart.BodyParts[i] as BodyPartText; } if (text != null && (html ? text.IsHtml : text.IsPlain)) { body = text; return(true); } } body = null; return(false); }
static BodyPart GetMultipartRelatedRoot(BodyPartMultipart related) { string start = related.ContentType.Parameters["start"]; string contentId; if (start == null) { return(related.BodyParts.Count > 0 ? related.BodyParts[0] : null); } if ((contentId = MimeUtils.EnumerateReferences(start).FirstOrDefault()) == null) { contentId = start; } var cid = new Uri(string.Format("cid:{0}", contentId)); for (int i = 0; i < related.BodyParts.Count; i++) { var basic = related.BodyParts[i] as BodyPartBasic; if (basic != null && (basic.ContentId == contentId || basic.ContentLocation == cid)) { return(basic); } var multipart = related.BodyParts[i] as BodyPartMultipart; if (multipart != null && multipart.ContentLocation == cid) { return(multipart); } } return(null); }
static bool TryParse(string text, ref int index, string path, out BodyPart part) { ContentDisposition disposition; ContentType contentType; string[] array; string nstring; Uri location; uint number; part = null; while (index < text.Length && text[index] == ' ') { index++; } if (index >= text.Length || text[index] != '(') { if (index + 3 <= text.Length && text.Substring(index, 3) == "NIL") { index += 3; return(true); } return(false); } index++; if (index >= text.Length) { return(false); } if (text[index] == '(') { var prefix = path.Length > 0 ? path + "." : string.Empty; var multipart = new BodyPartMultipart(); IList <BodyPart> children; if (!TryParse(text, ref index, prefix, out children)) { return(false); } foreach (var child in children) { multipart.BodyParts.Add(child); } if (!TryParse(text, ref index, true, out contentType)) { return(false); } multipart.ContentType = contentType; if (!TryParse(text, ref index, out disposition)) { return(false); } multipart.ContentDisposition = disposition; if (!TryParse(text, ref index, out array)) { return(false); } multipart.ContentLanguage = array; if (!TryParse(text, ref index, out location)) { return(false); } multipart.ContentLocation = location; part = multipart; } else { BodyPartMessage message = null; BodyPartText txt = null; BodyPartBasic basic; if (!TryParse(text, ref index, false, out contentType)) { return(false); } if (contentType.IsMimeType("message", "rfc822")) { basic = message = new BodyPartMessage(); } else if (contentType.IsMimeType("text", "*")) { basic = txt = new BodyPartText(); } else { basic = new BodyPartBasic(); } basic.ContentType = contentType; if (!TryParse(text, ref index, out nstring)) { return(false); } basic.ContentId = nstring; if (!TryParse(text, ref index, out nstring)) { return(false); } basic.ContentDescription = nstring; if (!TryParse(text, ref index, out nstring)) { return(false); } basic.ContentTransferEncoding = nstring; if (!TryParse(text, ref index, out number)) { return(false); } basic.Octets = number; if (!TryParse(text, ref index, out nstring)) { return(false); } basic.ContentMd5 = nstring; if (!TryParse(text, ref index, out disposition)) { return(false); } basic.ContentDisposition = disposition; if (!TryParse(text, ref index, out array)) { return(false); } basic.ContentLanguage = array; if (!TryParse(text, ref index, out location)) { return(false); } basic.ContentLocation = location; if (message != null) { Envelope envelope; BodyPart body; if (!Envelope.TryParse(text, ref index, out envelope)) { return(false); } message.Envelope = envelope; if (!TryParse(text, ref index, path, out body)) { return(false); } message.Body = body; if (!TryParse(text, ref index, out number)) { return(false); } message.Lines = number; } else if (txt != null) { if (!TryParse(text, ref index, out number)) { return(false); } txt.Lines = number; } part = basic; } part.PartSpecifier = path; if (index >= text.Length || text[index] != ')') { return(false); } index++; return(true); }
static bool TryGetMessageBody (BodyPartMultipart multipart, bool html, out BodyPartText body) { BodyPartMultipart multi; BodyPartText text; if (multipart.ContentType.IsMimeType ("multipart", "alternative")) return TryGetMultipartAlternativeBody (multipart, html, out body); if (!multipart.ContentType.IsMimeType ("multipart", "related")) { // Note: This is probably a multipart/mixed... and if not, we can still treat it like it is. for (int i = 0; i < multipart.BodyParts.Count; i++) { multi = multipart.BodyParts[i] as BodyPartMultipart; // descend into nested multiparts, if there are any... if (multi != null) { if (TryGetMessageBody (multi, html, out body)) return true; // The text body should never come after a multipart. break; } text = multipart.BodyParts[i] as BodyPartText; // Look for the first non-attachment text part (realistically, the body text will // preceed any attachments, but I'm not sure we can rely on that assumption). if (text != null && !text.IsAttachment) { if (html ? text.IsHtml : text.IsPlain) { body = text; return true; } // Note: the first text/* part in a multipart/mixed is the text body. // If it's not in the format we're looking for, then it doesn't exist. break; } } } else { // Note: If the multipart/related root document is HTML, then this is the droid we are looking for. var root = GetMultipartRelatedRoot (multipart); text = root as BodyPartText; if (text != null) { body = (html ? text.IsHtml : text.IsPlain) ? text : null; return body != null; } // maybe the root is another multipart (like multipart/alternative)? multi = root as BodyPartMultipart; if (multi != null) return TryGetMessageBody (multi, html, out body); } body = null; return false; }
static bool TryGetMultipartAlternativeBody (BodyPartMultipart multipart, bool html, out BodyPartText body) { // walk the multipart/alternative children backwards from greatest level of faithfulness to the least faithful for (int i = multipart.BodyParts.Count - 1; i >= 0; i--) { var multi = multipart.BodyParts[i] as BodyPartMultipart; BodyPartText text = null; if (multi != null) { if (multi.ContentType.IsMimeType ("multipart", "related")) { text = GetMultipartRelatedRoot (multi) as BodyPartText; } else if (multi.ContentType.IsMimeType ("multipart", "alternative")) { // Note: nested multipart/alternatives make no sense... yet here we are. if (TryGetMultipartAlternativeBody (multi, html, out body)) return true; } } else { text = multipart.BodyParts[i] as BodyPartText; } if (text != null && (html ? text.IsHtml : text.IsPlain)) { body = text; return true; } } body = null; return false; }
static BodyPart GetMultipartRelatedRoot (BodyPartMultipart related) { string start = related.ContentType.Parameters["start"]; string contentId; if (start == null) return related.BodyParts.Count > 0 ? related.BodyParts[0] : null; if ((contentId = MimeUtils.EnumerateReferences (start).FirstOrDefault ()) == null) contentId = start; var cid = new Uri (string.Format ("cid:{0}", contentId)); for (int i = 0; i < related.BodyParts.Count; i++) { var basic = related.BodyParts[i] as BodyPartBasic; if (basic != null && (basic.ContentId == contentId || basic.ContentLocation == cid)) return basic; var multipart = related.BodyParts[i] as BodyPartMultipart; if (multipart != null && multipart.ContentLocation == cid) return multipart; } return null; }
static bool TryGetMessageBody(BodyPartMultipart multipart, bool html, out BodyPartText body) { BodyPartMultipart multi; BodyPartText text; if (multipart.ContentType.IsMimeType("multipart", "alternative")) { return(TryGetMultipartAlternativeBody(multipart, html, out body)); } if (!multipart.ContentType.IsMimeType("multipart", "related")) { // Note: This is probably a multipart/mixed... and if not, we can still treat it like it is. for (int i = 0; i < multipart.BodyParts.Count; i++) { multi = multipart.BodyParts[i] as BodyPartMultipart; // descend into nested multiparts, if there are any... if (multi != null) { if (TryGetMessageBody(multi, html, out body)) { return(true); } // The text body should never come after a multipart. break; } text = multipart.BodyParts[i] as BodyPartText; // Look for the first non-attachment text part (realistically, the body text will // preceed any attachments, but I'm not sure we can rely on that assumption). if (text != null && !text.IsAttachment) { if (html ? text.IsHtml : text.IsPlain) { body = text; return(true); } // Note: the first text/* part in a multipart/mixed is the text body. // If it's not in the format we're looking for, then it doesn't exist. break; } } } else { // Note: If the multipart/related root document is HTML, then this is the droid we are looking for. var root = GetMultipartRelatedRoot(multipart); text = root as BodyPartText; if (text != null) { body = (html ? text.IsHtml : text.IsPlain) ? text : null; return(body != null); } // maybe the root is another multipart (like multipart/alternative)? multi = root as BodyPartMultipart; if (multi != null) { return(TryGetMessageBody(multi, html, out body)); } } body = null; return(false); }
static BodyPartMultipart CreateMultipart (string type, string subtype, string partSpecifier, params BodyPart[] bodyParts) { var multipart = new BodyPartMultipart { ContentType = CreateContentType (type, subtype, partSpecifier) }; foreach (var bodyPart in bodyParts) multipart.BodyParts.Add (bodyPart); return multipart; }
static bool TryParse (string text, ref int index, string path, out BodyPart part) { ContentDisposition disposition; ContentType contentType; string[] array; string nstring; Uri location; uint number; part = null; while (index < text.Length && text[index] == ' ') index++; if (index >= text.Length || text[index] != '(') { if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") { index += 3; return true; } return false; } index++; if (index >= text.Length) return false; if (text[index] == '(') { var prefix = path.Length > 0 ? path + "." : string.Empty; var multipart = new BodyPartMultipart (); IList<BodyPart> children; if (!TryParse (text, ref index, prefix, out children)) return false; foreach (var child in children) multipart.BodyParts.Add (child); if (!TryParse (text, ref index, true, out contentType)) return false; multipart.ContentType = contentType; if (!TryParse (text, ref index, out disposition)) return false; multipart.ContentDisposition = disposition; if (!TryParse (text, ref index, out array)) return false; multipart.ContentLanguage = array; if (!TryParse (text, ref index, out location)) return false; multipart.ContentLocation = location; part = multipart; } else { BodyPartMessage message = null; BodyPartText txt = null; BodyPartBasic basic; if (!TryParse (text, ref index, false, out contentType)) return false; if (contentType.Matches ("message", "rfc822")) basic = message = new BodyPartMessage (); else if (contentType.Matches ("text", "*")) basic = txt = new BodyPartText (); else basic = new BodyPartBasic (); basic.ContentType = contentType; if (!TryParse (text, ref index, out nstring)) return false; basic.ContentId = nstring; if (!TryParse (text, ref index, out nstring)) return false; basic.ContentDescription = nstring; if (!TryParse (text, ref index, out nstring)) return false; basic.ContentTransferEncoding = nstring; if (!TryParse (text, ref index, out number)) return false; basic.Octets = number; if (!TryParse (text, ref index, out nstring)) return false; basic.ContentMd5 = nstring; if (!TryParse (text, ref index, out disposition)) return false; basic.ContentDisposition = disposition; if (!TryParse (text, ref index, out array)) return false; basic.ContentLanguage = array; if (!TryParse (text, ref index, out location)) return false; basic.ContentLocation = location; if (message != null) { Envelope envelope; BodyPart body; if (!Envelope.TryParse (text, ref index, out envelope)) return false; message.Envelope = envelope; if (!TryParse (text, ref index, path, out body)) return false; message.Body = body; if (!TryParse (text, ref index, out number)) return false; message.Lines = number; } else if (txt != null) { if (!TryParse (text, ref index, out number)) return false; txt.Lines = number; } part = basic; } part.PartSpecifier = path; if (index >= text.Length || text[index] != ')') return false; index++; return true; }
async void RenderRelated (IMailFolder folder, UniqueId uid, BodyPartMultipart related) { var start = related.ContentType.Parameters["start"]; BodyPartText root = null; if (!string.IsNullOrEmpty (start)) { // if the 'start' parameter is set, it overrides the default behavior of using the first // body part as the main document. root = related.BodyParts.OfType<BodyPartText> ().FirstOrDefault (x => x.ContentId == start); } else if (related.BodyParts.Count > 0) { // this will generally either be a text/html part (which is what we are looking for) or a multipart/alternative var multipart = related.BodyParts[0] as BodyPartMultipart; if (multipart != null) { if (multipart.ContentType.Matches ("multipart", "alternative") && multipart.BodyParts.Count > 0) { // find the last text/html part (which will be the closest to what the sender saw in their WYSIWYG editor) // or, failing that, the last text part. for (int i = multipart.BodyParts.Count; i > 0; i--) { var bodyPart = multipart.BodyParts[i - 1] as BodyPartText; if (bodyPart == null) continue; if (bodyPart.ContentType.Matches ("text", "html")) { root = bodyPart; break; } if (root == null) root = bodyPart; } } } else { root = related.BodyParts[0] as BodyPartText; } } if (root == null) return; var text = await folder.GetBodyPartAsync (uid, root) as TextPart; if (text != null && text.ContentType.Matches ("text", "html")) { var doc = new HtmlAgilityPack.HtmlDocument (); var saved = new Dictionary<MimePart, string> (); TextPart html; doc.LoadHtml (text.Text); // find references to related MIME parts and replace them with links to links to the saved attachments foreach (var img in doc.DocumentNode.SelectNodes ("//img[@src]")) { var src = img.Attributes["src"]; int index; Uri uri; if (src == null || src.Value == null) continue; // parse the <img src=...> attribute value into a Uri if (Uri.IsWellFormedUriString (src.Value, UriKind.Absolute)) uri = new Uri (src.Value, UriKind.Absolute); else uri = new Uri (src.Value, UriKind.Relative); // locate the index of the attachment within the multipart/related (if it exists) if ((index = related.BodyParts.IndexOf (uri)) != -1) { var bodyPart = related.BodyParts[index] as BodyPartBasic; if (bodyPart == null) { // the body part is not a basic leaf part (IOW it's a multipart or message-part) continue; } var attachment = await folder.GetBodyPartAsync (uid, bodyPart) as MimePart; // make sure the referenced part is a MimePart (as opposed to another Multipart or MessagePart) if (attachment == null) continue; string fileName; // save the attachment (if we haven't already saved it) if (!saved.TryGetValue (attachment, out fileName)) { fileName = attachment.FileName; if (string.IsNullOrEmpty (fileName)) fileName = Guid.NewGuid ().ToString (); if (!Directory.Exists (uid.ToString ())) Directory.CreateDirectory (uid.ToString ()); fileName = Path.Combine (uid.ToString (), fileName); using (var stream = File.Create (fileName)) attachment.ContentObject.DecodeTo (stream); saved.Add (attachment, fileName); } // replace the <img src=...> value with the local file name src.Value = "file://" + Path.GetFullPath (fileName); } } if (saved.Count > 0) { // we had to make some modifications to the original html part, so create a new // (temporary) text/html part to render html = new TextPart ("html"); using (var writer = new StringWriter ()) { doc.Save (writer); html.Text = writer.GetStringBuilder ().ToString (); } } else { html = text; } RenderText (html); } else if (text != null) { RenderText (text); } }
async void RenderMultipartRelated (IMailFolder folder, UniqueId uid, BodyPartMultipart bodyPart) { // download the entire multipart/related for simplicity since we'll probably end up needing all of the image attachments anyway... var related = await folder.GetBodyPartAsync (uid, bodyPart) as MultipartRelated; RenderMultipartRelated (related); }