fusonic/messenger-mailer-bundle
Symfony bundle that bridges Mailer and Messenger, letting you dispatch emails as messages and send them asynchronously through your Messenger transport. Configure transports, queue mail instead of blocking requests, and keep email delivery reliable and decoupled from your application flow.
The bundle exists to solve one problem: Symfony's SendEmailMessage is serialized onto the Messenger transport, and Symfony\Component\Mime\Part\File (the thing Email::attachFromPath() uses) is not serializable. Queue an email with an attachment and you get a serialization exception at dispatch time, or a worker that fails because the file path no longer exists on the consuming machine. This bundle makes attachment-bearing emails survive the round trip.
Minimal path:
composer require fusonic/messenger-mailer-bundle (Flex registers it; otherwise add it to config/bundles.php).# config/packages/messenger.yaml
framework:
messenger:
transports:
async_mail:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
'Symfony\Component\Mailer\Messenger\SendEmailMessage': async_mail
# config/packages/mailer.yaml
framework:
mailer:
dsn: '%env(MAILER_DSN)%'
$email = (new Email())
->from('[email protected]')
->to($customer->getEmail())
->subject('Your invoice')
->html($this->twig->render('email/invoice.html.twig', ['invoice' => $invoice]))
->attachFromPath('/var/app/invoices/'.$invoice->getId().'.pdf', 'invoice.pdf', 'application/pdf');
$this->mailer->send($email); // returns immediately; nothing sent yet
bin/console messenger:consume async_mail -vv.Where to look first: the README for the supported Symfony/Messenger version matrix, then vendor/fusonic/messenger-mailer-bundle/src/ — specifically the DI extension and whatever handler/middleware it registers for SendEmailMessage. With 6 stars and zero dependents, the source is the documentation; budget 20 minutes to read it before designing around it. Also read tests/ — the fixtures usually reveal the intended attachment semantics (path vs. string vs. stream).
First use case: transactional email with a generated PDF (invoice, ticket, report). This is the sweet spot — the file exists on the web node, the worker is a separate process, and you don't want the HTTP request blocked on SMTP.
Build the email in the controller, send through the mailer. Don't hand-dispatch SendEmailMessage yourself. The bundle's guarantees apply to the mailer pipeline; bypassing it is how you reintroduce the original bug.
Separate "what to send" from "build the email" for anything with mutable state. Two viable shapes:
SendInvoiceEmail($invoiceId)) and build the Email inside the handler. Attachments are read from disk at consume time, which is better for large files and avoids stale serialized entities.Use the first for user-triggered one-offs (password resets, receipts); use the second for bulk/scheduled mail.
Dedicate a transport. Mail queues have different failure characteristics than job queues. Give them their own transport with a tuned retry strategy (max_retries: 3, exponential delay) and a failure transport, so a dead SMTP relay doesn't block unrelated async work:
framework:
messenger:
failure_transport: failed
routing:
'Symfony\Component\Mailer\Messenger\SendEmailMessage': async_mail
Tests stay synchronous. In config/packages/test/messenger.yaml override async_mail with sync:// so feature tests assert against mailer.transport / a profiler mailer assertion as usual. For unit-level tests asserting enqueueing, keep the transport in-memory and inspect it:
$transport = self::getContainer()->get('messenger.transport.async_mail');
$this->assertCount(1, $transport->getSent());
Integration tips that pay off immediately:
->html($this->twig->render(...)) and keep templates in templates/email/.$_FILES/UploadedFile (it's gone by the time the worker runs — move it to durable storage first).messenger:consume --time-limit=3600 --memory-limit=128M to recycle processes after long-running PHP memory creep.Attachment bytes live inside the queued message. Once inlined, a 10 MB PDF becomes ~13 MB of base64 in your Doctrine/Redis transport. Check your transport ceiling before you ship: SQS caps at 256 KB, Redis/AMQP have practical limits, and the Doctrine transport will happily bloat your messenger_messages table. For anything large, store the file (S3, local disk on a shared volume) and attach by path at consume time — or send a short-lived signed URL in the body instead of a file.
Path existence is not guaranteed at consume time. The whole reason this bundle exists is that the worker may be on another host with a different filesystem view. If you keep path-based attachments (queue-a-command pattern), make sure the storage is shared and the file isn't cleaned up by a temp-file reaper between dispatch and consume.
Don't serialize entities. A queued Email built from a Doctrine entity is fine (the email is a value object), but a queued custom message holding an entity will be stale or trigger lazy-loading inside __serialize. Pass scalars/IDs.
Retries re-send. A partially successful send that throws afterwards will be retried and the recipient gets two emails. Keep handlers idempotent, or accept at-least-once semantics explicitly.
Time-sensitive content is frozen at dispatch. "This link expires in 24 hours" signed URLs, OTP codes, and "your order ships today" copy are all computed in the web request. If the queue backs up, recipients get expired links. Generate those inside the handler when it matters.
Debugging checklist when nothing sends:
framework.mailer actually routing through the bus? If message_bus is unset/null, the bundle is inert and mail is synchronous — a common silent misconfiguration.bin/console debug:messenger — confirm a handler exists for SendEmailMessage.bin/console messenger:failed:show and messenger:failed:retry for anything that blew up in the worker.SELECT body FROM messenger_messages) to verify the attachment was inlined as a string and not left as a path reference.sync:// temporarily — if it works synchronously, the problem is serialization/worker-side, not the mailer.Extension points. If the bundle's behavior doesn't fit (custom storage, size limits, S3 offloading), don't fork it — decorate the SendEmailMessage handler, or add a Messenger middleware / SendMessageToTransportsEvent listener that rewrites attachments before the envelope is serialized. That's the same seam the bundle uses, and it keeps your logic portable.
Adoption risk is real. Six stars, zero dependents, one maintainer org. Pin the exact version in composer.json, read the source before upgrading Symfony major versions, and keep your mailer calls behind a thin wrapper service so you can drop the bundle in an afternoon by inlining attachments yourself:
new DataPart(file_get_contents($path), 'invoice.pdf', 'application/pdf')
That one-liner is the entire fallback strategy — worth knowing before you depend on the package in a critical path.
How can I help you explore Laravel packages today?