> For the complete documentation index, see [llms.txt](https://docs.johncms.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.johncms.com/10.0/obshie-svedeniya/otpravka-elektronnoi-pochty-email.md).

# Отправка электронной почты (email)

В JohnCMS для отправки электронной почты используется [symfony/mailer](https://symfony.com/doc/current/mailer.html). Способ отправки скрыт за общим интерфейсом, поэтому вы можете выбрать наиболее подходящий вашему хостингу транспорт, не меняя код: письма ставятся в очередь одинаково в любом случае.

### Транспорты и настройка

Настройки почты указываются в конфигурационном файле **config/autoload/mail.global.php**. Не редактируйте этот файл — создайте рядом **mail.local.php** и переопределите в нём нужные значения; подробнее — в разделе [Конфигурационные файлы](/10.0/obshie-svedeniya/konfiguracionnye-faily-configs.md).

#### Строка подключения (DSN)

Основной способ настройки — параметр `dsn`. Это родной формат symfony/mailer, поэтому доступны все поддерживаемые им транспорты:

```php
<?php

declare(strict_types=1);

return [
    'mail' => [
        'dsn' => 'smtps://mail@example.com:password@smtp.example.com:465',
    ],
];
```

Основные варианты:

| DSN                                      | Что означает                                                      |
| ---------------------------------------- | ----------------------------------------------------------------- |
| `smtp://user:pass@smtp.example.com:587`  | SMTP, шифрование STARTTLS согласуется, если сервер его предлагает |
| `smtps://user:pass@smtp.example.com:465` | SMTP, шифрование с первого байта (порт 465)                       |
| `sendmail://default`                     | отправка через программу sendmail на сервере                      |
| `native://default`                       | настройки из php.ini                                              |
| `null://null`                            | письма никуда не уходят, удобно для разработки                    |

Полезные параметры SMTP: `?verify_peer=0` — не проверять сертификат (самоподписанный сертификат на своём сервере), `?local_domain=example.com` — если сервер требует определённое имя в HELO.

Провайдеры со своим API (Mailgun, Postmark, Amazon SES, Brevo и другие) подключаются установкой соответствующего пакета-моста symfony и указываются его строкой DSN. Несколько транспортов можно объединить: `failover://` переключается на следующий, когда транспорт недоступен, `roundrobin://` распределяет письма между всеми.

#### Настройка отдельными параметрами

Если `dsn` не задан, строка подключения собирается из параметров `transport` и `options` — так настраивались сайты до появления `dsn`, и этот способ продолжает работать:

```php
<?php

declare(strict_types=1);

return [
    'mail' => [
        'transport' => 'smtp',
        'options'   => [
            'smtp' => [
                'host'       => 'smtp.example.com',
                'port'       => 465,
                'username'   => 'mail@example.com',
                'password'   => 'password',
                'encryption' => 'ssl',
            ],
        ],
    ],
];
```

Значения `transport`: `smtp`, `sendmail`, `native`, `null`.

Параметры транспорта **smtp**: `host`, `port`, `username`, `password`, `encryption`, `verify_peer`, `local_domain`. Если `username` и `password` не заданы, подключение выполняется без авторизации.

Параметр `encryption` — это `ssl` (шифрование с первого байта, порт 465), `tls` (STARTTLS) или пустая строка (без шифрования).

У транспорта **sendmail** есть единственный параметр `command` — команда запуска. Если он не задан, используется `sendmail_path` из php.ini.

### Настройка через админку

Основные настройки почты можно менять и без редактирования файлов — в админке, раздел **Система → Настройки почты** (`/admin/settings/mail`). Страница доступна тем, кому разрешено менять настройки сайта, и записывает те же значения в **mail.local.php**.

Там же есть отправка тестового сообщения: письмо уходит немедленно, минуя очередь, поэтому ответ почтового сервера — включая текст ошибки, если что-то не так, — виден сразу на той же странице. Настройки перед проверкой нужно сохранить: они читаются при сборке контейнера, то есть вступают в силу со следующего запроса.

Подпись DKIM и поведение очереди в админке не редактируются: их задают один раз в файле конфигурации.

### Перенаправление всей почты

Копия сайта для разработки обычно работает с базой рабочего сайта, поэтому первая же проверка регистрации напишет реальному посетителю. Чтобы этого не случилось, укажите адрес, на который уйдут все письма:

```php
'redirect_to' => 'dev@example.com',
```

Можно указать и несколько адресов списком. Настоящие получатели сохраняются в заголовке **X-Original-To**, так что по перенаправленному письму видно, кому оно предназначалось. Перенаправляется не только заголовок, но и SMTP-конверт — то, куда письмо доставляется на самом деле. На рабочем сайте параметр должен быть пустым.

### Подпись DKIM

Подпись DKIM доказывает принимающему серверу, что письмо действительно отправлено с вашего домена — без неё письма заметно чаще попадают в спам. Настраивается в секции `dkim`:

```php
'dkim' => [
    // Сам ключ или путь к нему в виде file:///path/to/key.pem
    'private_key' => 'file:///etc/ssl/dkim/example.com.pem',
    'domain'      => 'example.com',
    // Публичный ключ должен быть опубликован в DNS как <selector>._domainkey.<domain>
    'selector'    => 'mail',
    'passphrase'  => '',
],
```

Пока любой из трёх параметров пуст, письма отправляются без подписи. Если ключ указан, но прочитать его не удалось, письма всё равно отправляются — без подписи, а причина записывается в лог.

### Отправка сообщений

Отправка email достаточно затратная операция. Для решения этой проблемы отправку email можно переложить на сервер. Для этого в JohnCMS реализована очередь сообщений. Чтобы отправить письмо, необходимо просто добавить его в очередь.

Письмо ставится в очередь через `MailQueueInterface` — внедрите его в свой use case или сервис:

```php
use Johncms\Mail\Queue\MailQueueInterface;
use Johncms\Mail\Queue\QueuedEmailDTO;

final readonly class RegisterUserUseCase
{
    public function __construct(private MailQueueInterface $mailQueue)
    {
    }

    public function execute(): void
    {
        $this->mailQueue->push(
            new QueuedEmailDTO(
                template: '@theme/emails/registration.twig',
                locale: 'ru',
                recipient: 'user@example.com',
                recipientName: 'Имя Пользователя',
                subject: 'Регистрация на сайте',
                priority: 1,
                variables: [
                    'user_name'       => 'UserName',
                    'user_login'      => 'UserLogin',
                    'link_to_confirm' => 'https://johncms.com',
                ],
            )
        );
    }
}
```

Что делает этот код?\
Он добавляет запись в таблицу **email\_messages**. А дальше система проверяет наличие не отправленных писем в очереди и отправляет их.

#### Поля QueuedEmailDTO

* **template** - шаблон, по которому формируется тело письма. Путь Twig, например `@theme/emails/registration.twig`. Как устроены шаблоны писем — в разделе [Шаблоны электронных сообщений](/10.0/obshie-svedeniya/shablony-elektronnykh-soobshenii-email.md). (**обязательное**)
* **locale** - код языка, на котором будет отправлено письмо. Это язык получателя, а не того, кто вызвал отправку. (**обязательное**)
* **recipient** - E-mail адрес получателя. (**обязательное**)
* **recipientName** - Имя получателя, которое будет отображаться в почтовом клиенте.
* **subject** - Тема сообщения. Не обязательна, но рекомендуется.
* **priority** - Приоритет отправки. Чем меньше, тем выше; по умолчанию 100.
* **variables** - Массив значений, доступных в шаблоне письма.
* **replyTo**, **replyToName** - Адрес и имя для ответа, если отвечать нужно не на адрес сайта. Например, в уведомлении из формы обратной связи это адрес написавшего посетителя.
* **cc**, **bcc** - Массивы адресов для копии и скрытой копии.

Адреса проверяются сразу при постановке в очередь: если адрес такой, что почтовый сервер его не примет, `push()` выбрасывает `InvalidEmailAddressException` и письмо в очередь не попадает. Это происходит там, где ошибка допущена, а не через минуту в кроне. Если адрес приходит не из проверенной формы, а, например, из настроек сайта, — перехватите исключение и запишите его в лог, как это сделано в модуле обратной связи.

Письмо отправляется сразу в двух версиях — HTML и текстовой, — это заметно влияет на то, попадёт ли оно в спам. Текстовая версия берётся из шаблона с расширением **.txt.twig**, а если его нет, получается из HTML автоматически. Подробности — в разделе [Шаблоны электронных сообщений](/10.0/obshie-svedeniya/shablony-elektronnykh-soobshenii-email.md).

Очередь разбирает планировщик задач: команда **mail:send-pending** запускается им раз в минуту и отправляет письма пачками. Отправки «на хитах» больше нет — если cron-задача планировщика не настроена, письма так и останутся в очереди.

### Повторные попытки и неудачи

Письмо, которое не удалось отправить, остаётся в очереди и отправляется повторно. Различаются два вида неудач:

* **временная** — почтовый сервер недоступен или отклонил соединение. Письмо остаётся в очереди, и следующая попытка выполняется с задержкой. Когда попытки заканчиваются, письмо помечается как неотправленное, а причина сохраняется в колонке `last_error`.
* **окончательная** — письмо невозможно собрать в принципе: не указан получатель, адрес не соответствует стандарту, шаблон не рендерится. Такое письмо не отправляется повторно, потому что результат будет тот же; оно сразу помечается как неотправленное с указанием причины.

Ни в одном из случаев письмо не помечается отправленным — таблица очереди остаётся честной записью того, что дошло до получателя, а что нет.

Поведение очереди настраивается в секции `queue` файла **mail.local.php**:

```php
'queue' => [
    // Сколько раз пробовать отправить письмо, прежде чем отказаться от него
    'max_attempts'   => 3,

    // Задержки в секундах перед 2-й, 3-й и последующими попытками.
    // Последнее значение используется для всех дальнейших попыток.
    'retry_delays'   => [60, 300, 900],

    // Через сколько секунд письмо, взятое в работу процессом, который не завершился
    // (например, был убит во время отправки), снова попадёт в обработку
    'lock_timeout'   => 900,

    // Сколько дней хранятся доставленные письма. 0 — хранить всегда
    'keep_sent_days' => 30,
],
```

Одновременный запуск нескольких обработчиков безопасен: пачка писем сначала закрепляется за конкретным процессом и только потом отправляется, поэтому одно и то же письмо не уйдёт дважды.

Команда **mail:cleanup** (планировщик запускает её раз в сутки) удаляет доставленные письма старше `keep_sent_days`. Письма, которые доставить не удалось, автоматически не удаляются никогда — это записи о том, что требует внимания.

### Обновление с версий без повторных попыток

Повторные попытки опираются на колонки таблицы `email_messages`, которых нет на сайтах, установленных раньше. Их добавляет обычное обновление базы данных:

```bash
php system/bin/console migrate
```

То же самое доступно в админке в разделе задач обслуживания — «Update the database». Новые установки получают нужные колонки сразу. Подробности — в разделе [Миграции базы данных](/10.0/baza-dannykh/migracii.md).

### Настройка отправки Email

Добавьте задачу в cron:

```bash
php system/bin/console schedule:run --no-interaction
```

Периодичность выполнения установить раз в 1 минуту.\
Обратите внимание, что может потребоваться указать полный путь к файлу от корня. Посмотреть его можно в **phpinfo()**, параметр **DOCUMENT\_ROOT** или вывести так:\
**echo $\_SERVER\['DOCUMENT\_ROOT'];**\
Более подробно про то как добавить задачу, вы можете уточнить у вашего хостинг провайдера.

Если вы обновляетесь со старых версий, обратите внимание на изменения:

* было: `php system/cron.php`
* стало: `php system/bin/console schedule:run --no-interaction`

Константа `USE_CRON` в `config/constants.php` удалена — отправка выполняется только планировщиком, переключать больше нечего.

Подробнее про работу планировщика: [Планировщик задач (schedule)](/10.0/konsol/planirovshchik-zadach-schedule.md)
