Zend_Mail_Part で メール解析 する。

Zend_Mailを使った送信のサンプルはたくさんあったのだけど、逆のデコードについては情報がありませんでした。
デコードと言えば、PEARのMail_mimeDecodeが有名なので、みなさんそちらに走ってるのかなぁ。
あえて逆らって、Zend Frameworkで方法を探ってみた。

結論から書くと Zend_Mail_Message(Zend_Mail_Part) でできるのだけど、マルチバイト処理周りは無いので、そこは作ってやる必要がありました。
ついでに、添付ファイルのbase64のデコードとか追加して、MyMailPart というおれおれメールデコーダーを作ってみた。
動作検証はあまりしてない。

<?php
$BASE_DIR = dirname(__FILE__);
set_include_path($BASE_DIR.'/ZendFramework-1.5.2/library' . PATH_SEPARATOR . get_include_path());
require 'Zend/Mail/Message.php';

/**
 * Zend_Mail_Partを使ったメール解析クラス
 * 日本語も処理できるようにしたつもり。
 *
 */
class MyMailPart
{
    /**
     * @var Zend_Mail_Part
     */
    protected $_zend_part;

    /**
     * コンテンツに適用するエンコード(このコードに変換して返す)
     *
     * @var string
     */
    protected $_to_charset;

    /**
     * ヘッダー
     *
     * @var array
     */
    protected $_headers;

    /**
     * コンテンツ本体
     *
     * @var string
     */
    protected $_content;

    /**
     * constructor
     *
     * ex) $mmp = new MyMailPart('/hoge/mail.log');
     * ex) $fh = fopen('php://stdin', 'r'); $mmp = new MyMailPart($fh);
     *
     * @param string|resource|Zend_Mail_Part    $filename_or_handle_or_zend_part    ファイル名|ファイルハンドル
     * @param string                            $to_charset                         エンコーディング
     */
    public function __construct($filename_or_handle_or_zend_part, $to_charset = 'UTF-8')
    {
        if ($filename_or_handle_or_zend_part instanceof Zend_Mail_Part) {
            $zend_part = $filename_or_handle_or_zend_part;
        } else {
            $zend_part = new Zend_Mail_Message(array(
                'file' => $filename_or_handle_or_zend_part
            ));
        }
        $this->_zend_part = $zend_part;
        $this->_to_charset = $to_charset;
    }

    /**
     * ヘッダーの content-type を得る。
     *
     * @return string|null  ex)'text/plain'
     */
    function getContentType()
    {
        $h = $this->getHeader('content-type');
        if ($h === null) {
            return null;
        }
        $ct = Zend_Mime_Decode::splitContentType($h);
        return $ct['type'];
    }

    /**
     * 添付ファイル名を得る。
     *
     * @return string|null  ex)'dsc12345.jpg'
     */
    function getAttachFilename()
    {
        preg_match('#filename="(.*)"#i', $this->getHeader('content-disposition'), $matches);
        if (!isset($matches[1])) {
            return null;
        }
        return $matches[1];
    }

    /**
     * 指定のヘッダを得る。
     *
     * @param string        $name   ヘッダの項目名 ex)'from'
     * @return string|null          ex)'hoge@example.com'
     */
    function getHeader($name)
    {
        if (empty($this->_headers)) {
            $this->getHeaders();
        }
        if (!isset($this->_headers[$name])) {
            return null;
        }
        return $this->_headers[$name];
    }

    /**
     * 全てのヘッダを得る。
     *
     * @return array
     */
    function getHeaders()
    {
        if (empty($this->_headers)) {
            $old_ie = mb_internal_encoding();
            mb_internal_encoding($this->_to_charset);
            $h = $this->_zend_part->getHeaders();
            foreach ($h as &$item) {
                if (!is_array($item)) {
                    $item = mb_decode_mimeheader($item);
                }
            }
            mb_internal_encoding($old_ie);
            $this->_headers = $h;
        }
        return $this->_headers;
    }

