ローカル開発のメール送信、結局 MailHog も Mailpit も使わなかった話

tkmi
2026-09-15
2026-09-15

お疲れ様です。tkmiです。

案件で久しぶりにメールの動作確認をすることになりました。
実装そのものは別のメンバーがすでに終わらせてくれていて、私がやりたかったのは動かして確かめることだけ。
なのに、Docker に何も用意していないことに気づいて手が止まりました。

「これ、どうやって送るんだっけ」

メールサーバーを立てる? いや、めんどくさい。そんな大がかりなもの必要? そういえばメールキャッチャーってあったよな。でも docker-compose を直すことになるし、これもめんどい。

そこでソースを見に行ったら、送信用の関数がすでに用意されていて、.env にも設定項目がありました。じゃあキャッチャーを立てて .env をそっちに向けるか……いや、それもめんどい。

結局、手元にあったメール設定をそのまま .env に書いて終わりました。MailHog も Mailpit も使っていません。

メールが主役だった時代は終わって、いまは認証フローと業務通知あたりに残っている。だから実装頻度が低いのに、忘れた頃にやってくる。しかも調べると出てくるのは数年前の記事ばかり。というわけで以下、選択肢の整理と、なぜ入れなかったのかの記録です。

そもそもメールサーバーは必要か

不要です。

アプリケーションは SMTP クライアントでしかありません。どこかの SMTP サーバーに接続して「これを送ってください」と渡すだけ。実際の配送はサーバー側の仕事なので、自分で Postfix を立てて設定ファイルと格闘するような話にはなりません。

ただし、繋ぐ先は必要です。そしてこの「繋ぐ先をどうするか」に選択肢があって、久しぶりに実装する人(私です)が最初に迷うのはだいたいここでした。

繋ぐ先の選択肢

性質で分けると3つに整理できます。ローカルで止めるか、本当に送るか、外部だけど実宛先には届かないか。

① Mailpit を立てる

いわゆるメールキャッチャーです。受け取ったメールを外に配送せず、自分のストレージに溜めて Web UI で見せてくれます。リレー(転送)機能そのものは備えていますが、設定ファイルを明示的に渡さないと有効になりません。つまりデフォルトのままなら、宛先が実在のアドレスでも外には出ません。うっかり送ってしまう経路がない、というのが安心できるところです(詳しくは末尾の補足に書きました)。

services:
  mailpit:
   image: axllent/mailpit:latest
   ports:
    - "1025:1025" # SMTP
    - "8025:8025" # Web UI

.env の送信先を mailpit:1025 に向ければ、あとは localhost:8025 をブラウザで開くだけ。送った瞬間に一覧に出ます。

ひとつ注意点として、ホスト名がアプリ側とブラウザ側で違います。同じ docker-compose.yml で起動しているアプリから見ると mailpit(サービス名)ですが、ホストのブラウザからは localhost:8025
ここは最初に必ず引っかかるところだと思います。

HTML の整形表示もソースも生ヘッダーも見られるので、確認作業としてはむしろ受信箱より快適です。API も生えているので、CI で「パスワードリセットメールが送られたか」を検証することもできます。

コストは docker-compose の変更ひとつ。

② 手元にある SMTP サーバーの設定を使う

すでに使える SMTP サーバーがあるなら、その設定を .env に書くだけで終わります。

SMTP_HOST=smtp.example.co.jp
SMTP_PORT=587
SMTP_USER=xxxx
SMTP_PASSWORD=xxxx

コンテナも増えないし、docker-compose も触りません。導入コストは実質ゼロです。
今回私が選んだのはこれでした。

ただし本当に送られます
宛先を間違えれば本物のメールが届きます。
テストデータに実在のアドレスが混ざっていないか、ループで大量に送っていないか。安全性は仕組みではなく「宛先が正しいこと」に依存する構成なので、そこは自分で担保する必要があります。あとは .env .gitignore に入っていることの確認も。

宛先が全部自分の管理下にあって、送る通数も読める。その条件が揃っているなら、これで十分です。

③ クラウドのテスト用サービスを使う

Mailtrap のような、テスト用の受信箱を提供してくれるサービスです。
外部に送りますが、実際の宛先には届きません。全部サービス側の受信箱に集まります。
②の「本当に届いてしまう」リスクがなく、①と違ってチーム全員が同じ受信箱を見られるのが強みです。

Nodemailer チームの Ethereal のように、使い捨てアカウントを都度発行して使うタイプもあります。

向くのはステージング環境や、顧客向けの HTML メールを複数のメールクライアントで確認したい場面。逆に、オフラインでは使えません。無料枠のレート制限もあります。それと、メール本文が外部サービスを経由します。本番データを使ったテストでは、そこを確認してからにした方がいいと思います。

コストはアカウントを1つ作ること。それ以外は②と同じで、.env の送信先を差し替えるだけです。

まとめると

  導入コスト これが要るなら選ぶ
① Mailpit docker-compose の変更ひとつ 誤送信を仕組みで防ぎたい / CI で自動検証したい / オフラインで作業する
② 手元の SMTP 設定があれば実質ゼロ。なければ最も高い とりあえず1回動かしたい / 実際のメーラーで受信して見たい
③ クラウド アカウント作成 チームで同じ受信箱を共有したい / メーラーごとの表示差を確認したい

