// // ImapEngine.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.IO; using System.Linq; using System.Text; using System.Threading; using System.Diagnostics; using System.Globalization; using System.Threading.Tasks; using System.Collections.Generic; using MimeKit; namespace MailKit.Net.Imap { delegate ImapFolder CreateImapFolderDelegate (ImapFolderConstructorArgs args); /// /// The state of the . /// enum ImapEngineState { /// /// The ImapEngine is in the disconnected state. /// Disconnected, /// /// The ImapEngine is in the process of connecting. /// Connecting, /// /// The ImapEngine is connected but not yet authenticated. /// Connected, /// /// The ImapEngine is in the authenticated state. /// Authenticated, /// /// The ImapEngine is in the selected state. /// Selected, /// /// The ImapEngine is in the IDLE state. /// Idle } enum ImapProtocolVersion { Unknown, IMAP4, IMAP4rev1 } enum ImapUntaggedResult { Ok, No, Bad, Handled } enum ImapQuirksMode { None, Courier, Cyrus, Domino, Dovecot, Exchange, GMail, ProtonMail, SmarterMail, SunMicrosystems, UW, Yahoo, Yandex } class ImapFolderNameComparer : IEqualityComparer { public char DirectorySeparator; public ImapFolderNameComparer (char directorySeparator) { DirectorySeparator = directorySeparator; } public bool Equals (string x, string y) { x = ImapUtils.CanonicalizeMailboxName (x, DirectorySeparator); y = ImapUtils.CanonicalizeMailboxName (y, DirectorySeparator); return x == y; } public int GetHashCode (string obj) { return ImapUtils.CanonicalizeMailboxName (obj, DirectorySeparator).GetHashCode (); } } /// /// An IMAP command engine. /// class ImapEngine : IDisposable { internal const string GenericUntaggedResponseSyntaxErrorFormat = "Syntax error in untagged {0} response. Unexpected token: {1}"; internal const string GenericItemSyntaxErrorFormat = "Syntax error in {0}. Unexpected token: {1}"; internal const string FetchBodySyntaxErrorFormat = "Syntax error in BODY. Unexpected token: {0}"; const string GenericResponseCodeSyntaxErrorFormat = "Syntax error in {0} response code. Unexpected token: {1}"; const string GreetingSyntaxErrorFormat = "Syntax error in IMAP server greeting. Unexpected token: {0}"; internal static readonly Encoding Latin1; internal static readonly Encoding UTF8; static int TagPrefixIndex; internal readonly Dictionary FolderCache; readonly CreateImapFolderDelegate createImapFolder; readonly ImapFolderNameComparer cacheComparer; internal ImapQuirksMode QuirksMode; readonly List queue; internal char TagPrefix; ImapCommand current; MimeParser parser; internal int Tag; bool disposed; static ImapEngine () { UTF8 = Encoding.GetEncoding (65001, new EncoderExceptionFallback (), new DecoderExceptionFallback ()); try { Latin1 = Encoding.GetEncoding (28591); } catch (NotSupportedException) { Latin1 = Encoding.GetEncoding (1252); } } public ImapEngine (CreateImapFolderDelegate createImapFolderDelegate) { cacheComparer = new ImapFolderNameComparer ('.'); FolderCache = new Dictionary (cacheComparer); ThreadingAlgorithms = new HashSet (); AuthenticationMechanisms = new HashSet (StringComparer.Ordinal); CompressionAlgorithms = new HashSet (StringComparer.Ordinal); SupportedContexts = new HashSet (StringComparer.Ordinal); SupportedCharsets = new HashSet (StringComparer.OrdinalIgnoreCase); Rights = new AccessRights (); PersonalNamespaces = new FolderNamespaceCollection (); SharedNamespaces = new FolderNamespaceCollection (); OtherNamespaces = new FolderNamespaceCollection (); ProtocolVersion = ImapProtocolVersion.Unknown; createImapFolder = createImapFolderDelegate; Capabilities = ImapCapabilities.None; QuirksMode = ImapQuirksMode.None; queue = new List (); } /// /// Get the authentication mechanisms supported by the IMAP server. /// /// /// The authentication mechanisms are queried durring the /// method. /// /// The authentication mechanisms. public HashSet AuthenticationMechanisms { get; private set; } /// /// Get the compression algorithms supported by the IMAP server. /// /// /// The compression algorithms are populated by the /// method. /// /// The compression algorithms. public HashSet CompressionAlgorithms { get; private set; } /// /// Get the threading algorithms supported by the IMAP server. /// /// /// The threading algorithms are populated by the /// method. /// /// The threading algorithms. public HashSet ThreadingAlgorithms { get; private set; } /// /// Gets the append limit supported by the IMAP server. /// /// /// Gets the append limit supported by the IMAP server. /// /// The append limit. public uint? AppendLimit { get; private set; } /// /// Gets the I18NLEVEL supported by the IMAP server. /// /// /// Gets the I18NLEVEL supported by the IMAP server. /// /// The internationalization level. public int I18NLevel { get; private set; } /// /// Get the capabilities supported by the IMAP server. /// /// /// The capabilities will not be known until a successful connection /// has been made via the method. /// /// The capabilities. public ImapCapabilities Capabilities { get; set; } /// /// Indicates whether or not the engine is busy processing commands. /// /// /// Indicates whether or not the engine is busy processing commands. /// /// true if th e engine is busy processing commands; otherwise, false. internal bool IsBusy { get { return current != null; } } /// /// Get the capabilities version. /// /// /// Every time the engine receives an untagged CAPABILITIES /// response from the server, it increments this value. /// /// The capabilities version. public int CapabilitiesVersion { get; private set; } /// /// Get the IMAP protocol version. /// /// /// Gets the IMAP protocol version. /// /// The IMAP protocol version. public ImapProtocolVersion ProtocolVersion { get; private set; } /// /// Get the rights specified in the capabilities. /// /// /// Gets the rights specified in the capabilities. /// /// The rights. public AccessRights Rights { get; private set; } /// /// Get the supported charsets. /// /// /// Gets the supported charsets. /// /// The supported charsets. public HashSet SupportedCharsets { get; private set; } /// /// Get the supported contexts. /// /// /// Gets the supported contexts. /// /// The supported contexts. public HashSet SupportedContexts { get; private set; } /// /// Get whether or not the QRESYNC feature has been enabled. /// /// /// Gets whether or not the QRESYNC feature has been enabled. /// /// true if the QRESYNC feature has been enabled; otherwise, false. public bool QResyncEnabled { get; internal set; } /// /// Get whether or not the UTF8=ACCEPT feature has been enabled. /// /// /// Gets whether or not the UTF8=ACCEPT feature has been enabled. /// /// true if the UTF8=ACCEPT feature has been enabled; otherwise, false. public bool UTF8Enabled { get; internal set; } /// /// Get the URI of the IMAP server. /// /// /// Gets the URI of the IMAP server. /// /// The URI of the IMAP server. public Uri Uri { get; internal set; } /// /// Get the underlying IMAP stream. /// /// /// Gets the underlying IMAP stream. /// /// The IMAP stream. public ImapStream Stream { get; private set; } /// /// Get or sets the state of the engine. /// /// /// Gets or sets the state of the engine. /// /// The engine state. public ImapEngineState State { get; internal set; } /// /// Get whether or not the engine is currently connected to a IMAP server. /// /// /// Gets whether or not the engine is currently connected to a IMAP server. /// /// true if the engine is connected; otherwise, false. public bool IsConnected { get { return Stream != null && Stream.IsConnected; } } /// /// Gets the personal folder namespaces. /// /// /// Gets the personal folder namespaces. /// /// The personal folder namespaces. public FolderNamespaceCollection PersonalNamespaces { get; private set; } /// /// Gets the shared folder namespaces. /// /// /// Gets the shared folder namespaces. /// /// The shared folder namespaces. public FolderNamespaceCollection SharedNamespaces { get; private set; } /// /// Gets the other folder namespaces. /// /// /// Gets the other folder namespaces. /// /// The other folder namespaces. public FolderNamespaceCollection OtherNamespaces { get; private set; } /// /// Gets the selected folder. /// /// /// Gets the selected folder. /// /// The selected folder. public ImapFolder Selected { get; internal set; } /// /// Gets a value indicating whether the engine is disposed. /// /// /// Gets a value indicating whether the engine is disposed. /// /// true if the engine is disposed; otherwise, false. public bool IsDisposed { get { return disposed; } } /// /// Gets whether the current NOTIFY status prevents using indexes and * for referencing messages. /// /// /// Gets whether the current NOTIFY status prevents using indexes and * for referencing messages. This is the case when the client has asked for MessageNew or MessageExpunge events on the SELECTED mailbox. /// /// true if the use of indexes and * is prevented; otherwise, false. internal bool NotifySelectedNewExpunge { get; set; } #region Special Folders /// /// Gets the Inbox folder. /// /// The Inbox folder. public ImapFolder Inbox { get; private set; } /// /// Gets the special folder containing an aggregate of all messages. /// /// The folder containing all messages. public ImapFolder All { get; private set; } /// /// Gets the special archive folder. /// /// The archive folder. public ImapFolder Archive { get; private set; } /// /// Gets the special folder containing drafts. /// /// The drafts folder. public ImapFolder Drafts { get; private set; } /// /// Gets the special folder containing flagged messages. /// /// The flagged folder. public ImapFolder Flagged { get; private set; } /// /// Gets the special folder containing important messages. /// /// The important folder. public ImapFolder Important { get; private set; } /// /// Gets the special folder containing junk messages. /// /// The junk folder. public ImapFolder Junk { get; private set; } /// /// Gets the special folder containing sent messages. /// /// The sent. public ImapFolder Sent { get; private set; } /// /// Gets the folder containing deleted messages. /// /// The trash folder. public ImapFolder Trash { get; private set; } #endregion internal ImapFolder CreateImapFolder (string encodedName, FolderAttributes attributes, char delim) { var args = new ImapFolderConstructorArgs (this, encodedName, attributes, delim); return createImapFolder (args); } internal static ImapProtocolException UnexpectedToken (string format, params object[] args) { return new ImapProtocolException (string.Format (CultureInfo.InvariantCulture, format, args)) { UnexpectedToken = true }; } internal static void AssertToken (ImapToken token, ImapTokenType type, string format, params object[] args) { if (token.Type != type) throw UnexpectedToken (format, args); } internal static void AssertToken (ImapToken token, ImapTokenType type1, ImapTokenType type2, string format, params object[] args) { if (token.Type != type1 && token.Type != type2) throw UnexpectedToken (format, args); } internal static uint ParseNumber (ImapToken token, bool nonZero, string format, params object[] args) { uint value; AssertToken (token, ImapTokenType.Atom, format, args); if (!uint.TryParse ((string) token.Value, NumberStyles.None, CultureInfo.InvariantCulture, out value) || (nonZero && value == 0)) throw UnexpectedToken (format, args); return value; } internal static ulong ParseNumber64 (ImapToken token, bool nonZero, string format, params object[] args) { ulong value; AssertToken (token, ImapTokenType.Atom, format, args); if (!ulong.TryParse ((string) token.Value, NumberStyles.None, CultureInfo.InvariantCulture, out value) || (nonZero && value == 0)) throw UnexpectedToken (format, args); return value; } internal static UniqueIdSet ParseUidSet (ImapToken token, uint validity, string format, params object[] args) { UniqueIdSet uids; AssertToken (token, ImapTokenType.Atom, format, args); if (!UniqueIdSet.TryParse ((string) token.Value, validity, out uids)) throw UnexpectedToken (format, args); return uids; } /// /// Sets the stream - this is only here to be used by the unit tests. /// /// The IMAP stream. internal void SetStream (ImapStream stream) { Stream = stream; } /// /// Takes posession of the and reads the greeting. /// /// The IMAP stream. /// Whether or not asyncrhonois IO methods should be used. /// The cancellation token. /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// An IMAP protocol error occurred. /// public async Task ConnectAsync (ImapStream stream, bool doAsync, CancellationToken cancellationToken) { TagPrefix = (char) ('A' + (TagPrefixIndex++ % 26)); ProtocolVersion = ImapProtocolVersion.Unknown; Capabilities = ImapCapabilities.None; AuthenticationMechanisms.Clear (); CompressionAlgorithms.Clear (); ThreadingAlgorithms.Clear (); SupportedCharsets.Clear (); SupportedContexts.Clear (); Rights.Clear (); State = ImapEngineState.Connecting; QuirksMode = ImapQuirksMode.None; SupportedCharsets.Add ("US-ASCII"); SupportedCharsets.Add ("UTF-8"); CapabilitiesVersion = 0; QResyncEnabled = false; UTF8Enabled = false; AppendLimit = null; Selected = null; Stream = stream; I18NLevel = 0; Tag = 0; try { var token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); AssertToken (token, ImapTokenType.Asterisk, GreetingSyntaxErrorFormat, token); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); AssertToken (token, ImapTokenType.Atom, GreetingSyntaxErrorFormat, token); var atom = (string) token.Value; var text = string.Empty; var state = State; var bye = false; switch (atom.ToUpperInvariant ()) { case "BYE": bye = true; break; case "PREAUTH": state = ImapEngineState.Authenticated; break; case "OK": state = ImapEngineState.Connected; break; default: throw UnexpectedToken (GreetingSyntaxErrorFormat, token); } token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); if (token.Type == ImapTokenType.OpenBracket) { var code = await ParseResponseCodeAsync (false, doAsync, cancellationToken).ConfigureAwait (false); if (code.Type == ImapResponseCodeType.Alert) { OnAlert (code.Message); if (bye) throw new ImapProtocolException (code.Message); } else { text = code.Message; } } else if (token.Type != ImapTokenType.Eoln) { text = (string) token.Value; text += await ReadLineAsync (doAsync, cancellationToken).ConfigureAwait (false); text = text.TrimEnd (); if (bye) throw new ImapProtocolException (text); } else if (bye) { throw new ImapProtocolException ("The IMAP server unexpectedly refused the connection."); } if (text.StartsWith ("Courier-IMAP ready.", StringComparison.Ordinal)) QuirksMode = ImapQuirksMode.Courier; else if (text.Contains (" Cyrus IMAP ")) QuirksMode = ImapQuirksMode.Cyrus; else if (text.StartsWith ("Domino IMAP4 Server", StringComparison.Ordinal)) QuirksMode = ImapQuirksMode.Domino; else if (text.StartsWith ("Dovecot ready.", StringComparison.Ordinal)) QuirksMode = ImapQuirksMode.Dovecot; else if (text.StartsWith ("Microsoft Exchange Server 2007 IMAP4 service is ready", StringComparison.Ordinal)) QuirksMode = ImapQuirksMode.Exchange; else if (text.StartsWith ("The Microsoft Exchange IMAP4 service is ready.", StringComparison.Ordinal)) QuirksMode = ImapQuirksMode.Exchange; else if (text.StartsWith ("Gimap ready", StringComparison.Ordinal)) QuirksMode = ImapQuirksMode.GMail; else if (text.Contains (" IMAP4rev1 2007f.") || text.Contains (" Panda IMAP ")) QuirksMode = ImapQuirksMode.UW; else if (text.Contains ("SmarterMail")) QuirksMode = ImapQuirksMode.SmarterMail; else if (text.Contains ("Yandex IMAP4rev1 ")) QuirksMode = ImapQuirksMode.Yandex; State = state; } catch { Disconnect (); throw; } } /// /// Disconnects the . /// /// /// Disconnects the . /// public void Disconnect () { if (Selected != null) { Selected.Reset (); Selected.OnClosed (); Selected = null; } current = null; if (Stream != null) { Stream.Dispose (); Stream = null; } if (State != ImapEngineState.Disconnected) { State = ImapEngineState.Disconnected; OnDisconnected (); } } internal async Task ReadLineAsync (bool doAsync, CancellationToken cancellationToken) { using (var memory = new MemoryStream ()) { bool complete; byte[] buf; int count; do { if (doAsync) complete = await Stream.ReadLineAsync (memory, cancellationToken).ConfigureAwait (false); else complete = Stream.ReadLine (memory, cancellationToken); } while (!complete); count = (int) memory.Length; #if !NETSTANDARD1_3 && !NETSTANDARD1_6 buf = memory.GetBuffer (); #else buf = memory.ToArray (); #endif try { return UTF8.GetString (buf, 0, count); } catch (DecoderFallbackException) { return Latin1.GetString (buf, 0, count); } } } #if false /// /// Reads a single line from the . /// /// The line. /// The cancellation token. /// /// The engine is not connected. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// An IMAP protocol error occurred. /// public string ReadLine (CancellationToken cancellationToken) { return ReadLineAsync (false, cancellationToken).GetAwaiter ().GetResult (); } /// /// Asynchronously reads a single line from the . /// /// The line. /// The cancellation token. /// /// The engine is not connected. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// An IMAP protocol error occurred. /// public Task ReadLineAsync (CancellationToken cancellationToken) { return ReadLineAsync (true, cancellationToken); } #endif internal Task ReadTokenAsync (string specials, bool doAsync, CancellationToken cancellationToken) { return Stream.ReadTokenAsync (specials, doAsync, cancellationToken); } internal Task ReadTokenAsync (bool doAsync, CancellationToken cancellationToken) { return Stream.ReadTokenAsync (ImapStream.DefaultSpecials, doAsync, cancellationToken); } internal async Task PeekTokenAsync (string specials, bool doAsync, CancellationToken cancellationToken) { var token = await ReadTokenAsync (specials, doAsync, cancellationToken).ConfigureAwait (false); Stream.UngetToken (token); return token; } internal Task PeekTokenAsync (bool doAsync, CancellationToken cancellationToken) { return PeekTokenAsync (ImapStream.DefaultSpecials, doAsync, cancellationToken); } /// /// Reads the next token. /// /// The token. /// The cancellation token. /// /// The engine is not connected. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// An IMAP protocol error occurred. /// public ImapToken ReadToken (CancellationToken cancellationToken) { return Stream.ReadToken (cancellationToken); } #if false /// /// Asynchronously reads the next token. /// /// The token. /// The cancellation token. /// /// The engine is not connected. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// An IMAP protocol error occurred. /// public Task ReadTokenAsync (CancellationToken cancellationToken) { return Stream.ReadTokenAsync (cancellationToken); } /// /// Peeks at the next token. /// /// The next token. /// A list of characters that are not legal in bare string tokens. /// The cancellation token. /// /// The engine is not connected. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// An IMAP protocol error occurred. /// public ImapToken PeekToken (string specials, CancellationToken cancellationToken) { return PeekTokenAsync (specials, false, cancellationToken).GetAwaiter ().GetResult (); } /// /// Asynchronously peeks at the next token. /// /// The next token. /// A list of characters that are not legal in bare string tokens. /// The cancellation token. /// /// The engine is not connected. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// An IMAP protocol error occurred. /// public Task PeekTokenAsync (string specials, CancellationToken cancellationToken) { return PeekTokenAsync (specials, true, cancellationToken); } /// /// Peeks at the next token. /// /// The next token. /// The cancellation token. /// /// The engine is not connected. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// An IMAP protocol error occurred. /// public ImapToken PeekToken (CancellationToken cancellationToken) { return PeekTokenAsync (false, cancellationToken).GetAwaiter ().GetResult (); } /// /// Asynchronously peeks at the next token. /// /// The next token. /// The cancellation token. /// /// The engine is not connected. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// An IMAP protocol error occurred. /// public Task PeekTokenAsync (CancellationToken cancellationToken) { return PeekTokenAsync (true, cancellationToken); } #endif internal async Task ReadLiteralAsync (bool doAsync, CancellationToken cancellationToken) { if (Stream.Mode != ImapStreamMode.Literal) throw new InvalidOperationException (); using (var memory = new MemoryStream (Stream.LiteralLength)) { var buf = new byte[4096]; int nread; if (doAsync) { while ((nread = await Stream.ReadAsync (buf, 0, buf.Length, cancellationToken).ConfigureAwait (false)) > 0) memory.Write (buf, 0, nread); } else { while ((nread = Stream.Read (buf, 0, buf.Length, cancellationToken)) > 0) memory.Write (buf, 0, nread); } nread = (int) memory.Length; #if !NETSTANDARD1_3 && !NETSTANDARD1_6 buf = memory.GetBuffer (); #else buf = memory.ToArray (); #endif return Latin1.GetString (buf, 0, nread); } } #if false /// /// Reads the literal as a string. /// /// The literal. /// The cancellation token. /// /// The is not in literal mode. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// public string ReadLiteral (CancellationToken cancellationToken) { return ReadLiteralAsync (false, cancellationToken).GetAwaiter ().GetResult (); } /// /// Asynchronously reads the literal as a string. /// /// The literal. /// The cancellation token. /// /// The is not in literal mode. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// public Task ReadLiteralAsync (CancellationToken cancellationToken) { return ReadLiteralAsync (true, cancellationToken); } #endif async Task SkipLineAsync (bool doAsync, CancellationToken cancellationToken) { ImapToken token; do { token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); if (token.Type == ImapTokenType.Literal) { var buf = new byte[4096]; int nread; do { if (doAsync) nread = await Stream.ReadAsync (buf, 0, buf.Length, cancellationToken).ConfigureAwait (false); else nread = Stream.Read (buf, 0, buf.Length, cancellationToken); } while (nread > 0); } } while (token.Type != ImapTokenType.Eoln); } async Task UpdateCapabilitiesAsync (ImapTokenType sentinel, bool doAsync, CancellationToken cancellationToken) { // Clear the extensions except STARTTLS so that this capability stays set after a STARTTLS command. ProtocolVersion = ImapProtocolVersion.Unknown; Capabilities &= ImapCapabilities.StartTLS; AuthenticationMechanisms.Clear (); CompressionAlgorithms.Clear (); ThreadingAlgorithms.Clear (); SupportedContexts.Clear (); CapabilitiesVersion++; AppendLimit = null; Rights.Clear (); I18NLevel = 0; var token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); while (token.Type == ImapTokenType.Atom) { var atom = (string) token.Value; if (atom.StartsWith ("AUTH=", StringComparison.OrdinalIgnoreCase)) { AuthenticationMechanisms.Add (atom.Substring ("AUTH=".Length)); } else if (atom.StartsWith ("APPENDLIMIT=", StringComparison.OrdinalIgnoreCase)) { uint limit; if (uint.TryParse (atom.Substring ("APPENDLIMIT=".Length), NumberStyles.None, CultureInfo.InvariantCulture, out limit)) AppendLimit = limit; Capabilities |= ImapCapabilities.AppendLimit; } else if (atom.StartsWith ("COMPRESS=", StringComparison.OrdinalIgnoreCase)) { CompressionAlgorithms.Add (atom.Substring ("COMPRESS=".Length)); Capabilities |= ImapCapabilities.Compress; } else if (atom.StartsWith ("CONTEXT=", StringComparison.OrdinalIgnoreCase)) { SupportedContexts.Add (atom.Substring ("CONTEXT=".Length)); Capabilities |= ImapCapabilities.Context; } else if (atom.StartsWith ("I18NLEVEL=", StringComparison.OrdinalIgnoreCase)) { int level; int.TryParse (atom.Substring ("I18NLEVEL=".Length), NumberStyles.None, CultureInfo.InvariantCulture, out level); I18NLevel = level; Capabilities |= ImapCapabilities.I18NLevel; } else if (atom.StartsWith ("RIGHTS=", StringComparison.OrdinalIgnoreCase)) { var rights = atom.Substring ("RIGHTS=".Length); Rights.AddRange (rights); } else if (atom.StartsWith ("THREAD=", StringComparison.OrdinalIgnoreCase)) { var algorithm = atom.Substring ("THREAD=".Length); switch (algorithm.ToUpperInvariant ()) { case "ORDEREDSUBJECT": ThreadingAlgorithms.Add (ThreadingAlgorithm.OrderedSubject); break; case "REFERENCES": ThreadingAlgorithms.Add (ThreadingAlgorithm.References); break; } Capabilities |= ImapCapabilities.Thread; } else { switch (atom.ToUpperInvariant ()) { case "IMAP4": Capabilities |= ImapCapabilities.IMAP4; break; case "IMAP4REV1": Capabilities |= ImapCapabilities.IMAP4rev1; break; case "STATUS": Capabilities |= ImapCapabilities.Status; break; case "ACL": Capabilities |= ImapCapabilities.Acl; break; case "QUOTA": Capabilities |= ImapCapabilities.Quota; break; case "LITERAL+": Capabilities |= ImapCapabilities.LiteralPlus; break; case "IDLE": Capabilities |= ImapCapabilities.Idle; break; case "MAILBOX-REFERRALS": Capabilities |= ImapCapabilities.MailboxReferrals; break; case "LOGIN-REFERRALS": Capabilities |= ImapCapabilities.LoginReferrals; break; case "NAMESPACE": Capabilities |= ImapCapabilities.Namespace; break; case "ID": Capabilities |= ImapCapabilities.Id; break; case "CHILDREN": Capabilities |= ImapCapabilities.Children; break; case "LOGINDISABLED": Capabilities |= ImapCapabilities.LoginDisabled; break; case "STARTTLS": Capabilities |= ImapCapabilities.StartTLS; break; case "MULTIAPPEND": Capabilities |= ImapCapabilities.MultiAppend; break; case "BINARY": Capabilities |= ImapCapabilities.Binary; break; case "UNSELECT": Capabilities |= ImapCapabilities.Unselect; break; case "UIDPLUS": Capabilities |= ImapCapabilities.UidPlus; break; case "CATENATE": Capabilities |= ImapCapabilities.Catenate; break; case "CONDSTORE": Capabilities |= ImapCapabilities.CondStore; break; case "ESEARCH": Capabilities |= ImapCapabilities.ESearch; break; case "SASL-IR": Capabilities |= ImapCapabilities.SaslIR; break; case "WITHIN": Capabilities |= ImapCapabilities.Within; break; case "ENABLE": Capabilities |= ImapCapabilities.Enable; break; case "QRESYNC": Capabilities |= ImapCapabilities.QuickResync; break; case "SEARCHRES": Capabilities |= ImapCapabilities.SearchResults; break; case "SORT": Capabilities |= ImapCapabilities.Sort; break; case "ANNOTATE-EXPERIMENT-1": Capabilities |= ImapCapabilities.Annotate; break; case "LIST-EXTENDED": Capabilities |= ImapCapabilities.ListExtended; break; case "CONVERT": Capabilities |= ImapCapabilities.Convert; break; case "LANGUAGE": Capabilities |= ImapCapabilities.Language; break; case "ESORT": Capabilities |= ImapCapabilities.ESort; break; case "METADATA": Capabilities |= ImapCapabilities.Metadata; break; case "METADATA-SERVER": Capabilities |= ImapCapabilities.MetadataServer; break; case "NOTIFY": Capabilities |= ImapCapabilities.Notify; break; case "LIST-STATUS": Capabilities |= ImapCapabilities.ListStatus; break; case "SORT=DISPLAY": Capabilities |= ImapCapabilities.SortDisplay; break; case "CREATE-SPECIAL-USE": Capabilities |= ImapCapabilities.CreateSpecialUse; break; case "SPECIAL-USE": Capabilities |= ImapCapabilities.SpecialUse; break; case "SEARCH=FUZZY": Capabilities |= ImapCapabilities.FuzzySearch; break; case "MULTISEARCH": Capabilities |= ImapCapabilities.MultiSearch; break; case "MOVE": Capabilities |= ImapCapabilities.Move; break; case "UTF8=ACCEPT": Capabilities |= ImapCapabilities.UTF8Accept; break; case "UTF8=ONLY": Capabilities |= ImapCapabilities.UTF8Only; break; case "LITERAL-": Capabilities |= ImapCapabilities.LiteralMinus; break; case "APPENDLIMIT": Capabilities |= ImapCapabilities.AppendLimit; break; case "UNAUTHENTICATE": Capabilities |= ImapCapabilities.Unauthenticate; break; case "STATUS=SIZE": Capabilities |= ImapCapabilities.StatusSize; break; case "LIST-MYRIGHTS": Capabilities |= ImapCapabilities.ListMyRights; break; case "OBJECTID": Capabilities |= ImapCapabilities.ObjectID; break; case "REPLACE": Capabilities |= ImapCapabilities.Replace; break; case "XLIST": Capabilities |= ImapCapabilities.XList; break; case "X-GM-EXT-1": Capabilities |= ImapCapabilities.GMailExt1; QuirksMode = ImapQuirksMode.GMail; break; case "XSTOP": QuirksMode = ImapQuirksMode.ProtonMail; break; case "X-SUN-IMAP": QuirksMode = ImapQuirksMode.SunMicrosystems; break; case "XYMHIGHESTMODSEQ": QuirksMode = ImapQuirksMode.Yahoo; break; } } token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); } AssertToken (token, sentinel, GenericItemSyntaxErrorFormat, "CAPABILITIES", token); // unget the sentinel Stream.UngetToken (token); if ((Capabilities & ImapCapabilities.IMAP4rev1) != 0) { ProtocolVersion = ImapProtocolVersion.IMAP4rev1; Capabilities |= ImapCapabilities.Status; } else if ((Capabilities & ImapCapabilities.IMAP4) != 0) { ProtocolVersion = ImapProtocolVersion.IMAP4; } if ((Capabilities & ImapCapabilities.QuickResync) != 0) Capabilities |= ImapCapabilities.CondStore; if ((Capabilities & ImapCapabilities.UTF8Only) != 0) Capabilities |= ImapCapabilities.UTF8Accept; } async Task UpdateNamespacesAsync (bool doAsync, CancellationToken cancellationToken) { var namespaces = new List { PersonalNamespaces, OtherNamespaces, SharedNamespaces }; ImapFolder folder; ImapToken token; string path; char delim; int n = 0; PersonalNamespaces.Clear (); SharedNamespaces.Clear (); OtherNamespaces.Clear (); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); do { if (token.Type == ImapTokenType.OpenParen) { // parse the list of namespace pairs... token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); while (token.Type == ImapTokenType.OpenParen) { // parse the namespace pair - first token is the path token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); path = (string) token.Value; // second token is the directory separator token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); AssertToken (token, ImapTokenType.QString, ImapTokenType.Nil, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); var qstring = token.Type == ImapTokenType.Nil ? string.Empty : (string) token.Value; if (qstring.Length > 0) { delim = qstring[0]; // canonicalize the namespace path path = path.TrimEnd (delim); } else { delim = '\0'; } namespaces[n].Add (new FolderNamespace (delim, DecodeMailboxName (path))); if (!GetCachedFolder (path, out folder)) { folder = CreateImapFolder (path, FolderAttributes.None, delim); CacheFolder (folder); } folder.UpdateIsNamespace (true); do { token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); if (token.Type == ImapTokenType.CloseParen) break; // NAMESPACE extension AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); AssertToken (token, ImapTokenType.OpenParen, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); do { token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); if (token.Type == ImapTokenType.CloseParen) break; AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); } while (true); } while (true); // read the next token - it should either be '(' or ')' token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); } AssertToken (token, ImapTokenType.CloseParen, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); } else { AssertToken (token, ImapTokenType.Nil, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token); } token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); n++; } while (n < 3); while (token.Type != ImapTokenType.Eoln) token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); } void ProcessResponseCodes (ImapCommand ic) { foreach (var code in ic.RespCodes) { switch (code.Type) { case ImapResponseCodeType.Alert: OnAlert (code.Message); break; case ImapResponseCodeType.NotificationOverflow: OnNotificationOverflow (); break; } } } void EmitMetadataChanged (Metadata metadata) { var encodedName = metadata.EncodedName; ImapFolder folder; if (encodedName.Length == 0) { OnMetadataChanged (metadata); } else if (FolderCache.TryGetValue (encodedName, out folder)) { folder.OnMetadataChanged (metadata); } } internal MetadataCollection FilterMetadata (MetadataCollection metadata, string encodedName) { for (int i = 0; i < metadata.Count; i++) { if (metadata[i].EncodedName == encodedName) continue; EmitMetadataChanged (metadata[i]); metadata.RemoveAt (i); i--; } return metadata; } internal void ProcessMetadataChanges (MetadataCollection metadata) { for (int i = 0; i < metadata.Count; i++) EmitMetadataChanged (metadata[i]); } internal static ImapResponseCodeType GetResponseCodeType (string atom) { switch (atom.ToUpperInvariant ()) { case "ALERT": return ImapResponseCodeType.Alert; case "BADCHARSET": return ImapResponseCodeType.BadCharset; case "CAPABILITY": return ImapResponseCodeType.Capability; case "NEWNAME": return ImapResponseCodeType.NewName; case "PARSE": return ImapResponseCodeType.Parse; case "PERMANENTFLAGS": return ImapResponseCodeType.PermanentFlags; case "READ-ONLY": return ImapResponseCodeType.ReadOnly; case "READ-WRITE": return ImapResponseCodeType.ReadWrite; case "TRYCREATE": return ImapResponseCodeType.TryCreate; case "UIDNEXT": return ImapResponseCodeType.UidNext; case "UIDVALIDITY": return ImapResponseCodeType.UidValidity; case "UNSEEN": return ImapResponseCodeType.Unseen; case "REFERRAL": return ImapResponseCodeType.Referral; case "UNKNOWN-CTE": return ImapResponseCodeType.UnknownCte; case "APPENDUID": return ImapResponseCodeType.AppendUid; case "COPYUID": return ImapResponseCodeType.CopyUid; case "UIDNOTSTICKY": return ImapResponseCodeType.UidNotSticky; case "URLMECH": return ImapResponseCodeType.UrlMech; case "BADURL": return ImapResponseCodeType.BadUrl; case "TOOBIG": return ImapResponseCodeType.TooBig; case "HIGHESTMODSEQ": return ImapResponseCodeType.HighestModSeq; case "MODIFIED": return ImapResponseCodeType.Modified; case "NOMODSEQ": return ImapResponseCodeType.NoModSeq; case "COMPRESSIONACTIVE": return ImapResponseCodeType.CompressionActive; case "CLOSED": return ImapResponseCodeType.Closed; case "NOTSAVED": return ImapResponseCodeType.NotSaved; case "BADCOMPARATOR": return ImapResponseCodeType.BadComparator; case "ANNOTATE": return ImapResponseCodeType.Annotate; case "ANNOTATIONS": return ImapResponseCodeType.Annotations; case "MAXCONVERTMESSAGES": return ImapResponseCodeType.MaxConvertMessages; case "MAXCONVERTPARTS": return ImapResponseCodeType.MaxConvertParts; case "TEMPFAIL": return ImapResponseCodeType.TempFail; case "NOUPDATE": return ImapResponseCodeType.NoUpdate; case "METADATA": return ImapResponseCodeType.Metadata; case "NOTIFICATIONOVERFLOW": return ImapResponseCodeType.NotificationOverflow; case "BADEVENT": return ImapResponseCodeType.BadEvent; case "UNDEFINED-FILTER": return ImapResponseCodeType.UndefinedFilter; case "UNAVAILABLE": return ImapResponseCodeType.Unavailable; case "AUTHENTICATIONFAILED": return ImapResponseCodeType.AuthenticationFailed; case "AUTHORIZATIONFAILED": return ImapResponseCodeType.AuthorizationFailed; case "EXPIRED": return ImapResponseCodeType.Expired; case "PRIVACYREQUIRED": return ImapResponseCodeType.PrivacyRequired; case "CONTACTADMIN": return ImapResponseCodeType.ContactAdmin; case "NOPERM": return ImapResponseCodeType.NoPerm; case "INUSE": return ImapResponseCodeType.InUse; case "EXPUNGEISSUED": return ImapResponseCodeType.ExpungeIssued; case "CORRUPTION": return ImapResponseCodeType.Corruption; case "SERVERBUG": return ImapResponseCodeType.ServerBug; case "CLIENTBUG": return ImapResponseCodeType.ClientBug; case "CANNOT": return ImapResponseCodeType.CanNot; case "LIMIT": return ImapResponseCodeType.Limit; case "OVERQUOTA": return ImapResponseCodeType.OverQuota; case "ALREADYEXISTS": return ImapResponseCodeType.AlreadyExists; case "NONEXISTENT": return ImapResponseCodeType.NonExistent; case "USEATTR": return ImapResponseCodeType.UseAttr; case "MAILBOXID": return ImapResponseCodeType.MailboxId; default: return ImapResponseCodeType.Unknown; } } /// /// Parses the response code. /// /// The response code. /// Whether or not the resp-code is tagged vs untagged. /// Whether or not asynchronous IO methods should be used. /// The cancellation token. public async Task ParseResponseCodeAsync (bool isTagged, bool doAsync, CancellationToken cancellationToken) { uint validity = Selected != null ? Selected.UidValidity : 0; ImapResponseCode code; ImapToken token; string atom; // token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); // // if (token.Type != ImapTokenType.LeftBracket) { // Debug.WriteLine ("Expected a '[' followed by a RESP-CODE, but got: {0}", token); // throw UnexpectedToken (token, false); // } token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); AssertToken (token, ImapTokenType.Atom, "Syntax error in response code. Unexpected token: {0}", token); atom = (string) token.Value; token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); code = ImapResponseCode.Create (GetResponseCodeType (atom)); code.IsTagged = isTagged; switch (code.Type) { case ImapResponseCodeType.BadCharset: if (token.Type == ImapTokenType.OpenParen) { token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); SupportedCharsets.Clear (); while (token.Type == ImapTokenType.Atom || token.Type == ImapTokenType.QString) { SupportedCharsets.Add ((string) token.Value); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); } AssertToken (token, ImapTokenType.CloseParen, GenericResponseCodeSyntaxErrorFormat, "BADCHARSET", token); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); } break; case ImapResponseCodeType.Capability: Stream.UngetToken (token); await UpdateCapabilitiesAsync (ImapTokenType.CloseBracket, doAsync, cancellationToken).ConfigureAwait (false); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); break; case ImapResponseCodeType.PermanentFlags: var perm = (PermanentFlagsResponseCode) code; Stream.UngetToken (token); perm.Flags = await ImapUtils.ParseFlagsListAsync (this, "PERMANENTFLAGS", null, doAsync, cancellationToken).ConfigureAwait (false); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); break; case ImapResponseCodeType.UidNext: var next = (UidNextResponseCode) code; // Note: we allow '0' here because some servers have been known to send "* OK [UIDNEXT 0]". // The *probable* explanation here is that the folder has never been opened and/or no messages // have ever been delivered (yet) to that mailbox and so the UIDNEXT has not (yet) been // initialized. // // See https://github.com/jstedfast/MailKit/issues/1010 for an example. var uid = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "UIDNEXT", token); next.Uid = uid > 0 ? new UniqueId (uid) : UniqueId.Invalid; token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); break; case ImapResponseCodeType.UidValidity: var uidvalidity = (UidValidityResponseCode) code; // Note: we allow '0' here because some servers have been known to send "* OK [UIDVALIDITY 0]". // The *probable* explanation here is that the folder has never been opened and/or no messages // have ever been delivered (yet) to that mailbox and so the UIDVALIDITY has not (yet) been // initialized. // // See https://github.com/jstedfast/MailKit/issues/150 for an example. uidvalidity.UidValidity = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "UIDVALIDITY", token); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); break; case ImapResponseCodeType.Unseen: var unseen = (UnseenResponseCode) code; // Note: we allow '0' here because some servers have been known to send "* OK [UNSEEN 0]" when the // mailbox contains no messages. // // See https://github.com/jstedfast/MailKit/issues/34 for details. var n = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "UNSEEN", token); unseen.Index = n > 0 ? (int) (n - 1) : 0; token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); break; case ImapResponseCodeType.NewName: var rename = (NewNameResponseCode) code; // Note: this RESP-CODE existed in rfc2060 but has been removed in rfc3501: // // 85) Remove NEWNAME. It can't work because mailbox names can be // literals and can include "]". Functionality can be addressed via // referrals. AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericResponseCodeSyntaxErrorFormat, "NEWNAME", token); rename.OldName = (string) token.Value; // the next token should be another atom or qstring token representing the new name of the folder token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericResponseCodeSyntaxErrorFormat, "NEWNAME", token); rename.NewName = (string) token.Value; token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); break; case ImapResponseCodeType.AppendUid: var append = (AppendUidResponseCode) code; append.UidValidity = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "APPENDUID", token); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); // The MULTIAPPEND extension redefines APPENDUID's second argument to be a uid-set instead of a single uid. append.UidSet = ParseUidSet (token, append.UidValidity, GenericResponseCodeSyntaxErrorFormat, "APPENDUID", token); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); break; case ImapResponseCodeType.CopyUid: var copy = (CopyUidResponseCode) code; copy.UidValidity = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "COPYUID", token); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); // Note: Outlook.com will apparently sometimes issue a [COPYUID nz_number SPACE SPACE] resp-code // in response to a UID COPY or UID MOVE command. Likely this happens only when the source message // didn't exist or something? See https://github.com/jstedfast/MailKit/issues/555 for details. if (token.Type != ImapTokenType.CloseBracket) { copy.SrcUidSet = ParseUidSet (token, validity, GenericResponseCodeSyntaxErrorFormat, "COPYUID", token); } else { copy.SrcUidSet = new UniqueIdSet (); Stream.UngetToken (token); } token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); if (token.Type != ImapTokenType.CloseBracket) { copy.DestUidSet = ParseUidSet (token, copy.UidValidity, GenericResponseCodeSyntaxErrorFormat, "COPYUID", token); } else { copy.DestUidSet = new UniqueIdSet (); Stream.UngetToken (token); } token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); break; case ImapResponseCodeType.BadUrl: var badurl = (BadUrlResponseCode) code; AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericResponseCodeSyntaxErrorFormat, "BADURL", token); badurl.BadUrl = (string) token.Value; token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); break; case ImapResponseCodeType.HighestModSeq: var highest = (HighestModSeqResponseCode) code; highest.HighestModSeq = ParseNumber64 (token, false, GenericResponseCodeSyntaxErrorFormat, "HIGHESTMODSEQ", token); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); break; case ImapResponseCodeType.Modified: var modified = (ModifiedResponseCode) code; modified.UidSet = ParseUidSet (token, validity, GenericResponseCodeSyntaxErrorFormat, "MODIFIED", token); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); break; case ImapResponseCodeType.MaxConvertMessages: case ImapResponseCodeType.MaxConvertParts: var maxConvert = (MaxConvertResponseCode) code; maxConvert.MaxConvert = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, atom, token); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); break; case ImapResponseCodeType.NoUpdate: var noUpdate = (NoUpdateResponseCode) code; AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericResponseCodeSyntaxErrorFormat, "NOUPDATE", token); noUpdate.Tag = (string) token.Value; token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); break; case ImapResponseCodeType.Annotate: var annotate = (AnnotateResponseCode) code; AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "ANNOTATE", token); switch (((string) token.Value).ToUpperInvariant ()) { case "TOOBIG": annotate.SubType = AnnotateResponseCodeSubType.TooBig; break; case "TOOMANY": annotate.SubType = AnnotateResponseCodeSubType.TooMany; break; } token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); break; case ImapResponseCodeType.Annotations: var annotations = (AnnotationsResponseCode) code; AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "ANNOTATIONS", token); switch (((string) token.Value).ToUpperInvariant ()) { case "NONE": break; case "READ-ONLY": annotations.Access = AnnotationAccess.ReadOnly; break; default: annotations.Access = AnnotationAccess.ReadWrite; annotations.MaxSize = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "ANNOTATIONS", token); break; } token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); if (annotations.Access != AnnotationAccess.None) { annotations.Scopes = AnnotationScope.Both; if (token.Type != ImapTokenType.CloseBracket) { AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "ANNOTATIONS", token); if (((string) token.Value).Equals ("NOPRIVATE", StringComparison.OrdinalIgnoreCase)) annotations.Scopes = AnnotationScope.Shared; token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); } } break; case ImapResponseCodeType.Metadata: var metadata = (MetadataResponseCode) code; AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "METADATA", token); switch (((string) token.Value).ToUpperInvariant ()) { case "LONGENTRIES": metadata.SubType = MetadataResponseCodeSubType.LongEntries; metadata.IsError = false; token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); metadata.Value = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "METADATA LONGENTRIES", token); break; case "MAXSIZE": metadata.SubType = MetadataResponseCodeSubType.MaxSize; token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); metadata.Value = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "METADATA MAXSIZE", token); break; case "TOOMANY": metadata.SubType = MetadataResponseCodeSubType.TooMany; break; case "NOPRIVATE": metadata.SubType = MetadataResponseCodeSubType.NoPrivate; break; } token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); break; case ImapResponseCodeType.UndefinedFilter: var undefined = (UndefinedFilterResponseCode) code; AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "UNDEFINED-FILTER", token); undefined.Name = (string) token.Value; token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); break; case ImapResponseCodeType.MailboxId: var mailboxid = (MailboxIdResponseCode) code; AssertToken (token, ImapTokenType.OpenParen, GenericResponseCodeSyntaxErrorFormat, "MAILBOXID", token); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "MAILBOXID", token); mailboxid.MailboxId = (string) token.Value; token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); AssertToken (token, ImapTokenType.CloseParen, GenericResponseCodeSyntaxErrorFormat, "MAILBOXID", token); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); break; default: // Note: This code-path handles: [ALERT], [CLOSED], [READ-ONLY], [READ-WRITE], etc. //if (code.Type == ImapResponseCodeType.Unknown) // Debug.WriteLine (string.Format ("Unknown RESP-CODE encountered: {0}", atom)); // extensions are of the form: "[" atom [SPACE 1*] "]" // skip over tokens until we get to a ']' while (token.Type != ImapTokenType.CloseBracket && token.Type != ImapTokenType.Eoln) token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); break; } AssertToken (token, ImapTokenType.CloseBracket, "Syntax error in response code. Unexpected token: {0}", token); code.Message = (await ReadLineAsync (doAsync, cancellationToken).ConfigureAwait (false)).Trim (); return code; } async Task UpdateStatusAsync (bool doAsync, CancellationToken cancellationToken) { var token = await ReadTokenAsync (ImapStream.AtomSpecials, doAsync, cancellationToken).ConfigureAwait (false); ImapFolder folder; uint count, uid; ulong modseq; string name; switch (token.Type) { case ImapTokenType.Literal: name = await ReadLiteralAsync (doAsync, cancellationToken).ConfigureAwait (false); break; case ImapTokenType.QString: case ImapTokenType.Atom: name = (string) token.Value; break; case ImapTokenType.Nil: // Note: according to rfc3501, section 4.5, NIL is acceptable as a mailbox name. name = "NIL"; break; default: throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); } // Note: if the folder is null, then it probably means the user is using NOTIFY // and hasn't yet requested the folder. That's ok. GetCachedFolder (name, out folder); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); AssertToken (token, ImapTokenType.OpenParen, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); do { token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); if (token.Type == ImapTokenType.CloseParen) break; AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); var atom = (string) token.Value; token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); switch (atom.ToUpperInvariant ()) { case "HIGHESTMODSEQ": AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); modseq = ParseNumber64 (token, false, GenericItemSyntaxErrorFormat, atom, token); if (folder != null) folder.UpdateHighestModSeq (modseq); break; case "MESSAGES": AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); count = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token); if (folder != null) folder.OnExists ((int) count); break; case "RECENT": AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); count = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token); if (folder != null) folder.OnRecent ((int) count); break; case "UIDNEXT": AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); uid = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token); if (folder != null) folder.UpdateUidNext (uid > 0 ? new UniqueId (uid) : UniqueId.Invalid); break; case "UIDVALIDITY": AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); uid = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token); if (folder != null) folder.UpdateUidValidity (uid); break; case "UNSEEN": AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); count = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token); if (folder != null) folder.UpdateUnread ((int) count); break; case "APPENDLIMIT": if (token.Type == ImapTokenType.Atom) { var limit = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token); if (folder != null) folder.UpdateAppendLimit (limit); } else { AssertToken (token, ImapTokenType.Nil, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); if (folder != null) folder.UpdateAppendLimit (null); } break; case "SIZE": AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); var size = ParseNumber64 (token, false, GenericItemSyntaxErrorFormat, atom, token); if (folder != null) folder.UpdateSize (size); break; case "MAILBOXID": AssertToken (token, ImapTokenType.OpenParen, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); AssertToken (token, ImapTokenType.Atom, GenericItemSyntaxErrorFormat, atom, token); if (folder != null) folder.UpdateId ((string) token.Value); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); AssertToken (token, ImapTokenType.CloseParen, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); break; } } while (true); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); AssertToken (token, ImapTokenType.Eoln, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token); } /// /// Processes an untagged response. /// /// The untagged response. /// Whether or not asynchronous IO methods should be used. /// The cancellation token. internal async Task ProcessUntaggedResponseAsync (bool doAsync, CancellationToken cancellationToken) { var token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); var folder = current.Folder ?? Selected; var result = ImapUntaggedResult.Handled; ImapUntaggedHandler handler; uint number; string atom; // Note: work around broken IMAP servers such as home.pl which sends "* [COPYUID ...]" resp-codes // See https://github.com/jstedfast/MailKit/issues/115#issuecomment-313684616 for details. if (token.Type == ImapTokenType.OpenBracket) { // unget the '[' token and then pretend that we got an "OK" Stream.UngetToken (token); atom = "OK"; } else if (token.Type != ImapTokenType.Atom) { // if we get anything else here, just ignore it? Stream.UngetToken (token); await SkipLineAsync (doAsync, cancellationToken).ConfigureAwait (false); return result; } else { atom = (string) token.Value; } switch (atom.ToUpperInvariant ()) { case "BYE": token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); if (token.Type == ImapTokenType.OpenBracket) { var code = await ParseResponseCodeAsync (false, doAsync, cancellationToken).ConfigureAwait (false); current.RespCodes.Add (code); } else { var text = token.Value.ToString () + await ReadLineAsync (doAsync, cancellationToken).ConfigureAwait (false); current.ResponseText = text.TrimEnd (); } current.Bye = true; // Note: Yandex IMAP is broken and will continue sending untagged BYE responses until the client closes // the connection. In order to avoid this scenario, consider this command complete as soon as we receive // the very first untagged BYE response and do not hold out hoping for a tagged response following the // untagged BYE. // // See https://github.com/jstedfast/MailKit/issues/938 for details. if (QuirksMode == ImapQuirksMode.Yandex && !current.Logout) current.Status = ImapCommandStatus.Complete; break; case "CAPABILITY": await UpdateCapabilitiesAsync (ImapTokenType.Eoln, doAsync, cancellationToken); // read the eoln token await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); break; case "ENABLED": do { token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); if (token.Type == ImapTokenType.Eoln) break; AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, atom, token); var feature = (string) token.Value; switch (feature.ToUpperInvariant ()) { case "UTF8=ACCEPT": UTF8Enabled = true; break; case "QRESYNC": QResyncEnabled = true; break; } } while (true); break; case "FLAGS": folder.UpdateAcceptedFlags (await ImapUtils.ParseFlagsListAsync (this, atom, null, doAsync, cancellationToken).ConfigureAwait (false)); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); AssertToken (token, ImapTokenType.Eoln, GenericUntaggedResponseSyntaxErrorFormat, atom, token); break; case "NAMESPACE": await UpdateNamespacesAsync (doAsync, cancellationToken).ConfigureAwait (false); break; case "STATUS": await UpdateStatusAsync (doAsync, cancellationToken).ConfigureAwait (false); break; case "OK": case "NO": case "BAD": if (atom.Equals ("OK", StringComparison.OrdinalIgnoreCase)) result = ImapUntaggedResult.Ok; else if (atom.Equals ("NO", StringComparison.OrdinalIgnoreCase)) result = ImapUntaggedResult.No; else result = ImapUntaggedResult.Bad; token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); if (token.Type == ImapTokenType.OpenBracket) { var code = await ParseResponseCodeAsync (false, doAsync, cancellationToken).ConfigureAwait (false); current.RespCodes.Add (code); } else if (token.Type != ImapTokenType.Eoln) { var text = ((string) token.Value) + await ReadLineAsync (doAsync, cancellationToken).ConfigureAwait (false); current.ResponseText = text.TrimEnd (); } break; default: if (uint.TryParse (atom, NumberStyles.None, CultureInfo.InvariantCulture, out number)) { // we probably have something like "* 1 EXISTS" token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); AssertToken (token, ImapTokenType.Atom, "Syntax error in untagged response. Unexpected token: {0}", token); atom = (string) token.Value; if (current.UntaggedHandlers.TryGetValue (atom, out handler)) { // the command registered an untagged handler for this atom... await handler (this, current, (int) number - 1, doAsync).ConfigureAwait (false); } else if (folder != null) { switch (atom.ToUpperInvariant ()) { case "EXISTS": folder.OnExists ((int) number); break; case "EXPUNGE": if (number == 0) throw UnexpectedToken ("Syntax error in untagged EXPUNGE response. Unexpected message index: 0"); folder.OnExpunge ((int) number - 1); break; case "FETCH": // Apparently Courier-IMAP (2004) will reply with "* 0 FETCH ..." sometimes. // See https://github.com/jstedfast/MailKit/issues/428 for details. //if (number == 0) // throw UnexpectedToken ("Syntax error in untagged FETCH response. Unexpected message index: 0"); await folder.OnFetchAsync (this, (int) number - 1, doAsync, cancellationToken).ConfigureAwait (false); break; case "RECENT": folder.OnRecent ((int) number); break; default: //Debug.WriteLine ("Unhandled untagged response: * {0} {1}", number, atom); break; } } else { //Debug.WriteLine ("Unhandled untagged response: * {0} {1}", number, atom); } await SkipLineAsync (doAsync, cancellationToken).ConfigureAwait (false); } else if (current.UntaggedHandlers.TryGetValue (atom, out handler)) { // the command registered an untagged handler for this atom... await handler (this, current, -1, doAsync).ConfigureAwait (false); await SkipLineAsync (doAsync, cancellationToken).ConfigureAwait (false); } else if (atom.Equals ("LIST", StringComparison.OrdinalIgnoreCase)) { // unsolicited LIST response - probably due to NOTIFY MailboxName or MailboxSubscribe event await ImapUtils.ParseFolderListAsync (this, null, false, true, doAsync, cancellationToken).ConfigureAwait (false); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); AssertToken (token, ImapTokenType.Eoln, "Syntax error in untagged LIST response. Unexpected token: {0}", token); } else if (atom.Equals ("METADATA", StringComparison.OrdinalIgnoreCase)) { // unsolicited METADATA response - probably due to NOTIFY MailboxMetadataChange or ServerMetadataChange var metadata = new MetadataCollection (); await ImapUtils.ParseMetadataAsync (this, metadata, doAsync, cancellationToken).ConfigureAwait (false); ProcessMetadataChanges (metadata); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); AssertToken (token, ImapTokenType.Eoln, "Syntax error in untagged LIST response. Unexpected token: {0}", token); } else if (atom.Equals ("VANISHED", StringComparison.OrdinalIgnoreCase) && folder != null) { await folder.OnVanishedAsync (this, doAsync, cancellationToken).ConfigureAwait (false); await SkipLineAsync (doAsync, cancellationToken).ConfigureAwait (false); } else { // don't know how to handle this... eat it? await SkipLineAsync (doAsync, cancellationToken).ConfigureAwait (false); } break; } return result; } /// /// Iterate the command pipeline. /// async Task IterateAsync (bool doAsync) { lock (queue) { if (queue.Count == 0) throw new InvalidOperationException ("The IMAP command queue is empty."); if (IsBusy) throw new InvalidOperationException ("The ImapClient is currently busy processing a command in another thread. Lock the SyncRoot property to properly synchronize your threads."); current = queue[0]; queue.RemoveAt (0); try { current.CancellationToken.ThrowIfCancellationRequested (); } catch { queue.RemoveAll (x => x.CancellationToken.IsCancellationRequested); current = null; throw; } } current.Status = ImapCommandStatus.Active; try { while (await current.StepAsync (doAsync).ConfigureAwait (false)) { // more literal data to send... } if (current.Bye && !current.Logout) throw new ImapProtocolException ("Bye."); } catch (ImapProtocolException) { var ic = current; Disconnect (); if (ic.Bye) { if (ic.RespCodes.Count > 0) { var code = ic.RespCodes[ic.RespCodes.Count - 1]; if (code.Type == ImapResponseCodeType.Alert) { OnAlert (code.Message); throw new ImapProtocolException (code.Message); } } if (!string.IsNullOrEmpty (ic.ResponseText)) throw new ImapProtocolException (ic.ResponseText); } throw; } catch { Disconnect (); throw; } finally { current = null; } } /// /// Wait for the specified command to finish. /// /// The IMAP command. /// Whether or not asynchronous IO methods should be used. /// /// is null. /// public async Task RunAsync (ImapCommand ic, bool doAsync) { if (ic == null) throw new ArgumentNullException (nameof (ic)); while (ic.Status < ImapCommandStatus.Complete) { // continue processing commands... await IterateAsync (doAsync).ConfigureAwait (false); } ProcessResponseCodes (ic); } public IEnumerable CreateCommands (CancellationToken cancellationToken, ImapFolder folder, string format, IList uids, params object[] args) { var vargs = new List (); int maxLength; // we assume that uids is the first formatter (with a %s) vargs.Add ("1"); for (int i = 0; i < args.Length; i++) vargs.Add (args[i]); args = vargs.ToArray (); if (QuirksMode == ImapQuirksMode.Courier) { // Courier IMAP's command parser allows each token to be up to 16k in size. maxLength = 16 * 1024; } else { int estimated = ImapCommand.EstimateCommandLength (this, format, args); switch (QuirksMode) { case ImapQuirksMode.Dovecot: // Dovecot, by default, allows commands up to 64k. // See https://github.com/dovecot/core/blob/master/src/imap/imap-settings.c#L94 maxLength = Math.Max ((64 * 1042) - estimated, 24); break; case ImapQuirksMode.GMail: // GMail seems to support command-lines up to at least 16k. maxLength = Math.Max ((16 * 1042) - estimated, 24); break; case ImapQuirksMode.Yahoo: case ImapQuirksMode.UW: // Follow the IMAP4 Implementation Recommendations which states that clients // *SHOULD* limit their command lengths to 1000 octets. maxLength = Math.Max (1000 - estimated, 24); break; default: // Push the boundaries of the IMAP4 Implementation Recommendations which states // that servers *SHOULD* accept command lengths of up to 8000 octets. maxLength = Math.Max (8000 - estimated, 24); break; } } foreach (var subset in UniqueIdSet.EnumerateSerializedSubsets (uids, maxLength)) { args[0] = subset; yield return new ImapCommand (this, cancellationToken, folder, format, args); } } public IEnumerable QueueCommands (CancellationToken cancellationToken, ImapFolder folder, string format, IList uids, params object[] args) { foreach (var ic in CreateCommands (cancellationToken, folder, format, uids, args)) { QueueCommand (ic); yield return ic; } } /// /// Queues the command. /// /// The command. /// The cancellation token. /// The folder that the command operates on. /// The formatting options. /// The command format. /// The command arguments. public ImapCommand QueueCommand (CancellationToken cancellationToken, ImapFolder folder, FormatOptions options, string format, params object[] args) { var ic = new ImapCommand (this, cancellationToken, folder, options, format, args); QueueCommand (ic); return ic; } /// /// Queues the command. /// /// The command. /// The cancellation token. /// The folder that the command operates on. /// The command format. /// The command arguments. public ImapCommand QueueCommand (CancellationToken cancellationToken, ImapFolder folder, string format, params object[] args) { return QueueCommand (cancellationToken, folder, FormatOptions.Default, format, args); } /// /// Queues the command. /// /// The IMAP command. public void QueueCommand (ImapCommand ic) { lock (queue) { ic.Status = ImapCommandStatus.Queued; queue.Add (ic); } } /// /// Queries the capabilities. /// /// The command result. /// Whether or not asynchronous IO methods should be used. /// The cancellation token. public async Task QueryCapabilitiesAsync (bool doAsync, CancellationToken cancellationToken) { var ic = QueueCommand (cancellationToken, null, "CAPABILITY\r\n"); await RunAsync (ic, doAsync).ConfigureAwait (false); return ic.Response; } /// /// Cache the specified folder. /// /// The folder. public void CacheFolder (ImapFolder folder) { if ((folder.Attributes & FolderAttributes.Inbox) != 0) cacheComparer.DirectorySeparator = folder.DirectorySeparator; FolderCache.Add (folder.EncodedName, folder); } /// /// Gets the cached folder. /// /// true if the folder was retreived from the cache; otherwise, false. /// The encoded folder name. /// The cached folder. public bool GetCachedFolder (string encodedName, out ImapFolder folder) { return FolderCache.TryGetValue (encodedName, out folder); } /// /// Looks up and sets the property of each of the folders. /// /// The IMAP folders. /// Whether or not asynchronous IO methods should be used. /// The cancellation token. internal async Task LookupParentFoldersAsync (IEnumerable folders, bool doAsync, CancellationToken cancellationToken) { var list = new List (folders); string encodedName, pattern; ImapFolder parent; int index; // Note: we use a for-loop instead of foreach because we conditionally add items to the list. for (int i = 0; i < list.Count; i++) { var folder = list[i]; if (folder.ParentFolder != null) continue; // FIXME: should this search EncodedName instead of FullName? if ((index = folder.FullName.LastIndexOf (folder.DirectorySeparator)) != -1) { if (index == 0) continue; var parentName = folder.FullName.Substring (0, index); encodedName = EncodeMailboxName (parentName); } else { encodedName = string.Empty; } if (GetCachedFolder (encodedName, out parent)) { folder.ParentFolder = parent; continue; } // Note: folder names can contain wildcards (including '*' and '%'), so replace '*' with '%' // in order to reduce the list of folders returned by our LIST command. pattern = encodedName.Replace ('*', '%'); var command = new StringBuilder ("LIST \"\" %S"); var returnsSubscribed = false; if ((Capabilities & ImapCapabilities.ListExtended) != 0) { // Try to get the \Subscribed and \HasChildren or \HasNoChildren attributes command.Append (" RETURN (SUBSCRIBED CHILDREN)"); returnsSubscribed = true; } command.Append ("\r\n"); var ic = new ImapCommand (this, cancellationToken, null, command.ToString (), pattern); ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); ic.ListReturnsSubscribed = returnsSubscribed; ic.UserData = new List (); QueueCommand (ic); await RunAsync (ic, doAsync).ConfigureAwait (false); if (!GetCachedFolder (encodedName, out parent)) { parent = CreateImapFolder (encodedName, FolderAttributes.NonExistent, folder.DirectorySeparator); CacheFolder (parent); } else if (parent.ParentFolder == null && !parent.IsNamespace) { list.Add (parent); } folder.ParentFolder = parent; } } /// /// Queries the namespaces. /// /// The command result. /// Whether or not asynchronous IO methods should be used. /// The cancellation token. public async Task QueryNamespacesAsync (bool doAsync, CancellationToken cancellationToken) { ImapCommand ic; if ((Capabilities & ImapCapabilities.Namespace) != 0) { ic = QueueCommand (cancellationToken, null, "NAMESPACE\r\n"); await RunAsync (ic, doAsync).ConfigureAwait (false); } else { var list = new List (); ic = new ImapCommand (this, cancellationToken, null, "LIST \"\" \"\"\r\n"); ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); ic.UserData = list; QueueCommand (ic); await RunAsync (ic, doAsync).ConfigureAwait (false); PersonalNamespaces.Clear (); SharedNamespaces.Clear (); OtherNamespaces.Clear (); if (list.Count > 0) { var empty = list.FirstOrDefault (x => x.EncodedName.Length == 0); if (empty == null) { empty = CreateImapFolder (string.Empty, FolderAttributes.None, list[0].DirectorySeparator); CacheFolder (empty); } PersonalNamespaces.Add (new FolderNamespace (empty.DirectorySeparator, empty.FullName)); empty.UpdateIsNamespace (true); } await LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false); } return ic.Response; } internal static ImapFolder GetFolder (List folders, string encodedName) { for (int i = 0; i < folders.Count; i++) { if (encodedName.Equals (folders[i].EncodedName, StringComparison.OrdinalIgnoreCase)) return folders[i]; } return null; } /// /// Assigns a folder as a special folder. /// /// The special folder. public void AssignSpecialFolder (ImapFolder folder) { if ((folder.Attributes & FolderAttributes.All) != 0) All = folder; if ((folder.Attributes & FolderAttributes.Archive) != 0) Archive = folder; if ((folder.Attributes & FolderAttributes.Drafts) != 0) Drafts = folder; if ((folder.Attributes & FolderAttributes.Flagged) != 0) Flagged = folder; if ((folder.Attributes & FolderAttributes.Important) != 0) Important = folder; if ((folder.Attributes & FolderAttributes.Junk) != 0) Junk = folder; if ((folder.Attributes & FolderAttributes.Sent) != 0) Sent = folder; if ((folder.Attributes & FolderAttributes.Trash) != 0) Trash = folder; } /// /// Assigns the special folders. /// /// The list of folders. public void AssignSpecialFolders (IList list) { for (int i = 0; i < list.Count; i++) AssignSpecialFolder (list[i]); } /// /// Queries the special folders. /// /// Whether or not asynchronous IO methods should be used. /// The cancellation token. public async Task QuerySpecialFoldersAsync (bool doAsync, CancellationToken cancellationToken) { var command = new StringBuilder ("LIST \"\" \"INBOX\""); var list = new List (); var returnsSubscribed = false; ImapFolder folder; ImapCommand ic; if ((Capabilities & ImapCapabilities.ListExtended) != 0) { command.Append (" RETURN (SUBSCRIBED CHILDREN)"); returnsSubscribed = true; } command.Append ("\r\n"); ic = new ImapCommand (this, cancellationToken, null, command.ToString ()); ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); ic.ListReturnsSubscribed = returnsSubscribed; ic.UserData = list; QueueCommand (ic); await RunAsync (ic, doAsync).ConfigureAwait (false); GetCachedFolder ("INBOX", out folder); Inbox = folder; list.Clear (); if ((Capabilities & ImapCapabilities.SpecialUse) != 0) { // Note: Some IMAP servers like ProtonMail respond to SPECIAL-USE LIST queries with BAD, so fall // back to just issuing a standard LIST command and hope we get back some SPECIAL-USE attributes. // // See https://github.com/jstedfast/MailKit/issues/674 for dertails. returnsSubscribed = false; command.Clear (); command.Append ("LIST "); if (QuirksMode != ImapQuirksMode.ProtonMail) command.Append ("(SPECIAL-USE) \"\" \"*\""); else command.Append ("\"\" \"%%\""); if ((Capabilities & ImapCapabilities.ListExtended) != 0) { command.Append (" RETURN (SUBSCRIBED CHILDREN)"); returnsSubscribed = true; } command.Append ("\r\n"); ic = new ImapCommand (this, cancellationToken, null, command.ToString ()); ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); ic.ListReturnsSubscribed = returnsSubscribed; ic.UserData = list; QueueCommand (ic); await RunAsync (ic, doAsync).ConfigureAwait (false); await LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false); AssignSpecialFolders (list); } else if ((Capabilities & ImapCapabilities.XList) != 0) { ic = new ImapCommand (this, cancellationToken, null, "XLIST \"\" \"*\"\r\n"); ic.RegisterUntaggedHandler ("XLIST", ImapUtils.ParseFolderListAsync); ic.UserData = list; QueueCommand (ic); await RunAsync (ic, doAsync).ConfigureAwait (false); await LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false); AssignSpecialFolders (list); } } /// /// Gets the folder representing the specified quota root. /// /// The folder. /// The name of the quota root. /// Whether or not asynchronous IO methods should be used. /// The cancellation token. public async Task GetQuotaRootFolderAsync (string quotaRoot, bool doAsync, CancellationToken cancellationToken) { ImapFolder folder; if (GetCachedFolder (quotaRoot, out folder)) return folder; var command = new StringBuilder ("LIST \"\" %S"); var list = new List (); var returnsSubscribed = false; if ((Capabilities & ImapCapabilities.ListExtended) != 0) { command.Append (" RETURN (SUBSCRIBED CHILDREN)"); returnsSubscribed = true; } command.Append ("\r\n"); var ic = new ImapCommand (this, cancellationToken, null, command.ToString (), quotaRoot); ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); ic.ListReturnsSubscribed = returnsSubscribed; ic.UserData = list; QueueCommand (ic); await RunAsync (ic, doAsync).ConfigureAwait (false); if (ic.Response != ImapCommandResponse.Ok) throw ImapCommandException.Create ("LIST", ic); if ((folder = GetFolder (list, quotaRoot)) == null) { folder = CreateImapFolder (quotaRoot, FolderAttributes.NonExistent, '.'); CacheFolder (folder); return folder; } await LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false); return folder; } /// /// Gets the folder for the specified path. /// /// The folder. /// The folder path. /// Whether or not asynchronous IO methods should be used. /// The cancellation token. public async Task GetFolderAsync (string path, bool doAsync, CancellationToken cancellationToken) { var encodedName = EncodeMailboxName (path); ImapFolder folder; if (GetCachedFolder (encodedName, out folder)) return folder; var command = new StringBuilder ("LIST \"\" %S"); var list = new List (); var returnsSubscribed = false; if ((Capabilities & ImapCapabilities.ListExtended) != 0) { command.Append (" RETURN (SUBSCRIBED CHILDREN)"); returnsSubscribed = true; } command.Append ("\r\n"); var ic = new ImapCommand (this, cancellationToken, null, command.ToString (), encodedName); ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); ic.ListReturnsSubscribed = returnsSubscribed; ic.UserData = list; QueueCommand (ic); await RunAsync (ic, doAsync).ConfigureAwait (false); if (ic.Response != ImapCommandResponse.Ok) throw ImapCommandException.Create ("LIST", ic); if ((folder = GetFolder (list, encodedName)) == null) throw new FolderNotFoundException (path); await LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false); return folder; } internal string GetStatusQuery (StatusItems items) { var flags = string.Empty; if ((items & StatusItems.Count) != 0) flags += "MESSAGES "; if ((items & StatusItems.Recent) != 0) flags += "RECENT "; if ((items & StatusItems.UidNext) != 0) flags += "UIDNEXT "; if ((items & StatusItems.UidValidity) != 0) flags += "UIDVALIDITY "; if ((items & StatusItems.Unread) != 0) flags += "UNSEEN "; if ((Capabilities & ImapCapabilities.CondStore) != 0) { if ((items & StatusItems.HighestModSeq) != 0) flags += "HIGHESTMODSEQ "; } // Note: If the IMAP server specifies a limit in the CAPABILITY response, then // it seems we cannot expect to be able to query this in a STATUS command... if ((Capabilities & ImapCapabilities.AppendLimit) != 0 && !AppendLimit.HasValue) { if ((items & StatusItems.AppendLimit) != 0) flags += "APPENDLIMIT "; } if ((Capabilities & ImapCapabilities.StatusSize) != 0) { if ((items & StatusItems.Size) != 0) flags += "SIZE "; } if ((Capabilities & ImapCapabilities.ObjectID) != 0) { if ((items & StatusItems.MailboxId) != 0) flags += "MAILBOXID "; } return flags.TrimEnd (); } /// /// Get all of the folders within the specified namespace. /// /// /// Gets all of the folders within the specified namespace. /// /// The list of folders. /// The namespace. /// The status items to pre-populate. /// If set to true, only subscribed folders will be listed. /// Whether or not asynchronous IO methods should be used. /// The cancellation token. public async Task> GetFoldersAsync (FolderNamespace @namespace, StatusItems items, bool subscribedOnly, bool doAsync, CancellationToken cancellationToken) { var encodedName = EncodeMailboxName (@namespace.Path); var pattern = encodedName.Length > 0 ? encodedName + @namespace.DirectorySeparator : string.Empty; var status = items != StatusItems.None; var list = new List (); var command = new StringBuilder (); var returnsSubscribed = false; var lsub = subscribedOnly; ImapFolder folder; if (!GetCachedFolder (encodedName, out folder)) throw new FolderNotFoundException (@namespace.Path); if (subscribedOnly) { if ((Capabilities & ImapCapabilities.ListExtended) != 0) { command.Append ("LIST (SUBSCRIBED)"); returnsSubscribed = true; lsub = false; } else { command.Append ("LSUB"); } } else { command.Append ("LIST"); } command.Append (" \"\" %S"); if (!lsub) { if (items != StatusItems.None && (Capabilities & ImapCapabilities.ListStatus) != 0) { command.Append (" RETURN ("); if ((Capabilities & ImapCapabilities.ListExtended) != 0) { if (!subscribedOnly) { command.Append ("SUBSCRIBED "); returnsSubscribed = true; } command.Append ("CHILDREN "); } command.AppendFormat ("STATUS ({0})", GetStatusQuery (items)); command.Append (')'); status = false; } else if ((Capabilities & ImapCapabilities.ListExtended) != 0) { command.Append (" RETURN ("); if (!subscribedOnly) { command.Append ("SUBSCRIBED "); returnsSubscribed = true; } command.Append ("CHILDREN"); command.Append (')'); } } command.Append ("\r\n"); var ic = new ImapCommand (this, cancellationToken, null, command.ToString (), pattern + "*"); ic.RegisterUntaggedHandler (lsub ? "LSUB" : "LIST", ImapUtils.ParseFolderListAsync); ic.ListReturnsSubscribed = returnsSubscribed; ic.UserData = list; ic.Lsub = lsub; QueueCommand (ic); await RunAsync (ic, doAsync).ConfigureAwait (false); if (ic.Response != ImapCommandResponse.Ok) throw ImapCommandException.Create (lsub ? "LSUB" : "LIST", ic); await LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false); if (status) { for (int i = 0; i < list.Count; i++) { if (list[i].Exists) await list[i].StatusAsync (items, doAsync, false, cancellationToken).ConfigureAwait (false); } } return list; } /// /// Decodes the name of the mailbox. /// /// The mailbox name. /// The encoded name. public string DecodeMailboxName (string encodedName) { return UTF8Enabled ? encodedName : ImapEncoding.Decode (encodedName); } /// /// Encodes the name of the mailbox. /// /// The mailbox name. /// The encoded mailbox name. public string EncodeMailboxName (string mailboxName) { return UTF8Enabled ? mailboxName : ImapEncoding.Encode (mailboxName); } /// /// Determines whether the mailbox name is valid or not. /// /// true if the mailbox name is valid; otherwise, false. /// The mailbox name. /// The path delimeter. public bool IsValidMailboxName (string mailboxName, char delim) { // From rfc6855: // // Mailbox names MUST comply with the Net-Unicode Definition ([RFC5198], Section 2) // with the specific exception that they MUST NOT contain control characters // (U+0000-U+001F and U+0080-U+009F), a delete character (U+007F), a line separator (U+2028), // or a paragraph separator (U+2029). for (int i = 0; i < mailboxName.Length; i++) { char c = mailboxName[i]; if (c <= 0x1F || (c >= 0x80 && c <= 0x9F) || c == 0x7F || c == 0x2028 || c == 0x2029 || c == delim) return false; } return mailboxName.Length > 0; } void InitializeParser (Stream stream, bool persistent) { if (parser == null) parser = new MimeParser (ParserOptions.Default, stream, persistent); else parser.SetStream (ParserOptions.Default, stream, persistent); } public async Task ParseHeadersAsync (Stream stream, bool doAsync, CancellationToken cancellationToken) { InitializeParser (stream, false); if (doAsync) return await parser.ParseHeadersAsync (cancellationToken).ConfigureAwait (false); return parser.ParseHeaders (cancellationToken); } public async Task ParseMessageAsync (Stream stream, bool persistent, bool doAsync, CancellationToken cancellationToken) { InitializeParser (stream, persistent); if (doAsync) return await parser.ParseMessageAsync (cancellationToken).ConfigureAwait (false); return parser.ParseMessage (cancellationToken); } public async Task ParseEntityAsync (Stream stream, bool persistent, bool doAsync, CancellationToken cancellationToken) { InitializeParser (stream, persistent); if (doAsync) return await parser.ParseEntityAsync (cancellationToken).ConfigureAwait (false); return parser.ParseEntity (cancellationToken); } /// /// Occurs when the engine receives an alert message from the server. /// public event EventHandler Alert; internal void OnAlert (string message) { var handler = Alert; if (handler != null) handler (this, new AlertEventArgs (message)); } /// /// Occurs when the engine receives a notification that a folder has been created. /// public event EventHandler FolderCreated; internal void OnFolderCreated (IMailFolder folder) { var handler = FolderCreated; if (handler != null) handler (this, new FolderCreatedEventArgs (folder)); } /// /// Occurs when the engine receives a notification that metadata has changed. /// public event EventHandler MetadataChanged; internal void OnMetadataChanged (Metadata metadata) { var handler = MetadataChanged; if (handler != null) handler (this, new MetadataChangedEventArgs (metadata)); } /// /// Occurs when the engine receives a notification overflow message from the server. /// public event EventHandler NotificationOverflow; internal void OnNotificationOverflow () { // [NOTIFICATIONOVERFLOW] will reset to NOTIFY NONE NotifySelectedNewExpunge = false; var handler = NotificationOverflow; if (handler != null) handler (this, EventArgs.Empty); } public event EventHandler Disconnected; void OnDisconnected () { var handler = Disconnected; if (handler != null) handler (this, EventArgs.Empty); } /// /// Releases all resource used by the object. /// /// Call when you are finished using the . The /// method leaves the in an unusable state. After /// calling , you must release all references to the so /// the garbage collector can reclaim the memory that the was occupying. public void Dispose () { disposed = true; Disconnect (); } } }