• Stars
    star
    175
  • Rank 218,059 (Top 5 %)
  • Language
    PHP
  • License
    MIT License
  • Created about 3 years ago
  • Updated 9 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A package to keep track of outgoing emails in your Laravel application.

Keep track of outgoing emails and associate sent emails with Eloquent models

Latest Version on Packagist run-tests Check & fix styling Total Downloads

This package helps you to keep track of outgoing emails in your Laravel application. In addition, you can associate Models to the sent out emails.

Here are a few short examples what you can do:

// An email is sent to $user. The passed $product and $user models will be 
// associated with the sent out email generated by the `ProductReviewMail` mailable.
Mail::to($user)->send(new ProductReviewMail($product, $user));

In your application, you can then fetch all sent out emails for the user or the product.

$user->sends()->get();
$product->sends()->get();

Or you can fetch all sent out emails for the given Mailable class.

Send::forMailClass(ProductReviewMail::class)->get();

Each Send-model holds the following information:

  • FQN of mailable class
  • subject
  • from address
  • reply to address
  • to address
  • cc adresses
  • bcc adresses

Additionally, the sends-table has the following columns which can be filled by your own application (learn more).

  • delivered_at
  • last_opened_at
  • opens
  • clicks
  • last_clicked_at
  • complained_at
  • bounced_at
  • permanent_bounced_at
  • rejected_at

Installation

You can install the package via composer:

composer require wnx/laravel-sends

Then, publish and run the migrations:

php artisan vendor:publish --tag="sends-migrations"
php artisan migrate

Optionally, you can publish the config file with:

php artisan vendor:publish --tag="sends-config"

This is the contents of the published config file:

return [
    /*
     * The fully qualified class name of the send model.
     */
    'send_model' => \Wnx\Sends\Models\Send::class,

    /**
     * If set to true, the content of sent mails is saved to the database.
     */
    'store_content' => env('SENDS_STORE_CONTENT', false),

    'headers' => [
        /**
         * Header containing the encrypted FQN of the mailable class.
         */
        'mail_class' => env('SENDS_HEADERS_MAIL_CLASS', 'X-Laravel-Mail-Class'),

        /**
         * Header containing an encrypted JSON object with information which
         * Eloquent models are associated with the mailable class.
         */
        'models' => env('SENDS_HEADERS_MAIL_MODELS', 'X-Laravel-Mail-Models'),

        /**
         * Header containing unique ID of the sent out mailable class.
         */
        'send_uuid' => env('SENDS_HEADERS_SEND_UUID', 'X-Laravel-Send-UUID'),
    ],
];

Usage

After the installation is done, update your applications EventServiceProvider to listen to the MessageSent event. Add the StoreOutgoingMailListener-class as a listener.

// app/Providers/EventServiceProvider.php
protected $listen = [
    // ...
    \Illuminate\Mail\Events\MessageSent::class => [
        \Wnx\Sends\Listeners\StoreOutgoingMailListener::class,
    ],
]

The metadata of all outgoing emails created by mailables or notifications is now being stored in the sends-table. (Note that you can only associate models to mailables; but not to notifiations)

Read further to learn how to store the name and how to associate models with a Mailable class.

Store Mailable class name on Send Model

By default the Event Listener stores the mails subject and the recipient adresses. That's nice, but we can do better. It can be beneficial for your application to know which Mailable class triggered the sent email.

To store this information, add the StoreMailables-trait to your Mailable classes like below. You now have access to a couple of helper methods.

Depending on how you write Mailables, there a different ways to use these new methods. Call the storeClassName-method inside the build-method of your Mailable.

class ProductReviewMail extends Mailable
{
    use SerializesModels;
    use StoreMailables;

    public function __construct(public User $user, public Product $product)
    {
    }

    public function build()
    {
        return $this
            ->storeClassName()
            ->view('emails.products.review')
            ->subject("$this->product->name waits for your review");
    }
}

If you use the Mailable syntax introduced in Laravel v9.35, you can either use $this->storeClassName() in the headers-method or pass $this->getMailClassHeader()->toArray() to the Header object.

class ProductReviewMail extends Mailable
{
    use SerializesModels;
    use StoreMailables;

    public function __construct(
        public User $user,
        public Product $product
    ) { }

    // ...

    /**
     * @return \Illuminate\Mail\Mailables\Headers
     */
    public function headers()
    {
        // Call storeClassName() and let the package take care of adding the
        // header to the outgoing mail.
        $this->storeClassName();

        // Or – if you want more control – use the getMailClassHeader() method
        // to get a Header instance and merge it with your own headers.
        return new Headers(
            text: [
                'X-Custom-Header' => 'Custom Value',
                ...$this->getMailClassHeader()->toArray(),
            ],
        );
    }
}

