[00:48]
[🔒]
✪
Статья
Отправка писем с помощью Symfony Mailer
Категории: Программир.; Интернет, сети, web;
Оригинальный материал находится здесь
Дата обновления перевода 2025-08-30
Компоненты Symfony Mailer и Mime формируют мощную систему для создания и отправки электронных писем - с поддержкой составных сообщений, интеграцией Twig, встраиванием CSS, прикреплением файлов и многим другим. Установите их с помощью:
composer require symfony/mailer
Письма доставляются с помощью "транспорта". Сразу после установки, вы можете отправлять письма через SMTP. сконфигурировав DSN в вашем файле .env (параметры user, pass и port необязательны):
# .env MAILER_DSN=smtp://user:pass@smtp.example.com:port
# config/packages/mailer.yaml framework: mailer: dsn: '%env(MAILER_DSN)%'
Если имя пользователя, пароль или хост содержат в URI любой символ, считающийся особенным, (такой как : / ? # [ ] @ ! $ & ' ( ) * + , ; =), вы должны зашифровать их. См.`RFC 3986`_. чтобы увидеть полный список зарезервированных символов или используйте функцию urlencode, чтобы зашифровать их.
При использовании native://default, если php.ini использует команду sendmail -t, у вас не будет отчёта об ошибках, а заголовки Bcc не будут удалены. Крайне рекомендуется НЕ использовать native://default, так как вы не можете контролировать, как сконфигурирован sendmail (лучше использовать sendmail://default, если возможно).
Instead of using your own SMTP server or sendmail binary, you can send emails via a 3rd party provider. Mailer supports several - install whichever you want:
7.1 Интеграции Azure и Resend были представлены в Symfony 7.1.
7.2 Интеграции Mailomat, Mailtrap, Postal и Sweego были представлены в Symfony 7.2.
7.3 Интеграция AhaSend была представлена в Symfony 7.3.
Для удобства Symfony также предоставляет поддержку для Gmail (composer require symfony/google-mailer), но это не стоит использовать в производстве. В разработке вам, наверное, лучше стоит использовать ловца email'ов . Отметьте, что большинство поддерживаемых провайдеров также предлагают бесплатный уровень.
Каждая библиотека содержит рецепт Symfony Flex , который будет добавлять пример конфигурации в ваш файл .env. Например, представьте, что вы хотите использовать SendGrid. Для начала, установите его:
composer require symfony/sendgrid-mailer
Теперь у вас будет новая строка в вашем файле .env, которую вы можете раскомментировать:
# .env MAILER_DSN=sendgrid://KEY@default
MAILER_DSN - это не настоящий адрес: это удобный формат, который сгружает большую часть работы конфигурации почтовой программе. Схема sendgrid активирует поставщика SendGrid, который вы только что установили, который знает все о том, как доставить сообщения через SendGrid. Единственное, что вам нужно изменить - заполнитель KEY.
Какждый поставщик имеет разные переменные окружения, которые Mailer использует для конфигурации настоящего протокла, адрса и аутентификации для отправки. Некоторые также имеют опции, которые можно сконфигурировать с параметрами запроса в конце MAILER_DSN - как, например, ?region= для Amazon SES или Mailgun. Некоторые поставщики поддерживают отправку через http, api или smtp. Symfony выбирает лучший доступный транспорт, но вы можете форсировать использование одного из них:
# .env # форсировать использование SMTP вместо HTTP (который стоит по умолчанию) MAILER_DSN=sendgrid+smtp://$SENDGRID_KEY@default
Эта таблица отображает полный список доступных форматов DSN для каждого стороннего поставщика:
Если ваши данные безопасности содержат спциальные символы, вы должны URL-зашифровать их. К примеру, DSN ses+smtp://ABC1234:abc+12/345@default должна быть сконфигурирована как ses+smtp://ABC1234:abc%2B12%2F345@default
Если вы хотите использовать транспорт ses+smtp вместе с Messenger для фоновой отправки сообщений , вам нужно добавить параметр ping_threshold к вашему MAILER_DSN со значением меньшим, чем 10: ses+smtp://USERNAME:PASSWORD@default?ping_threshold=9
При использовании SMTP, тайм-аут по умолчанию для отправки сообщения до вызова исключения - это значение, определенное в опции PHP.ini default_socket_timeout.
Помимо SMTP, многие сторонние транспорты предлагают веб-API для отправки писем.Для этого необходимо установить (дополнительно к мосту)компонент HttpClient через composer require symfony/http-client.
Для использования Google Gmail у вас должен быть акаунт Google с включённой двухшаговой верификацией (2FA) и вы должны использовать App Password для аутнетификации. Также отметьте, что Google отзывает ваши пароли при смене пароля акаунта Google и затем вам нужно будет сгенерировать новый пароль. Использование других методов (вроде XOAUTH2 или Gmail API) на данный момент не поддерживается. Вы должны использовать Gmail только в целях тестирования и использовать настоящего провайдера в производстве.
Если вы хотите переопределить хост для поставщика по умолчанию (для отладки проблемы используя сервис вроде requestbin.com), измените default в вашем хостеt:
# .env MAILER_DSN=mailgun+https://KEY:DOMAIN@requestbin.com
Заметьте, что протокол всегда будет HTTPs и не может быть изменен.
Конкретные транспорты, например mailgun+smtp, предназначены для работы без какой-либо ручной конфигурации.Изменение порта путем добавления его в DSN не поддерживается ни для одного из этих транспортов +smtp.Если вам нужно изменить порт, используйте транспорт smtp, например, так:
# .env MAILER_DSN=smtp://KEY:DOMAIN@smtp.eu.mailgun.org.com:25
Некоторые сторонние почтовые программы, использующие API, поддерживают обратные вызовы статуса через веб-хуки. Подробнее см. в документации Webhook.
Mailer Symfony поддерживает высокую доступность через технику под названием "failover", чтобы гарантировать, что письма будут отправлены, даже если один сервер потерпит неудачу.
Транспорт failover сконфигурирован с двумя или более транспортами и ключевым словом failover:
MAILER_DSN="failover(postmark+api://ID@default sendgrid+smtp://KEY@default)"
Транспорт failover начинает с использования первого транспорта, и если он терпит неудачу, он повторно попробует отправку со следующим транспортом, пока один из них не приведет к успеху (или пока они все не потерпят неудачу).
По умолчанию, после неудачной попытки происходит повторная попытка доставки. Вы можете настроить период повторной попытки, установив опцию retry_period в DSN:
MAILER_DSN="failover(postmark+api://ID@default sendgrid+smtp://KEY@default)?retry_period=15"
7.3 Опция retry_period была представлена в Symfony 7.3.
Mailer Symfony поддерживает балансировку нагрузки через технику под названием "round-robin" для распределения рабочей нагрузки отправки писем по нескольким транспортам.
Транспорт round-robin сконфигурирован с двумя или более транспортами и ключевым словом roundrobin:
MAILER_DSN="roundrobin(postmark+api://ID@default sendgrid+smtp://KEY@default)"
Транспорт round-robin начинает с рандомно выбранного транспорта, а затем переключается на следующий доступный трансфер для каждого последующего письма.
Как и с транспортом failover, round-robin повторно пытается совершить отправку, пока транспорт не добьется успеха (или пока все не потерпят неудачу). В отличие от транспорта failover, он распространяет нагрузку по всем своим транспортам.
MAILER_DSN="roundrobin(postmark+api://ID@default sendgrid+smtp://KEY@default)?retry_period=15"
По умолчанию, транспорт SMTP выполняет верификацию точек TLS. Это поведение конфигурируется опцией verify_peer. Хотя и не рекомендуется отключать верификацию из соображений безопасности, это может быть полезно при разработке приложения или при использовании самозаверенного сертификата:
$dsn = 'smtp://user:pass@smtp.example.com?verify_peer=0';
Дополнительная верификация отпечатков пальцев может быть осуществлена с помощью опции peer_fingerprint. Это особенно полезно, когда используется самоподписанный сертификат и необходимо отключение verify_peer, но безопасность все равно желательна. Отпечаток может быть указан в виде хеша SHA1 или MD5:
$dsn = 'smtp://user:pass@smtp.example.com?peer_fingerprint=6A1CF3B08D175A284C30BC10DE19162307C7286E';
7.1 Опция отключения автоматического TLS была представлена в Symfony 7.1.
По умолчанию компонент Mailer будет использовать шифрование, если расширение OpenSSLи SMTP-сервер поддерживают STARTTLS. Это поведение можно отключить, вызвав setAutoTls(false) в экземпляре EsmtpTransport или установив опцию auto_tls в значение false в DSN:
$dsn = 'smtp://user:pass@10.0.0.25?auto_tls=false';
Не рекомендуется отключать TLS при подключении к SMTP-серверу через Интернет, но это может быть полезно, когда и приложение, и SMTP-сервер находятся в защищенной сети, где нет необходимости в дополнительном шифровании.
Эта настройка работает только при использовании протокола smtp://.
Вы можете захотеть убедиться, что TLS используется (напрямую либо через STARTTLS) во время отправки почты через SMTP, независимо от других опций или поддержки SMTP-сервера. Чтобы требовать TLS, вызовите setRequireTls(true) в экземпляре EsmtpTransport, или установите опцию require_tls как true в DSN:
$dsn = 'smtp://user:pass@10.0.0.25?require_tls=true';
Когда TLS является обязательным, вызывается TransportException, если во время первоначальной связи с SMTP-сервером не удается установить TLS-соединение.
Эта настройка применяется только при использовании протокола smtp://.
7.3 Опция require_tls была представлена в Symfony 7.3.
7.3 Опция привязки к IPv4, IPv6 или конкретному IP-адресу была представлена в Symfony 7.3.
По умолчанию, исходящий SocketStream будет привязываться к IPv4 или IPv6 на основании доступных интерфейсов. Вы можете принудительно привязать к конкретному протоколу или IP-адресу с помощью опции source_ip. Чтоб привязать к IPv4, используйте:
$dsn = 'smtp://smtp.example.com?source_ip=0.0.0.0';
Что касается RFC2732, IPv6 адреса должны быть заключены в квадратные скобки. Чтобы привязать к IPv6, используйте:
$dsn = 'smtp://smtp.example.com?source_ip=[::]';
Эта опция работает только при использовании протокола smtp://.
По умолчанию SMTP-транспорты будут пытаться войти в систему, используя все методы аутентификации, доступные на SMTP-сервере, один за другим. В некоторых случаях может быть полезно переопределить поддерживаемые методы аутентификации, чтобы гарантировать, что предпочтительный метод будет использоваться первым.
Это можно сделать из конструктора EsmtpTransport или с помощью метода setAuthenticators():
use Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; // Выберите один из двух вариантов: // Вариант 1: передать аутентификаторы конструктору $transport = new EsmtpTransport( host: 'oauth-smtp.domain.tld', authenticators: [new XOAuth2Authenticator()] ); // Вариант 2: вызвать метод для переопределения аутентификаторов $transport->setAuthenticators([new XOAuth2Authenticator()]);
command
Команда для выполнения транспортом sendmail:
$dsn = 'sendmail://default?command=/usr/sbin/sendmail%20-oi%20-t'
local_domain
Имя домена для использования в команде HELO:
$dsn = 'smtps://smtp.example.com?local_domain=example.org'
restart_threshold
Максимальное количество сообщений для отправки до перезагрузки транспорта. Может быть использована вместе с restart_threshold_sleep:
$dsn = 'smtps://smtp.example.com?restart_threshold=10&restart_threshold_sleep=1'
restart_threshold_sleep
Количество секунд сна между остановкой и перезагрузкой транспорта. Часто сочетается с restart_threshold:
ping_threshold
Минимальное количество секунд между двумя сообщениями, необходимое для пинга серверу:
$dsn = 'smtps://smtp.example.com?ping_threshold=200'
max_per_second
Количество сообщений, которые нужно отправлять за секунду (0, чтобы отключить это ограничение):
$dsn = 'smtps://smtp.example.com?max_per_second=2'
Если вы хотите поддерживать собственный пользовательский DSN (acme://...), вы можете создать фабрику пользовательского транспорта. Для этого создайте класс, который реализует TransportFactoryInterface или, есливы предпочитаете, расширьте класс AbstractTransportFactory, чтобы сократить количество кода:
// src/Mailer/AcmeTransportFactory.php final class AcmeTransportFactory extends AbstractTransportFactory { public function create(Dsn $dsn): TransportInterface { // проанализировать заданный DSN, извлечь данные/удостоверения из него // and then, create and return the transport } protected function getSupportedSchemes(): array { // это поддерживает DSN, начинающийся с `acme://` return ['acme']; } }
После создания пользовательского транспортного класса зарегистрируйте его как сервис в вашем приложении и добавьте к нему тег mailer.transport_factory.
Для отправки письма, получите экземпляр Mailer, используя подсказку MailerInterface, и создайте объект Email:
// src/Controller/MailerController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\Email; use Symfony\Component\Routing\Attribute\Route; class MailerController extends AbstractController { #[Route('/email')] public function sendEmail(MailerInterface $mailer): Response { $email = (new Email()) ->from('hello@example.com') ->to('you@example.com') //->cc('cc@example.com') //->bcc('bcc@example.com') //->replyTo('fabien@example.com') //->priority(Email::PRIORITY_HIGH) ->subject('Time for Symfony Mailer!') ->text('Sending emails is fun again!') ->html('See Twig integration for better HTML integration!'); $mailer->send($email); // ... } }
Вот и все! Сообщение будет немедленно отправлено с помощью транспорта, который вы сконфигурировали. Если вы предпочитаете отправлять сообщения асинхронно для повышения производительности, прочтите раздел Отправка сообщений асинхронно . Также, если в вашем приложении установлен компонент Messenger, то все письма будут отправляться асинхронно по умолчанию (но :ref:`вы можете изменить это ).
Все методы, требующие адресов электронной почты (from(), to(), etc.), принимают как строки, так и объекты адресов:
// ... use Symfony\Component\Mime\Address; $email = (new Email()) // адрес почты как обычная строка ->from('fabien@example.com') // символы, не относящиеся к стандарту ASCII, поддерживаются как в локальной части, так и в домене; // если SMTP-сервер не поддерживает эту функцию, вы увидите исключение ->from('jânë.dœ@ëxãmplę.com') // адрес почты как объект ->from(new Address('fabien@example.com')) // определение адреса и имени почты как объекта // (почтовые клиенты отобразят имя) ->from(new Address('fabien@example.com', 'Fabien')) // определение адреса и имени почты как строки // (формат должен соответствовать: 'Имя ') ->from(Address::create('Fabien Potencier ')) // ... ;
Вместо вызова ->from() каждый раз при создании письма, вы можете сконфигурировать письма глобально , чтобы установить одну и ту же From письма во всех сообщениях.
7.2 Поддержка адресов электронной почты, отличных от ASCII (например, jânë.dœ@ëxãmplę.com) была представлена в Symfony 7.2.
Локальная часть адреса (то, что перед @) может включать в себя символы UTF-8, кроме адреса отправителя (чтобы избежать ошибок с возвращенными письмами). Например: föóbàr@example.com, 用户@example.com, θσερ@example.com, и т.д.
Используйте методы addTo(), addCc(), или addBcc(), чтобы добавить больше адресов:
$email = (new Email()) ->to('foo@example.com') ->addTo('bar@example.com') ->cc('cc@example.com') ->addCc('cc2@example.com') // ... ;
Как вариант, вы можете передать несколько адресов каждому методу:
$toAddresses = ['foo@example.com', new Address('bar@example.com')]; $email = (new Email()) ->to(...$toAddresses) ->cc('cc1@example.com', 'cc2@example.com') // ... ;
Сообщения содержат некоторое количество полей заголовков, чтобы описать их содержание. Symfony устанавливает все заголовки автоматически, но вы также можете установить собственные заголовки. Существуют разные типы заголовков (заголовок Id, заголовок Mailbox, заголовок Date и т.д.), но в большинстве случаев вы будете устанавливать текстовые заголовки:
$email = (new Email()) ->getHeaders() // этот заголовок сообщает авто-ответчикам ("режим отпуска электронной почты") // не отвечать на это сообщение, так как это автоматизированное письмо ->addTextHeader('X-Auto-Response-Suppress', 'OOF, DR, RN, NRN, AutoReply'); // использовать массив, если вы хотите добавить заголовок с несколькими значениями // (например, в заголовке "References" или "In-Reply-To") ->addIdHeader('References', ['123@example.com', '456@example.com']) // ... ;
Вместо вызова ->addTextHeader() каждый раз при создании письма, вы можете сконфигурировать письма глобально , чтобы установить одни и те же заголовки во всех отправленных письмах.
Текстовое и HTML-содержание сообщений писем может быть строками (обычно в результате отображения какого-то шаблона), или PHP-источниками:
$email = (new Email()) // ... // простое содержание, определенное как строка ->text('Lorem ipsum...') ->html('Lorem ipsum...') // прикрепить поток файлов ->text(fopen('/path/to/emails/user_signup.txt', 'r')) ->html(fopen('/path/to/emails/user_signup.html', 'r')) ;
Вы также можете использовать шаблоны Twig, чтобы отобразить содержание текста и HTML. Прочтите раздел Twig: HTML & CSS далее в этой статье, чтобы узнать больше.
Используйте метод attachFromPath(), чтобы прикреплять файлы, существующие в вашей файловой системе:
use Symfony\Component\Mime\Part\DataPart; use Symfony\Component\Mime\Part\File; // ... $email = (new Email()) // ... ->addPart(new DataPart(new File('/path/to/documents/terms-of-use.pdf'))) // опционально, вы можете сообщить клиентам почты, чтобы они отображали пользовательское имя файла ->attachFromPath('/path/to/documents/privacy.pdf', 'Privacy Policy') // опционально, вы можете предоставить ясный тип MIME (в другом случае он будет угадываться) ->attachFromPath('/path/to/documents/contract.doc', 'Contract', 'application/msword') ;
Также вы можете присоединить содержание из потока, передав его непосредственно в DataPart:
$email = (new Email()) // ... ->addPart(new DataPart(fopen('/path/to/documents/contract.doc', 'r'))) ;
Если вы хотите отобразить изображения внутри вашего письма, вы должны их встроить, а не добавлять их как вложения. При использовании Twig для отображения содержания письма, как объясняется далее в этой статье , изображения встраиваются автоматически. В другом случае, вам нужно будет встроить их вручную.
Сначала используйте метод addPart() для добавления изображения из файла или потока:
$email = (new Email())// ... // получить содержание изображение из PHP-источника ->embed(fopen('/path/to/images/logo.png', 'r'), 'logo') // получить содержание изображение из существующего файла ->embedFromPath('/path/to/images/signature.gif', 'footer-signature');
$email = (new Email())
// ... // получить содержание изображение из PHP-источника ->embed(fopen('/path/to/images/logo.png', 'r'), 'logo') // получить содержание изображение из существующего файла ->embedFromPath('/path/to/images/signature.gif', 'footer-signature')
;
Используйте метод asInline(), чтобы встроить содержание, вместо того, чтобы прикреплять его.
Второй необязательный аругмент обоих методов - это имя изображения ("Content-ID" по стандарту MIME). Его значение - это произвольная строка, используемая позже для того, чтобы ссылаться на изображение внутри HTML содержания:
$email = (new Email()) // ... ->addPart((new DataPart(fopen('/path/to/images/logo.png', 'r'), 'logo', 'image/png'))->asInline()) ->addPart((new DataPart(new File('/path/to/images/signature.gif'), 'footer-signature', 'image/gif'))->asInline()) // сошлитесь на изображения, используя синтаксис 'cid:' + "image embed name" ->html(' ... ...') // использовать тот же синтаксис для изображений, добавленных как фоновые изображения HTML ->html('... ... ...') ;
Вы также можете использовать метод DataPart::setContentId(), чтобы определить пользовательский Content-ID изображения и использовать его в качестве ссылки на cid:
$part = new DataPart(new File('/path/to/images/signature.gif')); $part->setContentId('footer-signature');$email = (new Email())// ... ->addPart($part->asInline()) ->html('... cid:footer-signature"> ...');
$part = new DataPart(new File('/path/to/images/signature.gif')); $part->setContentId('footer-signature');
// ... ->addPart($part->asInline()) ->html('... cid:footer-signature"> ...')
Вместо вызова ->from() по каждому письму, которое вы создаете, вы можете сконфигурировать это значение глобально, чтобы оно было установлено во всех отправленных письмах. То же самое верно и для ->to(), и для заголовков.
# config/packages/mailer.yaml framework: mailer: envelope: sender: 'fabien@example.com' recipients: ['foo@example.com', 'bar@example.com'] headers: From: 'Fabien ' Bcc: 'baz@example.com' X-Custom-Header: 'foobar'
Некоторые сторонние поставщики не поддерживают использование ключевых слов вроде from в headers. Просмотрите документацию вашего поставщика перед установкой любого глобального заголовка.
Symfony Mailer считает отправку успешной, когда ваш транспорт (SMTP-сервер или сторонний поставщик) принимает письмо для дальнейшей доставки. Сообщение может быть утеряно или не доставлено позднее из-за проблем в вашем поставщике, но это выходит за пределы возможностей вашего приложения Symfony.
Если при передаче письма вашему транспорту произошла ошибка, Symfony вызывает TransportExceptionInterface. Поймайте исключение, чтобы восстановиться после ошибки или для отображения некого сообщения:
use Symfony\Component\Mailer\Exception\TransportExceptionInterface; $email = new Email(); // ... try { $mailer->send($email); } catch (TransportExceptionInterface $e) { // некая ошибка предотвратила отправку письма; отобразить сообщение // об ошибке или попробовать отправить сообщение повторно }
Метод send() почтового сервиса, внедренный при использовании MailerInterface, ничего не возвращает, поэтому вы не можете получить доступ к информации отправленного письма. Это так, потому что он отправляет сообщение почты асинхронно, если в приложении используется компонент Messenger.
Чтобы получить доступ к информации про отправленное письмо, обновите ваш код, чтобы заменить MailerInterface на TransportInterface:
-use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mailer\Transport\TransportInterface; // ... class MailerController extends AbstractController { #[Route('/email')] - public function sendEmail(MailerInterface $mailer): Response + public function sendEmail(TransportInterface $mailer): Response { $email = (new Email()) // ... $sentEmail = $mailer->send($email); // ... } }
Метод send() из TransportInterface возвращает объект типа SentMessage. Это так, потому что он всегда отправляет письма синхронно, даже если ваше приоложение использует компонентMessenger.
Объект SentMessage предоставляет доступ к изначальному сообщению (getOriginalMessage())и к некоторой информации отладки (getDebug()), такой как HTTP-вызовы, сделанные HTTP-транспортами, что полезно для отладки ошибок.
Вы также можете получить достук к объкту SentMessageпутем прослушивания SentMessageEvent , и получить getDebug() путем прослушивания FailedMessageEvent .
Некоторые поставщики почтовых программ изменяют Message-Id при отправке письма. Метод getMessageId() из SentMessage всегда возвращает определенный ID сообщения (тот самый рандомный ID, сгенерированный Symfony или новый ID, сгенерированный поставщиком почтовой программы).
Исключения, связанные с транспортом почтовой программы (которые реализуют TransportException) также предоставляют эту информацию отладки через метод getDebug().
Компонент Mime интегрируется с шаблонизатором Twig , чтобы предоставить продвинутые функции вроде встраивания CSS-стилей и поддержки для фреймворков HTML/CSS, чтобы создавать сложные сообщеня HTML-писем. Для начала, убедитесь, что Twig установлен:
composer require symfony/twig-bundle
Чтобы определить содержание вашего письма с Twig, используйте класс TemplatedEmail. Этот класс расширяет нормальный класс Email, но добавляет некоторые новые методы для шаблонов Twig:
use Symfony\Bridge\Twig\Mime\TemplatedEmail; $email = (new TemplatedEmail()) ->from('fabien@example.com') ->to(new Address('ryan@example.com')) ->subject('Thanks for signing up!') // путь шаблона Twig для отображения ->htmlTemplate('emails/signup.html.twig') // передайте переменные (name => value) to the template ->context([ 'expiration_date' => new \DateTime('+7 days'), 'username' => 'foo', ]) ;
Затем, создайте шаблон:
{# templates/emails/signup.html.twig #} Welcome {{ email.toName }}! You signed up as {{ username }} the following email: {{ email.to[0].address }} Click here to activate your account (this link is valid until {{ expiration_date|date('F jS') }})
Шаблон Twig имеет доступ к любым параметрам, переданным в метод context() класса TemplatedEmail и также специальной переменной под названием email, которая является экземпляром WrappedTemplatedEmail.
Когда текстовое содержание TemplatedEmail не определено ясно, почтовая программа автоматически сгенерирует его из HTML-содержания.
Symfony использует следующую стратегию при генерировании текстовой версии электронного письма:
Если вы хотите определить текстовое содержание самостоятельно, используйте метод text(), разъясненный в предыдущих разделах, или метод textTemplate(), предоставленный классом TemplatedEmail:
+use Symfony\Bridge\Twig\Mime\TemplatedEmail; $email = (new TemplatedEmail()) // ... ->htmlTemplate('emails/signup.html.twig') + ->textTemplate('emails/signup.txt.twig') // ... ;
Вместо того, чтобы разбираться с синтаксисом , который разъяснялся в предыдущих разделах, при использовании Twig для отображения содержания письма, вы можете сослаться на файлы изображений, как обычно. Для начала, чтобы упростить все, определите пространство имен Twig под названием images, которое указывает на тот каталог, где хранятся ваши изображения:
# config/packages/twig.yaml twig: # ... paths: # укажите туда, где живут ваши изображения '%kernel.project_dir%/assets/images': images
Теперь, используйте специального помощника Twig email.image(), чтобы встроить изображение в содержание письма:
{# '@images/' refers to the Twig namespace defined earlier #} Welcome {{ email.toName }}! {# ... #}
По умолчанию это создат вложение, используя путь файла как имя файла: Content-Disposition: inline; name="cid..."; filename="@images/logo.png". Это поведение можно переопределить, передав пользовательское имя файла в качестве третьего аргумента:
7.3
Третий аргумент email.image() был представлен в Symfony 7.3.
Дизайн HTML-содержания письма очень отличается от дизайна обычной HTML-страницы. Для начала, большинство почтовых клиентов поддержвают только часть всех CSS-функций. Кроме того, популярные почтовые клиенты, вроде Gmail, не поддерживают определяющие стили внутри разделов ... и вы должны встроить все CSS-стили.
Встраивание CSS означает, что каждый HTML-тег должен определять атрибут style со всеми его CSS-стилями. Это может сильно запутать упорядочивание вашего CSS. Поэтому Twig предоставляет CssInlinerExtension, который автоматизирует все для вас. Установите его:
composer require twig/extra-bundle twig/cssinliner-extra
Расширение включается автоматически. Чтобы использовать его, оберните весь шаблон в фильтр inline_css:
{% apply inline_css %} {# здесь, определите ваши СSS-стили, как обычно #} h1 { color: #333; } Welcome {{ email.toName }}! {# ... #} {% endapply %}
Вы также можете определять СSS-стили во внешних файлах и передавать их как аргументы фильтру:
{% apply inline_css(source('@styles/email.css')) %} Welcome {{ username }}! {# ... #} {% endapply %}
Вы можете передать неограниченное количество аргументов к inline_css() для загрузки нескольких CSS-файлов. Для того, чтобы этот пример работал, вам также нужно определить новое пространство имен Twig под названием styles, которое указывает на каталог, где живет email.css:
# config/packages/twig.yaml twig: # ... paths: # укажите туда, где живут ваши css-файлы '%kernel.project_dir%/assets/styles': styles
Twig предоставляет другое расширение под названием MarkdownExtension, которое позволяет вам определять содержание писем с использованием синтаксиса Markdown. Чтобы использовать его, установите расширение и библиотеку конверсий Markdown (расширение совместимо с несколькими популярными библиотеками):
composer require twig/extra-bundle twig/markdown-extra league/commonmark
Расширение добавляет фильтр markdown_to_html, который вы можете использовать, чтобы преобразовывать части или все содержание письма из Markdown в HTML:
{% apply markdown_to_html %} Welcome {{ email.toName }}! =========================== Вы подписались на наш сайт, используя следующий адрес электронной почты: `{{ email.to[0].address }}` [Нажмите здесь, чтобы активировать ваш аккаунт]({{ url('...') }}) {% endapply %}
Создание писем с прекрасным дизайном, которые работают в каждом почтовом клиенте, настолько сложно, что существуют целые фреймворки HTML/CSS, посвященные этому. Один из наиболее популярных фреймворков называется`Inky`_. Он определяет синтаксиси, основываясь на тегах типа HTML, которые позже преобразуются в настоящий HTML-код и отправляются пользователям:
This is a column.
Twig предоставляет интегракцию с Inky через InkyExtension. Для начала, установите расширение в вашем приложении:
composer require twig/extra-bundle twig/inky-extra
Расширение добавляет фильтр inky_to_html, которые может быть использован для преображения частей или всего содержания письма из Inky в HTML:
{% apply inky_to_html %} Welcome {{ email.toName }}! {# ... #} {% endapply %}
Вы можете скомбинировать все фильтры, чтобы создать сложные сообщения писем:
{% apply inky_to_html|inline_css(source('@styles/foundation-emails.css')) %} {# ... #} {% endapply %}
Это использует пространство имен стилей Twig , которое мы создали ранее. Вы могли бы, к примеру, скачать файл foundation-emails.css прямо с GitHub и сохранить его в assets/styles.
Существует возможность цифрового подписания и/или шифрования сообщений электронных писем для усиления их целосности/безопасности. Обе опции можно объединить для шифрования сообщения с электронной подписью и/или для цифрового подписания зашифрованного сообщения.
Перед тем, как подписывать/зашифровывать сообщения, убедитесь, что у вас есть:
При использовании OpenSSL для генерирования сертификатов, не забудьте добавить опцию команды -addtrust emailProtection.
Подписание и шифрование сообщений требуют полного отображения их содержания.Например, содержание templated emails отображается с помощью MessageListener. Поэтому, если вы хотите подписать и/или зашифровать такое сообщение, вам нужно сделать это в слушателе MessageEvent, запущенном после него (необходимо установить отрицательный приоритет своему слушателю).
При добавлении цифровой подписи к сообщению, генерируется криптографический хеш для всего содержания сообщения (включая вложения). Этот хеш добавляется как вложение, чтобы получатель мог валидировать целосность полученного сообщения. Однако, содержание изначального сообщения остается читаемым для почтовых агентов, не поддерживающих подписанные сообщения, поэтому вы также должны зашифровать сообщение, если вы хотите скрыть его содержание.
Вы можете подписывать сообщения используя S/MIME или DKIM. В обоих случаях, сертификат и приватный ключ долшжны быть PEM-зашифрованы, и могут быть либо созданы с использованием, к примеру, OpenSSL, либо получены у официальной Сертификационной компании (CA). Получатель письма должен иметь сертификат CA в списке доверенных лиц, чтобы верифицировать подпись.
Если вы используете подпись сообщения, отправка в Bcc будет удалена изсообщения. Если вам нужно отправить сообщение нескольким получателям, необходимо вычислять новую подпись для каждого получателя.
S/MIME стандартна для шифрования публичных ключей и подписания данных MIME. Она требует использование как сертификата, так и приватного ключа:
use Symfony\Component\Mime\Crypto\SMimeSigner; use Symfony\Component\Mime\Email; $email = (new Email()) ->from('hello@example.com') // ... ->html('...'); $signer = new SMimeSigner('/path/to/certificate.crt', '/path/to/certificate-private-key.key'); // если приватный ключ имеет кодовую фразу, передайте ее как третий аргумент // new SMimeSigner('/path/to/certificate.crt', '/path/to/certificate-private-key.key', 'the-passphrase'); $signedEmail = $signer->sign($email); // теперь используйте компонент Mailer, чтобы отправить этот $signedEmail вместо изначального письма
Класс SMimeSigner определяет другие необязательные аргументы для передачи промежуточных сертификатов и для конфигурации процесса подписания, используя побитовые опции оператора для PHP-функции openssl_pkcs7_sign.
DKIM - это метод аутентификации письма, который добавляет цифровую подпись, ссылающуюся на основной домен, к каждому исходящему письму. Он требует приватный ключ, но не сертификат:
use Symfony\Component\Mime\Crypto\DkimSigner; use Symfony\Component\Mime\Email; $email = (new Email()) ->from('hello@example.com') // ... ->html('...'); // первый аргумент: такой же, как openssl_pkey_get_private(), либо строка с содержанием // приватного ключа, либо абсолютный путь к нему (с префиксом 'file://') // второй и третий аргунметы: имя домена и "селектор", используемый для выполнения поиска DNS // (селектор - это строка, используемая для указания на конкретную запись публичного ключа DKIM в вашей DNS) $signer = new DkimSigner('file:///path/to/private-key.key', 'example.com', 'sf'); // если приватный ключ имеет кодовую фразу, передайте ее как пятый аргумент // new DkimSigner('file:///path/to/private-key.key', 'example.com', 'sf', [], 'the-passphrase'); $signedEmail = $signer->sign($email); // теперь используйте компонент Mailer, чтобы отправить этот $signedEmail вместо изначального письма // подпись DKIM предоставляет множество опций конфигурации и объект помощника для их конфигурирования use Symfony\Component\Mime\Crypto\DkimOptions; $signedEmail = $signer->sign($email, (new DkimOptions()) ->bodyCanon('relaxed') ->headerCanon('relaxed') ->headersToIgnore(['Message-ID']) ->toArray() );
Вместо того, чтобы создавать экземпляр подписателя для каждого электронного письма, вы можете сконфигурировать глобальный подписатель, который автоматически применяется ко всем исходящим сообщениям. Такой подход минимизирует повторение и централизирует вашу конфигурацию для подписей DKIM и S/MIME.
# config/packages/mailer.yaml framework: mailer: dkim_signer: key: 'file://%kernel.project_dir%/var/certificates/dkim.pem' domain: 'symfony.com' select: 's1' smime_signer: key: '%kernel.project_dir%/var/certificates/smime.key' certificate: '%kernel.project_dir%/var/certificates/smime.crt' passphrase: ''
7.3 Глобальная подпись сообщений была представлена в Symfony 7.3.
При шифровании сообщения, все сообщения (включая вложения) шифруется с использованием сертификата. Следовательно, только получатели, имеющие соответствующий приватный ключ, могут прочитать содержание изначального сообщения:
use Symfony\Component\Mime\Crypto\SMimeEncrypter; use Symfony\Component\Mime\Email; $email = (new Email()) ->from('hello@example.com') // ... ->html('...'); $encrypter = new SMimeEncrypter('/path/to/certificate.crt'); $encryptedEmail = $encrypter->encrypt($email); // теперь используйте компонент Mailer, чтобы отправить этот $encryptedEmail вместо изначального письма
Вы можете передать больше одного сертификата конструктору SMimeEncrypter, и он выберет соответствующий сертификат, в зависимости от опции To:
$firstEmail = (new Email()) // ... ->to('jane@example.com'); $secondEmail = (new Email()) // ... ->to('john@example.com'); // второй необязательный аргумент SMimeEncrypter определяет, какой алгоритм шифрования используется // (должен быть одной из этих констант: https://www.php.net/manual/en/openssl.ciphers.php) $encrypter = new SMimeEncrypter([ // ключ = получатель письма; значение = путь к файлу сертификата 'jane@example.com' => '/path/to/first-certificate.crt', 'john@example.com' => '/path/to/second-certificate.crt', ]); $firstEncryptedEmail = $encrypter->encrypt($firstEmail); $secondEncryptedEmail = $encrypter->encrypt($secondEmail);
Вместо того, чтобы создавать новый кодировщик для каждого электронного письма, вы можете сконфигурировать глобальный кодировщик S/MIME, который автоматически применяется ко всем исходящим сообщениям:
# config/packages/mailer.yaml framework: mailer: smime_encrypter: repository: App\Security\LocalFileCertificateRepository
Опция repository - это ID сервиса, который реализует SmimeCertificateRepositoryInterface. Этот интерфейс требует только одного метода: findCertificatePathFor(), который должен вернуть путь файла до сертификата, ассоциированного с заданным адресом электронной почты:
namespace App\Security; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Mailer\EventListener\SmimeCertificateRepositoryInterface; class LocalFileCertificateRepository implements SmimeCertificateRepositoryInterface { public function __construct( #[Autowire(param: 'kernel.project_dir')] private readonly string $projectDir ){} public function findCertificatePathFor(string $email): ?string { $hash = hash('sha256', strtolower(trim($email))); $path = sprintf('%s/storage/%s.crt', $this->projectDir, $hash); return file_exists($path) ? $path : null; } }
7.3 Конфигурация глобального шифрования сообщений была представлена в Symfony 7.3.
Вы можете захотеть использовать более одного транспорта почтовой программы для доставки ваших сообщений. Это можно сконфигурировать, заменив запись конфигурации dsn на запись transports, следующим образом:
# config/packages/mailer.yaml framework: mailer: transports: main: '%env(MAILER_DSN)%' alternative: '%env(MAILER_DSN_IMPORTANT)%'
По умолчанию используется первый транспорт. Другие транспорты могут быть использованы путем добавления к письму текстового заголовка X-Transport (который Mailer автоматически удалит из финального письма):
// Отправить, используя первый "главный" транспорт ... $mailer->send($email); // ... или использовать "альтернативный" $email->getHeaders()->addTextHeader('X-Transport', 'alternative'); $mailer->send($email);
Когда вы вызываете $mailer->send($email), письмо сразу же отправляется транспорту. Для улучшения производительности, вы можете использовать преимущества Мессенджера, чтобы отправлять сообщения позже через транспорт Мессенджера.
Начните, следуя документации Мессенджера и сконфигурировав транспорт. Когда все будет настроено, при вызове $mailer->send(), сообщение SendEmailMessage будет запущено через автобус сообщений по умолчанию (messenger.default_bus). Предполагая, что у вас есть транспорт под названием async, вы можете маршрутизировать сообщение в него:
# config/packages/messenger.yaml framework: messenger: transports: async: "%env(MESSENGER_TRANSPORT_DSN)%" routing: 'Symfony\Component\Mailer\Messenger\SendEmailMessage': async
Благодаря этому, вместо немедленной доставки, сообщения будут отправлены транспорту для обработки позже (см. ). Отметьте, что "отображение" электонного письма (вычисленные заголовки, отображение тела...) также отложено и произойдет прямо перед отправкой письма обработчиком Messenger.
При асинхронной отправке письма, его экземпляр должен быть сериализируемым. Это всегда так для экземпляров Email, но при отправке TemplatedEmail, вы должны гарантировать, что context сериализируемый. Если у вас есть несериализируемые переменные, вроде сушностей Doctrine, либо замените их на более конкретные сущности, либо отобразите письмо перед вызовом $mailer->send($email):
use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\BodyRendererInterface; public function action(MailerInterface $mailer, BodyRendererInterface $bodyRenderer): void { $email = (new TemplatedEmail()) ->htmlTemplate($template) ->context($context) ; $bodyRenderer->render($email); $mailer->send($email); }
Вы можете сконфигурировать, какой автобус используется для запуска сообщения, используя опцию message_bus. Вы также можете установить ее как false, чтобы вызвать транспорт Mailer напрямую и отключить асихнронную доставку.
# config/packages/mailer.yaml framework: mailer: message_bus: app.another_bus
В случаях долгосрочных скриптов, и когда Mailer использует SmtpTransport, вы можете вручную отсоединиться от SMTP-сервера, чтобы избежать открытого соединения с SMTP-сервером между отправкой писем. Вы можете сделать это, используя метод stop().
Вы также можете выбрать транспорт, добавив заголовок X-Bus-Transport (который будет удалён автоматически из финальной версии сообщения):
// Использовать транспорт автобуса "app.another_bus": $email->getHeaders()->addTextHeader('X-Bus-Transport', 'app.another_bus'); $mailer->send($email);
Определенные сторонние транспорты поддерживают теги и метаданные писем, что может быть использовано для группирования, отслеживания и рабочих процессов. Вы можете добавить их, используя классы TagHeader и MetadataHeader. Если ваш транспорт поддерживает заголовки, он преобразует их в соответствующий формат:
use Symfony\Component\Mailer\Header\MetadataHeader; use Symfony\Component\Mailer\Header\TagHeader; $email->getHeaders()->add(new TagHeader('password-reset')); $email->getHeaders()->add(new MetadataHeader('Color', 'blue')); $email->getHeaders()->add(new MetadataHeader('Client-ID', '12345'));
Если ваш транспорт не поддерживает теги и метаданные, они будут добавлены в виде пользовательских заголовков:
X-Tag: password-reset X-Metadata-Color: blue X-Metadata-Client-ID: 12345
На данный момент, следующий транспорт поддерживает теги и метаданные:
Следующий транспорт поддерживает только теги:
Следующий транспорт поддерживает только метаданные:
DraftEmail - это специальный экземпляр Email. Его цель состоит в создании письма (с телом, вложениями и т.д.) и предоставлении возможности его скачивания как .eml с заголовком X-Unsent. Многие email-клиенты могут открывать эти файлы и интерпретировать их как черновики писем. Вы можете использовать их, чтобы создавать продвинутые ссылки mailto:.
Вот пример, как сделать письмо доступным для скачивания:
// src/Controller/DownloadEmailController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\Mime\DraftEmail; use Symfony\Component\Routing\Attribute\Route; class DownloadEmailController extends AbstractController { #[Route('/download-email')] public function __invoke(): Response { $message = (new DraftEmail()) ->html($this->renderView(/* ... */)) ->addPart(/* ... */) ; $response = new Response($message->toString()); $contentDisposition = $response->headers->makeDisposition( ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'download.eml' ); $response->headers->set('Content-Type', 'message/rfc822'); $response->headers->set('Content-Disposition', $contentDisposition); return $response; } }
Так как для DraftEmail возможно создание без Кому/От, они не могут быть отправлены с помощью Mailer.
Класс события: MessageEvent
MessageEvent позволяет изменять сообщение Mailer и конверт перед отправкой письма:
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Mailer\Event\MessageEvent; use Symfony\Component\Mime\Email; public function onMessage(MessageEvent $event): void { $message = $event->getMessage(); if (!$message instanceof Email) { return; } // сделать что-то с сообщением (логирование, ...) // и/или добавить некторрые штампы Messenger $event->addStamp(new SomeMessengerStamp()); }
Если вы хотите остановить отправку сообщения, вызовите reject() (это также остановит распространение события):
use Symfony\Component\Mailer\Event\MessageEvent; public function onMessage(MessageEvent $event): void { $event->reject(); }
Выполните эту команду, чтобы узнать, какие слушатели зарегистрированы для этого события, и их приоритеты:
php bin/console debug:event-dispatcher "Symfony\Component\Mailer\Event\MessageEvent"
Класс события: SentMessageEvent
SentMessageEvent позволяет вам действовать в классе SentMessage, чтобы получить доступ к изначальному сообщению (getOriginalMessage()) и некоторой информамции отладки (getDebug()), вроде вызовов HTTP, сделанных транспортом HTTP, что полезно для отладки ошибок:
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Mailer\Event\SentMessageEvent; use Symfony\Component\Mailer\SentMessage; public function onMessage(SentMessageEvent $event): void { $message = $event->getMessage(); if (!$message instanceof SentMessage) { return; } // сделать что-то с сообщением }
php bin/console debug:event-dispatcher "Symfony\Component\Mailer\Event\SentMessageEvent"
Класс события: FailedMessageEvent
FailedMessageEvent разрешяет действовать в исходном сообщении в случае неудачи и предоставляет некоторую информацию отладки (getDebug()), вроде HTTP-запросов, сделанных HTTP-транспортами, что полезно для отладки ошибок:
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Mailer\Event\FailedMessageEvent; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; public function onMessage(FailedMessageEvent $event): void { // например, вы можете получить больше информации об этой ошибке при отправке письма $event->getError(); // сделать что-то с сообщением }
php bin/console debug:event-dispatcher "Symfony\Component\Mailer\Event\FailedMessageEvent"
При локальной разработке рекомендуется использовать ловца email'ов. Если у вас включена поддержка Docker через рецепты Symfony, ловец электронных писем конфигурируется автоматически. В дополнение, если вы используете локальный веб-сервер Symfony, DSN mailer'а автоматически отображается через бинарную интеграцию symfony с Docker .
Symfony предоставляет команду для отправки писем, которая полезна во время разработки, чтобы тестировать, правильно ли работает отправка писем:
php bin/console mailer:test someone@example.com
Эта команда пропускает автобус Messenger, если он сконфигурирован, чтобы облегчить тестирование писем. даже если потребитель Messenger не работает.
Во время разработки (или тестирования), вы можете захотеть отключить доставку сообщений полностью. Вы можете сделать это, используя null://null как DSN почтовой программы, либо в ваших файлах конфигурации .env , либо в файле конфигурации почтовой программы (например, в окружениях dev или test):
# config/packages/mailer.yaml when@dev: framework: mailer: dsn: 'null://null'
Если вы используете Messenger и машрутизируете его к транспорту, сообщение все равно будет отправлено этому транспорту.
Вместо полного отключения доставки, вы можете захотеть всегда отправлять письма по одному конкретному адресу, вместо настоящего адреса:
# config/packages/mailer.yaml when@dev: framework: mailer: envelope: recipients: ['youremail@example.com']
Используйте опцию allowed_recipients, чтобы указать исключения из поведения, определенногов опции recipients; это позволит письмам, направленным этим конкретным получателям сохранить их первоначальное назначение:
# config/packages/mailer.yaml when@dev: framework: mailer: envelope: recipients: ['youremail@example.com'] allowed_recipients: - 'internal@example.com' # вы также можете использовать регулярные выражения для определения разрешенных получателей - 'internal-.*@example.(com|fr)'
При такой конфигурации все письма будут отправляться на youremail@example.com,за исключением тех, которые отправляются на internal@example.com, internal-monitoring@example.fr, и т.д., которые будут получать письма как обычно.
7.1
Опция allowed_recipients была представлена в Symfony 7.1.
Symfony предоставляет множество встроенных утверждений почтового сервиса для функциональной проверки того, что письмо было отправлено, его содержания, заголовков и т.д. Они доступны в тестовых классах, расширяющих KernelTestCase или при использовании MailerAssertionsTrait:
// tests/Controller/MailControllerTest.php namespace App\Tests\Controller; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class MailControllerTest extends WebTestCase { public function testMailIsSentAndContentIsOk(): void { $client = static::createClient(); $client->request('GET', '/mail/send'); $this->assertResponseIsSuccessful(); $this->assertEmailCount(1); // use assertQueuedEmailCount() when using Messenger $email = $this->getMailerMessage(); $this->assertEmailHtmlBodyContains($email, 'Welcome'); $this->assertEmailTextBodyContains($email, 'Welcome'); } }
Если ваш контроллер возвращает ответ с перенаправлением после отправки письма, убедитесь, что ваш клиент не следует за перенаправлениями. Ядро перезагружается после после перенаправления, и сообщение будет потеряно из обработчика события mailer.
Чтобы написать комментарий нужно, зарегистрироваться
Голосования и тесты: 0