diff --git a/Wino.Calendar.ViewModels/AccountManagementViewModel.cs b/Wino.Calendar.ViewModels/AccountManagementViewModel.cs index 70b6108d..83219511 100644 --- a/Wino.Calendar.ViewModels/AccountManagementViewModel.cs +++ b/Wino.Calendar.ViewModels/AccountManagementViewModel.cs @@ -81,7 +81,7 @@ namespace Wino.Calendar.ViewModels if (accountCreationDialogResult == null) return; var accountCreationCancellationTokenSource = new CancellationTokenSource(); - var accountCreationDialog = CalendarDialogService.GetAccountCreationDialog(accountCreationDialogResult.ProviderType); + var accountCreationDialog = CalendarDialogService.GetAccountCreationDialog(accountCreationDialogResult); accountCreationDialog.ShowDialog(accountCreationCancellationTokenSource); accountCreationDialog.State = AccountCreationDialogState.SigningIn; @@ -92,7 +92,6 @@ namespace Wino.Calendar.ViewModels { ProviderType = accountCreationDialogResult.ProviderType, Name = accountCreationDialogResult.AccountName, - AccountColorHex = accountCreationDialogResult.AccountColorHex, Id = Guid.NewGuid() }; @@ -104,13 +103,8 @@ namespace Wino.Calendar.ViewModels if (accountCreationDialog.State == AccountCreationDialogState.Canceled) throw new AccountSetupCanceledException(); - tokenInformationResponse.ThrowIfFailed(); - //var tokenInformation = tokenInformationResponse.Data; - //createdAccount.Address = tokenInformation.Address; - //tokenInformation.AccountId = createdAccount.Id; - await AccountService.CreateAccountAsync(createdAccount, null); // Sync profile information if supported. diff --git a/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs b/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs index 086f1e2a..09e2b402 100644 --- a/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs +++ b/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs @@ -23,12 +23,6 @@ namespace Wino.Core.Domain.Interfaces /// Request to queue. void QueueRequest(IRequestBase request); - /// - /// TODO - /// - /// Whether active synchronization is stopped or not. - bool CancelActiveSynchronization(); - /// /// Synchronizes profile information with the server. /// Sender name and Profile picture are updated. diff --git a/Wino.Core.Domain/Interfaces/ISynchronizerFactory.cs b/Wino.Core.Domain/Interfaces/ISynchronizerFactory.cs index d86fff66..553eb95c 100644 --- a/Wino.Core.Domain/Interfaces/ISynchronizerFactory.cs +++ b/Wino.Core.Domain/Interfaces/ISynchronizerFactory.cs @@ -7,5 +7,6 @@ namespace Wino.Core.Domain.Interfaces { Task GetAccountSynchronizerAsync(Guid accountId); Task InitializeAsync(); + Task DeleteSynchronizerAsync(Guid accountId); } } diff --git a/Wino.Core.Domain/Interfaces/IWinoSynchronizerBase.cs b/Wino.Core.Domain/Interfaces/IWinoSynchronizerBase.cs index 7864414a..ec4648bf 100644 --- a/Wino.Core.Domain/Interfaces/IWinoSynchronizerBase.cs +++ b/Wino.Core.Domain/Interfaces/IWinoSynchronizerBase.cs @@ -28,5 +28,12 @@ namespace Wino.Core.Domain.Interfaces /// Optional progress reporting for download operation. /// Cancellation token. Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress, CancellationToken cancellationToken = default); + + /// + /// 1. Cancel active synchronization. + /// 2. Stop all running tasks. + /// 3. Dispose all resources. + /// + Task KillSynchronizerAsync(); } } diff --git a/Wino.Core/Integration/ImapClientPool.cs b/Wino.Core/Integration/ImapClientPool.cs index 761d6e7b..3737b343 100644 --- a/Wino.Core/Integration/ImapClientPool.cs +++ b/Wino.Core/Integration/ImapClientPool.cs @@ -25,8 +25,6 @@ namespace Wino.Core.Integration /// Provides a pooling mechanism for ImapClient. /// Makes sure that we don't have too many connections to the server. /// Rents a connected & authenticated client from the pool all the time. - /// TODO: Keeps the clients alive by sending NOOP command periodically. - /// TODO: Listens to the Inbox folder for new messages. /// /// Connection/Authentication info to be used to configure ImapClient. public class ImapClientPool : IDisposable @@ -57,6 +55,7 @@ namespace Wino.Core.Integration private readonly CustomServerInformation _customServerInformation; private readonly Stream _protocolLogStream; private readonly ILogger _logger = Log.ForContext(); + private bool _disposedValue; public ImapClientPool(ImapClientPoolOptions imapClientPoolOptions) { @@ -185,7 +184,7 @@ namespace Wino.Core.Integration _clients.TryPop(out _); item.Dispose(); } - else + else if (!_disposedValue) { _clients.Push(item); } @@ -332,24 +331,38 @@ namespace Wino.Core.Integration }; } + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _clients.ForEach(client => + { + lock (client.SyncRoot) + { + client.Disconnect(true); + } + }); + + _clients.ForEach(client => + { + client.Dispose(); + }); + + _clients.Clear(); + + _protocolLogStream?.Dispose(); + } + + _disposedValue = true; + } + } + public void Dispose() { - _clients.ForEach(client => - { - lock (client.SyncRoot) - { - client.Disconnect(true); - } - }); - - _clients.ForEach(client => - { - client.Dispose(); - }); - - _clients.Clear(); - - _protocolLogStream?.Dispose(); + Dispose(disposing: true); + GC.SuppressFinalize(this); } } } diff --git a/Wino.Core/Services/SynchronizerFactory.cs b/Wino.Core/Services/SynchronizerFactory.cs index 0b951227..952f163b 100644 --- a/Wino.Core/Services/SynchronizerFactory.cs +++ b/Wino.Core/Services/SynchronizerFactory.cs @@ -109,5 +109,18 @@ namespace Wino.Core.Services isInitialized = true; } + + public async Task DeleteSynchronizerAsync(Guid accountId) + { + var synchronizer = synchronizerCache.Find(a => a.Account.Id == accountId); + + if (synchronizer != null) + { + // Stop the current synchronization. + await synchronizer.KillSynchronizerAsync(); + + synchronizerCache.Remove(synchronizer); + } + } } } diff --git a/Wino.Core/Synchronizers/BaseSynchronizer.cs b/Wino.Core/Synchronizers/BaseSynchronizer.cs index 7c11e83d..d2269afc 100644 --- a/Wino.Core/Synchronizers/BaseSynchronizer.cs +++ b/Wino.Core/Synchronizers/BaseSynchronizer.cs @@ -51,9 +51,6 @@ namespace Wino.Core.Synchronizers /// Cancellation token public abstract Task ExecuteNativeRequestsAsync(List> batchedRequests, CancellationToken cancellationToken = default); - // TODO: What if account is deleted during synchronization? - public bool CancelActiveSynchronization() => true; - /// /// Refreshes remote mail account profile if possible. /// Profile picture, sender name and mailbox settings (todo) will be handled in this step. diff --git a/Wino.Core/Synchronizers/GmailSynchronizer.cs b/Wino.Core/Synchronizers/GmailSynchronizer.cs index 91d33478..66a89292 100644 --- a/Wino.Core/Synchronizers/GmailSynchronizer.cs +++ b/Wino.Core/Synchronizers/GmailSynchronizer.cs @@ -1193,5 +1193,15 @@ namespace Wino.Core.Synchronizers.Mail } #endregion + + public override async Task KillSynchronizerAsync() + { + await base.KillSynchronizerAsync(); + + _gmailService.Dispose(); + _peopleService.Dispose(); + _calendarService.Dispose(); + _googleHttpClient.Dispose(); + } } } diff --git a/Wino.Core/Synchronizers/ImapSync/CondstoreSynchronizer.cs b/Wino.Core/Synchronizers/ImapSync/CondstoreSynchronizer.cs index 414fd94c..11002200 100644 --- a/Wino.Core/Synchronizers/ImapSync/CondstoreSynchronizer.cs +++ b/Wino.Core/Synchronizers/ImapSync/CondstoreSynchronizer.cs @@ -139,11 +139,14 @@ namespace Wino.Core.Synchronizers.ImapSync } finally { - if (remoteFolder != null) + if (!cancellationToken.IsCancellationRequested) { - if (remoteFolder.IsOpen) + if (remoteFolder != null) { - await remoteFolder.CloseAsync().ConfigureAwait(false); + if (remoteFolder.IsOpen) + { + await remoteFolder.CloseAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } } } } diff --git a/Wino.Core/Synchronizers/ImapSynchronizer.cs b/Wino.Core/Synchronizers/ImapSynchronizer.cs index 25e92b84..5baf17bb 100644 --- a/Wino.Core/Synchronizers/ImapSynchronizer.cs +++ b/Wino.Core/Synchronizers/ImapSynchronizer.cs @@ -57,7 +57,8 @@ namespace Wino.Core.Synchronizers.Mail _imapSynchronizationStrategyProvider = imapSynchronizationStrategyProvider; _applicationConfiguration = applicationConfiguration; - var poolOptions = ImapClientPoolOptions.CreateDefault(Account.ServerInformation, CreateAccountProtocolLogFileStream()); + var protocolLogStream = CreateAccountProtocolLogFileStream(); + var poolOptions = ImapClientPoolOptions.CreateDefault(Account.ServerInformation, protocolLogStream); _clientPool = new ImapClientPool(poolOptions); } @@ -66,7 +67,7 @@ namespace Wino.Core.Synchronizers.Mail { if (Account == null) throw new ArgumentNullException(nameof(Account)); - var logFile = Path.Combine(_applicationConfiguration.ApplicationDataFolderPath, $"Protocol_{Account.Address}.log"); + var logFile = Path.Combine(_applicationConfiguration.ApplicationDataFolderPath, $"Protocol_{Account.Address}_{Account.Id}.log"); // Each session should start a new log. if (File.Exists(logFile)) File.Delete(logFile); @@ -313,6 +314,8 @@ namespace Wino.Core.Synchronizers.Mail var folderDownloadedMessageIds = await SynchronizeFolderInternalAsync(folder, cancellationToken).ConfigureAwait(false); + if (cancellationToken.IsCancellationRequested) return MailSynchronizationResult.Canceled; + downloadedMessageIds.AddRange(folderDownloadedMessageIds); } } @@ -524,6 +527,11 @@ namespace Wino.Core.Synchronizers.Mail if (remoteFolder.IsNamespace && !remoteFolder.Attributes.HasFlag(FolderAttributes.Inbox) || !remoteFolder.Exists) continue; + // Check for NoSelect folders. These are not selectable folders. + // TODO: With new MailKit version 'CanOpen' will be implemented for ease of use. Use that one. + if (remoteFolder.Attributes.HasFlag(FolderAttributes.NoSelect)) + continue; + var existingLocalFolder = localFolders.FirstOrDefault(a => a.RemoteFolderId == remoteFolder.FullName); if (existingLocalFolder == null) @@ -641,6 +649,10 @@ namespace Wino.Core.Synchronizers.Mail goto retry; } + catch (OperationCanceledException) + { + // Ignore cancellations. + } catch (Exception) { @@ -716,6 +728,10 @@ namespace Wino.Core.Synchronizers.Mail Log.Warning(ioException, "Idle client received IO exception."); reconnect = true; } + catch (OperationCanceledException) + { + reconnect = !IsDisposing; + } catch (Exception ex) { Log.Warning(ex, "Idle client failed to start."); @@ -782,5 +798,14 @@ namespace Wino.Core.Synchronizers.Mail return Task.CompletedTask; } + + public override async Task KillSynchronizerAsync() + { + await base.KillSynchronizerAsync(); + await StopIdleClientAsync(); + + // Make sure the client pool safely disconnects all ImapClients. + _clientPool.Dispose(); + } } } diff --git a/Wino.Core/Synchronizers/OutlookSynchronizer.cs b/Wino.Core/Synchronizers/OutlookSynchronizer.cs index e386b358..ead9740d 100644 --- a/Wino.Core/Synchronizers/OutlookSynchronizer.cs +++ b/Wino.Core/Synchronizers/OutlookSynchronizer.cs @@ -1163,5 +1163,12 @@ namespace Wino.Core.Synchronizers.Mail return !localCalendarName.Equals(remoteCalendarName, StringComparison.OrdinalIgnoreCase); } + + public override async Task KillSynchronizerAsync() + { + await base.KillSynchronizerAsync(); + + _graphClient.Dispose(); + } } } diff --git a/Wino.Core/Synchronizers/WinoSynchronizer.cs b/Wino.Core/Synchronizers/WinoSynchronizer.cs index c487d50e..98af4cbe 100644 --- a/Wino.Core/Synchronizers/WinoSynchronizer.cs +++ b/Wino.Core/Synchronizers/WinoSynchronizer.cs @@ -25,13 +25,13 @@ namespace Wino.Core.Synchronizers { public abstract class WinoSynchronizer : BaseSynchronizer, IWinoSynchronizerBase { - protected Dictionary PendingSynchronizationRequest = new(); + protected bool IsDisposing { get; private set; } + + protected Dictionary PendingSynchronizationRequest = new(); protected ILogger Logger = Log.ForContext>(); - protected WinoSynchronizer(MailAccount account) : base(account) - { - } + protected WinoSynchronizer(MailAccount account) : base(account) { } /// /// How many items per single HTTP call can be modified. @@ -91,9 +91,10 @@ namespace Wino.Core.Synchronizers return MailSynchronizationResult.Canceled; } - PendingSynchronizationRequest.Add(options, cancellationToken); + var newCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - activeSynchronizationCancellationToken = cancellationToken; + PendingSynchronizationRequest.Add(options, newCancellationTokenSource); + activeSynchronizationCancellationToken = newCancellationTokenSource.Token; await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken); @@ -413,6 +414,20 @@ namespace Wino.Core.Synchronizers return ret; } + public virtual Task KillSynchronizerAsync() + { + IsDisposing = true; + CancelAllSynchronizations(); + return Task.CompletedTask; + } + + protected void CancelAllSynchronizations() + { + foreach (var request in PendingSynchronizationRequest) + { + request.Value.Cancel(); + } + } } } diff --git a/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs b/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs index 713949a5..d160ac44 100644 --- a/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs +++ b/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs @@ -12,6 +12,7 @@ using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.Navigation; using Wino.Messaging.Client.Navigation; +using Wino.Messaging.Server; using Wino.Messaging.UI; namespace Wino.Mail.ViewModels @@ -20,6 +21,7 @@ namespace Wino.Mail.ViewModels { private readonly IMailDialogService _dialogService; private readonly IAccountService _accountService; + private readonly IWinoServerConnectionManager _serverConnectionManager; private readonly IFolderService _folderService; public MailAccount Account { get; set; } @@ -48,10 +50,12 @@ namespace Wino.Mail.ViewModels public AccountDetailsPageViewModel(IMailDialogService dialogService, IAccountService accountService, + IWinoServerConnectionManager serverConnectionManager, IFolderService folderService) { _dialogService = dialogService; _accountService = accountService; + _serverConnectionManager = serverConnectionManager; _folderService = folderService; } @@ -102,13 +106,17 @@ namespace Wino.Mail.ViewModels if (!confirmation) return; - await _accountService.DeleteAccountAsync(Account); - // TODO: Server: Cancel ongoing calls from server for this account. + var isSynchronizerKilledResponse = await _serverConnectionManager.GetResponseAsync(new KillAccountSynchronizerRequested(Account.Id)); - _dialogService.InfoBarMessage(Translator.Info_AccountDeletedTitle, string.Format(Translator.Info_AccountDeletedMessage, Account.Name), InfoBarMessageType.Success); + if (isSynchronizerKilledResponse.IsSuccess) + { + await _accountService.DeleteAccountAsync(Account); - Messenger.Send(new BackBreadcrumNavigationRequested()); + _dialogService.InfoBarMessage(Translator.Info_AccountDeletedTitle, string.Format(Translator.Info_AccountDeletedMessage, Account.Name), InfoBarMessageType.Success); + + Messenger.Send(new BackBreadcrumNavigationRequested()); + } } public override async void OnNavigatedTo(NavigationMode mode, object parameters) diff --git a/Wino.Mail.ViewModels/AccountManagementViewModel.cs b/Wino.Mail.ViewModels/AccountManagementViewModel.cs index 834ecb24..fe0b89f0 100644 --- a/Wino.Mail.ViewModels/AccountManagementViewModel.cs +++ b/Wino.Mail.ViewModels/AccountManagementViewModel.cs @@ -113,9 +113,6 @@ namespace Wino.Mail.ViewModels }; creationDialog.ShowDialog(accountCreationCancellationTokenSource); - - await Task.Delay(1000); - creationDialog.State = AccountCreationDialogState.SigningIn; string tokenInformation = string.Empty; @@ -140,16 +137,17 @@ namespace Wino.Mail.ViewModels } else { + // Hanle special imap providers like iCloud and Yahoo. if (accountCreationDialogResult.SpecialImapProviderDetails != null) { - createdAccount.SenderName = accountCreationDialogResult.SpecialImapProviderDetails.SenderName; - createdAccount.Address = customServerInformation.Address; - // Special imap provider testing dialog. This is only available for iCloud and Yahoo. customServerInformation = _specialImapProviderConfigResolver.GetServerInformation(createdAccount, accountCreationDialogResult); customServerInformation.Id = Guid.NewGuid(); customServerInformation.AccountId = createdAccount.Id; + createdAccount.SenderName = accountCreationDialogResult.SpecialImapProviderDetails.SenderName; + createdAccount.Address = customServerInformation.Address; + await _imapTestService.TestImapConnectionAsync(customServerInformation, true); } else diff --git a/Wino.Messages/Server/KillAccountSynchronizerRequested.cs b/Wino.Messages/Server/KillAccountSynchronizerRequested.cs new file mode 100644 index 00000000..956df6a5 --- /dev/null +++ b/Wino.Messages/Server/KillAccountSynchronizerRequested.cs @@ -0,0 +1,11 @@ +using System; +using Wino.Core.Domain.Interfaces; + +namespace Wino.Messaging.Server +{ + /// + /// Client message that requests to kill the account synchronizer. + /// + /// Account id to kill synchronizer for. + public record KillAccountSynchronizerRequested(Guid AccountId) : IClientMessage; +} diff --git a/Wino.Server/Core/ServerMessageHandlerFactory.cs b/Wino.Server/Core/ServerMessageHandlerFactory.cs index 790bf021..bf29cd0c 100644 --- a/Wino.Server/Core/ServerMessageHandlerFactory.cs +++ b/Wino.Server/Core/ServerMessageHandlerFactory.cs @@ -24,6 +24,7 @@ namespace Wino.Server.Core nameof(ServerTerminationModeChanged) => App.Current.Services.GetService(), nameof(TerminateServerRequested) => App.Current.Services.GetService(), nameof(ImapConnectivityTestRequested) => App.Current.Services.GetService(), + nameof(KillAccountSynchronizerRequested) => App.Current.Services.GetService(), _ => throw new Exception($"Server handler for {typeName} is not registered."), }; } @@ -42,6 +43,7 @@ namespace Wino.Server.Core serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); + serviceCollection.AddTransient(); } } } diff --git a/Wino.Server/MessageHandlers/KillAccountSynchronizerHandler.cs b/Wino.Server/MessageHandlers/KillAccountSynchronizerHandler.cs new file mode 100644 index 00000000..0f389fa7 --- /dev/null +++ b/Wino.Server/MessageHandlers/KillAccountSynchronizerHandler.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Server; +using Wino.Messaging.Server; +using Wino.Server.Core; + +namespace Wino.Server.MessageHandlers +{ + public class KillAccountSynchronizerHandler : ServerMessageHandler + { + private readonly ISynchronizerFactory _synchronizerFactory; + + public override WinoServerResponse FailureDefaultResponse(Exception ex) + => WinoServerResponse.CreateErrorResponse(ex.Message); + + public KillAccountSynchronizerHandler(ISynchronizerFactory synchronizerFactory) + { + _synchronizerFactory = synchronizerFactory; + } + + protected override async Task> HandleAsync(KillAccountSynchronizerRequested message, CancellationToken cancellationToken = default) + { + await _synchronizerFactory.DeleteSynchronizerAsync(message.AccountId); + + return WinoServerResponse.CreateSuccessResponse(true); + } + } +} diff --git a/Wino.Server/ServerContext.cs b/Wino.Server/ServerContext.cs index e7b21e62..67f2520b 100644 --- a/Wino.Server/ServerContext.cs +++ b/Wino.Server/ServerContext.cs @@ -328,6 +328,9 @@ namespace Wino.Server KillServer(); break; + case nameof(KillAccountSynchronizerRequested): + await ExecuteServerMessageSafeAsync(args, JsonSerializer.Deserialize(messageJson, _jsonSerializerOptions)); + break; default: Debug.WriteLine($"Missing handler for {typeName} in the server. Check ServerContext.cs - HandleServerMessageAsync."); break; diff --git a/Wino.Services/AccountService.cs b/Wino.Services/AccountService.cs index fcbb105c..726674fb 100644 --- a/Wino.Services/AccountService.cs +++ b/Wino.Services/AccountService.cs @@ -319,6 +319,8 @@ namespace Wino.Services } } + + ReportUIChange(new AccountRemovedMessage(account)); }