diff --git a/src/core/DigitalData.MessagingService.Application/Common/Interfaces/IImapEmailService.cs b/src/core/DigitalData.MessagingService.Application/Common/Interfaces/IImapEmailService.cs
index 0fd507f..4d4e773 100644
--- a/src/core/DigitalData.MessagingService.Application/Common/Interfaces/IImapEmailService.cs
+++ b/src/core/DigitalData.MessagingService.Application/Common/Interfaces/IImapEmailService.cs
@@ -19,6 +19,24 @@ public interface IImapEmailService
MailSearchFilter filter,
CancellationToken cancellationToken = default);
+ ///
+ /// Fetches only the UIDs of messages matching the specified filter.
+ ///
+ Task> FetchEmailUidsAsync(
+ EmailAccountDto account,
+ MailSearchFilter filter,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Fetches a single email by its UID.
+ ///
+ Task FetchEmailByUidAsync(
+ EmailAccountDto account,
+ long uid,
+ string folder = "INBOX",
+ bool withAttachments = false,
+ CancellationToken cancellationToken = default);
+
///
/// Marks a message as seen (read) on the server.
///
diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs
index 188d439..25f3552 100644
--- a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs
+++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs
@@ -125,6 +125,115 @@ public class LimilabsImapEmailService(
}
}
+ public async Task> FetchEmailUidsAsync(
+ EmailAccountDto account,
+ MailSearchFilter filter,
+ CancellationToken cancellationToken = default)
+ {
+ using var imap = new Imap();
+ try
+ {
+ await ConnectAndAuthenticateAsync(imap, account);
+ await SelectFolderAsync(imap, filter.Folder);
+
+ List criterions = [];
+
+ if (filter.UnseenOnly)
+ criterions.Add(Expression.HasFlag(Flag.Unseen));
+
+ if (filter.SubjectContains is not null)
+ criterions.Add(Expression.Subject(filter.SubjectContains));
+
+ if (filter.SenderContains is not null)
+ criterions.Add(Expression.From(filter.SenderContains));
+
+ if (filter.RecipientContains is not null)
+ criterions.Add(Expression.To(filter.RecipientContains));
+
+ if (filter.BodyContains is not null)
+ criterions.Add(Expression.Body(filter.BodyContains));
+
+ if (filter.Uid is UidFilter uidF)
+ {
+ if (uidF.Absolute is long exactUid)
+ criterions.Add(Expression.UID(new Limilabs.Client.IMAP.Range(exactUid, exactUid)));
+ else
+ {
+ long lo = uidF.Min ?? 1L;
+ long? hi = uidF.Max;
+ criterions.Add(Expression.UID(new Limilabs.Client.IMAP.Range(lo, hi)));
+ }
+ }
+
+ if (filter.Date is DateFilter dateF)
+ {
+ if (dateF.After is DateTime after)
+ criterions.Add(Expression.SentSince(after.Date));
+
+ if (dateF.Before is DateTime before)
+ criterions.Add(Expression.SentBefore(before.Date.AddDays(1)));
+ }
+
+ var searchExpression = criterions.Count > 0 ? Expression.And([.. criterions]) : Expression.All();
+ List uids = [.. await imap.SearchAsync(searchExpression, cancellationToken)];
+
+ if (filter.SortOrder == MailSortOrder.NewestFirst)
+ uids.Reverse();
+
+ if (filter.MaxCount > 0 && uids.Count > filter.MaxCount)
+ uids = [.. uids.Take(filter.MaxCount)];
+
+ await imap.CloseAsync(cancellationToken);
+ return uids;
+ }
+ catch (Limilabs.Client.ServerException ex)
+ {
+ await imap.CloseSafelyAsync();
+ throw new AuthenticationFailedException(
+ $"IMAP authentication failed for account '{account.Username}'.", ex);
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ await imap.CloseSafelyAsync();
+ throw new InvalidOperationException(
+ $"Failed to fetch email UIDs from IMAP server '{account.ImapServer}'.", ex);
+ }
+ }
+
+ public async Task FetchEmailByUidAsync(
+ EmailAccountDto account,
+ long uid,
+ string folder = "INBOX",
+ bool withAttachments = false,
+ CancellationToken cancellationToken = default)
+ {
+ using var imap = new Imap();
+ try
+ {
+ await ConnectAndAuthenticateAsync(imap, account);
+ await SelectFolderAsync(imap, folder);
+
+ var eml = await imap.PeekMessageByUIDAsync(uid, cancellationToken);
+ var mail = new MailBuilder().CreateFromEml(eml);
+ var flags = await imap.GetFlagsByUIDAsync(uid, cancellationToken);
+
+ await imap.CloseAsync(cancellationToken);
+ return MapToContext(uid, mail, flags, withAttachments);
+ }
+ catch (Limilabs.Client.ServerException ex)
+ {
+ await imap.CloseSafelyAsync();
+ throw new AuthenticationFailedException(
+ $"IMAP authentication failed for account '{account.Username}'.", ex);
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ await imap.CloseSafelyAsync();
+ logger.LogWarning(ex, "Failed to fetch IMAP message UID={Uid} from folder {Folder}.", uid, folder);
+ return null;
+ }
+ }
+
public async Task MarkAsSeenAsync(
EmailAccountDto account,
long uid,