diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs index b4077f2..43d7487 100644 --- a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs +++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs @@ -36,7 +36,55 @@ public class LimilabsImapEmailService( using var imap = await OpenAsync(account, filter.Folder); try { - var uids = await FetchEmailUidsAsync(imap, filter, cancellationToken); + #region Find UIDs + 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)); + + // IMAP BEFORE is exclusive, so add one day to make the bound inclusive + 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 is int maxCount && maxCount > 0 && uids.Count > maxCount) + uids = [.. uids.Take(maxCount)]; + #endregion if (uids.Count == 0) return []; @@ -49,10 +97,64 @@ public class LimilabsImapEmailService( try { - var email = await FetchEmailByUidAsync(imap, uid, filter.WithAttachments, cancellationToken) - ?? throw new NotFoundException($"Email with UID={uid} not found in folder '{filter.Folder}' in {account.Username}."); - results.Add(email); + #region Read email + var email = await Cache.GetOrCreateAsync( + CacheKeyPrefix + uid, + async entry => + { + var eml = await imap.GetMessageByUIDAsync(uid, cancellationToken); + var mail = new MailBuilder().CreateFromEml(eml); + + var attachments = new List(); + + foreach (var att in mail.Attachments) + { + attachments.Add(new EmailAttachmentContext + { + FileName = att.FileName ?? "attachment", + Content = att.Data, + ContentType = att.ContentType?.ToString() ?? "application/octet-stream", + IsInline = false, + ContentId = att.ContentId + }); + } + + foreach (var vis in mail.Visuals) + { + attachments.Add(new EmailAttachmentContext + { + FileName = vis.FileName ?? "inline", + Content = vis.Data, + ContentType = vis.ContentType?.ToString() ?? "application/octet-stream", + IsInline = true, + ContentId = vis.ContentId + }); + } + + return new ReceivedEmailContext + { + Uid = uid, + From = mail.From.FirstOrDefault()?.Address ?? string.Empty, + To = [.. mail.To.SelectMany(m => m.GetMailboxes()).Select(mb => mb.Address)], + Cc = [.. mail.Cc.SelectMany(m => m.GetMailboxes()).Select(mb => mb.Address)], + Subject = mail.Subject ?? string.Empty, + TextBody = mail.Text ?? string.Empty, + HtmlBody = mail.Html ?? string.Empty, + Date = mail.Date ?? DateTime.MinValue, + Attachments = attachments, + }; + }); + #endregion Read email + + if(email is not null) + { + if (filter.WithAttachments) + results.Add(email); + else + results.Add(email with { Attachments = [] }); + } + } catch (Exception ex) { @@ -105,119 +207,6 @@ public class LimilabsImapEmailService( } } - // Private helpers - private static async Task> FetchEmailUidsAsync( - Imap imap, - MailSearchFilter filter, - CancellationToken cancellationToken = default) - { - 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)); - - // IMAP BEFORE is exclusive, so add one day to make the bound inclusive - 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 is int maxCount && maxCount > 0 && uids.Count > maxCount) - uids = [.. uids.Take(maxCount)]; - - return uids; - } - - private async Task FetchEmailByUidAsync( - Imap imap, - long uid, - bool withAttachments = false, - CancellationToken cancellationToken = default) - { - var email = await Cache.GetOrCreateAsync( - CacheKeyPrefix + uid, - async entry => - { - var eml = await imap.GetMessageByUIDAsync(uid, cancellationToken); - var mail = new MailBuilder().CreateFromEml(eml); - - var attachments = new List(); - - foreach (var att in mail.Attachments) - { - attachments.Add(new EmailAttachmentContext - { - FileName = att.FileName ?? "attachment", - Content = att.Data, - ContentType = att.ContentType?.ToString() ?? "application/octet-stream", - IsInline = false, - ContentId = att.ContentId - }); - } - - foreach (var vis in mail.Visuals) - { - attachments.Add(new EmailAttachmentContext - { - FileName = vis.FileName ?? "inline", - Content = vis.Data, - ContentType = vis.ContentType?.ToString() ?? "application/octet-stream", - IsInline = true, - ContentId = vis.ContentId - }); - } - - return new ReceivedEmailContext - { - Uid = uid, - From = mail.From.FirstOrDefault()?.Address ?? string.Empty, - To = [.. mail.To.SelectMany(m => m.GetMailboxes()).Select(mb => mb.Address)], - Cc = [.. mail.Cc.SelectMany(m => m.GetMailboxes()).Select(mb => mb.Address)], - Subject = mail.Subject ?? string.Empty, - TextBody = mail.Text ?? string.Empty, - HtmlBody = mail.Html ?? string.Empty, - Date = mail.Date ?? DateTime.MinValue, - Attachments = attachments, - }; - }); - - return withAttachments || email is null ? email : email with { Attachments = [] }; - } - private async Task OpenAsync(EmailAccountDto account, string folder) { var imap = new Imap();