mcpField
M
mailpit-integration

mailpit-integration

github.com/wshaddixテスト・品質8667 回インストール35 週間前に更新
リポジトリ

mailpit-integration について

.NET Aspire プロジェクトで Mailpit を使ってメールをテストするスキルガイドです。SMTP の設定、捕捉したメールの確認、実メールを送信しない統合テストの方法を説明します。

主な機能

  1. 01Mailpit による SMTP 通信の捕捉
  2. 02Aspire コンテナとしての Mailpit 設定
  3. 03メール表示、ヘッダー、添付ファイルの確認
  4. 04API 経由での捕捉メールへのアクセス
  5. 05統合テストでのメール送信検証

活用シーン

  1. 01実メールを送らずに送信処理をテスト
  2. 02ローカルでメール表示やヘッダーを確認
  3. 03統合テストでメール内容を検証

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill mailpit-integration

インストール後、Claude Code で/mailpit-integrationを入力して読み込まれたか確認します。

詳細

Mailpit と .NET Aspire によるメールテスト

このスキルを使用する場面

次の場合にこのスキルを使用します。

  • 実際のメールを送信せずに、ローカルでメール配信をテストする場合
  • .NET Aspire でメールインフラストラクチャを設定する場合
  • メールが送信されることを検証する統合テストを作成する場合
  • メールのレンダリングやヘッダーをデバッグする場合

関連スキル:

  • aspnetcore/mjml-email-templates - MJML テンプレートの作成
  • testing/verify-email-snapshots - レンダリングされた HTML のスナップショットテスト
  • aspire/integration-testing - Aspire の一般的なテストパターン

Mailpit とは?

Mailpit は、軽量なメールテストツールです。次の機能があります。

  • メールを配信せずに、すべての SMTP トラフィックをキャプチャします
  • キャプチャしたメールを表示する Web UI を提供します
  • プログラムからアクセスするための API を公開します
  • HTML のレンダリング、ヘッダー、添付ファイルに対応しています

開発や統合テストに最適です。


Aspire AppHost の設定

AppHost に Mailpit をコンテナーとして追加します。

// 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 の設定

appsettings.json
{
  "Smtp": {
    "Host": "localhost",
    "Port": 1025,
    "EnableSsl": false,
    "FromAddress": "[email protected]",
    "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; } = "[email protected]";
    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);
    }
}

MailKit パッケージが必要です。

dotnet add package MailKit

キャプチャしたメールの表示

Web UI

http://localhost:8025 にアクセスすると、次の内容を確認できます。

  • 受信トレイ - キャプチャしたすべてのメール
  • HTML 表示 - レンダリングされたメール
  • ソース表示 - HTML/MJML の生データ
  • ヘッダー - メールの完全なヘッダー
  • 添付ファイル - 添付されたファイル
Aspire ダッシュボード

Mailpit の UI エンドポイントは、Aspire ダッシュボードの[リソース]に表示されます。


統合テスト

Aspire を使用したテストフィクスチャ
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 = "[email protected]",
            Password = "SecurePassword123!"
        });
        response.EnsureSuccessStatusCode();

        // Assert - Verify email was sent
        var messages = await _mailpit.GetMessagesAsync();

        var welcomeEmail = messages.Should().ContainSingle()
            .Which;

        welcomeEmail.To.Should().Contain("[email protected]");
        welcomeEmail.Subject.Should().Contain("Welcome");
        welcomeEmail.HtmlBody.Should().Contain("Thank you for signing up");
    }
}
Mailpit API クライアント
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 テストフィクスチャ

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();
    }
}

よくあるテストパターン

非同期メールを待機する

メールが非同期で送信される場合があります。次のようにしてメールを待機します。

[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[] { "[email protected]", "[email protected]", "[email protected]" }
    });

    var messages = await _mailpit.WaitForMessagesAsync(
        expectedCount: 3,
        timeout: TimeSpan.FromSeconds(10));

    messages.Should().HaveCount(3);
    messages.Select(m => m.To.First())
        .Should().BeEquivalentTo("[email protected]", "[email protected]", "[email protected]");
}
メールの内容を検証する
[Fact]
public async Task PasswordReset_ContainsValidResetLink()
{
    await _mailpit.ClearMessagesAsync();

    await _client.PostAsJsonAsync("/api/auth/forgot-password", new
    {
        Email = "[email protected]"
    });

    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
        });
    }
});

トラブルシューティング

メールが表示されない
  1. Aspire ダッシュボードで Mailpit コンテナーが実行中であることを確認します
  2. SMTP ホスト/ポートの設定を確認します
  3. アプリケーションログに例外がないか確認します
接続が拒否される
# Verify Mailpit is listening
curl http://localhost:8025/api/v1/messages
Aspire エンドポイントが解決されない
// Ensure endpoint reference is correct
.WithEnvironment("Smtp__Host", mailpit.GetEndpoint("smtp").Property(EndpointProperty.Host))
.WithEnvironment("Smtp__Port", mailpit.GetEndpoint("smtp").Property(EndpointProperty.Port))

リソース

コメント