Merge branch 'codex/mail-categories-v1'

This commit is contained in:
Burak Kaan Köse
2026-04-15 01:18:12 +02:00
61 changed files with 2171 additions and 75 deletions
+8
View File
@@ -49,6 +49,8 @@ public class DatabaseService : IDatabaseService
{
await Task.WhenAll(
Connection.CreateTableAsync<MailCopy>(),
Connection.CreateTableAsync<MailCategory>(),
Connection.CreateTableAsync<MailCategoryAssignment>(),
Connection.CreateTableAsync<MailItemFolder>(),
Connection.CreateTableAsync<MailAccount>(),
Connection.CreateTableAsync<AccountContact>(),
@@ -226,6 +228,12 @@ SET {nameof(KeyboardShortcut.Action)} =
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCopy_MessageId ON MailCopy(MessageId)").ConfigureAwait(false);
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCopy_FolderId_IsRead ON MailCopy(FolderId, IsRead)").ConfigureAwait(false);
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCopy_CreationDate ON MailCopy(CreationDate)").ConfigureAwait(false);
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCategory_MailAccountId ON MailCategory(MailAccountId)").ConfigureAwait(false);
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCategory_MailAccountId_Name ON MailCategory(MailAccountId, Name)").ConfigureAwait(false);
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCategory_MailAccountId_IsFavorite ON MailCategory(MailAccountId, IsFavorite)").ConfigureAwait(false);
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCategoryAssignment_MailCategoryId ON MailCategoryAssignment(MailCategoryId)").ConfigureAwait(false);
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailCategoryAssignment_MailCopyUniqueId ON MailCategoryAssignment(MailCopyUniqueId)").ConfigureAwait(false);
await Connection.ExecuteAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_MailCategoryAssignment_Category_MailCopy ON MailCategoryAssignment(MailCategoryId, MailCopyUniqueId)").ConfigureAwait(false);
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailItemFolder_MailAccountId ON MailItemFolder(MailAccountId)").ConfigureAwait(false);
await Connection.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_MailItemFolder_MailAccountId_RemoteFolderId ON MailItemFolder(MailAccountId, RemoteFolderId)").ConfigureAwait(false);
+60 -1
View File
@@ -22,6 +22,7 @@ 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 =
@@ -34,9 +35,11 @@ public class FolderService : BaseDatabaseService, IFolderService
];
public FolderService(IDatabaseService databaseService,
IAccountService accountService) : base(databaseService)
IAccountService accountService,
IMailCategoryService mailCategoryService) : base(databaseService)
{
_accountService = accountService;
_mailCategoryService = mailCategoryService;
}
public async Task ChangeStickyStatusAsync(Guid folderId, bool isSticky)
@@ -269,6 +272,9 @@ public class FolderService : BaseDatabaseService, IFolderService
}
}
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);
@@ -309,9 +315,62 @@ public class FolderService : BaseDatabaseService, IFolderService
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();
+358
View File
@@ -0,0 +1,358 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Messaging.Client.Accounts;
using Wino.Messaging.UI;
namespace Wino.Services;
public class MailCategoryService : BaseDatabaseService, IMailCategoryService
{
public MailCategoryService(IDatabaseService databaseService) : base(databaseService)
{
}
public Task<List<MailCategory>> GetCategoriesAsync(Guid accountId)
=> Connection.QueryAsync<MailCategory>(
$"SELECT * FROM {nameof(MailCategory)} WHERE {nameof(MailCategory.MailAccountId)} = ? ORDER BY {nameof(MailCategory.IsFavorite)} DESC, {nameof(MailCategory.Name)} COLLATE NOCASE",
accountId);
public Task<List<MailCategory>> GetFavoriteCategoriesAsync(Guid accountId)
=> Connection.QueryAsync<MailCategory>(
$"SELECT * FROM {nameof(MailCategory)} WHERE {nameof(MailCategory.MailAccountId)} = ? AND {nameof(MailCategory.IsFavorite)} = 1 ORDER BY {nameof(MailCategory.Name)} COLLATE NOCASE",
accountId);
public Task<MailCategory> GetCategoryAsync(Guid categoryId)
=> Connection.FindAsync<MailCategory>(categoryId);
public async Task<bool> CategoryNameExistsAsync(Guid accountId, string name, Guid? excludedCategoryId = null)
{
var normalizedName = NormalizeCategoryName(name);
if (string.IsNullOrWhiteSpace(normalizedName))
return false;
var sql = $"SELECT COUNT(*) FROM {nameof(MailCategory)} WHERE {nameof(MailCategory.MailAccountId)} = ? AND lower(trim({nameof(MailCategory.Name)})) = ?";
var parameters = new List<object> { accountId, normalizedName.ToLowerInvariant() };
if (excludedCategoryId.HasValue)
{
sql += $" AND {nameof(MailCategory.Id)} <> ?";
parameters.Add(excludedCategoryId.Value);
}
return await Connection.ExecuteScalarAsync<int>(sql, parameters.ToArray()).ConfigureAwait(false) > 0;
}
public async Task<MailCategory> CreateCategoryAsync(MailCategory category)
{
category.Id = category.Id == Guid.Empty ? Guid.NewGuid() : category.Id;
category.Name = NormalizeCategoryName(category.Name);
await Connection.InsertAsync(category, typeof(MailCategory)).ConfigureAwait(false);
NotifyCategoryStructureChanged(category.MailAccountId);
return category;
}
public async Task UpdateCategoryAsync(MailCategory category)
{
category.Name = NormalizeCategoryName(category.Name);
await Connection.UpdateAsync(category, typeof(MailCategory)).ConfigureAwait(false);
NotifyCategoryStructureChanged(category.MailAccountId);
}
public async Task DeleteCategoryAsync(Guid categoryId)
{
var category = await GetCategoryAsync(categoryId).ConfigureAwait(false);
if (category == null)
return;
await Connection.ExecuteAsync($"DELETE FROM {nameof(MailCategoryAssignment)} WHERE {nameof(MailCategoryAssignment.MailCategoryId)} = ?", categoryId).ConfigureAwait(false);
await Connection.DeleteAsync<MailCategory>(categoryId).ConfigureAwait(false);
NotifyCategoryStructureChanged(category.MailAccountId);
}
public async Task DeleteCategoriesAsync(Guid accountId)
{
var categories = await GetCategoriesAsync(accountId).ConfigureAwait(false);
if (categories.Count == 0)
return;
var categoryIds = categories.Select(a => a.Id).ToList();
var placeholders = string.Join(",", categoryIds.Select(_ => "?"));
var deleteAssignmentsSql = $"DELETE FROM {nameof(MailCategoryAssignment)} WHERE {nameof(MailCategoryAssignment.MailCategoryId)} IN ({placeholders})";
await Connection.ExecuteAsync(deleteAssignmentsSql, categoryIds.Cast<object>().ToArray()).ConfigureAwait(false);
await Connection.Table<MailCategory>().DeleteAsync(a => a.MailAccountId == accountId).ConfigureAwait(false);
NotifyCategoryStructureChanged(accountId);
}
public async Task ToggleFavoriteAsync(Guid categoryId, bool isFavorite)
{
var category = await GetCategoryAsync(categoryId).ConfigureAwait(false);
if (category == null || category.IsFavorite == isFavorite)
return;
category.IsFavorite = isFavorite;
await Connection.UpdateAsync(category, typeof(MailCategory)).ConfigureAwait(false);
NotifyCategoryStructureChanged(category.MailAccountId);
}
public async Task UpdateRemoteIdAsync(Guid categoryId, string remoteId)
{
var category = await GetCategoryAsync(categoryId).ConfigureAwait(false);
if (category == null)
return;
category.RemoteId = remoteId;
await Connection.UpdateAsync(category, typeof(MailCategory)).ConfigureAwait(false);
}
public async Task ReplaceCategoriesAsync(Guid accountId, IEnumerable<MailCategory> categories)
{
var existingCategories = await GetCategoriesAsync(accountId).ConfigureAwait(false);
var existingByRemoteId = existingCategories
.Where(a => !string.IsNullOrWhiteSpace(a.RemoteId))
.ToDictionary(a => a.RemoteId, StringComparer.OrdinalIgnoreCase);
var existingByName = existingCategories
.GroupBy(a => NormalizeCategoryName(a.Name), StringComparer.OrdinalIgnoreCase)
.ToDictionary(a => a.Key, a => a.First(), StringComparer.OrdinalIgnoreCase);
var incomingCategories = categories?.ToList() ?? [];
var preservedIds = new HashSet<Guid>();
foreach (var incoming in incomingCategories)
{
incoming.MailAccountId = accountId;
incoming.Id = incoming.Id == Guid.Empty ? Guid.NewGuid() : incoming.Id;
incoming.Name = NormalizeCategoryName(incoming.Name);
MailCategory existing = null;
if (!string.IsNullOrWhiteSpace(incoming.RemoteId) && existingByRemoteId.TryGetValue(incoming.RemoteId, out var byRemote))
{
existing = byRemote;
}
else if (existingByName.TryGetValue(incoming.Name, out var byName))
{
existing = byName;
}
if (existing == null)
{
await Connection.InsertAsync(incoming, typeof(MailCategory)).ConfigureAwait(false);
preservedIds.Add(incoming.Id);
}
else
{
incoming.Id = existing.Id;
incoming.IsFavorite = existing.IsFavorite;
await Connection.UpdateAsync(incoming, typeof(MailCategory)).ConfigureAwait(false);
preservedIds.Add(existing.Id);
}
}
var categoryIdsToDelete = existingCategories
.Where(a => !preservedIds.Contains(a.Id))
.Select(a => a.Id)
.ToList();
if (categoryIdsToDelete.Count > 0)
{
var placeholders = string.Join(",", categoryIdsToDelete.Select(_ => "?"));
await Connection.ExecuteAsync(
$"DELETE FROM {nameof(MailCategoryAssignment)} WHERE {nameof(MailCategoryAssignment.MailCategoryId)} IN ({placeholders})",
categoryIdsToDelete.Cast<object>().ToArray()).ConfigureAwait(false);
foreach (var categoryId in categoryIdsToDelete)
{
await Connection.DeleteAsync<MailCategory>(categoryId).ConfigureAwait(false);
}
}
NotifyCategoryStructureChanged(accountId);
}
public async Task ReplaceMailAssignmentsAsync(Guid accountId, Guid mailCopyUniqueId, IEnumerable<string> categoryNames)
{
var normalizedNames = categoryNames?
.Select(NormalizeCategoryName)
.Where(a => !string.IsNullOrWhiteSpace(a))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList() ?? [];
var availableCategories = await GetCategoriesAsync(accountId).ConfigureAwait(false);
var categoryIds = availableCategories
.Where(a => normalizedNames.Contains(NormalizeCategoryName(a.Name), StringComparer.OrdinalIgnoreCase))
.Select(a => a.Id)
.ToHashSet();
var existingAssignments = await Connection.QueryAsync<MailCategoryAssignment>(
$"SELECT * FROM {nameof(MailCategoryAssignment)} WHERE {nameof(MailCategoryAssignment.MailCopyUniqueId)} = ?",
mailCopyUniqueId).ConfigureAwait(false);
var assignmentsToDelete = existingAssignments.Where(a => !categoryIds.Contains(a.MailCategoryId)).ToList();
var existingIds = existingAssignments.Select(a => a.MailCategoryId).ToHashSet();
var assignmentsToAdd = categoryIds.Where(a => !existingIds.Contains(a)).ToList();
foreach (var assignment in assignmentsToDelete)
{
await Connection.DeleteAsync<MailCategoryAssignment>(assignment.Id).ConfigureAwait(false);
}
foreach (var categoryId in assignmentsToAdd)
{
await Connection.InsertAsync(new MailCategoryAssignment
{
Id = Guid.NewGuid(),
MailCategoryId = categoryId,
MailCopyUniqueId = mailCopyUniqueId
}, typeof(MailCategoryAssignment)).ConfigureAwait(false);
}
WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(accountId));
}
public async Task AssignCategoryAsync(Guid categoryId, IEnumerable<Guid> mailCopyUniqueIds)
{
var uniqueIds = mailCopyUniqueIds?.Distinct().ToList() ?? [];
if (uniqueIds.Count == 0)
return;
var category = await GetCategoryAsync(categoryId).ConfigureAwait(false);
if (category == null)
return;
var placeholders = string.Join(",", uniqueIds.Select(_ => "?"));
var query = $"SELECT * FROM {nameof(MailCategoryAssignment)} WHERE {nameof(MailCategoryAssignment.MailCategoryId)} = ? AND {nameof(MailCategoryAssignment.MailCopyUniqueId)} IN ({placeholders})";
var existingAssignments = await Connection.QueryAsync<MailCategoryAssignment>(
query,
[categoryId, .. uniqueIds.Cast<object>()]).ConfigureAwait(false);
var existingUniqueIds = existingAssignments.Select(a => a.MailCopyUniqueId).ToHashSet();
foreach (var uniqueId in uniqueIds.Where(a => !existingUniqueIds.Contains(a)))
{
await Connection.InsertAsync(new MailCategoryAssignment
{
Id = Guid.NewGuid(),
MailCategoryId = categoryId,
MailCopyUniqueId = uniqueId
}, typeof(MailCategoryAssignment)).ConfigureAwait(false);
}
WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(category.MailAccountId));
}
public async Task UnassignCategoryAsync(Guid categoryId, IEnumerable<Guid> mailCopyUniqueIds)
{
var uniqueIds = mailCopyUniqueIds?.Distinct().ToList() ?? [];
if (uniqueIds.Count == 0)
return;
var category = await GetCategoryAsync(categoryId).ConfigureAwait(false);
if (category == null)
return;
var placeholders = string.Join(",", uniqueIds.Select(_ => "?"));
await Connection.ExecuteAsync(
$"DELETE FROM {nameof(MailCategoryAssignment)} WHERE {nameof(MailCategoryAssignment.MailCategoryId)} = ? AND {nameof(MailCategoryAssignment.MailCopyUniqueId)} IN ({placeholders})",
[categoryId, .. uniqueIds.Cast<object>()]).ConfigureAwait(false);
WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(category.MailAccountId));
}
public async Task<List<MailCategory>> GetCategoriesForMailAsync(Guid accountId, IEnumerable<Guid> mailCopyUniqueIds)
{
var uniqueIds = mailCopyUniqueIds?.Distinct().ToList() ?? [];
if (uniqueIds.Count == 0)
return [];
var placeholders = string.Join(",", uniqueIds.Select(_ => "?"));
var sql = $"SELECT DISTINCT MailCategory.* FROM {nameof(MailCategory)} " +
$"INNER JOIN {nameof(MailCategoryAssignment)} ON {nameof(MailCategory)}.{nameof(MailCategory.Id)} = {nameof(MailCategoryAssignment)}.{nameof(MailCategoryAssignment.MailCategoryId)} " +
$"WHERE {nameof(MailCategory)}.{nameof(MailCategory.MailAccountId)} = ? AND {nameof(MailCategoryAssignment)}.{nameof(MailCategoryAssignment.MailCopyUniqueId)} IN ({placeholders}) " +
$"ORDER BY {nameof(MailCategory.Name)} COLLATE NOCASE";
return await Connection.QueryAsync<MailCategory>(
sql,
[accountId, .. uniqueIds.Cast<object>()]).ConfigureAwait(false);
}
public async Task<List<Guid>> GetAssignedCategoryIdsForAllAsync(IEnumerable<Guid> mailCopyUniqueIds)
{
var uniqueIds = mailCopyUniqueIds?.Distinct().ToList() ?? [];
if (uniqueIds.Count == 0)
return [];
var placeholders = string.Join(",", uniqueIds.Select(_ => "?"));
var sql = $"SELECT {nameof(MailCategoryAssignment.MailCategoryId)} " +
$"FROM {nameof(MailCategoryAssignment)} " +
$"WHERE {nameof(MailCategoryAssignment.MailCopyUniqueId)} IN ({placeholders}) " +
$"GROUP BY {nameof(MailCategoryAssignment.MailCategoryId)} " +
$"HAVING COUNT(DISTINCT {nameof(MailCategoryAssignment.MailCopyUniqueId)}) = ?";
return await Connection.QueryScalarsAsync<Guid>(
sql,
[.. uniqueIds.Cast<object>(), uniqueIds.Count]).ConfigureAwait(false);
}
public async Task<List<string>> GetCategoryNamesForMailAsync(Guid mailCopyUniqueId)
{
var sql = $"SELECT {nameof(MailCategory.Name)} " +
$"FROM {nameof(MailCategory)} " +
$"INNER JOIN {nameof(MailCategoryAssignment)} ON {nameof(MailCategory)}.{nameof(MailCategory.Id)} = {nameof(MailCategoryAssignment.MailCategoryId)} " +
$"WHERE {nameof(MailCategoryAssignment.MailCopyUniqueId)} = ? " +
$"ORDER BY {nameof(MailCategory.Name)} COLLATE NOCASE";
return await Connection.QueryScalarsAsync<string>(sql, mailCopyUniqueId).ConfigureAwait(false);
}
public async Task<List<MailCopy>> GetMailCopiesForCategoryAsync(Guid categoryId)
{
var sql = $"SELECT {nameof(MailCopy)}.* " +
$"FROM {nameof(MailCopy)} " +
$"INNER JOIN {nameof(MailCategoryAssignment)} ON {nameof(MailCopy)}.{nameof(MailCopy.UniqueId)} = {nameof(MailCategoryAssignment.MailCopyUniqueId)} " +
$"WHERE {nameof(MailCategoryAssignment.MailCategoryId)} = ?";
return await Connection.QueryAsync<MailCopy>(sql, categoryId).ConfigureAwait(false);
}
public Task<List<UnreadCategoryCountResult>> GetUnreadCategoryCountResultsAsync(IEnumerable<Guid> accountIds)
{
var accountIdList = accountIds?.Distinct().ToList() ?? [];
if (accountIdList.Count == 0)
return Task.FromResult(new List<UnreadCategoryCountResult>());
var placeholders = string.Join(",", accountIdList.Select(_ => "?"));
var sql =
$"SELECT MailCategory.{nameof(MailCategory.Id)} as {nameof(UnreadCategoryCountResult.CategoryId)}, " +
$"MailCategory.{nameof(MailCategory.MailAccountId)} as {nameof(UnreadCategoryCountResult.AccountId)}, " +
$"COUNT(DISTINCT MailCopy.{nameof(MailCopy.UniqueId)}) as {nameof(UnreadCategoryCountResult.UnreadItemCount)} " +
$"FROM {nameof(MailCategory)} " +
$"INNER JOIN {nameof(MailCategoryAssignment)} ON {nameof(MailCategory)}.{nameof(MailCategory.Id)} = {nameof(MailCategoryAssignment)}.{nameof(MailCategoryAssignment.MailCategoryId)} " +
$"INNER JOIN {nameof(MailCopy)} ON {nameof(MailCategoryAssignment)}.{nameof(MailCategoryAssignment.MailCopyUniqueId)} = {nameof(MailCopy)}.{nameof(MailCopy.UniqueId)} " +
$"WHERE MailCategory.{nameof(MailCategory.MailAccountId)} IN ({placeholders}) AND MailCopy.{nameof(MailCopy.IsRead)} = 0 " +
$"GROUP BY MailCategory.{nameof(MailCategory.Id)}";
return Connection.QueryAsync<UnreadCategoryCountResult>(sql, accountIdList.Cast<object>().ToArray());
}
private void NotifyCategoryStructureChanged(Guid accountId)
{
WeakReferenceMessenger.Default.Send(new AccountsMenuRefreshRequested(false));
WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(accountId));
}
private static string NormalizeCategoryName(string name)
=> name?.Trim() ?? string.Empty;
}
+24 -4
View File
@@ -32,6 +32,7 @@ public class MailService : BaseDatabaseService, IMailService
private readonly IMimeFileService _mimeFileService;
private readonly IPreferencesService _preferencesService;
private readonly ISentMailReceiptService _sentMailReceiptService;
private readonly IMailCategoryService _mailCategoryService;
private readonly ILogger _logger = Log.ForContext<MailService>();
@@ -42,7 +43,8 @@ public class MailService : BaseDatabaseService, IMailService
ISignatureService signatureService,
IMimeFileService mimeFileService,
IPreferencesService preferencesService,
ISentMailReceiptService sentMailReceiptService) : base(databaseService)
ISentMailReceiptService sentMailReceiptService,
IMailCategoryService mailCategoryService) : base(databaseService)
{
_folderService = folderService;
_contactService = contactService;
@@ -51,6 +53,7 @@ public class MailService : BaseDatabaseService, IMailService
_mimeFileService = mimeFileService;
_preferencesService = preferencesService;
_sentMailReceiptService = sentMailReceiptService;
_mailCategoryService = mailCategoryService;
}
public async Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(Guid accountId, DraftCreationOptions draftCreationOptions)
@@ -171,7 +174,9 @@ public class MailService : BaseDatabaseService, IMailService
private static (string Query, object[] Parameters) BuildMailFetchQuery(MailListInitializationOptions options)
{
var sql = new StringBuilder();
sql.Append("SELECT MailCopy.* FROM MailCopy INNER JOIN MailItemFolder ON MailCopy.FolderId = MailItemFolder.Id");
sql.Append(options.IsCategoryView
? "SELECT DISTINCT MailCopy.* FROM MailCopy INNER JOIN MailItemFolder ON MailCopy.FolderId = MailItemFolder.Id INNER JOIN MailCategoryAssignment ON MailCopy.UniqueId = MailCategoryAssignment.MailCopyUniqueId"
: "SELECT MailCopy.* FROM MailCopy INNER JOIN MailItemFolder ON MailCopy.FolderId = MailItemFolder.Id");
var whereClauses = new List<string>();
var parameters = new List<object>();
@@ -181,6 +186,13 @@ public class MailService : BaseDatabaseService, IMailService
whereClauses.Add($"MailCopy.FolderId IN ({folderPlaceholders})");
parameters.AddRange(options.Folders.Select(f => (object)f.Id));
if (options.IsCategoryView)
{
var categoryPlaceholders = string.Join(",", options.CategoryIds.Select(_ => "?"));
whereClauses.Add($"MailCategoryAssignment.MailCategoryId IN ({categoryPlaceholders})");
parameters.AddRange(options.CategoryIds.Select(a => (object)a));
}
// Filter type
switch (options.FilterType)
{
@@ -338,7 +350,7 @@ public class MailService : BaseDatabaseService, IMailService
{
List<MailCopy> mails;
if (options.PreFetchMailCopies != null)
if (options.PreFetchMailCopies != null && !options.IsCategoryView)
{
mails = ApplyOptionsToPreFetchedMails(options);
}
@@ -398,7 +410,7 @@ public class MailService : BaseDatabaseService, IMailService
mails.RemoveAll(m => m.AssignedAccount == null || m.AssignedFolder == null);
await _sentMailReceiptService.PopulateReceiptStatesAsync(mails).ConfigureAwait(false);
if (!options.CreateThreads || mails.Count == 0)
if (!options.CreateThreads || mails.Count == 0 || options.IsCategoryView)
return [.. mails];
// 6. Expand threads: one batch query for all sibling mails across all threads.
@@ -727,6 +739,7 @@ public class MailService : BaseDatabaseService, IMailService
_logger.Debug("Deleting mail {Id} from folder {FolderName}", mailCopy.Id, mailCopy.AssignedFolder.FolderName);
await Connection.DeleteAsync<MailCopy>(mailCopy.UniqueId).ConfigureAwait(false);
await Connection.ExecuteAsync("DELETE FROM MailCategoryAssignment WHERE MailCopyUniqueId = ?", mailCopy.UniqueId).ConfigureAwait(false);
// If there are no more copies exists of the same mail, delete the MIME file as well.
var isMailExists = await IsMailExistsAsync(mailCopy.Id).ConfigureAwait(false);
@@ -965,6 +978,7 @@ public class MailService : BaseDatabaseService, IMailService
mailCopy.UniqueId = existingCopyItem.UniqueId;
await UpdateMailAsync(mailCopy).ConfigureAwait(false);
await ReplaceMailCategoriesForPackageAsync(accountId, mailCopy, package).ConfigureAwait(false);
await _sentMailReceiptService.TrackSentMailAsync(mailCopy, mimeMessage).ConfigureAwait(false);
await _sentMailReceiptService.ProcessIncomingReceiptAsync(mailCopy, mimeMessage).ConfigureAwait(false);
@@ -981,6 +995,7 @@ public class MailService : BaseDatabaseService, IMailService
}
await InsertMailAsync(mailCopy).ConfigureAwait(false);
await ReplaceMailCategoriesForPackageAsync(accountId, mailCopy, package).ConfigureAwait(false);
await _sentMailReceiptService.TrackSentMailAsync(mailCopy, mimeMessage).ConfigureAwait(false);
await _sentMailReceiptService.ProcessIncomingReceiptAsync(mailCopy, mimeMessage).ConfigureAwait(false);
@@ -1017,6 +1032,11 @@ public class MailService : BaseDatabaseService, IMailService
await _contactService.SaveAddressInformationAsync(contacts).ConfigureAwait(false);
}
private Task ReplaceMailCategoriesForPackageAsync(Guid accountId, MailCopy mailCopy, NewMailItemPackage package)
=> package?.CategoryNames == null
? Task.CompletedTask
: _mailCategoryService.ReplaceMailAssignmentsAsync(accountId, mailCopy.UniqueId, package.CategoryNames);
private async Task<MimeMessage> CreateDraftMimeAsync(MailAccount account, DraftCreationOptions draftCreationOptions, MailAccountAlias selectedAlias)
{
// This unique id is stored in mime headers for Wino to identify remote message with local copy.
+1
View File
@@ -20,6 +20,7 @@ public static class ServicesContainerSetup
services.AddTransient<ICalendarService, CalendarService>();
services.AddTransient<IMailService, MailService>();
services.AddTransient<IMailCategoryService, MailCategoryService>();
services.AddTransient<ISentMailReceiptService, SentMailReceiptService>();
services.AddTransient<IFolderService, FolderService>();
services.AddTransient<IAccountService, AccountService>();