From dotnet-skills
Test email sending locally using Mailpit with .NET Aspire. Captures all outgoing emails without sending them. View rendered HTML, inspect headers, and verify delivery in integration tests. Use when testing email delivery locally without sending real emails, setting up email infrastructure in .NET Aspire, or writing integration tests that verify emails are sent.
npx claudepluginhub wshaddix/dotnet-skillsThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Use this skill when:
Related skills:
aspnetcore/mjml-email-templates - MJML template authoringtesting/verify-email-snapshots - Snapshot test rendered HTMLaspire/integration-testing - General Aspire testing patternsMailpit is a lightweight email testing tool that:
Perfect for development and integration testing.
Add Mailpit as a container in your AppHost:
// AppHost/Program.cs
var builder = DistributedApplication.CreateBuilder(args);
// Add Mailpit for email testing
var mailpit = builder.AddContainer("mailpit", "axllent/mailpit")
.WithHttpEndpoint(port: 8025, targetPort: 8025, name: "ui")
.WithEndpoint(port: 1025, targetPort: 1025, name: "smtp");
// Reference in your API project
var api = builder.AddProject<Projects.MyApp_Api>("api")
.WithReference(mailpit.GetEndpoint("smtp"))
.WithEnvironment("Smtp__Host", mailpit.GetEndpoint("smtp"));
builder.Build().Run();
{
"Smtp": {
"Host": "localhost",
"Port": 1025,
"EnableSsl": false,
"FromAddress": "noreply@myapp.com",
"FromName": "MyApp"
}
}
public class SmtpSettings
{
public string Host { get; set; } = "localhost";
public int Port { get; set; } = 1025;
public bool EnableSsl { get; set; } = false;
public string FromAddress { get; set; } = "noreply@myapp.com";
public string FromName { get; set; } = "MyApp";
// Optional: For production SMTP
public string? Username { get; set; }
public string? Password { get; set; }
}
// In Program.cs or extension method
services.Configure<SmtpSettings>(configuration.GetSection("Smtp"));
services.AddSingleton<IEmailSender>(sp =>
{
var settings = sp.GetRequiredService<IOptions<SmtpSettings>>().Value;
return new SmtpEmailSender(settings);
});
public interface IEmailSender
{
Task SendEmailAsync(EmailMessage message, CancellationToken ct = default);
}
public sealed class SmtpEmailSender : IEmailSender
{
private readonly SmtpSettings _settings;
public SmtpEmailSender(SmtpSettings settings)
{
_settings = settings;
}
public async Task SendEmailAsync(EmailMessage message, CancellationToken ct = default)
{
using var client = new SmtpClient();
await client.ConnectAsync(
_settings.Host,
_settings.Port,
_settings.EnableSsl ? SecureSocketOptions.StartTls : SecureSocketOptions.None,
ct);
if (!string.IsNullOrEmpty(_settings.Username))
{
await client.AuthenticateAsync(_settings.Username, _settings.Password, ct);
}
var mailMessage = new MimeMessage();
mailMessage.From.Add(new MailboxAddress(_settings.FromName, _settings.FromAddress));
mailMessage.To.Add(new MailboxAddress(message.ToName, message.To));
mailMessage.Subject = message.Subject;
var bodyBuilder = new BodyBuilder { HtmlBody = message.HtmlBody };
mailMessage.Body = bodyBuilder.ToMessageBody();
await client.SendAsync(mailMessage, ct);
await client.DisconnectAsync(true, ct);
}
}
Requires MailKit package:
dotnet add package MailKit
Navigate to http://localhost:8025 to see:
The Mailpit UI endpoint appears in the Aspire dashboard under Resources.
public class EmailIntegrationTests : IClassFixture<AspireFixture>
{
private readonly HttpClient _client;
private readonly MailpitClient _mailpit;
public EmailIntegrationTests(AspireFixture fixture)
{
_client = fixture.CreateClient();
_mailpit = new MailpitClient(fixture.GetMailpitUrl());
}
[Fact]
public async Task SignupFlow_SendsWelcomeEmail()
{
// Arrange
await _mailpit.ClearMessagesAsync();
// Act - Trigger signup flow
var response = await _client.PostAsJsonAsync("/api/auth/signup", new
{
Email = "test@example.com",
Password = "SecurePassword123!"
});
response.EnsureSuccessStatusCode();
// Assert - Verify email was sent
var messages = await _mailpit.GetMessagesAsync();
var welcomeEmail = messages.Should().ContainSingle()
.Which;
welcomeEmail.To.Should().Contain("test@example.com");
welcomeEmail.Subject.Should().Contain("Welcome");
welcomeEmail.HtmlBody.Should().Contain("Thank you for signing up");
}
}
public class MailpitClient
{
private readonly HttpClient _client;
public MailpitClient(string baseUrl)
{
_client = new HttpClient { BaseAddress = new Uri(baseUrl) };
}
public async Task<List<MailpitMessage>> GetMessagesAsync()
{
var response = await _client.GetFromJsonAsync<MailpitResponse>("/api/v1/messages");
return response?.Messages ?? new List<MailpitMessage>();
}
public async Task ClearMessagesAsync()
{
await _client.DeleteAsync("/api/v1/messages");
}
public async Task<MailpitMessage?> WaitForMessageAsync(
Func<MailpitMessage, bool> predicate,
TimeSpan timeout)
{
var deadline = DateTime.UtcNow + timeout;
while (DateTime.UtcNow < deadline)
{
var messages = await GetMessagesAsync();
var match = messages.FirstOrDefault(predicate);
if (match != null)
return match;
await Task.Delay(100);
}
return null;
}
}
public class MailpitResponse
{
public List<MailpitMessage> Messages { get; set; } = new();
}
public class MailpitMessage
{
public string Id { get; set; } = "";
public List<string> To { get; set; } = new();
public string Subject { get; set; } = "";
public string HtmlBody { get; set; } = "";
}
public class AspireFixture : IAsyncLifetime
{
private DistributedApplication? _app;
private string _mailpitUrl = "";
public async Task InitializeAsync()
{
var appHost = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.MyApp_AppHost>();
// Disable persistence for clean tests
appHost.Configuration["MyApp:UseVolumes"] = "false";
_app = await appHost.BuildAsync();
await _app.StartAsync();
// Get Mailpit URL from Aspire
var mailpit = _app.GetContainerResource("mailpit");
_mailpitUrl = await mailpit.GetEndpointAsync("ui");
}
public HttpClient CreateClient()
{
var api = _app!.GetProjectResource("api");
return api.CreateHttpClient();
}
public string GetMailpitUrl() => _mailpitUrl;
public async Task DisposeAsync()
{
if (_app != null)
await _app.DisposeAsync();
}
}
Some emails are sent asynchronously. Wait for them:
[Fact]
public async Task AsyncWorkflow_EventuallySendsEmail()
{
await _mailpit.ClearMessagesAsync();
// Trigger async workflow
await _client.PostAsync("/api/workflows/start", null);
// Wait for email (with timeout)
var email = await _mailpit.WaitForMessageAsync(
m => m.Subject.Contains("Workflow Complete"),
timeout: TimeSpan.FromSeconds(10));
email.Should().NotBeNull();
}
[Fact]
public async Task BulkOperation_SendsMultipleEmails()
{
await _mailpit.ClearMessagesAsync();
await _client.PostAsJsonAsync("/api/invitations/bulk", new
{
Emails = new[] { "a@test.com", "b@test.com", "c@test.com" }
});
var messages = await _mailpit.WaitForMessagesAsync(
expectedCount: 3,
timeout: TimeSpan.FromSeconds(10));
messages.Should().HaveCount(3);
messages.Select(m => m.To.First())
.Should().BeEquivalentTo("a@test.com", "b@test.com", "c@test.com");
}
[Fact]
public async Task PasswordReset_ContainsValidResetLink()
{
await _mailpit.ClearMessagesAsync();
await _client.PostAsJsonAsync("/api/auth/forgot-password", new
{
Email = "user@test.com"
});
var email = await _mailpit.WaitForMessageAsync(
m => m.Subject.Contains("Password Reset"),
timeout: TimeSpan.FromSeconds(5));
// Extract reset link from HTML
var resetLink = Regex.Match(email!.HtmlBody, @"href=""([^""]+/reset/[^""]+)""")
.Groups[1].Value;
resetLink.Should().StartWith("https://myapp.com/reset/");
// Verify the link works
var resetResponse = await _client.GetAsync(resetLink);
resetResponse.StatusCode.Should().Be(HttpStatusCode.OK);
}
services.AddSingleton<IEmailSender>(sp =>
{
var settings = sp.GetRequiredService<IOptions<SmtpSettings>>().Value;
var env = sp.GetRequiredService<IHostEnvironment>();
if (env.IsDevelopment())
{
// Mailpit - no auth, no SSL
return new SmtpEmailSender(settings);
}
else
{
// Production SMTP (SendGrid, Postmark, etc.)
return new SmtpEmailSender(settings with
{
EnableSsl = true
});
}
});
# Verify Mailpit is listening
curl http://localhost:8025/api/v1/messages
// Ensure endpoint reference is correct
.WithEnvironment("Smtp__Host", mailpit.GetEndpoint("smtp").Property(EndpointProperty.Host))
.WithEnvironment("Smtp__Port", mailpit.GetEndpoint("smtp").Property(EndpointProperty.Port))