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:
2026-08-11 15:52:17 +02:00
parent e840d026aa
commit f3761e96d9
2 changed files with 27 additions and 13 deletions

View File

@@ -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)

View File

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