diff --git a/src/core/DigitalData.MessagingService.Application/Common/Interfaces/IImapEmailService.cs b/src/core/DigitalData.MessagingService.Application/Common/Interfaces/IImapEmailService.cs
index f5a97f1..34145aa 100644
--- a/src/core/DigitalData.MessagingService.Application/Common/Interfaces/IImapEmailService.cs
+++ b/src/core/DigitalData.MessagingService.Application/Common/Interfaces/IImapEmailService.cs
@@ -13,7 +13,6 @@ public interface IImapEmailService
///
/// Account whose IMAP settings will be used.
/// Filter to apply when fetching emails.
- ///
/// When (default), fetched messages are marked as \Seen on the server.
/// Set to for a non-destructive read (uses BODY.PEEK internally).
///
@@ -21,7 +20,6 @@ public interface IImapEmailService
Task> FetchEmailsAsync(
EmailAccountDto account,
MailSearchFilter filter,
- bool markAsSeen = true,
CancellationToken cancellationToken = default);
///
@@ -35,7 +33,6 @@ public interface IImapEmailService
///
/// Fetches a single email by its UID.
///
- ///
/// When (default), the message is marked as \Seen on the server.
/// Set to for a non-destructive read.
///
@@ -44,7 +41,6 @@ public interface IImapEmailService
long uid,
string folder = "INBOX",
bool withAttachments = false,
- bool markAsSeen = true,
CancellationToken cancellationToken = default);
///
diff --git a/src/core/DigitalData.MessagingService.Application/Common/Models/MailSearch/MailSearchFilter.cs b/src/core/DigitalData.MessagingService.Application/Common/Models/MailSearch/MailSearchFilter.cs
index f0fc2f3..8d00d20 100644
--- a/src/core/DigitalData.MessagingService.Application/Common/Models/MailSearch/MailSearchFilter.cs
+++ b/src/core/DigitalData.MessagingService.Application/Common/Models/MailSearch/MailSearchFilter.cs
@@ -29,7 +29,7 @@ public record MailSearchFilter
/// Maximum number of messages to retrieve. 0 means unlimited.
/// Applied after sorting; defaults to 50.
///
- public int MaxCount { get; init; } = 50;
+ public int? MaxCount { get; init; } = null;
///
/// Controls the order of the returned messages. Defaults to .
diff --git a/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/FetchEmailByUidQuery.cs b/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/FetchEmailByUidQuery.cs
index 37c4fc2..9597666 100644
--- a/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/FetchEmailByUidQuery.cs
+++ b/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/FetchEmailByUidQuery.cs
@@ -36,11 +36,6 @@ public record FetchEmailByUidQuery : IRequest
/// When , attachment data is included in the result.
///
public bool WithAttachments { get; init; } = false;
- ///
- /// When (default), the message is marked as \Seen on the server.
- /// Set to for a non-destructive peek.
- ///
- public bool MarkAsSeen { get; init; } = true;
}
public class FetchEmailByUidQueryHandler(
@@ -62,7 +57,6 @@ public class FetchEmailByUidQueryHandler(
(long)request.Uid!,
request.Folder,
request.WithAttachments,
- request.MarkAsSeen,
cancellationToken);
}
}
diff --git a/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/FetchEmailsQuery.cs b/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/FetchEmailsQuery.cs
index 7074d8f..9b1a6bb 100644
--- a/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/FetchEmailsQuery.cs
+++ b/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/FetchEmailsQuery.cs
@@ -21,12 +21,6 @@ public record FetchEmailsQuery : IRequest>
/// Mail query used to filter and limit the emails retrieved.
///
public MailSearchFilter Mail { get; init; } = new();
-
- ///
- /// When (default), fetched messages are marked as \Seen on the server.
- /// Set to for a non-destructive peek.
- ///
- public bool MarkAsSeen { get; init; } = true;
}
public class FetchEmailsQueryHandler(
@@ -46,7 +40,6 @@ public class FetchEmailsQueryHandler(
return await ImapService.FetchEmailsAsync(
account,
request.Mail,
- request.MarkAsSeen,
cancellationToken);
}
}
diff --git a/src/core/DigitalData.MessagingService.Application/EmailReceiving/Validators/FetchEmailByUidQueryValidator.cs b/src/core/DigitalData.MessagingService.Application/EmailReceiving/Validators/FetchEmailByUidQueryValidator.cs
deleted file mode 100644
index c9ecfe8..0000000
--- a/src/core/DigitalData.MessagingService.Application/EmailReceiving/Validators/FetchEmailByUidQueryValidator.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using DigitalData.MessagingService.Application.EmailReceiving.Queries;
-using FluentValidation;
-
-namespace DigitalData.MessagingService.Application.EmailReceiving.Validators;
-
-///
-/// Validates a before it is handled by .
-///
-public class FetchEmailByUidQueryValidator : AbstractValidator
-{
- public FetchEmailByUidQueryValidator()
- {
- RuleFor(x => x.Account)
- .NotNull()
- .WithMessage("Account query must not be null.");
-
- RuleFor(x => x.Uid)
- .NotNull()
- .WithMessage("UID must be provided. Use WithUid() to set the UID before dispatching the query.")
- .GreaterThan(0)
- .WithMessage("UID must be greater than 0.");
-
- RuleFor(x => x.Folder)
- .NotEmpty()
- .WithMessage("Folder must not be empty.");
- }
-}
diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs
index c74fa21..2a8ec86 100644
--- a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs
+++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs
@@ -27,74 +27,16 @@ public class LimilabsImapEmailService(
public async Task> FetchEmailsAsync(
EmailAccountDto account,
MailSearchFilter filter,
- bool markAsSeen = true,
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));
-
- // 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 > 0 && uids.Count > filter.MaxCount)
- uids = [.. uids.Take(filter.MaxCount)];
+ var uids = await FetchEmailUidsAsync(imap, account, filter, cancellationToken);
if (uids.Count == 0)
return [];
- // Fetch flags for all UIDs in one round-trip
- var allFlags = await imap.GetFlagsByUIDAsync(uids, cancellationToken);
- var flagsById = allFlags
- .Where(f => f.UID.HasValue)
- .ToDictionary(f => f.UID!.Value, f => f.Flags);
-
- // markAsSeen=true → BODY[] (server sets \Seen automatically)
- // markAsSeen=false → BODY.PEEK[] (\Seen untouched, non-destructive read)
var results = new List(uids.Count);
foreach (var uid in uids)
@@ -103,12 +45,10 @@ public class LimilabsImapEmailService(
try
{
- var eml = markAsSeen
- ? await imap.GetMessageByUIDAsync(uid, cancellationToken)
- : await imap.PeekMessageByUIDAsync(uid, cancellationToken);
- var mail = new MailBuilder().CreateFromEml(eml);
- flagsById.TryGetValue(uid, out var flags);
- results.Add(MapToContext(uid, mail, flags, filter.WithAttachments));
+ 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);
}
catch (Exception ex)
{
@@ -146,52 +86,7 @@ public class LimilabsImapEmailService(
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)];
+ List uids = await FetchEmailUidsAsync(imap, account, filter, cancellationToken);
await imap.CloseAsync(cancellationToken);
return uids;
@@ -215,7 +110,6 @@ public class LimilabsImapEmailService(
long uid,
string folder = "INBOX",
bool withAttachments = false,
- bool markAsSeen = true,
CancellationToken cancellationToken = default)
{
using var imap = new Imap();
@@ -224,28 +118,24 @@ public class LimilabsImapEmailService(
await ConnectAndAuthenticateAsync(imap, account);
await SelectFolderAsync(imap, folder);
- var eml = markAsSeen
- ? await imap.GetMessageByUIDAsync(uid, cancellationToken)
- : await imap.PeekMessageByUIDAsync(uid, cancellationToken);
- var mail = new MailBuilder().CreateFromEml(eml);
- var flags = await imap.GetFlagsByUIDAsync(uid, cancellationToken);
+ var mail = await FetchEmailByUidAsync(imap, uid, withAttachments, cancellationToken);
await imap.CloseAsync(cancellationToken);
- return MapToContext(uid, mail, flags, withAttachments);
+ return mail;
}
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,
@@ -276,6 +166,118 @@ public class LimilabsImapEmailService(
}
// Private helpers
+ private async Task> FetchEmailUidsAsync(
+ Imap imap,
+ EmailAccountDto account,
+ MailSearchFilter filter,
+ CancellationToken cancellationToken = default)
+ {
+ 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));
+
+ // 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 static async Task FetchEmailByUidAsync(
+ Imap imap,
+ long uid,
+ bool withAttachments = false,
+ CancellationToken cancellationToken = default)
+ {
+ var eml = await imap.GetMessageByUIDAsync(uid, cancellationToken);
+ var mail = new MailBuilder().CreateFromEml(eml);
+
+ var attachments = new List();
+
+ if (withAttachments)
+ {
+ 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,
+ };
+ }
+
private async Task ConnectAndAuthenticateAsync(Imap imap, EmailAccountDto account)
{
if (account.ImapUseSsl)
@@ -297,50 +299,4 @@ public class LimilabsImapEmailService(
else
await imap.SelectAsync(folder);
}
-
- private static ReceivedEmailContext MapToContext(long uid, IMail mail, List? flags, bool withAttachments = false)
- {
- var attachments = new List();
-
- if (withAttachments)
- {
- 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,
- IsSeen = flags?.Contains(Flag.Seen) ?? false
- };
- }
}