feat(application): add repository and service interfaces

Repository Interfaces (Clean Architecture - Application Layer):
- IRepository<T>: 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
This commit is contained in:
2026-07-08 10:39:16 +02:00
parent 111d2bf264
commit 3778c0b338
13 changed files with 455 additions and 0 deletions

View File

@@ -14,6 +14,7 @@
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" /> <PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageReference Include="MediatR" Version="14.2.0" /> <PackageReference Include="MediatR" Version="14.2.0" />
<PackageReference Include="MimeKit" Version="4.17.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,24 @@
using DigitalData.EmailProfiler.Domain.Entities;
namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories;
/// <summary>
/// Repository interface for EmailAccount entity.
/// </summary>
public interface IEmailAccountRepository : IRepository<EmailAccount>
{
/// <summary>
/// Get all active email accounts.
/// </summary>
Task<IEnumerable<EmailAccount>> GetActiveAccountsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Get email account by account name.
/// </summary>
Task<EmailAccount?> GetByAccountNameAsync(string accountName, CancellationToken cancellationToken = default);
/// <summary>
/// Get email account with its profiles.
/// </summary>
Task<EmailAccount?> GetWithProfilesAsync(int id, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,47 @@
using DigitalData.EmailProfiler.Domain.Entities;
namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories;
/// <summary>
/// Repository interface for EmailHistory entity.
/// </summary>
public interface IEmailHistoryRepository : IRepository<EmailHistory>
{
/// <summary>
/// Get email history by message ID hash.
/// </summary>
Task<EmailHistory?> GetByMessageIdHashAsync(string messageIdHash, CancellationToken cancellationToken = default);
/// <summary>
/// Get email history with attachments.
/// </summary>
Task<EmailHistory?> GetWithAttachmentsAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
/// Get email history by profile ID with pagination.
/// </summary>
Task<(IEnumerable<EmailHistory> Items, int TotalCount)> GetByProfileIdAsync(
int profileId,
int pageNumber = 1,
int pageSize = 50,
CancellationToken cancellationToken = default);
/// <summary>
/// Get failed emails that need retry.
/// </summary>
Task<IEnumerable<EmailHistory>> GetFailedEmailsAsync(int? profileId = null, CancellationToken cancellationToken = default);
/// <summary>
/// Get emails processed within date range.
/// </summary>
Task<IEnumerable<EmailHistory>> GetByDateRangeAsync(
DateTime startDate,
DateTime endDate,
int? profileId = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Check if email with given message ID hash already exists.
/// </summary>
Task<bool> IsDuplicateAsync(string messageIdHash, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,34 @@
using DigitalData.EmailProfiler.Domain.Entities;
namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories;
/// <summary>
/// Repository interface for EmailOutbox entity.
/// </summary>
public interface IEmailOutboxRepository : IRepository<EmailOutbox>
{
/// <summary>
/// Get all pending emails (not yet sent).
/// </summary>
Task<IEnumerable<EmailOutbox>> GetPendingEmailsAsync(int maxCount = 100, CancellationToken cancellationToken = default);
/// <summary>
/// Get failed emails that need retry (RetryCount < MaxRetryCount).
/// </summary>
Task<IEnumerable<EmailOutbox>> GetFailedEmailsForRetryAsync(int maxCount = 100, CancellationToken cancellationToken = default);
/// <summary>
/// Mark email as sent.
/// </summary>
Task MarkAsSentAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
/// Mark email as failed with error details.
/// </summary>
Task MarkAsFailedAsync(int id, string errorMessage, CancellationToken cancellationToken = default);
/// <summary>
/// Delete old sent emails (cleanup).
/// </summary>
Task DeleteOldSentEmailsAsync(int daysToKeep, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,24 @@
using DigitalData.EmailProfiler.Domain.Entities;
namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories;
/// <summary>
/// Repository interface for EmailProcess entity.
/// </summary>
public interface IEmailProcessRepository : IRepository<EmailProcess>
{
/// <summary>
/// Get process with all steps (ProcessSteps and IndexingSteps).
/// </summary>
Task<EmailProcess?> GetWithStepsAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
/// Get process by profile ID.
/// </summary>
Task<EmailProcess?> GetByProfileIdAsync(int profileId, CancellationToken cancellationToken = default);
/// <summary>
/// Get all processes by type.
/// </summary>
Task<IEnumerable<EmailProcess>> GetByProcessTypeAsync(string processType, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,29 @@
using DigitalData.EmailProfiler.Domain.Entities;
namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories;
/// <summary>
/// Repository interface for EmailProfile entity.
/// </summary>
public interface IEmailProfileRepository : IRepository<EmailProfile>
{
/// <summary>
/// Get all active profiles.
/// </summary>
Task<IEnumerable<EmailProfile>> GetActiveProfilesAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Get profiles that should be polled now (based on PollIntervalMinutes).
/// </summary>
Task<IEnumerable<EmailProfile>> GetProfilesDueForPollingAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Get profile with all related entities (EmailAccount, EmailProcess, ProcessSteps).
/// </summary>
Task<EmailProfile?> GetWithRelatedEntitiesAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
/// Get profiles by email account ID.
/// </summary>
Task<IEnumerable<EmailProfile>> GetByEmailAccountIdAsync(int emailAccountId, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,19 @@
using System.Linq.Expressions;
namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories;
/// <summary>
/// Base repository interface for common CRUD operations.
/// </summary>
/// <typeparam name="T">Entity type</typeparam>
public interface IRepository<T> where T : class
{
Task<T?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
Task<IEnumerable<T>> GetAllAsync(CancellationToken cancellationToken = default);
Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate, CancellationToken cancellationToken = default);
Task<T> AddAsync(T entity, CancellationToken cancellationToken = default);
Task UpdateAsync(T entity, CancellationToken cancellationToken = default);
Task DeleteAsync(T entity, CancellationToken cancellationToken = default);
Task<bool> ExistsAsync(Expression<Func<T, bool>> predicate, CancellationToken cancellationToken = default);
Task<int> CountAsync(Expression<Func<T, bool>>? predicate = null, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,33 @@
namespace DigitalData.EmailProfiler.Application.Interfaces.Repositories;
/// <summary>
/// Unit of Work pattern for transaction management.
/// </summary>
public interface IUnitOfWork : IDisposable
{
IEmailAccountRepository EmailAccounts { get; }
IEmailProfileRepository EmailProfiles { get; }
IEmailProcessRepository EmailProcesses { get; }
IEmailHistoryRepository EmailHistories { get; }
IEmailOutboxRepository EmailOutbox { get; }
/// <summary>
/// Save all changes to the database.
/// </summary>
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Begin a database transaction.
/// </summary>
Task BeginTransactionAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Commit the current transaction.
/// </summary>
Task CommitTransactionAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Rollback the current transaction.
/// </summary>
Task RollbackTransactionAsync(CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,71 @@
namespace DigitalData.EmailProfiler.Application.Interfaces.Services;
/// <summary>
/// windream DMS integration service interface.
/// </summary>
public interface IDmsService
{
/// <summary>
/// Connect to windream DMS.
/// </summary>
Task ConnectAsync(string server, string username, string password, CancellationToken cancellationToken = default);
/// <summary>
/// Archive document to windream DMS with indexing fields.
/// </summary>
Task<string> ArchiveDocumentAsync(
string filePath,
string documentType,
Dictionary<string, object> indexFields,
CancellationToken cancellationToken = default);
/// <summary>
/// Archive document from stream to windream DMS.
/// </summary>
Task<string> ArchiveDocumentAsync(
Stream fileStream,
string fileName,
string documentType,
Dictionary<string, object> indexFields,
CancellationToken cancellationToken = default);
/// <summary>
/// Check if document already exists in windream DMS.
/// </summary>
Task<bool> DocumentExistsAsync(string documentId, CancellationToken cancellationToken = default);
/// <summary>
/// Get document from windream DMS.
/// </summary>
Task<Stream> GetDocumentAsync(string documentId, CancellationToken cancellationToken = default);
/// <summary>
/// Update document indexing fields.
/// </summary>
Task UpdateIndexFieldsAsync(
string documentId,
Dictionary<string, object> indexFields,
CancellationToken cancellationToken = default);
/// <summary>
/// Search documents by index fields.
/// </summary>
Task<IEnumerable<DmsSearchResult>> SearchDocumentsAsync(
Dictionary<string, object> searchCriteria,
int maxResults = 100,
CancellationToken cancellationToken = default);
/// <summary>
/// Disconnect from windream DMS.
/// </summary>
Task DisconnectAsync(CancellationToken cancellationToken = default);
}
/// <summary>
/// Represents a windream DMS search result.
/// </summary>
public record DmsSearchResult(
string DocumentId,
string FileName,
DateTime CreatedDate,
Dictionary<string, object> IndexFields);

View File

@@ -0,0 +1,27 @@
using DigitalData.EmailProfiler.Domain.Entities;
namespace DigitalData.EmailProfiler.Application.Interfaces.Services;
/// <summary>
/// Email queue interface for asynchronous email sending.
/// Current implementation: In-memory Channel&lt;T&gt;
/// Future: RabbitMQ (see agents.md)
/// </summary>
public interface IEmailQueue
{
/// <summary>
/// Enqueue an email for sending.
/// </summary>
Task EnqueueAsync(EmailOutbox email, CancellationToken cancellationToken = default);
/// <summary>
/// Dequeue an email for sending.
/// Returns null if queue is empty.
/// </summary>
Task<EmailOutbox?> DequeueAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Get current queue depth (number of pending emails).
/// </summary>
Task<int> GetQueueDepthAsync(CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,59 @@
using MimeKit;
namespace DigitalData.EmailProfiler.Application.Interfaces.Services;
/// <summary>
/// Email service interface for IMAP/SMTP operations with OAuth2 support.
/// </summary>
public interface IEmailService
{
/// <summary>
/// Connect to IMAP server and authenticate.
/// </summary>
Task ConnectImapAsync(string server, int port, string username, string password, bool useSsl = true, CancellationToken cancellationToken = default);
/// <summary>
/// Connect to IMAP server using OAuth2.
/// </summary>
Task ConnectImapOAuth2Async(string server, int port, string username, string accessToken, bool useSsl = true, CancellationToken cancellationToken = default);
/// <summary>
/// Fetch unread emails from inbox.
/// </summary>
Task<IEnumerable<MimeMessage>> FetchUnreadEmailsAsync(int maxCount = 100, CancellationToken cancellationToken = default);
/// <summary>
/// Mark email as read.
/// </summary>
Task MarkAsReadAsync(int uid, CancellationToken cancellationToken = default);
/// <summary>
/// Move email to specified folder.
/// </summary>
Task MoveToFolderAsync(int uid, string folderName, CancellationToken cancellationToken = default);
/// <summary>
/// Delete email.
/// </summary>
Task DeleteEmailAsync(int uid, CancellationToken cancellationToken = default);
/// <summary>
/// Disconnect from IMAP server.
/// </summary>
Task DisconnectImapAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Send email via SMTP.
/// </summary>
Task SendEmailAsync(MimeMessage message, string smtpServer, int smtpPort, string username, string password, bool useSsl = true, CancellationToken cancellationToken = default);
/// <summary>
/// Send email via SMTP using OAuth2.
/// </summary>
Task SendEmailOAuth2Async(MimeMessage message, string smtpServer, int smtpPort, string username, string accessToken, bool useSsl = true, CancellationToken cancellationToken = default);
/// <summary>
/// Get OAuth2 access token for Microsoft 365.
/// </summary>
Task<string> GetOAuth2AccessTokenAsync(string tenantId, string clientId, string clientSecret, string[] scopes, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,28 @@
namespace DigitalData.EmailProfiler.Application.Interfaces.Services;
/// <summary>
/// Encryption service interface for sensitive data (passwords, OAuth tokens).
/// Uses ASP.NET Core Data Protection API.
/// </summary>
public interface IEncryptionService
{
/// <summary>
/// Encrypt a string value.
/// </summary>
string Encrypt(string plainText);
/// <summary>
/// Decrypt an encrypted string value.
/// </summary>
string Decrypt(string cipherText);
/// <summary>
/// Encrypt a byte array.
/// </summary>
byte[] Encrypt(byte[] plainData);
/// <summary>
/// Decrypt an encrypted byte array.
/// </summary>
byte[] Decrypt(byte[] cipherData);
}

View File

@@ -0,0 +1,59 @@
namespace DigitalData.EmailProfiler.Application.Interfaces.Services;
/// <summary>
/// PDF processing service interface for validation and embedded file extraction.
/// </summary>
public interface IPdfProcessingService
{
/// <summary>
/// Validate if file is a valid PDF.
/// </summary>
Task<bool> IsValidPdfAsync(string filePath, CancellationToken cancellationToken = default);
/// <summary>
/// Validate if stream contains a valid PDF.
/// </summary>
Task<bool> IsValidPdfAsync(Stream stream, CancellationToken cancellationToken = default);
/// <summary>
/// Extract embedded files from PDF (e.g., ZUGFeRD XML).
/// </summary>
Task<IEnumerable<EmbeddedFile>> ExtractEmbeddedFilesAsync(string pdfPath, CancellationToken cancellationToken = default);
/// <summary>
/// Extract embedded files from PDF stream.
/// </summary>
Task<IEnumerable<EmbeddedFile>> ExtractEmbeddedFilesAsync(Stream pdfStream, CancellationToken cancellationToken = default);
/// <summary>
/// Check if PDF contains ZUGFeRD data.
/// </summary>
Task<bool> HasZugFeRDDataAsync(string pdfPath, CancellationToken cancellationToken = default);
/// <summary>
/// Extract ZUGFeRD XML from PDF.
/// </summary>
Task<string?> ExtractZugFeRDXmlAsync(string pdfPath, CancellationToken cancellationToken = default);
/// <summary>
/// Get PDF metadata (title, author, creation date, etc.).
/// </summary>
Task<PdfMetadata> GetMetadataAsync(string pdfPath, CancellationToken cancellationToken = default);
}
/// <summary>
/// Represents an embedded file extracted from a PDF.
/// </summary>
public record EmbeddedFile(string FileName, byte[] Content, string? Description = null);
/// <summary>
/// Represents PDF metadata.
/// </summary>
public record PdfMetadata(
string? Title,
string? Author,
string? Subject,
string? Keywords,
DateTime? CreationDate,
DateTime? ModificationDate,
int PageCount);