Refactoring ImapClientPool. Implemented no-op timer and pre-warmup clients logic. Disabled protocol log per-account.

This commit is contained in:
Burak Kaan Köse
2025-02-26 23:11:16 +01:00
parent bd7f7b867e
commit b98fc91a99

View File

@@ -7,7 +7,7 @@ using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MailKit;
using System.Timers;
using MailKit.Net.Imap;
using MailKit.Net.Proxy;
using MailKit.Security;
@@ -20,7 +20,6 @@ using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Models.Connectivity;
namespace Wino.Core.Integration;
/// <summary>
/// Provides a pooling mechanism for ImapClient.
/// Makes sure that we don't have too many connections to the server.
@@ -34,7 +33,6 @@ public class ImapClientPool : IDisposable
// 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()
{
Version = "1.8.0",
@@ -46,16 +44,19 @@ public class ImapClientPool : IDisposable
public bool ThrowOnSSLHandshakeCallback { get; set; }
public ImapClientPoolOptions ImapClientPoolOptions { get; }
internal WinoImapClient IdleClient { get; set; }
private bool _disposedValue;
private readonly int MinimumPoolSize = 5;
private readonly ConcurrentStack<IImapClient> _clients = [];
private readonly ConcurrentStack<IImapClient> _clients = new();
private readonly SemaphoreSlim _semaphore;
private readonly CustomServerInformation _customServerInformation;
private readonly Stream _protocolLogStream;
private readonly ILogger _logger = Log.ForContext<ImapClientPool>();
private bool _disposedValue;
private readonly System.Timers.Timer _keepAliveTimer;
private readonly System.Timers.Timer _connectionMonitorTimer;
private const int KeepAliveInterval = 4 * 60 * 1000; // 4 minutes
private const int ConnectionMonitorInterval = 30 * 1000; // 30 seconds
public ImapClientPool(ImapClientPoolOptions imapClientPoolOptions)
{
@@ -67,19 +68,93 @@ public class ImapClientPool : IDisposable
CryptographyContext.Register(typeof(WindowsSecureMimeContext));
ImapClientPoolOptions = imapClientPoolOptions;
_keepAliveTimer = new System.Timers.Timer(KeepAliveInterval);
_connectionMonitorTimer = new System.Timers.Timer(ConnectionMonitorInterval);
_keepAliveTimer.Elapsed += KeepAliveTimerElapsed;
_connectionMonitorTimer.Elapsed += ConnectionMonitorTimerElapsed;
}
public async Task PreWarmPoolAsync()
{
try
{
for (int i = 0; i < MinimumPoolSize; i++)
{
var client = CreateNewClient();
await EnsureCapabilitiesAsync(client, true);
_clients.Push(client);
}
// Start monitoring timers after pool is warmed
_keepAliveTimer.Start();
_connectionMonitorTimer.Start();
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to pre-warm client pool");
}
}
private async void KeepAliveTimerElapsed(object sender, ElapsedEventArgs e)
{
foreach (var client in _clients)
{
try
{
if (client.IsConnected && !((WinoImapClient)client).IsBusy())
{
await SendNoOpAsync(client);
}
}
catch (Exception ex)
{
_logger.Warning(ex, "Failed to send NOOP to client");
}
}
}
private async void ConnectionMonitorTimerElapsed(object sender, ElapsedEventArgs e)
{
foreach (var client in _clients)
{
try
{
if (!client.IsConnected && !((WinoImapClient)client).IsBusy())
{
await EnsureCapabilitiesAsync(client, false);
}
}
catch (Exception ex)
{
_logger.Warning(ex, "Failed to reconnect client");
}
}
}
private async Task SendNoOpAsync(IImapClient client)
{
try
{
await client.NoOpAsync();
}
catch (Exception ex)
{
_logger.Warning(ex, "NOOP command failed");
}
}
/// <summary>
/// Ensures all supported capabilities are enabled in this connection.
/// Reconnects and reauthenticates if necessary.
/// </summary>
/// </summary>
/// <param name="isCreatedNew">Whether the client has been newly created.</param>
private async Task EnsureCapabilitiesAsync(IImapClient client, bool isCreatedNew)
{
try
{
bool isReconnected = await EnsureConnectedAsync(client);
bool mustDoPostAuthIdentification = false;
if ((isCreatedNew || isReconnected) && client.IsConnected)
@@ -118,7 +193,6 @@ public class ImapClientPool : IDisposable
if (client.Capabilities.HasFlag(ImapCapabilities.QuickResync))
{
await client.EnableQuickResyncAsync().ConfigureAwait(false);
if (client is WinoImapClient winoImapClient) winoImapClient.IsQResyncEnabled = true;
}
}
@@ -180,8 +254,6 @@ public class ImapClientPool : IDisposable
item.Disconnect(quit: true);
}
}
_clients.TryPop(out _);
item.Dispose();
}
else if (!_disposedValue)
@@ -197,11 +269,7 @@ public class ImapClientPool : IDisposable
{
WinoImapClient client = null;
// Make sure to create a ImapClient with a protocol logger if enabled.
client = _protocolLogStream != null
? new WinoImapClient(new ProtocolLogger(_protocolLogStream))
: new WinoImapClient();
client = new WinoImapClient();
HttpProxyClient proxyClient = null;
@@ -209,10 +277,9 @@ public class ImapClientPool : IDisposable
if (!string.IsNullOrEmpty(_customServerInformation.ProxyServer))
{
proxyClient = new HttpProxyClient(_customServerInformation.ProxyServer, int.Parse(_customServerInformation.ProxyServerPort));
client.ProxyClient = proxyClient;
}
client.ProxyClient = proxyClient;
_logger.Debug("Creating new ImapClient. Current clients: {Count}", _clients.Count);
return client;
@@ -256,13 +323,43 @@ public class ImapClientPool : IDisposable
}
return true;
}
public async Task EnsureAuthenticatedAsync(IImapClient 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)
=> method switch
{
ImapAuthenticationMethod.NormalPassword => "PLAIN",
ImapAuthenticationMethod.EncryptedPassword => "LOGIN",
ImapAuthenticationMethod.Ntlm => "NTLM",
ImapAuthenticationMethod.CramMd5 => "CRAM-MD5",
ImapAuthenticationMethod.DigestMd5 => "DIGEST-MD5",
_ => "PLAIN"
};
private void WriteToProtocolLog(string message)
{
if (_protocolLogStream == null) return;
try
{
var messageBytes = Encoding.UTF8.GetBytes($"W: {message}\n");
@@ -274,7 +371,6 @@ public class ImapClientPool : IDisposable
}
catch (Exception)
{
throw;
}
}
@@ -293,66 +389,28 @@ public class ImapClientPool : IDisposable
return true;
}
public async Task EnsureAuthenticatedAsync(IImapClient 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);
var mechanism = SaslMechanism.Create(saslMechanism, cred);
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"
};
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_keepAliveTimer.Stop();
_connectionMonitorTimer.Stop();
_keepAliveTimer.Dispose();
_connectionMonitorTimer.Dispose();
_clients.ForEach(client =>
{
lock (client.SyncRoot)
{
client.Disconnect(true);
}
});
_clients.ForEach(client =>
{
client.Dispose();
});
_clients.Clear();
_protocolLogStream?.Dispose();
}
_disposedValue = true;