TL;DR
- Fake SMTPについて
- MailHogとmailpitの機能差について
- maiplitの機能について(メール送信,API,SMTPS,SMTP_AUTHの検証)
今回の検証用ソースはこちらです。
Fake SMTPについて
Fake SMTPはメール送信テストの際に有用です。
わざわざテスト用のSMTPサーバーをレンタルしたりする必要がなくなります。
また実際にメール送信をするわけではないので、テスト時に誤って本番メールアドレスへ送信するという事態を防ぐことができます。
Fake SMTPの仕組みとしてはざっくり以下のような感じです。
- メール送信
- 実際にメール送信が行われるのではなく、Fake SMTPで設定したストレージにメールが保存される
- そのストレージに保存したメールをブラウザもしくはAPI等から確認できる
製品としては以下のようなものが有名どころのようです。
- mailtrap(SaaS)
- MailCatcher(Local, Ruby製)
- MailDev(Local, Node.js製)
- MailHog(Local, Go製)
MailHog
先に紹介したFake SMTPのうち近年最も使われているのはMailHogの印象を受けます。
自分なりに考察した結果、MailHogがここまで流行った要因というのはLaravel Sail(8系まで)に採用されていたことだと考えます。

mailpit
しかしLaravel 9系からはMailHogではなくmailpitが採用されています。

なぜmailpitに変えたのだろう?と思うのですが、おそらくこれが原因ではないだろうかというものを見つけました。
それは、mailpitのREADMEに記載されています。
I had been using MailHog for a few years to intercept and test emails generated from several projects. MailHog has a number of severe performance issues, many of the modules are horribly out of date, and other than a few accepted MRs, it is not actively developed.
Initially I started trying to upgrade a fork of MailHog (both the UI as well as the HTTP server & API), but soon discovered that it is (with all due respect) very poorly designed. It is over-engineered (split over 9 separate projects) and has too many unnecessary features for my purpose. It performs exceptionally poorly when dealing with large amounts of emails or processing any email with an attachment (a single email with a 3MB attachment can take over a minute to ingest). The API also transmits a lot of duplicate and unnecessary data on every message request for all web calls, and there is no HTTP compression.
In order to improve it I felt it needed to be completely rewritten, and so Mailpit was born.
https://github.com/axllent/mailpit#why-rewrite-mailhog
簡単に要約すると、
- MailHogは性能面で深刻な問題を抱えている
- モジュールが恐ろしく古く、最近は活発に開発されていない
- 大量メールや添付ファイルがあるメールを処理するとパフォーマンスが悪い
- APIは余計なデータを送信している
- 以上を解消するにはソースを一から書き直さないと無理なほど設計がお粗末
ということを言っています。(ボロカスやないか…)
MailHogは本当に性能が悪いのか?
ということで検証してみました。
手順としては、
- メール送信の直前直後にログを仕込む
- MailHog,mailpitそれぞれに対して1MBの添付ファイルを付けてメール送信を行う
- ログの時間から処理時間を計測
という単純なものです。
2に関して、大量件数で送信しないの?と思われた方がいらっしゃると思います。
当初そのつもりで準備していたのですが、テストで1件送信(1MB画像添付あり)した時点でその性能差が浮き彫りになってしまいました。
結論として、性能差は圧倒的。
2023-02-11 09:08:42,348 - INFO:root - mailhog begin
2023-02-11 09:09:06,724 - INFO:root - mailhog end
2023-02-11 09:13:36,661 - INFO:root - mailpit begin
2023-02-11 09:13:37,104 - INFO:root - mailpit end
これは1MBの画像を1件添付し、1通のメールをMailHogとmailpitで送った結果です。
MailHog -> 約24秒
mailpit -> 約500ミリ秒
ということで圧倒的にmailpitの勝利でした。
Laravel Sailがmailpitに乗り換えるのも納得。
メール送信のプログラムの全体は最初に記載したgithubに載せています。
一応ここにも全体を載せておきます。
以降の検証でも同じソースを使いまわします。
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s:%(name)s - %(message)s",filename="logs/test.log")
def sendmail(host, port):
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
smtp_obj = smtplib.SMTP(host, port)
# mailpitのみSMTPS, SMTP_AUTHのどちらも設定している
# smtp_obj.starttls()
# smtp_obj.login('user', 'pass')
body = "メールの本文"
msg = MIMEMultipart()
msg['Subject'] = "メールの件名"
msg['To'] = 'to@office54.net'
msg['From'] = 'from@office54.net '
msg.attach(MIMEText(body))
with open(r"/src/img/1MB.jpeg", "rb") as f:
attachment = MIMEApplication(f.read())
attachment.add_header("Content-Disposition", "attachment", filename="1MB.jpeg")
msg.attach(attachment)
smtp_obj.send_message(msg)
smtp_obj.quit()
# logging.info("mailhog begin")
# sendmail("mailhog", 1026)
# logging.info("mailhog end")
logging.info("mailpit begin")
sendmail("mailpit", 1025)
logging.info("mailpit end")
mailpit でいろいろ試してみる
性能面でMailHogを圧倒したmailpitですが、そのほかの機能についても使い勝手を確認していこうと思います。
今回試したのは以下の通りです。
- API
- SMTPSでのメール送信
- SMTP_AUTHの利用
以降で詳しく書きます。
API
APIを利用してメールの一覧取得、内容取得、削除が可能です。
今回は試さなかったのですが、Basic認証・HTTPSの利用もできそうです。
(ドキュメントへのリンクを貼っておきます)
今回はVSCodeの拡張機能の「REST Client」から試しています。
メールの一覧取得
### GET MESSAGE LIST
GET http://127.0.0.1:8025/api/v1/messages

