Easy way to track emails in Laravel 6,7,8 and 9 using AWS SES and SNS with Queue?

In this tutorial, we will see how to track emails in Laravel using AWS SES and SNS.

Introduction

In general, we attach some data to emails to track if the email was delivered or if the user opened it or the email failed (bounce).

We are going to use the SES and SNS to notify you whenever the above actions are performed.

Step 1 – Configure Access Key

In the .env file, update the below variables with your credentials,

AWS_ACCESS_KEY_ID='XYZ'
AWS_SECRET_ACCESS_KEY='**********'
AWS_DEFAULT_REGION='your-region'

If you don’t know how to create an AWS Access key in the AWS console, see here.

And then change default mailer to ses in env

MAIL_MAILER=ses

Step 2 Create SNS Topic and Subscription

Go to the SNS dashboard and create a topic with standard configuration.

After creating a topic, go to the subscription panel and select the topic you created. Then select the HTTP protocol and set your site URL as the endpoint.

How to track emails in Laravel

Step 3 Configuration Set

Create a configuration set in the AWS console and link the SNS topic you created.

After creating a configuration set in AWS, update it to your config/services.php file

'ses' => [
        'key' => env('AWS_ACCESS_KEY_ID'),
        'secret' => env('AWS_SECRET_ACCESS_KEY'),
        'region' => env('AWS_DEFAULT_REGION'),
        'options' => [
            'ConfigurationSetName' => env('AWS_SES_CONFIG_SET', 'Test'),
        ],
    ],

Step 4 – Verify Subscription – Routes

The most exclusive part is endpoint verification. You can only get notifications from AWS SNS after the subscription is verified. For that, you have to add the following code to your routes/api.php file.

Route::group([ 'middleware' => 'api' ], function ($router) {
    Route::post('sns', 'App\Http\Controllers\YourController@sns_notifications');
});

CSRF error – When the request comes from outside of the Laravel, it will throw an unauthenticated error like the CSRF token is missing.

You have to edit the except variable with the below code to exclude the SNS route from the CSRF Verification.

Middleware/VerifyCsrfToken.php

protected $except = [
   'api/sns'
];

Step 5 – Verify Subscription – Controller

Here is the main part, the SNS request will come to your sns_notifications function in your controller after setting up your route

Once you got the request, you have to verify the subscription by using the file_get_contents function.

public function sns_notifications(Request $request)
{
    $data = $request->json()->all();

    //Start verification
    if ($data['Type'] == 'SubscriptionConfirmation'){

        file_get_contents($data['SubscribeURL']);  // To verify. (When the first request comes from AWS SNS)
    }
    //End verification

    //Start tracking emails
    elseif ($data['Type'] == 'Notification'){
        // In this section, we will see how to track emails after a while.
    }
    //End tracking emails

    return response('OK', 200);
}

Step 6 – Sending Emails

public function send_emails(Request $request)
{
    $email_history = EmailHistory::create([
    'from_address'=>$request->from_address,
    'to_address' => $request->to_address,
    'subject'=>$request->subject,
    'body'=>$request->body,
    ]);
    Mail::to('no-reply@yoursite.com')->queue(new TrackingEmail($request->body, $request->subject, $email_history->id));
}

EmailHistory is just a model which can use a different name — up to you.

Here I’ve attached email_histories migration for your reference.

Schema::create('email_histories', function (Blueprint $table) {
    $table->id();

    $table->string('to_address')->nullable();
    $table->string('from_address')->nullable();
    $table->text('subject')->nullable();
    $table->longText('body')->nullable();

    $table->text('message_id')->nullable();
    $table->timestamp('opened')->nullable();
    $table->timestamp('delivered')->nullable();
    $table->timestamp('complaint')->nullable();
    $table->timestamp('bounced')->nullable();

    $table->timestamps();
});

Step 7 – Create a mailable – TrackingEmail

php artisan make:mailable TrackingEmail
<?php

namespace App\Mail;

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

class TrackingEmail extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * Create a new message instance.
     *
     * @return void
     */

    protected $email_body;
    protected $email_subject;
    protected $email_history_id;

    public function __construct($email_body, $email_subject, $email_history_id)
    {
        $this->email_body = $email_body;
        $this->email_subject = $email_subject;
        $this->email_history_id = $email_history_id;
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->view('mails.tracking')
        ->subject($this->email_subject)
        ->with([
            'email_body' => $this->email_body,
        ])
        ->withSwiftMessage(function ($message) {
                $message->getHeaders()->addTextHeader(
                    'Custom-Message-ID', $this->email_history_id  //Custom-Message-ID is the tracking id
            );
        });
    }
}

Step 8 – Tracking Emails – Controller

We have come to the last part. Let’s finish the unfinished function. – sns_notifications()

public function sns_notifications(Request $request)
{
    $data = $request->json()->all();

    //Start verification
    if ($data['Type'] == 'SubscriptionConfirmation'){

        file_get_contents($data['SubscribeURL']);  // To verify. (When the first request comes from AWS SNS)
    }
    //End verification

    //Start tracking emails
    elseif ($data['Type'] == 'Notification'){

        $message = json_decode($data['Message'], true);

        $message_id = null;
        foreach($message['mail']['headers'] as $key => $newV){
            if($newV['name'] == 'Custom-Message-ID'){  // This is a tracking variable that is attached when sending emails.
                $message_id = $newV['value'];
            }
        }

        try {
            switch($message['eventType']){
                case 'Bounce':
                    $bounce = $message['bounce'];
                    $email = EmailHistory::where('id', $message_id)->first();
                    $email->bounced = Carbon::now()->toDateTimeString();
                    $email->save();
                break;
                case 'Complaint':
                    $complaint = $message['complaint'];
                    $email = EmailHistory::where('id', $message_id)->first();
                    $email->complaint = Carbon::now()->toDateTimeString();
                    $email->save();
                break;
                case 'Open':
                    $open = $message['open'];
                    $email = EmailHistory::where('id', $message_id)->first();
                    $email->opened = Carbon::now()->toDateTimeString();
                    $email->save();
                break;
                case 'Delivery':
                    $delivery = $message['delivery'];
                    $email = EmailHistory::where('id', $message_id)->first();
                    $email->delivered = Carbon::now()->toDateTimeString();
                    $email->save();
                break;
                default:
                // Do Nothing
                break;
            }
        }catch (\Exception $e) {
            Log::info($e->getMessage());
        }
    }
    //End tracking emails

    return response('OK', 200);
}

Conclusion

I hope it helped you with how to track emails in Laravel using AWS SES and SNS along with the Queue process. If you have any doubts, feel free to ask in the comments section down below.

Cheers!