2024-04-18 01:44:37 +02:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Threading.Tasks ;
2024-07-09 01:05:16 +02:00
using CommunityToolkit.Mvvm.Messaging ;
2024-04-18 01:44:37 +02:00
using Serilog ;
using SqlKata ;
using Wino.Core.Domain ;
2024-11-10 23:28:25 +01:00
using Wino.Core.Domain.Entities.Mail ;
using Wino.Core.Domain.Entities.Shared ;
2024-04-18 01:44:37 +02:00
using Wino.Core.Domain.Enums ;
using Wino.Core.Domain.Interfaces ;
2024-11-30 23:05:07 +01:00
using Wino.Core.Domain.MenuItems ;
2024-07-09 01:05:16 +02:00
using Wino.Core.Domain.Models.Accounts ;
2024-04-18 01:44:37 +02:00
using Wino.Core.Domain.Models.Folders ;
using Wino.Core.Domain.Models.MailItem ;
using Wino.Core.Domain.Models.Synchronization ;
2024-08-05 00:36:26 +02:00
using Wino.Messaging.UI ;
2024-11-30 23:05:07 +01:00
using Wino.Services.Extensions ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
namespace Wino.Services
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:43:30 +01:00
public class FolderService : BaseDatabaseService , IFolderService
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:43:30 +01:00
private readonly IAccountService _accountService ;
private readonly ILogger _logger = Log . ForContext < FolderService > ( ) ;
private readonly SpecialFolderType [ ] gmailCategoryFolderTypes =
[
SpecialFolderType . Promotions ,
SpecialFolderType . Social ,
SpecialFolderType . Updates ,
SpecialFolderType . Forums ,
SpecialFolderType . Personal
] ;
public FolderService ( IDatabaseService databaseService ,
IAccountService accountService ) : base ( databaseService )
{
_accountService = accountService ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
public async Task ChangeStickyStatusAsync ( Guid folderId , bool isSticky )
= > await Connection . ExecuteAsync ( "UPDATE MailItemFolder SET IsSticky = ? WHERE Id = ?" , isSticky , folderId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
public async Task < int > GetFolderNotificationBadgeAsync ( Guid folderId )
{
var folder = await GetFolderAsync ( folderId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
if ( folder = = null | | ! folder . ShowUnreadCount ) return default ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
var account = await _accountService . GetAccountAsync ( folder . MailAccountId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
if ( account = = null ) return default ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
var query = new Query ( "MailCopy" )
. Where ( "FolderId" , folderId )
. SelectRaw ( "count (DISTINCT Id)" ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
// If focused inbox is enabled, we need to check if this is the inbox folder.
if ( account . Preferences . IsFocusedInboxEnabled . GetValueOrDefault ( ) & & folder . SpecialFolderType = = SpecialFolderType . Inbox )
{
query . Where ( "IsFocused" , 1 ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
// Draft and Junk folders are not counted as unread. They must return the item count instead.
if ( folder . SpecialFolderType ! = SpecialFolderType . Draft & & folder . SpecialFolderType ! = SpecialFolderType . Junk )
{
query . Where ( "IsRead" , 0 ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
return await Connection . ExecuteScalarAsync < int > ( query . GetRawQuery ( ) ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
public async Task < AccountFolderTree > GetFolderStructureForAccountAsync ( Guid accountId , bool includeHiddenFolders )
{
var account = await _accountService . GetAccountAsync ( accountId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
if ( account = = null )
throw new ArgumentException ( nameof ( account ) ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
var accountTree = new AccountFolderTree ( account ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
// Account folders.
var folderQuery = Connection . Table < MailItemFolder > ( ) . Where ( a = > a . MailAccountId = = accountId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
if ( ! includeHiddenFolders )
folderQuery = folderQuery . Where ( a = > ! a . IsHidden ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
// Load child folders for each folder.
var allFolders = await folderQuery . OrderBy ( a = > a . SpecialFolderType ) . ToListAsync ( ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
if ( allFolders . Any ( ) )
2025-02-16 11:35:43 +01:00
{
2025-02-16 11:43:30 +01:00
// Get sticky folders. Category type is always sticky.
// Sticky folders don't have tree structure. So they can be added to the main tree.
var stickyFolders = allFolders . Where ( a = > a . IsSticky & & a . SpecialFolderType ! = SpecialFolderType . Category ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
foreach ( var stickyFolder in stickyFolders )
{
var childStructure = await GetChildFolderItemsRecursiveAsync ( stickyFolder . Id , accountId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
accountTree . Folders . Add ( childStructure ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
// Check whether we need special 'Categories' kind of folder.
var categoryExists = allFolders . Any ( a = > a . SpecialFolderType = = SpecialFolderType . Category ) ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:43:30 +01:00
if ( categoryExists )
2025-02-16 11:35:43 +01:00
{
2025-02-16 11:43:30 +01:00
var categoryFolder = allFolders . First ( a = > a . SpecialFolderType = = SpecialFolderType . Category ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
// Construct category items under pinned items.
var categoryFolders = allFolders . Where ( a = > gmailCategoryFolderTypes . Contains ( a . SpecialFolderType ) ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
foreach ( var categoryFolderSubItem in categoryFolders )
{
categoryFolder . ChildFolders . Add ( categoryFolderSubItem ) ;
}
2025-02-16 11:35:43 +01:00
2025-02-16 11:43:30 +01:00
accountTree . Folders . Add ( categoryFolder ) ;
allFolders . Remove ( categoryFolder ) ;
}
// Move rest of the items into virtual More folder if any.
var nonStickyFolders = allFolders . Except ( stickyFolders ) ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:43:30 +01:00
if ( nonStickyFolders . Any ( ) )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:43:30 +01:00
var virtualMoreFolder = new MailItemFolder ( )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:43:30 +01:00
FolderName = Translator . More ,
SpecialFolderType = SpecialFolderType . More
} ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
foreach ( var unstickyItem in nonStickyFolders )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:43:30 +01:00
if ( account . ProviderType = = MailProviderType . Gmail )
{
// Gmail requires this check to not include child folders as
// separate folder without their parent for More folder...
if ( ! string . IsNullOrEmpty ( unstickyItem . ParentRemoteFolderId ) )
continue ;
}
else if ( account . ProviderType = = MailProviderType . Outlook )
{
bool belongsToExistingParent = await Connection
. Table < MailItemFolder > ( )
. Where ( a = > unstickyItem . ParentRemoteFolderId = = a . RemoteFolderId )
. CountAsync ( ) > 0 ;
// No need to include this as unsticky.
if ( belongsToExistingParent ) continue ;
}
var structure = await GetChildFolderItemsRecursiveAsync ( unstickyItem . Id , accountId ) ;
virtualMoreFolder . ChildFolders . Add ( structure ) ;
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:43:30 +01:00
// Only add more if there are any.
if ( virtualMoreFolder . ChildFolders . Count > 0 )
accountTree . Folders . Add ( virtualMoreFolder ) ;
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:35:43 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
return accountTree ;
}
2025-02-16 11:35:43 +01:00
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
public Task < IEnumerable < IMenuItem > > GetAccountFoldersForDisplayAsync ( IAccountMenuItem accountMenuItem )
2024-07-09 01:05:16 +02:00
{
2025-02-16 11:43:30 +01:00
if ( accountMenuItem is IMergedAccountMenuItem mergedAccountFolderMenuItem )
{
return GetMergedAccountFolderMenuItemsAsync ( mergedAccountFolderMenuItem ) ;
}
else
{
return GetSingleAccountFolderMenuItemsAsync ( accountMenuItem ) ;
}
2025-02-16 11:35:43 +01:00
}
2025-02-16 11:43:30 +01:00
private async Task < FolderMenuItem > GetPreparedFolderMenuItemRecursiveAsync ( MailAccount account , MailItemFolder parentFolder , IMenuItem parentMenuItem )
{
// Localize category folder name.
if ( parentFolder . SpecialFolderType = = SpecialFolderType . Category ) parentFolder . FolderName = Translator . CategoriesFolderNameOverride ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
var query = new Query ( nameof ( MailItemFolder ) )
. Where ( nameof ( MailItemFolder . ParentRemoteFolderId ) , parentFolder . RemoteFolderId )
. Where ( nameof ( MailItemFolder . MailAccountId ) , parentFolder . MailAccountId ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
var preparedFolder = new FolderMenuItem ( parentFolder , account , parentMenuItem ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
var childFolders = await Connection . QueryAsync < MailItemFolder > ( query . GetRawQuery ( ) ) . ConfigureAwait ( false ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
if ( childFolders . Any ( ) )
2024-07-09 01:05:16 +02:00
{
2025-02-16 11:43:30 +01:00
foreach ( var subChildFolder in childFolders )
{
var preparedChild = await GetPreparedFolderMenuItemRecursiveAsync ( account , subChildFolder , preparedFolder ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
if ( preparedChild = = null ) continue ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
preparedFolder . SubMenuItems . Add ( preparedChild ) ;
}
2024-07-09 01:05:16 +02:00
}
2025-02-16 11:43:30 +01:00
return preparedFolder ;
}
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
private async Task < IEnumerable < IMenuItem > > GetSingleAccountFolderMenuItemsAsync ( IAccountMenuItem accountMenuItem )
{
var accountId = accountMenuItem . EntityId . Value ;
var preparedFolderMenuItems = new List < IMenuItem > ( ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
// Get all folders for the account. Excluding hidden folders.
var folders = await GetVisibleFoldersAsync ( accountId ) . ConfigureAwait ( false ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
if ( ! folders . Any ( ) ) return new List < IMenuItem > ( ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
var mailAccount = accountMenuItem . HoldingAccounts . First ( ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
var listingFolders = folders . OrderBy ( a = > a . SpecialFolderType ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
var moreFolder = MailItemFolder . CreateMoreFolder ( ) ;
var categoryFolder = MailItemFolder . CreateCategoriesFolder ( ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
var moreFolderMenuItem = new FolderMenuItem ( moreFolder , mailAccount , accountMenuItem ) ;
var categoryFolderMenuItem = new FolderMenuItem ( categoryFolder , mailAccount , accountMenuItem ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
foreach ( var item in listingFolders )
{
// Category type folders should be skipped. They will be categorized under virtual category folder.
if ( ServiceConstants . SubCategoryFolderLabelIds . Contains ( item . RemoteFolderId ) ) continue ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
bool skipEmptyParentRemoteFolders = mailAccount . ProviderType = = MailProviderType . Gmail ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
if ( skipEmptyParentRemoteFolders & & ! string . IsNullOrEmpty ( item . ParentRemoteFolderId ) ) continue ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
// Sticky items belong to account menu item directly. Rest goes to More folder.
IMenuItem parentFolderMenuItem = item . IsSticky ? accountMenuItem : ServiceConstants . SubCategoryFolderLabelIds . Contains ( item . FolderName . ToUpper ( ) ) ? categoryFolderMenuItem : moreFolderMenuItem ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
var preparedItem = await GetPreparedFolderMenuItemRecursiveAsync ( mailAccount , item , parentFolderMenuItem ) . ConfigureAwait ( false ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
// Don't add menu items that are prepared for More folder. They've been included in More virtual folder already.
// We'll add More folder later on at the end of the list.
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
if ( preparedItem = = null ) continue ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
if ( item . IsSticky )
{
preparedFolderMenuItems . Add ( preparedItem ) ;
}
else if ( parentFolderMenuItem is FolderMenuItem baseParentFolderMenuItem )
{
baseParentFolderMenuItem . SubMenuItems . Add ( preparedItem ) ;
}
2025-02-16 11:35:43 +01:00
}
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
// Only add category folder if it's Gmail.
if ( mailAccount . ProviderType = = MailProviderType . Gmail ) preparedFolderMenuItems . Add ( categoryFolderMenuItem ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
// Only add More folder if there are any items in it.
if ( moreFolderMenuItem . SubMenuItems . Any ( ) ) preparedFolderMenuItems . Add ( moreFolderMenuItem ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
return preparedFolderMenuItems ;
}
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
private async Task < IEnumerable < IMenuItem > > GetMergedAccountFolderMenuItemsAsync ( IMergedAccountMenuItem mergedAccountFolderMenuItem )
{
var holdingAccounts = mergedAccountFolderMenuItem . HoldingAccounts ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
if ( holdingAccounts = = null | | ! holdingAccounts . Any ( ) ) return [ ] ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
var preparedFolderMenuItems = new List < IMenuItem > ( ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
// First gather all account folders.
// Prepare single menu items for both of them.
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
var allAccountFolders = new List < List < MailItemFolder > > ( ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
foreach ( var account in holdingAccounts )
{
var accountFolders = await GetVisibleFoldersAsync ( account . Id ) . ConfigureAwait ( false ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
allAccountFolders . Add ( accountFolders ) ;
}
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
var commonFolders = FindCommonFolders ( allAccountFolders ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
// Prepare menu items for common folders.
foreach ( var commonFolderType in commonFolders )
{
var folderItems = allAccountFolders . SelectMany ( a = > a . Where ( b = > b . SpecialFolderType = = commonFolderType ) ) . Cast < IMailItemFolder > ( ) . ToList ( ) ;
var menuItem = new MergedAccountFolderMenuItem ( folderItems , null , mergedAccountFolderMenuItem . Parameter ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
preparedFolderMenuItems . Add ( menuItem ) ;
}
return preparedFolderMenuItems ;
2024-07-09 01:05:16 +02:00
}
2025-02-16 11:43:30 +01:00
private HashSet < SpecialFolderType > FindCommonFolders ( List < List < MailItemFolder > > lists )
{
var allSpecialTypesExceptOther = Enum . GetValues < SpecialFolderType > ( ) . Cast < SpecialFolderType > ( ) . Where ( a = > a ! = SpecialFolderType . Other ) . ToList ( ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
// Start with all special folder types from the first list
var commonSpecialFolderTypes = new HashSet < SpecialFolderType > ( allSpecialTypesExceptOther ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
// Intersect with special folder types from all lists
foreach ( var list in lists )
{
commonSpecialFolderTypes . IntersectWith ( list . Select ( f = > f . SpecialFolderType ) ) ;
}
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
return commonSpecialFolderTypes ;
2024-07-09 01:05:16 +02:00
}
2025-02-16 11:43:30 +01:00
private async Task < MailItemFolder > GetChildFolderItemsRecursiveAsync ( Guid folderId , Guid accountId )
{
var folder = await Connection . Table < MailItemFolder > ( ) . Where ( a = > a . Id = = folderId & & a . MailAccountId = = accountId ) . FirstOrDefaultAsync ( ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
if ( folder = = null )
return null ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
var childFolders = await Connection . Table < MailItemFolder > ( )
. Where ( a = > a . ParentRemoteFolderId = = folder . RemoteFolderId & & a . MailAccountId = = folder . MailAccountId )
. ToListAsync ( ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
foreach ( var childFolder in childFolders )
{
var subChild = await GetChildFolderItemsRecursiveAsync ( childFolder . Id , accountId ) ;
folder . ChildFolders . Add ( subChild ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
return folder ;
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:43:30 +01:00
public async Task < MailItemFolder > GetSpecialFolderByAccountIdAsync ( Guid accountId , SpecialFolderType type )
= > await Connection . Table < MailItemFolder > ( ) . FirstOrDefaultAsync ( a = > a . MailAccountId = = accountId & & a . SpecialFolderType = = type ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
public async Task < MailItemFolder > GetFolderAsync ( Guid folderId )
= > await Connection . Table < MailItemFolder > ( ) . FirstOrDefaultAsync ( a = > a . Id . Equals ( folderId ) ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
public Task < int > GetCurrentItemCountForFolder ( Guid folderId )
= > Connection . Table < MailCopy > ( ) . Where ( a = > a . FolderId = = folderId ) . CountAsync ( ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
public Task < List < MailItemFolder > > GetFoldersAsync ( Guid accountId )
{
var query = new Query ( nameof ( MailItemFolder ) )
. Where ( nameof ( MailItemFolder . MailAccountId ) , accountId )
. OrderBy ( nameof ( MailItemFolder . SpecialFolderType ) ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
return Connection . QueryAsync < MailItemFolder > ( query . GetRawQuery ( ) ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
public Task < List < MailItemFolder > > GetVisibleFoldersAsync ( Guid accountId )
{
var query = new Query ( nameof ( MailItemFolder ) )
. Where ( nameof ( MailItemFolder . MailAccountId ) , accountId )
. Where ( nameof ( MailItemFolder . IsHidden ) , false )
. OrderBy ( nameof ( MailItemFolder . SpecialFolderType ) ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
return Connection . QueryAsync < MailItemFolder > ( query . GetRawQuery ( ) ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
public async Task < IList < uint > > GetKnownUidsForFolderAsync ( Guid folderId )
{
var mailCopyIds = await GetMailCopyIdsByFolderIdAsync ( folderId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
// Make sure we don't include Ids that doesn't have uid separator.
// Local drafts might not have it for example.
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
return new List < uint > ( mailCopyIds
. Where ( a = > a . Contains ( MailkitClientExtensions . MailCopyUidSeparator ) )
. Select ( a = > MailkitClientExtensions . ResolveUid ( a ) ) ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
public async Task < MailAccount > UpdateSystemFolderConfigurationAsync ( Guid accountId , SystemFolderConfiguration configuration )
{
if ( configuration = = null )
throw new ArgumentNullException ( nameof ( configuration ) ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
// Update system folders for this account.
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
await Task . WhenAll ( UpdateSystemFolderInternalAsync ( configuration . SentFolder , SpecialFolderType . Sent ) ,
UpdateSystemFolderInternalAsync ( configuration . DraftFolder , SpecialFolderType . Draft ) ,
UpdateSystemFolderInternalAsync ( configuration . JunkFolder , SpecialFolderType . Junk ) ,
UpdateSystemFolderInternalAsync ( configuration . TrashFolder , SpecialFolderType . Deleted ) ,
UpdateSystemFolderInternalAsync ( configuration . ArchiveFolder , SpecialFolderType . Archive ) ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
return await _accountService . GetAccountAsync ( accountId ) . ConfigureAwait ( false ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
private Task UpdateSystemFolderInternalAsync ( MailItemFolder folder , SpecialFolderType assignedSpecialFolderType )
{
if ( folder = = null ) return Task . CompletedTask ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
folder . IsSticky = true ;
folder . IsSynchronizationEnabled = true ;
folder . IsSystemFolder = true ;
folder . SpecialFolderType = assignedSpecialFolderType ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
return UpdateFolderAsync ( folder ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
public async Task ChangeFolderSynchronizationStateAsync ( Guid folderId , bool isSynchronizationEnabled )
2025-02-16 11:35:43 +01:00
{
2025-02-16 11:43:30 +01:00
var localFolder = await Connection . Table < MailItemFolder > ( ) . FirstOrDefaultAsync ( a = > a . Id = = folderId ) ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
if ( localFolder ! = null )
{
localFolder . IsSynchronizationEnabled = isSynchronizationEnabled ;
await UpdateFolderAsync ( localFolder ) . ConfigureAwait ( false ) ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:43:30 +01:00
Messenger . Send ( new FolderSynchronizationEnabled ( localFolder ) ) ;
}
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:43:30 +01:00
#region Repository Calls
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
public async Task InsertFolderAsync ( MailItemFolder folder )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:43:30 +01:00
if ( folder = = null )
{
_logger . Warning ( "Folder is null. Cannot insert." ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
return ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
var account = await _accountService . GetAccountAsync ( folder . MailAccountId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
if ( account = = null )
{
_logger . Warning ( "Account with id {MailAccountId} does not exist. Cannot insert folder." , folder . MailAccountId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
return ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
var existingFolder = await GetFolderAsync ( folder . Id ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
// IMAP servers don't have unique identifier for folders all the time.
// So we'll try to match them with remote folder id and account id relation.
// If we have a match, we'll update the folder instead of inserting.
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
existingFolder ? ? = await GetFolderAsync ( folder . MailAccountId , folder . RemoteFolderId ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
if ( existingFolder = = null )
{
_logger . Debug ( "Inserting folder {Id} - {FolderName}" , folder . Id , folder . FolderName , folder . MailAccountId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
await Connection . InsertAsync ( folder ) . ConfigureAwait ( false ) ;
}
else
{
// TODO: This is not alright. We should've updated the folder instead of inserting.
// Now we need to match the properties that user might've set locally.
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
folder . Id = existingFolder . Id ;
folder . IsSticky = existingFolder . IsSticky ;
folder . SpecialFolderType = existingFolder . SpecialFolderType ;
folder . ShowUnreadCount = existingFolder . ShowUnreadCount ;
folder . TextColorHex = existingFolder . TextColorHex ;
folder . BackgroundColorHex = existingFolder . BackgroundColorHex ;
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
_logger . Debug ( "Folder {Id} - {FolderName} already exists. Updating." , folder . Id , folder . FolderName ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
await UpdateFolderAsync ( folder ) . ConfigureAwait ( false ) ;
}
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:43:30 +01:00
public async Task UpdateFolderAsync ( MailItemFolder folder )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:43:30 +01:00
if ( folder = = null )
{
_logger . Warning ( "Folder is null. Cannot update." ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
return ;
}
2025-02-16 11:35:43 +01:00
2025-02-16 11:43:30 +01:00
_logger . Debug ( "Updating folder {FolderName}" , folder . Id , folder . FolderName ) ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:43:30 +01:00
await Connection . UpdateAsync ( folder ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:43:30 +01:00
private async Task DeleteFolderAsync ( MailItemFolder folder )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:43:30 +01:00
if ( folder = = null )
{
_logger . Warning ( "Folder is null. Cannot delete." ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
return ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
var account = await _accountService . GetAccountAsync ( folder . MailAccountId ) . ConfigureAwait ( false ) ;
if ( account = = null )
{
_logger . Warning ( "Account with id {MailAccountId} does not exist. Cannot delete folder." , folder . MailAccountId ) ;
return ;
}
2024-06-21 04:24:04 +02:00
2025-02-16 11:43:30 +01:00
_logger . Debug ( "Deleting folder {FolderName}" , folder . FolderName ) ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:43:30 +01:00
await Connection . DeleteAsync ( folder ) . ConfigureAwait ( false ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
// Delete all existing mails from this folder.
await Connection . ExecuteAsync ( "DELETE FROM MailCopy WHERE FolderId = ?" , folder . Id ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
// TODO: Delete MIME messages from the disk.
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
#endregion
2025-02-16 11:35:43 +01:00
2025-02-16 11:43:30 +01:00
private Task < List < string > > GetMailCopyIdsByFolderIdAsync ( Guid folderId )
{
var query = new Query ( "MailCopy" )
. Where ( "FolderId" , folderId )
. Select ( "Id" ) ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:43:30 +01:00
return Connection . QueryScalarsAsync < string > ( query . GetRawQuery ( ) ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
public async Task < List < MailFolderPairMetadata > > GetMailFolderPairMetadatasAsync ( IEnumerable < string > mailCopyIds )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:43:30 +01:00
// Get all assignments for all items.
var query = new Query ( nameof ( MailCopy ) )
. Join ( nameof ( MailItemFolder ) , $"{nameof(MailCopy)}.FolderId" , $"{nameof(MailItemFolder)}.Id" )
. WhereIn ( $"{nameof(MailCopy)}.Id" , mailCopyIds )
. SelectRaw ( $"{nameof(MailCopy)}.Id as MailCopyId, {nameof(MailItemFolder)}.Id as FolderId, {nameof(MailItemFolder)}.RemoteFolderId as RemoteFolderId" )
. Distinct ( ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
var rowQuery = query . GetRawQuery ( ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
return await Connection . QueryAsync < MailFolderPairMetadata > ( rowQuery ) ;
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:43:30 +01:00
public Task < List < MailFolderPairMetadata > > GetMailFolderPairMetadatasAsync ( string mailCopyId )
= > GetMailFolderPairMetadatasAsync ( new List < string > ( ) { mailCopyId } ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
public async Task < List < MailItemFolder > > GetSynchronizationFoldersAsync ( MailSynchronizationOptions options )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:43:30 +01:00
var folders = new List < MailItemFolder > ( ) ;
2025-02-15 12:53:32 +01:00
2025-02-16 11:43:30 +01:00
if ( options . Type = = MailSynchronizationType . IMAPIdle )
2025-02-16 11:35:43 +01:00
{
2025-02-16 11:43:30 +01:00
// Type Inbox will include Sent, Drafts and Deleted folders as well.
// For IMAP idle sync, we must include only Inbox folder.
var inboxFolder = await GetSpecialFolderByAccountIdAsync ( options . AccountId , SpecialFolderType . Inbox ) ;
if ( inboxFolder ! = null )
{
folders . Add ( inboxFolder ) ;
}
2025-02-15 12:53:32 +01:00
}
2025-02-16 11:43:30 +01:00
else if ( options . Type = = MailSynchronizationType . FullFolders )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:43:30 +01:00
// Only get sync enabled folders.
2024-04-18 01:44:37 +02:00
2024-08-21 13:15:50 +02:00
var synchronizationFolders = await Connection . Table < MailItemFolder > ( )
2025-02-16 11:43:30 +01:00
. Where ( a = > a . MailAccountId = = options . AccountId & & a . IsSynchronizationEnabled )
. OrderBy ( a = > a . SpecialFolderType )
2024-08-21 13:15:50 +02:00
. ToListAsync ( ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
folders . AddRange ( synchronizationFolders ) ;
}
else
{
// Inbox, Sent and Draft folders must always be synchronized regardless of whether they are enabled or not.
// Custom folder sync will add additional folders to the list if not specified.
var mustHaveFolders = await GetInboxSynchronizationFoldersAsync ( options . AccountId ) ;
if ( options . Type = = MailSynchronizationType . InboxOnly )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:43:30 +01:00
return mustHaveFolders ;
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:43:30 +01:00
else if ( options . Type = = MailSynchronizationType . CustomFolders )
{
// Only get the specified folders.
var synchronizationFolders = await Connection . Table < MailItemFolder > ( )
. Where ( a = >
a . MailAccountId = = options . AccountId & &
options . SynchronizationFolderIds . Contains ( a . Id ) )
. ToListAsync ( ) ;
if ( options . ExcludeMustHaveFolders )
{
return synchronizationFolders ;
}
2025-02-15 12:53:32 +01:00
2025-02-16 11:43:30 +01:00
// Order is important for moving.
// By implementation, removing mail folders must be synchronized first. Requests are made in that order for custom sync.
// eg. Moving item from Folder A to Folder B. If we start syncing Folder B first, we might miss adding assignment for Folder A.
2024-08-21 13:15:50 +02:00
2025-02-16 11:43:30 +01:00
var orderedCustomFolders = synchronizationFolders . OrderBy ( a = > options . SynchronizationFolderIds . IndexOf ( a . Id ) ) ;
2024-08-21 13:15:50 +02:00
2025-02-16 11:43:30 +01:00
foreach ( var item in orderedCustomFolders )
2024-08-21 13:15:50 +02:00
{
2025-02-16 11:43:30 +01:00
if ( ! mustHaveFolders . Any ( a = > a . Id = = item . Id ) )
{
mustHaveFolders . Add ( item ) ;
}
2024-08-21 13:15:50 +02:00
}
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:43:30 +01:00
return mustHaveFolders ;
2024-04-18 01:44:37 +02:00
}
2025-02-16 11:43:30 +01:00
return folders ;
2024-08-21 13:15:50 +02:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
private async Task < List < MailItemFolder > > GetInboxSynchronizationFoldersAsync ( Guid accountId )
{
var folders = new List < MailItemFolder > ( ) ;
2024-08-21 13:15:50 +02:00
2025-02-16 11:43:30 +01:00
var inboxFolder = await GetSpecialFolderByAccountIdAsync ( accountId , SpecialFolderType . Inbox ) ;
var sentFolder = await GetSpecialFolderByAccountIdAsync ( accountId , SpecialFolderType . Sent ) ;
var draftFolder = await GetSpecialFolderByAccountIdAsync ( accountId , SpecialFolderType . Draft ) ;
var deletedFolder = await GetSpecialFolderByAccountIdAsync ( accountId , SpecialFolderType . Deleted ) ;
2024-08-21 13:15:50 +02:00
2025-02-16 11:43:30 +01:00
if ( deletedFolder ! = null )
{
folders . Add ( deletedFolder ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
if ( inboxFolder ! = null )
{
folders . Add ( inboxFolder ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
// For properly creating threads we need Sent and Draft to be synchronized as well.
2024-06-21 23:44:59 +02:00
2025-02-16 11:43:30 +01:00
if ( sentFolder ! = null )
{
folders . Add ( sentFolder ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
if ( draftFolder ! = null )
{
folders . Add ( draftFolder ) ;
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
return folders ;
2025-02-16 11:35:43 +01:00
}
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
public Task < MailItemFolder > GetFolderAsync ( Guid accountId , string remoteFolderId )
= > Connection . Table < MailItemFolder > ( ) . FirstOrDefaultAsync ( a = > a . MailAccountId = = accountId & & a . RemoteFolderId = = remoteFolderId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
public async Task DeleteFolderAsync ( Guid accountId , string remoteFolderId )
2024-04-18 01:44:37 +02:00
{
2025-02-16 11:43:30 +01:00
var folder = await GetFolderAsync ( accountId , remoteFolderId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
if ( folder = = null )
{
_logger . Warning ( "Folder with id {RemoteFolderId} does not exist. Delete folder canceled." , remoteFolderId ) ;
2024-04-18 01:44:37 +02:00
2025-02-16 11:43:30 +01:00
return ;
}
2024-06-02 21:35:03 +02:00
2025-02-16 11:43:30 +01:00
await DeleteFolderAsync ( folder ) . ConfigureAwait ( false ) ;
}
2024-07-09 01:05:16 +02:00
2025-02-16 11:43:30 +01:00
public async Task ChangeFolderShowUnreadCountStateAsync ( Guid folderId , bool showUnreadCount )
2024-07-09 01:05:16 +02:00
{
2025-02-16 11:43:30 +01:00
var localFolder = await GetFolderAsync ( folderId ) ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:43:30 +01:00
if ( localFolder ! = null )
{
localFolder . ShowUnreadCount = showUnreadCount ;
await UpdateFolderAsync ( localFolder ) . ConfigureAwait ( false ) ;
}
2024-07-09 01:05:16 +02:00
}
2025-02-16 11:35:43 +01:00
2025-02-16 11:43:30 +01:00
public async Task < bool > IsInboxAvailableForAccountAsync ( Guid accountId )
= > await Connection . Table < MailItemFolder > ( )
. Where ( a = > a . SpecialFolderType = = SpecialFolderType . Inbox & & a . MailAccountId = = accountId )
. CountAsync ( ) = = 1 ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:43:30 +01:00
public Task UpdateFolderLastSyncDateAsync ( Guid folderId )
= > Connection . ExecuteAsync ( "UPDATE MailItemFolder SET LastSynchronizedDate = ? WHERE Id = ?" , DateTime . UtcNow , folderId ) ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:43:30 +01:00
public Task < List < UnreadItemCountResult > > GetUnreadItemCountResultsAsync ( IEnumerable < Guid > accountIds )
{
var query = new Query ( nameof ( MailCopy ) )
. Join ( nameof ( MailItemFolder ) , $"{nameof(MailCopy)}.FolderId" , $"{nameof(MailItemFolder)}.Id" )
. WhereIn ( $"{nameof(MailItemFolder)}.MailAccountId" , accountIds )
. Where ( $"{nameof(MailCopy)}.IsRead" , 0 )
. Where ( $"{nameof(MailItemFolder)}.ShowUnreadCount" , 1 )
. SelectRaw ( $"{nameof(MailItemFolder)}.Id as FolderId, {nameof(MailItemFolder)}.SpecialFolderType as SpecialFolderType, count (DISTINCT {nameof(MailCopy)}.Id) as UnreadItemCount, {nameof(MailItemFolder)}.MailAccountId as AccountId" )
. GroupBy ( $"{nameof(MailItemFolder)}.Id" ) ;
return Connection . QueryAsync < UnreadItemCountResult > ( query . GetRawQuery ( ) ) ;
}
2025-02-16 11:35:43 +01:00
}
2024-04-18 01:44:37 +02:00
}