AWS

Laravel6 AWS SES連携 + Bounce, Complaint対応 その①

AWS

 

 

私たちが達成すべきゴール

 

  • Laravel → SES → SNS → SQS → Laravel → DB

 

送信処理

  1. LaravelからSESを利用してメール送信する
    →正常にメール送信出来たらクローズ
  2. SESがバウンスメールを検知してSNSに発火
  3. SNSからSQSにバウンスメール情報であるJSONを登録

受信処理

  1. LaravelからSQSをポーリングしてキューが入っていないか確認しつづける
  2. LaravelからSQSにキューがあることを検知する
  3. データベースに処理したキューと同一のMessageIdがないか確認する
    ●データベースに同一のMessageIdが存在しない
    → データベースに登録してキューを削除
    ●データベースに同一のMessageIdが存在する
    →何も処理をせずにキューを削除
    // SQSの標準キューは同一のキューを複製して送信する場合があるのが仕様

 

バックナンバー

 

関連

AWS SESのアラーム

SESの設定

東京リージョンにSESはないのでバージニアで設定
https://console.aws.amazon.com/ses/home?region=us-east-1

 

  • Domain: <ドメイン>
  • Generate DKIM Settingsにチェック

Verify This Domainをクリックします。

 

 

  • 1番目のオレンジ枠:Domain Verification Record
  • 2番目のオレンジ枠: DKIM Record Set
    スクロールで他に複数あるので注意

 

 

Route53

 

アクセスする
https://console.aws.amazon.com/route53/home?region=us-east-1#hosted-zones:

 

【ホストゾーン】をクリックして、【ホストゾーンの作成】をクリックします。

 

 

SPF設定

 

 

SPFが登録された。

 

 

DKIM

DKIMも同じように登録していきます。

 

 

DKIMを3レコード追加しました。

SPFと合わせて設定できました。

 

バリュードメイン設定

ドメインの管理をRoute53で統一したいので設定します。

NSレコードをメモします。

 

バリュードメイン管理画面

設定します。

 

 

Laravel+SESに必要なライブラリをインストール

 

これらのライブラリをインストールしていることが前提です😆

aws-sdk-php

$ composer require aws/aws-sdk-php

 

guzzlehttp

$ composer require guzzlehttp/guzzle

 

laravel-sqs-fifo-queue

AWS SQSのFIFOを利用するのでlaravel-sqs-fifo-queueが必要

$ composer require shiftonelabs/laravel-sqs-fifo-queue

// 標準のQueueだと必要ないです。

  • 2回届いたりバグが怖いのでSQS FIFOで
  • FIFOは300/秒の性能
    それ以上の性能を求める場合は標準キュー採用を検討

 

 

SES操作用キーの取得

 

グループの作成

【グループ】をクリックして、【新しいグループの作成】をクリックします。

 

 

グループ名を”SES-Admin”に設定して、【次のステップ】をクリックします。

 

検索欄に”SES”を入力して、”AmazonSESFullAccess”にチェックをする。

【次のステップ】をクリックします。

 

 

 

 

ユーザの作成

 

【ユーザー】をクリックして、【ユーザーを追加】をクリックする。

 

  • ユーザ名:ses-admin
  • アクセスも種類:プログラムによるアクセス

【次のステップ:アクセス権限】をクリックします。

 

【ユーザをグループに追加】を選択し、”SES-Admin”にチェックを入れて、【次のステップ:タグ】をクリックします。

 

【次のステップ:確認】をクリックします。

 

【ユーザの作成】をクリックします。

 

アクセスキーIDとシークレットアクセスキーをメモしてください。

次に使いますよ!

 

 

コンフィグの設定

 

config/services.php

    'ses' => [
-       'key' => env('AWS_ACCESS_KEY_ID'),
-       'secret' => env('AWS_SECRET_ACCESS_KEY'),
-       'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
+       'key' => env('SES_KEY'),
+       'secret' => env('SES_SECRET'),
+       'region' => env('SES_REGION'),
    ],

上記に設定

 

.env

MAIL_DRIVER=ses
MAIL_FROM_ADDRESS=<認証されたメールアドレス>
MAIL_FROM_NAME=MyApp // 任意のアプリ名

