2025-10-04 23:10:07 +02:00
using System ;
using System.Collections.Concurrent ;
using System.Collections.Generic ;
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
2026-04-04 20:23:20 +02:00
using CommunityToolkit.Mvvm.Messaging ;
2025-10-04 23:10:07 +02:00
using Serilog ;
2026-04-04 20:23:20 +02:00
using Wino.Core.Domain ;
2025-10-04 23:10:07 +02:00
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 ;
2026-04-04 20:23:20 +02:00
using Wino.Messaging.UI ;
2025-10-04 23:10:07 +02:00
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 ( ) ;
2026-02-11 14:50:59 +01:00
private readonly ConcurrentDictionary < Guid , CancellationTokenSource > _accountSynchronizationCancellationSources = new ( ) ;
2026-04-04 20:23:20 +02:00
private readonly ConcurrentDictionary < Guid , SemaphoreSlim > _calendarSynchronizationLocks = new ( ) ;
2025-10-04 23:10:07 +02:00
private readonly SemaphoreSlim _initializationSemaphore = new ( 1 , 1 ) ;
private readonly ILogger _logger = Log . ForContext < SynchronizationManager > ( ) ;
private SynchronizerFactory _concreteSynchronizerFactory ;
private IImapTestService _imapTestService ;
private IAccountService _accountService ;
private IAuthenticationProvider _authenticationProvider ;
2025-11-12 15:44:43 +01:00
private INotificationBuilder _notificationBuilder ;
2025-10-04 23:10:07 +02:00
private bool _isInitialized = false ;
private SynchronizationManager ( ) { }
/// <summary>
/// Initializes the SynchronizationManager with required dependencies.
/// This must be called before using any other methods.
2025-11-14 11:37:26 +01:00
/// Note: Synchronizers are created lazily to avoid requiring window handles during app initialization.
2025-10-04 23:10:07 +02:00
/// </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>
2025-11-12 15:44:43 +01:00
public async Task InitializeAsync ( ISynchronizerFactory synchronizerFactory ,
2025-10-04 23:10:07 +02:00
IImapTestService imapTestService ,
IAccountService accountService ,
2025-11-12 15:44:43 +01:00
INotificationBuilder notificationBuilder ,
2025-10-04 23:10:07 +02:00
IAuthenticationProvider authenticationProvider )
{
await _initializationSemaphore . WaitAsync ( ) ;
2025-11-12 15:44:43 +01:00
2025-10-04 23:10:07 +02:00
try
{
if ( _isInitialized ) return ;
_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 ) ) ;
2025-11-12 15:44:43 +01:00
_notificationBuilder = notificationBuilder ? ? throw new ArgumentNullException ( nameof ( notificationBuilder ) ) ;
2025-10-04 23:10:07 +02:00
2025-11-14 11:37:26 +01:00
// DO NOT create synchronizers here to avoid requiring window handles during initialization.
// Synchronizers will be created lazily when first accessed via GetOrCreateSynchronizerAsync.
2025-10-04 23:10:07 +02:00
_isInitialized = true ;
2025-11-14 11:37:26 +01:00
_logger . Information ( "SynchronizationManager dependencies initialized. Synchronizers will be created lazily." ) ;
2025-10-04 23:10:07 +02:00
}
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
{
2025-11-12 15:44:43 +01:00
_logger . Information ( "Testing IMAP connectivity for {Server}:{Port}" ,
serverInformation . IncomingServer ,
2025-10-04 23:10:07 +02:00
serverInformation . IncomingServerPort ) ;
await _imapTestService . TestImapConnectionAsync ( serverInformation , allowSSLHandshake ) ;
2025-11-12 15:44:43 +01:00
2025-10-04 23:10:07 +02:00
_logger . Information ( "IMAP connectivity test successful" ) ;
return ImapConnectivityTestResults . Success ( ) ;
}
catch ( ImapTestSSLCertificateException sslTestException )
{
_logger . Warning ( "IMAP connectivity test requires SSL certificate confirmation" ) ;
return ImapConnectivityTestResults . CertificateUIRequired (
2025-11-12 15:44:43 +01:00
sslTestException . Issuer ,
sslTestException . ExpirationDateString ,
2025-10-04 23:10:07 +02:00
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>
2025-11-12 15:44:43 +01:00
public async Task < MailSynchronizationResult > SynchronizeMailAsync ( MailSynchronizationOptions options ,
2025-10-04 23:10:07 +02:00
CancellationToken cancellationToken = default )
{
EnsureInitialized ( ) ;
2026-04-04 20:23:20 +02:00
if ( await IsSynchronizationBlockedByAttentionAsync ( options . AccountId ) . ConfigureAwait ( false ) )
{
_logger . Information ( "Skipping mail synchronization for account {AccountId} because it requires credential attention." , options . AccountId ) ;
return MailSynchronizationResult . Canceled ;
}
2025-10-04 23:10:07 +02:00
var synchronizer = await GetOrCreateSynchronizerAsync ( options . AccountId ) ;
if ( synchronizer = = null )
{
_logger . Error ( "Could not find or create synchronizer for account {AccountId}" , options . AccountId ) ;
2025-11-14 12:12:13 +01:00
return MailSynchronizationResult . Failed ( new Exception ( "Can't create/get synchronizer." ) ) ;
2025-10-04 23:10:07 +02:00
}
2025-11-12 15:44:43 +01:00
_logger . Information ( "Starting mail synchronization for account {AccountId} with type {SyncType}" ,
2025-10-04 23:10:07 +02:00
options . AccountId , options . Type ) ;
2026-02-11 14:50:59 +01:00
var accountCancellationSource = _accountSynchronizationCancellationSources . GetOrAdd ( options . AccountId , _ = > new CancellationTokenSource ( ) ) ;
using var linkedCancellationTokenSource = CancellationTokenSource . CreateLinkedTokenSource (
cancellationToken ,
accountCancellationSource . Token ) ;
2025-10-04 23:10:07 +02:00
try
{
2026-02-11 14:50:59 +01:00
var result = await synchronizer . SynchronizeMailsAsync ( options , linkedCancellationTokenSource . Token ) ;
2025-11-12 15:44:43 +01:00
_logger . Information ( "Mail synchronization completed for account {AccountId} with state {State}" ,
2025-10-04 23:10:07 +02:00
options . AccountId , result . CompletedState ) ;
2025-11-12 15:44:43 +01:00
// Create notifications.
if ( result . DownloadedMessages ? . Any ( ) ? ? false )
await _notificationBuilder . CreateNotificationsAsync ( result . DownloadedMessages ) ;
2025-11-14 12:12:13 +01:00
await _notificationBuilder . UpdateTaskbarIconBadgeAsync ( ) ;
2025-10-04 23:10:07 +02:00
return result ;
}
2026-02-11 14:50:59 +01:00
catch ( OperationCanceledException )
{
_logger . Information ( "Mail synchronization canceled for account {AccountId}" , options . AccountId ) ;
return MailSynchronizationResult . Canceled ;
}
2025-11-14 12:31:24 +01:00
catch ( AuthenticationAttentionException authEx )
{
_logger . Warning ( "Account {AccountId} requires attention due to authentication issues" , options . AccountId ) ;
2026-04-04 20:23:20 +02:00
await SetInvalidCredentialAttentionAsync ( authEx . Account ) . ConfigureAwait ( false ) ;
2025-11-14 12:31:24 +01:00
// Create app notification for authentication attention
_notificationBuilder . CreateAttentionRequiredNotification ( authEx . Account ) ;
return MailSynchronizationResult . Failed ( authEx ) ;
}
2025-10-04 23:10:07 +02:00
catch ( Exception ex )
{
_logger . Error ( ex , "Mail synchronization failed for account {AccountId}" , options . AccountId ) ;
2025-11-14 12:12:13 +01:00
return MailSynchronizationResult . Failed ( ex ) ;
2025-10-04 23:10:07 +02:00
}
}
/// <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 ) )
{
2025-11-12 15:44:43 +01:00
return synchronizer . State = = AccountSynchronizerState . Synchronizing | |
2025-10-04 23:10:07 +02:00
synchronizer . State = = AccountSynchronizerState . ExecutingRequests ;
}
return false ;
}
2025-10-06 17:46:00 +02:00
/// <summary>
2025-12-30 11:59:54 +01:00
/// Queues a request to the corresponding account's synchronizer with optional synchronization triggering.
/// Automatically determines whether to trigger mail or calendar synchronization based on the request type.
2025-10-06 17:46:00 +02:00
/// </summary>
/// <param name="request">Request to queue</param>
/// <param name="accountId">Account ID to queue the request for</param>
/// <param name="triggerSynchronization">Whether to automatically trigger synchronization after queuing the request</param>
public async Task QueueRequestAsync ( IRequestBase request , Guid accountId , bool triggerSynchronization )
2025-10-04 23:10:07 +02:00
{
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 ;
}
2025-11-12 15:44:43 +01:00
_logger . Debug ( "Queuing request {RequestType} for account {AccountId}" ,
2025-10-04 23:10:07 +02:00
request . GetType ( ) . Name , accountId ) ;
synchronizer . QueueRequest ( request ) ;
2025-10-06 17:46:00 +02:00
if ( triggerSynchronization )
{
2025-12-30 11:59:54 +01:00
// Determine if this is a calendar or mail operation
bool isCalendarOperation = request is ICalendarActionRequest ;
2025-11-12 15:44:43 +01:00
2025-12-30 11:59:54 +01:00
if ( isCalendarOperation )
2025-10-06 17:46:00 +02:00
{
2025-12-30 11:59:54 +01:00
// Trigger calendar synchronization
_logger . Debug ( "Triggering calendar synchronization to execute queued request for account {AccountId}" , accountId ) ;
2025-10-06 17:46:00 +02:00
2025-12-30 11:59:54 +01:00
var calendarSyncOptions = new CalendarSynchronizationOptions ( )
{
AccountId = accountId
} ;
// Trigger synchronization asynchronously without waiting for completion
_ = Task . Run ( async ( ) = >
{
try
{
await SynchronizeCalendarAsync ( calendarSyncOptions ) ;
}
catch ( Exception ex )
{
_logger . Error ( ex , "Failed to execute calendar synchronization after queuing request for account {AccountId}" , accountId ) ;
}
} ) ;
}
else
2025-10-06 17:46:00 +02:00
{
2025-12-30 11:59:54 +01:00
// Trigger mail synchronization (includes mail and folder operations)
_logger . Debug ( "Triggering mail synchronization to execute queued request for account {AccountId}" , accountId ) ;
var mailSyncOptions = new MailSynchronizationOptions ( )
2025-10-06 17:46:00 +02:00
{
2025-12-30 11:59:54 +01:00
AccountId = accountId ,
Type = MailSynchronizationType . ExecuteRequests
} ;
// Trigger synchronization asynchronously without waiting for completion
// This matches the pattern used in WinoRequestDelegator
_ = Task . Run ( async ( ) = >
2025-10-06 17:46:00 +02:00
{
2025-12-30 11:59:54 +01:00
try
{
await SynchronizeMailAsync ( mailSyncOptions ) ;
}
catch ( Exception ex )
{
_logger . Error ( ex , "Failed to execute mail synchronization after queuing request for account {AccountId}" , accountId ) ;
}
} ) ;
}
2025-10-06 17:46:00 +02:00
}
2025-10-04 23:10:07 +02:00
}
/// <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>
2025-11-12 15:44:43 +01:00
public async Task < MailSynchronizationResult > SynchronizeFoldersAsync ( Guid accountId ,
2025-10-04 23:10:07 +02:00
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>
2025-11-12 15:44:43 +01:00
public async Task < MailSynchronizationResult > SynchronizeAliasesAsync ( Guid accountId ,
2025-10-04 23:10:07 +02:00
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>
2025-11-12 15:44:43 +01:00
public async Task < MailSynchronizationResult > SynchronizeProfileAsync ( Guid accountId ,
2025-10-04 23:10:07 +02:00
CancellationToken cancellationToken = default )
{
EnsureInitialized ( ) ;
var options = new MailSynchronizationOptions
{
AccountId = accountId ,
Type = MailSynchronizationType . UpdateProfile
} ;
return await SynchronizeMailAsync ( options , cancellationToken ) ;
}
2025-12-26 20:46:48 +01:00
/// <summary>
/// Handles calendar synchronization for the given account.
/// </summary>
/// <param name="options">Calendar synchronization options</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Synchronization result</returns>
public async Task < CalendarSynchronizationResult > SynchronizeCalendarAsync ( CalendarSynchronizationOptions options ,
CancellationToken cancellationToken = default )
2026-04-04 20:23:20 +02:00
= > options . Type = = CalendarSynchronizationType . Strict
? await SynchronizeCalendarStrictAsync ( options , cancellationToken ) . ConfigureAwait ( false )
: await RunCalendarSynchronizationWithLockAsync (
options . AccountId ,
cancellationToken ,
( ) = > SynchronizeCalendarCoreAsync ( options , cancellationToken , reportState : true ) ) . ConfigureAwait ( false ) ;
private async Task < CalendarSynchronizationResult > SynchronizeCalendarStrictAsync (
CalendarSynchronizationOptions options ,
CancellationToken cancellationToken )
{
var metadataOptions = new CalendarSynchronizationOptions
{
AccountId = options . AccountId ,
Type = CalendarSynchronizationType . CalendarMetadata ,
SynchronizationCalendarIds = options . SynchronizationCalendarIds
} ;
var eventOptions = new CalendarSynchronizationOptions
{
AccountId = options . AccountId ,
Type = CalendarSynchronizationType . CalendarEvents ,
SynchronizationCalendarIds = options . SynchronizationCalendarIds
} ;
return await RunCalendarSynchronizationWithLockAsync ( options . AccountId , cancellationToken , async ( ) = >
{
try
{
PublishCalendarSynchronizationState (
options . AccountId ,
CalendarSynchronizationType . Strict ,
isSynchronizationInProgress : true ,
Translator . SyncAction_SynchronizingCalendarMetadata ) ;
var metadataResult = await SynchronizeCalendarCoreAsync ( metadataOptions , cancellationToken , reportState : false ) . ConfigureAwait ( false ) ;
if ( metadataResult . CompletedState is SynchronizationCompletedState . Failed or SynchronizationCompletedState . Canceled )
{
return metadataResult ;
}
PublishCalendarSynchronizationState (
options . AccountId ,
CalendarSynchronizationType . Strict ,
isSynchronizationInProgress : true ,
Translator . SyncAction_SynchronizingCalendarEvents ) ;
return await SynchronizeCalendarCoreAsync ( eventOptions , cancellationToken , reportState : false ) . ConfigureAwait ( false ) ;
}
finally
{
PublishCalendarSynchronizationState ( options . AccountId , CalendarSynchronizationType . Strict , isSynchronizationInProgress : false ) ;
}
} ) . ConfigureAwait ( false ) ;
}
private async Task < CalendarSynchronizationResult > SynchronizeCalendarCoreAsync (
CalendarSynchronizationOptions options ,
CancellationToken cancellationToken ,
bool reportState )
2025-12-26 20:46:48 +01:00
{
EnsureInitialized ( ) ;
2026-04-04 20:23:20 +02:00
if ( await IsSynchronizationBlockedByAttentionAsync ( options . AccountId ) . ConfigureAwait ( false ) )
{
_logger . Information ( "Skipping calendar synchronization for account {AccountId} because it requires credential attention." , options . AccountId ) ;
return CalendarSynchronizationResult . Canceled ;
}
2025-12-26 20:46:48 +01:00
var synchronizer = await GetOrCreateSynchronizerAsync ( options . AccountId ) ;
if ( synchronizer = = null )
{
_logger . Error ( "Could not find or create synchronizer for account {AccountId}" , options . AccountId ) ;
return CalendarSynchronizationResult . Failed ;
}
_logger . Information ( "Starting calendar synchronization for account {AccountId} with type {SyncType}" ,
options . AccountId , options . Type ) ;
2026-04-04 20:23:20 +02:00
if ( reportState )
{
PublishCalendarSynchronizationState (
options . AccountId ,
options . Type ,
isSynchronizationInProgress : true ,
GetCalendarSynchronizationStatus ( options . Type ) ) ;
}
2026-02-11 14:50:59 +01:00
var accountCancellationSource = _accountSynchronizationCancellationSources . GetOrAdd ( options . AccountId , _ = > new CancellationTokenSource ( ) ) ;
using var linkedCancellationTokenSource = CancellationTokenSource . CreateLinkedTokenSource (
cancellationToken ,
accountCancellationSource . Token ) ;
2025-12-26 20:46:48 +01:00
try
{
2026-02-11 14:50:59 +01:00
var result = await synchronizer . SynchronizeCalendarEventsAsync ( options , linkedCancellationTokenSource . Token ) ;
2025-12-26 20:46:48 +01:00
_logger . Information ( "Calendar synchronization completed for account {AccountId} with state {State}" ,
options . AccountId , result . CompletedState ) ;
// TODO: Create notifications for new calendar events when INotificationBuilder supports it
// if (result.DownloadedEvents?.Any() ?? false)
// await _notificationBuilder.CreateCalendarNotificationsAsync(result.DownloadedEvents);
return result ;
}
2026-02-11 14:50:59 +01:00
catch ( OperationCanceledException )
{
_logger . Information ( "Calendar synchronization canceled for account {AccountId}" , options . AccountId ) ;
return CalendarSynchronizationResult . Canceled ;
}
2025-12-26 20:46:48 +01:00
catch ( AuthenticationAttentionException authEx )
{
_logger . Warning ( "Account {AccountId} requires attention due to authentication issues" , options . AccountId ) ;
2026-04-04 20:23:20 +02:00
await SetInvalidCredentialAttentionAsync ( authEx . Account ) . ConfigureAwait ( false ) ;
2025-12-26 20:46:48 +01:00
// Create app notification for authentication attention
_notificationBuilder . CreateAttentionRequiredNotification ( authEx . Account ) ;
return CalendarSynchronizationResult . Failed ;
}
catch ( Exception ex )
{
_logger . Error ( ex , "Calendar synchronization failed for account {AccountId}" , options . AccountId ) ;
return CalendarSynchronizationResult . Failed ;
}
2026-04-04 20:23:20 +02:00
finally
{
if ( reportState )
{
PublishCalendarSynchronizationState ( options . AccountId , options . Type , isSynchronizationInProgress : false ) ;
}
}
2025-12-26 20:46:48 +01:00
}
2025-10-04 23:10:07 +02:00
/// <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>
2025-11-12 15:44:43 +01:00
public async Task < string > DownloadMimeMessageAsync ( MailCopy mailItem , Guid accountId ,
2025-10-04 23:10:07 +02:00
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
}
2026-02-08 22:20:38 +01:00
catch ( SynchronizerEntityNotFoundException )
{
_logger . Warning ( "MIME message for mail item {MailItemId} no longer exists on server. Removed locally." , mailItem . Id ) ;
return null ;
}
2025-10-04 23:10:07 +02:00
catch ( Exception ex )
{
_logger . Error ( ex , "Failed to download MIME message for mail item {MailItemId}" , mailItem . Id ) ;
return null ;
}
}
2026-01-03 23:59:37 +01:00
/// <summary>
/// Downloads a calendar attachment using the appropriate synchronizer.
/// </summary>
public async Task DownloadCalendarAttachmentAsync (
Wino . Core . Domain . Entities . Calendar . CalendarItem calendarItem ,
Wino . Core . Domain . Entities . Calendar . CalendarAttachment attachment ,
string localFilePath ,
CancellationToken cancellationToken = default )
{
EnsureInitialized ( ) ;
if ( calendarItem = = null )
throw new ArgumentNullException ( nameof ( calendarItem ) ) ;
if ( attachment = = null )
throw new ArgumentNullException ( nameof ( attachment ) ) ;
var accountId = calendarItem . AssignedCalendar ? . AccountId ? ? Guid . Empty ;
if ( accountId = = Guid . Empty )
throw new InvalidOperationException ( "Calendar item does not have an assigned account." ) ;
var synchronizer = await GetOrCreateSynchronizerAsync ( accountId ) ;
if ( synchronizer = = null )
{
_logger . Error ( "Could not find or create synchronizer for account {AccountId} to download calendar attachment" , accountId ) ;
throw new InvalidOperationException ( "No synchronizer available for downloading calendar attachment." ) ;
}
_logger . Debug ( "Downloading calendar attachment {AttachmentId} for calendar item {CalendarItemId}" ,
attachment . Id , calendarItem . Id ) ;
try
{
await synchronizer . DownloadCalendarAttachmentAsync (
calendarItem ,
attachment ,
localFilePath ,
cancellationToken ) ;
}
catch ( Exception ex )
{
_logger . Error ( ex , "Failed to download calendar attachment {AttachmentId}" , attachment . Id ) ;
throw ;
}
}
2025-10-04 23:10:07 +02:00
/// <summary>
/// Creates a new synchronizer for a newly added account.
/// </summary>
/// <param name="account">Account to create synchronizer for</param>
/// <returns>Created synchronizer</returns>
2025-12-30 11:59:54 +01:00
public IWinoSynchronizerBase CreateSynchronizerForAccount ( MailAccount account )
2025-10-04 23:10:07 +02:00
{
EnsureInitialized ( ) ;
try
{
var synchronizer = _concreteSynchronizerFactory . CreateNewSynchronizer ( account ) ;
_synchronizerCache . TryAdd ( account . Id , synchronizer ) ;
2025-11-12 15:44:43 +01:00
_logger . Information ( "Created new synchronizer for account {AccountName} ({AccountId})" ,
2025-10-04 23:10:07 +02:00
account . Name , account . Id ) ;
2025-11-12 15:44:43 +01:00
2025-12-30 11:59:54 +01:00
return synchronizer ;
2025-10-04 23:10:07 +02:00
}
catch ( Exception ex )
{
2025-11-12 15:44:43 +01:00
_logger . Error ( ex , "Failed to create synchronizer for account {AccountName} ({AccountId})" ,
2025-10-04 23:10:07 +02:00
account . Name , account . Id ) ;
2025-12-30 11:59:54 +01:00
return null ;
2025-10-04 23:10:07 +02:00
}
}
2026-02-11 14:50:59 +01:00
/// <summary>
/// Cancels all in-flight synchronizations for the given account.
/// </summary>
/// <param name="accountId">Account ID to cancel synchronizations for</param>
public Task CancelSynchronizationsAsync ( Guid accountId )
{
EnsureInitialized ( ) ;
if ( _accountSynchronizationCancellationSources . TryRemove ( accountId , out var cancellationSource ) )
{
try
{
if ( ! cancellationSource . IsCancellationRequested )
{
cancellationSource . Cancel ( ) ;
}
}
catch ( ObjectDisposedException )
{
// no-op
}
finally
{
cancellationSource . Dispose ( ) ;
}
_logger . Information ( "Canceled ongoing synchronizations for account {AccountId}" , accountId ) ;
}
return Task . CompletedTask ;
}
2025-10-04 23:10:07 +02:00
/// <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 ( ) ;
2026-02-11 14:50:59 +01:00
await CancelSynchronizationsAsync ( accountId ) ;
2025-10-04 23:10:07 +02:00
if ( _synchronizerCache . TryRemove ( accountId , out var synchronizer ) )
{
try
{
await synchronizer . KillSynchronizerAsync ( ) ;
_logger . Information ( "Destroyed synchronizer for account {AccountId}" , accountId ) ;
2026-02-11 14:50:59 +01:00
}
catch ( OperationCanceledException )
{
_logger . Information ( "Synchronizer destruction canceled for account {AccountId}" , accountId ) ;
2025-10-04 23:10:07 +02:00
}
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 )
{
2025-12-30 11:59:54 +01:00
return CreateSynchronizerForAccount ( account ) ;
2025-10-04 23:10:07 +02:00
}
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>
2025-11-12 15:44:43 +01:00
public async Task < TokenInformationEx > HandleAuthorizationAsync ( MailProviderType providerType ,
MailAccount account = null ,
2025-10-04 23:10:07 +02:00
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." ) ;
}
}
2026-04-04 20:23:20 +02:00
private async Task SetInvalidCredentialAttentionAsync ( MailAccount account )
{
if ( account = = null | | _accountService = = null )
return ;
var persistedAccount = await _accountService . GetAccountAsync ( account . Id ) . ConfigureAwait ( false ) ;
if ( persistedAccount = = null )
return ;
if ( persistedAccount . AttentionReason = = AccountAttentionReason . InvalidCredentials )
return ;
persistedAccount . AttentionReason = AccountAttentionReason . InvalidCredentials ;
await _accountService . UpdateAccountAsync ( persistedAccount ) . ConfigureAwait ( false ) ;
}
private async Task < bool > IsSynchronizationBlockedByAttentionAsync ( Guid accountId )
{
if ( _accountService = = null )
return false ;
var account = await _accountService . GetAccountAsync ( accountId ) . ConfigureAwait ( false ) ;
return account ? . AttentionReason = = AccountAttentionReason . InvalidCredentials ;
}
private void PublishCalendarSynchronizationState (
Guid accountId ,
CalendarSynchronizationType synchronizationType ,
bool isSynchronizationInProgress ,
string synchronizationStatus = "" )
{
WeakReferenceMessenger . Default . Send ( new AccountCalendarSynchronizationStateChanged (
accountId ,
synchronizationType ,
isSynchronizationInProgress ,
synchronizationStatus ) ) ;
}
private static string GetCalendarSynchronizationStatus ( CalendarSynchronizationType synchronizationType )
= > synchronizationType switch
{
CalendarSynchronizationType . CalendarMetadata = > Translator . SyncAction_SynchronizingCalendarMetadata ,
CalendarSynchronizationType . Strict = > Translator . SyncAction_SynchronizingCalendarData ,
_ = > Translator . SyncAction_SynchronizingCalendarEvents
} ;
private async Task < CalendarSynchronizationResult > RunCalendarSynchronizationWithLockAsync (
Guid accountId ,
CancellationToken cancellationToken ,
Func < Task < CalendarSynchronizationResult > > synchronizationFactory )
{
var calendarSemaphore = _calendarSynchronizationLocks . GetOrAdd ( accountId , _ = > new SemaphoreSlim ( 1 , 1 ) ) ;
await calendarSemaphore . WaitAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
try
{
return await synchronizationFactory ( ) . ConfigureAwait ( false ) ;
}
finally
{
calendarSemaphore . Release ( ) ;
}
}
2025-11-12 15:44:43 +01:00
}