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);
+ }
+}