GithubHelp home page GithubHelp logo

juhasev / laravel-ses Goto Github PK

View Code? Open in Web Editor NEW

This project forked from ope-tech/laravel-ses

16.0 3.0 4.0 564 KB

A Laravel package that enables detailed tracking of emails sent via AWS SES. Supports tracking deliveries, opens, rejects, bounces, complaints and link clicks

PHP 99.91% Blade 0.09%

laravel-ses's Introduction

alt text

Laravel SES (Simple Email Service AWS)

Laravel SES is package that allows you to get sending statistics for emails you send through AWS SES (Simple Email Service), including deliveries, opens, bounces, complaints and link tracking. This package was originally written by Oliveready7. Unfortunately the original author had stopped maintaining this package so I decided to create this fork so that this package can be used with current versions of Laravel. The minimum requirement is PHP 7.3, Laravel 9 requires PHP 8.x.

All packages have been updated to modern versions. I have optimized the original database storage for space and proper indexing. This package is compatible with Laravel 9.x.

Laravel SES also supports SMTP errors codes will throw meaning exceptions like when you exceed your rate limits so you can handle proper back off.

Laravel version support:

  • If you are using Laravel 11 use v5.*
  • If you are using Laravel 10 use v4.*
  • If you are using Laravel 9 use v3.*
  • If you are using Laravel 7 or 8 use v1.1.5
  • If you are using Laravel 6 use v0.8.4

Installation

Install via composer

composer require juhasev/laravel-ses
composer require aws/aws-php-sns-message-validator (optional)

In config/app.php make sure you load up the service provider. This should happen automatically.

Juhasev\LaravelSes\LaravelSesServiceProvider::class

Laravel configuration

Make sure your app/config/services.php has SES values set

'ses' => [
    'key' => your_ses_key,
    'secret' => your_ses_secret,
    'domain' => your_ses_domain,
    'region' => your_ses_region
],

Make sure your mail driver located in app/config/mail.php is set to 'ses'

    'default' => env('MAIL_MAILER', 'ses')

Publish public assets

php artisan vendor:publish --tag=ses-assets --force

Publish migrations

php artisan vendor:publish --tag=ses-migrations --force

Publish the package's config (laravelses.php)

php artisan vendor:publish --tag=ses-config

Routes

This package add 3 public routes to your application that AWS SNS callbacks target

/ses/notification/bounce
/ses/notification/complaint
/ses/notification/delivery

We also add two more public routes for tracking opens and link clicks

/ses/beacon
/ses/link

Config Options

  • aws_sns_validator - whether the package uses AWS's SNS validator for inbound SNS requests. Default = false
  • debug - Debug mode that logs all SNS call back requests

https://github.com/aws/aws-php-sns-message-validator

AWS Configuration

Pre-reading

If you are new to using SES Notification this article is a good starting point

https://docs.aws.amazon.com/sns/latest/dg/sns-http-https-endpoint-as-subscriber.html

IAM User and policies

Your application IAM user needs to be send email via SES and subscribe to SNS notifications. This can be done in the AWS Control Panel as the article above suggests or AWS CloudFormation template like one below:

AWS CloudFormation policy example:

  ApplicationSNSPolicy:
    Type: "AWS::IAM::ManagedPolicy"
    Properties:
      Description: "Policy for sending subscribing to SNS bounce notifications"
      Path: "/"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action:
              - sns:CreateTopic
              - sns:DeleteTopic
              - sns:Subscribe
              - sns:Unsubscribe
            Resource:
              - 'arn:aws:sns:*'

  ApplicationSESPolicy:
    Type: "AWS::IAM::ManagedPolicy"
    Properties:
      Description: "Policy for creating SES bounce notification"
      Path: "/"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action:
              - ses:*
            Resource:
              - '*'

Once policies are defined they need to added to the configured IAM user.

  # AWS PHP API User
  APIUser:
    Type: "AWS::IAM::User"
    Properties:
      ManagedPolicyArns:
        - !Ref ApplicationSNSPolicy
        - !Ref ApplicationSESPolicy
      UserName: staging-user

