2014/01/06

HainekoがCPANモジュールになりました

去年の夏ごろから作ってたHainekoを大晦日にCPANにアップロードしました。HainekoというのはHTTPサーバとして動いてJSONでPOSTされたメールを指定したSMTPサーバやメールクラウドにリレーするという代物なのですが、現時点でHainekoに関する最新情報は昨年渋谷であったShibuya Plack/PSGI Conference #1発表した資料とgithubに置いているリポジトリのREADME-JA.mdです。

細かい説明まで含めるとREADMEに収めるには量が多い感じなので、ドキュメント類は思いつきで取得したhaineko.orgに纏めてそのうち置く予定ではありますが、開発開始〜今日に至るまでちょいちょい仕様が変わっているので、一旦全体的な概要を書いておこうかと思います。

Hainekoとは何か

HainekoはHTTP API INTO ESMTPの頭文字を都合よくつまみ食いした名前の、Perlで実装されたPlackベースのHTTPサーバです。起動したHainekoにJSONでメールのデータをPOSTすると、Hainekoの設定ファイルで指定した別のSMTPサーバやメールクラウド(SendGridとかSESとか)にメールをリレーします。簡潔に言うと、メール送信専用のHTTP-APIサーバです。

リポジトリにも入れている図ですが、下記のようにメールの送信から応答受信まで流れます。

CPANからインストール出来るようになりました

0.2.12からCPANにもアップロードするようにしたので、cpanmコマンドで容易にHainekoをインストールする事が出来ます。動作するPerlは5.10.1以上です。
% sudo cpanm Haineko ⏎
--> Working on Haineko
Fetching http://search.cpan.org/CPAN/authors/id/A/AK/AKXLIX/Haineko-0.2.12.tar.gz ... OK
...
Successfully installed Net-DNS-0.73
Building and testing Haineko-0.2.12 ... OK
Successfully installed Haineko-0.2.12
3 distributions installed
% perldoc -l Haineko ⏎
/usr/local/lib/perl5/site_perl/5.12.1/Haineko.pm
XML::SimpleとかXML周辺のインストールに失敗する場合は、expatをpkg_addとかyumとかaptとかで、あるいはportsから入れるといけるかもしれません。

簡単なチュートリアル

セットアップ

Hainekoがインストール出来たら、hainekoctlってスクリプトが/usr/binか/usr/local/binか、perlバイナリと同じディレクトリに入っていますので、それを使うと起動とか停止が楽に出来ます。

Hainekoは幾つかの設定ファイルを必要としますので、先ずは適当なディレクトリにファイルを用意します。
% sudo hainekoctl setup --dest /usr/local/haineko ⏎
 * debug1: Destination directory = /usr/local/haineko
 * debug1: Temporary directory = /tmp/BUT1IP0WBd
 * debug1: Archive file = /tmp/BUT1IP0WBd/haineko-setup-files.tar.gz
 * debug1: Extracted directory = /tmp/BUT1IP0WBd/haineko-setup-files
 * debug1: [MAKE] /usr/local/haineko/bin
 * debug1: [COPY] /usr/local/haineko/bin/hainekoctl
 * debug1: [PERM] 0755 /usr/local/haineko/bin/hainekoctl
 * debug1: [MAKE] /usr/local/haineko/etc
 * debug1: [COPY] /usr/local/haineko/etc/authinfo
 * debug1: [PERM] 0600 /usr/local/haineko/etc/authinfo
 * debug1: [COPY] /usr/local/haineko/etc/haineko.cf
 * debug1: [COPY] /usr/local/haineko/etc/mailertable
 * debug1: [COPY] /usr/local/haineko/etc/password
 * debug1: [COPY] /usr/local/haineko/etc/recipients
 * debug1: [COPY] /usr/local/haineko/etc/relayhosts
 * debug1: [COPY] /usr/local/haineko/etc/sendermt
 * debug1: [MAKE] /usr/local/haineko/libexec
 * debug1: [COPY] /usr/local/haineko/libexec/haineko.psgi
 * debug1: [DONE] hainekoctl --dest /usr/local/haineko
/usr/local/hainekoにファイルを展開していますが、~/hainekoでも何処でも良いです。etc/以下に設定ファイルが沢山ありますが、存在しなければHaineko::Defaultの値を読込んで動きますので、無くても良いですがあった方が良いです。

