diff --git a/Wino.Core.Domain/Enums/InvalidMoveTargetReason.cs b/Wino.Core.Domain/Enums/InvalidMoveTargetReason.cs new file mode 100644 index 00000000..8b0f4b37 --- /dev/null +++ b/Wino.Core.Domain/Enums/InvalidMoveTargetReason.cs @@ -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. +} diff --git a/Wino.Core.Domain/Exceptions/InvalidMoveTargetException.cs b/Wino.Core.Domain/Exceptions/InvalidMoveTargetException.cs index 830a8205..f3e7b1ae 100644 --- a/Wino.Core.Domain/Exceptions/InvalidMoveTargetException.cs +++ b/Wino.Core.Domain/Exceptions/InvalidMoveTargetException.cs @@ -1,5 +1,9 @@ using System; +using Wino.Core.Domain.Enums; namespace Wino.Core.Domain.Exceptions; -public class InvalidMoveTargetException : Exception { } +public class InvalidMoveTargetException(InvalidMoveTargetReason reason) : Exception +{ + public InvalidMoveTargetReason Reason { get; } = reason; +} diff --git a/Wino.Core.Domain/Interfaces/IMailService.cs b/Wino.Core.Domain/Interfaces/IMailService.cs index e4617bba..34331e16 100644 --- a/Wino.Core.Domain/Interfaces/IMailService.cs +++ b/Wino.Core.Domain/Interfaces/IMailService.cs @@ -141,4 +141,12 @@ public interface IMailService /// /// Account id. Task HasAccountAnyDraftAsync(Guid accountId); + + /// + /// Compares the ids returned from online search result for Archive folder against the local database. + /// + /// Archive folder id. + /// Retrieved MailCopy ids from search result. + /// Result model that contains added and removed mail copy ids. + Task GetGmailArchiveComparisonResultAsync(Guid archiveFolderId, List onlineArchiveMailIds); } diff --git a/Wino.Core.Domain/MenuItems/FolderMenuItem.cs b/Wino.Core.Domain/MenuItems/FolderMenuItem.cs index ce5d5abc..c5cbbf5d 100644 --- a/Wino.Core.Domain/MenuItems/FolderMenuItem.cs +++ b/Wino.Core.Domain/MenuItems/FolderMenuItem.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; -using Wino.Core.Domain; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; @@ -32,6 +31,8 @@ public partial class FolderMenuItem : MenuItemBase +/// Comparison result of the Gmail archive. +/// +/// Mail copy ids to be added to Archive. +/// Mail copy ids to be removed from Archive. +public record GmailArchiveComparisonResult(string[] Added, string[] Removed); diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index f6e3fd26..15cbec78 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -184,6 +184,7 @@ "Exception_ImapClientPoolFailed": "IMAP Client Pool failed.", "Exception_InboxNotAvailable": "Couldn't setup account folders.", "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_MissingAlias": "Primary alias does not exist for this account. Creating draft failed.", "Exception_NullAssignedAccount": "Assigned account is null", @@ -218,6 +219,7 @@ "GeneralTitle_Warning": "Warning", "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.", + "GmailArchiveFolderNameOverride": "Archive", "HoverActionOption_Archive": "Archive", "HoverActionOption_Delete": "Delete", "HoverActionOption_MoveJunk": "Move to Junk", diff --git a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs index e7cab535..2b44cdbb 100644 --- a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs +++ b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs @@ -63,6 +63,16 @@ public interface IDefaultChangeProcessor Task GetMailCopyAsync(string mailCopyId); Task CreateMailRawAsync(MailAccount account, MailItemFolder mailItemFolder, NewMailItemPackage package); Task DeleteUserMailCacheAsync(Guid accountId); + + /// + /// 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. + /// + /// Message id + /// Folder's local id. + /// Whether mail exists in the folder or not. + Task IsMailExistsInFolderAsync(string messageId, Guid folderId); } public interface IGmailChangeProcessor : IDefaultChangeProcessor @@ -71,19 +81,11 @@ public interface IGmailChangeProcessor : IDefaultChangeProcessor Task MapLocalDraftAsync(string mailCopyId, string newDraftId, string newThreadId); Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId); Task ManageCalendarEventAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount); + Task GetGmailArchiveComparisonResultAsync(Guid archiveFolderId, List onlineArchiveMailIds); } public interface IOutlookChangeProcessor : IDefaultChangeProcessor { - /// - /// 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. - /// - /// Message id - /// Folder's local id. - /// Whether mail exists in the folder or not. - Task IsMailExistsInFolderAsync(string messageId, Guid folderId); - /// /// Updates Folder's delta synchronization identifier. /// 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 AccountService.DeleteAccountMailCacheAsync(accountId, AccountCacheResetReason.ExpiredCache).ConfigureAwait(false); } + + public Task IsMailExistsInFolderAsync(string messageId, Guid folderId) + => MailService.IsMailExistsAsync(messageId, folderId); } diff --git a/Wino.Core/Integration/Processors/GmailChangeProcessor.cs b/Wino.Core/Integration/Processors/GmailChangeProcessor.cs index 0c1ea2da..c478308d 100644 --- a/Wino.Core/Integration/Processors/GmailChangeProcessor.cs +++ b/Wino.Core/Integration/Processors/GmailChangeProcessor.cs @@ -8,6 +8,7 @@ using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.MailItem; using Wino.Core.Extensions; using Wino.Services; using CalendarEventAttendee = Wino.Core.Domain.Entities.Calendar.CalendarEventAttendee; @@ -310,4 +311,7 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso public Task HasAccountAnyDraftAsync(Guid accountId) => MailService.HasAccountAnyDraftAsync(accountId); + + public Task GetGmailArchiveComparisonResultAsync(Guid archiveFolderId, List onlineArchiveMailIds) + => MailService.GetGmailArchiveComparisonResultAsync(archiveFolderId, onlineArchiveMailIds); } diff --git a/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs b/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs index aaecf767..74efaea1 100644 --- a/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs +++ b/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs @@ -21,9 +21,6 @@ public class OutlookChangeProcessor(IDatabaseService databaseService, , IOutlookChangeProcessor { - public Task IsMailExistsInFolderAsync(string messageId, Guid folderId) - => MailService.IsMailExistsAsync(messageId, folderId); - public Task ResetAccountDeltaTokenAsync(Guid accountId) => AccountService.UpdateSynchronizationIdentifierAsync(accountId, null); diff --git a/Wino.Core/Services/WinoRequestDelegator.cs b/Wino.Core/Services/WinoRequestDelegator.cs index 7f7384fa..38003c62 100644 --- a/Wino.Core/Services/WinoRequestDelegator.cs +++ b/Wino.Core/Services/WinoRequestDelegator.cs @@ -53,9 +53,19 @@ public class WinoRequestDelegator : IWinoRequestDelegator _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) { diff --git a/Wino.Core/Services/WinoRequestProcessor.cs b/Wino.Core/Services/WinoRequestProcessor.cs index 9aae4f09..5eead88c 100644 --- a/Wino.Core/Services/WinoRequestProcessor.cs +++ b/Wino.Core/Services/WinoRequestProcessor.cs @@ -75,8 +75,13 @@ public class WinoRequestProcessor : IWinoRequestProcessor if (action == MailOperation.Move && moveTargetStructure == null) { - // TODO: Handle multiple accounts for move operation. - // What happens if we move 2 different mails from 2 different accounts? + // Handle the case when user is trying to move multiple mails that belong to 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; @@ -142,7 +147,7 @@ public class WinoRequestProcessor : IWinoRequestProcessor else if (action == MailOperation.Move) { if (moveTargetStructure == null) - throw new InvalidMoveTargetException(); + throw new InvalidMoveTargetException(InvalidMoveTargetReason.NonMoveTarget); // TODO // Rule: You can't move items to non-move target folders; diff --git a/Wino.Core/Synchronizers/GmailSynchronizer.cs b/Wino.Core/Synchronizers/GmailSynchronizer.cs index 54965f50..f02778f1 100644 --- a/Wino.Core/Synchronizers/GmailSynchronizer.cs +++ b/Wino.Core/Synchronizers/GmailSynchronizer.cs @@ -63,6 +63,9 @@ public class GmailSynchronizer : WinoSynchronizer(); + // Keeping a reference for quick access to the virtual archive folder. + private Guid? archiveFolderId; + public GmailSynchronizer(MailAccount account, IGmailAuthenticator authenticator, IGmailChangeProcessor gmailChangeProcessor) : base(account) @@ -120,6 +123,10 @@ public class GmailSynchronizer : WinoSynchronizer 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. 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. await MapDraftIdsAsync(cancellationToken).ConfigureAwait(false); @@ -484,6 +468,51 @@ public class GmailSynchronizer : WinoSynchronizer 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) { var localFolders = await _gmailChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false); @@ -502,12 +531,14 @@ public class GmailSynchronizer : WinoSynchronizer deletedFolders = new(); // 1. Handle deleted labels. - foreach (var localFolder in localFolders) { // Category folder is virtual folder for Wino. Skip it. 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); if (remoteFolder == null) @@ -558,7 +589,6 @@ public class GmailSynchronizer : WinoSynchronizer Insert, Update. Deleted ones are already processed. - foreach (var folder in insertedFolders) { await _gmailChangeProcessor.InsertFolderAsync(folder).ConfigureAwait(false); @@ -731,6 +761,29 @@ public class GmailSynchronizer : WinoSynchronizer + /// Gmail Archive is a special folder that is not visible in the Gmail web interface. + /// We need to handle it separately. + /// + /// Cancellation token. + 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(); + + 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); + } + } + /// /// Maps existing Gmail Draft resources to local mail copies. /// This uses indexed search, therefore it's quite fast. diff --git a/Wino.Mail.ViewModels/AppShellViewModel.cs b/Wino.Mail.ViewModels/AppShellViewModel.cs index 057a3b71..4b23fe77 100644 --- a/Wino.Mail.ViewModels/AppShellViewModel.cs +++ b/Wino.Mail.ViewModels/AppShellViewModel.cs @@ -976,7 +976,12 @@ public partial class AppShellViewModel : MailBaseViewModel, { 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); } diff --git a/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs b/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs index 85856bb7..dfa4c0ee 100644 --- a/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs +++ b/Wino.Mail.ViewModels/Collections/WinoMailCollection.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Collections; +using Serilog; using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; @@ -363,53 +364,62 @@ public class WinoMailCollection public MailItemViewModel GetNextItem(MailCopy mailCopy) { - var groupCount = _mailItemSource.Count; - - for (int i = 0; i < groupCount; i++) + try { - 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) - { - 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; + var item = group[k]; - if (singleItemViewModel == null) return null; - - var singleItemIndex = threadMailItemViewModel.ThreadItems.IndexOf(singleItemViewModel); - - if (singleItemIndex + 1 < threadMailItemViewModel.ThreadItems.Count) + if (item is MailItemViewModel singleMailItemViewModel && singleMailItemViewModel.UniqueId == mailCopy.UniqueId) { - 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; - } - else - { - return null; + var singleItemViewModel = threadMailItemViewModel.GetItemById(mailCopy.UniqueId) as MailItemViewModel; + + if (singleItemViewModel == 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; diff --git a/Wino.Mail.ViewModels/MailListPageViewModel.cs b/Wino.Mail.ViewModels/MailListPageViewModel.cs index baf7e40d..056e493f 100644 --- a/Wino.Mail.ViewModels/MailListPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailListPageViewModel.cs @@ -711,7 +711,10 @@ public partial class MailListPageViewModel : MailBaseViewModel, if (isDeletedMailSelected && PreferencesService.AutoSelectNextItem) { - nextItem = MailCollection.GetNextItem(removedMail); + await ExecuteUIThread(() => + { + nextItem = MailCollection.GetNextItem(removedMail); + }); } // Remove the deleted item from the list. diff --git a/Wino.Server/ServerContext.cs b/Wino.Server/ServerContext.cs index 79b2454e..82bb7092 100644 --- a/Wino.Server/ServerContext.cs +++ b/Wino.Server/ServerContext.cs @@ -86,6 +86,7 @@ public class ServerContext : private async void SynchronizationTimerTriggered(object sender, System.Timers.ElapsedEventArgs e) { +#if !DEBUG // Send sync request for all accounts. var accounts = await _accountService.GetAccountsAsync(); @@ -102,6 +103,7 @@ public class ServerContext : await ExecuteServerMessageSafeAsync(null, request); } +#endif } #region Message Handlers diff --git a/Wino.Services/MailService.cs b/Wino.Services/MailService.cs index 054a781d..c498fc27 100644 --- a/Wino.Services/MailService.cs +++ b/Wino.Services/MailService.cs @@ -1059,4 +1059,16 @@ public class MailService : BaseDatabaseService, IMailService public Task IsMailExistsAsync(string mailCopyId, Guid folderId) => Connection.ExecuteScalarAsync("SELECT EXISTS(SELECT 1 FROM MailCopy WHERE Id = ? AND FolderId = ?)", mailCopyId, folderId); + + public async Task GetGmailArchiveComparisonResultAsync(Guid archiveFolderId, List onlineArchiveMailIds) + { + var localArchiveMails = await Connection.Table() + .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); + } } diff --git a/Wino.Services/ServiceConstants.cs b/Wino.Services/ServiceConstants.cs index 005ff31f..331506fe 100644 --- a/Wino.Services/ServiceConstants.cs +++ b/Wino.Services/ServiceConstants.cs @@ -16,6 +16,7 @@ public static class ServiceConstants public const string SPAM_LABEL_ID = "SPAM"; public const string CHAT_LABEL_ID = "CHAT"; public const string TRASH_LABEL_ID = "TRASH"; + public const string ARCHIVE_LABEL_ID = "ARCHIVE"; // Category labels. public const string FORUMS_LABEL_ID = "FORUMS";