diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Commands/CreateEmailAccountCommand.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Commands/CreateEmailAccountCommand.cs
new file mode 100644
index 0000000..ea1c808
--- /dev/null
+++ b/src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Commands/CreateEmailAccountCommand.cs
@@ -0,0 +1,24 @@
+using MediatR;
+
+namespace DigitalData.EmailProfiler.Application.Features.EmailAccounts.Commands;
+
+///
+/// Command to create a new email account.
+///
+public record CreateEmailAccountCommand : IRequest
+{
+ 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;
+}
diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Commands/CreateEmailAccountCommandHandler.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Commands/CreateEmailAccountCommandHandler.cs
new file mode 100644
index 0000000..016c721
--- /dev/null
+++ b/src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Commands/CreateEmailAccountCommandHandler.cs
@@ -0,0 +1,37 @@
+using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
+using DigitalData.EmailProfiler.Domain.Entities;
+using MediatR;
+
+namespace DigitalData.EmailProfiler.Application.Features.EmailAccounts.Commands;
+
+public class CreateEmailAccountCommandHandler(IUnitOfWork unitOfWork)
+ : IRequestHandler
+{
+ public async Task Handle(CreateEmailAccountCommand request, CancellationToken cancellationToken)
+ {
+ var account = new EmailAccount
+ {
+ AccountName = request.AccountName,
+ Username = request.Username,
+ ImapServer = request.ImapServer,
+ ImapPort = request.ImapPort,
+ ImapUseSsl = request.ImapUseSsl,
+ SmtpServer = request.SmtpServer,
+ SmtpPort = request.SmtpPort,
+ SmtpUseSsl = request.SmtpUseSsl,
+ UseOAuth2 = request.UseOAuth2,
+ EncryptedPassword = request.EncryptedPassword,
+ TenantId = request.TenantId,
+ ClientId = request.ClientId,
+ EncryptedClientSecret = request.EncryptedClientSecret,
+ IsActive = request.IsActive,
+ AddedWhen = DateTime.Now,
+ AddedWho = "System" // TODO: Get from current user context
+ };
+
+ var createdAccount = await unitOfWork.EmailAccounts.AddAsync(account, cancellationToken);
+ await unitOfWork.SaveChangesAsync(cancellationToken);
+
+ return createdAccount.Id;
+ }
+}
diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailProcessing/Commands/ProcessEmailCommand.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailProcessing/Commands/ProcessEmailCommand.cs
new file mode 100644
index 0000000..e38e1a4
--- /dev/null
+++ b/src/DigitalData.EmailProfiler.Application/Features/EmailProcessing/Commands/ProcessEmailCommand.cs
@@ -0,0 +1,28 @@
+using MediatR;
+
+namespace DigitalData.EmailProfiler.Application.Features.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);
diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailProcessing/Commands/ProcessEmailCommandHandler.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailProcessing/Commands/ProcessEmailCommandHandler.cs
new file mode 100644
index 0000000..28e0f8c
--- /dev/null
+++ b/src/DigitalData.EmailProfiler.Application/Features/EmailProcessing/Commands/ProcessEmailCommandHandler.cs
@@ -0,0 +1,127 @@
+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.Features.EmailProcessing.Commands;
+
+public class ProcessEmailCommandHandler(
+ IUnitOfWork unitOfWork,
+ 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 unitOfWork.EmailProfiles.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 unitOfWork.EmailHistories.IsDuplicateAsync(messageId.Hash, cancellationToken);
+ if (isDuplicate)
+ {
+ throw new ValidationException("Email already processed (duplicate detected)", ErrorCode.DuplicateMessageId);
+ }
+
+ // 4. Create email history record
+ var emailHistory = new EmailHistory
+ {
+ ProfileId = profile.Id,
+ MessageIdHash = messageId.Hash,
+ OriginalMessageId = request.MessageId,
+ SenderAddress = request.Sender,
+ EmailDate = request.ReceivedDate,
+ Subject = request.Subject,
+ EmailBodyText = request.BodyText,
+ EmailBodyHtml = request.BodyHtml,
+ Status = EmailStatus.Processing.ToString(),
+ AddedWhen = DateTime.Now
+ };
+
+ var createdHistory = await unitOfWork.EmailHistories.AddAsync(emailHistory, cancellationToken);
+ await unitOfWork.SaveChangesAsync(cancellationToken);
+
+ try
+ {
+ // 5. Process attachments
+ foreach (var attachmentData in request.Attachments)
+ {
+ var attachment = new EmailAttachment
+ {
+ 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),
+ Status = AttachmentStatus.Pending.ToString(),
+ AddedWhen = DateTime.Now
+ };
+
+ // Validate PDF attachments
+ if (attachmentData.ContentType.Contains("pdf", StringComparison.OrdinalIgnoreCase))
+ {
+ using var stream = new MemoryStream(attachmentData.Content);
+ var isValidPdf = await pdfService.IsValidPdfAsync(stream, cancellationToken);
+
+ if (isValidPdf)
+ {
+ attachment.MarkAsValid();
+ }
+ else
+ {
+ attachment.MarkAsCorrupt(ErrorCode.PdfStructureInvalid, "Invalid PDF structure");
+ }
+ }
+
+ createdHistory.Attachments.Add(attachment);
+ }
+
+ // 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
+ createdHistory.MarkAsProcessed();
+ await unitOfWork.EmailHistories.UpdateAsync(createdHistory, cancellationToken);
+ await unitOfWork.SaveChangesAsync(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
+ createdHistory.MarkAsFailed(ErrorCode.AttachmentExtractionFailed, ex.Message);
+ await unitOfWork.EmailHistories.UpdateAsync(createdHistory, cancellationToken);
+ await unitOfWork.SaveChangesAsync(cancellationToken);
+
+ throw;
+ }
+ }
+}
diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/CreateEmailProfileCommand.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/CreateEmailProfileCommand.cs
new file mode 100644
index 0000000..8b5473a
--- /dev/null
+++ b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/CreateEmailProfileCommand.cs
@@ -0,0 +1,16 @@
+using MediatR;
+
+namespace DigitalData.EmailProfiler.Application.Features.EmailProfiles.Commands;
+
+///
+/// Command to create a new email profile.
+///
+public record CreateEmailProfileCommand : IRequest
+{
+ 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;
+}
diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/CreateEmailProfileCommandHandler.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/CreateEmailProfileCommandHandler.cs
new file mode 100644
index 0000000..0773c3b
--- /dev/null
+++ b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/CreateEmailProfileCommandHandler.cs
@@ -0,0 +1,29 @@
+using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
+using DigitalData.EmailProfiler.Domain.Entities;
+using MediatR;
+
+namespace DigitalData.EmailProfiler.Application.Features.EmailProfiles.Commands;
+
+public class CreateEmailProfileCommandHandler(IUnitOfWork unitOfWork)
+ : IRequestHandler
+{
+ public async Task Handle(CreateEmailProfileCommand request, CancellationToken cancellationToken)
+ {
+ var profile = new EmailProfile
+ {
+ ProfileName = request.ProfileName,
+ EmailAccountId = request.EmailAccountId,
+ ProcessId = request.ProcessId,
+ ValidationSql = request.ValidationSql,
+ PollIntervalMinutes = request.PollIntervalMinutes,
+ IsActive = request.IsActive,
+ AddedWhen = DateTime.Now,
+ AddedWho = "System" // TODO: Get from current user context
+ };
+
+ var createdProfile = await unitOfWork.EmailProfiles.AddAsync(profile, cancellationToken);
+ await unitOfWork.SaveChangesAsync(cancellationToken);
+
+ return createdProfile.Id;
+ }
+}
diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/DeleteEmailProfileCommand.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/DeleteEmailProfileCommand.cs
new file mode 100644
index 0000000..f4ec819
--- /dev/null
+++ b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/DeleteEmailProfileCommand.cs
@@ -0,0 +1,8 @@
+using MediatR;
+
+namespace DigitalData.EmailProfiler.Application.Features.EmailProfiles.Commands;
+
+///
+/// Command to delete an email profile.
+///
+public record DeleteEmailProfileCommand(int Id) : IRequest;
diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/DeleteEmailProfileCommandHandler.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/DeleteEmailProfileCommandHandler.cs
new file mode 100644
index 0000000..b3d1e2c
--- /dev/null
+++ b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/DeleteEmailProfileCommandHandler.cs
@@ -0,0 +1,20 @@
+using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
+using DigitalData.EmailProfiler.Domain.Exceptions;
+using MediatR;
+
+namespace DigitalData.EmailProfiler.Application.Features.EmailProfiles.Commands;
+
+public class DeleteEmailProfileCommandHandler(IUnitOfWork unitOfWork)
+ : IRequestHandler
+{
+ public async Task Handle(DeleteEmailProfileCommand request, CancellationToken cancellationToken)
+ {
+ var profile = await unitOfWork.EmailProfiles.GetByIdAsync(request.Id, cancellationToken)
+ ?? throw new DomainException($"Email profile with ID {request.Id} not found");
+
+ await unitOfWork.EmailProfiles.DeleteAsync(profile, cancellationToken);
+ await unitOfWork.SaveChangesAsync(cancellationToken);
+
+ return Unit.Value;
+ }
+}
diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/UpdateEmailProfileCommand.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/UpdateEmailProfileCommand.cs
new file mode 100644
index 0000000..1dd9363
--- /dev/null
+++ b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/UpdateEmailProfileCommand.cs
@@ -0,0 +1,15 @@
+using MediatR;
+
+namespace DigitalData.EmailProfiler.Application.Features.EmailProfiles.Commands;
+
+///
+/// Command to update an existing email profile.
+///
+public record UpdateEmailProfileCommand : IRequest
+{
+ 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; }
+}
diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/UpdateEmailProfileCommandHandler.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/UpdateEmailProfileCommandHandler.cs
new file mode 100644
index 0000000..52663d3
--- /dev/null
+++ b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/UpdateEmailProfileCommandHandler.cs
@@ -0,0 +1,27 @@
+using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
+using DigitalData.EmailProfiler.Domain.Exceptions;
+using MediatR;
+
+namespace DigitalData.EmailProfiler.Application.Features.EmailProfiles.Commands;
+
+public class UpdateEmailProfileCommandHandler(IUnitOfWork unitOfWork)
+ : IRequestHandler
+{
+ public async Task Handle(UpdateEmailProfileCommand request, CancellationToken cancellationToken)
+ {
+ var profile = await unitOfWork.EmailProfiles.GetByIdAsync(request.Id, cancellationToken)
+ ?? throw new DomainException($"Email profile with ID {request.Id} not found");
+
+ profile.ProfileName = request.ProfileName;
+ profile.ValidationSql = request.ValidationSql;
+ profile.PollIntervalMinutes = request.PollIntervalMinutes;
+ profile.IsActive = request.IsActive;
+ profile.ChangedWhen = DateTime.Now;
+ profile.ChangedWho = "System"; // TODO: Get from current user context
+
+ await unitOfWork.EmailProfiles.UpdateAsync(profile, cancellationToken);
+ await unitOfWork.SaveChangesAsync(cancellationToken);
+
+ return Unit.Value;
+ }
+}
diff --git a/src/DigitalData.EmailProfiler.Domain/Exceptions/AttachmentProcessingException.cs b/src/DigitalData.EmailProfiler.Domain/Exceptions/AttachmentProcessingException.cs
index ee0fe44..5951802 100644
--- a/src/DigitalData.EmailProfiler.Domain/Exceptions/AttachmentProcessingException.cs
+++ b/src/DigitalData.EmailProfiler.Domain/Exceptions/AttachmentProcessingException.cs
@@ -4,16 +4,12 @@ namespace DigitalData.EmailProfiler.Domain.Exceptions;
public class AttachmentProcessingException : DomainException
{
- public ErrorCode ErrorCode { get; }
-
- public AttachmentProcessingException(ErrorCode errorCode, string message) : base(message)
+ public AttachmentProcessingException(ErrorCode errorCode, string message) : base(message, errorCode)
{
- ErrorCode = errorCode;
}
public AttachmentProcessingException(ErrorCode errorCode, string message, Exception innerException)
- : base(message, innerException)
+ : base(message, errorCode)
{
- ErrorCode = errorCode;
}
}
diff --git a/src/DigitalData.EmailProfiler.Domain/Exceptions/DomainException.cs b/src/DigitalData.EmailProfiler.Domain/Exceptions/DomainException.cs
index 3c311f7..5a9ce2b 100644
--- a/src/DigitalData.EmailProfiler.Domain/Exceptions/DomainException.cs
+++ b/src/DigitalData.EmailProfiler.Domain/Exceptions/DomainException.cs
@@ -1,11 +1,20 @@
+using DigitalData.EmailProfiler.Domain.Enums;
+
namespace DigitalData.EmailProfiler.Domain.Exceptions;
public class DomainException : Exception
{
+ public ErrorCode? ErrorCode { get; }
+
public DomainException(string message) : base(message)
{
}
+ public DomainException(string message, ErrorCode errorCode) : base(message)
+ {
+ ErrorCode = errorCode;
+ }
+
public DomainException(string message, Exception innerException) : base(message, innerException)
{
}
diff --git a/src/DigitalData.EmailProfiler.Domain/Exceptions/ValidationException.cs b/src/DigitalData.EmailProfiler.Domain/Exceptions/ValidationException.cs
index d6432e7..8b82244 100644
--- a/src/DigitalData.EmailProfiler.Domain/Exceptions/ValidationException.cs
+++ b/src/DigitalData.EmailProfiler.Domain/Exceptions/ValidationException.cs
@@ -1,3 +1,5 @@
+using DigitalData.EmailProfiler.Domain.Enums;
+
namespace DigitalData.EmailProfiler.Domain.Exceptions;
public class ValidationException : DomainException
@@ -6,6 +8,10 @@ public class ValidationException : DomainException
{
}
+ public ValidationException(string message, ErrorCode errorCode) : base(message, errorCode)
+ {
+ }
+
public ValidationException(string message, Exception innerException) : base(message, innerException)
{
}