Build an email tracking service using Laravel, AWS SES & SNS services.
Tags: laravel, aws, tutorial, email
Published: 2020-10-18 05:41:00


**AWS setup:**

1. Go to the SNS dashboard and **create a new topic** of which we want to receive notifications. This topic will serve as a bridge between SNS and SES. For example, I have created an **email-notifications** topic.
2. Create a **subscription** for this topic where we will define our web app's public URL and process the notification data.
3. Select the **topic** we created. The protocol can be https. The endpoint will be our web app's public route. We want a notification in JSON format so no need to enable raw message delivery. As soon as we create the subscription SNS will send a notification for confirmation of the link. We have to visit a **subscription URL** for confirming the subscription after that we will start receiving the notification. The code for confirming the subscribe URL is done below in the Laravel setup. If we miss confirming the subscription we can request a reconfirm.
4. Set up the **SES configuration set** from the SES dashboard. Add a new configuration set, name it, and then edit it for defining the destination of email notifications like delivery, opened, etc. Select SNS from the add destination option, type the name, select the event types we want SES to send it to SNS, and the topic which we created in the first step. Now set up for SES and SNS is done.


**Laravel setup:**

1. To use the Amazon SES driver for sending mail we have to install the **Amazon AWS SDK** for PHP, by adding the **"aws/aws-sdk-php": "~3.0"** to your **composer.json** file's require section and running the **composer update** command.

2. Set the default option in your **config/mail.php** configuration file to SES and in **config/services.php** add key, secret, region and in options, array add configuration set name which we created in AWS setup.
```
'ses' => [
       'key' => env('AWS_ACCESS_KEY_ID'),
       'secret' => env('AWS_SECRET_ACCESS_KEY'),
       'region' => env('AWS_DEFAULT_REGION', 'ap-south-1'),
       'options' => [
           'ConfigurationSetName' => env('AWS_SES_CONFIG_SET', 'email-notifications'),
       ],
   ],
```
3. Whenever we send an email we will be getting a unique message-id through an **X-SES-message-ID** header which is added by AWS SES, we have to store this id in the database table for matching it with SNS notification **messageId**. So I have created an **Amazon Controller** and **sendEmail** method for sending the email and persisting the message-id and email details in the **sent_emails** table.  

4. Add a public route that we define as an endpoint for our SNS subscription in **web.php**. We will receive notifications as a post request. I have created an **emailNotifications** method in **Amazon controller** for handling this post request.
**Route::post(‘amazon-sns/email-notifications’, ‘AmazonController@emailNotifications’);**

5. As the data comes as a post request we have to tell our verify csrf token middleware to exclude this route. So in **VerifyCsrfToken.php** add **‘/amazon-sns/email-notifications’** in the except array.

6. We receive 2 types of notifications, a subscription confirmation which is for confirming the URL endpoint when we create a subscription for the first time from the SNS dashboard and the other one is just normal notification with SNS events.
So in the **emailNotifications** method, we take all the JSON from the request, then check the value of the **‘Type’** key if it is **‘SubscriptionConfirmation’** then we get the value of **‘SubscribeURL’** key and then using **file_get_contents()** method we visit the URL and confirm the subscription.
7. If the **‘Type’** key is **‘Notification’** then we take the value of **‘Message’** key and decode it to an array, then check the value of **‘eventType’** key which can be **‘Bounce’, ‘Open’, ‘Delivery’**, etc. Now we take the **‘Mail’** key whose value is an array and take the value of **‘messageId’** key from it and update the **sent_emails** table records according to the event by using this messageId and persisted messageId.

**JSON data for Subscription confirmation example:**

