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,4 +1,5 @@
|
|||||||
using DigitalData.MessagingService.Abstraction;
|
using DigitalData.MessagingService.Abstraction;
|
||||||
|
using DigitalData.MessagingService.Application.Common.Models.MailSearch;
|
||||||
|
|
||||||
namespace DigitalData.MessagingService.Application.Common.Interfaces;
|
namespace DigitalData.MessagingService.Application.Common.Interfaces;
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ public interface IImapEmailService
|
|||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
Task<IEnumerable<ReceivedEmailContext>> FetchEmailsAsync(
|
Task<IEnumerable<ReceivedEmailContext>> FetchEmailsAsync(
|
||||||
EmailAccountDto account,
|
EmailAccountDto account,
|
||||||
FetchEmailsQuery.SearchFilter filter,
|
MailSearchFilter filter,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace DigitalData.MessagingService.Application.Common.Models.MailSearch;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constrains the search to messages within an arrival-date range.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// At least one of <see cref="After"/> or <see cref="Before"/> must be provided.
|
||||||
|
/// When both are set <see cref="After"/> must be earlier than <see cref="Before"/>.
|
||||||
|
/// Both bounds are <b>inclusive</b>.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="After">Earliest date to include (inclusive). Maps to IMAP <c>SINCE</c>.</param>
|
||||||
|
/// <param name="Before">Latest date to include (inclusive). Maps to IMAP <c>BEFORE</c> (next day is used internally).</param>
|
||||||
|
public record DateFilter(DateTime? After = null, DateTime? Before = null);
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
namespace DigitalData.MessagingService.Application.Common.Models.MailSearch;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Describes all criteria and options used when searching an IMAP mailbox.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// All text-match properties are <b>case-insensitive</b> substring searches performed server-side via IMAP SEARCH.
|
||||||
|
/// Combine multiple criteria freely; an implicit AND is applied across all non-null fields.
|
||||||
|
/// </remarks>
|
||||||
|
public record MailSearchFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Mailbox folder to search. Defaults to <c>"INBOX"</c>.
|
||||||
|
/// </summary>
|
||||||
|
public string Folder { get; init; } = "INBOX";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When <see langword="true"/>, only unread (UNSEEN) messages are returned.
|
||||||
|
/// </summary>
|
||||||
|
public bool UnseenOnly { get; init; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When <see langword="true"/>, only messages that carry at least one attachment are returned.
|
||||||
|
/// Filtered client-side after fetching the message envelope; does not affect the IMAP SEARCH query.
|
||||||
|
/// </summary>
|
||||||
|
public bool HasAttachments { get; init; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of messages to retrieve. <c>0</c> means unlimited.
|
||||||
|
/// Applied after sorting; defaults to <c>50</c>.
|
||||||
|
/// </summary>
|
||||||
|
public int MaxCount { get; init; } = 50;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls the order of the returned messages. Defaults to <see cref="MailSortOrder.NewestFirst"/>.
|
||||||
|
/// </summary>
|
||||||
|
public MailSortOrder SortOrder { get; init; } = MailSortOrder.NewestFirst;
|
||||||
|
|
||||||
|
// ── Text filters ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Case-insensitive substring the message subject must contain.
|
||||||
|
/// Maps to IMAP <c>SUBJECT</c>.
|
||||||
|
/// </summary>
|
||||||
|
public string? SubjectContains { get; init; } = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Case-insensitive substring that must appear in the <c>From</c> header.
|
||||||
|
/// Maps to IMAP <c>FROM</c>.
|
||||||
|
/// </summary>
|
||||||
|
public string? SenderContains { get; init; } = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Case-insensitive substring that must appear in <c>To</c> or <c>Cc</c>.
|
||||||
|
/// Maps to IMAP <c>TO</c>.
|
||||||
|
/// </summary>
|
||||||
|
public string? RecipientContains { get; init; } = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Case-insensitive substring that must appear anywhere in the message body (text or HTML part).
|
||||||
|
/// Maps to IMAP <c>BODY</c>.
|
||||||
|
/// </summary>
|
||||||
|
public string? BodyContains { get; init; } = null;
|
||||||
|
|
||||||
|
// ── Structured filters ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constrains results to a specific UID or a UID range.
|
||||||
|
/// See <see cref="UidFilter"/> for mutual-exclusion rules between its fields.
|
||||||
|
/// </summary>
|
||||||
|
public UidFilter? Uid { get; init; } = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constrains results to messages received within a date range.
|
||||||
|
/// See <see cref="DateFilter"/> for rules between its fields.
|
||||||
|
/// </summary>
|
||||||
|
public DateFilter? Date { get; init; } = null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace DigitalData.MessagingService.Application.Common.Models.MailSearch;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls the order in which fetched messages are returned.
|
||||||
|
/// </summary>
|
||||||
|
public enum MailSortOrder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Newest messages first (default).
|
||||||
|
/// </summary>
|
||||||
|
NewestFirst,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Oldest messages first.
|
||||||
|
/// </summary>
|
||||||
|
OldestFirst
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace DigitalData.MessagingService.Application.Common.Models.MailSearch;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constrains the search to a specific UID or a UID range.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Use <see cref="Absolute"/> for an exact single-message lookup.
|
||||||
|
/// Use <see cref="Min"/> and/or <see cref="Max"/> for an open or closed range.
|
||||||
|
/// Mixing <see cref="Absolute"/> with <see cref="Min"/> or <see cref="Max"/> is not allowed.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="Min">Lower bound of the UID range (inclusive). Ignored when <see cref="Absolute"/> is set.</param>
|
||||||
|
/// <param name="Max">Upper bound of the UID range (inclusive). Ignored when <see cref="Absolute"/> is set.</param>
|
||||||
|
/// <param name="Absolute">Exact UID to match. When set, <see cref="Min"/> and <see cref="Max"/> must be <see langword="null"/>.</param>
|
||||||
|
public record UidFilter(long? Min = null, long? Max = null, long? Absolute = null);
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using DigitalData.MessagingService.Application.Common.Interfaces;
|
|
||||||
using DigitalData.MessagingService.Application.EmailAccount.Queries;
|
|
||||||
using DigitalData.MessagingService.Abstraction;
|
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 DigitalData.MessagingService.Domain.Exceptions;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
@@ -19,25 +20,7 @@ public record FetchEmailsQuery : IRequest<IEnumerable<ReceivedEmailContext>>
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Mail query used to filter and limit the emails retrieved.
|
/// Mail query used to filter and limit the emails retrieved.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SearchFilter Mail { get; init; } = new();
|
public MailSearchFilter 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 class FetchEmailsQueryHandler(
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
using System.Text;
|
|
||||||
using DigitalData.MessagingService.Application.Common.Interfaces;
|
|
||||||
using DigitalData.MessagingService.Abstraction;
|
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.Domain.Exceptions;
|
||||||
using DigitalData.MessagingService.Infrastructure.Services.Extensions;
|
using DigitalData.MessagingService.Infrastructure.Services.Extensions;
|
||||||
using Limilabs.Client.IMAP;
|
using Limilabs.Client.IMAP;
|
||||||
using Limilabs.Mail;
|
using Limilabs.Mail;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace DigitalData.MessagingService.Infrastructure.Services;
|
namespace DigitalData.MessagingService.Infrastructure.Services;
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ public class LimilabsImapEmailService(
|
|||||||
// Public API
|
// Public API
|
||||||
public async Task<IEnumerable<ReceivedEmailContext>> FetchEmailsAsync(
|
public async Task<IEnumerable<ReceivedEmailContext>> FetchEmailsAsync(
|
||||||
EmailAccountDto account,
|
EmailAccountDto account,
|
||||||
FetchEmailsQuery.SearchFilter filter,
|
MailSearchFilter filter,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
using var imap = new Imap();
|
using var imap = new Imap();
|
||||||
@@ -34,17 +35,59 @@ public class LimilabsImapEmailService(
|
|||||||
await ConnectAndAuthenticateAsync(imap, account);
|
await ConnectAndAuthenticateAsync(imap, account);
|
||||||
await SelectFolderAsync(imap, filter.Folder);
|
await SelectFolderAsync(imap, filter.Folder);
|
||||||
|
|
||||||
// Get UIDs to fetch
|
List<ICriterion> criterions = [];
|
||||||
List<long> uids = filter.UnseenOnly
|
|
||||||
? [.. await imap.SearchAsync(Flag.Unseen, cancellationToken)]
|
|
||||||
: [.. await imap.GetAllAsync(cancellationToken)];
|
|
||||||
|
|
||||||
// Most-recent first; honour maxCount
|
if (filter.UnseenOnly)
|
||||||
uids.Reverse();
|
criterions.Add((ICriterion)Flag.Unseen);
|
||||||
if (filter.MaxCount > 0 && uids.Count() > filter.MaxCount)
|
|
||||||
|
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)];
|
uids = [.. uids.Take(filter.MaxCount)];
|
||||||
|
|
||||||
var results = new List<ReceivedEmailContext>(uids.Count());
|
var results = new List<ReceivedEmailContext>(uids.Count);
|
||||||
|
|
||||||
foreach (var uid in uids)
|
foreach (var uid in uids)
|
||||||
{
|
{
|
||||||
@@ -52,10 +95,14 @@ public class LimilabsImapEmailService(
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var eml = await imap.PeekMessageByUIDAsync(uid, cancellationToken);
|
var eml = await imap.PeekMessageByUIDAsync(uid, cancellationToken);
|
||||||
var mail = new MailBuilder().CreateFromEml(eml);
|
var mail = new MailBuilder().CreateFromEml(eml);
|
||||||
var flags = await imap.GetFlagsByUIDAsync(uid, cancellationToken);
|
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));
|
results.Add(MapToContext(uid, mail, flags));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
Reference in New Issue
Block a user