Refactor solution structure and add RabbitMQ config

Reorganized the solution structure to align with a layered architecture:
- Replaced `src` folder with `core`, `infrastructure`, and `presentation`.
- Moved projects to their respective folders.
- Added `DigitalData.MessagingService.Publisher.Abstraction` project.
- Removed `DigitalData.MessagingService.Client` project.

Updated project configurations and nesting in the solution file.

Added `appsettings.Secrets.json` with RabbitMQ and email account settings:
- RabbitMQ configuration includes hostname, port, credentials, and queue/exchange details.
- Email configuration includes SMTP server details and credentials.
This commit is contained in:
2026-07-28 10:26:15 +02:00
parent 2ad2dc6b4d
commit 78c82bf129
40 changed files with 67 additions and 40 deletions

View File

@@ -0,0 +1,20 @@
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Infrastructure.Queue;
using Microsoft.Extensions.Hosting;
namespace DigitalData.MessagingService.Infrastructure.Services.Background;
/// <summary>
/// A hosted background service responsible for initializing the outgoing email queue consumer.
/// Leverages a push-based, event-driven RabbitMQ consumer to eliminate polling overhead.
/// Email account configuration is resolved exclusively from application settings; no database access is performed.
/// </summary>
public class AsyncInitWorker(OutgoingEmailConsumer EmailConsumer) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await EmailConsumer.InitAsync();
await Task.Delay(Timeout.Infinite, stoppingToken).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
}
}

View File

@@ -0,0 +1,23 @@
using DigitalData.MessagingService.Application.Common.Interfaces;
using Microsoft.AspNetCore.DataProtection;
namespace DigitalData.MessagingService.Infrastructure.Services;
/// <summary>
/// Encryption service using ASP.NET Core Data Protection API.
/// Passwords are encrypted at rest in the database.
/// </summary>
public class DataProtectionEncryptionService(IDataProtectionProvider Provider) : IEncryptionService
{
private readonly IDataProtector Protector = Provider.CreateProtector("MessagingService.Passwords");
public string Encrypt(string plainText)
{
return Protector.Protect(plainText);
}
public string Decrypt(string cipherText)
{
return Protector.Unprotect(cipherText);
}
}

View File

@@ -0,0 +1,95 @@
using DevExpress.Pdf;
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Domain.Exceptions;
namespace DigitalData.MessagingService.Infrastructure.Services;
/// <summary>
/// PDF processing service using DevExpress.Pdf.
/// Implements PDF validation and embedded file extraction using streams.
/// </summary>
public class DevExpressPdfProcessingService : IPdfProcessingService
{
public Task<bool> ValidatePdfAsync(Stream pdfStream, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(pdfStream);
if (!pdfStream.CanRead)
throw new ArgumentException("Stream must be readable.", nameof(pdfStream));
if (!pdfStream.CanSeek)
throw new ArgumentException("Stream must be seekable.", nameof(pdfStream));
if (pdfStream.Position != 0)
pdfStream.Position = 0;
using var processor = new PdfDocumentProcessor();
processor.LoadDocument(pdfStream);
return Task.FromResult(true);
}
public async Task<IEnumerable<string>> ExtractEmbeddedFilesAsync(
Stream pdfStream,
string outputDirectory,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(pdfStream);
ArgumentException.ThrowIfNullOrWhiteSpace(outputDirectory);
if (!pdfStream.CanRead)
throw new ArgumentException("Stream must be readable.", nameof(pdfStream));
if (!pdfStream.CanSeek)
throw new ArgumentException("Stream must be seekable.", nameof(pdfStream));
if (pdfStream.Position != 0)
pdfStream.Position = 0;
if (!Directory.Exists(outputDirectory))
Directory.CreateDirectory(outputDirectory);
using var processor = new PdfDocumentProcessor();
processor.LoadDocument(pdfStream);
var extractedFiles = new List<string>();
var attachments = processor.Document.FileAttachments;
if (attachments == null || !attachments.Any())
return extractedFiles;
foreach (var attachment in attachments)
{
var fileName = attachment.FileName ?? $"attachment_{Guid.NewGuid()}.dat";
var outputPath = Path.Combine(outputDirectory, fileName);
var fileData = attachment.Data;
if (fileData == null || fileData.Length == 0)
continue;
await File.WriteAllBytesAsync(outputPath, fileData, cancellationToken);
extractedFiles.Add(outputPath);
}
return extractedFiles;
}
public Task<int> GetPageCountAsync(Stream pdfStream, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(pdfStream);
if (!pdfStream.CanRead)
throw new ArgumentException("Stream must be readable.", nameof(pdfStream));
if (!pdfStream.CanSeek)
throw new ArgumentException("Stream must be seekable.", nameof(pdfStream));
if (pdfStream.Position != 0)
pdfStream.Position = 0;
using var processor = new PdfDocumentProcessor();
processor.LoadDocument(pdfStream);
return Task.FromResult(processor.Document.Pages.Count);
}
}

