Keep track of outgoing emails and associate sent emails with Eloquent models
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. Then call the storeClassName()
-method inside the build
-method.
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");
}
}
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");
}
}
You can now access the sent out emails from the product's send
-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");
}
}
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
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,
sends
-table
Further Usage of the 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.