using MailKit.Net.Smtp; using MailKit.Security; using MimeKit; namespace Infrastructure.Email; /// /// SMTP email service implementation using MailKit. /// Configured via environment variables. /// public class SmtpEmailProvider : IEmailProvider { private readonly string _fromEmail; private readonly string _fromName; private readonly string _host; private readonly string? _password; private readonly int _port; private readonly string? _username; private readonly bool _useSsl; /// /// Initializes a new instance of , reading SMTP configuration /// from environment variables. /// /// /// Reads the following environment variables: /// /// /// SMTP_HOST (required) - the SMTP server hostname. /// /// /// SMTP_PORT (optional, default "587") - the SMTP server port, must be a valid integer. /// /// /// SMTP_USERNAME (optional) - the username used for authentication. /// /// /// SMTP_PASSWORD (optional) - the password used for authentication. /// /// /// SMTP_USE_SSL (optional, default "true") - whether to use StartTls when connecting. /// /// /// SMTP_FROM_EMAIL (required) - the email address used as the sender. /// /// /// /// SMTP_FROM_NAME (optional, default "The Biergarten") - the display name used as the /// sender. /// /// /// /// /// /// Thrown when SMTP_HOST or SMTP_FROM_EMAIL is not set, or when SMTP_PORT is not a valid integer. /// public SmtpEmailProvider() { _host = Environment.GetEnvironmentVariable("SMTP_HOST") ?? throw new InvalidOperationException("SMTP_HOST environment variable is not set"); string portString = Environment.GetEnvironmentVariable("SMTP_PORT") ?? "587"; if (!int.TryParse(portString, out _port)) throw new InvalidOperationException($"SMTP_PORT '{portString}' is not a valid integer"); _username = Environment.GetEnvironmentVariable("SMTP_USERNAME"); _password = Environment.GetEnvironmentVariable("SMTP_PASSWORD"); string useSslString = Environment.GetEnvironmentVariable("SMTP_USE_SSL") ?? "true"; _useSsl = bool.Parse(useSslString); _fromEmail = Environment.GetEnvironmentVariable("SMTP_FROM_EMAIL") ?? throw new InvalidOperationException( "SMTP_FROM_EMAIL environment variable is not set" ); _fromName = Environment.GetEnvironmentVariable("SMTP_FROM_NAME") ?? "The Biergarten"; } /// /// Sends an email to a single recipient by delegating to the multi-recipient overload. /// /// Recipient email address /// Email subject line /// Email body (HTML or plain text) /// Whether the body is HTML (default: true) /// Thrown when connecting, authenticating, or sending via SMTP fails. public async Task SendAsync(string to, string subject, string body, bool isHtml = true) { await SendAsync([to], subject, body, isHtml); } /// /// Sends an email to multiple recipients using MailKit's . /// Connects using StartTls when SSL is enabled (or no encryption otherwise), authenticates /// if credentials were configured, sends the message, and disconnects. /// /// List of recipient email addresses /// Email subject line /// Email body (HTML or plain text) /// Whether the body is HTML (default: true) /// /// Thrown when connecting, authenticating, or sending via SMTP fails. The /// original exception is included as the inner exception. /// public async Task SendAsync( IEnumerable to, string subject, string body, bool isHtml = true ) { MimeMessage message = new(); message.From.Add(new MailboxAddress(_fromName, _fromEmail)); foreach (string recipient in to) message.To.Add(MailboxAddress.Parse(recipient)); message.Subject = subject; BodyBuilder bodyBuilder = new(); if (isHtml) bodyBuilder.HtmlBody = body; else bodyBuilder.TextBody = body; message.Body = bodyBuilder.ToMessageBody(); using SmtpClient client = new(); try { // Determine the SecureSocketOptions based on SSL setting SecureSocketOptions secureSocketOptions = _useSsl ? SecureSocketOptions.StartTls : SecureSocketOptions.None; await client.ConnectAsync(_host, _port, secureSocketOptions); // Authenticate if credentials are provided if (!string.IsNullOrEmpty(_username) && !string.IsNullOrEmpty(_password)) await client.AuthenticateAsync(_username, _password); await client.SendAsync(message); await client.DisconnectAsync(true); } catch (Exception ex) { throw new InvalidOperationException($"Failed to send email: {ex.Message}", ex); } } }