PHPackages                             grim-reapper/laravel-advanced-email - 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. grim-reapper/laravel-advanced-email

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

grim-reapper/laravel-advanced-email
===================================

An advanced email package for Laravel offering queuing, Blade/HTML templates, attachments, and dynamic configuration.

3.0.0(1mo ago)127MITPHPPHP ^8.2CI failing

Since May 21Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/grim-reapper/laravel-advanced-email)[ Packagist](https://packagist.org/packages/grim-reapper/laravel-advanced-email)[ RSS](/packages/grim-reapper-laravel-advanced-email/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (7)Dependencies (25)Versions (13)Used By (0)

Laravel Advanced Email
======================

[](#laravel-advanced-email)

A powerful package that enhances Laravel's email capabilities with advanced features for enterprise-level email management. Works standalone in single-tenant apps out of the box, and gains full tenant isolation with a couple lines of config in multi-tenant apps — see [Multi-Tenancy](#multi-tenancy) below.

Features
--------

[](#features)

- **Template Management**: Database-driven email templates with versioning support
- **Advanced Scheduling**: Schedule one-time and recurring emails with conditions, retries, and expiry
- **A/B Testing**: Run subject/content variants and automatically declare a winner by open or click rate
- **Multi-Provider Support**: Automatic failover between multiple email providers
- **Email Tracking**: Track email opens and link clicks for analytics
- **Comprehensive Analytics**: Detailed reporting on email performance, with a bundled (optional) dashboard
- **Attachment Handling**: Multiple ways to attach files to emails
- **Multi-Tenancy**: Optional, framework-agnostic tenant isolation — opt in without touching any code that doesn't need it
- **Bounce/Complaint Webhooks**: Ingests real delivery events from SES, Mailgun, Postmark, SendGrid, Mailjet, SMTP2GO, or any other ESP via a config-driven generic driver
- **Suppression List**: Auto-suppresses hard bounces/complaints, honors unsubscribes, checked before every send
- **Send-Rate Limiting**: Optional throttling built on Laravel's own rate limiter
- **GDPR Export/Erasure**: Find, export, or redact/delete everything stored about a recipient
- **JSON REST API**: Optional, fully authenticated API for non-Blade frontends (React, Next.js, mobile, ...)

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

[](#requirements)

- PHP 8.2+
- Laravel 9, 10, 11, 12, or 13

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

[](#installation)

```
composer require grim-reapper/laravel-advanced-email
```

Publish the configuration:

```
php artisan vendor:publish --provider="GrimReapper\AdvancedEmail\AdvancedEmailServiceProvider" --tag="config"
```

Run the migrations:

```
php artisan migrate
```

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

[](#basic-usage)

```
use GrimReapper\AdvancedEmail\Facades\Email;

Email::to('recipient@example.com')
    ->subject('Welcome to Our Application')
    ->html('Welcome!Thank you for signing up.')
    ->send();
```

A/B Testing
-----------

[](#ab-testing)

Create a test with a few variants (each can override subject and/or content):

```
use GrimReapper\AdvancedEmail\Models\EmailAbTest;
use GrimReapper\AdvancedEmail\Models\EmailAbTestVariant;

$test = EmailAbTest::create([
    'uuid' => \Illuminate\Support\Str::uuid(),
    'name' => 'Welcome subject line',
    'status' => 'running',
    'winner_selection_strategy' => 'automatic_best_performing',
    'decision_metric' => 'open_rate', // or 'click_rate'
    'test_duration_hours' => 48,
]);

EmailAbTestVariant::create(['email_ab_test_id' => $test->id, 'name' => 'A', 'subject' => 'Welcome!', 'html_content' => '...', 'weight' => 50]);
EmailAbTestVariant::create(['email_ab_test_id' => $test->id, 'name' => 'B', 'subject' => "You're in", 'html_content' => '...', 'weight' => 50]);
```

Send for that test — `->abTest()` picks a variant via weighted-random selection (respecting each variant's `weight`); `->abTestVariant()` lets you target one directly:

```
Email::to($user->email)->from('hello@example.com')->abTest($test)->send();
```

The variant's subject/content is used as a default (an explicit `->subject()`/`->html()` still wins). Sending increments the variant's `sent_count`; opening/clicking the resulting email increments `open_count`/`click_count` automatically via the tracking routes. Run `php artisan email:process-ab-tests` (e.g. on a schedule) to declare a winner once `test_duration_hours` has elapsed — it sets `status` to `completed` and marks the best-performing variant `is_winner`.

Multi-Tenancy
-------------

[](#multi-tenancy)

By default, this package behaves exactly like a single-tenant package — nothing below is required. To isolate email logs, templates, and scheduled emails per tenant, enable it in `config/advanced_email.php` (or via env vars) and tell the package how to find the "current" tenant:

```
// config/advanced_email.php
'multitenancy' => [
    'enabled' => true,
    'resolver' => \App\Support\TenantResolver::class, // or leave null and bind it yourself
    'column' => 'tenant_id',
    'strict' => false,
],
```

Implement the resolver — it's a single method, with no dependency on any specific tenancy package:

```
namespace App\Support;

use GrimReapper\AdvancedEmail\Contracts\TenantResolver;

class TenantResolver implements TenantResolver
{
    public function resolveId(): int|string|null
    {
        // Any of these, depending on how your app tracks the current tenant:
        return auth()->user()?->tenant_id;
        // return tenancy()->tenant?->id;                 // stancl/tenancy
        // return \Spatie\Multitenancy\Models\Tenant::current()?->id; // spatie/laravel-multitenancy
    }
}
```

Or bind it directly in your own service provider instead of setting `resolver` in config (this always wins, regardless of registration order):

```
$this->app->bind(\GrimReapper\AdvancedEmail\Contracts\TenantResolver::class, TenantResolver::class);
```

Once enabled, `EmailLog`, `EmailTemplate`, `EmailTemplateVersion`, `ScheduledEmail`, and the A/B testing models are automatically scoped to the resolved tenant (a resolver returning `null` — the default, and always true outside HTTP context — means no scoping is applied, so console commands and background jobs correctly operate across all tenants). Every write is auto-attributed to the current tenant unless you set one explicitly:

```
Email::to('user@example.com')
    ->subject('Invoice')
    ->html($body)
    ->tenant($someOtherTenantId) // explicit override, wins over the resolver
    ->send();
```

Admin/cross-tenant tooling can bypass scoping on a per-query basis:

```
EmailLog::withoutTenancy()->count();      // all tenants
EmailLog::forTenant($specificId)->get();  // one specific tenant, regardless of the current one
```

Bounce/Complaint Webhooks
-------------------------

[](#bouncecomplaint-webhooks)

Point your ESP's webhook at `POST /email-tracking/webhooks/{provider}` and turn it on in config:

```
// config/advanced_email.php
'webhooks' => [
    'providers' => [
        'mailgun' => [
            'driver' => \GrimReapper\AdvancedEmail\Webhooks\Drivers\MailgunDriver::class,
            'enabled' => true,
            'signing_key' => env('MAILGUN_WEBHOOK_SIGNING_KEY'),
        ],
        // ses, postmark, sendgrid, mailjet, smtp2go — same shape, see the shipped config file for each provider's specific option(s).
    ],
],
```

Every provider entry is `enabled => false` by default — an unconfigured or disabled provider 404s rather than accepting unverified requests. Each built-in driver verifies the request using that ESP's actual documented scheme (HMAC-SHA256 for Mailgun, ECDSA for SendGrid, SNS message signatures for SES with SSRF-guarded certificate fetching, shared-secret/basic-auth for Postmark/Mailjet/SMTP2GO). Verified events update `EmailLog.status`, auto-suppress hard bounces and complaints, and fire `EmailBounced`/`EmailComplaint`/`EmailDelivered`/`EmailUnsubscribed` events you can listen for. Every event is also recorded in `email_webhook_events` for a full audit trail regardless of whether it matched a known send.

**Not using one of the six built-in providers?** Use the config-driven `GenericDriver` — no code required, just a field-mapping config:

```
'my_esp' => [
    'driver' => \GrimReapper\AdvancedEmail\Webhooks\Drivers\GenericDriver::class,
    'enabled' => true,
    'secret' => env('MY_ESP_WEBHOOK_SECRET'),
    'secret_header' => 'X-Webhook-Secret',
    'events_path' => 'events', // dot-path to an array of events, or null if the payload IS one event
    'field_map' => ['type' => 'event_type', 'message_id' => 'message.id', 'recipient' => 'recipient_email', 'timestamp' => 'occurred_at'],
    'type_map' => ['delivery' => 'delivered', 'hard_bounce' => 'bounced', 'complaint' => 'complained'],
],
```

Or implement `GrimReapper\AdvancedEmail\Contracts\WebhookDriver` yourself for full control over parsing/verification.

Suppression List
----------------

[](#suppression-list)

Checked automatically before every `send()`/`queue()` — no setup needed beyond `advanced_email.suppression.enabled` (on by default):

```
use GrimReapper\AdvancedEmail\Contracts\SuppressionRepository;

app(SuppressionRepository::class)->suppress('bounced@example.com', 'manual');
app(SuppressionRepository::class)->isSuppressed('bounced@example.com'); // true

Email::to(['bounced@example.com', 'ok@example.com'])->from('hello@example.com')->subject('Hi')->html('...')->send();
// 'filter' mode (default): sends only to ok@example.com
// 'block' mode: refuses the whole send
```

Bounce/complaint webhooks (above) suppress automatically. Recipient-initiated unsubscribes go through a signed link:

```
Email::unsubscribeUrl('user@example.com'); // put this in your own template/footer
```

Critical/system mail that must never be suppressed can opt out per-send: `->bypassSuppression()`.

Send-Rate Limiting
------------------

[](#send-rate-limiting)

Off by default. Turn it on and it applies automatically — queued sends (`->queue()`) release back onto the queue when throttled (via Laravel's own `RateLimited` job middleware); synchronous sends (`->send()`) throw `RateLimitExceededException` since there's no queue to wait on:

```
// config/advanced_email.php
'rate_limiting' => [
    'enabled' => true,
    'max_attempts' => 60,
    'decay_seconds' => 60,
    'key' => 'mailer', // or 'tenant', or 'mailer_tenant'
],
```

`->bypassRateLimit()` on a per-send basis for mail that must never be throttled.

GDPR Export/Erasure
-------------------

[](#gdpr-exporterasure)

```
php artisan email:gdpr-export user@example.com --output=export.json
php artisan email:gdpr-erase user@example.com --mode=anonymize   # or --mode=delete
```

Or call it directly from your own code (e.g. an account-deletion flow):

```
use GrimReapper\AdvancedEmail\Services\PrivacyService;

$data = app(PrivacyService::class)->export('user@example.com');
app(PrivacyService::class)->erase('user@example.com', tenantId: null, mode: 'anonymize');
```

`anonymize` redacts just the matching address within `to`/`cc`/`bcc`, keeping subjects/timestamps/engagement metrics intact for aggregate reporting; `delete` hard-deletes matching rows. Both search `EmailLog`, `ScheduledEmail`, `EmailSuppression`, and `EmailWebhookEvent`, scoped to a tenant or across all of them.

Frontend Framework Integration (React, Next.js, mobile, ...)
------------------------------------------------------------

[](#frontend-framework-integration-react-nextjs-mobile-)

This package is backend-only (it's a Laravel package) — if your frontend isn't Blade, don't reach for the bundled dashboard. Two options, usable together:

**1. The JSON REST API** — off by default (`advanced_email.api.enabled`), since it's a new PII-bearing surface:

```
// config/advanced_email.php
'api' => [
    'enabled' => true,
    'middleware' => ['api', 'auth:sanctum'], // swap for Passport, a custom API-key guard, whatever your app already uses
],
```

This gives you normal authenticated JSON endpoints under `/api/advanced-email/...` for email logs (read-only), templates (+ versions), A/B tests (+ variants), and suppressions — build your React/Next.js admin UI against them like any other API. `laravel/sanctum` is a *suggested*, not required, dependency — the default middleware assumes it (it ships with a standard Laravel install), but you're free to point `middleware` at anything else.

Every list endpoint (logs, templates, versions, A/B tests, variants, suppressions) is paginated by default, driven by `advanced_email.api.pagination`:

```
'pagination' => [
    'enabled' => true,       // false returns the full unpaginated collection instead
    'per_page' => 25,        // default page size; override per-request with ?per_page=
    'max_per_page' => 100,   // hard ceiling on ?per_page=, regardless of what's requested
],
```

**2. Optional live updates via broadcasting** — instead of polling the API, subscribe to a channel with Laravel Echo for real-time status:

```
// config/advanced_email.php
'broadcasting' => ['enabled' => true, 'channel' => 'advanced-email'],
```

```
// Your React/Next.js app, via Laravel Echo
Echo.private(`advanced-email.${tenantId}`).listen('.sent', (e) => { /* ... */ })
    .listen('.bounced', (e) => { /* ... */ });
```

Define the channel's authorization callback in your own `routes/channels.php` — the package can't do that for you since it's inherently app-specific.

Using Your Own Models
---------------------

[](#using-your-own-models)

Every model the package uses internally — `EmailLog`, `EmailLink`, `EmailTemplate`, `EmailTemplateVersion`, `ScheduledEmail`, `EmailAbTest`, `EmailAbTestVariant`, `EmailSuppression`, `EmailWebhookEvent` — is resolved through `GrimReapper\AdvancedEmail\Support\Models` rather than referenced directly anywhere in the package's services, controllers, jobs, or commands. That means you can point any of them at your own subclass — to add relationships to your own app's models, extra casts/accessors, custom scopes, or override behavior — without patching this package:

```
// config/advanced_email.php
'models' => [
    'email_template' => \App\Models\Email\Template::class, // must extend GrimReapper\AdvancedEmail\Models\EmailTemplate
    // ... the rest default to the package's own models when left null
],
```

`EmailLog` is the one exception — it's swapped via the pre-existing `logging.database.model` key instead of a key here, since that already served this exact purpose:

```
'logging' => [
    'database' => [
        'model' => \App\Models\Email\Log::class, // must extend GrimReapper\AdvancedEmail\Models\EmailLog
    ],
],
```

Your replacement class must extend the corresponding package model — it's still expected to have the same table/columns; this only changes which class gets instantiated/queried, not the underlying schema.

Testing
-------

[](#testing)

```
composer install
composer test
```

Documentation
-------------

[](#documentation)

For detailed documentation, please refer to the following guides:

- [Full Documentation](docs/documentation.md)
- [Installation Guide](docs/installation.md)
- [Configuration Guide](docs/configuration-guide.md)
- [Usage Guide](docs/usage-guide.md)
- [API Reference](docs/api-reference.md)
- [Changelog](CHANGELOG.md)

License
-------

[](#license)

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

###  Health Score

45

—

FairBetter than 91% of packages

Maintenance94

Actively maintained with recent releases

Popularity10

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity57

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 67.6% 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 ~46 days

Recently: every ~86 days

Total

10

Last Release

31d ago

Major Versions

1.3.1 → 2.0.02026-07-16

2.0.0 → 3.0.02026-07-17

PHP version history (2 changes)1.2.1PHP ^8.1

2.0.0PHP ^8.2

### Community

Maintainers

![](https://www.gravatar.com/avatar/7252105628ffa6f064585370161626c7c72e68dd2e74ee66aa50d5894543c183?d=identicon)[webz2feel](/maintainers/webz2feel)

---

Top Contributors

[![grim-reapper](https://avatars.githubusercontent.com/u/7957389?v=4)](https://github.com/grim-reapper "grim-reapper (23 commits)")[![google-labs-jules[bot]](https://avatars.githubusercontent.com/in/842251?v=4)](https://github.com/google-labs-jules[bot] "google-labs-jules[bot] (11 commits)")

---

Tags

laravelmailemailqueueattachment

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/grim-reapper-laravel-advanced-email/health.svg)

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

###  Alternatives

[illuminate/notifications

The Illuminate Notifications package.

483.1M1.2k](/packages/illuminate-notifications)[psalm/plugin-laravel

Psalm plugin for Laravel

3345.4M354](/packages/psalm-plugin-laravel)[laravel/scout

Laravel Scout provides a driver based solution to searching your Eloquent models.

1.7k57.2M682](/packages/laravel-scout)[laravel/ai

The official AI SDK for Laravel.

1.1k4.6M319](/packages/laravel-ai)[pressbooks/pressbooks

Pressbooks is an open source book publishing tool built on a WordPress multisite platform. Pressbooks outputs books in multiple formats, including PDF, EPUB, web, and a variety of XML flavours, using a theming/templating system, driven by CSS.

45844.8k1](/packages/pressbooks-pressbooks)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9922.4M146](/packages/roots-acorn)

PHPackages © 2026

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