SES_KEY=<アクセスキーid>
SES_SECRET=<シークレットアクセスキー>
SES_REGION=us-east-1

 

 

設定反映

$ php artisan config:clear
$ php artisan config:cache

 

送信先アドレス登録

 

 

 

認証リンクをクリックします。

 

検証が成功しました。

これでこのメールアドレスからメール送信ができるようになります。

 

テスト送信

 

 

認証された送信先メール宛に送信テストを行います。

 

 

Gmailで受信できたので、【メッセージのソースを表示】をクリックします。

 

SPFとDKIMを確認してPASSになってればOK。

 

Laravelからのメール送信

 

make:mailを利用します。

ActivationMailableクラスの作成

$ php artisan make:mail ActivationMailable --markdown=emails.activation.created

 

app/Mail.ActivationMailable.php

<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use App\Models\Activation;

class ActivationCreated extends Mailable
{
    use Queueable, SerializesModels;

    protected $activation;


    public function __construct(Activation $activation)
    {
        $this->activation = $activation;
    }

    /**
     * メール認証にて、メール本文に付与する確認用URL情報を設定
     *
     * @return $this
     */
    public function build()
    {
        $apiUrl = config('app.url');

        return $this->markdown('emails.activation.created')
        ->with([
                  'url' => $apiUrl."/users/me/verify?code={$this->activation->code}",
                  'user_name' => $this->activation->user_name
               ]);;
    }
}

 

 

@component('mail::message')

@if (!empty($user_name))
    {{ $user_name }} さん
@endif


** 以下の認証リンクをクリックしてください。 **
@component('mail::button', ['url' => $url])
メールアドレスを認証する
@endcomponent


@if (!empty($url))
###### 「ログインして本登録を完了する」ボタンをクリックできない場合は、下記のURLをコピーしてWebブラウザに貼り付けてください。
###### {{ $url }}
@endif

---

※もしこのメールに覚えが無い場合は破棄してください。

---


ご利用有難う御座います。<br>
{{ config('app.name') }}
@endcomponent

 

登録用コントローラ

 

app/Http/Api/V1_0/RegisterController.php

<?php

namespace App\Http\Controllers\Api\V1_0;

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V1_0\RegistUserRequest;
use App\Models\User;
use App\Models\Activation;
use Illuminate\Http\Request;
use Ramsey\Uuid\Uuid;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Mail\ActivationCreated;
use App\Models\UserRole;
use App\Models\Device;


class RegisterController extends Controller
{
    private $code;
    private $userGuestRoleId;
    private $now;
    private $activation;

    public function __construct()
    {
        $userRole = new UserRole();
        $this->userGuestRoleId = $userRole->getGuestRoleId();
        $this->now = Carbon::now()->format('Y-m-d H:i:s');
    }
    /**
     * 登録リクエストを受付
     *
     * @param App\Http\Requests\Api\V1_0\RegistUserRequest
     * @return void
     */
    public function register(RegistUserRequest $request): string
    {
        $this->createActivation($request);

        return response()->json([
            'message' => config('mail.message.send_verify_mail')
        ]);
    }

    /**
     * アクティベーションコードを生成して認証コードをメールで送信
     *
     * @param App\Http\Requests\Api\V1_0\RegistUserRequest
     * @return void
     */
    private function createActivation(RegistUserRequest $request): void
    {
        $activation = new Activation;
        $activation->user_name = $request->name;
        $activation->email = $request->email;
        $activation->password = bcrypt($request->password);
        $activation->udid = $request->udid;
        $activation->device_os = $request->device_os;
        $activation->device_token = $request->device_token;
        $activation->code = Uuid::uuid4();
        $activation->save();

        Mail::to($activation->email)->send(new ActivationCreated($activation));
    }

