Synchronization manager.

This commit is contained in:
Burak Kaan Köse
2025-10-04 23:10:07 +02:00
parent 3b1eff1702
commit 9623c2e6d2
17 changed files with 709 additions and 352 deletions
@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Authentication;
using Wino.Core.Domain.Models.Connectivity;
using Wino.Core.Domain.Models.Synchronization;
namespace Wino.Core.Domain.Interfaces;
/// <summary>
/// Interface for the singleton synchronization manager that handles synchronizer instances and operations.
/// </summary>
public interface ISynchronizationManager
{
/// <summary>
/// Initializes the SynchronizationManager with required dependencies.
/// </summary>
Task InitializeAsync(ISynchronizerFactory synchronizerFactory,
IImapTestService imapTestService,
IAccountService accountService,
IAuthenticationProvider authenticationProvider);
/// <summary>
/// Tests IMAP server connectivity for the given server information.
/// </summary>
Task<ImapConnectivityTestResults> TestImapConnectivityAsync(CustomServerInformation serverInformation, bool allowSSLHandshake);
/// <summary>
/// Starts a new mail synchronization for the given account.
/// </summary>
Task<MailSynchronizationResult> SynchronizeMailAsync(MailSynchronizationOptions options,
CancellationToken cancellationToken = default);
/// <summary>
/// Checks if there is an ongoing synchronization for the given account.
/// </summary>
bool IsAccountSynchronizing(Guid accountId);
/// <summary>
/// Queues a mail action request to the corresponding account's synchronizer.
/// </summary>
Task QueueRequestAsync(IRequestBase request, Guid accountId);
/// <summary>
/// Handles folder synchronization for the given account.
/// </summary>
Task<MailSynchronizationResult> SynchronizeFoldersAsync(Guid accountId,
CancellationToken cancellationToken = default);
/// <summary>
/// Handles alias synchronization for the given account.
/// </summary>
Task<MailSynchronizationResult> SynchronizeAliasesAsync(Guid accountId,
CancellationToken cancellationToken = default);
/// <summary>
/// Handles profile synchronization for the given account.
/// </summary>
Task<MailSynchronizationResult> SynchronizeProfileAsync(Guid accountId,
CancellationToken cancellationToken = default);
/// <summary>
/// Downloads a MIME message for the given mail item.
/// </summary>
Task<string> DownloadMimeMessageAsync(MailCopy mailItem, Guid accountId,
CancellationToken cancellationToken = default);
/// <summary>
/// Creates a new synchronizer for a newly added account.
/// </summary>
Task<IWinoSynchronizerBase> CreateSynchronizerForAccountAsync(MailAccount account);
/// <summary>
/// Destroys the synchronizer for the given account.
/// </summary>
Task DestroySynchronizerAsync(Guid accountId);
/// <summary>
/// Gets all cached synchronizers.
/// </summary>
IEnumerable<IWinoSynchronizerBase> GetAllSynchronizers();
/// <summary>
/// Gets a synchronizer for the given account ID.
/// </summary>
Task<IWinoSynchronizerBase> GetSynchronizerAsync(Guid accountId);
/// <summary>
/// Handles OAuth authentication for the specified provider.
/// </summary>
Task<TokenInformationEx> HandleAuthorizationAsync(MailProviderType providerType,
MailAccount account = null,
bool proposeCopyAuthorizationURL = false);
}
@@ -1,106 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Wino.Core.Domain.Interfaces;
/// <summary>
/// Simple wrapper class to maintain compatibility with the original WinoServerResponse structure.
/// </summary>
/// <typeparam name="T">Type of the expected response.</typeparam>
public class WinoServerResponse<T>
{
public bool IsSuccess { get; set; }
public string Message { get; set; }
public T Data { get; set; }
public static WinoServerResponse<T> CreateSuccessResponse(T data)
{
return new WinoServerResponse<T>
{
IsSuccess = true,
Data = data
};
}
public static WinoServerResponse<T> CreateErrorResponse(string message)
{
return new WinoServerResponse<T>
{
IsSuccess = false,
Message = message
};
}
public void ThrowIfFailed()
{
if (!IsSuccess)
throw new InvalidOperationException(Message);
}
}
/// <summary>
/// Connection status enum to maintain compatibility.
/// </summary>
public enum WinoServerConnectionStatus
{
None,
Connecting,
Connected,
Disconnected,
Failed
}
public interface IWinoServerConnectionManager
{
/// <summary>
/// When the connection status changes, this event will be triggered.
/// </summary>
event EventHandler<WinoServerConnectionStatus> StatusChanged;
/// <summary>
/// Gets the connection status.
/// </summary>
WinoServerConnectionStatus Status { get; }
/// <summary>
/// Launches Full Trust process (Wino Server) and awaits connection completion.
/// If connection is not established in 10 seconds, it will return false.
/// If the server process is already running, it'll connect to existing one.
/// If the server process is not running, it'll be launched and connection establishment is awaited.
/// </summary>
/// <returns>Whether connection is established or not.</returns>
Task<bool> ConnectAsync();
/// <summary>
/// Queues a new user request to be processed by Wino Server.
/// Healthy connection must present before calling this method.
/// </summary>
/// <param name="request">Request to queue for synchronizer in the server.</param>
/// <param name="accountId">Account id to queueu request for.</param>
Task QueueRequestAsync(IRequestBase request, Guid accountId);
/// <summary>
/// Returns response from server for the given request.
/// </summary>
/// <typeparam name="TResponse">Response type.</typeparam>
/// <typeparam name="TRequestType">Request type.</typeparam>
/// <param name="clientMessage">Request type.</param>
/// <returns>Response received from the server for the given TResponse type.</returns>
Task<WinoServerResponse<TResponse>> GetResponseAsync<TResponse, TRequestType>(TRequestType clientMessage, CancellationToken cancellationToken = default) where TRequestType : IClientMessage;
/// <summary>
/// Handle for connecting to the server.
/// If the server is already running, it'll connect to existing one.
/// Callers can await this handle to wait for connection establishment.
/// </summary>
TaskCompletionSource<bool> ConnectingHandle { get; }
}
public interface IWinoServerConnectionManager<TAppServiceConnection> : IWinoServerConnectionManager, IInitializeAsync
{
/// <summary>
/// Existing connection handle to the server of TAppServiceConnection type.
/// </summary>
TAppServiceConnection Connection { get; set; }
}
@@ -40,7 +40,6 @@ public abstract partial class AccountManagementPageViewModelBase : CoreBaseViewM
public int FREE_ACCOUNT_COUNT { get; } = 3; public int FREE_ACCOUNT_COUNT { get; } = 3;
protected IDialogServiceBase DialogService { get; } protected IDialogServiceBase DialogService { get; }
protected IWinoServerConnectionManager WinoServerConnectionManager { get; }
protected INavigationService NavigationService { get; } protected INavigationService NavigationService { get; }
protected IAccountService AccountService { get; } protected IAccountService AccountService { get; }
protected IProviderService ProviderService { get; } protected IProviderService ProviderService { get; }
@@ -49,7 +48,6 @@ public abstract partial class AccountManagementPageViewModelBase : CoreBaseViewM
protected IPreferencesService PreferencesService { get; } protected IPreferencesService PreferencesService { get; }
public AccountManagementPageViewModelBase(IDialogServiceBase dialogService, public AccountManagementPageViewModelBase(IDialogServiceBase dialogService,
IWinoServerConnectionManager winoServerConnectionManager,
INavigationService navigationService, INavigationService navigationService,
IAccountService accountService, IAccountService accountService,
IProviderService providerService, IProviderService providerService,
@@ -58,7 +56,6 @@ public abstract partial class AccountManagementPageViewModelBase : CoreBaseViewM
IPreferencesService preferencesService) IPreferencesService preferencesService)
{ {
DialogService = dialogService; DialogService = dialogService;
WinoServerConnectionManager = winoServerConnectionManager;
NavigationService = navigationService; NavigationService = navigationService;
AccountService = accountService; AccountService = accountService;
ProviderService = providerService; ProviderService = providerService;
-5
View File
@@ -1,6 +1,5 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Windows.ApplicationModel.AppService;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.ViewModels; using Wino.Core.ViewModels;
using Wino.Core.WinUI.Services; using Wino.Core.WinUI.Services;
@@ -12,10 +11,6 @@ public static class CoreUWPContainerSetup
{ {
public static void RegisterCoreUWPServices(this IServiceCollection services) public static void RegisterCoreUWPServices(this IServiceCollection services)
{ {
var serverConnectionManager = new EmptyWinoServerConnectionManager<AppServiceConnection>();
services.AddSingleton<IWinoServerConnectionManager>(serverConnectionManager);
services.AddSingleton<IWinoServerConnectionManager<AppServiceConnection>>(serverConnectionManager);
services.AddSingleton<IApplicationResourceManager<ResourceDictionary>, ApplicationResourceManager>(); services.AddSingleton<IApplicationResourceManager<ResourceDictionary>, ApplicationResourceManager>();
services.AddSingleton<IUnderlyingThemeService, UnderlyingThemeService>(); services.AddSingleton<IUnderlyingThemeService, UnderlyingThemeService>();
@@ -1,55 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.WinUI.Services;
/// <summary>
/// Empty implementation of IWinoServerConnectionManager that returns default values.
/// This replaces the old AppServiceConnection-based implementation.
/// </summary>
public class EmptyWinoServerConnectionManager : IWinoServerConnectionManager
{
public event EventHandler<WinoServerConnectionStatus> StatusChanged { add { } remove { } }
public WinoServerConnectionStatus Status => WinoServerConnectionStatus.Connected;
public TaskCompletionSource<bool> ConnectingHandle { get; } = new TaskCompletionSource<bool>();
public EmptyWinoServerConnectionManager()
{
ConnectingHandle.SetResult(true);
}
public Task<bool> ConnectAsync()
{
return Task.FromResult(true);
}
public Task QueueRequestAsync(IRequestBase request, Guid accountId)
{
return Task.CompletedTask;
}
public Task<WinoServerResponse<TResponse>> GetResponseAsync<TResponse, TRequestType>(TRequestType clientMessage, CancellationToken cancellationToken = default)
where TRequestType : IClientMessage
{
var response = WinoServerResponse<TResponse>.CreateSuccessResponse(default(TResponse));
return Task.FromResult(response);
}
}
/// <summary>
/// Generic empty implementation for typed connection managers.
/// </summary>
/// <typeparam name="TAppServiceConnection">The connection type (not used in this implementation)</typeparam>
public class EmptyWinoServerConnectionManager<TAppServiceConnection> : EmptyWinoServerConnectionManager, IWinoServerConnectionManager<TAppServiceConnection>
{
public TAppServiceConnection Connection { get; set; }
public Task InitializeAsync()
{
return Task.CompletedTask;
}
}
+2
View File
@@ -18,6 +18,7 @@ using Windows.Storage;
using Wino.Core.Domain; using Wino.Core.Domain;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Translations; using Wino.Core.Domain.Models.Translations;
using Wino.Core.Services;
using Wino.Messaging.Client.Shell; using Wino.Messaging.Client.Shell;
using Wino.Services; using Wino.Services;
using WinUIEx; using WinUIEx;
@@ -84,6 +85,7 @@ public abstract class WinoApplication : Application, IRecipient<LanguageChanged>
yield return DatabaseService; yield return DatabaseService;
yield return TranslationService; yield return TranslationService;
yield return NewThemeService; // Initialize NewThemeService instead of old ThemeService yield return NewThemeService; // Initialize NewThemeService instead of old ThemeService
yield return Services.GetService<SynchronizationManagerInitializer>();
// yield return ThemeService; // Keep old service for backward compatibility but don't initialize // yield return ThemeService; // Keep old service for backward compatibility but don't initialize
} }
+2
View File
@@ -17,6 +17,8 @@ public static class CoreContainerSetup
services.AddSingleton(loggerLevelSwitcher); services.AddSingleton(loggerLevelSwitcher);
services.AddSingleton<ISynchronizerFactory, SynchronizerFactory>(); services.AddSingleton<ISynchronizerFactory, SynchronizerFactory>();
services.AddSingleton<ISynchronizationManager>(provider => SynchronizationManager.Instance);
services.AddTransient<SynchronizationManagerInitializer>();
services.AddTransient<IGmailChangeProcessor, GmailChangeProcessor>(); services.AddTransient<IGmailChangeProcessor, GmailChangeProcessor>();
services.AddTransient<IImapChangeProcessor, ImapChangeProcessor>(); services.AddTransient<IImapChangeProcessor, ImapChangeProcessor>();
@@ -0,0 +1,451 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Authentication;
using Wino.Core.Domain.Models.Connectivity;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Messaging.Server;
namespace Wino.Core.Services;
/// <summary>
/// Singleton manager that handles synchronizer instances and operations for all accounts.
/// Replaces the old WinoServerConnectionManager functionality.
/// </summary>
public class SynchronizationManager : ISynchronizationManager
{
private static readonly Lazy<SynchronizationManager> _instance = new(() => new SynchronizationManager());
public static SynchronizationManager Instance => _instance.Value;
private readonly ConcurrentDictionary<Guid, IWinoSynchronizerBase> _synchronizerCache = new();
private readonly SemaphoreSlim _initializationSemaphore = new(1, 1);
private readonly ILogger _logger = Log.ForContext<SynchronizationManager>();
private ISynchronizerFactory _synchronizerFactory;
private SynchronizerFactory _concreteSynchronizerFactory;
private IImapTestService _imapTestService;
private IAccountService _accountService;
private IAuthenticationProvider _authenticationProvider;
private bool _isInitialized = false;
private SynchronizationManager() { }
/// <summary>
/// Initializes the SynchronizationManager with required dependencies.
/// This must be called before using any other methods.
/// </summary>
/// <param name="synchronizerFactory">Factory for creating synchronizers</param>
/// <param name="imapTestService">Service for testing IMAP connectivity</param>
/// <param name="accountService">Service for account operations</param>
/// <param name="authenticationProvider">Provider for OAuth authentication</param>
public async Task InitializeAsync(ISynchronizerFactory synchronizerFactory,
IImapTestService imapTestService,
IAccountService accountService,
IAuthenticationProvider authenticationProvider)
{
await _initializationSemaphore.WaitAsync();
try
{
if (_isInitialized) return;
_synchronizerFactory = synchronizerFactory ?? throw new ArgumentNullException(nameof(synchronizerFactory));
_concreteSynchronizerFactory = synchronizerFactory as SynchronizerFactory ?? throw new ArgumentException("SynchronizerFactory must be the concrete implementation");
_imapTestService = imapTestService ?? throw new ArgumentNullException(nameof(imapTestService));
_accountService = accountService ?? throw new ArgumentNullException(nameof(accountService));
_authenticationProvider = authenticationProvider ?? throw new ArgumentNullException(nameof(authenticationProvider));
// Get all accounts and create synchronizers for them
var accounts = await _accountService.GetAccountsAsync();
foreach (var account in accounts)
{
try
{
var synchronizer = _concreteSynchronizerFactory.CreateNewSynchronizer(account);
_synchronizerCache.TryAdd(account.Id, synchronizer);
_logger.Information("Created synchronizer for account {AccountName} ({AccountId})",
account.Name, account.Id);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to create synchronizer for account {AccountName} ({AccountId})",
account.Name, account.Id);
}
}
_isInitialized = true;
_logger.Information("SynchronizationManager initialized with {Count} synchronizers", _synchronizerCache.Count);
}
finally
{
_initializationSemaphore.Release();
}
}
/// <summary>
/// Tests IMAP server connectivity for the given server information.
/// </summary>
/// <param name="serverInformation">Server information to test</param>
/// <param name="allowSSLHandshake">Whether to allow SSL handshake</param>
/// <returns>Test results indicating success or failure with details</returns>
public async Task<ImapConnectivityTestResults> TestImapConnectivityAsync(CustomServerInformation serverInformation, bool allowSSLHandshake)
{
EnsureInitialized();
try
{
_logger.Information("Testing IMAP connectivity for {Server}:{Port}",
serverInformation.IncomingServer,
serverInformation.IncomingServerPort);
await _imapTestService.TestImapConnectionAsync(serverInformation, allowSSLHandshake);
_logger.Information("IMAP connectivity test successful");
return ImapConnectivityTestResults.Success();
}
catch (ImapTestSSLCertificateException sslTestException)
{
_logger.Warning("IMAP connectivity test requires SSL certificate confirmation");
return ImapConnectivityTestResults.CertificateUIRequired(
sslTestException.Issuer,
sslTestException.ExpirationDateString,
sslTestException.ValidFromDateString);
}
catch (ImapClientPoolException clientPoolException)
{
_logger.Error(clientPoolException, "IMAP connectivity test failed with protocol log");
return ImapConnectivityTestResults.Failure(clientPoolException, clientPoolException.ProtocolLog);
}
catch (Exception exception)
{
_logger.Error(exception, "IMAP connectivity test failed");
return ImapConnectivityTestResults.Failure(exception, string.Empty);
}
}
/// <summary>
/// Starts a new mail synchronization for the given account.
/// </summary>
/// <param name="options">Mail synchronization options</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Synchronization result</returns>
public async Task<MailSynchronizationResult> SynchronizeMailAsync(MailSynchronizationOptions options,
CancellationToken cancellationToken = default)
{
EnsureInitialized();
var synchronizer = await GetOrCreateSynchronizerAsync(options.AccountId);
if (synchronizer == null)
{
_logger.Error("Could not find or create synchronizer for account {AccountId}", options.AccountId);
return MailSynchronizationResult.Failed;
}
_logger.Information("Starting mail synchronization for account {AccountId} with type {SyncType}",
options.AccountId, options.Type);
try
{
var result = await synchronizer.SynchronizeMailsAsync(options, cancellationToken);
_logger.Information("Mail synchronization completed for account {AccountId} with state {State}",
options.AccountId, result.CompletedState);
return result;
}
catch (Exception ex)
{
_logger.Error(ex, "Mail synchronization failed for account {AccountId}", options.AccountId);
return MailSynchronizationResult.Failed;
}
}
/// <summary>
/// Checks if there is an ongoing synchronization for the given account.
/// </summary>
/// <param name="accountId">Account ID to check</param>
/// <returns>True if synchronization is ongoing, false otherwise</returns>
public bool IsAccountSynchronizing(Guid accountId)
{
EnsureInitialized();
if (_synchronizerCache.TryGetValue(accountId, out var synchronizer))
{
return synchronizer.State == AccountSynchronizerState.Synchronizing ||
synchronizer.State == AccountSynchronizerState.ExecutingRequests;
}
return false;
}
/// <summary>
/// Queues a mail action request to the corresponding account's synchronizer.
/// </summary>
/// <param name="request">Request to queue</param>
/// <param name="accountId">Account ID to queue the request for</param>
public async Task QueueRequestAsync(IRequestBase request, Guid accountId)
{
EnsureInitialized();
var synchronizer = await GetOrCreateSynchronizerAsync(accountId);
if (synchronizer == null)
{
_logger.Error("Could not find or create synchronizer for account {AccountId} to queue request", accountId);
return;
}
_logger.Debug("Queuing request {RequestType} for account {AccountId}",
request.GetType().Name, accountId);
synchronizer.QueueRequest(request);
}
/// <summary>
/// Handles folder synchronization for the given account.
/// </summary>
/// <param name="accountId">Account ID to synchronize folders for</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Synchronization result</returns>
public async Task<MailSynchronizationResult> SynchronizeFoldersAsync(Guid accountId,
CancellationToken cancellationToken = default)
{
EnsureInitialized();
var options = new MailSynchronizationOptions
{
AccountId = accountId,
Type = MailSynchronizationType.FoldersOnly
};
return await SynchronizeMailAsync(options, cancellationToken);
}
/// <summary>
/// Handles alias synchronization for the given account.
/// </summary>
/// <param name="accountId">Account ID to synchronize aliases for</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Synchronization result</returns>
public async Task<MailSynchronizationResult> SynchronizeAliasesAsync(Guid accountId,
CancellationToken cancellationToken = default)
{
EnsureInitialized();
var options = new MailSynchronizationOptions
{
AccountId = accountId,
Type = MailSynchronizationType.Alias
};
return await SynchronizeMailAsync(options, cancellationToken);
}
/// <summary>
/// Handles profile synchronization for the given account.
/// </summary>
/// <param name="accountId">Account ID to synchronize profile for</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Synchronization result</returns>
public async Task<MailSynchronizationResult> SynchronizeProfileAsync(Guid accountId,
CancellationToken cancellationToken = default)
{
EnsureInitialized();
var options = new MailSynchronizationOptions
{
AccountId = accountId,
Type = MailSynchronizationType.UpdateProfile
};
return await SynchronizeMailAsync(options, cancellationToken);
}
/// <summary>
/// Downloads a MIME message for the given mail item.
/// </summary>
/// <param name="mailItem">Mail item to download</param>
/// <param name="accountId">Account ID that owns the mail item</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Downloaded MIME content path</returns>
public async Task<string> DownloadMimeMessageAsync(MailCopy mailItem, Guid accountId,
CancellationToken cancellationToken = default)
{
EnsureInitialized();
var synchronizer = await GetOrCreateSynchronizerAsync(accountId);
if (synchronizer == null)
{
_logger.Error("Could not find or create synchronizer for account {AccountId} to download MIME", accountId);
return null;
}
_logger.Debug("Downloading MIME message for mail item {MailItemId}", mailItem.Id);
try
{
await synchronizer.DownloadMissingMimeMessageAsync(mailItem, null, cancellationToken);
return mailItem.Id.ToString(); // Return some identifier, actual implementation might be different
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to download MIME message for mail item {MailItemId}", mailItem.Id);
return null;
}
}
/// <summary>
/// Creates a new synchronizer for a newly added account.
/// </summary>
/// <param name="account">Account to create synchronizer for</param>
/// <returns>Created synchronizer</returns>
public Task<IWinoSynchronizerBase> CreateSynchronizerForAccountAsync(MailAccount account)
{
EnsureInitialized();
try
{
var synchronizer = _concreteSynchronizerFactory.CreateNewSynchronizer(account);
_synchronizerCache.TryAdd(account.Id, synchronizer);
_logger.Information("Created new synchronizer for account {AccountName} ({AccountId})",
account.Name, account.Id);
return Task.FromResult(synchronizer);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to create synchronizer for account {AccountName} ({AccountId})",
account.Name, account.Id);
return Task.FromResult<IWinoSynchronizerBase>(null);
}
}
/// <summary>
/// Destroys the synchronizer for the given account.
/// </summary>
/// <param name="accountId">Account ID to destroy synchronizer for</param>
public async Task DestroySynchronizerAsync(Guid accountId)
{
EnsureInitialized();
if (_synchronizerCache.TryRemove(accountId, out var synchronizer))
{
try
{
await synchronizer.KillSynchronizerAsync();
_logger.Information("Destroyed synchronizer for account {AccountId}", accountId);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to destroy synchronizer for account {AccountId}", accountId);
}
}
}
/// <summary>
/// Gets all cached synchronizers.
/// </summary>
/// <returns>Collection of all cached synchronizers</returns>
public IEnumerable<IWinoSynchronizerBase> GetAllSynchronizers()
{
EnsureInitialized();
return _synchronizerCache.Values.ToList();
}
/// <summary>
/// Gets a synchronizer for the given account ID.
/// </summary>
/// <param name="accountId">Account ID</param>
/// <returns>Synchronizer if found, null otherwise</returns>
public async Task<IWinoSynchronizerBase> GetSynchronizerAsync(Guid accountId)
{
EnsureInitialized();
return await GetOrCreateSynchronizerAsync(accountId);
}
private async Task<IWinoSynchronizerBase> GetOrCreateSynchronizerAsync(Guid accountId)
{
if (_synchronizerCache.TryGetValue(accountId, out var existingSynchronizer))
{
return existingSynchronizer;
}
// Try to create a new synchronizer if not found
var account = await _accountService.GetAccountAsync(accountId);
if (account != null)
{
return await CreateSynchronizerForAccountAsync(account);
}
return null;
}
/// <summary>
/// Handles OAuth authentication for the specified provider.
/// </summary>
/// <param name="providerType">The mail provider type to authenticate</param>
/// <param name="account">Optional account to authenticate (null for initial authentication)</param>
/// <param name="proposeCopyAuthorizationURL">Whether to propose copying auth URL for Gmail</param>
/// <returns>Token information containing access token and username</returns>
public async Task<TokenInformationEx> HandleAuthorizationAsync(MailProviderType providerType,
MailAccount account = null,
bool proposeCopyAuthorizationURL = false)
{
EnsureInitialized();
try
{
var authenticator = _authenticationProvider.GetAuthenticator(providerType);
// Some users are having issues with Gmail authentication.
// Their browsers may never launch to complete authentication.
// Offer to copy auth url for them to complete it manually.
// Redirection will occur to the app and the token will be saved.
if (proposeCopyAuthorizationURL && authenticator is IGmailAuthenticator gmailAuthenticator)
{
gmailAuthenticator.ProposeCopyAuthURL = true;
}
TokenInformationEx tokenInfo;
if (account != null)
{
// Get token for existing account (may trigger interactive auth if token is expired)
tokenInfo = await authenticator.GetTokenInformationAsync(account);
_logger.Information("Retrieved token for existing account {AccountAddress}", account.Address);
}
else
{
// Initial authentication request - there is no account to get token for
// This will always trigger interactive authentication
tokenInfo = await authenticator.GenerateTokenInformationAsync(null);
_logger.Information("Generated new token for {ProviderType} authentication", providerType);
}
return tokenInfo;
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to handle authorization for {ProviderType}", providerType);
throw;
}
}
private void EnsureInitialized()
{
if (!_isInitialized)
{
throw new InvalidOperationException("SynchronizationManager must be initialized before use. Call InitializeAsync first.");
}
}
}
@@ -0,0 +1,33 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Services;
namespace Wino.Core.Services;
/// <summary>
/// Service responsible for initializing the SynchronizationManager during app startup.
/// </summary>
public class SynchronizationManagerInitializer : IInitializeAsync
{
private readonly IServiceProvider _serviceProvider;
public SynchronizationManagerInitializer(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task InitializeAsync()
{
var synchronizerFactory = _serviceProvider.GetRequiredService<ISynchronizerFactory>();
var imapTestService = _serviceProvider.GetRequiredService<IImapTestService>();
var accountService = _serviceProvider.GetRequiredService<IAccountService>();
var authenticationProvider = _serviceProvider.GetRequiredService<IAuthenticationProvider>();
// Cast to concrete type to access CreateNewSynchronizer method
var concreteSynchronizerFactory = synchronizerFactory as SynchronizerFactory;
await SynchronizationManager.Instance.InitializeAsync(concreteSynchronizerFactory, imapTestService, accountService, authenticationProvider);
}
}
+3 -15
View File
@@ -19,17 +19,14 @@ namespace Wino.Core.Services;
public class WinoRequestDelegator : IWinoRequestDelegator public class WinoRequestDelegator : IWinoRequestDelegator
{ {
private readonly IWinoRequestProcessor _winoRequestProcessor; private readonly IWinoRequestProcessor _winoRequestProcessor;
private readonly IWinoServerConnectionManager _winoServerConnectionManager;
private readonly IFolderService _folderService; private readonly IFolderService _folderService;
private readonly IMailDialogService _dialogService; private readonly IMailDialogService _dialogService;
public WinoRequestDelegator(IWinoRequestProcessor winoRequestProcessor, public WinoRequestDelegator(IWinoRequestProcessor winoRequestProcessor,
IWinoServerConnectionManager winoServerConnectionManager,
IFolderService folderService, IFolderService folderService,
IMailDialogService dialogService) IMailDialogService dialogService)
{ {
_winoRequestProcessor = winoRequestProcessor; _winoRequestProcessor = winoRequestProcessor;
_winoServerConnectionManager = winoServerConnectionManager;
_folderService = folderService; _folderService = folderService;
_dialogService = dialogService; _dialogService = dialogService;
} }
@@ -138,14 +135,11 @@ public class WinoRequestDelegator : IWinoRequestDelegator
private async Task QueueRequestAsync(IRequestBase request, Guid accountId) private async Task QueueRequestAsync(IRequestBase request, Guid accountId)
{ {
await EnsureServerConnectedAsync(); await SynchronizationManager.Instance.QueueRequestAsync(request, accountId);
await _winoServerConnectionManager.QueueRequestAsync(request, accountId);
} }
private async Task QueueSynchronizationAsync(Guid accountId) private Task QueueSynchronizationAsync(Guid accountId)
{ {
await EnsureServerConnectedAsync();
var options = new MailSynchronizationOptions() var options = new MailSynchronizationOptions()
{ {
AccountId = accountId, AccountId = accountId,
@@ -153,12 +147,6 @@ public class WinoRequestDelegator : IWinoRequestDelegator
}; };
WeakReferenceMessenger.Default.Send(new NewMailSynchronizationRequested(options, SynchronizationSource.Client)); WeakReferenceMessenger.Default.Send(new NewMailSynchronizationRequested(options, SynchronizationSource.Client));
} return Task.CompletedTask;
private async Task EnsureServerConnectedAsync()
{
if (_winoServerConnectionManager.Status == WinoServerConnectionStatus.Connected) return;
await _winoServerConnectionManager.ConnectAsync();
} }
} }
@@ -11,8 +11,8 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Services;
using Wino.Messaging.Client.Navigation; using Wino.Messaging.Client.Navigation;
using Wino.Messaging.Server;
namespace Wino.Mail.ViewModels; namespace Wino.Mail.ViewModels;
@@ -20,7 +20,6 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
{ {
private readonly IMailDialogService _dialogService; private readonly IMailDialogService _dialogService;
private readonly IAccountService _accountService; private readonly IAccountService _accountService;
private readonly IWinoServerConnectionManager _serverConnectionManager;
private readonly IFolderService _folderService; private readonly IFolderService _folderService;
private bool isLoaded = false; private bool isLoaded = false;
@@ -50,12 +49,10 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
public AccountDetailsPageViewModel(IMailDialogService dialogService, public AccountDetailsPageViewModel(IMailDialogService dialogService,
IAccountService accountService, IAccountService accountService,
IWinoServerConnectionManager serverConnectionManager,
IFolderService folderService) IFolderService folderService)
{ {
_dialogService = dialogService; _dialogService = dialogService;
_accountService = accountService; _accountService = accountService;
_serverConnectionManager = serverConnectionManager;
_folderService = folderService; _folderService = folderService;
} }
@@ -95,21 +92,16 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
return; return;
var isSynchronizerKilledResponse = await _serverConnectionManager.GetResponseAsync<bool, KillAccountSynchronizerRequested>(new KillAccountSynchronizerRequested(Account.Id)); await SynchronizationManager.Instance.DestroySynchronizerAsync(Account.Id);
if (isSynchronizerKilledResponse.IsSuccess) await _accountService.DeleteAccountAsync(Account);
{
await _accountService.DeleteAccountAsync(Account);
_dialogService.InfoBarMessage(Translator.Info_AccountDeletedTitle, string.Format(Translator.Info_AccountDeletedMessage, Account.Name), InfoBarMessageType.Success); _dialogService.InfoBarMessage(Translator.Info_AccountDeletedTitle, string.Format(Translator.Info_AccountDeletedMessage, Account.Name), InfoBarMessageType.Success);
Messenger.Send(new BackBreadcrumNavigationRequested()); Messenger.Send(new BackBreadcrumNavigationRequested());
}
} }
public override async void OnNavigatedTo(NavigationMode mode, object parameters) public override async void OnNavigatedTo(NavigationMode mode, object parameters)
{ {
base.OnNavigatedTo(mode, parameters); base.OnNavigatedTo(mode, parameters);
@@ -12,15 +12,13 @@ using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions; using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Authentication;
using Wino.Core.Domain.Models.Connectivity;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Services;
using Wino.Core.ViewModels; using Wino.Core.ViewModels;
using Wino.Core.ViewModels.Data; using Wino.Core.ViewModels.Data;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Navigation; using Wino.Messaging.Client.Navigation;
using Wino.Messaging.Server;
using Wino.Messaging.UI; using Wino.Messaging.UI;
namespace Wino.Mail.ViewModels; namespace Wino.Mail.ViewModels;
@@ -34,7 +32,6 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
public IMailDialogService MailDialogService { get; } public IMailDialogService MailDialogService { get; }
public AccountManagementViewModel(IMailDialogService dialogService, public AccountManagementViewModel(IMailDialogService dialogService,
IWinoServerConnectionManager winoServerConnectionManager,
INavigationService navigationService, INavigationService navigationService,
IAccountService accountService, IAccountService accountService,
ISpecialImapProviderConfigResolver specialImapProviderConfigResolver, ISpecialImapProviderConfigResolver specialImapProviderConfigResolver,
@@ -43,7 +40,7 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
IStoreManagementService storeManagementService, IStoreManagementService storeManagementService,
IWinoLogger winoLogger, IWinoLogger winoLogger,
IAuthenticationProvider authenticationProvider, IAuthenticationProvider authenticationProvider,
IPreferencesService preferencesService) : base(dialogService, winoServerConnectionManager, navigationService, accountService, providerService, storeManagementService, authenticationProvider, preferencesService) IPreferencesService preferencesService) : base(dialogService, navigationService, accountService, providerService, storeManagementService, authenticationProvider, preferencesService)
{ {
MailDialogService = dialogService; MailDialogService = dialogService;
_specialImapProviderConfigResolver = specialImapProviderConfigResolver; _specialImapProviderConfigResolver = specialImapProviderConfigResolver;
@@ -154,37 +151,36 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
createdAccount.Address = customServerInformation.Address; createdAccount.Address = customServerInformation.Address;
// Let server validate the imap/smtp connection. // Let server validate the imap/smtp connection.
var testResultResponse = await WinoServerConnectionManager.GetResponseAsync<ImapConnectivityTestResults, ImapConnectivityTestRequested>(new ImapConnectivityTestRequested(customServerInformation, true)); // TODO: Protocol log with detailed failure.
if (!testResultResponse.IsSuccess) await _imapTestService.TestImapConnectionAsync(customServerInformation, true);
{ //var testResultResponse = await WinoServerConnectionManager.GetResponseAsync<ImapConnectivityTestResults, ImapConnectivityTestRequested>(new ImapConnectivityTestRequested(customServerInformation, true));
throw new Exception($"{Translator.IMAPSetupDialog_ConnectionFailedTitle}\n{testResultResponse.Message}");
} //if (!testResultResponse.IsSuccess)
else if (!testResultResponse.Data.IsSuccess) //{
{ // throw new Exception($"{Translator.IMAPSetupDialog_ConnectionFailedTitle}\n{testResultResponse.Message}");
// Server connectivity might succeed, but result might be failed. //}
throw new ImapClientPoolException(testResultResponse.Data.FailedReason, customServerInformation, testResultResponse.Data.FailureProtocolLog); //else if (!testResultResponse.Data.IsSuccess)
} //{
// // Server connectivity might succeed, but result might be failed.
// throw new ImapClientPoolException(testResultResponse.Data.FailedReason, customServerInformation, testResultResponse.Data.FailureProtocolLog);
//}
} }
else else
{ {
// OAuth authentication is handled here. // OAuth authentication is handled here.
// Server authenticates, returns the token info here. // Use SynchronizationManager to handle OAuth authentication.
var tokenInformationResponse = await WinoServerConnectionManager var authTokenInfo = await SynchronizationManager.Instance.HandleAuthorizationAsync(
.GetResponseAsync<TokenInformationEx, AuthorizationRequested>(new AuthorizationRequested(accountCreationDialogResult.ProviderType, accountCreationDialogResult.ProviderType,
createdAccount, createdAccount,
createdAccount.ProviderType == MailProviderType.Gmail), accountCreationCancellationTokenSource.Token); createdAccount.ProviderType == MailProviderType.Gmail);
if (creationDialog.State == AccountCreationDialogState.Canceled) if (creationDialog.State == AccountCreationDialogState.Canceled)
throw new AccountSetupCanceledException(); throw new AccountSetupCanceledException();
if (!tokenInformationResponse.IsSuccess) // Update account address with authenticated user information
throw new Exception(tokenInformationResponse.Message); createdAccount.Address = authTokenInfo.AccountAddress;
createdAccount.Address = tokenInformationResponse.Data.AccountAddress;
tokenInformationResponse.ThrowIfFailed();
} }
} }
@@ -207,22 +203,23 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
Type = MailSynchronizationType.UpdateProfile Type = MailSynchronizationType.UpdateProfile
}; };
var profileSynchronizationResponse = await WinoServerConnectionManager.GetResponseAsync<MailSynchronizationResult, NewMailSynchronizationRequested>(new NewMailSynchronizationRequested(profileSyncOptions, SynchronizationSource.Client)); var profileSynchronizationResult = await SynchronizationManager.Instance.SynchronizeProfileAsync(createdAccount.Id);
var profileSynchronizationResult = profileSynchronizationResponse.Data;
if (profileSynchronizationResult.CompletedState != SynchronizationCompletedState.Success) if (profileSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation); throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation);
createdAccount.SenderName = profileSynchronizationResult.ProfileInformation.SenderName; if (profileSynchronizationResult.ProfileInformation != null)
createdAccount.Base64ProfilePictureData = profileSynchronizationResult.ProfileInformation.Base64ProfilePictureData;
if (!string.IsNullOrEmpty(profileSynchronizationResult.ProfileInformation.AccountAddress))
{ {
createdAccount.Address = profileSynchronizationResult.ProfileInformation.AccountAddress; createdAccount.SenderName = profileSynchronizationResult.ProfileInformation.SenderName;
} createdAccount.Base64ProfilePictureData = profileSynchronizationResult.ProfileInformation.Base64ProfilePictureData;
await AccountService.UpdateProfileInformationAsync(createdAccount.Id, profileSynchronizationResult.ProfileInformation); if (!string.IsNullOrEmpty(profileSynchronizationResult.ProfileInformation.AccountAddress))
{
createdAccount.Address = profileSynchronizationResult.ProfileInformation.AccountAddress;
}
await AccountService.UpdateProfileInformationAsync(createdAccount.Id, profileSynchronizationResult.ProfileInformation);
}
} }
if (creationDialog is IImapAccountCreationDialog customServerAccountCreationDialog) if (creationDialog is IImapAccountCreationDialog customServerAccountCreationDialog)
@@ -237,26 +234,16 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
Type = MailSynchronizationType.FoldersOnly Type = MailSynchronizationType.FoldersOnly
}; };
var folderSynchronizationResponse = await WinoServerConnectionManager.GetResponseAsync<MailSynchronizationResult, NewMailSynchronizationRequested>(new NewMailSynchronizationRequested(folderSyncOptions, SynchronizationSource.Client)); var folderSynchronizationResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(createdAccount.Id);
var folderSynchronizationResult = folderSynchronizationResponse.Data;
if (folderSynchronizationResult == null || folderSynchronizationResult.CompletedState != SynchronizationCompletedState.Success) if (folderSynchronizationResult == null || folderSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception($"{Translator.Exception_FailedToSynchronizeFolders}\n{folderSynchronizationResponse.Message}"); throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
// Sync aliases if supported. // Sync aliases if supported.
if (createdAccount.IsAliasSyncSupported) if (createdAccount.IsAliasSyncSupported)
{ {
// Try to synchronize aliases for the account. // Try to synchronize aliases for the account.
var aliasSynchronizationResult = await SynchronizationManager.Instance.SynchronizeAliasesAsync(createdAccount.Id);
var aliasSyncOptions = new MailSynchronizationOptions()
{
AccountId = createdAccount.Id,
Type = MailSynchronizationType.Alias
};
var aliasSyncResponse = await WinoServerConnectionManager.GetResponseAsync<MailSynchronizationResult, NewMailSynchronizationRequested>(new NewMailSynchronizationRequested(aliasSyncOptions, SynchronizationSource.Client));
var aliasSynchronizationResult = folderSynchronizationResponse.Data;
if (aliasSynchronizationResult.CompletedState != SynchronizationCompletedState.Success) if (aliasSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeAliases); throw new Exception(Translator.Exception_FailedToSynchronizeAliases);
@@ -12,7 +12,7 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Domain.Models.Synchronization;
using Wino.Messaging.Server; using Wino.Core.Services;
namespace Wino.Mail.ViewModels; namespace Wino.Mail.ViewModels;
@@ -20,7 +20,6 @@ public partial class AliasManagementPageViewModel : MailBaseViewModel
{ {
private readonly IMailDialogService _dialogService; private readonly IMailDialogService _dialogService;
private readonly IAccountService _accountService; private readonly IAccountService _accountService;
private readonly IWinoServerConnectionManager _winoServerConnectionManager;
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanSynchronizeAliases))] [NotifyPropertyChangedFor(nameof(CanSynchronizeAliases))]
@@ -32,12 +31,10 @@ public partial class AliasManagementPageViewModel : MailBaseViewModel
public bool CanSynchronizeAliases => Account?.IsAliasSyncSupported ?? false; public bool CanSynchronizeAliases => Account?.IsAliasSyncSupported ?? false;
public AliasManagementPageViewModel(IMailDialogService dialogService, public AliasManagementPageViewModel(IMailDialogService dialogService,
IAccountService accountService, IAccountService accountService)
IWinoServerConnectionManager winoServerConnectionManager)
{ {
_dialogService = dialogService; _dialogService = dialogService;
_accountService = accountService; _accountService = accountService;
_winoServerConnectionManager = winoServerConnectionManager;
} }
public override async void OnNavigatedTo(NavigationMode mode, object parameters) public override async void OnNavigatedTo(NavigationMode mode, object parameters)
@@ -82,12 +79,12 @@ public partial class AliasManagementPageViewModel : MailBaseViewModel
Type = MailSynchronizationType.Alias Type = MailSynchronizationType.Alias
}; };
var aliasSyncResponse = await _winoServerConnectionManager.GetResponseAsync<MailSynchronizationResult, NewMailSynchronizationRequested>(new NewMailSynchronizationRequested(aliasSyncOptions, SynchronizationSource.Client)); var aliasSyncResult = await SynchronizationManager.Instance.SynchronizeAliasesAsync(Account.Id);
if (aliasSyncResponse.IsSuccess) if (aliasSyncResult.CompletedState == SynchronizationCompletedState.Success)
await LoadAliasesAsync(); await LoadAliasesAsync();
else else
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error, aliasSyncResponse.Message, InfoBarMessageType.Error); _dialogService.InfoBarMessage(Translator.GeneralTitle_Error, "Failed to synchronize aliases", InfoBarMessageType.Error);
} }
[RelayCommand] [RelayCommand]
+7 -11
View File
@@ -13,11 +13,11 @@ using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions; using Wino.Core.Domain.Exceptions;
using Wino.Core.Services;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Extensions; using Wino.Core.Extensions;
using Wino.Core.Services;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Mails; using Wino.Messaging.Client.Mails;
using Wino.Messaging.Server; using Wino.Messaging.Server;
@@ -102,7 +102,6 @@ public partial class ComposePageViewModel : MailBaseViewModel
private readonly IWinoRequestDelegator _worker; private readonly IWinoRequestDelegator _worker;
public readonly IFontService FontService; public readonly IFontService FontService;
public readonly IPreferencesService PreferencesService; public readonly IPreferencesService PreferencesService;
private readonly IWinoServerConnectionManager _winoServerConnectionManager;
public readonly IContactService ContactService; public readonly IContactService ContactService;
public ComposePageViewModel(IMailDialogService dialogService, public ComposePageViewModel(IMailDialogService dialogService,
@@ -115,8 +114,7 @@ public partial class ComposePageViewModel : MailBaseViewModel
IWinoRequestDelegator worker, IWinoRequestDelegator worker,
IContactService contactService, IContactService contactService,
IFontService fontService, IFontService fontService,
IPreferencesService preferencesService, IPreferencesService preferencesService)
IWinoServerConnectionManager winoServerConnectionManager)
{ {
NativeAppService = nativeAppService; NativeAppService = nativeAppService;
ContactService = contactService; ContactService = contactService;
@@ -130,7 +128,6 @@ public partial class ComposePageViewModel : MailBaseViewModel
_fileService = fileService; _fileService = fileService;
_accountService = accountService; _accountService = accountService;
_worker = worker; _worker = worker;
_winoServerConnectionManager = winoServerConnectionManager;
} }
[RelayCommand] [RelayCommand]
@@ -412,13 +409,12 @@ public partial class ComposePageViewModel : MailBaseViewModel
{ {
downloadIfNeeded = false; downloadIfNeeded = false;
var package = new DownloadMissingMessageRequested(CurrentMailDraftItem.AssignedAccount.Id, CurrentMailDraftItem.MailCopy); // Download missing MIME message using SynchronizationManager
var downloadResponse = await _winoServerConnectionManager.GetResponseAsync<bool, DownloadMissingMessageRequested>(package); await SynchronizationManager.Instance.DownloadMimeMessageAsync(
CurrentMailDraftItem.MailCopy,
CurrentMailDraftItem.AssignedAccount.Id);
if (downloadResponse.IsSuccess) goto retry;
{
goto retry;
}
} }
else else
_dialogService.InfoBarMessage(Translator.Info_ComposerMissingMIMETitle, Translator.Info_ComposerMissingMIMEMessage, InfoBarMessageType.Error); _dialogService.InfoBarMessage(Translator.Info_ComposerMissingMIMETitle, Translator.Info_ComposerMissingMIMEMessage, InfoBarMessageType.Error);
+43 -46
View File
@@ -23,6 +23,7 @@ using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Menus; using Wino.Core.Domain.Models.Menus;
using Wino.Core.Domain.Models.Reader; using Wino.Core.Domain.Models.Reader;
using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Services;
using Wino.Mail.ViewModels.Collections; using Wino.Mail.ViewModels.Collections;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages; using Wino.Mail.ViewModels.Messages;
@@ -81,7 +82,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
private readonly IWinoRequestDelegator _winoRequestDelegator; private readonly IWinoRequestDelegator _winoRequestDelegator;
private readonly IKeyPressService _keyPressService; private readonly IKeyPressService _keyPressService;
private readonly IWinoLogger _winoLogger; private readonly IWinoLogger _winoLogger;
private readonly IWinoServerConnectionManager _winoServerConnectionManager;
private MailItemViewModel _activeMailItem; private MailItemViewModel _activeMailItem;
public List<SortingOption> SortingOptions { get; } = public List<SortingOption> SortingOptions { get; } =
@@ -160,14 +160,12 @@ public partial class MailListPageViewModel : MailBaseViewModel,
IKeyPressService keyPressService, IKeyPressService keyPressService,
IPreferencesService preferencesService, IPreferencesService preferencesService,
INewThemeService themeService, INewThemeService themeService,
IWinoLogger winoLogger, IWinoLogger winoLogger)
IWinoServerConnectionManager winoServerConnectionManager)
{ {
MailCollection = new WinoMailCollection(threadingStrategyProvider); MailCollection = new WinoMailCollection(threadingStrategyProvider);
PreferencesService = preferencesService; PreferencesService = preferencesService;
ThemeService = themeService; ThemeService = themeService;
_winoLogger = winoLogger; _winoLogger = winoLogger;
_winoServerConnectionManager = winoServerConnectionManager;
StatePersistenceService = statePersistenceService; StatePersistenceService = statePersistenceService;
NavigationService = navigationService; NavigationService = navigationService;
_accountService = accountService; _accountService = accountService;
@@ -841,51 +839,52 @@ public partial class MailListPageViewModel : MailBaseViewModel,
// Perform online search. // Perform online search.
if (isDoingOnlineSearch) if (isDoingOnlineSearch)
{ {
WinoServerResponse<OnlineSearchResult> onlineSearchResult = null; // TODO: Burak: Handle online search.
string onlineSearchFailedMessage = null; //WinoServerResponse<OnlineSearchResult> onlineSearchResult = null;
//string onlineSearchFailedMessage = null;
try //try
{ //{
var accountIds = ActiveFolder.HandlingFolders.Select(a => a.MailAccountId).ToList(); // var accountIds = ActiveFolder.HandlingFolders.Select(a => a.MailAccountId).ToList();
var folders = ActiveFolder.HandlingFolders.ToList(); // var folders = ActiveFolder.HandlingFolders.ToList();
var searchRequest = new OnlineSearchRequested(accountIds, SearchQuery, folders); // var searchRequest = new OnlineSearchRequested(accountIds, SearchQuery, folders);
onlineSearchResult = await _winoServerConnectionManager.GetResponseAsync<OnlineSearchResult, OnlineSearchRequested>(searchRequest, cancellationToken); // onlineSearchResult = await _winoServerConnectionManager.GetResponseAsync<OnlineSearchResult, OnlineSearchRequested>(searchRequest, cancellationToken);
if (onlineSearchResult.IsSuccess) // if (onlineSearchResult.IsSuccess)
{ // {
await ExecuteUIThread(() => { AreSearchResultsOnline = true; }); // await ExecuteUIThread(() => { AreSearchResultsOnline = true; });
onlineSearchItems = onlineSearchResult.Data.SearchResult; // onlineSearchItems = onlineSearchResult.Data.SearchResult;
} // }
else // else
{ // {
onlineSearchFailedMessage = onlineSearchResult.Message; // onlineSearchFailedMessage = onlineSearchResult.Message;
} // }
} //}
catch (OperationCanceledException) //catch (OperationCanceledException)
{ //{
throw; // throw;
} //}
catch (Exception ex) //catch (Exception ex)
{ //{
Log.Warning(ex, "Failed to perform online search."); // Log.Warning(ex, "Failed to perform online search.");
onlineSearchFailedMessage = ex.Message; // onlineSearchFailedMessage = ex.Message;
} //}
if (onlineSearchResult != null && !onlineSearchResult.IsSuccess) //if (onlineSearchResult != null && !onlineSearchResult.IsSuccess)
{ //{
// Query or server error. // // Query or server error.
var serverErrorMessage = string.Format(Translator.OnlineSearchFailed_Message, onlineSearchResult.Message); // var serverErrorMessage = string.Format(Translator.OnlineSearchFailed_Message, onlineSearchResult.Message);
_mailDialogService.InfoBarMessage(Translator.GeneralTitle_Error, serverErrorMessage, InfoBarMessageType.Warning); // _mailDialogService.InfoBarMessage(Translator.GeneralTitle_Error, serverErrorMessage, InfoBarMessageType.Warning);
} //}
else if (!string.IsNullOrEmpty(onlineSearchFailedMessage)) //else if (!string.IsNullOrEmpty(onlineSearchFailedMessage))
{ //{
// Fatal error. // // Fatal error.
var serverErrorMessage = string.Format(Translator.OnlineSearchFailed_Message, onlineSearchFailedMessage); // var serverErrorMessage = string.Format(Translator.OnlineSearchFailed_Message, onlineSearchFailedMessage);
_mailDialogService.InfoBarMessage(Translator.GeneralTitle_Error, serverErrorMessage, InfoBarMessageType.Warning); // _mailDialogService.InfoBarMessage(Translator.GeneralTitle_Error, serverErrorMessage, InfoBarMessageType.Warning);
} //}
} }
} }
@@ -1110,9 +1109,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
foreach (var accountId in accountIds) foreach (var accountId in accountIds)
{ {
var serverResponse = await _winoServerConnectionManager.GetResponseAsync<bool, SynchronizationExistenceCheckRequest>(new SynchronizationExistenceCheckRequest(accountId)); if (SynchronizationManager.Instance.IsAccountSynchronizing(accountId))
if (serverResponse.IsSuccess && serverResponse.Data == true)
{ {
isAnyAccountSynchronizing = true; isAnyAccountSynchronizing = true;
break; break;
@@ -21,6 +21,7 @@ using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Menus; using Wino.Core.Domain.Models.Menus;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Reader; using Wino.Core.Domain.Models.Reader;
using Wino.Core.Services;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages; using Wino.Mail.ViewModels.Messages;
using Wino.Messaging.Client.Mails; using Wino.Messaging.Client.Mails;
@@ -46,7 +47,6 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
private readonly IClipboardService _clipboardService; private readonly IClipboardService _clipboardService;
private readonly IUnsubscriptionService _unsubscriptionService; private readonly IUnsubscriptionService _unsubscriptionService;
private readonly IApplicationConfiguration _applicationConfiguration; private readonly IApplicationConfiguration _applicationConfiguration;
private readonly IWinoServerConnectionManager _winoServerConnectionManager;
private bool forceImageLoading = false; private bool forceImageLoading = false;
private MailItemViewModel initializedMailItemViewModel = null; private MailItemViewModel initializedMailItemViewModel = null;
@@ -142,8 +142,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
IUnsubscriptionService unsubscriptionService, IUnsubscriptionService unsubscriptionService,
IPreferencesService preferencesService, IPreferencesService preferencesService,
IPrintService printService, IPrintService printService,
IApplicationConfiguration applicationConfiguration, IApplicationConfiguration applicationConfiguration)
IWinoServerConnectionManager winoServerConnectionManager)
{ {
_dialogService = dialogService; _dialogService = dialogService;
NativeAppService = nativeAppService; NativeAppService = nativeAppService;
@@ -152,7 +151,6 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
PreferencesService = preferencesService; PreferencesService = preferencesService;
PrintService = printService; PrintService = printService;
_applicationConfiguration = applicationConfiguration; _applicationConfiguration = applicationConfiguration;
_winoServerConnectionManager = winoServerConnectionManager;
_clipboardService = clipboardService; _clipboardService = clipboardService;
_unsubscriptionService = unsubscriptionService; _unsubscriptionService = unsubscriptionService;
_underlyingThemeService = underlyingThemeService; _underlyingThemeService = underlyingThemeService;
@@ -355,8 +353,10 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
// To show the progress on the UI. // To show the progress on the UI.
CurrentDownloadPercentage = 1; CurrentDownloadPercentage = 1;
var package = new DownloadMissingMessageRequested(mailItemViewModel.AssignedAccount.Id, mailItemViewModel.MailCopy); // Download missing MIME message using SynchronizationManager
await _winoServerConnectionManager.GetResponseAsync<bool, DownloadMissingMessageRequested>(package); await SynchronizationManager.Instance.DownloadMimeMessageAsync(
mailItemViewModel.MailCopy,
mailItemViewModel.AssignedAccount.Id);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -1,25 +1,20 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation; using Microsoft.UI.Xaml.Navigation;
using Wino.Core.Domain; using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Exceptions; using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.AutoDiscovery; using Wino.Core.Domain.Models.AutoDiscovery;
using Wino.Core.Domain.Models.Connectivity; using Wino.Core.Services;
using Wino.Mail.WinUI;
using Wino.Messaging.Client.Mails; using Wino.Messaging.Client.Mails;
using Wino.Messaging.Server;
namespace Wino.Views.ImapSetup; namespace Wino.Views.ImapSetup;
public sealed partial class TestingImapConnectionPage : Page public sealed partial class TestingImapConnectionPage : Page
{ {
private IWinoServerConnectionManager _winoServerConnectionManager = App.Current.Services.GetService<IWinoServerConnectionManager>();
private AutoDiscoverySettings autoDiscoverySettings; private AutoDiscoverySettings autoDiscoverySettings;
private CustomServerInformation serverInformationToTest; private CustomServerInformation serverInformationToTest;
@@ -67,46 +62,34 @@ public sealed partial class TestingImapConnectionPage : Page
await Task.Delay(1000); await Task.Delay(1000);
var testResultResponse = await _winoServerConnectionManager var testResultData = await SynchronizationManager.Instance.TestImapConnectivityAsync(serverInformationToTest, allowSSLHandshake);
.GetResponseAsync<ImapConnectivityTestResults, ImapConnectivityTestRequested>(new ImapConnectivityTestRequested(serverInformationToTest, allowSSLHandshake));
if (!testResultResponse.IsSuccess) if (testResultData.IsSuccess)
{ {
// Wino Server is connection is failed. // All success. Finish setup with validated server information.
ReturnWithError(testResultResponse.Message); ReturnWithSuccess();
} }
else else
{ {
var testResultData = testResultResponse.Data; // Check if certificate UI is required.
if (testResultData.IsCertificateUIRequired)
if (testResultData.IsSuccess)
{ {
// All success. Finish setup with validated server information. // Certificate UI is required. Show certificate dialog.
ReturnWithSuccess();
CertIssuer.Text = testResultData.CertificateIssuer;
CertValidFrom.Text = testResultData.CertificateValidFromDateString;
CertValidTo.Text = testResultData.CertificateExpirationDateString;
TestingConnectionPanel.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
CertificateDialog.Visibility = Microsoft.UI.Xaml.Visibility.Visible;
} }
else else
{ {
// Check if certificate UI is required. // Connection test failed. Show error dialog.
if (testResultData.IsCertificateUIRequired) var protocolLog = testResultData.FailureProtocolLog;
{
// Certificate UI is required. Show certificate dialog.
CertIssuer.Text = testResultData.CertificateIssuer; ReturnWithError(testResultData.FailedReason, protocolLog);
CertValidFrom.Text = testResultData.CertificateValidFromDateString;
CertValidTo.Text = testResultData.CertificateExpirationDateString;
TestingConnectionPanel.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
CertificateDialog.Visibility = Microsoft.UI.Xaml.Visibility.Visible;
}
else
{
// Connection test failed. Show error dialog.
var protocolLog = testResultData.FailureProtocolLog;
ReturnWithError(testResultData.FailedReason, protocolLog);
}
} }
} }
} }