Refactor LimilabsImapEmailService for clarity and efficiency

Refactored the `LimilabsImapEmailService` class to improve email fetching and processing. Consolidated logic by inlining and removing redundant private helper methods (`FetchEmailUidsAsync` and `FetchEmailByUidAsync`). Introduced structured `#region` blocks for UID fetching and email reading.

Enhanced filtering capabilities with support for unseen emails, subject, sender, recipient, body, UID ranges, and date ranges. Improved attachment handling by categorizing inline and regular attachments into `EmailAttachmentContext`. Added conditional attachment inclusion based on `filter.WithAttachments`.

Integrated caching (`Cache.GetOrCreateAsync`) to avoid redundant email fetches. Improved error handling and logging for better fault tolerance. Overall, the changes simplify the codebase, improve readability, and enhance functionality.
This commit is contained in:
2026-08-12 13:01:49 +02:00
parent 7b5596f3e5
commit f6ada2ad9e

View File

@@ -36,7 +36,55 @@ public class LimilabsImapEmailService(
using var imap = await OpenAsync(account, filter.Folder);
try
{
var uids = await FetchEmailUidsAsync(imap, filter, cancellationToken);
#region Find UIDs
List<ICriterion> 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<long> 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)];
#endregion
if (uids.Count == 0)
return [];
@@ -49,10 +97,64 @@ public class LimilabsImapEmailService(
try
{
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);
#region Read email
var email = await Cache.GetOrCreateAsync(
CacheKeyPrefix + uid,
async entry =>
{
var eml = await imap.GetMessageByUIDAsync(uid, cancellationToken);
var mail = new MailBuilder().CreateFromEml(eml);
var attachments = new List<EmailAttachmentContext>();
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,
};
});
#endregion Read email
if(email is not null)
{
if (filter.WithAttachments)
results.Add(email);
else
results.Add(email with { Attachments = [] });
}
}
catch (Exception ex)
{
@@ -105,119 +207,6 @@ public class LimilabsImapEmailService(
}
}
// Private helpers
private static async Task<List<long>> FetchEmailUidsAsync(
Imap imap,
MailSearchFilter filter,
CancellationToken cancellationToken = default)
{
List<ICriterion> 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<long> 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 async Task<ReceivedEmailContext?> FetchEmailByUidAsync(
Imap imap,
long uid,
bool withAttachments = false,
CancellationToken cancellationToken = default)
{
var email = await Cache.GetOrCreateAsync(
CacheKeyPrefix + uid,
async entry =>
{
var eml = await imap.GetMessageByUIDAsync(uid, cancellationToken);
var mail = new MailBuilder().CreateFromEml(eml);
var attachments = new List<EmailAttachmentContext>();
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,
};
});
return withAttachments || email is null ? email : email with { Attachments = [] };
}
private async Task<Imap> OpenAsync(EmailAccountDto account, string folder)
{
var imap = new Imap();