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:
@@ -14,6 +14,7 @@
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
|
||||
<PackageReference Include="MediatR" Version="14.2.0" />
|
||||
<PackageReference Include="MimeKit" Version="4.17.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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<T>
|
||||
/// 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user