PHPackages                             r0bdiabl0/laravel-email-tracker - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Mail &amp; Notifications](/categories/mail)
4. /
5. r0bdiabl0/laravel-email-tracker

ActiveLibrary[Mail &amp; Notifications](/categories/mail)

r0bdiabl0/laravel-email-tracker
===============================

Multi-provider email tracking for Laravel - track opens, clicks, bounces, complaints across SES, Resend, Postal, and more

v1.6.1(3mo ago)0180↓50%12MITPHPPHP ^8.2

Since Jan 10Pushed 3mo agoCompare

[ Source](https://github.com/r0bdiabl0/laravel-email-tracker)[ Packagist](https://packagist.org/packages/r0bdiabl0/laravel-email-tracker)[ RSS](/packages/r0bdiabl0-laravel-email-tracker/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (20)Versions (19)Used By (2)

Laravel Email Tracker
=====================

[](#laravel-email-tracker)

[![Latest Version on Packagist](https://camo.githubusercontent.com/dd3a26f286e21c6173d841e5748f225e22a434a7d3d4c53fb14290429f698abb/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f723062646961626c302f6c61726176656c2d656d61696c2d747261636b65722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/r0bdiabl0/laravel-email-tracker)[![Total Downloads](https://camo.githubusercontent.com/a54fec7fc4c265e5762512f3dd3e0581a270ef11b3da0434dcd76bac8f743fca/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f723062646961626c302f6c61726176656c2d656d61696c2d747261636b65722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/r0bdiabl0/laravel-email-tracker)[![License](https://camo.githubusercontent.com/755cf9c604a93bbb87470157485da906576a38f8a0e30c525fe521ea2ddad0f9/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f723062646961626c302f6c61726176656c2d656d61696c2d747261636b65722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/r0bdiabl0/laravel-email-tracker)

A **multi-provider email tracking and bounce management package** for Laravel 11+ that provides unified tracking for opens, clicks, bounces, complaints, and deliveries across **AWS SES, Resend, Postal, Mailgun, SendGrid, and Postmark**. Includes optional suppression to automatically skip sending to problematic addresses.

Table of Contents
-----------------

[](#table-of-contents)

- [What This Package Does](#what-this-package-does)
- [What This Package Does NOT Do](#what-this-package-does-not-do)
- [Requirements](#requirements)
- [Installation](#installation)
- [Configuration](#configuration)
- [Basic Usage](#basic-usage)
- [Webhook Setup](#webhook-setup)
    - [AWS SES Setup](#aws-ses-setup)
    - [Resend Setup](#resend-setup)
    - [Mailgun Setup](#mailgun-setup)
    - [SendGrid Setup](#sendgrid-setup)
    - [Postmark Setup](#postmark-setup)
    - [Postal Setup](#postal-setup)
- [Security Considerations](#security-considerations)
- [One-Click Unsubscribe (RFC 8058)](#one-click-unsubscribe-rfc-8058)
- [Events](#events)
- [Suppression (Bounce Management)](#suppression-bounce-management)
- [Database Schema](#database-schema)
- [Querying Data](#querying-data)
- [Migrating from juhasev/laravel-ses](#migrating-from-juhasevlaravel-ses)
- [Admin Panel Plugins](#admin-panel-plugins)
- [Laravel Boost AI Integration](#laravel-boost-ai-integration)
- [Extending](#extending)
    - [Custom Providers](#custom-providers)
    - [Custom Models](#custom-models)
- [Testing](#testing)
- [Troubleshooting](#troubleshooting)
- [Contributing](#contributing)
- [License](#license)
- [Credits](#credits)

What This Package Does
----------------------

[](#what-this-package-does)

- **Tracks Sent Emails** - Stores records of all emails sent through the package with their message IDs
- **Open Tracking** - Injects a 1x1 tracking pixel to detect when recipients open emails
- **Link Click Tracking** - Rewrites links to track when recipients click them, with click counts
- **Bounce Handling** - Receives and processes bounce notifications from email providers via webhooks
- **Complaint Handling** - Tracks spam complaints reported by recipients
- **Delivery Confirmation** - Records successful deliveries reported by email providers
- **Batch Grouping** - Organize emails into named batches for campaigns or bulk sends
- **Multi-Provider Support** - Unified interface across 6 major email providers
- **Suppression** - Optionally skip sending to previously bounced or complained addresses (bounce management)
- **One-Click Unsubscribe** - RFC 8058 compliant List-Unsubscribe headers for improved deliverability
- **Event Dispatching** - Laravel events for all tracking activities for your own listeners

What This Package Does NOT Do
-----------------------------

[](#what-this-package-does-not-do)

- **Does NOT send emails** - This package tracks emails sent via Laravel's mail system. You still need to configure Laravel Mail with your provider (SES, Mailgun, etc.)
- **Does NOT provide SMTP services** - You need your own email provider account
- **Does NOT guarantee open tracking accuracy** - Many email clients block tracking pixels. Open tracking should be considered a lower-bound estimate
- **Does NOT track replies** - This package tracks delivery events, not incoming mail
- **Does NOT provide analytics dashboards** - It stores data in your database; you build your own reports or use tools like Filament
- **Does NOT provide a template builder** - You design emails using Laravel's Mailable and Blade views (which are fully supported and tracked)
- **Does NOT replace your email provider's dashboard** - It supplements it with data in your own database

Requirements
------------

[](#requirements)

- PHP 8.2+
- Laravel 11.0+
- An email provider account (AWS SES, Resend, Postal, Mailgun, SendGrid, or Postmark)

Installation
------------

[](#installation)

```
composer require r0bdiabl0/laravel-email-tracker
```

Run the install command:

```
php artisan email-tracker:install
```

This will:

1. Publish the configuration file to `config/email-tracker.php`
2. Publish the migrations
3. Optionally run the migrations

Configuration
-------------

[](#configuration)

### Environment Variables

[](#environment-variables)

Add these to your `.env` file:

```
# =============================================================================
# CORE SETTINGS
# =============================================================================

# Default provider (ses, resend, postal, mailgun, sendgrid, postmark)
EMAIL_TRACKER_DEFAULT_PROVIDER=ses

# Optional table prefix (leave empty for no prefix)
EMAIL_TRACKER_TABLE_PREFIX=

# Enable/disable route registration
EMAIL_TRACKER_ROUTES_ENABLED=true

# Route prefix for all email tracker routes (default: email-tracker)
EMAIL_TRACKER_ROUTE_PREFIX=email-tracker

# Debug logging (disable in production)
EMAIL_TRACKER_DEBUG=false

# Log message prefix
EMAIL_TRACKER_LOG_PREFIX=EMAIL-TRACKER

# =============================================================================
# PROVIDER SETTINGS
# =============================================================================

# Enable/disable providers
EMAIL_TRACKER_SES_ENABLED=true
EMAIL_TRACKER_RESEND_ENABLED=false
EMAIL_TRACKER_POSTAL_ENABLED=false
EMAIL_TRACKER_MAILGUN_ENABLED=false
EMAIL_TRACKER_SENDGRID_ENABLED=false
EMAIL_TRACKER_POSTMARK_ENABLED=false

# AWS SES specific settings
EMAIL_TRACKER_SNS_VALIDATOR=true              # Validate SNS message signatures (recommended)

# Webhook signing secrets (provider-specific)
RESEND_WEBHOOK_SECRET=whsec_...               # Resend: Svix webhook signature
MAILGUN_WEBHOOK_SIGNING_KEY=key-...           # Mailgun: HMAC-SHA256 signing key
SENDGRID_VERIFICATION_KEY="-----BEGIN..."     # SendGrid: ECDSA public key (PEM format)
POSTAL_WEBHOOK_KEY=your-secret-key            # Postal: X-Postal-Webhook-Key header
POSTMARK_WEBHOOK_TOKEN=your-token             # Postmark: X-Postmark-Webhook-Token header

# =============================================================================
# SUPPRESSION / BOUNCE MANAGEMENT (disabled by default)
# =============================================================================

# Automatically skip sending to problematic addresses (recommended for production)
EMAIL_TRACKER_SKIP_BOUNCED=false              # Set true to suppress bounced addresses
EMAIL_TRACKER_SKIP_COMPLAINED=false           # Set true to suppress addresses that complained (spam)

# =============================================================================
# ONE-CLICK UNSUBSCRIBE (RFC 8058)
# =============================================================================

EMAIL_TRACKER_UNSUBSCRIBE_ENABLED=false       # Enable List-Unsubscribe headers
EMAIL_TRACKER_UNSUBSCRIBE_MAILTO=             # Optional mailto: fallback address
EMAIL_TRACKER_UNSUBSCRIBE_EXPIRATION=0        # Signature expiration in hours (0 = never)
EMAIL_TRACKER_UNSUBSCRIBE_REDIRECT=           # Redirect URL after unsubscribe (null = JSON)

# =============================================================================
# METADATA / STORAGE OPTIONS
# =============================================================================

# Store raw webhook payloads in metadata column (default: false)
# When false, metadata is still available in events for real-time processing
EMAIL_TRACKER_STORE_METADATA=false

# =============================================================================
# OTHER OPTIONS
# =============================================================================

# Enable legacy routes for backwards compatibility with juhasev/laravel-ses
EMAIL_TRACKER_LEGACY_ROUTES=false
```

### Table Names

[](#table-names)

By default, tables are created without a prefix:

- `sent_emails`
- `email_opens`
- `email_bounces`
- `email_complaints`
- `email_links`
- `batches`

With a prefix like `tracker`:

- `tracker_sent_emails`
- `tracker_email_bounces`
- etc.

### Enable Providers

[](#enable-providers)

Enable only the providers you use:

```
// config/email-tracker.php
'providers' => [
    'ses' => [
        'enabled' => env('EMAIL_TRACKER_SES_ENABLED', true),
        'sns_validator' => true, // Validate SNS message signatures
    ],
    'resend' => [
        'enabled' => env('EMAIL_TRACKER_RESEND_ENABLED', false),
        'webhook_secret' => env('RESEND_WEBHOOK_SECRET'),
    ],
    'mailgun' => [
        'enabled' => env('EMAIL_TRACKER_MAILGUN_ENABLED', false),
        'webhook_signing_key' => env('MAILGUN_WEBHOOK_SIGNING_KEY'),
    ],
    // ... etc.
],
```

### Transport Configuration

[](#transport-configuration)

The package provides custom Symfony transports for providers that need HTTP API access for full tracking support. Configure these in your `config/mail.php`:

```
// config/mail.php
'mailers' => [
    // Resend API transport (recommended for full tracking)
    'resend' => [
        'transport' => 'resend',
        'key' => env('RESEND_API_KEY'),
    ],

    // Postal API transport (recommended for full tracking)
    'postal' => [
        'transport' => 'postal',
        'url' => env('POSTAL_URL'),
        'key' => env('POSTAL_API_KEY'),
    ],

    // SES, Mailgun, Postmark use Laravel/Symfony built-in transports
    // SendGrid uses SMTP
],
```

**Provider Transport Summary:**

ProviderTransport TypeSDK RequiredAWS SESLaravel built-in (`ses`)`aws/aws-sdk-php` (included)ResendPackage transport (`resend`)`resend/resend-php` (optional)PostalPackage transport (`postal`)`postal/postal` (optional)MailgunSymfony built-in (`mailgun`)`symfony/mailgun-mailer`PostmarkSymfony built-in (`postmark`)`symfony/postmark-mailer`SendGridSMTPNoneInstall optional SDKs as needed:

```
# For Resend API transport
composer require resend/resend-php

# For Postal API transport
composer require postal/postal

# For Mailgun transport
composer require symfony/mailgun-mailer

# For Postmark transport
composer require symfony/postmark-mailer
```

### Using Multiple Providers

[](#using-multiple-providers)

You can enable multiple providers simultaneously and switch between them per-send:

```
# Set your default provider
EMAIL_TRACKER_DEFAULT_PROVIDER=ses

# Enable multiple providers
EMAIL_TRACKER_SES_ENABLED=true
EMAIL_TRACKER_RESEND_ENABLED=true
EMAIL_TRACKER_MAILGUN_ENABLED=true
```

```
use R0bdiabl0\EmailTracker\Facades\EmailTracker;

// Uses the default provider (from EMAIL_TRACKER_DEFAULT_PROVIDER)
EmailTracker::enableAllTracking()
    ->to('user@example.com')
    ->send(new WelcomeMail($user));

// Override to use a specific provider for this send
// Automatically switches to the Resend transport
EmailTracker::provider('resend')
    ->enableAllTracking()
    ->to('user@example.com')
    ->send(new WelcomeMail($user));

// Use Mailgun for transactional emails
EmailTracker::provider('mailgun')
    ->enableAllTracking()
    ->to('user@example.com')
    ->send(new OrderConfirmation($order));
```

Each provider has its own webhook endpoint. When you receive bounce/complaint notifications, they'll be routed to the correct handler based on the URL:

- SES: `POST /email-tracker/webhook/ses`
- Resend: `POST /email-tracker/webhook/resend`
- Mailgun: `POST /email-tracker/webhook/mailgun`
- etc.

The `provider` column in the database tracks which service sent each email, allowing you to query statistics by provider.

Basic Usage
-----------

[](#basic-usage)

### Sending Tracked Emails

[](#sending-tracked-emails)

```
use R0bdiabl0\EmailTracker\Facades\EmailTracker;

// Enable all tracking (opens, links, bounces, complaints, deliveries)
// Note: This does NOT enable unsubscribe headers - use enableUnsubscribeHeaders() separately
EmailTracker::enableAllTracking()
    ->to('user@example.com')
    ->send(new WelcomeMail($user));

// With unsubscribe headers for bulk/marketing emails
EmailTracker::enableAllTracking()
    ->enableUnsubscribeHeaders()  // Add RFC 8058 List-Unsubscribe headers
    ->to('user@example.com')
    ->send(new MarketingMail($user));

// With batch grouping for campaigns
EmailTracker::enableAllTracking()
    ->setBatch('welcome-campaign-2024')
    ->to('user@example.com')
    ->send(new WelcomeMail($user));

// Enable specific tracking only
EmailTracker::enableOpenTracking()
    ->enableLinkTracking()
    ->to('user@example.com')
    ->send(new WelcomeMail($user));

// Specify provider explicitly
EmailTracker::provider('resend')
    ->enableAllTracking()
    ->to('user@example.com')
    ->send(new WelcomeMail($user));
```

### Using the TracksWithEmail Trait (Optional)

[](#using-the-trackswithemail-trait-optional)

Add the trait to your Mailable for convenience methods:

```
use Illuminate\Mail\Mailable;
use R0bdiabl0\EmailTracker\Traits\TracksWithEmail;

class WelcomeMail extends Mailable
{
    use TracksWithEmail;

    public function build()
    {
        return $this->view('emails.welcome');
    }
}

// Static methods for quick sending
WelcomeMail::sendTracked('user@example.com', batch: 'welcome');
WelcomeMail::queueTracked(['user@example.com'], batch: 'welcome', queue: 'emails');
```

### Using the Notification Channel (Optional)

[](#using-the-notification-channel-optional)

```
use Illuminate\Notifications\Notification;
use R0bdiabl0\EmailTracker\Notifications\EmailTrackerChannel;

class WelcomeNotification extends Notification
{
    public function via($notifiable): array
    {
        return [EmailTrackerChannel::class];
    }

    public function toEmailTracker($notifiable): Mailable
    {
        return new WelcomeMail($notifiable);
    }
}
```

Webhook Setup
-------------

[](#webhook-setup)

Your email provider will send event notifications (bounces, complaints, deliveries) to these webhook URLs. You must configure these URLs in each provider's dashboard.

### Webhook URLs

[](#webhook-urls)

ProviderWebhook URLAWS SES`https://your-app.com/email-tracker/webhook/ses/bounce`
`https://your-app.com/email-tracker/webhook/ses/complaint`
`https://your-app.com/email-tracker/webhook/ses/delivery`Resend`https://your-app.com/email-tracker/webhook/resend`Postal`https://your-app.com/email-tracker/webhook/postal`Mailgun`https://your-app.com/email-tracker/webhook/mailgun`SendGrid`https://your-app.com/email-tracker/webhook/sendgrid`Postmark`https://your-app.com/email-tracker/webhook/postmark`### AWS SES Setup

[](#aws-ses-setup)

1. Create SNS topics for bounces, complaints, and deliveries in AWS Console
2. Add HTTPS subscriptions pointing to your webhook URLs
3. Configure your SES domain/email to publish to these SNS topics
4. The package automatically validates SNS message signatures

```
# Example: Create SNS subscription via AWS CLI
aws sns subscribe \
  --topic-arn arn:aws:sns:us-east-1:123456789:ses-bounces \
  --protocol https \
  --notification-endpoint https://your-app.com/email-tracker/webhook/ses/bounce
```

### Resend Setup

[](#resend-setup)

1. Go to Resend Dashboard &gt; Webhooks
2. Add a new webhook pointing to `https://your-app.com/email-tracker/webhook/resend`
3. Select events: `email.bounced`, `email.complained`, `email.delivered`
4. Copy the signing secret (starts with `whsec_`) to your `.env`

### Mailgun Setup

[](#mailgun-setup)

1. Go to Mailgun Dashboard &gt; Sending &gt; Webhooks
2. Add webhook URLs for Permanent Failures, Temporary Failures, and Delivered
3. Copy your webhook signing key to your `.env`

### SendGrid Setup

[](#sendgrid-setup)

1. Go to SendGrid Dashboard &gt; Settings &gt; Mail Settings &gt; Event Webhook
2. Set the HTTP POST URL to `https://your-app.com/email-tracker/webhook/sendgrid`
3. Select events: Bounced, Spam Reports, Delivered
4. Enable Event Webhook Security and copy the verification key

### Postmark Setup

[](#postmark-setup)

1. Go to Postmark &gt; Servers &gt; Your Server &gt; Webhooks
2. Add webhooks for Bounces, Spam Complaints, and Deliveries
3. Set the webhook URL and optionally configure Basic Auth for security

### Postal Setup

[](#postal-setup)

1. Go to your Postal server admin panel
2. Add a webhook endpoint pointing to `https://your-app.com/email-tracker/webhook/postal`
3. Configure the shared secret key in your `.env`

Security Considerations
-----------------------

[](#security-considerations)

### Webhook Signature Validation

[](#webhook-signature-validation)

All providers support webhook signature validation to ensure requests are authentic:

ProviderValidation MethodRequired ConfigAWS SESSNS certificate validationAutomaticResendSvix HMAC-SHA256`webhook_secret`MailgunHMAC-SHA256`webhook_signing_key`SendGridECDSA P-256`verification_key`PostmarkHeader token or Basic Auth`webhook_token`PostalHeader token`webhook_key`**Important**: In development, validation is skipped if no secret is configured. In production, always configure your webhook secrets.

### Protecting Webhook Routes

[](#protecting-webhook-routes)

The webhook routes are public by default (no auth middleware). This is required because email providers need to access them. Security is provided through signature validation.

If you need additional protection, you can:

1. Configure IP allowlists in your web server (nginx/Apache)
2. Add custom middleware in the config:

```
// config/email-tracker.php
'routes' => [
    'middleware' => ['throttle:60,1'], // Rate limiting
],
```

### CSRF Protection

[](#csrf-protection)

Webhook routes must be excluded from CSRF protection since they receive POST requests from external services. The package routes are loaded outside the `web` middleware group, but if your application applies CSRF middleware globally, you need to exclude the webhook routes.

Add to your `bootstrap/app.php` (Laravel 11+):

```
->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: [
        'email-tracker/webhook/*',
    ]);
})
```

Or in `app/Http/Middleware/VerifyCsrfToken.php` (Laravel 10):

```
protected $except = [
    'email-tracker/webhook/*',
];
```

One-Click Unsubscribe (RFC 8058)
--------------------------------

[](#one-click-unsubscribe-rfc-8058)

The package supports RFC 8058 compliant one-click unsubscribe headers, which are now required by Gmail, Yahoo, and other major email providers for bulk senders. This feature improves deliverability and helps you comply with sender requirements.

### How It Works

[](#how-it-works)

1. When enabled, the package adds `List-Unsubscribe` and `List-Unsubscribe-Post` headers to your emails
2. Email clients show an "Unsubscribe" button in their UI
3. When clicked, a POST request is sent to your app's signed unsubscribe endpoint
4. The package validates the signature and fires an `EmailUnsubscribeEvent`
5. **You handle the business logic** in your event listener

### Enabling Unsubscribe Headers

[](#enabling-unsubscribe-headers)

#### Option 1: Global (All Tracked Emails)

[](#option-1-global-all-tracked-emails)

```
EMAIL_TRACKER_UNSUBSCRIBE_ENABLED=true
```

#### Option 2: Per-Email

[](#option-2-per-email)

```
use R0bdiabl0\EmailTracker\Facades\EmailTracker;

EmailTracker::enableAllTracking()
    ->enableUnsubscribeHeaders()
    ->to('user@example.com')
    ->send(new NewsletterMail($user));
```

### Configuration

[](#configuration-1)

```
// config/email-tracker.php
'unsubscribe' => [
    // Enable one-click unsubscribe headers globally
    'enabled' => env('EMAIL_TRACKER_UNSUBSCRIBE_ENABLED', false),

    // Optional: Include a mailto: fallback (some older clients prefer this)
    'mailto' => env('EMAIL_TRACKER_UNSUBSCRIBE_MAILTO'),

    // Signature expiration in hours (0 = no expiration)
    'signature_expiration' => env('EMAIL_TRACKER_UNSUBSCRIBE_EXPIRATION', 0),

    // Redirect URL after unsubscribe (null = return JSON response)
    'redirect_url' => env('EMAIL_TRACKER_UNSUBSCRIBE_REDIRECT'),
],
```

### Handling Unsubscribe Events

[](#handling-unsubscribe-events)

Register a listener for the `EmailUnsubscribeEvent`:

```
use R0bdiabl0\EmailTracker\Events\EmailUnsubscribeEvent;

// In EventServiceProvider
protected $listen = [
    EmailUnsubscribeEvent::class => [
        \App\Listeners\HandleUnsubscribe::class,
    ],
];
```

```
namespace App\Listeners;

use R0bdiabl0\EmailTracker\Events\EmailUnsubscribeEvent;

class HandleUnsubscribe
{
    public function handle(EmailUnsubscribeEvent $event): void
    {
        $email = $event->email;
        $messageId = $event->messageId;
        $sentEmail = $event->sentEmail; // May be null

        // Update user preferences
        User::where('email', $email)
            ->update(['marketing_emails' => false]);

        // Or remove from specific mailing list based on batch
        if ($sentEmail && $sentEmail->batch) {
            MailingListSubscription::where('email', $email)
                ->where('list', $sentEmail->batch->name)
                ->delete();
        }

        Log::info("User unsubscribed", ['email' => $email]);
    }
}
```

### CSRF Protection

[](#csrf-protection-1)

The unsubscribe endpoint needs to be excluded from CSRF protection (it receives POST requests from external email clients):

```
// bootstrap/app.php (Laravel 11+)
->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: [
        'email-tracker/webhook/*',  // Adjust if using custom EMAIL_TRACKER_ROUTE_PREFIX
        'email-tracker/unsubscribe',
    ]);
})
```

> **Note:** If you configured a custom route prefix via `EMAIL_TRACKER_ROUTE_PREFIX`, update the CSRF exclusion paths accordingly.

### Security Recommendations

[](#security-recommendations)

- **Rate Limiting:** Consider adding rate limiting middleware to your `HandleUnsubscribe` listener or at the route level to prevent abuse: ```
    // In your event listener
    if (RateLimiter::tooManyAttempts('unsubscribe:' . $event->email, 5)) {
        Log::warning('Unsubscribe rate limit exceeded', ['email' => $event->email]);
        return;
    }
    RateLimiter::hit('unsubscribe:' . $event->email, 3600);
    ```
- **Signature Expiration:** For added security, set `signature_expiration` to expire unsubscribe links after a reasonable time (e.g., 720 hours / 30 days)

### What This Feature Does NOT Do

[](#what-this-feature-does-not-do)

- Does NOT manage subscription lists - you define what "unsubscribe" means for your app
- Does NOT store unsubscribe preferences - you update your own user/subscription models
- Does NOT decide per-list vs global unsubscribe - your listener implements this logic

Events
------

[](#events)

The package dispatches events for all tracking activities. Listen to these in your `EventServiceProvider`:

```
use R0bdiabl0\EmailTracker\Events\EmailSentEvent;
use R0bdiabl0\EmailTracker\Events\EmailDeliveryEvent;
use R0bdiabl0\EmailTracker\Events\EmailBounceEvent;
use R0bdiabl0\EmailTracker\Events\EmailComplaintEvent;
use R0bdiabl0\EmailTracker\Events\EmailOpenEvent;
use R0bdiabl0\EmailTracker\Events\EmailLinkClickEvent;
use R0bdiabl0\EmailTracker\Events\EmailUnsubscribeEvent;

protected $listen = [
    EmailBounceEvent::class => [
        \App\Listeners\HandleEmailBounce::class,
    ],
    EmailComplaintEvent::class => [
        \App\Listeners\HandleEmailComplaint::class,
    ],
    EmailUnsubscribeEvent::class => [
        \App\Listeners\HandleUnsubscribe::class,
    ],
];
```

### Example Listener

[](#example-listener)

```
namespace App\Listeners;

use R0bdiabl0\EmailTracker\Events\EmailBounceEvent;

class HandleEmailBounce
{
    public function handle(EmailBounceEvent $event): void
    {
        $bounce = $event->emailBounce;
        $email = $bounce->email;
        $type = $bounce->type; // 'Permanent' or 'Transient'
        $metadata = $bounce->metadata; // Raw webhook payload

        if ($type === 'Permanent') {
            // Mark user as having invalid email
            User::where('email', $email)->update(['email_valid' => false]);
        }

        // Access provider-specific diagnostic information from metadata
        // For SES: $metadata['bounce']['bouncedRecipients'][0]['diagnosticCode']
        // For Mailgun: $metadata['event-data']['delivery-status']['message']
        // For SendGrid: $metadata['reason']
        $diagnosticCode = $this->extractDiagnosticCode($metadata, $bounce->provider);

        // Log for monitoring
        Log::warning("Email bounced", [
            'email' => $email,
            'type' => $type,
            'provider' => $bounce->provider,
            'diagnostic_code' => $diagnosticCode,
        ]);
    }

    private function extractDiagnosticCode(?array $metadata, string $provider): ?string
    {
        if (!$metadata) {
            return null;
        }

        return match ($provider) {
            'ses' => $metadata['bounce']['bouncedRecipients'][0]['diagnosticCode'] ?? null,
            'mailgun' => $metadata['event-data']['delivery-status']['message'] ?? null,
            'sendgrid' => $metadata['reason'] ?? null,
            'postmark' => $metadata['Description'] ?? null,
            default => null,
        };
    }
}
```

Suppression (Bounce Management)
-------------------------------

[](#suppression-bounce-management)

Automatically skip sending to bounced or complained addresses. This is disabled by default - enable it to protect your sender reputation:

```
// config/email-tracker.php
'suppression' => [
    'skip_bounced' => true,    // Skip permanently bounced addresses
    'skip_complained' => true, // Skip addresses that filed complaints
],
```

When enabled, suppression works automatically across all sending methods:

- `EmailTracker::send()` facade
- `TracksWithEmail` trait on Mailables
- `EmailTrackerChannel` for Notifications

If a suppressed address is detected, an `AddressSuppressedException` is thrown with the email and reason.

### Manual Suppression Checking

[](#manual-suppression-checking)

You can also check suppression manually:

```
use R0bdiabl0\EmailTracker\Services\EmailValidator;

// Check if email should be blocked
if (EmailValidator::shouldBlock('user@example.com')) {
    return; // Don't send
}

// Get specific counts
$bounceCount = EmailValidator::getBounceCount('user@example.com');
$hasComplaint = EmailValidator::hasComplaint('user@example.com');

// Filter a list of emails
$validEmails = EmailValidator::filterBlockedEmails($emailList);
```

Database Schema
---------------

[](#database-schema)

The package creates the following tables (with optional prefix):

### sent\_emails

[](#sent_emails)

ColumnTypeDescriptionidbigintPrimary keyproviderstringEmail provider (ses, resend, etc.)message\_idstringProvider's message IDemailstringRecipient email addressbatch\_idbigintOptional batch referencesent\_attimestampWhen email was sentdelivered\_attimestampWhen delivery was confirmedbounce\_trackingbooleanWhether bounce tracking is enabledcomplaint\_trackingbooleanWhether complaint tracking is enableddelivery\_trackingbooleanWhether delivery tracking is enabled### email\_bounces

[](#email_bounces)

ColumnTypeDescriptionidbigintPrimary keyproviderstringEmail providersent\_email\_idbigintReference to sent emailtypestringBounce type (Permanent/Transient)emailstringBounced email addressbounced\_attimestampWhen bounce occurredmetadatajsonRaw webhook payload (for diagnostic details)### email\_complaints

[](#email_complaints)

ColumnTypeDescriptionidbigintPrimary keyproviderstringEmail providersent\_email\_idbigintReference to sent emailtypestringComplaint type (spam, etc.)emailstringComplaining email addresscomplained\_attimestampWhen complaint occurredmetadatajsonRaw webhook payload (for diagnostic details)### email\_opens

[](#email_opens)

ColumnTypeDescriptionidbigintPrimary keysent\_email\_idbigintReference to sent emailbeacon\_identifierstringUnique identifier for tracking pixelopened\_attimestampWhen email was opened### email\_links

[](#email_links)

ColumnTypeDescriptionidbigintPrimary keysent\_email\_idbigintReference to sent emaillink\_identifierstringUnique identifier for link trackingoriginal\_urltextOriginal link URLclickedbooleanWhether link has been clickedclick\_countintegerNumber of clicks### batches

[](#batches)

ColumnTypeDescriptionidbigintPrimary keynamestringBatch identifier### Metadata Storage Considerations

[](#metadata-storage-considerations)

The `metadata` column in `email_bounces` and `email_complaints` tables can store raw webhook payloads from email providers. This provides valuable diagnostic information but is **disabled by default**.

**Configuration:**

```
# Enable metadata storage (default: false)
EMAIL_TRACKER_STORE_METADATA=true
```

**Important:** Even when `store_metadata` is `false`, the raw webhook payload is still available in event listeners via the `metadata` property. This allows you to process diagnostic information in real-time without persisting it to the database.

```
// In your event listener - metadata is ALWAYS available
public function handle(EmailBounceEvent $event): void
{
    $metadata = $event->emailBounce->metadata; // Available regardless of store_metadata config
    $diagnosticCode = $metadata['bounce']['bouncedRecipients'][0]['diagnosticCode'] ?? null;

    // Process in real-time...
}
```

**When to enable persistent storage:**

- You need to analyze bounce/complaint patterns historically
- You want to debug delivery issues after the fact
- You're building reporting dashboards that query metadata
- You don't have real-time event listeners processing webhooks

**When to keep disabled (default):**

- You process events in real-time via listeners
- You store relevant data in your own application tables
- You want to minimize database storage
- You have PII concerns about storing raw payloads

**What metadata contains:**

- Full webhook payload from the email provider
- SMTP error codes and diagnostic messages
- Email addresses and timestamps
- Provider-specific debugging information

**Storage considerations:**

- Payloads vary by provider (typically 1-5 KB per record)
- High-volume senders should monitor database growth
- Consider implementing a cleanup job for old records

**Privacy considerations:**

- Metadata may contain email addresses (PII)
- Apply appropriate data retention policies
- Ensure database access controls are in place

**Example cleanup job:**

```
// Delete bounce/complaint records older than 90 days
EmailBounce::where('bounced_at', 'to($email)->send($mailable);
```

Enable legacy routes to keep old webhook URLs working:

```
EMAIL_TRACKER_LEGACY_ROUTES=true
```

Admin Panel Plugins
-------------------

[](#admin-panel-plugins)

### Filament Plugin

[](#filament-plugin)

For **Filament v3/v4** users, install the companion plugin for dashboard widgets, statistics, and resource pages:

```
composer require r0bdiabl0/laravel-email-tracker-filament
```

Features:

- **Dashboard Widgets** - Stats overview, delivery charts, health scores, recent activity
- **Resource Pages** - Browse, search, and filter sent emails, bounces, and complaints
- **Statistics Service** - Query aggregated stats for custom integrations

Register in your Filament panel provider:

```
use R0bdiabl0\EmailTrackerFilament\EmailTrackerFilamentPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        ->plugins([
            EmailTrackerFilamentPlugin::make(),
        ]);
}
```

See [r0bdiabl0/laravel-email-tracker-filament](https://github.com/r0bdiabl0/laravel-email-tracker-filament) for full documentation.

### Nova Plugin

[](#nova-plugin)

For **Laravel Nova v4/v5** users, install the companion plugin for resource management:

```
composer require r0bdiabl0/laravel-email-tracker-nova
```

Features:

- **Sent Emails Resource** - Browse, search, filter by provider and status
- **Bounces Resource** - View bounce records with type badges
- **Complaints Resource** - Track spam complaints
- **Read-Only** - Safe viewing without accidental modifications

The resources are auto-registered. See [r0bdiabl0/laravel-email-tracker-nova](https://github.com/r0bdiabl0/laravel-email-tracker-nova) for customization options.

Laravel Boost AI Integration
----------------------------

[](#laravel-boost-ai-integration)

This package includes [Laravel Boost](https://github.com/laravelboost/boost) AI guidelines and skills to help AI assistants generate correct code for your email tracking implementation.

When you run `php artisan boost:install` in your Laravel application, Boost automatically loads:

- **AI Guidelines** - Package overview, API examples, and configuration reference
- **Skills** - Interactive commands for common tasks:
    - `/send-tracked-email` - Send emails with tracking, batches, and unsubscribe headers
    - `/handle-email-events` - Create event listeners for bounces, complaints, opens, clicks
    - `/create-email-provider` - Build custom provider integrations
    - `/setup-suppression` - Configure bounce management

No additional configuration required - Boost discovers the package's AI resources automatically.

Extending
---------

[](#extending)

### Custom Providers

[](#custom-providers)

This package is fully extensible. You can add support for any email provider by implementing your own webhook handler.

**Step 1: Create your provider class**

Extend `AbstractProvider` which implements `EmailProviderInterface`:

```
namespace App\EmailProviders;

use Carbon\Carbon;
use Illuminate\Http\Request;
use R0bdiabl0\EmailTracker\DataTransferObjects\EmailEventData;
use R0bdiabl0\EmailTracker\Enums\EmailEventType;
use R0bdiabl0\EmailTracker\Providers\AbstractProvider;
use Symfony\Component\HttpFoundation\Response;

class CustomSmtpProvider extends AbstractProvider
{
    /**
     * Unique provider name (used in routes and database).
     */
    public function getName(): string
    {
        return 'custom-smtp';
    }

    /**
     * Handle incoming webhook from your email provider.
     */
    public function handleWebhook(Request $request, ?string $event = null): Response
    {
        $this->logRawPayload($request);

        // Validate webhook signature
        if (! $this->validateSignature($request)) {
            return response()->json(['error' => 'Invalid signature'], 403);
        }

        $payload = $request->all();
        $eventType = $payload['event_type'] ?? 'unknown';

        // Parse into standardized format
        $data = $this->parsePayload($payload);

        // Route to appropriate handler based on event type
        // The base class helpers expect EmailEventData objects
        return match ($eventType) {
            'bounce' => $this->processBounceEvent($data),
            'complaint' => $this->processComplaintEvent($data),
            'delivered' => $this->processDeliveryEvent($data),
            default => response()->json(['success' => true]),
        };
    }

    /**
     * Parse webhook payload into standardized EmailEventData.
     */
    public function parsePayload(array $payload): EmailEventData
    {
        return new EmailEventData(
            messageId: $payload['message_id'] ?? '',
            email: $payload['recipient'] ?? '',
            provider: $this->getName(),
            eventType: $this->mapEventType($payload['event_type'] ?? ''),
            timestamp: isset($payload['timestamp'])
                ? Carbon::parse($payload['timestamp'])
                : null,
            bounceType: $payload['bounce_type'] ?? null,
            metadata: $payload,
        );
    }

    /**
     * Validate webhook signature/authenticity.
     */
    public function validateSignature(Request $request): bool
    {
        $secret = $this->getConfig('webhook_secret');

        if (! $secret) {
            return true; // Skip validation if no secret configured
        }

        $signature = $request->header('X-Custom-Signature');
        $expectedSignature = hash_hmac('sha256', $request->getContent(), $secret);

        return hash_equals($expectedSignature, $signature ?? '');
    }

    /**
     * Map provider event types to EmailEventType enum.
     */
    protected function mapEventType(string $event): EmailEventType
    {
        return match ($event) {
            'bounce' => EmailEventType::Bounced,
            'complaint' => EmailEventType::Complained,
            'delivered' => EmailEventType::Delivered,
            'opened' => EmailEventType::Opened,
            'clicked' => EmailEventType::Clicked,
            default => EmailEventType::Sent,
        };
    }
}
```

**Step 2: Register your provider**

In your `AppServiceProvider` or a dedicated service provider:

```
use R0bdiabl0\EmailTracker\Facades\EmailTracker;
use App\EmailProviders\CustomSmtpProvider;

public function boot(): void
{
    EmailTracker::registerProvider('custom-smtp', CustomSmtpProvider::class);
}
```

**Step 3: Add configuration** (optional)

```
// config/email-tracker.php
'providers' => [
    // ... built-in providers ...

    'custom-smtp' => [
        'enabled' => env('EMAIL_TRACKER_CUSTOM_SMTP_ENABLED', true),
        'webhook_secret' => env('EMAIL_TRACKER_CUSTOM_SMTP_SECRET'),
    ],
],
```

**Step 4: Configure webhooks in your email provider**

Your custom provider's webhook endpoint is automatically registered at:

```
POST https://your-app.com/email-tracker/webhook/custom-smtp

```

Configure this URL in your email provider's dashboard/settings:

1. **Set the webhook URL** to `https://your-app.com/email-tracker/webhook/custom-smtp`
2. **Select event types** to receive (bounces, complaints, deliveries, opens, clicks)
3. **Configure authentication** - if your provider supports webhook signing:
    - Copy the signing secret/key from your provider
    - Add it to your `.env`: `EMAIL_TRACKER_CUSTOM_SMTP_SECRET=your-secret-here`
4. **Test the webhook** - most providers have a "send test" feature

The package handles routing automatically - any POST request to `/email-tracker/webhook/{provider-name}` will be routed to your provider's `handleWebhook()` method.

### AbstractProvider Helper Methods

[](#abstractprovider-helper-methods)

The `AbstractProvider` base class provides useful helper methods:

```
// Logging (respects EMAIL_TRACKER_DEBUG setting)
$this->logDebug('Processing event');
$this->logError('Failed to process');
$this->logInfo('Event received');
$this->logRawPayload($request); // Log full webhook payload

// Configuration access
$secret = $this->getConfig('webhook_secret');

// Event processing helpers - pass an EmailEventData object
// These create database records and fire events automatically
$data = $this->parsePayload($payload);  // You implement this
$this->processBounceEvent($data);       // Creates EmailBounce record
$this->processComplaintEvent($data);    // Creates EmailComplaint record
$this->processDeliveryEvent($data);     // Updates SentEmail.delivered_at
```

**Example using the helper methods in your handleWebhook():**

```
public function handleWebhook(Request $request, ?string $event = null): Response
{
    $this->logRawPayload($request);

    if (! $this->validateSignature($request)) {
        return response()->json(['error' => 'Invalid signature'], 403);
    }

    $payload = $request->all();
    $eventType = $payload['event_type'] ?? 'unknown';

    // Parse into standardized format
    $data = $this->parsePayload($payload);

    // Use base class event processors
    return match ($eventType) {
        'bounce' => $this->processBounceEvent($data),
        'complaint' => $this->processComplaintEvent($data),
        'delivered' => $this->processDeliveryEvent($data),
        default => response()->json(['success' => true]),
    };
}
```

### Custom Models

[](#custom-models)

Override default models:

```
// config/email-tracker.php
'models' => [
    'sent_email' => \App\Models\TrackedEmail::class,
    'email_bounce' => \App\Models\CustomBounce::class,
    // ...
],
```

Your custom model should extend the package model or implement the contract:

```
namespace App\Models;

use R0bdiabl0\EmailTracker\Models\SentEmail as BaseSentEmail;

class TrackedEmail extends BaseSentEmail
{
    // Add custom methods or relationships
    public function user()
    {
        return $this->belongsTo(User::class, 'email', 'email');
    }
}
```

Testing
-------

[](#testing)

```
# Run tests
composer test

# Run with coverage
composer test-coverage

# Static analysis
composer analyse

# Code formatting
composer format
```

Troubleshooting
---------------

[](#troubleshooting)

### Webhooks not receiving data

[](#webhooks-not-receiving-data)

1. Verify the webhook URL is accessible from the internet
2. Check your web server logs for incoming requests
3. Enable debug logging: `EMAIL_TRACKER_DEBUG=true`
4. Verify signature validation secrets are correct
5. Check Laravel logs for validation errors

### Open tracking not working

[](#open-tracking-not-working)

1. Open tracking requires HTML emails (not plain text)
2. Many email clients block tracking pixels by default
3. Gmail, Apple Mail, and others may proxy images
4. Consider open tracking as approximate data only

### Message IDs not matching

[](#message-ids-not-matching)

1. Ensure you're storing the message ID from the send response
2. Different providers format message IDs differently
3. Check that the same message ID format is used in webhooks

Contributing
------------

[](#contributing)

Contributions are welcome! Please feel free to submit a Pull Request.

1. Fork the repository
2. Create a feature branch
3. Make your changes with tests
4. Run `composer test` and `composer analyse`
5. Submit a pull request

For bugs and feature requests, please [open an issue](https://github.com/r0bdiabl0/laravel-email-tracker/issues).

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE) for more information.

Credits
-------

[](#credits)

- [Robert Pettique](https://github.com/r0bdiabl0) - Author and maintainer
- Based on the excellent work from [juhasev/laravel-ses](https://github.com/juhasev/laravel-ses)

###  Health Score

42

—

FairBetter than 90% of packages

Maintenance80

Actively maintained with recent releases

Popularity14

Limited adoption so far

Community11

Small or concentrated contributor base

Maturity55

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

18

Last Release

105d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/432557?v=4)[Robert](/maintainers/r0bdiabl0)[@r0bdiabl0](https://github.com/r0bdiabl0)

---

Top Contributors

[![r0bdiabl0](https://avatars.githubusercontent.com/u/432557?v=4)](https://github.com/r0bdiabl0 "r0bdiabl0 (39 commits)")

---

Tags

laravelpostalemailtrackingsendgridresendmailgunemail marketinganalyticssespostmarkBounces

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/r0bdiabl0-laravel-email-tracker/health.svg)

```
[![Health](https://phpackages.com/badges/r0bdiabl0-laravel-email-tracker/health.svg)](https://phpackages.com/packages/r0bdiabl0-laravel-email-tracker)
```

###  Alternatives

[s-ichikawa/laravel-sendgrid-driver

This library adds a 'sendgrid' mail driver to Laravel.

4139.3M1](/packages/s-ichikawa-laravel-sendgrid-driver)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.5k25.9M107](/packages/laravel-cashier)[laravel-notification-channels/telegram

Telegram Notifications Channel for Laravel

1.1k3.4M35](/packages/laravel-notification-channels-telegram)[spatie/laravel-health

Monitor the health of a Laravel application

86910.0M83](/packages/spatie-laravel-health)[juhasev/laravel-ses

Allows you to track opens, deliveries, bounces, complaints and clicked links when sending emails through Laravel and Amazon SES

1710.0k](/packages/juhasev-laravel-ses)[laravel/cashier-paddle

Cashier Paddle provides an expressive, fluent interface to Paddle's subscription billing services.

264778.4k3](/packages/laravel-cashier-paddle)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
