Hatena::ブログ(Diary)

お前の予定!! 日記 RSSフィード

2008-04-15

マルチパートなメールを解析する PEAR::Mail::mimeDecode をラップするクラス

お前の予定!にメールを送信すると、そのメールを予定として登録できます。この機能を実装するためにPEAR::Mail_mimeDecodeを使っているのですが、実際に使うときにはちょっと手間というか煩雑になってしまうので、Mail_mimeDecodeをラップするクラスを作りました。添付ファイル付きのメールもかなりシンプルに処理できると思います。

ReceiptMailDecoderクラスです。

(誘導)メール受信をフックする方法

メール受信をフックして処理するためのプログラム。 - お前の予定!! 日記

class ReceiptMailDecoder

PEAR::Mail_mimeDecodeをもっとシンプルに使えるようにラップするクラスです。

携帯の写メール対応をするときに使うと便利です。もちろん通常のPCメールでも対応できます。

  • 使い方
<?php
require_once('ReceiptMailDecoder.class.php');

$decoder =& new ReceiptMailDecoder($raw_mail);
// To:アドレスのみを取得する
$toAddr = $decoder->getToAddr();
// To:ヘッダの値を取得する
$toString = $decoder->getDecodedHeader( 'to' );
// Subject:ヘッダの値を取得する
$subject = $decoder->getDecodedHeader( 'subject' );
// text/planなメール本文を取得する
$body = mb_convert_encoding($decoder->body['text'],"eucjp-win","jis");
// text/htmlなメール本文を取得する
$body = mb_convert_encoding($decoder->body['html'],"eucjp-win","jis");
// マルチパートのデータを取得する
if ( $decoder->isMultipart() ) {
    $tempFiles = array();
    $num_of_attaches = $decoder->getNumOfAttach();
    for ( $i=0 ; $i < $num_of_attaches ; ++$i ) {
        /*
         * ファイルを一時ディレクトリ _TEMP_ATTACH_FILE_DIR_ に保存する
         * 一時ファイルには tempnam()を使用する
         * この部分は使用に合わせて変更して下せい
         */
        $fpath = tempnam( _TEMP_ATTACH_FILE_DIR_, "todoattach_" );
        if ( $decoder->saveAttachFile( $i, $fpath ) ) {
            $tempFiles["$fpath"] = $decoder->attachments[$i]['mime_type'];
        }
    }
}
?>
  • プロパティ
    • array $attachments = array()
      • var: array[] = array('mime_type'=>{mime_type}, 'file_name'=>{file_name},'binary'=>{binary} )
    • array $body = array( 'text'=> null , 'html'=> null )
      • var: array('text'=>{text}, 'html'=>{html} )
  • メソッド
    • ReceiptMailDecoder ReceiptMailDecoder ( &$raw_mail, string $raw_mail)
    • string extractionEmails (string $raw_string)
    • string getDecodedHeader (string $header_name)
    • string getFromAddr ()
    • string getHeaderAddresses (string $header_name)
    • int getNumOfAttach ()
    • string getRawHeader (string $header_name)
    • string getToAddr ()
    • bool isMultiPart ()
    • bool saveAttachFile (int $index, string $str_path)
<?php /* -*-java-*- */
require_once('Mail/mimeDecode.php');


/**
 * 受信メールのヘッダの値やマルチパートの本文、ファイルを取得するクラス
 *  
 * @author	k5959k+yamada@gmail.com
 * @create	2008-03-11
 * @package	ya--mada
 */
class ReceiptMailDecoder {
    
    
    /**
     * 本文
     * 
     * @var array array('text'=>{text}, 'html'=>{html} )
     */
    var $body = array( 'text'=> null , 'html'=> null );
    
