MailCatcher

メール回りのテストやデバッグには「MailCatcher」が便利ですぞ

  • このエントリーをはてなブックマークに追加
  • Evernoteに保存

こんにちは。リスペクトの木村です。
今日は、「MailCatcher」というRubyで使うGemライブラリの話をお送りします。

MailCatcher とは

mailcatcher_1

Samuel Cochran氏が開発した、シンプルなSMTPサーバーです。特に細かい設定は不要で、起動するだけでSMTPサーバーが起動します。(ポートは1025番)
これだけであればよくあるSMTPサーバーなのですが、MailCatcherの特徴は「SMTPサーバーを経由したメールをブラウザ上から確認できる」という所にあります。送信しようとしたメールはMailCatcherのSMTPサーバーから先には送信されません。

Webサーバーが同時に起動(ポートは1080番)するので、ブラウザからアクセスすると下記のような画面が表示されるので、そこから確認できます。
届いたメールはほぼリアルタイムで受信トレイに表示されるため、リロードの必要はありません。

mailcatcher_6

APIも搭載しているため、起動したWebサーバーにリクエストを送ると、受信していたメールの一覧やメールごとの詳細を確認する事ができます。

RubyGemではありますが、SMTPサーバーにアクセスできる環境であれば言語を問わず利用できます。
また、Sendmailダミーも搭載しているので、PHPの場合はphp.iniの設定を変更すればmail()やmb_send_mail()でも利用できます。

ちなみに、今回はPHPで利用します。

動作環境

MailCatcher自体はRuby1.8以上で動作するため、比較的多くの環境で動作します。

  • Linux
    • OS: CentOS 6.6
    • Ruby: 2.1.5p273 (yumだと1.8.7が入ってくるため、rvmを使用)
    • PHP: 5.5.23
  • Windows
    • OS: 7 Pro
    • Ruby: 2.0.0p643
    • PHP: 5.6.3
  • MailCatcher: 0.6.1

導入する

導入は簡単です。基本的にはサイトの「How」の欄に沿って進めます。

Rubyがまだ無い場合は、Linuxは各種パッケージマネージャーやRVM、rbenvでインストールします。
パッケージマネージャー経由の場合は、rubygemsも合わせてインストールする必要がありますのでお忘れ無く。

WindowsはRubyInstallerで導入しますが、加えてMailCatcherをGemでインストールする時にDevelopment Kitが必要になるため合わせてインストール/設定します。
(一連のインストールの流れはこちらに詳しく掲載されています。)

Rubyのインストール完了後、gem install mailcatcherで本体をインストールします。
依存するパッケージも一緒にインストールしてくれるので、これだけで大丈夫です。
(Linuxの場合はSQLiteのライブラリが必要かもしれません。)

無事にインストールできたら、mailcatcherを実行して、下記のようなログが出てくれば完了です。

C:\> mailcatcher
Starting MailCatcher
==> smtp://127.0.0.1:1025
==> http://127.0.0.1:1080

試しにhttp://127.0.0.1:1080にアクセスして、WebのUIが出てくるのも確認しておきます。
終了する時はCtrl+Cで抜けます。

Linuxの場合は、デフォルトでデーモンとして起動してくれるので、終了時はWebのUIにアクセスして「Quit」を選びます。

[vagrant@localhost ~]$ mailcatcher
Starting MailCatcher
==> smtp://127.0.0.1:1025
==> http://127.0.0.1:1080
*** MailCatcher runs as a daemon by default. Go to the web interface to quit.

起動オプション

各種サーバのIPやポート、フォアグラウンドで起動するかどうかはオプションで制御できます。

[vagrant@localhost ~]$ mailcatcher --help
Usage: mailcatcher [options]
        --ip IP                      Set the ip address of both servers
        --smtp-ip IP                 Set the ip address of the smtp server
        --smtp-port PORT             Set the port of the smtp server
        --http-ip IP                 Set the ip address of the http server
        --http-port PORT             Set the port address of the http server
        --no-quit                    Don't allow quitting the process
    -f, --foreground                 Run in the foreground
    -v, --verbose                    Be more verbose
    -h, --help                       Display this help information

Vagrant環境の場合、そのまま http://仮想環境のIP:1080 にアクセスしても確認できないため、–http-ipで仮想環境に割り当てているIPを指定する事でアクセスできるようになります。

[vagrant@localhost ~]$ mailcatcher --http-ip 仮想環境のIP
Starting MailCatcher
==> smtp://127.0.0.1:1025
==> http://仮想環境のIP:1080
*** MailCatcher runs as a daemon by default. Go to the web interface to quit.

WebUIの使い方

mailcatcher_4

UIはとてもシンプルで、メインメニューは検索ボックスと「Clear」「Quit」のみです。
「Clear」はメールを全削除、「Quit」はMailCatcher自体を終了します。

mailcatcher_5

