// // ImapFolderFetch.cs // // Authors: Steffen Kieß // 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.Collections.Generic; using MimeKit; namespace MailKit.Net.Imap { /// /// An IMAP event group used with the NOTIFY command. /// /// /// An IMAP event group used with the NOTIFY command. /// public sealed class ImapEventGroup { /// /// Initializes a new instance of the class. /// /// /// Initializes a new instance of the class. /// /// The mailbox filter. /// The list of IMAP events. /// /// is null. /// -or- /// is null. /// public ImapEventGroup (ImapMailboxFilter mailboxFilter, IList events) { if (mailboxFilter == null) throw new ArgumentNullException (nameof (mailboxFilter)); if (events == null) throw new ArgumentNullException (nameof (events)); MailboxFilter = mailboxFilter; Events = events; } /// /// Get the mailbox filter. /// /// /// Gets the mailbox filter. /// /// The mailbox filter. public ImapMailboxFilter MailboxFilter { get; private set; } /// /// Get the list of IMAP events. /// /// /// Gets the list of IMAP events. /// /// The events. public IList Events { get; private set; } /// /// Format the IMAP NOTIFY command for this particular IMAP event group. /// /// /// Formats the IMAP NOTIFY command for this particular IMAP event group. /// /// The IMAP engine. /// The IMAP command builder. /// The IMAP command argument builder. /// Gets set to true if the NOTIFY command requests the MessageNew or /// MessageExpunged events for a SELECTED or SELECTED-DELAYED mailbox filter; otherwise it is left unchanged. internal void Format (ImapEngine engine, StringBuilder command, IList args, ref bool notifySelectedNewExpunge) { bool isSelectedFilter = MailboxFilter == ImapMailboxFilter.Selected || MailboxFilter == ImapMailboxFilter.SelectedDelayed; command.Append ("("); MailboxFilter.Format (engine, command, args); command.Append (" "); if (Events.Count > 0) { var haveAnnotationChange = false; var haveMessageExpunge = false; var haveMessageNew = false; var haveFlagChange = false; command.Append ("("); for (int i = 0; i < Events.Count; i++) { var @event = Events[i]; if (isSelectedFilter && !@event.IsMessageEvent) throw new InvalidOperationException ("Only message events may be specified when SELECTED or SELECTED-DELAYED is used."); if (@event is ImapEvent.MessageNew) haveMessageNew = true; else if (@event == ImapEvent.MessageExpunge) haveMessageExpunge = true; else if (@event == ImapEvent.FlagChange) haveFlagChange = true; else if (@event == ImapEvent.AnnotationChange) haveAnnotationChange = true; if (i > 0) command.Append (" "); @event.Format (engine, command, args, isSelectedFilter); } command.Append (")"); // https://tools.ietf.org/html/rfc5465#section-5 if ((haveMessageNew && !haveMessageExpunge) || (!haveMessageNew && haveMessageExpunge)) throw new InvalidOperationException ("If MessageNew or MessageExpunge is specified, both must be specified."); if ((haveFlagChange || haveAnnotationChange) && (!haveMessageNew || !haveMessageExpunge)) throw new InvalidOperationException ("If FlagChange and/or AnnotationChange are specified, MessageNew and MessageExpunge must also be specified."); notifySelectedNewExpunge = (haveMessageNew || haveMessageExpunge) && MailboxFilter == ImapMailboxFilter.Selected; } else { command.Append ("NONE"); } command.Append (")"); } } /// /// An IMAP mailbox filter for use with the NOTIFY command. /// /// /// An IMAP mailbox filter for use with the NOTIFY command. /// public class ImapMailboxFilter { /// /// An IMAP mailbox filter specifying that the client wants immediate notifications for /// the currently selected folder. /// /// /// The SELECTED mailbox specifier requires the server to send immediate /// notifications for the currently selected mailbox about all specified /// message events. /// public static readonly ImapMailboxFilter Selected = new ImapMailboxFilter ("SELECTED"); /// /// An IMAP mailbox filter specifying the currently selected folder but delays notifications /// until a command has been issued. /// /// /// The SELECTED-DELAYED mailbox specifier requires the server to delay a /// event until the client issues a command that allows /// returning information about expunged messages (see /// Section 7.4.1 of RFC3501] /// for more details), for example, till a NOOP or an IDLE command has been issued. /// When SELECTED-DELAYED is specified, the server MAY also delay returning other message /// events until the client issues one of the commands specified above, or it MAY return them /// immediately. /// public static readonly ImapMailboxFilter SelectedDelayed = new ImapMailboxFilter ("SELECTED-DELAYED"); /// /// An IMAP mailbox filter specifying the currently selected folder. /// /// /// The INBOXES mailbox specifier refers to all selectable mailboxes in the user's /// personal namespace(s) to which messages may be delivered by a Message Delivery Agent (MDA). /// /// If the IMAP server cannot easily compute this set, it MUST treat /// as equivalent to . /// public static readonly ImapMailboxFilter Inboxes = new ImapMailboxFilter ("INBOXES"); /// /// An IMAP mailbox filter specifying all selectable folders within the user's personal namespace. /// /// /// The PERSONAL mailbox specifier refers to all selectable folders within the user's personal namespace. /// public static readonly ImapMailboxFilter Personal = new ImapMailboxFilter ("PERSONAL"); /// /// An IMAP mailbox filter that refers to all subscribed folders. /// /// /// The SUBSCRIBED mailbox specifier refers to all folders subscribed to by the user. /// If the subscription list changes, the server MUST reevaluate the list. /// public static readonly ImapMailboxFilter Subscribed = new ImapMailboxFilter ("SUBSCRIBED"); /// /// An IMAP mailbox filter that specifies a list of folders to receive notifications about. /// /// /// An IMAP mailbox filter that specifies a list of folders to receive notifications about. /// public class Mailboxes : ImapMailboxFilter { readonly ImapFolder[] folders; /// /// Initializes a new instance of the class. /// /// /// Initializes a new instance of the class. /// /// The list of folders to watch for events. /// /// is null. /// /// /// The list of is empty. /// -or- /// The list of contains folders that are not of /// type . /// public Mailboxes (IList folders) : this ("MAILBOXES", folders) { } /// /// Initializes a new instance of the class. /// /// /// Initializes a new instance of the class. /// /// The list of folders to watch for events. /// /// is null. /// /// /// The list of is empty. /// -or- /// The list of contains folders that are not of /// type . /// public Mailboxes (params IMailFolder[] folders) : this ("MAILBOXES", folders) { } /// /// Initializes a new instance of the class. /// /// /// Initializes a new instance of the class. /// /// The name of the mailbox filter. /// The list of folders to watch for events. /// /// is null. /// /// /// The list of is empty. /// -or- /// The list of contains folders that are not of /// type . /// internal Mailboxes (string name, IList folders) : base (name) { if (folders == null) throw new ArgumentNullException (nameof (folders)); if (folders.Count == 0) throw new ArgumentException ("Must supply at least one folder.", nameof (folders)); this.folders = new ImapFolder[folders.Count]; for (int i = 0; i < folders.Count; i++) { if (!(folders[i] is ImapFolder folder)) throw new ArgumentException ("All folders must be ImapFolders.", nameof (folders)); this.folders[i] = folder; } } /// /// Format the IMAP NOTIFY command for this particular IMAP mailbox filter. /// /// /// Formats the IMAP NOTIFY command for this particular IMAP mailbox filter. /// /// The IMAP engine. /// The IMAP command builder. /// The IMAP command argument builder. internal override void Format (ImapEngine engine, StringBuilder command, IList args) { command.Append (Name); command.Append (' '); // FIXME: should we verify that each ImapFolder belongs to this ImapEngine? if (folders.Length == 1) { command.Append ("%F"); args.Add (folders[0]); } else { command.Append ("("); for (int i = 0; i < folders.Length; i++) { if (i > 0) command.Append (" "); command.Append ("%F"); args.Add (folders[i]); } command.Append (")"); } } } /// /// An IMAP mailbox filter that specifies a list of folder subtrees to get notifications about. /// /// /// The client will receive notifications for each specified folder plus all selectable /// folders that are subordinate to any of the specified folders. /// public class Subtree : Mailboxes { /// /// Initializes a new instance of the class. /// /// /// Initializes a new instance of the class. /// /// The list of folders to watch for events. /// /// is null. /// /// /// The list of is empty. /// -or- /// The list of contains folders that are not of /// type . /// public Subtree (IList folders) : base ("SUBTREE", folders) { } /// /// Initializes a new instance of the class. /// /// /// Initializes a new instance of the class. /// /// The list of folders to watch for events. /// /// is null. /// /// /// The list of is empty. /// -or- /// The list of contains folders that are not of /// type . /// public Subtree (params IMailFolder[] folders) : base ("SUBTREE", folders) { } } /// /// Initializes a new instance of the class. /// /// /// Initializes a new instance of the class. /// /// The name of the mailbox filter. internal ImapMailboxFilter (string name) { Name = name; } /// /// Get the name of the mailbox filter. /// /// /// Gets the name of the mailbox filter. /// /// The name. public string Name { get; private set; } /// /// Format the IMAP NOTIFY command for this particular IMAP mailbox filter. /// /// /// Formats the IMAP NOTIFY command for this particular IMAP mailbox filter. /// /// The IMAP engine. /// The IMAP command builder. /// The IMAP command argument builder. internal virtual void Format (ImapEngine engine, StringBuilder command, IList args) { command.Append (Name); } } /// /// An IMAP notification event. /// /// /// An IMAP notification event. /// public class ImapEvent { /// /// An IMAP event notification for expunged messages. /// /// /// If the expunged message or messages are in the selected mailbox, the server notifies the client /// using (or if /// the QRESYNC extension has been enabled via /// or /// ). /// If the expunged message or messages are in another mailbox, the /// and properties will be updated and the appropriate /// and events will be /// emitted for the relevant folder. If the QRESYNC /// extension is enabled, the property will also be updated and /// the event will be emitted. /// if a client requests with the /// mailbox specifier, the meaning of a message index can change at any time, so the client cannot use /// message indexes in commands anymore. The client MUST use API variants that take or /// a . The meaning of ** can also change when messages are added or expunged. /// A client wishing to keep using message indexes can either use the /// mailbox specifier or can avoid using the event entirely. /// public static readonly ImapEvent MessageExpunge = new ImapEvent ("MessageExpunge", true); /// /// An IMAP event notification for message flag changes. /// /// /// If the notification arrives for a message located in the currently selected /// folder, then that folder will emit a event as well as a /// event with an appropriately populated /// . /// On the other hand, if the notification arrives for a message that is not /// located in the currently selected folder, then the events that are emitted will depend on the /// of the IMAP server. /// If the server supports the capability (or the /// capability and the client has enabled it via /// ), then the /// event will be emitted as well as the /// event (if the latter has changed). If the number of /// seen messages has changed, then the event may also be emitted. /// If the server does not support either the capability nor /// the capability and the client has not enabled the later capability /// via , then the server may choose /// only to notify the client of changes by emitting the /// event. /// public static readonly ImapEvent FlagChange = new ImapEvent ("FlagChange", true); /// /// An IMAP event notification for message annotation changes. /// /// /// If the notification arrives for a message located in the currently selected /// folder, then that folder will emit a event as well as a /// event with an appropriately populated /// . /// On the other hand, if the notification arrives for a message that is not /// located in the currently selected folder, then the events that are emitted will depend on the /// of the IMAP server. /// If the server supports the capability (or the /// capability and the client has enabled it via /// ), then the /// event will be emitted as well as the /// event (if the latter has changed). If the number of /// seen messages has changed, then the event may also be emitted. /// If the server does not support either the capability nor /// the capability and the client has not enabled the later capability /// via , then the server may choose /// only to notify the client of changes by emitting the /// event. /// public static readonly ImapEvent AnnotationChange = new ImapEvent ("AnnotationChange", true); /// /// AN IMAP event notification for folders that have been created, deleted, or renamed. /// /// /// These notifications are sent if an affected mailbox name was created, deleted, or renamed. /// As these notifications are received by the client, the apropriate will be emitted: /// , , or /// , respectively. /// If the server supports , granting or revocation of the /// right to the current user on the affected folder will also be /// considered folder creation or deletion, respectively. If a folder is created or deleted, the folder itself /// and its direct parent (whether it is an existing folder or not) are considered to be affected. /// public static readonly ImapEvent MailboxName = new ImapEvent ("MailboxName", false); /// /// An IMAP event notification for folders who have had their subscription status changed. /// /// /// This event requests that the server notifies the client of any subscription changes, /// causing the or /// events to be emitted accordingly on the affected . /// public static readonly ImapEvent SubscriptionChange = new ImapEvent ("SubscriptionChange", false); /// /// An IMAP event notification for changes to folder metadata. /// /// /// Support for this event type is OPTIONAL unless is supported /// by the server, in which case support for this event type is REQUIRED. /// If the server does support this event, then the event /// will be emitted whenever metadata changes for any folder included in the . /// public static readonly ImapEvent MailboxMetadataChange = new ImapEvent ("MailboxMetadataChange", false); /// /// An IMAP event notification for changes to server metadata. /// /// /// Support for this event type is OPTIONAL unless is supported /// by the server, in which case support for this event type is REQUIRED. /// If the server does support this event, then the event /// will be emitted whenever metadata changes. /// public static readonly ImapEvent ServerMetadataChange = new ImapEvent ("ServerMetadataChange", false); /// /// Initializes a new instance of the class. /// /// /// Initializes a new instance of the class. /// /// The name of the IMAP event. /// true if the event is a message event; otherwise, false. internal ImapEvent (string name, bool isMessageEvent) { IsMessageEvent = isMessageEvent; Name = name; } /// /// Get whether or not this is a message event. /// /// /// Gets whether or not this is a message event. /// /// true if is message event; otherwise, false. internal bool IsMessageEvent { get; private set; } /// /// Get the name of the IMAP event. /// /// /// Gets the name of the IMAP event. /// /// The name of the IMAP event. public string Name { get; private set; } /// /// Format the IMAP NOTIFY command for this particular IMAP mailbox filter. /// /// /// Formats the IMAP NOTIFY command for this particular IMAP mailbox filter. /// /// The IMAP engine. /// The IMAP command builder. /// The IMAP command argument builder. /// true if the event is being registered for a /// or /// mailbox filter. internal virtual void Format (ImapEngine engine, StringBuilder command, IList args, bool isSelectedFilter) { command.Append (Name); } /// /// An IMAP event notification for new or appended messages. /// /// /// An IMAP event notification for new or appended messages. /// If the new or appended message is in the selected folder, the folder will emit the /// event, followed by a /// event containing the information requested by the client. /// These events will not be emitted for any message created by the client on this particular folder /// as a result of, for example, a call to /// /// or . /// public class MessageNew : ImapEvent { readonly MessageSummaryItems items; readonly HashSet headers; /// /// Initializes a new instance of the class. /// /// /// Initializes a new instance of the class. /// /// The message summary items to automatically retrieve for new messages. public MessageNew (MessageSummaryItems items = MessageSummaryItems.None) : base ("MessageNew", true) { headers = ImapFolder.EmptyHeaderFields; this.items = items; } /// /// Initializes a new instance of the class. /// /// /// Initializes a new instance of the class. /// /// The message summary items to automatically retrieve for new messages. /// Additional message headers to retrieve for new messages. public MessageNew (MessageSummaryItems items, HashSet headers) : this (items) { this.headers = ImapUtils.GetUniqueHeaders (headers); } /// /// Initializes a new instance of the class. /// /// /// Initializes a new instance of the class. /// /// The message summary items to automatically retrieve for new messages. /// Additional message headers to retrieve for new messages. public MessageNew (MessageSummaryItems items, HashSet headers) : this (items) { this.headers = ImapUtils.GetUniqueHeaders (headers); } /// /// Format the IMAP NOTIFY command for this particular IMAP mailbox filter. /// /// /// Formats the IMAP NOTIFY command for this particular IMAP mailbox filter. /// /// The IMAP engine. /// The IMAP command builder. /// The IMAP command argument builder. /// true if the event is being registered for a /// or /// mailbox filter. internal override void Format (ImapEngine engine, StringBuilder command, IList args, bool isSelectedFilter) { command.Append (Name); if (items == MessageSummaryItems.None && headers.Count == 0) return; if (!isSelectedFilter) throw new InvalidOperationException ("The MessageNew event cannot have any parameters for mailbox filters other than SELECTED and SELECTED-DELAYED."); var xitems = items; bool previewText; command.Append (" "); command.Append (ImapFolder.FormatSummaryItems (engine, ref xitems, headers, out previewText, isNotify: true)); } } } }