From 4964fa4344ddc9c16d688b92a85b3a1d450c61c0 Mon Sep 17 00:00:00 2001 From: TekH Date: Thu, 13 Aug 2026 14:18:43 +0200 Subject: [PATCH] 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`. 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. --- .../Common/Interfaces/IImapEmailService.cs | 9 +- .../Services/Background/EmailSyncWorker.cs | 2 +- .../Services/LimilabsImapEmailService.cs | 104 +++++------------- 3 files changed, 31 insertions(+), 84 deletions(-) diff --git a/src/core/DigitalData.MessagingService.Application/Common/Interfaces/IImapEmailService.cs b/src/core/DigitalData.MessagingService.Application/Common/Interfaces/IImapEmailService.cs index f73b4c4..fdf7cc8 100644 --- a/src/core/DigitalData.MessagingService.Application/Common/Interfaces/IImapEmailService.cs +++ b/src/core/DigitalData.MessagingService.Application/Common/Interfaces/IImapEmailService.cs @@ -14,15 +14,16 @@ public interface IImapEmailService /// Fetches emails from the specified mailbox folder. /// /// 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). /// /// Cancellation token. - Task> FetchEmailsAsync( + public Task SyncEmailsAsync( EmailAccount account, - MailSearchFilter filter, - CancellationToken cancellationToken = default); + DateFilter date, + string folder = "INBOX", + CancellationToken cancel = default); /// /// Marks a message as seen (read) on the server. diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/Background/EmailSyncWorker.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/Background/EmailSyncWorker.cs index 7222811..706381f 100644 --- a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/Background/EmailSyncWorker.cs +++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/Background/EmailSyncWorker.cs @@ -47,7 +47,7 @@ public class EmailSyncWorker(IImapEmailService imapService, IOptions -public class LimilabsImapEmailService(ILogger Logger) : IImapEmailService +public class LimilabsImapEmailService(ILogger Logger, IRepository Repository) : IImapEmailService { static LimilabsImapEmailService() { @@ -23,42 +25,40 @@ public class LimilabsImapEmailService(ILogger Logger) } // Public API - public async Task> FetchEmailsAsync( + public async Task SyncEmailsAsync( EmailAccount account, - MailSearchFilter filter, + DateFilter date, + string folder = "INBOX", CancellationToken cancel = default) { - using var imap = await OpenAsync(account, filter.Folder, cancel); + using var imap = await OpenAsync(account, folder, cancel); try { #region Find UIDs // Server-side: only date range; all other filters are applied in-process after cache retrieval List criterions = []; - if (filter.Date is DateFilter dateF) - { - if (dateF.After is DateTime after) - criterions.Add(Expression.SentSince(after.Date)); + if (date.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))); - } + // IMAP BEFORE is exclusive, so add one day to make the bound inclusive + if (date.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, cancel)]; - - if (filter.SortOrder == MailSortOrder.NewestFirst) - uids.Reverse(); #endregion if (uids.Count == 0) - return []; + return; - var results = new List(uids.Count); + var emails = new List(uids.Count); foreach (var uid in uids) { + if (await Repository.AnyAsync(x => x.Uid == uid, cancel)) + continue; + cancel.ThrowIfCancellationRequested(); try @@ -109,71 +109,24 @@ public class LimilabsImapEmailService(ILogger Logger) }; #endregion Read email - if (filter.UnseenOnly && email.IsSeen) - 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 = [] }); - + emails.Add(email); } catch (Exception ex) { Logger.LogWarning(ex, "Failed to fetch IMAP message UID={Uid} from folder {Folder}. Skipping.", - uid, filter.Folder); + uid, folder); } } await imap.CloseAsync(cancel); - if (filter.MaxCount is int maxCount && maxCount > 0 && results.Count > maxCount) - return results.Take(maxCount); - - return results; + await Repository.CreateAsync(emails, cancel); } - catch (Limilabs.Client.ServerException ex) + catch { 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(); - throw new InvalidOperationException( - $"Failed to fetch emails from IMAP server '{account.ImapServer}'.", ex); + throw; } } @@ -189,21 +142,14 @@ public class LimilabsImapEmailService(ILogger Logger) await imap.MarkMessageSeenByUIDAsync(uid, cancel); await imap.CloseAsync(cancel); } - catch (Limilabs.Client.ServerException ex) + catch { 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(); - throw new InvalidOperationException( - $"Failed to mark message UID={uid} as seen on '{account.ImapServer}'.", ex); + throw; } } - private static async Task OpenAsync(EmailAccount account, string folder, CancellationToken cancel) + private static async Task OpenAsync(EmailAccount account, string folder = "INBOX", CancellationToken cancel = default) { var imap = new Imap();