using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using Serilog;
using Wino.Core.Domain;
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.UI;
namespace Wino.Core.Services;
///
/// Singleton manager that handles synchronizer instances and operations for all accounts.
/// Replaces the old WinoServerConnectionManager functionality.
///
public class SynchronizationManager : ISynchronizationManager
{
private static readonly Lazy _instance = new(() => new SynchronizationManager());
public static SynchronizationManager Instance => _instance.Value;
private readonly ConcurrentDictionary _synchronizerCache = new();
private readonly ConcurrentDictionary _accountSynchronizationCancellationSources = new();
private readonly ConcurrentDictionary _calendarSynchronizationLocks = new();
private readonly SemaphoreSlim _initializationSemaphore = new(1, 1);
private readonly ILogger _logger = Log.ForContext();
private SynchronizerFactory _concreteSynchronizerFactory;
private IImapTestService _imapTestService;
private IAccountService _accountService;
private IAuthenticationProvider _authenticationProvider;
private INotificationBuilder _notificationBuilder;
private bool _isInitialized = false;
private SynchronizationManager() { }
///
/// Initializes the SynchronizationManager with required dependencies.
/// This must be called before using any other methods.
/// Note: Synchronizers are created lazily to avoid requiring window handles during app initialization.
///
/// Factory for creating synchronizers
/// Service for testing IMAP connectivity
/// Service for account operations
/// Provider for OAuth authentication
public async Task InitializeAsync(ISynchronizerFactory synchronizerFactory,
IImapTestService imapTestService,
IAccountService accountService,
INotificationBuilder notificationBuilder,
IAuthenticationProvider authenticationProvider)
{
await _initializationSemaphore.WaitAsync();
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));
_notificationBuilder = notificationBuilder ?? throw new ArgumentNullException(nameof(notificationBuilder));
// DO NOT create synchronizers here to avoid requiring window handles during initialization.
// Synchronizers will be created lazily when first accessed via GetOrCreateSynchronizerAsync.
_isInitialized = true;
_logger.Information("SynchronizationManager dependencies initialized. Synchronizers will be created lazily.");
}
finally
{
_initializationSemaphore.Release();
}
}
///
/// Tests IMAP server connectivity for the given server information.
///
/// Server information to test
/// Whether to allow SSL handshake
/// Test results indicating success or failure with details
public async Task 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);
}
}
///
/// Starts a new mail synchronization for the given account.
///
/// Mail synchronization options
/// Cancellation token
/// Synchronization result
public async Task SynchronizeMailAsync(MailSynchronizationOptions options,
CancellationToken cancellationToken = default)
{
EnsureInitialized();
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;
}
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(new Exception("Can't create/get synchronizer."));
}
_logger.Information("Starting mail synchronization for account {AccountId} with type {SyncType}",
options.AccountId, options.Type);
var accountCancellationSource = _accountSynchronizationCancellationSources.GetOrAdd(options.AccountId, _ => new CancellationTokenSource());
using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken,
accountCancellationSource.Token);
try
{
var result = await synchronizer.SynchronizeMailsAsync(options, linkedCancellationTokenSource.Token);
_logger.Information("Mail synchronization completed for account {AccountId} with state {State}",
options.AccountId, result.CompletedState);
// Create notifications.
if (result.DownloadedMessages?.Any() ?? false)
await _notificationBuilder.CreateNotificationsAsync(result.DownloadedMessages);
await _notificationBuilder.UpdateTaskbarIconBadgeAsync();
return result;
}
catch (OperationCanceledException)
{
_logger.Information("Mail synchronization canceled for account {AccountId}", options.AccountId);
return MailSynchronizationResult.Canceled;
}
catch (AuthenticationAttentionException authEx)
{
_logger.Warning("Account {AccountId} requires attention due to authentication issues", options.AccountId);
await SetInvalidCredentialAttentionAsync(authEx.Account).ConfigureAwait(false);
// Create app notification for authentication attention
_notificationBuilder.CreateAttentionRequiredNotification(authEx.Account);
return MailSynchronizationResult.Failed(authEx);
}
catch (Exception ex)
{
_logger.Error(ex, "Mail synchronization failed for account {AccountId}", options.AccountId);
return MailSynchronizationResult.Failed(ex);
}
}
///
/// Checks if there is an ongoing synchronization for the given account.
///
/// Account ID to check
/// True if synchronization is ongoing, false otherwise
public bool IsAccountSynchronizing(Guid accountId)
{
EnsureInitialized();
if (_synchronizerCache.TryGetValue(accountId, out var synchronizer))
{
return synchronizer.State == AccountSynchronizerState.Synchronizing ||
synchronizer.State == AccountSynchronizerState.ExecutingRequests;
}
return false;
}
///
/// 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.
///
/// Request to queue
/// Account ID to queue the request for
/// Whether to automatically trigger synchronization after queuing the request
public async Task QueueRequestAsync(IRequestBase request, Guid accountId, bool triggerSynchronization)
{
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);
if (triggerSynchronization)
{
// Determine if this is a calendar or mail operation
bool isCalendarOperation = request is ICalendarActionRequest;
if (isCalendarOperation)
{
// Trigger calendar synchronization
_logger.Debug("Triggering calendar synchronization to execute queued request for account {AccountId}", accountId);
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
{
// 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()
{
AccountId = accountId,
Type = MailSynchronizationType.ExecuteRequests
};
// Trigger synchronization asynchronously without waiting for completion
// This matches the pattern used in WinoRequestDelegator
_ = Task.Run(async () =>
{
try
{
await SynchronizeMailAsync(mailSyncOptions);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to execute mail synchronization after queuing request for account {AccountId}", accountId);
}
});
}
}
}
///
/// Handles folder synchronization for the given account.
///
/// Account ID to synchronize folders for
/// Cancellation token
/// Synchronization result
public async Task SynchronizeFoldersAsync(Guid accountId,
CancellationToken cancellationToken = default)
{
EnsureInitialized();
var options = new MailSynchronizationOptions
{
AccountId = accountId,
Type = MailSynchronizationType.FoldersOnly
};
return await SynchronizeMailAsync(options, cancellationToken);
}
///
/// Handles alias synchronization for the given account.
///
/// Account ID to synchronize aliases for
/// Cancellation token
/// Synchronization result
public async Task SynchronizeAliasesAsync(Guid accountId,
CancellationToken cancellationToken = default)
{
EnsureInitialized();
var options = new MailSynchronizationOptions
{
AccountId = accountId,
Type = MailSynchronizationType.Alias
};
return await SynchronizeMailAsync(options, cancellationToken);
}
///
/// Handles profile synchronization for the given account.
///
/// Account ID to synchronize profile for
/// Cancellation token
/// Synchronization result
public async Task SynchronizeProfileAsync(Guid accountId,
CancellationToken cancellationToken = default)
{
EnsureInitialized();
var options = new MailSynchronizationOptions
{
AccountId = accountId,
Type = MailSynchronizationType.UpdateProfile
};
return await SynchronizeMailAsync(options, cancellationToken);
}
///
/// Handles calendar synchronization for the given account.
///
/// Calendar synchronization options
/// Cancellation token
/// Synchronization result
public async Task SynchronizeCalendarAsync(CalendarSynchronizationOptions options,
CancellationToken cancellationToken = default)
=> 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 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 SynchronizeCalendarCoreAsync(
CalendarSynchronizationOptions options,
CancellationToken cancellationToken,
bool reportState)
{
EnsureInitialized();
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;
}
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);
if (reportState)
{
PublishCalendarSynchronizationState(
options.AccountId,
options.Type,
isSynchronizationInProgress: true,
GetCalendarSynchronizationStatus(options.Type));
}
var accountCancellationSource = _accountSynchronizationCancellationSources.GetOrAdd(options.AccountId, _ => new CancellationTokenSource());
using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken,
accountCancellationSource.Token);
try
{
var result = await synchronizer.SynchronizeCalendarEventsAsync(options, linkedCancellationTokenSource.Token);
_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;
}
catch (OperationCanceledException)
{
_logger.Information("Calendar synchronization canceled for account {AccountId}", options.AccountId);
return CalendarSynchronizationResult.Canceled;
}
catch (AuthenticationAttentionException authEx)
{
_logger.Warning("Account {AccountId} requires attention due to authentication issues", options.AccountId);
await SetInvalidCredentialAttentionAsync(authEx.Account).ConfigureAwait(false);
// 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;
}
finally
{
if (reportState)
{
PublishCalendarSynchronizationState(options.AccountId, options.Type, isSynchronizationInProgress: false);
}
}
}
///
/// Downloads a MIME message for the given mail item.
///
/// Mail item to download
/// Account ID that owns the mail item
/// Cancellation token
/// Downloaded MIME content path
public async Task 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 (SynchronizerEntityNotFoundException)
{
_logger.Warning("MIME message for mail item {MailItemId} no longer exists on server. Removed locally.", mailItem.Id);
return null;
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to download MIME message for mail item {MailItemId}", mailItem.Id);
return null;
}
}
///
/// Downloads a calendar attachment using the appropriate synchronizer.
///
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;
}
}
///
/// Creates a new synchronizer for a newly added account.
///
/// Account to create synchronizer for
/// Created synchronizer
public IWinoSynchronizerBase CreateSynchronizerForAccount(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 synchronizer;
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to create synchronizer for account {AccountName} ({AccountId})",
account.Name, account.Id);
return null;
}
}
///
/// Cancels all in-flight synchronizations for the given account.
///
/// Account ID to cancel synchronizations for
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;
}
///
/// Destroys the synchronizer for the given account.
///
/// Account ID to destroy synchronizer for
public async Task DestroySynchronizerAsync(Guid accountId)
{
EnsureInitialized();
await CancelSynchronizationsAsync(accountId);
if (_synchronizerCache.TryRemove(accountId, out var synchronizer))
{
try
{
await synchronizer.KillSynchronizerAsync();
_logger.Information("Destroyed synchronizer for account {AccountId}", accountId);
}
catch (OperationCanceledException)
{
_logger.Information("Synchronizer destruction canceled for account {AccountId}", accountId);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to destroy synchronizer for account {AccountId}", accountId);
}
}
}
///
/// Gets all cached synchronizers.
///
/// Collection of all cached synchronizers
public IEnumerable GetAllSynchronizers()
{
EnsureInitialized();
return _synchronizerCache.Values.ToList();
}
///
/// Gets a synchronizer for the given account ID.
///
/// Account ID
/// Synchronizer if found, null otherwise
public async Task GetSynchronizerAsync(Guid accountId)
{
EnsureInitialized();
return await GetOrCreateSynchronizerAsync(accountId);
}
private async Task 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 CreateSynchronizerForAccount(account);
}
return null;
}
///
/// Handles OAuth authentication for the specified provider.
///
/// The mail provider type to authenticate
/// Optional account to authenticate (null for initial authentication)
/// Whether to propose copying auth URL for Gmail
/// Token information containing access token and username
public async Task 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.");
}
}
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 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 RunCalendarSynchronizationWithLockAsync(
Guid accountId,
CancellationToken cancellationToken,
Func> 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();
}
}
}