diff --git a/src/DigitalData.EmailProfiler.Application/Common/Interfaces/IDmsService.cs b/src/DigitalData.EmailProfiler.Application/Common/Interfaces/IDmsService.cs deleted file mode 100644 index 42963e3..0000000 --- a/src/DigitalData.EmailProfiler.Application/Common/Interfaces/IDmsService.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace DigitalData.EmailProfiler.Application.Common.Interfaces; - -/// -/// DMS service interface for windream integration. -/// -public interface IDmsService -{ - Task ImportDocumentAsync(string filePath, string objectType, Dictionary metadata, CancellationToken cancellationToken = default); - Task DocumentExistsAsync(string documentId, CancellationToken cancellationToken = default); - Task UpdateMetadataAsync(string documentId, Dictionary metadata, CancellationToken cancellationToken = default); -} diff --git a/src/DigitalData.EmailProfiler.Application/Common/Interfaces/IEmailService.cs b/src/DigitalData.EmailProfiler.Application/Common/Interfaces/IEmailService.cs index 4188635..31f9452 100644 --- a/src/DigitalData.EmailProfiler.Application/Common/Interfaces/IEmailService.cs +++ b/src/DigitalData.EmailProfiler.Application/Common/Interfaces/IEmailService.cs @@ -3,14 +3,16 @@ using DigitalData.EmailProfiler.Application.Common.Dtos; namespace DigitalData.EmailProfiler.Application.Common.Interfaces; /// -/// Email service interface for IMAP/SMTP operations. -/// Implementation uses MailKit. -/// Throws AuthenticationFailedException when OAuth2/password auth fails. +/// Email service interface for SMTP operations. +/// Implementation uses Limilabs Mail.dll for production email sending. +/// SMTP configuration is injected via IOptions<EmailAccountDto> in appsettings.json. +/// Throws AuthenticationFailedException when SMTP authentication fails. /// public interface IEmailService { - Task> ReceiveEmailsAsync(EmailAccountDto account, CancellationToken cancellationToken = default); - Task SendEmailAsync(EmailAccountDto account, string to, string subject, string body, bool isHtml = true, CancellationToken cancellationToken = default); - Task DeleteEmailAsync(EmailAccountDto account, int imapUid, CancellationToken cancellationToken = default); - Task GetOAuth2TokenAsync(string tenantId, string clientId, string clientSecret, CancellationToken cancellationToken = default); + /// + /// Sends an email using the configured SMTP account. + /// SMTP credentials are configured in appsettings.json (EmailAccount section). + /// + Task SendEmailAsync(string to, string subject, string body, bool isHtml = true, CancellationToken cancellationToken = default); } diff --git a/src/DigitalData.EmailProfiler.Infrastructure/Services/MailKitEmailService.cs b/src/DigitalData.EmailProfiler.Infrastructure/Services/MailKitEmailService.cs deleted file mode 100644 index 4b5e78e..0000000 --- a/src/DigitalData.EmailProfiler.Infrastructure/Services/MailKitEmailService.cs +++ /dev/null @@ -1,259 +0,0 @@ -using DigitalData.EmailProfiler.Application.Common.Dtos; -using DigitalData.EmailProfiler.Application.Common.Interfaces; -using DigitalData.EmailProfiler.Domain.Exceptions; -using MailKit; -using MailKit.Net.Imap; -using MailKit.Net.Smtp; -using MailKit.Search; -using MailKit.Security; -using Microsoft.Identity.Client; -using MimeKit; - -namespace DigitalData.EmailProfiler.Infrastructure.Services; - -/// -/// Email service using MailKit/MimeKit for IMAP/SMTP operations. -/// Supports OAuth2 authentication via Microsoft.Identity.Client (MSAL). -/// -public class MailKitEmailService(IEncryptionService encryptionService) : IEmailService -{ - private readonly IEncryptionService _encryptionService = encryptionService; - - public async Task> ReceiveEmailsAsync( - EmailAccountDto account, - CancellationToken cancellationToken = default) - { - using var imap = new ImapClient(); - - try - { - await ConnectAndAuthenticateImapAsync(imap, account, cancellationToken); - - var inbox = imap.Inbox; - await inbox.OpenAsync(FolderAccess.ReadWrite, cancellationToken); - - var uids = await inbox.SearchAsync(SearchQuery.NotSeen, cancellationToken); - var messages = new List(); - - foreach (var uid in uids) - { - var message = await inbox.GetMessageAsync(uid, cancellationToken); - - var emailMessage = new - { - MessageId = message.MessageId, - Sender = message.From.Mailboxes.FirstOrDefault()?.Address ?? string.Empty, - Subject = message.Subject ?? string.Empty, - Date = message.Date.DateTime, - BodyHtml = message.HtmlBody ?? string.Empty, - BodyText = message.TextBody ?? string.Empty, - Attachments = message.Attachments.Select(a => new - { - FileName = a.ContentDisposition?.FileName ?? "attachment", - FileSize = a is MimePart part ? (int)part.Content.Stream.Length : 0, - Content = a is MimePart mimePart ? ReadPartContent(mimePart) : Array.Empty() - }).ToList(), - ImapUid = (int)uid.Id - }; - - messages.Add(emailMessage); - } - - await imap.DisconnectAsync(true, cancellationToken); - return messages; - } - catch (AuthenticationException ex) - { - await DisconnectSafelyAsync(imap, cancellationToken); - throw new AuthenticationFailedException("IMAP authentication failed. Check credentials or OAuth2 configuration.", ex); - } - catch (Exception ex) - { - await DisconnectSafelyAsync(imap, cancellationToken); - throw new InvalidOperationException("Failed to receive emails from IMAP server.", ex); - } - } - - public async Task SendEmailAsync( - EmailAccountDto account, - string to, - string subject, - string body, - bool isHtml = true, - CancellationToken cancellationToken = default) - { - using var smtp = new SmtpClient(); - - try - { - await ConnectAndAuthenticateSmtpAsync(smtp, account, cancellationToken); - - var message = new MimeMessage(); - message.From.Add(MailboxAddress.Parse(account.Username)); - message.To.Add(MailboxAddress.Parse(to)); - message.Subject = subject; - - var builder = new BodyBuilder - { - HtmlBody = isHtml ? body : null, - TextBody = isHtml ? null : body - }; - - message.Body = builder.ToMessageBody(); - - await smtp.SendAsync(message, cancellationToken); - await smtp.DisconnectAsync(true, cancellationToken); - } - catch (AuthenticationException ex) - { - await DisconnectSafelyAsync(smtp, cancellationToken); - throw new AuthenticationFailedException("SMTP authentication failed. Check credentials or OAuth2 configuration.", ex); - } - catch (Exception ex) - { - await DisconnectSafelyAsync(smtp, cancellationToken); - throw new InvalidOperationException("Failed to send email via SMTP server.", ex); - } - } - - public async Task DeleteEmailAsync( - EmailAccountDto account, - int imapUid, - CancellationToken cancellationToken = default) - { - using var imap = new ImapClient(); - - try - { - await ConnectAndAuthenticateImapAsync(imap, account, cancellationToken); - - var inbox = imap.Inbox; - await inbox.OpenAsync(FolderAccess.ReadWrite, cancellationToken); - - var uid = new UniqueId((uint)imapUid); - await inbox.AddFlagsAsync(uid, MessageFlags.Deleted, true, cancellationToken); - await inbox.ExpungeAsync(cancellationToken); - - await imap.DisconnectAsync(true, cancellationToken); - } - catch (AuthenticationException ex) - { - await DisconnectSafelyAsync(imap, cancellationToken); - throw new AuthenticationFailedException("IMAP authentication failed. Check credentials or OAuth2 configuration.", ex); - } - catch (Exception ex) - { - await DisconnectSafelyAsync(imap, cancellationToken); - throw new InvalidOperationException($"Failed to delete email with UID {imapUid}.", ex); - } - } - - public async Task GetOAuth2TokenAsync( - string tenantId, - string clientId, - string clientSecret, - CancellationToken cancellationToken = default) - { - try - { - var decryptedSecret = _encryptionService.Decrypt(clientSecret); - - var app = ConfidentialClientApplicationBuilder - .Create(clientId) - .WithTenantId(tenantId) - .WithClientSecret(decryptedSecret) - .Build(); - - // Microsoft Graph scope for mail access - var scopes = new[] { "https://graph.microsoft.com/.default" }; - var result = await app - .AcquireTokenForClient(scopes) - .ExecuteAsync(cancellationToken); - - return result.AccessToken; - } - catch (MsalException ex) - { - throw new AuthenticationFailedException("Failed to acquire OAuth2 token from Microsoft Identity Platform.", ex); - } - } - - // --- Private Helper Methods --- - - private async Task ConnectAndAuthenticateImapAsync( - ImapClient imap, - EmailAccountDto account, - CancellationToken cancellationToken) - { - var secureSocketOptions = account.ImapUseSsl - ? SecureSocketOptions.SslOnConnect - : SecureSocketOptions.None; - - await imap.ConnectAsync(account.ImapServer, account.ImapPort, secureSocketOptions, cancellationToken); - - if (account.UseOAuth2) - { - var token = await GetOAuth2TokenAsync( - account.TenantId!, - account.ClientId!, - account.EncryptedClientSecret!, - cancellationToken); - - var oauth2 = new SaslMechanismOAuth2(account.Username, token); - await imap.AuthenticateAsync(oauth2, cancellationToken); - } - else - { - var password = _encryptionService.Decrypt(account.EncryptedPassword!); - await imap.AuthenticateAsync(account.Username, password, cancellationToken); - } - } - - private async Task ConnectAndAuthenticateSmtpAsync( - SmtpClient smtp, - EmailAccountDto account, - CancellationToken cancellationToken) - { - var secureSocketOptions = account.SmtpUseSsl - ? SecureSocketOptions.SslOnConnect - : SecureSocketOptions.None; - - await smtp.ConnectAsync(account.SmtpServer, account.SmtpPort, secureSocketOptions, cancellationToken); - - if (account.UseOAuth2) - { - var token = await GetOAuth2TokenAsync( - account.TenantId!, - account.ClientId!, - account.EncryptedClientSecret!, - cancellationToken); - - var oauth2 = new SaslMechanismOAuth2(account.Username, token); - await smtp.AuthenticateAsync(oauth2, cancellationToken); - } - else - { - var password = _encryptionService.Decrypt(account.EncryptedPassword!); - await smtp.AuthenticateAsync(account.Username, password, cancellationToken); - } - } - - private static async Task DisconnectSafelyAsync(ImapClient imap, CancellationToken cancellationToken) - { - if (imap.IsConnected) - await imap.DisconnectAsync(true, cancellationToken); - } - - private static async Task DisconnectSafelyAsync(SmtpClient smtp, CancellationToken cancellationToken) - { - if (smtp.IsConnected) - await smtp.DisconnectAsync(true, cancellationToken); - } - - private static byte[] ReadPartContent(MimePart part) - { - using var memory = new MemoryStream(); - part.Content.DecodeTo(memory); - return memory.ToArray(); - } -} diff --git a/src/DigitalData.EmailProfiler.Infrastructure/Services/WindreamDmsService.cs b/src/DigitalData.EmailProfiler.Infrastructure/Services/WindreamDmsService.cs deleted file mode 100644 index c8751a2..0000000 --- a/src/DigitalData.EmailProfiler.Infrastructure/Services/WindreamDmsService.cs +++ /dev/null @@ -1,316 +0,0 @@ -using DigitalData.EmailProfiler.Application.Common.Interfaces; -using DigitalData.EmailProfiler.Domain.Exceptions; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using System.Runtime.InteropServices; - -namespace DigitalData.EmailProfiler.Infrastructure.Services; - -/// -/// windream DMS service using COM Interop. -/// -/// IMPORTANT: Requires windream COM Interop DLLs to be registered on the system. -/// Throws DmsNotAvailableException if COM objects cannot be created. -/// -/// COM ProgIDs used: -/// - Windream.WMSession (WINDREAMLib) -/// - Windream.WMConnect (WINDREAMLib) -/// -/// Legacy reference: M:\Bibliotheken\3rdParty\windream\Interop.WINDREAMLib.dll -/// -/// NOTE: This service is OBSOLETE. The application now only provides email sending functionality. -/// This class is kept for reference but should not be used in new code. -/// -[Obsolete("WindreamDmsService is obsolete. The application now only provides email sending functionality.")] -public class WindreamDmsService : IDmsService -{ - private readonly string _windreamServer; - private readonly ILogger _logger; - private readonly object _sessionLock = new(); - - private object? _wmSession; - private object? _wmConnect; - private bool _isInitialized; - - public WindreamDmsService(IConfiguration configuration, ILogger logger) - { - _windreamServer = configuration["Windream:Server"] ?? throw new ArgumentNullException(nameof(configuration), "Windream:Server configuration is required."); - _logger = logger; - } - - public Task ImportDocumentAsync( - string filePath, - string objectType, - Dictionary metadata, - CancellationToken cancellationToken = default) - { - ArgumentException.ThrowIfNullOrWhiteSpace(filePath); - ArgumentException.ThrowIfNullOrWhiteSpace(objectType); - - if (!File.Exists(filePath)) - throw new FileNotFoundException($"File not found: {filePath}", filePath); - - lock (_sessionLock) - { - EnsureSessionInitialized(); - - try - { - var fileName = Path.GetFileName(filePath); - - // CreateWMObject(1, fileName) - 1 = WMEntityDocument - var oDocument = InvokeMember(_wmSession!, "CreateWMObject", 1, fileName); - - // Lock document - var isLocked = (bool)GetProperty(oDocument, "aLocked"); - if (!isLocked) - { - InvokeMember(oDocument, "lock"); - } - - // Set object type - var oObjectType = InvokeMember(_wmSession!, "GetWMObjectByName", 2, objectType); // 2 = WMEntityObjectType - SetProperty(oDocument, "aObjectType", oObjectType); - - InvokeMember(oDocument, "Save"); - - // Import file from disk - InvokeMember(oDocument, "FromDisk", filePath); - - // Index metadata - foreach (var (key, value) in metadata) - { - var indexValue = value.Length > 512 ? value[..512] : value; - - try - { - InvokeMember(oDocument, "SetVariableValue", key, indexValue); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to set windream index '{IndexName}' to '{IndexValue}'", key, indexValue); - } - } - - InvokeMember(oDocument, "Save"); - InvokeMember(oDocument, "unlock"); - - // Return windream document ID - var documentId = (int)GetProperty(oDocument, "aID"); - return Task.FromResult($"WD_{documentId}"); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to import document to windream: {FilePath}", filePath); - throw new InvalidOperationException($"Failed to import document to windream: {filePath}", ex); - } - } - } - - public Task DocumentExistsAsync( - string documentId, - CancellationToken cancellationToken = default) - { - ArgumentException.ThrowIfNullOrWhiteSpace(documentId); - - if (!documentId.StartsWith("WD_")) - return Task.FromResult(false); - - lock (_sessionLock) - { - EnsureSessionInitialized(); - - var idString = documentId.Replace("WD_", ""); - if (!int.TryParse(idString, out var id)) - return Task.FromResult(false); - - try - { - // GetWMObjectByID(1, id) - 1 = WMEntityDocument - var oDocument = InvokeMember(_wmSession!, "GetWMObjectByID", 1, id); - return Task.FromResult(oDocument != null); - } - catch - { - return Task.FromResult(false); - } - } - } - - public Task UpdateMetadataAsync( - string documentId, - Dictionary metadata, - CancellationToken cancellationToken = default) - { - ArgumentException.ThrowIfNullOrWhiteSpace(documentId); - - if (!documentId.StartsWith("WD_")) - throw new ArgumentException($"Invalid windream document ID: {documentId}", nameof(documentId)); - - lock (_sessionLock) - { - EnsureSessionInitialized(); - - var idString = documentId.Replace("WD_", ""); - if (!int.TryParse(idString, out var id)) - throw new ArgumentException($"Invalid windream document ID: {documentId}", nameof(documentId)); - - try - { - var oDocument = InvokeMember(_wmSession!, "GetWMObjectByID", 1, id); - if (oDocument == null) - throw new NotFoundException($"windream document with ID '{documentId}' not found."); - - var isLocked = (bool)GetProperty(oDocument, "aLocked"); - if (!isLocked) - { - InvokeMember(oDocument, "lock"); - } - - foreach (var (key, value) in metadata) - { - var indexValue = value.Length > 512 ? value[..512] : value; - - try - { - InvokeMember(oDocument, "SetVariableValue", key, indexValue); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to update windream index '{IndexName}' to '{IndexValue}'", key, indexValue); - } - } - - InvokeMember(oDocument, "Save"); - InvokeMember(oDocument, "unlock"); - - return Task.FromResult(true); - } - catch (NotFoundException) - { - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to update windream document metadata: {DocumentId}", documentId); - throw new InvalidOperationException($"Failed to update windream document metadata: {documentId}", ex); - } - } - } - - public void Dispose() - { - lock (_sessionLock) - { - try - { - if (_wmConnect != null && _wmSession != null && _isInitialized) - { - InvokeMember(_wmConnect, "Disconnect"); - } - - if (_wmConnect != null && Marshal.IsComObject(_wmConnect)) - { - Marshal.ReleaseComObject(_wmConnect); - } - - if (_wmSession != null && Marshal.IsComObject(_wmSession)) - { - Marshal.ReleaseComObject(_wmSession); - } - - _wmConnect = null; - _wmSession = null; - _isInitialized = false; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Error during windream COM cleanup"); - } - } - } - - // --- Private Helper Methods --- - - private void EnsureSessionInitialized() - { - if (_isInitialized && _wmSession != null && _wmConnect != null) - return; - - try - { - // Create WMSession object - var wmSessionType = Type.GetTypeFromProgID("Windream.WMSession") - ?? throw new DmsNotAvailableException("windream COM type 'Windream.WMSession' not found. Ensure windream is installed and COM components are registered."); - - _wmSession = Activator.CreateInstance(wmSessionType, _windreamServer) - ?? throw new DmsNotAvailableException("Failed to create WMSession instance."); - - // Create WMConnect object - var wmConnectType = Type.GetTypeFromProgID("Windream.WMConnect") - ?? throw new DmsNotAvailableException("windream COM type 'Windream.WMConnect' not found. Ensure windream is installed and COM components are registered."); - - _wmConnect = Activator.CreateInstance(wmConnectType) - ?? throw new DmsNotAvailableException("Failed to create WMConnect instance."); - - // Configure and login - SetProperty(_wmConnect, "ModuleID", 0); - SetProperty(_wmConnect, "MinReqVersion", "3"); - InvokeMember(_wmConnect, "LoginSession", _wmSession); - - var isLoggedIn = (bool)GetProperty(_wmSession, "aLoggedin"); - if (!isLoggedIn) - throw new DmsNotAvailableException("windream login failed. Check server configuration and connectivity."); - - _isInitialized = true; - _logger.LogInformation("windream session initialized successfully (Server: {Server})", _windreamServer); - } - catch (DmsNotAvailableException) - { - _isInitialized = false; - throw; - } - catch (COMException ex) - { - _isInitialized = false; - _logger.LogError(ex, "windream COM error during initialization"); - throw new DmsNotAvailableException("windream COM components are not available or not properly registered.", ex); - } - catch (Exception ex) - { - _isInitialized = false; - _logger.LogError(ex, "windream initialization failed"); - throw new DmsNotAvailableException("windream initialization failed. See inner exception for details.", ex); - } - } - - private static object InvokeMember(object obj, string memberName, params object[] args) - { - return obj.GetType().InvokeMember( - memberName, - System.Reflection.BindingFlags.InvokeMethod, - null, - obj, - args)!; - } - - private static object GetProperty(object obj, string propertyName) - { - return obj.GetType().InvokeMember( - propertyName, - System.Reflection.BindingFlags.GetProperty, - null, - obj, - null)!; - } - - private static void SetProperty(object obj, string propertyName, object value) - { - obj.GetType().InvokeMember( - propertyName, - System.Reflection.BindingFlags.SetProperty, - null, - obj, - [value]); - } -}