henryavila/email-tracking

Track e-mail delivery with Mailgun Hooks. All data are stored in the database on Email model

Installs: 3 193

Dependents: 1

Suggesters: 0

Security: 0

Stars: 7

Watchers: 1

Forks: 0

Open Issues: 3

pkg:composer/henryavila/email-tracking


README

Latest Version on Packagist Total Downloads Tests PHPStan Laravel Pint codecov License

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

Coverage Graph

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-tracking

2. Publish and Run Migrations

php artisan vendor:publish --tag="email-tracking-migrations"
php artisan migrate

3. 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-99999999999999999999999999999999

5. 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 delivery
  • DeliveredEmailEvent - Email successfully delivered
  • OpenedEmailEvent - Email opened by recipient
  • ClickedEmailEvent - Link clicked in email
  • PermanentFailureEmailEvent - Permanent delivery failure (bounce)
  • TemporaryFailureEmailEvent - Temporary delivery issue
  • SpamComplaintsEmailEvent - Marked as spam
  • UnsubscribeEmailEvent - 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 order

Custom 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

πŸ§ͺ 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.0

Code 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

⭐ Show Your Support

Give a ⭐️ if this project helped you!