Add IMAP support for email fetching and marking as seen
Introduced IMAP functionality to enable fetching emails from an IMAP server and marking messages as seen. Updated the `EmailAccountDto` class with IMAP-related properties (`ImapServer`, `ImapPort`, `ImapUseSsl`) for configuration. Added `ReceivedEmailContext` to represent received emails and created the `IImapEmailService` interface with methods `FetchEmailsAsync` and `MarkAsSeenAsync`. Implemented the `LimilabsImapEmailService` class using Limilabs Mail.dll for IMAP operations, including connection handling, email fetching, and marking messages as seen. Added `FetchEmailsQuery` and `MarkEmailAsSeenCommand` with handlers to encapsulate IMAP logic. Updated `EmailsController` with new endpoints for fetching emails and marking messages as seen. Registered `IImapEmailService` in `DependencyInjection`. Included exception handling and logging for robust error management during IMAP operations.
This commit is contained in:
@@ -23,8 +23,11 @@ public static class DependencyInjection
|
||||
IConfiguration configuration)
|
||||
{
|
||||
// --- External Services ---
|
||||
// Email Service (using Limilabs Mail.dll - Singleton for use in EmailSenderWorker)
|
||||
// Email Service - SMTP outbound (Limilabs Mail.dll)
|
||||
services.AddSingleton<IEmailService, LimilabsEmailService>();
|
||||
|
||||
// Email Service - IMAP inbound (Limilabs Mail.dll)
|
||||
services.AddSingleton<IImapEmailService, LimilabsImapEmailService>();
|
||||
|
||||
// PDF Processing Service (using DevExpress.Pdf)
|
||||
services.AddScoped<IPdfProcessingService, DevExpressPdfProcessingService>();
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using Limilabs.Client.IMAP;
|
||||
|
||||
namespace DigitalData.MessagingService.Infrastructure.Services.Extensions;
|
||||
|
||||
public static class ImapExtensions
|
||||
{
|
||||
public static async Task CloseSafelyAsync(this Imap imap)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (imap.Connected)
|
||||
await imap.CloseAsync();
|
||||
}
|
||||
catch { /* Ignore disconnect errors */ }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
using System.Text;
|
||||
using DigitalData.MessagingService.Application.Common.Interfaces;
|
||||
using DigitalData.MessagingService.Abstraction;
|
||||
using DigitalData.MessagingService.Domain.Exceptions;
|
||||
using DigitalData.MessagingService.Infrastructure.Services.Extensions;
|
||||
using Limilabs.Client.IMAP;
|
||||
using Limilabs.Mail;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DigitalData.MessagingService.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// IMAP email service using Limilabs Mail.dll.
|
||||
/// Opens a fresh connection per call — stateless and thread-safe.
|
||||
/// </summary>
|
||||
public class LimilabsImapEmailService(
|
||||
IEncryptionService encryptionService,
|
||||
ILogger<LimilabsImapEmailService> logger) : IImapEmailService
|
||||
{
|
||||
static LimilabsImapEmailService()
|
||||
{
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
}
|
||||
|
||||
// Public API
|
||||
public async Task<IEnumerable<ReceivedEmailContext>> FetchEmailsAsync(
|
||||
EmailAccountDto account,
|
||||
string folder = "INBOX",
|
||||
bool unseenOnly = false,
|
||||
int maxCount = 50,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var imap = new Imap();
|
||||
try
|
||||
{
|
||||
await ConnectAndAuthenticateAsync(imap, account);
|
||||
await SelectFolderAsync(imap, folder);
|
||||
|
||||
// Get UIDs to fetch
|
||||
List<long> uids = unseenOnly
|
||||
? [.. await imap.SearchAsync(Flag.Unseen, cancellationToken)]
|
||||
: [.. await imap.GetAllAsync(cancellationToken)];
|
||||
|
||||
// Most-recent first; honour maxCount
|
||||
uids.Reverse();
|
||||
if (maxCount > 0 && uids.Count > maxCount)
|
||||
uids = uids.Take(maxCount).ToList();
|
||||
|
||||
var results = new List<ReceivedEmailContext>(uids.Count);
|
||||
|
||||
foreach (var uid in uids)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
var eml = await imap.PeekMessageByUIDAsync(uid, cancellationToken);
|
||||
var mail = new MailBuilder().CreateFromEml(eml);
|
||||
var flags = await imap.GetFlagsByUIDAsync(uid, cancellationToken);
|
||||
|
||||
results.Add(MapToContext(uid, mail, flags));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to fetch IMAP message UID={Uid} from folder {Folder}. Skipping.", uid, folder);
|
||||
}
|
||||
}
|
||||
|
||||
await imap.CloseAsync(cancellationToken);
|
||||
return results;
|
||||
}
|
||||
catch (Limilabs.Client.ServerException ex)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task MarkAsSeenAsync(
|
||||
EmailAccountDto account,
|
||||
long uid,
|
||||
string folder = "INBOX",
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var imap = new Imap();
|
||||
try
|
||||
{
|
||||
await ConnectAndAuthenticateAsync(imap, account);
|
||||
await SelectFolderAsync(imap, folder);
|
||||
await imap.MarkMessageSeenByUIDAsync(uid, cancellationToken);
|
||||
await imap.CloseAsync(cancellationToken);
|
||||
}
|
||||
catch (Limilabs.Client.ServerException ex)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Private helpers
|
||||
private async Task ConnectAndAuthenticateAsync(Imap imap, EmailAccountDto account)
|
||||
{
|
||||
if (account.ImapUseSsl)
|
||||
await imap.ConnectSSLAsync(account.ImapServer!, account.ImapPort);
|
||||
else
|
||||
await imap.ConnectAsync(account.ImapServer!, account.ImapPort);
|
||||
|
||||
var password = account.PasswordEncrypted
|
||||
? encryptionService.Decrypt(account.Password)
|
||||
: account.Password;
|
||||
|
||||
await imap.LoginAsync(account.Username, password);
|
||||
}
|
||||
|
||||
private static async Task SelectFolderAsync(Imap imap, string folder)
|
||||
{
|
||||
if (string.Equals(folder, "INBOX", StringComparison.OrdinalIgnoreCase))
|
||||
await imap.SelectInboxAsync();
|
||||
else
|
||||
await imap.SelectAsync(folder);
|
||||
}
|
||||
|
||||
private static ReceivedEmailContext MapToContext(long uid, IMail mail, List<Flag> flags)
|
||||
{
|
||||
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,
|
||||
IsSeen = flags?.Contains(Flag.Seen) ?? false
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user