vormkracht10/laravel-mails
Laravel Mails is a Laravel package for managing transactional email. It lets you store mailables and templates in the database, preview, log, and resend outgoing messages, and keep email workflows organized with Laravel's native mail system.
Get it running in four commands:
composer require vormkracht10/laravel-mails
php artisan vendor:publish --tag="laravel-mails-config" # and/or --tag="laravel-mails-migrations"
php artisan migrate
Then send any mail in your app — through a Mailable, a Notification, or Mail::raw() — and check the mails table. There is no code to add to your Mailables; the package hooks Laravel's mail events and records what was sent.
Where to look first, in this order:
config/mails.php — the knobs that matter are the storage disk for attachments, whether mail bodies are persisted, and any retention/pruning settings. Read this before writing a single line of app code, because changing the disk later does not move already-stored attachments.to, cc, bcc, subject, body, headers, timestamps, attachment relations). This is your de facto API surface.Mail model — check whether it is bound in the config so you can extend it.First real use case: support tickets of the form "I never received my invoice." Instead of trawling logs or asking the customer to check spam, you open the record for that address, see the exact rendered HTML and headers that left your app, confirm the recipient, and resend from the same screen or a tinker one-liner. That single workflow justifies the install for most teams.
Automatic capture is the default, keep it that way. Resist the urge to wrap sends in a custom service. Every Mail::to(...)->send(...), queued Mailable, and Notification goes through the same pipeline, so a listener-based package gives you coverage you would never maintain by hand. If a specific mail must not be recorded (password resets, magic links), prefer excluding by Mailable class in config over bypassing the mailer.
Build an admin index on top of the model. The package gives you persistence, not a UI. A simple resource — filters on recipient, subject, date range, and a detail view rendering the stored body in an iframe with srcdoc — is a half-day of work and is where the package earns its keep. Add a "resend" action that reconstructs the Mailable from the stored record, or simply re-sends the stored body via a raw message if you only need the identical payload.
Attachments: treat the disk as production storage. Configure a dedicated disk (S3 or a private local disk), not public. Attachments should be streamed through a controller with authorization, never hot-linked. Decide early whether you store attachments at all — they are the single biggest source of growth.
Scope by tenant if you are multi-tenant. The package stores a flat table. Add a tenant_id (or your app's foreign key) via a small migration and a global scope on your extended model, otherwise a shared admin view leaks other customers' mail content. This is the one place where extending the package model is genuinely required rather than nice-to-have.
Pair it with scheduled pruning. Whatever retention you choose — 30, 90, 180 days — wire it into the scheduler on day one. A mail audit table with no retention policy becomes a multi-gigabyte liability within a year in any app that sends transactional mail at volume.
Use it in tests sparingly. Asserting on Mail::fake() is still the right tool for "did we send the right Mailable." Reach for the stored records only when you need to assert on rendered content, headers, or attachment presence — that is where faking falls short.
DB::transaction. Records are written when the job runs, not when you dispatch. Wrapping a dispatch in a transaction that later rolls back leaves a mail record for a mail that was actually sent — because it was. Test the queued path explicitly rather than assuming synchronous behavior from local testing.php artisan config:cache after changing the disk or model binding, and remember that every queue worker needs the same storage credentials. A worker that cannot write to the attachments disk will silently degrade to records without files unless you watch for it.Mail::fake() in a stray test, or a Notification::route bypassing mail), then check whether the config restricts capture to specific mailers or Mailables. Nine times out of ten it is one of those two.How can I help you explore Laravel packages today?