Add support for email attachments in SendEmailCommand

Enhanced the email-sending workflow to support attachments:
- Updated `SendEmailCommand` with an `Attachments` property.
- Added `WithAttachments` method to handle attachment initialization.
- Modified `EmailsController` to accept file uploads via `IFormFileCollection`.
- Implemented `BuildAttachmentsAsync` to process uploaded files.
- Updated `EmailMappingProfile` to map `Attachments` to `EmailContext`.
- Adjusted `SendEmail` endpoint to consume `multipart/form-data`.
- Enhanced `SendEmail` response to include the queued event ID.
- Updated project file to include Swagger infrastructure folder.

These changes enable handling of email attachments and improve API functionality.
This commit is contained in:
2026-08-05 17:14:44 +02:00
parent 890c32f1c8
commit 2e69fac250
4 changed files with 59 additions and 12 deletions

View File

@@ -14,6 +14,7 @@ public class EmailMappingProfile : Profile
// SendEmailCommand -> Email // SendEmailCommand -> Email
// Sender is resolved via MediatR in the handler and set separately after mapping. // Sender is resolved via MediatR in the handler and set separately after mapping.
CreateMap<SendEmailCommand, EmailContext>() CreateMap<SendEmailCommand, EmailContext>()
.ForMember(dest => dest.Sender, opt => opt.Ignore()); .ForMember(dest => dest.Sender, opt => opt.Ignore())
.ForMember(dest => dest.Attachments, opt => opt.MapFrom(src => src.Attachments));
} }
} }

View File

@@ -3,6 +3,7 @@ using DigitalData.MessagingService.Application.EmailAccount.Queries;
using DigitalData.MessagingService.Domain.Exceptions; using DigitalData.MessagingService.Domain.Exceptions;
using DigitalData.MessagingService.Abstraction; using DigitalData.MessagingService.Abstraction;
using MediatR; using MediatR;
using System.Text.Json.Serialization;
namespace DigitalData.MessagingService.Application.EmailSending.Commands; namespace DigitalData.MessagingService.Application.EmailSending.Commands;
@@ -32,6 +33,16 @@ public record SendEmailCommand : IRequest<Guid>
/// Is HTML email (default: true) /// Is HTML email (default: true)
/// </summary> /// </summary>
public bool IsHtml { get; init; } = true; public bool IsHtml { get; init; } = true;
[JsonIgnore]
internal IEnumerable<EmailAttachmentContext> Attachments { get; private init; } = [];
/// <summary>
/// Returns a new command instance with the supplied attachments.
/// Called by the controller after resolving uploaded files.
/// </summary>
public SendEmailCommand WithAttachments(IEnumerable<EmailAttachmentContext> attachments)
=> this with { Attachments = attachments };
} }
/// <summary> /// <summary>

View File

@@ -1,33 +1,64 @@
using DigitalData.MessagingService.Application.EmailSending.Commands; using DigitalData.MessagingService.Application.EmailSending.Commands;
using DigitalData.MessagingService.Abstraction;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace DigitalData.MessagingService.API.Controllers; namespace DigitalData.MessagingService.API.Controllers;
/// <summary> /// <summary>
/// Email sending API controller /// Email sending API controller.
/// Enqueues outgoing emails to RabbitMQ for async processing /// Enqueues outgoing emails to RabbitMQ for async processing.
/// </summary> /// </summary>
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
public class EmailsController(IMediator mediator) : ControllerBase public class EmailsController(IMediator mediator) : ControllerBase
{ {
/// <summary> /// <summary>
/// Send email (enqueue to RabbitMQ for background processing) /// Send an email, optionally with file attachments.
/// Omit the <c>attachments</c> field for a plain send.
/// </summary> /// </summary>
/// <param name="command">Send email command</param> /// <param name="command">Email fields as form values</param>
/// <param name="attachments">Optional uploaded files</param>
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
/// <returns>HTTP 202 Accepted (queued for processing)</returns> /// <returns>HTTP 202 Accepted with the queued event ID</returns>
[HttpPost("send")] [HttpPost]
[Consumes("multipart/form-data")]
[ProducesResponseType(StatusCodes.Status202Accepted)] [ProducesResponseType(StatusCodes.Status202Accepted)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> SendEmail([FromBody] SendEmailCommand command, CancellationToken cancellationToken) public async Task<IActionResult> SendEmail(
[FromForm] SendEmailCommand command,
IFormFileCollection? attachments,
CancellationToken cancellationToken)
{ {
var eventId = await mediator.Send(command, cancellationToken); var commandWithAttachments = command.WithAttachments(
await BuildAttachmentsAsync(attachments, cancellationToken));
return Accepted(new var eventId = await mediator.Send(commandWithAttachments, cancellationToken);
return Accepted(new { Id = eventId });
}
private static async Task<IEnumerable<EmailAttachmentContext>> BuildAttachmentsAsync(
IFormFileCollection? files,
CancellationToken cancellationToken)
{ {
Id = eventId if (files is null || files.Count == 0)
return [];
var result = new List<EmailAttachmentContext>(files.Count);
foreach (var file in files)
{
using var ms = new MemoryStream();
await file.CopyToAsync(ms, cancellationToken);
result.Add(new EmailAttachmentContext
{
FileName = file.FileName,
Content = ms.ToArray(),
ContentType = file.ContentType
}); });
} }
return result;
}
} }

View File

@@ -36,4 +36,8 @@
<ProjectReference Include="..\..\infrastructure\DigitalData.MessagingService.Infrastructure\DigitalData.MessagingService.Infrastructure.csproj" /> <ProjectReference Include="..\..\infrastructure\DigitalData.MessagingService.Infrastructure\DigitalData.MessagingService.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Infrastructure\Swagger\" />
</ItemGroup>
</Project> </Project>