    /**
     * 添付ファイル
     * 
     * @var array array[] = array('mime_type'=>{mime_type},
     *                            'file_name'=>{file_name},
     *                            'binary'=>{binary} )
     */
    var $attachments = array();
    
    
    /**
     * Mail_mimeDecode オブジェクト
     * 
     * @var object
     */
    var $_decoder;
    
    
    /**
     * constractor ReceiptMailDecoder class.
     * 
     * @param string $raw_mail 受信したそのままメール文字列
     */
    function ReceiptMailDecoder ( &$raw_mail )
    {
	if ( !is_null( $raw_mail ) ) {
	    $this->_decode( $raw_mail );
	}
    }
    
    /**
     * このクラスを使用する際の初期化
     * 
     * @access public
     * @param  array
     */
    function init() {
	
    }
    
    /**
     * 生メールをデコードしてプロパティに代入する
     * 
     * @access public
     * @param  string $raw_mail 受信したそのままメール文字列
     */
    function _decode ( &$raw_mail ) {
	if ( is_null($raw_mail) ) {
	    return false;
	}
	
	$params = array();
	$params['include_bodies'] = true;
	$params['decode_bodies']  = true;
	$params['decode_headers']  = true;
	// $params['input'] = $raw_mail."\n";
	
	/*
	 * Mail_mime::Decode をつかって分解解析する
	 * マルチパートの場合は、本文と添付に分けます。
	 */
	$this->_decoder =& new Mail_mimeDecode( $raw_mail."\n" );
	$this->_decoder = $this->_decoder->decode($params);
	
	$this->_decodeMultiPart($this->_decoder);
    }
    
    /**
     * 指定ヘッダの取得
     * 
     * @access public
     * @param  string  $header_name
     * @return string
     */
    function getRawHeader ( $header_name ) {
	
	return isset($this->_decoder->headers["$header_name"])
	    ? $this->_decoder->headers["$header_name"]
	    : null;
    }
    
    /**
     * ヘッダがmimeエンコードされている場合はデコードして取得する。
     * 
     * @todo   携帯絵文字には対応していない。
     * @access public
     * @param  string $header_name
     * @return string
     */
    function getDecodedHeader( $header_name ) {
	return mb_decode_mimeheader($this->getRawHeader( $header_name ));
    }
    
    
    /**
     * 指定ヘッダ内のE-mailアドレスだけを抜き出して返す
     * 
     * @access public
     * @param  string $header_name
     * @return string
     * @see extractionEmails()
     */
    function getHeaderAddresses ( $header_name ) {
	return $this->extractionEmails($this->getRawHeader( $header_name ));
    }
    
    /**
     * STATIC
     * 文字列の中からemailアドレスっぽいものだけを抽出して返します。
     * emailアドレスっぽいものの正規表現をあらためた
     * see. http://red.ribbon.to/~php/memo_003.php
     * 
     * @access public
     * @param  string $raw_string
     * @return string $mail_addresses メールアドレスっぽいものを複数あれば,(カンマ)区切りで
     */
    function extractionEmails( $raw_string ) {
	
	/*
	 * 旧emailアドレスっぽい正規表現
	 * see. http://www.tt.rim.or.jp/~canada/comp/cgi/tech/mailaddrmatch/
	 *
	$email_regex_pattern = "/[\x01-\x7F]+@(([-a-z0-9]+\.)*[a-z]+|\[\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\])/";
	*/
	
	/*
	 * 新emailアドレスっぽい正規表現
	 * see. http://red.ribbon.to/~php/memo_003.php
	 */
	$email_regex_pattern = '/(?:[^(\040)<>@,;:".\\\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\\\[\]\000-\037\x80-\xff])|"[^\\\\\x80-\xff\n\015"]*(?:\\\\[^\x80-\xff][^\\\\\x80-\xff\n\015"]*)*")(?:\.(?:[^(\040)<>@,;:".\\\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\\\[\]\000-\037\x80-\xff])|"[^\\\\\x80-\xff\n\015"]*(?:\\\\[^\x80-\xff][^\\\\\x80-\xff\n\015"]*)*"))*@(?:[^(\040)<>@,;:".\\\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\\\[\]\000-\037\x80-\xff])|\[(?:[^\\\\\x80-\xff\n\015\[\]]|\\\\[^\x80-\xff])*\])(?:\.(?:[^(\040)<>@,;:".\\\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\\\[\]\000-\037\x80-\xff])|\[(?:[^\\\\\x80-\xff\n\015\[\]]|\\\\[^\x80-\xff])*\]))*/';
	
	
	if ( preg_match_all( $email_regex_pattern, $raw_string, $matches, PREG_PATTERN_ORDER ) ) {
	    if ( isset($matches[0]) ) {
		return implode( ",", $matches[0] );
	    }
	}
	
	return null;
    }
    
