Add support for optional email attachments fetching

Introduced a `withAttachments` parameter to the `FetchEmailsAsync` method in `IImapEmailService` and related layers, allowing callers to include or exclude attachment data when fetching emails. Updated `FetchEmailsQuery` and `FetchEmailsQueryHandler` to propagate this parameter.

Refactored `LimilabsImapEmailService` to conditionally process attachments and inline visuals based on the `withAttachments` flag, improving performance when attachments are not required. Replaced `Flag.Unseen` with `Expression.HasFlag(Flag.Unseen)` for better criteria handling. Cleaned up attachment-processing logic for improved readability and maintainability.
This commit is contained in:
2026-08-10 11:54:35 +02:00
parent 8e2e9af451
commit 763ba67d34
3 changed files with 34 additions and 21 deletions

View File

@@ -13,10 +13,12 @@ public interface IImapEmailService
/// </summary> /// </summary>
/// <param name="account">Account whose IMAP settings will be used.</param> /// <param name="account">Account whose IMAP settings will be used.</param>
/// <param name="filter">Filter to apply when fetching emails.</param> /// <param name="filter">Filter to apply when fetching emails.</param>
/// <param name="withAttachments">When <see langword="true"/>, attachment data is included in the results; otherwise attachments are omitted. Defaults to <see langword="false"/>.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
Task<IEnumerable<ReceivedEmailContext>> FetchEmailsAsync( Task<IEnumerable<ReceivedEmailContext>> FetchEmailsAsync(
EmailAccountDto account, EmailAccountDto account,
MailSearchFilter filter, MailSearchFilter filter,
bool withAttachments = false,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
/// <summary> /// <summary>

View File

@@ -21,6 +21,12 @@ public record FetchEmailsQuery : IRequest<IEnumerable<ReceivedEmailContext>>
/// Mail query used to filter and limit the emails retrieved. /// Mail query used to filter and limit the emails retrieved.
/// </summary> /// </summary>
public MailSearchFilter Mail { get; init; } = new(); public MailSearchFilter Mail { get; init; } = new();
/// <summary>
/// When <see langword="true"/>, attachment data is included in the results; otherwise attachments are omitted.
/// Defaults to <see langword="false"/>.
/// </summary>
public bool WithAttachments { get; init; } = false;
} }
public class FetchEmailsQueryHandler( public class FetchEmailsQueryHandler(
@@ -40,6 +46,7 @@ public class FetchEmailsQueryHandler(
return await ImapService.FetchEmailsAsync( return await ImapService.FetchEmailsAsync(
account, account,
request.Mail, request.Mail,
request.WithAttachments,
cancellationToken); cancellationToken);
} }
} }

View File

@@ -27,6 +27,7 @@ public class LimilabsImapEmailService(
public async Task<IEnumerable<ReceivedEmailContext>> FetchEmailsAsync( public async Task<IEnumerable<ReceivedEmailContext>> FetchEmailsAsync(
EmailAccountDto account, EmailAccountDto account,
MailSearchFilter filter, MailSearchFilter filter,
bool withAttachments = false,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
using var imap = new Imap(); using var imap = new Imap();
@@ -38,7 +39,7 @@ public class LimilabsImapEmailService(
List<ICriterion> criterions = []; List<ICriterion> criterions = [];
if (filter.UnseenOnly) if (filter.UnseenOnly)
criterions.Add((ICriterion)Flag.Unseen); criterions.Add(Expression.HasFlag(Flag.Unseen));
if (filter.SubjectContains is not null) if (filter.SubjectContains is not null)
criterions.Add(Expression.Subject(filter.SubjectContains)); criterions.Add(Expression.Subject(filter.SubjectContains));
@@ -103,7 +104,7 @@ public class LimilabsImapEmailService(
if (filter.HasAttachments && mail.Attachments.Count == 0 && mail.Visuals.Count == 0) if (filter.HasAttachments && mail.Attachments.Count == 0 && mail.Visuals.Count == 0)
continue; continue;
results.Add(MapToContext(uid, mail, flags)); results.Add(MapToContext(uid, mail, flags, withAttachments));
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -179,32 +180,35 @@ public class LimilabsImapEmailService(
await imap.SelectAsync(folder); await imap.SelectAsync(folder);
} }
private static ReceivedEmailContext MapToContext(long uid, IMail mail, List<Flag> flags) private static ReceivedEmailContext MapToContext(long uid, IMail mail, List<Flag> flags, bool withAttachments = false)
{ {
var attachments = new List<EmailAttachmentContext>(); var attachments = new List<EmailAttachmentContext>();
foreach (var att in mail.Attachments) if (withAttachments)
{ {
attachments.Add(new EmailAttachmentContext foreach (var att in mail.Attachments)
{ {
FileName = att.FileName ?? "attachment", attachments.Add(new EmailAttachmentContext
Content = att.Data, {
ContentType = att.ContentType?.ToString() ?? "application/octet-stream", FileName = att.FileName ?? "attachment",
IsInline = false, Content = att.Data,
ContentId = att.ContentId ContentType = att.ContentType?.ToString() ?? "application/octet-stream",
}); IsInline = false,
} ContentId = att.ContentId
});
}
foreach (var vis in mail.Visuals) foreach (var vis in mail.Visuals)
{
attachments.Add(new EmailAttachmentContext
{ {
FileName = vis.FileName ?? "inline", attachments.Add(new EmailAttachmentContext
Content = vis.Data, {
ContentType = vis.ContentType?.ToString() ?? "application/octet-stream", FileName = vis.FileName ?? "inline",
IsInline = true, Content = vis.Data,
ContentId = vis.ContentId ContentType = vis.ContentType?.ToString() ?? "application/octet-stream",
}); IsInline = true,
ContentId = vis.ContentId
});
}
} }
return new ReceivedEmailContext return new ReceivedEmailContext