Refactored all synchronizers to deal with some of the chronic issues.

This commit is contained in:
Burak Kaan Köse
2026-02-06 01:18:12 +01:00
parent d1425ca9ca
commit 071f1c9786
43 changed files with 2785 additions and 582 deletions
@@ -0,0 +1,40 @@
using System.Threading.Tasks;
using MailKit.Security;
using Serilog;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Synchronization;
namespace Wino.Core.Synchronizers.Errors.Imap;
/// <summary>
/// Handles IMAP authentication failures (AuthenticationException, SaslException).
/// Marks the error as requiring re-authentication.
/// </summary>
public class ImapAuthenticationFailedHandler : ISynchronizerErrorHandler
{
private readonly ILogger _logger = Log.ForContext<ImapAuthenticationFailedHandler>();
public bool CanHandle(SynchronizerErrorContext error)
{
return error.Exception is AuthenticationException ||
error.Exception is SaslException ||
(error.ErrorMessage?.Contains("authentication", System.StringComparison.OrdinalIgnoreCase) ?? false);
}
public Task<bool> HandleAsync(SynchronizerErrorContext error)
{
_logger.Warning(error.Exception,
"IMAP authentication failed for account {AccountName} ({AccountId}). User needs to re-authenticate.",
error.Account?.Name, error.Account?.Id);
// Mark as requiring authentication - this will stop sync and notify user
error.Severity = SynchronizerErrorSeverity.AuthRequired;
error.Category = SynchronizerErrorCategory.Authentication;
// No point in retrying auth failures - credentials need to be updated
error.RetryDelay = null;
return Task.FromResult(true);
}
}
@@ -0,0 +1,45 @@
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading.Tasks;
using MailKit;
using Serilog;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Synchronization;
namespace Wino.Core.Synchronizers.Errors.Imap;
/// <summary>
/// Handles IMAP connection loss errors (IOException, SocketException, ServiceNotConnectedException).
/// Marks the error as transient for retry with backoff.
/// </summary>
public class ImapConnectionLostHandler : ISynchronizerErrorHandler
{
private readonly ILogger _logger = Log.ForContext<ImapConnectionLostHandler>();
public bool CanHandle(SynchronizerErrorContext error)
{
return error.Exception is IOException ||
error.Exception is SocketException ||
error.Exception is ServiceNotConnectedException ||
error.Exception?.InnerException is IOException ||
error.Exception?.InnerException is SocketException;
}
public Task<bool> HandleAsync(SynchronizerErrorContext error)
{
_logger.Warning(error.Exception,
"IMAP connection lost for account {AccountName} ({AccountId}). Folder: {FolderName}. Operation: {Operation}. Will retry.",
error.Account?.Name, error.Account?.Id, error.FolderName ?? "N/A", error.OperationType ?? "N/A");
// Mark as transient - the RetryExecutor will handle the retry logic
error.Severity = SynchronizerErrorSeverity.Transient;
error.Category = SynchronizerErrorCategory.Network;
// Suggest a reasonable retry delay for connection issues
error.RetryDelay = TimeSpan.FromSeconds(2);
return Task.FromResult(true);
}
}
@@ -0,0 +1,67 @@
using System.Threading.Tasks;
using MailKit;
using Serilog;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Integration.Processors;
namespace Wino.Core.Synchronizers.Errors.Imap;
/// <summary>
/// Handles IMAP folder not found errors (FolderNotFoundException).
/// Deletes the folder locally and allows sync to continue with other folders.
/// </summary>
public class ImapFolderNotFoundHandler : ISynchronizerErrorHandler
{
private readonly ILogger _logger = Log.ForContext<ImapFolderNotFoundHandler>();
private readonly IImapChangeProcessor _imapChangeProcessor;
public ImapFolderNotFoundHandler(IImapChangeProcessor imapChangeProcessor)
{
_imapChangeProcessor = imapChangeProcessor;
}
public bool CanHandle(SynchronizerErrorContext error)
{
return error.Exception is FolderNotFoundException ||
error.ErrorCode == 404 ||
(error.ErrorMessage?.Contains("folder not found", System.StringComparison.OrdinalIgnoreCase) ?? false) ||
(error.ErrorMessage?.Contains("mailbox not found", System.StringComparison.OrdinalIgnoreCase) ?? false);
}
public async Task<bool> HandleAsync(SynchronizerErrorContext error)
{
_logger.Warning(error.Exception,
"IMAP folder not found for account {AccountName} ({AccountId}). Folder: {FolderName} ({FolderId}). Removing locally.",
error.Account?.Name, error.Account?.Id, error.FolderName, error.FolderId);
// Mark as recoverable - sync can continue with other folders
error.Severity = SynchronizerErrorSeverity.Recoverable;
error.Category = SynchronizerErrorCategory.ResourceNotFound;
// Try to delete the folder locally if we have the folder ID
if (error.FolderId.HasValue && error.Account != null)
{
try
{
// Get the folder's remote ID from the exception if available
var remoteId = error.Exception is FolderNotFoundException fnf ? fnf.FolderName : null;
if (!string.IsNullOrEmpty(remoteId))
{
await _imapChangeProcessor.DeleteFolderAsync(error.Account.Id, remoteId).ConfigureAwait(false);
_logger.Information("Successfully deleted local folder {FolderName} after server deletion.",
error.FolderName);
}
}
catch (System.Exception ex)
{
_logger.Warning(ex, "Failed to delete local folder {FolderName} ({FolderId})",
error.FolderName, error.FolderId);
}
}
return true;
}
}
@@ -0,0 +1,100 @@
using System;
using System.Threading.Tasks;
using MailKit.Net.Imap;
using Serilog;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Synchronization;
namespace Wino.Core.Synchronizers.Errors.Imap;
/// <summary>
/// Handles generic IMAP protocol errors (ImapProtocolException, ImapCommandException).
/// This is the catch-all handler for IMAP errors not handled by more specific handlers.
/// </summary>
public class ImapProtocolErrorHandler : ISynchronizerErrorHandler
{
private readonly ILogger _logger = Log.ForContext<ImapProtocolErrorHandler>();
public bool CanHandle(SynchronizerErrorContext error)
{
// This is a catch-all for IMAP-related exceptions
return error.Exception is ImapProtocolException ||
error.Exception is ImapCommandException;
}
public Task<bool> HandleAsync(SynchronizerErrorContext error)
{
var severity = ClassifyProtocolError(error);
var category = SynchronizerErrorCategory.ProtocolError;
_logger.Warning(error.Exception,
"IMAP protocol error for account {AccountName} ({AccountId}). Folder: {FolderName}. Operation: {Operation}. Severity: {Severity}",
error.Account?.Name, error.Account?.Id, error.FolderName ?? "N/A", error.OperationType ?? "N/A", severity);
error.Severity = severity;
error.Category = category;
// For transient protocol errors, suggest a retry delay
if (severity == SynchronizerErrorSeverity.Transient)
{
error.RetryDelay = TimeSpan.FromSeconds(5);
}
return Task.FromResult(true);
}
/// <summary>
/// Classifies the protocol error to determine if it's transient, recoverable, or fatal.
/// </summary>
private static SynchronizerErrorSeverity ClassifyProtocolError(SynchronizerErrorContext error)
{
var message = error.ErrorMessage?.ToLowerInvariant() ?? string.Empty;
var exMessage = error.Exception?.Message?.ToLowerInvariant() ?? string.Empty;
// Check for rate limiting / throttling
if (message.Contains("too many") || message.Contains("rate limit") ||
message.Contains("throttl") || exMessage.Contains("too many"))
{
return SynchronizerErrorSeverity.Transient;
}
// Check for temporary server issues
if (message.Contains("try again") || message.Contains("temporary") ||
message.Contains("busy") || exMessage.Contains("try again"))
{
return SynchronizerErrorSeverity.Transient;
}
// Check for command-specific errors that are usually transient
if (error.Exception is ImapCommandException cmdEx)
{
// NO response usually means the operation failed but can be retried
if (cmdEx.Response == ImapCommandResponse.No)
{
// Unless it's a permanent failure indication
if (message.Contains("permanent") || message.Contains("invalid"))
{
return SynchronizerErrorSeverity.Recoverable;
}
return SynchronizerErrorSeverity.Transient;
}
// BAD response usually indicates a protocol violation - don't retry
if (cmdEx.Response == ImapCommandResponse.Bad)
{
return SynchronizerErrorSeverity.Recoverable;
}
}
// Protocol exceptions that indicate connection issues
if (error.Exception is ImapProtocolException)
{
// Most protocol exceptions are connection-related and transient
return SynchronizerErrorSeverity.Transient;
}
// Default to recoverable for unknown protocol errors
return SynchronizerErrorSeverity.Recoverable;
}
}