    /**
     * デコードした本文の取得
     * 
     * $this->body['text']; // テキスト形式の本文
     * $this->body['html']; // html形式の本文
     */
    
    /**
     * 添付ファイルの取得
     * 
     * $this->attachments[$i]['mime_type']; // MimeType
     * $this->attachments[$i]['file_name']; // ファイル名
     * $this->attachments[$i]['binary'];    // ファイル本体
     */
    
    /**
     * メール本文部分の処理
     * マルチパートの場合は、その処理もしますよ。
     * 
     * @access private
     */
    function _decodeMultiPart(&$decoder) {
	
	// マルチパートの場合 それぞれがparts配列内に再配置されているので
	// 再帰的に処理をする。
	if ( !empty($decoder->parts) ) {
	    foreach ( $decoder->parts as $part ) {
		$this->_decodeMultiPart($part);
	    }
	}
	else {
	    
	    if ( !empty($decoder->body) ) {
		
		// 本文 (text or html )
		if ( 'text' === strToLower($decoder->ctype_primary) ) {
		    if ( 'plain' === strToLower($decoder->ctype_secondary) ) {
			$this->body['text'] =& $decoder->body;
		    }
		    elseif ( 'html' === strToLower($decoder->ctype_secondary) ) {
			$this->body['html'] =& $decoder->body;
		    }
		    // その他のtext系マルチパート
		    else {
			$this->attachments[] = array( 'mime_type'=>$decoder->ctype_primary.'/'.$decoder->ctype_secondary,
						      'file_name'=>$decoder->ctype_parameters['name'],
						      'binary'=>&$decoder->body
						      );
		    }
		}
		// その他
		else {
		    $this->attachments[] = array( 'mime_type'=>$decoder->ctype_primary.'/'.$decoder->ctype_secondary,
						  'file_name'=>$decoder->ctype_parameters['name'],
						  'binary'=>&$decoder->body
						  );
		}
	    }
	}
    }
    
    /**
     * メールが添付ファイルつきか調べる
     * 
     * @access private
     * @return bool      添付付きなら true 無ければ false を返す
     */
    function isMultiPart() {
	
	return (count($this->attachments) > 0) ? true : false;
    }
    
    /**
     * 添付ファイルの数を数える
     * 
     * @access private
     * @return int
     */
    function getNumOfAttach() {
	return count($this->attachments);
    }
    
    /**
     * Toヘッダからアドレスのみを取得する
     * 
     * @access public
     * @return string toアドレス 複数あればカンマ区切りで返す
     * @see getHeaderAddresses(), extractionEmails()
     */
    function getToAddr() {
	return $this->getHeaderAddresses( 'to' );
    }
    
    /**
     * Fromヘッダからアドレスのみを取得する
     * 
     * @access public
     * @return string Fromアドレス 複数あればカンマ区切りで返す
     * @see getHeaderAddresses(), extractionEmails()
     */
    function getFromAddr () {
	return $this->getHeaderAddresses( 'from' );
    }
    
