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));
}