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.
This commit is contained in:
@@ -1,27 +0,0 @@
|
|||||||
namespace DigitalData.MessagingService.Application.Common.Dtos;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DTO for a single email account configuration.
|
|
||||||
/// </summary>
|
|
||||||
public class EmailAccountDto
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Logical name to identify this account (e.g. "default", "support").
|
|
||||||
/// </summary>
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using DigitalData.MessagingService.Application.Common.Dtos;
|
||||||
|
|
||||||
namespace DigitalData.MessagingService.Application.Common.Interfaces;
|
namespace DigitalData.MessagingService.Application.Common.Interfaces;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -12,5 +14,5 @@ public interface IEmailService
|
|||||||
/// Sends an email using the configured SMTP account.
|
/// Sends an email using the configured SMTP account.
|
||||||
/// SMTP credentials are configured in appsettings.json (EmailAccount section).
|
/// SMTP credentials are configured in appsettings.json (EmailAccount section).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ public class EmailMappingProfile : Profile
|
|||||||
public EmailMappingProfile()
|
public EmailMappingProfile()
|
||||||
{
|
{
|
||||||
// SendEmailCommand -> OutgoingEmailEvent
|
// SendEmailCommand -> OutgoingEmailEvent
|
||||||
|
// Sender is resolved via MediatR in the handler and set separately after mapping.
|
||||||
CreateMap<SendEmailCommand, OutgoingEmailEvent>()
|
CreateMap<SendEmailCommand, OutgoingEmailEvent>()
|
||||||
.ForMember(dest => dest.Id, opt => opt.MapFrom(_ => Guid.NewGuid()))
|
.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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,4 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Common\Dtos\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -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<EmailAccountDto?>
|
||||||
|
{
|
||||||
|
public int? Id { get; init; }
|
||||||
|
|
||||||
|
public string? Username { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Options"></param>
|
||||||
|
/// <param name="Logger"></param>
|
||||||
|
public class GetSenderQueryHandler(IOptions<EmailAccountsOptions> Options, ILogger<GetSenderQueryHandler> Logger) : IRequestHandler<GetSenderQuery, EmailAccountDto?>
|
||||||
|
{
|
||||||
|
public Task<EmailAccountDto?> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using DigitalData.MessagingService.Application.EmailAccount.Queries;
|
||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace DigitalData.MessagingService.Application.EmailAccount.Validators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validator for <see cref="GetSenderQuery"/>.
|
||||||
|
/// Either <see cref="GetSenderQuery.Id"/> or <see cref="GetSenderQuery.Username"/> must be provided, but not both.
|
||||||
|
/// </summary>
|
||||||
|
public class GetSenderQueryValidator : AbstractValidator<GetSenderQuery>
|
||||||
|
{
|
||||||
|
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.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
using AutoMapper;
|
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 DigitalData.MessagingService.Publisher.Abstraction;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
@@ -9,6 +12,8 @@ namespace DigitalData.MessagingService.Application.EmailSending.Commands;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public record SendEmailCommand : IRequest<OutgoingEmailEvent>
|
public record SendEmailCommand : IRequest<OutgoingEmailEvent>
|
||||||
{
|
{
|
||||||
|
public required GetSenderQuery Sender { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Recipient email address
|
/// Recipient email address
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -32,13 +37,20 @@ public record SendEmailCommand : IRequest<OutgoingEmailEvent>
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handler for SendEmailCommand
|
/// 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
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SendEmailCommandHandler(IOutgoingEmailPublisher Publisher, IMapper Mapper) : IRequestHandler<SendEmailCommand, OutgoingEmailEvent>
|
public class SendEmailCommandHandler(
|
||||||
|
ISender Sender,
|
||||||
|
IOutgoingEmailPublisher Publisher,
|
||||||
|
IMapper Mapper) : IRequestHandler<SendEmailCommand, OutgoingEmailEvent>
|
||||||
{
|
{
|
||||||
public async Task<OutgoingEmailEvent> Handle(SendEmailCommand request, CancellationToken cancellationToken)
|
public async Task<OutgoingEmailEvent> Handle(SendEmailCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var outgoingEmailEvent = Mapper.Map<OutgoingEmailEvent>(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<OutgoingEmailEvent>(request) with { Sender = senderAccount };
|
||||||
|
|
||||||
// Enqueue to RabbitMQ
|
// Enqueue to RabbitMQ
|
||||||
await Publisher.EnqueueAsync(outgoingEmailEvent, cancellationToken);
|
await Publisher.EnqueueAsync(outgoingEmailEvent, cancellationToken);
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
namespace DigitalData.MessagingService.Application.Common.Dtos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DTO for a single email account configuration.
|
||||||
|
/// </summary>
|
||||||
|
public class EmailAccountDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Logical name to identify this account (e.g. "default", "support").
|
||||||
|
/// </summary>
|
||||||
|
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; }
|
||||||
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
namespace DigitalData.MessagingService.Publisher.Abstraction;
|
using DigitalData.MessagingService.Application.Common.Dtos;
|
||||||
|
|
||||||
|
namespace DigitalData.MessagingService.Publisher.Abstraction;
|
||||||
|
|
||||||
public record OutgoingEmailEvent
|
public record OutgoingEmailEvent
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public EmailAccountDto Sender { get; set; } = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Recipient email address
|
/// Recipient email address
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ public sealed class OutgoingEmailConsumer : IAsyncDisposable
|
|||||||
{
|
{
|
||||||
// Send email via SMTP (SMTP config is injected in IEmailService via IOptions)
|
// Send email via SMTP (SMTP config is injected in IEmailService via IOptions)
|
||||||
await EmailService.SendEmailAsync(
|
await EmailService.SendEmailAsync(
|
||||||
|
oMailEvent.Sender,
|
||||||
oMailEvent.Recipient,
|
oMailEvent.Recipient,
|
||||||
oMailEvent.Subject,
|
oMailEvent.Subject,
|
||||||
oMailEvent.Body,
|
oMailEvent.Body,
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ namespace DigitalData.MessagingService.Infrastructure.Services;
|
|||||||
/// or falls back to the first account if none is named "default".
|
/// or falls back to the first account if none is named "default".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LimilabsEmailService(
|
public class LimilabsEmailService(
|
||||||
IEncryptionService encryptionService,
|
IEncryptionService encryptionService) : IEmailService
|
||||||
IOptions<EmailAccountsOptions> smtpConfig) : IEmailService
|
|
||||||
{
|
{
|
||||||
// Register encoding provider for Limilabs (requires windows-1252 and other code pages)
|
// Register encoding provider for Limilabs (requires windows-1252 and other code pages)
|
||||||
static LimilabsEmailService()
|
static LimilabsEmailService()
|
||||||
@@ -28,18 +27,16 @@ public class LimilabsEmailService(
|
|||||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
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();
|
using var smtp = new Smtp();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await ConnectAndAuthenticateSmtpAsync(smtp, smtpAccount);
|
await ConnectAndAuthenticateSmtpAsync(smtp, from);
|
||||||
|
|
||||||
var builder = new MailBuilder();
|
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.To.Add(new MailBox(to));
|
||||||
builder.Subject = subject;
|
builder.Subject = subject;
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"EmailAccounts": {
|
"EmailAccounts": {
|
||||||
"Accounts": [
|
"Accounts": [
|
||||||
{
|
{
|
||||||
"Name": "1",
|
"Id": "1",
|
||||||
"Username": "test-flow@digitaldata.works",
|
"Username": "test-flow@digitaldata.works",
|
||||||
"Password": "ddemail108",
|
"Password": "ddemail108",
|
||||||
"PasswordEncrypted": false,
|
"PasswordEncrypted": false,
|
||||||
|
|||||||
Reference in New Issue
Block a user