The method will add a X-Laravel-Mail-Class-header to the outgoing email containing the fully qualified name (FQN) of the Mailable class as an encrypted string. (eg. App\Mails\ProductReviewMail). Update the SENDS_HEADERS_MAIL_CLASS-env variable to adjust the header name. (See config for details).

The package's event listener will then look for the header, decrypt the value and store it in the database.

Associate Sends with Related Models

Now that you already keep track of all outgoing emails, wouldn't it be great if you could access the records from an associated Model? In the example above we send a ProductReviewMail to a user. Wouldn't it be great to see how many emails you have sent for a given Product-model?

To achieve this, you have to add the HasSends-interface and the HasSendsTrait trait to your models.

use Wnx\Sends\Contracts\HasSends;
use Wnx\Sends\Support\HasSendsTrait;

class Product extends Model implements HasSends
{
    use HasSendsTrait;
}

You can now call the associateWith()-method within the build()-method. Pass the Models you want to associate with the Mailable class to the method as arguments. Instead of passing the Models as arguments, you can also pass them as an array.

class ProductReviewMail extends Mailable
{
    use SerializesModels;
    use StoreMailables;

    public function __construct(
        private User $user,
        private Product $product
    ) { }

    public function build()
    {
        return $this
            ->storeClassName()
            ->associateWith($this->product)
            // ->associateWith([$this->product])
            ->view('emails.products.review')
            ->subject("$this->user->name, $this->product->name waits for your review");
    }
}

If you're using the Mailable syntax introduced in Laravel v9.35, you can call associateWith() or getMailModelsHeader() in the headers-method too.

class ProductReviewMail extends Mailable
{
    use SerializesModels;
    use StoreMailables;

    public function __construct(
        private User $user,
        private Product $product
    ) { }

    // ...

    /**
     * @return \Illuminate\Mail\Mailables\Headers
     */
    public function headers()
    {
        // Call associateWith() and the package automatically associates the public
        // properties with this mailable.
        $this->associateWith($this->product);
        $this->associateWith([$this->product]);

        // Or – if you want more control – use the getMailModelsHeader() method
        // to get a Header instance and merge it with your own headers.
        return new Headers(
            text: [
                'X-Custom-Header' => 'Custom Value',
                ...$this->getMailModelsHeader($this->product)->toArray(),
                ...$this->getMailModelsHeader([$this->product])->toArray(),
            ],
        );
    }
}

You can now access the sent out emails from the product's sends-relationship.

$product->sends()->get();

Automatically associate Models with Mailables

If you do not pass an argument to the associateWith-method, the package will automatically associate all public properties which implement the HasSends-interface with the Mailable class. πŸͺ„

In the example below, the ProductReviewMail-Mailable will automatically be associated with the $product Model, as Product implements the HasSends interface. The $user model will be ignored, as it's declared as a private property.

class ProductReviewMail extends Mailable
{
    use SerializesModels;
    use StoreMailables;

    public function __construct(
        private User $user, 
        public Product $product
    ) { }

    public function build()
    {
        return $this
            ->associateWith()
            ->view('emails.products.review')
            ->subject("$this->user->name, $this->product->name waits for your review");
    }
}

If you're using the Mailable syntax introduced in Laravel v9.35, you can call associateWith() or getMailModelsHeader() in the headers-method.

class ProductReviewMail extends Mailable
{
    use SerializesModels;
    use StoreMailables;

    public function __construct(
        private User $user,
        public Product $product
    ) { }

    // ...

    /**
     * @return \Illuminate\Mail\Mailables\Headers
     */
    public function headers()
    {
        // Call associateWith() and the package automatically associates the public
        // properties with this mailable.
        $this->associateWith();

        // Or – if you want more control – use the getMailModelsHeader() method
        // to get a Header instance and merge it with your own headers.
        return new Headers(
            text: [
                'X-Custom-Header' => 'Custom Value',
                ...$this->getMailModelsHeader()->toArray(),
            ],
        );
    }
}

Attach custom Message ID to Mails

If you're sending emails through AWS SES or a similar service, you might want to identify the sent email in the future (for example when a webhook for the "Delivered"-event is sent to your application).

The package comes with an event listener helping you here. Update the EventServiceProvider to listen to the MessageSending event and add the AttachSendUuidListener as a listener. A X-Laravel-Message-UUID header will be attached to all outgoing emails. The header contains a UUID value. (This value can not be compared to the Message-ID defined in RFC 2392)
You can then use the value of X-Laravel-Message-UUID or $send->uuid later in your application.

