Skip to content

Commit

Permalink
Gmail - Archive/Unarchive (#582)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
bkaankose authored Feb 23, 2025
1 parent 9d0a2f6 commit 8e1c60d
Show file tree
Hide file tree
Showing 18 changed files with 277 additions and 86 deletions.
7 changes: 7 additions & 0 deletions Wino.Core.Domain/Enums/InvalidMoveTargetReason.cs
Original file line number Diff line number Diff line change
@@ -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.
}
6 changes: 5 additions & 1 deletion Wino.Core.Domain/Exceptions/InvalidMoveTargetException.cs
Original file line number Diff line number Diff line change
@@ -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;
}
8 changes: 8 additions & 0 deletions Wino.Core.Domain/Interfaces/IMailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,12 @@ public interface IMailService
/// </summary>
/// <param name="accountId">Account id.</param>
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);
}
3 changes: 2 additions & 1 deletion Wino.Core.Domain/MenuItems/FolderMenuItem.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -32,6 +31,8 @@ public string FolderName
return Translator.MoreFolderNameOverride;
else if (Parameter.SpecialFolderType == SpecialFolderType.Category)
return Translator.CategoriesFolderNameOverride;
else if (Parameter.SpecialFolderType == SpecialFolderType.Archive && ParentAccount.ProviderType == MailProviderType.Gmail)
return Translator.GmailArchiveFolderNameOverride;
else
return Parameter.FolderName;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
2 changes: 2 additions & 0 deletions Wino.Core.Domain/Translations/en_US/resources.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
23 changes: 14 additions & 9 deletions Wino.Core/Integration/Processors/DefaultChangeProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ public interface IDefaultChangeProcessor
Task<MailCopy> GetMailCopyAsync(string mailCopyId);
Task CreateMailRawAsync(MailAccount account, MailItemFolder mailItemFolder, NewMailItemPackage package);
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
Expand All @@ -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<GmailArchiveComparisonResult> GetGmailArchiveComparisonResultAsync(Guid archiveFolderId, List<string> onlineArchiveMailIds);
}

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>
/// Updates Folder's delta synchronization identifier.
/// Only used in Outlook since it does per-folder sync.
Expand Down Expand Up @@ -211,4 +213,7 @@ public async Task DeleteUserMailCacheAsync(Guid accountId)
await _mimeFileService.DeleteUserMimeCacheAsync(accountId).ConfigureAwait(false);
await AccountService.DeleteAccountMailCacheAsync(accountId, AccountCacheResetReason.ExpiredCache).ConfigureAwait(false);
}

public Task<bool> IsMailExistsInFolderAsync(string messageId, Guid folderId)
=> MailService.IsMailExistsAsync(messageId, folderId);
}
4 changes: 4 additions & 0 deletions Wino.Core/Integration/Processors/GmailChangeProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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;
Expand Down Expand Up @@ -310,4 +311,7 @@ private CalendarItemVisibility GetVisibility(string visibility)

public Task<bool> HasAccountAnyDraftAsync(Guid accountId)
=> MailService.HasAccountAnyDraftAsync(accountId);

public Task<GmailArchiveComparisonResult> GetGmailArchiveComparisonResultAsync(Guid archiveFolderId, List<string> onlineArchiveMailIds)
=> MailService.GetGmailArchiveComparisonResultAsync(archiveFolderId, onlineArchiveMailIds);
}
3 changes: 0 additions & 3 deletions Wino.Core/Integration/Processors/OutlookChangeProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
, IOutlookChangeProcessor
{

public Task<bool> IsMailExistsInFolderAsync(string messageId, Guid folderId)
=> MailService.IsMailExistsAsync(messageId, folderId);

public Task<string> ResetAccountDeltaTokenAsync(Guid accountId)
=> AccountService.UpdateSynchronizationIdentifierAsync(accountId, null);

Expand Down
14 changes: 12 additions & 2 deletions Wino.Core/Services/WinoRequestDelegator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,19 @@ public async Task ExecuteAsync(MailOperationPreperationRequest request)
_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)
{
Expand Down
11 changes: 8 additions & 3 deletions Wino.Core/Services/WinoRequestProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,13 @@ public async Task<List<IMailActionRequest>> PrepareRequestsAsync(MailOperationPr

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;

Expand Down Expand Up @@ -142,7 +147,7 @@ private async Task<IMailActionRequest> GetSingleRequestAsync(MailCopy mailItem,
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;
Expand Down
Loading

0 comments on commit 8e1c60d

Please sign in to comment.