Cleaning up the solution. Separating Shared.WinRT, Services and Synchronization. Removing synchronization from app. Reducing bundle size by 45mb.

This commit is contained in:
Burak Kaan Köse
2024-07-21 05:45:02 +02:00
parent f112f369a7
commit 495885e006
523 changed files with 2254 additions and 2375 deletions

View File

@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using MailKit.Net.Imap;
using MoreLinq;
using Wino.Domain.Interfaces;
using Wino.Services.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);
}
public IEnumerable<IRequestBundle<TNativeRequestType>> CreateHttpBundleWithResponse<TResponse>(
IRequestBase item,
Func<IRequestBase, TNativeRequestType> action)
{
yield return new HttpRequestBundle<TNativeRequestType, TResponse>(action(item), item);
}
/// <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)];
}
}
}

View File

@@ -0,0 +1,281 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MailKit;
using MailKit.Net.Imap;
using MailKit.Net.Proxy;
using MailKit.Security;
using MoreLinq;
using Serilog;
using Wino.Domain.Exceptions;
using Wino.Domain.Entities;
using Wino.Domain.Enums;
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 : IDisposable
{
// 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 readonly int MinimumPoolSize = 5;
private readonly ConcurrentStack<ImapClient> _clients = [];
private readonly SemaphoreSlim _semaphore;
private readonly CustomServerInformation _customServerInformation;
private readonly Stream _protocolLogStream;
private readonly ILogger _logger = Log.ForContext<ImapClientPool>();
public ImapClientPool(CustomServerInformation customServerInformation, Stream protocolLogStream = null)
{
_customServerInformation = customServerInformation;
_protocolLogStream = protocolLogStream;
// Set the maximum pool size to 5 or the custom value if it's greater.
_semaphore = new(Math.Max(MinimumPoolSize, customServerInformation.MaxConcurrentClients));
}
private async Task EnsureConnectivityAsync(ImapClient client, bool isCreatedNew)
{
try
{
await EnsureConnectedAsync(client);
bool mustDoPostAuthIdentification = false;
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.
// Some servers require it pre-authentication, some post-authentication.
// We'll observe the response here and do it after authentication if needed.
if (client.Capabilities.HasFlag(ImapCapabilities.Id))
{
try
{
await client.IdentifyAsync(_implementation);
}
catch (ImapCommandException commandException) when (commandException.Response == ImapCommandResponse.No || commandException.Response == ImapCommandResponse.Bad)
{
mustDoPostAuthIdentification = true;
}
catch (Exception)
{
throw;
}
}
}
await EnsureAuthenticatedAsync(client);
if (isCreatedNew && client.IsAuthenticated)
{
if (mustDoPostAuthIdentification) await client.IdentifyAsync(_implementation);
// Activate post-auth capabilities.
if (client.Capabilities.HasFlag(ImapCapabilities.QuickResync))
await client.EnableQuickResyncAsync();
}
}
catch (Exception ex)
{
throw new ImapClientPoolException(ex, GetProtocolLogContent());
}
finally
{
// Release it even if it fails.
_semaphore.Release();
}
}
public string GetProtocolLogContent()
{
if (_protocolLogStream == null) return default;
// Set the position to the beginning of the stream in case it is not already at the start
if (_protocolLogStream.CanSeek)
_protocolLogStream.Seek(0, SeekOrigin.Begin);
using var reader = new StreamReader(_protocolLogStream, Encoding.UTF8, true, 1024, leaveOpen: true);
return reader.ReadToEnd();
}
public async Task<ImapClient> GetClientAsync()
{
await _semaphore.WaitAsync();
if (_clients.TryPop(out ImapClient item))
{
await EnsureConnectivityAsync(item, false);
return item;
}
var client = CreateNewClient();
await EnsureConnectivityAsync(client, true);
return client;
}
public void Release(ImapClient item, bool destroyClient = false)
{
if (item != null)
{
if (destroyClient)
{
lock (item.SyncRoot)
{
item.Disconnect(true);
}
item.Dispose();
}
else
{
_clients.Push(item);
}
_semaphore.Release();
}
}
public void DestroyClient(ImapClient client)
{
if (client == null) return;
client.Disconnect(true);
client.Dispose();
}
private ImapClient CreateNewClient()
{
ImapClient client = null;
// Make sure to create a ImapClient with a protocol logger if enabled.
client = _protocolLogStream != null
? new ImapClient(new ProtocolLogger(_protocolLogStream))
: 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;
var cred = new NetworkCredential(_customServerInformation.IncomingServerUsername, _customServerInformation.IncomingServerPassword);
var prefferedAuthenticationMethod = _customServerInformation.IncomingAuthenticationMethod;
if (prefferedAuthenticationMethod != ImapAuthenticationMethod.Auto)
{
// Anything beside Auto must be explicitly set for the client.
client.AuthenticationMechanisms.Clear();
var saslMechanism = GetSASLAuthenticationMethodName(prefferedAuthenticationMethod);
client.AuthenticationMechanisms.Add(saslMechanism);
await client.AuthenticateAsync(SaslMechanism.Create(saslMechanism, cred));
}
else
{
await client.AuthenticateAsync(cred);
}
}
private string GetSASLAuthenticationMethodName(ImapAuthenticationMethod method)
{
return method switch
{
ImapAuthenticationMethod.NormalPassword => "PLAIN",
ImapAuthenticationMethod.EncryptedPassword => "LOGIN",
ImapAuthenticationMethod.Ntlm => "NTLM",
ImapAuthenticationMethod.CramMd5 => "CRAM-MD5",
ImapAuthenticationMethod.DigestMd5 => "DIGEST-MD5",
_ => "PLAIN"
};
}
public void Dispose()
{
_clients.ForEach(client =>
{
lock (client.SyncRoot)
{
client.Disconnect(true);
}
});
_clients.ForEach(client =>
{
client.Dispose();
});
_clients.Clear();
if (_protocolLogStream != null)
{
_protocolLogStream.Dispose();
}
}
}
}