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; /// /// Command to process a single email from a profile. /// This is the core email processing logic. /// public record ProcessEmailCommand : IRequest { 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 Attachments { get; init; } = new(); } /// /// Attachment data for email processing. /// public record AttachmentData( string FileName, byte[] Content, string ContentType, long SizeBytes); public class ProcessEmailCommandHandler( IEmailProfileRepository profileRepository, IEmailHistoryRepository historyRepository, IRepository attachmentRepository, IPublisher publisher, MessageIdGenerator messageIdGenerator, IPdfProcessingService pdfService, IDmsService dmsService) : IRequestHandler { public async Task 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; } } }