Track e-mail delivery with Mailgun Hooks. All data are stored in the database on Email model
henryavila/email-tracking is a Laravel package for track e-mail delivery with mailgun hooks. all data are stored in the database on email model.
It currently has 7 GitHub stars and 3.912 downloads on Packagist (latest version v7.0.0).
Install it with composer require henryavila/email-tracking.
Discover more Laravel packages by henryavila
or browse all Laravel packages to compare alternatives.
Last updated
Track email delivery, opens, clicks, and more using Mailgun webhooks. All data is stored in your database for easy querying and analytics.
This package maintains high test coverage with comprehensive unit and integration tests. All new features are fully tested before release.
composer require henryavila/email-tracking
php artisan vendor:publish --tag="email-tracking-migrations"
php artisan migrate
php artisan vendor:publish --tag="email-tracking-config"
Setup Laravel Mail with Mailgun driver. See Laravel Mail Documentation.
Add to your .env file:
MAIL_MAILER=mailgun
MAILGUN_DOMAIN=yourdomain.com
MAILGUN_SECRET=key-99999999999999999999999999999999
In your Mailgun dashboard, add a webhook pointing to:
https://yourdomain.com/webhooks/mailgun
The package needs to listen for sent emails to track them.
Add to AppServiceProvider::boot():
public function boot(): void
{
\Illuminate\Support\Facades\Event::listen(
events: \Illuminate\Mail\Events\MessageSent::class,
listener: \HenryAvila\EmailTracking\Listeners\LogEmailSentListener::class
);
}
The published config file (config/email-tracking.php) allows customization:
return [
/**
* Database connection for Email model (optional)
* If null, uses default connection
*/
'email-db-connection' => null,
/**
* Save HTML body of sent emails
*/
'log-body-html' => true,
/**
* Save text body of sent emails
*/
'log-body-txt' => true,
];
Extend TrackableMail instead of Laravel's Mailable:
use HenryAvila\EmailTracking\Mail\TrackableMail;
class OrderShippedMail extends TrackableMail
{
public function __construct($order)
{
$viewData = [
'order' => $order,
'trackingNumber' => $order->tracking_number,
];
parent::__construct($order, 'emails.order-shipped', $viewData);
}
}
Send the email:
$order = Order::find(1);
Mail::to($order->customer)->send(new OrderShippedMail($order));
The email will be automatically tracked and linked to the $order model.
For notifications, use TrackableNotificationMailMessage:
use HenryAvila\EmailTracking\Notifications\TrackableNotificationMailMessage;
class OrderShippedNotification extends Notification
{
public function __construct(protected Order $order)
{
}
public function toMail($notifiable): MailMessage
{
return (new TrackableNotificationMailMessage($this->order))
->subject('Your order has been shipped!')
->line('Your order #' . $this->order->number . ' is on its way.')
->action('Track Shipment', url('/orders/' . $this->order->id))
->line('Thank you for your purchase!');
}
}
Categorize emails for better organization and analytics.
<?php
namespace App\Enums;
enum EmailType: string
{
case TRANSACTIONAL = 'transactional';
case MARKETING = 'marketing';
case NOTIFICATION = 'notification';
case ADMINISTRATIVE = 'administrative';
case SYSTEM = 'system';
}
use App\Enums\EmailType;
use HenryAvila\EmailTracking\Mail\TrackableMail;
class OrderConfirmationMail extends TrackableMail
{
protected function getEmailType(): EmailType
{
return EmailType::TRANSACTIONAL;
}
}
use App\Models\Email;
// Get all transactional emails
$transactional = Email::where('email_type', 'transactional')->get();
// Add convenient scopes to your Email model
Email::transactional()->delivered()->get();
// Analytics by type
$stats = Email::select('email_type')
->selectRaw('count(*) as total, sum(opened) as opens')
->groupBy('email_type')
->get();
Learn more: See Email Type Classification Documentation for complete guide with examples.
use HenryAvila\EmailTracking\Models\Email;
// Get all emails for a model
$order = Order::find(1);
$emails = $order->emails; // Requires ModelWithEmailsSenderTrait on Order model
// Query email status
$delivered = Email::whereNotNull('delivered_at')->get();
$opened = Email::where('opened', '>', 0)->get();
$clicked = Email::where('clicked', '>', 0)->get();
$failed = Email::whereNotNull('failed_at')->get();
// Get recent emails
$recentEmails = Email::orderBy('created_at', 'desc')->limit(10)->get();
// Search by recipient
$userEmails = Email::where('to', 'like', '%[email protected]%')->get();
// Delivery rate
$totalSent = Email::count();
$delivered = Email::whereNotNull('delivered_at')->count();
$deliveryRate = ($delivered / $totalSent) * 100;
// Open rate
$opened = Email::where('opened', '>', 0)->count();
$openRate = ($opened / $delivered) * 100;
// Click rate
$clicked = Email::where('clicked', '>', 0)->count();
$clickRate = ($clicked / $delivered) * 100;
When Mailgun processes an email event (delivered, opened, clicked, etc.), the EmailWebhookProcessed event is dispatched.
Create a listener:
<?php
namespace App\Listeners;
use HenryAvila\EmailTracking\Events\EmailWebhookProcessed;
use HenryAvila\EmailTracking\Events\Email\DeliveredEmailEvent;
use HenryAvila\EmailTracking\Events\Email\OpenedEmailEvent;
class MailgunWebhookProcessedListener
{
public function handle(EmailWebhookProcessed $event): void
{
match ($event->emailEvent::class) {
DeliveredEmailEvent::class => $this->handleDelivered($event->emailEvent),
OpenedEmailEvent::class => $this->handleOpened($event->emailEvent),
// Add other events as needed
default => null,
};
}
private function handleDelivered(DeliveredEmailEvent $event): void
{
// Your custom logic when email is delivered
$email = $event->email;
logger()->info("Email delivered to {$email->to}");
}
private function handleOpened(OpenedEmailEvent $event): void
{
// Your custom logic when email is opened
$email = $event->email;
logger()->info("Email opened by {$email->to}");
}
}
Register the listener in EventServiceProvider:
protected $listen = [
\HenryAvila\EmailTracking\Events\EmailWebhookProcessed::class => [
\App\Listeners\MailgunWebhookProcessedListener::class,
],
];
AcceptedEmailEvent - Email accepted for deliveryDeliveredEmailEvent - Email successfully deliveredOpenedEmailEvent - Email opened by recipientClickedEmailEvent - Link clicked in emailPermanentFailureEmailEvent - Permanent delivery failure (bounce)TemporaryFailureEmailEvent - Temporary delivery issueSpamComplaintsEmailEvent - Marked as spamUnsubscribeEmailEvent - Unsubscribe requestAdd the trait to models that send emails:
use HenryAvila\EmailTracking\Traits\ModelWithEmailsSenderTrait;
class Order extends Model
{
use ModelWithEmailsSenderTrait;
}
Now you can access emails:
$order = Order::find(1);
$emails = $order->emails; // All emails sent for this order
Extend the base Email model to add your own methods:
namespace App\Models;
use App\Enums\EmailType;
use Illuminate\Database\Eloquent\Builder;
class Email extends \HenryAvila\EmailTracking\Models\Email
{
protected function casts(): array
{
return array_merge(parent::casts(), [
'email_type' => EmailType::class,
]);
}
public function scopeTransactional(Builder $query): Builder
{
return $query->where('email_type', EmailType::TRANSACTIONAL);
}
public function scopeDelivered(Builder $query): Builder
{
return $query->whereNotNull('delivered_at');
}
public function scopeRecent(Builder $query, int $days = 7): Builder
{
return $query->where('created_at', '>=', now()->subDays($days));
}
}
Use your custom model by binding it in a service provider:
$this->app->bind(
\HenryAvila\EmailTracking\Models\Email::class,
\App\Models\Email::class
);
composer test
Please see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
Please review our security policy on how to report security vulnerabilities.
The MIT License (MIT). Please see License File for more information.
Version 7.0.0 drops support for PHP 8.1 and Laravel 9-10.
Laravel 10 has reached end-of-life and contains known security vulnerabilities. This major version ensures your application uses secure, actively maintained Laravel versions.
# 1. Update your Laravel application to 11.x first
composer require laravel/framework:^11.0
# 2. Update PHP to 8.2 or higher (if needed)
# 3. Update email-tracking package
composer require henryavila/email-tracking:^7.0
If you were using Laravel 10's EventServiceProvider pattern, migrate to Laravel 11's AppServiceProvider:
Before (Laravel 10):
// EventServiceProvider
protected $listen = [
\Illuminate\Mail\Events\MessageSent::class => [
\HenryAvila\EmailTracking\Listeners\LogEmailSentListener::class,
],
];
After (Laravel 11):
// AppServiceProvider::boot()
\Illuminate\Support\Facades\Event::listen(
events: \Illuminate\Mail\Events\MessageSent::class,
listener: \HenryAvila\EmailTracking\Listeners\LogEmailSentListener::class
);
The package now uses:
All tests pass on PHP 8.2, 8.3, 8.4, and 8.5 with Laravel 11 and 12.
Note: If you're on v6.x, upgrade directly to v7.0.0 using the guide above.
A new migration was added to track email events.
php artisan vendor:publish --tag="email-tracking-migrations"
php artisan migrate
Give a βοΈ if this project helped you!