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>();
|
services.AddSingleton<IEmailService, LimilabsEmailService>();
|
||||||
|
|
||||||
// Email Service - IMAP inbound (Limilabs Mail.dll)
|
// Email Service - IMAP inbound (Limilabs Mail.dll)
|
||||||
|
// Fresh connection per call — stateless and thread-safe.
|
||||||
services.AddSingleton<IImapEmailService, LimilabsImapEmailService>();
|
services.AddSingleton<IImapEmailService, LimilabsImapEmailService>();
|
||||||
|
|
||||||
// PDF Processing Service (using DevExpress.Pdf)
|
// PDF Processing Service (using DevExpress.Pdf)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace DigitalData.MessagingService.Infrastructure.Services;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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(
|
public class LimilabsImapEmailService(
|
||||||
IEncryptionService encryptionService,
|
IEncryptionService encryptionService,
|
||||||
@@ -27,6 +27,7 @@ public class LimilabsImapEmailService(
|
|||||||
public async Task<IEnumerable<ReceivedEmailContext>> FetchEmailsAsync(
|
public async Task<IEnumerable<ReceivedEmailContext>> FetchEmailsAsync(
|
||||||
EmailAccountDto account,
|
EmailAccountDto account,
|
||||||
MailSearchFilter filter,
|
MailSearchFilter filter,
|
||||||
|
bool markAsSeen = true,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
using var imap = new Imap();
|
using var imap = new Imap();
|
||||||
@@ -55,12 +56,9 @@ public class LimilabsImapEmailService(
|
|||||||
if (filter.Uid is UidFilter uidF)
|
if (filter.Uid is UidFilter uidF)
|
||||||
{
|
{
|
||||||
if (uidF.Absolute is long exactUid)
|
if (uidF.Absolute is long exactUid)
|
||||||
{
|
|
||||||
criterions.Add(Expression.UID(new Limilabs.Client.IMAP.Range(exactUid, exactUid)));
|
criterions.Add(Expression.UID(new Limilabs.Client.IMAP.Range(exactUid, exactUid)));
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Open-ended ranges: fall back to 1 / null when one side is omitted
|
|
||||||
long lo = uidF.Min ?? 1L;
|
long lo = uidF.Min ?? 1L;
|
||||||
long? hi = uidF.Max;
|
long? hi = uidF.Max;
|
||||||
criterions.Add(Expression.UID(new Limilabs.Client.IMAP.Range(lo, hi)));
|
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)));
|
criterions.Add(Expression.SentBefore(before.Date.AddDays(1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get UIDs to fetch
|
|
||||||
var searchExpression = criterions.Count > 0 ? Expression.And([.. criterions]) : Expression.All();
|
var searchExpression = criterions.Count > 0 ? Expression.And([.. criterions]) : Expression.All();
|
||||||
List<long> uids = [.. await imap.SearchAsync(searchExpression, cancellationToken)];
|
List<long> uids = [.. await imap.SearchAsync(searchExpression, cancellationToken)];
|
||||||
|
|
||||||
// Apply requested sort order
|
|
||||||
if (filter.SortOrder == MailSortOrder.NewestFirst)
|
if (filter.SortOrder == MailSortOrder.NewestFirst)
|
||||||
uids.Reverse();
|
uids.Reverse();
|
||||||
|
|
||||||
if (filter.MaxCount > 0 && uids.Count > filter.MaxCount)
|
if (filter.MaxCount > 0 && uids.Count > filter.MaxCount)
|
||||||
uids = [.. uids.Take(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);
|
var results = new List<ReceivedEmailContext>(uids.Count);
|
||||||
|
|
||||||
foreach (var uid in uids)
|
foreach (var uid in uids)
|
||||||
@@ -96,15 +103,18 @@ public class LimilabsImapEmailService(
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var eml = await imap.PeekMessageByUIDAsync(uid, cancellationToken);
|
var eml = markAsSeen
|
||||||
var mail = new MailBuilder().CreateFromEml(eml);
|
? await imap.GetMessageByUIDAsync(uid, cancellationToken)
|
||||||
var flags = await imap.GetFlagsByUIDAsync(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));
|
results.Add(MapToContext(uid, mail, flags, filter.WithAttachments));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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,
|
long uid,
|
||||||
string folder = "INBOX",
|
string folder = "INBOX",
|
||||||
bool withAttachments = false,
|
bool withAttachments = false,
|
||||||
|
bool markAsSeen = true,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
using var imap = new Imap();
|
using var imap = new Imap();
|
||||||
@@ -213,7 +224,9 @@ public class LimilabsImapEmailService(
|
|||||||
await ConnectAndAuthenticateAsync(imap, account);
|
await ConnectAndAuthenticateAsync(imap, account);
|
||||||
await SelectFolderAsync(imap, folder);
|
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 mail = new MailBuilder().CreateFromEml(eml);
|
||||||
var flags = await imap.GetFlagsByUIDAsync(uid, cancellationToken);
|
var flags = await imap.GetFlagsByUIDAsync(uid, cancellationToken);
|
||||||
|
|
||||||
@@ -285,7 +298,7 @@ public class LimilabsImapEmailService(
|
|||||||
await imap.SelectAsync(folder);
|
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>();
|
var attachments = new List<EmailAttachmentContext>();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user