    /**
     * コンテンツを得る。
     *
     * @return string
     */
    function getContent()
    {
        if (empty($this->_content)) {

            // content-type の charset よりエンコード変換
            $c = $this->_zend_part->getContent();
            $ct = Zend_Mime_Decode::splitContentType($this->_zend_part->getHeader('content-type'));
            $from_charset = 'ASCII';
            switch (strtoupper($ct['charset'])) {
                case 'UTF-8':       $from_charset = 'UTF-8';    break;
                case 'ISO-2022-JP': $from_charset = 'ISO-2022-JP';  break;
                case 'EUC-JP':      $from_charset = 'EUC-JP';   break;
                case 'SHIFT_JIS':   $from_charset = 'SJIS';     break;

            };
            if ($from_charset !== $this->_to_charset) {
                $c = mb_convert_encoding($c, $this->_to_charset, $from_charset);
            }

            // content-transfer-encoding: base64
            if ($this->getHeader('content-transfer-encoding') === 'base64') {
                $c = base64_decode($c);
            }

            $this->_content = $c;
        }
        return $this->_content;
    }

    /**
     * マルチパートを得る。
     *
     * @return array|null   MyMailPart の 配列、もしくは null
     */
    function getParts()
    {
        if (!$this->_zend_part->isMultipart()) {
            return null;
        }

        $ret = array();
        foreach ($this->_zend_part as $part) {
            $ret[] = new self($part, $this->_to_charset);
        }
        return $ret;
    }

    /**
     * ダンプ
     *
     * @return array
     */
    public function dump()
    {
        $ret = array();
        $ret['content-type'] = $this->getContentType();
        $ret['headers'] = $this->getHeaders();
        $ret['content'] = $this->getContent();
        $parts = $this->getParts();
        if ($parts !== null) {
            $ps = array();
            foreach ($parts as $val) {
                $ps[] = $val->dump();
            }
            $ret['parts'] = $ps;
        }
        return $ret;
    }
}
?>

サンプルをいくつか。

どんな感じでデータが入ってくるのかは、dump()してみてください。
(バイナリの添付ファイルがあると化け化けになりますが)

$mmp = new MyMailPart($BASE_DIR.'/mail.txt');
print_r($mmp->dump());
?>

マルチパートを考慮して、順番にcontent-typeを見て必要なものだけ抽出してみたり。

<?php
function dump_content($part)
{
    $ret = array('type' => 'unknown');
    $c = $part->getContent();
    switch ($part->getContentType()) {
        case 'text/plain':
            $ret = array(
                'type' => 'text',
                'content' => $c,
            );
            break;
        case 'text/html':
            $ret = array(
                'type' => 'text',
                'content' => strip_tags($c),
            );
            break;
        case 'application/octet-stream':
            $filename = $part->getAttachFilename();
            if ($filename !== null) {
                $ret = array(
                    'type' => 'attach',
                    'filename' => $filename,
                );
            }
            break;
        case 'image/png':
        case 'image/gif':
        case 'image/jpeg':
            $filename = $part->getAttachFilename();
            if ($filename !== null) {
                $ret = array(
                    'type' => 'image',
                    'filename' => $filename,
                );
            }
            break;
    }
    return $ret;
}

function dump($part)
{
    $ret = array();
    $ret['content-type'] = $part->getContentType();
    $ret['content'] = dump_content($part);
    $parts = $part->getParts();
    if ($parts !== null) {
        $ps = array();
        foreach ($parts as $val) {
            $ps[] = dump($val);
        }
        $ret['parts'] = $ps;
    }
    return $ret;
}

print_r(dump($mmp));
?>

最初の text/plain だけ抽出してみたり。

<?php
function get_first_text($part, &$ret)
{
    if (!isset($ret['subject']) && $part->getHeader('subject') !== null) {
        $ret['headers'] = $part->getHeaders();
        $ret['subject'] = $part->getHeader('subject');
    }
    if ($part->getContentType() === 'text/plain') {
        $ret['content'] = $part->getContent();
        return;
    }
    $parts = $part->getParts();
    if ($parts !== null) {
        $ps = array();
        foreach ($parts as $val) {
            get_first_text($val, $ret);
            if (isset($ret['content'])) return;
        }
    }
}

$ret = array();
get_first_text($mmp, $ret);
print_r($ret);
?>