    /**
     * 添付ファイルを保存する
     * 
     * @todo   Mail_mimeDecode はマルチパートのbase64decodeの面倒はみない?
     * @access public
     * @param  int
     * @param  string $str_path
     * @return bool  成功なら true 失敗なら false を返す
     */
    function saveAttachFile ( $index, $str_path ) {
	
	if ( !file_exists($str_path) ) {
	    if ( !is_writable(dirname($str_path)) ) {
		return false;
	    }
	}
	else {
	    if ( !is_writable($str_path) ) {
		return false;
	    }
	}
	
	if ( !isset($this->attachments[$index]) ) {
	    return false;
	}
	
	if ( $fp=fopen($str_path, "wb") ) {
	    fwrite($fp, $this->attachments[$index]['binary'] );
	    fclose($fp);
	    return true;
	}
	
	return false;
    }
}

nhisanhisa 2008/10/28 00:26 ya--mada様

nhisaと申します。
ここ数日、非常に参考にさせて頂いております。
こちらのmimedecodeのラッパーReceiptMailDecoderに関してですが、
私の方で、いじっています、オープンソースのeticketという
問い合わせ管理ソフトにGPLとして組み込ませて頂けないかと
考え、コメントさせて頂いております。

組み込んだものを全体として、私のサイトで公開したく、
可否をお聞かせ頂けませんでしょうか。

よろしくお願い申し上げます。

失礼いたします。

ya--madaya--mada 2008/10/28 19:46 nhisaさん、はじめましてこんにちは。
参考にしていただくのは結構ですがGPLで再配布ってのは翻って自分にもGPLの縛りが出てきそうなのでちょっと待ってください。
ライセンスに関しては私が正しく把握しておりませんので。
さらに、使用しているE-mailアドレスを抽出する正規表現も http://www.din.or.jp/~ohzaki/perl.htm さんの孫引きですし。

PEAR::mimeDecode をラップしているだけのクラスですので、ご自分でもっと汎用的に書き直すほうが良いかと思います。
で、あれば、私に断りを入れる必要も無くご使用いただけるのではないでしょうか。

nhisanhisa 2008/10/28 23:50 ya--mada様

ご返答ありがとうございます。
nhisaです。

ご丁寧にありがとうございます。
ご判断御尤もです。失礼いたしました。

自身で考えて一部実装に手をいれながら参考にさせて
頂きたいと思います。
一部、だいぶ似通ってしまう可能性があり、その点ご不快に
なることが心配点としてございますが、
改めて、書き直して、実現したいと思います。

その際、名称などは気をつけたいと思いますが、
どうしても基本的なアルゴリズムが似てしまう可能性が
少なからずあるかと思います。
まとめなおして、私のサイトで公開しますので、
その際に、お気に障る部分がありましたら、ご遠慮なく
お申し付けください。

できる限りお申し付けに沿って修正したいと思います。

ありがとうございました。

ya--madaya--mada 2008/10/31 14:02 クラス名はご自分で作成なさっているパッケージ名などを頭文字に使うのが一般的のようです。
apple(WebObjects)の場合は、NSHogehogeクラスなどとつけているみたいですね。
メソッド名は、どういう動作をしているかわかりやすく付けるのが基本ですから同名のメソッドが出来上がるのも当然のことだと思います。

hidekihideki 2009/01/31 08:06 こんにちわ。素晴らしいライブラリの配布ありがとうございます。早速使わせて頂きました。

http://www.hideblog.net/nikkis/show/123

な感じで紹介させて頂きました!

ya--madaya--mada 2009/02/08 10:53 今後のために、念のために言っておくと、「公開」はしているが「配布」はしていないのです。
勝手に使用していただく分にはなんら問題はありません。
「こんな風なことをしているひとも居るんだ」程度に参考してください。

Cakeの使い方が勉強になりました。ありがと。

yukikoyukiko 2009/09/10 22:45 とても素晴らしいライブラリの公開、ありがとうございます。重宝しています。

