Files
Wino-Mail/Wino.Core/Integration/ImapClientPool.cs
T

736 lines
25 KiB
C#
Raw Normal View History

using System;
2024-04-18 01:44:37 +02:00
using System.Collections.Concurrent;
using System.IO;
2026-02-14 12:52:17 +01:00
using System.Linq;
using System.Net;
2024-09-14 21:51:43 +02:00
using System.Net.Security;
2026-02-14 12:52:17 +01:00
using System.Reflection;
2024-09-14 21:51:43 +02:00
using System.Security.Cryptography.X509Certificates;
using System.Text;
2024-04-18 01:44:37 +02:00
using System.Threading;
using System.Threading.Channels;
2024-04-18 01:44:37 +02:00
using System.Threading.Tasks;
2026-02-14 12:52:17 +01:00
using MailKit;
2024-04-18 01:44:37 +02:00
using MailKit.Net.Imap;
using MailKit.Net.Proxy;
using MailKit.Security;
2024-09-14 21:51:43 +02:00
using MimeKit.Cryptography;
2024-04-18 01:44:37 +02:00
using Serilog;
2024-11-10 23:28:25 +01:00
using Wino.Core.Domain.Entities.Shared;
2024-04-18 01:44:37 +02:00
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
2024-09-29 21:21:51 +02:00
using Wino.Core.Domain.Models.Connectivity;
2024-04-18 01:44:37 +02:00
2025-02-16 11:54:23 +01:00
namespace Wino.Core.Integration;
2025-02-16 11:54:23 +01:00
/// <summary>
/// Connection state for tracking individual client health.
/// </summary>
public enum ImapClientState
{
Available,
InUse,
Idle,
Reconnecting,
Failed,
Disposed
}
/// <summary>
/// Provides an enhanced pooling mechanism for ImapClient with Channel-based async rental.
/// Maintains minimum active connections and a dedicated IDLE client.
2025-02-16 11:54:23 +01:00
/// </summary>
public class ImapClientPool : IDisposable
2024-04-18 01:44:37 +02:00
{
2026-02-14 12:52:17 +01:00
private const int DefaultAcquireTimeoutMs = 45_000;
private const int KeepAliveIntervalMs = 4 * 60 * 1000;
private const int MaintenanceIntervalMs = 60 * 1000;
2025-02-16 11:54:23 +01:00
private readonly ILogger _logger = Log.ForContext<ImapClientPool>();
2025-02-16 11:54:23 +01:00
private readonly CustomServerInformation _customServerInformation;
private readonly Stream _protocolLogStream;
private readonly ConcurrentDictionary<WinoImapClient, ImapClientState> _clientStates = new();
private readonly Channel<WinoImapClient> _availableClients;
private readonly CancellationTokenSource _maintenanceCts = new();
2026-02-14 12:52:17 +01:00
private readonly SemaphoreSlim _initializeSemaphore = new(1, 1);
private readonly object _idleClientLock = new();
2026-02-14 12:52:17 +01:00
private readonly ImapServerQuirkProfile _quirks;
private readonly ImapImplementation _implementation;
private readonly int _maxConnections;
private readonly int _targetMinimumConnections;
2026-02-14 12:52:17 +01:00
private DateTime _lastKeepAliveSentUtc = DateTime.MinValue;
private WinoImapClient _dedicatedIdleClient;
private bool _disposedValue;
private bool _initialized;
private Task _maintenanceTask;
public bool ThrowOnSSLHandshakeCallback { get; set; }
public ImapClientPoolOptions ImapClientPoolOptions { get; }
/// <summary>
/// Gets the current health status of the connection pool.
/// </summary>
public ConnectionPoolHealth Health => GetHealthInternal();
2025-02-16 11:54:23 +01:00
public ImapClientPool(ImapClientPoolOptions imapClientPoolOptions)
{
_customServerInformation = imapClientPoolOptions.ServerInformation;
_protocolLogStream = imapClientPoolOptions.ProtocolLog;
ImapClientPoolOptions = imapClientPoolOptions;
2026-02-14 12:52:17 +01:00
_quirks = ImapServerQuirks.Resolve(_customServerInformation.IncomingServer);
// Keep connection counts conservative by default and always cap by provider limits.
_maxConnections = CalculateMaxConnections(_customServerInformation.MaxConcurrentClients);
_targetMinimumConnections = CalculateTargetMinimumConnections(_maxConnections, _quirks.UseConservativeConnections);
_implementation = CreateImplementation();
CryptographyContext.Register(typeof(WindowsSecureMimeContext));
_availableClients = Channel.CreateUnbounded<WinoImapClient>(new UnboundedChannelOptions
{
SingleReader = false,
SingleWriter = false,
AllowSynchronousContinuations = false
});
}
/// <summary>
/// Initializes the pool by creating minimum connections and starting maintenance.
/// </summary>
public async Task InitializeAsync(CancellationToken cancellationToken = default)
{
if (_initialized) return;
2026-02-14 12:52:17 +01:00
await _initializeSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
2026-02-14 12:52:17 +01:00
if (_initialized) return;
_logger.Information("Initializing IMAP client pool with {MinimumConnections} minimum active connections (max: {MaxConnections})", _targetMinimumConnections, _maxConnections);
for (int i = 0; i < _targetMinimumConnections; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var client = await CreateAndConnectClientAsync(cancellationToken).ConfigureAwait(false);
if (client != null)
{
_clientStates[client] = ImapClientState.Available;
await _availableClients.Writer.WriteAsync(client, cancellationToken).ConfigureAwait(false);
}
}
2026-02-14 12:52:17 +01:00
if (CanCreateAdditionalConnection())
{
2026-02-14 12:52:17 +01:00
_dedicatedIdleClient = await CreateAndConnectClientAsync(cancellationToken).ConfigureAwait(false);
if (_dedicatedIdleClient != null)
{
_clientStates[_dedicatedIdleClient] = ImapClientState.Idle;
}
}
_maintenanceTask = Task.Run(() => MaintenanceLoopAsync(_maintenanceCts.Token), _maintenanceCts.Token);
_initialized = true;
_logger.Information("IMAP client pool initialized. Health: {Health}", Health.Summary);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to initialize IMAP client pool");
2026-02-14 12:52:17 +01:00
throw CreatePoolException("IMAP client pool initialization failed.", ex);
}
finally
{
_initializeSemaphore.Release();
}
}
/// <summary>
/// Pre-warms the pool (legacy compatibility method).
/// </summary>
public Task PreWarmPoolAsync() => InitializeAsync(CancellationToken.None);
/// <summary>
2026-02-14 12:52:17 +01:00
/// Rents a client from the pool with the default timeout.
/// </summary>
public Task<WinoImapClient> RentAsync(CancellationToken cancellationToken = default)
=> RentAsync(TimeSpan.FromMilliseconds(DefaultAcquireTimeoutMs), cancellationToken);
/// <summary>
/// Rents a client from the pool with explicit timeout and cancellation.
/// </summary>
2026-02-14 12:52:17 +01:00
public async Task<WinoImapClient> RentAsync(TimeSpan timeout, CancellationToken cancellationToken = default)
{
if (!_initialized)
await InitializeAsync(cancellationToken).ConfigureAwait(false);
2026-02-14 12:52:17 +01:00
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
linkedCts.CancelAfter(timeout);
var token = linkedCts.Token;
int createFailures = 0;
try
{
2026-02-14 12:52:17 +01:00
while (!token.IsCancellationRequested)
{
2026-02-14 12:52:17 +01:00
if (_availableClients.Reader.TryRead(out var pooledClient))
{
2026-02-14 12:52:17 +01:00
if (pooledClient != null && _clientStates.TryGetValue(pooledClient, out var state) && state == ImapClientState.Available)
{
2026-02-14 12:52:17 +01:00
try
{
await EnsureClientReadyAsync(pooledClient, token).ConfigureAwait(false);
_clientStates[pooledClient] = ImapClientState.InUse;
return pooledClient;
}
catch (Exception ex)
{
_logger.Warning(ex, "Pooled IMAP client was not ready. Marking as failed.");
MarkClientAsFailed(pooledClient);
}
}
2026-02-14 12:52:17 +01:00
}
if (CanCreateAdditionalConnection())
{
var newClient = await CreateAndConnectClientAsync(token).ConfigureAwait(false);
if (newClient != null)
{
2026-02-14 12:52:17 +01:00
_clientStates[newClient] = ImapClientState.InUse;
return newClient;
}
2026-02-14 12:52:17 +01:00
createFailures++;
}
2026-02-14 12:52:17 +01:00
await Task.Delay(150, token).ConfigureAwait(false);
}
2026-02-14 12:52:17 +01:00
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
throw CreatePoolException($"Timed out while acquiring an IMAP client after {timeout.TotalSeconds:F1} seconds. Failures: {createFailures}.");
}
2026-02-14 12:52:17 +01:00
throw cancellationToken.IsCancellationRequested
? new OperationCanceledException(cancellationToken)
: CreatePoolException($"Failed to acquire IMAP client within {timeout.TotalSeconds:F1} seconds. Failures: {createFailures}.");
}
/// <summary>
/// Gets a client from the pool (legacy compatibility method).
/// </summary>
2026-02-14 12:52:17 +01:00
public Task<IImapClient> GetClientAsync()
=> GetClientAsync(CancellationToken.None, null);
/// <summary>
/// Gets a client from the pool with explicit cancellation and timeout control.
/// </summary>
public async Task<IImapClient> GetClientAsync(CancellationToken cancellationToken, TimeSpan? timeout = null)
=> await RentAsync(timeout ?? TimeSpan.FromMilliseconds(DefaultAcquireTimeoutMs), cancellationToken).ConfigureAwait(false);
/// <summary>
/// Returns a client to the pool.
/// </summary>
public void Return(WinoImapClient client, bool isFaulted = false)
{
2026-02-14 12:52:17 +01:00
if (client == null || _disposedValue)
{
2026-02-14 12:52:17 +01:00
if (client != null)
DisposeClient(client);
return;
}
2026-02-14 12:52:17 +01:00
if (isFaulted || !client.IsConnected)
{
2026-02-14 12:52:17 +01:00
MarkClientAsFailed(client);
return;
}
2026-02-14 12:52:17 +01:00
_clientStates[client] = ImapClientState.Available;
_availableClients.Writer.TryWrite(client);
}
/// <summary>
/// Releases a client (legacy compatibility method).
/// </summary>
public void Release(IImapClient item, bool destroyClient = false)
{
if (item is WinoImapClient winoClient)
{
Return(winoClient, destroyClient);
}
else if (item != null)
{
DisposeClient(item);
}
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
/// <summary>
/// Gets the dedicated IDLE client. Creates one if not available.
/// </summary>
public async Task<WinoImapClient> GetIdleClientAsync(CancellationToken cancellationToken = default)
2025-02-16 11:54:23 +01:00
{
lock (_idleClientLock)
2024-04-18 01:44:37 +02:00
{
if (_dedicatedIdleClient != null && _dedicatedIdleClient.IsConnected)
2024-04-18 01:44:37 +02:00
{
return _dedicatedIdleClient;
}
}
2026-02-14 12:52:17 +01:00
if (!CanCreateAdditionalConnection())
{
_logger.Warning("Unable to allocate a dedicated IDLE client because pool is at max capacity ({MaxConnections}).", _maxConnections);
return null;
}
var idleClient = await CreateAndConnectClientAsync(cancellationToken).ConfigureAwait(false);
2026-02-14 12:52:17 +01:00
if (idleClient == null)
return null;
lock (_idleClientLock)
{
if (_dedicatedIdleClient != null)
{
2026-02-14 12:52:17 +01:00
MarkClientAsFailed(_dedicatedIdleClient);
2024-04-18 01:44:37 +02:00
}
2026-02-14 12:52:17 +01:00
_dedicatedIdleClient = idleClient;
2026-02-14 12:52:17 +01:00
_clientStates[idleClient] = ImapClientState.Idle;
}
return idleClient;
}
2025-02-16 11:54:23 +01:00
/// <summary>
/// Releases the IDLE client for reconnection.
/// </summary>
public void ReleaseIdleClient(bool isFaulted = false)
{
lock (_idleClientLock)
{
2026-02-14 12:52:17 +01:00
if (_dedicatedIdleClient == null)
return;
if (isFaulted || !_dedicatedIdleClient.IsConnected)
2025-02-16 11:43:30 +01:00
{
2026-02-14 12:52:17 +01:00
MarkClientAsFailed(_dedicatedIdleClient);
_dedicatedIdleClient = null;
return;
2025-02-16 11:43:30 +01:00
}
2026-02-14 12:52:17 +01:00
_clientStates[_dedicatedIdleClient] = ImapClientState.Idle;
2025-02-16 11:35:43 +01:00
}
}
private ConnectionPoolHealth GetHealthInternal()
{
var health = new ConnectionPoolHealth
2025-02-16 11:43:30 +01:00
{
LastHealthCheck = DateTime.UtcNow,
IdleConnectionActive = _dedicatedIdleClient?.IsConnected ?? false
};
2025-02-16 11:35:43 +01:00
foreach (var kvp in _clientStates)
2025-02-16 11:35:43 +01:00
{
health.TotalConnections++;
switch (kvp.Value)
{
case ImapClientState.Available:
health.AvailableConnections++;
break;
case ImapClientState.InUse:
health.InUseConnections++;
break;
case ImapClientState.Failed:
health.FailedConnections++;
break;
case ImapClientState.Reconnecting:
health.ReconnectingConnections++;
break;
}
2025-02-16 11:54:23 +01:00
}
return health;
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
private async Task MaintenanceLoopAsync(CancellationToken cancellationToken)
2025-02-16 11:54:23 +01:00
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
await Task.Delay(MaintenanceIntervalMs, cancellationToken).ConfigureAwait(false);
2024-04-18 01:44:37 +02:00
2026-02-14 12:52:17 +01:00
var keepAliveElapsedMs = (DateTime.UtcNow - _lastKeepAliveSentUtc).TotalMilliseconds;
if (keepAliveElapsedMs >= KeepAliveIntervalMs)
{
await SendNoOpToAvailableClientsAsync(cancellationToken).ConfigureAwait(false);
_lastKeepAliveSentUtc = DateTime.UtcNow;
}
2025-02-16 11:35:43 +01:00
await EnsureMinimumConnectionsAsync(cancellationToken).ConfigureAwait(false);
2026-02-14 12:52:17 +01:00
await CleanupFailedConnectionsAsync().ConfigureAwait(false);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
_logger.Warning(ex, "Error in pool maintenance loop");
}
}
2025-02-16 11:54:23 +01:00
}
private async Task SendNoOpToAvailableClientsAsync(CancellationToken cancellationToken)
2025-02-16 11:54:23 +01:00
{
foreach (var kvp in _clientStates)
2025-02-16 11:54:23 +01:00
{
2026-02-14 12:52:17 +01:00
if (kvp.Value != ImapClientState.Available)
continue;
if (!kvp.Key.IsConnected || kvp.Key.IsBusy())
continue;
try
{
2026-02-14 12:52:17 +01:00
await kvp.Key.NoOpAsync(cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.Debug(ex, "NOOP failed for pooled client. Marking as failed.");
MarkClientAsFailed(kvp.Key);
}
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
}
private async Task EnsureMinimumConnectionsAsync(CancellationToken cancellationToken)
2025-02-16 11:54:23 +01:00
{
2026-02-14 12:52:17 +01:00
var availableConnections = _clientStates.Count(kvp => kvp.Value == ImapClientState.Available);
var neededConnections = _targetMinimumConnections - availableConnections;
2026-02-14 12:52:17 +01:00
if (neededConnections <= 0)
return;
for (int i = 0; i < neededConnections; i++)
2024-04-18 01:44:37 +02:00
{
2026-02-14 12:52:17 +01:00
if (!CanCreateAdditionalConnection())
break;
2026-02-14 12:52:17 +01:00
try
2024-04-18 01:44:37 +02:00
{
2026-02-14 12:52:17 +01:00
var client = await CreateAndConnectClientAsync(cancellationToken).ConfigureAwait(false);
if (client == null)
continue;
_clientStates[client] = ImapClientState.Available;
await _availableClients.Writer.WriteAsync(client, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.Warning(ex, "Failed to create minimum pool connection during maintenance.");
break;
2025-02-16 11:54:23 +01:00
}
}
}
2026-02-14 12:52:17 +01:00
private Task CleanupFailedConnectionsAsync()
{
foreach (var kvp in _clientStates)
{
2026-02-14 12:52:17 +01:00
if (kvp.Value != ImapClientState.Failed && kvp.Value != ImapClientState.Disposed)
continue;
DisposeClient(kvp.Key);
_clientStates.TryRemove(kvp.Key, out _);
2025-02-16 11:54:23 +01:00
}
2026-02-14 12:52:17 +01:00
return Task.CompletedTask;
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
private async Task<WinoImapClient> CreateAndConnectClientAsync(CancellationToken cancellationToken)
2025-02-16 11:54:23 +01:00
{
var client = CreateNewClient();
2024-09-14 21:51:43 +02:00
try
2025-02-16 11:54:23 +01:00
{
await EnsureClientReadyAsync(client, cancellationToken).ConfigureAwait(false);
return client;
}
catch (Exception ex)
{
2026-02-14 12:52:17 +01:00
_logger.Warning(ex, "Failed to create and connect IMAP client.");
DisposeClient(client);
return null;
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:43:30 +01:00
private async Task EnsureClientReadyAsync(WinoImapClient client, CancellationToken cancellationToken)
{
if (!client.IsConnected)
2025-02-16 11:54:23 +01:00
{
client.ServerCertificateValidationCallback = MyServerCertificateValidationCallback;
2025-02-16 11:43:30 +01:00
await client.ConnectAsync(
_customServerInformation.IncomingServer,
int.Parse(_customServerInformation.IncomingServerPort),
GetSocketOptions(_customServerInformation.IncomingServerSocketOption),
cancellationToken).ConfigureAwait(false);
if (client.Capabilities.HasFlag(ImapCapabilities.Compress))
{
try
{
2026-02-14 12:52:17 +01:00
await client.CompressAsync(cancellationToken).ConfigureAwait(false);
}
2026-02-14 12:52:17 +01:00
catch (Exception ex)
{
2026-02-14 12:52:17 +01:00
_logger.Debug(ex, "Failed to enable IMAP compression. Continuing without compression.");
}
}
2026-02-14 12:52:17 +01:00
await TryIdentifyAsync(client, cancellationToken).ConfigureAwait(false);
}
2024-09-29 21:21:51 +02:00
if (!client.IsAuthenticated)
2024-09-29 21:21:51 +02:00
{
var cred = new NetworkCredential(
_customServerInformation.IncomingServerUsername,
_customServerInformation.IncomingServerPassword);
2024-09-29 21:21:51 +02:00
var authMethod = _customServerInformation.IncomingAuthenticationMethod;
if (authMethod != ImapAuthenticationMethod.Auto)
2025-02-15 12:53:32 +01:00
{
client.AuthenticationMechanisms.Clear();
var saslMechanism = GetSASLAuthenticationMethodName(authMethod);
client.AuthenticationMechanisms.Add(saslMechanism);
await client.AuthenticateAsync(SaslMechanism.Create(saslMechanism, cred), cancellationToken).ConfigureAwait(false);
2025-02-15 12:53:32 +01:00
}
2025-02-16 11:54:23 +01:00
else
2025-02-16 11:43:30 +01:00
{
await client.AuthenticateAsync(cred, cancellationToken).ConfigureAwait(false);
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:35:43 +01:00
2026-02-14 12:52:17 +01:00
await TryIdentifyAsync(client, cancellationToken).ConfigureAwait(false);
client.IsQResyncEnabled = false;
if (!_quirks.DisableQResync && client.Capabilities.HasFlag(ImapCapabilities.QuickResync))
{
try
{
2026-02-14 12:52:17 +01:00
await client.EnableQuickResyncAsync(cancellationToken).ConfigureAwait(false);
client.IsQResyncEnabled = true;
}
catch (Exception ex)
{
_logger.Debug(ex, "Failed to enable QRESYNC for {Server}. Falling back to non-QRESYNC synchronization.", _customServerInformation.IncomingServer);
}
}
2026-02-14 12:52:17 +01:00
}
}
2026-02-14 12:52:17 +01:00
private async Task TryIdentifyAsync(WinoImapClient client, CancellationToken cancellationToken)
{
if (!client.Capabilities.HasFlag(ImapCapabilities.Id))
return;
try
{
await client.IdentifyAsync(_implementation, cancellationToken).ConfigureAwait(false);
}
catch (ImapCommandException)
{
// Some servers refuse ID even if advertised. Ignore and continue.
}
catch (Exception ex)
{
_logger.Debug(ex, "Failed to send IMAP ID payload. Continuing without Identify().");
}
}
private WinoImapClient CreateNewClient()
{
2026-02-14 12:52:17 +01:00
IProtocolLogger protocolLogger = null;
if (_protocolLogStream != null)
{
protocolLogger = new ProtocolLogger(_protocolLogStream, leaveOpen: true);
}
var client = protocolLogger != null ? new WinoImapClient(protocolLogger) : new WinoImapClient();
2024-09-14 21:51:43 +02:00
if (!string.IsNullOrEmpty(_customServerInformation.ProxyServer))
{
client.ProxyClient = new HttpProxyClient(
_customServerInformation.ProxyServer,
int.Parse(_customServerInformation.ProxyServerPort));
}
2025-02-16 11:54:23 +01:00
2026-02-14 12:52:17 +01:00
_logger.Debug("Created new IMAP client. Current tracked pool size: {Count}", _clientStates.Count);
return client;
}
private void DisposeClient(IImapClient client)
2025-02-16 11:54:23 +01:00
{
2026-02-14 12:52:17 +01:00
if (client == null)
return;
2025-02-16 11:54:23 +01:00
try
{
if (client.IsConnected)
{
lock (client.SyncRoot)
{
client.Disconnect(quit: true);
}
}
2026-02-14 12:52:17 +01:00
client.Dispose();
2025-02-16 11:54:23 +01:00
}
catch (Exception ex)
2025-02-16 11:54:23 +01:00
{
2026-02-14 12:52:17 +01:00
_logger.Debug(ex, "Error disposing IMAP client.");
2025-02-16 11:35:43 +01:00
}
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2026-02-14 12:52:17 +01:00
private void MarkClientAsFailed(WinoImapClient client)
{
if (client == null)
return;
_clientStates[client] = ImapClientState.Failed;
}
private bool CanCreateAdditionalConnection()
{
var activeCount = _clientStates.Count(kvp => kvp.Value != ImapClientState.Failed && kvp.Value != ImapClientState.Disposed);
return activeCount < _maxConnections;
}
private ImapClientPoolException CreatePoolException(string message, Exception innerException = null)
{
var protocolLog = GetProtocolLogContent() ?? string.Empty;
return innerException == null
? new ImapClientPoolException(message, _customServerInformation, protocolLog)
: new ImapClientPoolException(innerException, protocolLog);
}
private static ImapImplementation CreateImplementation()
{
var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown";
return new ImapImplementation
{
Name = "Wino Mail",
Version = version,
Vendor = "Wino",
OS = Environment.OSVersion.VersionString,
SupportUrl = "https://www.winomail.app"
};
}
public static int CalculateMaxConnections(int configuredMaxConcurrentClients)
=> Math.Clamp(configuredMaxConcurrentClients <= 0 ? 5 : configuredMaxConcurrentClients, 1, 10);
public static int CalculateTargetMinimumConnections(int maxConnections, bool useConservativeConnections)
=> useConservativeConnections ? 1 : Math.Min(2, Math.Max(1, maxConnections));
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
};
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 bool MyServerCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
2025-02-16 11:54:23 +01:00
{
if (sslPolicyErrors == SslPolicyErrors.None) return true;
if (ThrowOnSSLHandshakeCallback)
2025-02-16 11:43:30 +01:00
{
throw new ImapTestSSLCertificateException(
certificate.Issuer,
certificate.GetExpirationDateString(),
certificate.GetEffectiveDateString());
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:54:23 +01:00
return true;
}
public string GetProtocolLogContent()
{
if (_protocolLogStream == null) return default;
if (_protocolLogStream.CanSeek)
_protocolLogStream.Seek(0, SeekOrigin.Begin);
using var reader = new StreamReader(_protocolLogStream, Encoding.UTF8, true, 1024, leaveOpen: true);
return reader.ReadToEnd();
}
// Legacy compatibility methods
public Task<bool> EnsureConnectedAsync(IImapClient client) =>
Task.FromResult(client.IsConnected);
public Task EnsureAuthenticatedAsync(IImapClient client) =>
Task.CompletedTask;
2025-02-16 11:54:23 +01:00
protected virtual void Dispose(bool disposing)
{
2026-02-14 12:52:17 +01:00
if (_disposedValue)
return;
if (disposing)
2025-02-16 11:54:23 +01:00
{
2026-02-14 12:52:17 +01:00
_maintenanceCts.Cancel();
_maintenanceTask?.Wait(TimeSpan.FromSeconds(5));
_maintenanceCts.Dispose();
_initializeSemaphore.Dispose();
2026-02-14 12:52:17 +01:00
_availableClients.Writer.Complete();
2026-02-14 12:52:17 +01:00
foreach (var kvp in _clientStates)
{
DisposeClient(kvp.Key);
}
2026-02-14 12:52:17 +01:00
_clientStates.Clear();
lock (_idleClientLock)
{
_dedicatedIdleClient = null;
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:35:43 +01:00
2026-02-14 12:52:17 +01:00
_protocolLogStream?.Dispose();
2025-02-16 11:43:30 +01:00
}
2026-02-14 12:52:17 +01:00
_disposedValue = true;
2025-02-16 11:35:43 +01:00
}
2025-02-16 11:54:23 +01:00
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
2024-04-18 01:44:37 +02:00
}