From f3761e96d9ff999208da00cbc4fa4adf25dd572e Mon Sep 17 00:00:00 2001 From: TekH Date: Tue, 11 Aug 2026 15:52:17 +0200 Subject: [PATCH] refactor(imap): remove connection pool, revert to per-call Imap, add batch flags fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ImapConnectionPool removed (over-engineered; IMAP server allows multiple concurrent connections) - LimilabsImapEmailService reverted to stateless per-call new Imap() pattern (same as original) - GetFlagsByUIDAsync(List) 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 --- .../DependencyInjection.cs | 1 + .../Services/LimilabsImapEmailService.cs | 39 ++++++++++++------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/DependencyInjection.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/DependencyInjection.cs index 14d4a91..f60b874 100644 --- a/src/infrastructure/DigitalData.MessagingService.Infrastructure/DependencyInjection.cs +++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/DependencyInjection.cs @@ -27,6 +27,7 @@ public static class DependencyInjection services.AddSingleton(); // Email Service - IMAP inbound (Limilabs Mail.dll) + // Fresh connection per call — stateless and thread-safe. services.AddSingleton(); // PDF Processing Service (using DevExpress.Pdf) diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs index 25f3552..c74fa21 100644 --- a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs +++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs @@ -12,7 +12,7 @@ namespace DigitalData.MessagingService.Infrastructure.Services; /// /// 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. /// public class LimilabsImapEmailService( IEncryptionService encryptionService, @@ -27,6 +27,7 @@ public class LimilabsImapEmailService( public async Task> 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 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(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 flags, bool withAttachments = false) + private static ReceivedEmailContext MapToContext(long uid, IMail mail, List? flags, bool withAttachments = false) { var attachments = new List();