File scoped namespaces
This commit is contained in:
@@ -11,99 +11,98 @@ using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Requests.Bundles;
|
||||
using Wino.Messaging.UI;
|
||||
|
||||
namespace Wino.Core.Synchronizers
|
||||
namespace Wino.Core.Synchronizers;
|
||||
|
||||
public abstract class BaseSynchronizer<TBaseRequest> : IBaseSynchronizer
|
||||
{
|
||||
public abstract class BaseSynchronizer<TBaseRequest> : IBaseSynchronizer
|
||||
protected SemaphoreSlim synchronizationSemaphore = new(1);
|
||||
protected CancellationToken activeSynchronizationCancellationToken;
|
||||
|
||||
protected List<IRequestBase> changeRequestQueue = [];
|
||||
public MailAccount Account { get; }
|
||||
|
||||
private AccountSynchronizerState state;
|
||||
public AccountSynchronizerState State
|
||||
{
|
||||
protected SemaphoreSlim synchronizationSemaphore = new(1);
|
||||
protected CancellationToken activeSynchronizationCancellationToken;
|
||||
|
||||
protected List<IRequestBase> changeRequestQueue = [];
|
||||
public MailAccount Account { get; }
|
||||
|
||||
private AccountSynchronizerState state;
|
||||
public AccountSynchronizerState State
|
||||
get { return state; }
|
||||
set
|
||||
{
|
||||
get { return state; }
|
||||
set
|
||||
{
|
||||
state = value;
|
||||
state = value;
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new AccountSynchronizerStateChanged(Account.Id, value));
|
||||
}
|
||||
}
|
||||
|
||||
protected BaseSynchronizer(MailAccount account)
|
||||
{
|
||||
Account = account;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queues a single request to be executed in the next synchronization.
|
||||
/// </summary>
|
||||
/// <param name="request">Request to execute.</param>
|
||||
public void QueueRequest(IRequestBase request) => changeRequestQueue.Add(request);
|
||||
|
||||
/// <summary>
|
||||
/// Runs existing queued requests in the queue.
|
||||
/// </summary>
|
||||
/// <param name="batchedRequests">Batched requests to execute. Integrator methods will only receive batched requests.</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
public abstract Task ExecuteNativeRequestsAsync(List<IRequestBundle<TBaseRequest>> batchedRequests, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes remote mail account profile if possible.
|
||||
/// Profile picture, sender name and mailbox settings (todo) will be handled in this step.
|
||||
/// </summary>
|
||||
public virtual Task<ProfileInformation> GetProfileInformationAsync() => default;
|
||||
|
||||
/// <summary>
|
||||
/// Safely updates account's profile information.
|
||||
/// Database changes are reflected after this call.
|
||||
/// </summary>
|
||||
protected async Task<ProfileInformation> SynchronizeProfileInformationInternalAsync()
|
||||
{
|
||||
var profileInformation = await GetProfileInformationAsync();
|
||||
|
||||
if (profileInformation != null)
|
||||
{
|
||||
Account.SenderName = profileInformation.SenderName;
|
||||
Account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData;
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInformation.AccountAddress))
|
||||
{
|
||||
Account.Address = profileInformation.AccountAddress;
|
||||
}
|
||||
}
|
||||
|
||||
return profileInformation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the base64 encoded profile picture of the account from the given URL.
|
||||
/// </summary>
|
||||
/// <param name="url">URL to retrieve picture from.</param>
|
||||
/// <returns>base64 encoded profile picture</returns>
|
||||
protected async Task<string> GetProfilePictureBase64EncodedAsync(string url)
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
|
||||
var response = await client.GetAsync(url).ConfigureAwait(false);
|
||||
var byteContent = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
|
||||
|
||||
return Convert.ToBase64String(byteContent);
|
||||
}
|
||||
|
||||
public List<IRequestBundle<TBaseRequest>> ForEachRequest<TWinoRequestType>(IEnumerable<TWinoRequestType> requests,
|
||||
Func<TWinoRequestType, TBaseRequest> action)
|
||||
where TWinoRequestType : IRequestBase
|
||||
{
|
||||
List<IRequestBundle<TBaseRequest>> ret = [];
|
||||
|
||||
foreach (var request in requests)
|
||||
ret.Add(new HttpRequestBundle<TBaseRequest>(action(request), request, request));
|
||||
|
||||
return ret;
|
||||
WeakReferenceMessenger.Default.Send(new AccountSynchronizerStateChanged(Account.Id, value));
|
||||
}
|
||||
}
|
||||
|
||||
protected BaseSynchronizer(MailAccount account)
|
||||
{
|
||||
Account = account;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queues a single request to be executed in the next synchronization.
|
||||
/// </summary>
|
||||
/// <param name="request">Request to execute.</param>
|
||||
public void QueueRequest(IRequestBase request) => changeRequestQueue.Add(request);
|
||||
|
||||
/// <summary>
|
||||
/// Runs existing queued requests in the queue.
|
||||
/// </summary>
|
||||
/// <param name="batchedRequests">Batched requests to execute. Integrator methods will only receive batched requests.</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
public abstract Task ExecuteNativeRequestsAsync(List<IRequestBundle<TBaseRequest>> batchedRequests, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes remote mail account profile if possible.
|
||||
/// Profile picture, sender name and mailbox settings (todo) will be handled in this step.
|
||||
/// </summary>
|
||||
public virtual Task<ProfileInformation> GetProfileInformationAsync() => default;
|
||||
|
||||
/// <summary>
|
||||
/// Safely updates account's profile information.
|
||||
/// Database changes are reflected after this call.
|
||||
/// </summary>
|
||||
protected async Task<ProfileInformation> SynchronizeProfileInformationInternalAsync()
|
||||
{
|
||||
var profileInformation = await GetProfileInformationAsync();
|
||||
|
||||
if (profileInformation != null)
|
||||
{
|
||||
Account.SenderName = profileInformation.SenderName;
|
||||
Account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData;
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInformation.AccountAddress))
|
||||
{
|
||||
Account.Address = profileInformation.AccountAddress;
|
||||
}
|
||||
}
|
||||
|
||||
return profileInformation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the base64 encoded profile picture of the account from the given URL.
|
||||
/// </summary>
|
||||
/// <param name="url">URL to retrieve picture from.</param>
|
||||
/// <returns>base64 encoded profile picture</returns>
|
||||
protected async Task<string> GetProfilePictureBase64EncodedAsync(string url)
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
|
||||
var response = await client.GetAsync(url).ConfigureAwait(false);
|
||||
var byteContent = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
|
||||
|
||||
return Convert.ToBase64String(byteContent);
|
||||
}
|
||||
|
||||
public List<IRequestBundle<TBaseRequest>> ForEachRequest<TWinoRequestType>(IEnumerable<TWinoRequestType> requests,
|
||||
Func<TWinoRequestType, TBaseRequest> action)
|
||||
where TWinoRequestType : IRequestBase
|
||||
{
|
||||
List<IRequestBundle<TBaseRequest>> ret = [];
|
||||
|
||||
foreach (var request in requests)
|
||||
ret.Add(new HttpRequestBundle<TBaseRequest>(action(request), request, request));
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,120 +12,119 @@ using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Integration;
|
||||
using IMailService = Wino.Core.Domain.Interfaces.IMailService;
|
||||
|
||||
namespace Wino.Core.Synchronizers.ImapSync
|
||||
namespace Wino.Core.Synchronizers.ImapSync;
|
||||
|
||||
/// <summary>
|
||||
/// RFC 4551 CONDSTORE IMAP Synchronization strategy.
|
||||
/// </summary>
|
||||
internal class CondstoreSynchronizer : ImapSynchronizationStrategyBase
|
||||
{
|
||||
/// <summary>
|
||||
/// RFC 4551 CONDSTORE IMAP Synchronization strategy.
|
||||
/// </summary>
|
||||
internal class CondstoreSynchronizer : ImapSynchronizationStrategyBase
|
||||
public CondstoreSynchronizer(IFolderService folderService, IMailService mailService) : base(folderService, mailService)
|
||||
{
|
||||
public CondstoreSynchronizer(IFolderService folderService, IMailService mailService) : base(folderService, mailService)
|
||||
}
|
||||
|
||||
public async override Task<List<string>> 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));
|
||||
|
||||
if (!client.Capabilities.HasFlag(ImapCapabilities.CondStore))
|
||||
throw new ImapSynchronizerStrategyException("Server does not support CONDSTORE.");
|
||||
|
||||
IMailFolder remoteFolder = null;
|
||||
|
||||
var downloadedMessageIds = new List<string>();
|
||||
|
||||
try
|
||||
{
|
||||
remoteFolder = await winoClient.GetFolderAsync(folder.RemoteFolderId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await remoteFolder.OpenAsync(FolderAccess.ReadOnly, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var localHighestModSeq = (ulong)folder.HighestModeSeq;
|
||||
|
||||
bool isInitialSynchronization = localHighestModSeq == 0;
|
||||
|
||||
// There are some changes on new messages or flag changes.
|
||||
// Deletions are tracked separately because some servers do not increase
|
||||
// the MODSEQ value for deleted messages.
|
||||
if (remoteFolder.HighestModSeq > localHighestModSeq)
|
||||
{
|
||||
var changedUids = await GetChangedUidsAsync(client, remoteFolder, synchronizer, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Get locally exists mails for the returned UIDs.
|
||||
downloadedMessageIds = await HandleChangedUIdsAsync(synchronizer, remoteFolder, changedUids, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
folder.HighestModeSeq = unchecked((long)remoteFolder.HighestModSeq);
|
||||
|
||||
await FolderService.UpdateFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await ManageUUIdBasedDeletedMessagesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return downloadedMessageIds;
|
||||
}
|
||||
|
||||
public async override Task<List<string>> HandleSynchronizationAsync(IImapClient client,
|
||||
MailItemFolder folder,
|
||||
IImapSynchronizer synchronizer,
|
||||
CancellationToken cancellationToken = default)
|
||||
catch (FolderNotFoundException)
|
||||
{
|
||||
if (client is not WinoImapClient winoClient)
|
||||
throw new ArgumentException("Client must be of type WinoImapClient.", nameof(client));
|
||||
await FolderService.DeleteFolderAsync(folder.MailAccountId, folder.RemoteFolderId).ConfigureAwait(false);
|
||||
|
||||
if (!client.Capabilities.HasFlag(ImapCapabilities.CondStore))
|
||||
throw new ImapSynchronizerStrategyException("Server does not support CONDSTORE.");
|
||||
|
||||
IMailFolder remoteFolder = null;
|
||||
|
||||
var downloadedMessageIds = new List<string>();
|
||||
|
||||
try
|
||||
return default;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
remoteFolder = await winoClient.GetFolderAsync(folder.RemoteFolderId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await remoteFolder.OpenAsync(FolderAccess.ReadOnly, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var localHighestModSeq = (ulong)folder.HighestModeSeq;
|
||||
|
||||
bool isInitialSynchronization = localHighestModSeq == 0;
|
||||
|
||||
// There are some changes on new messages or flag changes.
|
||||
// Deletions are tracked separately because some servers do not increase
|
||||
// the MODSEQ value for deleted messages.
|
||||
if (remoteFolder.HighestModSeq > localHighestModSeq)
|
||||
if (remoteFolder != null)
|
||||
{
|
||||
var changedUids = await GetChangedUidsAsync(client, remoteFolder, synchronizer, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Get locally exists mails for the returned UIDs.
|
||||
downloadedMessageIds = await HandleChangedUIdsAsync(synchronizer, remoteFolder, changedUids, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
folder.HighestModeSeq = unchecked((long)remoteFolder.HighestModSeq);
|
||||
|
||||
await FolderService.UpdateFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await ManageUUIdBasedDeletedMessagesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return downloadedMessageIds;
|
||||
}
|
||||
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)
|
||||
{
|
||||
if (remoteFolder.IsOpen)
|
||||
{
|
||||
await remoteFolder.CloseAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
await remoteFolder.CloseAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal override async Task<IList<UniqueId>> GetChangedUidsAsync(IImapClient winoClient, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default)
|
||||
internal override async Task<IList<UniqueId>> GetChangedUidsAsync(IImapClient winoClient, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var localHighestModSeq = (ulong)Folder.HighestModeSeq;
|
||||
var remoteHighestModSeq = remoteFolder.HighestModSeq;
|
||||
|
||||
// Search for emails with a MODSEQ greater than the last known value.
|
||||
// Use SORT extension if server supports.
|
||||
|
||||
IList<UniqueId> changedUids = null;
|
||||
|
||||
if (winoClient.Capabilities.HasFlag(ImapCapabilities.Sort))
|
||||
{
|
||||
var localHighestModSeq = (ulong)Folder.HighestModeSeq;
|
||||
var remoteHighestModSeq = remoteFolder.HighestModSeq;
|
||||
|
||||
// Search for emails with a MODSEQ greater than the last known value.
|
||||
// Use SORT extension if server supports.
|
||||
|
||||
IList<UniqueId> changedUids = null;
|
||||
|
||||
if (winoClient.Capabilities.HasFlag(ImapCapabilities.Sort))
|
||||
{
|
||||
// Highest mod seq must be greater than 0 for SORT.
|
||||
changedUids = await remoteFolder.SortAsync(SearchQuery.ChangedSince(Math.Max(localHighestModSeq, 1)), [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;
|
||||
// Highest mod seq must be greater than 0 for SORT.
|
||||
changedUids = await remoteFolder.SortAsync(SearchQuery.ChangedSince(Math.Max(localHighestModSeq, 1)), [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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,173 +13,172 @@ using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Services.Extensions;
|
||||
using IMailService = Wino.Core.Domain.Interfaces.IMailService;
|
||||
|
||||
namespace Wino.Core.Synchronizers.ImapSync
|
||||
namespace Wino.Core.Synchronizers.ImapSync;
|
||||
|
||||
public abstract class ImapSynchronizationStrategyBase : IImapSynchronizerStrategy
|
||||
{
|
||||
public abstract class ImapSynchronizationStrategyBase : IImapSynchronizerStrategy
|
||||
// Minimum summary items to Fetch for mail synchronization from IMAP.
|
||||
protected readonly MessageSummaryItems MailSynchronizationFlags =
|
||||
MessageSummaryItems.Flags |
|
||||
MessageSummaryItems.UniqueId |
|
||||
MessageSummaryItems.ThreadId |
|
||||
MessageSummaryItems.EmailId |
|
||||
MessageSummaryItems.Headers |
|
||||
MessageSummaryItems.PreviewText |
|
||||
MessageSummaryItems.GMailThreadId |
|
||||
MessageSummaryItems.References |
|
||||
MessageSummaryItems.ModSeq;
|
||||
|
||||
protected IFolderService FolderService { get; }
|
||||
protected IMailService MailService { get; }
|
||||
protected MailItemFolder Folder { get; set; }
|
||||
|
||||
protected ImapSynchronizationStrategyBase(IFolderService folderService, IMailService mailService)
|
||||
{
|
||||
// Minimum summary items to Fetch for mail synchronization from IMAP.
|
||||
protected readonly MessageSummaryItems MailSynchronizationFlags =
|
||||
MessageSummaryItems.Flags |
|
||||
MessageSummaryItems.UniqueId |
|
||||
MessageSummaryItems.ThreadId |
|
||||
MessageSummaryItems.EmailId |
|
||||
MessageSummaryItems.Headers |
|
||||
MessageSummaryItems.PreviewText |
|
||||
MessageSummaryItems.GMailThreadId |
|
||||
MessageSummaryItems.References |
|
||||
MessageSummaryItems.ModSeq;
|
||||
FolderService = folderService;
|
||||
MailService = mailService;
|
||||
}
|
||||
|
||||
protected IFolderService FolderService { get; }
|
||||
protected IMailService MailService { get; }
|
||||
protected MailItemFolder Folder { get; set; }
|
||||
public abstract Task<List<string>> HandleSynchronizationAsync(IImapClient client, MailItemFolder folder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default);
|
||||
internal abstract Task<IList<UniqueId>> GetChangedUidsAsync(IImapClient client, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default);
|
||||
|
||||
protected ImapSynchronizationStrategyBase(IFolderService folderService, IMailService mailService)
|
||||
protected async Task<List<string>> HandleChangedUIdsAsync(IImapSynchronizer synchronizer, IMailFolder remoteFolder, IList<UniqueId> changedUids, CancellationToken cancellationToken)
|
||||
{
|
||||
List<string> 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)
|
||||
{
|
||||
FolderService = folderService;
|
||||
MailService = mailService;
|
||||
}
|
||||
|
||||
public abstract Task<List<string>> HandleSynchronizationAsync(IImapClient client, MailItemFolder folder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default);
|
||||
internal abstract Task<IList<UniqueId>> GetChangedUidsAsync(IImapClient client, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default);
|
||||
|
||||
protected async Task<List<string>> HandleChangedUIdsAsync(IImapSynchronizer synchronizer, IMailFolder remoteFolder, IList<UniqueId> changedUids, CancellationToken cancellationToken)
|
||||
{
|
||||
List<string> 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)
|
||||
{
|
||||
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);
|
||||
Log.Warning($"Couldn't fetch UniqueId for the mail. FetchAsync failed.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fetch the new mails in batch.
|
||||
|
||||
var batchedMessageIds = newMessageIds.Batch(50);
|
||||
|
||||
foreach (var group in batchedMessageIds)
|
||||
if (update.Flags == null)
|
||||
{
|
||||
var summaries = await remoteFolder.FetchAsync(group, MailSynchronizationFlags, cancellationToken).ConfigureAwait(false);
|
||||
Log.Warning($"Couldn't fetch flags for the mail with UID {update.UniqueId.Id}. FetchAsync failed.");
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var summary in summaries)
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
foreach (var package in mailPackages)
|
||||
{
|
||||
// Local draft is mapped. We don't need to create a new mail copy.
|
||||
if (package == null) continue;
|
||||
// Local draft is mapped. We don't need to create a new mail copy.
|
||||
if (package == null) continue;
|
||||
|
||||
bool isCreatedNew = await MailService.CreateMailAsync(Folder.MailAccountId, package).ConfigureAwait(false);
|
||||
bool isCreatedNew = await MailService.CreateMailAsync(Folder.MailAccountId, package).ConfigureAwait(false);
|
||||
|
||||
// This is upsert. We are not interested in updated mails.
|
||||
if (isCreatedNew) downloadedMessageIds.Add(package.Copy.Id);
|
||||
}
|
||||
// This is upsert. We are not interested in updated mails.
|
||||
if (isCreatedNew) downloadedMessageIds.Add(package.Copy.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return downloadedMessageIds;
|
||||
}
|
||||
|
||||
protected async Task HandleMessageFlagsChangeAsync(UniqueId? uniqueId, MessageFlags flags)
|
||||
|
||||
return downloadedMessageIds;
|
||||
}
|
||||
|
||||
protected async Task HandleMessageFlagsChangeAsync(UniqueId? uniqueId, MessageFlags flags)
|
||||
{
|
||||
if (Folder == null) return;
|
||||
if (uniqueId == null) return;
|
||||
|
||||
var localMailCopyId = MailkitClientExtensions.CreateUid(Folder.Id, uniqueId.Value.Id);
|
||||
|
||||
var isFlagged = MailkitClientExtensions.GetIsFlagged(flags);
|
||||
var isRead = MailkitClientExtensions.GetIsRead(flags);
|
||||
|
||||
await MailService.ChangeReadStatusAsync(localMailCopyId, isRead).ConfigureAwait(false);
|
||||
await MailService.ChangeFlagStatusAsync(localMailCopyId, isFlagged).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected async Task HandleMessageFlagsChangeAsync(MailCopy mailCopy, MessageFlags flags)
|
||||
{
|
||||
if (mailCopy == null) return;
|
||||
|
||||
var isFlagged = MailkitClientExtensions.GetIsFlagged(flags);
|
||||
var isRead = MailkitClientExtensions.GetIsRead(flags);
|
||||
|
||||
if (isFlagged != mailCopy.IsFlagged)
|
||||
{
|
||||
if (Folder == null) return;
|
||||
if (uniqueId == null) return;
|
||||
|
||||
var localMailCopyId = MailkitClientExtensions.CreateUid(Folder.Id, uniqueId.Value.Id);
|
||||
|
||||
var isFlagged = MailkitClientExtensions.GetIsFlagged(flags);
|
||||
var isRead = MailkitClientExtensions.GetIsRead(flags);
|
||||
|
||||
await MailService.ChangeReadStatusAsync(localMailCopyId, isRead).ConfigureAwait(false);
|
||||
await MailService.ChangeFlagStatusAsync(localMailCopyId, isFlagged).ConfigureAwait(false);
|
||||
await MailService.ChangeFlagStatusAsync(mailCopy.Id, isFlagged).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected async Task HandleMessageFlagsChangeAsync(MailCopy mailCopy, MessageFlags flags)
|
||||
if (isRead != mailCopy.IsRead)
|
||||
{
|
||||
if (mailCopy == null) return;
|
||||
|
||||
var isFlagged = MailkitClientExtensions.GetIsFlagged(flags);
|
||||
var isRead = MailkitClientExtensions.GetIsRead(flags);
|
||||
|
||||
if (isFlagged != mailCopy.IsFlagged)
|
||||
{
|
||||
await MailService.ChangeFlagStatusAsync(mailCopy.Id, isFlagged).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (isRead != mailCopy.IsRead)
|
||||
{
|
||||
await MailService.ChangeReadStatusAsync(mailCopy.Id, isRead).ConfigureAwait(false);
|
||||
}
|
||||
await MailService.ChangeReadStatusAsync(mailCopy.Id, isRead).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task HandleMessageDeletedAsync(IList<UniqueId> uniqueIds)
|
||||
protected async Task HandleMessageDeletedAsync(IList<UniqueId> uniqueIds)
|
||||
{
|
||||
if (Folder == null) return;
|
||||
if (uniqueIds == null || uniqueIds.Count == 0) return;
|
||||
|
||||
foreach (var uniqueId in uniqueIds)
|
||||
{
|
||||
if (Folder == null) return;
|
||||
if (uniqueIds == null || uniqueIds.Count == 0) return;
|
||||
if (uniqueId == null) continue;
|
||||
var localMailCopyId = MailkitClientExtensions.CreateUid(Folder.Id, uniqueId.Id);
|
||||
|
||||
foreach (var uniqueId in uniqueIds)
|
||||
{
|
||||
if (uniqueId == null) continue;
|
||||
var localMailCopyId = MailkitClientExtensions.CreateUid(Folder.Id, uniqueId.Id);
|
||||
|
||||
await MailService.DeleteMailAsync(Folder.MailAccountId, localMailCopyId).ConfigureAwait(false);
|
||||
}
|
||||
await MailService.DeleteMailAsync(Folder.MailAccountId, localMailCopyId).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected void OnMessagesVanished(object sender, MessagesVanishedEventArgs args)
|
||||
=> HandleMessageDeletedAsync(args.UniqueIds).ConfigureAwait(false);
|
||||
protected void OnMessagesVanished(object sender, MessagesVanishedEventArgs args)
|
||||
=> HandleMessageDeletedAsync(args.UniqueIds).ConfigureAwait(false);
|
||||
|
||||
protected void OnMessageFlagsChanged(object sender, MessageFlagsChangedEventArgs args)
|
||||
=> HandleMessageFlagsChangeAsync(args.UniqueId, args.Flags).ConfigureAwait(false);
|
||||
protected void OnMessageFlagsChanged(object sender, MessageFlagsChangedEventArgs args)
|
||||
=> HandleMessageFlagsChangeAsync(args.UniqueId, args.Flags).ConfigureAwait(false);
|
||||
|
||||
protected async Task ManageUUIdBasedDeletedMessagesAsync(MailItemFolder localFolder, IMailFolder remoteFolder, CancellationToken cancellationToken = default)
|
||||
protected async Task ManageUUIdBasedDeletedMessagesAsync(MailItemFolder localFolder, IMailFolder remoteFolder, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var allUids = (await FolderService.GetKnownUidsForFolderAsync(localFolder.Id)).Select(a => new UniqueId(a)).ToList();
|
||||
|
||||
if (allUids.Count > 0)
|
||||
{
|
||||
var allUids = (await FolderService.GetKnownUidsForFolderAsync(localFolder.Id)).Select(a => new UniqueId(a)).ToList();
|
||||
var remoteAllUids = await remoteFolder.SearchAsync(SearchQuery.All, cancellationToken);
|
||||
var deletedUids = allUids.Except(remoteAllUids).ToList();
|
||||
|
||||
if (allUids.Count > 0)
|
||||
{
|
||||
var remoteAllUids = await remoteFolder.SearchAsync(SearchQuery.All, cancellationToken);
|
||||
var deletedUids = allUids.Except(remoteAllUids).ToList();
|
||||
|
||||
await HandleMessageDeletedAsync(deletedUids).ConfigureAwait(false);
|
||||
}
|
||||
await HandleMessageDeletedAsync(deletedUids).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,30 +2,29 @@
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Integration;
|
||||
|
||||
namespace Wino.Core.Synchronizers.ImapSync
|
||||
namespace Wino.Core.Synchronizers.ImapSync;
|
||||
|
||||
internal class ImapSynchronizationStrategyProvider : IImapSynchronizationStrategyProvider
|
||||
{
|
||||
internal class ImapSynchronizationStrategyProvider : IImapSynchronizationStrategyProvider
|
||||
private readonly QResyncSynchronizer _qResyncSynchronizer;
|
||||
private readonly CondstoreSynchronizer _condstoreSynchronizer;
|
||||
private readonly UidBasedSynchronizer _uidBasedSynchronizer;
|
||||
|
||||
public ImapSynchronizationStrategyProvider(QResyncSynchronizer qResyncSynchronizer, CondstoreSynchronizer condstoreSynchronizer, UidBasedSynchronizer uidBasedSynchronizer)
|
||||
{
|
||||
private readonly QResyncSynchronizer _qResyncSynchronizer;
|
||||
private readonly CondstoreSynchronizer _condstoreSynchronizer;
|
||||
private readonly UidBasedSynchronizer _uidBasedSynchronizer;
|
||||
_qResyncSynchronizer = qResyncSynchronizer;
|
||||
_condstoreSynchronizer = condstoreSynchronizer;
|
||||
_uidBasedSynchronizer = uidBasedSynchronizer;
|
||||
}
|
||||
|
||||
public ImapSynchronizationStrategyProvider(QResyncSynchronizer qResyncSynchronizer, CondstoreSynchronizer condstoreSynchronizer, UidBasedSynchronizer uidBasedSynchronizer)
|
||||
{
|
||||
_qResyncSynchronizer = qResyncSynchronizer;
|
||||
_condstoreSynchronizer = condstoreSynchronizer;
|
||||
_uidBasedSynchronizer = uidBasedSynchronizer;
|
||||
}
|
||||
public IImapSynchronizerStrategy GetSynchronizationStrategy(IImapClient client)
|
||||
{
|
||||
if (client is not WinoImapClient winoImapClient)
|
||||
throw new System.ArgumentException("Client must be of type WinoImapClient.", nameof(client));
|
||||
|
||||
public IImapSynchronizerStrategy GetSynchronizationStrategy(IImapClient client)
|
||||
{
|
||||
if (client is not WinoImapClient winoImapClient)
|
||||
throw new System.ArgumentException("Client must be of type WinoImapClient.", nameof(client));
|
||||
if (client.Capabilities.HasFlag(ImapCapabilities.QuickResync) && winoImapClient.IsQResyncEnabled) return _qResyncSynchronizer;
|
||||
if (client.Capabilities.HasFlag(ImapCapabilities.CondStore)) return _condstoreSynchronizer;
|
||||
|
||||
if (client.Capabilities.HasFlag(ImapCapabilities.QuickResync) && winoImapClient.IsQResyncEnabled) return _qResyncSynchronizer;
|
||||
if (client.Capabilities.HasFlag(ImapCapabilities.CondStore)) return _condstoreSynchronizer;
|
||||
|
||||
return _uidBasedSynchronizer;
|
||||
}
|
||||
return _uidBasedSynchronizer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,110 +12,109 @@ using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Integration;
|
||||
using IMailService = Wino.Core.Domain.Interfaces.IMailService;
|
||||
|
||||
namespace Wino.Core.Synchronizers.ImapSync
|
||||
namespace Wino.Core.Synchronizers.ImapSync;
|
||||
|
||||
/// <summary>
|
||||
/// RFC 5162 QRESYNC IMAP Synchronization strategy.
|
||||
/// </summary>
|
||||
internal class QResyncSynchronizer : ImapSynchronizationStrategyBase
|
||||
{
|
||||
/// <summary>
|
||||
/// RFC 5162 QRESYNC IMAP Synchronization strategy.
|
||||
/// </summary>
|
||||
internal class QResyncSynchronizer : ImapSynchronizationStrategyBase
|
||||
public QResyncSynchronizer(IFolderService folderService, IMailService mailService) : base(folderService, mailService)
|
||||
{
|
||||
public QResyncSynchronizer(IFolderService folderService, IMailService mailService) : base(folderService, mailService)
|
||||
}
|
||||
|
||||
public override async Task<List<string>> HandleSynchronizationAsync(IImapClient client,
|
||||
MailItemFolder folder,
|
||||
IImapSynchronizer synchronizer,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var downloadedMessageIds = new List<string>();
|
||||
|
||||
if (client is not WinoImapClient winoClient)
|
||||
throw new ImapSynchronizerStrategyException("Client must be of type WinoImapClient.");
|
||||
|
||||
if (!client.Capabilities.HasFlag(ImapCapabilities.QuickResync))
|
||||
throw new ImapSynchronizerStrategyException("Server does not support QRESYNC.");
|
||||
|
||||
if (!winoClient.IsQResyncEnabled)
|
||||
throw new ImapSynchronizerStrategyException("QRESYNC is not enabled for WinoImapClient.");
|
||||
|
||||
// Ready to implement QRESYNC synchronization.
|
||||
|
||||
IMailFolder remoteFolder = null;
|
||||
|
||||
Folder = folder;
|
||||
|
||||
try
|
||||
{
|
||||
remoteFolder = await client.GetFolderAsync(folder.RemoteFolderId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Check the Uid validity first.
|
||||
// If they don't match, clear all the local data and perform full-resync.
|
||||
|
||||
bool isCacheValid = remoteFolder.UidValidity == folder.UidValidity;
|
||||
|
||||
if (!isCacheValid)
|
||||
{
|
||||
// TODO: Remove all local data.
|
||||
}
|
||||
|
||||
// Perform QRESYNC synchronization.
|
||||
var localHighestModSeq = (ulong)folder.HighestModeSeq;
|
||||
|
||||
remoteFolder.MessagesVanished += OnMessagesVanished;
|
||||
remoteFolder.MessageFlagsChanged += OnMessageFlagsChanged;
|
||||
|
||||
var allUids = await FolderService.GetKnownUidsForFolderAsync(folder.Id);
|
||||
var allUniqueIds = allUids.Select(a => new UniqueId(a)).ToList();
|
||||
|
||||
await remoteFolder.OpenAsync(FolderAccess.ReadOnly, folder.UidValidity, localHighestModSeq, allUniqueIds).ConfigureAwait(false);
|
||||
|
||||
var changedUids = await GetChangedUidsAsync(client, remoteFolder, synchronizer, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
downloadedMessageIds = await HandleChangedUIdsAsync(synchronizer, remoteFolder, changedUids, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Update the local folder with the new highest mod-seq and validity.
|
||||
folder.HighestModeSeq = unchecked((long)remoteFolder.HighestModSeq);
|
||||
folder.UidValidity = remoteFolder.UidValidity;
|
||||
|
||||
await ManageUUIdBasedDeletedMessagesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await FolderService.UpdateFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override async Task<List<string>> HandleSynchronizationAsync(IImapClient client,
|
||||
MailItemFolder folder,
|
||||
IImapSynchronizer synchronizer,
|
||||
CancellationToken cancellationToken = default)
|
||||
catch (FolderNotFoundException)
|
||||
{
|
||||
var downloadedMessageIds = new List<string>();
|
||||
await FolderService.DeleteFolderAsync(folder.MailAccountId, folder.RemoteFolderId).ConfigureAwait(false);
|
||||
|
||||
if (client is not WinoImapClient winoClient)
|
||||
throw new ImapSynchronizerStrategyException("Client must be of type WinoImapClient.");
|
||||
|
||||
if (!client.Capabilities.HasFlag(ImapCapabilities.QuickResync))
|
||||
throw new ImapSynchronizerStrategyException("Server does not support QRESYNC.");
|
||||
|
||||
if (!winoClient.IsQResyncEnabled)
|
||||
throw new ImapSynchronizerStrategyException("QRESYNC is not enabled for WinoImapClient.");
|
||||
|
||||
// Ready to implement QRESYNC synchronization.
|
||||
|
||||
IMailFolder remoteFolder = null;
|
||||
|
||||
Folder = folder;
|
||||
|
||||
try
|
||||
return default;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
remoteFolder = await client.GetFolderAsync(folder.RemoteFolderId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Check the Uid validity first.
|
||||
// If they don't match, clear all the local data and perform full-resync.
|
||||
|
||||
bool isCacheValid = remoteFolder.UidValidity == folder.UidValidity;
|
||||
|
||||
if (!isCacheValid)
|
||||
if (remoteFolder != null)
|
||||
{
|
||||
// TODO: Remove all local data.
|
||||
}
|
||||
remoteFolder.MessagesVanished -= OnMessagesVanished;
|
||||
remoteFolder.MessageFlagsChanged -= OnMessageFlagsChanged;
|
||||
|
||||
// Perform QRESYNC synchronization.
|
||||
var localHighestModSeq = (ulong)folder.HighestModeSeq;
|
||||
|
||||
remoteFolder.MessagesVanished += OnMessagesVanished;
|
||||
remoteFolder.MessageFlagsChanged += OnMessageFlagsChanged;
|
||||
|
||||
var allUids = await FolderService.GetKnownUidsForFolderAsync(folder.Id);
|
||||
var allUniqueIds = allUids.Select(a => new UniqueId(a)).ToList();
|
||||
|
||||
await remoteFolder.OpenAsync(FolderAccess.ReadOnly, folder.UidValidity, localHighestModSeq, allUniqueIds).ConfigureAwait(false);
|
||||
|
||||
var changedUids = await GetChangedUidsAsync(client, remoteFolder, synchronizer, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
downloadedMessageIds = await HandleChangedUIdsAsync(synchronizer, remoteFolder, changedUids, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Update the local folder with the new highest mod-seq and validity.
|
||||
folder.HighestModeSeq = unchecked((long)remoteFolder.HighestModSeq);
|
||||
folder.UidValidity = remoteFolder.UidValidity;
|
||||
|
||||
await ManageUUIdBasedDeletedMessagesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await FolderService.UpdateFolderAsync(folder).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)
|
||||
{
|
||||
remoteFolder.MessagesVanished -= OnMessagesVanished;
|
||||
remoteFolder.MessageFlagsChanged -= OnMessageFlagsChanged;
|
||||
|
||||
if (remoteFolder.IsOpen)
|
||||
{
|
||||
await remoteFolder.CloseAsync();
|
||||
}
|
||||
await remoteFolder.CloseAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return downloadedMessageIds;
|
||||
}
|
||||
|
||||
internal override async Task<IList<UniqueId>> GetChangedUidsAsync(IImapClient client, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var localHighestModSeq = (ulong)Folder.HighestModeSeq;
|
||||
return await remoteFolder.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
return downloadedMessageIds;
|
||||
}
|
||||
|
||||
internal override async Task<IList<UniqueId>> GetChangedUidsAsync(IImapClient client, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var localHighestModSeq = (ulong)Folder.HighestModeSeq;
|
||||
return await remoteFolder.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,72 +10,71 @@ using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Integration;
|
||||
|
||||
namespace Wino.Core.Synchronizers.ImapSync
|
||||
namespace Wino.Core.Synchronizers.ImapSync;
|
||||
|
||||
/// <summary>
|
||||
/// Uid based IMAP Synchronization strategy.
|
||||
/// </summary>
|
||||
internal class UidBasedSynchronizer : ImapSynchronizationStrategyBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Uid based IMAP Synchronization strategy.
|
||||
/// </summary>
|
||||
internal class UidBasedSynchronizer : ImapSynchronizationStrategyBase
|
||||
public UidBasedSynchronizer(IFolderService folderService, Domain.Interfaces.IMailService mailService) : base(folderService, mailService)
|
||||
{
|
||||
public UidBasedSynchronizer(IFolderService folderService, Domain.Interfaces.IMailService mailService) : base(folderService, mailService)
|
||||
}
|
||||
|
||||
public override async Task<List<string>> 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));
|
||||
|
||||
Folder = folder;
|
||||
|
||||
var downloadedMessageIds = new List<string>();
|
||||
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(synchronizer, remoteFolder, remoteUids, cancellationToken).ConfigureAwait(false);
|
||||
await ManageUUIdBasedDeletedMessagesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override async Task<List<string>> HandleSynchronizationAsync(IImapClient client, MailItemFolder folder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default)
|
||||
catch (FolderNotFoundException)
|
||||
{
|
||||
if (client is not WinoImapClient winoClient)
|
||||
throw new ArgumentException("Client must be of type WinoImapClient.", nameof(client));
|
||||
await FolderService.DeleteFolderAsync(folder.MailAccountId, folder.RemoteFolderId).ConfigureAwait(false);
|
||||
|
||||
Folder = folder;
|
||||
return default;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
var downloadedMessageIds = new List<string>();
|
||||
IMailFolder remoteFolder = null;
|
||||
|
||||
try
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
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(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 != null)
|
||||
if (remoteFolder.IsOpen)
|
||||
{
|
||||
if (remoteFolder.IsOpen)
|
||||
{
|
||||
await remoteFolder.CloseAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
await remoteFolder.CloseAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return downloadedMessageIds;
|
||||
}
|
||||
|
||||
internal override Task<IList<UniqueId>> GetChangedUidsAsync(IImapClient client, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
return downloadedMessageIds;
|
||||
}
|
||||
|
||||
internal override Task<IList<UniqueId>> GetChangedUidsAsync(IImapClient client, IMailFolder remoteFolder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -21,435 +21,434 @@ using Wino.Core.Requests.Folder;
|
||||
using Wino.Core.Requests.Mail;
|
||||
using Wino.Messaging.UI;
|
||||
|
||||
namespace Wino.Core.Synchronizers
|
||||
namespace Wino.Core.Synchronizers;
|
||||
|
||||
public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEventType> : BaseSynchronizer<TBaseRequest>, IWinoSynchronizerBase
|
||||
{
|
||||
public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEventType> : BaseSynchronizer<TBaseRequest>, IWinoSynchronizerBase
|
||||
protected bool IsDisposing { get; private set; }
|
||||
|
||||
protected Dictionary<MailSynchronizationOptions, CancellationTokenSource> PendingSynchronizationRequest = new();
|
||||
|
||||
protected ILogger Logger = Log.ForContext<WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEventType>>();
|
||||
|
||||
protected WinoSynchronizer(MailAccount account) : base(account) { }
|
||||
|
||||
/// <summary>
|
||||
/// How many items per single HTTP call can be modified.
|
||||
/// </summary>
|
||||
public abstract uint BatchModificationSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// How many items must be downloaded per folder when the folder is first synchronized.
|
||||
/// </summary>
|
||||
public abstract uint InitialMessageDownloadCountPerFolder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Wino Mail Item package out of native message type with full Mime.
|
||||
/// </summary>
|
||||
/// <param name="message">Native message type for the synchronizer.</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Package that encapsulates downloaded Mime and additional information for adding new mail.</returns>
|
||||
public abstract Task<List<NewMailItemPackage>> CreateNewMailPackagesAsync(TMessageType message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the aliases of the account.
|
||||
/// Only available for Gmail right now.
|
||||
/// </summary>
|
||||
protected virtual Task SynchronizeAliasesAsync() => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Internally synchronizes the account's mails with the given options.
|
||||
/// Not exposed and overriden for each synchronizer.
|
||||
/// </summary>
|
||||
/// <param name="options">Synchronization options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Synchronization result that contains summary of the sync.</returns>
|
||||
protected abstract Task<MailSynchronizationResult> SynchronizeMailsInternalAsync(MailSynchronizationOptions options, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Internally synchronizes the events of the account with given options.
|
||||
/// Not exposed and overriden for each synchronizer.
|
||||
/// </summary>
|
||||
/// <param name="options">Synchronization options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Synchronization result that contains summary of the sync.</returns>
|
||||
protected abstract Task<CalendarSynchronizationResult> SynchronizeCalendarEventsInternalAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Batches network requests, executes them, and does the needed synchronization after the batch request execution.
|
||||
/// </summary>
|
||||
/// <param name="options">Synchronization options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Synchronization result that contains summary of the sync.</returns>
|
||||
public async Task<MailSynchronizationResult> SynchronizeMailsAsync(MailSynchronizationOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
protected bool IsDisposing { get; private set; }
|
||||
|
||||
protected Dictionary<MailSynchronizationOptions, CancellationTokenSource> PendingSynchronizationRequest = new();
|
||||
|
||||
protected ILogger Logger = Log.ForContext<WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEventType>>();
|
||||
|
||||
protected WinoSynchronizer(MailAccount account) : base(account) { }
|
||||
|
||||
/// <summary>
|
||||
/// How many items per single HTTP call can be modified.
|
||||
/// </summary>
|
||||
public abstract uint BatchModificationSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// How many items must be downloaded per folder when the folder is first synchronized.
|
||||
/// </summary>
|
||||
public abstract uint InitialMessageDownloadCountPerFolder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Wino Mail Item package out of native message type with full Mime.
|
||||
/// </summary>
|
||||
/// <param name="message">Native message type for the synchronizer.</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Package that encapsulates downloaded Mime and additional information for adding new mail.</returns>
|
||||
public abstract Task<List<NewMailItemPackage>> CreateNewMailPackagesAsync(TMessageType message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the aliases of the account.
|
||||
/// Only available for Gmail right now.
|
||||
/// </summary>
|
||||
protected virtual Task SynchronizeAliasesAsync() => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Internally synchronizes the account's mails with the given options.
|
||||
/// Not exposed and overriden for each synchronizer.
|
||||
/// </summary>
|
||||
/// <param name="options">Synchronization options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Synchronization result that contains summary of the sync.</returns>
|
||||
protected abstract Task<MailSynchronizationResult> SynchronizeMailsInternalAsync(MailSynchronizationOptions options, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Internally synchronizes the events of the account with given options.
|
||||
/// Not exposed and overriden for each synchronizer.
|
||||
/// </summary>
|
||||
/// <param name="options">Synchronization options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Synchronization result that contains summary of the sync.</returns>
|
||||
protected abstract Task<CalendarSynchronizationResult> SynchronizeCalendarEventsInternalAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Batches network requests, executes them, and does the needed synchronization after the batch request execution.
|
||||
/// </summary>
|
||||
/// <param name="options">Synchronization options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Synchronization result that contains summary of the sync.</returns>
|
||||
public async Task<MailSynchronizationResult> SynchronizeMailsAsync(MailSynchronizationOptions options, CancellationToken cancellationToken = default)
|
||||
try
|
||||
{
|
||||
try
|
||||
if (!ShouldQueueMailSynchronization(options))
|
||||
{
|
||||
if (!ShouldQueueMailSynchronization(options))
|
||||
Log.Debug($"{options.Type} synchronization is ignored.");
|
||||
return MailSynchronizationResult.Canceled;
|
||||
}
|
||||
|
||||
var newCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
|
||||
PendingSynchronizationRequest.Add(options, newCancellationTokenSource);
|
||||
activeSynchronizationCancellationToken = newCancellationTokenSource.Token;
|
||||
|
||||
await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken);
|
||||
|
||||
PublishSynchronizationProgress(1);
|
||||
|
||||
// ImapSynchronizer will send this type when an Idle client receives a notification of changes.
|
||||
// We should not execute requests in this case.
|
||||
bool shouldExecuteRequests = options.Type != MailSynchronizationType.IMAPIdle;
|
||||
|
||||
bool shouldDelayExecution = false;
|
||||
int maxExecutionDelay = 0;
|
||||
|
||||
if (shouldExecuteRequests && changeRequestQueue.Any())
|
||||
{
|
||||
State = AccountSynchronizerState.ExecutingRequests;
|
||||
|
||||
List<IRequestBundle<TBaseRequest>> nativeRequests = new();
|
||||
|
||||
List<IRequestBase> requestCopies = new(changeRequestQueue);
|
||||
|
||||
var keys = changeRequestQueue.GroupBy(a => a.GroupingKey());
|
||||
|
||||
foreach (var group in keys)
|
||||
{
|
||||
Log.Debug($"{options.Type} synchronization is ignored.");
|
||||
return MailSynchronizationResult.Canceled;
|
||||
}
|
||||
var key = group.Key;
|
||||
|
||||
var newCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
|
||||
PendingSynchronizationRequest.Add(options, newCancellationTokenSource);
|
||||
activeSynchronizationCancellationToken = newCancellationTokenSource.Token;
|
||||
|
||||
await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken);
|
||||
|
||||
PublishSynchronizationProgress(1);
|
||||
|
||||
// ImapSynchronizer will send this type when an Idle client receives a notification of changes.
|
||||
// We should not execute requests in this case.
|
||||
bool shouldExecuteRequests = options.Type != MailSynchronizationType.IMAPIdle;
|
||||
|
||||
bool shouldDelayExecution = false;
|
||||
int maxExecutionDelay = 0;
|
||||
|
||||
if (shouldExecuteRequests && changeRequestQueue.Any())
|
||||
{
|
||||
State = AccountSynchronizerState.ExecutingRequests;
|
||||
|
||||
List<IRequestBundle<TBaseRequest>> nativeRequests = new();
|
||||
|
||||
List<IRequestBase> requestCopies = new(changeRequestQueue);
|
||||
|
||||
var keys = changeRequestQueue.GroupBy(a => a.GroupingKey());
|
||||
|
||||
foreach (var group in keys)
|
||||
if (key is MailSynchronizerOperation mailSynchronizerOperation)
|
||||
{
|
||||
var key = group.Key;
|
||||
|
||||
if (key is MailSynchronizerOperation mailSynchronizerOperation)
|
||||
switch (mailSynchronizerOperation)
|
||||
{
|
||||
switch (mailSynchronizerOperation)
|
||||
{
|
||||
case MailSynchronizerOperation.MarkRead:
|
||||
nativeRequests.AddRange(MarkRead(new BatchMarkReadRequest(group.Cast<MarkReadRequest>())));
|
||||
break;
|
||||
case MailSynchronizerOperation.Move:
|
||||
nativeRequests.AddRange(Move(new BatchMoveRequest(group.Cast<MoveRequest>())));
|
||||
break;
|
||||
case MailSynchronizerOperation.Delete:
|
||||
nativeRequests.AddRange(Delete(new BatchDeleteRequest(group.Cast<DeleteRequest>())));
|
||||
break;
|
||||
case MailSynchronizerOperation.CreateDraft:
|
||||
nativeRequests.AddRange(CreateDraft(group.ElementAt(0) as CreateDraftRequest));
|
||||
break;
|
||||
case MailSynchronizerOperation.Send:
|
||||
nativeRequests.AddRange(SendDraft(group.ElementAt(0) as SendDraftRequest));
|
||||
break;
|
||||
case MailSynchronizerOperation.ChangeFlag:
|
||||
nativeRequests.AddRange(ChangeFlag(new BatchChangeFlagRequest(group.Cast<ChangeFlagRequest>())));
|
||||
break;
|
||||
case MailSynchronizerOperation.AlwaysMoveTo:
|
||||
nativeRequests.AddRange(AlwaysMoveTo(new BatchAlwaysMoveToRequest(group.Cast<AlwaysMoveToRequest>())));
|
||||
break;
|
||||
case MailSynchronizerOperation.MoveToFocused:
|
||||
nativeRequests.AddRange(MoveToFocused(new BatchMoveToFocusedRequest(group.Cast<MoveToFocusedRequest>())));
|
||||
break;
|
||||
case MailSynchronizerOperation.Archive:
|
||||
nativeRequests.AddRange(Archive(new BatchArchiveRequest(group.Cast<ArchiveRequest>())));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (key is FolderSynchronizerOperation folderSynchronizerOperation)
|
||||
{
|
||||
switch (folderSynchronizerOperation)
|
||||
{
|
||||
case FolderSynchronizerOperation.RenameFolder:
|
||||
nativeRequests.AddRange(RenameFolder(group.ElementAt(0) as RenameFolderRequest));
|
||||
break;
|
||||
case FolderSynchronizerOperation.EmptyFolder:
|
||||
nativeRequests.AddRange(EmptyFolder(group.ElementAt(0) as EmptyFolderRequest));
|
||||
break;
|
||||
case FolderSynchronizerOperation.MarkFolderRead:
|
||||
nativeRequests.AddRange(MarkFolderAsRead(group.ElementAt(0) as MarkFolderAsReadRequest));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
case MailSynchronizerOperation.MarkRead:
|
||||
nativeRequests.AddRange(MarkRead(new BatchMarkReadRequest(group.Cast<MarkReadRequest>())));
|
||||
break;
|
||||
case MailSynchronizerOperation.Move:
|
||||
nativeRequests.AddRange(Move(new BatchMoveRequest(group.Cast<MoveRequest>())));
|
||||
break;
|
||||
case MailSynchronizerOperation.Delete:
|
||||
nativeRequests.AddRange(Delete(new BatchDeleteRequest(group.Cast<DeleteRequest>())));
|
||||
break;
|
||||
case MailSynchronizerOperation.CreateDraft:
|
||||
nativeRequests.AddRange(CreateDraft(group.ElementAt(0) as CreateDraftRequest));
|
||||
break;
|
||||
case MailSynchronizerOperation.Send:
|
||||
nativeRequests.AddRange(SendDraft(group.ElementAt(0) as SendDraftRequest));
|
||||
break;
|
||||
case MailSynchronizerOperation.ChangeFlag:
|
||||
nativeRequests.AddRange(ChangeFlag(new BatchChangeFlagRequest(group.Cast<ChangeFlagRequest>())));
|
||||
break;
|
||||
case MailSynchronizerOperation.AlwaysMoveTo:
|
||||
nativeRequests.AddRange(AlwaysMoveTo(new BatchAlwaysMoveToRequest(group.Cast<AlwaysMoveToRequest>())));
|
||||
break;
|
||||
case MailSynchronizerOperation.MoveToFocused:
|
||||
nativeRequests.AddRange(MoveToFocused(new BatchMoveToFocusedRequest(group.Cast<MoveToFocusedRequest>())));
|
||||
break;
|
||||
case MailSynchronizerOperation.Archive:
|
||||
nativeRequests.AddRange(Archive(new BatchArchiveRequest(group.Cast<ArchiveRequest>())));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
changeRequestQueue.Clear();
|
||||
|
||||
Console.WriteLine($"Prepared {nativeRequests.Count()} native requests");
|
||||
|
||||
await ExecuteNativeRequestsAsync(nativeRequests, activeSynchronizationCancellationToken).ConfigureAwait(false);
|
||||
|
||||
PublishUnreadItemChanges();
|
||||
|
||||
// Execute request sync options should be re-calculated after execution.
|
||||
// This is the part we decide which individual folders must be synchronized
|
||||
// after the batch request execution.
|
||||
if (options.Type == MailSynchronizationType.ExecuteRequests)
|
||||
options = GetSynchronizationOptionsAfterRequestExecution(requestCopies, options.Id);
|
||||
|
||||
// Let servers to finish their job. Sometimes the servers doesn't respond immediately.
|
||||
// Bug: if Outlook can't create the message in Sent Items folder before this delay,
|
||||
// message will not appear in user's inbox since it's not in the Sent Items folder.
|
||||
|
||||
shouldDelayExecution =
|
||||
(Account.ProviderType == MailProviderType.Outlook)
|
||||
&& requestCopies.Any(a => a.ResynchronizationDelay > 0);
|
||||
|
||||
if (shouldDelayExecution)
|
||||
else if (key is FolderSynchronizerOperation folderSynchronizerOperation)
|
||||
{
|
||||
maxExecutionDelay = requestCopies.Aggregate(0, (max, next) => Math.Max(max, next.ResynchronizationDelay));
|
||||
}
|
||||
|
||||
// In terms of flag/read changes, there is no point of synchronizing must have folders.
|
||||
options.ExcludeMustHaveFolders = requestCopies.All(a => a is ICustomFolderSynchronizationRequest request && request.ExcludeMustHaveFolders);
|
||||
}
|
||||
|
||||
State = AccountSynchronizerState.Synchronizing;
|
||||
|
||||
// Handle special synchronization types.
|
||||
|
||||
// Profile information sync.
|
||||
if (options.Type == MailSynchronizationType.UpdateProfile)
|
||||
{
|
||||
if (!Account.IsProfileInfoSyncSupported) return MailSynchronizationResult.Empty;
|
||||
|
||||
ProfileInformation newProfileInformation = null;
|
||||
|
||||
try
|
||||
{
|
||||
newProfileInformation = await SynchronizeProfileInformationInternalAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to update profile information for {Name}", Account.Name);
|
||||
|
||||
return MailSynchronizationResult.Failed;
|
||||
}
|
||||
|
||||
return MailSynchronizationResult.Completed(newProfileInformation);
|
||||
}
|
||||
|
||||
// Alias sync.
|
||||
if (options.Type == MailSynchronizationType.Alias)
|
||||
{
|
||||
if (!Account.IsAliasSyncSupported) return MailSynchronizationResult.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
await SynchronizeAliasesAsync();
|
||||
|
||||
return MailSynchronizationResult.Empty;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to update aliases for {Name}", Account.Name);
|
||||
|
||||
return MailSynchronizationResult.Failed;
|
||||
switch (folderSynchronizerOperation)
|
||||
{
|
||||
case FolderSynchronizerOperation.RenameFolder:
|
||||
nativeRequests.AddRange(RenameFolder(group.ElementAt(0) as RenameFolderRequest));
|
||||
break;
|
||||
case FolderSynchronizerOperation.EmptyFolder:
|
||||
nativeRequests.AddRange(EmptyFolder(group.ElementAt(0) as EmptyFolderRequest));
|
||||
break;
|
||||
case FolderSynchronizerOperation.MarkFolderRead:
|
||||
nativeRequests.AddRange(MarkFolderAsRead(group.ElementAt(0) as MarkFolderAsReadRequest));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDelayExecution)
|
||||
{
|
||||
await Task.Delay(maxExecutionDelay);
|
||||
}
|
||||
changeRequestQueue.Clear();
|
||||
|
||||
// Start the internal synchronization.
|
||||
var synchronizationResult = await SynchronizeMailsInternalAsync(options, activeSynchronizationCancellationToken).ConfigureAwait(false);
|
||||
Console.WriteLine($"Prepared {nativeRequests.Count()} native requests");
|
||||
|
||||
await ExecuteNativeRequestsAsync(nativeRequests, activeSynchronizationCancellationToken).ConfigureAwait(false);
|
||||
|
||||
PublishUnreadItemChanges();
|
||||
|
||||
return synchronizationResult;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Logger.Warning("Synchronization canceled.");
|
||||
// Execute request sync options should be re-calculated after execution.
|
||||
// This is the part we decide which individual folders must be synchronized
|
||||
// after the batch request execution.
|
||||
if (options.Type == MailSynchronizationType.ExecuteRequests)
|
||||
options = GetSynchronizationOptionsAfterRequestExecution(requestCopies, options.Id);
|
||||
|
||||
return MailSynchronizationResult.Canceled;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, "Synchronization failed for {Name}", Account.Name);
|
||||
// Let servers to finish their job. Sometimes the servers doesn't respond immediately.
|
||||
// Bug: if Outlook can't create the message in Sent Items folder before this delay,
|
||||
// message will not appear in user's inbox since it's not in the Sent Items folder.
|
||||
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Find the request and remove it from the pending list.
|
||||
shouldDelayExecution =
|
||||
(Account.ProviderType == MailProviderType.Outlook)
|
||||
&& requestCopies.Any(a => a.ResynchronizationDelay > 0);
|
||||
|
||||
var pendingRequest = PendingSynchronizationRequest.FirstOrDefault(a => a.Key.Id == options.Id);
|
||||
|
||||
if (pendingRequest.Key != null)
|
||||
if (shouldDelayExecution)
|
||||
{
|
||||
PendingSynchronizationRequest.Remove(pendingRequest.Key);
|
||||
maxExecutionDelay = requestCopies.Aggregate(0, (max, next) => Math.Max(max, next.ResynchronizationDelay));
|
||||
}
|
||||
|
||||
// Reset account progress to hide the progress.
|
||||
PublishSynchronizationProgress(0);
|
||||
|
||||
State = AccountSynchronizerState.Idle;
|
||||
synchronizationSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batches network requests, executes them, and does the needed synchronization after the batch request execution.
|
||||
/// </summary>
|
||||
/// <param name="options">Synchronization options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Synchronization result that contains summary of the sync.</returns>
|
||||
public Task<CalendarSynchronizationResult> SynchronizeCalendarEventsAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// TODO: Execute requests for calendar events.
|
||||
return SynchronizeCalendarEventsInternalAsync(options, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates unread item counts for some folders and account.
|
||||
/// Sends a message that shell can pick up and update the UI.
|
||||
/// </summary>
|
||||
private void PublishUnreadItemChanges()
|
||||
=> WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(Account.Id));
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message to the shell to update the synchronization progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">Percentage of the progress.</param>
|
||||
public void PublishSynchronizationProgress(double progress)
|
||||
=> WeakReferenceMessenger.Default.Send(new AccountSynchronizationProgressUpdatedMessage(Account.Id, progress));
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to find out the best possible synchronization options after the batch request execution.
|
||||
/// </summary>
|
||||
/// <param name="batches">Batch requests to run in synchronization.</param>
|
||||
/// <returns>New synchronization options with minimal HTTP effort.</returns>
|
||||
private MailSynchronizationOptions GetSynchronizationOptionsAfterRequestExecution(List<IRequestBase> requests, Guid existingSynchronizationId)
|
||||
{
|
||||
List<Guid> synchronizationFolderIds = requests
|
||||
.Where(a => a is ICustomFolderSynchronizationRequest)
|
||||
.Cast<ICustomFolderSynchronizationRequest>()
|
||||
.SelectMany(a => a.SynchronizationFolderIds)
|
||||
.ToList();
|
||||
|
||||
var options = new MailSynchronizationOptions()
|
||||
{
|
||||
AccountId = Account.Id,
|
||||
};
|
||||
|
||||
options.Id = existingSynchronizationId;
|
||||
|
||||
if (synchronizationFolderIds.Count > 0)
|
||||
{
|
||||
// Gather FolderIds to synchronize.
|
||||
|
||||
options.Type = MailSynchronizationType.CustomFolders;
|
||||
options.SynchronizationFolderIds = synchronizationFolderIds;
|
||||
}
|
||||
else
|
||||
{
|
||||
// At this point it's a mix of everything. Do full sync.
|
||||
options.Type = MailSynchronizationType.FullFolders;
|
||||
// In terms of flag/read changes, there is no point of synchronizing must have folders.
|
||||
options.ExcludeMustHaveFolders = requestCopies.All(a => a is ICustomFolderSynchronizationRequest request && request.ExcludeMustHaveFolders);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
State = AccountSynchronizerState.Synchronizing;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the mail synchronization should be queued or not.
|
||||
/// </summary>
|
||||
/// <param name="options">New mail sync request.</param>
|
||||
/// <returns>Whether sync should be queued or not.</returns>
|
||||
private bool ShouldQueueMailSynchronization(MailSynchronizationOptions options)
|
||||
{
|
||||
// Multiple IMAPIdle requests are ignored.
|
||||
if (options.Type == MailSynchronizationType.IMAPIdle &&
|
||||
PendingSynchronizationRequest.Any(a => a.Key.Type == MailSynchronizationType.IMAPIdle))
|
||||
// Handle special synchronization types.
|
||||
|
||||
// Profile information sync.
|
||||
if (options.Type == MailSynchronizationType.UpdateProfile)
|
||||
{
|
||||
return false;
|
||||
if (!Account.IsProfileInfoSyncSupported) return MailSynchronizationResult.Empty;
|
||||
|
||||
ProfileInformation newProfileInformation = null;
|
||||
|
||||
try
|
||||
{
|
||||
newProfileInformation = await SynchronizeProfileInformationInternalAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to update profile information for {Name}", Account.Name);
|
||||
|
||||
return MailSynchronizationResult.Failed;
|
||||
}
|
||||
|
||||
return MailSynchronizationResult.Completed(newProfileInformation);
|
||||
}
|
||||
|
||||
// Executing requests may trigger idle sync.
|
||||
// If there are pending execute requests cancel idle change.
|
||||
|
||||
// TODO: Ideally this check should only work for Inbox execute requests.
|
||||
// Check if request folders contains Inbox.
|
||||
|
||||
if (options.Type == MailSynchronizationType.IMAPIdle &&
|
||||
PendingSynchronizationRequest.Any(a => a.Key.Type == MailSynchronizationType.ExecuteRequests))
|
||||
// Alias sync.
|
||||
if (options.Type == MailSynchronizationType.Alias)
|
||||
{
|
||||
return false;
|
||||
if (!Account.IsAliasSyncSupported) return MailSynchronizationResult.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
await SynchronizeAliasesAsync();
|
||||
|
||||
return MailSynchronizationResult.Empty;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to update aliases for {Name}", Account.Name);
|
||||
|
||||
return MailSynchronizationResult.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#region Mail/Folder Operations
|
||||
|
||||
public virtual bool DelaySendOperationSynchronization() => false;
|
||||
public virtual List<IRequestBundle<TBaseRequest>> Move(BatchMoveRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> ChangeFlag(BatchChangeFlagRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> MarkRead(BatchMarkReadRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> Delete(BatchDeleteRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> AlwaysMoveTo(BatchAlwaysMoveToRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> MoveToFocused(BatchMoveToFocusedRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> CreateDraft(CreateDraftRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> SendDraft(SendDraftRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> Archive(BatchArchiveRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> RenameFolder(RenameFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> EmptyFolder(EmptyFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> MarkFolderAsRead(MarkFolderAsReadRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
|
||||
#endregion
|
||||
|
||||
#region Calendar Operations
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a single missing message from synchronizer and saves it to given FileId from IMailItem.
|
||||
/// </summary>
|
||||
/// <param name="mailItem">Mail item that its mime file does not exist on the disk.</param>
|
||||
/// <param name="transferProgress">Optional download progress for IMAP synchronizer.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public virtual Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress = null, CancellationToken cancellationToken = default) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
|
||||
public List<IRequestBundle<ImapRequest>> CreateSingleTaskBundle(Func<IImapClient, IRequestBase, Task> action, IRequestBase request, IUIChangeRequest uIChangeRequest)
|
||||
{
|
||||
return [new ImapRequestBundle(new ImapRequest(action, request), request, uIChangeRequest)];
|
||||
}
|
||||
|
||||
public List<IRequestBundle<ImapRequest>> CreateTaskBundle<TSingeRequestType>(Func<IImapClient, TSingeRequestType, Task> value,
|
||||
List<TSingeRequestType> requests)
|
||||
where TSingeRequestType : IRequestBase, IUIChangeRequest
|
||||
{
|
||||
List<IRequestBundle<ImapRequest>> ret = [];
|
||||
|
||||
foreach (var request in requests)
|
||||
if (shouldDelayExecution)
|
||||
{
|
||||
ret.Add(new ImapRequestBundle(new ImapRequest<TSingeRequestType>(value, request), request, request));
|
||||
await Task.Delay(maxExecutionDelay);
|
||||
}
|
||||
|
||||
return ret;
|
||||
// Start the internal synchronization.
|
||||
var synchronizationResult = await SynchronizeMailsInternalAsync(options, activeSynchronizationCancellationToken).ConfigureAwait(false);
|
||||
|
||||
PublishUnreadItemChanges();
|
||||
|
||||
return synchronizationResult;
|
||||
}
|
||||
|
||||
public virtual Task KillSynchronizerAsync()
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
IsDisposing = true;
|
||||
CancelAllSynchronizations();
|
||||
Logger.Warning("Synchronization canceled.");
|
||||
|
||||
return Task.CompletedTask;
|
||||
return MailSynchronizationResult.Canceled;
|
||||
}
|
||||
|
||||
protected void CancelAllSynchronizations()
|
||||
catch (Exception ex)
|
||||
{
|
||||
foreach (var request in PendingSynchronizationRequest)
|
||||
Logger.Error(ex, "Synchronization failed for {Name}", Account.Name);
|
||||
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Find the request and remove it from the pending list.
|
||||
|
||||
var pendingRequest = PendingSynchronizationRequest.FirstOrDefault(a => a.Key.Id == options.Id);
|
||||
|
||||
if (pendingRequest.Key != null)
|
||||
{
|
||||
request.Value.Cancel();
|
||||
request.Value.Dispose();
|
||||
PendingSynchronizationRequest.Remove(pendingRequest.Key);
|
||||
}
|
||||
|
||||
// Reset account progress to hide the progress.
|
||||
PublishSynchronizationProgress(0);
|
||||
|
||||
State = AccountSynchronizerState.Idle;
|
||||
synchronizationSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batches network requests, executes them, and does the needed synchronization after the batch request execution.
|
||||
/// </summary>
|
||||
/// <param name="options">Synchronization options.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Synchronization result that contains summary of the sync.</returns>
|
||||
public Task<CalendarSynchronizationResult> SynchronizeCalendarEventsAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// TODO: Execute requests for calendar events.
|
||||
return SynchronizeCalendarEventsInternalAsync(options, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates unread item counts for some folders and account.
|
||||
/// Sends a message that shell can pick up and update the UI.
|
||||
/// </summary>
|
||||
private void PublishUnreadItemChanges()
|
||||
=> WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(Account.Id));
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message to the shell to update the synchronization progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">Percentage of the progress.</param>
|
||||
public void PublishSynchronizationProgress(double progress)
|
||||
=> WeakReferenceMessenger.Default.Send(new AccountSynchronizationProgressUpdatedMessage(Account.Id, progress));
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to find out the best possible synchronization options after the batch request execution.
|
||||
/// </summary>
|
||||
/// <param name="batches">Batch requests to run in synchronization.</param>
|
||||
/// <returns>New synchronization options with minimal HTTP effort.</returns>
|
||||
private MailSynchronizationOptions GetSynchronizationOptionsAfterRequestExecution(List<IRequestBase> requests, Guid existingSynchronizationId)
|
||||
{
|
||||
List<Guid> synchronizationFolderIds = requests
|
||||
.Where(a => a is ICustomFolderSynchronizationRequest)
|
||||
.Cast<ICustomFolderSynchronizationRequest>()
|
||||
.SelectMany(a => a.SynchronizationFolderIds)
|
||||
.ToList();
|
||||
|
||||
var options = new MailSynchronizationOptions()
|
||||
{
|
||||
AccountId = Account.Id,
|
||||
};
|
||||
|
||||
options.Id = existingSynchronizationId;
|
||||
|
||||
if (synchronizationFolderIds.Count > 0)
|
||||
{
|
||||
// Gather FolderIds to synchronize.
|
||||
|
||||
options.Type = MailSynchronizationType.CustomFolders;
|
||||
options.SynchronizationFolderIds = synchronizationFolderIds;
|
||||
}
|
||||
else
|
||||
{
|
||||
// At this point it's a mix of everything. Do full sync.
|
||||
options.Type = MailSynchronizationType.FullFolders;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the mail synchronization should be queued or not.
|
||||
/// </summary>
|
||||
/// <param name="options">New mail sync request.</param>
|
||||
/// <returns>Whether sync should be queued or not.</returns>
|
||||
private bool ShouldQueueMailSynchronization(MailSynchronizationOptions options)
|
||||
{
|
||||
// Multiple IMAPIdle requests are ignored.
|
||||
if (options.Type == MailSynchronizationType.IMAPIdle &&
|
||||
PendingSynchronizationRequest.Any(a => a.Key.Type == MailSynchronizationType.IMAPIdle))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Executing requests may trigger idle sync.
|
||||
// If there are pending execute requests cancel idle change.
|
||||
|
||||
// TODO: Ideally this check should only work for Inbox execute requests.
|
||||
// Check if request folders contains Inbox.
|
||||
|
||||
if (options.Type == MailSynchronizationType.IMAPIdle &&
|
||||
PendingSynchronizationRequest.Any(a => a.Key.Type == MailSynchronizationType.ExecuteRequests))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#region Mail/Folder Operations
|
||||
|
||||
public virtual bool DelaySendOperationSynchronization() => false;
|
||||
public virtual List<IRequestBundle<TBaseRequest>> Move(BatchMoveRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> ChangeFlag(BatchChangeFlagRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> MarkRead(BatchMarkReadRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> Delete(BatchDeleteRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> AlwaysMoveTo(BatchAlwaysMoveToRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> MoveToFocused(BatchMoveToFocusedRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> CreateDraft(CreateDraftRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> SendDraft(SendDraftRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> Archive(BatchArchiveRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> RenameFolder(RenameFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> EmptyFolder(EmptyFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> MarkFolderAsRead(MarkFolderAsReadRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
|
||||
#endregion
|
||||
|
||||
#region Calendar Operations
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a single missing message from synchronizer and saves it to given FileId from IMailItem.
|
||||
/// </summary>
|
||||
/// <param name="mailItem">Mail item that its mime file does not exist on the disk.</param>
|
||||
/// <param name="transferProgress">Optional download progress for IMAP synchronizer.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
public virtual Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress = null, CancellationToken cancellationToken = default) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
|
||||
public List<IRequestBundle<ImapRequest>> CreateSingleTaskBundle(Func<IImapClient, IRequestBase, Task> action, IRequestBase request, IUIChangeRequest uIChangeRequest)
|
||||
{
|
||||
return [new ImapRequestBundle(new ImapRequest(action, request), request, uIChangeRequest)];
|
||||
}
|
||||
|
||||
public List<IRequestBundle<ImapRequest>> CreateTaskBundle<TSingeRequestType>(Func<IImapClient, TSingeRequestType, Task> value,
|
||||
List<TSingeRequestType> requests)
|
||||
where TSingeRequestType : IRequestBase, IUIChangeRequest
|
||||
{
|
||||
List<IRequestBundle<ImapRequest>> ret = [];
|
||||
|
||||
foreach (var request in requests)
|
||||
{
|
||||
ret.Add(new ImapRequestBundle(new ImapRequest<TSingeRequestType>(value, request), request, request));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public virtual Task KillSynchronizerAsync()
|
||||
{
|
||||
IsDisposing = true;
|
||||
CancelAllSynchronizations();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected void CancelAllSynchronizations()
|
||||
{
|
||||
foreach (var request in PendingSynchronizationRequest)
|
||||
{
|
||||
request.Value.Cancel();
|
||||
request.Value.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user