メールの内容取得
### GET MESSAGE
GET http://127.0.0.1:8025/api/v1/message/4067ddc0-a6e9-4905-bec6-52ffc8aa8307

メールの削除
### DELETE MESSAGE
DELETE http://127.0.0.1:8025/api/v1/messages
{
"ids": ["4067ddc0-a6e9-4905-bec6-52ffc8aa8307"]
}

SMTPSの有効化
テストなので自己署名の証明書を使います。
まずaxllent/mailpitのコンテナに入ってopensslをインストールします。
鍵と証明書の置き場所をつ口、鍵と証明書を生成。
apk add openssl
mkdir keys
openssl req -x509 -newkey rsa:4096 -nodes -keyout /keys/privkey.pem -out /keys/cert.pem -sha256 -days 3650
生成したらDockerから抜けて、鍵の置き場所をdocker-compose.ymlから指定します。
(ここまでの手順はDockerfileベースで作るか、あるいはあらかじめ別途用意しておいた鍵と証明書を使うほうがいいかもです)
mailpit:
image: axllent/mailpit
tty: true
ports:
- "8025:8025"
- "1025:1025"
environment:
MP_DATA_FILE: /home/mailpit/mails
MP_SMTP_SSL_CERT: /keys/cert.pem #これを追加
MP_SMTP_SSL_KEY: /keys/privkey.pem #これを追加
volumes:
- ./mails/mailpit:/home/mailpit/mails
docker-compose.ymlを修正したらコンテナを起動しなおして、Pythonのコードを修正します。
def sendmail(host, port):
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
smtp_obj = smtplib.SMTP(host, port)
# mailpitであればSMTPS, SMTP_AUTHのどちらも対応できる
smtp_obj.starttls() #このコメントを外す
# smtp_obj.login('from@office54.net', 'Password')
これでSMTPSを使ったメール送信ができます。
プログラムの実行は、appコンテナに入ってpythonを実行します。
docker-compose exec app bash
python sendmail.py
SMTP_AUTHの有効化
ドキュメントによると、SMTPSを有効にしないとSMTP_AUTHは利用できないようです。
なので、先述のSMTPSの手順を行った上で以降を実施する必要があります。
そこでまず、SMTP_AUTHで利用するユーザー認証情報を用意します。
認証の形式はbasic認証と同じであることがドキュメントに記載されているので、今回はhtpasswdを利用して認証ファイルを作成します。
あらかじめファイルを用意する形でも問題ないと思います。
ではhtpasswdのインストール
# htpasswdが入ってる
apk add apache2-utils
# ヘルプが見れればインストールできている
htpasswd --help
インストールしたら認証ファイルを作成します。
htpasswd -c ./keys/.htpasswd mailpit
パスワードを聞かれるので、今回はmailpitとします。
ここまでできたら先ほどと同じくdocker-compose.ymlにユーザー認証ファイルの場所を設定します。
mailpit:
image: axllent/mailpit
tty: true
ports:
- "8025:8025"
- "1025:1025"
environment:
MP_DATA_FILE: /home/mailpit/mails
MP_SMTP_SSL_CERT: /keys/cert.pem
MP_SMTP_SSL_KEY: /keys/privkey.pem
MP_SMTP_AUTH_FILE: /keys/.htpasswd #これを追加
volumes:
- ./mails/mailpit:/home/mailpit/mails
- ./keys:/keys
ではPythonのコードを修正します。
今回はまず出鱈目なユーザーを指定してみます。
# mailpitであればSMTPS, SMTP_AUTHのどちらも対応できる
smtp_obj.starttls()
smtp_obj.login('from@office54.net', 'Password') #ここを修正
この状態で実行すると認証に失敗して怒られることが確認できます。
root@4e12d0b511af:/src# python sendmail.py
Traceback (most recent call last):
File "/src/sendmail.py", line 33, in <module>
sendmail("mailpit", 1025)
File "/src/sendmail.py", line 12, in sendmail
smtp_obj.login('from@office54.net', 'Password')
File "/usr/local/lib/python3.11/smtplib.py", line 750, in login
raise last_exception
File "/usr/local/lib/python3.11/smtplib.py", line 739, in login
(code, resp) = self.auth(
^^^^^^^^^^
File "/usr/local/lib/python3.11/smtplib.py", line 662, in auth
raise SMTPAuthenticationError(code, resp)
smtplib.SMTPAuthenticationError: (535, b'5.7.8 Authentication credentials invalid')
次は正しいユーザーを指定してみます。
smtp_obj.starttls()
smtp_obj.login('mailpit', 'mailpit') #ここを修正
今度は正常にメール送信することができました。
MailHogの優位性は失われたのか?
ここまでみてきた感じだと性能面、機能面ともにmailpitの方が上位互換な感じがします。
MailHogは完全に過去の遺物になってしまったのだろうか…ということを考えてみたのですが、MailHogの優位性としてカオスモンキー=Jimがあるのではないかと思います。
この記事を書いた2023/02/11時点ではmailpitのwikiの中にはカオスモンキーの記載を見つけることはできませんでした。
なので、障害テストを一つのソフトの中で完結できるという点がMailHogの優位性であると考えます。
まとめ
今回いろいろと検証してみた結果、私個人としては今後Fake SMTPの導入する際にはmailpitを使おうと思いました。
MailHogに若干の優位性は残るものの、基本的にはmailpitがMailHogの上位互換として君臨することになりそうです。
ただ先日OSSにMailHogを導入してプルリクを出してしまったんですよね…
再度修正することを検討しようかと思います。