webdata
DX推進をサポートする技術者向け情報提供サイト

技術者向け技術情報

TOP >技術者向け技術情報 > メールの受信

【PHP】メールの受信

 2022-07-05 (更新日:2022-07-05)
 メールの受信は難しい。いまだ完全に理解できていないがなんとか受信できるプログラムを作れたので、
このプログラムの変数を変えたりや機能追加で活用してます。google先生の各種サイトの紹介されているソースを コピペしてもなかなかうまく動かない、文字コードによる文字化け、接続できてもメールの内容が複雑な配列構造のため取り出せない、 といった具合で悪戦苦労しました。
 今回私が利用していソースを紹介します。ご自身の環境に合わせ変数等書き替えることにより受信し、 内容の表示等利用できるようになることを目標としてご紹介します。

<利用環境準備>
 メール受信には、PEARのNet_POP3とMail_mimeDecodeを利用します。

 初めに、Net_POP3をダウンロードしサーバ環境にコピーする。
ダウンロードは GitHub PHPMailer のサイトより行います。

緑の「Code」をクリックし
Download Zip を選択する
 ダウンロードするとフォルダ名は、「Net_POP3-master」なります。これを「POP3」に変更します。
配置場所は、どこでも構いません。(例:/www/lib)へフォルダーごとコピーします。

 次に、Mail_mimeDecodeをダウンロードしサーバ環境にコピーする。
ダウンロードは GitHub PHPMailer のサイトより行います。

緑の「Code」をクリックし
Download Zip を選択する
 ダウンロードするとフォルダ名は、「Mail_mimeDecode-master」なります。これを「mimeDecode」に変更します。
配置場所は、どこでも構いません。(例:/www/lib/)へフォルダーごとコピーします。

■接続と件数取得

 メール受信サンプルプログラム recv_mail.php
<?php

//メール送受信&文字コードライブラリー
require_once('/opt/lampp/lib/php/Net/POP3.php');		// PEAR Net_POP3(lamppの場合)
require_once('/opt/lampp/lib/php/Mail/mimeDecode.php');	// PEAR MimeDecode(lamppの場合)
//ダウンロードした場合は、配置した絶対パスにて指定ください。


//-------------------------------------------------------------------------------
//変数・定数(メールアカウント)
//-------------------------------------------------------------------------------

$mail_host = 'smtp.office365.com';	//利用している送信サーバを指定
$port = "995";						//ポート番号(メールサーバの仕様に合わせて設定)
$user = 'test@sample.jp';			//認証ユーザアドレス
$pass = '*********';				//認証ユーザパスワード
$from = 'test@sample.jp';			//送信元メールアドレス
$pop = new Net_POP3();