メーラー部分もよくある2カラムのタイプです。
上部で確認したいメールを選択すると下部に表示されます。
本文は、「Plain Text」や「Source」、HTMLメールの場合は「HTML」のタブが表示されますので、切り替えて確認できます。

「Download」は選択しているメールをeml形式でダウンロードします。

MailCatcherを経由するよう設定する

PHPから使う場合、使い方としては2通りあります。

  • SMTPを利用して、1025番ポート経由でメールを送信する
  • Sendmailクローン(catchmail)を経由してメールを送信する

SMTPを利用して送る場合は、各種ライブラリや設定で127.0.0.1:1025に送るよう設定します。

CakePHP2のCakeEmail向け設定(Config/email.php)だとこんな感じです。

class EmailConfig {

	public $default = array(
		'transport' => 'Smtp',
		'from' => 'hoge@hugahuga.com',
		'host' => '127.0.0.1',
		'port' => 1025
	);

}

Sendmailクローンを経由する場合は、php.iniの設定を変更する必要があります。
具体的には、sendmail_pathの設定に、catchmailコマンドへのパスを渡します。

  • Linux … sendmail_path = “/usr/bin/env /home/vagrant/.rvm/gems/ruby-2.1.5/bin/catchmail”
  • Windows … sendmail_path = “C:\Ruby200-x64\bin\catchmail”

フルパスで無くても良いかもしれませんが、念のためフルパスで記述します。
Linuxの場合はwhich catchmailで検索するなどしてパスを特定できますし、WindowsでRubyInstallerを使っている場合は「C:\Ruby-(バージョン)\bin\」下にcatchmailが入っています。

設定が完了したらにテストし、WebのUIにメールが届いているのを確認します。
SMTPで設定した場合は各アプリから、Sendmailで設定した場合はmail()/mb_send_mail()を利用して送信します。

試してみる

実際にメールが送れるか確認してみます。

まずは、mail()で確認します。
こんな感じのスクリプトを作成して、最低限の設定だけで送ってみます。

<?php
$to = "taro@example.com";
$from = "jiro@example.com";
$subject = "TEST SUBJECT";
$body = "TEST BODY";

mail($to,$subject,$body,"From:".$from);

mailcatcher_2

無事に届きました。

では、mb_send_mail()を利用して日本語のメールを送ってみます。
先ほどとあまり変わっていませんが、件名と本文が日本語になり、本文には更に改行も加わっています。
果たして無事に届くでしょうか・・・。

<?php
mb_language("ja");
mb_internal_encoding("UTF-8");

$to = "taro@example.com";
$from = "jiro@example.com";
$subject = "テストのサブジェクト";
$body = "テスト\nテストテスト\nテストテストテスト\nテストテスト\nテスト";

mb_send_mail($to,$subject,$body,"From:".$from);

mailcatcher_3

日本語のメールも無事届きました。文字化けせず、改行もそのままで表示できています。

APIを使ってみる

お次は、APIでどんな事ができるか試してみます。
MailCatcherは次のAPIを搭載しています。

  • /messages … メッセージ一覧を取得
  • /messages/:id.json … 指定したIDのメッセージのメタデータを取得
  • /messages/:id.html … 指定したIDのメッセージのテキスト(HTML)を取得
  • /messages/:id.plain … 指定したIDのメッセージのテキスト(プレーン)を取得
  • /messages/:id/:cid … IDに紐付くCIDの添付ファイルを取得
  • /messages/:id.source メッセージ一覧で取れるデータの中から、指定したIDのものを取得

本文や添付ファイルの取得以外は、全てJSONで結果が返ってきます。

まずは、/messagesにリクエストを投げてみます。

<?php
var_dump(json_decode(file_get_contents('http://127.0.0.1:1080/messages')));
array(2) {
  [0]=>
  object(stdClass)#2 (6) {
    ["id"]=>
    int(1)
    ["sender"]=>
    string(18) "<jiro@example.com>"
    ["recipients"]=>
    array(1) {
      [0]=>
      string(18) "<taro@example.com>"
    }
    ["subject"]=>
    string(12) "TEST SUBJECT"
    ["size"]=>
    string(3) "333"
    ["created_at"]=>
    string(29) "2015-05-14T07:29:55.000+00:00"
  }
  [1]=>
  object(stdClass)#3 (6) {
    ["id"]=>
    int(2)
    ["sender"]=>
    string(18) "<jiro@example.com>"
    ["recipients"]=>
    array(1) {
      [0]=>
      string(18) "<taro@example.com>"
    }
    ["subject"]=>
    string(30) "テストのサブジェクト"
    ["size"]=>
    string(3) "514"
    ["created_at"]=>
    string(29) "2015-05-14T07:29:58.000+00:00"
  }

先ほど送った2通のメッセージの概要が返ってきました。
取得するメールは、古い順で取得しているようです。

次に、/messages/1.jsonにリクエストを投げてみます。

