Folder operations, Gmail folder sync improvements and rework of menu items. (#273)
* New rename folder dialog keys. * Insfra work for folder operations and rename folder code. * RenameFolder for Gmail. * Fixed input dialog to take custom take for primary button. * Missing rename for DS call. * Outlook to throw exception in case of error. * Implemented rename folder functionality for Outlook. * Remove default primary text from input dialog. * Fixed an issue where outlook folder rename does not work. * Disable vertical scroll for composing page editor items. * Fixing some issues with imap folder sync. * fix copy pasta * TODO folder update/removed overrides for shell. * New rename folder dialog keys. * Insfra work for folder operations and rename folder code. * RenameFolder for Gmail. * Fixed input dialog to take custom take for primary button. * Missing rename for DS call. * Outlook to throw exception in case of error. * Implemented rename folder functionality for Outlook. * Remove default primary text from input dialog. * Fixed an issue where outlook folder rename does not work. * Disable vertical scroll for composing page editor items. * Fixing some issues with imap folder sync. * fix copy pasta * TODO folder update/removed overrides for shell. * New rename folder dialog keys. * Insfra work for folder operations and rename folder code. * RenameFolder for Gmail. * Fixed input dialog to take custom take for primary button. * Missing rename for DS call. * Outlook to throw exception in case of error. * Implemented rename folder functionality for Outlook. * Remove default primary text from input dialog. * Fixed an issue where outlook folder rename does not work. * Disable vertical scroll for composing page editor items. * Fixing some issues with imap folder sync. * fix copy pasta * TODO folder update/removed overrides for shell. * New rename folder dialog keys. * Fixed an issue where redundant older updates causing pivots to be re-created. * New empty folder request * New rename folder dialog keys. * Insfra work for folder operations and rename folder code. * RenameFolder for Gmail. * Fixed input dialog to take custom take for primary button. * Missing rename for DS call. * Outlook to throw exception in case of error. * Implemented rename folder functionality for Outlook. * Remove default primary text from input dialog. * Fixed an issue where outlook folder rename does not work. * Fixing some issues with imap folder sync. * fix copy pasta * TODO folder update/removed overrides for shell. * New rename folder dialog keys. * New rename folder dialog keys. * New rename folder dialog keys. * Fixed an issue where redundant older updates causing pivots to be re-created. * New empty folder request * Enable empty folder on base sync. * Move updates on event listeners. * Remove folder UI messages. * Reworked folder synchronization for gmail. * Loading folders on the fly as the selected account changed instead of relying on cached menu items. * Merged account folder items, re-navigating to existing rendering page. * - Reworked merged account menu system. - Reworked unread item count loadings. - Fixed back button visibility. - Instant rendering of mails if renderer is active. - Animation fixes. - Menu item re-load crash/hang fixes. * Handle folder renaming on the UI. * Empty folder for all synchronizers. * New execution delay mechanism and handling folder mark as read for all synchronizers. * Revert UI changes on failure for IMAP. * Remove duplicate translation keys. * Cleanup.
This commit is contained in:
@@ -149,11 +149,10 @@ namespace Wino.Core.Synchronizers
|
||||
await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken);
|
||||
|
||||
// Let servers to finish their job. Sometimes the servers doesn't respond immediately.
|
||||
// TODO: Outlook sends back the deleted Draft. Might be a bug in the graph API or in Wino.
|
||||
|
||||
var hasSendDraftRequest = batches.Any(a => a is BatchSendDraftRequestRequest);
|
||||
bool shouldDelayExecution = batches.Any(a => a.DelayExecution);
|
||||
|
||||
if (hasSendDraftRequest && DelaySendOperationSynchronization())
|
||||
if (shouldDelayExecution)
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
}
|
||||
@@ -227,8 +226,13 @@ namespace Wino.Core.Synchronizers
|
||||
changeRequestQueue.TryTake(out _);
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (changeRequestQueue.TryTake(out request))
|
||||
{
|
||||
// This is a folder operation.
|
||||
// There is no need to batch them since Users can't do folder ops in bulk.
|
||||
|
||||
batchList.Add(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,6 +276,15 @@ namespace Wino.Core.Synchronizers
|
||||
case MailSynchronizerOperation.CreateDraft:
|
||||
yield return CreateDraft((BatchCreateDraftRequest)item);
|
||||
break;
|
||||
case MailSynchronizerOperation.RenameFolder:
|
||||
yield return RenameFolder((RenameFolderRequest)item);
|
||||
break;
|
||||
case MailSynchronizerOperation.EmptyFolder:
|
||||
yield return EmptyFolder((EmptyFolderRequest)item);
|
||||
break;
|
||||
case MailSynchronizerOperation.MarkFolderRead:
|
||||
yield return MarkFolderAsRead((MarkFolderAsReadRequest)item);
|
||||
break;
|
||||
case MailSynchronizerOperation.Archive:
|
||||
yield return Archive((BatchArchiveRequest)item);
|
||||
break;
|
||||
@@ -287,12 +300,9 @@ namespace Wino.Core.Synchronizers
|
||||
/// </summary>
|
||||
/// <param name="batches">Batch requests to run in synchronization.</param>
|
||||
/// <returns>New synchronization options with minimal HTTP effort.</returns>
|
||||
private SynchronizationOptions GetSynchronizationOptionsAfterRequestExecution(IEnumerable<IRequestBase> batches)
|
||||
private SynchronizationOptions GetSynchronizationOptionsAfterRequestExecution(IEnumerable<IRequestBase> requests)
|
||||
{
|
||||
// TODO: Check folders only.
|
||||
var batchItems = batches.Where(a => a is IBatchChangeRequest).Cast<IBatchChangeRequest>();
|
||||
|
||||
var requests = batchItems.SelectMany(a => a.Items);
|
||||
bool isAllCustomSynchronizationRequests = requests.All(a => a is ICustomFolderSynchronizationRequest);
|
||||
|
||||
var options = new SynchronizationOptions()
|
||||
{
|
||||
@@ -300,9 +310,7 @@ namespace Wino.Core.Synchronizers
|
||||
Type = SynchronizationType.FoldersOnly
|
||||
};
|
||||
|
||||
bool isCustomSynchronization = requests.All(a => a is ICustomFolderSynchronizationRequest);
|
||||
|
||||
if (isCustomSynchronization)
|
||||
if (isAllCustomSynchronizationRequests)
|
||||
{
|
||||
// Gather FolderIds to synchronize.
|
||||
|
||||
@@ -327,6 +335,9 @@ namespace Wino.Core.Synchronizers
|
||||
public virtual IEnumerable<IRequestBundle<TBaseRequest>> MoveToFocused(BatchMoveToFocusedRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual IEnumerable<IRequestBundle<TBaseRequest>> CreateDraft(BatchCreateDraftRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual IEnumerable<IRequestBundle<TBaseRequest>> SendDraft(BatchSendDraftRequestRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual IEnumerable<IRequestBundle<TBaseRequest>> RenameFolder(RenameFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual IEnumerable<IRequestBundle<TBaseRequest>> EmptyFolder(EmptyFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual IEnumerable<IRequestBundle<TBaseRequest>> MarkFolderAsRead(MarkFolderAsReadRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual IEnumerable<IRequestBundle<TBaseRequest>> Archive(BatchArchiveRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Apis.Gmail.v1;
|
||||
@@ -245,111 +244,97 @@ namespace Wino.Core.Synchronizers
|
||||
|
||||
private async Task SynchronizeFoldersAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var folderRequest = _gmailService.Users.Labels.List("me");
|
||||
|
||||
var labelsResponse = await folderRequest.ExecuteAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (labelsResponse.Labels == null)
|
||||
try
|
||||
{
|
||||
_logger.Warning("No folders found for {Name}", Account.Name);
|
||||
return;
|
||||
}
|
||||
var localFolders = await _gmailChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false);
|
||||
var folderRequest = _gmailService.Users.Labels.List("me");
|
||||
|
||||
_logger.Debug($"Gmail folders found: {string.Join(",", labelsResponse.Labels.Select(a => a.Name))}");
|
||||
var labelsResponse = await folderRequest.ExecuteAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Gmail has special Categories parent folder.
|
||||
var categoriesFolder = new MailItemFolder()
|
||||
{
|
||||
SpecialFolderType = SpecialFolderType.Category,
|
||||
MailAccountId = Account.Id,
|
||||
FolderName = "Categories",
|
||||
IsSynchronizationEnabled = false,
|
||||
IsSticky = true,
|
||||
Id = Guid.NewGuid(),
|
||||
RemoteFolderId = "Categories"
|
||||
};
|
||||
|
||||
var initializedFolders = new List<MailItemFolder>() { categoriesFolder };
|
||||
|
||||
foreach (var label in labelsResponse.Labels)
|
||||
{
|
||||
var localFolder = label.GetLocalFolder(Account.Id);
|
||||
|
||||
localFolder.MailAccountId = Account.Id;
|
||||
|
||||
initializedFolders.Add(localFolder);
|
||||
}
|
||||
|
||||
// 1. Create parent-child relations but first order by name desc to be able to not mess up FolderNames in memory.
|
||||
// 2. Mark special folder types that belong to categories folder.
|
||||
|
||||
var ordered = initializedFolders.OrderByDescending(a => a.FolderName);
|
||||
|
||||
// TODO: This can be refactored better.
|
||||
foreach (var label in ordered)
|
||||
{
|
||||
// Gmail categorizes sub-labels by '/' For example:
|
||||
// ParentTest/SubTest in the name of label means
|
||||
// SubTest is a sub-label of ParentTest label.
|
||||
|
||||
if (label.SpecialFolderType == SpecialFolderType.Promotions ||
|
||||
label.SpecialFolderType == SpecialFolderType.Updates ||
|
||||
label.SpecialFolderType == SpecialFolderType.Social ||
|
||||
label.SpecialFolderType == SpecialFolderType.Forums ||
|
||||
label.SpecialFolderType == SpecialFolderType.Personal)
|
||||
if (labelsResponse.Labels == null)
|
||||
{
|
||||
label.ParentRemoteFolderId = categoriesFolder.RemoteFolderId;
|
||||
|
||||
// These folders can not be a sub folder.
|
||||
continue;
|
||||
_logger.Warning("No folders found for {Name}", Account.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
var isSubFolder = label.FolderName.Contains("/");
|
||||
List<MailItemFolder> insertedFolders = new();
|
||||
List<MailItemFolder> updatedFolders = new();
|
||||
List<MailItemFolder> deletedFolders = new();
|
||||
|
||||
if (isSubFolder)
|
||||
// 1. Handle deleted labels.
|
||||
|
||||
foreach (var localFolder in localFolders)
|
||||
{
|
||||
var splittedFolderName = label.FolderName.Split('/');
|
||||
var partCount = splittedFolderName.Length;
|
||||
// Category folder is virtual folder for Wino. Skip it.
|
||||
if (localFolder.SpecialFolderType == SpecialFolderType.Category) continue;
|
||||
|
||||
if (partCount > 1)
|
||||
var remoteFolder = labelsResponse.Labels.FirstOrDefault(a => a.Id == localFolder.RemoteFolderId);
|
||||
|
||||
if (remoteFolder == null)
|
||||
{
|
||||
// Only make the last part connection since other relations will build up in the loop.
|
||||
// Local folder doesn't exists remotely. Delete local copy.
|
||||
await _gmailChangeProcessor.DeleteFolderAsync(Account.Id, localFolder.RemoteFolderId).ConfigureAwait(false);
|
||||
|
||||
var realChildFolderName = splittedFolderName[splittedFolderName.Length - 1];
|
||||
deletedFolders.Add(localFolder);
|
||||
}
|
||||
}
|
||||
|
||||
var childFolder = initializedFolders.Find(a => a.FolderName.EndsWith(realChildFolderName));
|
||||
// Delete the deleted folders from local list.
|
||||
deletedFolders.ForEach(a => localFolders.Remove(a));
|
||||
|
||||
string GetParentFolderName(string[] parts)
|
||||
// 2. Handle update/insert based on remote folders.
|
||||
foreach (var remoteFolder in labelsResponse.Labels)
|
||||
{
|
||||
var existingLocalFolder = localFolders.FirstOrDefault(a => a.RemoteFolderId == remoteFolder.Id);
|
||||
|
||||
if (existingLocalFolder == null)
|
||||
{
|
||||
// Insert new folder.
|
||||
var localFolder = remoteFolder.GetLocalFolder(labelsResponse, Account.Id);
|
||||
|
||||
insertedFolders.Add(localFolder);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update existing folder. Right now we only update the name.
|
||||
|
||||
// TODO: Moving folders around different parents. This is not supported right now.
|
||||
// We will need more comphrensive folder update mechanism to support this.
|
||||
|
||||
if (ShouldUpdateFolder(remoteFolder, existingLocalFolder))
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < parts.Length - 1; i++)
|
||||
{
|
||||
builder.Append(parts[i]);
|
||||
|
||||
if (i != parts.Length - 2)
|
||||
builder.Append("/");
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
existingLocalFolder.FolderName = remoteFolder.Name;
|
||||
updatedFolders.Add(existingLocalFolder);
|
||||
}
|
||||
|
||||
var parentFolderName = GetParentFolderName(splittedFolderName);
|
||||
|
||||
var parentFolder = initializedFolders.Find(a => a.FolderName == parentFolderName);
|
||||
|
||||
if (childFolder != null && parentFolder != null)
|
||||
else
|
||||
{
|
||||
childFolder.FolderName = realChildFolderName;
|
||||
childFolder.ParentRemoteFolderId = parentFolder.RemoteFolderId;
|
||||
// Remove it from the local folder list to skip additional folder updates.
|
||||
localFolders.Remove(existingLocalFolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _gmailChangeProcessor.UpdateFolderStructureAsync(Account.Id, initializedFolders).ConfigureAwait(false);
|
||||
// 3.Process changes in order-> Insert, Update. Deleted ones are already processed.
|
||||
|
||||
foreach (var folder in insertedFolders)
|
||||
{
|
||||
await _gmailChangeProcessor.InsertFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var folder in updatedFolders)
|
||||
{
|
||||
await _gmailChangeProcessor.UpdateFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldUpdateFolder(Label remoteFolder, MailItemFolder existingLocalFolder)
|
||||
=> existingLocalFolder.FolderName.Equals(GoogleIntegratorExtensions.GetFolderName(remoteFolder), StringComparison.OrdinalIgnoreCase) == false;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a single get request to retrieve the raw message with the given id
|
||||
/// </summary>
|
||||
@@ -706,6 +691,34 @@ namespace Wino.Core.Synchronizers
|
||||
await _gmailChangeProcessor.SaveMimeFileAsync(mailItem.FileId, mimeMessage, Account.Id).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override IEnumerable<IRequestBundle<IClientServiceRequest>> RenameFolder(RenameFolderRequest request)
|
||||
{
|
||||
return CreateHttpBundleWithResponse<Label>(request, (item) =>
|
||||
{
|
||||
if (item is not RenameFolderRequest renameFolderRequest)
|
||||
throw new ArgumentException($"Renaming folder must be handled with '{nameof(RenameFolderRequest)}'");
|
||||
|
||||
var label = new Label()
|
||||
{
|
||||
Name = renameFolderRequest.NewFolderName
|
||||
};
|
||||
|
||||
return _gmailService.Users.Labels.Update(label, "me", request.Folder.RemoteFolderId);
|
||||
});
|
||||
}
|
||||
|
||||
public override IEnumerable<IRequestBundle<IClientServiceRequest>> EmptyFolder(EmptyFolderRequest request)
|
||||
{
|
||||
// Create batch delete request.
|
||||
|
||||
var deleteRequests = request.MailsToDelete.Select(a => new DeleteRequest(a));
|
||||
|
||||
return Delete(new BatchDeleteRequest(deleteRequests));
|
||||
}
|
||||
|
||||
public override IEnumerable<IRequestBundle<IClientServiceRequest>> MarkFolderAsRead(MarkFolderAsReadRequest request)
|
||||
=> MarkRead(new BatchMarkReadRequest(request.MailsToMarkRead.Select(a => new MarkReadRequest(a, true)), true));
|
||||
|
||||
#endregion
|
||||
|
||||
#region Request Execution
|
||||
|
||||
@@ -273,7 +273,13 @@ namespace Wino.Core.Synchronizers
|
||||
}
|
||||
|
||||
public override IEnumerable<IRequestBundle<ImapRequest>> Archive(BatchArchiveRequest request)
|
||||
=> Move(new BatchMoveRequest(request.Items, request.FromFolder, request.ToFolder));
|
||||
=> Move(new BatchMoveRequest(request.Items, request.FromFolder, request.ToFolder));
|
||||
|
||||
public override IEnumerable<IRequestBundle<ImapRequest>> EmptyFolder(EmptyFolderRequest request)
|
||||
=> Delete(new BatchDeleteRequest(request.MailsToDelete.Select(a => new DeleteRequest(a))));
|
||||
|
||||
public override IEnumerable<IRequestBundle<ImapRequest>> MarkFolderAsRead(MarkFolderAsReadRequest request)
|
||||
=> MarkRead(new BatchMarkReadRequest(request.MailsToMarkRead.Select(a => new MarkReadRequest(a, true)), true));
|
||||
|
||||
public override IEnumerable<IRequestBundle<ImapRequest>> SendDraft(BatchSendDraftRequestRequest request)
|
||||
{
|
||||
@@ -351,6 +357,15 @@ namespace Wino.Core.Synchronizers
|
||||
_clientPool.Release(client);
|
||||
}
|
||||
|
||||
public override IEnumerable<IRequestBundle<ImapRequest>> RenameFolder(RenameFolderRequest request)
|
||||
{
|
||||
return CreateTaskBundle(async (ImapClient client) =>
|
||||
{
|
||||
var folder = await client.GetFolderAsync(request.Folder.RemoteFolderId).ConfigureAwait(false);
|
||||
await folder.RenameAsync(folder.ParentFolder, request.NewFolderName).ConfigureAwait(false);
|
||||
}, request);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public override async Task<List<NewMailItemPackage>> CreateNewMailPackagesAsync(ImapMessageCreationPackage message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default)
|
||||
@@ -406,10 +421,7 @@ namespace Wino.Core.Synchronizers
|
||||
// Therefore this should be avoided as many times as possible.
|
||||
|
||||
// This may create some inconsistencies, but nothing we can do...
|
||||
if (options.Type == SynchronizationType.FoldersOnly || options.Type == SynchronizationType.Full)
|
||||
{
|
||||
await SynchronizeFoldersAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
await SynchronizeFoldersAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (options.Type != SynchronizationType.FoldersOnly)
|
||||
{
|
||||
@@ -492,7 +504,7 @@ namespace Wino.Core.Synchronizers
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
item.Request.RevertUIChanges();
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
@@ -546,26 +558,20 @@ namespace Wino.Core.Synchronizers
|
||||
localFolder.SpecialFolderType = SpecialFolderType.Starred;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the local folder should be updated with the remote folder.
|
||||
/// </summary>
|
||||
/// <param name="remoteFolder">Remote folder</param>
|
||||
/// <param name="localFolder">Local folder.</param>
|
||||
private bool ShouldUpdateFolder(IMailFolder remoteFolder, MailItemFolder localFolder)
|
||||
{
|
||||
return remoteFolder.Name != localFolder.FolderName;
|
||||
}
|
||||
|
||||
private async Task SynchronizeFoldersAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// https://www.rfc-editor.org/rfc/rfc4549#section-1.1
|
||||
|
||||
var localFolders = await _imapChangeProcessor.GetLocalIMAPFoldersAsync(Account.Id).ConfigureAwait(false);
|
||||
var localFolders = await _imapChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false);
|
||||
|
||||
ImapClient executorClient = null;
|
||||
|
||||
try
|
||||
{
|
||||
List<MailItemFolder> insertedFolders = new();
|
||||
List<MailItemFolder> updatedFolders = new();
|
||||
List<MailItemFolder> deletedFolders = new();
|
||||
|
||||
executorClient = await _clientPool.GetClientAsync().ConfigureAwait(false);
|
||||
|
||||
var remoteFolders = (await executorClient.GetFoldersAsync(executorClient.PersonalNamespaces[0], cancellationToken: cancellationToken)).ToList();
|
||||
@@ -575,8 +581,6 @@ namespace Wino.Core.Synchronizers
|
||||
// 1.a If local folder doesn't exists remotely, delete it.
|
||||
// 1.b If local folder exists remotely, check if it is still a valid folder. If UidValidity is changed, delete it.
|
||||
|
||||
List<MailItemFolder> deletedFolders = new();
|
||||
|
||||
foreach (var localFolder in localFolders)
|
||||
{
|
||||
IMailFolder remoteFolder = null;
|
||||
@@ -628,8 +632,6 @@ namespace Wino.Core.Synchronizers
|
||||
|
||||
var nameSpace = executorClient.PersonalNamespaces[0];
|
||||
|
||||
// var remoteFolders = (await executorClient.GetFoldersAsync(nameSpace, cancellationToken: cancellationToken)).ToList();
|
||||
|
||||
IMailFolder inbox = executorClient.Inbox;
|
||||
|
||||
// Sometimes Inbox is the root namespace. We need to check for that.
|
||||
@@ -684,18 +686,19 @@ namespace Wino.Core.Synchronizers
|
||||
|
||||
await remoteFolder.CloseAsync(cancellationToken: cancellationToken);
|
||||
|
||||
localFolders.Add(localFolder);
|
||||
insertedFolders.Add(localFolder);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update existing folder. Right now we only update the name.
|
||||
|
||||
// TODO: Moving servers around different parents. This is not supported right now.
|
||||
// TODO: Moving folders around different parents. This is not supported right now.
|
||||
// We will need more comphrensive folder update mechanism to support this.
|
||||
|
||||
if (ShouldUpdateFolder(remoteFolder, existingLocalFolder))
|
||||
{
|
||||
existingLocalFolder.FolderName = remoteFolder.Name;
|
||||
updatedFolders.Add(existingLocalFolder);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -705,13 +708,16 @@ namespace Wino.Core.Synchronizers
|
||||
}
|
||||
}
|
||||
|
||||
if (localFolders.Any())
|
||||
// Process changes in order-> Insert, Update. Deleted ones are already processed.
|
||||
|
||||
foreach (var folder in insertedFolders)
|
||||
{
|
||||
await _imapChangeProcessor.UpdateFolderStructureAsync(Account.Id, localFolders);
|
||||
await _imapChangeProcessor.InsertFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
|
||||
foreach (var folder in updatedFolders)
|
||||
{
|
||||
_logger.Information("No update is needed for imap folders.");
|
||||
await _imapChangeProcessor.UpdateFolderAsync(folder).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -729,6 +735,8 @@ namespace Wino.Core.Synchronizers
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async Task<IEnumerable<string>> SynchronizeFolderInternalAsync(MailItemFolder folder, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!folder.IsSynchronizationEnabled) return default;
|
||||
@@ -978,5 +986,14 @@ namespace Wino.Core.Synchronizers
|
||||
_clientPool.Release(_synchronizationClient);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether the local folder should be updated with the remote folder.
|
||||
/// IMAP only compares folder name for now.
|
||||
/// </summary>
|
||||
/// <param name="remoteFolder">Remote folder</param>
|
||||
/// <param name="localFolder">Local folder.</param>
|
||||
public bool ShouldUpdateFolder(IMailFolder remoteFolder, MailItemFolder localFolder) => remoteFolder.Name != localFolder.FolderName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ namespace Wino.Core.Synchronizers
|
||||
"InternetMessageId",
|
||||
];
|
||||
|
||||
private readonly SemaphoreSlim _handleItemRetrievalSemaphore = new(1);
|
||||
|
||||
private readonly ILogger _logger = Log.ForContext<OutlookSynchronizer>();
|
||||
private readonly IOutlookChangeProcessor _outlookChangeProcessor;
|
||||
private readonly GraphServiceClient _graphClient;
|
||||
@@ -184,7 +186,21 @@ namespace Wino.Core.Synchronizers
|
||||
|
||||
var messageIteratorAsync = PageIterator<Message, Microsoft.Graph.Me.MailFolders.Item.Messages.Delta.DeltaGetResponse>.CreatePageIterator(_graphClient, messageCollectionPage, async (item) =>
|
||||
{
|
||||
return await HandleItemRetrievedAsync(item, folder, downloadedMessageIds, cancellationToken);
|
||||
try
|
||||
{
|
||||
await _handleItemRetrievalSemaphore.WaitAsync();
|
||||
return await HandleItemRetrievedAsync(item, folder, downloadedMessageIds, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Error occurred while handling item {Id} for folder {FolderName}", item.Id, folder.FolderName);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_handleItemRetrievalSemaphore.Release();
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
await messageIteratorAsync
|
||||
@@ -368,7 +384,7 @@ namespace Wino.Core.Synchronizers
|
||||
|
||||
graphFolders = await _graphClient.RequestAdapter.SendAsync(deltaRequest,
|
||||
Microsoft.Graph.Me.MailFolders.Delta.DeltaGetResponse.CreateFromDiscriminatorValue,
|
||||
cancellationToken: cancellationToken);
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -380,7 +396,7 @@ namespace Wino.Core.Synchronizers
|
||||
deltaRequest.QueryParameters.Add("%24deltaToken", currentDeltaLink);
|
||||
graphFolders = await _graphClient.RequestAdapter.SendAsync(deltaRequest,
|
||||
Microsoft.Graph.Me.MailFolders.Delta.DeltaGetResponse.CreateFromDiscriminatorValue,
|
||||
cancellationToken: cancellationToken);
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var iterator = PageIterator<MailFolder, Microsoft.Graph.Me.MailFolders.Delta.DeltaGetResponse>.CreatePageIterator(_graphClient, graphFolders, (folder) =>
|
||||
@@ -575,6 +591,8 @@ namespace Wino.Core.Synchronizers
|
||||
public override IEnumerable<IRequestBundle<RequestInformation>> Archive(BatchArchiveRequest request)
|
||||
=> Move(new BatchMoveRequest(request.Items, request.FromFolder, request.ToFolder));
|
||||
|
||||
|
||||
|
||||
public override async Task DownloadMissingMimeMessageAsync(IMailItem mailItem,
|
||||
MailKit.ITransferProgress transferProgress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -583,6 +601,28 @@ namespace Wino.Core.Synchronizers
|
||||
await _outlookChangeProcessor.SaveMimeFileAsync(mailItem.FileId, mimeMessage, Account.Id).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override IEnumerable<IRequestBundle<RequestInformation>> RenameFolder(RenameFolderRequest request)
|
||||
{
|
||||
return CreateHttpBundleWithResponse<MailFolder>(request, (item) =>
|
||||
{
|
||||
if (item is not RenameFolderRequest renameFolderRequest)
|
||||
throw new ArgumentException($"Renaming folder must be handled with '{nameof(RenameFolderRequest)}'");
|
||||
|
||||
var requestBody = new MailFolder
|
||||
{
|
||||
DisplayName = request.NewFolderName,
|
||||
};
|
||||
|
||||
return _graphClient.Me.MailFolders[request.Folder.RemoteFolderId].ToPatchRequestInformation(requestBody);
|
||||
});
|
||||
}
|
||||
|
||||
public override IEnumerable<IRequestBundle<RequestInformation>> EmptyFolder(EmptyFolderRequest request)
|
||||
=> Delete(new BatchDeleteRequest(request.MailsToDelete.Select(a => new DeleteRequest(a))));
|
||||
|
||||
public override IEnumerable<IRequestBundle<RequestInformation>> MarkFolderAsRead(MarkFolderAsReadRequest request)
|
||||
=> MarkRead(new BatchMarkReadRequest(request.MailsToMarkRead.Select(a => new MarkReadRequest(a, true)), true));
|
||||
|
||||
#endregion
|
||||
|
||||
public override async Task ExecuteNativeRequestsAsync(IEnumerable<IRequestBundle<RequestInformation>> batchedRequests, CancellationToken cancellationToken = default)
|
||||
@@ -646,7 +686,11 @@ namespace Wino.Core.Synchronizers
|
||||
HttpResponseMessage httpResponseMessage,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (bundle is HttpRequestBundle<RequestInformation, Message> messageBundle)
|
||||
if (!httpResponseMessage.IsSuccessStatusCode)
|
||||
{
|
||||
throw new SynchronizerException(string.Format(Translator.Exception_SynchronizerFailureHTTP, httpResponseMessage.StatusCode));
|
||||
}
|
||||
else if (bundle is HttpRequestBundle<RequestInformation, Message> messageBundle)
|
||||
{
|
||||
var outlookMessage = await messageBundle.DeserializeBundleAsync(httpResponseMessage, cancellationToken);
|
||||
|
||||
@@ -666,11 +710,6 @@ namespace Wino.Core.Synchronizers
|
||||
{
|
||||
// TODO: Handle mime retrieve message.
|
||||
}
|
||||
else if (!httpResponseMessage.IsSuccessStatusCode)
|
||||
{
|
||||
// TODO: Should we even handle this?
|
||||
throw new SynchronizerException(string.Format(Translator.Exception_SynchronizerFailureHTTP, httpResponseMessage.StatusCode));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<MimeMessage> DownloadMimeMessageAsync(string messageId, CancellationToken cancellationToken = default)
|
||||
|
||||
Reference in New Issue
Block a user