メールをトリガーに任意のPerlスクリプトを実行する(さくらのレンタルサーバを使用)

ケータイのパケット定額サービスの料金は数千円とまだまだ高く契約を躊躇してしまいます。でも外出中にネットにつなげられるとやっぱり便利ですよね。
そんな中、NTTドコモの「メール使いホーダイ」を発見。メールだけならお安いのね。例えば件名「web」本文にWebアドレスを入力してメール送信したらページ内容がメールで送られてくる、とか、件名「google」本文に検索したい文字列なら検索結果、とか、「whois」とか、「nslookup」とか、何とかかんとか、いろいろできそうやん!と、セコい考えを思い立ってしまったわけです。

ひとまず、メールを受信してPerlスクリプトを実行するところまでをざーっと作ってみました。

スクリプトファイルをサーバに置く

末尾のスクリプトをサーバに置いてください。これ以降、以下の条件で記載するので適宜読みかえてくださいね。

アカウント名                : ACCOUNT
スクリプトファイルを置く場所: /home/ACCOUNT/www/bin/
スクリプトのファイル名      : run.pl
メールアドレス              : run

安全のため、http経由では実行できないよう、スクリプトファイルを置く場所には以下のよう設定しましょう。.htaccessでアクセス制限しておくとより安心です。

% chmod 700 /home/ACCOUNT/www/bin

run.plには実行フラグを付与します。

% chmod +x /home/ACCOUNT/www/bin/run.pl

run()の中に任意の処理を書く

run.pl先頭部のrunサブルーチンの中に、実行したい処理を書いてください。第1引数にメールの件名、第2引数にメールの本文が渡されます。
メールの文字コードに依らず、いずれもUTF-8に変換され、UTF8フラグも付きます。

メールアドレスを作成する

サーバコントロールパネルでrunというメールアドレスを作成してください。

.mailfilterを作成する

作成したメールアドレスに.mailfilterを設定します。

% vi /home/ACCOUNT/MailBox/run/.mailfilter

以下の内容を記述してください。

if ( (1) )
{
        to "| /home/ACCOUNT/www/bin/run.pl"
}

if文で囲んだのは、サーバコントロールパネルからメールの設定変更をしても内容が上書きされないようにするためです。それをしないのであれば、toの行だけでも良いと思います。

あと、ファイルモードを600にしないといけません。

% chmod 600 /home/ACCOUNT/MailBox/run/.mailfilter

※この.mailfilterの設定が重要なのですが、公式な機能では無いと思いますので、行う場合は自己責任の下でお願いします。設定方法は「さくらのレンタルサーバ非公式FAQ」を参考にしました。感謝です!

※でも、カスタマーセンターから.mailfiterに関する回答の引用があったり、あと実際に公式のFAQにも.mailfilterの記載があったり、と実は公式な機能なのかもしれません。

完了!

これで、run宛にメールを送信したらrun.plが実行されます。

スクリプト

#!/usr/bin/perl

use strict;
use warnings;

sub run {
    my ($subject, $body) = @_;

    # ここに任意の処理を書いてください。メールを受信する度に実行されます

    # $subject と $body はいずれも UTF-8 で、UTF8フラグも付いています

}

#
# メールを解析して Subject と body を取り出し、run() に渡す
#

# 標準エラー出力は /dev/null 行き
BEGIN { open STDERR, '>', '/dev/null' }

# カレントディレクトリを run.pl と同じディレクトリに移動する
use FindBin qw( $Bin );
BEGIN { chdir $Bin }

require Encode;
require MIME::Parser;

my $parser = MIME::Parser->new;

# 用途上、メールのサイズはそんなに大きくならないので、オンメモリで処理する
$parser->output_to_core(1);

my $entity = $parser->parse(*STDIN);

#
# まず Subject
#

# Subject を取り出して、UTF-8 にする
my $encoded_subject = $entity->head->get('Subject');
my $subject = eval { Encode::decode('MIME-Header', $encoded_subject) };

if ( $@ ) {
    # MIME-Header の decode に失敗したら、仕方ないのでそのまま
    $subject = $encoded_subject;
}

# 余分な空白文字が含まれる事があるので除去
$subject =~ s/A s+ | s+ z//gxms;

#
# 次に body
#

# multipart の場合は最初のパートのみを対象にする
my $first_part = $entity->is_multipart ? $entity->parts(0) : $entity;

# body を取り出して、UTF-8 にする
my $charset = $first_part->head->mime_attr('Content-Type.charset');
my $encoded_body = $first_part->bodyhandle->as_string;
my $body = eval { Encode::decode($charset, $encoded_body) };

if ( $@ ) {
    # Subject と同様、失敗したらそのまま
    $body = $encoded_body;
}

#
# 処理を実行
#

run($subject, $body);

exit;

正規表現の先読みと後読みを使いこなす

Perlにはルックアラウンドアサーション(lookaround assertion)という4つの正規表現拡張があります。肯定の先読み・否定の先読み・肯定の後読み・否定の後読みの4つです。

(?=...) : 肯定の先読み
(?!...) : 否定の先読み
(?<=...): 肯定の後読み
(?<!...): 否定の後読み

これらの使い方を紹介します。

例えば、HTMLのタグとタグの間の空白文字をs///演算子で全て取り除きます。

ルックアラウンドを使わない場合は、

$html =~ s/>\s+</></g;                    # ルックアラウンドを使わない

と書けます。少し読みにくくなるので、Perlベストプラクティスで書きます。

$html =~ s{ > \s+ < }{><}gxms;            # ルックアラウンドを使わない。Perlベストプラクティスの書き方

となります。

ですが、置き換える必要のない山カッコまで置き換えるのはスマートではないですよね?
そこでルックアラウンドを使います。

直前に左山カッコが現れる位置を肯定の後読みでマッチさせて、直後に右山カッコが現れる位置を肯定の先読みでマッチさせる事で、山カッコを置き換えず、空白文字だけを置き換える(ここでは取り除く)対象にできます。

$html =~ s{ (?<= > ) \s+ (?= < ) }{}gxms; # ルックアラウンドを使う

ただ、これではファイル先頭もしくは末尾に空白文字がある場合にそれを取り除く事ができません。
それも取り除くには次のように、先頭および末尾にもマッチする以下のパターンでも良いですが、

$html =~ s{ (?: \A | (?<= > ) ) \s+ (?: (?= < ) | \z ) }{}gxms;

と長くなってしまいます。

否定の後読みおよび否定の先読みと文字クラスの否定を使うと、以下のようにもっとシンプルに表現できます。

$html =~ s{ (?<! [^>] ) \s+ (?! [^<] ) }{}gxms;

これは、

(?<![^>]): 直前に右山カッコ以外の文字は現れない
(?![^<]) : 直後に左山カッコ以外の文字は現れない

という表現になり、つまり、

(?<![^>]): 直前に右山カッコが現れる、もしくは文字が無い
(?![^<]) : 直後に左山カッコが現れる、もしくは文字が無い

という意味になるためです。おもしろいですね!