Initial commit.
This commit is contained in:
130
Wino.Core/Integration/BaseMailIntegrator.cs
Normal file
130
Wino.Core/Integration/BaseMailIntegrator.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using MailKit.Net.Imap;
|
||||
using MoreLinq;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Requests;
|
||||
using Wino.Core.Requests.Bundles;
|
||||
|
||||
namespace Wino.Core.Integration
|
||||
{
|
||||
public abstract class BaseMailIntegrator<TNativeRequestType>
|
||||
{
|
||||
/// <summary>
|
||||
/// How many items per single HTTP call can be modified.
|
||||
/// </summary>
|
||||
public abstract uint BatchModificationSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// How many items must be downloaded per folder when the folder is first synchronized.
|
||||
/// </summary>
|
||||
public abstract uint InitialMessageDownloadCountPerFolder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a batched HttpBundle without a response for a collection of MailItem.
|
||||
/// </summary>
|
||||
/// <param name="batchChangeRequest">Generated batch request.</param>
|
||||
/// <param name="action">An action to get the native request from the MailItem.</param>
|
||||
/// <returns>Collection of http bundle that contains batch and native request.</returns>
|
||||
public IEnumerable<IRequestBundle<TNativeRequestType>> CreateBatchedHttpBundleFromGroup(
|
||||
IBatchChangeRequest batchChangeRequest,
|
||||
Func<IEnumerable<IRequest>, TNativeRequestType> action)
|
||||
{
|
||||
if (batchChangeRequest.Items == null) yield break;
|
||||
|
||||
var groupedItems = batchChangeRequest.Items.Batch((int)BatchModificationSize);
|
||||
|
||||
foreach (var group in groupedItems)
|
||||
yield return new HttpRequestBundle<TNativeRequestType>(action(group), batchChangeRequest);
|
||||
}
|
||||
|
||||
public IEnumerable<IRequestBundle<TNativeRequestType>> CreateBatchedHttpBundle(
|
||||
IBatchChangeRequest batchChangeRequest,
|
||||
Func<IRequest, TNativeRequestType> action)
|
||||
{
|
||||
if (batchChangeRequest.Items == null) yield break;
|
||||
|
||||
var groupedItems = batchChangeRequest.Items.Batch((int)BatchModificationSize);
|
||||
|
||||
foreach (var group in groupedItems)
|
||||
foreach (var item in group)
|
||||
yield return new HttpRequestBundle<TNativeRequestType>(action(item), item);
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a single HttpBundle without a response for a collection of MailItem.
|
||||
/// </summary>
|
||||
/// <param name="batchChangeRequest">Batch request</param>
|
||||
/// <param name="action">An action to get the native request from the MailItem</param>
|
||||
/// <returns>Collection of http bundle that contains batch and native request.</returns>
|
||||
public IEnumerable<IRequestBundle<TNativeRequestType>> CreateHttpBundle(
|
||||
IBatchChangeRequest batchChangeRequest,
|
||||
Func<IRequest, TNativeRequestType> action)
|
||||
{
|
||||
if (batchChangeRequest.Items == null) yield break;
|
||||
|
||||
foreach (var item in batchChangeRequest.Items)
|
||||
yield return new HttpRequestBundle<TNativeRequestType>(action(item), batchChangeRequest);
|
||||
}
|
||||
|
||||
public IEnumerable<IRequestBundle<TNativeRequestType>> CreateHttpBundle<TResponseType>(
|
||||
IBatchChangeRequest batchChangeRequest,
|
||||
Func<IRequest, TNativeRequestType> action)
|
||||
{
|
||||
if (batchChangeRequest.Items == null) yield break;
|
||||
|
||||
foreach (var item in batchChangeRequest.Items)
|
||||
yield return new HttpRequestBundle<TNativeRequestType, TResponseType>(action(item), item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates HttpBundle with TResponse of expected response type from the http call for each of the items in the batch.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">Expected http response type after the call.</typeparam>
|
||||
/// <param name="batchChangeRequest">Generated batch request.</param>
|
||||
/// <param name="action">An action to get the native request from the MailItem.</param>
|
||||
/// <returns>Collection of http bundle that contains batch and native request.</returns>
|
||||
public IEnumerable<IRequestBundle<TNativeRequestType>> CreateHttpBundleWithResponse<TResponse>(
|
||||
IBatchChangeRequest batchChangeRequest,
|
||||
Func<IRequest, TNativeRequestType> action)
|
||||
{
|
||||
if (batchChangeRequest.Items == null) yield break;
|
||||
|
||||
foreach (var item in batchChangeRequest.Items)
|
||||
yield return new HttpRequestBundle<TNativeRequestType, TResponse>(action(item), batchChangeRequest);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a batched HttpBundle with TResponse of expected response type from the http call for each of the items in the batch.
|
||||
/// Func will be executed for each item separately in the batch request.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">Expected http response type after the call.</typeparam>
|
||||
/// <param name="batchChangeRequest">Generated batch request.</param>
|
||||
/// <param name="action">An action to get the native request from the MailItem.</param>
|
||||
/// <returns>Collection of http bundle that contains batch and native request.</returns>
|
||||
public IEnumerable<IRequestBundle<TNativeRequestType>> CreateBatchedHttpBundle<TResponse>(
|
||||
IBatchChangeRequest batchChangeRequest,
|
||||
Func<IRequest, TNativeRequestType> action)
|
||||
{
|
||||
if (batchChangeRequest.Items == null) yield break;
|
||||
|
||||
var groupedItems = batchChangeRequest.Items.Batch((int)BatchModificationSize);
|
||||
|
||||
foreach (var group in groupedItems)
|
||||
foreach (var item in group)
|
||||
yield return new HttpRequestBundle<TNativeRequestType, TResponse>(action(item), item);
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
public IEnumerable<IRequestBundle<ImapRequest>> CreateTaskBundle(Func<ImapClient, Task> value, IRequestBase request)
|
||||
{
|
||||
var imapreq = new ImapRequest(value, request);
|
||||
|
||||
return [new ImapRequestBundle(imapreq, request)];
|
||||
}
|
||||
}
|
||||
}
|
||||
179
Wino.Core/Integration/ImapClientPool.cs
Normal file
179
Wino.Core/Integration/ImapClientPool.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MailKit.Net.Imap;
|
||||
using MailKit.Net.Proxy;
|
||||
using MailKit.Security;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
|
||||
namespace Wino.Core.Integration
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a pooling mechanism for ImapClient.
|
||||
/// Makes sure that we don't have too many connections to the server.
|
||||
/// Rents a connected & authenticated client from the pool all the time.
|
||||
/// TODO: Keeps the clients alive by sending NOOP command periodically.
|
||||
/// TODO: Listens to the Inbox folder for new messages.
|
||||
/// </summary>
|
||||
/// <param name="customServerInformation">Connection/Authentication info to be used to configure ImapClient.</param>
|
||||
public class ImapClientPool
|
||||
{
|
||||
// Hardcoded implementation details for ID extension if the server supports.
|
||||
// Some providers like Chinese 126 require Id to be sent before authentication.
|
||||
// We don't expose any customer data here. Therefore it's safe for now.
|
||||
// Later on maybe we can make it configurable and leave it to the user with passing
|
||||
// real implementation details.
|
||||
|
||||
private readonly ImapImplementation _implementation = new ImapImplementation()
|
||||
{
|
||||
Version = "1.0",
|
||||
OS = "Windows",
|
||||
Vendor = "Wino"
|
||||
};
|
||||
|
||||
private const int MaxPoolSize = 5;
|
||||
|
||||
private readonly ConcurrentBag<ImapClient> _clients = [];
|
||||
private readonly SemaphoreSlim _semaphore = new(MaxPoolSize);
|
||||
private readonly CustomServerInformation _customServerInformation;
|
||||
private readonly ILogger _logger = Log.ForContext<ImapClientPool>();
|
||||
|
||||
public ImapClientPool(CustomServerInformation customServerInformation)
|
||||
{
|
||||
_customServerInformation = customServerInformation;
|
||||
}
|
||||
|
||||
private async Task EnsureConnectivityAsync(ImapClient client, bool isCreatedNew)
|
||||
{
|
||||
try
|
||||
{
|
||||
await EnsureConnectedAsync(client);
|
||||
|
||||
if (isCreatedNew && client.IsConnected)
|
||||
{
|
||||
// Activate supported pre-auth capabilities.
|
||||
if (client.Capabilities.HasFlag(ImapCapabilities.Compress))
|
||||
await client.CompressAsync();
|
||||
|
||||
// Identify if the server supports ID extension.
|
||||
if (client.Capabilities.HasFlag(ImapCapabilities.Id))
|
||||
await client.IdentifyAsync(_implementation);
|
||||
}
|
||||
|
||||
await EnsureAuthenticatedAsync(client);
|
||||
|
||||
if (isCreatedNew && client.IsAuthenticated)
|
||||
{
|
||||
// Activate post-auth capabilities.
|
||||
if (client.Capabilities.HasFlag(ImapCapabilities.QuickResync))
|
||||
await client.EnableQuickResyncAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new ImapClientPoolException(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Release it even if it fails.
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ImapClient> GetClientAsync()
|
||||
{
|
||||
await _semaphore.WaitAsync();
|
||||
|
||||
if (_clients.TryTake(out ImapClient item))
|
||||
{
|
||||
await EnsureConnectivityAsync(item, false);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
var client = CreateNewClient();
|
||||
|
||||
await EnsureConnectivityAsync(client, true);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
public void Release(ImapClient item)
|
||||
{
|
||||
if (item != null)
|
||||
{
|
||||
_clients.Add(item);
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public ImapClient CreateNewClient()
|
||||
{
|
||||
var client = new ImapClient();
|
||||
|
||||
HttpProxyClient proxyClient = null;
|
||||
|
||||
// Add proxy client if exists.
|
||||
if (!string.IsNullOrEmpty(_customServerInformation.ProxyServer))
|
||||
{
|
||||
proxyClient = new HttpProxyClient(_customServerInformation.ProxyServer, int.Parse(_customServerInformation.ProxyServerPort));
|
||||
}
|
||||
|
||||
client.ProxyClient = proxyClient;
|
||||
|
||||
_logger.Debug("Created new ImapClient. Current clients: {Count}", _clients.Count);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
private SecureSocketOptions GetSocketOptions(ImapConnectionSecurity connectionSecurity)
|
||||
=> connectionSecurity switch
|
||||
{
|
||||
ImapConnectionSecurity.Auto => SecureSocketOptions.Auto,
|
||||
ImapConnectionSecurity.None => SecureSocketOptions.None,
|
||||
ImapConnectionSecurity.StartTls => SecureSocketOptions.StartTlsWhenAvailable,
|
||||
ImapConnectionSecurity.SslTls => SecureSocketOptions.SslOnConnect,
|
||||
_ => SecureSocketOptions.None
|
||||
};
|
||||
|
||||
public async Task EnsureConnectedAsync(ImapClient client)
|
||||
{
|
||||
if (client.IsConnected) return;
|
||||
|
||||
await client.ConnectAsync(_customServerInformation.IncomingServer,
|
||||
int.Parse(_customServerInformation.IncomingServerPort),
|
||||
GetSocketOptions(_customServerInformation.IncomingServerSocketOption));
|
||||
}
|
||||
|
||||
public async Task EnsureAuthenticatedAsync(ImapClient client)
|
||||
{
|
||||
if (client.IsAuthenticated) return;
|
||||
|
||||
switch (_customServerInformation.IncomingAuthenticationMethod)
|
||||
{
|
||||
case ImapAuthenticationMethod.Auto:
|
||||
break;
|
||||
case ImapAuthenticationMethod.None:
|
||||
break;
|
||||
case ImapAuthenticationMethod.NormalPassword:
|
||||
break;
|
||||
case ImapAuthenticationMethod.EncryptedPassword:
|
||||
break;
|
||||
case ImapAuthenticationMethod.Ntlm:
|
||||
break;
|
||||
case ImapAuthenticationMethod.CramMd5:
|
||||
break;
|
||||
case ImapAuthenticationMethod.DigestMd5:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
await client.AuthenticateAsync(_customServerInformation.IncomingServerUsername, _customServerInformation.IncomingServerPassword);
|
||||
}
|
||||
}
|
||||
}
|
||||
124
Wino.Core/Integration/Processors/DefaultChangeProcessor.cs
Normal file
124
Wino.Core/Integration/Processors/DefaultChangeProcessor.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using MimeKit;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.Services;
|
||||
|
||||
namespace Wino.Core.Integration.Processors
|
||||
{
|
||||
/// <summary>
|
||||
/// Database change processor that handles common operations for all synchronizers.
|
||||
/// When a synchronizer detects a change, it should call the appropriate method in this class to reflect the change in the database.
|
||||
/// Different synchronizers might need additional implementations.
|
||||
/// <see cref="IGmailChangeProcessor"/> and <see cref="IOutlookChangeProcessor"/>
|
||||
/// None of the synchronizers can directly change anything in the database.
|
||||
/// </summary>
|
||||
public interface IDefaultChangeProcessor
|
||||
{
|
||||
Task<string> UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string deltaSynchronizationIdentifier);
|
||||
Task<string> UpdateFolderDeltaSynchronizationIdentifierAsync(Guid folderId, string deltaSynchronizationIdentifier);
|
||||
|
||||
Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
|
||||
Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
|
||||
|
||||
Task ChangeMailReadStatusAsync(string mailCopyId, bool isRead);
|
||||
Task ChangeFlagStatusAsync(string mailCopyId, bool isFlagged);
|
||||
|
||||
Task<bool> CreateMailAsync(Guid AccountId, NewMailItemPackage package);
|
||||
Task DeleteMailAsync(Guid accountId, string mailId);
|
||||
|
||||
Task<bool> MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId);
|
||||
Task MapLocalDraftAsync(string mailCopyId, string newDraftId, string newThreadId);
|
||||
|
||||
Task<List<MailCopy>> GetDownloadedUnreadMailsAsync(Guid accountId, IEnumerable<string> downloadedMailCopyIds);
|
||||
|
||||
Task SaveMimeFileAsync(Guid fileId, MimeMessage mimeMessage, Guid accountId);
|
||||
|
||||
// For gmail.
|
||||
Task UpdateFolderStructureAsync(Guid accountId, List<MailItemFolder> allFolders);
|
||||
|
||||
Task DeleteFolderAsync(Guid accountId, string remoteFolderId);
|
||||
Task<List<MailItemFolder>> GetSynchronizationFoldersAsync(SynchronizationOptions options);
|
||||
Task InsertFolderAsync(MailItemFolder folder);
|
||||
|
||||
Task<IList<uint>> GetKnownUidsForFolderAsync(Guid folderId);
|
||||
}
|
||||
|
||||
public interface IGmailChangeProcessor : IDefaultChangeProcessor
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public interface IOutlookChangeProcessor : IDefaultChangeProcessor
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class DefaultChangeProcessor(IDatabaseService databaseService,
|
||||
IFolderService folderService,
|
||||
IMailService mailService,
|
||||
IAccountService accountService,
|
||||
IMimeFileService mimeFileService) : BaseDatabaseService(databaseService), IDefaultChangeProcessor
|
||||
{
|
||||
private readonly IFolderService _folderService = folderService;
|
||||
private readonly IMailService _mailService = mailService;
|
||||
private readonly IAccountService _accountService = accountService;
|
||||
private readonly IMimeFileService _mimeFileService = mimeFileService;
|
||||
|
||||
public Task<string> UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string synchronizationDeltaIdentifier)
|
||||
=> _accountService.UpdateSynchronizationIdentifierAsync(accountId, synchronizationDeltaIdentifier);
|
||||
|
||||
public Task ChangeFlagStatusAsync(string mailCopyId, bool isFlagged)
|
||||
=> _mailService.ChangeFlagStatusAsync(mailCopyId, isFlagged);
|
||||
|
||||
public Task ChangeMailReadStatusAsync(string mailCopyId, bool isRead)
|
||||
=> _mailService.ChangeReadStatusAsync(mailCopyId, isRead);
|
||||
|
||||
public Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId)
|
||||
=> _mailService.DeleteAssignmentAsync(accountId, mailCopyId, remoteFolderId);
|
||||
|
||||
public Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId)
|
||||
=> _mailService.CreateAssignmentAsync(accountId, mailCopyId, remoteFolderId);
|
||||
|
||||
public Task DeleteMailAsync(Guid accountId, string mailId)
|
||||
=> _mailService.DeleteMailAsync(accountId, mailId);
|
||||
|
||||
public Task<bool> CreateMailAsync(Guid accountId, NewMailItemPackage package)
|
||||
=> _mailService.CreateMailAsync(accountId, package);
|
||||
|
||||
// Folder methods
|
||||
public Task UpdateFolderStructureAsync(Guid accountId, List<MailItemFolder> allFolders)
|
||||
=> _folderService.BulkUpdateFolderStructureAsync(accountId, allFolders);
|
||||
|
||||
public Task<bool> MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId)
|
||||
=> _mailService.MapLocalDraftAsync(accountId, localDraftCopyUniqueId, newMailCopyId, newDraftId, newThreadId);
|
||||
|
||||
public Task MapLocalDraftAsync(string mailCopyId, string newDraftId, string newThreadId)
|
||||
=> _mailService.MapLocalDraftAsync(mailCopyId, newDraftId, newThreadId);
|
||||
|
||||
public Task<List<MailItemFolder>> GetSynchronizationFoldersAsync(SynchronizationOptions options)
|
||||
=> _folderService.GetSynchronizationFoldersAsync(options);
|
||||
|
||||
public Task<string> UpdateFolderDeltaSynchronizationIdentifierAsync(Guid folderId, string deltaSynchronizationIdentifier)
|
||||
=> _folderService.UpdateFolderDeltaSynchronizationIdentifierAsync(folderId, deltaSynchronizationIdentifier);
|
||||
|
||||
public Task DeleteFolderAsync(Guid accountId, string remoteFolderId)
|
||||
=> _folderService.DeleteFolderAsync(accountId, remoteFolderId);
|
||||
|
||||
public Task InsertFolderAsync(MailItemFolder folder)
|
||||
=> _folderService.InsertFolderAsync(folder);
|
||||
|
||||
public Task<List<MailCopy>> GetDownloadedUnreadMailsAsync(Guid accountId, IEnumerable<string> downloadedMailCopyIds)
|
||||
=> _mailService.GetDownloadedUnreadMailsAsync(accountId, downloadedMailCopyIds);
|
||||
|
||||
public Task<IList<uint>> GetKnownUidsForFolderAsync(Guid folderId)
|
||||
=> _folderService.GetKnownUidsForFolderAsync(folderId);
|
||||
|
||||
public Task SaveMimeFileAsync(Guid fileId, MimeMessage mimeMessage, Guid accountId)
|
||||
=> _mimeFileService.SaveMimeMessageAsync(fileId, mimeMessage, accountId);
|
||||
}
|
||||
}
|
||||
141
Wino.Core/Integration/Threading/APIThreadingStrategy.cs
Normal file
141
Wino.Core/Integration/Threading/APIThreadingStrategy.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
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;
|
||||
}
|
||||
|
||||
public async Task<List<IMailItem>> ThreadItemsAsync(List<MailCopy> items)
|
||||
{
|
||||
var accountId = items.First().AssignedAccount.Id;
|
||||
|
||||
var threads = new List<ThreadMailItem>();
|
||||
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);
|
||||
|
||||
if (sentFolder == null || draftFolder == null) return default;
|
||||
|
||||
// Child -> Parent approach.
|
||||
|
||||
var potentialThreadItems = items.Distinct().Where(a => !string.IsNullOrEmpty(a.ThreadId));
|
||||
|
||||
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);
|
||||
|
||||
foreach (var potentialItem in potentialThreadItems)
|
||||
{
|
||||
if (mailLookupTable[potentialItem.Id])
|
||||
continue;
|
||||
|
||||
mailLookupTable[potentialItem.Id] = true;
|
||||
|
||||
var allThreadItems = await GetThreadItemsAsync(potentialItem.ThreadId, accountId, potentialItem.AssignedFolder, sentFolder.Id, draftFolder.Id);
|
||||
|
||||
if (allThreadItems.Count == 1)
|
||||
{
|
||||
// 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 (mailLookupTable.ContainsKey(childThreadItem.Id))
|
||||
mailLookupTable[childThreadItem.Id] = true;
|
||||
|
||||
childThreadItem.AssignedAccount = assignedAccount;
|
||||
childThreadItem.AssignedFolder = await _folderService.GetFolderAsync(childThreadItem.FolderId);
|
||||
|
||||
threadItem.AddThreadItem(childThreadItem);
|
||||
}
|
||||
|
||||
// Multiple mail copy ids from different folders are thing for Gmail.
|
||||
if (threadItem.ThreadItems.Count == 1)
|
||||
mailLookupTable[potentialItem.Id] = false;
|
||||
else
|
||||
threads.Add(threadItem);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
private async Task<List<MailCopy>> GetThreadItemsAsync(string threadId,
|
||||
Guid accountId,
|
||||
MailItemFolder threadingFolder,
|
||||
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.
|
||||
|
||||
string query = string.Empty;
|
||||
|
||||
if (threadingFolder.SpecialFolderType == SpecialFolderType.Draft || threadingFolder.SpecialFolderType == SpecialFolderType.Sent)
|
||||
{
|
||||
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}'";
|
||||
}
|
||||
|
||||
|
||||
return await _databaseService.Connection.QueryAsync<MailCopy>(query);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
Wino.Core/Integration/Threading/GmailThreadingStrategy.cs
Normal file
10
Wino.Core/Integration/Threading/GmailThreadingStrategy.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
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) { }
|
||||
}
|
||||
}
|
||||
178
Wino.Core/Integration/Threading/ImapThreadStrategy.cs
Normal file
178
Wino.Core/Integration/Threading/ImapThreadStrategy.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using SqlKata;
|
||||
using Wino.Core.Domain.Entities;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
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)
|
||||
{
|
||||
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);
|
||||
|
||||
if (sentFolder == null || draftFolder == null) return default;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
Wino.Core/Integration/Threading/OutlookThreadingStrategy.cs
Normal file
14
Wino.Core/Integration/Threading/OutlookThreadingStrategy.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
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) { }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user