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