Files

110 lines
4.7 KiB
C#

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<EmailAccountsOptions> Options, IServiceProvider Provider, ILogger<EmailSyncWorker> Logger) : BackgroundService
{
private readonly string DefaultFolder = "INBOX";
/// <summary>
/// Signals the current <see cref="Task.Delay"/> 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.
/// </summary>
private volatile TaskCompletionSource<bool> _syncTrigger = new(TaskCreationOptions.RunContinuationsAsynchronously);
/// <summary>
/// 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.
/// </summary>
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<IRepository<EmailAccount>>();
var imapService = scope.ServiceProvider.GetRequiredService<IImapEmailService>();
var pop3Service = scope.ServiceProvider.GetRequiredService<IPop3EmailService>();
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<bool>(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<IRepository<EmailAccount>>();
foreach (var account in Options.Value.Accounts)
await emailAccountRepo.UpsertAsync(a => a.Username == account.Username, account, stoppingToken);
}
}