nuxia/mail-storage-bundle
A Symfony bundle for persisting outgoing emails so they can be inspected, audited, and debugged later. Integrates with the mailer to capture messages and store them through configurable storage with simple setup and service configuration.
Reality check before you install anything: nuxia/mail-storage-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no config/mail-storage.php, no facade, and no Laravel auto-discovery. It targets the SwiftMailer-era Symfony stack (Doctrine ORM entities + a mailer plugin/spool). Treat this assessment as "what the package does, and what you should actually build in Laravel instead."
If you still want to read the source (the useful part):
composer.json — note the type: symfony-bundle and the SwiftMailer/Doctrine requirements.NuxiaMailStorageBundle) and its DependencyInjection/Configuration.php — that file tells you the real feature surface: a storage adapter (Doctrine entity, filesystem/maildir) and a Swift plugin that intercepts sent messages.StoredEmail / attachments — that schema is the design worth copying.First use case: "I want a durable archive of every email my application sends — recipients, subject, raw MIME, attachments — queryable later for support, compliance, or resend." That is the job this bundle does in Symfony. In Laravel you get there in ~30 lines with a MessageSent listener, no third-party bundle required (see below).
Do not try to shoehorn this bundle into a modern Laravel app: it depends on SwiftMailer, which Laravel replaced with Symfony Mailer in Laravel 9. You would be fighting two EOL layers at once.
The bundle's actual pattern is: hook the transport, persist the message out-of-band, never block the send. Replicate that pattern natively.
1. Event-listener archive (the direct Laravel equivalent of the bundle)
// app/Listeners/ArchiveSentMail.php
namespace App\Listeners;
use Illuminate\Mail\Events\MessageSent;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class ArchiveSentMail
{
public function handle(MessageSent $event): void
{
$message = $event->message; // Symfony\Component\Mime\Email
$raw = $message->toString(); // full MIME incl. headers/attachments
$path = sprintf(
'mail-archive/%s/%s.eml',
now()->format('Y/m/d'),
Str::uuid()
);
Storage::disk('local')->put($path, $raw);
\App\Models\SentMail::create([
'path' => $path,
'subject' => $message->getSubject(),
'to' => collect($message->getTo())->map->getAddress()->implode(','),
'from' => collect($message->getFrom())->map->getAddress()->implode(','),
'message_id' => $message->getHeaders()->get('Message-ID')?->getBodyAsString(),
'sent_at' => now(),
]);
}
}
Register in EventServiceProvider::$listen. Queue the listener if you want zero latency on the request path.
2. Storage adapter choice (mirrors the bundle's two backends)
sent_mails table with indexed message_id, to, sent_at. Good for search and UI. Bad for multi-MB attachments..eml on disk. Perfect fidelity, cheap, replayable with php artisan tinker or imported into Mailpit. This is what the bundle's filesystem adapter does; prefer it.Hybrid (what the bundle effectively encourages): raw MIME on a disk, metadata row in the DB pointing at it.
3. Integration points in a Laravel workflow
Mail::fake(); assert with Mail::assertSent(). Don't archive in tests — guard the listener with if (app()->runningUnitTests()) return; or only register it in non-testing envs.MAIL_MAILER=smtp at Mailpit/MailHog — you get the same archive value for free without touching production mail flow.message_id lets you correlate with provider webhooks (Postmark/SES) for delivery status.4. Retention & pruning
Add php artisan mail:prune --days=90 as a scheduled command that deletes expired rows and disk files. The Symfony bundle does not manage retention for you; neither should your port.
ServiceProvider, no Facade, no Laravel config publishing, no PSR-4 autoload into App\. Anything you composer require will simply be dead code unless you manually boot it — and it won't boot against Symfony Mailer.Swift_Plugins_*, spool) does not exist in a modern Laravel app. This alone should rule it out for anything greenfield.MessageSent fires after the transport accepts the message. Failed sends are never archived. If you need "attempted sends," hook MessageSending (pre-send) as well, or archive from a queued job with retry semantics.$message->toString() includes base64 attachments. A 20 MB attachment becomes a ~27 MB DB row (or disk file). Cap size, store large attachments separately, or store a stub.Log::info($raw) — the raw MIME contains every header.DoctrineBundle and its entity mappings. In a Laravel app that means dragging in doctrine/dbal and maintaining a parallel schema — not worth it versus the ~30-line listener above.SentMailRepository / MailArchiveDisk interface into your listener so you can swap backends without changing call sites.MessageSent vs MessageSending), (c) queue workers were restarted after deploying the listener (php artisan queue:restart), (d) storage disk permissions.Bottom line: the idea (persist sent mail via a transport hook) is sound and worth implementing; the package is a Symfony-era artifact with zero adoption that does not fit Laravel. Implement the MessageSent listener, store .eml files on disk plus a metadata row, add pruning and access control, and you'll have everything this bundle offers with none of the framework friction.
How can I help you explore Laravel packages today?