Refactor IMAP service for repository integration
Replaced `FetchEmailsAsync` with `SyncEmailsAsync` in `IImapEmailService` to simplify email synchronization. Updated method signatures to use `DateFilter` and added default parameters for `folder` and `cancel`. Refactored `LimilabsImapEmailService` to remove client-side filtering logic and delegate storage to `IRepository<ReceivedEmail>`. Simplified IMAP search criteria to focus on date-based filtering and added checks to skip already-synced emails. Updated `EmailSyncWorker` to use the new `SyncEmailsAsync` method. Removed unused code, streamlined exception handling, and improved maintainability by reducing complexity and focusing on server-side filtering.
This commit is contained in:
@@ -14,15 +14,16 @@ public interface IImapEmailService
|
|||||||
/// Fetches emails from the specified mailbox folder.
|
/// Fetches emails from the specified mailbox folder.
|
||||||
/// </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="date"></param>
|
||||||
/// When <see langword="true"/> (default), fetched messages are marked as <c>\Seen</c> on the server.
|
/// When <see langword="true"/> (default), fetched messages are marked as <c>\Seen</c> on the server.
|
||||||
/// Set to <see langword="false"/> for a non-destructive read (uses <c>BODY.PEEK</c> internally).
|
/// Set to <see langword="false"/> for a non-destructive read (uses <c>BODY.PEEK</c> internally).
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
Task<IEnumerable<ReceivedEmailDto>> FetchEmailsAsync(
|
public Task SyncEmailsAsync(
|
||||||
EmailAccount account,
|
EmailAccount account,
|
||||||
MailSearchFilter filter,
|
DateFilter date,
|
||||||
CancellationToken cancellationToken = default);
|
string folder = "INBOX",
|
||||||
|
CancellationToken cancel = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Marks a message as seen (read) on the server.
|
/// Marks a message as seen (read) on the server.
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public class EmailSyncWorker(IImapEmailService imapService, IOptions<EmailAccoun
|
|||||||
Before = DateTime.UtcNow
|
Before = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
await limapService.FetchEmailsAsync(account, new MailSearchFilter { Date = _dateFilter }, stoppingToken);
|
await limapService.SyncEmailsAsync(account, _dateFilter, cancel: stoppingToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.Delay(interval, stoppingToken).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
|
await Task.Delay(interval, stoppingToken).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
using DigitalData.MessagingService.Application.Common.Dto;
|
using DigitalData.MessagingService.Application.Common.Dto;
|
||||||
using DigitalData.MessagingService.Application.Common.Dto.MailSearch;
|
using DigitalData.MessagingService.Application.Common.Dto.MailSearch;
|
||||||
using DigitalData.MessagingService.Application.Common.Interfaces;
|
using DigitalData.MessagingService.Application.Common.Interfaces;
|
||||||
|
using DigitalData.MessagingService.Application.Common.Interfaces.Repositories;
|
||||||
using DigitalData.MessagingService.Domain.Entities;
|
using DigitalData.MessagingService.Domain.Entities;
|
||||||
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.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
@@ -15,7 +17,7 @@ namespace DigitalData.MessagingService.Infrastructure.Services;
|
|||||||
/// IMAP email service using Limilabs Mail.dll.
|
/// IMAP email service using Limilabs Mail.dll.
|
||||||
/// Opens a fresh connection per call — stateless and thread-safe.
|
/// Opens a fresh connection per call — stateless and thread-safe.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LimilabsImapEmailService(ILogger<LimilabsImapEmailService> Logger) : IImapEmailService
|
public class LimilabsImapEmailService(ILogger<LimilabsImapEmailService> Logger, IRepository<ReceivedEmail> Repository) : IImapEmailService
|
||||||
{
|
{
|
||||||
static LimilabsImapEmailService()
|
static LimilabsImapEmailService()
|
||||||
{
|
{
|
||||||
@@ -23,42 +25,40 @@ public class LimilabsImapEmailService(ILogger<LimilabsImapEmailService> Logger)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Public API
|
// Public API
|
||||||
public async Task<IEnumerable<ReceivedEmailDto>> FetchEmailsAsync(
|
public async Task SyncEmailsAsync(
|
||||||
EmailAccount account,
|
EmailAccount account,
|
||||||
MailSearchFilter filter,
|
DateFilter date,
|
||||||
|
string folder = "INBOX",
|
||||||
CancellationToken cancel = default)
|
CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
using var imap = await OpenAsync(account, filter.Folder, cancel);
|
using var imap = await OpenAsync(account, folder, cancel);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
#region Find UIDs
|
#region Find UIDs
|
||||||
// Server-side: only date range; all other filters are applied in-process after cache retrieval
|
// Server-side: only date range; all other filters are applied in-process after cache retrieval
|
||||||
List<ICriterion> criterions = [];
|
List<ICriterion> criterions = [];
|
||||||
|
|
||||||
if (filter.Date is DateFilter dateF)
|
if (date.After is DateTime after)
|
||||||
{
|
criterions.Add(Expression.SentSince(after.Date));
|
||||||
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
|
// IMAP BEFORE is exclusive, so add one day to make the bound inclusive
|
||||||
if (dateF.Before is DateTime before)
|
if (date.Before is DateTime before)
|
||||||
criterions.Add(Expression.SentBefore(before.Date.AddDays(1)));
|
criterions.Add(Expression.SentBefore(before.Date.AddDays(1)));
|
||||||
}
|
|
||||||
|
|
||||||
var searchExpression = criterions.Count > 0 ? Expression.And([.. criterions]) : Expression.All();
|
var searchExpression = criterions.Count > 0 ? Expression.And([.. criterions]) : Expression.All();
|
||||||
List<long> uids = [.. await imap.SearchAsync(searchExpression, cancel)];
|
List<long> uids = [.. await imap.SearchAsync(searchExpression, cancel)];
|
||||||
|
|
||||||
if (filter.SortOrder == MailSortOrder.NewestFirst)
|
|
||||||
uids.Reverse();
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
if (uids.Count == 0)
|
if (uids.Count == 0)
|
||||||
return [];
|
return;
|
||||||
|
|
||||||
var results = new List<ReceivedEmailDto>(uids.Count);
|
var emails = new List<ReceivedEmailDto>(uids.Count);
|
||||||
|
|
||||||
foreach (var uid in uids)
|
foreach (var uid in uids)
|
||||||
{
|
{
|
||||||
|
if (await Repository.AnyAsync(x => x.Uid == uid, cancel))
|
||||||
|
continue;
|
||||||
|
|
||||||
cancel.ThrowIfCancellationRequested();
|
cancel.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -109,71 +109,24 @@ public class LimilabsImapEmailService(ILogger<LimilabsImapEmailService> Logger)
|
|||||||
};
|
};
|
||||||
#endregion Read email
|
#endregion Read email
|
||||||
|
|
||||||
if (filter.UnseenOnly && email.IsSeen)
|
emails.Add(email);
|
||||||
continue;
|
|
||||||
|
|
||||||
if (filter.SubjectContains is string subject &&
|
|
||||||
!email.Subject.Contains(subject, StringComparison.OrdinalIgnoreCase))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (filter.SenderContains is string sender &&
|
|
||||||
!email.From.Contains(sender, StringComparison.OrdinalIgnoreCase))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (filter.RecipientContains is string recipient &&
|
|
||||||
!email.To.Any(t => t.Contains(recipient, StringComparison.OrdinalIgnoreCase)) &&
|
|
||||||
!email.Cc.Any(c => c.Contains(recipient, StringComparison.OrdinalIgnoreCase)))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (filter.BodyContains is string body &&
|
|
||||||
!email.TextBody.Contains(body, StringComparison.OrdinalIgnoreCase) &&
|
|
||||||
!email.HtmlBody.Contains(body, StringComparison.OrdinalIgnoreCase))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (filter.Uid is UidFilter uidF)
|
|
||||||
{
|
|
||||||
if (uidF.Absolute is long exactUid && email.Uid != exactUid)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (uidF.Min is long min && email.Uid < min)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (uidF.Max is long max && email.Uid > max)
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.WithAttachments)
|
|
||||||
results.Add(email);
|
|
||||||
else
|
|
||||||
results.Add(email with { Attachments = [] });
|
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogWarning(ex,
|
Logger.LogWarning(ex,
|
||||||
"Failed to fetch IMAP message UID={Uid} from folder {Folder}. Skipping.",
|
"Failed to fetch IMAP message UID={Uid} from folder {Folder}. Skipping.",
|
||||||
uid, filter.Folder);
|
uid, folder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await imap.CloseAsync(cancel);
|
await imap.CloseAsync(cancel);
|
||||||
|
|
||||||
if (filter.MaxCount is int maxCount && maxCount > 0 && results.Count > maxCount)
|
await Repository.CreateAsync(emails, cancel);
|
||||||
return results.Take(maxCount);
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
}
|
||||||
catch (Limilabs.Client.ServerException ex)
|
catch
|
||||||
{
|
{
|
||||||
await imap.CloseSafelyAsync();
|
await imap.CloseSafelyAsync();
|
||||||
throw new AuthenticationFailedException(
|
throw;
|
||||||
$"IMAP authentication failed for account '{account.Username}'.", ex);
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
|
||||||
{
|
|
||||||
await imap.CloseSafelyAsync();
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Failed to fetch emails from IMAP server '{account.ImapServer}'.", ex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,21 +142,14 @@ public class LimilabsImapEmailService(ILogger<LimilabsImapEmailService> Logger)
|
|||||||
await imap.MarkMessageSeenByUIDAsync(uid, cancel);
|
await imap.MarkMessageSeenByUIDAsync(uid, cancel);
|
||||||
await imap.CloseAsync(cancel);
|
await imap.CloseAsync(cancel);
|
||||||
}
|
}
|
||||||
catch (Limilabs.Client.ServerException ex)
|
catch
|
||||||
{
|
{
|
||||||
await imap.CloseSafelyAsync();
|
await imap.CloseSafelyAsync();
|
||||||
throw new AuthenticationFailedException(
|
throw;
|
||||||
$"IMAP authentication failed for account '{account.Username}'.", ex);
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
|
||||||
{
|
|
||||||
await imap.CloseSafelyAsync();
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Failed to mark message UID={uid} as seen on '{account.ImapServer}'.", ex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<Imap> OpenAsync(EmailAccount account, string folder, CancellationToken cancel)
|
private static async Task<Imap> OpenAsync(EmailAccount account, string folder = "INBOX", CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
var imap = new Imap();
|
var imap = new Imap();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user