Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Mail Preview Laravel Package

themsaid/laravel-mail-preview

Laravel Mail Preview captures outgoing emails and lets you inspect them in the browser at a dedicated route. It stores sent messages for easy debugging, so you can review HTML and plain-text content during development without digging through logs or real inboxes.

View on GitHub
Deep Wiki
Context7

Getting Started

The package registers a custom mail transport called preview. Instead of sending, it writes each message to disk as an HTML file and exposes a small browser UI to inspect them — perfect for iterating on transactional mail locally without an SMTP inbox or a real recipient.

Minimal setup:

composer require spatie/laravel-mail-preview
php artisan vendor:publish --provider="Spatie\MailPreview\MailPreviewServiceProvider"

Register a dedicated mailer in config/mail.php (Laravel 9+ shape):

'mailers' => [
    // ...
    'preview' => [
        'transport' => 'preview',
    ],
],

Point your local environment at it and clear cached config:

MAIL_MAILER=preview
php artisan config:clear

Trigger any mailable — a route, a Tinker call, a seeded registration flow — then open the preview route (default /mail-preview). You'll see an index of captured emails; clicking one renders the HTML exactly as it would be sent.

Where to look first:

  • The published config file — it controls the preview storage path, the route, the file-name date format, and which real driver (if any) the preview transport delegates to after capturing.
  • The service provider — to see how the route is registered and whether it's gated by environment.
  • storage/framework/emails/ (or whatever preview_path resolves to) — the raw HTML files are there if the UI ever misbehaves.

First real use case: you're building a welcome/reset-password/receipt email and need to check layout, subject, and variables without spamming yourself or wiring up Mailtrap. Send it once, refresh the preview route, iterate on the Blade view.

Implementation Patterns

Dedicated mailer, not a global override. Keep the preview mailer as a separate entry in config/mail.php and switch via MAIL_MAILER. This lets teammates keep log or smtp locally while you opt in, and avoids accidentally committing a .env change that breaks CI.

Preview a specific mailable on demand. You don't have to flip the whole app:

Mail::mailer('preview')->to('[email protected]')->send(new OrderShipped($order));

Useful in a Tinker session or a throwaway route when you only want to check one template.

Batch render for design review. A tiny artisan command that loops over every mailable with fixture data gives you a full set of previews in one pass — ideal before a release or when a designer wants to review all templates at once:

foreach ([new WelcomeMail($user), new ReceiptMail($order)] as $mailable) {
    Mail::mailer('preview')->to('[email protected]')->send($mailable);
}

Because 6.2.3 added a Unix timestamp (U) to the default file-name date format, a tight loop that renders several mailables within the same second no longer overwrites earlier previews. On older versions, add U to preview_date_format yourself if you're batching.

Cover notifications too. Anything routed through the mail channel — MailMessage notifications, password resets, verification emails — uses the same transport, so the preview route doubles as a notification inspector. This matters more than it sounds: a notification fanned out to several recipients sends several messages in the same request, which is exactly the case the 6.2.3 timestamp fix protects.

Keep assertions separate. Mail::fake() is still the right tool for tests. Don't try to assert against the preview driver's files; use the fake for behavior and the preview driver for eyeballing. Mixing the two leads to brittle tests coupled to storage paths.

Environment gating. Register the preview route only where it belongs. Most teams wrap the service provider's route registration in an app()->environment('local') check, or put the preview mailer behind MAIL_MAILER=preview locally and never reference it in staging/production config. If you do expose it on a shared dev server, put auth middleware in front of the route.

Queue-aware workflow. If mail is queued (the default for many Mailables), the preview file only appears after the worker processes the job. Run php artisan queue:work alongside your dev server, or set QUEUE_CONNECTION=sync locally so previews land immediately — a common source of "the package is broken" confusion.

Housekeeping. Preview files accumulate silently. Add a scheduled prune or simply rm -rf the directory in your dev reset script, and make sure the storage path is gitignored. Unique timestamps mean the directory grows faster than it used to — no collisions, but no overwrites either.

Gotchas and Tips

  • Config cache is the number-one gotcha. After changing MAIL_MAILER or the mailer block, run php artisan config:clear. Similarly, long-running queue workers cache config at boot — restart them after any change.
  • Published configs don't upgrade themselves. This bites hardest with preview_date_format: the 6.2.3 fix only lands in the package's own default config. If you published the config file (or copied an older snippet), the U token is missing and same-second previews still overwrite each other. Add U to your format string, or republish with --force and re-apply your customizations. Same reasoning applies to any future default change — diff your published config after each minor bump.
  • The transport name must exist. Setting MAIL_MAILER=preview without adding the preview mailer to config/mail.php yields an "unsupported mailer" exception. The env var alone isn't enough on modern Laravel.
  • Route 404s. If /mail-preview doesn't resolve, check: provider auto-discovery is enabled, route:clear has been run after config edits, and the route isn't gated to an environment you're not in. The route name/path is configurable if it collides with an app route.
  • Storage permissions. On fresh containers or CI images, the preview directory may not be writable. storage/framework should be www-data-owned in Docker setups; a silent failure to write shows up as an empty preview index.
  • Attachments and inline images. The saved preview is an HTML snapshot — cid: inline images and attachment links often won't resolve the way a real client handles them. Verify attachments separately (check the mailable's attachments() in a test) rather than trusting the browser render.
  • Browser ≠ email client. This package previews your HTML, not how Gmail, Outlook, or Apple Mail will render it. Treat it as a fast layout check; use a dedicated email-client testing service for final fidelity, especially around CSS support, dark mode, and Outlook's Word rendering engine.
  • Subject and headers are visible, body is the star. Use the preview to sanity-check merge variables, pluralization, and date formatting — the things that are painful to catch in a real inbox.
  • Queued jobs capture config at dispatch time in some setups. If you flip the mailer mid-session and queued mail still goes out the old way, restart the worker — the process is holding stale config.
  • File naming is your audit trail. Previews are named from the timestamp plus a hash of the message, so the index order roughly mirrors send order. If you're debugging "did this one actually go out?", the presence or absence of a file is the fastest signal — faster than tailing the log.
  • Extension points. The underlying driver after capture is configurable, so you can have the preview transport log or even forward to a real driver if you want a "capture but still send" mode. The storage path, route, and date format are the knobs most teams customize; overriding the index view is possible if you want the list to show recipients or timestamps differently.
  • Composer resolution. The package lives at spatie/laravel-mail-preview; the older themsaid/laravel-mail-preview name is still floating around in blog posts. Pin the Spatie package in composer.json and check its README — config file names and key names have shifted between the original and the fork, so don't blindly copy snippets from old tutorials.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager