Laravel + Amazon SES でクリック率が分かるメルマガを一斉送信する

こんにちは❗フリーランス・エンジニアの 九保すこひ です。

さてさて、「AWSへの出遅れ感」を取り戻すために主要サービスを体験しているのですが、今回はユーザー登録が必要なサイトに有効な機能をお届けします。

それは・・・・・・

メールの一括送信機能 📪

です。

つまり、オンラインショップで言うと「メルマガ」の送信ということになりますが、実はシンプルに見えていろいろ難しかったりします。

なぜなら、ユーザー全員にメール送信しないといけませんし、戦略的にメルマガを使う場合、各メールで内容を変えないといけない(例:名前やクリック率を知るために個別IDを含める)からです。

もちろん、1件ずつループして送信してもいいのですが、途中で止まってしまったり、キューを使うにしてもいろいろと設定をしないといけません。

そこで登場するのが、「Amazon Simple Email Service(Amazon SES)」です。

Amazon SESを使うとメールの一括送信&パーソナライズ化が簡単に実装できます。

そこで❗

今回はLaravel + Amazon SESで「クリック率が分かるメルマガ送信機能」を作ってみたいと思います。

ぜひ何かの参考になりましたら嬉しいです😊✨

「初めてOFUSEから
募金のご支援をいただきました❗
ホント励みになります。m(_ _)m」

開発環境: Laravel 8.x

やりたいこと

今回Amazon SESを使って開発する内容は次のとおりです。

  • ユーザー全員に一括送信
  • メールのリンクにはユーザー名を入れる
  • リンクには個別IDをいれておき、クリック率の計算に使う

ではやっていきましょう❗

前提として

ログイン機能がインストールされ、さらにテストユーザーがusersテーブルに登録されていることが前提です。

もしまだの方は以下を参考にしてみてください。

📝 参考ページ

Amazon SESの設定

SESを使うためには、ドメインが自分のものであることを証明する必要があります。

そのため、まずはドメインを登録して認証をさせましょう。

では、AWSにログインしてAmazon Simple Email Service(SES)へ移動します。

ページ移動するとページ左側のメニューから「Domains」リンクをクリック。

さらに、表示されたページで「Verify a New Domain」ボタンをクリック。

すると、ポップアップが表示されるので、認証させたいドメイン名と「Generate DKIM Settings」にチェックを入れて「Verify This Domain」ボタンをクリックします。

クリックすると、ドメインのDNSに設定すべき以下の情報が表示されます。

  • TXT: 1件だけ
  • CNAME: 表示エリアをスクロールすると全部で3件あります
  • MX: 1件だけ

これらの情報を全て控えておいてください。

DNSの設定

続いて、取得した情報をDNSに設定します。

ただ、設定方法は各ドメインサービスで違ってきますので、Q&Aページなどを参照して適宜設定を完了させてください。

ちなみに私の環境では以下のようになりました。

txt _amazonses.******** ******************************=
cname 1111111111111111111111111111._domainkey 1111111111111111111111111111.dkim.amazonses.com.
cname 2222222222222222222222222222._domainkey 2222222222222222222222222222.dkim.amazonses.com.
cname 3333333333333333333333333333._domainkey 3333333333333333333333333333.dkim.amazonses.com.
mx inbound-smtp.us-east-1.amazonaws.com. 10

※ 連続数字、アスタリスクは伏せ字として使っています。
※ ここのトラップは、「cname」と「mx」のドメインの最後に「.」が必要という部分です。逆に「txt」は不要です。
※ mx 部分は結局別のメールサーバーで使うことになりましたので、認証には不要のようです。

設定が完了し、認証が完了すると以下のように表示が変更になります。(私の場合は10分程度でOKでした)

これで、ドメインが認証されました。

アクセス情報(IAM)を取得する

続いて、LaravelからAmazon SESにアクセスできるよう「アクセスキー ID」と「シークレットアクセスキー」を取得し、.envへ書き込んでおきます。

以下のページを参考にしてください。

📝 参考ページ: アクセス情報(IAM)を取得する

なお、必要な権限は「AmazonSESFullAccess」です。

では、次からLaravel側の作業になります👍