②のコストだけ状況で大きく変わります。ここが後述の分かれ目になります。

なぜ今回は入れなかったのか

「めんどい」と感じていたものを分解すると、docker-compose の変更、コンテナ1つ、ポート2つ、それとチームの他のメンバーへの説明でした。大した量ではありませんが、ゼロではありません。

一方でメールキャッチャーを入れて得られるものは、大きく3つあります。開発ループが速くなること。API でメール内容を自動検証できること。そして宛先を間違えても外に出ないという、仕組みによる保証。

今回確認したかったのは、フォームから操作すると、DB に登録されたアドレス宛てに完了通知が届くかどうかでした。

この用途だと、3つ目は必要ありません。宛先は DB の値なので、自分のアドレスに書き換えれば済みます。事故る余地がない。

1つ目も効きません。確認1回にかかる時間はフォームを操作している部分が大半で、メールを見るのは最後の1ステップです。そこが速くなっても全体は縮まらないし、そもそも何度も往復する作業ではありませんでした。

残るのは2つ目です。これは正直、今回の用途に合っています。 フォーム操作から通知メールの内容まで通して検証するのは、E2E テストとしてまさに書きたい形です。

ただ、これは「今日の動作確認をどうするか」とは別の意思決定でした。テストの仕組みを入れるかどうかはプロジェクトとして決める話で、時間の単位が違う。手元の確認作業に付随して片手間に決めるものではありません。メールの E2E を書くと決めたときが、Mailpit を入れるタイミングです。 そのときは迷わず入れます。

判断フロー

ここまで書いてきて気づいたんですが、条件を並べると偏りがはっきりします。

②が成立するには「設定が手元にある」「宛先が管理下」「通数が読める」「CI で検証しない」「オフラインじゃない」を全部満たす必要があります。一方①は、どれか1つ崩れれば選ばれます。

つまり既定解は① Mailpit です。迷ったら入れた方がいい。②は条件が全部揃ったときにだけ成立する例外で、今回はたまたま揃っていた、というのが正確なところです。

まとめ

結論としては、普通は Mailpit を入れた方がいいと思います。手元に使える SMTP サーバーがない人にとっては②が最も高くつきますし、宛先や通数を人間の注意力で担保する構成は、条件がひとつ崩れた瞬間に事故ります。既定解は①です。

その上で、条件が全部揃っているなら②で終わらせていい。今回はそうでした。「使わなくてもいい場合がある」のではなく、「普通は使うべきだが、これだけの条件が揃うなら要らない」という順序で書いておきたいところです。

それと、今回いちばん反省したのはここです。外部にツールを3回探しに行った後で、手元のコードに答えがあったことに気づきました。 送信関数も .env の設定項目も、すでに用意されていた。自分が書いたコードではなかったとはいえ、順番が逆でした。既存プロジェクトに合流したときや、久しぶりに触るときは、まず何が用意されているかを見た方が早い。

なお、到達性(SPF / DKIM / DMARC)やバウンス処理の確認は、ここまでの話とは別のレイヤーです。そちらは SendGrid の Sandbox Mode や Amazon SES の Mailbox Simulator といった、本番サービス側のテスト機能の出番になります。

補足(読み飛ばし可)

送信処理が呼ばれたかだけ見たい場合
SMTP を使わず、送信をログ出力に差し替える手もあります。Django なら console backend、FastAPI-Mail なら SUPPRESS_SEND。実装の最序盤ならこれで足ります。ただし認証・TLS・エンコードまわりのバグは一切検出できません。

Python で軽く SMTP サーバーを立てたい場合
aiosmtpd が使えます。python -m aiosmtpd -n -l localhost:1025 で立ちます。古い記事にある標準ライブラリの smtpd は PEP 594 で 3.12 に削除済みなので、コピペしても動きません。受信内容は標準出力に出るだけで UI はないため、Mailpit が使えるならそちらが上位互換です。

既存コードが SMTP のユーザー・パスワードを要求してくる場合
Mailpit ならダミー値で通せます。MP_SMTP_AUTH_ACCEPT_ANY=true で任意のユーザー名・パスワード(あるいは無し)を全部受け入れます。ただしデフォルトでは認証に TLS が要求されるので、平文接続で認証を通すには MP_SMTP_AUTH_ALLOW_INSECURE=true も併用します。「認証が必要だからキャッチャーは組み込めない」は成立しません。

MailHog ではなく Mailpit を挙げた理由
Mailpit は MailHog に着想を得たツールですが、MailHog 側は数年にわたって開発もセキュリティ更新も止まっています。ポート(1025 / 8025)も API も互換で、Laravel Sail や DDEV も Mailpit に切り替え済み。古い記事で MailHog を見かけたら、Mailpit に読み替えて構いません。

Mailpit は本当に送らないのか
本文で触れたとおり、リレー(メッセージのリリース)と転送の機能自体は持っています。ただし --smtp-relay-config で設定ファイルを明示的に渡さないと有効になりません。デフォルトでは無効なので、意図せず外に出ることはありません。有効にする場合も、許可する宛先を正規表現で絞り込めます。

参考