ところで、質問なんですが、添付ファイルの
"CONTENT-ID"
を取得したいのですが、できないでしょうか?

よろしくお願いします。

ya--madaya--mada 2009/09/15 23:45 返事遅れてすみません。日記をチェックしていなかったのでコメントして頂いたことに気付いていませんでした。
質問についてですが、

function _decodeMultiPart(&$decoder) 内の $this->attachments[] に代入しているところで、
'content-id' => $decoder->header['content_id']
を加えてやればいけそうな気がしますがどうでしょうか?
Mail/mimeDecode.php を覗いてもらえれば修正箇所の検討つくと思います。ヨロシコ

yukikoyukiko 2009/09/18 21:48 ありがとうございます。

$decoder->header['content_id']

で、上手く CONTENT-ID の取得ができました。

素晴らしいですね。
これで完璧です。

matsunmatsun 2009/10/08 17:24 デコメの解析につまってしまい、とても参考になりました。

前の方と同じで、機種?によって複数添付写真の並び順が変わってしまう場合があって、「content-id」による制御ができればと思い試行錯誤してみました。

「ReceiptMailDecoder.class.php」の
function _decodeMultiPart(&$decoder) 内の $this->attachments[] に代入しているところで、
'content-id' => $decoder->header['content_id']
について修正してみたのですが、'type'などのような取得がうまくできておりません。

大変お手数ですが、ご指導いただけると幸いです。
宜しくお願い致します。

matsunmatsun 2009/10/14 18:36 大変お騒がせしました・・・なんとか自己解決できました!!

おっしゃる通り、「Mail/mimeDecode.php 」の中身をprintrしながら思ったような動きをする事ができました。

本当に参考になりました、ありがとうございます!

azaz 2010/09/19 07:17 デコメのContent-IDを取得するのは

$decoder->headers['content-id']
を追記するといいみたいです。

大分古い記事ですけど、迷った人が行き着いた際の参考までにー。

beatmasterbeatmaster 2010/09/24 12:52 お世話になります。

サイト作成前に、使用するプログラムを検討しています。
このプログラムの動作にあたり、サーバーのroot権限が必要になりますか?

初歩的な質問を失礼致します。

ya--madaya--mada 2010/10/05 00:15 azさん、ありがとうございます。
なるべく近日、追記という形でパッチ提供できればと思います。

beatmasterさん。
サーバのroot権限が何故必要なのかわからないのでなんともいえません。
しかし、このソースを使うのは、pearと同じように呼び出して使う、というか単なるphpのスクリプトですから、root権限なんて不要ですよ。
までも、どこぞの誰が書いたか分からないようなソースをレビューも何も出来ない状態でコピペして使うのは、まったくもっておススメしません。

bluezbluez 2011/01/14 12:45 お世話になっております。こちらのサイトを参考に、写メ日記のシステムを構築しています。

現在、別ライブラリを用いて絵文字対応させようとしているのですが、キャリアの判別ができない為に、行き詰まっております。

このクラスでキャリアを判別するメソッドを用意する事は可能でしょうか?

宜しくお願い致します。

ya--madaya--mada 2011/01/28 02:03 このクラスはマルチパートなメールを簡便に扱うためのものです。という前提で。
キャリアを判別というのはどういう意味でしょうか?senderのドメインで
@docomo.ne.jp
@ezweb.ne.jp
@softbank.ne.jp
@willcom.com
@emnet.ne.jp
で判別すればよくないですか?それをメソッドに入れろっていう要望?
すみませんが、どういうことをおっしゃりたいか教えていただけますか。

このクラスのメソッドを使うとしたら、getFromAddr()でFromのアドレスを取ってきて、各キャリアのドメインでstrstr()でも掛けるのが良いのではないでしょうか。
どうですか?

mozenmozen 2011/02/17 19:13 ya--madaさん

大変参考になっています!

