IMAP Improvements (#558)

* Fixing an issue where scrollviewer overrides a part of template in mail list. Adjusted zoomed out header grid's corner radius.

* IDLE implementation, imap synchronization strategies basics and condstore synchronization.

* Adding iCloud and Yahoo as special IMAP handling scenario.

* iCloud special imap handling.

* Support for killing synchronizers.

* Update privacy policy url.

* Batching condstore downloads into 50, using SORT extension for searches if supported.

* Bumping some nugets. More on the imap synchronizers.

* Delegating idle synchronizations to server to post-sync operations.

* Update mailkit to resolve qresync bug with iCloud.

* Fixing remote highest mode seq checks for qresync and condstore synchronizers.

* Yahoo custom settings.

* Bump google sdk package.

* Fixing the build issue....

* NRE on canceled token accounts during setup.

* Server crash handlers.

* Remove ARM32. Upgrade server to .NET 9.

* Fix icons for yahoo and apple.

* Fixed an issue where disabled folders causing an exception on forced sync.

* Remove smtp encoding constraint.

* Remove commented code.

* Fixing merge conflict

* Addressing double registrations for mailkit remote folder events in synchronizers.

* Making sure idle canceled result is not reported.

* Fixing custom imap server dialog opening.

* Fixing the issue with account creation making the previously selected account as selected as well.

* Fixing app close behavior and logging app close.
This commit is contained in:
Burak Kaan Köse
2025-02-15 12:53:32 +01:00
committed by GitHub
parent 30f1257983
commit ee9e41c5a7
108 changed files with 2092 additions and 1166 deletions

View File

@@ -25,8 +25,6 @@ namespace Wino.Core.Integration
/// 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
@@ -48,14 +46,16 @@ namespace Wino.Core.Integration
public bool ThrowOnSSLHandshakeCallback { get; set; }
public ImapClientPoolOptions ImapClientPoolOptions { get; }
internal WinoImapClient IdleClient { get; set; }
private readonly int MinimumPoolSize = 5;
private readonly ConcurrentStack<ImapClient> _clients = [];
private readonly ConcurrentStack<IImapClient> _clients = [];
private readonly SemaphoreSlim _semaphore;
private readonly CustomServerInformation _customServerInformation;
private readonly Stream _protocolLogStream;
private readonly ILogger _logger = Log.ForContext<ImapClientPool>();
private bool _disposedValue;
public ImapClientPool(ImapClientPoolOptions imapClientPoolOptions)
{
@@ -74,7 +74,7 @@ namespace Wino.Core.Integration
/// Reconnects and reauthenticates if necessary.
/// </summary>
/// <param name="isCreatedNew">Whether the client has been newly created.</param>
private async Task EnsureCapabilitiesAsync(ImapClient client, bool isCreatedNew)
private async Task EnsureCapabilitiesAsync(IImapClient client, bool isCreatedNew)
{
try
{
@@ -84,6 +84,9 @@ namespace Wino.Core.Integration
if ((isCreatedNew || isReconnected) && client.IsConnected)
{
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.
@@ -113,10 +116,11 @@ namespace Wino.Core.Integration
// Activate post-auth capabilities.
if (client.Capabilities.HasFlag(ImapCapabilities.QuickResync))
await client.EnableQuickResyncAsync();
{
await client.EnableQuickResyncAsync().ConfigureAwait(false);
if (client.Capabilities.HasFlag(ImapCapabilities.Compress))
await client.CompressAsync();
if (client is WinoImapClient winoImapClient) winoImapClient.IsQResyncEnabled = true;
}
}
}
catch (Exception ex)
@@ -145,11 +149,11 @@ namespace Wino.Core.Integration
return reader.ReadToEnd();
}
public async Task<ImapClient> GetClientAsync()
public async Task<IImapClient> GetClientAsync()
{
await _semaphore.WaitAsync();
if (_clients.TryPop(out ImapClient item))
if (_clients.TryPop(out IImapClient item))
{
await EnsureCapabilitiesAsync(item, false);
@@ -163,20 +167,24 @@ namespace Wino.Core.Integration
return client;
}
public void Release(ImapClient item, bool destroyClient = false)
public void Release(IImapClient item, bool destroyClient = false)
{
if (item != null)
{
if (destroyClient)
{
lock (item.SyncRoot)
if (item.IsConnected)
{
item.Disconnect(true);
lock (item.SyncRoot)
{
item.Disconnect(quit: true);
}
}
_clients.TryPop(out _);
item.Dispose();
}
else
else if (!_disposedValue)
{
_clients.Push(item);
}
@@ -185,23 +193,15 @@ namespace Wino.Core.Integration
}
}
public void DestroyClient(ImapClient client)
private IImapClient CreateNewClient()
{
if (client == null) return;
client.Disconnect(true);
client.Dispose();
}
private ImapClient CreateNewClient()
{
ImapClient client = null;
WinoImapClient client = null;
// Make sure to create a ImapClient with a protocol logger if enabled.
client = _protocolLogStream != null
? new ImapClient(new ProtocolLogger(_protocolLogStream))
: new ImapClient();
? new WinoImapClient(new ProtocolLogger(_protocolLogStream))
: new WinoImapClient();
HttpProxyClient proxyClient = null;
@@ -213,7 +213,7 @@ namespace Wino.Core.Integration
client.ProxyClient = proxyClient;
_logger.Debug("Created new ImapClient. Current clients: {Count}", _clients.Count);
_logger.Debug("Creating new ImapClient. Current clients: {Count}", _clients.Count);
return client;
}
@@ -229,7 +229,7 @@ namespace Wino.Core.Integration
};
/// <returns>True if the connection is newly established.</returns>
public async Task<bool> EnsureConnectedAsync(ImapClient client)
public async Task<bool> EnsureConnectedAsync(IImapClient client)
{
if (client.IsConnected) return false;
@@ -263,8 +263,20 @@ namespace Wino.Core.Integration
{
if (_protocolLogStream == null) return;
var messageBytes = Encoding.UTF8.GetBytes($"W: {message}\n");
_protocolLogStream.Write(messageBytes, 0, messageBytes.Length);
try
{
var messageBytes = Encoding.UTF8.GetBytes($"W: {message}\n");
_protocolLogStream.Write(messageBytes, 0, messageBytes.Length);
}
catch (ObjectDisposedException)
{
Log.Warning($"Protocol log stream is disposed. Cannot write to it.");
}
catch (Exception)
{
throw;
}
}
bool MyServerCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
@@ -281,7 +293,7 @@ namespace Wino.Core.Integration
return true;
}
public async Task EnsureAuthenticatedAsync(ImapClient client)
public async Task EnsureAuthenticatedAsync(IImapClient client)
{
if (client.IsAuthenticated) return;
@@ -319,27 +331,38 @@ namespace Wino.Core.Integration
};
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_clients.ForEach(client =>
{
lock (client.SyncRoot)
{
client.Disconnect(true);
}
});
_clients.ForEach(client =>
{
client.Dispose();
});
_clients.Clear();
_protocolLogStream?.Dispose();
}
_disposedValue = true;
}
}
public void Dispose()
{
_clients.ForEach(client =>
{
lock (client.SyncRoot)
{
client.Disconnect(true);
}
});
_clients.ForEach(client =>
{
client.Dispose();
});
_clients.Clear();
if (_protocolLogStream != null)
{
_protocolLogStream.Dispose();
}
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}