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:
@@ -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>
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user