一点だけ気になるところがあるので、近々自分で調べる予定です。

携帯で複数ファイルを添付しても、最初の添付ファイルしか取れないです。ちなみに、PCの場合は全部取れます。

取り急ぎご連絡まで。

ya--madaya--mada 2011/02/18 12:48 以前のコメントでは、デコメに関してのものもありますので、サンプルコードそのままではうまくいかないという意味でしょうか。
キャリアも何なのか解りませんので私も確認出来ません。

mimeDecode本体の方も合わせて確認しないといけないですね。(バージョンなど)

鉄ちゃん鉄ちゃん 2012/01/02 13:37 有益な情報の公開ありがとうございます。メールの解析の手順など勉強になりました。

ところで、少し疑問に思ったことがあります。
$params['decode_heders'] = true;
では、ヘッダ部はデコードしてくれないのでは?
__ $params['decode_headers'] = true; じゃないかと

でも、ヘッダを勝手にデコードされてしまうとどんな charset なのか不明なので、必要なヘッダのみ mb_decode_mimeheader() でデコードするつもり(添付のファイル名も合わせて)だったので不都合は感じませんでしたが。

それともうひとつ。
メールサーバからの不着通知が来た場合は、送信したメールが添付ファイルとして付いてきますが、そんな時など text/plain な部分が複数回あると(ソース上)最後のパートが有効になってしまうのですね。

不着通知の処理をチャンとしておかないとスパム認定されてしまうのでキッチリやりたいと思うのですが、マルチパートのどこに添いうメッセージがあるのか不定(大概は最初でしょうが)なので、都度それらしきものが無いかを調べる事になってしまいます。このクラス内でやって、ステイタスを特定の所に出るようにしようかなどと考えています。
何れにしろとても良いきっかけになりました。ありがとうございました。

ya--madaya--mada 2012/01/02 20:20 もう4年くらい前の日記なのに、コンスタントにアクセスされていて、なんともいえない感が漂っていたりします。
ご参考いただいて、コメントまでいただけるなんて何となく感謝です。

エラーメールについては、当時もどうしようもないなぁと言う感じでした。
MTAだけでなく、サービス提供者ごとに返すエラーが異なっていて、コメントしていただいたように、逐次リスト化して対応するしかないのだと思います。
そこで、当時はその辺りをGmailに丸投げするという方法で解決を図りました。
いったんGmailで受けたモノを自鯖に転送しています。エラーメールやSPAMはGmailのフィルターで処理しています。
自鯖ではGmailからのメールのみリレーするようにsmtpdを設定しています。
そして、各サービスで共通のフックスクリプトを使って振分けをおこなっている。
このラッパはそういう前提で作られているのでした。

なので、エラーメールの処理と、MIME処理と、処理振分けetc.は、別々のものとして分割する考え方なのです。

こちらが受信時のフック処理、qmail前提です。
http://d.hatena.ne.jp/ya--mada/20080410/1207809302


同一のクラス内でエラーメールは処理しない方がよいと思いますよ。私の主観では。

ya--madaya--mada 2012/01/04 00:21 > $params['decode_heders'] = true;
> では、ヘッダ部はデコードしてくれないのでは?
> __ $params['decode_headers'] = true; じゃないかと

なんのことか分からなくて、PEAR::Mail_mimeDecode の変更かPHPのバージョンアップで書き方が新しくなったのかと思ってしまいました。
単なるtypoのことですよね?

すみません、headers と書くべきところを heders にしてました。
修正しておきます。

TNTN 2017/04/01 19:40 こんにちは!
メールをトリガーにDBへの登録の方法を探していてたどり着きました。
参考にさせて頂き、試してみたところphp7でエラーが返ってきました。
$decoder =& new ReceiptMailDecoder($raw_mail);
の部分の&を外すとエラーはなくなるのですが、題名、本文など取得できません。
何が原因なのでしょうか??

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

Connection: close