Refactor email fetching and handling logic
Consolidated email fetching queries into a unified `ReadEmailQuery` to support multiple protocols (IMAP, IMAP OAuth2, POP3) dynamically. Removed redundant query handlers (`ReadEmailViaImapQuery`, `ReadEmailViaOAuth2Query`, `ReadEmailViaPop3Query`) and merged their functionality into `ReadEmailQueryHandler` with protocol-specific methods. Updated `FetchEmailsQueryValidator` to validate the new query. Refactored `EmailController` to use the unified query, simplifying endpoints and consolidating functionality. Removed obsolete endpoints for POP3 and OAuth2 fetching. Performed code cleanup, including removing redundant imports and consolidating attachment handling logic. Updated project version to `2.0.0-beta` to reflect these significant changes. Improved documentation to align with the new structure.
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
#if NET
|
||||
using AutoMapper;
|
||||
using DigitalData.MessagingService.Application.Common.Dto;
|
||||
using DigitalData.MessagingService.Application.Common.Dto.MailSearch;
|
||||
using DigitalData.MessagingService.Application.Common.Interfaces;
|
||||
using DigitalData.MessagingService.Application.Common.Interfaces.Repositories;
|
||||
using DigitalData.MessagingService.Application.EmailAccounts.Queries;
|
||||
using DigitalData.MessagingService.Domain.Entities;
|
||||
using DigitalData.MessagingService.Domain.Enums;
|
||||
using DigitalData.MessagingService.Domain.Exceptions;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DigitalData.MessagingService.Application.EmailReceiving.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Query to fetch emails from an email account.
|
||||
/// The protocol and authentication method are determined by <see cref="EmailAccount.IncomingProtocol"/>.
|
||||
/// </summary>
|
||||
public record ReadEmailQuery : IRequest<ReadEmailQueryResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies the email account to use.
|
||||
/// </summary>
|
||||
public required GetEmailAccountQuery Account { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Mail query used to filter and limit the emails retrieved.
|
||||
/// </summary>
|
||||
public MailSearchFilter Mail { get; init; } = new();
|
||||
}
|
||||
|
||||
public class ReadEmailQueryHandler(
|
||||
IMapper Mapper,
|
||||
ILogger<ReadEmailQueryHandler> Logger,
|
||||
IRepository<EmailAccount> EmailAccountRepo,
|
||||
IReceivedEmailRepository MailRepo,
|
||||
IImapEmailService ImapEmailService,
|
||||
IPop3EmailService Pop3EmailService) : IRequestHandler<ReadEmailQuery, ReadEmailQueryResponse>
|
||||
{
|
||||
public async Task<ReadEmailQueryResponse> Handle(ReadEmailQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var accounts = await EmailAccountRepo.FindAsync(
|
||||
request.Account.Id is int id ? x => x.Id == id : x => x.Username == request.Account.Username,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
if (accounts.Count() > 1)
|
||||
Logger.LogWarning("Multiple email accounts found for the given criteria ({Criteria}). Returning the first one.",
|
||||
request.Account.Id is not null ? $"Id: {request.Account.Id}" : $"Username: {request.Account.Username}");
|
||||
|
||||
var account = accounts.FirstOrDefault()
|
||||
?? throw new NotFoundException($"No email account found for the given criteria (Id: {request.Account.Id}, Username: {request.Account.Username}).");
|
||||
|
||||
return account.IncomingProtocol switch
|
||||
{
|
||||
IncomingProtocol.Imap => await HandleImapAsync(account, request.Mail, cancellationToken),
|
||||
IncomingProtocol.ImapOAuth2 => await HandleImapOAuth2Async(account, request.Mail, cancellationToken),
|
||||
IncomingProtocol.Pop3 => await HandlePop3Async(account, request.Mail, cancellationToken),
|
||||
IncomingProtocol.Pop3OAuth2 => throw new NotImplementedException(
|
||||
$"POP3 OAuth2 is not yet supported. Account: '{account.Username}' (Id: {account.Id})."),
|
||||
IncomingProtocol.None => throw new BadRequestException(
|
||||
$"Account '{account.Username}' (Id: {account.Id}) has no incoming protocol configured (IncomingProtocol = None)."),
|
||||
_ => throw new BadRequestException(
|
||||
$"Unknown IncomingProtocol value '{account.IncomingProtocol}' for account '{account.Username}' (Id: {account.Id}).")
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<ReadEmailQueryResponse> HandleImapAsync(EmailAccount account, MailSearchFilter filter, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(account.ImapServer))
|
||||
throw new BadRequestException(
|
||||
$"IMAP is not configured for account '{account.Username}' (Id: {account.Id}). Set ImapServer in EmailAccounts configuration.");
|
||||
|
||||
var mails = await MailRepo.FindAsync(filter, account, cancellationToken);
|
||||
var lastSync = ImapEmailService.GetLastImapSyncDate(account.Id, filter.Folder);
|
||||
|
||||
return new ReadEmailQueryResponse
|
||||
{
|
||||
LastSync = lastSync,
|
||||
Emails = Mapper.Map<IEnumerable<ReceivedEmailDto>>(mails)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<ReadEmailQueryResponse> HandleImapOAuth2Async(EmailAccount account, MailSearchFilter filter, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(account.OAuth2ClientId) || string.IsNullOrWhiteSpace(account.OAuth2ClientSecret))
|
||||
throw new BadRequestException(
|
||||
$"OAuth2 credentials (ClientId, ClientSecret) are not configured for account '{account.Username}' (Id: {account.Id}).");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(account.ImapServer))
|
||||
throw new BadRequestException(
|
||||
$"IMAP is not configured for account '{account.Username}' (Id: {account.Id}). Set ImapServer in EmailAccounts configuration.");
|
||||
|
||||
var mails = await MailRepo.FindAsync(filter, account, cancellationToken);
|
||||
var lastSync = ImapEmailService.GetLastImapSyncDate(account.Id, filter.Folder);
|
||||
|
||||
return new ReadEmailQueryResponse
|
||||
{
|
||||
LastSync = lastSync,
|
||||
Emails = Mapper.Map<IEnumerable<ReceivedEmailDto>>(mails)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<ReadEmailQueryResponse> HandlePop3Async(EmailAccount account, MailSearchFilter filter, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(account.Pop3Server))
|
||||
throw new BadRequestException(
|
||||
$"POP3 is not configured for account '{account.Username}' (Id: {account.Id}). Set Pop3Server in EmailAccounts configuration.");
|
||||
|
||||
// Trigger on-demand POP3 sync before querying local storage
|
||||
await Pop3EmailService.SyncEmailsAsync(account, cancellationToken);
|
||||
|
||||
// POP3 has no folder concept — always query INBOX
|
||||
var pop3Filter = filter with { Folder = "INBOX" };
|
||||
var mails = await MailRepo.FindAsync(pop3Filter, account, cancellationToken);
|
||||
var lastSync = Pop3EmailService.GetLastPop3SyncDate(account.Id);
|
||||
|
||||
return new ReadEmailQueryResponse
|
||||
{
|
||||
LastSync = lastSync,
|
||||
Emails = Mapper.Map<IEnumerable<ReceivedEmailDto>>(mails)
|
||||
};
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,58 +0,0 @@
|
||||
#if NET
|
||||
using AutoMapper;
|
||||
using DigitalData.MessagingService.Application.Common.Dto;
|
||||
using DigitalData.MessagingService.Application.Common.Dto.MailSearch;
|
||||
using DigitalData.MessagingService.Application.Common.Interfaces;
|
||||
using DigitalData.MessagingService.Application.Common.Interfaces.Repositories;
|
||||
using DigitalData.MessagingService.Application.EmailAccounts.Queries;
|
||||
using DigitalData.MessagingService.Domain.Entities;
|
||||
using DigitalData.MessagingService.Domain.Exceptions;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DigitalData.MessagingService.Application.EmailReceiving.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Query to fetch emails from an IMAP mailbox.
|
||||
/// </summary>
|
||||
public record ReadEmailViaImapQuery : IRequest<ReadEmailQueryResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies the email account to use.
|
||||
/// </summary>
|
||||
public required GetEmailAccountQuery Account { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Mail query used to filter and limit the emails retrieved.
|
||||
/// </summary>
|
||||
public MailSearchFilter Mail { get; init; } = new();
|
||||
}
|
||||
|
||||
public class ReadEmailViaImapQueryHandler(IMapper Mapper, ILogger<ReadEmailViaImapQueryHandler> Logger, IRepository<EmailAccount> EmailAccountRepo, IReceivedEmailRepository MailRepo, IImapEmailService imapEmailService) : IRequestHandler<ReadEmailViaImapQuery, ReadEmailQueryResponse>
|
||||
{
|
||||
public async Task<ReadEmailQueryResponse> Handle(ReadEmailViaImapQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var accounts = await EmailAccountRepo.FindAsync(request.Account.Id is int id ? x => x.Id == id : x => x.Username == request.Account.Username, cancellationToken: cancellationToken);
|
||||
|
||||
if (accounts.Count() > 1)
|
||||
Logger.LogWarning("Multiple email accounts found for the given criteria ({Criteria}). Returning the first one.", request.Account.Id is not null ? $"Id: {request.Account.Id}" : $"Username: {request.Account.Username}");
|
||||
|
||||
var account = accounts.FirstOrDefault()
|
||||
?? 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 BadRequestException(
|
||||
$"IMAP is not configured for account '{account.Username}' (Id: {account.Id}). Set ImapServer in EmailAccounts configuration.");
|
||||
|
||||
var mails = await MailRepo.FindAsync(request.Mail, account, cancellationToken);
|
||||
|
||||
var lastSync = imapEmailService.GetLastImapSyncDate(account.Id, request.Mail.Folder);
|
||||
|
||||
return new ReadEmailQueryResponse
|
||||
{
|
||||
LastSync = lastSync,
|
||||
Emails = Mapper.Map<IEnumerable<ReceivedEmailDto>>(mails)
|
||||
};
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,77 +0,0 @@
|
||||
#if NET
|
||||
using AutoMapper;
|
||||
using DigitalData.MessagingService.Application.Common.Dto;
|
||||
using DigitalData.MessagingService.Application.Common.Dto.MailSearch;
|
||||
using DigitalData.MessagingService.Application.Common.Interfaces;
|
||||
using DigitalData.MessagingService.Application.Common.Interfaces.Repositories;
|
||||
using DigitalData.MessagingService.Application.EmailAccounts.Queries;
|
||||
using DigitalData.MessagingService.Application.EmailReceiving.Queries;
|
||||
using DigitalData.MessagingService.Domain.Entities;
|
||||
using DigitalData.MessagingService.Domain.Exceptions;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DigitalData.MessagingService.Application.EmailReceiving.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Query to fetch emails from an IMAP mailbox using OAuth2 authentication.
|
||||
/// The account must have <c>UseOAuth2 = true</c> and valid OAuth2 credentials configured.
|
||||
/// </summary>
|
||||
public record ReadEmailViaOAuth2Query : IRequest<ReadEmailQueryResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies the email account to use.
|
||||
/// </summary>
|
||||
public required GetEmailAccountQuery Account { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Mail query used to filter and limit the emails retrieved.
|
||||
/// </summary>
|
||||
public MailSearchFilter Mail { get; init; } = new();
|
||||
}
|
||||
|
||||
public class ReadEmailViaOAuth2QueryHandler(
|
||||
IMapper Mapper,
|
||||
ILogger<ReadEmailViaOAuth2QueryHandler> Logger,
|
||||
IRepository<EmailAccount> EmailAccountRepo,
|
||||
IReceivedEmailRepository MailRepo,
|
||||
IImapEmailService imapEmailService) : IRequestHandler<ReadEmailViaOAuth2Query, ReadEmailQueryResponse>
|
||||
{
|
||||
public async Task<ReadEmailQueryResponse> Handle(ReadEmailViaOAuth2Query request, CancellationToken cancellationToken)
|
||||
{
|
||||
var accounts = await EmailAccountRepo.FindAsync(
|
||||
request.Account.Id is int id ? x => x.Id == id : x => x.Username == request.Account.Username,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
if (accounts.Count() > 1)
|
||||
Logger.LogWarning("Multiple email accounts found ({Criteria}). Using first.",
|
||||
request.Account.Id is not null ? $"Id: {request.Account.Id}" : $"Username: {request.Account.Username}");
|
||||
|
||||
var account = accounts.FirstOrDefault()
|
||||
?? throw new NotFoundException($"No email account found (Id: {request.Account.Id}, Username: {request.Account.Username}).");
|
||||
|
||||
if (!account.UseOAuth2)
|
||||
throw new BadRequestException(
|
||||
$"Account '{account.Username}' (Id: {account.Id}) is not configured for OAuth2. Set UseOAuth2 = true.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(account.OAuth2ClientId) ||
|
||||
string.IsNullOrWhiteSpace(account.OAuth2ClientSecret))
|
||||
throw new BadRequestException(
|
||||
$"OAuth2 credentials (ClientId, ClientSecret) are not configured for account '{account.Username}'.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(account.ImapServer))
|
||||
throw new BadRequestException(
|
||||
$"IMAP is not configured for account '{account.Username}' (Id: {account.Id}). Set ImapServer in EmailAccounts configuration.");
|
||||
|
||||
var mails = await MailRepo.FindAsync(request.Mail, account, cancellationToken);
|
||||
|
||||
var lastSync = imapEmailService.GetLastImapSyncDate(account.Id, request.Mail.Folder);
|
||||
|
||||
return new ReadEmailQueryResponse
|
||||
{
|
||||
LastSync = lastSync,
|
||||
Emails = Mapper.Map<IEnumerable<ReceivedEmailDto>>(mails)
|
||||
};
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,74 +0,0 @@
|
||||
#if NET
|
||||
using AutoMapper;
|
||||
using DigitalData.MessagingService.Application.Common.Dto;
|
||||
using DigitalData.MessagingService.Application.Common.Dto.MailSearch;
|
||||
using DigitalData.MessagingService.Application.Common.Interfaces;
|
||||
using DigitalData.MessagingService.Application.Common.Interfaces.Repositories;
|
||||
using DigitalData.MessagingService.Application.EmailAccounts.Queries;
|
||||
using DigitalData.MessagingService.Application.EmailReceiving.Queries;
|
||||
using DigitalData.MessagingService.Domain.Entities;
|
||||
using DigitalData.MessagingService.Domain.Exceptions;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DigitalData.MessagingService.Application.EmailReceiving.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Query to fetch emails from a POP3 mailbox.
|
||||
/// Triggers an on-demand sync and returns stored results filtered by <see cref="Mail"/>.
|
||||
/// </summary>
|
||||
public record ReadEmailViaPop3Query : IRequest<ReadEmailQueryResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies the email account to use.
|
||||
/// </summary>
|
||||
public required GetEmailAccountQuery Account { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Mail query used to filter and limit the emails retrieved from local storage.
|
||||
/// Note: POP3 has no folder concept — all messages are stored under "INBOX".
|
||||
/// </summary>
|
||||
public MailSearchFilter Mail { get; init; } = new();
|
||||
}
|
||||
|
||||
public class ReadEmailViaPop3QueryHandler(
|
||||
IMapper Mapper,
|
||||
ILogger<ReadEmailViaPop3QueryHandler> Logger,
|
||||
IRepository<EmailAccount> EmailAccountRepo,
|
||||
IReceivedEmailRepository MailRepo,
|
||||
IPop3EmailService pop3EmailService) : IRequestHandler<ReadEmailViaPop3Query, ReadEmailQueryResponse>
|
||||
{
|
||||
public async Task<ReadEmailQueryResponse> Handle(ReadEmailViaPop3Query request, CancellationToken cancellationToken)
|
||||
{
|
||||
var accounts = await EmailAccountRepo.FindAsync(
|
||||
request.Account.Id is int id ? x => x.Id == id : x => x.Username == request.Account.Username,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
if (accounts.Count() > 1)
|
||||
Logger.LogWarning("Multiple email accounts found ({Criteria}). Using first.",
|
||||
request.Account.Id is not null ? $"Id: {request.Account.Id}" : $"Username: {request.Account.Username}");
|
||||
|
||||
var account = accounts.FirstOrDefault()
|
||||
?? throw new NotFoundException($"No email account found (Id: {request.Account.Id}, Username: {request.Account.Username}).");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(account.Pop3Server))
|
||||
throw new BadRequestException(
|
||||
$"POP3 is not configured for account '{account.Username}' (Id: {account.Id}). Set Pop3Server in EmailAccounts configuration.");
|
||||
|
||||
// Trigger on-demand POP3 sync before querying local storage
|
||||
await pop3EmailService.SyncEmailsAsync(account, cancellationToken);
|
||||
|
||||
// POP3 has no folder concept — always query INBOX
|
||||
var filter = request.Mail with { Folder = "INBOX" };
|
||||
var mails = await MailRepo.FindAsync(filter, account, cancellationToken);
|
||||
|
||||
var lastSync = pop3EmailService.GetLastPop3SyncDate(account.Id);
|
||||
|
||||
return new ReadEmailQueryResponse
|
||||
{
|
||||
LastSync = lastSync,
|
||||
Emails = Mapper.Map<IEnumerable<ReceivedEmailDto>>(mails)
|
||||
};
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -5,9 +5,9 @@ using FluentValidation;
|
||||
namespace DigitalData.MessagingService.Application.EmailReceiving.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Validates a <see cref="ReadEmailViaImapQuery"/> before it is handled by <see cref="ReadEmailViaImapQueryHandler"/>.
|
||||
/// Validates a <see cref="ReadEmailQuery"/> before it is handled by <see cref="ReadEmailQueryHandler"/>.
|
||||
/// </summary>
|
||||
public class FetchEmailsQueryValidator : AbstractValidator<ReadEmailViaImapQuery>
|
||||
public class FetchEmailsQueryValidator : AbstractValidator<ReadEmailQuery>
|
||||
{
|
||||
public FetchEmailsQueryValidator()
|
||||
{
|
||||
|
||||
@@ -1,37 +1,71 @@
|
||||
using DigitalData.MessagingService.Application.EmailSending.Commands;
|
||||
using DigitalData.MessagingService.Application.Common.Dto;
|
||||
using DigitalData.MessagingService.Application.EmailReceiving.Queries;
|
||||
using DigitalData.MessagingService.Application.EmailSending.Commands;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using DigitalData.MessagingService.Application.Common.Dto;
|
||||
|
||||
namespace DigitalData.MessagingService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Email sending API controller.
|
||||
/// Enqueues outgoing emails to RabbitMQ for async processing.
|
||||
/// Email API controller.
|
||||
/// Handles sending and receiving emails across all supported protocols and auth methods.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class EmailController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// Controls which field(s) to return from a fetch-emails response.
|
||||
/// </summary>
|
||||
public enum OnlyFilter
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <summary>Returns only the HTML body of the first matching email.</summary>
|
||||
HtmlBody,
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <summary>Returns only the UID list of matching emails.</summary>
|
||||
Uid,
|
||||
}
|
||||
|
||||
#region Receive
|
||||
/// <summary>
|
||||
/// Fetch emails from an email account.
|
||||
/// The protocol (IMAP, POP3) and authentication method (basic, OAuth2) are determined
|
||||
/// by the <c>IncomingProtocol</c> field configured on the account.
|
||||
/// </summary>
|
||||
/// <param name="query">Query parameters for filtering and fetching emails.</param>
|
||||
/// <param name="only">Optional filter to return only specific fields.</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] ReadEmailQuery query,
|
||||
[FromQuery] OnlyFilter? only = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var res = await mediator.Send(query, cancellationToken);
|
||||
|
||||
if (!res.Emails.Any())
|
||||
return NotFound("No emails found matching the specified criteria.");
|
||||
|
||||
if (only == OnlyFilter.HtmlBody)
|
||||
{
|
||||
if (res.Emails.FirstOrDefault()?.HtmlBody is string htmlBody)
|
||||
return Content(htmlBody, "text/html");
|
||||
else
|
||||
return NotFound();
|
||||
}
|
||||
else if (only == OnlyFilter.Uid)
|
||||
return Ok(res.Emails.Select(e => e.Uid).ToList());
|
||||
else
|
||||
return Ok(res);
|
||||
}
|
||||
#endregion Receive
|
||||
|
||||
#region SMTP Send
|
||||
/// <summary>
|
||||
/// Send an email, optionally with file attachments.
|
||||
/// Send an email via SMTP, optionally with file attachments.
|
||||
/// Omit the <c>attachments</c> field for a plain send.
|
||||
/// </summary>
|
||||
/// <param name="command">Email fields as form values</param>
|
||||
@@ -53,66 +87,8 @@ public class EmailController(IMediator mediator) : ControllerBase
|
||||
var eventId = await mediator.Send(commandWithAttachments, cancellationToken);
|
||||
return Accepted(new { Id = eventId });
|
||||
}
|
||||
|
||||
private static async Task<IEnumerable<EmailAttachmentDto>> BuildAttachmentsAsync(
|
||||
IFormFileCollection? files,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (files is null || files.Count == 0)
|
||||
return [];
|
||||
|
||||
var result = new List<EmailAttachmentDto>(files.Count);
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await file.CopyToAsync(ms, cancellationToken);
|
||||
|
||||
result.Add(new EmailAttachmentDto
|
||||
{
|
||||
FileName = file.FileName,
|
||||
Content = ms.ToArray(),
|
||||
ContentType = file.ContentType
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
#endregion SMTP Send
|
||||
|
||||
#region IMAP Receive
|
||||
/// <summary>
|
||||
/// Fetch emails from an IMAP mailbox.
|
||||
/// </summary>
|
||||
/// <param name="query">Query parameters for filtering and fetching emails from the IMAP mailbox.</param>
|
||||
/// <param name="only"></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] ReadEmailViaImapQuery query, [FromQuery] OnlyFilter? only = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var res = await mediator.Send(query, cancellationToken);
|
||||
|
||||
if(!res.Emails.Any())
|
||||
return NotFound("No emails found matching the specified criteria.");
|
||||
|
||||
if (only == OnlyFilter.HtmlBody)
|
||||
{
|
||||
if (res.Emails.FirstOrDefault()?.HtmlBody is string htmlBody)
|
||||
return Content(htmlBody, "text/html");
|
||||
else
|
||||
return NotFound();
|
||||
}
|
||||
else if (only == OnlyFilter.Uid)
|
||||
return Ok(res.Emails.Select(e => e.Uid).ToList());
|
||||
else
|
||||
return Ok(res);
|
||||
}
|
||||
#endregion IMAP Receive
|
||||
|
||||
#region IMAP Send
|
||||
/// <summary>
|
||||
/// Send an email using IMAP account credentials (via RabbitMQ queue).
|
||||
@@ -139,43 +115,6 @@ public class EmailController(IMediator mediator) : ControllerBase
|
||||
}
|
||||
#endregion IMAP Send
|
||||
|
||||
#region POP3 Receive
|
||||
/// <summary>
|
||||
/// Fetch emails from a POP3 mailbox.
|
||||
/// Triggers an on-demand POP3 sync before returning results.
|
||||
/// </summary>
|
||||
/// <param name="query">Query parameters for filtering and fetching emails from the POP3 mailbox.</param>
|
||||
/// <param name="only">Optional filter to return only specific fields.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>HTTP 200 with list of received emails.</returns>
|
||||
[HttpGet("pop3")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> FetchEmailsViaPop3(
|
||||
[FromQuery] ReadEmailViaPop3Query query,
|
||||
[FromQuery] OnlyFilter? only = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var res = await mediator.Send(query, cancellationToken);
|
||||
|
||||
if (!res.Emails.Any())
|
||||
return NotFound("No emails found matching the specified criteria.");
|
||||
|
||||
if (only == OnlyFilter.HtmlBody)
|
||||
{
|
||||
if (res.Emails.FirstOrDefault()?.HtmlBody is string htmlBody)
|
||||
return Content(htmlBody, "text/html");
|
||||
else
|
||||
return NotFound();
|
||||
}
|
||||
else if (only == OnlyFilter.Uid)
|
||||
return Ok(res.Emails.Select(e => e.Uid).ToList());
|
||||
else
|
||||
return Ok(res);
|
||||
}
|
||||
#endregion POP3 Receive
|
||||
|
||||
#region OAuth2 Send
|
||||
/// <summary>
|
||||
/// Send an email via SMTP using OAuth2 authentication (via RabbitMQ queue).
|
||||
@@ -202,40 +141,28 @@ public class EmailController(IMediator mediator) : ControllerBase
|
||||
}
|
||||
#endregion OAuth2 Send
|
||||
|
||||
#region OAuth2 Receive
|
||||
/// <summary>
|
||||
/// Fetch emails from an IMAP mailbox using OAuth2 authentication.
|
||||
/// The account must have <c>UseOAuth2 = true</c> and valid OAuth2 credentials configured.
|
||||
/// </summary>
|
||||
/// <param name="query">Query parameters for filtering and fetching emails.</param>
|
||||
/// <param name="only">Optional filter to return only specific fields.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>HTTP 200 with list of received emails.</returns>
|
||||
[HttpGet("oauth2/imap")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> FetchEmailsViaOAuth2(
|
||||
[FromQuery] ReadEmailViaOAuth2Query query,
|
||||
[FromQuery] OnlyFilter? only = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
private static async Task<IEnumerable<EmailAttachmentDto>> BuildAttachmentsAsync(
|
||||
IFormFileCollection? files,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var res = await mediator.Send(query, cancellationToken);
|
||||
if (files is null || files.Count == 0)
|
||||
return [];
|
||||
|
||||
if (!res.Emails.Any())
|
||||
return NotFound("No emails found matching the specified criteria.");
|
||||
var result = new List<EmailAttachmentDto>(files.Count);
|
||||
|
||||
if (only == OnlyFilter.HtmlBody)
|
||||
foreach (var file in files)
|
||||
{
|
||||
if (res.Emails.FirstOrDefault()?.HtmlBody is string htmlBody)
|
||||
return Content(htmlBody, "text/html");
|
||||
else
|
||||
return NotFound();
|
||||
using var ms = new MemoryStream();
|
||||
await file.CopyToAsync(ms, cancellationToken);
|
||||
|
||||
result.Add(new EmailAttachmentDto
|
||||
{
|
||||
FileName = file.FileName,
|
||||
Content = ms.ToArray(),
|
||||
ContentType = file.ContentType
|
||||
});
|
||||
}
|
||||
else if (only == OnlyFilter.Uid)
|
||||
return Ok(res.Emails.Select(e => e.Uid).ToList());
|
||||
else
|
||||
return Ok(res);
|
||||
|
||||
return result;
|
||||
}
|
||||
#endregion OAuth2 Receive
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
<Authors>Digital Data GmbH</Authors>
|
||||
<Company>Digital Data GmbH</Company>
|
||||
<Product>DigitalData.MessagingService.API</Product>
|
||||
<Version>1.0.0-beta</Version>
|
||||
<FileVersion>1.0.0.0</FileVersion>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<Version>2.0.0-beta</Version>
|
||||
<FileVersion>2.0.0.0</FileVersion>
|
||||
<AssemblyVersion>2.0.0.0</AssemblyVersion>
|
||||
<Copyright>Copyright © 2026 Digital Data GmbH. All rights reserved.</Copyright>
|
||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
Reference in New Issue
Block a user