diff --git a/Wino.Core/CoreContainerSetup.cs b/Wino.Core/CoreContainerSetup.cs index 50477b28..1853c4c9 100644 --- a/Wino.Core/CoreContainerSetup.cs +++ b/Wino.Core/CoreContainerSetup.cs @@ -4,6 +4,7 @@ using Wino.Authentication; using Wino.Core.Domain.Interfaces; using Wino.Core.Integration.Processors; using Wino.Core.Services; +using Wino.Core.Synchronizers.Errors.Outlook; using Wino.Core.Synchronizers.ImapSync; namespace Wino.Core; @@ -34,5 +35,11 @@ public static class CoreContainerSetup services.AddTransient(); services.AddTransient(); services.AddTransient(); + + // Register error factory handlers + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); } } diff --git a/Wino.Core/Domain/Interfaces/ISynchronizerErrorHandler.cs b/Wino.Core/Domain/Interfaces/ISynchronizerErrorHandler.cs new file mode 100644 index 00000000..6e5dcad2 --- /dev/null +++ b/Wino.Core/Domain/Interfaces/ISynchronizerErrorHandler.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using Wino.Core.Domain.Models.Errors; + +namespace Wino.Core.Domain.Interfaces; + +/// +/// Interface for handling specific synchronizer errors +/// +public interface ISynchronizerErrorHandler +{ + /// + /// Determines if this handler can handle the specified error + /// + /// The error to check + /// True if this handler can handle the error, false otherwise + bool CanHandle(SynchronizerErrorContext error); + + /// + /// Handles the specified error + /// + /// The error to handle + /// A task that completes when the error is handled + Task HandleAsync(SynchronizerErrorContext error); +} + +public interface ISynchronizerErrorHandlerFactory +{ + Task HandleErrorAsync(SynchronizerErrorContext error); +} + +public interface IOutlookSynchronizerErrorHandlerFactory : ISynchronizerErrorHandlerFactory; +public interface IGmailSynchronizerErrorHandlerFactory : ISynchronizerErrorHandlerFactory; diff --git a/Wino.Core/Domain/Models/Errors/SynchronizerErrorContext.cs b/Wino.Core/Domain/Models/Errors/SynchronizerErrorContext.cs new file mode 100644 index 00000000..4f868ea3 --- /dev/null +++ b/Wino.Core/Domain/Models/Errors/SynchronizerErrorContext.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Interfaces; + +namespace Wino.Core.Domain.Models.Errors; + +/// +/// Contains context information about a synchronizer error +/// +public class SynchronizerErrorContext +{ + /// + /// Account associated with the error + /// + public MailAccount Account { get; set; } + + /// + /// Gets or sets the error code + /// + public int? ErrorCode { get; set; } + + /// + /// Gets or sets the error message + /// + public string ErrorMessage { get; set; } + + /// + /// Gets or sets the request bundle associated with the error + /// + public IRequestBundle RequestBundle { get; set; } + + /// + /// Gets or sets additional data associated with the error + /// + public Dictionary AdditionalData { get; set; } = new Dictionary(); + + /// + /// Gets or sets the exception associated with the error + /// + public Exception Exception { get; set; } +} diff --git a/Wino.Core/Services/GmailSynchronizerErrorHandlingFactory.cs b/Wino.Core/Services/GmailSynchronizerErrorHandlingFactory.cs new file mode 100644 index 00000000..b2153c32 --- /dev/null +++ b/Wino.Core/Services/GmailSynchronizerErrorHandlingFactory.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Errors; + +namespace Wino.Core.Services; +public class GmailSynchronizerErrorHandlingFactory : SynchronizerErrorHandlingFactory, IGmailSynchronizerErrorHandlerFactory +{ + public bool CanHandle(SynchronizerErrorContext error) => CanHandle(error); + + public Task HandleAsync(SynchronizerErrorContext error) => HandleErrorAsync(error); +} diff --git a/Wino.Core/Services/OutlookSynchronizerErrorHandlingFactory.cs b/Wino.Core/Services/OutlookSynchronizerErrorHandlingFactory.cs new file mode 100644 index 00000000..4947060f --- /dev/null +++ b/Wino.Core/Services/OutlookSynchronizerErrorHandlingFactory.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Errors; +using Wino.Core.Synchronizers.Errors.Outlook; + +namespace Wino.Core.Services; + +public class OutlookSynchronizerErrorHandlingFactory : SynchronizerErrorHandlingFactory, IOutlookSynchronizerErrorHandlerFactory +{ + public OutlookSynchronizerErrorHandlingFactory(ObjectCannotBeDeletedHandler objectCannotBeDeleted) + { + RegisterHandler(objectCannotBeDeleted); + } + + public bool CanHandle(SynchronizerErrorContext error) => CanHandle(error); + + public Task HandleAsync(SynchronizerErrorContext error) => HandleErrorAsync(error); +} diff --git a/Wino.Core/Services/SynchronizerErrorHandlingFactory.cs b/Wino.Core/Services/SynchronizerErrorHandlingFactory.cs new file mode 100644 index 00000000..7c9336c0 --- /dev/null +++ b/Wino.Core/Services/SynchronizerErrorHandlingFactory.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Serilog; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Errors; + +namespace Wino.Core.Services; + +/// +/// Factory for handling synchronizer errors +/// +public class SynchronizerErrorHandlingFactory +{ + private readonly ILogger _logger = Log.ForContext(); + private readonly List _handlers = new(); + + /// + /// Registers an error handler + /// + /// The handler to register + public void RegisterHandler(ISynchronizerErrorHandler handler) + { + _handlers.Add(handler); + } + + /// + /// Handles an error using the registered handlers + /// + /// The error to handle + /// True if the error was handled, false otherwise + public async Task HandleErrorAsync(SynchronizerErrorContext error) + { + foreach (var handler in _handlers) + { + if (handler.CanHandle(error)) + { + _logger.Debug("Found handler {HandlerType} for error code {ErrorCode} message {ErrorMessage}", + handler.GetType().Name, error.ErrorCode, error.ErrorMessage); + + return await handler.HandleAsync(error); + } + } + + _logger.Debug("No handler found for error code {ErrorCode} message {ErrorMessage}", + error.ErrorCode, error.ErrorMessage); + + return false; + } +} diff --git a/Wino.Core/Services/SynchronizerFactory.cs b/Wino.Core/Services/SynchronizerFactory.cs index bed739e9..1cab069c 100644 --- a/Wino.Core/Services/SynchronizerFactory.cs +++ b/Wino.Core/Services/SynchronizerFactory.cs @@ -15,6 +15,8 @@ public class SynchronizerFactory : ISynchronizerFactory private readonly IAccountService _accountService; private readonly IImapSynchronizationStrategyProvider _imapSynchronizationStrategyProvider; private readonly IApplicationConfiguration _applicationConfiguration; + private readonly IOutlookSynchronizerErrorHandlerFactory _outlookSynchronizerErrorHandlerFactory; + private readonly IGmailSynchronizerErrorHandlerFactory _gmailSynchronizerErrorHandlerFactory; private readonly IOutlookChangeProcessor _outlookChangeProcessor; private readonly IGmailChangeProcessor _gmailChangeProcessor; private readonly IImapChangeProcessor _imapChangeProcessor; @@ -30,7 +32,9 @@ public class SynchronizerFactory : ISynchronizerFactory IGmailAuthenticator gmailAuthenticator, IAccountService accountService, IImapSynchronizationStrategyProvider imapSynchronizationStrategyProvider, - IApplicationConfiguration applicationConfiguration) + IApplicationConfiguration applicationConfiguration, + IOutlookSynchronizerErrorHandlerFactory outlookSynchronizerErrorHandlerFactory, + IGmailSynchronizerErrorHandlerFactory gmailSynchronizerErrorHandlerFactory) { _outlookChangeProcessor = outlookChangeProcessor; _gmailChangeProcessor = gmailChangeProcessor; @@ -40,6 +44,8 @@ public class SynchronizerFactory : ISynchronizerFactory _accountService = accountService; _imapSynchronizationStrategyProvider = imapSynchronizationStrategyProvider; _applicationConfiguration = applicationConfiguration; + _outlookSynchronizerErrorHandlerFactory = outlookSynchronizerErrorHandlerFactory; + _gmailSynchronizerErrorHandlerFactory = gmailSynchronizerErrorHandlerFactory; } public async Task GetAccountSynchronizerAsync(Guid accountId) @@ -69,9 +75,9 @@ public class SynchronizerFactory : ISynchronizerFactory switch (providerType) { case Domain.Enums.MailProviderType.Outlook: - return new OutlookSynchronizer(mailAccount, _outlookAuthenticator, _outlookChangeProcessor); + return new OutlookSynchronizer(mailAccount, _outlookAuthenticator, _outlookChangeProcessor, _outlookSynchronizerErrorHandlerFactory); case Domain.Enums.MailProviderType.Gmail: - return new GmailSynchronizer(mailAccount, _gmailAuthenticator, _gmailChangeProcessor); + return new GmailSynchronizer(mailAccount, _gmailAuthenticator, _gmailChangeProcessor, _gmailSynchronizerErrorHandlerFactory); case Domain.Enums.MailProviderType.IMAP4: return new ImapSynchronizer(mailAccount, _imapChangeProcessor, _imapSynchronizationStrategyProvider, _applicationConfiguration); default: diff --git a/Wino.Core/Synchronizers/Errors/Outlook/ObjectCannotBeDeletedHandler.cs b/Wino.Core/Synchronizers/Errors/Outlook/ObjectCannotBeDeletedHandler.cs new file mode 100644 index 00000000..2d78ad64 --- /dev/null +++ b/Wino.Core/Synchronizers/Errors/Outlook/ObjectCannotBeDeletedHandler.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Microsoft.Kiota.Abstractions; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Errors; +using Wino.Core.Domain.Models.Requests; +using Wino.Core.Requests.Bundles; + +namespace Wino.Core.Synchronizers.Errors.Outlook; + +public class ObjectCannotBeDeletedHandler : ISynchronizerErrorHandler +{ + private readonly IMailService _mailService; + + public ObjectCannotBeDeletedHandler(IMailService mailService) + { + _mailService = mailService; + } + + public bool CanHandle(SynchronizerErrorContext error) + { + return error.ErrorMessage.Contains("ErrorCannotDeleteObject") && error.RequestBundle is HttpRequestBundle; + } + + public async Task HandleAsync(SynchronizerErrorContext error) + { + var castedBundle = error.RequestBundle as HttpRequestBundle; + + if (castedBundle?.Request is MailRequestBase mailRequest) + { + var request = castedBundle.Request; + + await _mailService.DeleteMailAsync(error.Account.Id, mailRequest.Item.Id); + + return true; + } + + return false; + } +} diff --git a/Wino.Core/Synchronizers/GmailSynchronizer.cs b/Wino.Core/Synchronizers/GmailSynchronizer.cs index 106c9cc3..e1fb9b49 100644 --- a/Wino.Core/Synchronizers/GmailSynchronizer.cs +++ b/Wino.Core/Synchronizers/GmailSynchronizer.cs @@ -25,6 +25,7 @@ using Wino.Core.Domain.Enums; using Wino.Core.Domain.Exceptions; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Accounts; +using Wino.Core.Domain.Models.Errors; using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Synchronization; @@ -57,6 +58,7 @@ public class GmailSynchronizer : WinoSynchronizer(); // Keeping a reference for quick access to the virtual archive folder. @@ -64,7 +66,8 @@ public class GmailSynchronizer : WinoSynchronizer _googleHttpClient; @@ -1106,30 +1110,50 @@ public class GmailSynchronizer : WinoSynchronizer bundle) + private async Task ProcessGmailRequestErrorAsync(RequestError error, IRequestBundle bundle) { if (error == null) return; - // OutOfMemoryException is a known bug in Gmail SDK. - if (error.Code == 0) + // Create error context + var errorContext = new SynchronizerErrorContext { - bundle?.UIChangeRequest?.RevertUIChanges(); - throw new OutOfMemoryException(error.Message); - } + ErrorCode = error.Code, + ErrorMessage = error.Message, + RequestBundle = bundle, + AdditionalData = new Dictionary + { + { "Account", Account }, + { "Error", error } + } + }; - // Entity not found. - if (error.Code == 404) + // Try to handle the error with registered handlers + var handled = await _gmailSynchronizerErrorHandlerFactory.HandleErrorAsync(errorContext); + + // If not handled by any specific handler, apply default error handling + if (!handled) { - bundle?.UIChangeRequest?.RevertUIChanges(); - throw new SynchronizerEntityNotFoundException(error.Message); - } + // OutOfMemoryException is a known bug in Gmail SDK. + if (error.Code == 0) + { + bundle?.UIChangeRequest?.RevertUIChanges(); + throw new OutOfMemoryException(error.Message); + } - if (!string.IsNullOrEmpty(error.Message)) - { - bundle?.UIChangeRequest?.RevertUIChanges(); - error.Errors?.ForEach(error => _logger.Error("Unknown Gmail SDK error for {Name}\n{Error}", Account.Name, error)); + // Entity not found. + if (error.Code == 404) + { + bundle?.UIChangeRequest?.RevertUIChanges(); + throw new SynchronizerEntityNotFoundException(error.Message); + } - throw new SynchronizerException(error.Message); + if (!string.IsNullOrEmpty(error.Message)) + { + bundle?.UIChangeRequest?.RevertUIChanges(); + error.Errors?.ForEach(error => _logger.Error("Unknown Gmail SDK error for {Name}\n{Error}", Account.Name, error)); + + throw new SynchronizerException(error.Message); + } } } @@ -1148,7 +1172,7 @@ public class GmailSynchronizer : WinoSynchronizer messageBundle) { diff --git a/Wino.Core/Synchronizers/OutlookSynchronizer.cs b/Wino.Core/Synchronizers/OutlookSynchronizer.cs index 879453d1..528790c1 100644 --- a/Wino.Core/Synchronizers/OutlookSynchronizer.cs +++ b/Wino.Core/Synchronizers/OutlookSynchronizer.cs @@ -30,6 +30,7 @@ using Wino.Core.Domain.Enums; using Wino.Core.Domain.Exceptions; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Accounts; +using Wino.Core.Domain.Models.Errors; using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Synchronization; @@ -83,9 +84,12 @@ public class OutlookSynchronizer : WinoSynchronizer(); private readonly IOutlookChangeProcessor _outlookChangeProcessor; private readonly GraphServiceClient _graphClient; + private readonly IOutlookSynchronizerErrorHandlerFactory _errorHandlingFactory; + public OutlookSynchronizer(MailAccount account, IAuthenticator authenticator, - IOutlookChangeProcessor outlookChangeProcessor) : base(account) + IOutlookChangeProcessor outlookChangeProcessor, + IOutlookSynchronizerErrorHandlerFactory errorHandlingFactory) : base(account) { var tokenProvider = new MicrosoftTokenProvider(Account, authenticator); @@ -106,6 +110,7 @@ public class OutlookSynchronizer : WinoSynchronizer errors) { - bundle.UIChangeRequest?.RevertUIChanges(); - var content = await response.Content.ReadAsStringAsync(); var errorJson = JsonNode.Parse(content); - var errorString = $"[{response.StatusCode}] {errorJson["error"]["code"]} - {errorJson["error"]["message"]}\n"; + var errorCode = errorJson["error"]["code"].GetValue(); + var errorMessage = errorJson["error"]["message"].GetValue(); + var errorString = $"[{response.StatusCode}] {errorCode} - {errorMessage}\n"; - Debug.WriteLine(errorString); - errors.Add(errorString); + // Create error context + var errorContext = new SynchronizerErrorContext + { + Account = Account, + ErrorCode = (int)response.StatusCode, + ErrorMessage = errorMessage, + RequestBundle = bundle, + AdditionalData = new Dictionary + { + { "ErrorCode", errorCode }, + { "HttpResponse", response }, + { "Content", content } + } + }; + + // Try to handle the error with registered handlers + var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext); + + // If not handled by any specific handler, revert UI changes and add to error list + if (!handled) + { + bundle.UIChangeRequest?.RevertUIChanges(); + Debug.WriteLine(errorString); + errors.Add(errorString); + } } private void ThrowBatchExecutionException(List errors) diff --git a/Wino.Mail/Views/MailListPage.xaml b/Wino.Mail/Views/MailListPage.xaml index c7e077fa..f9053698 100644 --- a/Wino.Mail/Views/MailListPage.xaml +++ b/Wino.Mail/Views/MailListPage.xaml @@ -203,6 +203,8 @@ Key="A" Invoked="SelectAllInvoked" Modifiers="Control" /> + + diff --git a/Wino.Mail/Views/MailListPage.xaml.cs b/Wino.Mail/Views/MailListPage.xaml.cs index f86bbb3b..6153a6ea 100644 --- a/Wino.Mail/Views/MailListPage.xaml.cs +++ b/Wino.Mail/Views/MailListPage.xaml.cs @@ -495,4 +495,7 @@ public sealed partial class MailListPage : MailListPageAbstract, private void SelectAllInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) => MailListView.SelectAllWino(); + + private void DeleteAllInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) + => ViewModel.ExecuteMailOperationCommand.Execute(MailOperation.SoftDelete); } diff --git a/Wino.Server/MessageHandlers/UserActionRequestHandler.cs b/Wino.Server/MessageHandlers/UserActionRequestHandler.cs index f6514b8c..efaf8bb6 100644 --- a/Wino.Server/MessageHandlers/UserActionRequestHandler.cs +++ b/Wino.Server/MessageHandlers/UserActionRequestHandler.cs @@ -24,17 +24,6 @@ public class UserActionRequestHandler : ServerMessageHandler.CreateSuccessResponse(true); } }