feat(infrastructure): replace DateFilter-based sync with IMemoryCache last-sync tracking in LimilabsImapEmailService; add CacheExtensions helper; return EmailSyncResult with processed/failed counts

This commit is contained in:
2026-08-13 16:48:47 +02:00
parent 578ecc7ba1
commit cda70c8ced
2 changed files with 47 additions and 17 deletions

View File

@@ -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<DateTime?>(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);
}
}

View File

@@ -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.
/// </summary>
public class LimilabsImapEmailService(ILogger<LimilabsImapEmailService> Logger, IRepository<ReceivedEmail> Repository) : IImapEmailService
public class LimilabsImapEmailService(ILogger<LimilabsImapEmailService> Logger, IRepository<ReceivedEmail> 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<EmailSyncResult> 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<LimilabsImapEmailService> Logger,
// Server-side: only date range; all other filters are applied in-process after cache retrieval
List<ICriterion> 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<long> uids = [.. await imap.SearchAsync(searchExpression, cancel)];
#endregion
if (uids.Count == 0)
return;
return new EmailSyncResult();
var emails = new List<ReceivedEmailDto>(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<LimilabsImapEmailService> 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<LimilabsImapEmailService> 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<LimilabsImapEmailService> Logger,
await imap.CloseAsync(cancel);
await Repository.CreateAsync(emails, cancel);
await Repository.CreateRangeAsync(emails, cancel);
return new EmailSyncResult(ProcessedCount: emails.Count, FailedCount: failedCount);
}
catch
{