Writing Some Code

2008-02-08

[]メール受信からのシェル機能実行 01:02 メール受信からのシェル機能実行を含むブックマーク メール受信からのシェル機能実行のブックマークコメント

空メール送信での会員登録や、メールに添付されたファイルをサーバに自動でアップロードなど、メール受信からのスクリプト実行というのは様々なシーンで用途があると思います。

今回はそれをCakePHP 1.2系のシェル機能を使って実現してみました。(1.2.0.6311-betaで確認)

使用するもの

メールサーバqmail、メールの解析処理はPEARのMail_mimeDecodeを使用します。


PEARを使用できるようにセットアップ

まずapp/vendorsの下にPEARフォルダを作り、PEARのコアクラス(PEAR.php)とMail_Mimeパッケージのクラスを置く。

次に、以下のソースをapp/vendors/pear_ini.phpとして保存。

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

f:id:ngtn:20080208005512j:image

セットアップ後の配置は上のスクリーンショットの様になっていればOKです。

シェル機能の実装

Mail_mimeDecodeを使ったスクリプトは、2008-01-23 - Stellaqua - TOMの技術日記に掲載されているスクリプトをベースにさせて頂きました。

以下のソースをapp/vendors/shells/receiver.phpとして保存。

<?php
vendor('pear_ini');
require_once('Mail' . DS . 'mimeDecode.php');
class ReceiverShell extends Shell {
	
	function receiveMail() {
		$mail = "";
		$image_filename = "";
		$from = array();
		$headers = array();

		// 標準入力から受け取ったメールを取得
		$stdin=$this->Dispatch->stdin;
		while( !feof($stdin) ){
			$mail .= fgets($stdin,4096);
		}
	    
		// デコード方法の指定
		$params['include_bodies'] = true;
		$params['decode_bodies']  = true;
		$params['decode_headers'] = true;
		// デコード処理
		$decoder = new Mail_mimeDecode($mail);
		$decoded = $decoder->decode($params);

		// 添付ファイルの有無で処理を分岐
		if ( !empty($decoded->parts) ) {
			// bodyを取得
			$body = $decoded->parts[0]->body;
			// 添付ファイルの数だけループ
			for($idx = 1; $idx < count($decoded->parts); $idx++) {
				$file = $decoded->parts[$idx]->body;
				$filetype = $decoded->parts[$idx]->ctype_secondary;
				/**********************************************************
				 * ファイルタイプのチェックしたり、指定したディレクトリに *
				 * 添付ファイルを保存したりの処理をここでする             *
				 **********************************************************/
			}
		} else {
			// 添付ファイルが無い場合はbodyを取得するだけ
			$body = $decoded->body;
		}
		// ヘッダ部分を取得して漢字コードを変換する
		$headers = $decoded->headers;
		$from_text = mb_convert_encoding($headers['from'], mb_internal_encoding(),'ISO-2022-JP');
		ereg("[0-9a-zA-Z_\.\-]+@[0-9a-zA-Z_\.\-]+",$from_text,$from);
		$from = $from[0];
		$subject = mb_convert_encoding($headers['subject'], mb_internal_encoding(),'ISO-2022-JP');
		$body = mb_convert_encoding($body, mb_internal_encoding(),'ISO-2022-JP');
		/**********************************************************
		 * DBに格納したり、メールを送り返したりの処理をここでする *
		 **********************************************************/
	}
	
}
?>

シェルスクリプトファイル、.qmailファイルの作成

上記で実装したCakePHPのメール受信スクリプトを呼ぶシェルスクリプト(receiver.sh)は以下のように書きました。

#!/bin/sh
cd /appフォルダまでのパス/app
../cake/console/cake receiver receiveMail

そして、メール受信時にそのシェルスクリプトを呼ぶように.qmailファイルは

|/シェルスクリプトまでのパス/receiver.sh

として、しかるべき場所に配置します。


以上で、

メール受信を契機に上記のスクリプトが実行されるコトが確認できました。

あとは用途によって、添付ファイルの保存やDBへのデータ格納など実装すれば良いかと。


ハマッた点

標準入力のオープン・クローズを以下のように自前でやると、標準入力からメールがうまく読み込めてなくてしばらくハマってしまいました。

$stdin=fopen("php://stdin",'r');
while( !feof($stdin) ){
    $mailtext .= fgets($stdin,4096);
}
fclose($stdin);

ソースを追っていたら、ShellDispatcherクラスの__initEnvironmentメソッドで標準入力をオープンして$stdinにセット⇒ShellDispatcherのインスタンスがShellクラスの$Dispatchにセットされている、という事が分かったので標準入力は自前で開かずに$this->Dispatch->stdinを使ってみると、うまく読み込むことが出来ました。

inetinet 2009/07/08 21:43 非常に有益な情報をありがとうございます。
1点後確認いただきたいことがあります。

// bodyを取得
$body = $decoded->parts[0]->body;
ここのコードは

$body = $decoded->body;
が正解ではないでしょうか。ご確認ください。

shinshin 2010/07/01 09:29 はじめまして。
ひとつ質問なのですが、
receiveMail() が引数を持つfunctionの場合、
例:receiveMail($id=""){省略;}など
どのようにshellを記述すればよいのでしょうか?

ngtnngtn 2010/07/08 09:45 shinさん >

亀レスですみませんが、
質問の意図としてはメソッドに外部から情報を与えたいということでしょうか?

それであれば、シェルを
../cake/console/cake receiver receiveMail 12345
とした場合、receiveMail()の中では$this->argsでコマンドライン引数から値を取得できると思います。