電子メールと連動するシステムの開発

小学校からの連絡が電子メールになり、配布物がPDFファイルになりました。PDFをダウンロードできるのが5日以内で、保存するのを忘れると面倒なことになりそうです。またファイル名が内容と無関係で、探すのが大変そうなので電子メールを自動的に処理するシステムをつくることにしました。家庭の居間のパソコンのお気に入りにいれておけば簡単にチェックできます。

要件

  • 特定アドレスからの電子メールを受信する
  • 電子メール内のリンクを検出して、リンク先をダウンロードする
  • ウェブで電子メールとPDFを表示する

ファイル構成

  • receive.php

    procmailによって起動される。標準入力からメールを受け取り、指定のディレクトリに保存する。メール本文中のPDFへのリンクを探して、それをダウンロードする。現在のところ複数のPDFファイルが含まれることはないが、そこまでは対応できるようにしておく。

  • index.php

    受信したメールの一覧を表示する。ウェブサーバーが起動する。メール保存ディレクトリ中のファイルを読み込み、新しい順に表示する。

  • functions.php

    メールの内容を解析する関数、メール・PDFのディレクトリの設定など、receive.phpとindex.phpで共通する処理を書く。

コード

/var/www/mailsys

これをシステムの基本ディレクトリとする。ウェブサーバー(apache)とメール受信者が読み書きできるようなパーミッションが必要。

.procmail

小学校からのお知らせメールを受信するユーザーのホームディレクトリにおく。

:0 Hc
* ^From:.*xxxxx@mamail.jp
|/usr/bin/php -f /var/www/mailsys/receive.php

:0 に続くHはメールのヘッダー部分だけを振り分け条件に使うことを意味する。cはメールのコピーを振り分けの対象とすることを意味する。cがついていないと処理に失敗したときに失われてしまう。
*に続く^From:.*xxxxx@mamail.jpは振り分け条件を意味する。^Fromで送信元が一致するものを振り分ける。
|に続くプログラムの標準入力にメールを渡す。今回はPHPで書いたreceive.phpの標準入力(STDIN)にメール本文が渡される。

http.conf

Alias /mailsys /var/www/mailsys
<Directory "/var/www/mailsys">
    Order deny,allow
    Allow from all
    Deny from all
</Directory>

実際には/etc/httpd/conf.d/以下に適当な名前(mailsys.conf)で保存する。http://example.com/mailsys/ にブラウザーでアクセスする。

functions.php

<?php
require_once("Zend/Mail/Message.php");
require_once("Zend/Http/Client.php");

define('APPLICATION_DIR', '/var/www/mailsys');
define('MAIL_DIR', APPLICATION_DIR . '/mail/');
define('FILE_DIR', APPLICATION_DIR . '/file/');

function getMails($offset = 0, $limit = 20){
    $mails = array();
    $files = array_reverse(glob(MAIL_DIR . "*"));
    foreach(array_slice($files, $offset, $limit) as $mail){
        $mails[] = parse($mail);
    }
    return $mails;
}

function receive(){
    $path = sprintf("%s%s.mamail", MAIL_DIR, time());
    while(file_exists($path)) $path .= '.' . time();
    saveFile($path, stream_get_contents(STDIN));

    $Hc = new Zend_Http_Client;
    $mail = parse($path);

    printf("Date: %s\n", $mail['date']);
    printf("Subject: %s\n", $mail['subject']);

    foreach($mail['files'] as $file){
        $response = $Hc->setUri($file['url'])->request(Zend_Http_Client::GET);
        if('application/pdf' == $response->getHeader('content-type')){
            saveFile(FILE_DIR . $file['file'], $response->getBody());
            printf("File: %s\n", $file['file']);
        }
    }
}

function parse($file){
    $Mail = new Zend_Mail_Message(array('file' => $file));
    mb_internal_encoding('ISO-2022-JP');
    $mail = array('subject' => 'no subject', 'from' => 'no from', 'date' => date('Y-m-d H:i:s'));
    foreach(array('subject', 'from', 'date') as $header){
        try{
            $mail[$header] = myconvert(mb_decode_mimeheader($Mail->getHeader($header)));
            if('date' == $header){
                $mail[$header] = strtotime($mail[$header]);
            }
        }catch(Zend_Mail_Exception $e){
            printf("%s %s", get_class($e), $file);
        }
    }
    mb_internal_encoding('UTF-8');
    $mail['body'] = myconvert($Mail->getContent());
    $mail['files'] = array();

    foreach(explode("\n", $mail['body']) as $line){
        if(preg_match("/(https:\/\/www.mamail.jp\/.*(mamail[0-9]*\.pdf))/", $line, $match)){
            $mail['files'][] = array('url' => trim($match[1]), 'file' => trim($match[2]));
        }
    }
    return $mail;
}

function myconvert($text){
    return mb_convert_encoding($text, 'UTF-8', 'ISO-2022-JP');
}

function saveFile($path, $content){
    file_put_contents($path, $content);
    chmod($path, 0644);
}

receive.php

<?php
require_once("functions.php");
receive();

index.php

<?php
ini_set('display_errors', 'On');

require_once("functions.php");
if(isset($_GET['file'])){
    $path = FILE_DIR . basename($_GET['file']);
    if(is_readable($path)){
        header('content-type: application/pdf');
        readfile($path);
    }
    exit;
}

require_once("Zend/View.php");
$View = new Zend_View(array(
    'scriptPath' => '.',
));

$View->mails = getMails(0, 20);
echo $View->render('index.tpl');

index.tpl

<?php
echo $this->doctype();
?>
<html>
    <head>
    <meta charset="utf-8">
    <title>mailsys</title>
    <style type='text/css'><!--
        dt {
            border-top : 1px solid black;
            font-weight: bold;
            margin-top: 1em;
        }
        dd {
            white-space: pre-wrap;
            padding: 5px;
            background-color: #EEE;
            font-size: small;
            border-radius: 5px;
        }
    --></style>
</head>

<body>
    <dl>
    <?php foreach($this->mails as $mail){
        $dt = array(
            date('Y-m-d:', $mail['date']),
            $mail['subject'],
            join(" ", array_map(
                function ($file){
                    return sprintf("<a href='?file=%s'>PDF</a>", basename($file['file']));
                },
                $mail['files']
            )),
        );
        printf("<dt>%s</dt><dd>%s</dd>\n", join(" ", $dt), trim($mail['body']));
    }
    ?>
    </dl>
</body>
</html>

課題

  • メールが増えたときのページング。これを実現するにはindex.tplにメール総数を渡す必要がある。GETの引数としてpageがわたるようにして、getMailsへの引数を調整する。
  • メール本文もしくはPDF内容の検索。メール本文をデータベースにいれるとすると、システムが複雑になる。検索ウィンドウ、一覧表示も少し面倒。
  • 配布書類がPDFに決め打ちになっている。ファイルをダウンロードするときのcontent-typeをデータベースに保存すればいいが、上記とどうようシステムが複雑になる。
  • メールの日本語コーディングが決め打ちになっている。MIMEエンコードされたものの文字コードを取得する適切な方法がわからない。