Move Infrastructure.Email into Features.Emails (#288)

This commit is contained in:
Aaron Po
2026-09-11 23:46:04 -04:00
committed by GitHub
parent 7187631ebc
commit de63e9f216
12 changed files with 4 additions and 31 deletions

View File

@@ -1,6 +1,5 @@
using Features.Emails.Services;
using Features.Emails.Templates.Rendering;
using Infrastructure.Email;
using Microsoft.Extensions.DependencyInjection;

View File

@@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MailKit" Version="4.16.0" />
<PackageReference
Include="Microsoft.Extensions.Configuration.Abstractions"
Version="10.0.1"
@@ -15,7 +16,6 @@
<ItemGroup>
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
<ProjectReference Include="..\Features.Emails.Templates\Features.Emails.Templates.csproj" />
<ProjectReference Include="..\Features.Users\Features.Users.csproj" />

View File

@@ -1,6 +1,5 @@
using Features.Emails.Templates.Rendering;
using Infrastructure.Configuration;
using Infrastructure.Email;
using Microsoft.Extensions.Configuration;
namespace Features.Emails.Services;

View File

@@ -0,0 +1,21 @@
namespace Features.Emails.Services;
/// <summary>
/// Service for sending emails.
/// </summary>
public interface IEmailProvider
{
/// <summary>Sends an email to a single recipient.</summary>
/// <param name="to">The recipient's email address.</param>
/// <param name="subject">The email subject line.</param>
/// <param name="body">The email body content.</param>
/// <param name="isHtml">Whether <paramref name="body"/> is HTML; when <see langword="false"/> it is sent as plain text.</param>
Task SendAsync(string to, string subject, string body, bool isHtml = true);
/// <summary>Sends an email to multiple recipients.</summary>
/// <param name="to">The recipients' email addresses.</param>
/// <param name="subject">The email subject line.</param>
/// <param name="body">The email body content.</param>
/// <param name="isHtml">Whether <paramref name="body"/> is HTML; when <see langword="false"/> it is sent as plain text.</param>
Task SendAsync(IEnumerable<string> to, string subject, string body, bool isHtml = true);
}

View File

@@ -0,0 +1,110 @@
using Infrastructure.Configuration;
using MailKit.Net.Smtp;
using MailKit.Security;
using Microsoft.Extensions.Configuration;
using MimeKit;
namespace Features.Emails.Services;
/// <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>, <c>SMTP_PORT</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 = ConfigurationHelpers.GetKeyOrThrow(configuration, ConfigurationKeys.SmtpHost);
_port = ConfigurationHelpers.GetIntKeyOrThrow(configuration, ConfigurationKeys.SmtpPort);
_username = configuration[ConfigurationKeys.SmtpUsername];
_password = configuration[ConfigurationKeys.SmtpPassword];
string useSslString = configuration[ConfigurationKeys.SmtpUseSsl] ?? "true";
_useSsl = bool.Parse(useSslString);
_fromEmail = ConfigurationHelpers.GetKeyOrThrow(
configuration,
ConfigurationKeys.SmtpFromEmail
);
_fromName = configuration[ConfigurationKeys.SmtpFromName] ?? "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);
}
}
}