忍び歩く男 - SLYWALKER

大阪のこっそりPHPer

CakePHP1.2beta PEAR::Mail_mimeDecodeを使ったメール分解コンポーネント

2009-09-09 追記

結構、検索されてるようなので書いときます。
現在、筆者はPEAR依存が面倒くさくなったので、こちらを使ってます
QdmailReceiverとは - QdmailReceiver Multibyte mail decoder & POP Client

自重気味にα版ってことで、よろしくお願いします。
なんかヤバイとこがあれば、コメントいただけるとうれしいです。

  • スパムっぽいものを検出できるようにしてみた。
  • 添付ファイルにも対応(イメージファイルのみ)
  • エラーメール時のアドレス取得ができるようにしてみた

使い方

Controller

<?php
var $components = array('MimeDecode');

function decode()
{
    // $msg にメールを取り込み後

    $this->MimeDecode->decode($msg);
    $mail = $this->MimeDecode->getDecoded();
}
?>

準備

vendors フォルダにPEAR::Mail_mimeDecodeを配置

/vendors
 ├ pear_ini.php
 └ /Pear
     └ /Mail
        └ mimeDecode.php

pear_ini.php

<?php
define('PEAR_PATH', dirname(__FILE__) . DS . 'Pear');
set_include_path(PEAR_PATH . PATH_SEPARATOR . get_include_path()); 
?>

MimeDecodeComponent

mime_decode.php

<?php
vendor('pear_ini');
vendor('Pear/Mail/mimeDecode');

class MimeDecodeComponent extends Object
{
    /**
     * Mail_mimeDecode::decodeへ渡すパラメータ
     *
     * @var array
     */
    var $params = array(
        'include_bodies' => true,
        'decode_bodies' => true,
        'decode_headers' => true,
    );

    /**
     * デフォルトのcharset
     * 日本語メールならISO-2022-JPかな
     *
     * @var string
     */
    var $default_charset = 'ISO-2022-JP';
    /**
     * charsetの順番
     *
     * @var string
     */
    var $charset_order = 'ISO-2022-JP, SJIS, EUC-JP, UTF-8';

    /**
     * スパム判定メッセージ
     *
     * @var array
     */
    var $spam = array();
    /**
     * エラーでもどってきたであろう、メールアドレス
     *
     * @var array
     */
    var $return_address = array();
    /**
     * 送信者メールアドレス
     *
     * @var string
     */
    var $from = '';
    /**
     * subject
     *
     * @var string
     */
    var $subject = '';
    /**
     * headers
     *
     * @var array
     */
    var $headers = array();
    /**
     * 本文
     *
     * @var array array('text'=>{text}, 'html'=>{html})
     */
    var $body = array('text'=>'', 'html'=>'');
    /**
     * 添付ファイル
     *
     * @var array array[] = array('mime_type'=>{mime_type}, 'filename'=>{filename}, 'binary'=>{binary})
     */
    var $attachments = array();

    /**
     * 初期化
     *
     */
    function reset()
    {
        $this->spam = array();
        $this->return_address = array();
        $this->from = '';
        $this->subject = '';
        $this->headers = '';
        $this->body = array('text'=>'', 'html'=>'');
        $this->attachments = array();
    }

