ダウンロードできる!Laravelでメールが開封されたか確かめる機能をつくる

さてさて、ウェブ開発を長らく続けていると様々なクライアントさんたちのご希望に触れる機会があるわけですが、今回の記事はそんな中からセキュリティに関わるものをチョイスしました。

その内容とは、「送信されたメールが開封されたかどうかをチェックする」機能です。

あまり詳しい内容は書けませんが、セキュリティの訓練のために以下のようなシステムが必要なことがありました。

  1. 自社の社員全員に訓練用のメールを送信する
  2. もしそのメールが開封されたら、それが誰かを保存する
  3. 後からその人に、実は訓練メールだったことを伝え、今後メールの開封は気をつけてもらう

つまり、企業が重大なハッキングをされてしまう場合、原因はたった1通のメールだった、というケースが結構あるため(例のコインチェック事件の場合もそうだと言われています)、その対策の一貫として社員の訓練を実施したいとのことでした。

ということで、今回はメールが開封されたかどうかをチェックする方法をお届けします。

※ 開発環境: Laravel 5.7

メール開封をチェックする仕組み

まずは、メール開封されたかどうかをチェックする仕組みですが、例えばあなたは以前にこんなメールを受け取ったことはありませんか??

何の変哲もないメールですが、通常のテキスト・メールとは違って色々と装飾が施されています。

これはメールをより綺麗に見せることができるHTMLメールと呼ばれるものですが、実はメールが開封されたかどうかは、このメール上部にある画像(上の例ではロゴ画像)を使って実現することができます。

つまり、流れはこうなります。

  1. メールが開かれると同時に「個人ID」がついた画像URLへアクセス
  2. 画像データを表示する時に個人IDを保存
  3. 保存した個人IDを元に誰がメールを開いたかが分かる

という仕組みですね。

※ もちろん、利用しているメールによっては画像を表示していない/できないことがあり、その場合この方法は利用できませんが、大手のGMailでも初期設定で画像が表示されるようになっているので、利用できるケースは多いでしょう。

※ ちなみに、今回のサンプルではロゴ画像が表示されていますが悪意のあるメールの場合は、1×1のほぼ目では認識できないごく小さなな画像を用意し、通常のテキストメールのように偽装していることもあります。お気をつけください。

個人IDが保存できる画像の作り方

サンプル画像を準備する

サンプルとして、以下の笑顔マークを使って開発をしていきましょう。(ちなみにfa2pngというサイトで作りました)

この画像をstorage/app/images/フォルダ内に保存します。

メールを送信する部分をつくる

テストユーザーを作る

今回はテストなので、以下の3人のユーザーにメールを送信して、この中から誰がメールを開封したかをチェックしてみましょう。

  • 太郎くん
  • 次郎くん
  • 花子さん

※ すでにユーザー機能のインストールが完了しているものとします。もしまだの場合は【Laravel5.6】インストール直後にやること3点を参照してください。

では、以下のコマンドでusersテーブル用のSeederを作ります。

php artisan make:seed UsersTableSeeder
public function run()
{
    $names = [
        'taro' => '太郎',
        'jiro' => '次郎',
        'hanako' => '花子'
    ];

    foreach ($names as $name_en => $name_jp) {

        \App\User::create([
            'name' => $name_jp,
            'email' => $name_en .'@example.com',
            'password' => bcrypt('xxxxxxxx')
        ]);

    }
}

UsersTableSeeder.phpの変更が完了したら、DatabaseSeeder.phpに登録してコマンドを実行します。

public function run()
{
     $this->call(UsersTableSeeder::class);
}
php artisan migrate:fresh --seed

これで以下のように、テストユーザーの準備が完了しました。

メール本体を作る

では、開封チェックメール本体を作成する部分です。

まずメール送信に使うMailableを以下のコマンドで作成します。

php artisan make:mail CheckMail

そして、作成されたapp/Mail/CheckMail.phpを開いて以下のような記述をします。

<?php

namespace App\Mail;

use App\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class CheckMail extends Mailable
{
    use Queueable, SerializesModels;

    public $user;

    public function __construct(User $user)
    {
        $this->user = $user;
    }

    public function build()
    {
        return $this->view('mail.check_mail')
            ->with('user', $this->user);
    }
}

そして、HTMLメールのテンプレートmail.check_mailがまだ存在していないので、resources/views/mail/check_mail.blade.phpを作って、以下ような内容を保存します。

こんにちは、{{ $user->name }}!<br>
テストメールです。<br><br>
<img src="{{ url('images/smile.png') }}?user_id={{ $user->id }}">

ここで重要なのが、user_id=の部分です。
なぜなら、後でこのIDを使って誰がメールを開封したかが分かるからです。

メール送信する部分をつくる

