From cda70c8ced11442aafc88da43604b90d0a378a9f Mon Sep 17 00:00:00 2001 From: TekH Date: Thu, 13 Aug 2026 16:48:47 +0200 Subject: [PATCH] feat(infrastructure): replace DateFilter-based sync with IMemoryCache last-sync tracking in LimilabsImapEmailService; add CacheExtensions helper; return EmailSyncResult with processed/failed counts --- .../Services/Extensions/CacheExtensions.cs | 24 +++++++++++ .../Services/LimilabsImapEmailService.cs | 40 +++++++++++-------- 2 files changed, 47 insertions(+), 17 deletions(-) create mode 100644 src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/Extensions/CacheExtensions.cs diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/Extensions/CacheExtensions.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/Extensions/CacheExtensions.cs new file mode 100644 index 0000000..3af2739 --- /dev/null +++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/Extensions/CacheExtensions.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Caching.Memory; + +namespace DigitalData.MessagingService.Infrastructure.Services.Extensions; + +public static class CacheExtensions +{ + private readonly static string ImapCacheKeyPrefix = Guid.NewGuid().ToString(); + + private static string CreateImapLastSyncDateCacheKey(int accountId, string folder) + { + return $"{ImapCacheKeyPrefix}_{accountId}_{folder}_LastImapSyncDate"; + } + + public static DateTime? GetLastImapSyncDate(this IMemoryCache cache, int accountId, string folder) + { + return cache.Get(CreateImapLastSyncDateCacheKey(accountId, folder)); + } + + public static void SetLastImapSyncDate(this IMemoryCache cache, int accountId, string folder, DateTime date) + { + var key = CreateImapLastSyncDateCacheKey(accountId, folder); + cache.Set(key, date); + } +} diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs index fb60dac..41a3263 100644 --- a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs +++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs @@ -1,13 +1,11 @@ using DigitalData.MessagingService.Application.Common.Dto; -using DigitalData.MessagingService.Application.Common.Dto.MailSearch; using DigitalData.MessagingService.Application.Common.Interfaces; using DigitalData.MessagingService.Application.Common.Interfaces.Repositories; using DigitalData.MessagingService.Domain.Entities; -using DigitalData.MessagingService.Domain.Exceptions; using DigitalData.MessagingService.Infrastructure.Services.Extensions; using Limilabs.Client.IMAP; using Limilabs.Mail; -using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using System.Text; @@ -17,19 +15,17 @@ namespace DigitalData.MessagingService.Infrastructure.Services; /// IMAP email service using Limilabs Mail.dll. /// Opens a fresh connection per call — stateless and thread-safe. /// -public class LimilabsImapEmailService(ILogger Logger, IRepository Repository) : IImapEmailService +public class LimilabsImapEmailService(ILogger Logger, IRepository Repository, IMemoryCache Cache) : IImapEmailService { + private static readonly string CacheKeyPrefix = Guid.NewGuid().ToString(); + static LimilabsImapEmailService() { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); } // Public API - public async Task SyncEmailsAsync( - EmailAccount account, - DateFilter date, - string folder = "INBOX", - CancellationToken cancel = default) + public async Task SyncEmailsAsync(EmailAccount account, string folder = "INBOX", CancellationToken cancel = default) { using var imap = await OpenAsync(account, folder, cancel); try @@ -38,22 +34,25 @@ public class LimilabsImapEmailService(ILogger Logger, // Server-side: only date range; all other filters are applied in-process after cache retrieval List criterions = []; - if (date.After is DateTime after) - criterions.Add(Expression.SentSince(after.Date)); + var since = Cache.GetLastImapSyncDate(account.Id, folder); - // IMAP BEFORE is exclusive, so add one day to make the bound inclusive - if (date.Before is DateTime before) - criterions.Add(Expression.SentBefore(before.Date.AddDays(1))); + if (since is not null && since != default) + criterions.Add(Expression.SentSince(since.Value)); var searchExpression = criterions.Count > 0 ? Expression.And([.. criterions]) : Expression.All(); + + var operationStartTime = DateTime.UtcNow; + List uids = [.. await imap.SearchAsync(searchExpression, cancel)]; #endregion - + if (uids.Count == 0) - return; + return new EmailSyncResult(); var emails = new List(uids.Count); + var failedCount = 0; + foreach (var uid in uids) { if (await Repository.AnyAsync(x => x.Uid == uid, cancel)) @@ -97,6 +96,7 @@ public class LimilabsImapEmailService(ILogger Logger, var email = new ReceivedEmailDto { Uid = uid, + AccountId = account.Id, From = mail.From.FirstOrDefault()?.Address ?? string.Empty, To = [.. mail.To.SelectMany(m => m.GetMailboxes()).Select(mb => mb.Address)], Cc = [.. mail.Cc.SelectMany(m => m.GetMailboxes()).Select(mb => mb.Address)], @@ -106,13 +106,17 @@ public class LimilabsImapEmailService(ILogger Logger, Date = mail.Date ?? DateTime.MinValue, IsSeen = flags.Contains(Flag.Seen), Attachments = attachments, + Folder = folder }; #endregion Read email emails.Add(email); + + Cache.SetLastImapSyncDate(account.Id, folder, operationStartTime); } catch (Exception ex) { + failedCount += 1; Logger.LogWarning(ex, "Failed to fetch IMAP message UID={Uid} from folder {Folder}. Skipping.", uid, folder); @@ -121,7 +125,9 @@ public class LimilabsImapEmailService(ILogger Logger, await imap.CloseAsync(cancel); - await Repository.CreateAsync(emails, cancel); + await Repository.CreateRangeAsync(emails, cancel); + + return new EmailSyncResult(ProcessedCount: emails.Count, FailedCount: failedCount); } catch {