    /**
     * メール認証コードを検証してユーザ情報の登録
     *
     * @param Illuminate\Http\Request
     * @return string
     */
    public function verify(Request $request) :string
    {
        $this->code = $request->code;

        // 認証確認
        if (!$this->checkCode($this->code)) {

            // 認証確認エラー処理
            return response()->json(config('error.mailActivationError'));
        } else {
            // ユーザ情報, デバイス情報の登録
            try {
                $retries = (int)3; // トランザクションリトライ回数
                DB::beginTransaction(function() {}, $retries);

                $this->activation = Activation::where('code',$this->code)->first();
                $generalRoleId = $this->userGuestRoleId;
                $user = new User();
                $user->user_name = $this->activation->user_name;
                $user->email = $this->activation->email;
                $user->password = $this->activation->password;
                $user->user_role_id = $generalRoleId;
                $user->save();

                Activation::where('code', $this->code)->update(['email_verified_at' => Carbon::now()]);
                $user_id = $user->user_id;
                $udid = $this->activation->udid;
                $device_os = $this->activation->device_os;
                $device_token = $this->activation->device_token;
                Device::create([
                        'udid' => $udid,
                        'device_os' => $device_os,
                        'device_token' => $device_token
                ]);

                $user->devices()->attach(
                    ['user_id' => $user_id],
                    ['udid' => $udid],
                    ['created_at' => $this->now],
                    ['updated_at' => $this->now]
                );

                DB::commit();

                return response()->json(config('mail.message.add_user_success'));
            } catch (\Illuminate\Database\QueryException $e) {
                // トランザクションでのエラー処理
                DB::rollback();
                Log::error('WEB /users/me/verify - Class ' . get_class() . ' - PDOException Error. Rollback was executed.' . $e->getMessage());

                return response()->json(config('error.databaseTransactionRollback'));
            } catch (\Exception $e) {
                // その他のエラー処理
                DB::rollback();
                Log::error('WEB /users/me/verify - Class ' . get_class() . ' - something went wrong elsewhere.' . $e->getMessage());

                return response()->json(config('error.databaseSomethingWentWrongError'));
            }
        }
    }

    /**
     *  メール認証コードの検証
     *
     *  1. 与えられた認証コードがActivations.codeに存在するか?
     *  2. users.emailが存在しユーザ登録が既に完了したメールアドレスかどうか?
     *  3. 認証コード発行後1日以内に発行された認証コードであるか?
     *
     * @param string $code - メール認証のURLパラメータから取得する認証コード
     * @return boolean
     */
    private function checkCode($code): bool
    {
        $activation = Activation::where('code',$code)->first();
        if (!$activation) {
            return false;
        }

        $activation_email = $activation->email;
        $latest = Activation::where('email',$activation_email)->orderBy('created_at', 'desc')
                                                              ->first();
        $user = User::where('email',$activation_email)->first();
        $activation_created_at = Carbon::parse($activation->created_at);
        $expire_at = $activation_created_at->addDay(1);
        $now = Carbon::now();

        return $code === $latest->code && !$user && $now->lt($expire_at);
    }
}

 

ルーティング

 

routes/api.php

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/


Route::prefix('V1_0')->group(function () {

    Route::middleware([
        'jwt_check', // JWTトークン検証 - \App\Http\Middleware\CheckToken::class
        'jwt_auth',  // JWTトークンによる認証を強制 - \Tymon\JWTAuth\Http\Middleware\Authenticate::class
        ])->group(function(){
        Route::post('/refresh-token', 'Api\V1_0\RefreshTokenController@refreshToken');
    });

    Route::group([
        "middleware" => 'guest:api', // 認証不要なAPIとして設定
    ], function () {
+           Route::post('/users/me', 'Api\V1_0\RegisterController@register');
            Route::post('/login', 'Api\V1_0\LoginController@login');
            Route::post('/login/guest', 'Api\V1_0\GuestLoginController@login');
    });
});

 

メソッド:POST

  • http://localhost/api/V1_0/users/me

JSON

{
	"udid": "udid_string1",
	"device_os": "ios",
	"device_token": "device_token_string",
	"name": "yuu",
        "email": "<●認証されたメールアドレス>",
        "password": "password"
}

 

Gmailで確認

 

SES経由であることが確認できました😊

 

 

その②へ

Laravel6 AWS SES連携 + Bounce, Complaint対応 その②

 

Amazonおすすめ

iPad 9世代 2021年最新作

iPad 9世代出たから買い替え。安いぞ!🐱 初めてならiPad。Kindleを外で見るならiPad mini。ほとんどの人には通常のiPadをおすすめします><

Laravel6 AWS SES連携 + Bounce, Complaint対応 その①”への1件のコメント

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)