From 14180860d3b9a24d3064e579ecd1f0352f961ec1 Mon Sep 17 00:00:00 2001 From: TekH Date: Thu, 13 Aug 2026 11:57:57 +0200 Subject: [PATCH] refactor(application): replace ISender dispatch with direct IRepository usage in email handlers - FetchEmailsQuery, MarkEmailAsSeenCommand, PublishEmailCommand: resolve EmailAccount via IRepository instead of dispatching GetEmailAccountQuery through ISender - Swap InvalidOperationException for BadRequestException when IMAP server is not configured - Add ILogger to handlers; warn when multiple accounts match the lookup criteria - EmailAccountsOptions.Accounts now typed as IEnumerable instead of IEnumerable to decouple config from domain entity - EmailMappingProfile: add EmailAccount <-> EmailAccountDto/EmailAccountModificationDto maps --- .../Common/Mappings/EmailMappingProfile.cs | 6 +++++ .../Common/Options/EmailAccountsOptions.cs | 5 +++- .../Commands/MarkEmailAsSeenCommand.cs | 22 ++++++++++-------- .../Queries/FetchEmailsQuery.cs | 21 ++++++++++------- .../Commands/PublishEmailCommand.cs | 23 ++++++++++++------- 5 files changed, 51 insertions(+), 26 deletions(-) diff --git a/src/core/DigitalData.MessagingService.Application/Common/Mappings/EmailMappingProfile.cs b/src/core/DigitalData.MessagingService.Application/Common/Mappings/EmailMappingProfile.cs index 2362cc8..c778f7c 100644 --- a/src/core/DigitalData.MessagingService.Application/Common/Mappings/EmailMappingProfile.cs +++ b/src/core/DigitalData.MessagingService.Application/Common/Mappings/EmailMappingProfile.cs @@ -2,6 +2,8 @@ using AutoMapper; using DigitalData.MessagingService.Application.EmailSending.Commands; using DigitalData.MessagingService.Application.Common.Dto; +using DigitalData.MessagingService.Domain.Entities; +using DigitalData.MessagingService.Application.Common.Dto.EmailAccounts; namespace DigitalData.MessagingService.Application.Common.Mappings; @@ -17,6 +19,10 @@ public class EmailMappingProfile : Profile CreateMap() .ForMember(dest => dest.Sender, opt => opt.Ignore()) .ForMember(dest => dest.Attachments, opt => opt.MapFrom(src => src.Attachments)); + + // EmailAccountDto -> EmailAccount + CreateMap(); + CreateMap(); } } #endif \ No newline at end of file diff --git a/src/core/DigitalData.MessagingService.Application/Common/Options/EmailAccountsOptions.cs b/src/core/DigitalData.MessagingService.Application/Common/Options/EmailAccountsOptions.cs index 06ac656..affc839 100644 --- a/src/core/DigitalData.MessagingService.Application/Common/Options/EmailAccountsOptions.cs +++ b/src/core/DigitalData.MessagingService.Application/Common/Options/EmailAccountsOptions.cs @@ -1,4 +1,7 @@ #if NET +using DigitalData.MessagingService.Application.Common.Dto; +using DigitalData.MessagingService.Application.Common.Dto.EmailAccounts; +using DigitalData.MessagingService.Application.EmailAccounts.Commands; using DigitalData.MessagingService.Domain.Entities; namespace DigitalData.MessagingService.Application.Common.Options; @@ -14,7 +17,7 @@ public class EmailAccountsOptions /// /// The list of configured email accounts. /// - public required IEnumerable Accounts { get; init; } = []; + public required IEnumerable Accounts { get; init; } = []; /// /// How often the IMAP sync worker polls for new emails, in seconds. diff --git a/src/core/DigitalData.MessagingService.Application/EmailReceiving/Commands/MarkEmailAsSeenCommand.cs b/src/core/DigitalData.MessagingService.Application/EmailReceiving/Commands/MarkEmailAsSeenCommand.cs index 4e54038..7d5429f 100644 --- a/src/core/DigitalData.MessagingService.Application/EmailReceiving/Commands/MarkEmailAsSeenCommand.cs +++ b/src/core/DigitalData.MessagingService.Application/EmailReceiving/Commands/MarkEmailAsSeenCommand.cs @@ -1,8 +1,11 @@ #if NET 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.Commands; @@ -11,7 +14,7 @@ namespace DigitalData.MessagingService.Application.EmailReceiving.Commands; /// public record MarkEmailAsSeenCommand : IRequest { - public required GetSenderQuery Account { get; init; } + public required GetEmailAccountQuery Account { get; init; } /// /// UID of the message to mark as seen. @@ -24,19 +27,20 @@ public record MarkEmailAsSeenCommand : IRequest public string Folder { get; init; } = "INBOX"; } -public class MarkEmailAsSeenCommandHandler( - ISender Sender, - IImapEmailService ImapService) : IRequestHandler +public class MarkEmailAsSeenCommandHandler(IImapEmailService ImapService, ILogger Logger, IRepository Repo) : IRequestHandler { 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})."); + var accounts = await Repo.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 InvalidOperationException( - $"IMAP is not configured for account '{account.Username}' (Id: {account.Id}). Set ImapServer in EmailAccounts configuration."); + throw new BadRequestException($"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); } diff --git a/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/FetchEmailsQuery.cs b/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/FetchEmailsQuery.cs index eeebe4e..8c69d8b 100644 --- a/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/FetchEmailsQuery.cs +++ b/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/FetchEmailsQuery.cs @@ -2,9 +2,12 @@ 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; @@ -16,7 +19,7 @@ public record FetchEmailsQuery : IRequest> /// /// Identifies the email account to use. /// - public required GetSenderQuery Account { get; init; } + public required GetEmailAccountQuery Account { get; init; } /// /// Mail query used to filter and limit the emails retrieved. @@ -24,18 +27,20 @@ public record FetchEmailsQuery : IRequest> public MailSearchFilter Mail { get; init; } = new(); } -public class FetchEmailsQueryHandler( - ISender Sender, - IImapEmailService ImapService) : IRequestHandler> +public class FetchEmailsQueryHandler(IImapEmailService ImapService, ILogger Logger, IRepository Repo) : IRequestHandler> { public async Task> 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})."); + var accounts = await Repo.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}"); + + EmailAccount 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 InvalidOperationException( + throw new BadRequestException( $"IMAP is not configured for account '{account.Username}' (Id: {account.Id}). Set ImapServer in EmailAccounts configuration."); return await ImapService.FetchEmailsAsync( diff --git a/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/PublishEmailCommand.cs b/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/PublishEmailCommand.cs index d6299de..fcc0f81 100644 --- a/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/PublishEmailCommand.cs +++ b/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/PublishEmailCommand.cs @@ -1,11 +1,14 @@ #if NET using AutoMapper; -using DigitalData.MessagingService.Domain.Exceptions; using DigitalData.MessagingService.Application.Common.Dto; -using MediatR; -using System.Text.Json.Serialization; 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; +using System.Text.Json.Serialization; namespace DigitalData.MessagingService.Application.EmailSending.Commands; @@ -14,7 +17,7 @@ namespace DigitalData.MessagingService.Application.EmailSending.Commands; /// public record PublishEmailCommand : IRequest { - public required GetSenderQuery Sender { get; init; } + public required GetEmailAccountQuery Sender { get; init; } /// /// Recipient email addresses @@ -51,13 +54,17 @@ public record PublishEmailCommand : IRequest /// Handler for PublishEmailCommand /// Resolves the sender account via MediatR, maps to SendingEmailEvent and enqueues to RabbitMQ /// -public class PublishEmailCommandHandler(ISender Sender, ISendingEmailPublisher Publisher, IMapper Mapper) : IRequestHandler +public class PublishEmailCommandHandler(IRepository Repo, ISendingEmailPublisher Publisher, IMapper Mapper, ILogger Logger) : IRequestHandler { public async Task Handle(PublishEmailCommand request, CancellationToken cancellationToken) { - var senderAccount = await Sender.Send(request.Sender, cancellationToken) - ?? throw new NotFoundException( - $"No email account found for the given sender criteria (Id: {request.Sender.Id}, Username: {request.Sender.Username})."); + var senderAccounts = await Repo.FindAsync(request.Sender.Id is int id ? x => x.Id == id : x => x.Username == request.Sender.Username, cancellationToken: cancellationToken); + + if (senderAccounts.Count() > 1) + Logger.LogWarning("Multiple email accounts found for the given criteria ({Criteria}). Returning the first one.", request.Sender.Id is not null ? $"Id: {request.Sender.Id}" : $"Username: {request.Sender.Username}"); + + var senderAccount = senderAccounts.FirstOrDefault() + ?? throw new NotFoundException($"No email account found for the given sender criteria (Id: {request.Sender.Id}, Username: {request.Sender.Username})."); var email = Mapper.Map(request) with { Sender = senderAccount };