Track email delivery, opens, clicks, and more using Mailgun webhooks. All data is stored in your database for easy querying and analytics.
- π§ Complete Email Tracking - Track sent, delivered, opened, clicked, bounced, and failed emails
- π Model Association - Link emails to any Eloquent model (User, Order, Invoice, etc.)
- π― Email Categorization - Classify emails by type (transactional, marketing, notifications, etc.)
- π Built-in Analytics - Query delivery rates, open rates, click rates by email type
- πͺ Mailgun Webhooks - Automatic event processing from Mailgun
- π Secure Webhooks - Signature verification for webhook security
- πΎ Database Storage - All email data stored in your database
- π§ͺ Fully Tested - Comprehensive test suite included
- π± Laravel 11+ & 12 - Modern Laravel support with latest features
- PHP 8.2, 8.3, or 8.4 (PHP 8.5 support planned for v7.1.0)
- Laravel 11.0 or higher (Laravel 11 LTS and Laravel 12 supported)
- Mailgun account
This package maintains high test coverage with comprehensive unit and integration tests. All new features are fully tested before release.
composer require henryavila/email-trackingphp artisan vendor:publish --tag="email-tracking-migrations"
php artisan migratephp 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-99999999999999999999999999999999In 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', '%user@example.com%')->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 request
Add 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 orderExtend 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
);- Email Type Classification Guide - Complete guide for email categorization
- Changelog - Version history and upgrade guides
composer testPlease 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.
- PHP 8.2+ (was 8.1+)
- Laravel 11+ (was 9+)
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.0If 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:
- Pest 3.x for testing
- PHPUnit 11.x as test runner
- PHPStan Level 4 for static analysis
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- π Documentation
- π Issue Tracker
- π¬ Discussions
Give a βοΈ if this project helped you!