Add FetchEmailByUid endpoint and improve middleware

Added a new endpoint to `EmailController` for fetching a single email by UID, including route and query parameter handling, and appropriate Swagger annotations.

Refactored `ExceptionHandlingMiddleware` to use constructor injection, simplified exception handling with a `switch` expression, and improved logging and response formatting. Added support for `ValidationException`.

Updated `FormatValidationErrors` to handle `ValidationException` and improved error formatting.

Made minor documentation updates in `EmailController` and performed general code cleanup for readability and modernization.
This commit is contained in:
2026-08-11 09:48:22 +02:00
parent 59e8780345
commit 1162a07454
2 changed files with 74 additions and 61 deletions

View File

@@ -72,6 +72,7 @@ public class EmailController(IMediator mediator) : ControllerBase
/// Fetch emails from an IMAP mailbox. /// Fetch emails from an IMAP mailbox.
/// </summary> /// </summary>
/// <param name="query">Query parameters for filtering and fetching emails from the IMAP mailbox.</param> /// <param name="query">Query parameters for filtering and fetching emails from the IMAP mailbox.</param>
/// <param name="firstHtmlBodyOnly"></param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>HTTP 200 with list of received emails.</returns> /// <returns>HTTP 200 with list of received emails.</returns>
[HttpGet] [HttpGet]
@@ -91,6 +92,28 @@ public class EmailController(IMediator mediator) : ControllerBase
return Ok(emails); return Ok(emails);
} }
/// <summary>
/// Fetch a single email by its UID from an IMAP mailbox.
/// </summary>
/// <param name="uid">The unique identifier (UID) of the message.</param>
/// <param name="query">Query parameters including account, folder and attachment flag.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>HTTP 200 with the matched email, or HTTP 404 if not found.</returns>
[HttpGet("{uid:long}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> FetchEmailByUid([FromRoute] long uid, [FromQuery] FetchEmailByUidQuery query, CancellationToken cancellationToken = default)
{
var queryWithUid = query.WithUid(uid);
var email = await mediator.Send(queryWithUid, cancellationToken);
if (email is null)
return NotFound($"No email found with UID {uid}.");
return Ok(email);
}
/// <summary> /// <summary>
/// Mark a single IMAP message as seen (read). /// Mark a single IMAP message as seen (read).
/// </summary> /// </summary>

View File

@@ -1,89 +1,79 @@
using System.Net; using System.Net;
using System.Text.Json; using System.Text.Json;
using DigitalData.MessagingService.Domain.Exceptions; using DigitalData.MessagingService.Domain.Exceptions;
using FluentValidation;
namespace DigitalData.MessagingService.API.Middleware; namespace DigitalData.MessagingService.API.Middleware;
/// <summary> /// <summary>
/// Global exception handling middleware /// Global exception handling middleware
/// </summary> /// </summary>
public class ExceptionHandlingMiddleware public class ExceptionHandlingMiddleware(RequestDelegate Next, ILogger<ExceptionHandlingMiddleware> Logger)
{ {
private static readonly JsonSerializerOptions _jsonSerializerOptions = new() private static readonly JsonSerializerOptions _jsonSerializerOptions = new()
{ {
PropertyNamingPolicy = JsonNamingPolicy.CamelCase PropertyNamingPolicy = JsonNamingPolicy.CamelCase
}; };
private readonly RequestDelegate _next; /// <summary>
private readonly ILogger<ExceptionHandlingMiddleware> _logger; ///
/// </summary>
public ExceptionHandlingMiddleware( /// <param name="context"></param>
RequestDelegate next, /// <returns></returns>
ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context) public async Task InvokeAsync(HttpContext context)
{ {
try try
{ {
await _next(context); await Next(context);
} }
catch (Exception ex) catch (Exception ex)
{ {
await HandleExceptionAsync(context, ex); context.Response.ContentType = "application/json";
var (statusCode, message) = ex switch
{
NotFoundException notFoundEx =>
(HttpStatusCode.NotFound, notFoundEx.Message),
AuthenticationFailedException authEx =>
(HttpStatusCode.Unauthorized, authEx.Message),
ValidationException validationEx =>
(HttpStatusCode.BadRequest, FormatValidationErrors(validationEx)),
_ => (HttpStatusCode.InternalServerError, "An internal server error occurred")
};
context.Response.StatusCode = (int)statusCode;
// Log the exception
if (statusCode == HttpStatusCode.InternalServerError)
{
Logger.LogError(ex, "Unhandled exception: {Message}", ex.Message);
}
else
{
Logger.LogWarning(ex, "Exception handled: {StatusCode} - {Message}",
statusCode, message);
}
var response = new
{
StatusCode = (int)statusCode,
Message = message,
DetailedMessage = statusCode == HttpStatusCode.InternalServerError
? ex.Message
: null,
Timestamp = DateTime.Now
};
var json = JsonSerializer.Serialize(response, _jsonSerializerOptions);
await context.Response.WriteAsync(json);
} }
} }
private async Task HandleExceptionAsync(HttpContext context, Exception exception) private static string FormatValidationErrors(ValidationException exception)
{
context.Response.ContentType = "application/json";
var (statusCode, message) = exception switch
{
NotFoundException notFoundEx =>
(HttpStatusCode.NotFound, notFoundEx.Message),
AuthenticationFailedException authEx =>
(HttpStatusCode.Unauthorized, authEx.Message),
FluentValidation.ValidationException validationEx =>
(HttpStatusCode.BadRequest, FormatValidationErrors(validationEx)),
_ => (HttpStatusCode.InternalServerError, "An internal server error occurred")
};
context.Response.StatusCode = (int)statusCode;
// Log the exception
if (statusCode == HttpStatusCode.InternalServerError)
{
_logger.LogError(exception, "Unhandled exception: {Message}", exception.Message);
}
else
{
_logger.LogWarning(exception, "Exception handled: {StatusCode} - {Message}",
statusCode, message);
}
var response = new
{
StatusCode = (int)statusCode,
Message = message,
DetailedMessage = statusCode == HttpStatusCode.InternalServerError
? exception.Message
: null,
Timestamp = DateTime.Now
};
var json = JsonSerializer.Serialize(response, _jsonSerializerOptions);
await context.Response.WriteAsync(json);
}
private static string FormatValidationErrors(FluentValidation.ValidationException exception)
{ {
var errors = exception.Errors var errors = exception.Errors
.Select(e => $"{e.PropertyName}: {e.ErrorMessage}") .Select(e => $"{e.PropertyName}: {e.ErrorMessage}")