From 0999c49b50b49cad8aeecbde2b119766aa32f92c Mon Sep 17 00:00:00 2001 From: Jeffrey Stedfast Date: Sat, 18 Jan 2025 11:21:47 -0500 Subject: [PATCH] Add work-around for iCloud responses to ENABLE QRESYNC Fixes issue #1871 --- MailKit/Net/Imap/ImapClient.cs | 11 +- UnitTests/Net/Imap/ImapClientTests.cs | 106 ++++++++++++++++++ .../Resources/icloud/authenticate-plain.txt | 1 + .../Net/Imap/Resources/icloud/capability.txt | 2 + .../Imap/Resources/icloud/enable-qresync.txt | 1 + .../Net/Imap/Resources/icloud/greeting.txt | 2 +- .../Net/Imap/Resources/icloud/list-inbox.txt | 2 + .../Net/Imap/Resources/icloud/namespace.txt | 2 + 8 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 UnitTests/Net/Imap/Resources/icloud/authenticate-plain.txt create mode 100644 UnitTests/Net/Imap/Resources/icloud/capability.txt create mode 100644 UnitTests/Net/Imap/Resources/icloud/enable-qresync.txt create mode 100644 UnitTests/Net/Imap/Resources/icloud/list-inbox.txt create mode 100644 UnitTests/Net/Imap/Resources/icloud/namespace.txt diff --git a/MailKit/Net/Imap/ImapClient.cs b/MailKit/Net/Imap/ImapClient.cs index aae635eecb..9cae93d807 100644 --- a/MailKit/Net/Imap/ImapClient.cs +++ b/MailKit/Net/Imap/ImapClient.cs @@ -380,9 +380,18 @@ bool TryQueueEnableQuickResyncCommand (CancellationToken cancellationToken, out return true; } - static void ProcessEnableResponse (ImapCommand ic) + void ProcessEnableResponse (ImapCommand ic) { ic.ThrowIfNotOk ("ENABLE"); + + if (engine.QuirksMode == ImapQuirksMode.iCloud) { + // Note: iCloud's response to the `ENABLE QRESYNC CONDSTORE` command does not include an untagged response + // notifying us that QRESYNC or CONDSTORE have been enabled. Instead, if we get a tagged OK response, we + // assume that these features were enabled successfully. + // + // See https://github.com/jstedfast/MailKit/issues/1871 for details. + engine.QResyncEnabled = true; + } } /// diff --git a/UnitTests/Net/Imap/ImapClientTests.cs b/UnitTests/Net/Imap/ImapClientTests.cs index 2420e454d7..c70356673c 100644 --- a/UnitTests/Net/Imap/ImapClientTests.cs +++ b/UnitTests/Net/Imap/ImapClientTests.cs @@ -71,6 +71,16 @@ public class ImapClientTests ImapCapabilities.ESearch | ImapCapabilities.Compress | ImapCapabilities.Enable | ImapCapabilities.ListExtended | ImapCapabilities.ListStatus | ImapCapabilities.Move | ImapCapabilities.UTF8Accept | ImapCapabilities.XList | ImapCapabilities.GMailExt1 | ImapCapabilities.LiteralMinus | ImapCapabilities.AppendLimit; + static readonly ImapCapabilities ICloudInitialCapabilities = ImapCapabilities.IMAP4 | ImapCapabilities.IMAP4rev1 | + ImapCapabilities.Status | ImapCapabilities.SaslIR; + static readonly ImapCapabilities ICloudAuthenticatedCapabilities = ImapCapabilities.IMAP4 | ImapCapabilities.IMAP4rev1 | + ImapCapabilities.Status | ImapCapabilities.CondStore | ImapCapabilities.Enable | ImapCapabilities.QuickResync | + ImapCapabilities.Quota | ImapCapabilities.Namespace | ImapCapabilities.UidPlus | ImapCapabilities.Children | + ImapCapabilities.Binary | ImapCapabilities.Unselect | ImapCapabilities.Sort | ImapCapabilities.Catenate | + ImapCapabilities.Language | ImapCapabilities.ESearch | ImapCapabilities.ESort | ImapCapabilities.Thread | + ImapCapabilities.Context | ImapCapabilities.Within | ImapCapabilities.SaslIR | ImapCapabilities.SearchResults | + ImapCapabilities.Metadata | ImapCapabilities.Id | ImapCapabilities.Annotate | ImapCapabilities.MultiSearch | + ImapCapabilities.Idle | ImapCapabilities.ListStatus; static readonly ImapCapabilities IMAP4rev2CoreCapabilities = ImapCapabilities.IMAP4rev2 | ImapCapabilities.Status | ImapCapabilities.Namespace | ImapCapabilities.Unselect | ImapCapabilities.UidPlus | ImapCapabilities.ESearch | ImapCapabilities.SearchResults | ImapCapabilities.Enable | ImapCapabilities.Idle | ImapCapabilities.SaslIR | ImapCapabilities.ListExtended | @@ -3564,6 +3574,8 @@ public void TestEnableQuickResync () client.EnableQuickResync (); + Assert.That (client.Inbox.Supports (FolderFeature.QuickResync), Is.True, "Expected the INBOX to support QRESYNC"); + // ENABLE QRESYNC a second time should no-op. client.EnableQuickResync (); @@ -3606,6 +3618,100 @@ public async Task TestEnableQuickResyncAsync () await client.EnableQuickResyncAsync (); + Assert.That (client.Inbox.Supports (FolderFeature.QuickResync), Is.True, "Expected the INBOX to support QRESYNC"); + + // ENABLE QRESYNC a second time should no-op. + await client.EnableQuickResyncAsync (); + + await client.DisconnectAsync (false); + } + } + + static List CreateEnableQuickResynciCloudCommands () + { + return new List { + new ImapReplayCommand ("", "icloud.greeting.txt"), + new ImapReplayCommand ("A00000000 AUTHENTICATE PLAIN AHVzZXJuYW1lAHBhc3N3b3Jk\r\n", "icloud.authenticate-plain.txt"), + new ImapReplayCommand ("A00000001 CAPABILITY\r\n", "icloud.capability.txt"), + new ImapReplayCommand ("A00000002 NAMESPACE\r\n", "icloud.namespace.txt"), + new ImapReplayCommand ("A00000003 LIST \"\" \"INBOX\"\r\n", "icloud.list-inbox.txt"), + new ImapReplayCommand ("A00000004 ENABLE QRESYNC CONDSTORE\r\n", "icloud.enable-qresync.txt"), + }; + } + + [Test] + public void TestEnableQuickResynciCloud () + { + var commands = CreateEnableQuickResynciCloudCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + client.Connect (new ImapReplayStream (commands, false), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (ICloudInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (4)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("ATOKEN"), "Expected SASL ATOKEN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("ATOKEN2"), "Expected SASL ATOKEN2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + + try { + client.Authenticate ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (ICloudAuthenticatedCapabilities)); + Assert.That (client.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.OrderedSubject), "Expected THREAD=ORDEREDSUBJECT"); + Assert.That (client.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.References), "Expected THREAD=REFERENCES"); + + client.EnableQuickResync (); + + Assert.That (client.Inbox.Supports (FolderFeature.QuickResync), Is.True, "Expected the INBOX to support QRESYNC"); + + // ENABLE QRESYNC a second time should no-op. + client.EnableQuickResync (); + + client.Disconnect (false); + } + } + + [Test] + public async Task TestEnableQuickResynciCloudAsync () + { + var commands = CreateEnableQuickResynciCloudCommands (); + + using (var client = new ImapClient () { TagPrefix = 'A' }) { + try { + await client.ConnectAsync (new ImapReplayStream (commands, true), "localhost", 143, SecureSocketOptions.None); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Connect: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (ICloudInitialCapabilities)); + Assert.That (client.AuthenticationMechanisms, Has.Count.EqualTo (4)); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("ATOKEN"), "Expected SASL ATOKEN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("PLAIN"), "Expected SASL PLAIN auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("ATOKEN2"), "Expected SASL ATOKEN2 auth mechanism"); + Assert.That (client.AuthenticationMechanisms, Does.Contain ("XOAUTH2"), "Expected SASL XOAUTH2 auth mechanism"); + + try { + await client.AuthenticateAsync ("username", "password"); + } catch (Exception ex) { + Assert.Fail ($"Did not expect an exception in Authenticate: {ex}"); + } + + Assert.That (client.Capabilities, Is.EqualTo (ICloudAuthenticatedCapabilities)); + Assert.That (client.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.OrderedSubject), "Expected THREAD=ORDEREDSUBJECT"); + Assert.That (client.ThreadingAlgorithms, Does.Contain (ThreadingAlgorithm.References), "Expected THREAD=REFERENCES"); + + await client.EnableQuickResyncAsync (); + + Assert.That (client.Inbox.Supports (FolderFeature.QuickResync), Is.True, "Expected the INBOX to support QRESYNC"); + // ENABLE QRESYNC a second time should no-op. await client.EnableQuickResyncAsync (); diff --git a/UnitTests/Net/Imap/Resources/icloud/authenticate-plain.txt b/UnitTests/Net/Imap/Resources/icloud/authenticate-plain.txt new file mode 100644 index 0000000000..ae2425de37 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/icloud/authenticate-plain.txt @@ -0,0 +1 @@ +A######## OK user username authenticated diff --git a/UnitTests/Net/Imap/Resources/icloud/capability.txt b/UnitTests/Net/Imap/Resources/icloud/capability.txt new file mode 100644 index 0000000000..3680287256 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/icloud/capability.txt @@ -0,0 +1,2 @@ +* CAPABILITY XAPPLEPUSHSERVICE IMAP4 IMAP4rev1 CONDSTORE ENABLE QRESYNC QUOTA XAPPLELITERAL NAMESPACE UIDPLUS CHILDREN BINARY UNSELECT SORT CATENATE URLAUTH LANGUAGE ESEARCH ESORT THREAD=ORDEREDSUBJECT THREAD=REFERENCES CONTEXT=SEARCH CONTEXT=SORT WITHIN SASL-IR SEARCHRES METADATA ID XMSEARCH X-SUN-SORT ANNOTATE-EXPERIMENT-1 X-UNAUTHENTICATE X-SUN-IMAP XUM1 MULTISEARCH IDLE X-APPLE-REMOTE-LINKS LIST-STATUS +A######## OK Completed diff --git a/UnitTests/Net/Imap/Resources/icloud/enable-qresync.txt b/UnitTests/Net/Imap/Resources/icloud/enable-qresync.txt new file mode 100644 index 0000000000..7c15b2d274 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/icloud/enable-qresync.txt @@ -0,0 +1 @@ +A######## OK ENABLE completed diff --git a/UnitTests/Net/Imap/Resources/icloud/greeting.txt b/UnitTests/Net/Imap/Resources/icloud/greeting.txt index 8d071defdc..f0e66b3ef4 100644 --- a/UnitTests/Net/Imap/Resources/icloud/greeting.txt +++ b/UnitTests/Net/Imap/Resources/icloud/greeting.txt @@ -1 +1 @@ -* OK [CAPABILITY XAPPLEPUSHSERVICE IMAP4 IMAP4rev1 SASL-IR AUTH=ATOKEN AUTH=PLAIN] (2313B20-3ff771b405ab) pv50p00im-tygg10010601.me.com +* OK [CAPABILITY XAPPLEPUSHSERVICE IMAP4 IMAP4rev1 SASL-IR AUTH=ATOKEN AUTH=PLAIN AUTH=ATOKEN2 AUTH=XOAUTH2] (2428B47-26126cfe23cd) p00-iscream-f9f7bf749-qjr87 diff --git a/UnitTests/Net/Imap/Resources/icloud/list-inbox.txt b/UnitTests/Net/Imap/Resources/icloud/list-inbox.txt new file mode 100644 index 0000000000..b83825aa0b --- /dev/null +++ b/UnitTests/Net/Imap/Resources/icloud/list-inbox.txt @@ -0,0 +1,2 @@ +* LIST (\Noinferiors) "/" "INBOX" +A######## OK LIST completed (took 0 ms) diff --git a/UnitTests/Net/Imap/Resources/icloud/namespace.txt b/UnitTests/Net/Imap/Resources/icloud/namespace.txt new file mode 100644 index 0000000000..2f127dfb95 --- /dev/null +++ b/UnitTests/Net/Imap/Resources/icloud/namespace.txt @@ -0,0 +1,2 @@ +* NAMESPACE (("" "/")) NIL NIL +A######## OK NAMESPACE completed (took 0 ms)