Community코딩 & 개발github.com

mailpit-integration

Mailpit을 사용하여 로컬에서 이메일 전송을 테스트하고, 실제 전송 없이 모든 발신 이메일을 캡처합니다. 렌더링된 HTML을 확인하고, 헤더를 검사하며, 통합 테스트에서 전달을 검증할 수 있습니다.

mailpit-integration란 무엇인가요?

이 스킬은 .NET Aspire 환경에서 Mailpit을 활용한 이메일 테스트 방법을 설명합니다. Mailpit은 가상 SMTP 서버로, 실제 이메일을 전송하지 않고 애플리케이션에서 보낸 모든 이메일을 캡처하여 웹 UI와 API를 통해 확인할 수 있게 해줍니다. AppHost에 Mailpit 컨테이너를 추가하고, SMTP 설정을 구성하며, `MailKit`을 사용한 이메일 발송 구현과 통합 테스트 코드 작성을 다룹니다. 또한 비동기 이메일 대기, 다중 이메일 확인, 이메일 콘텐츠 검증 등 다양한 테스트 패턴을 지원하며, 개발/운영 환경별 SMTP 설정 분기, 문제 해결 방법, Aspire 테스트 픽스처 구성도 포함되어 있습니다. 주요 플랫폼은 .NET Aspire와 C#이며, `aspnetcore/mjml-email-templates`, `testing/verify-email-snapshots`, `aspire/integration-testing` 스킬과 관련됩니다.

지원 대상~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 트래픽을 캡처하는 경량 이메일 테스트 도구입니다:

  • 캡처된 이메일을 볼 수 있는 웹 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

캡처된 이메일 보기

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

프로덕션 vs 개발

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.

관련 스킬