mailpit-integration
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseEmail Testing with Mailpit and .NET Aspire
结合Mailpit与.NET Aspire进行邮件测试
When to Use This Skill
何时使用此技能
Use this skill when:
- Testing email delivery locally without sending real emails
- Setting up email infrastructure in .NET Aspire
- Writing integration tests that verify emails are sent
- Debugging email rendering and headers
Related skills:
- - MJML template authoring
aspnetcore/mjml-email-templates - - Snapshot test rendered HTML
testing/verify-email-snapshots - - General Aspire testing patterns
aspire/integration-testing
在以下场景使用此技能:
- 在本地测试邮件投递,无需发送真实邮件
- 在.NET Aspire中搭建邮件基础设施
- 编写验证邮件发送情况的集成测试
- 调试邮件渲染效果与邮件头
相关技能:
- - MJML模板编写
aspnetcore/mjml-email-templates - - 快照测试渲染后的HTML
testing/verify-email-snapshots - - Aspire通用测试模式
aspire/integration-testing
What is Mailpit?
什么是Mailpit?
Mailpit is a lightweight email testing tool that:
- Captures all SMTP traffic without delivering emails
- Provides a web UI to view captured emails
- Exposes an API for programmatic access
- Supports HTML rendering, headers, and attachments
Perfect for development and integration testing.
Mailpit是一款轻量级邮件测试工具,具备以下功能:
- 捕获所有SMTP流量,不实际投递邮件
- 提供Web UI查看捕获的邮件
- 暴露API支持程序化访问
- 支持HTML渲染、邮件头与附件查看
非常适合开发与集成测试场景。
Aspire AppHost Configuration
Aspire AppHost配置
Add Mailpit as a container in your AppHost:
csharp
// 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();在你的AppHost中添加Mailpit容器:
csharp
// AppHost/Program.cs
var builder = DistributedApplication.CreateBuilder(args);
// 添加Mailpit用于邮件测试
var mailpit = builder.AddContainer("mailpit", "axllent/mailpit")
.WithHttpEndpoint(port: 8025, targetPort: 8025, name: "ui")
.WithEndpoint(port: 1025, targetPort: 1025, name: "smtp");
// 在API项目中引用
var api = builder.AddProject<Projects.MyApp_Api>("api")
.WithReference(mailpit.GetEndpoint("smtp"))
.WithEnvironment("Smtp__Host", mailpit.GetEndpoint("smtp"));
builder.Build().Run();SMTP Configuration
SMTP配置
appsettings.json
appsettings.json
json
{
"Smtp": {
"Host": "localhost",
"Port": 1025,
"EnableSsl": false,
"FromAddress": "noreply@myapp.com",
"FromName": "MyApp"
}
}json
{
"Smtp": {
"Host": "localhost",
"Port": 1025,
"EnableSsl": false,
"FromAddress": "noreply@myapp.com",
"FromName": "MyApp"
}
}Configuration Class
配置类
csharp
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; }
}csharp
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";
// 可选:生产环境SMTP配置
public string? Username { get; set; }
public string? Password { get; set; }
}Service Registration
服务注册
csharp
// 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);
});csharp
// 在Program.cs或扩展方法中
services.Configure<SmtpSettings>(configuration.GetSection("Smtp"));
services.AddSingleton<IEmailSender>(sp =>
{
var settings = sp.GetRequiredService<IOptions<SmtpSettings>>().Value;
return new SmtpEmailSender(settings);
});Email Sender Implementation
邮件发送器实现
csharp
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 package:
MailKitbash
dotnet add package MailKitcsharp
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);
}
}需要安装包:
MailKitbash
dotnet add package MailKitViewing Captured Emails
查看捕获的邮件
Web UI
Web UI
Navigate to to see:
http://localhost:8025- Inbox - All captured emails
- HTML view - Rendered email
- Source view - Raw HTML/MJML output
- Headers - Full email headers
- Attachments - Any attached files
访问即可查看:
http://localhost:8025- 收件箱 - 所有捕获的邮件
- HTML视图 - 渲染后的邮件内容
- 源码视图 - 原始HTML/MJML输出
- 邮件头 - 完整的邮件头信息
- 附件 - 所有附加文件
Aspire Dashboard
Aspire仪表板
The Mailpit UI endpoint appears in the Aspire dashboard under Resources.
Mailpit的UI端点会显示在Aspire仪表板的资源列表中。
Integration Testing
集成测试
Test Fixture with Aspire
基于Aspire的测试夹具
csharp
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");
}
}csharp
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()
{
// 准备
await _mailpit.ClearMessagesAsync();
// 执行 - 触发注册流程
var response = await _client.PostAsJsonAsync("/api/auth/signup", new
{
Email = "test@example.com",
Password = "SecurePassword123!"
});
response.EnsureSuccessStatusCode();
// 断言 - 验证邮件已发送
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");
}
}Mailpit API Client
Mailpit API客户端
csharp
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; } = "";
}csharp
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; } = "";
}Aspire Test Fixture
Aspire测试夹具
csharp
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();
}
}csharp
public class AspireFixture : IAsyncLifetime
{
private DistributedApplication? _app;
private string _mailpitUrl = "";
public async Task InitializeAsync()
{
var appHost = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.MyApp_AppHost>();
// 禁用持久化以保证测试环境干净
appHost.Configuration["MyApp:UseVolumes"] = "false";
_app = await appHost.BuildAsync();
await _app.StartAsync();
// 从Aspire获取Mailpit的URL
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();
}
}Common Test Patterns
常见测试模式
Wait for Async Email
等待异步邮件
Some emails are sent asynchronously. Wait for them:
csharp
[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();
}部分邮件是异步发送的,需等待其生成:
csharp
[Fact]
public async Task AsyncWorkflow_EventuallySendsEmail()
{
await _mailpit.ClearMessagesAsync();
// 触发异步工作流
await _client.PostAsync("/api/workflows/start", null);
// 等待邮件(带超时)
var email = await _mailpit.WaitForMessageAsync(
m => m.Subject.Contains("Workflow Complete"),
timeout: TimeSpan.FromSeconds(10));
email.Should().NotBeNull();
}Verify Multiple Emails
验证多封邮件
csharp
[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");
}csharp
[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");
}Verify Email Content
验证邮件内容
csharp
[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);
}csharp
[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));
// 从HTML中提取重置链接
var resetLink = Regex.Match(email!.HtmlBody, @"href=""([^""]+/reset/[^""]+)""")
.Groups[1].Value;
resetLink.Should().StartWith("https://myapp.com/reset/");
// 验证链接有效性
var resetResponse = await _client.GetAsync(resetLink);
resetResponse.StatusCode.Should().Be(HttpStatusCode.OK);
}Production vs Development
生产环境与开发环境对比
csharp
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
});
}
});csharp
services.AddSingleton<IEmailSender>(sp =>
{
var settings = sp.GetRequiredService<IOptions<SmtpSettings>>().Value;
var env = sp.GetRequiredService<IHostEnvironment>();
if (env.IsDevelopment())
{
// 开发环境使用Mailpit - 无需认证、无需SSL
return new SmtpEmailSender(settings);
}
else
{
// 生产环境SMTP(如SendGrid、Postmark等)
return new SmtpEmailSender(settings with
{
EnableSsl = true
});
}
});Troubleshooting
故障排查
Emails Not Appearing
邮件未显示
- Check Mailpit container is running in Aspire dashboard
- Verify SMTP host/port configuration
- Check for exceptions in application logs
- 检查Aspire仪表板中Mailpit容器是否正在运行
- 验证SMTP主机/端口配置是否正确
- 查看应用程序日志中的异常信息
Connection Refused
连接被拒绝
bash
undefinedbash
undefinedVerify Mailpit is listening
验证Mailpit是否在监听
undefinedundefinedAspire Endpoint Not Resolving
Aspire端点无法解析
csharp
// Ensure endpoint reference is correct
.WithEnvironment("Smtp__Host", mailpit.GetEndpoint("smtp").Property(EndpointProperty.Host))
.WithEnvironment("Smtp__Port", mailpit.GetEndpoint("smtp").Property(EndpointProperty.Port))csharp
// 确保端点引用正确
.WithEnvironment("Smtp__Host", mailpit.GetEndpoint("smtp").Property(EndpointProperty.Host))
.WithEnvironment("Smtp__Port", mailpit.GetEndpoint("smtp").Property(EndpointProperty.Port))Resources
资源
- Mailpit: https://github.com/axllent/mailpit
- Mailpit API: https://mailpit.axllent.org/docs/api-v1/
- MailKit: https://github.com/jstedfast/MailKit
- Aspire Containers: https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/container-resources