2013/08/05

JSON+HTTPでメールが送信出来るAPIサーバ``Haineko''

夏に``Haineko''というHTTPでメール送信が出来るWebアプリケーションといいますか、HTTPでアクセス可能なAPIを作りました。

ざっくりした話は七月にはてなさんで開催されたKyoto.pm発表したのですが、Hainekoの全体を見渡した話はまだ書いていなかったので、暦の上で秋になる前に書いておきます。

2014/01/06追記:

最新版のversion 0.2.13を基準として新たに概要を「HainekoがCPANモジュールになりました」として書きました。

Hainekoとは何か

HainekoはHTTPでメールを送信する為のAPIを提供するサーバとして動作します。別の表現をすると、Javascriptやcurl, PerlのモジュールならFurl等HTTPクライアントから、メールの宛先や内容等をJSONでHainekoに渡す事によってメールの送信ができるというものです。

こんな感じでメールが送信出来る

コマンドラインからcurlコマンドを使ってHainekoにメールをJSONで渡すのが、最も手軽にHaineko経由でメールを送る方法でしょう。試運転には丁度良いです、curlは。

ソースのeg/ディレクトリにサンプルとしてメールデータをJSONで表現したファイルを置いています。そのファイルを編集して、Hainekoを起動してから、次のようなコマンドを実行すればHaineko経由でメールが送れます。
接続先のURLは、HainekoがLISTENしているアドレスの/submitです。
$ cp eg/email-01.json /tmp/1.eml ⏎
$ vi /tmp/1.eml ⏎
$ curl -X POST -H 'Content-Type: application/json' -d '@/tmp/1.eml' 'http://127.0.0.1:2794/submit' | jq -M . ⏎
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   892  100   508  100   384    253    191  0:00:02  0:00:02 --:--:--   253
{
  "smtp.remoteport": 51762,
  "smtp.addresser": "kijitora@example.org",
  "smtp.remoteaddr": "127.0.0.1",
  "smtp.queueid": "r75FLUu048375jJI",
  "smtp.response": {
    "code": "250",
    "command": "QUIT",
    "message": [
      "2.0.0 OK Authenticated\n",
      "2.1.0 <kijitora@example.org>... Sender ok\n"
    ],
    "error": 0,
    "dsn": "2.1.0"
  },
  "smtp.useragent": "curl/7.21.4 (universal-apple-darwin11.0) libcurl/7.21.4 OpenSSL/0.9.8x zlib/1.2.5",
  "smtp.started": "Sun Aug  4 22:22:22 2013",
  "smtp.stage": 0,
  "smtp.referer": null,
  "smtp.recipient": [
    "mikeneko@example.jp"
  ]
}

curlでメールデータを記述したJSONを渡して、Hainekoが返す応答をjqコマンドで見やすくしています。jqコマンド、便利です。メール送信はJSONで渡して応答もJSONで返ってきますので、JSONが扱える言語であればPerlでなくてもPythonでもRubyでもPHPでもなんでもOKです。

作った理由

きっかけ: Javascriptからメール送信

そろそろ夏になるかという頃、Javascriptからメールを送りたいという要求がありました。お問合せフォームのようなものではなく、単に通知目的で決まった送信者から既定の宛先に送るというものでしたが、その機能を設置する箇所が複数個あり、使えるモジュールや環境など、メール送信にかかわる部分がそれぞれ少しずつ異なっていました。

様々な送信方法がある

実際にPerlでメールを送ると言っても、Net::SMTPを使うケース、加えて認証やTLSが要るケース、Email::Senderを使って/usr/sbin/sendmailで送るケース、送り先がEmailクラウドでAPIを使うケース、と多種多様な実装方法があります。その時は設置箇所に合わせてJSONで送られたデータをメールで送るプログラムを用意したわけです。

汎用的な仕組みがない

実際に設置箇所がたくさんある場合、こういった用途の汎用的な仕組みがないものかと思い、一通り終わってから調べてみたのですが、どうも無さそうでした。たぶん無いです。

