1 Commits

Author SHA1 Message Date
Burak Kaan Köse
ed6a7d71b4 Listening imap inbox changes with idle client. 2024-08-13 14:12:54 +02:00
13 changed files with 195 additions and 94 deletions

View File

@@ -103,9 +103,9 @@ namespace Wino.Core.Domain.Interfaces
/// Creates a draft MailCopy and MimeMessage based on the given options. /// Creates a draft MailCopy and MimeMessage based on the given options.
/// For forward/reply it would include the referenced message. /// For forward/reply it would include the referenced message.
/// </summary> /// </summary>
/// <param name="accountId">AccountId which should have new draft.</param> /// <param name="composerAccount">Account which should have new draft.</param>
/// <param name="draftCreationOptions">Options like new email/forward/draft.</param> /// <param name="draftCreationOptions">Options like new email/forward/draft.</param>
/// <returns>Draft MailCopy and Draft MimeMessage as base64.</returns> /// <returns>Draft MailCopy and Draft MimeMessage as base64.</returns>
Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(Guid accountId, DraftCreationOptions draftCreationOptions); Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(MailAccount composerAccount, DraftCreationOptions draftCreationOptions);
} }
} }

View File

@@ -109,12 +109,12 @@ namespace Wino.Core.UWP.Services
{ {
try try
{ {
ConnectingHandle = new TaskCompletionSource<bool>(); ConnectingHandle ??= new TaskCompletionSource<bool>();
Status = WinoServerConnectionStatus.Connecting;
var connectionCancellationToken = new CancellationTokenSource(TimeSpan.FromMilliseconds(ServerConnectionTimeoutMs)); var connectionCancellationToken = new CancellationTokenSource(TimeSpan.FromMilliseconds(ServerConnectionTimeoutMs));
Status = WinoServerConnectionStatus.Connecting;
await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync(); await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync();
// Connection establishment handler is in App.xaml.cs OnBackgroundActivated. // Connection establishment handler is in App.xaml.cs OnBackgroundActivated.
@@ -125,21 +125,10 @@ namespace Wino.Core.UWP.Services
Log.Information("Server connection established successfully."); Log.Information("Server connection established successfully.");
} }
catch (OperationCanceledException canceledException)
{
Log.Error(canceledException, $"Server process did not start in {ServerConnectionTimeoutMs} ms. Operation is canceled.");
ConnectingHandle?.TrySetException(canceledException);
Status = WinoServerConnectionStatus.Failed;
return false;
}
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "Failed to connect to the server."); Log.Error(ex, "Failed to connect to the server.");
ConnectingHandle?.TrySetException(ex);
Status = WinoServerConnectionStatus.Failed; Status = WinoServerConnectionStatus.Failed;
return false; return false;
} }

View File