```
array (
 'Type' => 'SubscriptionConfirmation',
 'MessageId' => 'c9f90bc3-6b7a-4ab9-8156-5b27b17797f8',
 'Token' => '2336412f37fb687f5d51e6e2425f004aee4aa0c75a56575973dba0d4ca2fa9f2d12a4ee88495d74faebbdda39e034e91fc520e03aa05796e452ab5c12cc81a666e25fe99529b3975bb34a500d84d692dbcace3d64cc36273504da5b0464a954b7d48239c61198e2eb161f6cfcfa6304b566c685c407b9c65b7c07d7d2d11da32',
 'TopicArn' => 'arn:aws:sns:ap-south-1:499826649778:email-notifications',
'Message' => 'You have chosen to subscribe to the topic arn:aws:sns:ap-south-1:499826649778:email-notifications.
To confirm the subscription, visit the SubscribeURL included in this message.',
 'SubscribeURL' => 'https://sns.ap-south-1.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:ap-south-1:499826649778:email-notifications&Token=2336412f37fb687f5d51e6e2425f004aee4aa0c75a56575973dba0d4ca2fa9f2d12a4ee88495d74faebbdda39e034e91fc520e03aa05796e452ab5c12cc81a666e25fe99529b3975bb34a500d84d692dbcace3d64cc36273504da5b0464a954b7d48239c61198e2eb161f6cfcfa6304b566c685c407b9c65b7c07d7d2d11da32',
 'Timestamp' => '2020-07-21T11:26:03.334Z',
 'SignatureVersion' => '1',
 'Signature' => 'QVIAlzHwf4DkxvtwBpKp9SByF9Z9Yh88t46ksTUkYA3Vr3nqkaKgVDmIE6wjdLn/B3LlkDU27zl9R+O2Sn4X9CSw9TT7B221zFCciEm6lbrQpIAX+KJ81blvaanR45pQY1VH9ageUv+2fk8VJvz1VnXw1+0fNIGx7k67aQbI1OttagbRr9wUIay/bwGq8LlP+rTviU8qg+AWbJgfg0iLhyoYSw5VaVL4aFyu7v5tLACMpRfPRDfIMaejmCJQBRfxWLfSFmALgkWcTIcUcWPllrw1sFEqgP+PzXlFfSfEkCzkcmqdy8++XfzEjME9lHky3kcOqgcaWse8Fs4GTFEsiQ==',
 'SigningCertURL' => 'https://sns.ap-south-1.amazonaws.com/SimpleNotificationService-a86cb10b4e1f29c941702d737128f7b6.pem',
)
```
**JSON data for Notification of delivery event example:**
```
array (
 'Type' => 'Notification',
 'MessageId' => '81d5d58e-7838-5a2a-8fac-821ee35fc2e1',
 'TopicArn' => 'arn:aws:sns:ap-south-1:499826649778:email-notifications',
 'Subject' => 'Amazon SES Email Event Notification',
 'Message' => '{"eventType":"Delivery","mail":{"timestamp":"2020-07-21T07:28:36.625Z","source":"foo@gmail.com","sourceArn":"arn:aws:ses:ap-south-1:499826649778:identity/foo@gmail.com","sendingAccountId":"499826649778","messageId":"0109017370463711-e110c671-cfe4-48d2-9f16-a6ee18b6edad-000000","destination":["bar@gmail.com"],"headersTruncated":false,"headers":[{"name":"Message-ID","value":"<c48ec65d8e3ffcc42020b52f3c28ecb5@127.0.0.1>"},{"name":"Date","value":"Tue, 21 Jul 2020 07:28:36 +0000"},{"name":"Subject","value":"SES TEST"},{"name":"From","value":"Email_Tracker <foo@gmail.com>"},{"name":"To","value":"bar@gmail.com"},{"name":"MIME-Version","value":"1.0"},{"name":"Content-Type","value":"text/html; charset=utf-8"},{"name":"Content-Transfer-Encoding","value":"quoted-printable"}],"commonHeaders":{"from":["Email_Tracker <foo@gmail.com>"],"date":"Tue, 21 Jul 2020 07:28:36 +0000","to":["bar@gmail.com"],"messageId":"0109017370463711-e110c671-cfe4-48d2-9f16-a6ee18b6edad-000000","subject":"SES TEST"},"tags":{"ses:operation":["SendRawEmail"],"ses:configuration-set":["email-notifications"],"ses:source-ip":["103.88.223.122"],"ses:from-domain":["gmail.com"],"ses:caller-identity":["root"],"ses:outgoing-ip":["76.223.180.12"]}},"delivery":{"timestamp":"2020-07-21T07:28:39.465Z","processingTimeMillis":2840,"recipients":["bar@gmail.com"],"smtpResponse":"250 2.0.0 OK  1595316519 r188si12628852pfc.330 - gsmtp","reportingMTA":"c180-12.smtp-out.ap-south-1.amazonses.com"}}
',
 'Timestamp' => '2020-07-21T07:28:39.537Z',
 'SignatureVersion' => '1',
 'Signature' => 'I1OrPbUNXeFM681n6E9K0NlJovNUGTBOpk3OIylVAufwJQCeUF0/Syrn9RlUbDXcfe5z/HaI5T8SGB0Opnh53om7FU7++R5Nte2Zltp/MoC2DCVxGv+ycSlP4Zv4csftHLcrVu/XtXDGRA6JHystxeznY4iUnFY4IKz84d0l+eOAdTi/Z71qb3swMpzhGybLeNBhMUyBzv4FFlSbIYF2z5/gDCCPjDSIkvFE1nm4pl7Gk7YcU3Dyw8zlT05LbRMQRNR8jHzNhTzOXBLCA6Ej+mTZ+F2qWe+7dDRiSSOoVk2nO4NMo+9r0RmdslpRJzAaas/8e8i1uAwz9WskS/aX1A==',
 'SigningCertURL' => 'https://sns.ap-south-1.amazonaws.com/SimpleNotificationService-a86cb10b4e1f29c941702d737128f7b6.pem',
 'UnsubscribeURL' => 'https://sns.ap-south-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:ap-south-1:499826649778:email-notifications:c4f4e401-f0c6-4643-9fd4-8ba36fe34c8f',
)
```
In the above JSON, only ‘eventType’ changes according to the event that happens and the rest remains the same.

**Link to the source code:** [aws-ses-email-app](https://github.com/faisalshaikh8433/aws-ses-email-app)