ghadeer / lara-mail-preview
Secure, configurable browser previews for Laravel mailables.
Requires
- php: ^8.2
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^10.0|^11.0
- phpunit/phpunit: ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Preview Laravel mailables in the browser without delivering email. Lara Mail Preview is intended for local development and protected development environments.
Requirements
| Package version | Laravel | PHP |
|---|---|---|
| 2.x | 12.x, 13.x | 8.2–8.5 (Laravel 13 requires 8.3+) |
Install
composer require ghadeer/lara-mail-preview php artisan vendor:publish --tag=mail-preview-config
The preview route is enabled only in the local environment by default. Visit /mail-preview after installing. To use it on a shared development system, add authentication or your own authorization middleware first:
'environments' => ['local', 'development'], 'middleware' => ['web', 'auth', 'can:preview-mail'],
Never add a public route or enable this package in production. Rendering a mailable may execute application code, query data, or use external services.
Configuration
config/mail-preview.php controls the route, enabled environments, middleware, discovery locations and model selects. Mailables in app/Mail are discovered when they are Composer-autoloadable and extend Illuminate\Mail\Mailable.
For mailables outside that location, or to make the allow-list completely explicit, configure them directly:
'mailables' => [ App\Mail\WelcomeMail::class, App\Mail\Billing\InvoiceMail::class => ['label' => 'Invoice'], ], 'paths' => [ app_path('Mail'), 'Domain\\Billing\\Mail' => app_path('../src/Billing/Mail'), ],
Only discovered or configured mailables can be previewed. Requests cannot instantiate arbitrary class names.
Complex mailables
The UI safely supports strings, integers, floats, booleans, JSON arrays, Eloquent models and backed/pure enums. Complex objects, union types and intersection types need a trusted preview scenario:
'mailables' => [ App\Mail\ReceiptMail::class => [ 'label' => 'Latest receipt', 'scenario' => function (): App\Mail\ReceiptMail { return new App\Mail\ReceiptMail( App\Models\Receipt::query()->latest()->firstOrFail(), ); }, ], ],
The scenario is application code and is trusted by definition. Prefer an invokable class or static method if your application caches configuration.
Model inputs
Model choices are capped at 100 by default and use the first available label column (name, title, description, author, email, last_name, first_name, then the model key). Scope sensitive data with a configured query callable, or compose a human-friendly label with a callback:
'models' => [ 'queries' => [ App\Models\Customer::class => fn (App\Models\Customer $customer) => $customer->newQuery()->where('account_id', auth()->user()->account_id), ], 'label_callbacks' => [ App\Models\Customer::class => fn (App\Models\Customer $customer) => "{$customer->first_name} {$customer->last_name} <{$customer->email}>", ], ],
Use authorization middleware as the primary access control; the query callback further limits which model records can be chosen.
Preview output
The page shows HTML, plain text and envelope metadata (subject, addresses and tags). HTML is placed in a sandboxed iframe so email styles and scripts cannot affect the preview application. This is a rendering isolation boundary, not a reason to expose untrusted emails publicly.
Development
composer update
composer test
The GitHub Actions matrix runs Laravel 12 on PHP 8.2 and Laravel 13 on PHP 8.3–8.5.
Releasing
This repository uses Git tags as the package version source. Do not add a Composer version field for Packagist/VCS releases. After merging a release, create and push a semantic tag, for example v2.0.0.