// // ImapClient.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.Net; using System.Text; using System.Threading; using System.Net.Sockets; using System.Net.Security; using System.Globalization; using System.Threading.Tasks; using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using MailKit.Security; using SslStream = MailKit.Net.SslStream; using NetworkStream = MailKit.Net.NetworkStream; namespace MailKit.Net.Imap { /// /// An IMAP client that can be used to retrieve messages from a server. /// /// /// The class supports both the "imap" and "imaps" /// protocols. The "imap" protocol makes a clear-text connection to the IMAP /// server and does not use SSL or TLS unless the IMAP server supports the /// STARTTLS extension. /// The "imaps" protocol, however, connects to the IMAP server using an /// SSL-wrapped connection. /// /// /// /// /// /// /// public partial class ImapClient : MailStore, IImapClient { static readonly char[] ReservedUriCharacters = { ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '%' }; const string HexAlphabet = "0123456789ABCDEF"; readonly ImapEngine engine; int timeout = 2 * 60 * 1000; string identifier; bool disconnecting; bool connecting; bool disposed; bool secure; /// /// Initializes a new instance of the class. /// /// /// Before you can retrieve messages with the , you must first /// call one of the Connect /// methods and then authenticate with the one of the /// Authenticate /// methods. /// /// /// /// public ImapClient () : this (new NullProtocolLogger ()) { } /// /// Initializes a new instance of the class. /// /// /// Before you can retrieve messages with the , you must first /// call one of the Connect /// methods and then authenticate with the one of the /// Authenticate /// methods. /// /// /// /// /// The protocol logger. /// /// is null. /// public ImapClient (IProtocolLogger protocolLogger) : base (protocolLogger) { // FIXME: should this take a ParserOptions argument? engine = new ImapEngine (CreateImapFolder); engine.MetadataChanged += OnEngineMetadataChanged; engine.FolderCreated += OnEngineFolderCreated; engine.Disconnected += OnEngineDisconnected; engine.Alert += OnEngineAlert; } /// /// Gets an object that can be used to synchronize access to the IMAP server. /// /// /// Gets an object that can be used to synchronize access to the IMAP server. /// When using the non-Async methods from multiple threads, it is important to lock the /// object for thread safety when using the synchronous methods. /// /// The lock object. public override object SyncRoot { get { return engine; } } /// /// Get the protocol supported by the message service. /// /// /// Gets the protocol supported by the message service. /// /// The protocol. protected override string Protocol { get { return "imap"; } } /// /// Get the capabilities supported by the IMAP server. /// /// /// The capabilities will not be known until a successful connection has been made via one of /// the Connect methods and may /// change as a side-effect of calling one of the /// Authenticate /// methods. /// /// /// /// /// The capabilities. /// /// Capabilities cannot be enabled, they may only be disabled. /// public ImapCapabilities Capabilities { get { return engine.Capabilities; } set { if ((engine.Capabilities | value) > engine.Capabilities) throw new ArgumentException ("Capabilities cannot be enabled, they may only be disabled.", nameof (value)); engine.Capabilities = value; } } /// /// Gets the maximum size of a message that can be appended to a folder. /// /// /// Gets the maximum size of a message, in bytes, that can be appended to a folder. /// If the value is not set, then the limit is unspecified. /// /// The append limit. public uint? AppendLimit { get { return engine.AppendLimit; } } /// /// Gets the internationalization level supported by the IMAP server. /// /// /// Gets the internationalization level supported by the IMAP server. /// For more information, see /// section 4 of rfc5255. /// /// The internationalization level. public int InternationalizationLevel { get { return engine.I18NLevel; } } /// /// Get the access rights supported by the IMAP server. /// /// /// These rights are additional rights supported by the IMAP server beyond the standard rights /// defined in section 2.1 of rfc4314 /// and will not be populated until the client is successfully connected. /// /// /// /// /// The rights. public AccessRights Rights { get { return engine.Rights; } } void CheckDisposed () { if (disposed) throw new ObjectDisposedException (nameof (ImapClient)); } void CheckConnected () { if (!IsConnected) throw new ServiceNotConnectedException ("The ImapClient is not connected."); } void CheckAuthenticated () { if (!IsAuthenticated) throw new ServiceNotAuthenticatedException ("The ImapClient is not authenticated."); } /// /// Instantiate a new . /// /// /// Creates a new instance. /// This method's purpose is to allow subclassing . /// /// The IMAP folder instance. /// The constructior arguments. /// /// is null. /// protected virtual ImapFolder CreateImapFolder (ImapFolderConstructorArgs args) { var folder = new ImapFolder (args); folder.UpdateAppendLimit (AppendLimit); return folder; } bool ValidateRemoteCertificate (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { if (ServerCertificateValidationCallback != null) return ServerCertificateValidationCallback (engine.Uri.Host, certificate, chain, sslPolicyErrors); #if !NETSTANDARD1_3 && !NETSTANDARD1_6 if (ServicePointManager.ServerCertificateValidationCallback != null) return ServicePointManager.ServerCertificateValidationCallback (engine.Uri.Host, certificate, chain, sslPolicyErrors); #endif return DefaultServerCertificateValidationCallback (engine.Uri.Host, certificate, chain, sslPolicyErrors); } async Task CompressAsync (bool doAsync, CancellationToken cancellationToken) { CheckDisposed (); CheckConnected (); if ((engine.Capabilities & ImapCapabilities.Compress) == 0) throw new NotSupportedException ("The IMAP server does not support the COMPRESS extension."); if (engine.State >= ImapEngineState.Selected) throw new InvalidOperationException ("Compression must be enabled before selecting a folder."); int capabilitiesVersion = engine.CapabilitiesVersion; var ic = engine.QueueCommand (cancellationToken, null, "COMPRESS DEFLATE\r\n"); await engine.RunAsync (ic, doAsync).ConfigureAwait (false); if (ic.Response != ImapCommandResponse.Ok) { for (int i = 0; i < ic.RespCodes.Count; i++) { if (ic.RespCodes[i].Type == ImapResponseCodeType.CompressionActive) return; } throw ImapCommandException.Create ("COMPRESS", ic); } engine.Stream.Stream = new CompressedStream (engine.Stream.Stream); } /// /// Enable compression over the IMAP connection. /// /// /// Enables compression over the IMAP connection. /// If the IMAP server supports the extension, /// it is possible at any point after connecting to enable compression to reduce network /// bandwidth usage. Ideally, this method should be called before authenticating. /// /// The cancellation token. /// /// The has been disposed. /// /// /// The is not connected. /// /// /// Compression must be enabled before a folder has been selected. /// /// /// The IMAP server does not support the extension. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// The server replied to the COMPRESS command with a NO or BAD response. /// /// /// An IMAP protocol error occurred. /// public void Compress (CancellationToken cancellationToken = default (CancellationToken)) { CompressAsync (false, cancellationToken).GetAwaiter ().GetResult (); } async Task EnableQuickResyncAsync (bool doAsync, CancellationToken cancellationToken) { CheckDisposed (); CheckConnected (); CheckAuthenticated (); if (engine.State != ImapEngineState.Authenticated) throw new InvalidOperationException ("QRESYNC needs to be enabled immediately after authenticating."); if ((engine.Capabilities & ImapCapabilities.QuickResync) == 0) throw new NotSupportedException ("The IMAP server does not support the QRESYNC extension."); if (engine.QResyncEnabled) return; var ic = engine.QueueCommand (cancellationToken, null, "ENABLE QRESYNC CONDSTORE\r\n"); await engine.RunAsync (ic, doAsync).ConfigureAwait (false); if (ic.Response != ImapCommandResponse.Ok) throw ImapCommandException.Create ("ENABLE", ic); } /// /// Enable the QRESYNC feature. /// /// /// Enables the QRESYNC feature. /// The QRESYNC extension improves resynchronization performance of folders by /// querying the IMAP server for a list of changes when the folder is opened using the /// /// method. /// If this feature is enabled, the event is replaced /// with the event. /// This method needs to be called immediately after calling one of the /// Authenticate methods, before /// opening any folders. /// /// The cancellation token. /// /// The has been disposed. /// /// /// The is not connected. /// /// /// The is not authenticated. /// /// /// Quick resynchronization needs to be enabled before selecting a folder. /// /// /// The IMAP server does not support the QRESYNC extension. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// The server replied to the ENABLE command with a NO or BAD response. /// /// /// An IMAP protocol error occurred. /// public override void EnableQuickResync (CancellationToken cancellationToken = default (CancellationToken)) { EnableQuickResyncAsync (false, cancellationToken).GetAwaiter ().GetResult (); } async Task EnableUTF8Async (bool doAsync, CancellationToken cancellationToken) { CheckDisposed (); CheckConnected (); CheckAuthenticated (); if (engine.State != ImapEngineState.Authenticated) throw new InvalidOperationException ("UTF8=ACCEPT needs to be enabled immediately after authenticating."); if ((engine.Capabilities & ImapCapabilities.UTF8Accept) == 0) throw new NotSupportedException ("The IMAP server does not support the UTF8=ACCEPT extension."); if (engine.UTF8Enabled) return; var ic = engine.QueueCommand (cancellationToken, null, "ENABLE UTF8=ACCEPT\r\n"); await engine.RunAsync (ic, doAsync).ConfigureAwait (false); if (ic.Response != ImapCommandResponse.Ok) throw ImapCommandException.Create ("ENABLE", ic); } /// /// Enable the UTF8=ACCEPT extension. /// /// /// Enables the UTF8=ACCEPT extension. /// /// The cancellation token. /// /// The has been disposed. /// /// /// The is not connected. /// /// /// The is not authenticated. /// /// /// UTF8=ACCEPT needs to be enabled before selecting a folder. /// /// /// The IMAP server does not support the UTF8=ACCEPT extension. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// The server replied to the ENABLE command with a NO or BAD response. /// /// /// An IMAP protocol error occurred. /// public void EnableUTF8 (CancellationToken cancellationToken = default (CancellationToken)) { EnableUTF8Async (false, cancellationToken).GetAwaiter ().GetResult (); } async Task IdentifyAsync (ImapImplementation clientImplementation, bool doAsync, CancellationToken cancellationToken) { CheckDisposed (); CheckConnected (); if ((engine.Capabilities & ImapCapabilities.Id) == 0) throw new NotSupportedException ("The IMAP server does not support the ID extension."); var command = new StringBuilder ("ID "); var args = new List (); if (clientImplementation != null && clientImplementation.Properties.Count > 0) { command.Append ('('); foreach (var property in clientImplementation.Properties) { command.Append ("%Q "); args.Add (property.Key); if (property.Value != null) { command.Append ("%Q "); args.Add (property.Value); } else { command.Append ("NIL "); } } command[command.Length - 1] = ')'; command.Append ("\r\n"); } else { command.Append ("NIL\r\n"); } var ic = new ImapCommand (engine, cancellationToken, null, command.ToString (), args.ToArray ()); ic.RegisterUntaggedHandler ("ID", ImapUtils.ParseImplementationAsync); engine.QueueCommand (ic); await engine.RunAsync (ic, doAsync).ConfigureAwait (false); if (ic.Response != ImapCommandResponse.Ok) throw ImapCommandException.Create ("ID", ic); return (ImapImplementation) ic.UserData; } /// /// Identify the client implementation to the server and obtain the server implementation details. /// /// /// Passes along the client implementation details to the server while also obtaining implementation /// details from the server. /// If the is null or no properties have been set, no /// identifying information will be sent to the server. /// /// Security Implications /// This command has the danger of violating the privacy of users if misused. Clients should /// notify users that they send the ID command. /// It is highly desirable that implementations provide a method of disabling ID support, perhaps by /// not calling this method at all, or by passing null as the /// argument. /// Implementors must exercise extreme care in adding properties to the . /// Some properties, such as a processor ID number, Ethernet address, or other unique (or mostly unique) identifier /// would allow tracking of users in ways that violate user privacy expectations and may also make it easier for /// attackers to exploit security holes in the client. /// /// /// /// /// /// The implementation details of the server if available; otherwise, null. /// The client implementation. /// The cancellation token. /// /// The has been disposed. /// /// /// The is not connected. /// /// /// The IMAP server does not support the ID extension. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// The server replied to the ID command with a NO or BAD response. /// /// /// An IMAP protocol error occurred. /// public ImapImplementation Identify (ImapImplementation clientImplementation, CancellationToken cancellationToken = default (CancellationToken)) { return IdentifyAsync (clientImplementation, false, cancellationToken).GetAwaiter ().GetResult (); } #region IMailService implementation /// /// Get the authentication mechanisms supported by the IMAP server. /// /// /// The authentication mechanisms are queried as part of the /// Connect /// method. /// To prevent the usage of certain authentication mechanisms, /// simply remove them from the hash set /// before authenticating. /// /// /// /// /// The authentication mechanisms. public override HashSet AuthenticationMechanisms { get { return engine.AuthenticationMechanisms; } } /// /// Get the threading algorithms supported by the IMAP server. /// /// /// The threading algorithms are queried as part of the /// Connect /// and Authenticate methods. /// /// /// /// /// The supported threading algorithms. public override HashSet ThreadingAlgorithms { get { return engine.ThreadingAlgorithms; } } /// /// Get or set the timeout for network streaming operations, in milliseconds. /// /// /// Gets or sets the underlying socket stream's /// and values. /// /// The timeout in milliseconds. public override int Timeout { get { return timeout; } set { if (IsConnected && engine.Stream.CanTimeout) { engine.Stream.WriteTimeout = value; engine.Stream.ReadTimeout = value; } timeout = value; } } /// /// Get whether or not the client is currently connected to an IMAP server. /// /// /// The state is set to true immediately after /// one of the Connect /// methods succeeds and is not set back to false until either the client /// is disconnected via or until an /// is thrown while attempting to read or write to /// the underlying network socket. /// When an is caught, the connection state of the /// should be checked before continuing. /// /// true if the client is connected; otherwise, false. public override bool IsConnected { get { return engine.IsConnected; } } /// /// Get whether or not the connection is secure (typically via SSL or TLS). /// /// /// Gets whether or not the connection is secure (typically via SSL or TLS). /// /// true if the connection is secure; otherwise, false. public override bool IsSecure { get { return IsConnected && secure; } } /// /// Get whether or not the client is currently authenticated with the IMAP server. /// /// /// Gets whether or not the client is currently authenticated with the IMAP server. /// To authenticate with the IMAP server, use one of the /// Authenticate /// methods. /// /// true if the client is connected; otherwise, false. public override bool IsAuthenticated { get { return engine.State >= ImapEngineState.Authenticated; } } /// /// Get whether or not the client is currently in the IDLE state. /// /// /// Gets whether or not the client is currently in the IDLE state. /// /// true if an IDLE command is active; otherwise, false. public bool IsIdle { get { return engine.State == ImapEngineState.Idle; } } static AuthenticationException CreateAuthenticationException (ImapCommand ic) { for (int i = 0; i < ic.RespCodes.Count; i++) { if (ic.RespCodes[i].IsError || ic.RespCodes[i].Type == ImapResponseCodeType.Alert) return new AuthenticationException (ic.RespCodes[i].Message); } if (ic.ResponseText != null) return new AuthenticationException (ic.ResponseText); return new AuthenticationException (); } void EmitAndThrowOnAlert (ImapCommand ic) { for (int i = 0; i < ic.RespCodes.Count; i++) { if (ic.RespCodes[i].Type != ImapResponseCodeType.Alert) continue; OnAlert (ic.RespCodes[i].Message); throw new AuthenticationException (ic.ResponseText ?? ic.RespCodes[i].Message); } } static bool IsHexDigit (char c) { return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); } static uint HexUnescape (uint c) { if (c >= 'a') return (c - 'a') + 10; if (c >= 'A') return (c - 'A') + 10; return c - '0'; } static char HexUnescape (string pattern, ref int index) { uint value, c; if (pattern[index++] != '%' || !IsHexDigit (pattern[index]) || !IsHexDigit (pattern[index + 1])) return '%'; c = (uint) pattern[index++]; value = HexUnescape (c) << 4; c = pattern[index++]; value |= HexUnescape (c); return (char) value; } internal static string UnescapeUserName (string escaped) { StringBuilder userName; int startIndex, index; if ((index = escaped.IndexOf ('%')) == -1) return escaped; userName = new StringBuilder (); startIndex = 0; do { userName.Append (escaped, startIndex, index - startIndex); userName.Append (HexUnescape (escaped, ref index)); startIndex = index; if (startIndex >= escaped.Length) break; index = escaped.IndexOf ('%', startIndex); } while (index != -1); if (index == -1) userName.Append (escaped, startIndex, escaped.Length - startIndex); return userName.ToString (); } static string HexEscape (char c) { return "%" + HexAlphabet[(c >> 4) & 0xF] + HexAlphabet[c & 0xF]; } internal static string EscapeUserName (string userName) { StringBuilder escaped; int startIndex, index; if ((index = userName.IndexOfAny (ReservedUriCharacters)) == -1) return userName; escaped = new StringBuilder (); startIndex = 0; do { escaped.Append (userName, startIndex, index - startIndex); escaped.Append (HexEscape (userName[index++])); startIndex = index; if (startIndex >= userName.Length) break; index = userName.IndexOfAny (ReservedUriCharacters, startIndex); } while (index != -1); if (index == -1) escaped.Append (userName, startIndex, userName.Length - startIndex); return escaped.ToString (); } string GetSessionIdentifier (string userName) { var uri = engine.Uri; return string.Format (CultureInfo.InvariantCulture, "{0}://{1}@{2}:{3}", uri.Scheme, EscapeUserName (userName), uri.Host, uri.Port); } async Task OnAuthenticatedAsync (string message, bool doAsync, CancellationToken cancellationToken) { await engine.QueryNamespacesAsync (doAsync, cancellationToken).ConfigureAwait (false); await engine.QuerySpecialFoldersAsync (doAsync, cancellationToken).ConfigureAwait (false); OnAuthenticated (message); } async Task AuthenticateAsync (SaslMechanism mechanism, bool doAsync, CancellationToken cancellationToken) { if (mechanism == null) throw new ArgumentNullException (nameof (mechanism)); CheckDisposed (); CheckConnected (); if (engine.State >= ImapEngineState.Authenticated) throw new InvalidOperationException ("The ImapClient is already authenticated."); int capabilitiesVersion = engine.CapabilitiesVersion; var uri = new Uri ("imap://" + engine.Uri.Host); NetworkCredential cred; ImapCommand ic = null; string id; cancellationToken.ThrowIfCancellationRequested (); mechanism.Uri = uri; var command = string.Format ("AUTHENTICATE {0}", mechanism.MechanismName); if ((engine.Capabilities & ImapCapabilities.SaslIR) != 0 && mechanism.SupportsInitialResponse) { var ir = mechanism.Challenge (null); command += " " + ir + "\r\n"; } else { command += "\r\n"; } ic = engine.QueueCommand (cancellationToken, null, command); ic.ContinuationHandler = async (imap, cmd, text, xdoAsync) => { string challenge; if (mechanism.IsAuthenticated) { // The server claims we aren't done authenticating, but our SASL mechanism thinks we are... // Send an empty string to abort the AUTHENTICATE command. challenge = string.Empty; } else { challenge = mechanism.Challenge (text); } var buf = Encoding.ASCII.GetBytes (challenge + "\r\n"); if (xdoAsync) { await imap.Stream.WriteAsync (buf, 0, buf.Length, cmd.CancellationToken).ConfigureAwait (false); await imap.Stream.FlushAsync (cmd.CancellationToken).ConfigureAwait (false); } else { imap.Stream.Write (buf, 0, buf.Length, cmd.CancellationToken); imap.Stream.Flush (cmd.CancellationToken); } }; await engine.RunAsync (ic, doAsync).ConfigureAwait (false); if (ic.Response != ImapCommandResponse.Ok) { EmitAndThrowOnAlert (ic); throw new AuthenticationException (); } engine.State = ImapEngineState.Authenticated; cred = mechanism.Credentials.GetCredential (mechanism.Uri, mechanism.MechanismName); id = GetSessionIdentifier (cred.UserName); if (id != identifier) { engine.FolderCache.Clear (); identifier = id; } // Query the CAPABILITIES again if the server did not include an // untagged CAPABILITIES response to the AUTHENTICATE command. if (engine.CapabilitiesVersion == capabilitiesVersion) await engine.QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); await OnAuthenticatedAsync (ic.ResponseText ?? string.Empty, doAsync, cancellationToken).ConfigureAwait (false); } /// /// Authenticate using the specified SASL mechanism. /// /// /// Authenticates using the specified SASL mechanism. /// For a list of available SASL authentication mechanisms supported by the server, /// check the property after the service has been /// connected. /// /// The SASL mechanism. /// The cancellation token. /// /// is null. /// /// /// The has been disposed. /// /// /// The is not connected. /// /// /// The is already authenticated. /// /// /// The operation was canceled via the cancellation token. /// /// /// Authentication using the supplied credentials has failed. /// /// /// A SASL authentication error occurred. /// /// /// An I/O error occurred. /// /// /// An IMAP command failed. /// /// /// An IMAP protocol error occurred. /// public override void Authenticate (SaslMechanism mechanism, CancellationToken cancellationToken = default (CancellationToken)) { AuthenticateAsync (mechanism, false, cancellationToken).GetAwaiter ().GetResult (); } async Task AuthenticateAsync (Encoding encoding, ICredentials credentials, bool doAsync, CancellationToken cancellationToken) { if (encoding == null) throw new ArgumentNullException (nameof (encoding)); if (credentials == null) throw new ArgumentNullException (nameof (credentials)); CheckDisposed (); CheckConnected (); if (engine.State >= ImapEngineState.Authenticated) throw new InvalidOperationException ("The ImapClient is already authenticated."); int capabilitiesVersion = engine.CapabilitiesVersion; var uri = new Uri ("imap://" + engine.Uri.Host); NetworkCredential cred; ImapCommand ic = null; SaslMechanism sasl; string id; foreach (var authmech in SaslMechanism.AuthMechanismRank) { if (!engine.AuthenticationMechanisms.Contains (authmech)) continue; if ((sasl = SaslMechanism.Create (authmech, uri, encoding, credentials)) == null) continue; cancellationToken.ThrowIfCancellationRequested (); var command = string.Format ("AUTHENTICATE {0}", sasl.MechanismName); if ((engine.Capabilities & ImapCapabilities.SaslIR) != 0 && sasl.SupportsInitialResponse) { var ir = sasl.Challenge (null); command += " " + ir + "\r\n"; } else { command += "\r\n"; } ic = engine.QueueCommand (cancellationToken, null, command); ic.ContinuationHandler = async (imap, cmd, text, xdoAsync) => { string challenge; if (sasl.IsAuthenticated) { // The server claims we aren't done authenticating, but our SASL mechanism thinks we are... // Send an empty string to abort the AUTHENTICATE command. challenge = string.Empty; } else { challenge = sasl.Challenge (text); } var buf = Encoding.ASCII.GetBytes (challenge + "\r\n"); if (xdoAsync) { await imap.Stream.WriteAsync (buf, 0, buf.Length, cmd.CancellationToken).ConfigureAwait (false); await imap.Stream.FlushAsync (cmd.CancellationToken).ConfigureAwait (false); } else { imap.Stream.Write (buf, 0, buf.Length, cmd.CancellationToken); imap.Stream.Flush (cmd.CancellationToken); } }; await engine.RunAsync (ic, doAsync).ConfigureAwait (false); if (ic.Response != ImapCommandResponse.Ok) { EmitAndThrowOnAlert (ic); if (ic.Bye) throw new ImapProtocolException (ic.ResponseText); continue; } engine.State = ImapEngineState.Authenticated; cred = credentials.GetCredential (uri, sasl.MechanismName); id = GetSessionIdentifier (cred.UserName); if (id != identifier) { engine.FolderCache.Clear (); identifier = id; } // Query the CAPABILITIES again if the server did not include an // untagged CAPABILITIES response to the AUTHENTICATE command. if (engine.CapabilitiesVersion == capabilitiesVersion) await engine.QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); await OnAuthenticatedAsync (ic.ResponseText ?? string.Empty, doAsync, cancellationToken).ConfigureAwait (false); return; } if ((Capabilities & ImapCapabilities.LoginDisabled) != 0) { if (ic == null) throw new AuthenticationException ("The LOGIN command is disabled."); throw CreateAuthenticationException (ic); } // fall back to the classic LOGIN command... cred = credentials.GetCredential (uri, "DEFAULT"); ic = engine.QueueCommand (cancellationToken, null, "LOGIN %S %S\r\n", cred.UserName, cred.Password); await engine.RunAsync (ic, doAsync).ConfigureAwait (false); if (ic.Response != ImapCommandResponse.Ok) throw CreateAuthenticationException (ic); engine.State = ImapEngineState.Authenticated; id = GetSessionIdentifier (cred.UserName); if (id != identifier) { engine.FolderCache.Clear (); identifier = id; } // Query the CAPABILITIES again if the server did not include an // untagged CAPABILITIES response to the LOGIN command. if (engine.CapabilitiesVersion == capabilitiesVersion) await engine.QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); await OnAuthenticatedAsync (ic.ResponseText ?? string.Empty, doAsync, cancellationToken).ConfigureAwait (false); } /// /// Authenticate using the supplied credentials. /// /// /// If the IMAP server supports one or more SASL authentication mechanisms, /// then the SASL mechanisms that both the client and server support are tried /// in order of greatest security to weakest security. Once a SASL /// authentication mechanism is found that both client and server support, /// the credentials are used to authenticate. /// If the server does not support SASL or if no common SASL mechanisms /// can be found, then LOGIN command is used as a fallback. /// To prevent the usage of certain authentication mechanisms, /// simply remove them from the hash set /// before calling this method. /// /// The text encoding to use for the user's credentials. /// The user's credentials. /// The cancellation token. /// /// is null. /// -or- /// is null. /// /// /// The has been disposed. /// /// /// The is not connected. /// /// /// The is already authenticated. /// /// /// The operation was canceled via the cancellation token. /// /// /// Authentication using the supplied credentials has failed. /// /// /// A SASL authentication error occurred. /// /// /// An I/O error occurred. /// /// /// An IMAP command failed. /// /// /// An IMAP protocol error occurred. /// public override void Authenticate (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default (CancellationToken)) { AuthenticateAsync (encoding, credentials, false, cancellationToken).GetAwaiter ().GetResult (); } internal void ReplayConnect (string host, Stream replayStream, CancellationToken cancellationToken = default (CancellationToken)) { CheckDisposed (); if (host == null) throw new ArgumentNullException (nameof (host)); if (replayStream == null) throw new ArgumentNullException (nameof (replayStream)); engine.Uri = new Uri ($"imap://{host}:143"); engine.ConnectAsync (new ImapStream (replayStream, ProtocolLogger), false, cancellationToken).GetAwaiter ().GetResult (); engine.TagPrefix = 'A'; secure = false; if (engine.CapabilitiesVersion == 0) engine.QueryCapabilitiesAsync (false, cancellationToken).GetAwaiter ().GetResult (); // Note: we capture the state here in case someone calls Authenticate() from within the Connected event handler. var authenticated = engine.State == ImapEngineState.Authenticated; OnConnected (host, 143, SecureSocketOptions.None); if (authenticated) OnAuthenticatedAsync (string.Empty, false, cancellationToken).GetAwaiter ().GetResult (); } internal async Task ReplayConnectAsync (string host, Stream replayStream, CancellationToken cancellationToken = default (CancellationToken)) { CheckDisposed (); if (host == null) throw new ArgumentNullException (nameof (host)); if (replayStream == null) throw new ArgumentNullException (nameof (replayStream)); engine.Uri = new Uri ($"imap://{host}:143"); await engine.ConnectAsync (new ImapStream (replayStream, ProtocolLogger), true, cancellationToken).ConfigureAwait (false); engine.TagPrefix = 'A'; secure = false; if (engine.CapabilitiesVersion == 0) await engine.QueryCapabilitiesAsync (true, cancellationToken).ConfigureAwait (false); // Note: we capture the state here in case someone calls Authenticate() from within the Connected event handler. var authenticated = engine.State == ImapEngineState.Authenticated; OnConnected (host, 143, SecureSocketOptions.None); if (authenticated) await OnAuthenticatedAsync (string.Empty, true, cancellationToken).ConfigureAwait (false); } internal static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOptions options, out Uri uri, out bool starttls) { switch (options) { default: if (port == 0) port = 143; break; case SecureSocketOptions.Auto: switch (port) { case 0: port = 143; goto default; case 993: options = SecureSocketOptions.SslOnConnect; break; default: options = SecureSocketOptions.StartTlsWhenAvailable; break; } break; case SecureSocketOptions.SslOnConnect: if (port == 0) port = 993; break; } switch (options) { case SecureSocketOptions.StartTlsWhenAvailable: uri = new Uri (string.Format (CultureInfo.InvariantCulture, "imap://{0}:{1}/?starttls=when-available", host, port)); starttls = true; break; case SecureSocketOptions.StartTls: uri = new Uri (string.Format (CultureInfo.InvariantCulture, "imap://{0}:{1}/?starttls=always", host, port)); starttls = true; break; case SecureSocketOptions.SslOnConnect: uri = new Uri (string.Format (CultureInfo.InvariantCulture, "imaps://{0}:{1}", host, port)); starttls = false; break; default: uri = new Uri (string.Format (CultureInfo.InvariantCulture, "imap://{0}:{1}", host, port)); starttls = false; break; } } async Task ConnectAsync (string host, int port, SecureSocketOptions options, bool doAsync, CancellationToken cancellationToken) { if (host == null) throw new ArgumentNullException (nameof (host)); if (host.Length == 0) throw new ArgumentException ("The host name cannot be empty.", nameof (host)); if (port < 0 || port > 65535) throw new ArgumentOutOfRangeException (nameof (port)); CheckDisposed (); if (IsConnected) throw new InvalidOperationException ("The ImapClient is already connected."); Stream stream; bool starttls; Uri uri; ComputeDefaultValues (host, ref port, ref options, out uri, out starttls); var socket = await ConnectSocket (host, port, doAsync, cancellationToken).ConfigureAwait (false); engine.Uri = uri; if (options == SecureSocketOptions.SslOnConnect) { var ssl = new SslStream (new NetworkStream (socket, true), false, ValidateRemoteCertificate); try { if (doAsync) { await ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); } else { #if NETSTANDARD1_3 || NETSTANDARD1_6 ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).GetAwaiter ().GetResult (); #else ssl.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); #endif } } catch (Exception ex) { ssl.Dispose (); throw SslHandshakeException.Create (this, ex, false); } secure = true; stream = ssl; } else { stream = new NetworkStream (socket, true); secure = false; } if (stream.CanTimeout) { stream.WriteTimeout = timeout; stream.ReadTimeout = timeout; } try { ProtocolLogger.LogConnect (uri); } catch { stream.Dispose (); secure = false; throw; } connecting = true; try { await engine.ConnectAsync (new ImapStream (stream, ProtocolLogger), doAsync, cancellationToken).ConfigureAwait (false); } catch { connecting = false; secure = false; throw; } try { // Only query the CAPABILITIES if the greeting didn't include them. if (engine.CapabilitiesVersion == 0) await engine.QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); if (options == SecureSocketOptions.StartTls && (engine.Capabilities & ImapCapabilities.StartTLS) == 0) throw new NotSupportedException ("The IMAP server does not support the STARTTLS extension."); if (starttls && (engine.Capabilities & ImapCapabilities.StartTLS) != 0) { var ic = engine.QueueCommand (cancellationToken, null, "STARTTLS\r\n"); await engine.RunAsync (ic, doAsync).ConfigureAwait (false); if (ic.Response == ImapCommandResponse.Ok) { try { var tls = new SslStream (stream, false, ValidateRemoteCertificate); engine.Stream.Stream = tls; if (doAsync) { await tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); } else { #if NETSTANDARD1_3 || NETSTANDARD1_6 tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).GetAwaiter ().GetResult (); #else tls.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); #endif } } catch (Exception ex) { throw SslHandshakeException.Create (this, ex, true); } secure = true; // Query the CAPABILITIES again if the server did not include an // untagged CAPABILITIES response to the STARTTLS command. if (engine.CapabilitiesVersion == 1) await engine.QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); } else if (options == SecureSocketOptions.StartTls) { throw ImapCommandException.Create ("STARTTLS", ic); } } } catch { secure = false; engine.Disconnect (); throw; } finally { connecting = false; } // Note: we capture the state here in case someone calls Authenticate() from within the Connected event handler. var authenticated = engine.State == ImapEngineState.Authenticated; OnConnected (host, port, options); if (authenticated) await OnAuthenticatedAsync (string.Empty, doAsync, cancellationToken).ConfigureAwait (false); } /// /// Establish a connection to the specified IMAP server. /// /// /// Establishes a connection to the specified IMAP or IMAP/S server. /// If the has a value of 0, then the /// parameter is used to determine the default port to /// connect to. The default port used with /// is 993. All other values will use a default port of 143. /// If the has a value of /// , then the is used /// to determine the default security options. If the has a value /// of 993, then the default options used will be /// . All other values will use /// . /// Once a connection is established, properties such as /// and will be /// populated. /// /// /// /// /// The host name to connect to. /// The port to connect to. If the specified port is 0, then the default port will be used. /// The secure socket options to when connecting. /// The cancellation token. /// /// is null. /// /// /// is not between 0 and 65535. /// /// /// The is a zero-length string. /// /// /// The has been disposed. /// /// /// The is already connected. /// /// /// was set to /// /// and the IMAP server does not support the STARTTLS extension. /// /// /// The operation was canceled via the cancellation token. /// /// /// A socket error occurred trying to connect to the remote host. /// /// /// An I/O error occurred. /// /// /// An IMAP command failed. /// /// /// An IMAP protocol error occurred. /// public override void Connect (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) { ConnectAsync (host, port, options, false, cancellationToken).GetAwaiter ().GetResult (); } async Task ConnectAsync (Stream stream, string host, int port, SecureSocketOptions options, bool doAsync, CancellationToken cancellationToken) { if (stream == null) throw new ArgumentNullException (nameof (stream)); if (host == null) throw new ArgumentNullException (nameof (host)); if (host.Length == 0) throw new ArgumentException ("The host name cannot be empty.", nameof (host)); if (port < 0 || port > 65535) throw new ArgumentOutOfRangeException (nameof (port)); CheckDisposed (); if (IsConnected) throw new InvalidOperationException ("The ImapClient is already connected."); Stream network; bool starttls; Uri uri; ComputeDefaultValues (host, ref port, ref options, out uri, out starttls); engine.Uri = uri; if (options == SecureSocketOptions.SslOnConnect) { var ssl = new SslStream (stream, false, ValidateRemoteCertificate); try { if (doAsync) { await ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); } else { #if NETSTANDARD1_3 || NETSTANDARD1_6 ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).GetAwaiter ().GetResult (); #else ssl.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); #endif } } catch (Exception ex) { ssl.Dispose (); throw SslHandshakeException.Create (this, ex, false); } network = ssl; secure = true; } else { network = stream; secure = false; } if (network.CanTimeout) { network.WriteTimeout = timeout; network.ReadTimeout = timeout; } try { ProtocolLogger.LogConnect (uri); } catch { network.Dispose (); secure = false; throw; } connecting = true; try { await engine.ConnectAsync (new ImapStream (network, ProtocolLogger), doAsync, cancellationToken).ConfigureAwait (false); } catch { connecting = false; throw; } try { // Only query the CAPABILITIES if the greeting didn't include them. if (engine.CapabilitiesVersion == 0) await engine.QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); if (options == SecureSocketOptions.StartTls && (engine.Capabilities & ImapCapabilities.StartTLS) == 0) throw new NotSupportedException ("The IMAP server does not support the STARTTLS extension."); if (starttls && (engine.Capabilities & ImapCapabilities.StartTLS) != 0) { var ic = engine.QueueCommand (cancellationToken, null, "STARTTLS\r\n"); await engine.RunAsync (ic, doAsync).ConfigureAwait (false); if (ic.Response == ImapCommandResponse.Ok) { var tls = new SslStream (network, false, ValidateRemoteCertificate); engine.Stream.Stream = tls; try { if (doAsync) { await tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); } else { #if NETSTANDARD1_3 || NETSTANDARD1_6 tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).GetAwaiter ().GetResult (); #else tls.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); #endif } } catch (Exception ex) { throw SslHandshakeException.Create (this, ex, true); } secure = true; // Query the CAPABILITIES again if the server did not include an // untagged CAPABILITIES response to the STARTTLS command. if (engine.CapabilitiesVersion == 1) await engine.QueryCapabilitiesAsync (doAsync, cancellationToken).ConfigureAwait (false); } else if (options == SecureSocketOptions.StartTls) { throw ImapCommandException.Create ("STARTTLS", ic); } } } catch { secure = false; engine.Disconnect (); throw; } finally { connecting = false; } // Note: we capture the state here in case someone calls Authenticate() from within the Connected event handler. var authenticated = engine.State == ImapEngineState.Authenticated; OnConnected (host, port, options); if (authenticated) await OnAuthenticatedAsync (string.Empty, doAsync, cancellationToken).ConfigureAwait (false); } Task ConnectAsync (Socket socket, string host, int port, SecureSocketOptions options, bool doAsync, CancellationToken cancellationToken) { if (socket == null) throw new ArgumentNullException (nameof (socket)); if (!socket.Connected) throw new ArgumentException ("The socket is not connected.", nameof (socket)); return ConnectAsync (new NetworkStream (socket, true), host, port, options, doAsync, cancellationToken); } /// /// Establish a connection to the specified IMAP or IMAP/S server using the provided socket. /// /// /// Establishes a connection to the specified IMAP or IMAP/S server using /// the provided socket. /// If the has a value of /// , then the is used /// to determine the default security options. If the has a value /// of 993, then the default options used will be /// . All other values will use /// . /// Once a connection is established, properties such as /// and will be /// populated. /// With the exception of using the to determine the /// default to use when the value /// is , the and /// parameters are only used for logging purposes. /// /// The socket to use for the connection. /// The host name to connect to. /// The port to connect to. If the specified port is 0, then the default port will be used. /// The secure socket options to when connecting. /// The cancellation token. /// /// is null. /// -or- /// is null. /// /// /// is not between 0 and 65535. /// /// /// is not connected. /// -or- /// The is a zero-length string. /// /// /// The has been disposed. /// /// /// The is already connected. /// /// /// was set to /// /// and the IMAP server does not support the STARTTLS extension. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// An IMAP command failed. /// /// /// An IMAP protocol error occurred. /// public override void Connect (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) { ConnectAsync (socket, host, port, options, false, cancellationToken).GetAwaiter ().GetResult (); } /// /// Establish a connection to the specified IMAP or IMAP/S server using the provided stream. /// /// /// Establishes a connection to the specified IMAP or IMAP/S server using /// the provided stream. /// If the has a value of /// , then the is used /// to determine the default security options. If the has a value /// of 993, then the default options used will be /// . All other values will use /// . /// Once a connection is established, properties such as /// and will be /// populated. /// With the exception of using the to determine the /// default to use when the value /// is , the and /// parameters are only used for logging purposes. /// /// The stream to use for the connection. /// The host name to connect to. /// The port to connect to. If the specified port is 0, then the default port will be used. /// The secure socket options to when connecting. /// The cancellation token. /// /// is null. /// -or- /// is null. /// /// /// is not between 0 and 65535. /// /// /// The is a zero-length string. /// /// /// The has been disposed. /// /// /// The is already connected. /// /// /// was set to /// /// and the IMAP server does not support the STARTTLS extension. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// An IMAP command failed. /// /// /// An IMAP protocol error occurred. /// public override void Connect (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default (CancellationToken)) { ConnectAsync (stream, host, port, options, false, cancellationToken).GetAwaiter ().GetResult (); } async Task DisconnectAsync (bool quit, bool doAsync, CancellationToken cancellationToken) { CheckDisposed (); if (!engine.IsConnected) return; if (quit) { try { var ic = engine.QueueCommand (cancellationToken, null, "LOGOUT\r\n"); await engine.RunAsync (ic, doAsync).ConfigureAwait (false); } catch (OperationCanceledException) { } catch (ImapProtocolException) { } catch (ImapCommandException) { } catch (IOException) { } } disconnecting = true; engine.Disconnect (); } /// /// Disconnect the service. /// /// /// If is true, a LOGOUT command will be issued in order to disconnect cleanly. /// /// /// /// /// If set to true, a LOGOUT command will be issued in order to disconnect cleanly. /// The cancellation token. /// /// The has been disposed. /// public override void Disconnect (bool quit, CancellationToken cancellationToken = default (CancellationToken)) { DisconnectAsync (quit, false, cancellationToken).GetAwaiter ().GetResult (); } async Task NoOpAsync (bool doAsync, CancellationToken cancellationToken) { CheckDisposed (); CheckConnected (); CheckAuthenticated (); var ic = engine.QueueCommand (cancellationToken, null, "NOOP\r\n"); await engine.RunAsync (ic, doAsync).ConfigureAwait (false); if (ic.Response != ImapCommandResponse.Ok) throw ImapCommandException.Create ("NOOP", ic); } /// /// Ping the IMAP server to keep the connection alive. /// /// /// The NOOP command is typically used to keep the connection with the IMAP server /// alive. When a client goes too long (typically 30 minutes) without sending any commands to the /// IMAP server, the IMAP server will close the connection with the client, forcing the client to /// reconnect before it can send any more commands. /// The NOOP command also provides a great way for a client to check for new /// messages. /// When the IMAP server receives a NOOP command, it will reply to the client with a /// list of pending updates such as EXISTS and RECENT counts on the currently /// selected folder. To receive these notifications, subscribe to the /// and events, /// respectively. /// For more information about the NOOP command, see /// rfc3501. /// /// /// /// /// The cancellation token. /// /// The has been disposed. /// /// /// The is not connected. /// /// /// The is not authenticated. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// The server replied to the NOOP command with a NO or BAD response. /// /// /// The server responded with an unexpected token. /// public override void NoOp (CancellationToken cancellationToken = default (CancellationToken)) { NoOpAsync (false, cancellationToken).GetAwaiter ().GetResult (); } async Task IdleAsync (CancellationToken doneToken, bool doAsync, CancellationToken cancellationToken) { if (!doneToken.CanBeCanceled) throw new ArgumentException ("The doneToken must be cancellable.", nameof (doneToken)); CheckDisposed (); CheckConnected (); CheckAuthenticated (); if ((engine.Capabilities & ImapCapabilities.Idle) == 0) throw new NotSupportedException ("The IMAP server does not support the IDLE extension."); if (engine.State != ImapEngineState.Selected) throw new InvalidOperationException ("An ImapFolder has not been opened."); if (doneToken.IsCancellationRequested) return; using (var context = new ImapIdleContext (engine, doneToken, cancellationToken)) { var ic = engine.QueueCommand (cancellationToken, null, "IDLE\r\n"); ic.ContinuationHandler = context.ContinuationHandler; ic.UserData = context; await engine.RunAsync (ic, doAsync).ConfigureAwait (false); if (ic.Response != ImapCommandResponse.Ok) throw ImapCommandException.Create ("IDLE", ic); } } /// /// Toggle the into the IDLE state. /// /// /// When a client enters the IDLE state, the IMAP server will send /// events to the client as they occur on the selected folder. These events /// may include notifications of new messages arriving, expunge notifications, /// flag changes, etc. /// Due to the nature of the IDLE command, a folder must be selected /// before a client can enter into the IDLE state. This can be done by /// opening a folder using /// /// or any of the other variants. /// While the IDLE command is running, no other commands may be issued until the /// is cancelled. /// It is especially important to cancel the /// before cancelling the when using SSL or TLS due to /// the fact that cannot be polled. /// /// /// /// /// The cancellation token used to return to the non-idle state. /// The cancellation token. /// /// must be cancellable (i.e. cannot be used). /// /// /// The has been disposed. /// /// /// The is not connected. /// /// /// The is not authenticated. /// /// /// A has not been opened. /// /// /// The IMAP server does not support the IDLE extension. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// The server replied to the IDLE command with a NO or BAD response. /// /// /// The server responded with an unexpected token. /// public void Idle (CancellationToken doneToken, CancellationToken cancellationToken = default (CancellationToken)) { IdleAsync (doneToken, false, cancellationToken).GetAwaiter ().GetResult (); } async Task NotifyAsync (bool status, IList eventGroups, bool doAsync, CancellationToken cancellationToken) { if (eventGroups == null) throw new ArgumentNullException (nameof (eventGroups)); if (eventGroups.Count == 0) throw new ArgumentException ("No event groups specified.", nameof (eventGroups)); CheckDisposed (); CheckConnected (); CheckAuthenticated (); if ((engine.Capabilities & ImapCapabilities.Notify) == 0) throw new NotSupportedException ("The IMAP server does not support the NOTIFY extension."); var command = new StringBuilder ("NOTIFY SET"); var notifySelectedNewExpunge = false; var args = new List (); if (status) command.Append (" STATUS"); foreach (var group in eventGroups) { command.Append (" "); group.Format (engine, command, args, ref notifySelectedNewExpunge); } command.Append ("\r\n"); var ic = new ImapCommand (engine, cancellationToken, null, command.ToString (), args.ToArray ()); engine.QueueCommand (ic); await engine.RunAsync (ic, doAsync).ConfigureAwait (false); if (ic.Response != ImapCommandResponse.Ok) throw ImapCommandException.Create ("NOTIFY", ic); engine.NotifySelectedNewExpunge = notifySelectedNewExpunge; } /// /// Request the specified notification events from the IMAP server. /// /// /// The NOTIFY command is used to expand /// which notifications the client wishes to be notified about, including status notifications /// about folders other than the currently selected folder. It can also be used to automatically /// FETCH information about new messages that have arrived in the currently selected folder. /// This, combined with , /// can be used to get instant notifications for changes to any of the specified folders. /// /// true if the server should immediately notify the client of the /// selected folder's status; otherwise, false. /// The specific event groups that the client would like to receive notifications for. /// The cancellation token. /// /// is null. /// /// /// is empty. /// /// /// The has been disposed. /// /// /// The is not connected. /// /// /// The is not authenticated. /// /// /// One or more is invalid. /// /// /// The IMAP server does not support the NOTIFY extension. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// The server replied to the NOTIFY command with a NO or BAD response. /// /// /// The server responded with an unexpected token. /// public void Notify (bool status, IList eventGroups, CancellationToken cancellationToken = default (CancellationToken)) { NotifyAsync (status, eventGroups, false, cancellationToken).GetAwaiter ().GetResult (); } async Task DisableNotifyAsync (bool doAsync, CancellationToken cancellationToken) { CheckDisposed (); CheckConnected (); CheckAuthenticated (); if ((engine.Capabilities & ImapCapabilities.Notify) == 0) throw new NotSupportedException ("The IMAP server does not support the NOTIFY extension."); var ic = new ImapCommand (engine, cancellationToken, null, "NOTIFY NONE\r\n"); engine.QueueCommand (ic); await engine.RunAsync (ic, doAsync).ConfigureAwait (false); if (ic.Response != ImapCommandResponse.Ok) throw ImapCommandException.Create ("NOTIFY", ic); engine.NotifySelectedNewExpunge = false; } /// /// Disable any previously requested notification events from the IMAP server. /// /// /// Disables any notification events requested in a prior call to /// . /// request. /// /// The cancellation token. /// /// The has been disposed. /// /// /// The is not connected. /// /// /// The is not authenticated. /// /// /// The IMAP server does not support the NOTIFY extension. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// The server replied to the NOTIFY command with a NO or BAD response. /// /// /// The server responded with an unexpected token. /// public void DisableNotify (CancellationToken cancellationToken = default (CancellationToken)) { DisableNotifyAsync (false, cancellationToken).GetAwaiter ().GetResult (); } #endregion #region IMailStore implementation /// /// Get the personal namespaces. /// /// /// The personal folder namespaces contain a user's personal mailbox folders. /// /// The personal namespaces. public override FolderNamespaceCollection PersonalNamespaces { get { return engine.PersonalNamespaces; } } /// /// Get the shared namespaces. /// /// /// The shared folder namespaces contain mailbox folders that are shared with the user. /// /// The shared namespaces. public override FolderNamespaceCollection SharedNamespaces { get { return engine.SharedNamespaces; } } /// /// Get the other namespaces. /// /// /// The other folder namespaces contain other mailbox folders. /// /// The other namespaces. public override FolderNamespaceCollection OtherNamespaces { get { return engine.OtherNamespaces; } } /// /// Get whether or not the mail store supports quotas. /// /// /// Gets whether or not the mail store supports quotas. /// /// true if the mail store supports quotas; otherwise, false. public override bool SupportsQuotas { get { return (engine.Capabilities & ImapCapabilities.Quota) != 0; } } /// /// Get the Inbox folder. /// /// /// The Inbox folder is the default folder and always exists on the server. /// This property will only be available after the client has been authenticated. /// /// /// /// /// The Inbox folder. /// /// The has been disposed. /// /// /// The is not connected. /// /// /// The is not authenticated. /// public override IMailFolder Inbox { get { CheckDisposed (); CheckConnected (); CheckAuthenticated (); return engine.Inbox; } } /// /// Get the specified special folder. /// /// /// Not all IMAP servers support special folders. Only IMAP servers /// supporting the or /// extensions may have /// special folders. /// /// The folder if available; otherwise null. /// The type of special folder. /// /// is out of range. /// /// /// The has been disposed. /// /// /// The is not connected. /// /// /// The is not authenticated. /// /// /// The IMAP server does not support the SPECIAL-USE nor XLIST extensions. /// public override IMailFolder GetFolder (SpecialFolder folder) { CheckDisposed (); CheckConnected (); CheckAuthenticated (); if ((Capabilities & (ImapCapabilities.SpecialUse | ImapCapabilities.XList)) == 0) throw new NotSupportedException ("The IMAP server does not support the SPECIAL-USE nor XLIST extensions."); switch (folder) { case SpecialFolder.All: return engine.All; case SpecialFolder.Archive: return engine.Archive; case SpecialFolder.Drafts: return engine.Drafts; case SpecialFolder.Flagged: return engine.Flagged; case SpecialFolder.Important: return engine.Important; case SpecialFolder.Junk: return engine.Junk; case SpecialFolder.Sent: return engine.Sent; case SpecialFolder.Trash: return engine.Trash; default: throw new ArgumentOutOfRangeException (nameof (folder)); } } /// /// Get the folder for the specified namespace. /// /// /// Gets the folder for the specified namespace. /// /// The folder. /// The namespace. /// /// is null. /// /// /// The has been disposed. /// /// /// The is not connected. /// /// /// The is not authenticated. /// /// /// The folder could not be found. /// public override IMailFolder GetFolder (FolderNamespace @namespace) { if (@namespace == null) throw new ArgumentNullException (nameof (@namespace)); CheckDisposed (); CheckConnected (); CheckAuthenticated (); var encodedName = engine.EncodeMailboxName (@namespace.Path); ImapFolder folder; if (engine.GetCachedFolder (encodedName, out folder)) return folder; throw new FolderNotFoundException (@namespace.Path); } async Task> GetFoldersAsync (FolderNamespace @namespace, StatusItems items, bool subscribedOnly, bool doAsync, CancellationToken cancellationToken) { if (@namespace == null) throw new ArgumentNullException (nameof (@namespace)); CheckDisposed (); CheckConnected (); CheckAuthenticated (); var folders = await engine.GetFoldersAsync (@namespace, items, subscribedOnly, doAsync, cancellationToken).ConfigureAwait (false); var list = new IMailFolder[folders.Count]; for (int i = 0; i < list.Length; i++) list[i] = (IMailFolder) folders[i]; return list; } /// /// Get all of the folders within the specified namespace. /// /// /// Gets all of the folders within the specified namespace. /// /// The folders. /// The namespace. /// The status items to pre-populate. /// If set to true, only subscribed folders will be listed. /// The cancellation token. /// /// is null. /// /// /// The has been disposed. /// /// /// The is not connected. /// /// /// The is not authenticated. /// /// /// The operation was canceled via the cancellation token. /// /// /// The namespace folder could not be found. /// /// /// An I/O error occurred. /// /// /// The server replied to the LIST or LSUB command with a NO or BAD response. /// /// /// The server responded with an unexpected token. /// public override IList GetFolders (FolderNamespace @namespace, StatusItems items = StatusItems.None, bool subscribedOnly = false, CancellationToken cancellationToken = default (CancellationToken)) { return GetFoldersAsync (@namespace, items, subscribedOnly, false, cancellationToken).GetAwaiter ().GetResult (); } /// /// Get the folder for the specified path. /// /// /// Gets the folder for the specified path. /// /// The folder. /// The folder path. /// The cancellation token. /// /// is null. /// /// /// The has been disposed. /// /// /// The is not connected. /// /// /// The is not authenticated. /// /// /// The operation was canceled via the cancellation token. /// /// /// The folder could not be found. /// /// /// An I/O error occurred. /// /// /// The server replied to the IDLE command with a NO or BAD response. /// /// /// The server responded with an unexpected token. /// public override IMailFolder GetFolder (string path, CancellationToken cancellationToken = default (CancellationToken)) { if (path == null) throw new ArgumentNullException (nameof (path)); CheckDisposed (); CheckConnected (); CheckAuthenticated (); return engine.GetFolderAsync (path, false, cancellationToken).GetAwaiter ().GetResult (); } async Task GetMetadataAsync (MetadataTag tag, bool doAsync, CancellationToken cancellationToken) { CheckDisposed (); CheckConnected (); CheckAuthenticated (); if ((engine.Capabilities & (ImapCapabilities.Metadata | ImapCapabilities.MetadataServer)) == 0) throw new NotSupportedException ("The IMAP server does not support the METADATA extension."); var ic = new ImapCommand (engine, cancellationToken, null, "GETMETADATA \"\" %S\r\n", tag.Id); ic.RegisterUntaggedHandler ("METADATA", ImapUtils.ParseMetadataAsync); var metadata = new MetadataCollection (); ic.UserData = metadata; engine.QueueCommand (ic); await engine.RunAsync (ic, doAsync).ConfigureAwait (false); if (ic.Response != ImapCommandResponse.Ok) throw ImapCommandException.Create ("GETMETADATA", ic); string value = null; for (int i = 0; i < metadata.Count; i++) { if (metadata[i].EncodedName.Length == 0 && metadata[i].Tag.Id == tag.Id) { value = metadata[i].Value; metadata.RemoveAt (i); break; } } engine.ProcessMetadataChanges (metadata); return value; } /// /// Gets the specified metadata. /// /// /// Gets the specified metadata. /// /// The requested metadata value. /// The metadata tag. /// The cancellation token. /// /// The has been disposed. /// /// /// The is not connected. /// /// /// The is not authenticated. /// /// /// The IMAP server does not support the METADATA or METADATA-SERVER extension. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// The server's response contained unexpected tokens. /// /// /// The server replied with a NO or BAD response. /// public override string GetMetadata (MetadataTag tag, CancellationToken cancellationToken = default (CancellationToken)) { return GetMetadataAsync (tag, false, cancellationToken).GetAwaiter ().GetResult (); } async Task GetMetadataAsync (MetadataOptions options, IEnumerable tags, bool doAsync, CancellationToken cancellationToken) { if (options == null) throw new ArgumentNullException (nameof (options)); if (tags == null) throw new ArgumentNullException (nameof (tags)); CheckDisposed (); CheckConnected (); CheckAuthenticated (); if ((engine.Capabilities & (ImapCapabilities.Metadata | ImapCapabilities.MetadataServer)) == 0) throw new NotSupportedException ("The IMAP server does not support the METADATA or METADATA-SERVER extension."); var command = new StringBuilder ("GETMETADATA \"\""); var args = new List (); bool hasOptions = false; if (options.MaxSize.HasValue || options.Depth != 0) { command.Append (" ("); if (options.MaxSize.HasValue) command.AppendFormat ("MAXSIZE {0} ", options.MaxSize.Value); if (options.Depth > 0) command.AppendFormat ("DEPTH {0} ", options.Depth == int.MaxValue ? "infinity" : "1"); command[command.Length - 1] = ')'; command.Append (' '); hasOptions = true; } int startIndex = command.Length; foreach (var tag in tags) { command.Append (" %S"); args.Add (tag.Id); } if (hasOptions) { command[startIndex] = '('; command.Append (')'); } command.Append ("\r\n"); if (args.Count == 0) return new MetadataCollection (); var ic = new ImapCommand (engine, cancellationToken, null, command.ToString (), args.ToArray ()); ic.RegisterUntaggedHandler ("METADATA", ImapUtils.ParseMetadataAsync); ic.UserData = new MetadataCollection (); options.LongEntries = 0; engine.QueueCommand (ic); await engine.RunAsync (ic, doAsync).ConfigureAwait (false); if (ic.Response != ImapCommandResponse.Ok) throw ImapCommandException.Create ("GETMETADATA", ic); if (ic.RespCodes.Count > 0 && ic.RespCodes[ic.RespCodes.Count - 1].Type == ImapResponseCodeType.Metadata) { var metadata = (MetadataResponseCode) ic.RespCodes[ic.RespCodes.Count - 1]; if (metadata.SubType == MetadataResponseCodeSubType.LongEntries) options.LongEntries = metadata.Value; } return engine.FilterMetadata ((MetadataCollection) ic.UserData, string.Empty); } /// /// Gets the specified metadata. /// /// /// Gets the specified metadata. /// /// The requested metadata. /// The metadata options. /// The metadata tags. /// The cancellation token. /// /// is null. /// -or- /// is null. /// /// /// The has been disposed. /// /// /// The is not connected. /// /// /// The is not authenticated. /// /// /// The IMAP server does not support the METADATA or METADATA-SERVER extension. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// The server's response contained unexpected tokens. /// /// /// The server replied with a NO or BAD response. /// public override MetadataCollection GetMetadata (MetadataOptions options, IEnumerable tags, CancellationToken cancellationToken = default (CancellationToken)) { return GetMetadataAsync (options, tags, false, cancellationToken).GetAwaiter ().GetResult (); } async Task SetMetadataAsync (MetadataCollection metadata, bool doAsync, CancellationToken cancellationToken) { if (metadata == null) throw new ArgumentNullException (nameof (metadata)); CheckDisposed (); CheckConnected (); CheckAuthenticated (); if ((engine.Capabilities & (ImapCapabilities.Metadata | ImapCapabilities.MetadataServer)) == 0) throw new NotSupportedException ("The IMAP server does not support the METADATA or METADATA-SERVER extension."); if (metadata.Count == 0) return; var command = new StringBuilder ("SETMETADATA \"\" ("); var args = new List (); for (int i = 0; i < metadata.Count; i++) { if (i > 0) command.Append (' '); if (metadata[i].Value != null) { command.Append ("%S %S"); args.Add (metadata[i].Tag.Id); args.Add (metadata[i].Value); } else { command.Append ("%S NIL"); args.Add (metadata[i].Tag.Id); } } command.Append (")\r\n"); var ic = new ImapCommand (engine, cancellationToken, null, command.ToString (), args.ToArray ()); engine.QueueCommand (ic); await engine.RunAsync (ic, doAsync).ConfigureAwait (false); if (ic.Response != ImapCommandResponse.Ok) throw ImapCommandException.Create ("SETMETADATA", ic); } /// /// Sets the specified metadata. /// /// /// Sets the specified metadata. /// /// The metadata. /// The cancellation token. /// /// is null. /// /// /// The has been disposed. /// /// /// The is not connected. /// /// /// The is not authenticated. /// /// /// The IMAP server does not support the METADATA or METADATA-SERVER extension. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// The server's response contained unexpected tokens. /// /// /// The server replied with a NO or BAD response. /// public override void SetMetadata (MetadataCollection metadata, CancellationToken cancellationToken = default (CancellationToken)) { SetMetadataAsync (metadata, false, cancellationToken).GetAwaiter ().GetResult (); } #endregion void OnEngineMetadataChanged (object sender, MetadataChangedEventArgs e) { OnMetadataChanged (e.Metadata); } void OnEngineFolderCreated (object sender, FolderCreatedEventArgs e) { OnFolderCreated (e.Folder); } void OnEngineAlert (object sender, AlertEventArgs e) { OnAlert (e.Message); } void OnEngineDisconnected (object sender, EventArgs e) { if (connecting) return; var requested = disconnecting; var uri = engine.Uri; disconnecting = false; secure = false; OnDisconnected (uri.Host, uri.Port, GetSecureSocketOptions (uri), requested); } /// /// Releases the unmanaged resources used by the and /// optionally releases the managed resources. /// /// /// Releases the unmanaged resources used by the and /// optionally releases the managed resources. /// /// true to release both managed and unmanaged resources; /// false to release only the unmanaged resources. protected override void Dispose (bool disposing) { if (disposing && !disposed) { engine.MetadataChanged -= OnEngineMetadataChanged; engine.FolderCreated -= OnEngineFolderCreated; engine.Disconnected -= OnEngineDisconnected; engine.Alert -= OnEngineAlert; engine.Dispose (); disposed = true; } base.Dispose (disposing); } } }