<?php
var_dump(json_decode(file_get_contents('http://127.0.0.1:1080/messages/1.json')));
object(stdClass)#2 (10) {
  ["id"]=>
  int(1)
  ["sender"]=>
  string(18) "<jiro@example.com>"
  ["recipients"]=>
  array(1) {
    [0]=>
    string(18) "<taro@example.com>"
  }
  ["subject"]=>
  string(12) "TEST SUBJECT"
  ["source"]=>
  string(333) "Date: Thu, 14 May 2015 07:29:55 +0000
From: jiro@example.com
To: taro@example.com
Message-ID: <55544ef38464_13d3f333c42664@localhost.localdomain.mail>
Subject: TEST SUBJECT
Mime-Version: 1.0
Content-Type: text/plain;
 charset=UTF-8
Content-Transfer-Encoding: 7bit
X-PHP-Originating-Script: 501:mailtest_en.php

TEST BODY
"
  ["size"]=>
  string(3) "333"
  ["type"]=>
  string(10) "text/plain"
  ["created_at"]=>
  string(29) "2015-05-14T07:29:55.000+00:00"
  ["formats"]=>
  array(2) {
    [0]=>
    string(6) "source"
    [1]=>
    string(5) "plain"
  }
  ["attachments"]=>
  array(0) {
  }
}

メタデータや本文を含む全データが取得できました。

本文のみ取得するには、/messages/1.plainにアクセスします。

<?php
var_dump(file_get_contents('http://127.0.0.1:1080/messages/1.plain'));
string(10) "TEST BODY
"

HTMLメールではないと判断されているため、/messages/1.htmlにアクセスすると404が返ってきてしまいます。

<?php
var_dump(file_get_contents('http://127.0.0.1:1080/messages/1.html'));
PHP Warning:  file_get_contents(http://192.168.33.33:1080/messages/1.html): failed to open stream: HTTP request failed! HTTP/1.1 404 Not Found

どのフォーマットで取得できるかは、/messages/1.jsonの返り値に「formats」が含まれているので、ここで判断できます。

object(stdClass)#2 (10) {
 (略)
  ["formats"]=>
  array(2) {
    [0]=>
    string(6) "source"
    [1]=>
    string(5) "plain"
  }

一歩踏み込んでみる

このAPI、取得はできるのですが削除についてはエンドポイントがありません。
「削除も出来ればテストとかで使えるんだけどな・・・」と思い色々調べてみると、UIで使用しているJavascriptにヒントがありました。
メニューの「Clear」「Quit」ボタンの動きを追ってみると・・・

  • 「Clear」は「/messages」に
  • 「Quit」は「/」に

それぞれDELETEメソッドでリクエストを送信していました。

1件ごとの削除はできないようですが、まとめて削除する事は可能なようです。

<?php
$context = stream_context_create(array(
  'http' => array(
    'method' => 'DELETE'
  )
));

file_get_contents('http://127.0.0.1:1080/messages', false, $context);

var_dump(json_decode(file_get_contents('http://127.0.0.1:1080/messages')));
array(0) {
}

file_get_contents()で無理矢理DELETEメソッド扱いとしてリクエストを送り、再度/messagesを取得すると、0件になっていました。

スクリプトはほぼそのまま、リクエストの送信先を「/」に変更して再度試してみると接続できなくなりました。
プロセスを確認するとデーモンも消えていましたので、プログラム自体が終了しているようです。

<?php
$context = stream_context_create(array(
  'http' => array(
    'method' => 'DELETE'
  )
));

file_get_contents('http://127.0.0.1:1080/', false, $context);

var_dump(json_decode(file_get_contents('http://127.0.0.1:1080/messages')));
PHP Warning:  file_get_contents(http://127.0.0.1:1080/messages): failed to open stream: Connection refused

一通ごとの削除はできませんが、削除自体は可能なのでシンプルなテストであれば組み込めそうです。
「/messages」ではなく「/」にリクエストを送ってしまうとMailCatcher自体が終了してしまうので、そこに注意する必要があります。

おわりに

MailCatcherの導入~簡単な使い方や、API回りについてのご紹介でした。

メールのテストというと、自分のアドレスやデバッグ用のアドレスに送って・・・という方法を取っている方が多いのではないでしょうか。
その場合は送信先を間違っていたり、間違って本番環境に送ってしまった・・・なんて事が発生する場合もありますが、あらかじめMailCatcherを経由して送信するように変更するだけで、プログラムに大きく変更を加える事無く安全で確実にデバッグができるようになります。
さらに、MailCatcher経由の場合、Toで指定されたメールアドレスにはメールは送信されませんので、上に書いたような送り先を間違えた、という事故も防ぐことができるので安心です。

導入も使い方も簡単なので、メール回りのデバッグやテストを効率よく安全に進めてみたいという方は試してみてはいかがでしょうか。

現場からは以上です。

  • このエントリーをはてなブックマークに追加
  • Evernoteに保存