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:
@@ -35,4 +35,20 @@ public class EmailAccountDto
|
|||||||
public bool SmtpUseSsl { get; set; }
|
public bool SmtpUseSsl { get; set; }
|
||||||
|
|
||||||
public bool UseOAuth2 { get; set; }
|
public bool UseOAuth2 { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IMAP server hostname (e.g. "imap.example.com").
|
||||||
|
/// Leave empty when this account is send-only.
|
||||||
|
/// </summary>
|
||||||
|
public string? ImapServer { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IMAP server port (993 for SSL, 143 for plain/STARTTLS).
|
||||||
|
/// </summary>
|
||||||
|
public int ImapPort { get; set; } = 993;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Use SSL/TLS when connecting to the IMAP server.
|
||||||
|
/// </summary>
|
||||||
|
public bool ImapUseSsl { get; set; } = true;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
namespace DigitalData.MessagingService.Abstraction;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an email message received via IMAP.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ReceivedEmailContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Unique identifier of the message on the IMAP server (UID).
|
||||||
|
/// </summary>
|
||||||
|
#if NET
|
||||||
|
public long Uid { get; init; }
|
||||||
|
#else
|
||||||
|
public long Uid { get; set; }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sender address (From header).
|
||||||
|
/// </summary>
|
||||||
|
#if NET
|
||||||
|
public string From { get; init; } = string.Empty;
|
||||||
|
#else
|
||||||
|
public string From { get; set; } = string.Empty;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recipient addresses (To header).
|
||||||
|
/// </summary>
|
||||||
|
#if NET
|
||||||
|
public IEnumerable<string> To { get; init; } = [];
|
||||||
|
#else
|
||||||
|
public IEnumerable<string> To { get; set; } = [];
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CC addresses.
|
||||||
|
/// </summary>
|
||||||
|
#if NET
|
||||||
|
public IEnumerable<string> Cc { get; init; } = [];
|
||||||
|
#else
|
||||||
|
public IEnumerable<string> Cc { get; set; } = [];
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Email subject.
|
||||||
|
/// </summary>
|
||||||
|
#if NET
|
||||||
|
public string Subject { get; init; } = string.Empty;
|
||||||
|
#else
|
||||||
|
public string Subject { get; set; } = string.Empty;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Plain-text body (may be empty when only HTML is present).
|
||||||
|
/// </summary>
|
||||||
|
#if NET
|
||||||
|
public string TextBody { get; init; } = string.Empty;
|
||||||
|
#else
|
||||||
|
public string TextBody { get; set; } = string.Empty;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HTML body (may be empty when only plain-text is present).
|
||||||
|
/// </summary>
|
||||||
|
#if NET
|
||||||
|
public string HtmlBody { get; init; } = string.Empty;
|
||||||
|
#else
|
||||||
|
public string HtmlBody { get; set; } = string.Empty;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Date/time the message was sent (Date header).
|
||||||
|
/// </summary>
|
||||||
|
#if NET
|
||||||
|
public DateTime Date { get; init; }
|
||||||
|
#else
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attachments included with this message.
|
||||||
|
/// </summary>
|
||||||
|
#if NET
|
||||||
|
public IEnumerable<EmailAttachmentContext> Attachments { get; init; } = [];
|
||||||
|
#else
|
||||||
|
public IEnumerable<EmailAttachmentContext> Attachments { get; set; } = [];
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the message has been marked as seen/read on the server.
|
||||||
|
/// </summary>
|
||||||
|
#if NET
|
||||||
|
public bool IsSeen { get; init; }
|
||||||
|
#else
|
||||||
|
public bool IsSeen { get; set; }
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,9 +23,12 @@ public static class DependencyInjection
|
|||||||
IConfiguration configuration)
|
IConfiguration configuration)
|
||||||
{
|
{
|
||||||
// --- External Services ---
|
// --- External Services ---
|
||||||
// Email Service (using Limilabs Mail.dll - Singleton for use in EmailSenderWorker)
|
// Email Service - SMTP outbound (Limilabs Mail.dll)
|
||||||
services.AddSingleton<IEmailService, LimilabsEmailService>();
|
services.AddSingleton<IEmailService, LimilabsEmailService>();
|
||||||
|
|
||||||
|
// Email Service - IMAP inbound (Limilabs Mail.dll)
|
||||||
|
services.AddSingleton<IImapEmailService, LimilabsImapEmailService>();
|
||||||
|
|
||||||
// PDF Processing Service (using DevExpress.Pdf)
|
// PDF Processing Service (using DevExpress.Pdf)
|
||||||
services.AddScoped<IPdfProcessingService, DevExpressPdfProcessingService>();
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
using DigitalData.MessagingService.Application.EmailSending.Commands;
|
using DigitalData.MessagingService.Application.EmailSending.Commands;
|
||||||
|
using DigitalData.MessagingService.Application.EmailReceiving.Queries;
|
||||||
|
using DigitalData.MessagingService.Application.EmailReceiving.Commands;
|
||||||
|
using DigitalData.MessagingService.Application.EmailAccount.Queries;
|
||||||
using DigitalData.MessagingService.Abstraction;
|
using DigitalData.MessagingService.Abstraction;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@@ -13,6 +16,7 @@ namespace DigitalData.MessagingService.API.Controllers;
|
|||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class EmailsController(IMediator mediator) : ControllerBase
|
public class EmailsController(IMediator mediator) : ControllerBase
|
||||||
{
|
{
|
||||||
|
#region Send
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send an email, optionally with file attachments.
|
/// Send an email, optionally with file attachments.
|
||||||
/// Omit the <c>attachments</c> field for a plain send.
|
/// Omit the <c>attachments</c> field for a plain send.
|
||||||
@@ -61,4 +65,68 @@ public class EmailsController(IMediator mediator) : ControllerBase
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
#endregion Send
|
||||||
|
|
||||||
|
#region Receive
|
||||||
|
/// <summary>
|
||||||
|
/// Fetch emails from an IMAP mailbox.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="accountId">Id of the email account (must have ImapServer configured).</param>
|
||||||
|
/// <param name="folder">Mailbox folder to read (default: INBOX).</param>
|
||||||
|
/// <param name="unseenOnly">Return only unread messages.</param>
|
||||||
|
/// <param name="maxCount">Maximum number of messages to return (most-recent first, default: 50).</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>HTTP 200 with list of received emails.</returns>
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<IActionResult> FetchEmails(
|
||||||
|
[FromQuery] int accountId,
|
||||||
|
[FromQuery] string folder = "INBOX",
|
||||||
|
[FromQuery] bool unseenOnly = false,
|
||||||
|
[FromQuery] int maxCount = 50,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var query = new FetchEmailsQuery
|
||||||
|
{
|
||||||
|
Account = new GetSenderQuery { Id = accountId },
|
||||||
|
Folder = folder,
|
||||||
|
UnseenOnly = unseenOnly,
|
||||||
|
MaxCount = maxCount
|
||||||
|
};
|
||||||
|
|
||||||
|
var emails = await mediator.Send(query, cancellationToken);
|
||||||
|
return Ok(emails);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mark a single IMAP message as seen (read).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="accountId">Id of the email account.</param>
|
||||||
|
/// <param name="uid">UID of the message on the IMAP server.</param>
|
||||||
|
/// <param name="folder">Mailbox folder the message resides in (default: INBOX).</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>HTTP 204 No Content.</returns>
|
||||||
|
[HttpPatch("{uid}/seen")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<IActionResult> MarkAsSeen(
|
||||||
|
[FromRoute] long uid,
|
||||||
|
[FromQuery] int accountId,
|
||||||
|
[FromQuery] string folder = "INBOX",
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var command = new MarkEmailAsSeenCommand
|
||||||
|
{
|
||||||
|
Account = new GetSenderQuery { Id = accountId },
|
||||||
|
Uid = uid,
|
||||||
|
Folder = folder
|
||||||
|
};
|
||||||
|
|
||||||
|
await mediator.Send(command, cancellationToken);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
#endregion Receive
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user