grim-reapper / laravel-advanced-email
An advanced email package for Laravel offering queuing, Blade/HTML templates, attachments, and dynamic configuration.
Package info
github.com/grim-reapper/laravel-advanced-email
pkg:composer/grim-reapper/laravel-advanced-email
Requires
- php: ^8.2
- illuminate/contracts: ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/filesystem: ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/mail: ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/queue: ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/support: ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0
Requires (Dev)
- orchestra/testbench: ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0
- phpunit/phpunit: ^9.0 || ^10.0 || ^11.0 || ^12.0
README
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 below.
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
Requirements
- PHP 8.2+
- Laravel 9, 10, 11, 12, or 13
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
use GrimReapper\AdvancedEmail\Facades\Email; Email::to('recipient@example.com') ->subject('Welcome to Our Application') ->html('<h1>Welcome!</h1><p>Thank you for signing up.</p>') ->send();
A/B 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
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
Testing
composer install
composer test
Documentation
For detailed documentation, please refer to the following guides:
License
The MIT License (MIT). Please see License File for more information.