refactor(imap): remove connection pool, revert to per-call Imap, add batch flags fetch
- ImapConnectionPool removed (over-engineered; IMAP server allows multiple concurrent connections) - LimilabsImapEmailService reverted to stateless per-call new Imap() pattern (same as original) - GetFlagsByUIDAsync(List<long>) replaces per-message GetFlagsByUID — one round-trip for all flags - markAsSeen wired through: GetMessageByUIDAsync (true) vs PeekMessageByUIDAsync (false) - DependencyInjection: pool registration removed, service registered directly as before
This commit is contained in:
@@ -27,6 +27,7 @@ public static class DependencyInjection
|
||||
services.AddSingleton<IEmailService, LimilabsEmailService>();
|
||||
|
||||
// Email Service - IMAP inbound (Limilabs Mail.dll)
|
||||
// Fresh connection per call — stateless and thread-safe.
|
||||
services.AddSingleton<IImapEmailService, LimilabsImapEmailService>();
|
||||
|
||||
// PDF Processing Service (using DevExpress.Pdf)
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace DigitalData.MessagingService.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
public class LimilabsImapEmailService(
|
||||
IEncryptionService encryptionService,
|
||||
@@ -27,6 +27,7 @@ public class LimilabsImapEmailService(
|
||||
public async Task<IEnumerable<ReceivedEmailContext>> FetchEmailsAsync(
|
||||
EmailAccountDto account,
|
||||
MailSearchFilter filter,
|
||||
bool markAsSeen = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var imap = new Imap();
|
||||
@@ -55,12 +56,9 @@ public class LimilabsImapEmailService(
|
||||
if (filter.Uid is UidFilter uidF)
|
||||
{
|
||||
if (uidF.Absolute is long exactUid)
|
||||
{
|
||||
criterions.Add(Expression.UID(new Limilabs.Client.IMAP.Range(exactUid, exactUid)));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Open-ended ranges: fall back to 1 / null when one side is omitted
|
||||
long lo = uidF.Min ?? 1L;
|
||||
long? hi = uidF.Max;
|
||||
criterions.Add(Expression.UID(new Limilabs.Client.IMAP.Range(lo, hi)));
|
||||
@@ -77,17 +75,26 @@ public class LimilabsImapEmailService(
|
||||
criterions.Add(Expression.SentBefore(before.Date.AddDays(1)));
|
||||
}
|
||||
|
||||
// Get UIDs to fetch
|
||||
var searchExpression = criterions.Count > 0 ? Expression.And([.. criterions]) : Expression.All();
|
||||
List<long> uids = [.. await imap.SearchAsync(searchExpression, cancellationToken)];
|
||||
|
||||
// Apply requested sort order
|
||||
if (filter.SortOrder == MailSortOrder.NewestFirst)
|
||||
uids.Reverse();
|
||||
|
||||
if (filter.MaxCount > 0 && uids.Count > filter.MaxCount)
|
||||
uids = [.. uids.Take(filter.MaxCount)];
|
||||
|
||||
if (uids.Count == 0)
|
||||
return [];
|
||||
|
||||
// Fetch flags for all UIDs in one round-trip
|
||||
var allFlags = await imap.GetFlagsByUIDAsync(uids, cancellationToken);
|
||||
var flagsById = allFlags
|
||||
.Where(f => f.UID.HasValue)
|
||||
.ToDictionary(f => f.UID!.Value, f => f.Flags);
|
||||
|
||||
// markAsSeen=true → BODY[] (server sets \Seen automatically)
|
||||
// markAsSeen=false → BODY.PEEK[] (\Seen untouched, non-destructive read)
|
||||
var results = new List<ReceivedEmailContext>(uids.Count);
|
||||
|
||||
foreach (var uid in uids)
|
||||
@@ -96,15 +103,18 @@ public class LimilabsImapEmailService(
|
||||
|
||||
try
|
||||
{
|
||||
var eml = await imap.PeekMessageByUIDAsync(uid, cancellationToken);
|
||||
var mail = new MailBuilder().CreateFromEml(eml);
|
||||
var flags = await imap.GetFlagsByUIDAsync(uid, cancellationToken);
|
||||
|
||||
var eml = markAsSeen
|
||||
? await imap.GetMessageByUIDAsync(uid, cancellationToken)
|
||||
: await imap.PeekMessageByUIDAsync(uid, cancellationToken);
|
||||
var mail = new MailBuilder().CreateFromEml(eml);
|
||||
flagsById.TryGetValue(uid, out var flags);
|
||||
results.Add(MapToContext(uid, mail, flags, filter.WithAttachments));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to fetch IMAP message UID={Uid} from folder {Folder}. Skipping.", uid, filter.Folder);
|
||||
logger.LogWarning(ex,
|
||||
"Failed to fetch IMAP message UID={Uid} from folder {Folder}. Skipping.",
|
||||
uid, filter.Folder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +215,7 @@ public class LimilabsImapEmailService(
|
||||
long uid,
|
||||
string folder = "INBOX",
|
||||
bool withAttachments = false,
|
||||
bool markAsSeen = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var imap = new Imap();
|
||||
@@ -213,7 +224,9 @@ public class LimilabsImapEmailService(
|
||||
await ConnectAndAuthenticateAsync(imap, account);
|
||||
await SelectFolderAsync(imap, folder);
|
||||
|
||||
var eml = await imap.PeekMessageByUIDAsync(uid, cancellationToken);
|
||||
var eml = markAsSeen
|
||||
? await imap.GetMessageByUIDAsync(uid, cancellationToken)
|
||||
: await imap.PeekMessageByUIDAsync(uid, cancellationToken);
|
||||
var mail = new MailBuilder().CreateFromEml(eml);
|
||||
var flags = await imap.GetFlagsByUIDAsync(uid, cancellationToken);
|
||||
|
||||
@@ -285,7 +298,7 @@ public class LimilabsImapEmailService(
|
||||
await imap.SelectAsync(folder);
|
||||
}
|
||||
|
||||
private static ReceivedEmailContext MapToContext(long uid, IMail mail, List<Flag> flags, bool withAttachments = false)
|
||||
private static ReceivedEmailContext MapToContext(long uid, IMail mail, List<Flag>? flags, bool withAttachments = false)
|
||||
{
|
||||
var attachments = new List<EmailAttachmentContext>();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user