@@ -26,7 +26,7 @@ namespace Wino.Core.Services
{ {
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.MinimumLevel.ControlledBy(_levelSwitch) .MinimumLevel.ControlledBy(_levelSwitch)
.WriteTo.File(fullLogFilePath, retainedFileCountLimit: 3, rollOnFileSizeLimit: true, rollingInterval: RollingInterval.Day) .WriteTo.File(fullLogFilePath, retainedFileCountLimit: 2, rollOnFileSizeLimit: true, rollingInterval: RollingInterval.Day)
.WriteTo.Debug() .WriteTo.Debug()
.Enrich.FromLogContext() .Enrich.FromLogContext()
.Enrich.WithExceptionDetails() .Enrich.WithExceptionDetails()

View File

@@ -51,9 +51,8 @@ namespace Wino.Core.Services
_preferencesService = preferencesService; _preferencesService = preferencesService;
} }
public async Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(Guid accountId, DraftCreationOptions draftCreationOptions) public async Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(MailAccount composerAccount, DraftCreationOptions draftCreationOptions)
{ {
var composerAccount = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false);
var createdDraftMimeMessage = await CreateDraftMimeAsync(composerAccount, draftCreationOptions); var createdDraftMimeMessage = await CreateDraftMimeAsync(composerAccount, draftCreationOptions);
var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(composerAccount.Id, SpecialFolderType.Draft); var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(composerAccount.Id, SpecialFolderType.Draft);

View File

@@ -1,8 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using MailKit; using MailKit;
using MailKit.Net.Imap; using MailKit.Net.Imap;
using MailKit.Search; using MailKit.Search;
@@ -21,13 +23,14 @@ using Wino.Core.Integration.Processors;
using Wino.Core.Mime; using Wino.Core.Mime;
using Wino.Core.Requests; using Wino.Core.Requests;
using Wino.Core.Requests.Bundles; using Wino.Core.Requests.Bundles;
using Wino.Messaging.Server;
namespace Wino.Core.Synchronizers namespace Wino.Core.Synchronizers
{ {
public class ImapSynchronizer : BaseSynchronizer<ImapRequest, ImapMessageCreationPackage> public class ImapSynchronizer : BaseSynchronizer<ImapRequest, ImapMessageCreationPackage>
{ {
private CancellationTokenSource idleDoneToken; private CancellationTokenSource idleDoneToken;
private CancellationTokenSource cancelInboxListeningToken = new CancellationTokenSource(); private CancellationTokenSource cancelInboxListeningToken;
private IMailFolder inboxFolder; private IMailFolder inboxFolder;
@@ -35,6 +38,8 @@ namespace Wino.Core.Synchronizers
private readonly ImapClientPool _clientPool; private readonly ImapClientPool _clientPool;
private readonly IImapChangeProcessor _imapChangeProcessor; private readonly IImapChangeProcessor _imapChangeProcessor;
public bool IsChangeListeningActive { get; set; }
// Minimum summary items to Fetch for mail synchronization from IMAP. // Minimum summary items to Fetch for mail synchronization from IMAP.
private readonly MessageSummaryItems mailSynchronizationFlags = private readonly MessageSummaryItems mailSynchronizationFlags =
MessageSummaryItems.Flags | MessageSummaryItems.Flags |
@@ -47,12 +52,6 @@ namespace Wino.Core.Synchronizers
MessageSummaryItems.References | MessageSummaryItems.References |
MessageSummaryItems.ModSeq; MessageSummaryItems.ModSeq;
/// <summary>
/// Timer that keeps the <see cref="InboxClient"/> alive for the lifetime of the pool.
/// Sends NOOP command to the server periodically.
/// </summary>
private Timer _noOpTimer;
/// <summary> /// <summary>
/// ImapClient that keeps the Inbox folder opened all the time for listening notifications. /// ImapClient that keeps the Inbox folder opened all the time for listening notifications.
/// </summary> /// </summary>
@@ -65,44 +64,69 @@ namespace Wino.Core.Synchronizers
{ {
_clientPool = new ImapClientPool(Account.ServerInformation); _clientPool = new ImapClientPool(Account.ServerInformation);
_imapChangeProcessor = imapChangeProcessor; _imapChangeProcessor = imapChangeProcessor;
idleDoneToken = new CancellationTokenSource();
} }
// TODO
// private async void NoOpTimerTriggered(object state) => await AwaitInboxIdleAsync();
private async Task AwaitInboxIdleAsync() private async Task AwaitInboxIdleAsync()
{ {
if (_inboxIdleClient == null) if (_inboxIdleClient == null)
{ {
_logger.Warning("InboxClient is null. Cannot send NOOP command."); _logger.Warning("InboxClient is null. Cannot send IDLE command.");
return; return;
} }
if (inboxFolder == null)
{
_logger.Warning("Inbox folder is null. Cannot listen for changes.");
return;
}
idleDoneToken ??= new CancellationTokenSource();
cancelInboxListeningToken ??= new CancellationTokenSource();
await _clientPool.EnsureConnectedAsync(_inboxIdleClient); await _clientPool.EnsureConnectedAsync(_inboxIdleClient);
await _clientPool.EnsureAuthenticatedAsync(_inboxIdleClient); await _clientPool.EnsureAuthenticatedAsync(_inboxIdleClient);
try try
{ {
if (inboxFolder == null) if (!inboxFolder.IsOpen)
{ {
inboxFolder = _inboxIdleClient.Inbox;
await inboxFolder.OpenAsync(FolderAccess.ReadOnly, cancelInboxListeningToken.Token); await inboxFolder.OpenAsync(FolderAccess.ReadOnly, cancelInboxListeningToken.Token);
} }
idleDoneToken = new CancellationTokenSource(); if (!_inboxIdleClient.IsIdle)
{
idleDoneToken = new CancellationTokenSource();
await _inboxIdleClient.IdleAsync(idleDoneToken.Token, cancelInboxListeningToken.Token); IsChangeListeningActive = true;
await _inboxIdleClient.IdleAsync(idleDoneToken.Token, cancelInboxListeningToken.Token);
}
}
catch (ImapProtocolException)
{
await ReconnectAsync();
}
catch (IOException)
{
await ReconnectAsync();
}
catch (Exception exception)
{
Logger.Error(exception, "Error occured while listening for Inbox changes.");
} }
finally finally
{ {
idleDoneToken.Dispose(); IsChangeListeningActive = false;
idleDoneToken = null;
} }
} }
private async Task StopInboxListeningAsync() private async Task ReconnectAsync()
{
await _clientPool.EnsureConnectedAsync(_inboxIdleClient);
await _clientPool.EnsureAuthenticatedAsync(_inboxIdleClient);
}
public async Task StopInboxListeningAsync()
{ {
if (inboxFolder != null) if (inboxFolder != null)
{ {
@@ -111,12 +135,6 @@ namespace Wino.Core.Synchronizers
inboxFolder.MessageFlagsChanged -= InboxFolderMessageFlagsChanged; inboxFolder.MessageFlagsChanged -= InboxFolderMessageFlagsChanged;
} }
if (_noOpTimer != null)
{
_noOpTimer.Dispose();
_noOpTimer = null;
}
if (idleDoneToken != null) if (idleDoneToken != null)
{ {
idleDoneToken.Cancel(); idleDoneToken.Cancel();
@@ -124,9 +142,19 @@ namespace Wino.Core.Synchronizers
idleDoneToken = null; idleDoneToken = null;
} }
if (cancelInboxListeningToken != null)
{
cancelInboxListeningToken.Cancel();
cancelInboxListeningToken.Dispose();
cancelInboxListeningToken = null;
}
IsChangeListeningActive = false;
if (_inboxIdleClient != null) if (_inboxIdleClient != null)
{ {
await _inboxIdleClient.DisconnectAsync(true); await _inboxIdleClient.DisconnectAsync(true);
_inboxIdleClient.Dispose(); _inboxIdleClient.Dispose();
_inboxIdleClient = null; _inboxIdleClient = null;
} }
@@ -136,20 +164,14 @@ namespace Wino.Core.Synchronizers
/// Tries to connect & authenticate with the given credentials. /// Tries to connect & authenticate with the given credentials.
/// Prepares synchronizer for active listening of Inbox folder. /// Prepares synchronizer for active listening of Inbox folder.
/// </summary> /// </summary>
public async Task StartInboxListeningAsync() public async Task<bool> StartInboxListeningAsync()
{ {
_inboxIdleClient = await _clientPool.GetClientAsync(); _inboxIdleClient = await _clientPool.GetClientAsync();
// Run it every 8 minutes after 1 minute delay.
// _noOpTimer = new Timer(NoOpTimerTriggered, null, 60000, 8 * 60 * 1000);
await _clientPool.EnsureConnectedAsync(_inboxIdleClient);
await _clientPool.EnsureAuthenticatedAsync(_inboxIdleClient);
if (!_inboxIdleClient.Capabilities.HasFlag(ImapCapabilities.Idle)) if (!_inboxIdleClient.Capabilities.HasFlag(ImapCapabilities.Idle))
{ {
_logger.Information("Imap server does not support IDLE command. Listening live changes is not supported for {Name}", Account.Name); _logger.Information("Imap server does not support IDLE command. Listening live changes is not supported for {Name}", Account.Name);
return; return false;
} }
inboxFolder = _inboxIdleClient.Inbox; inboxFolder = _inboxIdleClient.Inbox;
@@ -157,35 +179,62 @@ namespace Wino.Core.Synchronizers
if (inboxFolder == null) if (inboxFolder == null)
{ {
_logger.Information("Inbox folder is null. Cannot listen for changes."); _logger.Information("Inbox folder is null. Cannot listen for changes.");
return;
return false;
} }
inboxFolder.CountChanged += InboxFolderCountChanged; inboxFolder.CountChanged += InboxFolderCountChanged;
inboxFolder.MessageExpunged += InboxFolderMessageExpunged; inboxFolder.MessageExpunged += InboxFolderMessageExpunged;
inboxFolder.MessageFlagsChanged += InboxFolderMessageFlagsChanged; inboxFolder.MessageFlagsChanged += InboxFolderMessageFlagsChanged;
while (!cancelInboxListeningToken.IsCancellationRequested) try
{ {
await AwaitInboxIdleAsync(); while (true)
{
await AwaitInboxIdleAsync();
}
}
catch (OperationCanceledException)
{
Log.Information("Listening Inbox changes for IMAP is canceled.");
}
catch (Exception ex)
{
Log.Error(ex, "Error occured while listening for Inbox changes.");
return false;
} }
await StopInboxListeningAsync(); return true;
}
/// <summary>
/// When a change occurs in the Inbox folder, this method is called to force synchronization.
/// Quueing new sync is ignored if the synchronizer is already in a synchronization process.
/// </summary>
private void ForceInboxIdleSynchronization()
{
// Do not try to synchronize if we're already in a synchronization process.
if (State == AccountSynchronizerState.Idle)
{
var options = new SynchronizationOptions()
{
Type = SynchronizationType.Inbox,
AccountId = Account.Id
};
WeakReferenceMessenger.Default.Send(new NewSynchronizationRequested(options, SynchronizationSource.Server));
}
} }
private void InboxFolderMessageFlagsChanged(object sender, MessageFlagsChangedEventArgs e) private void InboxFolderMessageFlagsChanged(object sender, MessageFlagsChangedEventArgs e)
{ => ForceInboxIdleSynchronization();
Console.WriteLine("Flags have changed for message #{0} ({1}).", e.Index, e.Flags);
}
private void InboxFolderMessageExpunged(object sender, MessageEventArgs e) private void InboxFolderMessageExpunged(object sender, MessageEventArgs e)
{ => ForceInboxIdleSynchronization();
_logger.Information("Inbox folder message expunged");
}
private void InboxFolderCountChanged(object sender, EventArgs e) private void InboxFolderCountChanged(object sender, EventArgs e)
{ => ForceInboxIdleSynchronization();
_logger.Information("Inbox folder count changed.");
}
/// <summary> /// <summary>
/// Parses List of string of mail copy ids and return valid uIds. /// Parses List of string of mail copy ids and return valid uIds.
@@ -922,7 +971,7 @@ namespace Wino.Core.Synchronizers
} }
// In case of the high input, we'll batch them by 50 to reflect changes quickly. // In case of the high input, we'll batch them by 50 to reflect changes quickly.
var batchedMissingMailIds = missingMailIds.Batch(50).Select(a => new UniqueIdSet(a, SortOrder.Ascending)); var batchedMissingMailIds = missingMailIds.Batch(50).Select(a => new UniqueIdSet(a, SortOrder.Descending));
foreach (var batchMissingMailIds in batchedMissingMailIds) foreach (var batchMissingMailIds in batchedMissingMailIds)
{ {
@@ -998,5 +1047,21 @@ namespace Wino.Core.Synchronizers
/// <param name="remoteFolder">Remote folder</param> /// <param name="remoteFolder">Remote folder</param>
/// <param name="localFolder">Local folder.</param> /// <param name="localFolder">Local folder.</param>
public bool ShouldUpdateFolder(IMailFolder remoteFolder, MailItemFolder localFolder) => remoteFolder.Name != localFolder.FolderName; public bool ShouldUpdateFolder(IMailFolder remoteFolder, MailItemFolder localFolder) => remoteFolder.Name != localFolder.FolderName;
/// <summary>
/// 1. Stops active inbox listening.
/// 2. Disconnects client pool with all clients.
/// 3. Disposes everything gracefully.
///
/// Usefull for killing server scenario where nothing is needed to be connected to IMAP server anymore.
/// </summary>
public async Task KillAsync()
{
Log.Debug("Killing ImapSynchronizer for {Name}", Account.Name);
await StopInboxListeningAsync();
_clientPool.Dispose();
}
} }
} }

View File

@@ -777,7 +777,7 @@ namespace Wino.Mail.ViewModels
MailToUri = _launchProtocolService.MailToUri MailToUri = _launchProtocolService.MailToUri
}; };
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(account.Id, draftOptions).ConfigureAwait(false); var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(account, draftOptions).ConfigureAwait(false);
var draftPreparationRequest = new DraftPreparationRequest(account, draftMailCopy, draftBase64MimeMessage); var draftPreparationRequest = new DraftPreparationRequest(account, draftMailCopy, draftBase64MimeMessage);
await _winoRequestDelegator.ExecuteAsync(draftPreparationRequest); await _winoRequestDelegator.ExecuteAsync(draftPreparationRequest);

View File

@@ -610,9 +610,6 @@ namespace Wino.Mail.ViewModels
if (ActiveFolder == null) return; if (ActiveFolder == null) return;
// At least accounts must match.
if (ActiveFolder.HandlingFolders.Any(a => a.MailAccountId != addedMail.AssignedAccount.Id)) return;
// Messages coming to sent or draft folder must be inserted regardless of the filter. // Messages coming to sent or draft folder must be inserted regardless of the filter.
bool shouldPreventIgnoringFilter = addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Draft || bool shouldPreventIgnoringFilter = addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Draft ||
addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Sent; addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Sent;
@@ -620,7 +617,6 @@ namespace Wino.Mail.ViewModels
// Item does not belong to this folder and doesn't have special type to be inserted. // Item does not belong to this folder and doesn't have special type to be inserted.
if (!shouldPreventIgnoringFilter && !ActiveFolder.HandlingFolders.Any(a => a.Id == addedMail.AssignedFolder.Id)) return; if (!shouldPreventIgnoringFilter && !ActiveFolder.HandlingFolders.Any(a => a.Id == addedMail.AssignedFolder.Id)) return;
// Item should be prevented from being added to the list due to filter.
if (!shouldPreventIgnoringFilter && ShouldPreventItemAdd(addedMail)) return; if (!shouldPreventIgnoringFilter && ShouldPreventItemAdd(addedMail)) return;
await MailCollection.AddAsync(addedMail); await MailCollection.AddAsync(addedMail);

View File

@@ -271,7 +271,7 @@ namespace Wino.Mail.ViewModels
} }
}; };
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount.Id, draftOptions).ConfigureAwait(false); var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount, draftOptions).ConfigureAwait(false);
var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.AssignedAccount, draftMailCopy, draftBase64MimeMessage, initializedMailItemViewModel.MailCopy); var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.AssignedAccount, draftMailCopy, draftBase64MimeMessage, initializedMailItemViewModel.MailCopy);