そこで、クライアントからメール送信に必要なデータを受けとって、サーバ側でSMTPやSMTPS、認証やTLSを入れたり、クラウドのAPIを使ったり、適切な方法でメールを送る仕組みとしてHainekoを作ってみました。意外と便利かもしれません、これ。

名前について

Perlモジュールというよりはアプリケーションなので、機能を表す単語を並べて頭文字を都合よく取って``Haineko''となりました。はいねこです。HTTP API INTO ESMTP K=undef O=undef です。NEKOが入っていますが、猫は特に関係ありません。

Hainekoの実装

フレームワーク: Mojoliciousで実装

HainekoはHTTPでJSONを受けとるので、Mojoliciousを使って実装しています、Perlです。SMTPに比べてHTTPは然程詳しくないので、必要な機能という観点からはMojoliciousですら重量級のフレームワークなのですが、最近Webサイトを作るときはMojoliciousを選択しているので、そうしました。

将来的にMojoliciousはやめて、必要な機能だけのWAFに変えるか自作するかになるかもしれませんが、今のところMojoliciousです。

2013年9月19日(木)追記

Haineko 0.1.0からMojoliciousをやめてPlackベースのPSGIアプリケーションになりました。フレームワーク部分はPlack::Request, Plack::Responseを継承して必要な部分だけのうすーいものになっています。

全体の流れ: リレーサーバとして動く

次の図はKyoto.pmで発表したスライドの該当部分を切り出して、今の実装に合わせて更新しただけのものですが、Hainekoはキューを持たずリレーサーバとして動作します。データの受け取りもメール送信の結果応答も全てJSONです。


HTTPクライアントから受けとったJSONデータを、接続元IPアドレス(REMOTE_ADDR)や宛先が許可されたものであるか、そしてメールアドレスが正しい形式であるか等、最低限の検査をして、宛先ドメインまたは発信者ドメインに基づいてリレーする方法を決定します。

エラー応答も、送信成功の応答もいずれもJSONで返ってきます。

リレー: SMTPサーバやクラウドのAPIへ

次の図も発表したスライドの該当部分を切り出して手直ししたものです。

HainekoはHTTPでJSON形式のメールデータを渡せば、あとはHaineko::Relay::以下のモジュールがうまいこと定義された方法でどこかにリレーするようになっています。Sendmailのそれと同じような動作をするmailertableは、送信するメールの宛先アドレスのドメイン部分がmailertableに定義されていたら、そこで指定されたリレー方法(ESMTP,SendGrid等)で指定されたホストの指定ポートにリレーします。

mailertableに一致する定義がなければ、次はsendermtからメールの発信者アドレスのドメイン部分に一致する定義を探して、mailertableと同じくその定義内容に従ってリレーします。



mailertableにもsendermtにも一致する定義がない場合は、mailertableの``default''という名前で定義されたリレー方法を使います。defaultも定義されていない場合は、127.0.0.1の25番ポートにESMTPでリレーをします。

今のところ、宛先メールアドレスのドメインからMXRRを検索してそこに直接繋ぐという方法は実装していません。将来実装するとは思いますが、まだです。

設定ファイル: etc/haineko.cf

script/hainekoを起動した時にetc/haineko.cfを読込みます。この設定ファイルが存在しな場合は lib/Haineko.pmにハードコードされた既定値が使用されます。全く別の場所に設定ファイルを置いている場合は、環境変数$HAINEKO_CONFにPATHを入れておくとそっちを読込みます。

設定できる内容は主に下記のものが可能です。
  • 外部にリレーする時に使用するEHLOの引数としてのホスト名
  • 受け付けるメールの最大サイズ(デフォルトは4KB)
  • 受け付けるメールあたりの受信者数(デフォルトは4)
  • リレー経路定義ファイルの場所と名前(mailertable, sendermt, authinfo)
  • リレー許可設定ファイルの場所と名前(relayhosts, recipients)
  • ログの設定(デフォルトはsyslogにlocal2で書きだす)
  • Mojoliciousのセッション(セッションキーと既定値300秒の有効期間)

