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 _host; private readonly int _port; private readonly string? _username; private readonly string? _password; private readonly bool _useSsl; private readonly string _fromEmail; private readonly string _fromName; /// /// 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" ); var 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"); var 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 ) { var message = new MimeMessage(); message.From.Add(new MailboxAddress(_fromName, _fromEmail)); foreach (var recipient in to) { message.To.Add(MailboxAddress.Parse(recipient)); } message.Subject = subject; var bodyBuilder = new BodyBuilder(); if (isHtml) { bodyBuilder.HtmlBody = body; } else { bodyBuilder.TextBody = body; } message.Body = bodyBuilder.ToMessageBody(); using var client = new SmtpClient(); try { // Determine the SecureSocketOptions based on SSL setting var 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 ); } } }