using DigitalData.MessagingService.Application.Common.Interfaces; using DigitalData.MessagingService.Application.Common.Interfaces.Repositories; using DigitalData.MessagingService.Application.Common.Options; using DigitalData.MessagingService.Domain.Entities; using DigitalData.MessagingService.Domain.Enums; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace DigitalData.MessagingService.Infrastructure.Services.Background; public class EmailSyncWorker(IOptions Options, IServiceProvider Provider, ILogger Logger) : BackgroundService { private readonly string DefaultFolder = "INBOX"; /// /// Signals the current to complete immediately, /// causing the sync loop to start the next cycle without waiting. /// A new TCS is created at the start of each delay so repeated triggers work correctly. /// private volatile TaskCompletionSource _syncTrigger = new(TaskCreationOptions.RunContinuationsAsynchronously); /// /// Triggers an immediate sync cycle by completing the current delay early. /// Safe to call from any thread or HTTP request at any time. /// If a sync is already running, the trigger is ignored — the next cycle starts normally. /// public void TriggerSync() => _syncTrigger.TrySetResult(true); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await UpsertSeedEmailAccount(stoppingToken); var interval = TimeSpan.FromSeconds(Options.Value.SyncIntervalSeconds); while (!stoppingToken.IsCancellationRequested) { await using var scope = Provider.CreateAsyncScope(); var emailAccountRepo = scope.ServiceProvider.GetRequiredService>(); var imapService = scope.ServiceProvider.GetRequiredService(); var pop3Service = scope.ServiceProvider.GetRequiredService(); foreach (var account in await emailAccountRepo.GetAllAsync(stoppingToken)) { await SyncAccountAsync(account, imapService, pop3Service, stoppingToken); } // Reset trigger before waiting so any TriggerSync() call during the delay is caught _syncTrigger = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var delay = Task.Delay(interval, stoppingToken); var triggered = _syncTrigger.Task; await Task.WhenAny(delay, triggered).ConfigureAwait(false); // Propagate cancellation if the host is stopping stoppingToken.ThrowIfCancellationRequested(); } } private async Task SyncAccountAsync( EmailAccount account, IImapEmailService imapService, IPop3EmailService pop3Service, CancellationToken stoppingToken) { if (account.IncomingProtocol == IncomingProtocol.None) return; Logger.LogDebug( "Email sync started. Account={Username}, Protocol={Protocol}.", account.Username, account.IncomingProtocol); try { var result = account.IncomingProtocol switch { IncomingProtocol.Imap or IncomingProtocol.ImapOAuth2 => await imapService.SyncEmailsAsync(account, DefaultFolder, stoppingToken), IncomingProtocol.Pop3 or IncomingProtocol.Pop3OAuth2 => await pop3Service.SyncEmailsAsync(account, stoppingToken), _ => throw new NotSupportedException( $"IncomingProtocol '{account.IncomingProtocol}' is not supported by the sync worker.") }; Logger.LogDebug( "Email sync completed. Account={Username}, Protocol={Protocol}, Processed={Processed}, Failed={Failed}.", account.Username, account.IncomingProtocol, result.ProcessedCount, result.FailedCount); } catch (Exception ex) { Logger.LogError(ex, "Email sync failed. Account={Username}, Protocol={Protocol}.", account.Username, account.IncomingProtocol); } } public async Task UpsertSeedEmailAccount(CancellationToken stoppingToken) { await using var scope = Provider.CreateAsyncScope(); var emailAccountRepo = scope.ServiceProvider.GetRequiredService>(); foreach (var account in Options.Value.Accounts) await emailAccountRepo.UpsertAsync(a => a.Username == account.Username, account, stoppingToken); } }