# Symfony

Use the official [Lettermint Symfony Mailer package](https://github.com/lettermint/lettermint-symfony-mailer) to send email through the Sending API. The transport supports Symfony email messages, attachments, tags, metadata, and queued mail. See [SDKs and integrations](/sdks) for other packages.

## Requirements

Before you start, you need:

- PHP 8.2 or newer, with the PHP version required by your Symfony release
- Symfony Mailer 6.4, 7.4, or 8.x and Composer
- A [Lettermint account](https://app.lettermint.co) with a [verified sending domain](/platform/domains/introduction)
- A Project API token from your [project settings](https://app.lettermint.co/projects)

## 1. Installation

Install the package with Composer:

```bash
composer require lettermint/symfony-mailer
```

## 2. Configuration

### Register the transport

Add the transport factory to the existing `services` section:

```yaml title="config/services.yaml"
services:
    Lettermint\SymfonyMailer\Transport\LettermintTransportFactory:
        autoconfigure: false
        arguments:
            $dispatcher: '@?event_dispatcher'
            $client: '@http_client'
            $logger: '@?logger'
        tags: ['mailer.transport_factory']
```

:::note
You must register this factory. Package installation alone does not register the Lettermint transport with Symfony Mailer.
:::

### Set the Project API token

Add the DSN to `.env.local`. Replace `PROJECT_TOKEN` with your Project API token:

```dotenv title=".env.local"
MAILER_DSN=lettermint+api://PROJECT_TOKEN@default
```

URL-encode the token if it contains reserved URL characters. Keep the host as `default`.

Configure Symfony Mailer to use this DSN:

```yaml title="config/packages/mailer.yaml"
framework:
    mailer:
        dsn: '%env(MAILER_DSN)%'
```

## 3. Send your first email

Create a service that receives `MailerInterface`. This example uses the standard Symfony service configuration with autowiring enabled:

```php title="src/Service/WelcomeEmail.php"
<?php

namespace App\Service;

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

final class WelcomeEmail
{
    public function __construct(private MailerInterface $mailer)
    {
    }

    public function send(string $recipient): void
    {
        $email = (new Email())
            ->from(new Address('hello@yourdomain.com', 'Example'))
            ->to($recipient)
            ->subject('Welcome to Example')
            ->text('Your account is ready.')
            ->html('<h1>Welcome!</h1><p>Your account is ready.</p>');

        $this->mailer->send($email);
    }
}
```

Replace the sender with an address on your verified domain. Inject `WelcomeEmail` into a controller or service, then call `send('recipient@example.com')`.

## 4. Email options

The examples below modify a Symfony `Email` object named `$email`. Add these options before you call `$mailer->send($email)`.

### Recipients and reply-to

```php
$email
    ->to('customer@example.com')
    ->cc('manager@yourdomain.com')
    ->bcc('archive@yourdomain.com')
    ->replyTo('support@yourdomain.com');
```

The transport uses Symfony's envelope recipients and respects recipient overrides. At least one To recipient must remain in the envelope. CC-only and BCC-only envelopes are not supported.

### Attachments

```php
$email->attachFromPath('/path/to/invoice.pdf', 'invoice.pdf', 'application/pdf');
```

The transport also supports inline images and stream attachments. It encodes attachment content for the API.

### Route and delivery settings

Add default options to the DSN:

```dotenv title=".env.local"
MAILER_DSN="lettermint+api://PROJECT_TOKEN@default?route=transactional&timeout=15&track_opens=false&track_clicks=true&tls=enforced"
```

- `route` selects a route by its slug.
- `timeout` sets the request timeout in seconds. Use a positive number. The default is 15 seconds.
- `track_opens` and `track_clicks` accept `true` or `false`.
- `tls` accepts `opportunistic` or `enforced`. It controls TLS for email delivery.

Use `OptionsHeader` to change options for one email:

```php
use Lettermint\SymfonyMailer\Header\OptionsHeader;

$email->getHeaders()->add(new OptionsHeader([
    'route' => 'transactional',
    'settings' => [
        'track_opens' => false,
        'tls' => 'enforced',
    ],
    'tags' => [
        ['name' => 'category', 'value' => 'receipt'],
    ],
]));
```

Per-email settings override the matching DSN defaults. Other default settings stay active. The transport removes this header from the delivered email.

To [schedule an email](/platform/emails/scheduling), add `scheduled_at` to the same options array. Use a future timestamp with an explicit time zone, such as `(new \DateTimeImmutable('+1 hour'))->format(\DateTimeInterface::ATOM)`.

### Tags and metadata

Use `OptionsHeader` for named tags, as shown above. For a single tag and metadata, use Symfony's headers:

```php
use Symfony\Component\Mailer\Header\MetadataHeader;
use Symfony\Component\Mailer\Header\TagHeader;

$email->getHeaders()->add(new TagHeader('receipt'));
$email->getHeaders()->add(new MetadataHeader('order_id', '12345'));
$email->getHeaders()->add(new MetadataHeader('customer_id', 'cust_789'));
```

Each `MetadataHeader` adds one string value to the API metadata. If you add multiple `TagHeader` objects, only the last tag is used. See [tags](/platform/emails/tags) for details.

### Idempotency

Set an explicit key to prevent duplicate emails when your application repeats a send:

```php
$email->getHeaders()->addTextHeader('Idempotency-Key', 'receipt-12345');
```

Use the same key for each retry of the same email. The transport sends the key as an HTTP header and removes it from the delivered email. It does not generate keys or retry requests automatically. See [idempotency](/platform/emails/idempotency) for the API behavior.

## 5. Queued mail

If your application sends email through Symfony Messenger, set all options and the idempotency key before you queue the message. The transport preserves these headers when Symfony serializes the email.

Run the Messenger worker for the transport configured in your application. With queued mail, the worker sends the API request and handles send failures.

## 6. Responses and errors

`MailerInterface::send()` returns no value. In a Symfony application, use `SentMessageEvent` to read the Lettermint message ID:

```php title="src/EventListener/EmailSentListener.php"
<?php

namespace App\EventListener;

use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Mailer\Event\SentMessageEvent;

#[AsEventListener]
final class EmailSentListener
{
    public function __construct(private LoggerInterface $logger)
    {
    }

    public function __invoke(SentMessageEvent $event): void
    {
        $this->logger->info('Email accepted by the mail transport.', [
            'message_id' => $event->getMessage()->getMessageId(),
        ]);
    }
}
```

This listener requires Symfony service autoconfiguration. For the Lettermint transport, `getMessageId()` returns the API's `message_id`. API acceptance does not confirm delivery. Use [webhooks](/platform/webhooks/introduction) for delivery and bounce events.

API failures throw `Lettermint\SymfonyMailer\Transport\ApiException`. It extends Symfony's `TransportException` and exposes `statusCode` and `responseBody`. Network failures throw `TransportException`. Catch `TransportExceptionInterface` to handle both types in a synchronous send. For queued mail, configure failure handling in Messenger.

## Use Symfony Mailer without the Symfony framework

Register the factory in a transport registry. Set `LETTERMINT_PROJECT_TOKEN` in your environment before you run this script:

```php title="send-email.php"
<?php

require_once 'vendor/autoload.php';

use Lettermint\SymfonyMailer\Transport\LettermintTransportFactory;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mime\Email;

$projectToken = getenv('LETTERMINT_PROJECT_TOKEN');

if ($projectToken === false || $projectToken === '') {
    throw new \RuntimeException('Set LETTERMINT_PROJECT_TOKEN before sending email.');
}

$registry = new Transport([new LettermintTransportFactory()]);
$transport = $registry->fromString(
    'lettermint+api://'.rawurlencode($projectToken).'@default'
);
$mailer = new Mailer($transport);

$email = (new Email())
    ->from('hello@yourdomain.com')
    ->to('recipient@example.com')
    ->subject('Hello from Lettermint')
    ->text('This is a test email.');

$mailer->send($email);
```

Use this registry to resolve the Lettermint DSN. Symfony's static `Transport::fromDsn()` method does not discover this package.

## Next steps

<CardGroup cols={2}>
    <Card title="Tracking" icon="chart-line" href="/platform/emails/tracking/introduction">
        Track email opens and link clicks.
    </Card>
    <Card title="Webhooks" icon="webhook" href="/platform/webhooks/introduction">
        Receive delivery and bounce events.
    </Card>
    <Card title="Test addresses" icon="envelope" href="/platform/emails/sending-test-emails">
        Test delivery and bounce handling.
    </Card>
    <Card title="PHP SDK" icon="php" href="/guides/send-email-with-php">
        Send email with the PHP SDK.
    </Card>
</CardGroup>

<Card title="GitHub repository" icon="github" href="https://github.com/lettermint/lettermint-symfony-mailer">
    Read the source code or report an issue.
</Card>
