Laravel Email Tracking with Mailgun
Track email delivery, opens, clicks, and more using Mailgun webhooks. All data is stored in your database for easy querying and analytics.
β¨ Features
- π§ 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
π Requirements
- 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
π Code Coverage
This package maintains high test coverage with comprehensive unit and integration tests. All new features are fully tested before release.
π¦ Installation
1. Install via Composer
composer require henryavila/email-tracking2. Publish and Run Migrations
php artisan vendor:publish --tag="email-tracking-migrations"
php artisan migrate3. Publish Configuration (Optional)
php artisan vendor:publish --tag="email-tracking-config"4. Configure Mailgun
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-999999999999999999999999999999995. Setup Mailgun Webhook
In your Mailgun dashboard, add a webhook pointing to:
https://yourdomain.com/webhooks/mailgun
βοΈ Configuration
Register Event Listener
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
);
}Configuration File
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,
];π Usage
Basic Mailable with Tracking
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.
Trackable Notifications
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!');
}
}Email Type Classification (v7.0.0+)
Categorize emails for better organization and analytics.
1. Create Email Type Enum
<?php
namespace App\Enums;
enum EmailType: string
{
case TRANSACTIONAL = 'transactional';
case MARKETING = 'marketing';
case NOTIFICATION = 'notification';
case ADMINISTRATIVE = 'administrative';
case SYSTEM = 'system';
}2. Implement in Mailable
use App\Enums\EmailType;
use HenryAvila\EmailTracking\Mail\TrackableMail;
class OrderConfirmationMail extends TrackableMail
{
protected function getEmailType(): EmailType
{
return EmailType::TRANSACTIONAL;
}
}3. Query by Type
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.
Querying Emails
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();Email Analytics
// 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;πͺ Webhook Events
When Mailgun processes an email event (delivered, opened, clicked, etc.), the EmailWebhookProcessed event is dispatched.
Listening to Webhook Events
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,
],
];Available Event Types
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
π§ Advanced Usage
Model Association
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 orderCustom Email Model
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
);π Documentation
- Email Type Classification Guide - Complete guide for email categorization
- Changelog - Version history and upgrade guides
π§ͺ 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.
π‘ Upgrade Guides
Upgrading to 7.0.0 from 6.x - BREAKING CHANGES
Version 7.0.0 drops support for PHP 8.1 and Laravel 9-10.
Requirements
- PHP 8.2+ (was 8.1+)
- Laravel 11+ (was 9+)
Why This Change?
Laravel 10 has reached end-of-life and contains known security vulnerabilities. This major version ensures your application uses secure, actively maintained Laravel versions.
Migration Path
# 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.0Code Changes Required
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
);Testing
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.
Upgrading to 6.2.0 from earlier versions (Legacy)
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π Support
- π Documentation
- π Issue Tracker
- π¬ Discussions
β Show Your Support
Give a βοΈ if this project helped you!