// app/Providers/EventServiceProvider.php
protected $listen = [
    // ...
    \Illuminate\Mail\Events\MessageSending::class => [
        \Wnx\Sends\Listeners\AttachSendUuidListener::class,
    ],
]

(If you want to store the value of Message-ID in your database, do not add the event listener but update the SENDS_HEADERS_SEND_UUID-env variable to Message-ID. The StoreOutgoingMailListener will then store the Message-ID in the database.)

Store Content of Mails

By default, the package does not store the content of sent out emails. By setting the sends.store_content configuration value to true, the body of all outgoing mails is stored in the content-column in the sends database table.

/**
 * If set to true, the contet of sent mails is saved to the database.
 */
'store_content' => true,
SENDS_STORE_CONTENT=true

Customize Attributes stored in Send Models

If you need to store more attributes with your Send-model, you can extend the StoreOutgoingMailListener and override the getSendAttributes-method.

For example, let's say we would like to store an audience-value with each sent out email. We create a new Event Listener called CustomStoreOutgoingMailListener and use the class as Listener to the MessageSent-event.

Our EventServiceProvider would look like this.

// app/Providers/EventServiceProvider.php
protected $listen = [
    // ...
    \Illuminate\Mail\Events\MessageSent::class => [
        \App\Listeners\CustomStoreOutgoingMailListener::class,
    ],
]

The Listener itself would look like the code below. We extend Wnx\Sends\Listeners\StoreOutgoingMailListener and override getSendAttributes. We merge the $defaultAttributes with our custom attributes we want to store. In the example below we store an audience value.

<?php

namespace App\Listeners;

use Illuminate\Mail\Events\MessageSent;
use Wnx\Sends\Listeners\StoreOutgoingMailListener;

class CustomStoreOutgoingMailListener extends StoreOutgoingMailListener
{
    protected function getSendAttributes(MessageSent $event, array $defaultAttributes): array
    {
        return array_merge($defaultAttributes, [
            'audience' => $this->getAudience($event),
        ]);
    }

Prune Send Models

By default, Send-models are kept forever in your database. If your application sends thousands of emails per day, you might want to prune records after a couple of days or months.

To do that, use the prunable feature of Laravel.

Create a new Send-model in your app/Models that extends Wnx\Sends\Models\Send. Then add the Prunable-trait and set up the prunable()-method to your liking. The example below deletes all Send-models older than 1 month.

// app/Models/Send.php
namespace App\Models;

use Illuminate\Database\Eloquent\Prunable;
use Wnx\Sends\Models\Send as BaseSend;

class Send extends BaseSend
{
    use Prunable;

