Refactor email search with new filter models

Replaced the old `SearchFilter` with the new `MailSearchFilter` model to enable more flexible and granular email search criteria. Introduced `DateFilter`, `UidFilter`, and `MailSortOrder` to support advanced filtering options such as date ranges, UID ranges, and sorting order. Updated `IImapEmailService` and `FetchEmailsQuery` to use the new models.

Added validators (`DateFilterValidator`, `MailSearchFilterValidator`, `UidFilterValidator`) to ensure input correctness. Refactored `LimilabsImapEmailService` to dynamically construct IMAP search queries based on the new filter properties, supporting unseen messages, text filters, and client-side attachment filtering.

Improved maintainability and scalability by cleaning up redundant code and leveraging the new models and validation framework.
This commit is contained in:
2026-08-07 14:52:28 +02:00
parent 53ba40b316
commit 8e2e9af451
11 changed files with 346 additions and 35 deletions

View File

@@ -1,11 +1,12 @@
using System.Text;
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Abstraction;
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Application.Common.Models.MailSearch;
using DigitalData.MessagingService.Domain.Exceptions;
using DigitalData.MessagingService.Infrastructure.Services.Extensions;
using Limilabs.Client.IMAP;
using Limilabs.Mail;
using Microsoft.Extensions.Logging;
using System.Text;
namespace DigitalData.MessagingService.Infrastructure.Services;
@@ -25,7 +26,7 @@ public class LimilabsImapEmailService(
// Public API
public async Task<IEnumerable<ReceivedEmailContext>> FetchEmailsAsync(
EmailAccountDto account,
FetchEmailsQuery.SearchFilter filter,
MailSearchFilter filter,
CancellationToken cancellationToken = default)
{
using var imap = new Imap();
@@ -34,17 +35,59 @@ public class LimilabsImapEmailService(
await ConnectAndAuthenticateAsync(imap, account);
await SelectFolderAsync(imap, filter.Folder);
// Get UIDs to fetch
List<long> uids = filter.UnseenOnly
? [.. await imap.SearchAsync(Flag.Unseen, cancellationToken)]
: [.. await imap.GetAllAsync(cancellationToken)];
List<ICriterion> criterions = [];
// Most-recent first; honour maxCount
uids.Reverse();
if (filter.MaxCount > 0 && uids.Count() > filter.MaxCount)
if (filter.UnseenOnly)
criterions.Add((ICriterion)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
{
// 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)));
}
}
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)));
}
// Get UIDs to fetch
List<long> uids = [.. await imap.SearchAsync(Expression.And([.. criterions]), 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)];
var results = new List<ReceivedEmailContext>(uids.Count());
var results = new List<ReceivedEmailContext>(uids.Count);
foreach (var uid in uids)
{
@@ -52,10 +95,14 @@ public class LimilabsImapEmailService(
try
{
var eml = await imap.PeekMessageByUIDAsync(uid, cancellationToken);
var mail = new MailBuilder().CreateFromEml(eml);
var eml = await imap.PeekMessageByUIDAsync(uid, cancellationToken);
var mail = new MailBuilder().CreateFromEml(eml);
var flags = await imap.GetFlagsByUIDAsync(uid, cancellationToken);
// Client-side attachment filter — IMAP has no native criterion for this
if (filter.HasAttachments && mail.Attachments.Count == 0 && mail.Visuals.Count == 0)
continue;
results.Add(MapToContext(uid, mail, flags));
}
catch (Exception ex)