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:
2026-08-07 11:48:12 +02:00
parent 20de7da93d
commit c47d78112c
9 changed files with 511 additions and 1 deletions

View File

@@ -0,0 +1,33 @@
using DigitalData.MessagingService.Abstraction;
namespace DigitalData.MessagingService.Application.Common.Interfaces;
/// <summary>
/// Service interface for reading emails via IMAP.
/// </summary>
public interface IImapEmailService
{
/// <summary>
/// Fetches emails from the specified mailbox folder.
/// </summary>
/// <param name="account">Account whose IMAP settings will be used.</param>
/// <param name="folder">Mailbox folder name (e.g. "INBOX"). Defaults to INBOX.</param>
/// <param name="unseenOnly">When <see langword="true"/> returns only unread messages.</param>
/// <param name="maxCount">Maximum number of messages to retrieve (most-recent first). 0 = unlimited.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task<IEnumerable<ReceivedEmailContext>> FetchEmailsAsync(
EmailAccountDto account,
string folder = "INBOX",
bool unseenOnly = false,
int maxCount = 50,
CancellationToken cancellationToken = default);
/// <summary>
/// Marks a message as seen (read) on the server.
/// </summary>
Task MarkAsSeenAsync(
EmailAccountDto account,
long uid,
string folder = "INBOX",
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,42 @@
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Application.EmailAccount.Queries;
using DigitalData.MessagingService.Domain.Exceptions;
using MediatR;
namespace DigitalData.MessagingService.Application.EmailReceiving.Commands;
/// <summary>
/// Command to mark a single IMAP message as seen (read).
/// </summary>
public record MarkEmailAsSeenCommand : IRequest
{
public required GetSenderQuery Account { get; init; }
/// <summary>
/// UID of the message to mark as seen.
/// </summary>
public required long Uid { get; init; }
/// <summary>
/// Mailbox folder the message resides in (default: "INBOX").
/// </summary>
public string Folder { get; init; } = "INBOX";
}
public class MarkEmailAsSeenCommandHandler(
ISender Sender,
IImapEmailService ImapService) : IRequestHandler<MarkEmailAsSeenCommand>
{
public async Task Handle(MarkEmailAsSeenCommand request, CancellationToken cancellationToken)
{
var account = await Sender.Send(request.Account, cancellationToken)
?? throw new NotFoundException(
$"No email account found for the given criteria (Id: {request.Account.Id}, Username: {request.Account.Username}).");
if (string.IsNullOrWhiteSpace(account.ImapServer))
throw new InvalidOperationException(
$"IMAP is not configured for account '{account.Username}' (Id: {account.Id}). Set ImapServer in EmailAccounts configuration.");
await ImapService.MarkAsSeenAsync(account, request.Uid, request.Folder, cancellationToken);
}
}

View File

@@ -0,0 +1,56 @@
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Application.EmailAccount.Queries;
using DigitalData.MessagingService.Abstraction;
using DigitalData.MessagingService.Domain.Exceptions;
using MediatR;
namespace DigitalData.MessagingService.Application.EmailReceiving.Queries;
/// <summary>
/// Query to fetch emails from an IMAP mailbox.
/// </summary>
public record FetchEmailsQuery : IRequest<IEnumerable<ReceivedEmailContext>>
{
/// <summary>
/// Identifies the email account to use.
/// </summary>
public required GetSenderQuery Account { get; init; }
/// <summary>
/// Mailbox folder to read from (default: "INBOX").
/// </summary>
public string Folder { get; init; } = "INBOX";
/// <summary>
/// When <see langword="true"/> returns only unread messages.
/// </summary>
public bool UnseenOnly { get; init; } = false;
/// <summary>
/// Maximum number of messages to retrieve (most-recent first). 0 = unlimited.
/// </summary>
public int MaxCount { get; init; } = 50;
}
public class FetchEmailsQueryHandler(
ISender Sender,
IImapEmailService ImapService) : IRequestHandler<FetchEmailsQuery, IEnumerable<ReceivedEmailContext>>
{
public async Task<IEnumerable<ReceivedEmailContext>> Handle(FetchEmailsQuery request, CancellationToken cancellationToken)
{
var account = await Sender.Send(request.Account, cancellationToken)
?? throw new NotFoundException(
$"No email account found for the given criteria (Id: {request.Account.Id}, Username: {request.Account.Username}).");
if (string.IsNullOrWhiteSpace(account.ImapServer))
throw new InvalidOperationException(
$"IMAP is not configured for account '{account.Username}' (Id: {account.Id}). Set ImapServer in EmailAccounts configuration.");
return await ImapService.FetchEmailsAsync(
account,
request.Folder,
request.UnseenOnly,
request.MaxCount,
cancellationToken);
}
}