Gmail - Archive/Unarchive (#582)
* Disable timer back sync for debug builds. * Archive / unarchive feature for Gmail. * Archive folder name override for Gmail. * Possible crash fix when the next item is being selected after a mail is removed. * Restore proper account selection after pin/unpin of folder. * Making sure that incorrect arcive folder id is not saved in Gmailsynchronizer due to migration.
This commit is contained in:
7
Wino.Core.Domain/Enums/InvalidMoveTargetReason.cs
Normal file
7
Wino.Core.Domain/Enums/InvalidMoveTargetReason.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
|
public enum InvalidMoveTargetReason
|
||||||
|
{
|
||||||
|
NonMoveTarget, // This folder does not allow moving mails.
|
||||||
|
MultipleAccounts // Multiple mails from different accounts cannot be moved.
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Exceptions;
|
namespace Wino.Core.Domain.Exceptions;
|
||||||
|
|
||||||
public class InvalidMoveTargetException : Exception { }
|
public class InvalidMoveTargetException(InvalidMoveTargetReason reason) : Exception
|
||||||
|
{
|
||||||
|
public InvalidMoveTargetReason Reason { get; } = reason;
|
||||||
|
}
|
||||||
|
|||||||
@@ -141,4 +141,12 @@ public interface IMailService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="accountId">Account id.</param>
|
/// <param name="accountId">Account id.</param>
|
||||||
Task<bool> HasAccountAnyDraftAsync(Guid accountId);
|
Task<bool> HasAccountAnyDraftAsync(Guid accountId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compares the ids returned from online search result for Archive folder against the local database.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="archiveFolderId">Archive folder id.</param>
|
||||||
|
/// <param name="onlineArchiveMailIds">Retrieved MailCopy ids from search result.</param>
|
||||||
|
/// <returns>Result model that contains added and removed mail copy ids.</returns>
|
||||||
|
Task<GmailArchiveComparisonResult> GetGmailArchiveComparisonResultAsync(Guid archiveFolderId, List<string> onlineArchiveMailIds);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using Wino.Core.Domain;
|
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
@@ -32,6 +31,8 @@ public partial class FolderMenuItem : MenuItemBase<IMailItemFolder, FolderMenuIt
|
|||||||
return Translator.MoreFolderNameOverride;
|
return Translator.MoreFolderNameOverride;
|
||||||
else if (Parameter.SpecialFolderType == SpecialFolderType.Category)
|
else if (Parameter.SpecialFolderType == SpecialFolderType.Category)
|
||||||
return Translator.CategoriesFolderNameOverride;
|
return Translator.CategoriesFolderNameOverride;
|
||||||
|
else if (Parameter.SpecialFolderType == SpecialFolderType.Archive && ParentAccount.ProviderType == MailProviderType.Gmail)
|
||||||
|
return Translator.GmailArchiveFolderNameOverride;
|
||||||
else
|
else
|
||||||
return Parameter.FolderName;
|
return Parameter.FolderName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Wino.Core.Domain.Models.MailItem;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Comparison result of the Gmail archive.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Added">Mail copy ids to be added to Archive.</param>
|
||||||
|
/// <param name="Removed">Mail copy ids to be removed from Archive.</param>
|
||||||
|
public record GmailArchiveComparisonResult(string[] Added, string[] Removed);
|
||||||
@@ -184,6 +184,7 @@
|
|||||||
"Exception_ImapClientPoolFailed": "IMAP Client Pool failed.",
|
"Exception_ImapClientPoolFailed": "IMAP Client Pool failed.",
|
||||||
"Exception_InboxNotAvailable": "Couldn't setup account folders.",
|
"Exception_InboxNotAvailable": "Couldn't setup account folders.",
|
||||||
"Exception_InvalidSystemFolderConfiguration": "System folder configuration is not valid. Check configuration and try again.",
|
"Exception_InvalidSystemFolderConfiguration": "System folder configuration is not valid. Check configuration and try again.",
|
||||||
|
"Exception_InvalidMultiAccountMoveTarget": "You can't move multiple items that belong to different accounts in linked account.",
|
||||||
"Exception_MailProcessing": "This mail is still being processed. Please try again after few seconds.",
|
"Exception_MailProcessing": "This mail is still being processed. Please try again after few seconds.",
|
||||||
"Exception_MissingAlias": "Primary alias does not exist for this account. Creating draft failed.",
|
"Exception_MissingAlias": "Primary alias does not exist for this account. Creating draft failed.",
|
||||||
"Exception_NullAssignedAccount": "Assigned account is null",
|
"Exception_NullAssignedAccount": "Assigned account is null",
|
||||||
@@ -218,6 +219,7 @@
|
|||||||
"GeneralTitle_Warning": "Warning",
|
"GeneralTitle_Warning": "Warning",
|
||||||
"GmailServiceDisabled_Title": "Gmail Error",
|
"GmailServiceDisabled_Title": "Gmail Error",
|
||||||
"GmailServiceDisabled_Message": "Your Google Workspace account seems to be disabled for Gmail service. Please contact your administrator to enable Gmail service for your account.",
|
"GmailServiceDisabled_Message": "Your Google Workspace account seems to be disabled for Gmail service. Please contact your administrator to enable Gmail service for your account.",
|
||||||
|
"GmailArchiveFolderNameOverride": "Archive",
|
||||||
"HoverActionOption_Archive": "Archive",
|
"HoverActionOption_Archive": "Archive",
|
||||||
"HoverActionOption_Delete": "Delete",
|
"HoverActionOption_Delete": "Delete",
|
||||||
"HoverActionOption_MoveJunk": "Move to Junk",
|
"HoverActionOption_MoveJunk": "Move to Junk",
|
||||||
|
|||||||
@@ -63,6 +63,16 @@ public interface IDefaultChangeProcessor
|
|||||||
Task<MailCopy> GetMailCopyAsync(string mailCopyId);
|
Task<MailCopy> GetMailCopyAsync(string mailCopyId);
|
||||||
Task CreateMailRawAsync(MailAccount account, MailItemFolder mailItemFolder, NewMailItemPackage package);
|
Task CreateMailRawAsync(MailAccount account, MailItemFolder mailItemFolder, NewMailItemPackage package);
|
||||||
Task DeleteUserMailCacheAsync(Guid accountId);
|
Task DeleteUserMailCacheAsync(Guid accountId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether the mail exists in the folder.
|
||||||
|
/// When deciding Create or Update existing mail, we need to check if the mail exists in the folder.
|
||||||
|
/// Also duplicate assignments for Gmail's virtual Archive folder is ignored.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="messageId">Message id</param>
|
||||||
|
/// <param name="folderId">Folder's local id.</param>
|
||||||
|
/// <returns>Whether mail exists in the folder or not.</returns>
|
||||||
|
Task<bool> IsMailExistsInFolderAsync(string messageId, Guid folderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IGmailChangeProcessor : IDefaultChangeProcessor
|
public interface IGmailChangeProcessor : IDefaultChangeProcessor
|
||||||
@@ -71,19 +81,11 @@ public interface IGmailChangeProcessor : IDefaultChangeProcessor
|
|||||||
Task MapLocalDraftAsync(string mailCopyId, string newDraftId, string newThreadId);
|
Task MapLocalDraftAsync(string mailCopyId, string newDraftId, string newThreadId);
|
||||||
Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
|
Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
|
||||||
Task ManageCalendarEventAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount);
|
Task ManageCalendarEventAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount);
|
||||||
|
Task<GmailArchiveComparisonResult> GetGmailArchiveComparisonResultAsync(Guid archiveFolderId, List<string> onlineArchiveMailIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IOutlookChangeProcessor : IDefaultChangeProcessor
|
public interface IOutlookChangeProcessor : IDefaultChangeProcessor
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Checks whether the mail exists in the folder.
|
|
||||||
/// When deciding Create or Update existing mail, we need to check if the mail exists in the folder.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="messageId">Message id</param>
|
|
||||||
/// <param name="folderId">Folder's local id.</param>
|
|
||||||
/// <returns>Whether mail exists in the folder or not.</returns>
|
|
||||||
Task<bool> IsMailExistsInFolderAsync(string messageId, Guid folderId);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates Folder's delta synchronization identifier.
|
/// Updates Folder's delta synchronization identifier.
|
||||||
/// Only used in Outlook since it does per-folder sync.
|
/// Only used in Outlook since it does per-folder sync.
|
||||||
@@ -211,4 +213,7 @@ public class DefaultChangeProcessor(IDatabaseService databaseService,
|
|||||||
await _mimeFileService.DeleteUserMimeCacheAsync(accountId).ConfigureAwait(false);
|
await _mimeFileService.DeleteUserMimeCacheAsync(accountId).ConfigureAwait(false);
|
||||||
await AccountService.DeleteAccountMailCacheAsync(accountId, AccountCacheResetReason.ExpiredCache).ConfigureAwait(false);
|
await AccountService.DeleteAccountMailCacheAsync(accountId, AccountCacheResetReason.ExpiredCache).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<bool> IsMailExistsInFolderAsync(string messageId, Guid folderId)
|
||||||
|
=> MailService.IsMailExistsAsync(messageId, folderId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using Wino.Core.Domain.Entities.Calendar;
|
|||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.MailItem;
|
||||||
using Wino.Core.Extensions;
|
using Wino.Core.Extensions;
|
||||||
using Wino.Services;
|
using Wino.Services;
|
||||||
using CalendarEventAttendee = Wino.Core.Domain.Entities.Calendar.CalendarEventAttendee;
|
using CalendarEventAttendee = Wino.Core.Domain.Entities.Calendar.CalendarEventAttendee;
|
||||||
@@ -310,4 +311,7 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso
|
|||||||
|
|
||||||
public Task<bool> HasAccountAnyDraftAsync(Guid accountId)
|
public Task<bool> HasAccountAnyDraftAsync(Guid accountId)
|
||||||
=> MailService.HasAccountAnyDraftAsync(accountId);
|
=> MailService.HasAccountAnyDraftAsync(accountId);
|
||||||
|
|
||||||
|
public Task<GmailArchiveComparisonResult> GetGmailArchiveComparisonResultAsync(Guid archiveFolderId, List<string> onlineArchiveMailIds)
|
||||||
|
=> MailService.GetGmailArchiveComparisonResultAsync(archiveFolderId, onlineArchiveMailIds);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
|
|||||||
, IOutlookChangeProcessor
|
, IOutlookChangeProcessor
|
||||||
{
|
{
|
||||||
|
|
||||||
public Task<bool> IsMailExistsInFolderAsync(string messageId, Guid folderId)
|
|
||||||
=> MailService.IsMailExistsAsync(messageId, folderId);
|
|
||||||
|
|
||||||
public Task<string> ResetAccountDeltaTokenAsync(Guid accountId)
|
public Task<string> ResetAccountDeltaTokenAsync(Guid accountId)
|
||||||
=> AccountService.UpdateSynchronizationIdentifierAsync(accountId, null);
|
=> AccountService.UpdateSynchronizationIdentifierAsync(accountId, null);
|
||||||
|
|
||||||
|
|||||||
@@ -53,9 +53,19 @@ public class WinoRequestDelegator : IWinoRequestDelegator
|
|||||||
_dialogService.HandleSystemFolderConfigurationDialogAsync(unavailableSpecialFolderException.AccountId, _folderService);
|
_dialogService.HandleSystemFolderConfigurationDialogAsync(unavailableSpecialFolderException.AccountId, _folderService);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (InvalidMoveTargetException)
|
catch (InvalidMoveTargetException invalidMoveTargetException)
|
||||||
{
|
{
|
||||||
_dialogService.InfoBarMessage(Translator.Info_InvalidMoveTargetTitle, Translator.Info_InvalidMoveTargetMessage, InfoBarMessageType.Warning);
|
switch (invalidMoveTargetException.Reason)
|
||||||
|
{
|
||||||
|
case InvalidMoveTargetReason.NonMoveTarget:
|
||||||
|
_dialogService.InfoBarMessage(Translator.Info_InvalidMoveTargetTitle, Translator.Info_InvalidMoveTargetMessage, InfoBarMessageType.Warning);
|
||||||
|
break;
|
||||||
|
case InvalidMoveTargetReason.MultipleAccounts:
|
||||||
|
_dialogService.InfoBarMessage(Translator.Info_InvalidMoveTargetTitle, Translator.Exception_InvalidMultiAccountMoveTarget, InfoBarMessageType.Warning);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (NotImplementedException)
|
catch (NotImplementedException)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -75,8 +75,13 @@ public class WinoRequestProcessor : IWinoRequestProcessor
|
|||||||
|
|
||||||
if (action == MailOperation.Move && moveTargetStructure == null)
|
if (action == MailOperation.Move && moveTargetStructure == null)
|
||||||
{
|
{
|
||||||
// TODO: Handle multiple accounts for move operation.
|
// Handle the case when user is trying to move multiple mails that belong to different accounts.
|
||||||
// What happens if we move 2 different mails from 2 different accounts?
|
// We can't handle this with only 1 picker dialog.
|
||||||
|
|
||||||
|
bool isInvalidMoveTarget = preperationRequest.MailItems.Select(a => a.AssignedAccount.Id).Distinct().Count() > 1;
|
||||||
|
|
||||||
|
if (isInvalidMoveTarget)
|
||||||
|
throw new InvalidMoveTargetException(InvalidMoveTargetReason.MultipleAccounts);
|
||||||
|
|
||||||
var accountId = preperationRequest.MailItems.FirstOrDefault().AssignedAccount.Id;
|
var accountId = preperationRequest.MailItems.FirstOrDefault().AssignedAccount.Id;
|
||||||
|
|
||||||
@@ -142,7 +147,7 @@ public class WinoRequestProcessor : IWinoRequestProcessor
|
|||||||
else if (action == MailOperation.Move)
|
else if (action == MailOperation.Move)
|
||||||
{
|
{
|
||||||
if (moveTargetStructure == null)
|
if (moveTargetStructure == null)
|
||||||
throw new InvalidMoveTargetException();
|
throw new InvalidMoveTargetException(InvalidMoveTargetReason.NonMoveTarget);
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
// Rule: You can't move items to non-move target folders;
|
// Rule: You can't move items to non-move target folders;
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
private readonly IGmailChangeProcessor _gmailChangeProcessor;
|
private readonly IGmailChangeProcessor _gmailChangeProcessor;
|
||||||
private readonly ILogger _logger = Log.ForContext<GmailSynchronizer>();
|
private readonly ILogger _logger = Log.ForContext<GmailSynchronizer>();
|
||||||
|
|
||||||
|
// Keeping a reference for quick access to the virtual archive folder.
|
||||||
|
private Guid? archiveFolderId;
|
||||||
|
|
||||||
public GmailSynchronizer(MailAccount account,
|
public GmailSynchronizer(MailAccount account,
|
||||||
IGmailAuthenticator authenticator,
|
IGmailAuthenticator authenticator,
|
||||||
IGmailChangeProcessor gmailChangeProcessor) : base(account)
|
IGmailChangeProcessor gmailChangeProcessor) : base(account)
|
||||||
@@ -120,6 +123,10 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
{
|
{
|
||||||
_logger.Information("Internal mail synchronization started for {Name}", Account.Name);
|
_logger.Information("Internal mail synchronization started for {Name}", Account.Name);
|
||||||
|
|
||||||
|
// Make sure that virtual archive folder exists before all.
|
||||||
|
if (!archiveFolderId.HasValue)
|
||||||
|
await InitializeArchiveFolderAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
// Gmail must always synchronize folders before because it doesn't have a per-folder sync.
|
// Gmail must always synchronize folders before because it doesn't have a per-folder sync.
|
||||||
bool shouldSynchronizeFolders = true;
|
bool shouldSynchronizeFolders = true;
|
||||||
|
|
||||||
@@ -266,38 +273,15 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
missingMessageIds.AddRange(addedMessageIds);
|
missingMessageIds.AddRange(addedMessageIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consolidate added/deleted elements.
|
|
||||||
// For example: History change might report downloading a mail first, then deleting it in another history change.
|
|
||||||
// In that case, downloading mail will return entity not found error.
|
|
||||||
// Plus, it's a redundant download the mail.
|
|
||||||
// Purge missing message ids from potentially deleted mails to prevent this.
|
|
||||||
|
|
||||||
var messageDeletedHistoryChanges = deltaChanges
|
|
||||||
.Where(a => a.History != null)
|
|
||||||
.SelectMany(a => a.History)
|
|
||||||
.Where(a => a.MessagesDeleted != null)
|
|
||||||
.SelectMany(a => a.MessagesDeleted);
|
|
||||||
|
|
||||||
var deletedMailIdsInHistory = messageDeletedHistoryChanges.Select(a => a.Message.Id);
|
|
||||||
|
|
||||||
if (deletedMailIdsInHistory.Any())
|
|
||||||
{
|
|
||||||
var mailIdsToConsolidate = missingMessageIds.Where(a => deletedMailIdsInHistory.Contains(a)).ToList();
|
|
||||||
|
|
||||||
int consolidatedMessageCount = missingMessageIds.RemoveAll(a => deletedMailIdsInHistory.Contains(a));
|
|
||||||
|
|
||||||
if (consolidatedMessageCount > 0)
|
|
||||||
{
|
|
||||||
// TODO: Also delete the history changes that are related to these mails.
|
|
||||||
// This will prevent unwanted logs and additional queries to look for them in processing.
|
|
||||||
|
|
||||||
_logger.Information($"Purged {consolidatedMessageCount} missing mail downloads. ({string.Join(",", mailIdsToConsolidate)})");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start downloading missing messages.
|
// Start downloading missing messages.
|
||||||
await BatchDownloadMessagesAsync(missingMessageIds, cancellationToken).ConfigureAwait(false);
|
await BatchDownloadMessagesAsync(missingMessageIds, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Map archive assignments if there are any changes reported.
|
||||||
|
if (listChanges.Any() || deltaChanges.Any())
|
||||||
|
{
|
||||||
|
await MapArchivedMailsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
// Map remote drafts to local drafts.
|
// Map remote drafts to local drafts.
|
||||||
await MapDraftIdsAsync(cancellationToken).ConfigureAwait(false);
|
await MapDraftIdsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -484,6 +468,51 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task InitializeArchiveFolderAsync()
|
||||||
|
{
|
||||||
|
var localFolders = await _gmailChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Handling of Gmail special virtual Archive folder.
|
||||||
|
// We will generate a new virtual folder if doesn't exist.
|
||||||
|
|
||||||
|
if (!localFolders.Any(a => a.SpecialFolderType == SpecialFolderType.Archive))
|
||||||
|
{
|
||||||
|
archiveFolderId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var archiveFolder = new MailItemFolder()
|
||||||
|
{
|
||||||
|
FolderName = "Archive", // will be localized. N/A
|
||||||
|
RemoteFolderId = ServiceConstants.ARCHIVE_LABEL_ID,
|
||||||
|
Id = archiveFolderId.Value,
|
||||||
|
MailAccountId = Account.Id,
|
||||||
|
SpecialFolderType = SpecialFolderType.Archive,
|
||||||
|
IsSynchronizationEnabled = true,
|
||||||
|
IsSystemFolder = true,
|
||||||
|
IsSticky = true,
|
||||||
|
IsHidden = false,
|
||||||
|
ShowUnreadCount = true
|
||||||
|
};
|
||||||
|
|
||||||
|
await _gmailChangeProcessor.InsertFolderAsync(archiveFolder).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Migration-> User might've already have another special folder for Archive.
|
||||||
|
// We must remove that type assignment.
|
||||||
|
// This code can be removed after sometime.
|
||||||
|
|
||||||
|
var otherArchiveFolders = localFolders.Where(a => a.SpecialFolderType == SpecialFolderType.Archive && a.Id != archiveFolderId.Value).ToList();
|
||||||
|
|
||||||
|
foreach (var otherArchiveFolder in otherArchiveFolders)
|
||||||
|
{
|
||||||
|
otherArchiveFolder.SpecialFolderType = SpecialFolderType.Other;
|
||||||
|
await _gmailChangeProcessor.UpdateFolderAsync(otherArchiveFolder).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
archiveFolderId = localFolders.First(a => a.SpecialFolderType == SpecialFolderType.Archive && a.RemoteFolderId == ServiceConstants.ARCHIVE_LABEL_ID).Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task SynchronizeFoldersAsync(CancellationToken cancellationToken = default)
|
private async Task SynchronizeFoldersAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var localFolders = await _gmailChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false);
|
var localFolders = await _gmailChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false);
|
||||||
@@ -502,12 +531,14 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
List<MailItemFolder> deletedFolders = new();
|
List<MailItemFolder> deletedFolders = new();
|
||||||
|
|
||||||
// 1. Handle deleted labels.
|
// 1. Handle deleted labels.
|
||||||
|
|
||||||
foreach (var localFolder in localFolders)
|
foreach (var localFolder in localFolders)
|
||||||
{
|
{
|
||||||
// Category folder is virtual folder for Wino. Skip it.
|
// Category folder is virtual folder for Wino. Skip it.
|
||||||
if (localFolder.SpecialFolderType == SpecialFolderType.Category) continue;
|
if (localFolder.SpecialFolderType == SpecialFolderType.Category) continue;
|
||||||
|
|
||||||
|
// Gmail's Archive folder is virtual older for Wino. Skip it.
|
||||||
|
if (localFolder.SpecialFolderType == SpecialFolderType.Archive) continue;
|
||||||
|
|
||||||
var remoteFolder = labelsResponse.Labels.FirstOrDefault(a => a.Id == localFolder.RemoteFolderId);
|
var remoteFolder = labelsResponse.Labels.FirstOrDefault(a => a.Id == localFolder.RemoteFolderId);
|
||||||
|
|
||||||
if (remoteFolder == null)
|
if (remoteFolder == null)
|
||||||
@@ -558,7 +589,6 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3.Process changes in order-> Insert, Update. Deleted ones are already processed.
|
// 3.Process changes in order-> Insert, Update. Deleted ones are already processed.
|
||||||
|
|
||||||
foreach (var folder in insertedFolders)
|
foreach (var folder in insertedFolders)
|
||||||
{
|
{
|
||||||
await _gmailChangeProcessor.InsertFolderAsync(folder).ConfigureAwait(false);
|
await _gmailChangeProcessor.InsertFolderAsync(folder).ConfigureAwait(false);
|
||||||
@@ -731,6 +761,29 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task HandleArchiveAssignmentAsync(string archivedMessageId)
|
||||||
|
{
|
||||||
|
// Ignore if the message is already in the archive.
|
||||||
|
bool archived = await _gmailChangeProcessor.IsMailExistsInFolderAsync(archivedMessageId, archiveFolderId.Value);
|
||||||
|
|
||||||
|
if (archived) return;
|
||||||
|
|
||||||
|
_logger.Debug("Processing archive assignment for message {Id}", archivedMessageId);
|
||||||
|
|
||||||
|
await _gmailChangeProcessor.CreateAssignmentAsync(Account.Id, archivedMessageId, ServiceConstants.ARCHIVE_LABEL_ID).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleUnarchiveAssignmentAsync(string unarchivedMessageId)
|
||||||
|
{
|
||||||
|
// Ignore if the message is not in the archive.
|
||||||
|
bool archived = await _gmailChangeProcessor.IsMailExistsInFolderAsync(unarchivedMessageId, archiveFolderId.Value);
|
||||||
|
if (!archived) return;
|
||||||
|
|
||||||
|
_logger.Debug("Processing un-archive assignment for message {Id}", unarchivedMessageId);
|
||||||
|
|
||||||
|
await _gmailChangeProcessor.DeleteAssignmentAsync(Account.Id, unarchivedMessageId, ServiceConstants.ARCHIVE_LABEL_ID).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task HandleLabelAssignmentAsync(HistoryLabelAdded addedLabel)
|
private async Task HandleLabelAssignmentAsync(HistoryLabelAdded addedLabel)
|
||||||
{
|
{
|
||||||
var messageId = addedLabel.Message.Id;
|
var messageId = addedLabel.Message.Id;
|
||||||
@@ -824,9 +877,18 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
AddLabelIds = [toFolder.RemoteFolderId]
|
AddLabelIds = [toFolder.RemoteFolderId]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only add remove label ids if the source folder is not sent folder.
|
// Archived item is being moved to different folder.
|
||||||
if (fromFolder.SpecialFolderType != SpecialFolderType.Sent)
|
// Unarchive will move it to Inbox, so this is a different case.
|
||||||
|
// We can't remove ARCHIVE label because it's a virtual folder and does not exist in Gmail.
|
||||||
|
// We will just add the target label and Gmail will handle the rest.
|
||||||
|
|
||||||
|
if (fromFolder.SpecialFolderType == SpecialFolderType.Archive)
|
||||||
{
|
{
|
||||||
|
batchModifyRequest.AddLabelIds = [toFolder.RemoteFolderId];
|
||||||
|
}
|
||||||
|
else if (fromFolder.SpecialFolderType != SpecialFolderType.Sent)
|
||||||
|
{
|
||||||
|
// Only add remove label ids if the source folder is not sent folder.
|
||||||
batchModifyRequest.RemoveLabelIds = [fromFolder.RemoteFolderId];
|
batchModifyRequest.RemoveLabelIds = [fromFolder.RemoteFolderId];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1251,6 +1313,51 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gmail Archive is a special folder that is not visible in the Gmail web interface.
|
||||||
|
/// We need to handle it separately.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
private async Task MapArchivedMailsAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var request = _gmailService.Users.Messages.List("me");
|
||||||
|
request.Q = "in:archive";
|
||||||
|
request.MaxResults = InitialMessageDownloadCountPerFolder;
|
||||||
|
|
||||||
|
string pageToken = null;
|
||||||
|
|
||||||
|
var archivedMessageIds = new List<string>();
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(pageToken)) request.PageToken = pageToken;
|
||||||
|
|
||||||
|
var response = await request.ExecuteAsync(cancellationToken);
|
||||||
|
if (response.Messages == null) break;
|
||||||
|
|
||||||
|
foreach (var message in response.Messages)
|
||||||
|
{
|
||||||
|
if (archivedMessageIds.Contains(message.Id)) continue;
|
||||||
|
|
||||||
|
archivedMessageIds.Add(message.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pageToken = response.NextPageToken;
|
||||||
|
} while (!string.IsNullOrEmpty(pageToken));
|
||||||
|
|
||||||
|
var result = await _gmailChangeProcessor.GetGmailArchiveComparisonResultAsync(archiveFolderId.Value, archivedMessageIds).ConfigureAwait(false);
|
||||||
|
|
||||||
|
foreach (var archiveAddedItem in result.Added)
|
||||||
|
{
|
||||||
|
await HandleArchiveAssignmentAsync(archiveAddedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var unAarchivedRemovedItem in result.Removed)
|
||||||
|
{
|
||||||
|
await HandleUnarchiveAssignmentAsync(unAarchivedRemovedItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maps existing Gmail Draft resources to local mail copies.
|
/// Maps existing Gmail Draft resources to local mail copies.
|
||||||
/// This uses indexed search, therefore it's quite fast.
|
/// This uses indexed search, therefore it's quite fast.
|
||||||
|
|||||||
@@ -976,7 +976,12 @@ public partial class AppShellViewModel : MailBaseViewModel,
|
|||||||
{
|
{
|
||||||
await RecreateMenuItemsAsync();
|
await RecreateMenuItemsAsync();
|
||||||
|
|
||||||
if (MenuItems.FirstOrDefault(a => a is IAccountMenuItem) is IAccountMenuItem firstAccount)
|
// Try to restore latest selected account.
|
||||||
|
if (latestSelectedAccountMenuItem != null)
|
||||||
|
{
|
||||||
|
await ChangeLoadedAccountAsync(latestSelectedAccountMenuItem, navigateInbox: true);
|
||||||
|
}
|
||||||
|
else if (MenuItems.FirstOrDefault(a => a is IAccountMenuItem) is IAccountMenuItem firstAccount)
|
||||||
{
|
{
|
||||||
await ChangeLoadedAccountAsync(firstAccount, message.AutomaticallyNavigateFirstItem);
|
await ChangeLoadedAccountAsync(firstAccount, message.AutomaticallyNavigateFirstItem);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.Collections;
|
using CommunityToolkit.Mvvm.Collections;
|
||||||
|
using Serilog;
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
using Wino.Core.Domain.Entities.Mail;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
@@ -363,53 +364,62 @@ public class WinoMailCollection
|
|||||||
|
|
||||||
public MailItemViewModel GetNextItem(MailCopy mailCopy)
|
public MailItemViewModel GetNextItem(MailCopy mailCopy)
|
||||||
{
|
{
|
||||||
var groupCount = _mailItemSource.Count;
|
try
|
||||||
|
|
||||||
for (int i = 0; i < groupCount; i++)
|
|
||||||
{
|
{
|
||||||
var group = _mailItemSource[i];
|
var groupCount = _mailItemSource.Count;
|
||||||
|
|
||||||
for (int k = 0; k < group.Count; k++)
|
for (int i = 0; i < groupCount; i++)
|
||||||
{
|
{
|
||||||
var item = group[k];
|
var group = _mailItemSource[i];
|
||||||
|
|
||||||
if (item is MailItemViewModel singleMailItemViewModel && singleMailItemViewModel.UniqueId == mailCopy.UniqueId)
|
for (int k = 0; k < group.Count; k++)
|
||||||
{
|
{
|
||||||
if (k + 1 < group.Count)
|
var item = group[k];
|
||||||
{
|
|
||||||
return group[k + 1] as MailItemViewModel;
|
|
||||||
}
|
|
||||||
else if (i + 1 < groupCount)
|
|
||||||
{
|
|
||||||
return _mailItemSource[i + 1][0] as MailItemViewModel;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (item is ThreadMailItemViewModel threadMailItemViewModel && threadMailItemViewModel.HasUniqueId(mailCopy.UniqueId))
|
|
||||||
{
|
|
||||||
var singleItemViewModel = threadMailItemViewModel.GetItemById(mailCopy.UniqueId) as MailItemViewModel;
|
|
||||||
|
|
||||||
if (singleItemViewModel == null) return null;
|
if (item is MailItemViewModel singleMailItemViewModel && singleMailItemViewModel.UniqueId == mailCopy.UniqueId)
|
||||||
|
|
||||||
var singleItemIndex = threadMailItemViewModel.ThreadItems.IndexOf(singleItemViewModel);
|
|
||||||
|
|
||||||
if (singleItemIndex + 1 < threadMailItemViewModel.ThreadItems.Count)
|
|
||||||
{
|
{
|
||||||
return threadMailItemViewModel.ThreadItems[singleItemIndex + 1] as MailItemViewModel;
|
if (k + 1 < group.Count)
|
||||||
|
{
|
||||||
|
return group[k + 1] as MailItemViewModel;
|
||||||
|
}
|
||||||
|
else if (i + 1 < groupCount)
|
||||||
|
{
|
||||||
|
return _mailItemSource[i + 1][0] as MailItemViewModel;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (i + 1 < groupCount)
|
else if (item is ThreadMailItemViewModel threadMailItemViewModel && threadMailItemViewModel.HasUniqueId(mailCopy.UniqueId))
|
||||||
{
|
{
|
||||||
return _mailItemSource[i + 1][0] as MailItemViewModel;
|
var singleItemViewModel = threadMailItemViewModel.GetItemById(mailCopy.UniqueId) as MailItemViewModel;
|
||||||
}
|
|
||||||
else
|
if (singleItemViewModel == null) return null;
|
||||||
{
|
|
||||||
return null;
|
var singleItemIndex = threadMailItemViewModel.ThreadItems.IndexOf(singleItemViewModel);
|
||||||
|
|
||||||
|
if (singleItemIndex + 1 < threadMailItemViewModel.ThreadItems.Count)
|
||||||
|
{
|
||||||
|
return threadMailItemViewModel.ThreadItems[singleItemIndex + 1] as MailItemViewModel;
|
||||||
|
}
|
||||||
|
else if (i + 1 < groupCount)
|
||||||
|
{
|
||||||
|
return _mailItemSource[i + 1][0] as MailItemViewModel;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "Failed to find the next item to select.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -711,7 +711,10 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
|
|
||||||
if (isDeletedMailSelected && PreferencesService.AutoSelectNextItem)
|
if (isDeletedMailSelected && PreferencesService.AutoSelectNextItem)
|
||||||
{
|
{
|
||||||
nextItem = MailCollection.GetNextItem(removedMail);
|
await ExecuteUIThread(() =>
|
||||||
|
{
|
||||||
|
nextItem = MailCollection.GetNextItem(removedMail);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the deleted item from the list.
|
// Remove the deleted item from the list.
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ public class ServerContext :
|
|||||||
|
|
||||||
private async void SynchronizationTimerTriggered(object sender, System.Timers.ElapsedEventArgs e)
|
private async void SynchronizationTimerTriggered(object sender, System.Timers.ElapsedEventArgs e)
|
||||||
{
|
{
|
||||||
|
#if !DEBUG
|
||||||
// Send sync request for all accounts.
|
// Send sync request for all accounts.
|
||||||
|
|
||||||
var accounts = await _accountService.GetAccountsAsync();
|
var accounts = await _accountService.GetAccountsAsync();
|
||||||
@@ -102,6 +103,7 @@ public class ServerContext :
|
|||||||
|
|
||||||
await ExecuteServerMessageSafeAsync(null, request);
|
await ExecuteServerMessageSafeAsync(null, request);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Message Handlers
|
#region Message Handlers
|
||||||
|
|||||||
@@ -1059,4 +1059,16 @@ public class MailService : BaseDatabaseService, IMailService
|
|||||||
|
|
||||||
public Task<bool> IsMailExistsAsync(string mailCopyId, Guid folderId)
|
public Task<bool> IsMailExistsAsync(string mailCopyId, Guid folderId)
|
||||||
=> Connection.ExecuteScalarAsync<bool>("SELECT EXISTS(SELECT 1 FROM MailCopy WHERE Id = ? AND FolderId = ?)", mailCopyId, folderId);
|
=> Connection.ExecuteScalarAsync<bool>("SELECT EXISTS(SELECT 1 FROM MailCopy WHERE Id = ? AND FolderId = ?)", mailCopyId, folderId);
|
||||||
|
|
||||||
|
public async Task<GmailArchiveComparisonResult> GetGmailArchiveComparisonResultAsync(Guid archiveFolderId, List<string> onlineArchiveMailIds)
|
||||||
|
{
|
||||||
|
var localArchiveMails = await Connection.Table<MailCopy>()
|
||||||
|
.Where(a => a.FolderId == archiveFolderId)
|
||||||
|
.ToListAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
var removedMails = localArchiveMails.Where(a => !onlineArchiveMailIds.Contains(a.Id)).Select(a => a.Id).Distinct().ToArray();
|
||||||
|
var addedMails = onlineArchiveMailIds.Where(a => !localArchiveMails.Select(b => b.Id).Contains(a)).Distinct().ToArray();
|
||||||
|
|
||||||
|
return new GmailArchiveComparisonResult(addedMails, removedMails);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public static class ServiceConstants
|
|||||||
public const string SPAM_LABEL_ID = "SPAM";
|
public const string SPAM_LABEL_ID = "SPAM";
|
||||||
public const string CHAT_LABEL_ID = "CHAT";
|
public const string CHAT_LABEL_ID = "CHAT";
|
||||||
public const string TRASH_LABEL_ID = "TRASH";
|
public const string TRASH_LABEL_ID = "TRASH";
|
||||||
|
public const string ARCHIVE_LABEL_ID = "ARCHIVE";
|
||||||
|
|
||||||
// Category labels.
|
// Category labels.
|
||||||
public const string FORUMS_LABEL_ID = "FORUMS";
|
public const string FORUMS_LABEL_ID = "FORUMS";
|
||||||
|
|||||||
Reference in New Issue
Block a user