View File

@@ -50,7 +50,7 @@
Description="Mail client designed for Windows 11" Description="Mail client designed for Windows 11"
BackgroundColor="transparent"> BackgroundColor="transparent">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" Square71x71Logo="Assets\SmallTile.png" Square310x310Logo="Assets\LargeTile.png"/> <uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" Square71x71Logo="Assets\SmallTile.png" Square310x310Logo="Assets\LargeTile.png"/>
<uap:SplashScreen Image="Assets\SplashScreen.png" BackgroundColor="transparent" uap5:Optional="true" /> <uap:SplashScreen Image="Assets\SplashScreen.png" BackgroundColor="transparent"/>
<uap:LockScreen BadgeLogo="Assets\BadgeLogo.png" Notification="badgeAndTileText"/> <uap:LockScreen BadgeLogo="Assets\BadgeLogo.png" Notification="badgeAndTileText"/>
</uap:VisualElements> </uap:VisualElements>
<Extensions> <Extensions>

View File

@@ -8,5 +8,5 @@ namespace Wino.Messaging.Server
/// Triggers a new synchronization if possible. /// Triggers a new synchronization if possible.
/// </summary> /// </summary>
/// <param name="Options">Options for synchronization.</param> /// <param name="Options">Options for synchronization.</param>
public record NewSynchronizationRequested(SynchronizationOptions Options, SynchronizationSource Source) : IClientMessage; public record NewSynchronizationRequested(SynchronizationOptions Options, SynchronizationSource Source) : IClientMessage, IUIMessage;
} }

