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;
|
||||||
using DigitalData.MessagingService.Application.Common.Dto.MailSearch;
|
|
||||||
using DigitalData.MessagingService.Application.Common.Interfaces;
|
using DigitalData.MessagingService.Application.Common.Interfaces;
|
||||||
using DigitalData.MessagingService.Application.Common.Interfaces.Repositories;
|
using DigitalData.MessagingService.Application.Common.Interfaces.Repositories;
|
||||||
using DigitalData.MessagingService.Domain.Entities;
|
using DigitalData.MessagingService.Domain.Entities;
|
||||||
using DigitalData.MessagingService.Domain.Exceptions;
|
|
||||||
using DigitalData.MessagingService.Infrastructure.Services.Extensions;
|
using DigitalData.MessagingService.Infrastructure.Services.Extensions;
|
||||||
using Limilabs.Client.IMAP;
|
using Limilabs.Client.IMAP;
|
||||||
using Limilabs.Mail;
|
using Limilabs.Mail;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
@@ -17,19 +15,17 @@ namespace DigitalData.MessagingService.Infrastructure.Services;
|
|||||||
/// IMAP email service using Limilabs Mail.dll.
|
/// IMAP email service using Limilabs Mail.dll.
|
||||||
/// Opens a fresh connection per call — stateless and thread-safe.
|
/// Opens a fresh connection per call — stateless and thread-safe.
|
||||||
/// </summary>
|
/// </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()
|
static LimilabsImapEmailService()
|
||||||
{
|
{
|
||||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public API
|
// Public API
|
||||||
public async Task SyncEmailsAsync(
|
public async Task<EmailSyncResult> SyncEmailsAsync(EmailAccount account, string folder = "INBOX", CancellationToken cancel = default)
|
||||||
EmailAccount account,
|
|
||||||
DateFilter date,
|
|
||||||
string folder = "INBOX",
|
|
||||||
CancellationToken cancel = default)
|
|
||||||
{
|
{
|
||||||
using var imap = await OpenAsync(account, folder, cancel);
|
using var imap = await OpenAsync(account, folder, cancel);
|
||||||
try
|
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
|
// Server-side: only date range; all other filters are applied in-process after cache retrieval
|
||||||
List<ICriterion> criterions = [];
|
List<ICriterion> criterions = [];
|
||||||
|
|
||||||
if (date.After is DateTime after)
|
var since = Cache.GetLastImapSyncDate(account.Id, folder);
|
||||||
criterions.Add(Expression.SentSince(after.Date));
|
|
||||||
|
|
||||||
// IMAP BEFORE is exclusive, so add one day to make the bound inclusive
|
if (since is not null && since != default)
|
||||||
if (date.Before is DateTime before)
|
criterions.Add(Expression.SentSince(since.Value));
|
||||||
criterions.Add(Expression.SentBefore(before.Date.AddDays(1)));
|
|
||||||
|
|
||||||
var searchExpression = criterions.Count > 0 ? Expression.And([.. criterions]) : Expression.All();
|
var searchExpression = criterions.Count > 0 ? Expression.And([.. criterions]) : Expression.All();
|
||||||
|
|
||||||
|
var operationStartTime = DateTime.UtcNow;
|
||||||
|
|
||||||
List<long> uids = [.. await imap.SearchAsync(searchExpression, cancel)];
|
List<long> uids = [.. await imap.SearchAsync(searchExpression, cancel)];
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
if (uids.Count == 0)
|
if (uids.Count == 0)
|
||||||
return;
|
return new EmailSyncResult();
|
||||||
|
|
||||||
var emails = new List<ReceivedEmailDto>(uids.Count);
|
var emails = new List<ReceivedEmailDto>(uids.Count);
|
||||||
|
|
||||||
|
var failedCount = 0;
|
||||||
|
|
||||||
foreach (var uid in uids)
|
foreach (var uid in uids)
|
||||||
{
|
{
|
||||||
if (await Repository.AnyAsync(x => x.Uid == uid, cancel))
|
if (await Repository.AnyAsync(x => x.Uid == uid, cancel))
|
||||||
@@ -97,6 +96,7 @@ public class LimilabsImapEmailService(ILogger<LimilabsImapEmailService> Logger,
|
|||||||
var email = new ReceivedEmailDto
|
var email = new ReceivedEmailDto
|
||||||
{
|
{
|
||||||
Uid = uid,
|
Uid = uid,
|
||||||
|
AccountId = account.Id,
|
||||||
From = mail.From.FirstOrDefault()?.Address ?? string.Empty,
|
From = mail.From.FirstOrDefault()?.Address ?? string.Empty,
|
||||||
To = [.. mail.To.SelectMany(m => m.GetMailboxes()).Select(mb => mb.Address)],
|
To = [.. mail.To.SelectMany(m => m.GetMailboxes()).Select(mb => mb.Address)],
|
||||||
Cc = [.. mail.Cc.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,
|
Date = mail.Date ?? DateTime.MinValue,
|
||||||
IsSeen = flags.Contains(Flag.Seen),
|
IsSeen = flags.Contains(Flag.Seen),
|
||||||
Attachments = attachments,
|
Attachments = attachments,
|
||||||
|
Folder = folder
|
||||||
};
|
};
|
||||||
#endregion Read email
|
#endregion Read email
|
||||||
|
|
||||||
emails.Add(email);
|
emails.Add(email);
|
||||||
|
|
||||||
|
Cache.SetLastImapSyncDate(account.Id, folder, operationStartTime);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
failedCount += 1;
|
||||||
Logger.LogWarning(ex,
|
Logger.LogWarning(ex,
|
||||||
"Failed to fetch IMAP message UID={Uid} from folder {Folder}. Skipping.",
|
"Failed to fetch IMAP message UID={Uid} from folder {Folder}. Skipping.",
|
||||||
uid, folder);
|
uid, folder);
|
||||||
@@ -121,7 +125,9 @@ public class LimilabsImapEmailService(ILogger<LimilabsImapEmailService> Logger,
|
|||||||
|
|
||||||
await imap.CloseAsync(cancel);
|
await imap.CloseAsync(cancel);
|
||||||
|
|
||||||
await Repository.CreateAsync(emails, cancel);
|
await Repository.CreateRangeAsync(emails, cancel);
|
||||||
|
|
||||||
|
return new EmailSyncResult(ProcessedCount: emails.Count, FailedCount: failedCount);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user