refactor: Rename EmailPorifler to MessagingService
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
namespace DigitalData.MessagingService.Application.Common.Dtos;
|
||||
|
||||
/// <summary>
|
||||
/// DTO for EmailAccount query results.
|
||||
/// </summary>
|
||||
public class EmailAccountDto
|
||||
{
|
||||
public string Username { get; set; } = null!;
|
||||
|
||||
public string Password { get; set; } = null!;
|
||||
|
||||
public bool PasswordEncrypted { get; set; } = false;
|
||||
|
||||
public string SmtpServer { get; set; } = null!;
|
||||
|
||||
public int SmtpPort { get; set; }
|
||||
|
||||
public bool SmtpUseSsl { get; set; }
|
||||
|
||||
public bool UseOAuth2 { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace DigitalData.MessagingService.Application.Common.Events;
|
||||
|
||||
public class OutgoingEmailEvent
|
||||
{
|
||||
public required Guid Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Recipient email address
|
||||
/// </summary>
|
||||
public required string Recipient { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Email subject
|
||||
/// </summary>
|
||||
public required string Subject { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Email body (HTML or plain text)
|
||||
/// </summary>
|
||||
public required string Body { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Is HTML email (default: true)
|
||||
/// </summary>
|
||||
public bool IsHtml { get; init; } = true;
|
||||
|
||||
public DateTime QueuedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using MediatR;
|
||||
|
||||
namespace DigitalData.MessagingService.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for publishing commands to a message broker (e.g., RabbitMQ)
|
||||
/// </summary>
|
||||
public interface ICommandPublisher
|
||||
{
|
||||
/// <summary>
|
||||
/// Publishes a command to the message broker for asynchronous processing
|
||||
/// </summary>
|
||||
/// <typeparam name="TCommand">The command type (must implement IBaseRequest - covers both IRequest and IRequest<T>)</typeparam>
|
||||
/// <param name="command">The command to publish</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Task representing the publish operation</returns>
|
||||
Task PublishAsync<TCommand>(TCommand command, CancellationToken cancellationToken = default)
|
||||
where TCommand : IBaseRequest;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace DigitalData.MessagingService.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Email service interface for SMTP operations.
|
||||
/// Implementation uses Limilabs Mail.dll for production email sending.
|
||||
/// SMTP configuration is injected via IOptions<EmailAccountDto> in appsettings.json.
|
||||
/// Throws AuthenticationFailedException when SMTP authentication fails.
|
||||
/// </summary>
|
||||
public interface IEmailService
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends an email using the configured SMTP account.
|
||||
/// SMTP credentials are configured in appsettings.json (EmailAccount section).
|
||||
/// </summary>
|
||||
Task SendEmailAsync(string to, string subject, string body, bool isHtml = true, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace DigitalData.MessagingService.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Encryption service interface for password encryption.
|
||||
/// </summary>
|
||||
public interface IEncryptionService
|
||||
{
|
||||
string Encrypt(string plainText);
|
||||
string Decrypt(string cipherText);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using DigitalData.MessagingService.Application.Common.Events;
|
||||
using DigitalData.MessagingService.Application.EmailSending.Commands;
|
||||
|
||||
namespace DigitalData.MessagingService.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Email queue interface for outgoing emails.
|
||||
/// </summary>
|
||||
public interface IOutgoingEmailQueue
|
||||
{
|
||||
Task EnqueueAsync(OutgoingEmailEvent outgoingEmailEvent, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<int> GetQueueDepthAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace DigitalData.MessagingService.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// PDF processing service interface.
|
||||
/// Operates on streams instead of file paths for flexibility.
|
||||
/// </summary>
|
||||
public interface IPdfProcessingService
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates if the provided stream contains a valid PDF document.
|
||||
/// Throws InvalidPdfException if the stream is not a valid PDF.
|
||||
/// </summary>
|
||||
Task<bool> ValidatePdfAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts embedded files from PDF stream to the specified output directory.
|
||||
/// Returns a list of paths to extracted files.
|
||||
/// </summary>
|
||||
Task<IEnumerable<string>> ExtractEmbeddedFilesAsync(Stream pdfStream, string outputDirectory, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the page count of the PDF document.
|
||||
/// Throws InvalidPdfException if the stream is not a valid PDF.
|
||||
/// </summary>
|
||||
Task<int> GetPageCountAsync(Stream pdfStream, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace DigitalData.MessagingService.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Generic repository interface for CRUD operations.
|
||||
/// All operations auto-save changes - NO explicit SaveChangesAsync needed!
|
||||
/// </summary>
|
||||
public interface IRepository<TEntity> where TEntity : class
|
||||
{
|
||||
// CREATE
|
||||
Task<TEntity> CreateAsync<TDto>(TDto dto, CancellationToken cancellationToken = default);
|
||||
|
||||
// READ
|
||||
Task<TEntity?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<TEntity>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<TEntity>> FindAsync(Expression<Func<TEntity, bool>> predicate, int? skip = null, int? take = null, CancellationToken cancellationToken = default);
|
||||
Task<TEntity?> FindFirstAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
|
||||
Task<TEntity?> FindSingleAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
|
||||
Task<int> CountAsync(Expression<Func<TEntity, bool>>? predicate = null, CancellationToken cancellationToken = default);
|
||||
Task<bool> AnyAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
|
||||
|
||||
// UPDATE
|
||||
Task UpdateSingleAsync<TDto>(Expression<Func<TEntity, bool>> predicate, TDto dto, CancellationToken cancellationToken = default);
|
||||
Task<int> UpdateAsync<TDto>(Expression<Func<TEntity, bool>> predicate, TDto dto, CancellationToken cancellationToken = default);
|
||||
|
||||
// DELETE
|
||||
Task DeleteSingleAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
|
||||
Task<int> DeleteAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.MessagingService.Application.Common.Events;
|
||||
using DigitalData.MessagingService.Application.EmailSending.Commands;
|
||||
|
||||
namespace DigitalData.MessagingService.Application.Common.Mappings;
|
||||
|
||||
/// <summary>
|
||||
/// AutoMapper profile for Emails
|
||||
/// </summary>
|
||||
public class EmailMappingProfile : Profile
|
||||
{
|
||||
public EmailMappingProfile()
|
||||
{
|
||||
// SendEmailCommand -> OutgoingEmailEvent
|
||||
CreateMap<SendEmailCommand, OutgoingEmailEvent>()
|
||||
.ForMember(dest => dest.Id, opt => opt.MapFrom(_ => Guid.NewGuid()))
|
||||
.ForMember(dest => dest.QueuedAt, opt => opt.MapFrom(_ => DateTime.Now));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Reflection;
|
||||
using FluentValidation;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DigitalData.MessagingService.Application;
|
||||
|
||||
/// <summary>
|
||||
/// Dependency injection configuration for Application layer.
|
||||
/// </summary>
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddApplicationServices(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
// Read LuckyPennySoft license key from appsettings.json
|
||||
var licenseKey = configuration.GetValue<string>("LuckyPennySoftLicenseKey")
|
||||
?? throw new InvalidOperationException("LuckyPennySoftLicenseKey not found in configuration");
|
||||
|
||||
// MediatR - Register all handlers
|
||||
services.AddMediatR(config =>
|
||||
{
|
||||
config.LicenseKey = licenseKey;
|
||||
config.RegisterServicesFromAssembly(assembly);
|
||||
});
|
||||
|
||||
// AutoMapper - Use built-in DI extension (AutoMapper 16.2.0+)
|
||||
services.AddAutoMapper(config =>
|
||||
{
|
||||
config.LicenseKey = licenseKey;
|
||||
config.AddMaps(assembly);
|
||||
});
|
||||
|
||||
// FluentValidation - Register all validators
|
||||
services.AddValidatorsFromAssembly(assembly);
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DigitalData.MessagingService.Domain\DigitalData.MessagingService.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="16.2.0" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
|
||||
<PackageReference Include="MediatR" Version="14.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" />
|
||||
<PackageReference Include="MimeKit" Version="4.17.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Common\Dtos\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,48 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.MessagingService.Application.Common.Events;
|
||||
using DigitalData.MessagingService.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
|
||||
namespace DigitalData.MessagingService.Application.EmailSending.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Command to send an email (enqueue to RabbitMQ)
|
||||
/// </summary>
|
||||
public record SendEmailCommand : IRequest<OutgoingEmailEvent>
|
||||
{
|
||||
/// <summary>
|
||||
/// Recipient email address
|
||||
/// </summary>
|
||||
public required string Recipient { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Email subject
|
||||
/// </summary>
|
||||
public required string Subject { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Email body (HTML or plain text)
|
||||
/// </summary>
|
||||
public required string Body { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Is HTML email (default: true)
|
||||
/// </summary>
|
||||
public bool IsHtml { get; init; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for SendEmailCommand
|
||||
/// Creates EmailOutbox entity via AutoMapper and enqueues to RabbitMQ
|
||||
/// </summary>
|
||||
public class SendEmailCommandHandler(IOutgoingEmailQueue EmailQueue, IMapper Mapper) : IRequestHandler<SendEmailCommand, OutgoingEmailEvent>
|
||||
{
|
||||
public async Task<OutgoingEmailEvent> Handle(SendEmailCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var outgoingEmailEvent = Mapper.Map<OutgoingEmailEvent>(request);
|
||||
|
||||
// Enqueue to RabbitMQ
|
||||
await EmailQueue.EnqueueAsync(outgoingEmailEvent, cancellationToken);
|
||||
return outgoingEmailEvent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using DigitalData.MessagingService.Application.EmailSending.Commands;
|
||||
using FluentValidation;
|
||||
|
||||
namespace DigitalData.MessagingService.Application.EmailSending.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Validator for SendEmailCommand
|
||||
/// </summary>
|
||||
public class SendEmailCommandValidator : AbstractValidator<SendEmailCommand>
|
||||
{
|
||||
public SendEmailCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Recipient)
|
||||
.NotEmpty()
|
||||
.WithMessage("Recipient is required")
|
||||
.MaximumLength(200)
|
||||
.WithMessage("Recipient must not exceed 200 characters")
|
||||
.EmailAddress()
|
||||
.WithMessage("Recipient must be a valid email address");
|
||||
|
||||
RuleFor(x => x.Subject)
|
||||
.NotEmpty()
|
||||
.WithMessage("Subject is required")
|
||||
.MaximumLength(500)
|
||||
.WithMessage("Subject must not exceed 500 characters");
|
||||
|
||||
RuleFor(x => x.Body)
|
||||
.NotEmpty()
|
||||
.WithMessage("Body is required");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user