diff --git a/Wino.Core.Domain/Interfaces/IThreadingStrategy.cs b/Wino.Core.Domain/Interfaces/IThreadingStrategy.cs
index ed5d57f0..cd0cec9c 100644
--- a/Wino.Core.Domain/Interfaces/IThreadingStrategy.cs
+++ b/Wino.Core.Domain/Interfaces/IThreadingStrategy.cs
@@ -7,6 +7,11 @@ namespace Wino.Core.Domain.Interfaces
{
public interface IThreadingStrategy
{
+ ///
+ /// Attach thread mails to the list.
+ ///
+ /// Original mails.
+ /// Original mails with thread mails.
Task> ThreadItemsAsync(List items);
bool ShouldThreadWithItem(IMailItem originalItem, IMailItem targetItem);
}
diff --git a/Wino.Core/Integration/Threading/APIThreadingStrategy.cs b/Wino.Core/Integration/Threading/APIThreadingStrategy.cs
index c1685f95..f1258f9d 100644
--- a/Wino.Core/Integration/Threading/APIThreadingStrategy.cs
+++ b/Wino.Core/Integration/Threading/APIThreadingStrategy.cs
@@ -26,87 +26,55 @@ namespace Wino.Core.Integration.Threading
return originalItem.ThreadId != null && originalItem.ThreadId == targetItem.ThreadId;
}
+ ///
public async Task> ThreadItemsAsync(List items)
{
- var accountId = items.First().AssignedAccount.Id;
+ var assignedAccount = items[0].AssignedAccount;
- var threads = new List();
- var assignedAccount = items.First().AssignedAccount;
-
- // TODO: Can be optimized by moving to the caller.
- var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(accountId, Domain.Enums.SpecialFolderType.Sent);
- var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(accountId, Domain.Enums.SpecialFolderType.Draft);
+ var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Sent);
+ var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Draft);
if (sentFolder == null || draftFolder == null) return default;
- // Child -> Parent approach.
+ // True: Non threaded items.
+ // False: Potentially threaded items.
+ var nonThreadedOrThreadedMails = items
+ .Distinct()
+ .GroupBy(x => string.IsNullOrEmpty(x.ThreadId))
+ .ToDictionary(x => x.Key, x => x);
- var potentialThreadItems = items.Distinct().Where(a => !string.IsNullOrEmpty(a.ThreadId));
+ _ = nonThreadedOrThreadedMails.TryGetValue(true, out var nonThreadedMails);
+ var isThreadedItems = nonThreadedOrThreadedMails.TryGetValue(false, out var potentiallyThreadedMails);
- var mailLookupTable = new Dictionary();
+ List resultList = nonThreadedMails is null ? [] : [.. nonThreadedMails];
- // Fill up the mail lookup table to prevent double thread creation.
- foreach (var mail in items)
- if (!mailLookupTable.ContainsKey(mail.Id))
- mailLookupTable.Add(mail.Id, false);
-
- foreach (var potentialItem in potentialThreadItems)
+ if (isThreadedItems)
{
- if (mailLookupTable[potentialItem.Id])
- continue;
+ var threadItems = (await GetThreadItemsAsync(potentiallyThreadedMails.Select(x => (x.ThreadId, x.AssignedFolder)).ToList(), assignedAccount.Id, sentFolder.Id, draftFolder.Id))
+ .GroupBy(x => x.ThreadId);
- mailLookupTable[potentialItem.Id] = true;
-
- var allThreadItems = await GetThreadItemsAsync(potentialItem.ThreadId, accountId, potentialItem.AssignedFolder, sentFolder.Id, draftFolder.Id);
-
- if (allThreadItems.Count == 1)
+ foreach (var threadItem in threadItems)
{
- // It's a single item.
- // Mark as not-processed as thread.
-
- mailLookupTable[potentialItem.Id] = false;
- }
- else
- {
- // Thread item. Mark all items as true in dict.
- var threadItem = new ThreadMailItem();
-
- foreach (var childThreadItem in allThreadItems)
+ if (threadItem.Count() == 1)
{
- if (mailLookupTable.ContainsKey(childThreadItem.Id))
- mailLookupTable[childThreadItem.Id] = true;
-
- childThreadItem.AssignedAccount = assignedAccount;
- childThreadItem.AssignedFolder = await _folderService.GetFolderAsync(childThreadItem.FolderId);
-
- threadItem.AddThreadItem(childThreadItem);
+ resultList.Add(threadItem.First());
+ continue;
}
- // Multiple mail copy ids from different folders are thing for Gmail.
- if (threadItem.ThreadItems.Count == 1)
- mailLookupTable[potentialItem.Id] = false;
- else
- threads.Add(threadItem);
+ var thread = new ThreadMailItem();
+ foreach (var childThreadItem in threadItem)
+ {
+ thread.AddThreadItem(childThreadItem);
+ }
+ resultList.Add(thread);
}
}
- // At this points all mails in the list belong to single items.
- // Merge with threads.
- // Last sorting will be done later on in MailService.
-
- // Remove single mails that are included in thread.
- items.RemoveAll(a => mailLookupTable.ContainsKey(a.Id) && mailLookupTable[a.Id]);
-
- var finalList = new List(items);
-
- finalList.AddRange(threads);
-
- return finalList;
+ return resultList;
}
- private async Task> GetThreadItemsAsync(string threadId,
+ private async Task> GetThreadItemsAsync(List<(string threadId, MailItemFolder threadingFolder)> potentialThread,
Guid accountId,
- MailItemFolder threadingFolder,
Guid sentFolderId,
Guid draftFolderId)
{
@@ -118,24 +86,20 @@ namespace Wino.Core.Integration.Threading
// TODO: Convert to SQLKata query.
- string query = string.Empty;
-
- if (threadingFolder.SpecialFolderType == SpecialFolderType.Draft || threadingFolder.SpecialFolderType == SpecialFolderType.Sent)
- {
- query = @$"SELECT DISTINCT MC.* FROM MailCopy MC
+ var query = @$"SELECT DISTINCT MC.* FROM MailCopy MC
INNER JOIN MailItemFolder MF on MF.Id = MC.FolderId
- WHERE MF.MailAccountId == '{accountId}' AND MC.ThreadId = '{threadId}'";
- }
- else
- {
- query = @$"SELECT DISTINCT MC.* FROM MailCopy MC
- INNER JOIN MailItemFolder MF on MF.Id = MC.FolderId
- WHERE MF.MailAccountId == '{accountId}' AND MC.FolderId IN ('{threadingFolder.Id}','{sentFolderId}','{draftFolderId}')
- AND MC.ThreadId = '{threadId}'";
- }
-
+ WHERE MF.MailAccountId == '{accountId}' AND
+ ({string.Join(" OR ", potentialThread.Select(x => ConditionForItem(x, sentFolderId, draftFolderId)))})";
return await _databaseService.Connection.QueryAsync(query);
+
+ static string ConditionForItem((string threadId, MailItemFolder threadingFolder) potentialThread, Guid sentFolderId, Guid draftFolderId)
+ {
+ if (potentialThread.threadingFolder.SpecialFolderType == SpecialFolderType.Draft || potentialThread.threadingFolder.SpecialFolderType == SpecialFolderType.Sent)
+ return $"(MC.ThreadId = '{potentialThread.threadId}')";
+
+ return $"(MC.ThreadId = '{potentialThread.threadId}' AND MC.FolderId IN ('{potentialThread.threadingFolder.Id}','{sentFolderId}','{draftFolderId}'))";
+ }
}
}
}
diff --git a/Wino.Core/Services/MailService.cs b/Wino.Core/Services/MailService.cs
index 7cc0bf63..18be6ec9 100644
--- a/Wino.Core/Services/MailService.cs
+++ b/Wino.Core/Services/MailService.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using Microsoft.Kiota.Abstractions.Extensions;
using MimeKit;
using MimeKit.Text;
using MoreLinq;
@@ -186,73 +187,97 @@ namespace Wino.Core.Services
var mails = await Connection.QueryAsync(query);
- // Fill in assigned account and folder for each mail.
- // To speed things up a bit, we'll load account and assigned folder in groups
- // to reduce the query time.
+ Dictionary folderCache = [];
+ Dictionary accountCache = [];
- var groupedByFolders = mails.GroupBy(a => a.FolderId);
-
- foreach (var group in groupedByFolders)
+ // Populate Folder Assignment for each single mail, to be able later group by "MailAccountId".
+ // This is needed to execute threading strategy by account type.
+ // Avoid DBs calls as possible, storing info in a dictionary.
+ foreach (var mail in mails)
{
- MailItemFolder folderAssignment = null;
- MailAccount accountAssignment = null;
-
- folderAssignment = await _folderService.GetFolderAsync(group.Key).ConfigureAwait(false);
-
- if (folderAssignment != null)
- {
- accountAssignment = await _accountService.GetAccountAsync(folderAssignment.MailAccountId).ConfigureAwait(false);
- }
-
- group.ForEach(a =>
- {
- a.AssignedFolder = folderAssignment;
- a.AssignedAccount = accountAssignment;
- });
+ await LoadAssignedPropertiesWithCacheAsync(mail, folderCache, accountCache).ConfigureAwait(false);
}
// Remove items that has no assigned account or folder.
mails.RemoveAll(a => a.AssignedAccount == null || a.AssignedFolder == null);
- // Each account items must be threaded separately.
-
- if (options.CreateThreads)
- {
- var threadedItems = new List();
-
- var groupedByAccounts = mails.GroupBy(a => a.AssignedAccount.Id);
-
- foreach (var group in groupedByAccounts)
- {
- if (!group.Any()) continue;
-
- var accountId = group.Key;
- var groupAccount = mails.First(a => a.AssignedAccount.Id == accountId).AssignedAccount;
-
- var threadingStrategy = _threadingStrategyProvider.GetStrategy(groupAccount.ProviderType);
-
- // Only thread items from Draft and Sent folders must present here.
- // Otherwise this strategy will fetch the items that are in Deleted folder as well.
- var accountThreadedItems = await threadingStrategy.ThreadItemsAsync(group.ToList());
-
- if (accountThreadedItems != null)
- {
- threadedItems.AddRange(accountThreadedItems);
- }
- }
-
- threadedItems.Sort(options.SortingOptionType == SortingOptionType.ReceiveDate ? new DateComparer() : new NameComparer());
-
- return threadedItems;
- }
- else
+ if (!options.CreateThreads)
{
// Threading is disabled. Just return everything as it is.
-
mails.Sort(options.SortingOptionType == SortingOptionType.ReceiveDate ? new DateComparer() : new NameComparer());
return new List(mails);
}
+
+ // Populate threaded items.
+
+ var threadedItems = new List();
+
+ // Each account items must be threaded separately.
+ foreach (var group in mails.GroupBy(a => a.AssignedAccount.Id))
+ {
+ var accountId = group.Key;
+ var groupAccount = mails.First(a => a.AssignedAccount.Id == accountId).AssignedAccount;
+
+ var threadingStrategy = _threadingStrategyProvider.GetStrategy(groupAccount.ProviderType);
+
+ // Only thread items from Draft and Sent folders must present here.
+ // Otherwise this strategy will fetch the items that are in Deleted folder as well.
+ var accountThreadedItems = await threadingStrategy.ThreadItemsAsync([.. group]);
+
+ // Populate threaded items with folder and account assignments.
+ // Almost everything already should be in cache from initial population.
+ foreach (var mail in accountThreadedItems)
+ {
+ await LoadAssignedPropertiesWithCacheAsync(mail, folderCache, accountCache).ConfigureAwait(false);
+ }
+
+ if (accountThreadedItems != null)
+ {
+ threadedItems.AddRange(accountThreadedItems);
+ }
+ }
+
+ threadedItems.Sort(options.SortingOptionType == SortingOptionType.ReceiveDate ? new DateComparer() : new NameComparer());
+
+ return threadedItems;
+
+ // Recursive function to populate folder and account assignments for each mail item.
+ async Task LoadAssignedPropertiesWithCacheAsync(IMailItem mail, Dictionary folderCache, Dictionary accountCache)
+ {
+ if (mail is ThreadMailItem threadMailItem)
+ {
+ foreach (var childMail in threadMailItem.ThreadItems)
+ {
+ await LoadAssignedPropertiesWithCacheAsync(childMail, folderCache, accountCache).ConfigureAwait(false);
+ }
+ }
+
+ if (mail is MailCopy mailCopy)
+ {
+ MailAccount accountAssignment = null;
+
+ var isFolderCached = folderCache.TryGetValue(mailCopy.FolderId, out MailItemFolder folderAssignment);
+ accountAssignment = null;
+ if (!isFolderCached)
+ {
+ folderAssignment = await _folderService.GetFolderAsync(mailCopy.FolderId).ConfigureAwait(false);
+ _ = folderCache.TryAdd(mailCopy.FolderId, folderAssignment);
+ }
+ if (folderAssignment != null)
+ {
+ var isAccountCached = accountCache.TryGetValue(folderAssignment.MailAccountId, out accountAssignment);
+ if (!isAccountCached)
+ {
+ accountAssignment = await _accountService.GetAccountAsync(folderAssignment.MailAccountId).ConfigureAwait(false);
+ _ = accountCache.TryAdd(folderAssignment.MailAccountId, accountAssignment);
+ }
+ }
+
+ mailCopy.AssignedFolder = folderAssignment;
+ mailCopy.AssignedAccount = accountAssignment;
+ }
+ }
}
private async Task> GetMailItemsAsync(string mailCopyId)