LISTENするポート番号

フロー図や例示、README.mdで2794番ポートでLISTENするように書いていますが、平安遷都2000年と覚えると記憶に残りやすいです。無くよウグイス平安京+2000!

インストールと起動

README.mdに書いている通りに、ソースを展開して、必要なPerlモジュールを入れて、その場で起動出来ます。ただ、実用的に使うには、設定ファイル haineko.cf やリレー経路の定義のmailertable, sendermt, SMTP認証やAPIキーの定義を行う authinfo等幾つかの外部ファイルが必要です。

また、デーモンとして起動しますので、他のサーバソフトウェアのように特定のディレクトリに纏めておきたいかもしれません。

なので、README.mdにも書いていますが、三通りの方法を用意しています。

A. ソースディレクトリで実行: インストール不要

etcディレクトリに*-exampleを除いたファイル名で設定ファイルを作って、morbo, hypnotoad, plackupで起動出来ます。README.mdに書いているPerlモジュールを先に入れておく必要がありますが。
$ sudo cpanm --installdeps .⏎
$ cp etc/haineko.cf-example etc/haineko.cf ⏎
$ 他、etc/以下の必要な設定ファイルをコピーして編集

$ plackup -o '127.0.0.1' -p 2794 -a libexec/haineko.psgi

B. /usr/local/hainekoにインストールして実行: makeを使う

サーバとして動くので、/usr/local/hainekoという専用ディレクトリに入れたいかもしれません。僕はそうします。一般的な構築方法と同じく、./bootstrap && configure && make depend && make && make install でインストールします。

別のディレクトリに入れたい場合は configure --prefix=/usr/local/nekochan という感じです。Haineko本体は/usr/local/haineko/libの中に入ります。

アプリケーション本体のスクリプトは/usr/local/haineko/scriptにインストールされるので、morboでもhypnotoadでもplackupでも好きなもので起動してください。
$ ./bootstrap ⏎
$ sh configure ⏎
$ make depend && make && make test ⏎
$ sudo make install ⏎
...

$ cd /usr/local/haineko ⏎
$ cp etc/haineko.cf-example etc/haineko.cf ⏎
$ 他、etc/以下の必要な設定ファイルをコピーして編集

$ export PERL5LIB=/usr/local/haineko/lib/perl5 ⏎
$ plackup -o '127.0.0.1' -p 2794 -a libexec/haineko.psgi

C. /usr/localにインストールして実行: cpanmだけでOK

Bと同じような感じですが、Haineko本体が/usr/local/lib/perl5/...のどこかに入ります。設定ファイルは/usr/local/etcに、アプリケーション本体のスクリプトは/usr/local/binにインストールされます。

起動方法はA, Bと同じく、好きなもので起動してください。
$ sudo cpanm . ⏎
$ sudo cpanm -L/usr/local --installdeps ⏎
...

$ cd /usr/local/etc ⏎
$ cp haineko.cf-example haineko.cf ⏎
$ 他、etc/以下の必要な設定ファイルをコピーして編集

$ cd /usr/local ⏎
$ plackup -o '127.0.0.1' -p 2794 -a libexec/haineko.psgi

セキュリティ

メールサーバを運用した事がある人なら一番注意を払う箇所、オープンリレーになっていないかという点です。Hainekoはデフォルトで2794番ポートでLISTENするので、25番で起動した時のように瞬く間にスパムの中継に使われてしまう可能性は低いのですが、ないとは言い切れません。

relayhosts: 接続元IPアドレスで

HainekoはHTTPの$REMOTE_ADDRの値が、relayhosts ファイルに定義したIPアドレスまたはネットワーク帯域に一致しないとメールを受け付けません。relayhostsファイルが存在しない場合は127.0.0.1からのみメールを受け付けます。

LAN内のアドレスでのみLISTENするのであれば、オープンリレーにしても多分問題ないですが、それでも明示的にリレーを許可するネットワークを定義したほうが安全です。

