From 95a602f91feb77bce7aa92c3385f8e478b315803 Mon Sep 17 00:00:00 2001 From: Jeffrey Stedfast Date: Sat, 9 Sep 2023 20:28:09 -0400 Subject: [PATCH] Refactored more IMAP untagged response handlers to split sync/async --- MailKit/Net/Imap/ImapClient.cs | 6 +- MailKit/Net/Imap/ImapEngine.cs | 74 +++- MailKit/Net/Imap/ImapFolder.cs | 284 ++++++++++--- MailKit/Net/Imap/ImapFolderFetch.cs | 18 +- MailKit/Net/Imap/ImapFolderSearch.cs | 331 ++++++++++++--- MailKit/Net/Imap/ImapUtils.cs | 575 +++++++++++++++++++++------ UnitTests/Net/Imap/ImapUtilsTests.cs | 28 +- 7 files changed, 1033 insertions(+), 283 deletions(-) diff --git a/MailKit/Net/Imap/ImapClient.cs b/MailKit/Net/Imap/ImapClient.cs index f34f042647..afab19d49e 100644 --- a/MailKit/Net/Imap/ImapClient.cs +++ b/MailKit/Net/Imap/ImapClient.cs @@ -529,7 +529,7 @@ ImapCommand QueueIdentifyCommand (ImapImplementation clientImplementation, Cance } var ic = new ImapCommand (engine, cancellationToken, null, command.ToString (), args.ToArray ()); - ic.RegisterUntaggedHandler ("ID", ImapUtils.ParseImplementationAsync); + ic.RegisterUntaggedHandler ("ID", ImapUtils.UntaggedIdHandler); engine.QueueCommand (ic); @@ -2523,7 +2523,7 @@ ImapCommand QueueGetMetadataCommand (MetadataTag tag, CancellationToken cancella 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); + ic.RegisterUntaggedHandler ("METADATA", ImapUtils.UntaggedMetadataHandler); var metadata = new MetadataCollection (); ic.UserData = metadata; @@ -2648,7 +2648,7 @@ ImapCommand QueueGetMetadataCommand (MetadataOptions options, IEnumerable ReadTokenAsync (CancellationToken cancellationToken) return Stream.ReadTokenAsync (cancellationToken); } + /// + /// Reads the next token. + /// + /// The token. + /// A list of characters that are not legal in bare string tokens. + /// The cancellation token. + /// + /// The engine is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP protocol error occurred. + /// + public ImapToken ReadToken (string specials, CancellationToken cancellationToken) + { + return Stream.ReadToken (specials, cancellationToken); + } + + /// + /// Asynchronously reads the next token. + /// + /// The token. + /// A list of characters that are not legal in bare string tokens. + /// The cancellation token. + /// + /// The engine is not connected. + /// + /// + /// The operation was canceled via the cancellation token. + /// + /// + /// An I/O error occurred. + /// + /// + /// An IMAP protocol error occurred. + /// + public ValueTask ReadTokenAsync (string specials, CancellationToken cancellationToken) + { + return Stream.ReadTokenAsync (specials, cancellationToken); + } + /// /// Peeks at the next token. /// @@ -2319,7 +2365,7 @@ internal void ProcessUntaggedResponse (CancellationToken cancellationToken) //if (number == 0) // throw UnexpectedToken ("Syntax error in untagged FETCH response. Unexpected message index: 0"); - folder.OnFetchAsync (this, (int) number - 1, doAsync: false, cancellationToken).GetAwaiter ().GetResult (); + folder.OnUntaggedFetchResponse (this, (int) number - 1, doAsync: false, cancellationToken).GetAwaiter ().GetResult (); } else if (atom.Equals ("RECENT", StringComparison.OrdinalIgnoreCase)) { folder.OnRecent ((int) number); } else { @@ -2336,13 +2382,13 @@ internal void ProcessUntaggedResponse (CancellationToken cancellationToken) SkipLine (cancellationToken); } else if (atom.Equals ("LIST", StringComparison.OrdinalIgnoreCase)) { // unsolicited LIST response - probably due to NOTIFY MailboxName or MailboxSubscribe event - ImapUtils.ParseFolderListAsync (this, null, false, true, doAsync: false, cancellationToken).GetAwaiter ().GetResult (); + ImapUtils.ParseFolderList (this, null, false, true, cancellationToken); token = ReadToken (cancellationToken); AssertToken (token, ImapTokenType.Eoln, "Syntax error in untagged LIST response. {0}", token); } else if (atom.Equals ("METADATA", StringComparison.OrdinalIgnoreCase)) { // unsolicited METADATA response - probably due to NOTIFY MailboxMetadataChange or ServerMetadataChange var metadata = new MetadataCollection (); - ImapUtils.ParseMetadataAsync (this, metadata, doAsync: false, cancellationToken).GetAwaiter ().GetResult (); + ImapUtils.ParseMetadata (this, metadata, cancellationToken); ProcessMetadataChanges (metadata); token = ReadToken (cancellationToken); @@ -2472,7 +2518,7 @@ internal async Task ProcessUntaggedResponseAsync (CancellationToken cancellation //if (number == 0) // throw UnexpectedToken ("Syntax error in untagged FETCH response. Unexpected message index: 0"); - await folder.OnFetchAsync (this, (int) number - 1, doAsync: true, cancellationToken).ConfigureAwait (false); + await folder.OnUntaggedFetchResponse (this, (int) number - 1, doAsync: true, cancellationToken).ConfigureAwait (false); } else if (atom.Equals ("RECENT", StringComparison.OrdinalIgnoreCase)) { folder.OnRecent ((int) number); } else { @@ -2489,13 +2535,13 @@ internal async Task ProcessUntaggedResponseAsync (CancellationToken cancellation await SkipLineAsync (cancellationToken).ConfigureAwait (false); } else if (atom.Equals ("LIST", StringComparison.OrdinalIgnoreCase)) { // unsolicited LIST response - probably due to NOTIFY MailboxName or MailboxSubscribe event - await ImapUtils.ParseFolderListAsync (this, null, false, true, doAsync: true, cancellationToken).ConfigureAwait (false); + await ImapUtils.ParseFolderListAsync (this, null, false, true, cancellationToken).ConfigureAwait (false); token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); AssertToken (token, ImapTokenType.Eoln, "Syntax error in untagged LIST response. {0}", token); } else if (atom.Equals ("METADATA", StringComparison.OrdinalIgnoreCase)) { // unsolicited METADATA response - probably due to NOTIFY MailboxMetadataChange or ServerMetadataChange var metadata = new MetadataCollection (); - await ImapUtils.ParseMetadataAsync (this, metadata, doAsync: true, cancellationToken).ConfigureAwait (false); + await ImapUtils.ParseMetadataAsync (this, metadata, cancellationToken).ConfigureAwait (false); ProcessMetadataChanges (metadata); token = await ReadTokenAsync (cancellationToken).ConfigureAwait (false); @@ -2861,7 +2907,7 @@ ImapCommand QueueLookupParentFolderCommand (string encodedName, CancellationToke command.Append ("\r\n"); var ic = new ImapCommand (this, cancellationToken, null, command.ToString (), pattern); - ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.UntaggedListHandler); ic.ListReturnsSubscribed = returnsSubscribed; ic.UserData = new List (); @@ -2957,7 +3003,7 @@ void ProcessNamespaceResponse (ImapCommand ic) ImapCommand QueueListNamespaceCommand (List list, CancellationToken cancellationToken) { var ic = new ImapCommand (this, cancellationToken, null, "LIST \"\" \"\"\r\n"); - ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.UntaggedListHandler); ic.UserData = list; QueueCommand (ic); @@ -3109,7 +3155,7 @@ ImapCommand QueueListInboxCommand (CancellationToken cancellationToken, out Stri command.Append ("\r\n"); var ic = new ImapCommand (this, cancellationToken, null, command.ToString ()); - ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.UntaggedListHandler); ic.ListReturnsSubscribed = returnsSubscribed; ic.UserData = list; @@ -3150,7 +3196,7 @@ ImapCommand QueueListSpecialUseCommand (StringBuilder command, List command.Append ("\r\n"); var ic = new ImapCommand (this, cancellationToken, null, command.ToString ()); - ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.UntaggedListHandler); ic.ListReturnsSubscribed = returnsSubscribed; ic.UserData = list; @@ -3162,7 +3208,7 @@ ImapCommand QueueListSpecialUseCommand (StringBuilder command, List ImapCommand QueueXListCommand (List list, CancellationToken cancellationToken) { var ic = new ImapCommand (this, cancellationToken, null, "XLIST \"\" \"*\"\r\n"); - ic.RegisterUntaggedHandler ("XLIST", ImapUtils.ParseFolderListAsync); + ic.RegisterUntaggedHandler ("XLIST", ImapUtils.UntaggedListHandler); ic.UserData = list; QueueCommand (ic); @@ -3256,7 +3302,7 @@ public async Task GetQuotaRootFolderAsync (string quotaRoot, bool do command.Append ("\r\n"); var ic = new ImapCommand (this, cancellationToken, null, command.ToString (), quotaRoot); - ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.UntaggedListHandler); ic.ListReturnsSubscribed = returnsSubscribed; ic.UserData = list; @@ -3292,7 +3338,7 @@ ImapCommand QueueGetFolderCommand (string encodedName, CancellationToken cancell command.Append ("\r\n"); var ic = new ImapCommand (this, cancellationToken, null, command.ToString (), encodedName); - ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.UntaggedListHandler); ic.ListReturnsSubscribed = returnsSubscribed; ic.UserData = list; @@ -3462,7 +3508,7 @@ ImapCommand QueueGetFoldersCommand (FolderNamespace @namespace, StatusItems item command.Append ("\r\n"); var ic = new ImapCommand (this, cancellationToken, null, command.ToString (), pattern + "*"); - ic.RegisterUntaggedHandler (lsub ? "LSUB" : "LIST", ImapUtils.ParseFolderListAsync); + ic.RegisterUntaggedHandler (lsub ? "LSUB" : "LIST", ImapUtils.UntaggedListHandler); ic.ListReturnsSubscribed = returnsSubscribed; ic.UserData = list; ic.Lsub = lsub; diff --git a/MailKit/Net/Imap/ImapFolder.cs b/MailKit/Net/Imap/ImapFolder.cs index 154cac96e3..5b471af029 100644 --- a/MailKit/Net/Imap/ImapFolder.cs +++ b/MailKit/Net/Imap/ImapFolder.cs @@ -25,7 +25,6 @@ // using System; -using System.Linq; using System.Text; using System.Threading; using System.Globalization; @@ -318,9 +317,9 @@ static string SelectOrExamine (FolderAccess access) return access == FolderAccess.ReadOnly ? "EXAMINE" : "SELECT"; } - static Task QResyncFetchAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + static Task UntaggedQResyncFetchHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) { - return ic.Folder.OnFetchAsync (engine, index, doAsync, ic.CancellationToken); + return ic.Folder.OnUntaggedFetchResponse (engine, index, doAsync, ic.CancellationToken); } async Task OpenAsync (ImapCommand ic, FolderAccess access, bool doAsync, CancellationToken cancellationToken) @@ -397,7 +396,7 @@ Task OpenAsync (FolderAccess access, uint uidValidity, ulong highe var command = string.Format ("{0} %F {1}\r\n", SelectOrExamine (access), qresync); var ic = new ImapCommand (Engine, cancellationToken, this, command, this); - ic.RegisterUntaggedHandler ("FETCH", QResyncFetchAsync); + ic.RegisterUntaggedHandler ("FETCH", UntaggedQResyncFetchHandler); return OpenAsync (ic, access, doAsync, cancellationToken); } @@ -730,7 +729,7 @@ async Task GetCreatedFolderAsync (string encodedName, string id, bo var list = new List (); ImapFolder folder; - ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.UntaggedListHandler); ic.UserData = list; Engine.QueueCommand (ic); @@ -1563,7 +1562,7 @@ async Task> GetSubfoldersAsync (StatusItems items, bool subsc command.Append ("\r\n"); var ic = new ImapCommand (Engine, cancellationToken, null, command.ToString (), pattern.ToString ()); - ic.RegisterUntaggedHandler (lsub ? "LSUB" : "LIST", ImapUtils.ParseFolderListAsync); + ic.RegisterUntaggedHandler (lsub ? "LSUB" : "LIST", ImapUtils.UntaggedListHandler); ic.ListReturnsSubscribed = returnsSubscribed; ic.UserData = list; ic.Lsub = lsub; @@ -1710,7 +1709,7 @@ async Task GetSubfolderAsync (string name, bool doAsync, Cancellati var pattern = encodedName.Replace ('*', '%'); var ic = new ImapCommand (Engine, cancellationToken, null, "LIST \"\" %S\r\n", pattern); - ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync); + ic.RegisterUntaggedHandler ("LIST", ImapUtils.UntaggedListHandler); ic.UserData = list = new List (); Engine.QueueCommand (ic); @@ -2038,20 +2037,27 @@ public override Task StatusAsync (StatusItems items, CancellationToken cancellat return StatusAsync (items, true, true, cancellationToken); } - static async Task ReadStringTokenAsync (ImapEngine engine, string format, bool doAsync, CancellationToken cancellationToken) + static void ParseAcl (ImapEngine engine, ImapCommand ic) { - var token = await engine.ReadTokenAsync (ImapStream.AtomSpecials, doAsync, cancellationToken).ConfigureAwait (false); + string format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ACL", "{0}"); + var acl = (AccessControlList) ic.UserData; + string name, rights; + ImapToken token; - switch (token.Type) { - case ImapTokenType.Literal: return await engine.ReadLiteralAsync (doAsync, cancellationToken).ConfigureAwait (false); - case ImapTokenType.QString: return (string) token.Value; - case ImapTokenType.Atom: return (string) token.Value; - default: - throw ImapEngine.UnexpectedToken (format, token); - } + // read the mailbox name + ImapUtils.ReadStringToken (engine, format, ic.CancellationToken); + + do { + name = ImapUtils.ReadStringToken (engine, format, ic.CancellationToken); + rights = ImapUtils.ReadStringToken (engine, format, ic.CancellationToken); + + acl.Add (new AccessControl (name, rights)); + + token = engine.PeekToken (ic.CancellationToken); + } while (token.Type != ImapTokenType.Eoln); } - static async Task UntaggedAclAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + static async Task ParseAclAsync (ImapEngine engine, ImapCommand ic) { string format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ACL", "{0}"); var acl = (AccessControlList) ic.UserData; @@ -2059,18 +2065,28 @@ static async Task UntaggedAclAsync (ImapEngine engine, ImapCommand ic, int index ImapToken token; // read the mailbox name - await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); + await ImapUtils.ReadStringTokenAsync (engine, format, ic.CancellationToken).ConfigureAwait (false); do { - name = await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); - rights = await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); + name = await ImapUtils.ReadStringTokenAsync (engine, format, ic.CancellationToken).ConfigureAwait (false); + rights = await ImapUtils.ReadStringTokenAsync (engine, format, ic.CancellationToken).ConfigureAwait (false); acl.Add (new AccessControl (name, rights)); - token = await engine.PeekTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (ic.CancellationToken).ConfigureAwait (false); } while (token.Type != ImapTokenType.Eoln); } + static Task UntaggedAclHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + if (doAsync) + return ParseAclAsync (engine, ic); + + ParseAcl (engine, ic); + + return Task.CompletedTask; + } + async Task GetAccessControlListAsync (bool doAsync, CancellationToken cancellationToken) { if ((Engine.Capabilities & ImapCapabilities.Acl) == 0) @@ -2079,7 +2095,7 @@ async Task GetAccessControlListAsync (bool doAsync, Cancellat CheckState (false, false); var ic = new ImapCommand (Engine, cancellationToken, null, "GETACL %F\r\n", this); - ic.RegisterUntaggedHandler ("ACL", UntaggedAclAsync); + ic.RegisterUntaggedHandler ("ACL", UntaggedAclHandler); ic.UserData = new AccessControlList (); Engine.QueueCommand (ic); @@ -2168,27 +2184,58 @@ public override Task GetAccessControlListAsync (CancellationT return GetAccessControlListAsync (true, cancellationToken); } - static async Task UntaggedListRightsAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + static void ParseListRights (ImapEngine engine, ImapCommand ic) + { + string format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "LISTRIGHTS", "{0}"); + var access = (AccessRights) ic.UserData; + ImapToken token; + + // read the mailbox name + ImapUtils.ReadStringToken (engine, format, ic.CancellationToken); + + // read the identity name + ImapUtils.ReadStringToken (engine, format, ic.CancellationToken); + + do { + var rights = ImapUtils.ReadStringToken (engine, format, ic.CancellationToken); + + access.AddRange (rights); + + token = engine.PeekToken (ic.CancellationToken); + } while (token.Type != ImapTokenType.Eoln); + } + + static async Task ParseListRightsAsync (ImapEngine engine, ImapCommand ic) { string format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "LISTRIGHTS", "{0}"); var access = (AccessRights) ic.UserData; ImapToken token; // read the mailbox name - await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); + await ImapUtils.ReadStringTokenAsync (engine, format, ic.CancellationToken).ConfigureAwait (false); // read the identity name - await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); + await ImapUtils.ReadStringTokenAsync (engine, format, ic.CancellationToken).ConfigureAwait (false); do { - var rights = await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); + var rights = await ImapUtils.ReadStringTokenAsync (engine, format, ic.CancellationToken).ConfigureAwait (false); access.AddRange (rights); - token = await engine.PeekTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (ic.CancellationToken).ConfigureAwait (false); } while (token.Type != ImapTokenType.Eoln); } + static Task UntaggedListRightsHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + if (doAsync) + return ParseListRightsAsync (engine, ic); + + ParseListRights (engine, ic); + + return Task.CompletedTask; + } + async Task GetAccessRightsAsync (string name, bool doAsync, CancellationToken cancellationToken) { if (name == null) @@ -2200,7 +2247,7 @@ async Task GetAccessRightsAsync (string name, bool doAsync, Cancel CheckState (false, false); var ic = new ImapCommand (Engine, cancellationToken, null, "LISTRIGHTS %F %S\r\n", this, name); - ic.RegisterUntaggedHandler ("LISTRIGHTS", UntaggedListRightsAsync); + ic.RegisterUntaggedHandler ("LISTRIGHTS", UntaggedListRightsHandler); ic.UserData = new AccessRights (); Engine.QueueCommand (ic); @@ -2297,16 +2344,38 @@ public override Task GetAccessRightsAsync (string name, Cancellati return GetAccessRightsAsync (name, true, cancellationToken); } - static async Task UntaggedMyRightsAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + static void ParseMyRights (ImapEngine engine, ImapCommand ic) { string format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "MYRIGHTS", "{0}"); var access = (AccessRights) ic.UserData; // read the mailbox name - await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); + ImapUtils.ReadStringToken (engine, format, ic.CancellationToken); // read the access rights - access.AddRange (await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false)); + access.AddRange (ImapUtils.ReadStringToken (engine, format, ic.CancellationToken)); + } + + static async Task ParseMyRightsAsync (ImapEngine engine, ImapCommand ic) + { + string format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "MYRIGHTS", "{0}"); + var access = (AccessRights) ic.UserData; + + // read the mailbox name + await ImapUtils.ReadStringTokenAsync (engine, format, ic.CancellationToken).ConfigureAwait (false); + + // read the access rights + access.AddRange (await ImapUtils.ReadStringTokenAsync (engine, format, ic.CancellationToken).ConfigureAwait (false)); + } + + static Task UntaggedMyRightsHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + if (doAsync) + return ParseMyRightsAsync (engine, ic); + + ParseMyRights (engine, ic); + + return Task.CompletedTask; } async Task GetMyAccessRightsAsync (bool doAsync, CancellationToken cancellationToken) @@ -2317,7 +2386,7 @@ async Task GetMyAccessRightsAsync (bool doAsync, CancellationToken CheckState (false, false); var ic = new ImapCommand (Engine, cancellationToken, null, "MYRIGHTS %F\r\n", this); - ic.RegisterUntaggedHandler ("MYRIGHTS", UntaggedMyRightsAsync); + ic.RegisterUntaggedHandler ("MYRIGHTS", UntaggedMyRightsHandler); ic.UserData = new AccessRights (); Engine.QueueCommand (ic); @@ -2853,7 +2922,7 @@ async Task GetMetadataAsync (MetadataTag tag, bool doAsync, Cancellation throw new NotSupportedException ("The IMAP server does not support the METADATA extension."); var ic = new ImapCommand (Engine, cancellationToken, null, "GETMETADATA %F %S\r\n", this, tag.Id); - ic.RegisterUntaggedHandler ("METADATA", ImapUtils.ParseMetadataAsync); + ic.RegisterUntaggedHandler ("METADATA", ImapUtils.UntaggedMetadataHandler); var metadata = new MetadataCollection (); ic.UserData = metadata; @@ -3010,7 +3079,7 @@ async Task GetMetadataAsync (MetadataOptions options, IEnume return new MetadataCollection (); var ic = new ImapCommand (Engine, cancellationToken, null, command.ToString (), args.ToArray ()); - ic.RegisterUntaggedHandler ("METADATA", ImapUtils.ParseMetadataAsync); + ic.RegisterUntaggedHandler ("METADATA", ImapUtils.UntaggedMetadataHandler); ic.UserData = new MetadataCollection (); options.LongEntries = 0; @@ -3269,33 +3338,120 @@ public IDictionary Quotas { } } - static async Task UntaggedQuotaRootAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + static void ParseQuotaRoot (ImapEngine engine, ImapCommand ic) + { + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "QUOTAROOT", "{0}"); + var ctx = (QuotaContext) ic.UserData; + + // The first token should be the mailbox name + ImapUtils.ReadStringToken (engine, format, ic.CancellationToken); + + // ...followed by 0 or more quota roots + var token = engine.PeekToken (ic.CancellationToken); + + while (token.Type != ImapTokenType.Eoln) { + var root = ImapUtils.ReadStringToken (engine, format, ic.CancellationToken); + ctx.QuotaRoots.Add (root); + + token = engine.PeekToken (ic.CancellationToken); + } + } + + static async Task ParseQuotaRootAsync (ImapEngine engine, ImapCommand ic) { var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "QUOTAROOT", "{0}"); var ctx = (QuotaContext) ic.UserData; // The first token should be the mailbox name - await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); + await ImapUtils.ReadStringTokenAsync (engine, format, ic.CancellationToken).ConfigureAwait (false); // ...followed by 0 or more quota roots - var token = await engine.PeekTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + var token = await engine.PeekTokenAsync (ic.CancellationToken).ConfigureAwait (false); while (token.Type != ImapTokenType.Eoln) { - var root = await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); + var root = await ImapUtils.ReadStringTokenAsync (engine, format, ic.CancellationToken).ConfigureAwait (false); ctx.QuotaRoots.Add (root); - token = await engine.PeekTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (ic.CancellationToken).ConfigureAwait (false); + } + } + + /// + /// Handles an untagged QUOTAROOT response. + /// + /// An asynchronous task. + /// The IMAP engine. + /// The IMAP command. + /// The index. + /// Whether or not asynchronous IO methods should be used. + static Task UntaggedQuotaRootHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + if (doAsync) + return ParseQuotaRootAsync (engine, ic); + + ParseQuotaRoot (engine, ic); + + return Task.CompletedTask; + } + + static void ParseQuota (ImapEngine engine, ImapCommand ic) + { + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "QUOTA", "{0}"); + var quotaRoot = ImapUtils.ReadStringToken (engine, format, ic.CancellationToken); + var ctx = (QuotaContext) ic.UserData; + var quota = new Quota (); + + var token = engine.ReadToken (ic.CancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + while (token.Type != ImapTokenType.CloseParen) { + ulong used, limit; + string resource; + + token = engine.ReadToken (ic.CancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.Atom, format, token); + + resource = (string) token.Value; + + token = engine.ReadToken (ic.CancellationToken); + + // Note: We parse these quota values as UInt64 because GMail uses 64bit integer values. + // See https://github.com/jstedfast/MailKit/issues/1602 for details. + used = ImapEngine.ParseNumber64 (token, false, format, token); + + token = engine.ReadToken (ic.CancellationToken); + + // Note: We parse these quota values as UInt64 because GMail uses 64bit integer values. + // See https://github.com/jstedfast/MailKit/issues/1602 for details. + limit = ImapEngine.ParseNumber64 (token, false, format, token); + + if (resource.Equals ("MESSAGE", StringComparison.OrdinalIgnoreCase)) { + quota.CurrentMessageCount = (uint) (used & 0xffffffff); + quota.MessageLimit = (uint) (limit & 0xffffffff); + } else if (resource.Equals ("STORAGE", StringComparison.OrdinalIgnoreCase)) { + quota.CurrentStorageSize = (uint) (used & 0xffffffff); + quota.StorageLimit = (uint) (limit & 0xffffffff); + } + + token = engine.PeekToken (ic.CancellationToken); } + + // read the closing paren + engine.ReadToken (ic.CancellationToken); + + ctx.Quotas[quotaRoot] = quota; } - static async Task UntaggedQuotaAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + static async Task ParseQuotaAsync (ImapEngine engine, ImapCommand ic) { var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "QUOTA", "{0}"); - var quotaRoot = await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); + var quotaRoot = await ImapUtils.ReadStringTokenAsync (engine, format, ic.CancellationToken).ConfigureAwait (false); var ctx = (QuotaContext) ic.UserData; var quota = new Quota (); - var token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + var token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); @@ -3303,19 +3459,19 @@ static async Task UntaggedQuotaAsync (ImapEngine engine, ImapCommand ic, int ind ulong used, limit; string resource; - token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); ImapEngine.AssertToken (token, ImapTokenType.Atom, format, token); resource = (string) token.Value; - token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); // Note: We parse these quota values as UInt64 because GMail uses 64bit integer values. // See https://github.com/jstedfast/MailKit/issues/1602 for details. used = ImapEngine.ParseNumber64 (token, false, format, token); - token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); // Note: We parse these quota values as UInt64 because GMail uses 64bit integer values. // See https://github.com/jstedfast/MailKit/issues/1602 for details. @@ -3329,15 +3485,33 @@ static async Task UntaggedQuotaAsync (ImapEngine engine, ImapCommand ic, int ind quota.StorageLimit = (uint) (limit & 0xffffffff); } - token = await engine.PeekTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (ic.CancellationToken).ConfigureAwait (false); } // read the closing paren - await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); ctx.Quotas[quotaRoot] = quota; } + /// + /// Handles an untagged QUOTA response. + /// + /// An asynchronous task. + /// The IMAP engine. + /// The IMAP command. + /// The index. + /// Whether or not asynchronous IO methods should be used. + static Task UntaggedQuotaHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + if (doAsync) + return ParseQuotaAsync (engine, ic); + + ParseQuota (engine, ic); + + return Task.CompletedTask; + } + async Task GetQuotaAsync (bool doAsync, CancellationToken cancellationToken) { CheckState (false, false); @@ -3348,8 +3522,8 @@ async Task GetQuotaAsync (bool doAsync, CancellationToken cancellat var ic = new ImapCommand (Engine, cancellationToken, null, "GETQUOTAROOT %F\r\n", this); var ctx = new QuotaContext (); - ic.RegisterUntaggedHandler ("QUOTAROOT", UntaggedQuotaRootAsync); - ic.RegisterUntaggedHandler ("QUOTA", UntaggedQuotaAsync); + ic.RegisterUntaggedHandler ("QUOTAROOT", UntaggedQuotaRootHandler); + ic.RegisterUntaggedHandler ("QUOTA", UntaggedQuotaHandler); ic.UserData = ctx; Engine.QueueCommand (ic); @@ -3483,7 +3657,7 @@ async Task SetQuotaAsync (uint? messageLimit, uint? storageLimit, b var ic = new ImapCommand (Engine, cancellationToken, null, command.ToString (), this); var ctx = new QuotaContext (); - ic.RegisterUntaggedHandler ("QUOTA", UntaggedQuotaAsync); + ic.RegisterUntaggedHandler ("QUOTA", UntaggedQuotaHandler); ic.UserData = ctx; Engine.QueueCommand (ic); @@ -4731,9 +4905,9 @@ async Task> GetIndexesAsync (IList uids, bool doAsync, Canc var results = new SearchResults (SortOrder.Ascending); if ((Engine.Capabilities & ImapCapabilities.ESearch) != 0) - ic.RegisterUntaggedHandler ("ESEARCH", ESearchMatchesAsync); + ic.RegisterUntaggedHandler ("ESEARCH", UntaggedESearchHandler); - ic.RegisterUntaggedHandler ("SEARCH", SearchMatchesAsync); + ic.RegisterUntaggedHandler ("SEARCH", UntaggedSearchHandler); ic.UserData = results; Engine.QueueCommand (ic); @@ -5496,14 +5670,14 @@ void OnFetchAsyncCompleted (MessageSummary message) OnMessageSummaryFetched (message); } - internal Task OnFetchAsync (ImapEngine engine, int index, bool doAsync, CancellationToken cancellationToken) + internal Task OnUntaggedFetchResponse (ImapEngine engine, int index, bool doAsync, CancellationToken cancellationToken) { var message = new MessageSummary (this, index); if (doAsync) - return FetchSummaryItemsAsync (engine, message, OnFetchAsyncCompleted, cancellationToken); + return ParseSummaryItemsAsync (engine, message, OnFetchAsyncCompleted, cancellationToken); - FetchSummaryItems (engine, message, OnFetchAsyncCompleted, cancellationToken); + ParseSummaryItems (engine, message, OnFetchAsyncCompleted, cancellationToken); return Task.CompletedTask; } diff --git a/MailKit/Net/Imap/ImapFolderFetch.cs b/MailKit/Net/Imap/ImapFolderFetch.cs index a913982c24..77d5a1a72a 100644 --- a/MailKit/Net/Imap/ImapFolderFetch.cs +++ b/MailKit/Net/Imap/ImapFolderFetch.cs @@ -236,7 +236,7 @@ static Task SkipParenthesizedListAsync (ImapEngine engine, bool doAsync, Cancell delegate void FetchSummaryItemsCompletedCallback (MessageSummary message); - void FetchSummaryItems (ImapEngine engine, MessageSummary message, FetchSummaryItemsCompletedCallback completed, CancellationToken cancellationToken) + void ParseSummaryItems (ImapEngine engine, MessageSummary message, FetchSummaryItemsCompletedCallback completed, CancellationToken cancellationToken) { var token = engine.ReadToken (cancellationToken); @@ -472,7 +472,7 @@ void FetchSummaryItems (ImapEngine engine, MessageSummary message, FetchSummaryI completed (message); } - async Task FetchSummaryItemsAsync (ImapEngine engine, MessageSummary message, FetchSummaryItemsCompletedCallback completed, CancellationToken cancellationToken) + async Task ParseSummaryItemsAsync (ImapEngine engine, MessageSummary message, FetchSummaryItemsCompletedCallback completed, CancellationToken cancellationToken) { var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); @@ -708,7 +708,7 @@ async Task FetchSummaryItemsAsync (ImapEngine engine, MessageSummary message, Fe completed (message); } - Task FetchSummaryItemsAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + Task UntaggedFetchSummaryItemsHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) { var ctx = (FetchSummaryContext) ic.UserData; @@ -718,9 +718,9 @@ Task FetchSummaryItemsAsync (ImapEngine engine, ImapCommand ic, int index, bool } if (doAsync) - return FetchSummaryItemsAsync (engine, message, OnMessageSummaryFetched, ic.CancellationToken); + return ParseSummaryItemsAsync (engine, message, OnMessageSummaryFetched, ic.CancellationToken); - FetchSummaryItems (engine, message, OnMessageSummaryFetched, ic.CancellationToken); + ParseSummaryItems (engine, message, OnMessageSummaryFetched, ic.CancellationToken); return Task.CompletedTask; } @@ -1173,7 +1173,7 @@ public override IList Fetch (IList uids, IFetchReques try { foreach (var ic in Engine.CreateCommands (cancellationToken, this, command, uids)) { - ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItemsAsync); + ic.RegisterUntaggedHandler ("FETCH", UntaggedFetchSummaryItemsHandler); ic.UserData = ctx; Engine.QueueCommand (ic); @@ -1261,7 +1261,7 @@ public override async Task> FetchAsync (IList u try { foreach (var ic in Engine.CreateCommands (cancellationToken, this, command, uids)) { - ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItemsAsync); + ic.RegisterUntaggedHandler ("FETCH", UntaggedFetchSummaryItemsHandler); ic.UserData = ctx; Engine.QueueCommand (ic); @@ -1313,7 +1313,7 @@ ImapCommand QueueFetchCommand (IList indexes, IFetchRequest request, Cancel var ic = new ImapCommand (Engine, cancellationToken, this, command); var ctx = new FetchSummaryContext (indexes.Count); - ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItemsAsync); + ic.RegisterUntaggedHandler ("FETCH", UntaggedFetchSummaryItemsHandler); ic.UserData = ctx; Engine.QueueCommand (ic); @@ -1511,7 +1511,7 @@ ImapCommand QueueFetchCommand (int min, int max, IFetchRequest request, Cancella var ic = new ImapCommand (Engine, cancellationToken, this, command); var ctx = new FetchSummaryContext (capacity); - ic.RegisterUntaggedHandler ("FETCH", FetchSummaryItemsAsync); + ic.RegisterUntaggedHandler ("FETCH", UntaggedFetchSummaryItemsHandler); ic.UserData = ctx; Engine.QueueCommand (ic); diff --git a/MailKit/Net/Imap/ImapFolderSearch.cs b/MailKit/Net/Imap/ImapFolderSearch.cs index 0942a9f628..dfb55a69d2 100644 --- a/MailKit/Net/Imap/ImapFolderSearch.cs +++ b/MailKit/Net/Imap/ImapFolderSearch.cs @@ -433,64 +433,140 @@ string BuildSortOrder (IList orderBy) return builder.ToString (); } - static async Task SearchMatchesAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + static void ParseESearchResults (ImapEngine engine, ImapCommand ic, SearchResults results) { - var results = (SearchResults) ic.UserData; - var uids = results.UniqueIds; - uint min = uint.MaxValue; - uint uid, max = 0; - ImapToken token; - - do { - token = await engine.PeekTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); - - // keep reading UIDs until we get to the end of the line or until we get a "(MODSEQ ####)" - if (token.Type == ImapTokenType.Eoln || token.Type == ImapTokenType.OpenParen) - break; - - token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); - - uid = ImapEngine.ParseNumber (token, true, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "SEARCH", token); - uids.Add (new UniqueId (ic.Folder.UidValidity, uid)); - min = Math.Min (min, uid); - max = Math.Max (max, uid); - } while (true); + var token = engine.ReadToken (ic.CancellationToken); + UniqueId? minValue = null, maxValue = null; + bool hasCount = false; + int parenDepth = 0; + //bool uid = false; + string atom, tag; if (token.Type == ImapTokenType.OpenParen) { - await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); - + // optional search correlator do { - token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + token = engine.ReadToken (ic.CancellationToken); if (token.Type == ImapTokenType.CloseParen) break; - ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "SEARCH", token); + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); - var atom = (string) token.Value; + atom = (string) token.Value; - if (atom.Equals ("MODSEQ", StringComparison.OrdinalIgnoreCase)) { - token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + if (atom == "TAG") { + token = engine.ReadToken (ic.CancellationToken); - results.ModSeq = ImapEngine.ParseNumber64 (token, false, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + tag = (string) token.Value; + + if (tag != ic.Tag) + throw new ImapProtocolException ("Unexpected TAG value in untagged ESEARCH response: " + tag); } + } while (true); - token = await engine.PeekTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); - } while (token.Type != ImapTokenType.Eoln); + token = engine.ReadToken (ic.CancellationToken); } - results.UniqueIds = uids; - results.Count = uids.Count; - if (uids.Count > 0) { - results.Min = new UniqueId (ic.Folder.UidValidity, min); - results.Max = new UniqueId (ic.Folder.UidValidity, max); + if (token.Type == ImapTokenType.Atom && ((string) token.Value) == "UID") { + token = engine.ReadToken (ic.CancellationToken); + //uid = true; } + + do { + if (token.Type == ImapTokenType.CloseParen) { + if (parenDepth == 0) + throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + token = engine.ReadToken (ic.CancellationToken); + parenDepth--; + } + + if (token.Type == ImapTokenType.Eoln) { + // unget the eoln token + engine.Stream.UngetToken (token); + break; + } + + if (token.Type == ImapTokenType.OpenParen) { + token = engine.ReadToken (ic.CancellationToken); + parenDepth++; + } + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + atom = (string) token.Value; + + token = engine.ReadToken (ic.CancellationToken); + + if (atom.Equals ("RELEVANCY", StringComparison.OrdinalIgnoreCase)) { + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + results.Relevancy = new List (); + + do { + token = engine.ReadToken (ic.CancellationToken); + + if (token.Type == ImapTokenType.CloseParen) + break; + + var score = ImapEngine.ParseNumber (token, true, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + if (score > 100) + throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + results.Relevancy.Add ((byte) score); + } while (true); + } else if (atom.Equals ("MODSEQ", StringComparison.OrdinalIgnoreCase)) { + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + results.ModSeq = ImapEngine.ParseNumber64 (token, false, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } else if (atom.Equals ("COUNT", StringComparison.OrdinalIgnoreCase)) { + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + var count = ImapEngine.ParseNumber (token, false, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + results.Count = (int) count; + hasCount = true; + } else if (atom.Equals ("MIN", StringComparison.OrdinalIgnoreCase)) { + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + var min = ImapEngine.ParseNumber (token, true, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + results.Min = new UniqueId (ic.Folder.UidValidity, min); + } else if (atom.Equals ("MAX", StringComparison.OrdinalIgnoreCase)) { + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + var max = ImapEngine.ParseNumber (token, true, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + results.Max = new UniqueId (ic.Folder.UidValidity, max); + } else if (atom.Equals ("ALL", StringComparison.OrdinalIgnoreCase)) { + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + + var uids = ImapEngine.ParseUidSet (token, ic.Folder.UidValidity, out minValue, out maxValue, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + + if (!hasCount) + results.Count = uids.Count; + + results.UniqueIds = uids; + } else { + throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); + } + + token = engine.ReadToken (ic.CancellationToken); + } while (true); + + if (!results.Min.HasValue) + results.Min = minValue; + + if (!results.Max.HasValue) + results.Max = maxValue; } - static async Task ESearchMatchesAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + static async Task ParseESearchResultsAsync (ImapEngine engine, ImapCommand ic, SearchResults results) { - var token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); - var results = (SearchResults) ic.UserData; + var token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); UniqueId? minValue = null, maxValue = null; bool hasCount = false; int parenDepth = 0; @@ -500,7 +576,7 @@ static async Task ESearchMatchesAsync (ImapEngine engine, ImapCommand ic, int in if (token.Type == ImapTokenType.OpenParen) { // optional search correlator do { - token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); if (token.Type == ImapTokenType.CloseParen) break; @@ -510,7 +586,7 @@ static async Task ESearchMatchesAsync (ImapEngine engine, ImapCommand ic, int in atom = (string) token.Value; if (atom == "TAG") { - token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); @@ -521,11 +597,11 @@ static async Task ESearchMatchesAsync (ImapEngine engine, ImapCommand ic, int in } } while (true); - token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); } if (token.Type == ImapTokenType.Atom && ((string) token.Value) == "UID") { - token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); //uid = true; } @@ -534,7 +610,7 @@ static async Task ESearchMatchesAsync (ImapEngine engine, ImapCommand ic, int in if (parenDepth == 0) throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); - token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); parenDepth--; } @@ -545,7 +621,7 @@ static async Task ESearchMatchesAsync (ImapEngine engine, ImapCommand ic, int in } if (token.Type == ImapTokenType.OpenParen) { - token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); parenDepth++; } @@ -553,7 +629,7 @@ static async Task ESearchMatchesAsync (ImapEngine engine, ImapCommand ic, int in atom = (string) token.Value; - token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); if (atom.Equals ("RELEVANCY", StringComparison.OrdinalIgnoreCase)) { ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); @@ -561,7 +637,7 @@ static async Task ESearchMatchesAsync (ImapEngine engine, ImapCommand ic, int in results.Relevancy = new List (); do { - token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); if (token.Type == ImapTokenType.CloseParen) break; @@ -609,7 +685,7 @@ static async Task ESearchMatchesAsync (ImapEngine engine, ImapCommand ic, int in throw ImapEngine.UnexpectedToken (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ESEARCH", token); } - token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); } while (true); if (!results.Min.HasValue) @@ -619,6 +695,136 @@ static async Task ESearchMatchesAsync (ImapEngine engine, ImapCommand ic, int in results.Max = maxValue; } + static Task UntaggedESearchHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + var results = (SearchResults) ic.UserData; + + if (doAsync) + return ParseESearchResultsAsync (engine, ic, results); + + ParseESearchResults (engine, ic, results); + + return Task.CompletedTask; + } + + static void ParseSearchResults (ImapEngine engine, ImapCommand ic, SearchResults results) + { + var uids = results.UniqueIds; + uint min = uint.MaxValue; + uint uid, max = 0; + ImapToken token; + + do { + token = engine.PeekToken (ic.CancellationToken); + + // keep reading UIDs until we get to the end of the line or until we get a "(MODSEQ ####)" + if (token.Type == ImapTokenType.Eoln || token.Type == ImapTokenType.OpenParen) + break; + + token = engine.ReadToken (ic.CancellationToken); + + uid = ImapEngine.ParseNumber (token, true, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "SEARCH", token); + uids.Add (new UniqueId (ic.Folder.UidValidity, uid)); + min = Math.Min (min, uid); + max = Math.Max (max, uid); + } while (true); + + if (token.Type == ImapTokenType.OpenParen) { + engine.ReadToken (ic.CancellationToken); + + do { + token = engine.ReadToken (ic.CancellationToken); + + if (token.Type == ImapTokenType.CloseParen) + break; + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "SEARCH", token); + + var atom = (string) token.Value; + + if (atom.Equals ("MODSEQ", StringComparison.OrdinalIgnoreCase)) { + token = engine.ReadToken (ic.CancellationToken); + + results.ModSeq = ImapEngine.ParseNumber64 (token, false, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } + + token = engine.PeekToken (ic.CancellationToken); + } while (token.Type != ImapTokenType.Eoln); + } + + results.UniqueIds = uids; + results.Count = uids.Count; + if (uids.Count > 0) { + results.Min = new UniqueId (ic.Folder.UidValidity, min); + results.Max = new UniqueId (ic.Folder.UidValidity, max); + } + } + + static async Task ParseSearchResultsAsync (ImapEngine engine, ImapCommand ic, SearchResults results) + { + var uids = results.UniqueIds; + uint min = uint.MaxValue; + uint uid, max = 0; + ImapToken token; + + do { + token = await engine.PeekTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + // keep reading UIDs until we get to the end of the line or until we get a "(MODSEQ ####)" + if (token.Type == ImapTokenType.Eoln || token.Type == ImapTokenType.OpenParen) + break; + + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + uid = ImapEngine.ParseNumber (token, true, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "SEARCH", token); + uids.Add (new UniqueId (ic.Folder.UidValidity, uid)); + min = Math.Min (min, uid); + max = Math.Max (max, uid); + } while (true); + + if (token.Type == ImapTokenType.OpenParen) { + await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + do { + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "SEARCH", token); + + var atom = (string) token.Value; + + if (atom.Equals ("MODSEQ", StringComparison.OrdinalIgnoreCase)) { + token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + results.ModSeq = ImapEngine.ParseNumber64 (token, false, ImapEngine.GenericItemSyntaxErrorFormat, atom, token); + } + + token = await engine.PeekTokenAsync (ic.CancellationToken).ConfigureAwait (false); + } while (token.Type != ImapTokenType.Eoln); + } + + results.UniqueIds = uids; + results.Count = uids.Count; + if (uids.Count > 0) { + results.Min = new UniqueId (ic.Folder.UidValidity, min); + results.Max = new UniqueId (ic.Folder.UidValidity, max); + } + } + + static Task UntaggedSearchHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + var results = (SearchResults) ic.UserData; + + if (doAsync) + return ParseSearchResultsAsync (engine, ic, results); + + ParseSearchResults (engine, ic, results); + + return Task.CompletedTask; + } + async Task SearchAsync (string query, bool doAsync, CancellationToken cancellationToken) { if (query == null) @@ -634,12 +840,12 @@ async Task SearchAsync (string query, bool doAsync, CancellationT var command = "UID SEARCH " + query + "\r\n"; var ic = new ImapCommand (Engine, cancellationToken, this, command); if ((Engine.Capabilities & ImapCapabilities.ESearch) != 0) - ic.RegisterUntaggedHandler ("ESEARCH", ESearchMatchesAsync); + ic.RegisterUntaggedHandler ("ESEARCH", UntaggedESearchHandler); // Note: always register the untagged SEARCH handler because some servers will brokenly // respond with "* SEARCH ..." instead of "* ESEARCH ..." even when using the extended // search syntax. - ic.RegisterUntaggedHandler ("SEARCH", SearchMatchesAsync); + ic.RegisterUntaggedHandler ("SEARCH", UntaggedSearchHandler); ic.UserData = new SearchResults (UidValidity, SortOrder.Ascending); Engine.QueueCommand (ic); @@ -795,12 +1001,12 @@ async Task SearchAsync (SearchOptions options, SearchQuery query, }; if ((Engine.Capabilities & ImapCapabilities.ESearch) != 0) - ic.RegisterUntaggedHandler ("ESEARCH", ESearchMatchesAsync); + ic.RegisterUntaggedHandler ("ESEARCH", UntaggedESearchHandler); // Note: always register the untagged SEARCH handler because some servers will brokenly // respond with "* SEARCH ..." instead of "* ESEARCH ..." even when using the extended // search syntax. - ic.RegisterUntaggedHandler ("SEARCH", SearchMatchesAsync); + ic.RegisterUntaggedHandler ("SEARCH", UntaggedSearchHandler); Engine.QueueCommand (ic); @@ -932,8 +1138,8 @@ async Task SortAsync (string query, bool doAsync, CancellationTok var command = "UID SORT " + query + "\r\n"; var ic = new ImapCommand (Engine, cancellationToken, this, command); if ((Engine.Capabilities & ImapCapabilities.ESort) != 0) - ic.RegisterUntaggedHandler ("ESEARCH", ESearchMatchesAsync); - ic.RegisterUntaggedHandler ("SORT", SearchMatchesAsync); + ic.RegisterUntaggedHandler ("ESEARCH", UntaggedESearchHandler); + ic.RegisterUntaggedHandler ("SORT", UntaggedSearchHandler); ic.UserData = new SearchResults (UidValidity); Engine.QueueCommand (ic); @@ -1079,9 +1285,9 @@ async Task> SortAsync (SearchQuery query, IList orderBy var ic = new ImapCommand (Engine, cancellationToken, this, command, args.ToArray ()); if ((Engine.Capabilities & ImapCapabilities.ESort) != 0) - ic.RegisterUntaggedHandler ("ESEARCH", ESearchMatchesAsync); + ic.RegisterUntaggedHandler ("ESEARCH", UntaggedESearchHandler); else - ic.RegisterUntaggedHandler ("SORT", SearchMatchesAsync); + ic.RegisterUntaggedHandler ("SORT", UntaggedSearchHandler); ic.UserData = new SearchResults (UidValidity); Engine.QueueCommand (ic); @@ -1257,9 +1463,9 @@ async Task SortAsync (SearchOptions options, SearchQuery query, I }; if ((Engine.Capabilities & ImapCapabilities.ESort) != 0) - ic.RegisterUntaggedHandler ("ESEARCH", ESearchMatchesAsync); + ic.RegisterUntaggedHandler ("ESEARCH", UntaggedESearchHandler); else - ic.RegisterUntaggedHandler ("SORT", SearchMatchesAsync); + ic.RegisterUntaggedHandler ("SORT", UntaggedSearchHandler); Engine.QueueCommand (ic); @@ -1383,11 +1589,6 @@ public override Task SortAsync (SearchOptions options, SearchQuer return SortAsync (options, query, orderBy, true, true, cancellationToken); } - static async Task ThreadMatchesAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) - { - ic.UserData = await ImapUtils.ParseThreadsAsync (engine, ic.Folder.UidValidity, doAsync, ic.CancellationToken).ConfigureAwait (false); - } - async Task> ThreadAsync (ThreadingAlgorithm algorithm, SearchQuery query, bool doAsync, bool retry, CancellationToken cancellationToken) { var method = algorithm.ToString ().ToUpperInvariant (); @@ -1412,7 +1613,7 @@ async Task> ThreadAsync (ThreadingAlgorithm algorithm, Sear command += expr + "\r\n"; var ic = new ImapCommand (Engine, cancellationToken, this, command, args.ToArray ()); - ic.RegisterUntaggedHandler ("THREAD", ThreadMatchesAsync); + ic.RegisterUntaggedHandler ("THREAD", ImapUtils.UntaggedThreadHandler); Engine.QueueCommand (ic); @@ -1568,7 +1769,7 @@ async Task> ThreadAsync (IList uids, ThreadingAlg command += "UID " + set + " " + expr + "\r\n"; var ic = new ImapCommand (Engine, cancellationToken, this, command, args.ToArray ()); - ic.RegisterUntaggedHandler ("THREAD", ThreadMatchesAsync); + ic.RegisterUntaggedHandler ("THREAD", ImapUtils.UntaggedThreadHandler); Engine.QueueCommand (ic); diff --git a/MailKit/Net/Imap/ImapUtils.cs b/MailKit/Net/Imap/ImapUtils.cs index 1d25f888c5..a210a3abe4 100644 --- a/MailKit/Net/Imap/ImapUtils.cs +++ b/MailKit/Net/Imap/ImapUtils.cs @@ -337,12 +337,10 @@ public static string FormatIndexSet (ImapEngine engine, IList indexes) /// /// The IMAP engine. /// The IMAP command. - /// The index. - /// Whether or not asynchronous IO methods should be used. - public static async Task ParseImplementationAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + static void ParseImplementation (ImapEngine engine, ImapCommand ic) { var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ID", "{0}"); - var token = await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + var token = engine.ReadToken (ic.CancellationToken); ImapImplementation implementation; if (token.Type == ImapTokenType.Nil) @@ -350,23 +348,76 @@ public static async Task ParseImplementationAsync (ImapEngine engine, ImapComman ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); - token = await engine.PeekTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + token = engine.PeekToken (ic.CancellationToken); implementation = new ImapImplementation (); while (token.Type != ImapTokenType.CloseParen) { - var property = await ReadStringTokenAsync (engine, format, doAsync, ic.CancellationToken).ConfigureAwait (false); - var value = await ReadNStringTokenAsync (engine, format, false, doAsync, ic.CancellationToken).ConfigureAwait (false); + var property = ReadStringToken (engine, format, ic.CancellationToken); + var value = ReadNStringToken (engine, format, false, ic.CancellationToken); implementation.Properties[property] = value; - token = await engine.PeekTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + token = engine.PeekToken (ic.CancellationToken); } ic.UserData = implementation; // read the ')' token - await engine.ReadTokenAsync (doAsync, ic.CancellationToken).ConfigureAwait (false); + engine.ReadToken (ic.CancellationToken); + } + + /// + /// Asynchronously parses an untagged ID response. + /// + /// The IMAP engine. + /// The IMAP command. + static async Task ParseImplementationAsync (ImapEngine engine, ImapCommand ic) + { + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "ID", "{0}"); + var token = await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + ImapImplementation implementation; + + if (token.Type == ImapTokenType.Nil) + return; + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + token = await engine.PeekTokenAsync (ic.CancellationToken).ConfigureAwait (false); + + implementation = new ImapImplementation (); + + while (token.Type != ImapTokenType.CloseParen) { + var property = await ReadStringTokenAsync (engine, format, ic.CancellationToken).ConfigureAwait (false); + var value = await ReadNStringTokenAsync (engine, format, false, ic.CancellationToken).ConfigureAwait (false); + + implementation.Properties[property] = value; + + token = await engine.PeekTokenAsync (ic.CancellationToken).ConfigureAwait (false); + } + + ic.UserData = implementation; + + // read the ')' token + await engine.ReadTokenAsync (ic.CancellationToken).ConfigureAwait (false); + } + + /// + /// Handles an untagged ID response. + /// + /// An asynchronous task. + /// The IMAP engine. + /// The IMAP command. + /// The index. + /// Whether or not asynchronous IO methods should be used. + public static Task UntaggedIdHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + if (doAsync) + return ParseImplementationAsync (engine, ic); + + ParseImplementation (engine, ic); + + return Task.CompletedTask; } /// @@ -403,14 +454,49 @@ public static bool IsInbox (string mailboxName) return string.Compare (mailboxName, "INBOX", StringComparison.OrdinalIgnoreCase) == 0; } - static async Task ReadFolderNameAsync (ImapEngine engine, char delim, string format, bool doAsync, CancellationToken cancellationToken) + static string ReadFolderName (ImapEngine engine, char delim, string format, CancellationToken cancellationToken) + { + var token = engine.ReadToken (ImapStream.AtomSpecials, cancellationToken); + string encodedName; + + switch (token.Type) { + case ImapTokenType.Literal: + encodedName = engine.ReadLiteral (cancellationToken); + break; + case ImapTokenType.QString: + case ImapTokenType.Atom: + encodedName = (string) token.Value; + + // Note: Exchange apparently doesn't quote folder names that contain tabs. + // + // See https://github.com/jstedfast/MailKit/issues/945 for details. + if (engine.QuirksMode == ImapQuirksMode.Exchange) { + var line = engine.ReadLine (cancellationToken); + + // unget the \r\n sequence + engine.Stream.UngetToken (ImapToken.Eoln); + + encodedName += line; + } + break; + case ImapTokenType.Nil: + // Note: according to rfc3501, section 4.5, NIL is acceptable as a mailbox name. + return "NIL"; + default: + throw ImapEngine.UnexpectedToken (format, token); + } + + return encodedName.TrimEnd (delim); + } + + static async Task ReadFolderNameAsync (ImapEngine engine, char delim, string format, CancellationToken cancellationToken) { - var token = await engine.ReadTokenAsync (ImapStream.AtomSpecials, doAsync, cancellationToken).ConfigureAwait (false); + var token = await engine.ReadTokenAsync (ImapStream.AtomSpecials, cancellationToken).ConfigureAwait (false); string encodedName; switch (token.Type) { case ImapTokenType.Literal: - encodedName = await engine.ReadLiteralAsync (doAsync, cancellationToken).ConfigureAwait (false); + encodedName = await engine.ReadLiteralAsync (cancellationToken).ConfigureAwait (false); break; case ImapTokenType.QString: case ImapTokenType.Atom: @@ -420,7 +506,7 @@ static async Task ReadFolderNameAsync (ImapEngine engine, char delim, st // // See https://github.com/jstedfast/MailKit/issues/945 for details. if (engine.QuirksMode == ImapQuirksMode.Exchange) { - var line = await engine.ReadLineAsync (doAsync, cancellationToken).ConfigureAwait (false); + var line = await engine.ReadLineAsync (cancellationToken).ConfigureAwait (false); // unget the \r\n sequence engine.Stream.UngetToken (ImapToken.Eoln); @@ -438,6 +524,90 @@ static async Task ReadFolderNameAsync (ImapEngine engine, char delim, st return encodedName.TrimEnd (delim); } + static void AddFolderAttribute (ref FolderAttributes attrs, string atom) + { + if (atom.Equals ("\\noinferiors", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.NoInferiors; + else if (atom.Equals ("\\noselect", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.NoSelect; + else if (atom.Equals ("\\marked", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Marked; + else if (atom.Equals ("\\unmarked", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Unmarked; + else if (atom.Equals ("\\nonexistent", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.NonExistent; + else if (atom.Equals ("\\subscribed", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Subscribed; + else if (atom.Equals ("\\remote", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Remote; + else if (atom.Equals ("\\haschildren", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.HasChildren; + else if (atom.Equals ("\\hasnochildren", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.HasNoChildren; + else if (atom.Equals ("\\all", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.All; + else if (atom.Equals ("\\archive", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Archive; + else if (atom.Equals ("\\drafts", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Drafts; + else if (atom.Equals ("\\flagged", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Flagged; + else if (atom.Equals ("\\important", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Important; + else if (atom.Equals ("\\junk", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Junk; + else if (atom.Equals ("\\sent", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Sent; + else if (atom.Equals ("\\trash", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Trash; + // XLIST flags: + else if (atom.Equals ("\\allmail", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.All; + else if (atom.Equals ("\\inbox", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Inbox; + else if (atom.Equals ("\\spam", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Junk; + else if (atom.Equals ("\\starred", StringComparison.OrdinalIgnoreCase)) + attrs |= FolderAttributes.Flagged; + } + + static void AddFolder (ImapEngine engine, List list, ImapFolder folder, string encodedName, char delim, FolderAttributes attrs, bool isLsub, bool returnsSubscribed) + { + if (folder != null || engine.TryGetCachedFolder (encodedName, out folder)) { + if ((attrs & FolderAttributes.NonExistent) != 0) { + folder.UnsetPermanentFlags (); + folder.UnsetAcceptedFlags (); + folder.UpdateUidNext (UniqueId.Invalid); + folder.UpdateHighestModSeq (0); + folder.UpdateUidValidity (0); + folder.UpdateUnread (0); + } + + if (isLsub) { + // Note: merge all pre-existing attributes since the LSUB response will not contain them + attrs |= folder.Attributes | FolderAttributes.Subscribed; + } else { + // Note: only merge the SPECIAL-USE and \Subscribed attributes for a LIST command + attrs |= folder.Attributes & SpecialUseAttributes; + + // Note: only merge \Subscribed if the LIST command isn't expected to include it + if (!returnsSubscribed) + attrs |= folder.Attributes & FolderAttributes.Subscribed; + } + + folder.UpdateAttributes (attrs); + } else { + folder = engine.CreateImapFolder (encodedName, attrs, delim); + engine.CacheFolder (folder); + + if (list == null) + engine.OnFolderCreated (folder); + } + + // Note: list will be null if this is an unsolicited LIST response due to an active NOTIFY request + list?.Add (folder); + } + /// /// Parses an untagged LIST or LSUB response. /// @@ -445,12 +615,11 @@ static async Task ReadFolderNameAsync (ImapEngine engine, char delim, st /// The list of folders to be populated. /// true if it is an LSUB response; otherwise, false. /// true if the LIST response is expected to return \Subscribed flags; otherwise, false. - /// Whether or not asynchronous IO methods should be used. /// The cancellation token. - public static async Task ParseFolderListAsync (ImapEngine engine, List list, bool isLsub, bool returnsSubscribed, bool doAsync, CancellationToken cancellationToken) + public static void ParseFolderList (ImapEngine engine, List list, bool isLsub, bool returnsSubscribed, CancellationToken cancellationToken) { var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, isLsub ? "LSUB" : "LIST", "{0}"); - var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + var token = engine.ReadToken (cancellationToken); var attrs = FolderAttributes.None; ImapFolder folder = null; string encodedName; @@ -459,62 +628,20 @@ public static async Task ParseFolderListAsync (ImapEngine engine, List + /// Asynchronously parses an untagged LIST or LSUB response. + /// + /// The IMAP engine. + /// The list of folders to be populated. + /// true if it is an LSUB response; otherwise, false. + /// true if the LIST response is expected to return \Subscribed flags; otherwise, false. + /// The cancellation token. + public static async Task ParseFolderListAsync (ImapEngine engine, List list, bool isLsub, bool returnsSubscribed, CancellationToken cancellationToken) + { + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, isLsub ? "LSUB" : "LIST", "{0}"); + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + var attrs = FolderAttributes.None; + ImapFolder folder = null; + string encodedName; + char delim; - folder.UpdateAttributes (attrs); + // parse the folder attributes list + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + while (token.Type == ImapTokenType.Flag || token.Type == ImapTokenType.Atom) { + var atom = (string) token.Value; + + AddFolderAttribute (ref attrs, atom); + + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + } + + ImapEngine.AssertToken (token, ImapTokenType.CloseParen, format, token); + + // parse the path delimeter + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.QString) { + var qstring = (string) token.Value; + + delim = qstring[0]; + } else if (token.Type == ImapTokenType.Nil) { + delim = '\0'; } else { - folder = engine.CreateImapFolder (encodedName, attrs, delim); - engine.CacheFolder (folder); + throw ImapEngine.UnexpectedToken (format, token); + } - if (list == null) - engine.OnFolderCreated (folder); + encodedName = await ReadFolderNameAsync (engine, delim, format, cancellationToken).ConfigureAwait (false); + + if (IsInbox (encodedName)) + attrs |= FolderAttributes.Inbox; + + // peek at the next token to see if we have a LIST extension + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.OpenParen) { + var renamed = false; + + // read the '(' token + await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + do { + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + // a LIST extension + + ImapEngine.AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, format, token); + + var atom = (string) token.Value; + + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + do { + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + engine.Stream.UngetToken (token); + + if (!renamed && atom.Equals ("OLDNAME", StringComparison.OrdinalIgnoreCase)) { + var oldEncodedName = await ReadFolderNameAsync (engine, delim, format, cancellationToken).ConfigureAwait (false); + + if (engine.FolderCache.TryGetValue (oldEncodedName, out ImapFolder oldFolder)) { + var args = new ImapFolderConstructorArgs (engine, encodedName, attrs, delim); + + engine.FolderCache.Remove (oldEncodedName); + engine.FolderCache[encodedName] = oldFolder; + oldFolder.OnRenamed (args); + folder = oldFolder; + } + + renamed = true; + } else { + await ReadNStringTokenAsync (engine, format, false, cancellationToken).ConfigureAwait (false); + } + } while (true); + } while (true); + } else { + ImapEngine.AssertToken (token, ImapTokenType.Eoln, format, token); } - // Note: list will be null if this is an unsolicited LIST response due to an active NOTIFY request - list?.Add (folder); + AddFolder (engine, list, folder, encodedName, delim, attrs, isLsub, returnsSubscribed); } /// - /// Parses an untagged LIST or LSUB response. + /// Handles an untagged LIST or LSUB response. /// /// The IMAP engine. /// The IMAP command. /// The index. /// Whether or not asynchronous IO methods should be used. - public static Task ParseFolderListAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + public static Task UntaggedListHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) { var list = (List) ic.UserData; - return ParseFolderListAsync (engine, list, ic.Lsub, ic.ListReturnsSubscribed, doAsync, ic.CancellationToken); + if (doAsync) + return ParseFolderListAsync (engine, list, ic.Lsub, ic.ListReturnsSubscribed, ic.CancellationToken); + + ParseFolderList (engine, list, ic.Lsub, ic.ListReturnsSubscribed, ic.CancellationToken); + + return Task.CompletedTask; } /// @@ -641,42 +850,75 @@ public static Task ParseFolderListAsync (ImapEngine engine, ImapCommand ic, int /// The encoded name of the folder that the metadata belongs to. /// The IMAP engine. /// The metadata collection to be populated. - /// Whether or not asynchronous IO methods should be used. /// The cancellation token. - public static async Task ParseMetadataAsync (ImapEngine engine, MetadataCollection metadata, bool doAsync, CancellationToken cancellationToken) + public static void ParseMetadata (ImapEngine engine, MetadataCollection metadata, CancellationToken cancellationToken) { var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "METADATA", "{0}"); - var encodedName = await ReadStringTokenAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + var encodedName = ReadStringToken (engine, format, cancellationToken); - var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + var token = engine.ReadToken (cancellationToken); ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); while (token.Type != ImapTokenType.CloseParen) { - var tag = await ReadStringTokenAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); - var value = await ReadStringTokenAsync (engine, format, doAsync, cancellationToken).ConfigureAwait (false); + var tag = ReadStringToken (engine, format, cancellationToken); + var value = ReadStringToken (engine, format, cancellationToken); metadata.Add (new Metadata (MetadataTag.Create (tag), value) { EncodedName = encodedName }); - token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + token = engine.PeekToken (cancellationToken); } // read the closing paren - await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + engine.ReadToken (cancellationToken); } /// - /// Parses an untagged METADATA response. + /// Asynchronously parses an untagged METADATA response. + /// + /// The encoded name of the folder that the metadata belongs to. + /// The IMAP engine. + /// The metadata collection to be populated. + /// The cancellation token. + public static async Task ParseMetadataAsync (ImapEngine engine, MetadataCollection metadata, CancellationToken cancellationToken) + { + var format = string.Format (ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "METADATA", "{0}"); + var encodedName = await ReadStringTokenAsync (engine, format, cancellationToken).ConfigureAwait (false); + + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, format, token); + + while (token.Type != ImapTokenType.CloseParen) { + var tag = await ReadStringTokenAsync (engine, format, cancellationToken).ConfigureAwait (false); + var value = await ReadStringTokenAsync (engine, format, cancellationToken).ConfigureAwait (false); + + metadata.Add (new Metadata (MetadataTag.Create (tag), value) { EncodedName = encodedName }); + + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); + } + + // read the closing paren + await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + } + + /// + /// Handles an untagged METADATA response. /// /// The IMAP engine. /// The IMAP command. /// The index. /// Whether or not asynchronous IO methods should be used. - public static Task ParseMetadataAsync (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + public static Task UntaggedMetadataHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) { var metadata = (MetadataCollection) ic.UserData; - return ParseMetadataAsync (engine, metadata, doAsync, ic.CancellationToken); + if (doAsync) + return ParseMetadataAsync (engine, metadata, ic.CancellationToken); + + ParseMetadata (engine, metadata, ic.CancellationToken); + + return Task.CompletedTask; } internal static string ReadStringToken (ImapEngine engine, string format, CancellationToken cancellationToken) @@ -1834,9 +2076,9 @@ public static async ValueTask> ParseLabelsListAsync ( return new ReadOnlyCollection (labels); } - static async Task ParseThreadAsync (ImapEngine engine, uint uidValidity, bool doAsync, CancellationToken cancellationToken) + static MessageThread ParseThread (ImapEngine engine, uint uidValidity, CancellationToken cancellationToken) { - var token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + var token = engine.ReadToken (cancellationToken); MessageThread thread, node, child; uint uid; @@ -1844,10 +2086,10 @@ static async Task ParseThreadAsync (ImapEngine engine, uint uidVa thread = new MessageThread ((UniqueId?) null /*UniqueId.Invalid*/); do { - child = await ParseThreadAsync (engine, uidValidity, doAsync, cancellationToken).ConfigureAwait (false); + child = ParseThread (engine, uidValidity, cancellationToken); thread.Children.Add (child); - token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + token = engine.ReadToken (cancellationToken); } while (token.Type != ImapTokenType.CloseParen); return thread; @@ -1857,13 +2099,13 @@ static async Task ParseThreadAsync (ImapEngine engine, uint uidVa node = thread = new MessageThread (new UniqueId (uidValidity, uid)); do { - token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + token = engine.ReadToken (cancellationToken); if (token.Type == ImapTokenType.CloseParen) break; if (token.Type == ImapTokenType.OpenParen) { - child = await ParseThreadAsync (engine, uidValidity, doAsync, cancellationToken).ConfigureAwait (false); + child = ParseThread (engine, uidValidity, cancellationToken); node.Children.Add (child); } else { uid = ImapEngine.ParseNumber (token, true, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "THREAD", token); @@ -1876,33 +2118,120 @@ static async Task ParseThreadAsync (ImapEngine engine, uint uidVa return thread; } + static async Task ParseThreadAsync (ImapEngine engine, uint uidValidity, CancellationToken cancellationToken) + { + var token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + MessageThread thread, node, child; + uint uid; + + if (token.Type == ImapTokenType.OpenParen) { + thread = new MessageThread ((UniqueId?) null /*UniqueId.Invalid*/); + + do { + child = await ParseThreadAsync (engine, uidValidity, cancellationToken).ConfigureAwait (false); + thread.Children.Add (child); + + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + } while (token.Type != ImapTokenType.CloseParen); + + return thread; + } + + uid = ImapEngine.ParseNumber (token, true, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "THREAD", token); + node = thread = new MessageThread (new UniqueId (uidValidity, uid)); + + do { + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); + + if (token.Type == ImapTokenType.CloseParen) + break; + + if (token.Type == ImapTokenType.OpenParen) { + child = await ParseThreadAsync (engine, uidValidity, cancellationToken).ConfigureAwait (false); + node.Children.Add (child); + } else { + uid = ImapEngine.ParseNumber (token, true, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "THREAD", token); + child = new MessageThread (new UniqueId (uidValidity, uid)); + node.Children.Add (child); + node = child; + } + } while (true); + + return thread; + } + + /// + /// Parses an untagged THREAD response. + /// + /// The task. + /// The IMAP engine. + /// The UIDVALIDITY of the folder. + /// The list of threads that this method will append to. + /// The cancellation token. + public static void ParseThreads (ImapEngine engine, uint uidValidity, List threads, CancellationToken cancellationToken) + { + ImapToken token; + + do { + token = engine.PeekToken (cancellationToken); + + if (token.Type == ImapTokenType.Eoln) + break; + + token = engine.ReadToken (cancellationToken); + + ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "THREAD", token); + + threads.Add (ParseThread (engine, uidValidity, cancellationToken)); + } while (true); + } + /// /// Parses the threads. /// - /// The threads. + /// The task. /// The IMAP engine. /// The UIDVALIDITY of the folder. /// Whether or not asynchronous IO methods should be used. /// The cancellation token. - public static async Task> ParseThreadsAsync (ImapEngine engine, uint uidValidity, bool doAsync, CancellationToken cancellationToken) + public static async Task ParseThreadsAsync (ImapEngine engine, uint uidValidity, List threads, CancellationToken cancellationToken) { - var threads = new List (); ImapToken token; do { - token = await engine.PeekTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + token = await engine.PeekTokenAsync (cancellationToken).ConfigureAwait (false); if (token.Type == ImapTokenType.Eoln) break; - token = await engine.ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false); + token = await engine.ReadTokenAsync (cancellationToken).ConfigureAwait (false); ImapEngine.AssertToken (token, ImapTokenType.OpenParen, ImapEngine.GenericUntaggedResponseSyntaxErrorFormat, "THREAD", token); - threads.Add (await ParseThreadAsync (engine, uidValidity, doAsync, cancellationToken).ConfigureAwait (false)); + threads.Add (await ParseThreadAsync (engine, uidValidity, cancellationToken).ConfigureAwait (false)); } while (true); + } + + /// + /// Handles an untagged THREAD response. + /// + /// The task. + /// The IMAP engine. + /// The IMAP command. + /// THe index. + /// Whether or not asynchronous IO methods should be used. + public static Task UntaggedThreadHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync) + { + var threads = new List (); + + ic.UserData = threads; + + if (doAsync) + return ParseThreadsAsync (engine, ic.Folder.UidValidity, threads, ic.CancellationToken); + + ParseThreads (engine, ic.Folder.UidValidity, threads, ic.CancellationToken); - return threads; + return Task.CompletedTask; } } } diff --git a/UnitTests/Net/Imap/ImapUtilsTests.cs b/UnitTests/Net/Imap/ImapUtilsTests.cs index 2301f59a5b..dc52ade59b 100644 --- a/UnitTests/Net/Imap/ImapUtilsTests.cs +++ b/UnitTests/Net/Imap/ImapUtilsTests.cs @@ -3523,12 +3523,12 @@ public void TestParseExampleThreads () using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { using (var engine = new ImapEngine (null)) { - IList threads; + var threads = new List (); engine.SetStream (tokenizer); try { - threads = ImapUtils.ParseThreadsAsync (engine, 0, false, CancellationToken.None).GetAwaiter ().GetResult (); + ImapUtils.ParseThreads (engine, 0, threads, CancellationToken.None); } catch (Exception ex) { Assert.Fail ("Parsing THREAD response failed: {0}", ex); return; @@ -3578,12 +3578,12 @@ public async Task TestParseExampleThreadsAsync () using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { using (var engine = new ImapEngine (null)) { - IList threads; + var threads = new List (); engine.SetStream (tokenizer); try { - threads = await ImapUtils.ParseThreadsAsync (engine, 0, true, CancellationToken.None); + await ImapUtils.ParseThreadsAsync (engine, 0, threads, CancellationToken.None); } catch (Exception ex) { Assert.Fail ("Parsing THREAD response failed: {0}", ex); return; @@ -3633,12 +3633,12 @@ public void TestParseLongDovecotExampleThread () using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { using (var engine = new ImapEngine (null)) { - IList threads; + var threads = new List (); engine.SetStream (tokenizer); try { - threads = ImapUtils.ParseThreadsAsync (engine, 0, false, CancellationToken.None).GetAwaiter ().GetResult (); + ImapUtils.ParseThreads (engine, 0, threads, CancellationToken.None); } catch (Exception ex) { Assert.Fail ("Parsing THREAD response failed: {0}", ex); return; @@ -3669,12 +3669,12 @@ public async Task TestParseLongDovecotExampleThreadAsync () using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { using (var engine = new ImapEngine (null)) { - IList threads; + var threads = new List (); engine.SetStream (tokenizer); try { - threads = await ImapUtils.ParseThreadsAsync (engine, 0, true, CancellationToken.None); + await ImapUtils.ParseThreadsAsync (engine, 0, threads, CancellationToken.None); } catch (Exception ex) { Assert.Fail ("Parsing THREAD response failed: {0}", ex); return; @@ -3705,12 +3705,12 @@ public void TestParseShortDovecotExampleThread () using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { using (var engine = new ImapEngine (null)) { - IList threads; + var threads = new List (); engine.SetStream (tokenizer); try { - threads = ImapUtils.ParseThreadsAsync (engine, 0, false, CancellationToken.None).GetAwaiter ().GetResult (); + ImapUtils.ParseThreads (engine, 0, threads, CancellationToken.None); } catch (Exception ex) { Assert.Fail ("Parsing THREAD response failed: {0}", ex); return; @@ -3742,12 +3742,12 @@ public async Task TestParseShortDovecotExampleThreadAsync () using (var memory = new MemoryStream (Encoding.ASCII.GetBytes (text), false)) { using (var tokenizer = new ImapStream (memory, new NullProtocolLogger ())) { using (var engine = new ImapEngine (null)) { - IList threads; + var threads = new List (); engine.SetStream (tokenizer); try { - threads = await ImapUtils.ParseThreadsAsync (engine, 0, true, CancellationToken.None); + await ImapUtils.ParseThreadsAsync (engine, 0, threads, CancellationToken.None); } catch (Exception ex) { Assert.Fail ("Parsing THREAD response failed: {0}", ex); return; @@ -4020,7 +4020,7 @@ public void TestParseFolderListWithFolderNameContainingUnquotedTabs () engine.SetStream (tokenizer); try { - ImapUtils.ParseFolderListAsync (engine, list, false, false, false, CancellationToken.None).GetAwaiter ().GetResult (); + ImapUtils.ParseFolderList (engine, list, false, false, CancellationToken.None); } catch (Exception ex) { Assert.Fail ("Parsing LIST response failed: {0}", ex); return; @@ -4050,7 +4050,7 @@ public async Task TestParseFolderListWithFolderNameContainingUnquotedTabsAsync ( engine.SetStream (tokenizer); try { - await ImapUtils.ParseFolderListAsync (engine, list, false, false, true, CancellationToken.None); + await ImapUtils.ParseFolderListAsync (engine, list, false, false, CancellationToken.None); } catch (Exception ex) { Assert.Fail ("Parsing LIST response failed: {0}", ex); return;