grim-reapper/laravel-advanced-email

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

Maintainers

Package info

github.com/grim-reapper/laravel-advanced-email

pkg:composer/grim-reapper/laravel-advanced-email

Transparency log

Statistics

Installs: 25

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

2.0.0 2026-07-16 22:07 UTC

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.