stackkit/laravel-database-emails
Store outgoing Laravel mail in your database instead of sending it. This package captures emails so you can inspect, preview, and debug them locally or in staging without external services or risking real deliveries.
Version note (9.0.0): this release adds Laravel 13 support and drops Laravel 11. If you're still on Laravel 11, stay on ^8.0; upgrade Laravel first, then bump the package. There are no reported API changes in 9.0.0 — the major bump is purely the framework floor — but treat the upgrade as a "run the test suite and eyeball the config" step, since a new framework major can shift mailer/transport registration details.
Minimal setup:
composer require stackkit/laravel-database-emails
php artisan vendor:publish --tag="database-emails-config"
php artisan vendor:publish --tag="database-emails-migrations"
php artisan migrate
Then point your mailer at the package's transport in .env:
MAIL_MAILER=database
Clear cached config (php artisan config:clear) and send any mail — a Mail::to(...)->send(...), a notification, a password reset — and it lands in the emails table instead of going out over SMTP. The package registers a browsing route (enabled/configured in config/database-emails.php) where you can read the rendered HTML, inspect headers, and download attachments.
Where to look first: the published config/database-emails.php. It's short and it answers the questions you'll actually have — whether the UI route is registered, what middleware protects it, which filesystem disk holds attachments, and what the retention setting is. First real use case: wire it into local and staging so QA can click through real transactional emails (password resets, invoices, order confirmations) without a Mailtrap/SMTP dependency and without any risk of mailing a real customer.
Environment-scoped swap (the common case). Keep MAIL_MAILER=database in .env.local / .env.staging and leave production on SES/Postmark. This is the lowest-friction pattern and requires zero application code. It also means every queued mailable, notification, and framework-generated email is captured for free.
Explicit secondary mailer (the safer production-adjacent case). Rather than replacing the default mailer, define a named mailer in config/mail.php that uses the database transport, then opt in per-send:
// config/mail.php
'database' => [
'transport' => 'database',
],
// usage
Mail::mailer('database')->to($user)->send(new InvoiceReady($invoice));
This lets you keep real delivery for transactional mail while diverting, say, marketing or digest mail for inspection.
QA / support workflow. Capture → browse the rendered HTML → confirm the reset link, the currency formatting, the logo URL, the attachment filename. When a support ticket says "I never got the receipt," search the emails table by recipient and subject, confirm what was rendered, then use the package's resend action on the record rather than asking the user to retrigger the flow. This is where the package earns its keep over an SMTP sink.
Retention as a scheduled concern. Emails accumulate fast, and the body column is the bulk. The package's email model is set up for Laravel's pruning, so schedule php artisan model:prune in routes/console.php / the scheduler and set the retention window in config. Without this, staging databases grow to hundreds of MB of stale HTML.
Assertions in feature tests. For tests that need to verify rendered output rather than just Mail::fake() semantics, capture with the database mailer and assert against the stored record (subject, recipient, rendered body). Keep Mail::fake() for "was it sent" assertions and reserve the database mailer for "what did it look like" assertions — mixing the two in one test is confusing.
MAIL_MAILER does the production environment resolve, and is config cached?queue:work process started with a stale config cache — or a supervisor entry that pins a different --env — will happily use the wrong mailer. After changing MAIL_MAILER, run php artisan config:clear and restart workers.auth (or better, a gate/can middleware) in the package config. Don't ship a route that lets anyone enumerate password-reset links.html column retention — but do it deliberately, since the HTML is the debugging value.composer.json (framework majors usually move the PHP floor too), run migrations even though none are announced — a no-op is cheap — and re-publish the config only as a diff, not a overwrite, since your route middleware and retention values live there.Email model — query it directly for custom dashboards or admin panels instead of building your own capture table.How can I help you explore Laravel packages today?