View File

@@ -11,7 +11,7 @@
<Identity <Identity
Name="58272BurakKSE.WinoMailPreview" Name="58272BurakKSE.WinoMailPreview"
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911" Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
Version="1.8.1.0" /> Version="1.8.0.0" />
<Extensions> <Extensions>
<!-- Publisher Cache Folders --> <!-- Publisher Cache Folders -->
@@ -48,7 +48,7 @@
Square150x150Logo="Images\Square150x150Logo.png" Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png"> Square44x44Logo="Images\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" Square71x71Logo="Images\SmallTile.png" Square310x310Logo="Images\LargeTile.png"/> <uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" Square71x71Logo="Images\SmallTile.png" Square310x310Logo="Images\LargeTile.png"/>
<uap:SplashScreen Image="Images\SplashScreen.png" uap5:Optional="true" /> <uap:SplashScreen Image="Images\SplashScreen.png" />
</uap:VisualElements> </uap:VisualElements>
<Extensions> <Extensions>

View File

@@ -13,6 +13,7 @@ using Wino.Core.Domain.Models.Requests;
using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Integration.Json; using Wino.Core.Integration.Json;
using Wino.Core.Services; using Wino.Core.Services;
using Wino.Core.Synchronizers;
using Wino.Messaging; using Wino.Messaging;
using Wino.Messaging.Client.Authorization; using Wino.Messaging.Client.Authorization;
using Wino.Messaging.Enums; using Wino.Messaging.Enums;
@@ -40,7 +41,8 @@ namespace Wino.Server
IRecipient<AccountSynchronizerStateChanged>, IRecipient<AccountSynchronizerStateChanged>,
IRecipient<RefreshUnreadCountsMessage>, IRecipient<RefreshUnreadCountsMessage>,
IRecipient<ServerTerminationModeChanged>, IRecipient<ServerTerminationModeChanged>,
IRecipient<AccountSynchronizationProgressUpdatedMessage> IRecipient<AccountSynchronizationProgressUpdatedMessage>,
IRecipient<NewSynchronizationRequested>
{ {
private readonly System.Timers.Timer _timer; private readonly System.Timers.Timer _timer;
private static object connectionLock = new object(); private static object connectionLock = new object();
@@ -57,6 +59,8 @@ namespace Wino.Server
TypeInfoResolver = new ServerRequestTypeInfoResolver() TypeInfoResolver = new ServerRequestTypeInfoResolver()
}; };
private Task imapIdleTask = null;
public ServerContext(IDatabaseService databaseService, public ServerContext(IDatabaseService databaseService,
IApplicationConfiguration applicationFolderConfiguration, IApplicationConfiguration applicationFolderConfiguration,
ISynchronizerFactory synchronizerFactory, ISynchronizerFactory synchronizerFactory,
@@ -171,7 +175,11 @@ namespace Wino.Server
AppServiceConnectionStatus status = await connection.OpenAsync(); AppServiceConnectionStatus status = await connection.OpenAsync();
if (status != AppServiceConnectionStatus.Success) if (status == AppServiceConnectionStatus.Success)
{
imapIdleTask = RegisterImapSynchronizerChangesAsync();
}
else
{ {
Log.Error("Opening server connection failed. Status: {status}", status); Log.Error("Opening server connection failed. Status: {status}", status);
@@ -179,6 +187,53 @@ namespace Wino.Server
} }
} }
private async Task PerformForAllImapSynchronizers(Action<ImapSynchronizer> action)
{
var allAccounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
var imapAccounts = allAccounts.FindAll(a => a.ProviderType == MailProviderType.IMAP4);
foreach (var account in imapAccounts)
{
var synchronizer = await _synchronizerFactory.GetAccountSynchronizerAsync(account.Id).ConfigureAwait(false);
if (synchronizer == null) continue;
if (synchronizer is not ImapSynchronizer accountImapSynchronizer)
{
Log.Warning("Account '{Name}' has IMAP4 type but synchronizer is not ImapSynchronizer.", account.Name);
continue;
}
action(accountImapSynchronizer);
}
}
/// <summary>
/// Hooks all ImapSynchronizer instances to listen for changes like new mail, folder rename etc.
/// </summary>
private Task RegisterImapSynchronizerChangesAsync()
=> PerformForAllImapSynchronizers(async accountImapSynchronizer =>
{
// First make sure that listening is stopped.
await accountImapSynchronizer.StopInboxListeningAsync();
var startListeningTask = accountImapSynchronizer.StartInboxListeningAsync();
await Task.Delay(10000); // Wait for 10 seconds.
if (startListeningTask.Exception == null)
{
Log.Information("IMAP change listening started for account '{Name}'.", accountImapSynchronizer.Account.Name);
}
});
public Task DisposeActiveImapConnectionsAsync()
=> PerformForAllImapSynchronizers(async accountImapSynchronizer =>
{
await accountImapSynchronizer.KillAsync();
});
/// <summary> /// <summary>
/// Disposes current connection to UWP app service. /// Disposes current connection to UWP app service.
/// </summary> /// </summary>
@@ -219,20 +274,7 @@ namespace Wino.Server
{ MessageConstants.MessageDataTypeKey, message.GetType().Name } { MessageConstants.MessageDataTypeKey, message.GetType().Name }
}; };
try await connection.SendMessageAsync(set);
{
await connection.SendMessageAsync(set);
}
catch (InvalidOperationException)
{
// Connection might've been disposed during the SendMessageAsync call.
// This is a safe way to handle the exception.
// We don't lock the connection since this request may take sometime to complete.
}
catch (Exception exception)
{
Log.Error(exception, "SendMessageAsync threw an exception");
}
} }
private void OnConnectionClosed(AppServiceConnection sender, AppServiceClosedEventArgs args) private void OnConnectionClosed(AppServiceConnection sender, AppServiceClosedEventArgs args)
@@ -350,5 +392,10 @@ namespace Wino.Server
App.Current.ChangeNotifyIconVisiblity(isServerTrayIconVisible); App.Current.ChangeNotifyIconVisiblity(isServerTrayIconVisible);
} }
public async void Receive(NewSynchronizationRequested message)
{
await ExecuteServerMessageSafeAsync(null, message);
}
} }
} }

