2026-02-06 01:18:12 +01:00
using System ;
2024-04-18 01:44:37 +02:00
using System.Collections.Concurrent ;
2026-02-14 12:52:17 +01:00
using System.Linq ;
2024-06-21 01:13:25 +02:00
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 ;
2024-04-18 01:44:37 +02:00
using System.Threading ;
2026-02-06 01:18:12 +01:00
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 ;
2026-02-06 01:18:12 +01:00
2025-02-16 11:54:23 +01:00
/// <summary>
2026-02-06 01:18:12 +01:00
/// 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
2026-02-06 01:18:12 +01:00
private readonly ILogger _logger = Log . ForContext < ImapClientPool > ( ) ;
2025-02-16 11:54:23 +01:00
private readonly CustomServerInformation _customServerInformation ;
2026-02-06 01:18:12 +01:00
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 ) ;
2026-02-06 01:18:12 +01:00
private readonly object _idleClientLock = new ( ) ;
2026-02-15 02:20:18 +01:00
private readonly object _initialWarmupLock = 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-06 01:18:12 +01:00
2026-02-14 12:52:17 +01:00
private DateTime _lastKeepAliveSentUtc = DateTime . MinValue ;
2026-02-06 01:18:12 +01:00
private WinoImapClient _dedicatedIdleClient ;
private bool _disposedValue ;
private bool _initialized ;
private Task _maintenanceTask ;
2026-02-15 02:20:18 +01:00
private Task _initialWarmupTask = Task . CompletedTask ;
2025-02-26 23:11:16 +01:00
2026-02-06 01:18:12 +01:00
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 ;
ImapClientPoolOptions = imapClientPoolOptions ;
2025-02-26 23:11:16 +01:00
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 ( ) ;
2026-02-06 01:18:12 +01:00
CryptographyContext . Register ( typeof ( WindowsSecureMimeContext ) ) ;
2025-02-26 23:11:16 +01:00
2026-02-06 01:18:12 +01:00
_availableClients = Channel . CreateUnbounded < WinoImapClient > ( new UnboundedChannelOptions
{
SingleReader = false ,
SingleWriter = false ,
AllowSynchronousContinuations = false
} ) ;
2025-02-26 23:11:16 +01:00
}
2026-02-06 01:18:12 +01:00
/// <summary>
/// Initializes the pool by creating minimum connections and starting maintenance.
/// </summary>
public async Task InitializeAsync ( CancellationToken cancellationToken = default )
2025-02-26 23:11:16 +01:00
{
2026-02-06 01:18:12 +01:00
if ( _initialized ) return ;
2026-02-14 12:52:17 +01:00
await _initializeSemaphore . WaitAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
2026-02-06 01:18:12 +01:00
2025-02-26 23:11:16 +01:00
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 ) ;
2026-02-15 02:20:18 +01:00
// Fast-path startup: create one client eagerly so first RentAsync() is not blocked by full warm-up.
var initialClient = await CreateAndConnectClientAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
if ( initialClient = = null )
2026-02-06 01:18:12 +01:00
{
2026-02-15 02:20:18 +01:00
throw CreatePoolException ( "Failed to create initial IMAP connection for the pool." ) ;
2026-02-06 01:18:12 +01:00
}
2026-02-15 02:20:18 +01:00
_clientStates [ initialClient ] = ImapClientState . Available ;
await _availableClients . Writer . WriteAsync ( initialClient , cancellationToken ) . ConfigureAwait ( false ) ;
2025-02-26 23:11:16 +01:00
2026-02-06 01:18:12 +01:00
_maintenanceTask = Task . Run ( ( ) = > MaintenanceLoopAsync ( _maintenanceCts . Token ) , _maintenanceCts . Token ) ;
_initialized = true ;
2026-02-15 02:20:18 +01:00
ScheduleInitialWarmup ( ) ;
2026-02-06 01:18:12 +01:00
_logger . Information ( "IMAP client pool initialized. Health: {Health}" , Health . Summary ) ;
2025-02-26 23:11:16 +01:00
}
catch ( Exception ex )
{
2026-02-06 01:18:12 +01:00
_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 ( ) ;
2025-02-26 23:11:16 +01:00
}
}
2026-02-06 01:18:12 +01:00
/// <summary>
/// Pre-warms the pool (legacy compatibility method).
/// </summary>
2026-02-15 02:20:18 +01:00
public async Task PreWarmPoolAsync ( )
{
await InitializeAsync ( CancellationToken . None ) . ConfigureAwait ( false ) ;
Task warmupTask ;
lock ( _initialWarmupLock )
{
warmupTask = _initialWarmupTask ;
}
if ( warmupTask ! = null )
{
await warmupTask . ConfigureAwait ( false ) ;
}
}
2026-02-06 01:18:12 +01:00
/// <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.
2026-02-06 01:18:12 +01:00
/// </summary>
2026-02-14 12:52:17 +01:00
public async Task < WinoImapClient > RentAsync ( TimeSpan timeout , CancellationToken cancellationToken = default )
2025-02-26 23:11:16 +01:00
{
2026-02-06 01:18:12 +01:00
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
2025-02-26 23:11:16 +01:00
{
2026-02-14 12:52:17 +01:00
while ( ! token . IsCancellationRequested )
2025-02-26 23:11:16 +01:00
{
2026-02-14 12:52:17 +01:00
if ( _availableClients . Reader . TryRead ( out var pooledClient ) )
2025-02-26 23:11:16 +01:00
{
2026-02-14 12:52:17 +01:00
if ( pooledClient ! = null & & _clientStates . TryGetValue ( pooledClient , out var state ) & & state = = ImapClientState . Available )
2026-02-06 01:18:12 +01:00
{
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-06 01:18:12 +01:00
}
2026-02-14 12:52:17 +01:00
}
if ( CanCreateAdditionalConnection ( ) )
{
var newClient = await CreateAndConnectClientAsync ( token ) . ConfigureAwait ( false ) ;
if ( newClient ! = null )
2026-02-06 01:18:12 +01:00
{
2026-02-14 12:52:17 +01:00
_clientStates [ newClient ] = ImapClientState . InUse ;
return newClient ;
2026-02-06 01:18:12 +01:00
}
2026-02-14 12:52:17 +01:00
createFailures + + ;
2025-02-26 23:11:16 +01:00
}
2026-02-06 01:18:12 +01:00
2026-02-14 12:52:17 +01:00
await Task . Delay ( 150 , token ) . ConfigureAwait ( false ) ;
2025-02-26 23:11:16 +01:00
}
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}." ) ;
2025-02-26 23:11:16 +01:00
}
2026-02-06 01:18:12 +01:00
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}." ) ;
2025-02-26 23:11:16 +01:00
}
2026-02-06 01:18:12 +01:00
/// <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 ) ;
2026-02-06 01:18:12 +01:00
/// <summary>
/// Returns a client to the pool.
/// </summary>
public void Return ( WinoImapClient client , bool isFaulted = false )
2025-02-26 23:11:16 +01:00
{
2026-02-14 12:52:17 +01:00
if ( client = = null | | _disposedValue )
2025-02-26 23:11:16 +01:00
{
2026-02-14 12:52:17 +01:00
if ( client ! = null )
DisposeClient ( client ) ;
2026-02-06 01:18:12 +01:00
return ;
}
2026-02-14 12:52:17 +01:00
if ( isFaulted | | ! client . IsConnected )
2026-02-06 01:18:12 +01:00
{
2026-02-14 12:52:17 +01:00
MarkClientAsFailed ( client ) ;
return ;
2025-02-26 23:11:16 +01:00
}
2026-02-14 12:52:17 +01:00
_clientStates [ client ] = ImapClientState . Available ;
_availableClients . Writer . TryWrite ( client ) ;
2025-02-26 23:11:16 +01:00
}
2026-02-06 01:18:12 +01:00
/// <summary>
/// Releases a client (legacy compatibility method).
/// </summary>
public void Release ( IImapClient item , bool destroyClient = false )
2025-02-26 23:11:16 +01:00
{
2026-02-06 01:18:12 +01:00
if ( item is WinoImapClient winoClient )
2025-02-26 23:11:16 +01:00
{
2026-02-06 01:18:12 +01:00
Return ( winoClient , destroyClient ) ;
2025-02-26 23:11:16 +01:00
}
2026-02-06 01:18:12 +01:00
else if ( item ! = null )
2025-02-26 23:11:16 +01:00
{
2026-02-06 01:18:12 +01:00
DisposeClient ( item ) ;
2025-02-26 23:11:16 +01:00
}
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>
2026-02-06 01:18:12 +01:00
/// 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
{
2026-02-06 01:18:12 +01:00
lock ( _idleClientLock )
2024-04-18 01:44:37 +02:00
{
2026-02-06 01:18:12 +01:00
if ( _dedicatedIdleClient ! = null & & _dedicatedIdleClient . IsConnected )
2024-04-18 01:44:37 +02:00
{
2026-02-06 01:18:12 +01:00
return _dedicatedIdleClient ;
}
}
2024-11-30 11:48:05 +01:00
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 ;
}
2026-02-06 01:18:12 +01:00
var idleClient = await CreateAndConnectClientAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
2026-02-14 12:52:17 +01:00
if ( idleClient = = null )
return null ;
2024-06-18 02:22:55 +02:00
2026-02-06 01:18:12 +01:00
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
2026-02-06 01:18:12 +01:00
_dedicatedIdleClient = idleClient ;
2026-02-14 12:52:17 +01:00
_clientStates [ idleClient ] = ImapClientState . Idle ;
2026-02-06 01:18:12 +01:00
}
2024-06-17 02:16:06 +02:00
2026-02-06 01:18:12 +01:00
return idleClient ;
}
2025-02-16 11:54:23 +01:00
2026-02-06 01:18:12 +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
}
2026-02-06 01:18:12 +01:00
}
private ConnectionPoolHealth GetHealthInternal ( )
{
var health = new ConnectionPoolHealth
2025-02-16 11:43:30 +01:00
{
2026-02-06 01:18:12 +01:00
LastHealthCheck = DateTime . UtcNow ,
IdleConnectionActive = _dedicatedIdleClient ? . IsConnected ? ? false
} ;
2025-02-16 11:35:43 +01:00
2026-02-06 01:18:12 +01:00
foreach ( var kvp in _clientStates )
2025-02-16 11:35:43 +01:00
{
2026-02-06 01:18:12 +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
}
2026-02-06 01:18:12 +01:00
return health ;
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2026-02-06 01:18:12 +01:00
private async Task MaintenanceLoopAsync ( CancellationToken cancellationToken )
2025-02-16 11:54:23 +01:00
{
2026-02-06 01:18:12 +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
2026-02-06 01:18:12 +01:00
await EnsureMinimumConnectionsAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
2026-02-14 12:52:17 +01:00
await CleanupFailedConnectionsAsync ( ) . ConfigureAwait ( false ) ;
2026-02-06 01:18:12 +01:00
}
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
}
2026-02-06 01:18:12 +01:00
private async Task SendNoOpToAvailableClientsAsync ( CancellationToken cancellationToken )
2025-02-16 11:54:23 +01:00
{
2026-02-06 01:18:12 +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-06 01:18:12 +01:00
{
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 ) ;
2026-02-06 01:18:12 +01:00
}
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
}
2026-02-06 01:18:12 +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-06 01:18:12 +01:00
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-06 01:18:12 +01:00
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-06 01:18:12 +01:00
}
}
2026-02-15 02:20:18 +01:00
private void ScheduleInitialWarmup ( )
{
lock ( _initialWarmupLock )
{
if ( _initialWarmupTask ! = null & & ! _initialWarmupTask . IsCompleted )
return ;
_initialWarmupTask = Task . Run ( ( ) = > EnsureWarmBaselineAsync ( _maintenanceCts . Token ) , _maintenanceCts . Token ) ;
}
}
private async Task EnsureWarmBaselineAsync ( CancellationToken cancellationToken )
{
try
{
await EnsureMinimumConnectionsAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
lock ( _idleClientLock )
{
if ( _dedicatedIdleClient ! = null & & _dedicatedIdleClient . IsConnected )
return ;
}
if ( ! CanCreateAdditionalConnection ( ) )
return ;
var idleCandidate = await CreateAndConnectClientAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
if ( idleCandidate = = null )
return ;
bool assignedAsIdle = false ;
lock ( _idleClientLock )
{
if ( _dedicatedIdleClient = = null | | ! _dedicatedIdleClient . IsConnected )
{
_dedicatedIdleClient = idleCandidate ;
_clientStates [ idleCandidate ] = ImapClientState . Idle ;
assignedAsIdle = true ;
}
}
if ( ! assignedAsIdle )
{
_clientStates [ idleCandidate ] = ImapClientState . Available ;
_availableClients . Writer . TryWrite ( idleCandidate ) ;
}
}
catch ( OperationCanceledException ) when ( cancellationToken . IsCancellationRequested )
{
// Pool is shutting down.
}
catch ( Exception ex )
{
_logger . Warning ( ex , "Initial IMAP pool warm-up failed. Pool will continue with maintenance recovery." ) ;
}
}
2026-02-14 12:52:17 +01:00
private Task CleanupFailedConnectionsAsync ( )
2026-02-06 01:18:12 +01:00
{
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
2026-02-06 01:18:12 +01:00
return Task . CompletedTask ;
2025-02-16 11:54:23 +01:00
}
2024-04-18 01:44:37 +02:00
2026-02-06 01:18:12 +01:00
private async Task < WinoImapClient > CreateAndConnectClientAsync ( CancellationToken cancellationToken )
2025-02-16 11:54:23 +01:00
{
2026-02-06 01:18:12 +01:00
var client = CreateNewClient ( ) ;
2024-09-14 21:51:43 +02:00
2026-02-06 01:18:12 +01:00
try
2025-02-16 11:54:23 +01:00
{
2026-02-06 01:18:12 +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." ) ;
2026-02-06 01:18:12 +01:00
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
2026-02-06 01:18:12 +01:00
private async Task EnsureClientReadyAsync ( WinoImapClient client , CancellationToken cancellationToken )
{
if ( ! client . IsConnected )
2025-02-16 11:54:23 +01:00
{
2026-02-06 01:18:12 +01:00
client . ServerCertificateValidationCallback = MyServerCertificateValidationCallback ;
2025-02-16 11:43:30 +01:00
2026-02-06 01:18:12 +01:00
await client . ConnectAsync (
_customServerInformation . IncomingServer ,
int . Parse ( _customServerInformation . IncomingServerPort ) ,
GetSocketOptions ( _customServerInformation . IncomingServerSocketOption ) ,
cancellationToken ) . ConfigureAwait ( false ) ;
2024-11-03 16:47:33 +02:00
2026-02-06 01:18:12 +01:00
if ( client . Capabilities . HasFlag ( ImapCapabilities . Compress ) )
{
try
{
2026-02-14 12:52:17 +01:00
await client . CompressAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
2026-02-06 01:18:12 +01:00
}
2026-02-14 12:52:17 +01:00
catch ( Exception ex )
2026-02-06 01:18:12 +01:00
{
2026-02-14 12:52:17 +01:00
_logger . Debug ( ex , "Failed to enable IMAP compression. Continuing without compression." ) ;
2026-02-06 01:18:12 +01:00
}
}
2026-02-14 12:52:17 +01:00
await TryIdentifyAsync ( client , cancellationToken ) . ConfigureAwait ( false ) ;
2026-02-06 01:18:12 +01:00
}
2024-09-29 21:21:51 +02:00
2026-02-06 01:18:12 +01:00
if ( ! client . IsAuthenticated )
2024-09-29 21:21:51 +02:00
{
2026-02-06 01:18:12 +01:00
var cred = new NetworkCredential (
_customServerInformation . IncomingServerUsername ,
_customServerInformation . IncomingServerPassword ) ;
2024-09-29 21:21:51 +02:00
2026-02-06 01:18:12 +01:00
var authMethod = _customServerInformation . IncomingAuthenticationMethod ;
if ( authMethod ! = ImapAuthenticationMethod . Auto )
2025-02-15 12:53:32 +01:00
{
2026-02-06 01:18:12 +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
{
2026-02-06 01:18:12 +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 ) )
2026-02-06 01:18:12 +01:00
{
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-06 01:18:12 +01:00
}
}
2026-02-14 12:52:17 +01:00
}
}
2026-02-06 01:18:12 +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()." ) ;
2026-02-06 01:18:12 +01:00
}
2025-02-26 23:11:16 +01:00
}
2026-02-06 01:18:12 +01:00
private WinoImapClient CreateNewClient ( )
2025-02-26 23:11:16 +01:00
{
2026-04-05 13:18:50 +02:00
var client = new WinoImapClient ( ) ;
2024-09-14 21:51:43 +02:00
2026-02-06 01:18:12 +01:00
if ( ! string . IsNullOrEmpty ( _customServerInformation . ProxyServer ) )
2025-02-26 23:11:16 +01:00
{
2026-02-06 01:18:12 +01:00
client . ProxyClient = new HttpProxyClient (
_customServerInformation . ProxyServer ,
int . Parse ( _customServerInformation . ProxyServerPort ) ) ;
2025-02-26 23:11:16 +01:00
}
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 ) ;
2026-02-06 01:18:12 +01:00
return client ;
}
2025-02-26 23:11:16 +01:00
2026-02-06 01:18:12 +01:00
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
{
2026-02-06 01:18:12 +01:00
if ( client . IsConnected )
{
lock ( client . SyncRoot )
{
client . Disconnect ( quit : true ) ;
}
}
2026-02-14 12:52:17 +01:00
2026-02-06 01:18:12 +01:00
client . Dispose ( ) ;
2025-02-16 11:54:23 +01:00
}
2026-02-06 01:18:12 +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 )
{
return innerException = = null
2026-04-05 13:18:50 +02:00
? new ImapClientPoolException ( message , _customServerInformation )
: new ImapClientPoolException ( innerException ) ;
2026-02-14 12:52:17 +01:00
}
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 ) ) ;
2026-02-06 01:18:12 +01:00
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
{
2026-02-06 01:18:12 +01:00
throw new ImapTestSSLCertificateException (
certificate . Issuer ,
certificate . GetExpirationDateString ( ) ,
certificate . GetEffectiveDateString ( ) ) ;
2025-02-16 11:54:23 +01:00
}
2024-06-21 01:13:25 +02:00
2025-02-16 11:54:23 +01:00
return true ;
}
2024-06-21 01:13:25 +02:00
2026-02-06 01:18:12 +01:00
// 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-06 01:18:12 +01:00
2026-02-14 12:52:17 +01:00
_availableClients . Writer . Complete ( ) ;
2025-02-26 23:11:16 +01:00
2026-02-14 12:52:17 +01:00
foreach ( var kvp in _clientStates )
{
DisposeClient ( kvp . Key ) ;
}
2025-02-26 23:11:16 +01:00
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
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
}