827 lines
34 KiB
C#
827 lines
34 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using CommunityToolkit.Mvvm.Messaging;
|
|
using Serilog;
|
|
using Wino.Core.Domain;
|
|
using Wino.Core.Domain.Entities.Mail;
|
|
using Wino.Core.Domain.Entities.Shared;
|
|
using Wino.Core.Domain.Enums;
|
|
using Wino.Core.Domain.Interfaces;
|
|
using Wino.Core.Domain.MenuItems;
|
|
using Wino.Core.Domain.Models.Accounts;
|
|
using Wino.Core.Domain.Models.Folders;
|
|
using Wino.Core.Domain.Models.MailItem;
|
|
using Wino.Core.Domain.Models.Synchronization;
|
|
using Wino.Messaging.UI;
|
|
using Wino.Services.Extensions;
|
|
|
|
namespace Wino.Services;
|
|
|
|
public class FolderService : BaseDatabaseService, IFolderService
|
|
{
|
|
private readonly IAccountService _accountService;
|
|
private readonly IMailCategoryService _mailCategoryService;
|
|
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,
|
|
IMailCategoryService mailCategoryService) : base(databaseService)
|
|
{
|
|
_accountService = accountService;
|
|
_mailCategoryService = mailCategoryService;
|
|
}
|
|
|
|
public async Task ChangeStickyStatusAsync(Guid folderId, bool isSticky)
|
|
=> await Connection.ExecuteAsync("UPDATE MailItemFolder SET IsSticky = ? WHERE Id = ?", isSticky, folderId);
|
|
|
|
public async Task ChangeFolderHiddenStatusAsync(Guid folderId, bool isHidden)
|
|
{
|
|
await Connection.ExecuteAsync("UPDATE MailItemFolder SET IsHidden = ? WHERE Id = ?", isHidden, folderId);
|
|
|
|
var folder = await GetFolderAsync(folderId).ConfigureAwait(false);
|
|
if (folder != null)
|
|
{
|
|
Messenger.Send(new AccountFolderConfigurationUpdated(folder.MailAccountId));
|
|
}
|
|
}
|
|
|
|
public async Task UpdateFolderOrdersAsync(Guid accountId, IReadOnlyList<Guid> orderedFolderIds)
|
|
{
|
|
if (orderedFolderIds == null || orderedFolderIds.Count == 0) return;
|
|
|
|
await Connection.RunInTransactionAsync(conn =>
|
|
{
|
|
for (int i = 0; i < orderedFolderIds.Count; i++)
|
|
{
|
|
conn.Execute("UPDATE MailItemFolder SET \"Order\" = ? WHERE Id = ? AND MailAccountId = ?",
|
|
i + 1, orderedFolderIds[i], accountId);
|
|
}
|
|
}).ConfigureAwait(false);
|
|
|
|
Messenger.Send(new AccountFolderConfigurationUpdated(accountId));
|
|
}
|
|
|
|
public async Task ResetFolderCustomizationAsync(Guid accountId)
|
|
{
|
|
await Connection.RunInTransactionAsync(conn =>
|
|
{
|
|
conn.Execute("UPDATE MailItemFolder SET \"Order\" = 0, IsHidden = 0 WHERE MailAccountId = ?", accountId);
|
|
|
|
// Restore system folder stickiness. Category-type folders are virtual stickies too.
|
|
conn.Execute(
|
|
"UPDATE MailItemFolder SET IsSticky = 1 WHERE MailAccountId = ? AND (IsSystemFolder = 1 OR SpecialFolderType = ?)",
|
|
accountId, (int)SpecialFolderType.Category);
|
|
}).ConfigureAwait(false);
|
|
|
|
Messenger.Send(new AccountFolderConfigurationUpdated(accountId));
|
|
}
|
|
|
|
private static int GetDefaultFolderOrder(MailItemFolder folder)
|
|
=> folder.SpecialFolderType == SpecialFolderType.Other
|
|
? int.MaxValue
|
|
: (int)folder.SpecialFolderType;
|
|
|
|
/// <summary>
|
|
/// Orders folders by user-set Order first (customized entries ahead of uncustomized ones),
|
|
/// then falls back to SpecialFolderType enum order for known special folders so defaults
|
|
/// like Inbox stay at the top, and finally to alphabetic folder name (culture-aware).
|
|
/// </summary>
|
|
private static IOrderedEnumerable<MailItemFolder> ApplyFolderSort(IEnumerable<MailItemFolder> folders)
|
|
=> folders
|
|
.OrderBy(a => a.Order == 0 ? 1 : 0)
|
|
.ThenBy(a => a.Order)
|
|
.ThenBy(GetDefaultFolderOrder)
|
|
.ThenBy(a => a.FolderName, StringComparer.CurrentCultureIgnoreCase)
|
|
.ThenBy(a => a.SpecialFolderType);
|
|
|
|
public async Task<int> GetFolderNotificationBadgeAsync(Guid folderId)
|
|
{
|
|
var folder = await GetFolderAsync(folderId);
|
|
|
|
if (folder == null || !folder.ShowUnreadCount) return default;
|
|
|
|
var account = await _accountService.GetAccountAsync(folder.MailAccountId);
|
|
|
|
if (account == null) return default;
|
|
|
|
// Convert to raw SQL
|
|
string sqlQuery;
|
|
object[] parameters;
|
|
|
|
if (account.Preferences.IsFocusedInboxEnabled.GetValueOrDefault() && folder.SpecialFolderType == SpecialFolderType.Inbox)
|
|
{
|
|
if (folder.SpecialFolderType != SpecialFolderType.Draft && folder.SpecialFolderType != SpecialFolderType.Junk)
|
|
{
|
|
sqlQuery = "SELECT COUNT(*) FROM MailCopy WHERE FolderId = ? AND IsFocused = ? AND IsRead = ?";
|
|
parameters = new object[] { folderId, 1, 0 };
|
|
}
|
|
else
|
|
{
|
|
sqlQuery = "SELECT COUNT(*) FROM MailCopy WHERE FolderId = ? AND IsFocused = ?";
|
|
parameters = new object[] { folderId, 1 };
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (folder.SpecialFolderType != SpecialFolderType.Draft && folder.SpecialFolderType != SpecialFolderType.Junk)
|
|
{
|
|
sqlQuery = "SELECT COUNT(*) FROM MailCopy WHERE FolderId = ? AND IsRead = ?";
|
|
parameters = new object[] { folderId, 0 };
|
|
}
|
|
else
|
|
{
|
|
sqlQuery = "SELECT COUNT(*) FROM MailCopy WHERE FolderId = ?";
|
|
parameters = new object[] { folderId };
|
|
}
|
|
}
|
|
|
|
return await Connection.ExecuteScalarAsync<int>(sqlQuery, parameters);
|
|
}
|
|
|
|
public async Task<AccountFolderTree> GetFolderStructureForAccountAsync(Guid accountId, bool includeHiddenFolders)
|
|
{
|
|
var account = await _accountService.GetAccountAsync(accountId);
|
|
|
|
if (account == null)
|
|
throw new ArgumentException(nameof(account));
|
|
|
|
var accountTree = new AccountFolderTree(account);
|
|
|
|
// Account folders.
|
|
var folderQuery = Connection.Table<MailItemFolder>().Where(a => a.MailAccountId == accountId);
|
|
|
|
if (!includeHiddenFolders)
|
|
folderQuery = folderQuery.Where(a => !a.IsHidden);
|
|
|
|
// Load child folders for each folder, applying user-defined ordering with
|
|
// alphabetic fallback for folders the user hasn't explicitly re-ordered.
|
|
var rawFolders = await folderQuery.ToListAsync();
|
|
var allFolders = ApplyFolderSort(rawFolders).ToList();
|
|
|
|
if (allFolders.Any())
|
|
{
|
|
// 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);
|
|
|
|
foreach (var stickyFolder in stickyFolders)
|
|
{
|
|
var childStructure = await GetChildFolderItemsRecursiveAsync(stickyFolder.Id, accountId);
|
|
|
|
accountTree.Folders.Add(childStructure);
|
|
}
|
|
|
|
// Check whether we need special 'Categories' kind of folder.
|
|
var categoryExists = allFolders.Any(a => a.SpecialFolderType == SpecialFolderType.Category);
|
|
|
|
if (categoryExists)
|
|
{
|
|
var categoryFolder = allFolders.First(a => a.SpecialFolderType == SpecialFolderType.Category);
|
|
|
|
// Construct category items under pinned items.
|
|
var categoryFolders = allFolders.Where(a => gmailCategoryFolderTypes.Contains(a.SpecialFolderType));
|
|
|
|
foreach (var categoryFolderSubItem in categoryFolders)
|
|
{
|
|
categoryFolder.ChildFolders.Add(categoryFolderSubItem);
|
|
}
|
|
|
|
accountTree.Folders.Add(categoryFolder);
|
|
allFolders.Remove(categoryFolder);
|
|
}
|
|
|
|
// Move rest of the items into virtual More folder if any.
|
|
var nonStickyFolders = allFolders.Except(stickyFolders);
|
|
|
|
if (nonStickyFolders.Any())
|
|
{
|
|
var virtualMoreFolder = new MailItemFolder()
|
|
{
|
|
FolderName = Translator.More,
|
|
SpecialFolderType = SpecialFolderType.More
|
|
};
|
|
|
|
foreach (var unstickyItem in nonStickyFolders)
|
|
{
|
|
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);
|
|
}
|
|
|
|
// Only add more if there are any.
|
|
if (virtualMoreFolder.ChildFolders.Count > 0)
|
|
accountTree.Folders.Add(virtualMoreFolder);
|
|
}
|
|
}
|
|
|
|
return accountTree;
|
|
}
|
|
|
|
|
|
public Task<IEnumerable<IMenuItem>> GetAccountFoldersForDisplayAsync(IAccountMenuItem accountMenuItem)
|
|
{
|
|
if (accountMenuItem is IMergedAccountMenuItem mergedAccountFolderMenuItem)
|
|
{
|
|
return GetMergedAccountFolderMenuItemsAsync(mergedAccountFolderMenuItem);
|
|
}
|
|
else
|
|
{
|
|
return GetSingleAccountFolderMenuItemsAsync(accountMenuItem);
|
|
}
|
|
}
|
|
|
|
private async Task<FolderMenuItem> GetPreparedFolderMenuItemRecursiveAsync(MailAccount account, MailItemFolder parentFolder, IMenuItem parentMenuItem)
|
|
{
|
|
// Localize category folder name.
|
|
if (parentFolder.SpecialFolderType == SpecialFolderType.Category) parentFolder.FolderName = Translator.CategoriesFolderNameOverride;
|
|
|
|
const string query = "SELECT * FROM MailItemFolder WHERE ParentRemoteFolderId = ? AND MailAccountId = ?";
|
|
var preparedFolder = new FolderMenuItem(parentFolder, account, parentMenuItem);
|
|
|
|
var childFolders = await Connection.QueryAsync<MailItemFolder>(query, parentFolder.RemoteFolderId, parentFolder.MailAccountId).ConfigureAwait(false);
|
|
|
|
if (childFolders.Any())
|
|
{
|
|
foreach (var subChildFolder in childFolders)
|
|
{
|
|
var preparedChild = await GetPreparedFolderMenuItemRecursiveAsync(account, subChildFolder, preparedFolder);
|
|
|
|
if (preparedChild == null) continue;
|
|
|
|
preparedFolder.SubMenuItems.Add(preparedChild);
|
|
}
|
|
}
|
|
|
|
return preparedFolder;
|
|
}
|
|
|
|
private async Task<IEnumerable<IMenuItem>> GetSingleAccountFolderMenuItemsAsync(IAccountMenuItem accountMenuItem)
|
|
{
|
|
var accountId = accountMenuItem.EntityId.Value;
|
|
var preparedFolderMenuItems = new List<IMenuItem>();
|
|
|
|
// Get all folders for the account. Excluding hidden folders.
|
|
var folders = await GetVisibleFoldersAsync(accountId).ConfigureAwait(false);
|
|
|
|
if (!folders.Any()) return new List<IMenuItem>();
|
|
|
|
var mailAccount = accountMenuItem.HoldingAccounts.First();
|
|
|
|
var listingFolders = ApplyFolderSort(folders);
|
|
|
|
var moreFolder = MailItemFolder.CreateMoreFolder();
|
|
var categoryFolder = MailItemFolder.CreateCategoriesFolder();
|
|
|
|
var moreFolderMenuItem = new FolderMenuItem(moreFolder, mailAccount, accountMenuItem);
|
|
var categoryFolderMenuItem = new FolderMenuItem(categoryFolder, mailAccount, accountMenuItem);
|
|
|
|
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;
|
|
|
|
bool skipEmptyParentRemoteFolders = mailAccount.ProviderType == MailProviderType.Gmail;
|
|
|
|
if (skipEmptyParentRemoteFolders && !string.IsNullOrEmpty(item.ParentRemoteFolderId)) continue;
|
|
|
|
// 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;
|
|
|
|
var preparedItem = await GetPreparedFolderMenuItemRecursiveAsync(mailAccount, item, parentFolderMenuItem).ConfigureAwait(false);
|
|
|
|
// 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.
|
|
|
|
if (preparedItem == null) continue;
|
|
|
|
if (item.IsSticky)
|
|
{
|
|
preparedFolderMenuItems.Add(preparedItem);
|
|
}
|
|
else if (parentFolderMenuItem is FolderMenuItem baseParentFolderMenuItem)
|
|
{
|
|
baseParentFolderMenuItem.SubMenuItems.Add(preparedItem);
|
|
}
|
|
}
|
|
|
|
var favoriteCategories = await GetFavoriteCategoryMenuItemsAsync(mailAccount, folders, accountMenuItem).ConfigureAwait(false);
|
|
preparedFolderMenuItems.AddRange(favoriteCategories);
|
|
|
|
// Only add category folder if it's Gmail.
|
|
if (mailAccount.ProviderType == MailProviderType.Gmail) preparedFolderMenuItems.Add(categoryFolderMenuItem);
|
|
|
|
// Only add More folder if there are any items in it.
|
|
if (moreFolderMenuItem.SubMenuItems.Any()) preparedFolderMenuItems.Add(moreFolderMenuItem);
|
|
|
|
return preparedFolderMenuItems;
|
|
}
|
|
|
|
private async Task<IEnumerable<IMenuItem>> GetMergedAccountFolderMenuItemsAsync(IMergedAccountMenuItem mergedAccountFolderMenuItem)
|
|
{
|
|
var holdingAccounts = mergedAccountFolderMenuItem.HoldingAccounts;
|
|
|
|
if (holdingAccounts == null || !holdingAccounts.Any()) return [];
|
|
|
|
var preparedFolderMenuItems = new List<IMenuItem>();
|
|
|
|
// First gather all account folders.
|
|
// Prepare single menu items for both of them.
|
|
|
|
var allAccountFolders = new List<List<MailItemFolder>>();
|
|
|
|
foreach (var account in holdingAccounts)
|
|
{
|
|
var accountFolders = await GetVisibleFoldersAsync(account.Id).ConfigureAwait(false);
|
|
|
|
allAccountFolders.Add(accountFolders);
|
|
}
|
|
|
|
var commonFolders = FindCommonFolders(allAccountFolders);
|
|
|
|
// 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);
|
|
|
|
preparedFolderMenuItems.Add(menuItem);
|
|
}
|
|
|
|
var favoriteCategories = await GetMergedFavoriteCategoryMenuItemsAsync(holdingAccounts, allAccountFolders, mergedAccountFolderMenuItem.Parameter).ConfigureAwait(false);
|
|
preparedFolderMenuItems.AddRange(favoriteCategories);
|
|
|
|
return preparedFolderMenuItems;
|
|
}
|
|
|
|
private async Task<IEnumerable<IMenuItem>> GetFavoriteCategoryMenuItemsAsync(MailAccount account, IEnumerable<IMailItemFolder> handlingFolders, IMenuItem parentMenuItem)
|
|
{
|
|
var favoriteCategories = await _mailCategoryService.GetFavoriteCategoriesAsync(account.Id).ConfigureAwait(false);
|
|
|
|
if (!favoriteCategories.Any())
|
|
return [];
|
|
|
|
var availableFolders = handlingFolders
|
|
.Where(a => a.IsMoveTarget)
|
|
.Cast<IMailItemFolder>()
|
|
.ToList();
|
|
|
|
return favoriteCategories
|
|
.Select(category => (IMenuItem)new MailCategoryMenuItem(category, account, availableFolders, parentMenuItem))
|
|
.ToList();
|
|
}
|
|
|
|
private async Task<IEnumerable<IMenuItem>> GetMergedFavoriteCategoryMenuItemsAsync(IEnumerable<MailAccount> holdingAccounts, IEnumerable<IEnumerable<MailItemFolder>> allAccountFolders, MergedInbox mergedInbox)
|
|
{
|
|
var categoriesByAccount = new List<(MailAccount Account, List<MailCategory> Categories)>();
|
|
|
|
foreach (var account in holdingAccounts)
|
|
{
|
|
var categories = await _mailCategoryService.GetFavoriteCategoriesAsync(account.Id).ConfigureAwait(false);
|
|
if (categories.Any())
|
|
{
|
|
categoriesByAccount.Add((account, categories));
|
|
}
|
|
}
|
|
|
|
if (!categoriesByAccount.Any())
|
|
return [];
|
|
|
|
var handlingFolders = allAccountFolders
|
|
.SelectMany(a => a)
|
|
.Where(a => a.IsMoveTarget)
|
|
.Cast<IMailItemFolder>()
|
|
.ToList();
|
|
|
|
return categoriesByAccount
|
|
.SelectMany(a => a.Categories)
|
|
.GroupBy(a => NormalizeCategoryName(a.Name), StringComparer.OrdinalIgnoreCase)
|
|
.Select(group => (IMenuItem)new MergedMailCategoryMenuItem(group.ToList(), handlingFolders, mergedInbox))
|
|
.OrderBy(item => ((MergedMailCategoryMenuItem)item).FolderName, StringComparer.CurrentCultureIgnoreCase)
|
|
.ToList();
|
|
}
|
|
|
|
private static string NormalizeCategoryName(string name)
|
|
=> name?.Trim() ?? string.Empty;
|
|
|
|
private HashSet<SpecialFolderType> FindCommonFolders(List<List<MailItemFolder>> lists)
|
|
{
|
|
var allSpecialTypesExceptOther = Enum.GetValues<SpecialFolderType>().Cast<SpecialFolderType>().Where(a => a != SpecialFolderType.Other).ToList();
|
|
|
|
// Start with all special folder types from the first list
|
|
var commonSpecialFolderTypes = new HashSet<SpecialFolderType>(allSpecialTypesExceptOther);
|
|
|
|
// Intersect with special folder types from all lists
|
|
foreach (var list in lists)
|
|
{
|
|
commonSpecialFolderTypes.IntersectWith(list.Select(f => f.SpecialFolderType));
|
|
}
|
|
|
|
return commonSpecialFolderTypes;
|
|
}
|
|
|
|
private async Task<MailItemFolder> GetChildFolderItemsRecursiveAsync(Guid folderId, Guid accountId)
|
|
{
|
|
var folder = await Connection.Table<MailItemFolder>().Where(a => a.Id == folderId && a.MailAccountId == accountId).FirstOrDefaultAsync();
|
|
|
|
if (folder == null)
|
|
return null;
|
|
|
|
var childFoldersRaw = await Connection.Table<MailItemFolder>()
|
|
.Where(a => a.ParentRemoteFolderId == folder.RemoteFolderId && a.MailAccountId == folder.MailAccountId)
|
|
.ToListAsync();
|
|
|
|
var childFolders = ApplyFolderSort(childFoldersRaw).ToList();
|
|
|
|
foreach (var childFolder in childFolders)
|
|
{
|
|
var subChild = await GetChildFolderItemsRecursiveAsync(childFolder.Id, accountId);
|
|
folder.ChildFolders.Add(subChild);
|
|
}
|
|
|
|
return folder;
|
|
}
|
|
|
|
public async Task<MailItemFolder> GetSpecialFolderByAccountIdAsync(Guid accountId, SpecialFolderType type)
|
|
=> await Connection.Table<MailItemFolder>().FirstOrDefaultAsync(a => a.MailAccountId == accountId && a.SpecialFolderType == type);
|
|
|
|
public async Task<MailItemFolder> GetFolderAsync(Guid folderId)
|
|
=> await Connection.Table<MailItemFolder>().FirstOrDefaultAsync(a => a.Id.Equals(folderId));
|
|
|
|
public Task<int> GetCurrentItemCountForFolder(Guid folderId)
|
|
=> Connection.Table<MailCopy>().Where(a => a.FolderId == folderId).CountAsync();
|
|
|
|
public async Task<List<MailItemFolder>> GetFoldersAsync(Guid accountId)
|
|
{
|
|
// Ordering is applied in managed code so that StringComparer.CurrentCultureIgnoreCase
|
|
// is honored. SQLite's default ORDER BY is not culture-aware.
|
|
const string query = "SELECT * FROM MailItemFolder WHERE MailAccountId = ?";
|
|
var rows = await Connection.QueryAsync<MailItemFolder>(query, accountId).ConfigureAwait(false);
|
|
return ApplyFolderSort(rows).ToList();
|
|
}
|
|
|
|
public async Task<List<MailItemFolder>> GetVisibleFoldersAsync(Guid accountId)
|
|
{
|
|
const string query = "SELECT * FROM MailItemFolder WHERE MailAccountId = ? AND IsHidden = ?";
|
|
var rows = await Connection.QueryAsync<MailItemFolder>(query, accountId, 0).ConfigureAwait(false);
|
|
return ApplyFolderSort(rows).ToList();
|
|
}
|
|
|
|
public async Task<IList<uint>> GetKnownUidsForFolderAsync(Guid folderId)
|
|
{
|
|
var mailCopyIds = await GetMailCopyIdsByFolderIdAsync(folderId);
|
|
|
|
// Make sure we don't include Ids that doesn't have uid separator.
|
|
// Local drafts might not have it for example.
|
|
|
|
return new List<uint>(mailCopyIds
|
|
.Where(a => a.Contains(MailkitClientExtensions.MailCopyUidSeparator))
|
|
.Select(a => MailkitClientExtensions.ResolveUid(a)));
|
|
}
|
|
|
|
public async Task<MailAccount> UpdateSystemFolderConfigurationAsync(Guid accountId, SystemFolderConfiguration configuration)
|
|
{
|
|
if (configuration == null)
|
|
throw new ArgumentNullException(nameof(configuration));
|
|
|
|
// Update system folders for this account.
|
|
|
|
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));
|
|
|
|
|
|
return await _accountService.GetAccountAsync(accountId).ConfigureAwait(false);
|
|
}
|
|
|
|
private Task UpdateSystemFolderInternalAsync(MailItemFolder folder, SpecialFolderType assignedSpecialFolderType)
|
|
{
|
|
if (folder == null) return Task.CompletedTask;
|
|
|
|
folder.IsSticky = true;
|
|
folder.IsSynchronizationEnabled = true;
|
|
folder.IsSystemFolder = true;
|
|
folder.SpecialFolderType = assignedSpecialFolderType;
|
|
|
|
return UpdateFolderAsync(folder);
|
|
}
|
|
|
|
public async Task ChangeFolderSynchronizationStateAsync(Guid folderId, bool isSynchronizationEnabled)
|
|
{
|
|
var localFolder = await Connection.Table<MailItemFolder>().FirstOrDefaultAsync(a => a.Id == folderId);
|
|
|
|
if (localFolder != null)
|
|
{
|
|
localFolder.IsSynchronizationEnabled = isSynchronizationEnabled;
|
|
|
|
await UpdateFolderAsync(localFolder).ConfigureAwait(false);
|
|
|
|
Messenger.Send(new FolderSynchronizationEnabled(localFolder));
|
|
}
|
|
}
|
|
|
|
#region Repository Calls
|
|
|
|
public async Task InsertFolderAsync(MailItemFolder folder)
|
|
{
|
|
if (folder == null)
|
|
{
|
|
_logger.Warning("Folder is null. Cannot insert.");
|
|
|
|
return;
|
|
}
|
|
|
|
var account = await _accountService.GetAccountAsync(folder.MailAccountId);
|
|
|
|
if (account == null)
|
|
{
|
|
_logger.Warning("Account with id {MailAccountId} does not exist. Cannot insert folder.", folder.MailAccountId);
|
|
|
|
return;
|
|
}
|
|
|
|
var existingFolder = await GetFolderAsync(folder.Id).ConfigureAwait(false);
|
|
|
|
// 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.
|
|
|
|
existingFolder ??= await GetFolderAsync(folder.MailAccountId, folder.RemoteFolderId).ConfigureAwait(false);
|
|
|
|
if (existingFolder == null)
|
|
{
|
|
_logger.Debug("Inserting folder {Id} - {FolderName}", folder.Id, folder.FolderName, folder.MailAccountId);
|
|
|
|
await Connection.InsertAsync(folder, typeof(MailItemFolder)).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.
|
|
|
|
folder.Id = existingFolder.Id;
|
|
folder.IsSticky = existingFolder.IsSticky;
|
|
folder.SpecialFolderType = existingFolder.SpecialFolderType;
|
|
folder.ShowUnreadCount = existingFolder.ShowUnreadCount;
|
|
folder.TextColorHex = existingFolder.TextColorHex;
|
|
folder.BackgroundColorHex = existingFolder.BackgroundColorHex;
|
|
folder.Order = existingFolder.Order;
|
|
folder.IsHidden = existingFolder.IsHidden;
|
|
|
|
_logger.Debug("Folder {Id} - {FolderName} already exists. Updating.", folder.Id, folder.FolderName);
|
|
|
|
await UpdateFolderAsync(folder).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
public async Task UpdateFolderAsync(MailItemFolder folder)
|
|
{
|
|
if (folder == null)
|
|
{
|
|
_logger.Warning("Folder is null. Cannot update.");
|
|
|
|
return;
|
|
}
|
|
|
|
_logger.Debug("Updating folder {FolderName}", folder.Id, folder.FolderName);
|
|
|
|
await Connection.UpdateAsync(folder, typeof(MailItemFolder)).ConfigureAwait(false);
|
|
}
|
|
|
|
public Task UpdateFolderHighestModeSeqAsync(Guid folderId, long highestModeSeq)
|
|
=> Connection.ExecuteAsync("UPDATE MailItemFolder SET HighestModeSeq = ? WHERE Id = ?", highestModeSeq, folderId);
|
|
|
|
private async Task DeleteFolderAsync(MailItemFolder folder)
|
|
{
|
|
if (folder == null)
|
|
{
|
|
_logger.Warning("Folder is null. Cannot delete.");
|
|
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
_logger.Debug("Deleting folder {FolderName}", folder.FolderName);
|
|
|
|
await Connection.DeleteAsync<MailItemFolder>(folder.Id).ConfigureAwait(false);
|
|
|
|
// Delete all existing mails from this folder.
|
|
await Connection.ExecuteAsync("DELETE FROM MailCopy WHERE FolderId = ?", folder.Id);
|
|
|
|
// TODO: Delete MIME messages from the disk.
|
|
}
|
|
|
|
#endregion
|
|
|
|
private Task<List<string>> GetMailCopyIdsByFolderIdAsync(Guid folderId)
|
|
{
|
|
const string query = "SELECT Id FROM MailCopy WHERE FolderId = ?";
|
|
return Connection.QueryScalarsAsync<string>(query, folderId);
|
|
}
|
|
|
|
public async Task<List<MailFolderPairMetadata>> GetMailFolderPairMetadatasAsync(IEnumerable<string> mailCopyIds)
|
|
{
|
|
var mailCopyIdList = mailCopyIds.ToList();
|
|
var placeholders = string.Join(",", mailCopyIdList.Select(_ => "?"));
|
|
var query = $"SELECT DISTINCT MailCopy.Id as MailCopyId, MailItemFolder.Id as FolderId, MailItemFolder.RemoteFolderId as RemoteFolderId FROM MailCopy INNER JOIN MailItemFolder ON MailCopy.FolderId = MailItemFolder.Id WHERE MailCopy.Id IN ({placeholders})";
|
|
var parameters = mailCopyIdList.Cast<object>().ToArray();
|
|
|
|
return await Connection.QueryAsync<MailFolderPairMetadata>(query, parameters);
|
|
}
|
|
|
|
public Task<List<MailFolderPairMetadata>> GetMailFolderPairMetadatasAsync(string mailCopyId)
|
|
=> GetMailFolderPairMetadatasAsync(new List<string>() { mailCopyId });
|
|
|
|
public async Task<List<MailItemFolder>> GetSynchronizationFoldersAsync(MailSynchronizationOptions options)
|
|
{
|
|
var folders = new List<MailItemFolder>();
|
|
|
|
if (options.Type == MailSynchronizationType.IMAPIdle)
|
|
{
|
|
// 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);
|
|
}
|
|
}
|
|
else if (options.Type == MailSynchronizationType.FullFolders)
|
|
{
|
|
// Only get sync enabled folders.
|
|
|
|
var synchronizationFolders = await Connection.Table<MailItemFolder>()
|
|
.Where(a => a.MailAccountId == options.AccountId && a.IsSynchronizationEnabled)
|
|
.OrderBy(a => a.SpecialFolderType)
|
|
.ToListAsync();
|
|
|
|
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)
|
|
{
|
|
return mustHaveFolders;
|
|
}
|
|
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;
|
|
}
|
|
|
|
// 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.
|
|
|
|
var orderedCustomFolders = synchronizationFolders.OrderBy(a => options.SynchronizationFolderIds.IndexOf(a.Id));
|
|
|
|
foreach (var item in orderedCustomFolders)
|
|
{
|
|
if (!mustHaveFolders.Any(a => a.Id == item.Id))
|
|
{
|
|
mustHaveFolders.Add(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
return mustHaveFolders;
|
|
}
|
|
|
|
return folders;
|
|
}
|
|
|
|
private async Task<List<MailItemFolder>> GetInboxSynchronizationFoldersAsync(Guid accountId)
|
|
{
|
|
var folders = new List<MailItemFolder>();
|
|
|
|
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);
|
|
|
|
if (deletedFolder != null)
|
|
{
|
|
folders.Add(deletedFolder);
|
|
}
|
|
|
|
if (inboxFolder != null)
|
|
{
|
|
folders.Add(inboxFolder);
|
|
}
|
|
|
|
// For properly creating threads we need Sent and Draft to be synchronized as well.
|
|
|
|
if (sentFolder != null)
|
|
{
|
|
folders.Add(sentFolder);
|
|
}
|
|
|
|
if (draftFolder != null)
|
|
{
|
|
folders.Add(draftFolder);
|
|
}
|
|
|
|
return folders;
|
|
}
|
|
|
|
public Task<MailItemFolder> GetFolderAsync(Guid accountId, string remoteFolderId)
|
|
=> Connection.Table<MailItemFolder>().FirstOrDefaultAsync(a => a.MailAccountId == accountId && a.RemoteFolderId == remoteFolderId);
|
|
|
|
public async Task DeleteFolderAsync(Guid accountId, string remoteFolderId)
|
|
{
|
|
var folder = await GetFolderAsync(accountId, remoteFolderId);
|
|
|
|
if (folder == null)
|
|
{
|
|
_logger.Warning("Folder with id {RemoteFolderId} does not exist. Delete folder canceled.", remoteFolderId);
|
|
|
|
return;
|
|
}
|
|
|
|
await DeleteFolderAsync(folder).ConfigureAwait(false);
|
|
}
|
|
|
|
public async Task ChangeFolderShowUnreadCountStateAsync(Guid folderId, bool showUnreadCount)
|
|
{
|
|
var localFolder = await GetFolderAsync(folderId);
|
|
|
|
if (localFolder != null)
|
|
{
|
|
localFolder.ShowUnreadCount = showUnreadCount;
|
|
|
|
await UpdateFolderAsync(localFolder).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
public async Task<bool> IsInboxAvailableForAccountAsync(Guid accountId)
|
|
=> await Connection.Table<MailItemFolder>()
|
|
.Where(a => a.SpecialFolderType == SpecialFolderType.Inbox && a.MailAccountId == accountId)
|
|
.CountAsync() == 1;
|
|
|
|
public Task UpdateFolderLastSyncDateAsync(Guid folderId)
|
|
=> Connection.ExecuteAsync("UPDATE MailItemFolder SET LastSynchronizedDate = ? WHERE Id = ?", DateTime.UtcNow, folderId);
|
|
|
|
public Task<List<UnreadItemCountResult>> GetUnreadItemCountResultsAsync(IEnumerable<Guid> accountIds)
|
|
{
|
|
var accountIdList = accountIds.ToList();
|
|
var placeholders = string.Join(",", accountIdList.Select(_ => "?"));
|
|
var query = $"SELECT MailItemFolder.Id as FolderId, MailItemFolder.SpecialFolderType as SpecialFolderType, count(DISTINCT MailCopy.Id) as UnreadItemCount, MailItemFolder.MailAccountId as AccountId FROM MailCopy INNER JOIN MailItemFolder ON MailCopy.FolderId = MailItemFolder.Id WHERE MailItemFolder.MailAccountId IN ({placeholders}) AND MailCopy.IsRead = ? AND MailItemFolder.ShowUnreadCount = ? GROUP BY MailItemFolder.Id";
|
|
var parameters = accountIdList.Cast<object>().Concat(new object[] { 0, 1 }).ToArray();
|
|
|
|
return Connection.QueryAsync<UnreadItemCountResult>(query, parameters);
|
|
}
|
|
}
|