// // Envelope.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.Text; using System.Linq; using System.Collections.Generic; using MimeKit; using MimeKit.Utils; namespace MailKit { /// /// A message envelope containing a brief summary of the message. /// /// /// 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. /// public class Envelope { /// /// Initializes a new instance of the class. /// /// /// Creates a new . /// public Envelope () { From = new InternetAddressList (); Sender = new InternetAddressList (); ReplyTo = new InternetAddressList (); To = new InternetAddressList (); Cc = new InternetAddressList (); Bcc = new InternetAddressList (); } /// /// Gets the address(es) that the message is from. /// /// /// Gets the address(es) that the message is from. /// /// The address(es) that the message is from. public InternetAddressList From { get; private set; } /// /// Gets the actual sender(s) of the message. /// /// /// The senders may differ from the addresses in if /// the message was sent by someone on behalf of someone else. /// /// The actual sender(s) of the message. public InternetAddressList Sender { get; private set; } /// /// Gets the address(es) that replies should be sent to. /// /// /// The senders of the message may prefer that replies are sent /// somewhere other than the address they used to send the message. /// /// The address(es) that replies should be sent to. public InternetAddressList ReplyTo { get; private set; } /// /// Gets the list of addresses that the message was sent to. /// /// /// Gets the list of addresses that the message was sent to. /// /// The address(es) that the message was sent to. public InternetAddressList To { get; private set; } /// /// Gets the list of addresses that the message was carbon-copied to. /// /// /// Gets the list of addresses that the message was carbon-copied to. /// /// The address(es) that the message was carbon-copied to. public InternetAddressList Cc { get; private set; } /// /// Gets the list of addresses that the message was blind-carbon-copied to. /// /// /// Gets the list of addresses that the message was blind-carbon-copied to. /// /// The address(es) that the message was carbon-copied to. public InternetAddressList Bcc { get; private set; } /// /// The Message-Id that the message is replying to. /// /// /// The Message-Id that the message is replying to. /// /// The Message-Id that the message is replying to. public string InReplyTo { get; set; } /// /// Gets the date that the message was sent on, if available. /// /// /// Gets the date that the message was sent on, if available. /// /// The date the message was sent. public DateTimeOffset? Date { get; set; } /// /// Gets the ID of the message, if available. /// /// /// Gets the ID of the message, if available. /// /// The message identifier. public string MessageId { get; set; } /// /// Gets the subject of the message. /// /// /// Gets the subject of the message. /// /// The subject. public string Subject { get; set; } static void EncodeMailbox (StringBuilder builder, MailboxAddress mailbox) { builder.Append ('('); if (mailbox.Name != null) builder.AppendFormat ("{0} ", MimeUtils.Quote (mailbox.Name)); else builder.Append ("NIL "); if (mailbox.Route.Count != 0) builder.AppendFormat ("\"{0}\" ", mailbox.Route); else builder.Append ("NIL "); int at = mailbox.Address.LastIndexOf ('@'); if (at >= 0) { var domain = mailbox.Address.Substring (at + 1); var user = mailbox.Address.Substring (0, at); builder.AppendFormat ("{0} {1}", MimeUtils.Quote (user), MimeUtils.Quote (domain)); } else { builder.AppendFormat ("{0} \"localhost\"", MimeUtils.Quote (mailbox.Address)); } builder.Append (')'); } static void EncodeInternetAddressListAddresses (StringBuilder builder, InternetAddressList addresses) { foreach (var addr in addresses) { var mailbox = addr as MailboxAddress; var group = addr as GroupAddress; if (mailbox != null) EncodeMailbox (builder, mailbox); else if (group != null) EncodeGroup (builder, group); } } static void EncodeGroup (StringBuilder builder, GroupAddress group) { builder.AppendFormat ("(NIL NIL {0} NIL)", MimeUtils.Quote (group.Name)); EncodeInternetAddressListAddresses (builder, group.Members); builder.Append ("(NIL NIL NIL NIL)"); } static void EncodeAddressList (StringBuilder builder, InternetAddressList list) { builder.Append ('('); EncodeInternetAddressListAddresses (builder, list); builder.Append (')'); } internal void Encode (StringBuilder builder) { builder.Append ('('); if (Date.HasValue) builder.AppendFormat ("\"{0}\" ", DateUtils.FormatDate (Date.Value)); else builder.Append ("NIL "); if (Subject != null) builder.AppendFormat ("{0} ", MimeUtils.Quote (Subject)); else builder.Append ("NIL "); if (From.Count > 0) { EncodeAddressList (builder, From); builder.Append (' '); } else { builder.Append ("NIL "); } if (Sender.Count > 0) { EncodeAddressList (builder, Sender); builder.Append (' '); } else { builder.Append ("NIL "); } if (ReplyTo.Count > 0) { EncodeAddressList (builder, ReplyTo); builder.Append (' '); } else { builder.Append ("NIL "); } if (To.Count > 0) { EncodeAddressList (builder, To); builder.Append (' '); } else { builder.Append ("NIL "); } if (Cc.Count > 0) { EncodeAddressList (builder, Cc); builder.Append (' '); } else { builder.Append ("NIL "); } if (Bcc.Count > 0) { EncodeAddressList (builder, Bcc); builder.Append (' '); } else { builder.Append ("NIL "); } if (InReplyTo != null) { if (InReplyTo.Length > 1 && InReplyTo[0] != '<' && InReplyTo[InReplyTo.Length - 1] != '>') builder.AppendFormat ("{0} ", MimeUtils.Quote ('<' + InReplyTo + '>')); else builder.AppendFormat ("{0} ", MimeUtils.Quote (InReplyTo)); } else builder.Append ("NIL "); if (MessageId != null) { if (MessageId.Length > 1 && MessageId[0] != '<' && MessageId[MessageId.Length - 1] != '>') builder.AppendFormat ("{0}", MimeUtils.Quote ('<' + MessageId + '>')); else builder.AppendFormat ("{0}", MimeUtils.Quote (MessageId)); } else builder.Append ("NIL"); builder.Append (')'); } /// /// Returns a that represents the current . /// /// /// The returned string can be parsed by . /// The syntax of the string returned, while similar to IMAP's ENVELOPE syntax, /// is not completely compatible. /// /// A that represents the current . public override string ToString () { var builder = new StringBuilder (); Encode (builder); return builder.ToString (); } static bool TryParse (string text, ref int index, out string nstring) { nstring = null; while (index < text.Length && text[index] == ' ') index++; if (index >= text.Length) return false; if (text[index] != '"') { if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") { index += 3; return true; } return false; } var token = new StringBuilder (); bool escaped = false; index++; while (index < text.Length) { if (text[index] == '"' && !escaped) break; if (escaped || text[index] != '\\') { token.Append (text[index]); escaped = false; } else { escaped = true; } index++; } if (index >= text.Length) return false; nstring = token.ToString (); index++; return true; } static bool TryParse (string text, ref int index, out InternetAddress addr) { string name, route, user, domain; DomainList domains; addr = null; if (text[index] != '(') return false; index++; if (!TryParse (text, ref index, out name)) return false; if (!TryParse (text, ref index, out route)) return false; if (!TryParse (text, ref index, out user)) return false; if (!TryParse (text, ref index, out domain)) return false; while (index < text.Length && text[index] == ' ') index++; if (index >= text.Length || text[index] != ')') return false; index++; if (domain != null) { var address = user + "@" + domain; if (route != null && DomainList.TryParse (route, out domains)) addr = new MailboxAddress (name, domains, address); else addr = new MailboxAddress (name, address); } else if (user != null) { addr = new GroupAddress (user); } return true; } static bool TryParse (string text, ref int index, out InternetAddressList list) { list = null; while (index < text.Length && text[index] == ' ') index++; if (index >= text.Length) return false; if (text[index] != '(') { if (index + 3 <= text.Length && text.Substring (index, 3) == "NIL") { list = new InternetAddressList (); index += 3; return true; } return false; } index++; if (index >= text.Length) return false; list = new InternetAddressList (); var stack = new List (); int sp = 0; stack.Add (list); do { if (text[index] == ')') break; if (!TryParse (text, ref index, out InternetAddress addr)) return false; if (addr != null) { var group = addr as GroupAddress; stack[sp].Add (addr); if (group != null) { stack.Add (group.Members); sp++; } } else if (sp > 0) { stack.RemoveAt (sp); sp--; } while (index < text.Length && text[index] == ' ') index++; } while (index < text.Length); // Note: technically, we should check that sp == 0 as well, since all groups should // be popped off the stack, but in the interest of being liberal in what we accept, // we'll ignore that. if (index >= text.Length) return false; index++; return true; } internal static bool TryParse (string text, ref int index, out Envelope envelope) { InternetAddressList from, sender, replyto, to, cc, bcc; string inreplyto, messageid, subject, nstring; DateTimeOffset? date = null; envelope = 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 (!TryParse (text, ref index, out nstring)) return false; if (nstring != null) { DateTimeOffset value; if (!DateUtils.TryParse (nstring, out value)) return false; date = value; } if (!TryParse (text, ref index, out subject)) return false; if (!TryParse (text, ref index, out from)) return false; if (!TryParse (text, ref index, out sender)) return false; if (!TryParse (text, ref index, out replyto)) return false; if (!TryParse (text, ref index, out to)) return false; if (!TryParse (text, ref index, out cc)) return false; if (!TryParse (text, ref index, out bcc)) return false; if (!TryParse (text, ref index, out inreplyto)) return false; if (!TryParse (text, ref index, out messageid)) return false; if (index >= text.Length || text[index] != ')') return false; index++; envelope = new Envelope { Date = date, Subject = subject, From = from, Sender = sender, ReplyTo = replyto, To = to, Cc = cc, Bcc = bcc, InReplyTo = inreplyto != null ? MimeUtils.EnumerateReferences (inreplyto).FirstOrDefault () ?? inreplyto : null, MessageId = messageid != null ? MimeUtils.EnumerateReferences (messageid).FirstOrDefault () ?? messageid : null }; return true; } /// /// Tries to parse the given text into a new instance. /// /// /// Parses an Envelope value from the specified text. /// This syntax, while similar to IMAP's ENVELOPE syntax, is not /// completely compatible. /// /// true, if the envelope was successfully parsed, false otherwise. /// The text to parse. /// The parsed envelope. /// /// is null. /// public static bool TryParse (string text, out Envelope envelope) { if (text == null) throw new ArgumentNullException (nameof (text)); int index = 0; return TryParse (text, ref index, out envelope) && index == text.Length; } } }