View File

@@ -0,0 +1,111 @@
using System.Text;
using DigitalData.MessagingService.Application.Common.Dtos;
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Domain.Exceptions;
using Limilabs.Client.SMTP;
using Limilabs.Mail;
using Limilabs.Mail.Headers;
using Microsoft.Extensions.Options;
namespace DigitalData.MessagingService.Infrastructure.Services;
/// <summary>
/// Email service using Limilabs Mail.dll for SMTP operations (send-only).
/// Commercial-grade library with superior Exchange support.
/// SMTP configuration is injected via IOptions&lt;EmailAccountDto&gt; from appsettings.json.
/// </summary>
public class LimilabsEmailService(
IEncryptionService encryptionService,
IOptions<EmailAccountDto> smtpConfig) : IEmailService
{
private readonly EmailAccountDto _smtpAccount = smtpConfig.Value;
// Register encoding provider for Limilabs (requires windows-1252 and other code pages)
static LimilabsEmailService()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
}
public async Task SendEmailAsync(string to, string subject, string body, bool isHtml = true, CancellationToken cancellationToken = default)
{
using var smtp = new Smtp();
try
{
await ConnectAndAuthenticateSmtpAsync(smtp);
var builder = new MailBuilder();
builder.From.Add(new MailBox(_smtpAccount.Username));
builder.To.Add(new MailBox(to));
builder.Subject = subject;
if (isHtml)
{
builder.Html = body;
}
else
{
builder.Text = body;
}
var mail = builder.Create();
var result = smtp.SendMessage(mail);
if (result.Status != SendMessageStatus.Success)
{
throw new InvalidOperationException($"Failed to send email. Status: {result.Status}");
}
smtp.Close();
await Task.CompletedTask; // For async consistency
}
catch (Limilabs.Client.ServerException ex)
{
DisconnectSafely(smtp);
throw new AuthenticationFailedException("SMTP authentication failed. Check credentials or OAuth2 configuration.", ex);
}
catch (Exception ex)
{
DisconnectSafely(smtp);
throw new InvalidOperationException("Failed to send email via SMTP server.", ex);
}
}
// --- Private Helper Methods ---
private async Task ConnectAndAuthenticateSmtpAsync(Smtp smtp)
{
if (_smtpAccount.SmtpUseSsl)
{
smtp.ConnectSSL(_smtpAccount.SmtpServer, _smtpAccount.SmtpPort);
}
else
{
smtp.Connect(_smtpAccount.SmtpServer, _smtpAccount.SmtpPort);
}
if (_smtpAccount.UseOAuth2)
{
throw new NotSupportedException("OAuth2 is not configured for this SMTP account. UseOAuth2 must be false.");
}
else
{
var password = _smtpAccount.PasswordEncrypted ? encryptionService.Decrypt(_smtpAccount.Password) : _smtpAccount.Password;
smtp.Login(_smtpAccount.Username, password);
}
await Task.CompletedTask; // For async consistency
}
private static void DisconnectSafely(Smtp smtp)
{
try
{
if (smtp.Connected)
smtp.Close();
}
catch { /* Ignore disconnect errors */ }
}
}