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

Leave a comment

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください