refactor(application): Remove old CQRS commands/queries for minimal API migration
- Remove EmailAccounts CQRS layer (4 files: commands, queries, validators) - Remove EmailHistories CQRS layer (2 files: queries) - Remove EmailProcessing CQRS layer (2 files: commands, validators) - Remove EmailProfiles CQRS layer (8 files: commands, queries, validators) - Remove corresponding API controllers (3 files) Total: 19 files removed Reason: Migrating from full CQRS pattern to minimal API with direct repository access Note: SendEmailCommand will be added separately for EmailSenderWorker
This commit is contained in:
@@ -1,70 +0,0 @@
|
||||
using DigitalData.EmailProfiler.Application.Common.Dtos;
|
||||
using DigitalData.EmailProfiler.Application.Common.Interfaces;
|
||||
using DigitalData.EmailProfiler.Application.EmailAccounts.Commands;
|
||||
using DigitalData.EmailProfiler.Application.EmailAccounts.Queries;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DigitalData.EmailProfiler.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Email accounts management API controller
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class EmailAccountsController(
|
||||
IMediator mediator,
|
||||
ICommandPublisher commandPublisher) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Get all email accounts
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(IEnumerable<EmailAccountDto>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetAll(CancellationToken cancellationToken)
|
||||
{
|
||||
var query = new GetEmailAccountsQuery();
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get email account by ID
|
||||
/// </summary>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(typeof(EmailAccountDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetById(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = new GetEmailAccountByIdQuery(id);
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
if (result == null)
|
||||
return NotFound(new { Message = $"Email account with ID {id} not found" });
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create new email account (async via RabbitMQ)
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status202Accepted)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> Create(
|
||||
[FromBody] CreateEmailAccountCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Publish command to RabbitMQ for async processing
|
||||
await commandPublisher.PublishAsync(command, cancellationToken);
|
||||
|
||||
return Accepted(new
|
||||
{
|
||||
Message = "Email account creation request queued for processing",
|
||||
AccountName = command.AccountName
|
||||
});
|
||||
}
|
||||
|
||||
// Note: Update and Delete operations can be added similarly
|
||||
// For now, we focus on Create as the main use case
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
using DigitalData.EmailProfiler.Application.Common.Dtos.EmailHistories;
|
||||
using DigitalData.EmailProfiler.Application.EmailHistories.Queries;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DigitalData.EmailProfiler.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Email history API controller (read-only)
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class EmailHistoryController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Get email history by profile ID with pagination
|
||||
/// </summary>
|
||||
[HttpGet("profile/{profileId:int}")]
|
||||
[ProducesResponseType(typeof(IEnumerable<EmailHistoryDto>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetByProfile(
|
||||
int profileId,
|
||||
[FromQuery] int pageNumber = 1,
|
||||
[FromQuery] int pageSize = 50,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = new GetEmailHistoryByProfileQuery(profileId, pageNumber, pageSize);
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
ProfileId = profileId,
|
||||
PageNumber = pageNumber,
|
||||
PageSize = pageSize,
|
||||
Data = result
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get email history by ID
|
||||
/// </summary>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(typeof(EmailHistoryDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetById(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = new GetEmailHistoryByIdQuery(id);
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
if (result == null)
|
||||
return NotFound(new { Message = $"Email history with ID {id} not found" });
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// Note: Email history is typically managed by ProcessEmailCommand
|
||||
// No direct CREATE/UPDATE/DELETE endpoints needed
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
using DigitalData.EmailProfiler.Application.Common.Dtos;
|
||||
using DigitalData.EmailProfiler.Application.Common.Interfaces;
|
||||
using DigitalData.EmailProfiler.Application.EmailProfiles.Commands;
|
||||
using DigitalData.EmailProfiler.Application.EmailProfiles.Queries;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DigitalData.EmailProfiler.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Email profiles management API controller
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class EmailProfilesController(
|
||||
IMediator mediator,
|
||||
ICommandPublisher commandPublisher) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Get all email profiles
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(IEnumerable<EmailProfileDto>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetAll(CancellationToken cancellationToken)
|
||||
{
|
||||
var query = new GetEmailProfilesQuery();
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get email profile by ID
|
||||
/// </summary>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(typeof(EmailProfileDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetById(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = new GetEmailProfileByIdQuery(id);
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
if (result == null)
|
||||
return NotFound(new { Message = $"Email profile with ID {id} not found" });
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all active email profiles
|
||||
/// </summary>
|
||||
[HttpGet("active")]
|
||||
[ProducesResponseType(typeof(IEnumerable<EmailProfileDto>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetActive(CancellationToken cancellationToken)
|
||||
{
|
||||
var query = new GetActiveEmailProfilesQuery();
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create new email profile (async via RabbitMQ)
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status202Accepted)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> Create(
|
||||
[FromBody] CreateEmailProfileCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Publish command to RabbitMQ for async processing
|
||||
await commandPublisher.PublishAsync(command, cancellationToken);
|
||||
|
||||
return Accepted(new
|
||||
{
|
||||
Message = "Email profile creation request queued for processing",
|
||||
ProfileName = command.ProfileName
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update email profile (async via RabbitMQ)
|
||||
/// </summary>
|
||||
[HttpPut("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status202Accepted)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> Update(
|
||||
int id,
|
||||
[FromBody] UpdateEmailProfileCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Ensure ID matches route
|
||||
if (id != command.Id)
|
||||
return BadRequest(new { Message = "Route ID does not match command ID" });
|
||||
|
||||
// Publish command to RabbitMQ for async processing
|
||||
await commandPublisher.PublishAsync(command, cancellationToken);
|
||||
|
||||
return Accepted(new
|
||||
{
|
||||
Message = "Email profile update request queued for processing",
|
||||
Id = command.Id
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete email profile (async via RabbitMQ)
|
||||
/// </summary>
|
||||
[HttpDelete("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status202Accepted)]
|
||||
public async Task<IActionResult> Delete(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var command = new DeleteEmailProfileCommand(id);
|
||||
|
||||
// Publish command to RabbitMQ for async processing
|
||||
await commandPublisher.PublishAsync(command, cancellationToken);
|
||||
|
||||
return Accepted(new
|
||||
{
|
||||
Message = "Email profile deletion request queued for processing",
|
||||
Id = id
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
|
||||
using DigitalData.EmailProfiler.Domain.Entities;
|
||||
using MediatR;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.EmailAccounts.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Command to create a new email account.
|
||||
/// </summary>
|
||||
public record CreateEmailAccountCommand : IRequest<int>
|
||||
{
|
||||
public string AccountName { get; init; } = string.Empty;
|
||||
public string Username { get; init; } = string.Empty;
|
||||
public string ImapServer { get; init; } = string.Empty;
|
||||
public int ImapPort { get; init; } = 993;
|
||||
public bool ImapUseSsl { get; init; } = true;
|
||||
public string SmtpServer { get; init; } = string.Empty;
|
||||
public int SmtpPort { get; init; } = 587;
|
||||
public bool SmtpUseSsl { get; init; } = true;
|
||||
public bool UseOAuth2 { get; init; }
|
||||
public string? EncryptedPassword { get; init; } // Already encrypted by client
|
||||
public string? TenantId { get; init; }
|
||||
public string? ClientId { get; init; }
|
||||
public string? EncryptedClientSecret { get; init; } // Already encrypted by client
|
||||
public bool IsActive { get; init; } = true;
|
||||
}
|
||||
|
||||
public class CreateEmailAccountCommandHandler(IRepository<EmailAccount> repository)
|
||||
: IRequestHandler<CreateEmailAccountCommand, int>
|
||||
{
|
||||
public async Task<int> Handle(CreateEmailAccountCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var account = await repository.CreateAsync(request, cancellationToken);
|
||||
return account.Id;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.EmailProfiler.Application.Common.Dtos;
|
||||
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
|
||||
using DigitalData.EmailProfiler.Domain.Entities;
|
||||
using MediatR;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.EmailAccounts.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Query to get email account by ID.
|
||||
/// </summary>
|
||||
public record GetEmailAccountByIdQuery(int Id) : IRequest<EmailAccountDto?>;
|
||||
|
||||
public class GetEmailAccountByIdQueryHandler(
|
||||
IRepository<EmailAccount> repository,
|
||||
IMapper mapper)
|
||||
: IRequestHandler<GetEmailAccountByIdQuery, EmailAccountDto?>
|
||||
{
|
||||
public async Task<EmailAccountDto?> Handle(
|
||||
GetEmailAccountByIdQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var account = await repository.GetByIdAsync(request.Id, cancellationToken);
|
||||
return account != null ? mapper.Map<EmailAccountDto>(account) : null;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.EmailProfiler.Application.Common.Dtos;
|
||||
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
|
||||
using DigitalData.EmailProfiler.Domain.Entities;
|
||||
using MediatR;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.EmailAccounts.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Query to get all email accounts.
|
||||
/// </summary>
|
||||
public record GetEmailAccountsQuery : IRequest<IEnumerable<EmailAccountDto>>;
|
||||
|
||||
public class GetEmailAccountsQueryHandler(
|
||||
IRepository<EmailAccount> repository,
|
||||
IMapper mapper)
|
||||
: IRequestHandler<GetEmailAccountsQuery, IEnumerable<EmailAccountDto>>
|
||||
{
|
||||
public async Task<IEnumerable<EmailAccountDto>> Handle(
|
||||
GetEmailAccountsQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var accounts = await repository.GetAllAsync(cancellationToken);
|
||||
return mapper.Map<IEnumerable<EmailAccountDto>>(accounts);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
using DigitalData.EmailProfiler.Application.EmailAccounts.Commands;
|
||||
using FluentValidation;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.EmailAccounts.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Validator for CreateEmailAccountCommand.
|
||||
/// </summary>
|
||||
public class CreateEmailAccountCommandValidator : AbstractValidator<CreateEmailAccountCommand>
|
||||
{
|
||||
public CreateEmailAccountCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.AccountName)
|
||||
.NotEmpty().WithMessage("Account name is required")
|
||||
.MaximumLength(100).WithMessage("Account name must not exceed 100 characters");
|
||||
|
||||
RuleFor(x => x.Username)
|
||||
.NotEmpty().WithMessage("Username is required")
|
||||
.MaximumLength(200).WithMessage("Username must not exceed 200 characters")
|
||||
.EmailAddress().WithMessage("Username must be a valid email address");
|
||||
|
||||
RuleFor(x => x.ImapServer)
|
||||
.NotEmpty().WithMessage("IMAP server is required")
|
||||
.MaximumLength(200).WithMessage("IMAP server must not exceed 200 characters");
|
||||
|
||||
RuleFor(x => x.ImapPort)
|
||||
.GreaterThan(0).WithMessage("IMAP port must be greater than 0")
|
||||
.LessThanOrEqualTo(65535).WithMessage("IMAP port must not exceed 65535");
|
||||
|
||||
RuleFor(x => x.SmtpServer)
|
||||
.NotEmpty().WithMessage("SMTP server is required")
|
||||
.MaximumLength(200).WithMessage("SMTP server must not exceed 200 characters");
|
||||
|
||||
RuleFor(x => x.SmtpPort)
|
||||
.GreaterThan(0).WithMessage("SMTP port must be greater than 0")
|
||||
.LessThanOrEqualTo(65535).WithMessage("SMTP port must not exceed 65535");
|
||||
|
||||
// OAuth2 validation
|
||||
RuleFor(x => x.TenantId)
|
||||
.NotEmpty().WithMessage("Tenant ID is required for OAuth2")
|
||||
.When(x => x.UseOAuth2);
|
||||
|
||||
RuleFor(x => x.ClientId)
|
||||
.NotEmpty().WithMessage("Client ID is required for OAuth2")
|
||||
.When(x => x.UseOAuth2);
|
||||
|
||||
RuleFor(x => x.EncryptedClientSecret)
|
||||
.NotEmpty().WithMessage("Client secret is required for OAuth2")
|
||||
.When(x => x.UseOAuth2);
|
||||
|
||||
// Password validation (non-OAuth2)
|
||||
RuleFor(x => x.EncryptedPassword)
|
||||
.NotEmpty().WithMessage("Password is required")
|
||||
.When(x => !x.UseOAuth2);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.EmailProfiler.Application.Common.Dtos.EmailHistories;
|
||||
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.EmailHistories.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Query to get email history by ID with attachments.
|
||||
/// </summary>
|
||||
public record GetEmailHistoryByIdQuery(int Id) : IRequest<EmailHistoryDto?>;
|
||||
|
||||
public class GetEmailHistoryByIdQueryHandler(
|
||||
IEmailHistoryRepository repository,
|
||||
IMapper mapper)
|
||||
: IRequestHandler<GetEmailHistoryByIdQuery, EmailHistoryDto?>
|
||||
{
|
||||
public async Task<EmailHistoryDto?> Handle(
|
||||
GetEmailHistoryByIdQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var history = await repository.GetWithAttachmentsAsync(request.Id, cancellationToken);
|
||||
return history != null ? mapper.Map<EmailHistoryDto>(history) : null;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.EmailProfiler.Application.Common.Dtos.EmailHistories;
|
||||
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.EmailHistories.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Query to get email history by profile with pagination.
|
||||
/// </summary>
|
||||
public record GetEmailHistoryByProfileQuery(
|
||||
int ProfileId,
|
||||
int PageNumber = 1,
|
||||
int PageSize = 50) : IRequest<EmailHistoryPagedResult>;
|
||||
|
||||
/// <summary>
|
||||
/// Paged result for email history.
|
||||
/// </summary>
|
||||
public record EmailHistoryPagedResult(
|
||||
IEnumerable<EmailHistoryDto> Items,
|
||||
int TotalCount,
|
||||
int PageNumber,
|
||||
int PageSize);
|
||||
|
||||
public class GetEmailHistoryByProfileQueryHandler(
|
||||
IEmailHistoryRepository repository,
|
||||
IMapper mapper)
|
||||
: IRequestHandler<GetEmailHistoryByProfileQuery, EmailHistoryPagedResult>
|
||||
{
|
||||
public async Task<EmailHistoryPagedResult> Handle(
|
||||
GetEmailHistoryByProfileQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var (items, totalCount) = await repository.GetByProfileIdAsync(
|
||||
request.ProfileId,
|
||||
request.PageNumber,
|
||||
request.PageSize,
|
||||
cancellationToken);
|
||||
|
||||
var dtos = mapper.Map<IEnumerable<EmailHistoryDto>>(items);
|
||||
|
||||
return new EmailHistoryPagedResult(
|
||||
dtos,
|
||||
totalCount,
|
||||
request.PageNumber,
|
||||
request.PageSize);
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
using DigitalData.EmailProfiler.Application.Common.Dtos.EmailAttachments;
|
||||
using DigitalData.EmailProfiler.Application.Common.Dtos.EmailHistories;
|
||||
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
|
||||
using DigitalData.EmailProfiler.Application.Interfaces.Services;
|
||||
using DigitalData.EmailProfiler.Domain.Entities;
|
||||
using DigitalData.EmailProfiler.Domain.Enums;
|
||||
using DigitalData.EmailProfiler.Domain.Events;
|
||||
using DigitalData.EmailProfiler.Domain.Exceptions;
|
||||
using DigitalData.EmailProfiler.Domain.Services;
|
||||
using MediatR;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.EmailProcessing.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Command to process a single email from a profile.
|
||||
/// This is the core email processing logic.
|
||||
/// </summary>
|
||||
public record ProcessEmailCommand : IRequest<int>
|
||||
{
|
||||
public int ProfileId { get; init; }
|
||||
public string MessageId { get; init; } = string.Empty;
|
||||
public string Sender { get; init; } = string.Empty;
|
||||
public DateTime ReceivedDate { get; init; }
|
||||
public string Subject { get; init; } = string.Empty;
|
||||
public string? BodyText { get; init; }
|
||||
public string? BodyHtml { get; init; }
|
||||
public List<AttachmentData> Attachments { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attachment data for email processing.
|
||||
/// </summary>
|
||||
public record AttachmentData(
|
||||
string FileName,
|
||||
byte[] Content,
|
||||
string ContentType,
|
||||
long SizeBytes);
|
||||
|
||||
public class ProcessEmailCommandHandler(
|
||||
IEmailProfileRepository profileRepository,
|
||||
IEmailHistoryRepository historyRepository,
|
||||
IRepository<EmailAttachment> attachmentRepository,
|
||||
IPublisher publisher,
|
||||
MessageIdGenerator messageIdGenerator,
|
||||
IPdfProcessingService pdfService,
|
||||
IDmsService dmsService)
|
||||
: IRequestHandler<ProcessEmailCommand, int>
|
||||
{
|
||||
public async Task<int> Handle(ProcessEmailCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. Get profile with related entities
|
||||
var profile = await profileRepository.GetWithRelatedEntitiesAsync(request.ProfileId, cancellationToken)
|
||||
?? throw new DomainException($"Profile with ID {request.ProfileId} not found");
|
||||
|
||||
// 2. Generate message ID hash for duplicate detection
|
||||
var messageId = messageIdGenerator.Generate(
|
||||
request.MessageId,
|
||||
request.Sender,
|
||||
request.ReceivedDate,
|
||||
request.Subject);
|
||||
|
||||
// 3. Check for duplicates
|
||||
var isDuplicate = await historyRepository.IsDuplicateAsync(messageId.Hash, cancellationToken);
|
||||
if (isDuplicate)
|
||||
{
|
||||
throw new ValidationException("Email already processed (duplicate detected)", ErrorCode.DuplicateMessageId);
|
||||
}
|
||||
|
||||
// 4. Create email history record using DTO approach
|
||||
var historyDto = new CreateEmailHistoryDto(
|
||||
ProfileId: profile.Id,
|
||||
MessageIdHash: messageId.Hash,
|
||||
OriginalMessageId: request.MessageId,
|
||||
SenderAddress: request.Sender,
|
||||
EmailDate: request.ReceivedDate,
|
||||
Subject: request.Subject,
|
||||
EmailBodyText: request.BodyText,
|
||||
EmailBodyHtml: request.BodyHtml);
|
||||
|
||||
var createdHistory = await historyRepository.CreateAsync(historyDto, cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
// 5. Process attachments
|
||||
foreach (var attachmentData in request.Attachments)
|
||||
{
|
||||
var attachmentDto = new CreateEmailAttachmentDto(
|
||||
EmailHistoryId: createdHistory.Id,
|
||||
OriginalFileName: attachmentData.FileName,
|
||||
SavedFileName: attachmentData.FileName, // TODO: Generate unique name
|
||||
FilePath: string.Empty, // TODO: Save to disk and get path
|
||||
FileSize: attachmentData.SizeBytes,
|
||||
Extension: Path.GetExtension(attachmentData.FileName),
|
||||
ContentType: attachmentData.ContentType,
|
||||
Content: attachmentData.Content);
|
||||
|
||||
var attachment = await attachmentRepository.CreateAsync(attachmentDto, cancellationToken);
|
||||
|
||||
// Validate PDF attachments - need to retrieve and update the entity
|
||||
if (attachmentData.ContentType.Contains("pdf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
using var stream = new MemoryStream(attachmentData.Content);
|
||||
var isValidPdf = await pdfService.IsValidPdfAsync(stream, cancellationToken);
|
||||
|
||||
// For domain methods like MarkAsValid/MarkAsCorrupt, we need to get the entity
|
||||
var attachmentEntity = await attachmentRepository.GetByIdAsync(attachment.Id, cancellationToken);
|
||||
if (attachmentEntity != null)
|
||||
{
|
||||
if (isValidPdf)
|
||||
{
|
||||
attachmentEntity.MarkAsValid();
|
||||
}
|
||||
else
|
||||
{
|
||||
attachmentEntity.MarkAsCorrupt(ErrorCode.PdfStructureInvalid, "Invalid PDF structure");
|
||||
}
|
||||
|
||||
// Update directly using UpdateSingleAsync for safety
|
||||
await attachmentRepository.UpdateSingleAsync(
|
||||
a => a.Id == attachmentEntity.Id,
|
||||
new UpdateEmailAttachmentStatusDto(
|
||||
Status: attachmentEntity.Status ?? string.Empty,
|
||||
ValidationErrorCode: attachmentEntity.ValidationErrorCode,
|
||||
ValidationErrorMessage: attachmentEntity.ValidationErrorMessage),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Archive to DMS if configured
|
||||
if (profile.EmailProcess != null && profile.EmailProcess.EnableWindreamImport)
|
||||
{
|
||||
// TODO: Implement DMS archiving with indexing steps
|
||||
// This will be implemented based on ProcessSteps and IndexingSteps
|
||||
}
|
||||
|
||||
// 7. Mark as processed - retrieve entity for domain method
|
||||
var historyEntity = await historyRepository.GetByIdAsync(createdHistory.Id, cancellationToken);
|
||||
if (historyEntity != null)
|
||||
{
|
||||
historyEntity.MarkAsProcessed();
|
||||
await historyRepository.UpdateSingleAsync(
|
||||
h => h.Id == historyEntity.Id,
|
||||
new UpdateEmailHistoryStatusDto(
|
||||
Status: historyEntity.Status ?? string.Empty,
|
||||
ProcessedDate: historyEntity.ProcessedDate),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
// 8. Publish domain event
|
||||
await publisher.Publish(
|
||||
new EmailProcessedEvent(
|
||||
createdHistory.Id,
|
||||
profile.Id,
|
||||
messageId.Hash,
|
||||
EmailStatus.Processed),
|
||||
cancellationToken);
|
||||
|
||||
return createdHistory.Id;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Mark as failed - retrieve entity for domain method
|
||||
var historyEntity = await historyRepository.GetByIdAsync(createdHistory.Id, cancellationToken);
|
||||
if (historyEntity != null)
|
||||
{
|
||||
historyEntity.MarkAsFailed(ErrorCode.AttachmentExtractionFailed, ex.Message);
|
||||
await historyRepository.UpdateSingleAsync(
|
||||
h => h.Id == historyEntity.Id,
|
||||
new UpdateEmailHistoryStatusDto(
|
||||
Status: historyEntity.Status ?? string.Empty,
|
||||
ErrorCodeValue: historyEntity.ErrorCodeValue,
|
||||
ErrorMessage: historyEntity.ErrorMessage),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
using DigitalData.EmailProfiler.Application.EmailProcessing.Commands;
|
||||
using FluentValidation;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.EmailProcessing.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Validator for ProcessEmailCommand.
|
||||
/// </summary>
|
||||
public class ProcessEmailCommandValidator : AbstractValidator<ProcessEmailCommand>
|
||||
{
|
||||
public ProcessEmailCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.ProfileId)
|
||||
.GreaterThan(0).WithMessage("Profile ID must be greater than 0");
|
||||
|
||||
RuleFor(x => x.MessageId)
|
||||
.NotEmpty().WithMessage("Message ID is required")
|
||||
.MaximumLength(500).WithMessage("Message ID must not exceed 500 characters");
|
||||
|
||||
RuleFor(x => x.Sender)
|
||||
.NotEmpty().WithMessage("Sender is required")
|
||||
.MaximumLength(200).WithMessage("Sender must not exceed 200 characters")
|
||||
.EmailAddress().WithMessage("Sender 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.ReceivedDate)
|
||||
.NotEmpty().WithMessage("Received date is required")
|
||||
.LessThanOrEqualTo(DateTime.Now.AddDays(1)).WithMessage("Received date cannot be in the future");
|
||||
|
||||
RuleFor(x => x.Attachments)
|
||||
.NotNull().WithMessage("Attachments collection cannot be null");
|
||||
|
||||
RuleForEach(x => x.Attachments).ChildRules(attachment =>
|
||||
{
|
||||
attachment.RuleFor(a => a.FileName)
|
||||
.NotEmpty().WithMessage("Attachment file name is required")
|
||||
.MaximumLength(500).WithMessage("Attachment file name must not exceed 500 characters");
|
||||
|
||||
attachment.RuleFor(a => a.Content)
|
||||
.NotEmpty().WithMessage("Attachment content is required");
|
||||
|
||||
attachment.RuleFor(a => a.SizeBytes)
|
||||
.GreaterThan(0).WithMessage("Attachment size must be greater than 0")
|
||||
.LessThanOrEqualTo(100 * 1024 * 1024).WithMessage("Attachment size must not exceed 100 MB");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
|
||||
using DigitalData.EmailProfiler.Domain.Entities;
|
||||
using MediatR;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.EmailProfiles.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Command to create a new email profile.
|
||||
/// </summary>
|
||||
public record CreateEmailProfileCommand : IRequest<int>
|
||||
{
|
||||
public string ProfileName { get; init; } = string.Empty;
|
||||
public int EmailAccountId { get; init; }
|
||||
public int? ProcessId { get; init; }
|
||||
public string? ValidationSql { get; init; }
|
||||
public int PollIntervalMinutes { get; init; } = 15;
|
||||
public bool IsActive { get; init; } = true;
|
||||
}
|
||||
|
||||
public class CreateEmailProfileCommandHandler(IRepository<EmailProfile> repository)
|
||||
: IRequestHandler<CreateEmailProfileCommand, int>
|
||||
{
|
||||
public async Task<int> Handle(CreateEmailProfileCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var profile = await repository.CreateAsync(request, cancellationToken);
|
||||
return profile.Id;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
|
||||
using DigitalData.EmailProfiler.Domain.Entities;
|
||||
using MediatR;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.EmailProfiles.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Command to delete an email profile.
|
||||
/// </summary>
|
||||
public record DeleteEmailProfileCommand(int Id) : IRequest<int>;
|
||||
|
||||
public class DeleteEmailProfileCommandHandler(IRepository<EmailProfile> repository)
|
||||
: IRequestHandler<DeleteEmailProfileCommand, int>
|
||||
{
|
||||
public async Task<int> Handle(DeleteEmailProfileCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
await repository.DeleteSingleAsync(p => p.Id == request.Id, cancellationToken);
|
||||
return request.Id;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
|
||||
using DigitalData.EmailProfiler.Domain.Entities;
|
||||
using MediatR;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.EmailProfiles.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Command to update an existing email profile.
|
||||
/// </summary>
|
||||
public record UpdateEmailProfileCommand : IRequest<int>
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public string ProfileName { get; init; } = string.Empty;
|
||||
public string? ValidationSql { get; init; }
|
||||
public int PollIntervalMinutes { get; init; }
|
||||
public bool IsActive { get; init; }
|
||||
}
|
||||
|
||||
public class UpdateEmailProfileCommandHandler(IRepository<EmailProfile> repository)
|
||||
: IRequestHandler<UpdateEmailProfileCommand, int>
|
||||
{
|
||||
public async Task<int> Handle(UpdateEmailProfileCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
await repository.UpdateSingleAsync(p => p.Id == request.Id, request, cancellationToken);
|
||||
return request.Id;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.EmailProfiler.Application.Common.Dtos;
|
||||
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.EmailProfiles.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Query to get all active email profiles.
|
||||
/// </summary>
|
||||
public record GetActiveEmailProfilesQuery : IRequest<IEnumerable<EmailProfileDto>>;
|
||||
|
||||
public class GetActiveEmailProfilesQueryHandler(
|
||||
IEmailProfileRepository repository,
|
||||
IMapper mapper)
|
||||
: IRequestHandler<GetActiveEmailProfilesQuery, IEnumerable<EmailProfileDto>>
|
||||
{
|
||||
public async Task<IEnumerable<EmailProfileDto>> Handle(
|
||||
GetActiveEmailProfilesQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var profiles = await repository.GetActiveProfilesAsync(cancellationToken);
|
||||
return mapper.Map<IEnumerable<EmailProfileDto>>(profiles);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.EmailProfiler.Application.Common.Dtos;
|
||||
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.EmailProfiles.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Query to get email profile by ID with related entities.
|
||||
/// </summary>
|
||||
public record GetEmailProfileByIdQuery(int Id) : IRequest<EmailProfileDto?>;
|
||||
|
||||
public class GetEmailProfileByIdQueryHandler(
|
||||
IEmailProfileRepository repository,
|
||||
IMapper mapper)
|
||||
: IRequestHandler<GetEmailProfileByIdQuery, EmailProfileDto?>
|
||||
{
|
||||
public async Task<EmailProfileDto?> Handle(
|
||||
GetEmailProfileByIdQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var profile = await repository.GetWithRelatedEntitiesAsync(request.Id, cancellationToken);
|
||||
return profile != null ? mapper.Map<EmailProfileDto>(profile) : null;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.EmailProfiler.Application.Common.Dtos;
|
||||
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
|
||||
using DigitalData.EmailProfiler.Domain.Entities;
|
||||
using MediatR;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.EmailProfiles.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Query to get all email profiles.
|
||||
/// </summary>
|
||||
public record GetEmailProfilesQuery : IRequest<IEnumerable<EmailProfileDto>>;
|
||||
|
||||
public class GetEmailProfilesQueryHandler(
|
||||
IRepository<EmailProfile> repository,
|
||||
IMapper mapper)
|
||||
: IRequestHandler<GetEmailProfilesQuery, IEnumerable<EmailProfileDto>>
|
||||
{
|
||||
public async Task<IEnumerable<EmailProfileDto>> Handle(
|
||||
GetEmailProfilesQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var profiles = await repository.GetAllAsync(cancellationToken);
|
||||
return mapper.Map<IEnumerable<EmailProfileDto>>(profiles);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using DigitalData.EmailProfiler.Application.EmailProfiles.Commands;
|
||||
using FluentValidation;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.EmailProfiles.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Validator for CreateEmailProfileCommand.
|
||||
/// </summary>
|
||||
public class CreateEmailProfileCommandValidator : AbstractValidator<CreateEmailProfileCommand>
|
||||
{
|
||||
public CreateEmailProfileCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.ProfileName)
|
||||
.NotEmpty().WithMessage("Profile name is required")
|
||||
.MaximumLength(100).WithMessage("Profile name must not exceed 100 characters");
|
||||
|
||||
RuleFor(x => x.EmailAccountId)
|
||||
.GreaterThan(0).WithMessage("Email account ID must be greater than 0");
|
||||
|
||||
RuleFor(x => x.PollIntervalMinutes)
|
||||
.GreaterThanOrEqualTo(1).WithMessage("Poll interval must be at least 1 minute")
|
||||
.LessThanOrEqualTo(1440).WithMessage("Poll interval must not exceed 1440 minutes (24 hours)");
|
||||
|
||||
RuleFor(x => x.ValidationSql)
|
||||
.MaximumLength(1000).WithMessage("Validation SQL must not exceed 1000 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.ValidationSql));
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using DigitalData.EmailProfiler.Application.EmailProfiles.Commands;
|
||||
using FluentValidation;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.EmailProfiles.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Validator for UpdateEmailProfileCommand.
|
||||
/// </summary>
|
||||
public class UpdateEmailProfileCommandValidator : AbstractValidator<UpdateEmailProfileCommand>
|
||||
{
|
||||
public UpdateEmailProfileCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Id)
|
||||
.GreaterThan(0).WithMessage("Profile ID must be greater than 0");
|
||||
|
||||
RuleFor(x => x.ProfileName)
|
||||
.NotEmpty().WithMessage("Profile name is required")
|
||||
.MaximumLength(100).WithMessage("Profile name must not exceed 100 characters");
|
||||
|
||||
RuleFor(x => x.PollIntervalMinutes)
|
||||
.GreaterThanOrEqualTo(1).WithMessage("Poll interval must be at least 1 minute")
|
||||
.LessThanOrEqualTo(1440).WithMessage("Poll interval must not exceed 1440 minutes (24 hours)");
|
||||
|
||||
RuleFor(x => x.ValidationSql)
|
||||
.MaximumLength(1000).WithMessage("Validation SQL must not exceed 1000 characters")
|
||||
.When(x => !string.IsNullOrEmpty(x.ValidationSql));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user