リレーサーバの設定

etc/mailertable

Hainekoが直接配送する事も出来ますが、先ずはetc/mailertableにリレーするメールサーバを定義します。mailertableファイルにコメントで書いている例を見れば書き方はなんとなくわかるかと思いますが、みんなが使っているであろうGmailに自分のアカウントで認証してリレーする事にします。
# etc/mailertable
default:
  mailer: 'ESMTP'
  host: 'smtp.gmail.com'
  port: 587
  auth: 'MyGoogleMail'
  starttls: 1

etc/authinfo

Gmailへ送る時は認証が必要ですので、etc/authinfoに認証情報を書いておきます。etc/mailertableの"auth"に書いた"MyGoogleMail"という値をキーにもつ認証情報が使用されますので、下記のように書いておきます。
# etc/authinfo
MyGoogleMail:
  username: 'Gmailのアカウント名'
  password: 'Gmailのパスワード'
キー名はmailertableとauthinfoで一致していてYAMLの規則に反していなければ何でも良いです。ユーザ名とパスワードの組をauthinfoに分離しているのは、ファイルそのもののパーミッションで保護する事が目的ですので、Hainekoを実行するユーザが読み取り権限を持っている必要があります。

メールを作る

送信するメールをJSON形式のテキストファイルで作ります。日本語はUTF-8を前提としていますが、伝統的なISO-2022-JPにも対応しています。
{
    "ehlo": "[127.0.0.1]",
    "mail": "kijitora@example.org",
    "rcpt": [ "mikeneko@example.jp" ],
    "header": {
        "from":    "キジトラ <kijitora@example.com>",
        "subject": "謹賀新年",
        "replyto": "straycats@cat-ml.example.org",
        "charset": "UTF-8"
    },
    "body": "あとでお年玉貰いに行きます"
}
"mail"の値には自分のメールアドレスを(From)入れて"rcpt"の値には宛先メールアドレスを複数個書きます。宛先メールアドレス数の上限はhaineko.cfの"max_rcpts_per_message"で定義される4件です。

headerの値

"from"にはFromヘッダに表示したい名前とメールアドレスを書きますが、"from"が無い場合は"mail"のメールアドレスがヘッダに使われます。

"replyto"も返信は別のメールアドレスに欲しい場合は書いておくと良いですが、無い場合はReply-Toヘッダが作られないだけなのでなくても良いです。

"charset"はメール本文やSubjectにマルチバイト文字を使っている場合の文字コードを指定しますが、"charset"が無い場合はUTF-8である前提で処理されます。もしも伝統的なISO-2022-JPで書いたSubjectや本文であれば、"charset"の値は"ISO-2022-JP"としてください。

このような感じで作ったメールを/tmp/mail.jsonとして保存しておきます。

送信を許可する宛先(etc/recipients)

メールサーバというのは非常に神経質に扱わなければならない部分があり、オープンリレーにしてはならない、つまり無条件にリレーしてはならないという点もその一つです。

Hainekoはリレーサーバなのですが、そう簡単にメールを送れるわけではありません。宛先メールアドレス("rcpt"の値)がetc/recipientsに列挙されたメールアドレスまたはドメインに一致しない場合はメールをリレーせずエラーを返します。
# etc/recipients
open-relay: 0
domainpart:
    - 'example.jp'

recipients:
    - 'sabatora@example.jp'
    - 'mi-chan@example.org'
宛先メールアドレスがいくつもないのであれば"recipients"シーケンスに、沢山あるのであれば共通するドメインを"domainpart"シーケンスに列挙しておくと良いです。

Hainekoを起動する

/usr/local/haineko以下に必要なファイルが用意され、メールのJSONファイルも出来たので、もう起動してもOKです。etc/haineko.cfはデフォルトの設定から変えなくても動きますし、変えるとしたらメールの最大サイズ(4KB)を増やすか、1通のメールに指定出来る宛先の数(4件)を増やすか、ぐらいです。
% /usr/local/haineko/bin/hainekoctl start -d ⏎
Watching /usr/local/haineko/etc /usr/local/haineko/lib /usr/local/haineko/libexec/lib /usr/local/haineko/libexec/haineko.psgi for file updates.
HTTP::Server::PSGI: Accepting connections at http://127.0.0.1:2794/
hainekoctlスクリプトを使わなくてもplackupで起動しても良いです。
% export HAINEKO_ROOT=/usr/local/hainekoplackup -o 127.0.0.1 -p 2794 /usr/local/haineko/libexec/haineko.psgi ⏎
HTTP::Server::PSGI: Accepting connections at http://127.0.0.1:2794/
上の例では-dオプションを付けていて、Hainekoへの接続が全て起動したターミナルの標準エラーに出力されます。-d(--devel)を付けない場合は本番モードで動きます。