Running setup

Make sure in your APP_URL (in .env) is set correctly, matching your sending domain. If you do send email for multiple domains (i.e. multi tenant application) you can set multiple domains using this command.

You need to have SES domain ready before continuing

The setup command automatically configures your SES domain to send SNS notifications that trigger call backs to your Laravel application.

php artisan sns:setup mydomain.com

NOTE: You should not attempt to use sub domains client.mydomain.com, this is not currently supported by AWS.

Usage

To send an email with all tracking enabled

SesMail::enableAllTracking()
    ->to('[email protected]')
    ->send(new Mailable);

Calling enableAllTracking() enables open, reject, bounce, delivery, complaint and link tracking.

Please note that an LaravelSesTooManyRecipients Exception is thrown if you attempt send a Mailable that contains multiple recipients when Open -tracking is enabled.

Other exception thrown are:

LaravelSesDailyQuotaExceededException::class
LaravelSesInvalidSenderAddressException::class
LaravelSesMaximumSendingRateExceeded::class
LaravelSesSendFailedException::class
LaravelSesTemporaryServiceFailureException::class
LaravelSesTooManyRecipientsException::class

You can catch them all using the base class:

try {
    SesMail::enableAllTracking()
        ->to('[email protected]')
        ->send(new Mailable);
        
} catch (LaravelSesMaximumSendingRateExceeded $e) {

    // Implement back off logic

} catch (LaravelSesException $e) {
    
    $smtpCode = $e->getCode();
    $smtpErrorMessage = $e->getMessage();
    
    // Do something like back of if rate limit is reached.
)

You can, of course, disable and enable all the tracking options

