Communityコーディング&開発github.com

mailpit-integration

Mailpit と .NET Aspire を使用してローカルでメール送信をテストします。送信されるすべてのメールをキャプチャし、実際に送信することなく確認できます。HTML レンダリング、ヘッダーの検査、統合テストでの配信確認を行えます。

mailpit-integration とは?

このスキルは、.NET Aspire アプリケーションにおけるローカルメールテストのために Mailpit を統合する方法を説明します。Mailpit は軽量なメールテストツールで、SMTP トラフィックをすべてキャプチャし、Web UI と API を提供します。スキルには、Aspire AppHost での Mailpit コンテナ設定、SMTP 設定、メール送信の実装(MailKit 使用)、キャプチャしたメールの表示、統合テスト(MailpitClient の使用)、非同期メールの待機や複数メールの検証などのテストパターンが含まれます。また、開発環境と本番環境の設定切り替えやトラブルシューティングのヒントも提供します。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Aaronontheweb/dotnet-skills/tree/main/skills/aspire-mailpit-integration

Installed? Explore more コーディング&開発 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

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

// メールテスト用に 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 設定

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";

    // オプション: 本番環境 SMTP 用
    public string? Username { get; set; }
    public string? Password { get; set; }
}

サービスの登録

// Program.cs または拡張メソッド内
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 - サインアップフローをトリガー
        var response = await _client.PostAsJsonAsync("/api/auth/signup", new
        {
            Email = "[email protected]",
            Password = "SecurePassword123!"
        });
        response.EnsureSuccessStatusCode();

        // Assert - メールが送信されたことを確認
        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>();

        // クリーンなテストのために永続化を無効にする
        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();
    }
}

一般的なテストパターン

非同期メールの待機

一部のメールは非同期的に送信されます。それらを待機します:

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

複数メールの検証

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

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

本番環境と開発環境の切り替え

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

トラブルシューティング

メールが表示されない

  1. Aspire ダッシュボードで Mailpit コンテナが実行中であることを確認
  2. SMTP ホスト/ポート設定を確認
  3. アプリケーションログで例外を確認

接続が拒否される

# Mailpit がリッスンしていることを確認
curl http://localhost:8025/api/v1/messages

Aspire エンドポイントが解決しない

// エンドポイント参照が正しいことを確認
.WithEnvironment("Smtp__Host", mailpit.GetEndpoint("smtp").Property(EndpointProperty.Host))
.WithEnvironment("Smtp__Port", mailpit.GetEndpoint("smtp").Property(EndpointProperty.Port))

リソース

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

akka-hosting-actor-patterns

Patterns for building entity actors with Akka.Hosting - GenericChildPerEntityParent, message extractors, cluster sharding abstraction, akka-reminders, and ITimeProvider. Supports both local testing and clustered production modes.

akka-net-aspire-configuration

Configure Akka.NET with .NET Aspire for local development and production deployments. Covers actor system setup, clustering, persistence, Akka.Management integration, and Aspire orchestration patterns.

akka-net-best-practices

Critical Akka.NET best practices including EventStream vs DistributedPubSub, supervision strategies, error handling, Props vs DependencyResolver, work distribution patterns, and cluster/local mode abstractions for testability.

akka-net-management

Akka.Management for cluster bootstrapping, service discovery (Kubernetes, Azure, Config), health checks, and dynamic cluster formation without static seed nodes.

akka-net-testing-patterns

Write unit and integration tests for Akka.NET actors using modern Akka.Hosting.TestKit patterns. Covers dependency injection, TestProbes, persistence testing, and actor interaction verification. Includes guidance on when to use traditional TestKit.

api-design

Design stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, and versioning for NuGet packages and distributed systems.

aspire-configuration

Configure Aspire AppHost to emit explicit app config via environment variables; keep app code free of Aspire clients and service discovery.

aspire-integration-testing

Write integration tests using .NET Aspire

aspire-service-defaults

Create a shared ServiceDefaults project for Aspire applications. Centralizes OpenTelemetry, health checks, resilience, and service discovery configuration across all services.

crap-analysis

Analyze code coverage and CRAP (Change Risk Anti-Patterns) scores to identify high-risk code. Use OpenCover format with ReportGenerator for Risk Hotspots showing cyclomatic complexity and untested code paths.

csharp-concurrency-patterns

Choosing the right concurrency abstraction in .NET - from async/await for I/O to Channels for producer/consumer to Akka.NET for stateful entity management. Avoid locks and manual synchronization unless absolutely necessary.

database-performance

Database access patterns for performance. Separate read/write models, avoid N+1 queries, use AsNoTracking, apply row limits, and never do application-side joins. Works with EF Core and Dapper.

dependency-injection-patterns

Organize DI registrations using IServiceCollection extension methods. Group related services into composable Add* methods for clean Program.cs and reusable configuration in tests.

dotnet-devcert-trust

Diagnose and fix .NET HTTPS dev certificate trust issues on Linux. Covers the full certificate lifecycle from generation to system CA bundle inclusion, with distro-specific guidance for Ubuntu, Fedora, Arch, and WSL2.

dotnet-local-tools

Managing local .NET tools with dotnet-tools.json for consistent tooling across development environments and CI/CD pipelines.

dotnet-project-structure

Modern .NET project structure including .slnx solution format, Directory.Build.props, central package management, SourceLink, version management with RELEASE_NOTES.md, and SDK pinning with global.json.

dotnet-slopwatch

Use Slopwatch to detect LLM reward hacking in .NET code changes. Run after every code modification to catch disabled tests, suppressed warnings, empty catch blocks, and other shortcuts that mask real problems.

efcore-patterns

Entity Framework Core best practices including NoTracking by default, query splitting for navigation collections, migration management, dedicated migration services, and common pitfalls to avoid.

ilspy-decompile

Understand implementation details of .NET code by decompiling assemblies. Use when you want to see how a .NET API works internally, inspect NuGet package source, view framework implementation, or understand compiled .NET binaries.

marketplace-publishing

Workflow for publishing skills and agents to the dotnet-skills Claude Code marketplace. Covers adding new content, updating plugin.json, validation, and release tagging.

関連スキル