メール受信をフックして処理するためのプログラム。

今後の課題

  1. 予期せぬバグで php が無限ループなどで終了しない場合の処理が課題!
注意すべきこと
  • CLIモードの場合だと、ライブラリーパスの設定がいつもと違う。
    • 普段は、set_include_path( "./Lib".":".get_include_path() ); をつかってメインプログラムの直下にあるスクリプトをコールしているが、.qmailから起動するのでフルパスで指定するか、php.ini で規定しているパスを参照させるしかない。どうするとスマートかな?
  • stdin から読み出しているのだが、一旦メモリに全部入れることになってしまうのがなんだかな。
    • $stdin=fopen('php://stdin','rb'); while(!feof($stdin)) { fread($stdin) } て感じ。
  • PEAR::Log ではログに記録するログレベルはデフォルトで決まっているのでちゃんと制御しないとダメだよ。
    • $log->setMask(Log::UPTO(PEAR_LOG_DEBUG));
  • 予期せぬバグでの php の暴走を抑える。
    • set_time_limit( int sec ); で良いのだろうか?

コードを呼び出すためのフックの仕組み

qmailを使用しており、ya--madaに向けてメールを受けているので、/home/ya--mada/.qmail に次のように記述します。

./Maildir/
|/usr/local/bin/php /home/ya--mada/ToDo/script/catch_mail.php

メールフックによって起動する最初のスクリプト catch_mail.php

  1. 受け取ったメールの X-Forwarded-To:フィールドを読み込む。
  2. 指定されたiniファイルから対象メールアドレスを探す。
  3. メールアドレス毎の処理スクリプトをコールする。
<?php /* -*-java-*- */
/**
 * メール受信フックで最初に起動するスクリプト
 * X-Forwarded-To: の宛て先で事実上のメインスクリプトを
 * コールするスクリプトを振り分ける
 */
require_once('IniParser.class.php');   // 自前のini形式ファイルのパーサークラス
require_once('Log.php');               // PEARライブラリ

/*
 * デバッグログの保存ディレクトリ
 */
define( '_DEBUG_LOG_DIR_', '/www/log/php_debug/catch_mail' );
define( '_DEBUG_LOGGING_', true );

/*
 * php の不慮のバグでの暴走を抑えるために
 * 90sec で処理がタイムアウトするようにしてみる。cliでもちゃんと効くかな?
 */
set_time_limit(90);

/*
 * ini形式ファイル
 * 受信アドレスをフィールド名
 * スクリプトファイルを script 値にフルパスで書く
 */
$iniFile = "/home/ya--mada/ToDo/script/script_call.ini";

/*
 * E-mailアドレスっぽい文字列の正規表現
 */
$emailCheckPattern = "([\x01-\x7F]+@(([-a-z0-9]+\.)*[a-z]+|\[\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\]))";

/*
 * X-Forwarded-To: フィールドを見つける正規表現
 */
$regexPattern = "/^X-Forwarded-To:\s*$emailCheckPattern/";

// 標準入力からメールデータの読み込み
$toAddr = null;
$stdin = fopen('php://stdin', 'rb');
$raw_mail = '';
$body_flag = false;
while(!feof($stdin)) {
    $data = fgets($stdin, 8192);

    // Bodyフラグが off の間はヘッダを解析するよ。
    // 空行があればそこから本文開始の合図なので空行があるか調べるだけ。
    if ( !$body_flag ) {
        $is_empty = trim($data);
        if ( empty($is_empty) ) {
            $body_flag = true;

            // Body になっても X-Forwarded-To:が無ければ処理を終了する。
            if ( is_null($toAddr) ) {
                break;
            }
        }

        if ( is_null($toAddr) ) {
            if ( preg_match( $regexPattern, $data, $matches ) ) {
                $toAddr = $matches[1];
            }
        }
    }
    $raw_mail .= $data;
}
fclose($stdin);


if ( is_null($toAddr) ) {
    // ログを出力して終了
    _debug_log( 'X-Forwarded-To:フィールドを取得できません。' );
    _debug_log( $raw_mail );
    die();
}

/*
 * iniファイルで $toAddr フィールドに一致するscriptヴァリューを取得
 */
$ini =& new IniParser( $iniFile );
$callScript = $ini->getValue( 'script', $toAddr );

if ( file_exists( $callScript ) ) {
    include( $callScript );
}
else {
    // ログに出力して終了
    _debug_log( "iniファイルで設定した$callScript ファイルがありません。" );
    die();
}
// 一応正常終了
// ログに出力して終了
_debug_log( "正常終了しました。$toAddr$callScript" );
exit();


/**
 * デバッグ用のログ保存をする
 * <code>
 * define( '_DEBUG_LOG_DIR_', '/www/log/php_debug/catch_mail' );
 * @ob_start();
 * _debug_log( ob_get_contents() );
 * while (@ob_end_clean());
 * </code>
 */
function _debug_log( $message, $priority = PEAR_LOG_NOTICE )
{
    if ( defined('_DEBUG_LOGGING_') ) {
        if ( !_DEBUG_LOGGING_ ) return null;
    }

    if ( !defined('_DEBUG_LOG_DIR_') ) {
        define( '_DEBUG_LOG_DIR_', '/tmp' );
    }

    $log_path = _DEBUG_LOG_DIR_.'/qmail_catch_'.date('Ymd');
    $conf = array( 'mode'=>0644, 'timeFormat'=>'%Y/%m/%d %H:%M:%S' );

    $log =& Log::singleton('file', $log_path, 'ident', $conf, $priority );

    $log->setMask(Log::UPTO(PEAR_LOG_DEBUG));

    $message = mb_convert_encoding( $message, 'eucjp-win', 'auto' );
    $log->open();
    $log->log($message);
    $log->close();
}
?>