From e47333cd1d3c1b759cc6fe0eefaf8d7c979a0052 Mon Sep 17 00:00:00 2001 From: TekH Date: Wed, 5 Aug 2026 13:02:57 +0200 Subject: [PATCH] Support multiple email recipients in email-sending flow Updated the `IEmailService` interface and related components to support multiple recipients in the `SendEmailAsync` method. - Replaced `Recipient` with `Recipients` in `SendEmailCommand`, `OutgoingEmailEvent`, and `EmailsController`. - Updated `SendEmailCommandValidator` to validate a collection of recipients, ensuring at least one valid email address. - Modified `LimilabsEmailService` to handle multiple recipients by iterating over the collection and adding each to the email. - Adjusted `OutgoingEmailConsumer` to process and log multiple recipients. - Updated logging and response structures to reflect the changes. These changes enable the system to handle emails with multiple recipients while maintaining proper validation and logging. --- .../Common/Interfaces/IEmailService.cs | 2 +- .../EmailSending/Commands/SendEmailCommand.cs | 4 ++-- .../Validators/SendEmailCommandValidator.cs | 15 +++++++++------ .../OutgoingEmailEvent.cs | 2 +- .../Queue/OutgoingEmailConsumer.cs | 4 ++-- .../Services/LimilabsEmailService.cs | 7 +++++-- .../Controllers/EmailsController.cs | 5 +---- 7 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/core/DigitalData.MessagingService.Application/Common/Interfaces/IEmailService.cs b/src/core/DigitalData.MessagingService.Application/Common/Interfaces/IEmailService.cs index 7c6c1a8..e1bc475 100644 --- a/src/core/DigitalData.MessagingService.Application/Common/Interfaces/IEmailService.cs +++ b/src/core/DigitalData.MessagingService.Application/Common/Interfaces/IEmailService.cs @@ -14,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(EmailAccountDto from, string to, string subject, string body, bool isHtml = true, CancellationToken cancellationToken = default); + Task SendEmailAsync(EmailAccountDto from, IEnumerable recipients, string subject, string body, bool isHtml = true, CancellationToken cancellationToken = default); } diff --git a/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/SendEmailCommand.cs b/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/SendEmailCommand.cs index c27fc1f..fc9757b 100644 --- a/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/SendEmailCommand.cs +++ b/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/SendEmailCommand.cs @@ -15,9 +15,9 @@ public record SendEmailCommand : IRequest public required GetSenderQuery Sender { get; init; } /// - /// Recipient email address + /// Recipient email addresses /// - public required string Recipient { get; init; } + public required IEnumerable Recipients { get; init; } /// /// Email subject diff --git a/src/core/DigitalData.MessagingService.Application/EmailSending/Validators/SendEmailCommandValidator.cs b/src/core/DigitalData.MessagingService.Application/EmailSending/Validators/SendEmailCommandValidator.cs index 37397b1..2ab5ad3 100644 --- a/src/core/DigitalData.MessagingService.Application/EmailSending/Validators/SendEmailCommandValidator.cs +++ b/src/core/DigitalData.MessagingService.Application/EmailSending/Validators/SendEmailCommandValidator.cs @@ -10,13 +10,16 @@ public class SendEmailCommandValidator : AbstractValidator { public SendEmailCommandValidator() { - RuleFor(x => x.Recipient) + RuleFor(x => x.Recipients) .NotEmpty() - .WithMessage("Recipient is required") - .MaximumLength(200) - .WithMessage("Recipient must not exceed 200 characters") - .EmailAddress() - .WithMessage("Recipient must be a valid email address"); + .WithMessage("Recipients are required") + .Must(x => x.Any()) + .WithMessage("At least one recipient is required") + .ForEach(recipient => recipient + .NotEmpty() + .WithMessage("Recipient email must not be empty") + .EmailAddress() + .WithMessage("Invalid email address format")); RuleFor(x => x.Subject) .NotEmpty() diff --git a/src/core/DigitalData.MessagingService.Publisher.Abstraction/OutgoingEmailEvent.cs b/src/core/DigitalData.MessagingService.Publisher.Abstraction/OutgoingEmailEvent.cs index a7e338e..971800b 100644 --- a/src/core/DigitalData.MessagingService.Publisher.Abstraction/OutgoingEmailEvent.cs +++ b/src/core/DigitalData.MessagingService.Publisher.Abstraction/OutgoingEmailEvent.cs @@ -11,7 +11,7 @@ public record OutgoingEmailEvent /// /// Recipient email address /// - public string Recipient { get; set; } = null!; + public IEnumerable Recipients { get; set; } = null!; /// /// Email subject diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Queue/OutgoingEmailConsumer.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Queue/OutgoingEmailConsumer.cs index b558c58..6e5aa7c 100644 --- a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Queue/OutgoingEmailConsumer.cs +++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Queue/OutgoingEmailConsumer.cs @@ -49,7 +49,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.Recipients, oMailEvent.Subject, oMailEvent.Body, isHtml: oMailEvent.IsHtml); @@ -65,7 +65,7 @@ public sealed class OutgoingEmailConsumer : IAsyncDisposable } catch (Exception ex) { - logger?.LogError(ex, "Failed to process email [To={To}, Subject={Subject}] message: DeliveryTag={DeliveryTag}. Moving to DLQ (NO retry).", oMailEvent?.Recipient, oMailEvent?.Subject, args.DeliveryTag); + logger?.LogError(ex, "Failed to process email [To={To}, Subject={Subject}] message: DeliveryTag={DeliveryTag}. Moving to DLQ (NO retry).", oMailEvent?.Recipients, oMailEvent?.Subject, args.DeliveryTag); // TODO: Error Reporting Strategy // Option 1: Separate RabbitMQ Queue (emailprofiler.errors) diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsEmailService.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsEmailService.cs index 94d3f33..cbd3321 100644 --- a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsEmailService.cs +++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsEmailService.cs @@ -27,7 +27,7 @@ public class LimilabsEmailService( Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); } - public async Task SendEmailAsync(EmailAccountDto from, string to, string subject, string body, bool isHtml = true, CancellationToken cancellationToken = default) + public async Task SendEmailAsync(EmailAccountDto from, IEnumerable recipients, string subject, string body, bool isHtml = true, CancellationToken cancellationToken = default) { using var smtp = new Smtp(); @@ -37,7 +37,10 @@ public class LimilabsEmailService( var builder = new MailBuilder(); builder.From.Add(new MailBox(from.Username)); - builder.To.Add(new MailBox(to)); + + foreach (var recipient in recipients) + builder.To.Add(new MailBox(recipient)); + builder.Subject = subject; if (isHtml) diff --git a/src/presentation/DigitalData.MessagingService.API/Controllers/EmailsController.cs b/src/presentation/DigitalData.MessagingService.API/Controllers/EmailsController.cs index 1ce5ad4..387cecc 100644 --- a/src/presentation/DigitalData.MessagingService.API/Controllers/EmailsController.cs +++ b/src/presentation/DigitalData.MessagingService.API/Controllers/EmailsController.cs @@ -27,10 +27,7 @@ public class EmailsController(IMediator mediator) : ControllerBase return Accepted(new { - CommandId = outgoingEmailEvent.Id, - To = outgoingEmailEvent.Recipient, - outgoingEmailEvent.Subject, - outgoingEmailEvent.QueuedAt + outgoingEmailEvent.Id, }); } }