では続いてメール送信する部分です。

まずroutes/web.phpに以下のルーティングを追加し、コマンドを使ってコントローラーを作成します。

Route::get('send_check_mail', 'HomeController@send_check_mail');
php artisan make:controller HomeController

そして、HomeController内に以下のようなコードを追加すれば完了です。

public function send_check_mail() {

    $users = \App\User::get();

    foreach ($users as $user) {

        // メール送信
        \Mail::to($user)->send(new CheckMail($user));

    }

}

これで、https://******/send_check_mailにアクセスすると開封チェックメールが送信されることになります。(ただし、まだ画像の表示部分ができていないので、メールは後で送信することにしましょう)

画像を表示する部分をつくる

画像ライブラリをインストールする

今回は画像を扱うので、まずは有名な画像ライブラリ Intervention/image をインストールしておきます。

以下のcomposerコマンドで一気にインストールできます。

composer require intervention/image

※ Laravel 5.5以上ならPackage Auto-Discoveryがあるので自動的にエイリアスなどを設定してくれますが、それより前のものならconfig/app.phpへ以下を登録してください。

// providers
Intervention\Image\ImageServiceProvider::class
// aliases
'Image' => Intervention\Image\Facades\Image::class

ルーティングをつくる

では次にHTMLで表示する画像のルーティングをつくります。
ファイルは、routes/web.phpです。

Route::get('images/smile.png', 'ImageController@smile'); // 画像に見せかけるため「.png」をつける

コントローラーをつくる

まだImageControllerは存在していないので、以下のコマンドで作成します。

php artisan make:controller ImageController

そして、作成したapp/Http/Controllers/ImageController.phpsmile()を追加し、ここに画像データの準備と、個人IDを保存するコードを記述することになります。

画像をレスポンスとして返す

画像の表示はとても簡単で、以下のようにinterventionmake()に画像のパスを指定してインスタンス化し、response()メソッドを返すだけです。

public function smile() {

    $path = storage_path('app/images/smile.png');
    $image = \Image::make($path);
    return $image->response();

}

これでhttps://*****/images/smile.pngにアクセスすると以下の画像が表示できるようになります。

個人IDを保存する

次に個人IDを保存する部分です。

まずはメールを開封したユーザー(のID)を保存するテーブルemail_opened_usersを作っておきましょう。

php artisan make:model EmailOpenedUser -m

これで、モデルに加えてマイグレーションも作成されるので、****_**_**_******_create_email_opened_users_table.phpを開いて以下のように変更します。

public function up()
{
    Schema::create('email_opened_users', function (Blueprint $table) {
        $table->increments('id');
        $table->unsignedInteger('user_id')->references('id')->on('users');
        $table->timestamps();
    });
}

では次に、このテーブルへメールを開封したユーザーIDを保存する方法です。と、言ってもメールを開封した時点でGETパラメータのuser_idにIDが入っているので、Requestから取得して保存するだけでOKです。

class ImageController extends Controller
{
    public function smile(Request $request) {

        $email_opened_user = new \App\EmailOpenedUser();
        $email_opened_user->user_id = $request->user_id;
        $email_opened_user->save();

        // 省略
    }
}

※ ちなみにuser_idなどと書いてしまうと、ユーザー側に発見されたとき不安になってしまうので、実際には事前にq1T9PKF6IMQRHilEというようなユニークなIDをユーザーに振っておいて、これをキーにして個人を特定する場合が多いようです。

※ また、パラメータが入っていること自体に不信感をもつユーザーもいるためか、中にはhttps://q1T9PKF6IMQRHilE.example.comのようにサブドメインの中にキーを埋め込むサイトもあるようです。その場合はapachenginxなどのウェブサーバーとの連携が必要になってきます。

テストメールを送信してみる

では、すべての準備が整いました。

http://******/send_check_mailへアクセスして、送信されたメール内の画像が表示されるか、また、メール開封したユーザーIDが保存されているかをチェックしてみましょう。

※ ちなみにメールのテスト送信には擬似的にローカルに送信できるmailcatcherがおすすめです。

はい!実行したところ、きちんと3人にメール送信されていたので、太郎くんのメールを開いてみました。うまく画像も表示されています。

では、開封者テーブルはどうなっているでしょうか?

太郎さんのIDは「1」なので、こちらもうまくいっています。
では、続いて花子さんのメールも開いてみましょう。

こちらもうまく画像まで表示されました。

テーブルにも花子さんのID「3」が追加されています。

これですべて完了です!

ソースコードをダウンロード

今回作成したコード一式を以下からダウンロードすることができます。

※ ただし、ファイルを上書きする場合は気をつけて行ってください。

Laravelでメールが開封されたか確かめる機能・ソースコード一式