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:
@@ -1,6 +1,7 @@
|
||||
using DigitalData.MessagingService.Application.Common.Interfaces;
|
||||
using DigitalData.MessagingService.Application.EmailAccount.Queries;
|
||||
using DigitalData.MessagingService.Abstraction;
|
||||
using DigitalData.MessagingService.Application.Common.Interfaces;
|
||||
using DigitalData.MessagingService.Application.Common.Models.MailSearch;
|
||||
using DigitalData.MessagingService.Application.EmailAccount.Queries;
|
||||
using DigitalData.MessagingService.Domain.Exceptions;
|
||||
using MediatR;
|
||||
|
||||
@@ -19,25 +20,7 @@ public record FetchEmailsQuery : IRequest<IEnumerable<ReceivedEmailContext>>
|
||||
/// <summary>
|
||||
/// Mail query used to filter and limit the emails retrieved.
|
||||
/// </summary>
|
||||
public SearchFilter Mail { get; init; } = new();
|
||||
|
||||
public record SearchFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Mailbox folder to read from (default: "INBOX").
|
||||
/// </summary>
|
||||
public string Folder { get; init; } = "INBOX";
|
||||
|
||||
/// <summary>
|
||||
/// When <see langword="true"/> returns only unread messages.
|
||||
/// </summary>
|
||||
public bool UnseenOnly { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of messages to retrieve (most-recent first). 0 = unlimited.
|
||||
/// </summary>
|
||||
public int MaxCount { get; init; } = 50;
|
||||
}
|
||||
public MailSearchFilter Mail { get; init; } = new();
|
||||
}
|
||||
|
||||
public class FetchEmailsQueryHandler(
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using DigitalData.MessagingService.Application.Common.Models.MailSearch;
|
||||
using FluentValidation;
|
||||
|
||||
namespace DigitalData.MessagingService.Application.EmailReceiving.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Validates a <see cref="DateFilter"/> value object.
|
||||
/// </summary>
|
||||
public class DateFilterValidator : AbstractValidator<DateFilter>
|
||||
{
|
||||
public DateFilterValidator()
|
||||
{
|
||||
// At least one field must be provided
|
||||
RuleFor(x => x)
|
||||
.Must(d => d.After is not null || d.Before is not null)
|
||||
.WithName("Date")
|
||||
.WithMessage("DateFilter must specify at least one of: After or Before.");
|
||||
|
||||
// Dates must not be in the future
|
||||
RuleFor(x => x.After)
|
||||
.LessThanOrEqualTo(DateTime.UtcNow)
|
||||
.WithMessage("Date.After must not be in the future.")
|
||||
.When(x => x.After is not null);
|
||||
|
||||
RuleFor(x => x.Before)
|
||||
.LessThanOrEqualTo(DateTime.UtcNow)
|
||||
.WithMessage("Date.Before must not be in the future.")
|
||||
.When(x => x.Before is not null);
|
||||
|
||||
// Range coherence: After must be earlier than Before
|
||||
RuleFor(x => x)
|
||||
.Must(d => d.After!.Value < d.Before!.Value)
|
||||
.WithName("Date.Range")
|
||||
.WithMessage("Date.After must be earlier than Date.Before.")
|
||||
.When(x => x.After is not null && x.Before is not null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using DigitalData.MessagingService.Application.EmailReceiving.Queries;
|
||||
using FluentValidation;
|
||||
|
||||
namespace DigitalData.MessagingService.Application.EmailReceiving.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Validates a <see cref="FetchEmailsQuery"/> before it is handled by <see cref="FetchEmailsQueryHandler"/>.
|
||||
/// </summary>
|
||||
public class FetchEmailsQueryValidator : AbstractValidator<FetchEmailsQuery>
|
||||
{
|
||||
public FetchEmailsQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.Account)
|
||||
.NotNull()
|
||||
.WithMessage("Account query must not be null.");
|
||||
|
||||
RuleFor(x => x.Mail)
|
||||
.NotNull()
|
||||
.WithMessage("Mail search filter must not be null.")
|
||||
.SetValidator(new MailSearchFilterValidator());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using DigitalData.MessagingService.Application.Common.Models.MailSearch;
|
||||
using FluentValidation;
|
||||
|
||||
namespace DigitalData.MessagingService.Application.EmailReceiving.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Validates a <see cref="MailSearchFilter"/> instance before it is dispatched to the IMAP service.
|
||||
/// </summary>
|
||||
public class MailSearchFilterValidator : AbstractValidator<MailSearchFilter>
|
||||
{
|
||||
public MailSearchFilterValidator()
|
||||
{
|
||||
RuleFor(x => x.Folder)
|
||||
.NotEmpty()
|
||||
.WithMessage("Folder must not be empty.")
|
||||
.MaximumLength(255)
|
||||
.WithMessage("Folder name must not exceed 255 characters.");
|
||||
|
||||
RuleFor(x => x.MaxCount)
|
||||
.GreaterThanOrEqualTo(0)
|
||||
.WithMessage("MaxCount must be 0 (unlimited) or a positive number.");
|
||||
|
||||
RuleFor(x => x.SubjectContains)
|
||||
.MaximumLength(500)
|
||||
.WithMessage("SubjectContains must not exceed 500 characters.")
|
||||
.When(x => x.SubjectContains is not null);
|
||||
|
||||
RuleFor(x => x.SenderContains)
|
||||
.MaximumLength(320)
|
||||
.WithMessage("SenderContains must not exceed 320 characters.")
|
||||
.When(x => x.SenderContains is not null);
|
||||
|
||||
RuleFor(x => x.RecipientContains)
|
||||
.MaximumLength(320)
|
||||
.WithMessage("RecipientContains must not exceed 320 characters.")
|
||||
.When(x => x.RecipientContains is not null);
|
||||
|
||||
RuleFor(x => x.BodyContains)
|
||||
.MaximumLength(1000)
|
||||
.WithMessage("BodyContains must not exceed 1000 characters.")
|
||||
.When(x => x.BodyContains is not null);
|
||||
|
||||
RuleFor(x => x.Uid)
|
||||
.SetValidator(new UidFilterValidator()!)
|
||||
.When(x => x.Uid is not null);
|
||||
|
||||
RuleFor(x => x.Date)
|
||||
.SetValidator(new DateFilterValidator()!)
|
||||
.When(x => x.Date is not null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using DigitalData.MessagingService.Application.Common.Models.MailSearch;
|
||||
using FluentValidation;
|
||||
|
||||
namespace DigitalData.MessagingService.Application.EmailReceiving.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Validates a <see cref="UidFilter"/> value object.
|
||||
/// </summary>
|
||||
public class UidFilterValidator : AbstractValidator<UidFilter>
|
||||
{
|
||||
public UidFilterValidator()
|
||||
{
|
||||
// Absolute and range are mutually exclusive
|
||||
RuleFor(x => x)
|
||||
.Must(u => u.Absolute is null || (u.Min is null && u.Max is null))
|
||||
.WithName("Uid.Absolute")
|
||||
.WithMessage("Uid.Absolute cannot be combined with Uid.Min or Uid.Max. Use either an exact UID or a range.");
|
||||
|
||||
// At least one field must be provided
|
||||
RuleFor(x => x)
|
||||
.Must(u => u.Absolute is not null || u.Min is not null || u.Max is not null)
|
||||
.WithName("Uid")
|
||||
.WithMessage("UidFilter must specify at least one of: Absolute, Min, or Max.");
|
||||
|
||||
// All UID values must be positive
|
||||
RuleFor(x => x.Absolute)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("Uid.Absolute must be a positive number.")
|
||||
.When(x => x.Absolute is not null);
|
||||
|
||||
RuleFor(x => x.Min)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("Uid.Min must be a positive number.")
|
||||
.When(x => x.Min is not null);
|
||||
|
||||
RuleFor(x => x.Max)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("Uid.Max must be a positive number.")
|
||||
.When(x => x.Max is not null);
|
||||
|
||||
// Range coherence: Min must be less than or equal to Max
|
||||
RuleFor(x => x)
|
||||
.Must(u => u.Min!.Value <= u.Max!.Value)
|
||||
.WithName("Uid.Range")
|
||||
.WithMessage("Uid.Min must be less than or equal to Uid.Max.")
|
||||
.When(x => x.Min is not null && x.Max is not null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user