Postfixでローカルに存在しないユーザ宛のメールを処理する
今までの流れからするとだいぶ唐突なネタだが、仕事で半日苦労したのでメモしておく。
まとめていたらえらく長文になったので先に結論。
- Postfixのaliasは正規表現を使ってマッピングできる
- Postfixでlocalがregexpを使う場合ローカルパートしか渡さない(らしい)
- $_ENVがnullの場合はphp.iniのvariables-orderディレクティブをチェック
●メールでデータを受け取りたい
ご存知の通り、携帯はブラウザのフォームからMultipartでPOSTできないようになっている。つまり、ファイルのアップロードができないということだ。ユーザからテキスト以外のデータ投稿を受け付けようとすると、メールに添付してもらうしか方法がない。
一方、携帯のユーザの行動パターンには「メールアドレスをころころ変える」という特徴がある。個人的には気に入ったアカウントは長く使いたいと思うのだが、若年層のケータイユーザはそうではないらしい。となると、ユーザ側で簡単に変更できるメールアドレスをキーにするのはあまり都合がよろしくない。それ以外の情報でユーザを識別する必要がある。
●メールを送ってきたユーザを識別したい
私が担当しているコンテンツの前任者は、以下の手段でメール送信ユーザを識別していた。
- ユーザIDを適当に暗号化
- mailto:タグのbody要素に仕込む
→クリックすると本文が入力された状態で端末のメーラが起動する - 受信したメールをパーズしてユーザIDを復号
正直あまりよろしくない方法である。ユーザが本文にテキスト情報を付与して送信することができないし、本文のキーをいじられるとあまりいいことはない。
一方、例えばmixiの「携帯で日記を投稿する機能」などは、ローカルパート(メールアドレスの内「@」以前の部分」に識別キーを入れて送るようになっている。これによってメールのsubjectが日記のタイトル / メールのbodyが日記の本文 / 画像が添付されていれば写真投稿、というメール一通で日記投稿を完結させることが可能になる。
ああ前置きが長くなった。つまり、ローカルパートに情報を載せたメールを処理するための設定方法をメモしておく、というのが本題である。
●Postfixの設定
受信に使うMTAがPostfixであり、アドレスの振り分け以外の設定はすべて完了しているものとする。
◆main.cf
・alias_database
エイリアスの設定を記述する「aliases」ファイルのありかを指定する。ここで指定したファイルがnewaliasesコマンドで.dbファイルに変換され、エイリアスの展開に使用される。
alias_database = hash:/etc/postfix/aliases
・alias_maps
エイリアスの展開を行うために参照する.dbなどのファイル。
alias_database = hash:/etc/postfix/aliases, regexp:/etc/postfix/application.regexp
alias_map = hash:/etc/postfix/aliases, regexp:/etc/postfix/application.regexp
2009-11-12 追記:こちらから指摘あり。もうこの環境からは離れちゃったので検証できないけど、たぶん指摘の通りで正解です。ご指摘感謝。
application.regexpについては後述。
・default_privs
Postfixが動作するユーザ。受信したメッセージは標準出力経由でプログラム / スクリプトに突っ込むのだが、ここで動作するユーザを明示しておかないとnobodyで動作されてしまい面倒なことになる(場合がある)。
default_privs = some_user
・local_recipient_maps
Postfixは受信したメッセージが「ローカルに存在するユーザ宛かどうか」をチェックし、存在しない場合はrejectする。そのリストがlocal_recipient_mapsに記述してある。意図せずrejectされてしまう場合はここをチェックし、明示的にblankを設定しよう。
local_recipient_maps =
Postfixのログに”User unknown in local recipient table”と表示されていたらここに引っかかっている。
◆受信したメッセージを処理する
メールを受信した場合、自分のサーバが最終的な宛先であるならば当該ユーザのメールボックスに格納するのが普通の動作である。しかし受信をリアルタイムにプログラムで処理する場合は、悠長にメールボックスに入れてからPOPで取り出して…などとやってられないので、標準出力を経由して処理プログラムに突っ込む。例えばPHPスクリプトだと…
・aliases
do_something: "| /usr/local/bin/php -f /path/to/script/do_something.php >> /tmp/do_something.log"
このようにすると、do_something@example.jp(自分のドメイン)に届いたメールは標準出力を経由してdo_something.phpに渡され、do_something.phpの標準出力がロギングされる、という動作になる。
・do_something.php
標準出力はfopenしてfgets / freadで読み込みfcloseする、という普通の処理で取得する。もう書いたコードから写経しただけなので動かなかったらごめんなさい。エラー処理は各自で実装してね。
<?php $fh = fopen('php://stdin', 'r') ; $buffer = null ; while(!feof($fh)) { $buffer .= fgets($fh, 4096) ; } fclose($fh) ; ?>
◆受信したメールを正規表現でマッピングする
やっとこのエントリの本題。先ほど例に出したmixiの日記投稿メールは、以下のようなアドレスで送られる。
{user_id}-{crypt}@d.mixi.jp # 例えば私のアカウントだと 17**18-462****4ed@d.mixi.jp
機能はサブドメインで分割されているようで、コミュニティに写真つきコメントを投稿するアドレスは「bc.mixi.jp」ドメインだった。
機能ごとに各ユーザ1つのメールアドレスが存在するので、これを全部aliasesに記述するのは非現実的だ。そのために正規表現によるマッピングをaliasとして記述する。そう、ここでやっと前述のapplication.regexpの出番なのである。
・ローカルパートの設計
まずはローカルパートに記述する情報の設計を行う。だいたいmixiのまんまでいいが、機能をサブドメインで切り分けるほどの規模ではないので、機能の識別子もローカルパートに乗せてしまう。
{user_id}-{crypt}-{function}@example.jp {user_id} = ユーザID(数字) {crypt} = 暗号化キー(適切な値のハッシュなど / 数字とアルファベット) {function} = 機能(d = do_something / e = do_somethingelse)
・application.regexp
/^[0-9]-[0-9a-zA-Z]-d(@.*)?$/ do_something
このようにすると、functionがdなら先ほど定義したdo_somethingというエイリアスにメッセージがマッピングされる。字が赤い部分は後述。
もちろん直接記述してもいい。
・application.regexp
/^[0-9]-[0-9a-zA-Z]-e(@.*)?$/ "| /usr/local/bin/php -f /path/to/script/do_somethingelse.php >> /tmp/do_somethingelse.log"
マッチするかどうかのテストはpostmapコマンドを使う。
# postmap -q 1234-abc-d@example.jp do_something
ここで私が半日棒に振った恥を晒しておく。regexpに送られるメールアドレスは「ローカルパートだけ」の場合がある。つまり「1234-abc-d@example.jp」宛のメールの場合、「1234-abc-d」にマッチしないとマッピングされないのだ。それに気づかず…
/^[0-9]-[0-9a-zA-Z]-e@.*$/ "| /usr/local/bin/php -f (ry"
などと書くとマッチしないのだ。
regexpに送られるメールアドレスは「ローカルパートだけ」の場合がある。
大事なことなので二度言いました。
・do_*.php
送られてきたメッセージについての情報は、スーパーグローバル変数であるところの$_ENVで取得できる。
var_dump($_ENV) ; ↓ array(13) { ["MAIL_CONFIG"]=> string(12) "/etc/postfix" ["SENDER"]=> string(31) "sender_user_address@example.com" ["RECIPIENT"]=> string(21) "1234-abc-d@example.jp" ["USER"]=> string(10) "1234-abc-d" ["LOCAL"]=> string(10) "1234-abc-d" ["PATH"]=> string(13) "/usr/bin:/bin" ["PWD"]=> string(18) "/var/spool/postfix" ["DOMAIN"]=> string(10) "example.jp" ["LANG"]=> string(1) "C" ["SHLVL"]=> string(1) "1" ["LOGNAME"]=> string(10) "1234-abc-d" ["ORIGINAL_RECIPIENT"]=> string(21) "1234-abc-d@example.jp" ["_"]=> string(18) "/usr/local/bin/php" }
application.regexpに直接記述した場合とaliasにマッピングした場合でRECIPIENTが変わったりするので注意しよう。また、php.iniのvariables-orderディレクティブに”E”が入っていないと$_ENVは生成されないので、getenv()を使ってちまちま取得する必要がある。これも忘れて時々ハマるので気をつけよう。
というわけで長い長いエントリになったが、これでユーザからメールを受け取ってスクリプトでごにょごにょ、などができるようになった。レンタルサーバだとmain.cfを修正するのは難しい場合が多いが、.forwardを設置することで実現できるようだ。機会があればいずれ。
余談ながら、ケータイユーザのローカルパートは大変バラエティに富んでいる。顔文字(|-o-|、k-.-d)、彼氏 / 彼女の名前ダダ漏れ(i_love_hogehoge)、中二病(angel.lucifer.hogehoge)、やる気ゼロ(a.a-a)など。そしてRFC違反で送信できないMTAが出たりとか混乱があるのだが、それはまた別のお話。
●ヽ( ・∀・)ノくまくまー(2006-10-25)
http://wota.jp/ac/?date=20061025
●Postfix で知らないローカルユーザを拒否する
http://www.postfix-jp.info/trans-2.3/jhtml/LOCAL_RECIPIENT_README.html
●OpenPNE 2.10 のメールサーバの設定 | Sun Limited Mt.
http://www.syuhari.jp/blog/archives/416
●PHP: コア php.ini ディレクティブに関する説明 – Manual
http://www.php.net/manual/ja/ini.core.php#ini.variables-order
●php.ini-recommendedで、variables_orderがGPCSである理由と、PHP5のauto_globals_jit – おぎろぐはてな
http://d.hatena.ne.jp/i_ogi/20071217/1197912203
●ドコモもauもいいかげんにメールアドレス設定の仕様を直せ。
http://neta.ywcafe.net/000799.html