```php
SesMail::disableAllTracking();
SesMail::disableOpenTracking();
SesMail::disableLinkTracking();
SesMail::disableBounceTracking();
SesMail::disableComplaintTracking();
SesMail::disableDeliveryTracking();

SesMail::enableAllTracking();
SesMail::enableOpenTracking();
SesMail::enableLinkTracking();
SesMail::enableBounceTracking();
SesMail::enableComplaintTracking();
SesMail::enableDeliveryTracking();

The batching option gives you the chance to group emails, so you can get the results for a specific group

SesMail::enableAllTracking()
    ->setBatch('welcome_emails')
    ->to('[email protected]')
    ->send(new Mailable);

You can also get aggregate stats:

Stats::statsForEmail($email);

$stats = Stats::statsForBatch('welcome_emails');

print_r($stats)
[
    "sent" => 8,
    "deliveries" => 7,
    "opens" => 4,
    "bounces" => 1,
    "complaints" => 2,
    "clicks" => 3,
    "link_popularity" => [
        "https://welcome.page" => [
            "clicks" => 3
        ],
        "https://facebook.com/brand" => [
            "clicks" => 1
        ]
    ]
]

To get individual stats via Repositories

EmailStatRepository::getBouncedCount($email);
EmailRepository::getBounces($email);

BatchStatRepository::getBouncedCount($batch);
BatchStatRepository::getDeliveredCount($batch);
BatchStatRepository::getComplaintsCount($batch);

You can also use the models directly as you would any other Eloquent model:

$sentEmails = SentEmail::whereEmail($email)->get();

$emailBounces = EmailBounce::whereEmail($email)->get();
$emailComplaints = EmailComplaint::whereEmail($email)->get();
$emailLink = EmailLink::whereEmail($email)->get();
$emailOpen = EmailOpen::whereEmail($email)->get();

If you are using custom models then you can use ModelResolver() helper like so

$sentEmail = ModelResolver::get('SentEmail')::take(100)->get();

Listening to event

Event subscriber can be created:

<?php

namespace App\Listeners;

use App\Actions\ProcessSesEvent;
use Juhasev\LaravelSes\Factories\Events\SesBounceEvent;
use Juhasev\LaravelSes\Factories\Events\SesComplaintEvent;
use Juhasev\LaravelSes\Factories\Events\SesDeliveryEvent;
use Juhasev\LaravelSes\Factories\Events\SesOpenEvent;
use Juhasev\LaravelSes\Factories\Events\SesSentEvent;

class SesSentEventSubscriber
{
    /**
     * Subscribe to events
     *
     * @param $events
     */
    public function subscribe($events)
    {
        $events->listen(SesBounceEvent::class, SesSentEventSubscriber::class . '@bounce');
        $events->listen(SesComplaintEvent::class, SesSentEventSubscriber::class . '@complaint');
        $events->listen(SesDeliveryEvent::class,SesSentEventSubscriber::class . '@delivery');
        $events->listen(SesOpenEvent::class, SesSentEventSubscriber::class . '@open');
        $events->listen(SesLinkEvent::class, SesSentEventSubscriber::class . '@link');
    }

    /**
     * SES bounce event took place
     *
     * @param SesBounceEvent $event
     */
    public function bounce(SesBounceEvent $event)
    {
        // Do something
    }

    /**
     * SES complaint event took place
     *
     * @param SesComplaintEvent $event
     */
    public function complaint(SesComplaintEvent $event)
    {
        // Do something
    }

    /**
     * SES delivery event took place
     *
     * @param SesDeliveryEvent $event
     */
    public function delivery(SesDeliveryEvent $event)
    {
        // Do something
    }

    /**
     * SES Open open event took place
     *
     * @param SesOpenEvent $event
     */
    public function open(SesOpenEvent $event)
    {
        // Do something
    }
   /**
     * SES Open link event took place
     *
     * @param SesLinkEvent $event
     */
    public function link(SesLinkEvent $event)
    {
        // Do something
    }

}

You will need to register EventSubscriber in the Laravel App/Providers/EventServiveProvider.php in order to work.

 protected $subscribe = [
    SesSentEventSubscriber::class
 ];

Example event data:

print_r($event->data);
(
    [id] => 22
    [sent_email_id] => 49
    [type] => Permanent
    [bounced_at] => 2020-04-03 19:42:31
    [sent_email] => Array
        (
            [id] => 49
            [message_id] => 31b530dce8e2a282d12e5627e7109580@localhost
            [email] => [email protected]
            [batch_id] => 7
            [sent_at] => 2020-04-03 19:42:31
            [delivered_at] => 
            [batch] => Array
                (
                    [id] => 7
                    [name] => fa04cbf2c2:Project:268
                    [created_at] => 2020-04-03 17:03:23
                    [updated_at] => 2020-04-03 17:03:23
                )

        )
    )
)

Terminology

Sent = number of emails that were attempted

Deliveries = number of emails that were delivered

Opens = number of emails that were opened

Complaints = number of people that put email into spam

Clicks = number of people that clicked at least one link in your email

Link Popularity = number of unique clicks on each link in the email, ordered by the most clicked.

Development

Clone Laravel SES repo to your project under /packages

git clone https://github.com/juhasev/laravel-ses.git

Setup Composer.json to resolve classes from your dev folder:

 "autoload": {
    "psr-4": {
      "App\\": "app/",
      "Juhasev\\LaravelSes\\": "packages/juhasev/laravel-ses/src"
    }
  },

Composer require

require: {
    "juhasev/laravel-ses": "dev-master"
}

Or run require

composer require juhasev/laravel-ses:dev-master

To run unit tests execute

phpunit

laravel-ses's People

Contributors

jubeki avatar juhasev avatar lemaur avatar nicholaszuccarelli avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

laravel-ses's Issues

Laravel 6.x ?

Hey there, the repo mentioned that Laravel 6.x is supported, but composer require juhasev/laravel-ses:* on Laravel version 6.20 returns "no matches found: juhasev/laravel-ses:*".

Can you confirm whether 6.x support is still available and how to get it ? Thank you!

Beacon and link

How does ses beacon and link work? are they supposed to be preconfigured in the emails themselves?
Thanks

Laravel 8

Hi

Great package !

Can be possible to update to use it on laravel 8 ?

Thank you so much!!!

LinkTracking needs null check for empty <a>

MailProcessor line 98.
If a supplied <a> in the email contents does not have a href (<a>Empty Link</a>), it will throw an error when attempting to perform link tracking on the email contents.

image

"Argument 1 passed to Juhasev\LaravelSes\MailProcessor::createAppLink() must be of type string, null given).
Simply need an if statement.

if ($originalUrl) {
$anchor->setAttribute(...)
}

Save to database

Hello

I receive POST from Amazon SES

[2020-05-09T19:00:37.419859+00:00] local.INFO: 4hfPzKnygi | HTTP/1.1 200 | x.x.x.x | null | POST https://xxxxxxxx.com/laravel-ses/notification/delivery [] | 0.13387799263 s | Amazon\sSimple\sNotification\sService\sAgent | {referer} ["RESPONSE"] []

but it doesn't save to database table, and i dont know why

On my controller i have

foreach ($users as $user) {
SesMail::enableAllTracking()
->to($user->email)
->send(new \App\Mail\OrderShipped($newsletter, $user));
}

it sends email, but it doesn't create on "laravel_ses_sent_emails" email sent registry. When delivery POST it's get, it cannot be updated with delivery date

Can anyone help me?

Broken message_id generation

Copying details from #20 comments

The changes in this commit have broken message_id usage.
The laravel_ses_sent_emails table is now storing the message_id as: (Notice the "Message-ID" portion in the string.
image

This commit also doesn't fix the issue I was attempting to resolve by fetching the generated Message-ID at time of email execution.
With v2 of the package, I could do (within a Laravel Mailable)
image
Which basically let me fetch that exact message-ID generated before the email was sent, however with v3, withSymfonyMessage no longer contains the Message-ID header.

As far as I can tell, this package doesn't offer a way to return the LaravelSesSentEmail model directly.

Call to undefined method Illuminate\Mail\Transport\LogTransport::setPingThreshold()

Recently upgraded to Laravel 9 (now using the v2 branch).
Attempting to enable email tracking when sending an email

$handler = SesMail::enableAllTracking();
$handler->send(...);

is throwing the error:

[2023-03-23 20:07:51] local.ERROR: Call to undefined method Illuminate\Mail\Transport\LogTransport::setPingThreshold()  

[2023-03-23 20:07:51] local.ERROR: 77  

[2023-03-23 20:07:51] local.ERROR: /var/www/html/vendor/juhasev/laravel-ses/src/LaravelSesServiceProvider.php  

[2023-03-23 20:07:51] local.ERROR: Error: Call to undefined method Illuminate\Mail\Transport\LogTransport::setPingThreshold() in /var/www/html/vendor/juhasev/laravel-ses/src/LaravelSesServiceProvider.php:77

My ENV is using the log mail driver.

Backtracking through LaravelSesServiceProvider, app('mailer') exists, as does ->getSymfonyTransport();
As the error says, it fails when attempting setPingThreshold()

email links: change from varchar to text for original_url

the laravel_ses_email_links table needs the original_url column changed from varchar to text.
There have been cases where I send out signed s3 URLs to my users and these URLs can be over 500 characters long.
Currently the link is being concat to the varchar(191) limit so the link they receive is invalid.

Your requirements could not be resolved to an installable set of packages

Problem 1
- juhasev/laravel-ses[v1.1.0, ..., v1.1.3] require illuminate/support 7.* -> found illuminate/support[v7.0.0, ..., 7.x-dev] but these were not loaded, likely because it conflicts with another require.
- Root composer.json requires juhasev/laravel-ses ^1.1 -> satisfiable by juhasev/laravel-ses[v1.1.0, v1.1.1, v1.1.2, v1.1.3].

Installation failed, reverting ./composer.json and ./composer.lock to their original content.

image

Can you help me to sort this out?

Laravel 7

Hi

Great package. Can be possible to update to use it on laravel 7?

Thankyou so much!!!

Undefined property: stdClass::$complaint

We've been receiving this error many times per day in our logs, thrown by the package.

We're not sure if we haven't set something up correctly, or whether this is a bug in the package itself.
Emails do still send though. (Some of our users have reported some emails they send to clients are not working however - but that is probably unrelated to the package itself).

{ "exception": { "message": "Undefined property: stdClass::$complaint", "code": 0, "file": "/var/app/current/vendor/juhasev/laravel-ses/src/Controllers/ComplaintController.php", "line": 95, "trace": "#0 /var/app/current/vendor/juhasev/laravel-ses/src/Controllers/ComplaintController.php(95): Illuminate\\Foundation\\Bootstrap\\HandleExceptions->handleError(8, 'Undefined prope...', '/var/app/curren...', 95, Array) #1 /var/app/current/vendor/juhasev/laravel-ses/src/Controllers/ComplaintController.php(61): Juhasev\\LaravelSes\\Controllers\\ComplaintController->persistComplaint(Object(stdClass)) #2 /var/app/current/vendor/laravel/framework/src/Illuminate/Routing/Controller.php(54): Juhasev\\LaravelSes\\Controllers\\ComplaintController->complaint(Object(Nyholm\\Psr7\\ServerRequest)) #3 /var/app/current/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php(45): Illuminate\\Routing\\Controller->callAction('complaint', Array) #4 /var/app/current/vendor/laravel/framework/src/Illuminate/Routing/Route.php(239): Illuminate\\Routing\\ControllerDispatcher->dispatch(Object(Illuminate\\Routing\\Route), Object(Juhasev\\LaravelSes\\Controllers\\ComplaintController), 'complaint') #5 /var/app/current/vendor/laravel/framework/src/Illuminate/Routing/Route.php(196): Illuminate\\Routing\\Route->runController() #6 /var/app/current/vendor/laravel/framework/src/Illuminate/Routing/Router.php(685): Illuminate\\Routing\\Route->run() #7 /var/app/current/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(128): Illuminate\\Routing\\Router->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request)) #8 /var/app/current/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(103): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request)) #9 /var/app/current/vendor/laravel/framework/src/Illuminate/Routing/Router.php(687): Illuminate\\Pipeline\\Pipeline->then(Object(Closure)) #10 /var/app/current/vendor/laravel/framework/src/Illuminate/Routing/Router.php(662): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request)) #11 /var/app/current/vendor/laravel/framework/src/Illuminate/Routing/Router.php(628): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route)) #12 /var/app/current/vendor/laravel/framework/src/Illuminate/Routing/Router.php(617): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request)) #13 /var/app/current/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(165): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request)) #14 /var/app/current/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(128): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request)) #15 /var/app/current/vendor/fideloper/proxy/src/TrustProxies.php(57): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request)) #16 /var/app/current/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Fideloper\\Proxy\\TrustProxies->handle(Object(Illuminate\\Http\\Request), Object(Closure)) #17 /var/app/current/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request)) #18 /var/app/current/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure)) #19 /var/app/current/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request)) #20 /var/app/current/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure)) #21 /var/app/current/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ValidatePostSize.php(27): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request)) #22 /var/app/current/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Illuminate\\Foundation\\Http\\Middleware\\ValidatePostSize->handle(Object(Illuminate\\Http\\Request), Object(Closure)) #23 /var/app/current/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/CheckForMaintenanceMode.php(63): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request)) #24 /var/app/current/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(167): Illuminate\\Foundation\\Http\\Middleware\\CheckForMaintenanceMode->handle(Object(Illuminate\\Http\\Request), Object(Closure)) #25 /var/app/current/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(103): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request)) #26 /var/app/current/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(140): Illuminate\\Pipeline\\Pipeline->then(Object(Closure)) #27 /var/app/current/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(109): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request)) #28 /var/app/current/public/index.php(52): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request)) #29 {main}", "severity": 8 } }

Possible Incorrect Relationship Types on Sent Email

Hey,
Love the package! I believe that emailOpen on the SentEmail model is down as hasOne and should be hasMany as I'm presuming we will get numerous open events?

If this is the case, I'm happy to PR this - just wanted to sanity check it.

Thanks

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.