recipients: 送信出来る宛先を定義

Hainekoはどんな宛先にもメールを中継するわけではありません。設定ファイル recipients に定義されたメールアドレスまたはドメインに一致する宛先にのみリレーします。設定ファイルではオープンリレーにする事も出来ますが、たぶんろくな事にならない気がします。

以上のように、許可されたIPアドレスから許可された宛先にのみ送信する事が出来る、という構造になっています、Hainekoは。

TODOs:  そのうち実装する予定の未実装項目

未だ実装してないけどそのうち実装しようと思っているものが幾つかあって、今のところ次のようなものがありそうです。

テンプレート

@nekokakさんのツイートにあったテンプレート機能、Emailクラウドでは当たり前の機能みたいにあるのですが、送信するJSONで
{ 'template' => 'greeting', 'param' => { 'var1' => 'cat', 'var2' => 'kijitora' } }

みたいなのを渡せば、指定ディレクトリにあるgreeting.emlとかそういうのをテンプレートとして処理してメール本文を作るのがよいかなぁという感じです。多くの宛先に同じ文面を送る際はテンプレート処理で本文を得たほうが、クライアントからHaineoに転送するデータが少なくて済みますし。

HainekoにPOSTする時の認証

現時点では送信元IPアドレスがrelayhostsファイルで許可されている必要があるのですが、SMTP-AUTHみたいに何かしらの認証手段があったほうがよいです。これについては@songmuさんのツイートでPlack::Middlewareに任せたほうが良いって意見を頂いたので、そのあたり試験して必要最低限の実装をしようかなと思います。

Haineko::Relay::*

Hainekoからリレー出来るのはHaineko::Relay::以下にモジュールがある方法のみで、ESMTPとSendGridのみ実装しています。他にはAmazonSESやPostmark、MailChimpとかMailgunあたりの有名どころのAPIに対応する予定です。

リレーはしませんが、単に削除するHaineko::Relay::Discardは0.0.3で実装しました。SendmailのaccessとかでRHSに書くDISCARDと同じ役割です。

Haineko::Relay::Haineko

別のホストまたは別のポートでListenするHainekoにリレーするモジュールです。Hainekoが投げるJSONをそのままリレーする感じですが、ループ防止の仕組みとReceivedヘッダをうまいこと追記したり、わりと簡単に実装出来そうな感じです。

Haineko::Milter

MilterはMTA構築・運用やってはる人にはおなじみですが、MTAで実行するメールフィルタ(Mail Filter)です。主にウィルスチェックやスパムフィルタリングで使われています。

そのSendmailやPostfixで使えるmilterと同じような仕組みを0.0.3でHaineko::Milterとして実装しました。基底となるクラスは実装済ですが、ちゃんと使えるモジュールをまだ作っていないので、一個か二個か作ってサンプルとしてgit addしておく予定です。

と、実用的な用途を前提として書いていますが、Haineko::Milterを作った理由は、受けとったメール本文をAcme::Nyaaで猫にしようと思ったのが動機です。

まとめ 

現時点でHainekoは特定のホストから特定の宛先にメールを送る事が出来ます。サイトのプログラムから簡易にメールを送るのであればこれで充分な気もしますが、ブラウザからJavascript経由で、不特定の接続元から不特定の宛先に送るようなケースに対応するには、認証の仕組みを含めてがっちり固めておく必要があります。

CPANにはまだ置いてませんし、置くかどうかも決めていません。そこそこ完成したらCPANに置いてこようかと思っています。

今、githubに置いているバージョンは0.0.3で、僕が必要な機能は概ね揃っているので、あとは貰った意見とか、ここに書いた未実装項目を中心に実装していく予定です。

あ、そういえばYAPC::AsiaにTalkを応募していたのですが、残念ながら今回は不採用でした。しかし!去年は行けなかったので今年はYAPC::Asia Tokyo 2013に行きます!


No comments:

Post a Comment