From 740bb8c313051f891779135c2aad818e3a1b5fd7 Mon Sep 17 00:00:00 2001 From: TekH Date: Wed, 5 Aug 2026 12:32:08 +0200 Subject: [PATCH] Refactor email account handling for dynamic resolution Reintroduced `EmailAccountDto` with conditional compilation to support both .NET and non-.NET environments. Updated `IEmailService` to accept `EmailAccountDto` as the sender, replacing reliance on pre-configured SMTP credentials. Added `GetSenderQuery` and its handler to dynamically resolve email accounts based on `Id` or `Username`. Introduced `GetSenderQueryValidator` for validation, ensuring proper usage of the query. Modified `SendEmailCommand` to include sender resolution via MediatR. Updated `OutgoingEmailEvent` to include sender information and adjusted `OutgoingEmailConsumer` and `LimilabsEmailService` to use the dynamically resolved sender. Updated `EmailMappingProfile` to ignore the `Sender` property during mapping. Replaced `Name` with `Id` in `appsettings.Secrets.json` for email accounts. Removed the old `EmailAccountDto` folder and performed general cleanup and restructuring. --- .../Common/Dtos/EmailAccountDto.cs | 27 ------------- .../Common/Interfaces/IEmailService.cs | 4 +- .../Common/Mappings/EmailMappingProfile.cs | 4 +- ...alData.MessagingService.Application.csproj | 4 -- .../EmailAccount/Queries/GetSenderQuery.cs | 39 +++++++++++++++++++ .../Validators/GetSenderQueryValidator.cs | 27 +++++++++++++ .../EmailSending/Commands/SendEmailCommand.cs | 18 +++++++-- .../EmailAccountDto.cs | 38 ++++++++++++++++++ .../OutgoingEmailEvent.cs | 6 ++- .../Queue/OutgoingEmailConsumer.cs | 1 + .../Services/LimilabsEmailService.cs | 11 ++---- .../appsettings.Secrets.json | 2 +- 12 files changed, 136 insertions(+), 45 deletions(-) delete mode 100644 src/core/DigitalData.MessagingService.Application/Common/Dtos/EmailAccountDto.cs create mode 100644 src/core/DigitalData.MessagingService.Application/EmailAccount/Queries/GetSenderQuery.cs create mode 100644 src/core/DigitalData.MessagingService.Application/EmailAccount/Validators/GetSenderQueryValidator.cs create mode 100644 src/core/DigitalData.MessagingService.Publisher.Abstraction/EmailAccountDto.cs diff --git a/src/core/DigitalData.MessagingService.Application/Common/Dtos/EmailAccountDto.cs b/src/core/DigitalData.MessagingService.Application/Common/Dtos/EmailAccountDto.cs deleted file mode 100644 index d2c3ebb..0000000 --- a/src/core/DigitalData.MessagingService.Application/Common/Dtos/EmailAccountDto.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace DigitalData.MessagingService.Application.Common.Dtos; - -/// -/// DTO for a single email account configuration. -/// -public class EmailAccountDto -{ - /// - /// Logical name to identify this account (e.g. "default", "support"). - /// - public int Id { get; init; } - - public required string Username { get; init; } - - public required string Password { get; init; } - - public bool PasswordEncrypted { get; init; } = false; - - public required string SmtpServer { get; init; } - - public int SmtpPort { get; init; } - - public bool SmtpUseSsl { get; init; } - - public bool UseOAuth2 { get; init; } -} - diff --git a/src/core/DigitalData.MessagingService.Application/Common/Interfaces/IEmailService.cs b/src/core/DigitalData.MessagingService.Application/Common/Interfaces/IEmailService.cs index c7639c5..7c6c1a8 100644 --- a/src/core/DigitalData.MessagingService.Application/Common/Interfaces/IEmailService.cs +++ b/src/core/DigitalData.MessagingService.Application/Common/Interfaces/IEmailService.cs @@ -1,3 +1,5 @@ +using DigitalData.MessagingService.Application.Common.Dtos; + namespace DigitalData.MessagingService.Application.Common.Interfaces; /// @@ -12,5 +14,5 @@ public interface IEmailService /// Sends an email using the configured SMTP account. /// SMTP credentials are configured in appsettings.json (EmailAccount section). /// - Task SendEmailAsync(string to, string subject, string body, bool isHtml = true, CancellationToken cancellationToken = default); + Task SendEmailAsync(EmailAccountDto from, string to, string subject, string body, bool isHtml = true, CancellationToken cancellationToken = default); } diff --git a/src/core/DigitalData.MessagingService.Application/Common/Mappings/EmailMappingProfile.cs b/src/core/DigitalData.MessagingService.Application/Common/Mappings/EmailMappingProfile.cs index 70659d7..fdb0c3b 100644 --- a/src/core/DigitalData.MessagingService.Application/Common/Mappings/EmailMappingProfile.cs +++ b/src/core/DigitalData.MessagingService.Application/Common/Mappings/EmailMappingProfile.cs @@ -12,8 +12,10 @@ public class EmailMappingProfile : Profile public EmailMappingProfile() { // SendEmailCommand -> OutgoingEmailEvent + // Sender is resolved via MediatR in the handler and set separately after mapping. CreateMap() .ForMember(dest => dest.Id, opt => opt.MapFrom(_ => Guid.NewGuid())) - .ForMember(dest => dest.QueuedAt, opt => opt.MapFrom(_ => DateTime.Now)); + .ForMember(dest => dest.QueuedAt, opt => opt.MapFrom(_ => DateTime.Now)) + .ForMember(dest => dest.Sender, opt => opt.Ignore()); } } diff --git a/src/core/DigitalData.MessagingService.Application/DigitalData.MessagingService.Application.csproj b/src/core/DigitalData.MessagingService.Application/DigitalData.MessagingService.Application.csproj index e774d62..b06f179 100644 --- a/src/core/DigitalData.MessagingService.Application/DigitalData.MessagingService.Application.csproj +++ b/src/core/DigitalData.MessagingService.Application/DigitalData.MessagingService.Application.csproj @@ -20,8 +20,4 @@ - - - - diff --git a/src/core/DigitalData.MessagingService.Application/EmailAccount/Queries/GetSenderQuery.cs b/src/core/DigitalData.MessagingService.Application/EmailAccount/Queries/GetSenderQuery.cs new file mode 100644 index 0000000..aaf760c --- /dev/null +++ b/src/core/DigitalData.MessagingService.Application/EmailAccount/Queries/GetSenderQuery.cs @@ -0,0 +1,39 @@ +using DigitalData.MessagingService.Application.Common.Dtos; +using DigitalData.MessagingService.Application.Common.Options; +using MediatR; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace DigitalData.MessagingService.Application.EmailAccount.Queries; + +public record GetSenderQuery : IRequest +{ + public int? Id { get; init; } + + public string? Username { get; init; } +} + +/// +/// +/// +/// +/// +public class GetSenderQueryHandler(IOptions Options, ILogger Logger) : IRequestHandler +{ + public Task Handle(GetSenderQuery request, CancellationToken cancellationToken) + { + var accounts = request.Id is not null + ? Options.Value.Accounts.Where(a => a.Id == request.Id) + : Options.Value.Accounts.Where(a => a.Username == request.Username); + + if(accounts.Count() > 1) + { + Logger.LogWarning( + "Multiple email accounts found for the given criteria ({Criteria}). Returning the first one.", + request.Id is not null ? $"Id: {request.Id}" : $"Username: {request.Username}" + ); + } + + return Task.FromResult(accounts.FirstOrDefault()); + } +} \ No newline at end of file diff --git a/src/core/DigitalData.MessagingService.Application/EmailAccount/Validators/GetSenderQueryValidator.cs b/src/core/DigitalData.MessagingService.Application/EmailAccount/Validators/GetSenderQueryValidator.cs new file mode 100644 index 0000000..b26ce32 --- /dev/null +++ b/src/core/DigitalData.MessagingService.Application/EmailAccount/Validators/GetSenderQueryValidator.cs @@ -0,0 +1,27 @@ +using DigitalData.MessagingService.Application.EmailAccount.Queries; +using FluentValidation; + +namespace DigitalData.MessagingService.Application.EmailAccount.Validators; + +/// +/// Validator for . +/// Either or must be provided, but not both. +/// +public class GetSenderQueryValidator : AbstractValidator +{ + public GetSenderQueryValidator() + { + RuleFor(x => x) + .Must(x => (x.Id is not null) ^ (x.Username is not null)) + .WithMessage("Either Id or Username must be provided, but not both."); + + When(x => x.Username is not null, () => + { + RuleFor(x => x.Username) + .NotEmpty() + .WithMessage("Username must not be empty.") + .MaximumLength(200) + .WithMessage("Username must not exceed 200 characters."); + }); + } +} diff --git a/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/SendEmailCommand.cs b/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/SendEmailCommand.cs index 3db5d89..c27fc1f 100644 --- a/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/SendEmailCommand.cs +++ b/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/SendEmailCommand.cs @@ -1,4 +1,7 @@ using AutoMapper; +using DigitalData.MessagingService.Application.Common.Dtos; +using DigitalData.MessagingService.Application.EmailAccount.Queries; +using DigitalData.MessagingService.Domain.Exceptions; using DigitalData.MessagingService.Publisher.Abstraction; using MediatR; @@ -9,6 +12,8 @@ namespace DigitalData.MessagingService.Application.EmailSending.Commands; /// public record SendEmailCommand : IRequest { + public required GetSenderQuery Sender { get; init; } + /// /// Recipient email address /// @@ -32,13 +37,20 @@ public record SendEmailCommand : IRequest /// /// Handler for SendEmailCommand -/// Creates EmailOutbox entity via AutoMapper and enqueues to RabbitMQ +/// Resolves the sender account via MediatR, maps to OutgoingEmailEvent and enqueues to RabbitMQ /// -public class SendEmailCommandHandler(IOutgoingEmailPublisher Publisher, IMapper Mapper) : IRequestHandler +public class SendEmailCommandHandler( +ISender Sender, +IOutgoingEmailPublisher Publisher, +IMapper Mapper) : IRequestHandler { public async Task Handle(SendEmailCommand request, CancellationToken cancellationToken) { - var outgoingEmailEvent = Mapper.Map(request); + 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 outgoingEmailEvent = Mapper.Map(request) with { Sender = senderAccount }; // Enqueue to RabbitMQ await Publisher.EnqueueAsync(outgoingEmailEvent, cancellationToken); diff --git a/src/core/DigitalData.MessagingService.Publisher.Abstraction/EmailAccountDto.cs b/src/core/DigitalData.MessagingService.Publisher.Abstraction/EmailAccountDto.cs new file mode 100644 index 0000000..c27bd8a --- /dev/null +++ b/src/core/DigitalData.MessagingService.Publisher.Abstraction/EmailAccountDto.cs @@ -0,0 +1,38 @@ +namespace DigitalData.MessagingService.Application.Common.Dtos; + +/// +/// DTO for a single email account configuration. +/// +public class EmailAccountDto +{ + /// + /// Logical name to identify this account (e.g. "default", "support"). + /// + public int Id { get; set; } + +#if NET + public required string Username { get; set; } +#else + public string Username { get; set; } = null!; +#endif + +#if NET + public required string Password { get; set; } +#else + public string Password { get; set; } = null!; +#endif + + public bool PasswordEncrypted { get; set; } = false; + +#if NET + public required string SmtpServer { get; set; } +#else + public string SmtpServer { get; set; } = null!; +#endif + + public int SmtpPort { get; set; } + + public bool SmtpUseSsl { get; set; } + + public bool UseOAuth2 { get; set; } +} \ No newline at end of file diff --git a/src/core/DigitalData.MessagingService.Publisher.Abstraction/OutgoingEmailEvent.cs b/src/core/DigitalData.MessagingService.Publisher.Abstraction/OutgoingEmailEvent.cs index 5250208..a7e338e 100644 --- a/src/core/DigitalData.MessagingService.Publisher.Abstraction/OutgoingEmailEvent.cs +++ b/src/core/DigitalData.MessagingService.Publisher.Abstraction/OutgoingEmailEvent.cs @@ -1,9 +1,13 @@ -namespace DigitalData.MessagingService.Publisher.Abstraction; +using DigitalData.MessagingService.Application.Common.Dtos; + +namespace DigitalData.MessagingService.Publisher.Abstraction; public record OutgoingEmailEvent { public Guid Id { get; set; } + public EmailAccountDto Sender { get; set; } = null!; + /// /// Recipient email address /// diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Queue/OutgoingEmailConsumer.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Queue/OutgoingEmailConsumer.cs index 80da0ee..b558c58 100644 --- a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Queue/OutgoingEmailConsumer.cs +++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Queue/OutgoingEmailConsumer.cs @@ -48,6 +48,7 @@ public sealed class OutgoingEmailConsumer : IAsyncDisposable { // Send email via SMTP (SMTP config is injected in IEmailService via IOptions) await EmailService.SendEmailAsync( + oMailEvent.Sender, oMailEvent.Recipient, oMailEvent.Subject, oMailEvent.Body, diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsEmailService.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsEmailService.cs index 44e8c86..94d3f33 100644 --- a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsEmailService.cs +++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsEmailService.cs @@ -19,8 +19,7 @@ namespace DigitalData.MessagingService.Infrastructure.Services; /// or falls back to the first account if none is named "default". /// public class LimilabsEmailService( - IEncryptionService encryptionService, - IOptions smtpConfig) : IEmailService + IEncryptionService encryptionService) : IEmailService { // Register encoding provider for Limilabs (requires windows-1252 and other code pages) static LimilabsEmailService() @@ -28,18 +27,16 @@ public class LimilabsEmailService( Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); } - public async Task SendEmailAsync(string to, string subject, string body, bool isHtml = true, CancellationToken cancellationToken = default) + public async Task SendEmailAsync(EmailAccountDto from, string to, string subject, string body, bool isHtml = true, CancellationToken cancellationToken = default) { - var smtpAccount = smtpConfig.Value.Accounts.First(); - using var smtp = new Smtp(); try { - await ConnectAndAuthenticateSmtpAsync(smtp, smtpAccount); + await ConnectAndAuthenticateSmtpAsync(smtp, from); var builder = new MailBuilder(); - builder.From.Add(new MailBox(smtpAccount.Username)); + builder.From.Add(new MailBox(from.Username)); builder.To.Add(new MailBox(to)); builder.Subject = subject; diff --git a/src/presentation/DigitalData.MessagingService.API/appsettings.Secrets.json b/src/presentation/DigitalData.MessagingService.API/appsettings.Secrets.json index 195654e..9f3529d 100644 --- a/src/presentation/DigitalData.MessagingService.API/appsettings.Secrets.json +++ b/src/presentation/DigitalData.MessagingService.API/appsettings.Secrets.json @@ -18,7 +18,7 @@ "EmailAccounts": { "Accounts": [ { - "Name": "1", + "Id": "1", "Username": "test-flow@digitaldata.works", "Password": "ddemail108", "PasswordEncrypted": false,