From b73eb3efcb054d5f756faefeec25649992ee6f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Tue, 28 Jan 2025 22:56:19 +0100 Subject: [PATCH] Bumping some nugets. More on the imap synchronizers. --- .../Wino.Authentication.csproj | 12 +- .../Wino.BackgroundTasks.csproj | 2 +- .../Wino.Calendar.Packaging.wapproj | 2 +- .../Wino.Calendar.ViewModels.csproj | 2 +- Wino.Calendar/Wino.Calendar.csproj | 4 +- .../Interfaces/IAccountCreationDialog.cs | 3 +- Wino.Core.Domain/Wino.Core.Domain.csproj | 6 +- .../Dialogs/AccountCreationDialog.xaml.cs | 29 +++- Wino.Core.UWP/Wino.Core.UWP.csproj | 10 +- .../Wino.Core.ViewModels.csproj | 2 +- .../ImapSync/CondstoreSynchronizer.cs | 127 ++++++------------ .../ImapSynchronizationStrategyBase.cs | 79 +++++++++++ .../ImapSync/QResyncSynchronizer.cs | 48 ++++--- .../ImapSync/UidBasedSynchronizer.cs | 60 ++++++++- Wino.Core/Synchronizers/ImapSynchronizer.cs | 12 +- Wino.Core/Wino.Core.csproj | 28 ++-- .../AccountManagementViewModel.cs | 2 +- .../Wino.Mail.ViewModels.csproj | 2 +- Wino.Mail/Dialogs/NewImapSetupDialog.xaml.cs | 13 +- Wino.Mail/Wino.Mail.csproj | 14 +- Wino.Messages/Wino.Messaging.csproj | 2 +- Wino.Packaging/Wino.Packaging.wapproj | 2 +- Wino.Server/Wino.Server.csproj | 8 +- Wino.Services/Wino.Services.csproj | 6 +- .../Wino.SourceGenerators.csproj | 4 +- 25 files changed, 300 insertions(+), 179 deletions(-) diff --git a/Wino.Authentication/Wino.Authentication.csproj b/Wino.Authentication/Wino.Authentication.csproj index a44ce5dc..01750ff7 100644 --- a/Wino.Authentication/Wino.Authentication.csproj +++ b/Wino.Authentication/Wino.Authentication.csproj @@ -11,12 +11,12 @@ - - - - - - + + + + + + diff --git a/Wino.BackgroundTasks/Wino.BackgroundTasks.csproj b/Wino.BackgroundTasks/Wino.BackgroundTasks.csproj index 67f9296c..07fb3d5e 100644 --- a/Wino.BackgroundTasks/Wino.BackgroundTasks.csproj +++ b/Wino.BackgroundTasks/Wino.BackgroundTasks.csproj @@ -87,7 +87,7 @@ - 4.66.2 + 4.67.2 6.2.14 diff --git a/Wino.Calendar.Packaging/Wino.Calendar.Packaging.wapproj b/Wino.Calendar.Packaging/Wino.Calendar.Packaging.wapproj index 47f8014e..7af3201e 100644 --- a/Wino.Calendar.Packaging/Wino.Calendar.Packaging.wapproj +++ b/Wino.Calendar.Packaging/Wino.Calendar.Packaging.wapproj @@ -76,7 +76,7 @@ - + diff --git a/Wino.Calendar.ViewModels/Wino.Calendar.ViewModels.csproj b/Wino.Calendar.ViewModels/Wino.Calendar.ViewModels.csproj index 4b029e10..002a1e9b 100644 --- a/Wino.Calendar.ViewModels/Wino.Calendar.ViewModels.csproj +++ b/Wino.Calendar.ViewModels/Wino.Calendar.ViewModels.csproj @@ -9,7 +9,7 @@ - + diff --git a/Wino.Calendar/Wino.Calendar.csproj b/Wino.Calendar/Wino.Calendar.csproj index 22555e01..7bd17912 100644 --- a/Wino.Calendar/Wino.Calendar.csproj +++ b/Wino.Calendar/Wino.Calendar.csproj @@ -328,13 +328,13 @@ 8.1.240916 - 4.66.2 + 4.67.2 6.2.14 - 1.28.0 + 1.28.1 diff --git a/Wino.Core.Domain/Interfaces/IAccountCreationDialog.cs b/Wino.Core.Domain/Interfaces/IAccountCreationDialog.cs index e534a042..8454a4ed 100644 --- a/Wino.Core.Domain/Interfaces/IAccountCreationDialog.cs +++ b/Wino.Core.Domain/Interfaces/IAccountCreationDialog.cs @@ -1,11 +1,12 @@ using System.Threading; +using System.Threading.Tasks; using Wino.Core.Domain.Enums; namespace Wino.Core.Domain.Interfaces { public interface IAccountCreationDialog { - void ShowDialog(CancellationTokenSource cancellationTokenSource); + Task ShowDialogAsync(CancellationTokenSource cancellationTokenSource); void Complete(bool cancel); AccountCreationDialogState State { get; set; } } diff --git a/Wino.Core.Domain/Wino.Core.Domain.csproj b/Wino.Core.Domain/Wino.Core.Domain.csproj index bf3c6728..da204424 100644 --- a/Wino.Core.Domain/Wino.Core.Domain.csproj +++ b/Wino.Core.Domain/Wino.Core.Domain.csproj @@ -60,8 +60,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -69,7 +69,7 @@ - + diff --git a/Wino.Core.UWP/Dialogs/AccountCreationDialog.xaml.cs b/Wino.Core.UWP/Dialogs/AccountCreationDialog.xaml.cs index 3abb5897..c5026c8e 100644 --- a/Wino.Core.UWP/Dialogs/AccountCreationDialog.xaml.cs +++ b/Wino.Core.UWP/Dialogs/AccountCreationDialog.xaml.cs @@ -1,4 +1,5 @@ using System.Threading; +using System.Threading.Tasks; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Wino.Core.Domain.Enums; @@ -8,6 +9,7 @@ namespace Wino.Dialogs { public sealed partial class AccountCreationDialog : ContentDialog, IAccountCreationDialog { + private TaskCompletionSource dialogOpened = new TaskCompletionSource(); public CancellationTokenSource CancellationTokenSource { get; private set; } public AccountCreationDialogState State @@ -32,13 +34,6 @@ namespace Wino.Dialogs } } - public void ShowDialog(CancellationTokenSource cancellationTokenSource) - { - CancellationTokenSource = cancellationTokenSource; - - _ = ShowAsync(); - } - public void Complete(bool cancel) { State = cancel ? AccountCreationDialogState.Canceled : AccountCreationDialogState.Completed; @@ -53,6 +48,26 @@ namespace Wino.Dialogs Hide(); } + private void CancelClicked(object sender, System.EventArgs e) => Complete(true); + + public async Task ShowDialogAsync(CancellationTokenSource cancellationTokenSource) + { + var tcs = + + CancellationTokenSource = cancellationTokenSource; + + Opened += DialogOpened; + _ = ShowAsync(); + + await dialogOpened.Task; + } + + private void DialogOpened(ContentDialog sender, ContentDialogOpenedEventArgs args) + { + Opened -= DialogOpened; + + dialogOpened?.SetResult(true); + } } } diff --git a/Wino.Core.UWP/Wino.Core.UWP.csproj b/Wino.Core.UWP/Wino.Core.UWP.csproj index 0e4f1e73..7f2ab5d1 100644 --- a/Wino.Core.UWP/Wino.Core.UWP.csproj +++ b/Wino.Core.UWP/Wino.Core.UWP.csproj @@ -187,13 +187,13 @@ - 8.3.2 + 8.4.0 - 8.3.2 + 8.4.0 - 8.3.2 + 8.4.0 8.1.240916 @@ -224,7 +224,7 @@ 5.0.6 - 4.66.2 + 4.67.2 6.2.14 @@ -236,7 +236,7 @@ 2.8.6 - 1.28.0 + 1.28.1 diff --git a/Wino.Core.ViewModels/Wino.Core.ViewModels.csproj b/Wino.Core.ViewModels/Wino.Core.ViewModels.csproj index d53fa682..17808ec2 100644 --- a/Wino.Core.ViewModels/Wino.Core.ViewModels.csproj +++ b/Wino.Core.ViewModels/Wino.Core.ViewModels.csproj @@ -14,7 +14,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Wino.Core/Synchronizers/ImapSync/CondstoreSynchronizer.cs b/Wino.Core/Synchronizers/ImapSync/CondstoreSynchronizer.cs index b5594d6b..b0028851 100644 --- a/Wino.Core/Synchronizers/ImapSync/CondstoreSynchronizer.cs +++ b/Wino.Core/Synchronizers/ImapSync/CondstoreSynchronizer.cs @@ -6,14 +6,10 @@ 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; using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.MailItem; using Wino.Core.Integration; -using Wino.Services.Extensions; using IMailService = Wino.Core.Domain.Interfaces.IMailService; namespace Wino.Core.Synchronizers.ImapSync @@ -41,6 +37,7 @@ namespace Wino.Core.Synchronizers.ImapSync IMailFolder remoteFolder = null; var downloadedMessageIds = new List(); + try { remoteFolder = await winoClient.GetFolderAsync(folder.RemoteFolderId, cancellationToken).ConfigureAwait(false); @@ -57,92 +54,10 @@ namespace Wino.Core.Synchronizers.ImapSync // the MODSEQ value for deleted messages. if (remoteHighestModSeq > localHighestModSeq) { - // 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(); + var changedUids = await GetChangedUidsAsync(client, folder, remoteFolder, synchronizer, cancellationToken).ConfigureAwait(false); // Get locally exists mails for the returned UIDs. - 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. - var newMessageIds = changedUids.Except(existingMailUids).ToList(); - - // Fetch minimum data for the existing mails in one query. - var existingFlagData = await remoteFolder.FetchAsync(existingMailUids, MessageSummaryItems.Flags | MessageSummaryItems.UniqueId).ConfigureAwait(false); - - foreach (var update in existingFlagData) - { - if (update.UniqueId == null) - { - Log.Warning($"Couldn't fetch UniqueId for the mail. FetchAsync failed."); - continue; - } - - if (update.Flags == null) - { - Log.Warning($"Couldn't fetch flags for the mail with UID {update.UniqueId.Id}. FetchAsync failed."); - continue; - } - - var existingMail = existingMails.FirstOrDefault(m => MailkitClientExtensions.ResolveUidStruct(m.Id).Id == update.UniqueId.Id); - - if (existingMail == null) - { - Log.Warning($"Couldn't find the mail with UID {update.UniqueId.Id} in the local database. Flag update is ignored."); - continue; - } - - await HandleMessageFlagsChangeAsync(existingMail, update.Flags.Value).ConfigureAwait(false); - } - - // Fetch the new mails in batch. - - var batchedMessageIds = newMessageIds.Batch(50); - - foreach (var group in batchedMessageIds) - { - var summaries = await remoteFolder.FetchAsync(group, MailSynchronizationFlags, cancellationToken).ConfigureAwait(false); - - foreach (var summary in summaries) - { - 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) - { - 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); - - // This is upsert. We are not interested in updated mails. - if (isCreatedNew) downloadedMessageIds.Add(package.Copy.Id); - } - } - } - } + downloadedMessageIds = await HandleChangedUIdsAsync(folder, synchronizer, remoteFolder, changedUids, cancellationToken).ConfigureAwait(false); folder.HighestModeSeq = (long)remoteHighestModSeq; @@ -177,5 +92,41 @@ namespace Wino.Core.Synchronizers.ImapSync } } } + + internal override async Task> GetChangedUidsAsync(IImapClient winoClient, MailItemFolder localFolder, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default) + { + var localHighestModSeq = (ulong)localFolder.HighestModeSeq; + var remoteHighestModSeq = remoteFolder.HighestModSeq; + + // Search for emails with a MODSEQ greater than the last known value. + // Use SORT extension if server supports. + + IList changedUids = null; + + // TODO: Temporarily disabled. + //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); + //} + + 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. + // Smaller uid means newer message. For initial sync, we need start taking items from the top. + + bool isInitialSynchronization = localHighestModSeq == 0; + + if (isInitialSynchronization) + { + changedUids = changedUids.OrderByDescending(a => a.Id).Take((int)synchronizer.InitialMessageDownloadCountPerFolder).ToList(); + } + + return changedUids; + } } } diff --git a/Wino.Core/Synchronizers/ImapSync/ImapSynchronizationStrategyBase.cs b/Wino.Core/Synchronizers/ImapSync/ImapSynchronizationStrategyBase.cs index 7f721a68..8b15b975 100644 --- a/Wino.Core/Synchronizers/ImapSync/ImapSynchronizationStrategyBase.cs +++ b/Wino.Core/Synchronizers/ImapSync/ImapSynchronizationStrategyBase.cs @@ -5,8 +5,11 @@ 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.Interfaces; +using Wino.Core.Domain.Models.MailItem; using Wino.Services.Extensions; using IMailService = Wino.Core.Domain.Interfaces.IMailService; @@ -36,6 +39,82 @@ namespace Wino.Core.Synchronizers.ImapSync } public abstract Task> HandleSynchronizationAsync(IImapClient client, MailItemFolder folder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default); + internal abstract Task> GetChangedUidsAsync(IImapClient client, MailItemFolder localFolder, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default); + + protected async Task> HandleChangedUIdsAsync(MailItemFolder folder, IImapSynchronizer synchronizer, IMailFolder remoteFolder, IList changedUids, CancellationToken cancellationToken) + { + List downloadedMessageIds = new(); + + 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. + var newMessageIds = changedUids.Except(existingMailUids).ToList(); + var deletedMessageIds = existingMailUids.Except(changedUids).ToList(); + + // Fetch minimum data for the existing mails in one query. + var existingFlagData = await remoteFolder.FetchAsync(existingMailUids, MessageSummaryItems.Flags | MessageSummaryItems.UniqueId).ConfigureAwait(false); + + foreach (var update in existingFlagData) + { + if (update.UniqueId == null) + { + Log.Warning($"Couldn't fetch UniqueId for the mail. FetchAsync failed."); + continue; + } + + if (update.Flags == null) + { + Log.Warning($"Couldn't fetch flags for the mail with UID {update.UniqueId.Id}. FetchAsync failed."); + continue; + } + + var existingMail = existingMails.FirstOrDefault(m => MailkitClientExtensions.ResolveUidStruct(m.Id).Id == update.UniqueId.Id); + + if (existingMail == null) + { + Log.Warning($"Couldn't find the mail with UID {update.UniqueId.Id} in the local database. Flag update is ignored."); + continue; + } + + await HandleMessageFlagsChangeAsync(existingMail, update.Flags.Value).ConfigureAwait(false); + } + + // Fetch the new mails in batch. + + var batchedMessageIds = newMessageIds.Batch(50); + + foreach (var group in batchedMessageIds) + { + var summaries = await remoteFolder.FetchAsync(group, MailSynchronizationFlags, cancellationToken).ConfigureAwait(false); + + foreach (var summary in summaries) + { + 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) + { + 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); + + // This is upsert. We are not interested in updated mails. + if (isCreatedNew) downloadedMessageIds.Add(package.Copy.Id); + } + } + } + } + + + return downloadedMessageIds; + } protected async Task HandleMessageFlagsChangeAsync(MailItemFolder folder, UniqueId? uniqueId, MessageFlags flags) { diff --git a/Wino.Core/Synchronizers/ImapSync/QResyncSynchronizer.cs b/Wino.Core/Synchronizers/ImapSync/QResyncSynchronizer.cs index 356a9b9a..00b1daf0 100644 --- a/Wino.Core/Synchronizers/ImapSync/QResyncSynchronizer.cs +++ b/Wino.Core/Synchronizers/ImapSync/QResyncSynchronizer.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -29,6 +28,8 @@ namespace Wino.Core.Synchronizers.ImapSync IImapSynchronizer synchronizer, CancellationToken cancellationToken = default) { + var downloadedMessageIds = new List(); + if (client is not WinoImapClient winoClient) throw new ImapSynchronizerStrategyException("Client must be of type WinoImapClient."); @@ -54,7 +55,6 @@ namespace Wino.Core.Synchronizers.ImapSync if (!isCacheValid) { // TODO: Remove all local data. - } // Perform QRESYNC synchronization. @@ -70,42 +70,52 @@ namespace Wino.Core.Synchronizers.ImapSync await remoteFolder.OpenAsync(FolderAccess.ReadOnly, folder.UidValidity, localHighestModSeq, allUniqueIds).ConfigureAwait(false); - var changedUids = await remoteFolder.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken).ConfigureAwait(false); + var changedUids = await GetChangedUidsAsync(client, folder, remoteFolder, synchronizer, cancellationToken).ConfigureAwait(false); - foreach (var uid in changedUids) - { - Debug.WriteLine($"Processing message with UID: {uid}"); - - // var message = await remoteFolder.GetMessageAsync(uid, cancellationToken).ConfigureAwait(false); - - // TODO: Process the message. - } + downloadedMessageIds = await HandleChangedUIdsAsync(folder, synchronizer, remoteFolder, changedUids, cancellationToken).ConfigureAwait(false); // Update the local folder with the new highest mod-seq and validity. folder.HighestModeSeq = (long)remoteHighestModSeq; folder.UidValidity = remoteFolder.UidValidity; + await ManageUUIdBasedDeletedMessagesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false); + await FolderService.UpdateFolderAsync(folder).ConfigureAwait(false); } - catch (Exception ex) + catch (FolderNotFoundException) + { + await FolderService.DeleteFolderAsync(folder.MailAccountId, folder.RemoteFolderId).ConfigureAwait(false); + + return default; + } + catch (Exception) { throw; } finally { - if (remoteFolder != null) + if (!cancellationToken.IsCancellationRequested) { - remoteFolder.MessagesVanished -= async (c, r) => await HandleMessageDeletedAsync(folder, r.UniqueIds).ConfigureAwait(false); - remoteFolder.MessageFlagsChanged -= async (c, r) => await HandleMessageFlagsChangeAsync(folder, r.UniqueId, r.Flags).ConfigureAwait(false); - - if (remoteFolder.IsOpen) + if (remoteFolder != null) { - await remoteFolder.CloseAsync(); + remoteFolder.MessagesVanished -= async (c, r) => await HandleMessageDeletedAsync(folder, r.UniqueIds).ConfigureAwait(false); + remoteFolder.MessageFlagsChanged -= async (c, r) => await HandleMessageFlagsChangeAsync(folder, r.UniqueId, r.Flags).ConfigureAwait(false); + + if (remoteFolder.IsOpen) + { + await remoteFolder.CloseAsync(); + } } } } - return default; + return downloadedMessageIds; + } + + internal override async Task> GetChangedUidsAsync(IImapClient client, MailItemFolder localFolder, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default) + { + var localHighestModSeq = (ulong)localFolder.HighestModeSeq; + return await remoteFolder.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken).ConfigureAwait(false); } } } diff --git a/Wino.Core/Synchronizers/ImapSync/UidBasedSynchronizer.cs b/Wino.Core/Synchronizers/ImapSync/UidBasedSynchronizer.cs index bec3f93d..ebb33608 100644 --- a/Wino.Core/Synchronizers/ImapSync/UidBasedSynchronizer.cs +++ b/Wino.Core/Synchronizers/ImapSync/UidBasedSynchronizer.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using MailKit; using MailKit.Net.Imap; +using MailKit.Search; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Interfaces; using Wino.Core.Integration; @@ -12,14 +15,65 @@ namespace Wino.Core.Synchronizers.ImapSync /// /// Uid based IMAP Synchronization strategy. /// - internal class UidBasedSynchronizer : IImapSynchronizerStrategy + internal class UidBasedSynchronizer : ImapSynchronizationStrategyBase { - public Task> HandleSynchronizationAsync(IImapClient client, MailItemFolder folder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default) + public UidBasedSynchronizer(IFolderService folderService, Domain.Interfaces.IMailService mailService) : base(folderService, mailService) + { + } + + public override async Task> HandleSynchronizationAsync(IImapClient client, MailItemFolder folder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default) { if (client is not WinoImapClient winoClient) throw new ArgumentException("Client must be of type WinoImapClient.", nameof(client)); - return Task.FromResult(new List()); + var downloadedMessageIds = new List(); + IMailFolder remoteFolder = null; + + try + { + remoteFolder = await winoClient.GetFolderAsync(folder.RemoteFolderId, cancellationToken).ConfigureAwait(false); + + await remoteFolder.OpenAsync(FolderAccess.ReadOnly, cancellationToken).ConfigureAwait(false); + + // Fetch UIDs from the remote folder + var remoteUids = await remoteFolder.SearchAsync(SearchQuery.All, cancellationToken).ConfigureAwait(false); + + remoteUids = remoteUids.OrderByDescending(a => a.Id).Take((int)synchronizer.InitialMessageDownloadCountPerFolder).ToList(); + + await HandleChangedUIdsAsync(folder, synchronizer, remoteFolder, remoteUids, cancellationToken).ConfigureAwait(false); + await ManageUUIdBasedDeletedMessagesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false); + } + catch (FolderNotFoundException) + { + await FolderService.DeleteFolderAsync(folder.MailAccountId, folder.RemoteFolderId).ConfigureAwait(false); + + return default; + } + catch (Exception) + { + + throw; + } + finally + { + if (!cancellationToken.IsCancellationRequested) + { + if (remoteFolder != null) + { + if (remoteFolder.IsOpen) + { + await remoteFolder.CloseAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + } + } + } + + return downloadedMessageIds; + } + + internal override async Task> GetChangedUidsAsync(IImapClient client, MailItemFolder localFolder, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); } } } diff --git a/Wino.Core/Synchronizers/ImapSynchronizer.cs b/Wino.Core/Synchronizers/ImapSynchronizer.cs index a3aac329..a86c5e27 100644 --- a/Wino.Core/Synchronizers/ImapSynchronizer.cs +++ b/Wino.Core/Synchronizers/ImapSynchronizer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; @@ -31,8 +32,9 @@ namespace Wino.Core.Synchronizers.Mail { public class ImapSynchronizer : WinoSynchronizer, IImapSynchronizer { + [Obsolete("N/A")] public override uint BatchModificationSize => 1000; - public override uint InitialMessageDownloadCountPerFolder => 250; + public override uint InitialMessageDownloadCountPerFolder => 500; #region Idle Implementation @@ -631,8 +633,6 @@ namespace Wino.Core.Synchronizers.Mail { if (!folder.IsSynchronizationEnabled) return default; - var downloadedMessageIds = new List(); - IImapClient availableClient = null; retry: @@ -666,7 +666,6 @@ namespace Wino.Core.Synchronizers.Mail return new List(); } - /// /// Whether the local folder should be updated with the remote folder. /// IMAP only compares folder name for now. @@ -705,7 +704,6 @@ namespace Wino.Core.Synchronizers.Mail // Setup idle client. idleClient = client; - idleDoneTokenSource ??= new CancellationTokenSource(); idleCancellationTokenSource ??= new CancellationTokenSource(); @@ -716,6 +714,7 @@ namespace Wino.Core.Synchronizers.Mail inboxFolder.CountChanged += IdleNotificationTriggered; inboxFolder.MessageFlagsChanged += IdleNotificationTriggered; inboxFolder.MessageExpunged += IdleNotificationTriggered; + inboxFolder.MessagesVanished += IdleNotificationTriggered; await client.IdleAsync(idleDoneTokenSource.Token, idleCancellationTokenSource.Token); } @@ -745,6 +744,7 @@ namespace Wino.Core.Synchronizers.Mail inboxFolder.CountChanged -= IdleNotificationTriggered; inboxFolder.MessageFlagsChanged -= IdleNotificationTriggered; inboxFolder.MessageExpunged -= IdleNotificationTriggered; + inboxFolder.MessagesVanished -= IdleNotificationTriggered; } if (idleDoneTokenSource != null) @@ -776,6 +776,8 @@ namespace Wino.Core.Synchronizers.Mail private void RequestIdleChangeSynchronization() { + Debug.WriteLine("Detected idle change."); + // We don't really need to act on the count change in detail. // Our synchronization should be enough to handle the changes with on-demand sync. // We can just trigger a sync here IMAPIdle type. diff --git a/Wino.Core/Wino.Core.csproj b/Wino.Core/Wino.Core.csproj index e632e9fb..43f67b00 100644 --- a/Wino.Core/Wino.Core.csproj +++ b/Wino.Core/Wino.Core.csproj @@ -16,34 +16,34 @@ - - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + - + - - + + - + - + diff --git a/Wino.Mail.ViewModels/AccountManagementViewModel.cs b/Wino.Mail.ViewModels/AccountManagementViewModel.cs index fe0b89f0..03e483de 100644 --- a/Wino.Mail.ViewModels/AccountManagementViewModel.cs +++ b/Wino.Mail.ViewModels/AccountManagementViewModel.cs @@ -112,7 +112,7 @@ namespace Wino.Mail.ViewModels Id = Guid.NewGuid() }; - creationDialog.ShowDialog(accountCreationCancellationTokenSource); + await creationDialog.ShowDialogAsync(accountCreationCancellationTokenSource); creationDialog.State = AccountCreationDialogState.SigningIn; string tokenInformation = string.Empty; diff --git a/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj b/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj index b84ad270..fb8a0477 100644 --- a/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj +++ b/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj @@ -16,7 +16,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Wino.Mail/Dialogs/NewImapSetupDialog.xaml.cs b/Wino.Mail/Dialogs/NewImapSetupDialog.xaml.cs index bd2d907f..c24050aa 100644 --- a/Wino.Mail/Dialogs/NewImapSetupDialog.xaml.cs +++ b/Wino.Mail/Dialogs/NewImapSetupDialog.xaml.cs @@ -78,8 +78,17 @@ namespace Wino.Dialogs public void Receive(ImapSetupDismissRequested message) => _getServerInfoTaskCompletionSource.TrySetResult(message.CompletedServerInformation); - public void ShowDialog(CancellationTokenSource cancellationTokenSource) - => _ = ShowAsync(); + public async Task ShowDialogAsync(CancellationTokenSource cancellationTokenSource) + { + var tcs = new TaskCompletionSource(); + + _ = ShowAsync().AsTask().ContinueWith((t) => + { + tcs.TrySetResult(true); + }); + + await tcs.Task; + } public void ShowPreparingFolders() { diff --git a/Wino.Mail/Wino.Mail.csproj b/Wino.Mail/Wino.Mail.csproj index 8e8f1b17..51ed2236 100644 --- a/Wino.Mail/Wino.Mail.csproj +++ b/Wino.Mail/Wino.Mail.csproj @@ -128,16 +128,16 @@ 1.0.0 - 8.3.2 + 8.4.0 - 8.3.2 + 8.4.0 0.1.240917-build.1755 - 8.3.2 + 8.4.0 8.1.240916 @@ -173,10 +173,10 @@ 5.0.6 - 8.0.1 + 9.0.1 - 4.66.2 + 4.67.2 2.2.12-rel-33220-00 @@ -196,7 +196,7 @@ 5.1.2 - 4.1.0 + 4.2.0 8.4.0 @@ -205,7 +205,7 @@ 1.9.172 - 1.28.0 + 1.28.1 diff --git a/Wino.Messages/Wino.Messaging.csproj b/Wino.Messages/Wino.Messaging.csproj index c012b615..54f1eda7 100644 --- a/Wino.Messages/Wino.Messaging.csproj +++ b/Wino.Messages/Wino.Messaging.csproj @@ -18,7 +18,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Wino.Packaging/Wino.Packaging.wapproj b/Wino.Packaging/Wino.Packaging.wapproj index c66a9cdc..42cb6d6d 100644 --- a/Wino.Packaging/Wino.Packaging.wapproj +++ b/Wino.Packaging/Wino.Packaging.wapproj @@ -133,6 +133,6 @@ - + \ No newline at end of file diff --git a/Wino.Server/Wino.Server.csproj b/Wino.Server/Wino.Server.csproj index b886e60f..f7e7c1cd 100644 --- a/Wino.Server/Wino.Server.csproj +++ b/Wino.Server/Wino.Server.csproj @@ -33,11 +33,11 @@ - - + + - - + + diff --git a/Wino.Services/Wino.Services.csproj b/Wino.Services/Wino.Services.csproj index 718d9107..9179bdac 100644 --- a/Wino.Services/Wino.Services.csproj +++ b/Wino.Services/Wino.Services.csproj @@ -7,10 +7,10 @@ - + - - + + diff --git a/Wino.SourceGenerators/Wino.SourceGenerators.csproj b/Wino.SourceGenerators/Wino.SourceGenerators.csproj index dba6e9d2..36506043 100644 --- a/Wino.SourceGenerators/Wino.SourceGenerators.csproj +++ b/Wino.SourceGenerators/Wino.SourceGenerators.csproj @@ -18,9 +18,9 @@ - + - +