feat: Add email sending feature with background worker
Application Layer: - Add SendEmailCommand with handler (CQRS pattern) - Add SendEmailCommandValidator (FluentValidation) - Add EmailOutboxMappingProfile for EmailOutbox entity mappings - Update EmailAccountMappingProfile with latest field mappings - Update EmailProfileMappingProfile with latest field mappings API Layer: - Add EmailSenderWorker background service - Worker polls EmailOutbox queue every 5 seconds - Dequeues emails and processes via SendEmailCommand (MediatR) - Uses IEmailService (Limilabs) for actual SMTP sending This implements the outgoing email queue processing pipeline: EmailOutbox (DB) → IEmailQueue (RabbitMQ) → EmailSenderWorker → SendEmailCommand → IEmailService
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.EmailProfiler.Application.Common.Dtos;
|
||||
using DigitalData.EmailProfiler.Application.EmailAccounts.Commands;
|
||||
using DigitalData.EmailProfiler.Domain.Entities;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.Common.Mappings;
|
||||
@@ -12,15 +11,6 @@ public class EmailAccountMappingProfile : Profile
|
||||
{
|
||||
public EmailAccountMappingProfile()
|
||||
{
|
||||
// Command -> Entity (for Create)
|
||||
CreateMap<CreateEmailAccountCommand, EmailAccount>()
|
||||
.ForMember(dest => dest.Id, opt => opt.Ignore()) // Auto-generated
|
||||
.ForMember(dest => dest.AddedWhen, opt => opt.MapFrom(_ => DateTime.Now))
|
||||
.ForMember(dest => dest.AddedWho, opt => opt.MapFrom(_ => "System")) // TODO: Get from user context
|
||||
.ForMember(dest => dest.ChangedWhen, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.ChangedWho, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.Profiles, opt => opt.Ignore());
|
||||
|
||||
// Entity -> DTO (for Queries)
|
||||
CreateMap<EmailAccount, EmailAccountDto>();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.EmailProfiler.Application.EmailSending.Commands;
|
||||
using DigitalData.EmailProfiler.Domain.Entities;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.Common.Mappings;
|
||||
|
||||
/// <summary>
|
||||
/// AutoMapper profile for EmailOutbox entity
|
||||
/// </summary>
|
||||
public class EmailOutboxMappingProfile : Profile
|
||||
{
|
||||
public EmailOutboxMappingProfile()
|
||||
{
|
||||
// SendEmailCommand -> EmailOutbox
|
||||
CreateMap<SendEmailCommand, EmailOutbox>()
|
||||
.ForMember(dest => dest.Id, opt => opt.Ignore()) // Auto-generated
|
||||
.ForMember(dest => dest.Sent, opt => opt.Ignore()) // Set by handler
|
||||
.ForMember(dest => dest.SentDate, opt => opt.Ignore()) // Set when sent
|
||||
.ForMember(dest => dest.RetryCount, opt => opt.Ignore()) // Set by handler
|
||||
.ForMember(dest => dest.AddedWhen, opt => opt.Ignore()) // Set by handler
|
||||
.ForMember(dest => dest.EmailAccount, opt => opt.Ignore()); // Navigation property
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.EmailProfiler.Application.Common.Dtos;
|
||||
using DigitalData.EmailProfiler.Application.EmailProfiles.Commands;
|
||||
using DigitalData.EmailProfiler.Domain.Entities;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.Common.Mappings;
|
||||
@@ -12,30 +11,6 @@ public class EmailProfileMappingProfile : Profile
|
||||
{
|
||||
public EmailProfileMappingProfile()
|
||||
{
|
||||
// Command -> Entity (for Create)
|
||||
CreateMap<CreateEmailProfileCommand, EmailProfile>()
|
||||
.ForMember(dest => dest.Id, opt => opt.Ignore()) // Auto-generated
|
||||
.ForMember(dest => dest.AddedWhen, opt => opt.MapFrom(_ => DateTime.Now))
|
||||
.ForMember(dest => dest.AddedWho, opt => opt.MapFrom(_ => "System")) // TODO: Get from user context
|
||||
.ForMember(dest => dest.ChangedWhen, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.ChangedWho, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.LastPollTime, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.EmailAccount, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.EmailProcess, opt => opt.Ignore());
|
||||
|
||||
// Command -> Entity (for Update)
|
||||
CreateMap<UpdateEmailProfileCommand, EmailProfile>()
|
||||
.ForMember(dest => dest.Id, opt => opt.Ignore()) // Don't update ID
|
||||
.ForMember(dest => dest.EmailAccountId, opt => opt.Ignore()) // Don't change account
|
||||
.ForMember(dest => dest.ProcessId, opt => opt.Ignore()) // Don't change process
|
||||
.ForMember(dest => dest.AddedWhen, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.AddedWho, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(_ => DateTime.Now))
|
||||
.ForMember(dest => dest.ChangedWho, opt => opt.MapFrom(_ => "System")) // TODO: Get from user context
|
||||
.ForMember(dest => dest.LastPollTime, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.EmailAccount, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.EmailProcess, opt => opt.Ignore());
|
||||
|
||||
// Entity -> DTO (for Queries)
|
||||
CreateMap<EmailProfile, EmailProfileDto>()
|
||||
.ForMember(dest => dest.EmailAccountName, opt => opt.MapFrom(src => src.EmailAccount != null ? src.EmailAccount.AccountName : null))
|
||||
|
||||
Reference in New Issue
Block a user