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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<EmailAccountDto> 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 */ }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user