From 360c070c13e3280767670afdded16080651ffe59 Mon Sep 17 00:00:00 2001 From: Jeffrey Stedfast Date: Sat, 19 Aug 2023 14:02:42 -0400 Subject: [PATCH] Refactor ImapClient.Connect/ConnectAsync methods to split out sync/async logic Related to issue #1335 --- MailKit/Net/Imap/AsyncImapClient.cs | 365 ++++++++++++++- MailKit/Net/Imap/ImapClient.cs | 635 ++++++++++++-------------- MailKit/Net/Imap/ImapEngine.cs | 627 ++++++++++++++++++------- MailKit/Net/Imap/ImapFolder.cs | 2 +- MailKit/Net/Imap/ImapUtils.cs | 2 +- UnitTests/Net/Imap/ImapEngineTests.cs | 68 ++- 6 files changed, 1161 insertions(+), 538 deletions(-) diff --git a/MailKit/Net/Imap/AsyncImapClient.cs b/MailKit/Net/Imap/AsyncImapClient.cs index 115072afbe..20c43f2909 100644 --- a/MailKit/Net/Imap/AsyncImapClient.cs +++ b/MailKit/Net/Imap/AsyncImapClient.cs @@ -237,6 +237,13 @@ public async Task IdentifyAsync (ImapImplementation clientIm return ProcessIdentifyResponse (ic); } + async Task OnAuthenticatedAsync (string message, CancellationToken cancellationToken) + { + await engine.QueryNamespacesAsync (cancellationToken).ConfigureAwait (false); + await engine.QuerySpecialFoldersAsync (cancellationToken).ConfigureAwait (false); + OnAuthenticated (message); + } + /// /// Asynchronously authenticate using the specified SASL mechanism. /// @@ -279,9 +286,49 @@ public async Task IdentifyAsync (ImapImplementation clientIm /// /// An IMAP protocol error occurred. /// - public override Task AuthenticateAsync (SaslMechanism mechanism, CancellationToken cancellationToken = default) + public override async Task AuthenticateAsync (SaslMechanism mechanism, CancellationToken cancellationToken = default) { - return AuthenticateAsync (mechanism, true, cancellationToken); + CheckCanAuthenticate (mechanism, cancellationToken); + + int capabilitiesVersion = engine.CapabilitiesVersion; + ImapCommand ic = null; + + ConfigureSaslMechanism (mechanism); + + var command = string.Format ("AUTHENTICATE {0}", mechanism.MechanismName); + + if ((engine.Capabilities & ImapCapabilities.SaslIR) != 0 && mechanism.SupportsInitialResponse) { + string ir = await mechanism.ChallengeAsync (null, cancellationToken).ConfigureAwait (false); + command += " " + ir + "\r\n"; + } else { + command += "\r\n"; + } + + ic = engine.QueueCommand (cancellationToken, null, command); + ic.ContinuationHandler = async (imap, cmd, text, xdoAsync) => { + string challenge = await mechanism.ChallengeAsync (text, cmd.CancellationToken).ConfigureAwait (false); + var buf = Encoding.ASCII.GetBytes (challenge + "\r\n"); + + await imap.Stream.WriteAsync (buf, 0, buf.Length, cmd.CancellationToken).ConfigureAwait (false); + await imap.Stream.FlushAsync (cmd.CancellationToken).ConfigureAwait (false); + }; + + detector.IsAuthenticating = true; + + try { + await engine.RunAsync (ic).ConfigureAwait (false); + } finally { + detector.IsAuthenticating = false; + } + + ProcessAuthenticateResponse (ic, mechanism); + + // 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 (cancellationToken).ConfigureAwait (false); + + await OnAuthenticatedAsync (ic.ResponseText ?? string.Empty, cancellationToken).ConfigureAwait (false); } /// @@ -336,9 +383,120 @@ public override Task AuthenticateAsync (SaslMechanism mechanism, CancellationTok /// /// An IMAP protocol error occurred. /// - public override Task AuthenticateAsync (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default) + public override async Task AuthenticateAsync (Encoding encoding, ICredentials credentials, CancellationToken cancellationToken = default) + { + CheckCanAuthenticate (encoding, credentials); + + 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.Rank (engine.AuthenticationMechanisms)) { + cred = credentials.GetCredential (uri, authmech); + + if ((sasl = SaslMechanism.Create (authmech, encoding, cred)) == null) + continue; + + ConfigureSaslMechanism (sasl, uri); + + cancellationToken.ThrowIfCancellationRequested (); + + var command = string.Format ("AUTHENTICATE {0}", sasl.MechanismName); + + if ((engine.Capabilities & ImapCapabilities.SaslIR) != 0 && sasl.SupportsInitialResponse) { + string ir = await sasl.ChallengeAsync (null, cancellationToken).ConfigureAwait (false); + + command += " " + ir + "\r\n"; + } else { + command += "\r\n"; + } + + ic = engine.QueueCommand (cancellationToken, null, command); + ic.ContinuationHandler = async (imap, cmd, text, xdoAsync) => { + string challenge = await sasl.ChallengeAsync (text, cmd.CancellationToken).ConfigureAwait (false); + var buf = Encoding.ASCII.GetBytes (challenge + "\r\n"); + + await imap.Stream.WriteAsync (buf, 0, buf.Length, cmd.CancellationToken).ConfigureAwait (false); + await imap.Stream.FlushAsync (cmd.CancellationToken).ConfigureAwait (false); + }; + + detector.IsAuthenticating = true; + + try { + await engine.RunAsync (ic).ConfigureAwait (false); + } finally { + detector.IsAuthenticating = 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 (cancellationToken).ConfigureAwait (false); + + await OnAuthenticatedAsync (ic.ResponseText ?? string.Empty, cancellationToken).ConfigureAwait (false); + return; + } + + CheckCanLogin (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); + + detector.IsAuthenticating = true; + + try { + await engine.RunAsync (ic).ConfigureAwait (false); + } finally { + detector.IsAuthenticating = 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 (cancellationToken).ConfigureAwait (false); + + await OnAuthenticatedAsync (ic.ResponseText ?? string.Empty, cancellationToken).ConfigureAwait (false); + } + + async Task SslHandshakeAsync (SslStream ssl, string host, CancellationToken cancellationToken) { - return AuthenticateAsync (encoding, credentials, true, cancellationToken); +#if NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + await ssl.AuthenticateAsClientAsync (GetSslClientAuthenticationOptions (host, ValidateRemoteCertificate), cancellationToken).ConfigureAwait (false); +#else + await ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); +#endif } /// @@ -406,9 +564,101 @@ public override Task AuthenticateAsync (Encoding encoding, ICredentials credenti /// /// An IMAP protocol error occurred. /// - public override Task ConnectAsync (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) + public override async Task ConnectAsync (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) { - return ConnectAsync (host, port, options, true, cancellationToken); + CheckCanConnect (host, port); + + ComputeDefaultValues (host, ref port, ref options, out var uri, out var starttls); + + var stream = await ConnectNetworkAsync (host, port, cancellationToken).ConfigureAwait (false); + stream.WriteTimeout = timeout; + stream.ReadTimeout = timeout; + + engine.Uri = uri; + + if (options == SecureSocketOptions.SslOnConnect) { + var ssl = new SslStream (stream, false, ValidateRemoteCertificate); + + try { + await SslHandshakeAsync (ssl, host, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + ssl.Dispose (); + + throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "IMAP", host, port, 993, 143); + } + + secure = true; + stream = ssl; + } else { + secure = false; + } + + try { + ProtocolLogger.LogConnect (uri); + } catch { + stream.Dispose (); + secure = false; + throw; + } + + connecting = true; + + try { + await engine.ConnectAsync (new ImapStream (stream, ProtocolLogger), 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 (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).ConfigureAwait (false); + + if (ic.Response == ImapCommandResponse.Ok) { + try { + var tls = new SslStream (stream, false, ValidateRemoteCertificate); + engine.Stream.Stream = tls; + + await SslHandshakeAsync (tls, host, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + throw SslHandshakeException.Create (ref sslValidationInfo, ex, true, "IMAP", host, port, 993, 143); + } + + 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 (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, cancellationToken).ConfigureAwait (false); } /// @@ -478,7 +728,9 @@ public override Task ConnectAsync (string host, int port = 0, SecureSocketOption /// public override Task ConnectAsync (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) { - return ConnectAsync (socket, host, port, options, true, cancellationToken); + CheckCanConnect (socket, host, port); + + return ConnectAsync (new NetworkStream (socket, true), host, port, options, cancellationToken); } /// @@ -544,9 +796,104 @@ public override Task ConnectAsync (Socket socket, string host, int port = 0, Sec /// /// An IMAP protocol error occurred. /// - public override Task ConnectAsync (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) + public override async Task ConnectAsync (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) { - return ConnectAsync (stream, host, port, options, true, cancellationToken); + CheckCanConnect (stream, host, port); + + Stream network; + + ComputeDefaultValues (host, ref port, ref options, out var uri, out var starttls); + + engine.Uri = uri; + + if (options == SecureSocketOptions.SslOnConnect) { + var ssl = new SslStream (stream, false, ValidateRemoteCertificate); + + try { + await SslHandshakeAsync (ssl, host, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + ssl.Dispose (); + + throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "IMAP", host, port, 993, 143); + } + + 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), 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 (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).ConfigureAwait (false); + + if (ic.Response == ImapCommandResponse.Ok) { + var tls = new SslStream (network, false, ValidateRemoteCertificate); + engine.Stream.Stream = tls; + + try { + await SslHandshakeAsync (tls, host, cancellationToken).ConfigureAwait (false); + } catch (Exception ex) { + throw SslHandshakeException.Create (ref sslValidationInfo, ex, true, "IMAP", host, port, 993, 143); + } + + 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 (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, cancellationToken).ConfigureAwait (false); } /// diff --git a/MailKit/Net/Imap/ImapClient.cs b/MailKit/Net/Imap/ImapClient.cs index 233e98dcf7..1a680b7fe3 100644 --- a/MailKit/Net/Imap/ImapClient.cs +++ b/MailKit/Net/Imap/ImapClient.cs @@ -1008,14 +1008,14 @@ string GetSessionIdentifier (string userName) return builder.ToString (); } - async Task OnAuthenticatedAsync (string message, bool doAsync, CancellationToken cancellationToken) + void OnAuthenticated (string message, CancellationToken cancellationToken) { - await engine.QueryNamespacesAsync (doAsync, cancellationToken).ConfigureAwait (false); - await engine.QuerySpecialFoldersAsync (doAsync, cancellationToken).ConfigureAwait (false); + engine.QueryNamespaces (cancellationToken); + engine.QuerySpecialFolders (cancellationToken); OnAuthenticated (message); } - async Task AuthenticateAsync (SaslMechanism mechanism, bool doAsync, CancellationToken cancellationToken) + void CheckCanAuthenticate (SaslMechanism mechanism, CancellationToken cancellationToken) { if (mechanism == null) throw new ArgumentNullException (nameof (mechanism)); @@ -1026,59 +1026,24 @@ async Task AuthenticateAsync (SaslMechanism mechanism, bool doAsync, Cancellatio 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); - ImapCommand ic = null; - string id; - cancellationToken.ThrowIfCancellationRequested (); + } + void ConfigureSaslMechanism (SaslMechanism mechanism, Uri uri) + { mechanism.ChannelBindingContext = engine.Stream.Stream as IChannelBindingContext; mechanism.Uri = uri; + } - var command = string.Format ("AUTHENTICATE {0}", mechanism.MechanismName); - - if ((engine.Capabilities & ImapCapabilities.SaslIR) != 0 && mechanism.SupportsInitialResponse) { - string ir; - - if (doAsync) - ir = await mechanism.ChallengeAsync (null, cancellationToken).ConfigureAwait (false); - else - ir = mechanism.Challenge (null, cancellationToken); - - command += " " + ir + "\r\n"; - } else { - command += "\r\n"; - } - - ic = engine.QueueCommand (cancellationToken, null, command); - ic.ContinuationHandler = async (imap, cmd, text, xdoAsync) => { - string challenge; - - if (xdoAsync) - challenge = await mechanism.ChallengeAsync (text, cmd.CancellationToken).ConfigureAwait (false); - else - challenge = mechanism.Challenge (text, cmd.CancellationToken); - - 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); - } - }; - - detector.IsAuthenticating = true; + void ConfigureSaslMechanism (SaslMechanism mechanism) + { + var uri = new Uri ("imap://" + engine.Uri.Host); - try { - await engine.RunAsync (ic, doAsync).ConfigureAwait (false); - } finally { - detector.IsAuthenticating = false; - } + ConfigureSaslMechanism (mechanism, uri); + } + void ProcessAuthenticateResponse (ImapCommand ic, SaslMechanism mechanism) + { if (ic.Response != ImapCommandResponse.Ok) { EmitAndThrowOnAlert (ic); @@ -1087,18 +1052,11 @@ async Task AuthenticateAsync (SaslMechanism mechanism, bool doAsync, Cancellatio engine.State = ImapEngineState.Authenticated; - id = GetSessionIdentifier (mechanism.Credentials.UserName); + var id = GetSessionIdentifier (mechanism.Credentials.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); } /// @@ -1144,10 +1102,53 @@ async Task AuthenticateAsync (SaslMechanism mechanism, bool doAsync, Cancellatio /// public override void Authenticate (SaslMechanism mechanism, CancellationToken cancellationToken = default) { - AuthenticateAsync (mechanism, false, cancellationToken).GetAwaiter ().GetResult (); + CheckCanAuthenticate (mechanism, cancellationToken); + + int capabilitiesVersion = engine.CapabilitiesVersion; + ImapCommand ic = null; + + ConfigureSaslMechanism (mechanism); + + var command = string.Format ("AUTHENTICATE {0}", mechanism.MechanismName); + + if ((engine.Capabilities & ImapCapabilities.SaslIR) != 0 && mechanism.SupportsInitialResponse) { + string ir = mechanism.Challenge (null, cancellationToken); + + command += " " + ir + "\r\n"; + } else { + command += "\r\n"; + } + + ic = engine.QueueCommand (cancellationToken, null, command); + ic.ContinuationHandler = (imap, cmd, text, xdoAsync) => { + string challenge = mechanism.Challenge (text, cmd.CancellationToken); + var buf = Encoding.ASCII.GetBytes (challenge + "\r\n"); + + imap.Stream.Write (buf, 0, buf.Length, cmd.CancellationToken); + imap.Stream.Flush (cmd.CancellationToken); + + return Task.CompletedTask; + }; + + detector.IsAuthenticating = true; + + try { + engine.Run (ic); + } finally { + detector.IsAuthenticating = false; + } + + ProcessAuthenticateResponse (ic, mechanism); + + // Query the CAPABILITIES again if the server did not include an + // untagged CAPABILITIES response to the AUTHENTICATE command. + if (engine.CapabilitiesVersion == capabilitiesVersion) + engine.QueryCapabilities (cancellationToken); + + OnAuthenticated (ic.ResponseText ?? string.Empty, cancellationToken); } - async Task AuthenticateAsync (Encoding encoding, ICredentials credentials, bool doAsync, CancellationToken cancellationToken) + void CheckCanAuthenticate (Encoding encoding, ICredentials credentials) { if (encoding == null) throw new ArgumentNullException (nameof (encoding)); @@ -1160,6 +1161,72 @@ async Task AuthenticateAsync (Encoding encoding, ICredentials credentials, bool if (engine.State >= ImapEngineState.Authenticated) throw new InvalidOperationException ("The ImapClient is already authenticated."); + } + + void CheckCanLogin (ImapCommand ic) + { + if ((Capabilities & ImapCapabilities.LoginDisabled) != 0) { + if (ic == null) + throw new AuthenticationException ("The LOGIN command is disabled."); + + throw CreateAuthenticationException (ic); + } + } + + /// + /// Authenticate using the supplied credentials. + /// + /// + /// Authenticates 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 (not including + /// any OAUTH mechanisms) 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) + { + CheckCanAuthenticate (encoding, credentials); int capabilitiesVersion = engine.CapabilitiesVersion; var uri = new Uri ("imap://" + engine.Uri.Host); @@ -1174,20 +1241,14 @@ async Task AuthenticateAsync (Encoding encoding, ICredentials credentials, bool if ((sasl = SaslMechanism.Create (authmech, encoding, cred)) == null) continue; - sasl.ChannelBindingContext = engine.Stream.Stream as IChannelBindingContext; - sasl.Uri = uri; + ConfigureSaslMechanism (sasl, uri); cancellationToken.ThrowIfCancellationRequested (); var command = string.Format ("AUTHENTICATE {0}", sasl.MechanismName); if ((engine.Capabilities & ImapCapabilities.SaslIR) != 0 && sasl.SupportsInitialResponse) { - string ir; - - if (doAsync) - ir = await sasl.ChallengeAsync (null, cancellationToken).ConfigureAwait (false); - else - ir = sasl.Challenge (null, cancellationToken); + string ir = sasl.Challenge (null, cancellationToken); command += " " + ir + "\r\n"; } else { @@ -1195,29 +1256,21 @@ async Task AuthenticateAsync (Encoding encoding, ICredentials credentials, bool } ic = engine.QueueCommand (cancellationToken, null, command); - ic.ContinuationHandler = async (imap, cmd, text, xdoAsync) => { - string challenge; - - if (xdoAsync) - challenge = await sasl.ChallengeAsync (text, cmd.CancellationToken).ConfigureAwait (false); - else - challenge = sasl.Challenge (text, cmd.CancellationToken); + ic.ContinuationHandler = (imap, cmd, text, xdoAsync) => { + string challenge = sasl.Challenge (text, cmd.CancellationToken); 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); - } + imap.Stream.Write (buf, 0, buf.Length, cmd.CancellationToken); + imap.Stream.Flush (cmd.CancellationToken); + + return Task.CompletedTask; }; detector.IsAuthenticating = true; try { - await engine.RunAsync (ic, doAsync).ConfigureAwait (false); + engine.Run (ic); } finally { detector.IsAuthenticating = false; } @@ -1241,18 +1294,13 @@ async Task AuthenticateAsync (Encoding encoding, ICredentials credentials, bool // 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); + engine.QueryCapabilities (cancellationToken); - await OnAuthenticatedAsync (ic.ResponseText ?? string.Empty, doAsync, cancellationToken).ConfigureAwait (false); + OnAuthenticated (ic.ResponseText ?? string.Empty, cancellationToken); return; } - if ((Capabilities & ImapCapabilities.LoginDisabled) != 0) { - if (ic == null) - throw new AuthenticationException ("The LOGIN command is disabled."); - - throw CreateAuthenticationException (ic); - } + CheckCanLogin (ic); // fall back to the classic LOGIN command... cred = credentials.GetCredential (uri, "DEFAULT"); @@ -1262,7 +1310,7 @@ async Task AuthenticateAsync (Encoding encoding, ICredentials credentials, bool detector.IsAuthenticating = true; try { - await engine.RunAsync (ic, doAsync).ConfigureAwait (false); + engine.Run (ic); } finally { detector.IsAuthenticating = false; } @@ -1281,65 +1329,9 @@ async Task AuthenticateAsync (Encoding encoding, ICredentials credentials, bool // 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); + engine.QueryCapabilities (cancellationToken); - await OnAuthenticatedAsync (ic.ResponseText ?? string.Empty, doAsync, cancellationToken).ConfigureAwait (false); - } - - /// - /// Authenticate using the supplied credentials. - /// - /// - /// Authenticates 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 (not including - /// any OAUTH mechanisms) 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) - { - AuthenticateAsync (encoding, credentials, false, cancellationToken).GetAwaiter ().GetResult (); + OnAuthenticated (ic.ResponseText ?? string.Empty, cancellationToken); } internal void ReplayConnect (string host, Stream replayStream, CancellationToken cancellationToken = default) @@ -1353,12 +1345,12 @@ internal void ReplayConnect (string host, Stream replayStream, CancellationToken throw new ArgumentNullException (nameof (replayStream)); engine.Uri = new Uri ($"imap://{host}:143"); - engine.ConnectAsync (new ImapStream (replayStream, ProtocolLogger), false, cancellationToken).GetAwaiter ().GetResult (); + engine.Connect (new ImapStream (replayStream, ProtocolLogger), cancellationToken); engine.TagPrefix = 'A'; secure = false; if (engine.CapabilitiesVersion == 0) - engine.QueryCapabilitiesAsync (false, cancellationToken).GetAwaiter ().GetResult (); + engine.QueryCapabilities (cancellationToken); // Note: we capture the state here in case someone calls Authenticate() from within the Connected event handler. var authenticated = engine.State == ImapEngineState.Authenticated; @@ -1366,7 +1358,7 @@ internal void ReplayConnect (string host, Stream replayStream, CancellationToken OnConnected (host, 143, SecureSocketOptions.None); if (authenticated) - OnAuthenticatedAsync (string.Empty, false, cancellationToken).GetAwaiter ().GetResult (); + OnAuthenticated (string.Empty, cancellationToken); } internal async Task ReplayConnectAsync (string host, Stream replayStream, CancellationToken cancellationToken = default) @@ -1380,12 +1372,12 @@ internal async Task ReplayConnectAsync (string host, Stream replayStream, Cancel throw new ArgumentNullException (nameof (replayStream)); engine.Uri = new Uri ($"imap://{host}:143"); - await engine.ConnectAsync (new ImapStream (replayStream, ProtocolLogger), true, cancellationToken).ConfigureAwait (false); + await engine.ConnectAsync (new ImapStream (replayStream, ProtocolLogger), cancellationToken).ConfigureAwait (false); engine.TagPrefix = 'A'; secure = false; if (engine.CapabilitiesVersion == 0) - await engine.QueryCapabilitiesAsync (true, cancellationToken).ConfigureAwait (false); + await engine.QueryCapabilitiesAsync (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; @@ -1393,7 +1385,7 @@ internal async Task ReplayConnectAsync (string host, Stream replayStream, Cancel OnConnected (host, 143, SecureSocketOptions.None); if (authenticated) - await OnAuthenticatedAsync (string.Empty, true, cancellationToken).ConfigureAwait (false); + await OnAuthenticatedAsync (string.Empty, cancellationToken).ConfigureAwait (false); } internal static void ComputeDefaultValues (string host, ref int port, ref SecureSocketOptions options, out Uri uri, out bool starttls) @@ -1439,7 +1431,7 @@ internal static void ComputeDefaultValues (string host, ref int port, ref Secure } } - async Task ConnectAsync (string host, int port, SecureSocketOptions options, bool doAsync, CancellationToken cancellationToken) + void CheckCanConnect (string host, int port) { if (host == null) throw new ArgumentNullException (nameof (host)); @@ -1454,126 +1446,15 @@ async Task ConnectAsync (string host, int port, SecureSocketOptions options, boo if (IsConnected) throw new InvalidOperationException ("The ImapClient is already connected."); + } - ComputeDefaultValues (host, ref port, ref options, out var uri, out var starttls); - - var stream = await ConnectNetwork (host, port, doAsync, cancellationToken).ConfigureAwait (false); - stream.WriteTimeout = timeout; - stream.ReadTimeout = timeout; - - engine.Uri = uri; - - if (options == SecureSocketOptions.SslOnConnect) { - var ssl = new SslStream (stream, false, ValidateRemoteCertificate); - - try { - if (doAsync) { -#if NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER - await ssl.AuthenticateAsClientAsync (GetSslClientAuthenticationOptions (host, ValidateRemoteCertificate), cancellationToken).ConfigureAwait (false); -#else - await ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); -#endif - } else { -#if NETSTANDARD1_3 || NETSTANDARD1_6 - ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).GetAwaiter ().GetResult (); -#elif NET5_0_OR_GREATER - ssl.AuthenticateAsClient (GetSslClientAuthenticationOptions (host, ValidateRemoteCertificate)); -#else - ssl.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); -#endif - } - } catch (Exception ex) { - ssl.Dispose (); - - throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "IMAP", host, port, 993, 143); - } - - secure = true; - stream = ssl; - } else { - secure = false; - } - - 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) { -#if NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER - await tls.AuthenticateAsClientAsync (GetSslClientAuthenticationOptions (host, ValidateRemoteCertificate), cancellationToken).ConfigureAwait (false); -#else - await tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); -#endif - } else { -#if NETSTANDARD1_3 || NETSTANDARD1_6 - tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).GetAwaiter ().GetResult (); -#elif NET5_0_OR_GREATER - tls.AuthenticateAsClient (GetSslClientAuthenticationOptions (host, ValidateRemoteCertificate)); + void SslHandshake (SslStream ssl, string host, CancellationToken cancellationToken) + { +#if NET5_0_OR_GREATER + ssl.AuthenticateAsClient (GetSslClientAuthenticationOptions (host, ValidateRemoteCertificate)); #else - tls.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); + ssl.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); #endif - } - } catch (Exception ex) { - throw SslHandshakeException.Create (ref sslValidationInfo, ex, true, "IMAP", host, port, 993, 143); - } - - 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); } /// @@ -1639,75 +1520,37 @@ async Task ConnectAsync (string host, int port, SecureSocketOptions options, boo /// public override void Connect (string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) { - 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; + CheckCanConnect (host, port); ComputeDefaultValues (host, ref port, ref options, out var uri, out var starttls); + var stream = ConnectNetwork (host, port, cancellationToken); + stream.WriteTimeout = timeout; + stream.ReadTimeout = timeout; + engine.Uri = uri; if (options == SecureSocketOptions.SslOnConnect) { var ssl = new SslStream (stream, false, ValidateRemoteCertificate); try { - if (doAsync) { -#if NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER - await ssl.AuthenticateAsClientAsync (GetSslClientAuthenticationOptions (host, ValidateRemoteCertificate), cancellationToken).ConfigureAwait (false); -#else - await ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); -#endif - } else { -#if NETSTANDARD1_3 || NETSTANDARD1_6 - ssl.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).GetAwaiter ().GetResult (); -#elif NET5_0_OR_GREATER - ssl.AuthenticateAsClient (GetSslClientAuthenticationOptions (host, ValidateRemoteCertificate)); -#else - ssl.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); -#endif - } + SslHandshake (ssl, host, cancellationToken); } catch (Exception ex) { ssl.Dispose (); throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "IMAP", host, port, 993, 143); } - network = ssl; secure = true; + stream = ssl; } else { - network = stream; secure = false; } - if (network.CanTimeout) { - network.WriteTimeout = timeout; - network.ReadTimeout = timeout; - } - try { ProtocolLogger.LogConnect (uri); } catch { - network.Dispose (); + stream.Dispose (); secure = false; throw; } @@ -1715,45 +1558,32 @@ async Task ConnectAsync (Stream stream, string host, int port, SecureSocketOptio connecting = true; try { - await engine.ConnectAsync (new ImapStream (network, ProtocolLogger), doAsync, cancellationToken).ConfigureAwait (false); + engine.Connect (new ImapStream (stream, ProtocolLogger), cancellationToken); } 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); - + engine.QueryCapabilities (cancellationToken); + 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); + engine.Run (ic); if (ic.Response == ImapCommandResponse.Ok) { - var tls = new SslStream (network, false, ValidateRemoteCertificate); - engine.Stream.Stream = tls; - try { - if (doAsync) { -#if NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER - await tls.AuthenticateAsClientAsync (GetSslClientAuthenticationOptions (host, ValidateRemoteCertificate), cancellationToken).ConfigureAwait (false); -#else - await tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).ConfigureAwait (false); -#endif - } else { -#if NETSTANDARD1_3 || NETSTANDARD1_6 - tls.AuthenticateAsClientAsync (host, ClientCertificates, SslProtocols, CheckCertificateRevocation).GetAwaiter ().GetResult (); -#elif NET5_0_OR_GREATER - tls.AuthenticateAsClient (GetSslClientAuthenticationOptions (host, ValidateRemoteCertificate)); -#else - tls.AuthenticateAsClient (host, ClientCertificates, SslProtocols, CheckCertificateRevocation); -#endif - } + var tls = new SslStream (stream, false, ValidateRemoteCertificate); + engine.Stream.Stream = tls; + + SslHandshake (tls, host, cancellationToken); } catch (Exception ex) { throw SslHandshakeException.Create (ref sslValidationInfo, ex, true, "IMAP", host, port, 993, 143); } @@ -1763,7 +1593,7 @@ async Task ConnectAsync (Stream stream, string host, int port, SecureSocketOptio // 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); + engine.QueryCapabilities (cancellationToken); } else if (options == SecureSocketOptions.StartTls) { throw ImapCommandException.Create ("STARTTLS", ic); } @@ -1782,10 +1612,18 @@ async Task ConnectAsync (Stream stream, string host, int port, SecureSocketOptio OnConnected (host, port, options); if (authenticated) - await OnAuthenticatedAsync (string.Empty, doAsync, cancellationToken).ConfigureAwait (false); + OnAuthenticated (string.Empty, cancellationToken); + } + + void CheckCanConnect (Stream stream, string host, int port) + { + if (stream == null) + throw new ArgumentNullException (nameof (stream)); + + CheckCanConnect (host, port); } - Task ConnectAsync (Socket socket, string host, int port, SecureSocketOptions options, bool doAsync, CancellationToken cancellationToken) + void CheckCanConnect (Socket socket, string host, int port) { if (socket == null) throw new ArgumentNullException (nameof (socket)); @@ -1793,7 +1631,7 @@ Task ConnectAsync (Socket socket, string host, int port, SecureSocketOptions opt if (!socket.Connected) throw new ArgumentException ("The socket is not connected.", nameof (socket)); - return ConnectAsync (new NetworkStream (socket, true), host, port, options, doAsync, cancellationToken); + CheckCanConnect (host, port); } /// @@ -1859,7 +1697,9 @@ Task ConnectAsync (Socket socket, string host, int port, SecureSocketOptions opt /// public override void Connect (Socket socket, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) { - ConnectAsync (socket, host, port, options, false, cancellationToken).GetAwaiter ().GetResult (); + CheckCanConnect (socket, host, port); + + Connect (new NetworkStream (socket, true), host, port, options, cancellationToken); } /// @@ -1923,7 +1763,102 @@ public override void Connect (Socket socket, string host, int port = 0, SecureSo /// public override void Connect (Stream stream, string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default) { - ConnectAsync (stream, host, port, options, false, cancellationToken).GetAwaiter ().GetResult (); + CheckCanConnect (stream, host, port); + + Stream network; + + ComputeDefaultValues (host, ref port, ref options, out var uri, out var starttls); + + engine.Uri = uri; + + if (options == SecureSocketOptions.SslOnConnect) { + var ssl = new SslStream (stream, false, ValidateRemoteCertificate); + + try { + SslHandshake (ssl, host, cancellationToken); + } catch (Exception ex) { + ssl.Dispose (); + + throw SslHandshakeException.Create (ref sslValidationInfo, ex, false, "IMAP", host, port, 993, 143); + } + + 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 { + engine.Connect (new ImapStream (network, ProtocolLogger), cancellationToken); + } catch { + connecting = false; + throw; + } + + try { + // Only query the CAPABILITIES if the greeting didn't include them. + if (engine.CapabilitiesVersion == 0) + engine.QueryCapabilities (cancellationToken); + + 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"); + + engine.Run (ic); + + if (ic.Response == ImapCommandResponse.Ok) { + var tls = new SslStream (network, false, ValidateRemoteCertificate); + engine.Stream.Stream = tls; + + try { + SslHandshake (tls, host, cancellationToken); + } catch (Exception ex) { + throw SslHandshakeException.Create (ref sslValidationInfo, ex, true, "IMAP", host, port, 993, 143); + } + + secure = true; + + // Query the CAPABILITIES again if the server did not include an + // untagged CAPABILITIES response to the STARTTLS command. + if (engine.CapabilitiesVersion == 1) + engine.QueryCapabilities (cancellationToken); + } 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) + OnAuthenticated (string.Empty, cancellationToken); } /// @@ -2472,7 +2407,7 @@ public override IMailFolder GetFolder (FolderNamespace @namespace) var encodedName = engine.EncodeMailboxName (@namespace.Path); - if (engine.GetCachedFolder (encodedName, out var folder)) + if (engine.TryGetCachedFolder (encodedName, out var folder)) return folder; throw new FolderNotFoundException (@namespace.Path); diff --git a/MailKit/Net/Imap/ImapEngine.cs b/MailKit/Net/Imap/ImapEngine.cs index a2756ed834..562bc0d9d2 100644 --- a/MailKit/Net/Imap/ImapEngine.cs +++ b/MailKit/Net/Imap/ImapEngine.cs @@ -183,7 +183,7 @@ public ImapEngine (CreateImapFolderDelegate createImapFolderDelegate) /// /// /// The authentication mechanisms are queried durring the - /// method. + /// or methods. /// /// The authentication mechanisms. public HashSet AuthenticationMechanisms { @@ -240,8 +240,8 @@ public int I18NLevel { /// 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 will not be known until a successful connection has been + /// made via the or method. /// /// The capabilities. public ImapCapabilities Capabilities { @@ -599,22 +599,7 @@ 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) + void Initialize (ImapStream stream) { TagPrefix = (char) ('A' + (TagPrefixIndex++ % 26)); ProtocolVersion = ImapProtocolVersion.Unknown; @@ -638,35 +623,150 @@ public async Task ConnectAsync (ImapStream stream, bool doAsync, CancellationTok Stream = stream; I18NLevel = 0; Tag = 0; + } + + ImapEngineState ParseConnectedState (ImapToken token, out bool bye) + { + var atom = (string) token.Value; + + bye = false; + + if (atom.Equals ("OK", StringComparison.OrdinalIgnoreCase)) { + return ImapEngineState.Connected; + } else if (atom.Equals ("BYE", StringComparison.OrdinalIgnoreCase)) { + bye = true; + + return State; + } else if (atom.Equals ("PREAUTH", StringComparison.OrdinalIgnoreCase)) { + return ImapEngineState.Authenticated; + } else { + throw UnexpectedToken (GreetingSyntaxErrorFormat, token); + } + } + + void DetectQuirksMode (string text) + { + 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 2003 IMAP4rev1", StringComparison.Ordinal)) + QuirksMode = ImapQuirksMode.Exchange2003; + else if (text.StartsWith ("Microsoft Exchange Server 2007 IMAP4 service is ready", StringComparison.Ordinal)) + QuirksMode = ImapQuirksMode.Exchange2007; + 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.StartsWith ("IMAPrev1", StringComparison.Ordinal)) // https://github.com/hmailserver/hmailserver/blob/master/hmailserver/source/Server/IMAP/IMAPConnection.cpp#L127 + QuirksMode = ImapQuirksMode.hMailServer; + 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; + } + + /// + /// Takes posession of the and reads the greeting. + /// + /// The IMAP stream. + /// The cancellation token. + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP protocol error occurred. + /// + public void Connect (ImapStream stream, CancellationToken cancellationToken) + { + Initialize (stream); try { - var token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + var token = ReadToken (cancellationToken); AssertToken (token, ImapTokenType.Asterisk, GreetingSyntaxErrorFormat, token); - token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + token = ReadToken (cancellationToken); AssertToken (token, ImapTokenType.Atom, GreetingSyntaxErrorFormat, token); - var atom = (string) token.Value; + var state = ParseConnectedState (token, out bool bye); var text = string.Empty; - var state = State; - var bye = false; - - if (atom.Equals ("OK", StringComparison.OrdinalIgnoreCase)) { - state = ImapEngineState.Connected; - } else if (atom.Equals ("BYE", StringComparison.OrdinalIgnoreCase)) { - bye = true; - } else if (atom.Equals ("PREAUTH", StringComparison.OrdinalIgnoreCase)) { - state = ImapEngineState.Authenticated; - } else { - throw UnexpectedToken (GreetingSyntaxErrorFormat, token); + + token = ReadToken (cancellationToken); + + if (token.Type == ImapTokenType.OpenBracket) { + var code = ParseResponseCodeAsync (false, false, cancellationToken).GetAwaiter ().GetResult (); + 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 = ReadLine (cancellationToken).TrimEnd (); + text = token.Value.ToString () + text; + + if (bye) + throw new ImapProtocolException (text); + } else if (bye) { + throw new ImapProtocolException ("The IMAP server unexpectedly refused the connection."); } - token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + DetectQuirksMode (text); + + State = state; + } catch { + Disconnect (); + throw; + } + } + + /// + /// Takes posession of the and reads the greeting. + /// + /// The IMAP stream. + /// 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, CancellationToken cancellationToken) + { + Initialize (stream); + + try { + var token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.Asterisk, GreetingSyntaxErrorFormat, token); + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + AssertToken (token, ImapTokenType.Atom, GreetingSyntaxErrorFormat, token); + + var state = ParseConnectedState (token, out bool bye); + var text = string.Empty; + + token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); if (token.Type == ImapTokenType.OpenBracket) { - var code = await ParseResponseCodeAsync (false, doAsync, cancellationToken).ConfigureAwait (false); + var code = await ParseResponseCodeAsync (false, true, cancellationToken).ConfigureAwait (false); if (code.Type == ImapResponseCodeType.Alert) { OnAlert (code.Message); @@ -676,7 +776,7 @@ public async Task ConnectAsync (ImapStream stream, bool doAsync, CancellationTok text = code.Message; } } else if (token.Type != ImapTokenType.Eoln) { - text = (await ReadLineAsync (doAsync, cancellationToken).ConfigureAwait (false)).TrimEnd (); + text = (await ReadLineAsync (cancellationToken).ConfigureAwait (false)).TrimEnd (); text = token.Value.ToString () + text; if (bye) @@ -685,30 +785,7 @@ public async Task ConnectAsync (ImapStream stream, bool doAsync, CancellationTok 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 2003 IMAP4rev1", StringComparison.Ordinal)) - QuirksMode = ImapQuirksMode.Exchange2003; - else if (text.StartsWith ("Microsoft Exchange Server 2007 IMAP4 service is ready", StringComparison.Ordinal)) - QuirksMode = ImapQuirksMode.Exchange2007; - 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.StartsWith ("IMAPrev1", StringComparison.Ordinal)) // https://github.com/hmailserver/hmailserver/blob/master/hmailserver/source/Server/IMAP/IMAPConnection.cpp#L127 - QuirksMode = ImapQuirksMode.hMailServer; - 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; + DetectQuirksMode (text); State = state; } catch { @@ -1445,7 +1522,7 @@ async ValueTask UpdateNamespacesAsync (bool doAsync, CancellationToken cancellat namespaces[n].Add (new FolderNamespace (delim, DecodeMailboxName (path))); - if (!GetCachedFolder (path, out var folder)) { + if (!TryGetCachedFolder (path, out var folder)) { folder = CreateImapFolder (path, FolderAttributes.None, delim); CacheFolder (folder); } @@ -2013,7 +2090,7 @@ async ValueTask UpdateStatusAsync (bool doAsync, CancellationToken cancellationT // 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 var folder); + TryGetCachedFolder (name, out var folder); token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); @@ -2548,6 +2625,32 @@ public void QueueCommand (ImapCommand ic) } } + /// + /// Queries the capabilities. + /// + /// The command result. + /// Whether or not asynchronous IO methods should be used. + /// The cancellation token. + public ImapCommandResponse QueryCapabilities (CancellationToken cancellationToken) + { + var ic = QueueCommand (cancellationToken, null, "CAPABILITY\r\n"); + + return Run (ic); + } + + /// + /// Queries the capabilities. + /// + /// The command result. + /// Whether or not asynchronous IO methods should be used. + /// The cancellation token. + public Task QueryCapabilitiesAsync (CancellationToken cancellationToken) + { + var ic = QueueCommand (cancellationToken, null, "CAPABILITY\r\n"); + + return RunAsync (ic); + } + /// /// Queries the capabilities. /// @@ -2579,78 +2682,177 @@ public void CacheFolder (ImapFolder 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) + public bool TryGetCachedFolder (string encodedName, out ImapFolder folder) { return FolderCache.TryGetValue (encodedName, out folder); } + bool RequiresParentLookup (ImapFolder folder, out string encodedParentName) + { + encodedParentName = null; + + if (folder.ParentFolder != null) + return false; + + int index; + + // FIXME: should this search EncodedName instead of FullName? + if ((index = folder.FullName.LastIndexOf (folder.DirectorySeparator)) != -1) { + if (index == 0) + return false; + + var parentName = folder.FullName.Substring (0, index); + encodedParentName = EncodeMailboxName (parentName); + } else { + encodedParentName = string.Empty; + } + + if (TryGetCachedFolder (encodedParentName, out var parent)) { + folder.ParentFolder = parent; + return false; + } + + return true; + } + + ImapCommand QueueLookupParentFolderCommand (string encodedName, CancellationToken cancellationToken) + { + // Note: folder names can contain wildcards (including '*' and '%'), so replace '*' with '%' + // in order to reduce the list of folders returned by our LIST command. + var 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); + + return ic; + } + + void ProcessLookupParentFolderResponse (ImapCommand ic, List list, ImapFolder folder, string encodedParentName) + { + if (!TryGetCachedFolder (encodedParentName, out var parent)) { + parent = CreateImapFolder (encodedParentName, FolderAttributes.NonExistent, folder.DirectorySeparator); + CacheFolder (parent); + } else if (parent.ParentFolder == null && !parent.IsNamespace) { + list.Add (parent); + } + + folder.ParentFolder = parent; + } + /// /// 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) + internal void LookupParentFolders (IEnumerable folders, CancellationToken cancellationToken) { var list = new List (folders); - string encodedName, pattern; - 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) + if (!RequiresParentLookup (folder, out var encodedParentName)) continue; - // FIXME: should this search EncodedName instead of FullName? - if ((index = folder.FullName.LastIndexOf (folder.DirectorySeparator)) != -1) { - if (index == 0) - continue; + var ic = QueueLookupParentFolderCommand (encodedParentName, cancellationToken); - var parentName = folder.FullName.Substring (0, index); - encodedName = EncodeMailboxName (parentName); - } else { - encodedName = string.Empty; - } + Run (ic); + + ProcessLookupParentFolderResponse (ic, list, folder, encodedParentName); + } + } + + /// + /// 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, CancellationToken cancellationToken) + { + var list = new List (folders); + + // 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 (GetCachedFolder (encodedName, out var parent)) { - folder.ParentFolder = parent; + if (!RequiresParentLookup (folder, out var encodedParentName)) 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 ic = QueueLookupParentFolderCommand (encodedParentName, cancellationToken); - var command = new StringBuilder ("LIST \"\" %S"); - var returnsSubscribed = false; + await RunAsync (ic).ConfigureAwait (false); - if ((Capabilities & ImapCapabilities.ListExtended) != 0) { - // Try to get the \Subscribed and \HasChildren or \HasNoChildren attributes - command.Append (" RETURN (SUBSCRIBED CHILDREN)"); - returnsSubscribed = true; - } + ProcessLookupParentFolderResponse (ic, list, folder, encodedParentName); + } + } - command.Append ("\r\n"); + /// + /// 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 Task LookupParentFoldersAsync (IEnumerable folders, bool doAsync, CancellationToken cancellationToken) + { + if (doAsync) + return LookupParentFoldersAsync (folders, cancellationToken); - var ic = new ImapCommand (this, cancellationToken, null, command.ToString (), pattern); - ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); - ic.ListReturnsSubscribed = returnsSubscribed; - ic.UserData = new List (); + LookupParentFolders (folders, cancellationToken); - QueueCommand (ic); + return Task.CompletedTask; + } - await RunAsync (ic, doAsync).ConfigureAwait (false); + void ProcessNamespaceResponse (ImapCommand ic) + { + if (QuirksMode == ImapQuirksMode.Exchange && ic.Response == ImapCommandResponse.Bad) { + State = ImapEngineState.Connected; // Reset back to Connected-but-not-Authenticated state + throw ImapCommandException.Create ("NAMESPACE", ic); + } + } + + ImapCommand QueueListNamespaceCommand (List list, CancellationToken cancellationToken) + { + var ic = new ImapCommand (this, cancellationToken, null, "LIST \"\" \"\"\r\n"); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); + ic.UserData = list; + + QueueCommand (ic); + + return ic; + } + + void ProcessListNamespaceResponse (ImapCommand ic, List list) + { + PersonalNamespaces.Clear (); + SharedNamespaces.Clear (); + OtherNamespaces.Clear (); + + if (list.Count > 0) { + var empty = list.FirstOrDefault (x => x.EncodedName.Length == 0); - if (!GetCachedFolder (encodedName, out parent)) { - parent = CreateImapFolder (encodedName, FolderAttributes.NonExistent, folder.DirectorySeparator); - CacheFolder (parent); - } else if (parent.ParentFolder == null && !parent.IsNamespace) { - list.Add (parent); + if (empty == null) { + empty = CreateImapFolder (string.Empty, FolderAttributes.None, list[0].DirectorySeparator); + CacheFolder (empty); } - folder.ParentFolder = parent; + PersonalNamespaces.Add (new FolderNamespace (empty.DirectorySeparator, empty.FullName)); + empty.UpdateIsNamespace (true); } } @@ -2660,48 +2862,63 @@ internal async Task LookupParentFoldersAsync (IEnumerable folders, b /// The command result. /// Whether or not asynchronous IO methods should be used. /// The cancellation token. - public async Task QueryNamespacesAsync (bool doAsync, CancellationToken cancellationToken) + public ImapCommandResponse QueryNamespaces (CancellationToken cancellationToken) { ImapCommand ic; // Note: It seems that on Exchange 2003 (maybe Chinese-only version?), the NAMESPACE command causes the server // to immediately drop the connection. Avoid this issue by not using the NAMESPACE command if we detect that // the server is Microsoft Exchange 2003. See https://github.com/jstedfast/MailKit/issues/1512 for details. - if (QuirksMode != ImapQuirksMode.Exchange2003 && (Capabilities & ImapCapabilities.Namespace) != 0) { + if (QuirksMode != ImapQuirksMode.Exchange2003 && (Capabilities & ImapCapabilities.Namespace) != 0) { ic = QueueCommand (cancellationToken, null, "NAMESPACE\r\n"); - await RunAsync (ic, doAsync).ConfigureAwait (false); - if (QuirksMode == ImapQuirksMode.Exchange && ic.Response == ImapCommandResponse.Bad) { - State = ImapEngineState.Connected; // Reset back to Connected-but-not-Authenticated state - throw ImapCommandException.Create ("NAMESPACE", ic); - } + Run (ic); + + ProcessNamespaceResponse (ic); } else { var list = new List (); - ic = new ImapCommand (this, cancellationToken, null, "LIST \"\" \"\"\r\n"); - ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); - ic.UserData = list; + ic = QueueListNamespaceCommand (list, cancellationToken); - QueueCommand (ic); - await RunAsync (ic, doAsync).ConfigureAwait (false); + Run (ic); - PersonalNamespaces.Clear (); - SharedNamespaces.Clear (); - OtherNamespaces.Clear (); + ProcessListNamespaceResponse (ic, list); - if (list.Count > 0) { - var empty = list.FirstOrDefault (x => x.EncodedName.Length == 0); + LookupParentFolders (list, cancellationToken); + } - if (empty == null) { - empty = CreateImapFolder (string.Empty, FolderAttributes.None, list[0].DirectorySeparator); - CacheFolder (empty); - } + return ic.Response; + } - PersonalNamespaces.Add (new FolderNamespace (empty.DirectorySeparator, empty.FullName)); - empty.UpdateIsNamespace (true); - } + /// + /// Asynchronously queries the namespaces. + /// + /// The command result. + /// Whether or not asynchronous IO methods should be used. + /// The cancellation token. + public async Task QueryNamespacesAsync (CancellationToken cancellationToken) + { + ImapCommand ic; + + // Note: It seems that on Exchange 2003 (maybe Chinese-only version?), the NAMESPACE command causes the server + // to immediately drop the connection. Avoid this issue by not using the NAMESPACE command if we detect that + // the server is Microsoft Exchange 2003. See https://github.com/jstedfast/MailKit/issues/1512 for details. + if (QuirksMode != ImapQuirksMode.Exchange2003 && (Capabilities & ImapCapabilities.Namespace) != 0) { + ic = QueueCommand (cancellationToken, null, "NAMESPACE\r\n"); + + await RunAsync (ic).ConfigureAwait (false); - await LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false); + ProcessNamespaceResponse (ic); + } else { + var list = new List (); + + ic = QueueListNamespaceCommand (list, cancellationToken); + + await RunAsync (ic).ConfigureAwait (false); + + ProcessListNamespaceResponse (ic, list); + + await LookupParentFoldersAsync (list, cancellationToken).ConfigureAwait (false); } return ic.Response; @@ -2751,17 +2968,12 @@ public void AssignSpecialFolders (IList list) 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) + ImapCommand QueueListInboxCommand (CancellationToken cancellationToken, out StringBuilder command, out List list) { - var command = new StringBuilder ("LIST \"\" \"INBOX\""); - var list = new List (); - var returnsSubscribed = false; - ImapCommand ic; + bool returnsSubscribed = false; + + command = new StringBuilder ("LIST \"\" \"INBOX\""); + list = new List (); if ((Capabilities & ImapCapabilities.ListExtended) != 0) { command.Append (" RETURN (SUBSCRIBED CHILDREN)"); @@ -2770,65 +2982,128 @@ public async Task QuerySpecialFoldersAsync (bool doAsync, CancellationToken canc command.Append ("\r\n"); - ic = new ImapCommand (this, cancellationToken, null, command.ToString ()); + var 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); + return ic; + } - GetCachedFolder ("INBOX", out var folder); + void ProcessListInboxResponse (ImapCommand ic, StringBuilder command, List list) + { + TryGetCachedFolder ("INBOX", out var folder); Inbox = folder; + command.Clear (); 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 (); + ImapCommand QueueListSpecialUseCommand (StringBuilder command, List list, CancellationToken cancellationToken) + { + bool returnsSubscribed = false; - command.Append ("LIST "); + command.Append ("LIST "); - if (QuirksMode != ImapQuirksMode.ProtonMail) - command.Append ("(SPECIAL-USE) \"\" \"*\""); - else - command.Append ("\"\" \"%%\""); + // 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. + if (QuirksMode != ImapQuirksMode.ProtonMail) + command.Append ("(SPECIAL-USE) \"\" \"*\""); + else + command.Append ("\"\" \"%%\""); - if ((Capabilities & ImapCapabilities.ListExtended) != 0) { - command.Append (" RETURN (SUBSCRIBED CHILDREN)"); - returnsSubscribed = true; - } + if ((Capabilities & ImapCapabilities.ListExtended) != 0) { + command.Append (" RETURN (SUBSCRIBED CHILDREN)"); + returnsSubscribed = true; + } - command.Append ("\r\n"); + command.Append ("\r\n"); + + var ic = new ImapCommand (this, cancellationToken, null, command.ToString ()); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); + ic.ListReturnsSubscribed = returnsSubscribed; + ic.UserData = list; - ic = new ImapCommand (this, cancellationToken, null, command.ToString ()); - ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); - ic.ListReturnsSubscribed = returnsSubscribed; - ic.UserData = list; + QueueCommand (ic); - QueueCommand (ic); + return ic; + } + + ImapCommand QueueXListCommand (List list, CancellationToken cancellationToken) + { + var ic = new ImapCommand (this, cancellationToken, null, "XLIST \"\" \"*\"\r\n"); + ic.RegisterUntaggedHandler ("XLIST", ImapUtils.ParseFolderListAsync); + ic.UserData = list; + + QueueCommand (ic); + + return ic; + } + + /// + /// Queries the special folders. + /// + /// The cancellation token. + public void QuerySpecialFolders (CancellationToken cancellationToken) + { + var ic = QueueListInboxCommand (cancellationToken, out var command, out var list); - await RunAsync (ic, doAsync).ConfigureAwait (false); - await LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false); + Run (ic); - AssignSpecialFolders (list); + ProcessListInboxResponse (ic, command, list); + + if ((Capabilities & ImapCapabilities.SpecialUse) != 0) { + ic = QueueListSpecialUseCommand (command, list, cancellationToken); + + Run (ic); + + // Note: We specifically don't throw if we get a LIST error. } else if ((Capabilities & ImapCapabilities.XList) != 0) { - ic = new ImapCommand (this, cancellationToken, null, "XLIST \"\" \"*\"\r\n"); - ic.RegisterUntaggedHandler ("XLIST", ImapUtils.ParseFolderListAsync); - ic.UserData = list; + ic = QueueXListCommand (list, cancellationToken); - QueueCommand (ic); + Run (ic); - await RunAsync (ic, doAsync).ConfigureAwait (false); - await LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false); + // Note: We specifically don't throw if we get a XLIST error. + } + + LookupParentFolders (list, cancellationToken); + + AssignSpecialFolders (list); + } + + /// + /// Queries the special folders. + /// + /// The cancellation token. + public async Task QuerySpecialFoldersAsync (CancellationToken cancellationToken) + { + var ic = QueueListInboxCommand (cancellationToken, out var command, out var list); - AssignSpecialFolders (list); + await RunAsync (ic).ConfigureAwait (false); + + ProcessListInboxResponse (ic, command, list); + + if ((Capabilities & ImapCapabilities.SpecialUse) != 0) { + ic = QueueListSpecialUseCommand (command, list, cancellationToken); + + await RunAsync (ic).ConfigureAwait (false); + + // Note: We specifically don't throw if we get a LIST error. + } else if ((Capabilities & ImapCapabilities.XList) != 0) { + ic = QueueXListCommand (list, cancellationToken); + + await RunAsync (ic).ConfigureAwait (false); + + // Note: We specifically don't throw if we get a LIST error. } + + await LookupParentFoldersAsync (list, cancellationToken).ConfigureAwait (false); + + AssignSpecialFolders (list); } /// @@ -2840,7 +3115,7 @@ public async Task QuerySpecialFoldersAsync (bool doAsync, CancellationToken canc /// The cancellation token. public async Task GetQuotaRootFolderAsync (string quotaRoot, bool doAsync, CancellationToken cancellationToken) { - if (GetCachedFolder (quotaRoot, out var folder)) + if (TryGetCachedFolder (quotaRoot, out var folder)) return folder; var command = new StringBuilder ("LIST \"\" %S"); @@ -2888,7 +3163,7 @@ public async Task GetFolderAsync (string path, bool doAsync, Cancell { var encodedName = EncodeMailboxName (path); - if (GetCachedFolder (encodedName, out var folder)) + if (TryGetCachedFolder (encodedName, out var folder)) return folder; var command = new StringBuilder ("LIST \"\" %S"); @@ -2984,7 +3259,7 @@ public async Task> GetFoldersAsync (FolderNamespace @namespace var returnsSubscribed = false; var lsub = subscribedOnly; - if (!GetCachedFolder (encodedName, out var folder)) + if (!TryGetCachedFolder (encodedName, out var folder)) throw new FolderNotFoundException (@namespace.Path); if (subscribedOnly) { diff --git a/MailKit/Net/Imap/ImapFolder.cs b/MailKit/Net/Imap/ImapFolder.cs index 49bbe9a30c..6e5f3e869a 100644 --- a/MailKit/Net/Imap/ImapFolder.cs +++ b/MailKit/Net/Imap/ImapFolder.cs @@ -1702,7 +1702,7 @@ async Task GetSubfolderAsync (string name, bool doAsync, Cancellati var encodedName = Engine.EncodeMailboxName (fullName); List list; - if (Engine.GetCachedFolder (encodedName, out var folder)) + if (Engine.TryGetCachedFolder (encodedName, out var folder)) return folder; // Note: folder names can contain wildcards (including '*' and '%'), so replace '*' with '%' diff --git a/MailKit/Net/Imap/ImapUtils.cs b/MailKit/Net/Imap/ImapUtils.cs index 5ea9f1fff9..4c2415b3d4 100644 --- a/MailKit/Net/Imap/ImapUtils.cs +++ b/MailKit/Net/Imap/ImapUtils.cs @@ -586,7 +586,7 @@ public static async Task ParseFolderListAsync (ImapEngine engine, List