181 lines
7.8 KiB
C#
181 lines
7.8 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|