Separation of core library from the UWP app.

This commit is contained in:
Burak Kaan Köse
2024-11-30 23:05:07 +01:00
parent 4e25dbf5e3
commit 0cd1568c64
88 changed files with 481 additions and 353 deletions

View File

@@ -7,7 +7,7 @@ using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Services;
using Wino.Services;
namespace Wino.Core.Integration.Processors
{

View File

@@ -1,7 +1,7 @@
using System;
using System.Threading.Tasks;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Services;
using Wino.Services;
namespace Wino.Core.Integration.Processors
{

View File

@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Services;
using Wino.Services;
namespace Wino.Core.Integration.Processors
{

View File

@@ -1,7 +1,7 @@
using System;
using System.Threading.Tasks;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Services;
using Wino.Services;
namespace Wino.Core.Integration.Processors
{

View File

@@ -1,138 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Services;
namespace Wino.Core.Integration.Threading
{
public class APIThreadingStrategy : IThreadingStrategy
{
private readonly IDatabaseService _databaseService;
private readonly IFolderService _folderService;
public APIThreadingStrategy(IDatabaseService databaseService, IFolderService folderService)
{
_databaseService = databaseService;
_folderService = folderService;
}
public virtual bool ShouldThreadWithItem(IMailItem originalItem, IMailItem targetItem)
{
return originalItem.ThreadId != null && originalItem.ThreadId == targetItem.ThreadId;
}
///<inheritdoc/>
public async Task<List<IMailItem>> ThreadItemsAsync(List<MailCopy> items, IMailItemFolder threadingForFolder)
{
var assignedAccount = items[0].AssignedAccount;
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;
// True: Non threaded items.
// False: Potentially threaded items.
var nonThreadedOrThreadedMails = items
.Distinct()
.GroupBy(x => string.IsNullOrEmpty(x.ThreadId))
.ToDictionary(x => x.Key, x => x);
_ = nonThreadedOrThreadedMails.TryGetValue(true, out var nonThreadedMails);
var isThreadedItems = nonThreadedOrThreadedMails.TryGetValue(false, out var potentiallyThreadedMails);
List<IMailItem> resultList = nonThreadedMails is null ? [] : [.. nonThreadedMails];
if (isThreadedItems)
{
var threadItems = (await GetThreadItemsAsync(potentiallyThreadedMails.Select(x => (x.ThreadId, x.AssignedFolder)).ToList(), assignedAccount.Id, sentFolder.Id, draftFolder.Id))
.GroupBy(x => x.ThreadId);
foreach (var threadItem in threadItems)
{
if (threadItem.Count() == 1)
{
resultList.Add(threadItem.First());
continue;
}
var thread = new ThreadMailItem();
foreach (var childThreadItem in threadItem)
{
if (thread.ThreadItems.Any(a => a.Id == childThreadItem.Id))
{
// Mail already exist in the thread.
// There should be only 1 instance of the mail in the thread.
// Make sure we add the correct one.
// Add the one with threading folder.
var threadingFolderItem = threadItem.FirstOrDefault(a => a.Id == childThreadItem.Id && a.FolderId == threadingForFolder.Id);
if (threadingFolderItem == null) continue;
// Remove the existing one.
thread.ThreadItems.Remove(thread.ThreadItems.First(a => a.Id == childThreadItem.Id));
// Add the correct one for listing.
thread.AddThreadItem(threadingFolderItem);
}
else
{
thread.AddThreadItem(childThreadItem);
}
}
if (thread.ThreadItems.Count > 1)
{
resultList.Add(thread);
}
else
{
// Don't make threads if the thread has only one item.
// Gmail has may have multiple assignments for the same item.
resultList.Add(thread.ThreadItems.First());
}
}
}
return resultList;
}
private async Task<List<MailCopy>> GetThreadItemsAsync(List<(string threadId, MailItemFolder threadingFolder)> potentialThread,
Guid accountId,
Guid sentFolderId,
Guid draftFolderId)
{
// Only items from the folder that we are threading for, sent and draft folder items must be included.
// This is important because deleted items or item assignments that belongs to different folder is
// affecting the thread creation here.
// If the threading is done from Sent or Draft folder, include everything...
// TODO: Convert to SQLKata query.
var query = @$"SELECT DISTINCT MC.* FROM MailCopy MC
INNER JOIN MailItemFolder MF on MF.Id = MC.FolderId
WHERE MF.MailAccountId == '{accountId}' AND
({string.Join(" OR ", potentialThread.Select(x => ConditionForItem(x, sentFolderId, draftFolderId)))})";
return await _databaseService.Connection.QueryAsync<MailCopy>(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}'))";
}
}
}
}

View File

@@ -1,10 +0,0 @@
using Wino.Core.Domain.Interfaces;
using Wino.Core.Services;
namespace Wino.Core.Integration.Threading
{
public class GmailThreadingStrategy : APIThreadingStrategy
{
public GmailThreadingStrategy(IDatabaseService databaseService, IFolderService folderService) : base(databaseService, folderService) { }
}
}

View File

@@ -1,181 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SqlKata;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Extensions;
using Wino.Core.Services;
namespace Wino.Core.Integration.Threading
{
public class ImapThreadStrategy : IThreadingStrategy
{
private readonly IDatabaseService _databaseService;
private readonly IFolderService _folderService;
public ImapThreadStrategy(IDatabaseService databaseService, IFolderService folderService)
{
_databaseService = databaseService;
_folderService = folderService;
}
private Task<MailCopy> GetReplyParentAsync(IMailItem replyItem, Guid accountId, Guid threadingFolderId, Guid sentFolderId, Guid draftFolderId)
{
if (string.IsNullOrEmpty(replyItem?.MessageId)) return Task.FromResult<MailCopy>(null);
var query = new Query("MailCopy")
.Distinct()
.Take(1)
.Join("MailItemFolder", "MailItemFolder.Id", "MailCopy.FolderId")
.Where("MailItemFolder.MailAccountId", accountId)
.WhereIn("MailItemFolder.Id", new List<Guid> { threadingFolderId, sentFolderId, draftFolderId })
.Where("MailCopy.MessageId", replyItem.InReplyTo)
.WhereNot("MailCopy.Id", replyItem.Id)
.Select("MailCopy.*");
return _databaseService.Connection.FindWithQueryAsync<MailCopy>(query.GetRawQuery());
}
private Task<MailCopy> GetInReplyToReplyAsync(IMailItem originalItem, Guid accountId, Guid threadingFolderId, Guid sentFolderId, Guid draftFolderId)
{
if (string.IsNullOrEmpty(originalItem?.MessageId)) return Task.FromResult<MailCopy>(null);
var query = new Query("MailCopy")
.Distinct()
.Take(1)
.Join("MailItemFolder", "MailItemFolder.Id", "MailCopy.FolderId")
.WhereNot("MailCopy.Id", originalItem.Id)
.Where("MailItemFolder.MailAccountId", accountId)
.Where("MailCopy.InReplyTo", originalItem.MessageId)
.WhereIn("MailItemFolder.Id", new List<Guid> { threadingFolderId, sentFolderId, draftFolderId })
.Select("MailCopy.*");
var raq = query.GetRawQuery();
return _databaseService.Connection.FindWithQueryAsync<MailCopy>(query.GetRawQuery());
}
public async Task<List<IMailItem>> ThreadItemsAsync(List<MailCopy> items, IMailItemFolder threadingForFolder)
{
var threads = new List<ThreadMailItem>();
var account = items.First().AssignedAccount;
var accountId = account.Id;
// Child -> Parent approach.
var mailLookupTable = new Dictionary<string, bool>();
// 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);
var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(accountId, Domain.Enums.SpecialFolderType.Sent);
var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(accountId, Domain.Enums.SpecialFolderType.Draft);
// Threading is not possible. Return items as it is.
if (sentFolder == null || draftFolder == null) return new List<IMailItem>(items);
foreach (var replyItem in items)
{
if (mailLookupTable[replyItem.Id])
continue;
mailLookupTable[replyItem.Id] = true;
var threadItem = new ThreadMailItem();
threadItem.AddThreadItem(replyItem);
var replyToChild = await GetReplyParentAsync(replyItem, accountId, replyItem.AssignedFolder.Id, sentFolder.Id, draftFolder.Id);
// Build up
while (replyToChild != null)
{
replyToChild.AssignedAccount = account;
if (replyToChild.FolderId == draftFolder.Id)
replyToChild.AssignedFolder = draftFolder;
if (replyToChild.FolderId == sentFolder.Id)
replyToChild.AssignedFolder = sentFolder;
if (replyToChild.FolderId == replyItem.AssignedFolder.Id)
replyToChild.AssignedFolder = replyItem.AssignedFolder;
threadItem.AddThreadItem(replyToChild);
if (mailLookupTable.ContainsKey(replyToChild.Id))
mailLookupTable[replyToChild.Id] = true;
replyToChild = await GetReplyParentAsync(replyToChild, accountId, replyToChild.AssignedFolder.Id, sentFolder.Id, draftFolder.Id);
}
// Build down
var replyToParent = await GetInReplyToReplyAsync(replyItem, accountId, replyItem.AssignedFolder.Id, sentFolder.Id, draftFolder.Id);
while (replyToParent != null)
{
replyToParent.AssignedAccount = account;
if (replyToParent.FolderId == draftFolder.Id)
replyToParent.AssignedFolder = draftFolder;
if (replyToParent.FolderId == sentFolder.Id)
replyToParent.AssignedFolder = sentFolder;
if (replyToParent.FolderId == replyItem.AssignedFolder.Id)
replyToParent.AssignedFolder = replyItem.AssignedFolder;
threadItem.AddThreadItem(replyToParent);
if (mailLookupTable.ContainsKey(replyToParent.Id))
mailLookupTable[replyToParent.Id] = true;
replyToParent = await GetInReplyToReplyAsync(replyToParent, accountId, replyToParent.AssignedFolder.Id, sentFolder.Id, draftFolder.Id);
}
// It's a thread item.
if (threadItem.ThreadItems.Count > 1 && !threads.Exists(a => a.Id == threadItem.Id))
{
threads.Add(threadItem);
}
else
{
// False alert. This is not a thread item.
mailLookupTable[replyItem.Id] = false;
// TODO: Here potentially check other algorithms for threading like References.
}
}
// 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<IMailItem>(items);
finalList.AddRange(threads);
return finalList;
}
public bool ShouldThreadWithItem(IMailItem originalItem, IMailItem targetItem)
{
bool isChild = originalItem.InReplyTo != null && originalItem.InReplyTo == targetItem.MessageId;
bool isParent = originalItem.MessageId != null && originalItem.MessageId == targetItem.InReplyTo;
return isChild || isParent;
}
}
}

View File

@@ -1,14 +0,0 @@
using Wino.Core.Domain.Interfaces;
using Wino.Core.Services;
namespace Wino.Core.Integration.Threading
{
// Outlook and Gmail is using the same threading strategy.
// Outlook: ConversationId -> it's set as ThreadId
// Gmail: ThreadId
public class OutlookThreadingStrategy : APIThreadingStrategy
{
public OutlookThreadingStrategy(IDatabaseService databaseService, IFolderService folderService) : base(databaseService, folderService) { }
}
}