symfony/azure-mailer
Symfony Azure Mailer provides an Azure Communication Services email transport for Symfony Mailer, letting applications send transactional email through Azure with a simple, framework-native mailer integration.
Install the bridge. composer require symfony/azure-mailer symfony/mailer. This package is a Symfony Mailer bridge — it does nothing on its own; it registers an azure+api transport in the Mailer component. Flex’s standard Mailer recipe handles the framework.mailer config, so you usually only touch the DSN.
Provision Azure Communication Services first. In the Azure portal create a Communication Services resource, then attach an Email Communication Services domain (the Azure-managed *.azurecomm.net domain is the fastest path; a custom domain requires DNS SPF/DKIM verification). Copy the connection string / access key and note the verified sender address (e.g. DoNotReply@<guid>.azurecomm.net).
Configure the DSN. Put it in .env.local, not .env:
MAILER_DSN=azure+api://<access-key>@<resource>.communication.azure.com
MAILER_FROM=DoNotReply@<guid>.azurecomm.net
The access key is base64 and often contains +, /, = — percent-encode it or the DSN parser will misread it.
Verify wiring. php bin/console debug:config framework mailer shows the configured DSN, and php bin/console debug:container --tag=mailer.transport_factory should list the Azure factory. If the scheme doesn’t appear, the bridge isn’t autoloaded.
First send. Inject Symfony\Component\Mailer\MailerInterface and send a Symfony\Component\Mime\Email:
$email = (new Email())
->from($from) // must match a verified ACS sender
->to($user->getEmail())
->subject('Welcome')
->text('...')
->html('<p>...</p>');
$mailer->send($email);
Where to look first. In the package source, read AzureTransportFactory (supported schemes and DSN parsing) and AzureApiTransport (how the message’s From becomes Azure’s senderAddress, how HTTP errors map to TransportException). Then read the Symfony Mailer docs section on transports and MessageEvent/SentMessageEvent.
Wrap Mailer in one application service. Inject MailerInterface into a NotificationMailer/AppMailer class so from-address, reply-to, and template selection are centralized. Don’t call $mailer->send() from controllers or entities.
Use named mailers for separation. With framework.mailer.mailers: { default: ..., transactional: ... }, inject Symfony\Component\Mailer\MailerInterface $transactionalMailer (autowiring by name) or set X-Transport on the Email header when you need per-message routing (e.g. marketing vs. password reset).
Queue everything in production. Route Symfony\Component\Mailer\Messenger\SendEmailMessage to a Messenger transport/queue:
framework:
messenger:
routing:
'Symfony\Component\Mailer\Messenger\SendEmailMessage': async
Configure retry strategy and a failure transport. Azure HTTP calls are slow and rate-limited; sending inline in a request is a latency and reliability liability.
Templated email. Use TemplatedEmail + BodyRenderer (Twig) for HTML/text pairs; keep the plaintext part, since ACS accepts both and many clients fall back to it.
Envelope vs. From. Use Envelope for the SMTP envelope (return-path, bounce handling) while keeping the visible From: header. Remember the Azure senderAddress is derived from the message sender — keep them aligned with your verified ACS identity.
Event hooks for cross-cutting concerns. Subscribe to Symfony\Component\Mailer\Event\MessageEvent to enforce BCC policies, add tags/headers, or log before send; use SentMessageEvent/FailedMessageEvent for metrics.
Testing pattern. Unit-test your mailer service against an in-memory/test transport rather than the Azure transport. In KernelTestCase use MailerAssertionsTrait (assertEmailCount, assertEmailTextBodyContains); for pure unit tests, build a Mailer with Symfony\Component\Mailer\Transport\NullTransport and assert on the RawMessage/SentMessage.
Environment strategy. dev → null://null or Mailpit (smtp://mailpit:1025); staging → Azure with a test domain and a dedicated key; prod → Azure with your verified custom domain. Never share the production access key with CI.
Health check / smoke test. Add a console command that sends a one-line message to an internal alias, and run it after deploys. It’s the fastest way to catch a rotated key or an unverified sender.
Sender must be verified — every time. ACS rejects a senderAddress outside a verified domain with a generic 400. The bridge takes the sender from From:, so a missing/incorrect ->from() fails opaquely. Set a default From in your mailer service.
URL-encode the access key. Base64 keys contain +, /, =; unencoded they break DSN parsing or truncate the key. If the DSN is assembled in code, use rawurlencode() on the key before interpolating. Don’t log the DSN — Symfony’s profiler will show it masked, but your own logs may not.
Scheme is azure+api, not azure. Transport::fromDsn('azure://...') will not resolve to this bridge. Check AzureTransportFactory::getSupportedSchemes() if a DSN throws “unsupported scheme.”
202 Accepted ≠ delivered. Azure returns an accepted status; bounces, complaints, and delivery events come asynchronously through Azure Event Grid — this bridge does not surface them as Symfony events. If you need bounce handling, build an Event Grid webhook and correlate by message ID.
Error diagnosis. Failures surface as TransportExceptionInterface. Catch, log the message, and inspect the HTTP response body (temporarily enable symfony/http-client debug or the Mailer profiler panel). Typical codes: 401 (bad key / clock skew / wrong endpoint region), 400 (unverified sender or malformed recipient), 429 (throttled).
Throttling is per-domain. Combine Messenger retry with a RateLimiter and cap worker concurrency. A burst of password resets can burn through ACS quotas fast.
Attachment size and recipient limits. ACS enforces message-size and recipient-count limits; validate in your mailer service and prefer links over large attachments. Large inline images are the usual culprit.
No SMTP fallback here. This bridge is HTTP-only. If you need fallback to an SMTP relay, configure a second named mailer with an smtp:// DSN and catch/route in your service; the bridge won’t silently fall back for you.
Auth modes. The DSN’s key auth is HMAC access-key based. Managed identity / Entra token auth is not exposed by the stock transport — if you need it, write a custom TransportInterface (decorate the existing one) or call the Azure SDK directly.
Version alignment. It’s a first-party Symfony subpackage but a niche bridge with few stars, so docs and issue answers are sparse. Pin it on the same minor as symfony/mailer; when debugging, read AzureTransportFactory/AzureApiTransport and compare against a raw curl to POST …/emails:send?api-version=… to isolate bridge vs. Azure problems.
Isolate layers when debugging. Temporarily swap to Mailpit (smtp://) to prove the message is well-formed, then switch back to Azure to prove credentials/domain. This splits “email is wrong” from “Azure is rejecting us” in one step.
How can I help you explore Laravel packages today?