    public function prunable()
    {
        return static::where('created_at', '<=', now()->subMonth());
    }
}

Optionally you can also update the configuration, so that the package internally uses your Send model.

// config/sends.php
/*
 * The fully qualified class name of the send model.
 */
'send_model' => \App\Models\Send::class,

Further Usage of the sends-table

As you might have noticed, the sends-table comes with more columns than that are currently filled by the package. This is by design.

You are encouraged to write your own application logic to fill these currently empty columns. For example, if you're sending emails through AWS SES, I highly encourage you to use the renoki-co/laravel-aws-webhooks package to handle AWS SNS webhooks.

A controller that handles the "Delivered" event might look like this.

class AwsSnsSesWebhookController extends SesWebhook {
    protected function onDelivery(array $message)
    {
        $uuidHeader = collect(Arr::get($message, 'mail.headers', []))
            ->firstWhere('name', config('sends.headers.send_uuid'));

        if ($uuidHeader === null) {
            return;
        }

        $send = Send::forUuid($uuidHeader['value'])->first();

        if ($send === null) {
            return;
        }

        $send->delivered_at = now();
        $send->save();
    }
}

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.

More Repositories

1

git-auto-commit-action

Automatically commit and push changed files back to GitHub with this GitHub Action for the 80% use case.
Shell
1,970
star
2

laravel-stats

πŸ“ˆ Get insights about your Laravel or Lumen Project
PHP
1,707
star
3

screeenly

πŸ“Έ Screenshot as a Service
PHP
493
star
4

sidecar-browsershot

A Sidecar function to run Browsershot on Lambda.
PHP
195
star
5

laravel-backup-restore

A package to restore database backups made with spatie/laravel-backup.
PHP
162
star
6

changelog-updater-action

A GitHub Action to automatically update a "Keep a Changelog" CHANGELOG with the latest release notes.
Shell
122
star
7

diary-app

πŸ”An End-To-End Encrypted Diary Web App
PHP
93
star
8

laravel-github-actions-demo

A demo application to showcase useful GitHub Actions for PHP developers
PHP
30
star
9

kirby-json-feed

Kirby Plugin to serve a JSON Feed
PHP
26
star
10

reusable-workflows

A collection of reusable GitHub Actions workflows I use in my public and private projects.
26
star
11

dotfiles

🐼 My dotfiles
Shell
23
star
12

imgubox

[Deprecated] Store Imgur favorites in Dropbox
PHP
22
star
13

php-changelog-updater

PHP CLI to add latest release notes to a CHANGELOG
PHP
22
star
14

ScreeenlyClient

PHP API Wrapper for Screeenly
PHP
22
star
15

laravel-phpinsights-action

Run PHP Insights in Laravel in Github Actions
Dockerfile
21
star
16

php-swiss-cantons

πŸ‡¨πŸ‡­ Find Swiss Canton by abbreviation, name or zipcode.
PHP
17
star
17

alfred-emoji-pack

PHP Script to generate Snippets for Alfred with all available Emojis.
PHP
14
star
18

commonmark-mark-extension

Render ==highlighted== texts as <mark> elements in league/commonmark
PHP
13
star
19

mp3-to-m4r-converter

Bulk-convert mp3 to m4r-files
Shell
13
star
20

deployer-on-github-actions-example

Example Repository showcasing how to run deployer on GitHub Actions
PHP
13
star
21

laravel-file-encryption-example

Example project to showcase backend file encryption
PHP
12
star
22

laravel-download-statistics-app

Aggregated download statistics for Laravel.
PHP
12
star
23

phpinsights-action

Run PHP Insights in Github Actions
Dockerfile
11
star
24

dropshare-landingpage

A minimal landingpage for Dropshare.app
HTML
7
star
25

uberspaceScripts

My personal collection of useful scripts, when hosting on uberspace.de
Shell
7
star
26

esbuild-mix-manifest-plugin

esbuild plugin to generate mix-manifest.json file compatible with Laravel Mix.
TypeScript
7
star
27

commonmark-markdown-renderer

Render a league/commonmark AST back to Markdown.
PHP
6
star
28

dirby

An opinionated Kirby theme for documentations
CSS
6
star
29

laravel-encryption-key-generator

A simple app to generate new Laravel encryption keys for you.
PHP
6
star
30

vue-tailwind-css-modules-demo

Demo application highlighting the use of Tailwind CSS in Vue Components
JavaScript
6
star
31

dropshare-tailwind-landingpage

Simple landingpage to use with Dropshare
JavaScript
5
star
32

js-swiss-cantons

πŸ‡¨πŸ‡­ Find Swiss Canton by abbreviation or name
JavaScript
5
star
33

getting-started-with-bash-testing

Example Bash Project to get started with testing with Bats.
Shell
5
star
34

kirby-laravel-mix-helper

Helper to use Version Busting of Laravel Mix
PHP
5
star
35

icq-christmas-card

Revive one of the old ICQ Christmas Cards from 2002.
HTML
4
star
36

multi-photo-crop

πŸ™ A tool to crop and split photos from a single image
PHP
4
star
37

swiss-canton-iconfont

Serve the 26 Swiss Cantons in a simple icon font
CSS
3
star
38

example-advanced-eloquent-with-pivot

Example project mentioned in an article of mine.
PHP
3
star
39

asana-expander-extension

Browser Extension to automatically expand long comments in Asana
TypeScript
3
star
40

photon-the-archive-theme

A "The Archive" theme based on the PHPStorm Theme "Photon" by Brent Roose
3
star
41

alfred-emoji-pack-node

Emojis at your fingertips as Alfred Snippets
JavaScript
2
star
42

radio-srf-menubarapp

πŸ“» A simple menubar application to play Radio SRF stations
Swift
2
star
43

faker-swiss-locations

Provider to generate valid Swiss location data for Faker PHP.
PHP
2
star
44

php-search-string-parser

[In Development] Use Search Input Strings similar to Github
PHP
2
star
45

git-auto-commit-action-demo-app

A demo application to test git-auto-commit Github Action
PHP
2
star
46

oh-dear-request-run-action

Trigger Oh Dear runs through GitHub Actions.
2
star
47

life-expectancy-visualisation

A small application to visualise the life expectancy of different people around the world.
TypeScript
2
star
48

scratchpad

Minimal Scratchpad
HTML
1
star
49

stefanzweifel

1
star
50

chrome-facebook-to-wikipedia-redirect

Read a Wikipedia Article instead of your Newsfeed
JavaScript
1
star
51

user-agent-analyzer

User Agent analysis as a Service.
PHP
1
star
52

git-auto-commit-demo-app

A demo application to test the git-auto-commit Action.
1
star
53

sidecar-browsershot-layer

AWS Lambda Layer containing puppeteer-core. Used by sidecar-browsershot
Shell
1
star