パッケージをインストールする

LaravelからAWSにアクセスするためのパッケージをインストールしておきましょう。

以下のコマンドを実行してください。

composer require aws/aws-sdk-php

データベースまわりをつくる

続いて、データベースまわりのマイグレーション&モデルをつくっていきます。
必要なテーブルは次の2つです。

  • newsletters: メルマガを管理するテーブル
  • newsletter_clicks: メルマガのリンクをクリックした履歴

マイグレーション&モデルをつくる

では、以下のコマンドを実行してください。

php artisan make:model Newsletter -m
php artisan make:model NewsletterClick -m

すると、ファイルが作成されるのでマイグレーションの中身を以下のように変更してください。

database/migrations/****_**_**_******_create_newsletters_table.php

// 省略

Schema::create('newsletters', function (Blueprint $table) {
    $table->id();
    $table->string('subject')->comment('件名');
    $table->string('html_view')->comment('HTMLビュー');
    $table->string('text_view')->comment('テキストビュー');
    $table->unsignedInteger('newsletter_total')->default(0)->comment('送信したメルマガ数');
    $table->timestamps();
});

database/migrations/****_**_**_******_create_newsletter_clicks_table.php

// 省略

Schema::create('newsletter_clicks', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('newsletter_id')->comment('メルマガID');
    $table->unsignedBigInteger('user_id')->comment('ユーザーID');
    $table->timestamps();

    $table->foreign('newsletter_id')->references('id')->on('newsletters');
    $table->foreign('user_id')->references('id')->on('users');
});

続いて、モデルも中身を以下のように変更してください。

app/Models/Newsletter.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Newsletter extends Model
{
    use HasFactory;

    protected $guarded = ['id'];
    protected $appends = ['click_rate'];

    // Relationship
    public function clicks() {

        return $this->hasMany(NewsletterClick::class, 'newsletter_id', 'id');

    }

    // Accessor
    public function getClickRateAttribute() { // クリック率

        if($this->newsletter_total > 0 && $this->clicks->isNotEmpty()) {

            return round($this->clicks->count() / $this->newsletter_total * 100, 2); // パーセント(小数点2位まで)

        }

        return 0;

    }
}

app/Models/NewsletterClick.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class NewsletterClick extends Model
{
    use HasFactory;

    protected $guarded = ['id'];
}

では、この状態でテーブルを再構築しておきましょう。
以下のコマンドを実行してください。

php artisan migrate:fresh --seed

メール内容(ビュー)をつくる

では、メルマガとして送信する内容として「HTMLメール用」と「テキストメール用」の2つをつくっていきます。

(HTML用)

resources/views/emails/html/founding_festival_2021.blade.php

@{{ name }} さん<br><br>

創業祭キャンペーンの6月6日(日)はポイントが通常の3倍!<br><br>

ぜひお見逃しなく!<br>
↓↓↓<br><br>

<a href="{{ url('/newsletter/clicked') }}/@{{ newsletter_id }}/@{{ user_id }}">創業祭ページはこちら</a><br><br>

-----<br>
いつも心に自由を!<br>
九保すこひ・テスト 商店

(テキスト用)

resources/views/emails/text/founding_festival_2021.blade.php

@{{ name }} さん

創業祭キャンペーンの6月6日(日)はポイントが通常の3倍!

ぜひお見逃しなく!
↓↓↓

{{ url('/newsletter/clicked') }}/@{{ newsletter_id }}/@{{ user_id }}

-----
いつも心に自由を!
九保すこひ・テスト 商店

なお、ここで重要なのが、@{{******}}の部分です。

これは、Amazon SESがメール送信ごとに書き換える部分が例えば、{{ name }}なのですが、Laravelのテンプレートも同じフォーマットを使って置き換えをすることになっているため、わざと@をつけて波カッコをそのまま表示するようにしています。

⚠ ご注意
メールに含めるURLは、誰がいたずら目的でURLを変更してアクセスしてくるかわからないので、本番環境ではUUIDなど一意のデータも含めておくほうがいいでしょう。

メルマガを送信する部分をつくる

では、ここからが本題のAmazon SESでメール送信する部分になります。

なお、メルマガ部分はブラウザから登録・変更・削除するべきですが、メインから外れるので、今回は以下の流れで実装します。

  1. メルマガのデータをDBに登録
  2. そのまま Amazon SESでメール送信
  3. クリックされたらクリック率を表示

コントローラーをつくる

ではLaravelにコントローラーをつくっていきます。
以下のコマンドを実行してください。

php artisan make:controller NewsletterController

するとファイルが作成されるので、中身を以下のように変更してください。

app/Http/Controllers/NewsletterController.php

<?php

namespace App\Http\Controllers;

use App\Models\Newsletter;
use App\Models\NewsletterClick;
use App\Models\User;
use Aws\Ses\SesClient;
use Illuminate\Http\Request;

class NewsletterController extends Controller
{
    public function clicked(Newsletter $newsletter, User $user) {

        $click = NewsletterClick::firstOrNew([
            'newsletter_id' => $newsletter->id,
            'user_id' => $user->id
        ]);
        $click->newsletter_id = $newsletter->id;
        $click->user_id = $user->id;
        $click->save();

        return 'クリック率: '. $newsletter->click_rate .'%'; // クリック率を表示

    }

    public function send() {

        $users = User::get(); // メルマガを送信するユーザー
        $newsletter = Newsletter::create([
            'subject' => '創業祭キャンペーン 2021 のお知らせ',
            'html_view' => 'emails.html.founding_festival_2021',
            'text_view' => 'emails.text.founding_festival_2021',
        ]);
        $template_name = 'newsletter_'. $newsletter->id;

        $client = new SesClient([
            'version' => '2010-12-01',
            'region'  => 'ap-northeast-1'
        ]);

        try {

            // テンプレートの登録
            $html_part = view($newsletter->html_view)->render();
            $text_part = view($newsletter->text_view)->render();
            $client->createTemplate([
                'Template' => [
                    'HtmlPart' => $html_part,
                    'TextPart' => $text_part,
                    'SubjectPart' => $newsletter->subject,
                    'TemplateName' => $template_name,
                ],
            ]);

            // Amazon SES でメール送信
            $destinations = $this->getDestinations($newsletter, $users);
            $result = $client->sendBulkTemplatedEmail([
                'Destinations' => $destinations,
                'Source' => '(送信元のメールアドレス)',
                'Template' => $template_name,
                'DefaultTemplateData' => $this->templateJsonData()
            ]);

            // 送信件数を更新
            $newsletter->newsletter_total = $users->count();
            $newsletter->save();

        } catch (\Exception $e) {}

        // テンプレート削除
        $client->deleteTemplate([
            'TemplateName' => $template_name
        ]);

        dd($result);

    }

    private function getDestinations($newsletter, $users) { // 送信データの準備

        $destinations = [];

        foreach ($users as $user) {

            $destinations[] = [
                'Destination' => [
                    'ToAddresses' => [$user->email]
                ],
                'ReplacementTemplateData' => $this->templateJsonData($newsletter, $user)
            ];

        }

        return $destinations;

    }

    private function templateJsonData($newsletter = null, $user = null) {

        return json_encode([
            'name' => $user->name ?? 'テスト太郎',
            'newsletter_id' => $newsletter->id ?? '-1',
            'user_id' => $user->id ?? '-1'
        ]);

    }
}

なお、この中で私がハマってしまったのは、「DefaultTemplateData」の部分です。毎回エラーになるのでなぜかと思っていたのですが、メール本文を置き換えるデータを用意した場合はこの項目も必要なようでした…💦。

また、今回は以下3箇所を個別に置き換えるようにしてますが、これは毎回同じではないと思いますので、abstract class から継承した独自クラスを使って送信するとより汎用的に使えるかもしれません👍

  • name
  • newsletter_id
  • user_id

ちなみに: サンドボックスについて

Amazon SESは初期状態では「サンドボックス」というモードになっていて、外部へメール送信できないようになっています。

これを解決する方法は、以下2つです。

  • サンドボックスを解除する
  • 送信先のメールアドレスを認証する

今回はテストですので、送信先のメールアドレスをひとつだけ認証して実行することにします。

まずページ左のメニューから「Email Address」リンクをクリックし、「Verify a New Email Address」ボタンをクリックします。

フォームがポップアップするので、許可したいメールアドレスを入力し、「Verify This Email Address」ボタンをクリックします。

すると、入力したメールアドレスに認証メールが届きますので中に書かれている認証リンクをクリックしてください。

これでメールアドレスが認証されます。

なお、今回テストはこの状態でやってみることにしますが、本番環境の場合はサンドボックスの解除をしてください。

おまけ: テンプレートを全削除するには

今回コードを書いていてひとつ「うーん…」となったのが、テンプレートの削除についてです。

2021.05.28現在、AWSのログインページからテンプレートを削除することができませんでした。

とはいえ、テストするたびにテンプレートは追加されていくので、どんどんいらないテンプレートが溜まっていくことになります。

そこで、おまけとしてテンプレートを全削除するコードもご紹介することにしました。ぜひ役立ててください。m(_ _)m

// ⚠ 注意: 全てのテンプレートが削除されます

$client = new SesClient([
    'version' => '2010-12-01',
    'region'  => 'ap-northeast-1'
]);
$templates = $client->listTemplates()->get('TemplatesMetadata');

foreach ($templates as $template) {

    $client->deleteTemplate([
        'TemplateName' => $template['Name']
    ]);

}

テストしてみる

ちなみに: サンドボックスについて」でも書いたとおり、今回は1件だけ送信が許可されたメールアドレスを用意し実行してみます。(つまり、それ以外は実際には送信されることはありません)

※ なお、usersテーブルの1件目のメールアドレスもこの許可されたメールアドレスへ変更して実行します。

では、「https://******/newsletter/send」へアクセスしてみましょう。

