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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user