email-service
SKILL.md
Email Service Integration
Overview
Abstracted email service with template support:
- Interface in Application -
IEmailService - SendGrid implementation - Production-ready provider
- Template support - Dynamic content with placeholders
- Localization - Multiple language templates
- Domain event integration - Send on user actions
Quick Reference
| Component | Purpose |
|---|---|
IEmailService |
Email abstraction interface |
SendGridEmailService |
SendGrid implementation |
EmailOptions |
Configuration settings |
EmailTemplate |
Template definitions |
Template: Email Service Interface
// src/{name}.application/Abstractions/Email/IEmailService.cs
namespace {name}.application.abstractions.email;
public interface IEmailService
{
Task SendAsync(
string to,
string subject,
string htmlBody,
CancellationToken cancellationToken = default);
Task SendTemplateAsync(
string to,
string templateId,
object templateData,
CancellationToken cancellationToken = default);
Task SendWelcomeEmailAsync(
string to,
string userName,
string language = "en",
CancellationToken cancellationToken = default);
Task SendPasswordResetEmailAsync(
string to,
string resetCode,
string language = "en",
CancellationToken cancellationToken = default);
Task SendAssessmentReadyEmailAsync(
string to,
string userName,
string assessmentName,
string language = "en",
CancellationToken cancellationToken = default);
}
Template: Email Options
// src/{name}.infrastructure/Email/EmailOptions.cs
namespace {name}.infrastructure.email;
public sealed class EmailOptions
{
public const string SectionName = "Email";
public string ApiKey { get; init; } = string.Empty;
public string FromEmail { get; init; } = string.Empty;
public string FromName { get; init; } = string.Empty;
public bool EnableSending { get; init; } = true;
// SendGrid template IDs
public EmailTemplateIds Templates { get; init; } = new();
}
public sealed class EmailTemplateIds
{
public string WelcomeEn { get; init; } = string.Empty;
public string WelcomeEs { get; init; } = string.Empty;
public string PasswordResetEn { get; init; } = string.Empty;
public string PasswordResetEs { get; init; } = string.Empty;
public string AssessmentReadyEn { get; init; } = string.Empty;
public string AssessmentReadyEs { get; init; } = string.Empty;
}
Template: SendGrid Implementation
// src/{name}.infrastructure/Email/SendGridEmailService.cs
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SendGrid;
using SendGrid.Helpers.Mail;
using {name}.application.abstractions.email;
namespace {name}.infrastructure.email;
internal sealed class SendGridEmailService : IEmailService
{
private readonly ISendGridClient _client;
private readonly EmailOptions _options;
private readonly ILogger<SendGridEmailService> _logger;
public SendGridEmailService(
IOptions<EmailOptions> options,
ILogger<SendGridEmailService> logger)
{
_options = options.Value;
_client = new SendGridClient(_options.ApiKey);
_logger = logger;
}
public async Task SendAsync(
string to,
string subject,
string htmlBody,
CancellationToken cancellationToken = default)
{
if (!_options.EnableSending)
{
_logger.LogInformation(
"Email sending disabled. Would send to {To}: {Subject}",
to, subject);
return;
}
var message = new SendGridMessage
{
From = new EmailAddress(_options.FromEmail, _options.FromName),
Subject = subject,
HtmlContent = htmlBody
};
message.AddTo(new EmailAddress(to));
var response = await _client.SendEmailAsync(message, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var body = await response.Body.ReadAsStringAsync(cancellationToken);
_logger.LogError(
"Failed to send email to {To}. Status: {Status}, Body: {Body}",
to, response.StatusCode, body);
throw new EmailSendException($"Failed to send email: {response.StatusCode}");
}
_logger.LogInformation("Email sent to {To}: {Subject}", to, subject);
}
public async Task SendTemplateAsync(
string to,
string templateId,
object templateData,
CancellationToken cancellationToken = default)
{
if (!_options.EnableSending)
{
_logger.LogInformation(
"Email sending disabled. Would send template {TemplateId} to {To}",
templateId, to);
return;
}
var message = new SendGridMessage
{
From = new EmailAddress(_options.FromEmail, _options.FromName),
TemplateId = templateId
};
message.AddTo(new EmailAddress(to));
message.SetTemplateData(templateData);
var response = await _client.SendEmailAsync(message, cancellationToken);
if (!response.IsSuccessStatusCode)
{
_logger.LogError(
"Failed to send template email to {To}. Template: {TemplateId}",
to, templateId);
throw new EmailSendException($"Failed to send email: {response.StatusCode}");
}
_logger.LogInformation(
"Template email sent to {To}. Template: {TemplateId}",
to, templateId);
}
public async Task SendWelcomeEmailAsync(
string to,
string userName,
string language = "en",
CancellationToken cancellationToken = default)
{
var templateId = language.ToLower() switch
{
"es" => _options.Templates.WelcomeEs,
_ => _options.Templates.WelcomeEn
};
await SendTemplateAsync(to, templateId, new
{
user_name = userName,
login_url = "https://app.example.com/login"
}, cancellationToken);
}
public async Task SendPasswordResetEmailAsync(
string to,
string resetCode,
string language = "en",
CancellationToken cancellationToken = default)
{
var templateId = language.ToLower() switch
{
"es" => _options.Templates.PasswordResetEs,
_ => _options.Templates.PasswordResetEn
};
await SendTemplateAsync(to, templateId, new
{
reset_code = resetCode,
reset_url = $"https://app.example.com/reset-password?code={resetCode}",
expiration_hours = 24
}, cancellationToken);
}
public async Task SendAssessmentReadyEmailAsync(
string to,
string userName,
string assessmentName,
string language = "en",
CancellationToken cancellationToken = default)
{
var templateId = language.ToLower() switch
{
"es" => _options.Templates.AssessmentReadyEs,
_ => _options.Templates.AssessmentReadyEn
};
await SendTemplateAsync(to, templateId, new
{
user_name = userName,
assessment_name = assessmentName,
assessment_url = "https://app.example.com/assessments"
}, cancellationToken);
}
}
public class EmailSendException : Exception
{
public EmailSendException(string message) : base(message) { }
}
Template: Domain Event Handler
// src/{name}.application/Users/EventHandlers/UserCreatedSendWelcomeEmailHandler.cs
using MediatR;
using Microsoft.Extensions.Logging;
using {name}.application.abstractions.email;
using {name}.domain.users.events;
namespace {name}.application.users.eventhandlers;
internal sealed class UserCreatedSendWelcomeEmailHandler
: INotificationHandler<UserCreatedDomainEvent>
{
private readonly IEmailService _emailService;
private readonly IUserRepository _userRepository;
private readonly ILogger<UserCreatedSendWelcomeEmailHandler> _logger;
public UserCreatedSendWelcomeEmailHandler(
IEmailService emailService,
IUserRepository userRepository,
ILogger<UserCreatedSendWelcomeEmailHandler> logger)
{
_emailService = emailService;
_userRepository = userRepository;
_logger = logger;
}
public async Task Handle(
UserCreatedDomainEvent notification,
CancellationToken cancellationToken)
{
var user = await _userRepository.GetByIdAsync(
notification.UserId,
cancellationToken);
if (user is null)
{
_logger.LogWarning(
"User {UserId} not found for welcome email",
notification.UserId);
return;
}
await _emailService.SendWelcomeEmailAsync(
user.Email.Value,
user.Name,
user.PreferredLanguage,
cancellationToken);
}
}
Template: Registration
// appsettings.json
{
"Email": {
"ApiKey": "SG.your-sendgrid-api-key",
"FromEmail": "noreply@example.com",
"FromName": "My Application",
"EnableSending": true,
"Templates": {
"WelcomeEn": "d-xxxxxxxxxxxxx",
"WelcomeEs": "d-xxxxxxxxxxxxx",
"PasswordResetEn": "d-xxxxxxxxxxxxx",
"PasswordResetEs": "d-xxxxxxxxxxxxx"
}
}
}
// DependencyInjection.cs
services.Configure<EmailOptions>(configuration.GetSection(EmailOptions.SectionName));
services.AddScoped<IEmailService, SendGridEmailService>();
Related Skills
domain-events-generator- Trigger emails from eventsquartz-background-jobs- Scheduled email jobsoutbox-pattern- Reliable email delivery
Weekly Installs
3
Repository
ronnythedev/dot…e-skillsGitHub Stars
42
First Seen
13 days ago
Security Audits
Installed on
opencode3
gemini-cli3
codebuddy3
github-copilot3
codex3
kimi-cli3