Docs / Symfony

Send email in Symfony over SMTP

Symfony Mailer configures its entire transport from a single MAILER_DSN environment variable — no config class, no per-setting keys. This guide points that DSN at Postwing and covers Twig-templated emails, attachments and asynchronous delivery through Messenger.

You can get them on the token management page. For security reasons, a token is shown only once — at the moment it is created.

SMTP connection settings

SettingValue
SMTP hostsmtp.postwing.app
Port587
EncryptionSTARTTLS (the connection is upgraded to TLS before login)
UsernameThe login of an SMTP token for your domain
PasswordThe password of that token — shown once, when the token is created
Every mode is also available on a high port: 8465 (SSL/TLS), 8587 (STARTTLS) and 8025 (plain). Many hosting providers and clouds block outbound 25, 465 and 587 — if the connection times out, switch to the matching high port.

Install Symfony Mailer

bash
composer require symfony/mailer
composer require symfony/twig-bundle   # for TemplatedEmail

Configure MAILER_DSN

.env.local
# .env.local — the whole transport is one DSN
MAILER_DSN=smtp://token-login%40your-domain.com:your-token-password@smtp.postwing.app:587

# Implicit TLS on port 465 uses the smtps scheme instead
# MAILER_DSN=smtps://token-login%40your-domain.com:your-token-password@smtp.postwing.app:465
The DSN is a URL, so the @ in a token login must be written %40 — otherwise the parser splits the string at the wrong place and reports a host it never found. The same goes for :, /, #, ? and & in the password.

Set a default sender once, rather than on every message:

config/packages/mailer.yaml
# config/packages/mailer.yaml — one default sender for every email
framework:
  mailer:
    envelope:
      sender: 'noreply@your-domain.com'
    headers:
      From: 'Acme <noreply@your-domain.com>'

Send your first email

Inject MailerInterface and send. Setting both text() and html() produces a multipart/alternative message:

php
<?php

namespace App\Controller;

use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;

class OrderController extends AbstractController
{
    #[Route('/orders', methods: ['POST'])]
    public function create(MailerInterface $mailer): Response
    {
        $email = (new Email())
            ->from('Acme <noreply@your-domain.com>')
            ->to('customer@example.com')
            ->subject('Your order #4417 is confirmed')
            ->text('Thanks! Your order ships tomorrow.')
            ->html('<h1>Order confirmed</h1><p>Your order ships tomorrow.</p>');

        $mailer->send($email);

        return $this->json(['ok' => true], 201);
    }
}

Render the email with Twig

TemplatedEmail renders the body from Twig templates, so email markup lives with the rest of your templates instead of inside a controller:

php
use Symfony\Bridge\Twig\Mime\TemplatedEmail;

$email = (new TemplatedEmail())
    ->from(new Address('noreply@your-domain.com', 'Acme'))
    ->to(new Address($order->getCustomerEmail(), $order->getCustomerName()))
    ->subject(sprintf('Your order #%d is confirmed', $order->getId()))
    ->htmlTemplate('emails/order_confirmed.html.twig')
    ->textTemplate('emails/order_confirmed.txt.twig')
    ->context(['order' => $order]);

$mailer->send($email);
templates/emails/order_confirmed.html.twig
{# templates/emails/order_confirmed.html.twig #}
<h1>Order #{{ order.id }} confirmed</h1>
<p>Thanks, {{ order.customerName }}. Your order ships tomorrow.</p>

Attachments and inline images

php
$email
    ->addPart(new DataPart(new File('/srv/invoices/2026-03.pdf'), 'invoice.pdf'))
    // Inline image, referenced from Twig as {{ email.image('@images/logo.png') }}
    ->addPart((new DataPart(new File('assets/logo.png'), 'logo'))->asInline());

Send asynchronously with Messenger

By default $mailer->send() talks to SMTP inside the request. Routing the mailer's message to an async transport moves delivery to a worker without changing a line of calling code:

config/packages/messenger.yaml
# config/packages/messenger.yaml
framework:
  messenger:
    transports:
      async: '%env(MESSENGER_TRANSPORT_DSN)%'
    routing:
      # Anything mailer sends now goes through the queue instead of the request.
      Symfony\Component\Mailer\Messenger\SendEmailMessage: async
bash
php bin/console messenger:consume async -vv
Once SendEmailMessage is routed to a transport, nothing is delivered until messenger:consume is running. Forgetting the worker in production is the usual reason mail stops the moment async is switched on.

Test without sending real email

properties
# .env.test / .env.dev — collect messages instead of delivering them
MAILER_DSN=null://null
php
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class OrderControllerTest extends WebTestCase
{
    public function testConfirmationIsSent(): void
    {
        $client = static::createClient();
        $client->request('POST', '/orders', [], [], [], '{"email":"c@example.com"}');

        $this->assertEmailCount(1);
        $this->assertEmailHtmlBodyContains(self::getMailerMessage(), 'Order confirmed');
    }
}

Troubleshooting

ErrorCause and fix
The "…" mailer DSN is invalid Unencoded @ or a special character in the credentials.
TransportException: Connection could not be established Outbound port blocked. Use 8587 (smtp) or 8465 (smtps).
Expected response code 250 but got 535Wrong token login or password in the DSN.
Nothing sends after enabling MessengerNo worker running. Start messenger:consume async.
Emails stuck in messenger_messages Worker crashed or the transport is misconfigured. Check messenger:failed:show.
550 sender rejected The from domain is not verified, or the token does not cover it.

Frequently asked questions

How do I write the MAILER_DSN if my username contains an @?

URL-encode it: @ becomes %40, so token-login@your-domain.com is written token-login%40your-domain.com. The same applies to any special character in the password — : / ? # and & all need encoding, or the DSN parses into the wrong parts and you get a confusing connection error.

What is the difference between the smtp and smtps schemes?

smtp:// opens a plain connection and upgrades it with STARTTLS, which is port 587. smtps:// opens an implicitly encrypted connection, which is port 465. Symfony picks the right behaviour from the scheme, so you do not configure encryption separately.

How do I send email asynchronously in Symfony?

Route Symfony\Component\Mailer\Messenger\SendEmailMessage to an async transport in messenger.yaml and run a worker with messenger:consume. Nothing in your calling code changes — $mailer->send() starts dispatching to the bus instead of talking to SMTP inline.

Why is my Symfony email not sending in production?

If Messenger routing is configured but no worker is running, the message sits in the queue forever. Check that messenger:consume is running under a supervisor, and look at the failed transport for messages that were retried and given up on.

How do I preview a Symfony email without sending it?

Set MAILER_DSN=null://null and open the Symfony Profiler's Email panel, which shows the rendered HTML, the headers and the recipients for each message the request produced. In tests, assertEmailCount and the assertEmail* helpers work against the same collected messages.

Should I use Symfony Mailer or PHPMailer?

Symfony Mailer if you are on Symfony — it integrates with Twig, Messenger, the Profiler and the test assertions. PHPMailer is the better fit for standalone scripts or legacy code with no framework, where a single small dependency matters more than integration.

Next steps