Laravel Sail9から導入されたMailhogの後継Fake SMTP/mailpitを使ってみた
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.
簡単に要約すると、
- 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 を導入してプルリクを出してしまったんですよね…
再度修正することを検討しようかと思います。
メンバー募集中!
サーバーサイド Kotlin コミュニティを作りました!
Kotlin ユーザーはぜひご参加ください!!
また関西在住のソフトウェア開発者を中心に、関西エンジニアコミュニティを一緒に盛り上げてくださる方を募集しています。
よろしければ Conpass からメンバー登録よろしくお願いいたします。
Discussion