    /**
     * decode
     *
     * @param string $msg
     */
    function decode($msg)
    {
        // http://www.tt.rim.or.jp/~canada/comp/cgi/tech/mailaddrmatch/ 参考にした
        // 2007-03-01 修正 ↓のように変更
        // DoCoMo・Ezweb はメールアドレスの前後に<>がつかない
        // $email_regix = "/<([\S]+@[0-9a-zA-Z_\.\-]+\.[a-zA-Z]+)>/";
        // 誤↑・正↓
        $email_regix = "/([_\w\.\-\"]+@[_0-9a-zA-Z\.\-]+\.[a-zA-Z]+)/";

        // \nをつけてるのは、そのままだとSoftbankの空メールが取得できなかったから
        $Decoder = new Mail_mimeDecode($msg."\n");
        $obj = $Decoder->decode($this->params);
         
        // 送信者のメールアドレスをチェックします
        if (!empty($obj->headers['from'])) {
            $charset = $this->__setCharset($obj->headers['from']);
            $from_text = trim(mb_convert_encoding($obj->headers['from'], mb_internal_encoding(), $charset));
            preg_match($email_regix, $from_text, $match);
            if (!empty($match[1])) {
                $this->from = $match[1];
            }
        }

        // subject
        if (!empty($obj->headers['subject'])) {
            $charset = $this->__setCharset($obj->headers['subject']);
            $this->subject = trim(mb_convert_encoding($obj->headers['subject'], mb_internal_encoding(), $charset));
        }
        // subjectのスパムチェック
        if (stristr($this->subject, 'spam')) {
            $this->spam[] = 'subject has spam !';
        }

        // headers
        $this->headers = $obj->headers;

        // body分割
        $this->__parts($obj);

        // エラーメール時のアドレス抽出(テキスト本文中のメールアドレスをとりあえず総ざらい)
        // fromにDAEMONがあるか判別いれたほうがいいかも
        preg_match_all($email_regix, $this->body['text'], $match);
        if (!empty($match[1])) {
            $this->return_address = array_unique($match[1]);
        }
    }

    /**
     * オブジェクトを配列にして返す
     *
     * @return array
     */
    function getDecoded()
    {
        $a = array();
        $a['spam'] = $this->spam;
        $a['return_address'] = $this->return_address;
        $a['headers'] = $this->headers;
        $a['from'] = $this->from;
        $a['subject'] = $this->subject;
        $a['body'] = $this->body;
        $a['attachments'] = $this->attachments;
        return $a;
    }

    /**
     * エンコードチェック
     * ヘッダでのSJIS使用は、スパムっぽいのでチェック
     *
     * @param string $string
     * @return string charset
     */
    function __setCharset($string)
    {
        mb_detect_order($this->charset_order);
        $charset = mb_detect_encoding($string);
        if ($this->default_charset === $charset) {
            return $this->default_charset;
        } else {
            $this->spam[] = 'different charset at header! '.$charset;
            return 'auto';
        }

    }

    /**
     * 分解されたbodyを再帰処理で再構築
     *
     * @param object $obj
     */
    function __parts($obj)
    {
        if (!empty($obj->parts)) {
            foreach ($obj->parts as $o) {
                $this->__parts($o);
            }
        } else {
            $charset = 'auto';

            if (!empty($obj->ctype_parameters['charset'])) {
                $charset = strtoupper($obj->ctype_parameters['charset']);
                mb_detect_order($charset.', '.$this->charset_order);
                // 宣言されてるcharsetと実際がちがうとか、スパムっぽい
                if ($charset !== mb_detect_encoding($obj->body)) {
                    $charset = 'auto';
                    $this->spam[] = 'different charset at body ! '.mb_detect_encoding($obj->body);
                }
            }

            if (!empty($obj->body)) {
                // text なら
                if ('text' === $obj->ctype_primary) {
                    if ('plain' === $obj->ctype_secondary) {
                        $this->body['text'] .= trim(mb_convert_encoding($obj->body, mb_internal_encoding(), $charset))."\n";
                    }
                    if ('html' === $obj->ctype_secondary) {
                        $this->body['html'] .= trim(mb_convert_encoding($obj->body, mb_internal_encoding(), $charset))."\n";
                    }
                }
                // image なら
                if ('image' === $obj->ctype_primary) {
                    $a = array();
                    $a['mime_type'] = $obj->ctype_primary.'/'.$obj->ctype_secondary;
                    $a['filename'] = $obj->ctype_parameters['name'];
                    $a['binary'] = $obj->body;
                    $this->attachments[] = $a;
                }
                // 他のタイプはいまのとこ無視
            }
        }
    }
}
?>