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

Filament Mails Laravel Package

backstage/filament-mails

Filament Mails adds a searchable mail log to your Filament admin panel, letting you browse, preview and inspect emails sent by your Laravel app, review recipients, subjects and content, and debug delivery issues without leaving the dashboard.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish everything before you migrate — and verify the package name first.

composer require backstage/mails
php artisan vendor:publish --tag="mails-config"
php artisan vendor:publish --tag="mails-migrations"
php artisan migrate

Check the actual composer name and publish tags with composer show backstage/mails and php artisan vendor:publish --list before copying commands from a blog post. The vendor namespace is Backstage\Mails\... and the release notes refer to the package as backstage/mails, while older docs and the Filament plugin class (Backstage\FilamentMails\FilamentMailsPlugin) suggest backstage/filament-mails. Run composer show | grep -i backstage once and write the correct string in your README — you will otherwise waste an afternoon on a composer require that resolves to nothing.

Confirm your Filament major before anything else. As of v3.0.15 this package tracks filament/filament: ^5.0 (the fix targets v5.7.0+ behaviour). If you are still on Filament v3 or v4, this release line is not for you — pin an older package tag rather than mixing majors.

Then register the plugin on the panel you want it on — not on every panel:

// app/Providers/Filament/AdminPanelProvider.php
use Backstage\FilamentMails\FilamentMailsPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->plugin(FilamentMailsPlugin::make());
}

Where to look first (in this order):

  1. composer.json of the package — check the filament/filament constraint and the exact tag you resolved. With a package this small, the constraint is the compatibility contract.
  2. config/filament-mails.php (or mails.php, per the publish tag you actually got) — enable/disable, model swapping, table and pruning options live here. Read it fully before changing anything.
  3. The published migration — confirm column types (text vs longtext vs json) and whether attachments are stored inline. This determines your retention strategy later.
  4. The Mail model and the listener that writes rows — that's your integration seam for anything custom.
  5. The Filament resource in the vendor tree — the fastest way to learn what actions, columns, and filters already exist, so you extend rather than rebuild.

First use case: stop relying on MAIL_MAILER=log + storage/logs/laravel.log grep sessions. Keep your real mailer (SMTP/Postmark/SES), let the package persist each outgoing message via Laravel's MessageSent event, and browse recipients, subject, headers, and rendered HTML in the panel. This is most valuable in staging and in a gated production panel, where "did the customer actually get the invoice mail?" is otherwise unanswerable without asking them.

Implementation Patterns

Gate the panel, then gate the resource. The plugin exposes mail bodies — password reset links, magic links, invoices, PII. Two layers:

->plugin(
    FilamentMailsPlugin::make()
        ->navigationGroup('System')
        ->navigationLabel('Outbox')
)

and a policy (or the plugin's canAccess()/visible() hook, depending on version) restricting it to a super-admin role. Do this on day one, not after the first security review.

Make it observable, not just browsable. Turn the resource into an operations tool:

  • A navigation badge showing the count of mails from the last hour, or failed jobs — cheap and immediately useful.
  • Table filters for recipient domain, subject, date range, and mailer.
  • A "copy reset link" / "open raw HTML" action for support workflows.

Resending is your code, not theirs. To re-send you need to reconstruct the Mailable, not just replay the stored body. If resend matters, log a discriminator alongside the package's row (a mailable_class + serialized payload column via a MessageSending listener) and build a Filament action that rehydrates it. Don't try to fake it from stored HTML — you'll lose headers, attachments, and tracking.

Attachments: store on disk, not in the row. If the package (or your config) inlines base64 attachments, a single 10 MB PDF becomes a 13 MB row and your mails table becomes the largest thing in the database. Prefer a custom model that writes attachments to a private disk and stores paths.

Multi-tenancy needs a scope. Out of the box the resource is global. In a multi-tenant panel, override the resource's query to constrain on tenant_id (or filter by the authenticated user's team), otherwise tenant A reads tenant B's mail. Same for a customer-facing panel: never register this plugin there.

Prune on a schedule. Mail bodies have no long-term value beyond debugging. Add $this->artisan('model:prune') to the scheduler with a 30/90-day window, or a Prunable trait on your custom model. This is the single most common cause of "why is the DB 40 GB?" after adopting a mail logger.

Subclass pages defensively. If you extend ListMails or any vendor page to add actions, match the widest signature the parent accepts, and mirror the pattern v3.0.15 adopted:

public function unmountAction(bool|string|null $cancelParentActions = null): void

Forward only the arguments you actually received; do not re-supply parent defaults. Filament changes these signatures in minor releases, and a narrower override in your subclass will fatal at discovery time exactly like the bug this release fixed.

Testing. Mail::fake() swaps the transport, so MessageSent never fires and nothing is logged — your feature tests will silently skip the integration. Either assert against Mail::assertSent() (correct for most cases) or unit-test the listener directly with a synthetic MessageSent event.

Gotchas and Tips

  • Signature drift is the failure mode you will actually hit. v3.0.15 exists solely because Filament v5.7.0 widened unmountAction(bool $canCancelParentActions = true) to bool|string|null $cancelParentActions = null and the package's ListMails override no longer matched, killing Laravel at package discovery — before any of your code runs. Symptom: php artisan dies with a "Declaration of ... must be compatible with ..." fatal. Fix on the package side is composer update backstage/mails; fix on your side is to audit your own overrides the same way. When you see this class of error, it is never your application logic — it is a signature mismatch between a vendor page and a newer Filament.
  • A patch upgrade can still be a compatibility cliff. v3.0.15 is a patch number but its content is "we now require the Filament v5.7 series to behave correctly." Re-read the Filament constraint after every composer update, and keep filament/filament pinned rather than floating ^5.0, or a routine composer update on Filament will pull the package into a state you did not test.
  • Failures aren't logged by default. MessageSent fires after a successful send. A mail that throws in the transport, or dies in a failed queue job, leaves no row. If you need failure visibility, hook MessageSending (write a pending row) and mark it sent/failed from the job's failed() callback — or just pair this with filament-failed-jobs-style monitoring.
  • Queue workers, transactions, and DB connections. Logging happens in the worker process, outside your request. If a job runs inside a wrapping transaction that rolls back, or the worker's connection dies mid-send, you get an inconsistent log. Keep the logging write outside transactions where possible.
  • HTML rendering is an XSS surface. Email HTML is hostile by design. Render bodies in a sandboxed <iframe srcdoc> or through an HTML sanitizer (e.g. mews/purifier). Never drop stored mail into {!! !!} in a Livewire component.
  • Treat every release as unreviewed by anyone but you. Low star count and near-zero dependents means you are the QA team. Pin exact versions (not ^) for this package, read the diff of the page classes before upgrading, and assume you may need to fork — MIT makes that painless.
  • Publish config before migrating. The migration may read config values (table name, connection). Editing config after migrate leaves you with a mismatched schema and a confusing "table not found."
  • Use a dedicated connection if the volume is high. Point the package's model at a mysql_logs connection so mail bloat never competes with your app tables for backups or disk alarms.
  • Extension points to prefer: custom model via config → custom Filament resource registered in place of the vendor one → view overrides. Avoid editing vendor files; the whole point of a low-adoption package is that the next release may surprise you — and v3.0.15 shows the surprises can arrive in a patch.
  • Retention is a policy decision, not a technical one. Password reset and magic-login bodies contain live credentials. Set a retention window (hours, not months) for those, and consider encrypting the body column via a model cast. The panel is only as safe as the least-privileged person who can open it.
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.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource