AWS SES Email Service
Overview
This skill implements email delivery via AWS SES for APIs:
- AWS SES Integration - Production-ready email delivery
- HTML Templates - File-based templates with placeholder replacement
- Result Pattern - No exceptions, returns
Result<T>for error handling - Enable/Disable Toggle - Development mode without actual sending
Quick Reference
| Component | Purpose | Location |
|---|---|---|
IEmailService | Email abstraction interface | Application/Abstractions/Email |
AwsSesEmailService | AWS SES implementation | Infrastructure/Email |
EmailOptions | AWS SES configuration | Infrastructure/Email |
EmailErrors | Error definitions | Application/Abstractions/Email |
Email Structure
/Application/Abstractions/
├── Email/
│ ├── IEmailService.cs
│ └── EmailErrors.cs
/Infrastructure/
├── Email/
│ ├── EmailOptions.cs
│ └── AwsSesEmailService.cs
/Api/
├── EmailTemplates/
│ ├── appointment-reminder.html
│ ├── appointment-reminder-es.html
│ ├── test-results-ready.html
│ ├── prescription-ready.html
│ ├── welcome.html
│ └── password-reset.html
Template: Email Service Interface
// src/{name}.application/Abstractions/Email/IEmailService.cs
using {name}.domain.abstractions;
namespace {name}.application.Abstractions.Email;
/// <summary>
/// Service for sending emails via AWS SES
/// Returns Result pattern for error handling (no exceptions)
/// </summary>
public interface IEmailService
{
/// <summary>
/// Send an email using a template file with placeholder replacements
/// </summary>
/// <param name="toEmail">Recipient email address</param>
/// <param name="subject">Email subject</param>
/// <param name="templateName">Name of the template file (without extension)</param>
/// <param name="placeholders">Dictionary of placeholder keys and replacement values</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Result indicating success or failure</returns>
Task<r> SendTemplatedEmailAsync(
string toEmail,
string subject,
string templateName,
Dictionary<string, string> placeholders,
CancellationToken cancellationToken = default);
/// <summary>
/// Send an email with raw HTML content
/// </summary>
/// <param name="toEmail">Recipient email address</param>
/// <param name="subject">Email subject</param>
/// <param name="htmlBody">HTML content of the email</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Result indicating success or failure</returns>
Task<r> SendHtmlEmailAsync(
string toEmail,
string subject,
string htmlBody,
CancellationToken cancellationToken = default);
}
Template: Email Errors
// src/{name}.application/Abstractions/Email/EmailErrors.cs
using {name}.domain.abstractions;
namespace {name}.application.Abstractions.Email;
public static class EmailErrors
{
public static readonly Error SendFailed = new(
"Email.SendFailed",
"Failed to send email. Please try again later.");
public static readonly Error TemplateNotFound = new(
"Email.TemplateNotFound",
"Email template not found.");
public static readonly Error InvalidRecipient = new(
"Email.InvalidRecipient",
"Invalid recipient email address.");
public static readonly Error EmailDisabled = new(
"Email.Disabled",
"Email sending is currently disabled.");
}
Template: Email Options
// src/{name}.infrastructure/Email/EmailOptions.cs
namespace {name}.infrastructure.Email;
public sealed class EmailOptions
{
public const string SectionName = "Email";
/// <summary>
/// AWS region for SES (e.g., "us-east-1")
/// </summary>
public string AwsRegion { get; init; } = "us-east-1";
/// <summary>
/// AWS access key ID (optional - use IAM role in production)
/// </summary>
public string? AwsAccessKeyId { get; init; }
/// <summary>
/// AWS secret access key (optional - use IAM role in production)
/// </summary>
public string? AwsSecretAccessKey { get; init; }
/// <summary>
/// Email address to send from
/// </summary>
public string FromAddress { get; init; } = string.Empty;
/// <summary>
/// Display name for the sender (e.g., "Support Team")
/// </summary>
public string FromName { get; init; } = string.Empty;
/// <summary>
/// Whether email sending is enabled (disable for development)
/// </summary>
public bool Enabled { get; init; } = false;
/// <summary>
/// Path to email templates directory (relative to app base)
/// </summary>
public string TemplatesPath { get; init; } = "EmailTemplates";
}
appsettings.json
{
"Email": {
"AwsRegion": "us-east-1",
"AwsAccessKeyId": "",
"AwsSecretAccessKey": "",
"FromAddress": "noreply@healthcare.example.com",
"FromName": "Supoort Team",
"Enabled": true,
"TemplatesPath": "EmailTemplates"
}
}
Template: AWS SES Email Service Implementation
// src/{name}.infrastructure/Email/AwsSesEmailService.cs
using Amazon;
using Amazon.SimpleEmailV2;
using Amazon.SimpleEmailV2.Model;
using {name}.application.Abstractions.Email;
using {name}.domain.abstractions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace {name}.infrastructure.Email;
internal sealed class AwsSesEmailService : IEmailService
{
private readonly EmailOptions _options;
private readonly ILogger<AwsSesEmailService> _logger;
private readonly IAmazonSimpleEmailServiceV2 _sesClient;
private readonly string _templatesPath;
public AwsSesEmailService(
IOptions<EmailOptions> options,
ILogger<AwsSesEmailService> logger)
{
_options = options.Value;
_logger = logger;
// Initialize SES v2 client
var region = RegionEndpoint.GetBySystemName(_options.AwsRegion);
if (!string.IsNullOrEmpty(_options.AwsAccessKeyId) &&
!string.IsNullOrEmpty(_options.AwsSecretAccessKey))
{
// Use explicit credentials (dev/staging)
_sesClient = new AmazonSimpleEmailServiceV2Client(
_options.AwsAccessKeyId,
_options.AwsSecretAccessKey,
region);
}
else
{
// Use IAM role or environment credentials (production)
_sesClient = new AmazonSimpleEmailServiceV2Client(region);
}
// Set templates path relative to application base directory
_templatesPath = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
_options.TemplatesPath);
}
public async Task<r> SendTemplatedEmailAsync(
string toEmail,
string subject,
string templateName,
Dictionary<string, string> placeholders,
CancellationToken cancellationToken = default)
{
// Load template file
var templatePath = Path.Combine(_templatesPath, $"{templateName}.html");
if (!File.Exists(templatePath))
{
_logger.LogError("Email template not found: {TemplatePath}", templatePath);
return Result.Failure(EmailErrors.TemplateNotFound);
}
var htmlBody = await File.ReadAllTextAsync(templatePath, cancellationToken);
// Replace placeholders using {{key}} syntax
foreach (var (key, value) in placeholders)
{
htmlBody = htmlBody.Replace($"{{{{{key}}}}}", value);
}
return await SendHtmlEmailAsync(toEmail, subject, htmlBody, cancellationToken);
}
public async Task<r> SendHtmlEmailAsync(
string toEmail,
string subject,
string htmlBody,
CancellationToken cancellati