Laravelで受信メールの中身をあれこれする

Laravelで受信したメールをフックしてコマンド実行したり、メール文面をデータベースに保存したりする方法です。

元々はLaravelで作ったアプリケーションの定期更新が必要になったのですが、更新内容としてはテキストの追加のみでわざわざ管理画面を作り込む必要はなかったので、メールで追加できるようにシステムを組んでみました。

  • Laravelからメールが届く
  • テキストの文面を考えて返信
  • テキスト追加される

の流れが実現できます。

前提条件と方針

  • Postfixのエイリアス機能を使ってメール受信をトリガーにしてLaravelのコマンド実行
    • メールサーバーにLaravelプロジェクトを配置しておく必要がある
    • 今回はレンタルサーバーのエックスサーバーで実装
  • php-mime-mail-parser を使ってメール内容のパースを行う
    • peclの mailparse.so エクステンションを入れる必要がある
  • Laravelからメール送信、メール返信された内容を保存
    • Category.id をメールタイトルに記載して
    • 返信内容を Message.word として保存して Category.id と紐づける

php-mime-mail-parser を使用してメール内容のパースを行うのですが、エクステンションの導入が少し手間でした。

メールパースだけできればいいので、別のライブラリとして pear/mail もあるようです。今回は php-mime-mail-parser でシステムを組んでしまいましたが、こちらの方法も検討してもいいかもしれません。

参考:

php-mime-mail-parser のセットアップ

Composer Installする前にエクステンションを用意する必要があります。

参考: https://github.com/php-mime-mail-parser/php-mime-mail-parser#install-mailparse-extension

今回はエックスサーバーに入れるのでソースインストールします。PHP8.1.12向けに入れます。

$ cd
$ mkdir php_source
$ cd php_source/
$ wget https://pecl.php.net/get/mailparse-3.1.5.tgz
$ tar zxvf mailparse-3.1.5.tgz
$ cd mailparse-3.1.5
$ /opt/php-8.1.12/bin/phpize
$ ./configure  --with-php-config=/opt/php-8.1.12/bin/php-config
$ make

現在 (2023年8月17日) 最新の3.1.5を入れました。以下の公式リンクで最新のものを確認したり、実行するPHPバージョンによって適時差し替えてください。

https://pecl.php.net/package/mailparse

これでComposer Installを実行

/usr/bin/php8.1 \
-d extension=/home/<ユーザー名>/php_source/mailparse-3.1.5/modules/mailparse.so \
/usr/bin/composer install

エクステンションを導入できていれば以下のv8以上がインストールできます

        "php-mime-mail-parser/php-mime-mail-parser": "^8.0"

エクステンションが指定されていない場合にはv1が入るようです。

Postfixのエイリアス設定

root権限が使えるVPSなどは方法が違うのですが、エックスサーバーでは以下の設定を行います。

メールアカウントの追加を行い ( https://www.xserver.ne.jp/manual/man_mail_add.php )

.alias に以下の設定を追加します。

 $ cat <ドメイン名>/mail/<サブドメイン名>/<作成したメールアドレス>/.alias
cc "| cd /full/path/to/laravel && /usr/bin/php8.1 -d extension=/home/<ユーザー名>/php_source/mailparse-3.1.5/modules/mailparse.so artisan app:message-registration-email-import "

コマンドラインのパイプでメール内容がLaravelに渡るように設定します。

Laravelのコマンド作成

メール送信、メール受信のコマンドを作ります。

php artisan make:command MessageRegistrationReminder
php artisan make:command MessageRegistrationEmailImport
php artisan make:notification MessageRegistration
mkdir resources/views/emails/
touch resources/views/emails/nothing.blade.php // 空ファイルを作成
use App\Models\Category;
use App\Notifications\MessageRegistration;
use Illuminate\Support\Facades\Notification;

~~~

    public function handle()
    {
        $categories = Category::query()
            ->inRandomOrder()
            ->limit(3)
            ->get();
        foreach ($categories as $category) {
            Notification::route('mail', '<受け取るメールアドレス>')
                ->notify(new MessageRegistration($category));
        }
    }
use App\Models\Category;

~~~

    public function __construct(
        public Category $category,
    ) {
    }

    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage())
                    ->subject('[id:' . $this->category->id . ']' . $this->category->name)
                    ->view('emails.nothing');
    }
use App\Models\Category;
use Illuminate\Support\Facades\Log;

~~~

    public function handle()
    {
        $parser = new \PhpMimeMailParser\Parser();
        $parser->setStream(fopen('php://stdin', 'r'));

        $arrayHeaderTo = $parser->getAddresses('to');
        $subject = $parser->getHeader('subject');
        $text = $parser->getMessageBody('text');
        Log::info($subject);
        Log::info($text);

        // 件名に記載されたカテゴリーIDを抜き出し
        $matches = [];
        preg_match('/\[id:(\d+)\]/', $subject, $matches);
        if (!count($matches)) {
            return;
        }
        $id = $matches[1];

        $lines = [];
        foreach (explode("\n", $text) as $line) {
            if (false !== strpos($line, $arrayHeaderTo[0]['address'])) {
                // 返信時の引用部分は除外
                break;
            }
            $lines[] = $line;
        }
        $text = trim(implode("\n", $lines));

        Log::info($id);
        Log::info($text);

        $category = Category::find($id);
        $category->messages()->create([
            'word' => $text,
        ]);
        Log::info('create');
    }

以上で基本的な実装は完了しました。

メール送信を定期実行させる場合にはこんな感じで

    protected function schedule(Schedule $schedule): void
    {
        $schedule->command('app:message-registration-reminder')->daily();
    }

crontabに定期実行の設定を記載します

crontab -e
* * * * * cd /path/to/laravel && /usr/bin/php8.1 artisan schedule:run -q

送信はsendmailで .env に記載

MAIL_MAILER=sendmail
MAIL_FROM_ADDRESS="<作成したメールアドレス>"
MAIL_FROM_NAME="${APP_NAME}"

届くメールは以下の空メールが届きます。

件名: [id:<カテゴリーID>]<カテゴリー名>
本文: (空)

返信する事で Category.id 配下の Message.word で作成されます。

メールアドレスや件名が判明すると誰でも更新可能な状態になってしまうので、別途セキュリティ面の強化を行ったり、リスクが許容できる場合のみ実装してください。

終わりに

以上、Laravelでメール受信をフックにあれこれする方法でした。

紹介した実装内容を参考に、ご自分のプロジェクトに取り入れてみてください。

php-mime-mail-parser はメールの文面以外にも、添付ファイルの取得にも対応しているのでファイルアップロードにも対応できそうです。

https://github.com/php-mime-mail-parser/php-mime-mail-parser

お疲れ様でした

参考:

モバイルバージョンを終了