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,