diff --git a/API/ROLAC.API/Services/Notifications/ISmtpDispatcher.cs b/API/ROLAC.API/Services/Notifications/ISmtpDispatcher.cs new file mode 100644 index 0000000..1cab005 --- /dev/null +++ b/API/ROLAC.API/Services/Notifications/ISmtpDispatcher.cs @@ -0,0 +1,14 @@ +namespace ROLAC.API.Services.Notifications; + +/// One outbound email envelope handed to the SMTP transport. +public sealed record OutboundEmail( + string ToAddress, + string Subject, + string HtmlBody, + IReadOnlyList Attachments); + +/// Thin seam over the actual MailKit send so EmailService stays unit-testable. +public interface ISmtpDispatcher +{ + Task SendAsync(OutboundEmail email, CancellationToken ct = default); +} diff --git a/API/ROLAC.API/Services/Notifications/MailKitSmtpDispatcher.cs b/API/ROLAC.API/Services/Notifications/MailKitSmtpDispatcher.cs new file mode 100644 index 0000000..b09e22c --- /dev/null +++ b/API/ROLAC.API/Services/Notifications/MailKitSmtpDispatcher.cs @@ -0,0 +1,38 @@ +using MailKit.Net.Smtp; +using MailKit.Security; +using Microsoft.Extensions.Options; +using MimeKit; + +namespace ROLAC.API.Services.Notifications; + +/// Sends a single email via MailKit using the configured SMTP server. +public sealed class MailKitSmtpDispatcher : ISmtpDispatcher +{ + private readonly SmtpOptions _options; + + public MailKitSmtpDispatcher(IOptions options) => _options = options.Value; + + public async Task SendAsync(OutboundEmail email, CancellationToken ct = default) + { + var message = new MimeMessage(); + message.From.Add(new MailboxAddress(_options.FromName, _options.FromAddress)); + message.To.Add(MailboxAddress.Parse(email.ToAddress)); + message.Subject = email.Subject; + + var builder = new BodyBuilder { HtmlBody = email.HtmlBody }; + foreach (var attachment in email.Attachments) + { + builder.Attachments.Add( + attachment.FileName, attachment.Content, ContentType.Parse(attachment.ContentType)); + } + message.Body = builder.ToMessageBody(); + + using var client = new SmtpClient(); + var socketOptions = _options.UseSsl ? SecureSocketOptions.StartTls : SecureSocketOptions.Auto; + await client.ConnectAsync(_options.Host, _options.Port, socketOptions, ct); + if (!string.IsNullOrEmpty(_options.User)) + await client.AuthenticateAsync(_options.User, _options.Password, ct); + await client.SendAsync(message, ct); + await client.DisconnectAsync(true, ct); + } +}