diff --git a/Wino.Core.Domain/Interfaces/IImapSynchronizer.cs b/Wino.Core.Domain/Interfaces/IImapSynchronizer.cs index d3c152f6..6d3fca5d 100644 --- a/Wino.Core.Domain/Interfaces/IImapSynchronizer.cs +++ b/Wino.Core.Domain/Interfaces/IImapSynchronizer.cs @@ -8,6 +8,8 @@ namespace Wino.Core.Domain.Interfaces { public interface IImapSynchronizer { + uint InitialMessageDownloadCountPerFolder { get; } + Task> CreateNewMailPackagesAsync(ImapMessageCreationPackage message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default); Task StartIdleClientAsync(); Task StopIdleClientAsync(); diff --git a/Wino.Core/Synchronizers/ImapSync/CondstoreSynchronizer.cs b/Wino.Core/Synchronizers/ImapSync/CondstoreSynchronizer.cs index 11002200..b5594d6b 100644 --- a/Wino.Core/Synchronizers/ImapSync/CondstoreSynchronizer.cs +++ b/Wino.Core/Synchronizers/ImapSync/CondstoreSynchronizer.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using MailKit; using MailKit.Net.Imap; using MailKit.Search; +using MoreLinq; using Serilog; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Exceptions; @@ -49,16 +50,35 @@ namespace Wino.Core.Synchronizers.ImapSync var localHighestModSeq = (ulong)folder.HighestModeSeq; var remoteHighestModSeq = remoteFolder.HighestModSeq; + bool isInitialSynchronization = localHighestModSeq == 0; + // There are some changes on new messages or flag changes. // Deletions are tracked separately because some servers do not increase // the MODSEQ value for deleted messages. if (remoteHighestModSeq > localHighestModSeq) { - // Search for emails with a MODSEQ greater than the last known value - var changedUids = await remoteFolder.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq)).ConfigureAwait(false); + // Search for emails with a MODSEQ greater than the last known value. + // Use SORT extension if server supports. + + IList changedUids = null; + + if (winoClient.Capabilities.HasFlag(ImapCapabilities.Sort)) + { + changedUids = await remoteFolder.SortAsync(SearchQuery.ChangedSince(localHighestModSeq), [OrderBy.ReverseDate], cancellationToken).ConfigureAwait(false); + } + else + { + changedUids = await remoteFolder.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken).ConfigureAwait(false); + } + + // For initial synchronizations, take the first allowed number of items. + // For consequtive synchronizations, take all the items. We don't want to miss any changes. + + if (isInitialSynchronization) + changedUids = changedUids.Take((int)synchronizer.InitialMessageDownloadCountPerFolder).ToList(); // Get locally exists mails for the returned UIDs. - var existingMails = await MailService.GetExistingMailsAsync(folder.Id, changedUids); + var existingMails = await MailService.GetExistingMailsAsync(folder.Id, changedUids).ConfigureAwait(false); var existingMailUids = existingMails.Select(m => MailkitClientExtensions.ResolveUidStruct(m.Id)).ToArray(); // These are the non-existing mails. They will be downloaded + processed. @@ -92,28 +112,34 @@ namespace Wino.Core.Synchronizers.ImapSync await HandleMessageFlagsChangeAsync(existingMail, update.Flags.Value).ConfigureAwait(false); } - // Fetch the new mails. - var summaries = await remoteFolder.FetchAsync(newMessageIds, MailSynchronizationFlags, cancellationToken).ConfigureAwait(false); + // Fetch the new mails in batch. - foreach (var summary in summaries) + var batchedMessageIds = newMessageIds.Batch(50); + + foreach (var group in batchedMessageIds) { - var mimeMessage = await remoteFolder.GetMessageAsync(summary.UniqueId, cancellationToken).ConfigureAwait(false); + var summaries = await remoteFolder.FetchAsync(group, MailSynchronizationFlags, cancellationToken).ConfigureAwait(false); - var creationPackage = new ImapMessageCreationPackage(summary, mimeMessage); - - var mailPackages = await synchronizer.CreateNewMailPackagesAsync(creationPackage, folder, cancellationToken).ConfigureAwait(false); - - if (mailPackages != null) + foreach (var summary in summaries) { - foreach (var package in mailPackages) + var mimeMessage = await remoteFolder.GetMessageAsync(summary.UniqueId, cancellationToken).ConfigureAwait(false); + + var creationPackage = new ImapMessageCreationPackage(summary, mimeMessage); + + var mailPackages = await synchronizer.CreateNewMailPackagesAsync(creationPackage, folder, cancellationToken).ConfigureAwait(false); + + if (mailPackages != null) { - // Local draft is mapped. We don't need to create a new mail copy. - if (package == null) continue; + foreach (var package in mailPackages) + { + // Local draft is mapped. We don't need to create a new mail copy. + if (package == null) continue; - bool isCreatedNew = await MailService.CreateMailAsync(folder.MailAccountId, package).ConfigureAwait(false); + bool isCreatedNew = await MailService.CreateMailAsync(folder.MailAccountId, package).ConfigureAwait(false); - // This is upsert. We are not interested in updated mails. - if (isCreatedNew) downloadedMessageIds.Add(package.Copy.Id); + // This is upsert. We are not interested in updated mails. + if (isCreatedNew) downloadedMessageIds.Add(package.Copy.Id); + } } } } diff --git a/Wino.Core/Synchronizers/ImapSynchronizer.cs b/Wino.Core/Synchronizers/ImapSynchronizer.cs index 5baf17bb..a3aac329 100644 --- a/Wino.Core/Synchronizers/ImapSynchronizer.cs +++ b/Wino.Core/Synchronizers/ImapSynchronizer.cs @@ -257,15 +257,16 @@ namespace Wino.Core.Synchronizers.Mail var mailCopy = message.MessageSummary.GetMailDetails(assignedFolder, message.MimeMessage); // Draft folder message updates must be updated as IsDraft. - // I couldn't find it in MimeMessage... + // I couldn't find it in MimeMesssage... mailCopy.IsDraft = assignedFolder.SpecialFolderType == SpecialFolderType.Draft; // Check draft mapping. // This is the same implementation as in the OutlookSynchronizer. - if (message.MimeMessage.Headers.Contains(Domain.Constants.WinoLocalDraftHeader) - && Guid.TryParse(message.MimeMessage.Headers[Domain.Constants.WinoLocalDraftHeader], out Guid localDraftCopyUniqueId)) + if (message.MimeMessage != null && + message.MimeMessage.Headers.Contains(Domain.Constants.WinoLocalDraftHeader) && + Guid.TryParse(message.MimeMessage.Headers[Domain.Constants.WinoLocalDraftHeader], out Guid localDraftCopyUniqueId)) { // This message belongs to existing local draft copy. // We don't need to create a new mail copy for this message, just update the existing one.