View File

@@ -26,6 +26,8 @@ namespace Wino.Server
[RelayCommand] [RelayCommand]
public Task LaunchWinoAsync() public Task LaunchWinoAsync()
{ {
// Stop listening active imap synchronizer changes and disconnect gracefully.
return Context.DisposeActiveImapConnectionsAsync();
//var opt = new SynchronizationOptions() //var opt = new SynchronizationOptions()
//{ //{
// Type = Wino.Core.Domain.Enums.SynchronizationType.Full, // Type = Wino.Core.Domain.Enums.SynchronizationType.Full,
@@ -37,7 +39,7 @@ namespace Wino.Server
// return Task.CompletedTask; // return Task.CompletedTask;
return Launcher.LaunchUriAsync(new Uri($"{App.WinoMailLaunchProtocol}:")).AsTask(); // return Launcher.LaunchUriAsync(new Uri($"{App.WinoMailLaunchProtocol}:")).AsTask();
//await _notificationBuilder.CreateNotificationsAsync(Guid.Empty, new List<IMailItem>() //await _notificationBuilder.CreateNotificationsAsync(Guid.Empty, new List<IMailItem>()
//{ //{
// new MailCopy(){ UniqueId = Guid.Parse("8f25d2a0-4448-4fee-96a9-c9b25a19e866")} // new MailCopy(){ UniqueId = Guid.Parse("8f25d2a0-4448-4fee-96a9-c9b25a19e866")}
@@ -71,6 +73,9 @@ namespace Wino.Server
} }
} }
// Stop listening active imap synchronizer changes and disconnect gracefully.
await Context.DisposeActiveImapConnectionsAsync();
Application.Current.Shutdown(); Application.Current.Shutdown();
} }