From 3778c0b338e46931bcdd5b4dce09edf2c14bc1da Mon Sep 17 00:00:00 2001 From: TekH Date: Wed, 8 Jul 2026 10:39:16 +0200 Subject: [PATCH] feat(application): add repository and service interfaces Repository Interfaces (Clean Architecture - Application Layer): - IRepository: Base repository interface with common CRUD operations - IEmailAccountRepository: Email account operations (GetActive, GetByName, GetWithProfiles) - IEmailProfileRepository: Profile operations (GetActive, GetDueForPolling, GetWithRelated) - IEmailProcessRepository: Process operations (GetWithSteps, GetByType) - IEmailHistoryRepository: History operations (pagination, duplicate detection, date range queries) - IEmailOutboxRepository: Outbox operations (GetPending, GetForRetry, MarkAsSent/Failed) - IUnitOfWork: Transaction management and repository aggregation Service Interfaces (Abstraction for Infrastructure): - IEmailService: IMAP/SMTP operations with OAuth2 support (MailKit wrapper) - IPdfProcessingService: PDF validation, embedded file extraction, ZUGFeRD support - IDmsService: windream DMS integration (archive, search, update index fields) - IEncryptionService: Data protection for passwords and OAuth tokens - IEmailQueue: Async email queue (in-memory Channel, future: RabbitMQ) Dependencies: - Added MimeKit 4.17.0 for email service interface definitions All interfaces follow Clean Architecture principles: - Interfaces in Application layer - Implementations will be in Infrastructure layer --- ...gitalData.EmailProfiler.Application.csproj | 1 + .../Repositories/IEmailAccountRepository.cs | 24 +++++++ .../Repositories/IEmailHistoryRepository.cs | 47 ++++++++++++ .../Repositories/IEmailOutboxRepository.cs | 34 +++++++++ .../Repositories/IEmailProcessRepository.cs | 24 +++++++ .../Repositories/IEmailProfileRepository.cs | 29 ++++++++ .../Interfaces/Repositories/IRepository.cs | 19 +++++ .../Interfaces/Repositories/IUnitOfWork.cs | 33 +++++++++ .../Interfaces/Services/IDmsService.cs | 71 +++++++++++++++++++ .../Interfaces/Services/IEmailQueue.cs | 27 +++++++ .../Interfaces/Services/IEmailService.cs | 59 +++++++++++++++ .../Interfaces/Services/IEncryptionService.cs | 28 ++++++++ .../Services/IPdfProcessingService.cs | 59 +++++++++++++++ 13 files changed, 455 insertions(+) create mode 100644 src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IEmailAccountRepository.cs create mode 100644 src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IEmailHistoryRepository.cs create mode 100644 src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IEmailOutboxRepository.cs create mode 100644 src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IEmailProcessRepository.cs create mode 100644 src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IEmailProfileRepository.cs create mode 100644 src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IRepository.cs create mode 100644 src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IUnitOfWork.cs create mode 100644 src/DigitalData.EmailProfiler.Application/Interfaces/Services/IDmsService.cs create mode 100644 src/DigitalData.EmailProfiler.Application/Interfaces/Services/IEmailQueue.cs create mode 100644 src/DigitalData.EmailProfiler.Application/Interfaces/Services/IEmailService.cs create mode 100644 src/DigitalData.EmailProfiler.Application/Interfaces/Services/IEncryptionService.cs create mode 100644 src/DigitalData.EmailProfiler.Application/Interfaces/Services/IPdfProcessingService.cs diff --git a/src/DigitalData.EmailProfiler.Application/DigitalData.EmailProfiler.Application.csproj b/src/DigitalData.EmailProfiler.Application/DigitalData.EmailProfiler.Application.csproj index 52ab069..2fbd34d 100644 --- a/src/DigitalData.EmailProfiler.Application/DigitalData.EmailProfiler.Application.csproj +++ b/src/DigitalData.EmailProfiler.Application/DigitalData.EmailProfiler.Application.csproj @@ -14,6 +14,7 @@ + diff --git a/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IEmailAccountRepository.cs b/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IEmailAccountRepository.cs new file mode 100644 index 0000000..3101a20 --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IEmailAccountRepository.cs @@ -0,0 +1,24 @@ +using DigitalData.EmailProfiler.Domain.Entities; + +namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories; + +/// +/// Repository interface for EmailAccount entity. +/// +public interface IEmailAccountRepository : IRepository +{ + /// + /// Get all active email accounts. + /// + Task> GetActiveAccountsAsync(CancellationToken cancellationToken = default); + + /// + /// Get email account by account name. + /// + Task GetByAccountNameAsync(string accountName, CancellationToken cancellationToken = default); + + /// + /// Get email account with its profiles. + /// + Task GetWithProfilesAsync(int id, CancellationToken cancellationToken = default); +} diff --git a/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IEmailHistoryRepository.cs b/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IEmailHistoryRepository.cs new file mode 100644 index 0000000..02068e4 --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IEmailHistoryRepository.cs @@ -0,0 +1,47 @@ +using DigitalData.EmailProfiler.Domain.Entities; + +namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories; + +/// +/// Repository interface for EmailHistory entity. +/// +public interface IEmailHistoryRepository : IRepository +{ + /// + /// Get email history by message ID hash. + /// + Task GetByMessageIdHashAsync(string messageIdHash, CancellationToken cancellationToken = default); + + /// + /// Get email history with attachments. + /// + Task GetWithAttachmentsAsync(int id, CancellationToken cancellationToken = default); + + /// + /// Get email history by profile ID with pagination. + /// + Task<(IEnumerable Items, int TotalCount)> GetByProfileIdAsync( + int profileId, + int pageNumber = 1, + int pageSize = 50, + CancellationToken cancellationToken = default); + + /// + /// Get failed emails that need retry. + /// + Task> GetFailedEmailsAsync(int? profileId = null, CancellationToken cancellationToken = default); + + /// + /// Get emails processed within date range. + /// + Task> GetByDateRangeAsync( + DateTime startDate, + DateTime endDate, + int? profileId = null, + CancellationToken cancellationToken = default); + + /// + /// Check if email with given message ID hash already exists. + /// + Task IsDuplicateAsync(string messageIdHash, CancellationToken cancellationToken = default); +} diff --git a/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IEmailOutboxRepository.cs b/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IEmailOutboxRepository.cs new file mode 100644 index 0000000..3dbf000 --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IEmailOutboxRepository.cs @@ -0,0 +1,34 @@ +using DigitalData.EmailProfiler.Domain.Entities; + +namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories; + +/// +/// Repository interface for EmailOutbox entity. +/// +public interface IEmailOutboxRepository : IRepository +{ + /// + /// Get all pending emails (not yet sent). + /// + Task> GetPendingEmailsAsync(int maxCount = 100, CancellationToken cancellationToken = default); + + /// + /// Get failed emails that need retry (RetryCount < MaxRetryCount). + /// + Task> GetFailedEmailsForRetryAsync(int maxCount = 100, CancellationToken cancellationToken = default); + + /// + /// Mark email as sent. + /// + Task MarkAsSentAsync(int id, CancellationToken cancellationToken = default); + + /// + /// Mark email as failed with error details. + /// + Task MarkAsFailedAsync(int id, string errorMessage, CancellationToken cancellationToken = default); + + /// + /// Delete old sent emails (cleanup). + /// + Task DeleteOldSentEmailsAsync(int daysToKeep, CancellationToken cancellationToken = default); +} diff --git a/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IEmailProcessRepository.cs b/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IEmailProcessRepository.cs new file mode 100644 index 0000000..7e262f0 --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IEmailProcessRepository.cs @@ -0,0 +1,24 @@ +using DigitalData.EmailProfiler.Domain.Entities; + +namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories; + +/// +/// Repository interface for EmailProcess entity. +/// +public interface IEmailProcessRepository : IRepository +{ + /// + /// Get process with all steps (ProcessSteps and IndexingSteps). + /// + Task GetWithStepsAsync(int id, CancellationToken cancellationToken = default); + + /// + /// Get process by profile ID. + /// + Task GetByProfileIdAsync(int profileId, CancellationToken cancellationToken = default); + + /// + /// Get all processes by type. + /// + Task> GetByProcessTypeAsync(string processType, CancellationToken cancellationToken = default); +} diff --git a/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IEmailProfileRepository.cs b/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IEmailProfileRepository.cs new file mode 100644 index 0000000..f3e3ae6 --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IEmailProfileRepository.cs @@ -0,0 +1,29 @@ +using DigitalData.EmailProfiler.Domain.Entities; + +namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories; + +/// +/// Repository interface for EmailProfile entity. +/// +public interface IEmailProfileRepository : IRepository +{ + /// + /// Get all active profiles. + /// + Task> GetActiveProfilesAsync(CancellationToken cancellationToken = default); + + /// + /// Get profiles that should be polled now (based on PollIntervalMinutes). + /// + Task> GetProfilesDueForPollingAsync(CancellationToken cancellationToken = default); + + /// + /// Get profile with all related entities (EmailAccount, EmailProcess, ProcessSteps). + /// + Task GetWithRelatedEntitiesAsync(int id, CancellationToken cancellationToken = default); + + /// + /// Get profiles by email account ID. + /// + Task> GetByEmailAccountIdAsync(int emailAccountId, CancellationToken cancellationToken = default); +} diff --git a/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IRepository.cs b/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IRepository.cs new file mode 100644 index 0000000..32ab791 --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IRepository.cs @@ -0,0 +1,19 @@ +using System.Linq.Expressions; + +namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories; + +/// +/// Base repository interface for common CRUD operations. +/// +/// Entity type +public interface IRepository where T : class +{ + Task GetByIdAsync(int id, CancellationToken cancellationToken = default); + Task> GetAllAsync(CancellationToken cancellationToken = default); + Task> FindAsync(Expression> predicate, CancellationToken cancellationToken = default); + Task AddAsync(T entity, CancellationToken cancellationToken = default); + Task UpdateAsync(T entity, CancellationToken cancellationToken = default); + Task DeleteAsync(T entity, CancellationToken cancellationToken = default); + Task ExistsAsync(Expression> predicate, CancellationToken cancellationToken = default); + Task CountAsync(Expression>? predicate = null, CancellationToken cancellationToken = default); +} diff --git a/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IUnitOfWork.cs b/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IUnitOfWork.cs new file mode 100644 index 0000000..58d9c9d --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Interfaces/Repositories/IUnitOfWork.cs @@ -0,0 +1,33 @@ +namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories; + +/// +/// Unit of Work pattern for transaction management. +/// +public interface IUnitOfWork : IDisposable +{ + IEmailAccountRepository EmailAccounts { get; } + IEmailProfileRepository EmailProfiles { get; } + IEmailProcessRepository EmailProcesses { get; } + IEmailHistoryRepository EmailHistories { get; } + IEmailOutboxRepository EmailOutbox { get; } + + /// + /// Save all changes to the database. + /// + Task SaveChangesAsync(CancellationToken cancellationToken = default); + + /// + /// Begin a database transaction. + /// + Task BeginTransactionAsync(CancellationToken cancellationToken = default); + + /// + /// Commit the current transaction. + /// + Task CommitTransactionAsync(CancellationToken cancellationToken = default); + + /// + /// Rollback the current transaction. + /// + Task RollbackTransactionAsync(CancellationToken cancellationToken = default); +} diff --git a/src/DigitalData.EmailProfiler.Application/Interfaces/Services/IDmsService.cs b/src/DigitalData.EmailProfiler.Application/Interfaces/Services/IDmsService.cs new file mode 100644 index 0000000..7897f47 --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Interfaces/Services/IDmsService.cs @@ -0,0 +1,71 @@ +namespace DigitalData.EmailProfiler.Application.Interfaces.Services; + +/// +/// windream DMS integration service interface. +/// +public interface IDmsService +{ + /// + /// Connect to windream DMS. + /// + Task ConnectAsync(string server, string username, string password, CancellationToken cancellationToken = default); + + /// + /// Archive document to windream DMS with indexing fields. + /// + Task ArchiveDocumentAsync( + string filePath, + string documentType, + Dictionary indexFields, + CancellationToken cancellationToken = default); + + /// + /// Archive document from stream to windream DMS. + /// + Task ArchiveDocumentAsync( + Stream fileStream, + string fileName, + string documentType, + Dictionary indexFields, + CancellationToken cancellationToken = default); + + /// + /// Check if document already exists in windream DMS. + /// + Task DocumentExistsAsync(string documentId, CancellationToken cancellationToken = default); + + /// + /// Get document from windream DMS. + /// + Task GetDocumentAsync(string documentId, CancellationToken cancellationToken = default); + + /// + /// Update document indexing fields. + /// + Task UpdateIndexFieldsAsync( + string documentId, + Dictionary indexFields, + CancellationToken cancellationToken = default); + + /// + /// Search documents by index fields. + /// + Task> SearchDocumentsAsync( + Dictionary searchCriteria, + int maxResults = 100, + CancellationToken cancellationToken = default); + + /// + /// Disconnect from windream DMS. + /// + Task DisconnectAsync(CancellationToken cancellationToken = default); +} + +/// +/// Represents a windream DMS search result. +/// +public record DmsSearchResult( + string DocumentId, + string FileName, + DateTime CreatedDate, + Dictionary IndexFields); diff --git a/src/DigitalData.EmailProfiler.Application/Interfaces/Services/IEmailQueue.cs b/src/DigitalData.EmailProfiler.Application/Interfaces/Services/IEmailQueue.cs new file mode 100644 index 0000000..4578c2e --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Interfaces/Services/IEmailQueue.cs @@ -0,0 +1,27 @@ +using DigitalData.EmailProfiler.Domain.Entities; + +namespace DigitalData.EmailProfiler.Application.Interfaces.Services; + +/// +/// Email queue interface for asynchronous email sending. +/// Current implementation: In-memory Channel<T> +/// Future: RabbitMQ (see agents.md) +/// +public interface IEmailQueue +{ + /// + /// Enqueue an email for sending. + /// + Task EnqueueAsync(EmailOutbox email, CancellationToken cancellationToken = default); + + /// + /// Dequeue an email for sending. + /// Returns null if queue is empty. + /// + Task DequeueAsync(CancellationToken cancellationToken = default); + + /// + /// Get current queue depth (number of pending emails). + /// + Task GetQueueDepthAsync(CancellationToken cancellationToken = default); +} diff --git a/src/DigitalData.EmailProfiler.Application/Interfaces/Services/IEmailService.cs b/src/DigitalData.EmailProfiler.Application/Interfaces/Services/IEmailService.cs new file mode 100644 index 0000000..9fcd404 --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Interfaces/Services/IEmailService.cs @@ -0,0 +1,59 @@ +using MimeKit; + +namespace DigitalData.EmailProfiler.Application.Interfaces.Services; + +/// +/// Email service interface for IMAP/SMTP operations with OAuth2 support. +/// +public interface IEmailService +{ + /// + /// Connect to IMAP server and authenticate. + /// + Task ConnectImapAsync(string server, int port, string username, string password, bool useSsl = true, CancellationToken cancellationToken = default); + + /// + /// Connect to IMAP server using OAuth2. + /// + Task ConnectImapOAuth2Async(string server, int port, string username, string accessToken, bool useSsl = true, CancellationToken cancellationToken = default); + + /// + /// Fetch unread emails from inbox. + /// + Task> FetchUnreadEmailsAsync(int maxCount = 100, CancellationToken cancellationToken = default); + + /// + /// Mark email as read. + /// + Task MarkAsReadAsync(int uid, CancellationToken cancellationToken = default); + + /// + /// Move email to specified folder. + /// + Task MoveToFolderAsync(int uid, string folderName, CancellationToken cancellationToken = default); + + /// + /// Delete email. + /// + Task DeleteEmailAsync(int uid, CancellationToken cancellationToken = default); + + /// + /// Disconnect from IMAP server. + /// + Task DisconnectImapAsync(CancellationToken cancellationToken = default); + + /// + /// Send email via SMTP. + /// + Task SendEmailAsync(MimeMessage message, string smtpServer, int smtpPort, string username, string password, bool useSsl = true, CancellationToken cancellationToken = default); + + /// + /// Send email via SMTP using OAuth2. + /// + Task SendEmailOAuth2Async(MimeMessage message, string smtpServer, int smtpPort, string username, string accessToken, bool useSsl = true, CancellationToken cancellationToken = default); + + /// + /// Get OAuth2 access token for Microsoft 365. + /// + Task GetOAuth2AccessTokenAsync(string tenantId, string clientId, string clientSecret, string[] scopes, CancellationToken cancellationToken = default); +} diff --git a/src/DigitalData.EmailProfiler.Application/Interfaces/Services/IEncryptionService.cs b/src/DigitalData.EmailProfiler.Application/Interfaces/Services/IEncryptionService.cs new file mode 100644 index 0000000..63a83a5 --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Interfaces/Services/IEncryptionService.cs @@ -0,0 +1,28 @@ +namespace DigitalData.EmailProfiler.Application.Interfaces.Services; + +/// +/// Encryption service interface for sensitive data (passwords, OAuth tokens). +/// Uses ASP.NET Core Data Protection API. +/// +public interface IEncryptionService +{ + /// + /// Encrypt a string value. + /// + string Encrypt(string plainText); + + /// + /// Decrypt an encrypted string value. + /// + string Decrypt(string cipherText); + + /// + /// Encrypt a byte array. + /// + byte[] Encrypt(byte[] plainData); + + /// + /// Decrypt an encrypted byte array. + /// + byte[] Decrypt(byte[] cipherData); +} diff --git a/src/DigitalData.EmailProfiler.Application/Interfaces/Services/IPdfProcessingService.cs b/src/DigitalData.EmailProfiler.Application/Interfaces/Services/IPdfProcessingService.cs new file mode 100644 index 0000000..db7907b --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Interfaces/Services/IPdfProcessingService.cs @@ -0,0 +1,59 @@ +namespace DigitalData.EmailProfiler.Application.Interfaces.Services; + +/// +/// PDF processing service interface for validation and embedded file extraction. +/// +public interface IPdfProcessingService +{ + /// + /// Validate if file is a valid PDF. + /// + Task IsValidPdfAsync(string filePath, CancellationToken cancellationToken = default); + + /// + /// Validate if stream contains a valid PDF. + /// + Task IsValidPdfAsync(Stream stream, CancellationToken cancellationToken = default); + + /// + /// Extract embedded files from PDF (e.g., ZUGFeRD XML). + /// + Task> ExtractEmbeddedFilesAsync(string pdfPath, CancellationToken cancellationToken = default); + + /// + /// Extract embedded files from PDF stream. + /// + Task> ExtractEmbeddedFilesAsync(Stream pdfStream, CancellationToken cancellationToken = default); + + /// + /// Check if PDF contains ZUGFeRD data. + /// + Task HasZugFeRDDataAsync(string pdfPath, CancellationToken cancellationToken = default); + + /// + /// Extract ZUGFeRD XML from PDF. + /// + Task ExtractZugFeRDXmlAsync(string pdfPath, CancellationToken cancellationToken = default); + + /// + /// Get PDF metadata (title, author, creation date, etc.). + /// + Task GetMetadataAsync(string pdfPath, CancellationToken cancellationToken = default); +} + +/// +/// Represents an embedded file extracted from a PDF. +/// +public record EmbeddedFile(string FileName, byte[] Content, string? Description = null); + +/// +/// Represents PDF metadata. +/// +public record PdfMetadata( + string? Title, + string? Author, + string? Subject, + string? Keywords, + DateTime? CreationDate, + DateTime? ModificationDate, + int PageCount);