どうなるでしょうか・・・・・

はい❗
想定どおり最初のメール送信は成功し、それ以外のものは拒否されています。

では、送信されてきたメールをチェックしてみましょう。

はい❗
ユーザーの名前など送信先によって置き換えたい内容もうまくいっているようです👍

では、リンクをクリックしてみましょう。

すると・・・・・・

はい❗❗

結果、クリック率が計算され表示されました。(実際にはここからキャンペーン・ページへ自動リダイレクトするといいでしょう)

では、最後にnewsletter_clicksも確認しておきます。

うまく保存されています。
全て成功です✨😄👍

企業様へのご提案

今回の機能を使えば、以下のようなメリットがあります。

  • クリック率を計算できるので、より成果につながりやすいメルマガがどうような内容なのかを検証することができる
  • Amazon のクラウドサービスからメール送信するので、メインサイトへの負荷はかからず、より早く、より確実に実行することが可能
  • メルマガだけに限らず、従業員さんたちの誕生日にメールでギフトを送るなど福利厚生としても使える
  • ユーザー主体のメール送信なので、「ここ3ヵ月ログインしていないユーザー」や「ここ半年購入履歴がないユーザー」などの「その後いかがでしょうかメール」を送信することもできる

ぜひこういった内容が必要な場合、お問い合わせからご連絡ください。m(_ _)m

なお、Amazon SESの料金は「メール送信1,000件につき 0.10USD(約11円)」となっており、安価に利用することができます。(2021.05.28現在)

📝参考ページ: Amazon SES 料金

おわりに

ということで、今回はAmazon SESを使ってメルマガ機能をつくってみました。

使ってみた感想としては、「認証関連がいろいろとめんどう」というイメージでしたが、最初に一回設定するだけでいいので使い続ける場合は問題ないかもしれません。

また、実際に送信するとき、もう少し時間がかかるかと思いましたが予想よりスンナリ処理が完了しましたし、実際のメールも1分も経たずに到着してくれました。

おそらく送信するメールが多ければ少し時間がかかるのかもしれませんが、さすがAWSといったところです。

ぜひ皆さんも試してみてくださいね。

ではでは〜❗

「ハト標識を見に行く
予定をたてています👍」

開発のご依頼お待ちしております 😊✨ お問い合わせ
また、こちらもお待ちしております。
  • 実案件の開発サポート: 詳細
  • ツイッターのフォロー: 詳細
どうぞよろしくお願いいたします!
このエントリーをはてなブックマークに追加       follow us in feedly