//-------------------------------------------------------------------------------
//処理開始
//-------------------------------------------------------------------------------
	//メールサーバ接続処理
	if ($pop->connect($host, $port) !== TRUE) {
		echo "接続失敗";
		exit;
	}else{
		//echo "接続OK
"; } //APOP → POP3においてパスワードの送信を暗号化して安全性を高める方式 if($pop->login($user, $pass,'APOP') !== TRUE) { $pop->disconnect(); //メールサーバ切断 echo '

ログイン失敗。


'; exit; } else { $mail_count = $pop->numMsg(); //総メール件数取得 $mailID_end = $mail_count; //取得する最終件数を指定 } //次に続く
<解説>
 まずは、メールサーバに接続する。次に、総メール件数取得をする。
メールサーバ接続後、メールデータは受信した順に1から総件数を取得する。次のプログラムで何件目から最終件名までを取得する設定を行うためです。

■ヘッダー情報の取込み

	//前からの続き

for ($loop = $mailID_start; $loop <= $mailID_end; $loop++) {
	$messages = ();				//メールボックスのメッセージの一覧を取得し、UIDLを取得
	$UIDL = $messages['uidl'];	//取得したUIDLが読込済UIDLに存在するか確認
	if(empty($UIDL)){
		$UIDL = $pop->_maildrop;
		$UIDL = $UIDL['num_msg'];
	}
	// データを取得したら削除マーク(実際の削除は $pop->disconnect(); )
	//$pop->deleteMsg($i);				//受信時にメール削除(本件はしない設定)
	//Decode(コード変換)
	$decoder = new Mail_mimeDecode($pop->getMsg($loop));
	$params['include_bodies'] = true;			//ボディを解析する
	$params['decode_bodies'] = true;			//ボディをコード変換する
	$params['decode_headers'] = true;			//ヘッダをコード変換する
	$structure = $decoder->decode($params);
	$parts = $structure->parts;					//添付ファイル有無をチェック
			$file = null;
		if(!empty($parts)){
			$file = '1';
		}
	//ヘッダ情報取得
	$header_list = $pop->getParsedHeaders($loop);
	$mailAddress  = mb_decode_mimeheader($header_list['From']);		//差出人
	$mailTo  = mb_decode_mimeheader($header_list['To']);			//宛先
	$mailCC  = mb_decode_mimeheader($header_list['CC']);			//CC
	$mailSubject  = mb_decode_mimeheader($header_list['Subject']);	//件名
	$mailDate = mb_decode_mimeheader($header_list['Date']);			//差出日時
	$mailDate = str_replace('(GMT+09:00)','',$mailDate);			//(GMT+09:00)があると変換されないため削除
	$mailDate = date('Y-m-d H:i:s', strtotime($mailDate));			//日時変換

	//次に続く

					
<解説>
 本件では、// $pop->deleteMsg($i);は//を記載し受信後削除しないようにしています。 通常メーラが受信データを残るが、PHPで受信データをDB等に残すようにしていないと全てのメールが消えてしまいます。 プログラムの不備によりデータが残らないと大きなトラブルになります。
 本サンプルプログラムには記載してませんが受信した最終の$mailID_endをDBに保存し、次のメール取得はこの$mailID_endの値を DBがら抽出し、$mailID_startとするように運用してます。別途メーラ等で任意のメール削除した場合、メールサーバ側の$pop->numMsg();が削除した数分だけ減ります。
 次に、$messages['uidl'];はメールそのものの固有のIDとなります。受信したデータの保存の際、重複しないように利用できます。

■本文の取込み

	//前からの続き

	//ヘッダーよりcharset取得
	$mailCharset = mb_decode_mimeheader($header_list['Content-Type']);									//Content-Type
	$intPosition = strpos($mailCharset, "charset=");													//"charset="の開始位置取得
	//'Content-Type'にcharsetが含まれている場合、文字コード取得
	if ($intPosition === false) {
		$mailCharset = null;
	} else {
		$getDidit = strlen($mailCharset) - $intPosition - strlen("charset=") - 2;							//取得文字列長
		$mailCharset = substr($mailCharset, $intPosition + strlen("charset=") + 1 , $getDidit);				//charset=以降の文字列で"以外の部分を取得
	}
	//本文データ分解と取り出し
	$mailCharset = mb_decode_mimeheader($header_list['Content-Type']);		//メールのパートごとにContent-Typeをチェックし、text/plainだったらそのまま出力
	//Content-Typeがtextもしくはmultipart判定
	if ("text" == $structure->ctype_primary) {
		$body = $structure->body;
	//Content-Typeがmultipartの場合、さらに各パートの内容を確認
	} elseif ("multipart" == $structure->ctype_primary) {
		foreach ($structure->parts as ) {  
			switch (strtolower($part->ctype_primary)) {
				case "text":										//text時bodyを取得
					$mailCharset = $part->ctype_parameters;			//charsetの抽出
					$mailCharset = $mailCharset['charset'];
					$body = $part->body;								//bodyの抽出
					break;

				case "multipart";									//multipart時、構造のpartを取得
					foreach ($part->parts as $part2) {
						if("text" == $part2->ctype_primary) {		//text時bodyを取得
							$mailCharset = $part2->ctype_parameters;	//charsetの抽出
							$mailCharset = $mailCharset['charset'];	
							$body = $part2->body;					//bodyの抽出
							break;
						}
					}
					break;
				}		
			}
		} else {
			$body = "";
		}

	//charsetが取得できた場合、エンコーディング
	if (strlen($mailCharset)) {
		$mailCharset = strtolower($mailCharset);
		if(preg_match('/2022/',$mailCharset)){		//バグか?「so-2022-j」→「SO-2022-J」に補正
			$mailCharset = 'iso-2022-jp';
		}
		if(preg_match('/tf-/',$mailCharset)){		//バグか?「TF-」→ 「utf-8」あり補正
			$mailCharset = 'utf-8';
		}
	}else{
		$mailCharset = 'iso-2022-jp';
	}
		$body = mb_convert_encoding($body, 'utf-8', $mailCharset);	//文字コード変換
		$body = str_replace('¥t','', $body);
		$body = strip_tags($body);										

}

?>

	//終わり
					
<解説>
 まず、送信された文字コードを取得。これにより本文の文字コード変換を行う。このとき文字コードがないときは、送信された文字コードが iso-2022-jpとして変換。また取得しても一部データが欠落している場合「so-2022-j」、「TF-」は正しいコードに補正して変換してます。
 本文の情報抽出が非常に難しく、取得した情報は配列で複雑な構造になってます。実際、取得した$structureをprint_r()で開いて一つひとつ本文がどこにあるか、添付ファイルの記述はどこか探しました。
 最初のctype_primaryがtextはテキストのみのメール
 最初のctype_primaryがmultipartは添付ファイル等あるメール
これにより本文の抽出の仕方が変わってきます。その後取得した本文を該当する文字コードに変換します。本プログラムではutf-8に変換するようにしてます。

■添付ファイルの取込みと保存

	$path= "/保存先のパスを記載/";
	$n = count($parts);				//マルチパート数
	for ($i = 0; $i < $n; $i++) {	//マルチパート取得ループ
		$arval = $parts[$i];
		if (array_key_exists("content-disposition", $arval->headers)) {
			$fileName = ["content-disposition"];			//headers["content-disposition"]取得
			$file_chk = strpos($fileName, "filename=\"");	//filename=があるかで添付ファイル有無判定
			if ($file_chk !== false) {
				$fileName = mb_convert_encoding($fileName, 'utf-8', $mailCharset);	//文字コード変換
				$fileName = explode("\"",$fileName);		//ファイル名のみ取り出し
				$fileName = $path.'/'.$fileName[1];			//保存先を指定
				file_put_contents($fileName, $arval->body);	//ファイルを書き出し
			}
		}
	}	
					
<解説>
 プログラム記載の注釈通りです。添付ファイル取得際は、recv_mail.phpの最後の方の行に追記下さい。
メールファイル(.msg)については取得できません。(色々挑戦しましたがダメでした。)
ただ添付されたメールファイルを読込ことは可能ですので下記プログラムを参考にして下さい。

■添付メールファイル(.msg)の取込み

	//Content-Typeがtextもしくはmultipart判定
		if ("text" == $structure->ctype_primary) {			//textは添付ファイルなし
			$body = $structure->body;
		//Content-Typeがmultipartの場合、さらに各パートの内容を確認
		} elseif ("multipart" == $structure->ctype_primary) {
			foreach ($structure->parts as $part) {
				switch ($part->ctype_primary) {
					case "text":							//textがあるのでbodyを取得
						$body_msg = $part->body;				//bodyの抽出
						$body_msg = mb_convert_encoding($body_msg, 'utf-8', $mailCharset);	//文字コード変換
						$body_msg = strip_tags($body_msg);	//HTMLタグの除去
						break;
					case "message":							//messageがあるのでbodyを取得
					foreach ($part->parts as $part2) {
						$body_msg = $part2->body;			//bodyの抽出
						$body_msg = mb_convert_encoding($body_msg, 'utf-8', $mailCharset);	//文字コード変換
						$body_msg = strip_tags($body_msg);	//HTMLタグの除去
						break;
					}
				}
			}
		}
					

<解説>
 $structure->ctype_primaryがmultipartのとき添付ファイルがあるので、その後の$part->ctype_primaryがtextもしくはmessage の場合がメールファイルのよです。こちらも必要な場合は、recv_mail.phpの最後の方の行に追記下さい。抽出したファイル本文はファイルやDB等に格納されれば良いかと思います。