メールの送信

一番手軽であろうcurlコマンドで送ります。
% curl -XPOST -d'@/tmp/mail.json' 'http://127.0.0.1:2794/submit' | jq -M . ⏎
{
  "referer": null,
  "addresser": "kijitora@example.org",
  "recipient": [
    "mikeneko@example.jp"
  ],
  "response": [
    {
      "command": "QUIT",
      "mailer": "ESMTP",
      "port": 587,
      "message": [
        "2.0.0 closing connection ae5sm140915401pac.18 - gsmtp"
      ],
      "host": "smtp.gmail.com",
      "dsn": "2.0.0",
      "error": 0,
      "rcpt": "mikeneko@example.jp",
      "code": "221"
    }
  ],
  "useragent": "curl/7.15.5 (i686-redhat-linux-gnu) libcurl/7.15.5 OpenSSL/0.9.8b zlib/1.2.3 libidn/0.6.5",
  "queueid": "s03IZwf48603hOYm",
  "remoteport": 50320,
  "timestamp": {
    "datetime": "Fri Jan  3 18:35:58 2014",
    "unixtime": "1388741758"
  },
  "remoteaddr": "127.0.0.1"
}
応答は改行なしのJSONで返ってきますが、そのままでは読みにくいため、jqを使って上記のように見やすくすると良いでしょう。

先に示した形式のJSONをHainekoの/submitに対してPOSTすれば良いので、HTTPとJSONが扱えるものであればPerlでもPythonでもJavaでもクライアントになれます。リポジトリのegディレクトリにいくつかのスクリプト言語とJavaでのサンプルコードを入れていますので、curl以外で送信する時は参考になるかも知れません。

Gmailの"Suspicious sign in prevented"

この記事を書く為に、試験用サーバとして使ってるVPSからGmailに対してSMTP-AUTH(mailertableとauthinfoで指定した)での認証をしたのですが、普段ログインしているIPアドレスでないからか、Googleから"Suspicious sign in prevented"ってタイトルのメールが来ました。

誰かがアカウントを乗っ取ったんちゃうの?って感じのメールで、心当たりが無ければパスワードをリセットしろとか、そういうのです。今回は自分のサーバで固定IPアドレスでもありますので、受けとったメールの指示に従って許可する設定をしてHaineko経由で送信しました。

11/20以降(v0.2.5)の主な変更点

Shibuya Plack/PSGI Conference #1のLTで発表してから変更した点について、大きなものだけ列挙します。
  • Haineko同士のリレーでループを検出する(検出したらエラーを返す)
  • 応答の"response"の中身はマップからシーケンスに変更(複数の宛先対応)
  • 宛先が複数在る時は一定の条件を満たした場合fork()して送信する(高速化)
  • 送信するメール(JSON)で"relpy-to"のキー名を"replyto"に変更
  • メールをリレーせずファイルとして書きだすHaineko::SMTPD::Relay::File
  • メールをリレーせず標準エラーに出力するHaineko::SMTPD::Relay::Screen
  • v0.2.12からCPANモジュールになった

まとめ

筆無精なのでもっとこまめに書いておけば良かったのですが、逆に一つの記事があまり長くなっても読むのがつらい気がしないでもないので、今回はここまでです。

Hainekoはメールサーバですので、おそらくSendmailやPostfixの運用経験がないとメールサーバには近寄りたくないという意見が多数派な気がしますし、適当に運用するとろくな事がありません。

開発開始から現在に至るまで、Hainekoには実際に自分が必要とする機能を実装し、且つ、適当に動かしてもなるべく安全に動作(メールサーバとして)するように気をはらった結果、しっかり説明を書いておくべき項目が増えたので、リレー方法、セキュリティ、ログ、サーバの選択等、なるべくソースコードを読まなくても良い程度の運用に必要な情報はブログに書こうと思います。

0 件のコメント:

コメントを投稿