[PHP]ソケット接続(fsockopen)でメール受信(POP3)

POP before SMTP をする際など、ちょっとした POP 認証をしておきたい時のために
単純なメール受信クラスを作ってみました。
open() でメールサーバーに接続し retrieve() で受信できます。

【注意】
このクラスは学習目的のサンプルです。
実用の際は PEAR::Net_POP3 などを利用して下さい。

・Pop3Retrieve.php

<?php
class Pop3Retrieve{
	private $connect;

	//接続
	function open($host, $user, $pass, $port=110){
		$this->connect = fsockopen( $host, $port, $err, $errno );
		if ( !$this->connect ) {
			return false;
		}
		
		fputs($this->connect, "USER {$user}\r\n");
		if(!$this->check_response()) return false;

		fputs($this->connect, "PASS {$pass}\r\n");
		if(!$this->check_response()) return false;
	}
	
	//レスポンスコード確認
	function check_response(){
		$buf = fgets($this->connect, 512);
		if( substr($buf, 0, 3) != '+OK' ) {
			return false;
		} else {
			return true;
		}
	}

	//メール数確認
	function status(){
		fputs($this->connect, "STAT\r\n");
		
		if( !$this->check_response() ) return false;
		$buf = fgets($this->connect, 512);
		sscanf($buf, '+OK %d %d', $num, $size);
		
		return $num;
	}
	
	//num番目のメールを受信
	function retrieve($num=1){
		$default_timeout = ini_get('default_socket_timeout');
		stream_set_timeout($this->connect, 3);
		
		fputs($this->connect, "RETR {$num}\r\n");

		if( !$this->check_response() ) return false;
		
		$data = "";
		$line = "";
		
		while ( !feof($this->connect) ) {
			$meta_data = stream_get_meta_data($this->connect);
			if($meta_data["timed_out"]) break;
			
			$line = fgets($this->connect);
			if($line === false || preg_match("/^\.\r\n/", $line)) break;
			$line = preg_replace("/^\.\./", ".", $line);
			$data .= $line;
		}
		
		stream_set_timeout($this->connect, $default_timeout);
		
		return $data;
	}
	
	//削除
	function delete($num=1){
		fputs($this->connect, "DELE {$num}\r\n");

		if( !$this->check_response() ) return false;
	
	}

	//終了
	function close(){
		fputs($this->connect, "QUIT\r\n");
		if( !$this->check_response() ) return false;
		
		fclose($this->connect);
	}
}

・使い方

<?php
include_once "Pop3Retrieve.php";

$host = 'tcp://mail.sample.com';
$user = 'user';
$pass = 'password';
$port = 110;

$pop3 = new Pop3Retrieve();
$pop3->open($host, $user, $pass, $port);

//総数を確認
$count = $pop3->status();

//全て受信
$mail = array();
for($i=1;$i<=$count;$i++){
	$mail[] = $pop3->retrieve($i);
}

$pop3->close();

取得されたメールはデコードされていない状態なので、
本文や件名などをパースする場合は PEAR の Mail_mimeDecode などを使うのが便利です。

POP Before SMTP 対策だけが目的であれば imap_open() 関数を使ったほうが簡単かもしれません。

$result = imap_open( sprintf('{%s:%d/pop3}INBOX', $host, $port), $user ,$pass );

サーバーによっては「/notls」フラグを追加する必要があります。

$result = imap_open( sprintf('{%s:%d/pop3/notls}INBOX', $host, $port), $user ,$pass );

[PHP]マルチパートメールの構造

マルチパートメールは、一般的に ヘッダ、プレーンテキスト、HTML、添付ファイルで構成されています。

マルチパートかどうかはヘッダの Content-Type を確認します。

シングルパートの場合

Content-Type: text/plain; charset="iso-2022-jp"

マルチパートの場合

Content-Type: multipart/mixed; boundary="----ABCDEFG"
Content-Transfer-Encoding: 7bit
Content-Type: multipart/alternative; boundary="----HIJKLMN"

「Content-Type: multipart/~」となっていればマルチパートです。
この場合、boundary の値が区切り文字になっているので、
そこでパートを分けることができます。
boundary の値はあくまで例なので実際は環境によって異なります。

「multipart/alternative」は通常プレーンテキストとHTMLの
表示切り替えの際に用いられます。
基本的にはこのパートがメールの本文となります。

----HIJKLMN
Content-Type: text/plain; charset="iso-2022-jp"
Content-Transfer-Encoding: 7bit

Hello, World
----HIJKLMN
Content-Type: text/html; charset="iso-2022-jp"
Content-Transfer-Encoding: quoted-printable

<HTML><HEAD><META http-equiv=3D"Content-Type" content=3D"text/html; charset=
=3Diso-2022-jp">
</HEAD>
<BODY>
<P>Hello, World</P>
</BODY>
</HTML>
----HIJKLMN

multipart/alternative の boundary で区切られた
上の部分がテキスト、下が HTML です。
所々「=3D」のようになっているのは「quoted-printable」で
エンコードされている影響です。
本文が日本語の時はほとんど読めない状態になっているはずです。
これをデコードするには quoted_printable_decode() を使います。

HTML本文中に画像が用いられていた場合、

<IMG src="cid:03@123456.123456">

のようになります。

「cid」は「Content-ID」のことです。
メールのソースを探すと、次のようなパートがあるはずです。

----ABCDEFG
Content-Type: IMAGE/GIF;
 name="sample.gif"
Content-Transfer-Encoding: base64
Content-ID: <03@123456.123456>
Content-Disposition: inline;
 filename="sample.gif"

(エンコードされた画像データの文字列)
----ABCDEFG

Content-ID の部分が先ほどの値と一致しているので、
これがその画像であるということになります。

Content-Disposition が「inline」なので、本文中で
使うためのファイルであるという意味です。
添付されている場合は「attachment」になります。

画像はエンコードされた文字列の部分をデコードすると
画像になります。
この場合は Content-Transfer-Encoding: base64 なので
base64_decode() を使えばデコードできます。