Files
the-biergarten-app/web/backend/Infrastructure/Infrastructure.Email/SmtpEmailProvider.cs
Aaron Po c825d99b0a Refactor comments and update documentation for backend and pipeline (#246)
* Remove restating comments in backend

* Remove restating comments in frontend stories

* Remove restating comments in pipeline

* Remove restating comments in pipeline scripts

* Restore location-clear comment in BreweryRepository

* Remove stale/restating comments found in second pass

* Restore mock curated-data branch comment in main.cc

* Remove more restating/filler comments in backend

* Document account-management endpoints, compose files, and env generator

* Squashed commit of the following:

commit a8a5b8abd9969859c695092fae6990d03b2135ed
Author: Aaron Po <aaron.po97@gmail.com>
Date:   Sun Aug 23 23:53:56 2026 -0400

    Add reverse geocoding for brewery street addresses (#245)

    * Add geocoding to seed application, remove postal codes from generator

    Update comments

* Update pipeline class diagram for OpenAI generator

* Update pipeline activity diagram for brewery coordinates and OpenAI backend

* Add mailpit, production, db-only, and minimal environments to deployment diagram

* Fix authentication flow diagram for Identity migration

* Add account-management flows to authentication diagram

* Fix database schema diagram note referencing removed AuthRepository
2026-08-24 01:09:48 -04:00

114 lines
3.9 KiB
C#

using MailKit.Net.Smtp;
using MailKit.Security;
using Microsoft.Extensions.Configuration;
using MimeKit;
namespace Infrastructure.Email;
/// <summary>
/// SMTP email service implementation using MailKit.
/// Configured via application configuration (backed by environment variables).
/// </summary>
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;
/// <summary>
/// Initializes a new instance of the <see cref="SmtpEmailProvider" /> class, reading SMTP configuration
/// (<c>SMTP_HOST</c>, <c>SMTP_PORT</c>, <c>SMTP_USERNAME</c>, <c>SMTP_PASSWORD</c>, <c>SMTP_USE_SSL</c>,
/// <c>SMTP_FROM_EMAIL</c>, <c>SMTP_FROM_NAME</c>) from <paramref name="configuration" />.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown when <c>SMTP_HOST</c> or <c>SMTP_FROM_EMAIL</c> is not set, or when <c>SMTP_PORT</c> is not a valid integer.
/// </exception>
public SmtpEmailProvider(IConfiguration configuration)
{
_host =
configuration["SMTP_HOST"]
?? throw new InvalidOperationException("SMTP_HOST environment variable is not set");
string portString = configuration["SMTP_PORT"] ?? "587";
if (!int.TryParse(portString, out _port))
throw new InvalidOperationException($"SMTP_PORT '{portString}' is not a valid integer");
_username = configuration["SMTP_USERNAME"];
_password = configuration["SMTP_PASSWORD"];
string useSslString = configuration["SMTP_USE_SSL"] ?? "true";
_useSsl = bool.Parse(useSslString);
_fromEmail =
configuration["SMTP_FROM_EMAIL"]
?? throw new InvalidOperationException(
"SMTP_FROM_EMAIL environment variable is not set"
);
_fromName = configuration["SMTP_FROM_NAME"] ?? "The Biergarten";
}
/// <inheritdoc/>
public async Task SendAsync(string to, string subject, string body, bool isHtml = true)
{
await SendAsync([to], subject, body, isHtml);
}
/// <inheritdoc/>
/// <remarks>
/// Connects using StartTls when SSL is enabled (or no encryption otherwise), authenticates
/// if credentials were configured, sends the message, and disconnects.
/// </remarks>
/// <exception cref="InvalidOperationException">
/// Thrown when connecting, authenticating, or sending via SMTP fails, wrapping the original exception.
/// </exception>
public async Task SendAsync(
IEnumerable<string> 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
{
SecureSocketOptions secureSocketOptions = _useSsl
? SecureSocketOptions.StartTls
: SecureSocketOptions.None;
await client.ConnectAsync(_host, _port, secureSocketOptions);
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);
}
}
}