// // MessageSummary.cs // // Author: Jeffrey Stedfast // // Copyright (c) 2013-2020 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // using System; using System.Linq; using System.Collections.Generic; using MimeKit; using MimeKit.Utils; namespace MailKit { /// /// A summary of a message. /// /// /// The Fetch and /// FetchAsync methods /// return lists of items. /// The properties of the that will be available /// depend on the passed to the aformentioned method. /// public class MessageSummary : IMessageSummary { int threadableReplyDepth = -1; string normalizedSubject; /// /// Initializes a new instance of the class. /// /// /// Creates a new . /// /// The message index. /// /// is negative. /// public MessageSummary (int index) { if (index < 0) throw new ArgumentOutOfRangeException (nameof (index)); Keywords = new HashSet (StringComparer.OrdinalIgnoreCase); Index = index; } /// /// Initializes a new instance of the class. /// /// /// Creates a new . /// /// The folder that the message belongs to. /// The message index. /// /// is null. /// /// /// is negative. /// public MessageSummary (IMailFolder folder, int index) : this (index) { if (folder == null) throw new ArgumentNullException (nameof (folder)); Folder = folder; } void UpdateThreadableSubject () { if (normalizedSubject != null) return; if (Envelope?.Subject != null) { normalizedSubject = MessageThreader.GetThreadableSubject (Envelope.Subject, out threadableReplyDepth); } else { normalizedSubject = string.Empty; threadableReplyDepth = 0; } } /// /// Get the folder that the message belongs to. /// /// /// Gets the folder that the message belongs to, if available. /// /// The folder. public IMailFolder Folder { get; private set; } /// /// Get a bitmask of fields that have been populated. /// /// /// Gets a bitmask of fields that have been populated. /// /// The fields that have been populated. public MessageSummaryItems Fields { get; internal set; } /// /// Gets the body structure of the message, if available. /// /// /// The body will be one of , /// , , /// or . /// This property will only be set if either the /// flag or the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// /// The body structure of the message. public BodyPart Body { get; set; } 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 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 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; } /// /// Gets the text body part of the message if it exists. /// /// /// Gets the text/plain body part of the message. /// This property will only be usable if the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// /// /// /// /// The text body if it exists; otherwise, null. public BodyPartText TextBody { get { var multipart = Body as BodyPartMultipart; if (multipart != null) { BodyPartText plain; if (TryGetMessageBody (multipart, false, out plain)) return plain; } else { var text = Body as BodyPartText; if (text != null && text.IsPlain) return text; } return null; } } /// /// Gets the html body part of the message if it exists. /// /// /// Gets the text/html body part of the message. /// This property will only be usable if the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// /// The html body if it exists; otherwise, null. public BodyPartText HtmlBody { get { var multipart = Body as BodyPartMultipart; if (multipart != null) { BodyPartText html; if (TryGetMessageBody (multipart, true, out html)) return html; } else { var text = Body as BodyPartText; if (text != null && text.IsHtml) return text; } return null; } } static IEnumerable EnumerateBodyParts (BodyPart entity, bool attachmentsOnly) { if (entity == null) yield break; var multipart = entity as BodyPartMultipart; if (multipart != null) { foreach (var subpart in multipart.BodyParts) { foreach (var part in EnumerateBodyParts (subpart, attachmentsOnly)) yield return part; } yield break; } var basic = (BodyPartBasic) entity; if (attachmentsOnly && !basic.IsAttachment) yield break; yield return basic; } /// /// Gets the body parts of the message. /// /// /// Traverses over the , enumerating all of the /// objects. /// This property will only be usable if either the /// flag or the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// /// The body parts. public IEnumerable BodyParts { get { return EnumerateBodyParts (Body, false); } } /// /// Gets the attachments. /// /// /// Traverses over the , enumerating all of the /// objects that have a Content-Disposition /// header set to "attachment". /// This property will only be usable if the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// /// /// /// /// The attachments. public IEnumerable Attachments { get { return EnumerateBodyParts (Body, true); } } /// /// Gets the preview text of the message. /// /// /// The preview text is a short snippet of the beginning of the message /// text, typically shown in a mail client's message list to provide the user /// with a sense of what the message is about. /// This property will only be set if the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// /// The preview text. public string PreviewText { get; set; } /// /// Gets the envelope of the message, if available. /// /// /// The envelope of a message contains information such as the /// date the message was sent, the subject of the message, /// the sender of the message, who the message was sent to, /// which message(s) the message may be in reply to, /// and the message id. /// This property will only be set if the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// /// The envelope of the message. public Envelope Envelope { get; set; } /// /// Gets the normalized subject. /// /// /// A normalized Subject header value where prefixes such as /// "Re:", "Re[#]:", etc have been pruned. /// /// The normalized subject. public string NormalizedSubject { get { UpdateThreadableSubject (); return normalizedSubject; } } /// /// Gets whether or not the message is a reply. /// /// /// This value should be based on whether the message subject contained any "Re:" or "Fwd:" prefixes. /// /// true if the message is a reply; otherwise, false. public bool IsReply { get { UpdateThreadableSubject (); return threadableReplyDepth != 0; } } /// /// Gets the Date header value. /// /// /// Gets the Date header value. If the Date header is not present, the arrival date is used. /// If neither are known, is returned. /// /// The date. public DateTimeOffset Date { get { return Envelope?.Date ?? InternalDate ?? DateTimeOffset.MinValue; } } /// /// Gets the message flags, if available. /// /// /// Gets the message flags, if available. /// This property will only be set if the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// /// The message flags. public MessageFlags? Flags { get; set; } /// /// Gets the user-defined message flags, if available. /// /// /// Gets the user-defined message flags, if available. /// This property will only be set if the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// /// The user-defined message flags. public HashSet Keywords { get; set; } /// /// Gets the user-defined message flags, if available. /// /// /// Gets the user-defined message flags, if available. /// This property will only be set if the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// /// The user-defined message flags. [Obsolete ("Use Keywords instead.")] public HashSet UserFlags { get { return Keywords; } set { Keywords = value; } } /// /// Gets the message annotations, if available. /// /// /// Gets the message annotations, if available. /// This property will only be set if the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// /// The message annotations. public IList Annotations { get; set; } /// /// Gets the list of headers, if available. /// /// /// Gets the list of headers, if available. /// This property will only be set if /// is specified in a call to one of the /// Fetch /// or FetchAsync /// methods or specific headers are requested via a one of the Fetch or FetchAsync methods /// that accept list of specific headers to request for each message such as /// . /// /// /// The list of headers. public HeaderList Headers { get; set; } /// /// Gets the internal date of the message (i.e. the "received" date), if available. /// /// /// Gets the internal date of the message (i.e. the "received" date), if available. /// This property will only be set if the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// /// The internal date of the message. public DateTimeOffset? InternalDate { get; set; } /// /// Gets the size of the message, in bytes, if available. /// /// /// Gets the size of the message, in bytes, if available. /// This property will only be set if the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// /// The size of the message. public uint? Size { get; set; } /// /// Gets the mod-sequence value for the message, if available. /// /// /// Gets the mod-sequence value for the message, if available. /// This property will only be set if the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// /// The mod-sequence value. public ulong? ModSeq { get; set; } /// /// Gets the message-ids that the message references, if available. /// /// /// Gets the message-ids that the message references, if available. /// This property will only be set if the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// /// The references. public MessageIdList References { get; set; } /// /// Get the globally unique identifier for the message, if available. /// /// /// Gets the globally unique identifier of the message, if available. /// This property will only be set if the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// This property maps to the EMAILID value defined in the /// OBJECTID extension. /// /// The globally unique message identifier. public string EmailId { get; set; } /// /// Get the globally unique identifier for the message, if available. /// /// /// Gets the globally unique identifier of the message, if available. /// This property will only be set if the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// This property maps to the EMAILID value defined in the /// OBJECTID extension. /// /// The globally unique message identifier. [Obsolete ("Use EmailId instead.")] public string Id { get { return EmailId; } } /// /// Get the globally unique thread identifier for the message, if available. /// /// /// Gets the globally unique thread identifier for the message, if available. /// This property will only be set if the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// This property maps to the THREADID value defined in the /// OBJECTID extension. /// /// The globally unique thread identifier. public string ThreadId { get; set; } /// /// Gets the unique identifier of the message, if available. /// /// /// Gets the unique identifier of the message, if available. /// This property will only be set if the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// /// The uid of the message. public UniqueId UniqueId { get; set; } /// /// Gets the index of the message. /// /// /// Gets the index of the message. /// This property is always set. /// /// The index of the message. public int Index { get; internal set; } #region GMail extension properties /// /// Gets the GMail message identifier, if available. /// /// /// Gets the GMail message identifier, if available. /// This property will only be set if the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// /// The GMail message identifier. public ulong? GMailMessageId { get; set; } /// /// Gets the GMail thread identifier, if available. /// /// /// Gets the GMail thread identifier, if available. /// This property will only be set if the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// /// The GMail thread identifier. public ulong? GMailThreadId { get; set; } /// /// Gets the list of GMail labels, if available. /// /// /// Gets the list of GMail labels, if available. /// This property will only be set if the /// flag is passed to /// one of the Fetch /// or FetchAsync /// methods. /// /// The GMail labels. public IList GMailLabels { get; set; } #endregion } }