diff --git a/src/core/DigitalData.MessagingService.Abstraction/ReceivedEmailContext.cs b/src/core/DigitalData.MessagingService.Abstraction/ReceivedEmailContext.cs
index 022b3de..83be50f 100644
--- a/src/core/DigitalData.MessagingService.Abstraction/ReceivedEmailContext.cs
+++ b/src/core/DigitalData.MessagingService.Abstraction/ReceivedEmailContext.cs
@@ -3,7 +3,7 @@ namespace DigitalData.MessagingService.Abstraction;
///
/// Represents an email message received via IMAP.
///
-public sealed class ReceivedEmailContext
+public sealed record ReceivedEmailContext
{
///
/// Unique identifier of the message on the IMAP server (UID).
diff --git a/src/core/DigitalData.MessagingService.Application/Common/Options/EmailAccountsOptions.cs b/src/core/DigitalData.MessagingService.Application/Common/Options/EmailAccountsOptions.cs
index 763f430..97e451e 100644
--- a/src/core/DigitalData.MessagingService.Application/Common/Options/EmailAccountsOptions.cs
+++ b/src/core/DigitalData.MessagingService.Application/Common/Options/EmailAccountsOptions.cs
@@ -14,4 +14,10 @@ public class EmailAccountsOptions
/// The list of configured email accounts.
///
public required IEnumerable Accounts { get; init; } = [];
+
+ ///
+ /// How often the IMAP sync worker polls for new emails, in seconds.
+ /// Defaults to 300 seconds (5 minutes).
+ ///
+ public int SyncIntervalSeconds { get; init; } = 300;
}
diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/DependencyInjection.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/DependencyInjection.cs
index f60b874..5c18aad 100644
--- a/src/infrastructure/DigitalData.MessagingService.Infrastructure/DependencyInjection.cs
+++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/DependencyInjection.cs
@@ -50,6 +50,9 @@ public static class DependencyInjection
// Register Background Workers
services.AddHostedService();
+ services.AddHostedService();
+
+ services.AddMemoryCache();
return services;
}
diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/Background/EmailSyncWorker.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/Background/EmailSyncWorker.cs
new file mode 100644
index 0000000..b1e2430
--- /dev/null
+++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/Background/EmailSyncWorker.cs
@@ -0,0 +1,47 @@
+using DigitalData.MessagingService.Application.Common.Interfaces;
+using DigitalData.MessagingService.Application.Common.Models.MailSearch;
+using DigitalData.MessagingService.Application.Common.Options;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Options;
+
+namespace DigitalData.MessagingService.Infrastructure.Services.Background;
+
+public class EmailSyncWorker(IImapEmailService imapService, IOptions Options) : BackgroundService
+{
+ private DateFilter? _dateFilter = null;
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ if (imapService is not LimilabsImapEmailService limapService)
+ {
+ await Task.Delay(Timeout.Infinite, stoppingToken).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
+ return;
+ }
+
+ var interval = TimeSpan.FromSeconds(Options.Value.SyncIntervalSeconds);
+
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ foreach (var account in Options.Value.Accounts)
+ if (account.ImapServer is not null)
+ {
+ // init or update last date filter
+ _dateFilter = _dateFilter is null
+ ? new DateFilter
+ {
+ After = null,
+ Before = DateTime.UtcNow
+ }
+ : new DateFilter
+ {
+ After = _dateFilter.Before,
+ Before = DateTime.UtcNow
+ };
+
+ await limapService.FetchEmailsAsync(account, new MailSearchFilter { Date = _dateFilter }, stoppingToken);
+ }
+
+ await Task.Delay(interval, stoppingToken).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs
index 2a8ec86..4244b99 100644
--- a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs
+++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs
@@ -5,6 +5,7 @@ using DigitalData.MessagingService.Domain.Exceptions;
using DigitalData.MessagingService.Infrastructure.Services.Extensions;
using Limilabs.Client.IMAP;
using Limilabs.Mail;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using System.Text;
@@ -15,9 +16,12 @@ namespace DigitalData.MessagingService.Infrastructure.Services;
/// Opens a fresh connection per call — stateless and thread-safe.
///
public class LimilabsImapEmailService(
- IEncryptionService encryptionService,
- ILogger logger) : IImapEmailService
+ IEncryptionService EncryptionService,
+ ILogger Logger,
+ IMemoryCache Cache) : IImapEmailService
{
+ private static readonly string CacheKeyPrefix = Guid.NewGuid().ToString();
+
static LimilabsImapEmailService()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
@@ -52,7 +56,7 @@ public class LimilabsImapEmailService(
}
catch (Exception ex)
{
- logger.LogWarning(ex,
+ Logger.LogWarning(ex,
"Failed to fetch IMAP message UID={Uid} from folder {Folder}. Skipping.",
uid, filter.Folder);
}
@@ -132,7 +136,7 @@ public class LimilabsImapEmailService(
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);
+ Logger.LogWarning(ex, "Failed to fetch IMAP message UID={Uid} from folder {Folder}.", uid, folder);
return null;
}
}
@@ -226,56 +230,60 @@ public class LimilabsImapEmailService(
return uids;
}
- private static async Task FetchEmailByUidAsync(
+ private 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)
+ var email = await Cache.GetOrCreateAsync(
+ CacheKeyPrefix + uid,
+ async entry =>
{
- attachments.Add(new EmailAttachmentContext
- {
- FileName = att.FileName ?? "attachment",
- Content = att.Data,
- ContentType = att.ContentType?.ToString() ?? "application/octet-stream",
- IsInline = false,
- ContentId = att.ContentId
- });
- }
+ var eml = await imap.GetMessageByUIDAsync(uid, cancellationToken);
+ var mail = new MailBuilder().CreateFromEml(eml);
- 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
- });
- }
- }
+ var attachments = new List();
- 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,
- };
+ 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 ConnectAndAuthenticateAsync(Imap imap, EmailAccountDto account)
@@ -286,7 +294,7 @@ public class LimilabsImapEmailService(
await imap.ConnectAsync(account.ImapServer!, account.ImapPort);
var password = account.PasswordEncrypted
- ? encryptionService.Decrypt(account.Password)
+ ? EncryptionService.Decrypt(account.Password)
: account.Password;
await imap.LoginAsync(account.Username, password);