Nette Mail
Собираетесь отправлять электронную почту, например рассылки или подтверждения заказов? Nette Framework даёт нужные инструменты с очень дружелюбным API. Мы покажем:
- как создать письмо, в том числе с вложениями
- как его отправить
- как соединить письма и шаблоны
Установка
Скачайте и установите библиотеку с помощью Composer:
composer require nette/mail
Создание писем
Письмо – объект Nette\Mail\Message. Создадим его так:
$mail = new Nette\Mail\Message;
$mail->setFrom('John <john@example.com>')
->addTo('peter@example.com')
->addTo('jack@example.com')
->setSubject('Order Confirmation')
->setBody("Hello,\nYour order has been accepted.");
Все указанные параметры должны быть в кодировке UTF-8.
Адреса с интернационализированным доменом, например
jan@příklad.cz, автоматически преобразуются в ASCII-форму, известную
как punycode, которую требуют почтовые серверы; для этого нужно расширение
intl.
Кроме указания получателей через addTo(), можно указать
получателей копии через addCc() или получателей скрытой копии
через addBcc(). Все эти методы, включая setFrom(), принимают
адресата тремя способами:
$mail->setFrom('john.doe@example.com');
$mail->setFrom('john.doe@example.com', 'John Doe');
$mail->setFrom('John Doe <john.doe@example.com>');
Тело письма, написанное на HTML, передаётся методом setHtmlBody():
$mail->setHtmlBody('<p>Hello,</p><p>Your order has been accepted.</p>');
Создавать текстовую альтернативу не нужно, Nette породит её за вас
автоматически. А если у письма не задана тема, она попытается взять её
из элемента <title>.
Изображения тоже можно исключительно легко встроить в HTML-тело. Достаточно передать вторым параметром путь, где изображения физически находятся, и Nette автоматически вложит их в письмо:
// автоматически добавит в письмо /path/to/images/background.gif
$mail->setHtmlBody(
'<b>Hello</b> <img src="background.gif">',
'/path/to/images',
);
Алгоритм встраивания изображений ищет такие образцы:
<img src=...>, <body background=...>, url(...) внутри
HTML-атрибута style и особый синтаксис [[...]].
Может ли отправка писем быть ещё проще?
Письма как открытки. Никогда не отправляйте по почте пароли или другие учётные данные.
Прочие параметры
Объект Message позволяет задать и адрес для ответа, обратный путь
для недоставленных сообщений и приоритет сообщения:
$mail->addReplyTo('reply@example.com', 'Support')
->setReturnPath('bounces@example.com')
->setPriority(Nette\Mail\Message::High);
Приоритет – одна из констант Message::High, Message::Normal или
Message::Low.
Отписка в один клик
Gmail и Yahoo требуют, чтобы массовая почта вроде рассылок предлагала
отписку в один щелчок прямо в почтовом клиенте. Этим занимается пара
заголовков, определённых RFC 8058, которую метод setUnsubscribe()
правильно за вас настроит:
$mail->setUnsubscribe('https://example.com/unsubscribe?token=xyz');
URL должен отписывать получателя в ответ на голый HTTP-запрос POST, без
всякого дальнейшего подтверждения. Вторым параметром можно указать
адрес электронной почты как запасной вариант для клиентов, которые не
умеют отправлять POST; он работает и сам по себе:
$mail->setUnsubscribe(email: 'unsubscribe@example.com').
Вложения
К письмам, разумеется, можно приложить файлы. Для этого служит метод
addAttachment(string $file, ?string $content = null, ?string $contentType = null).
// прикладывает к письму файл /path/to/example.zip под именем example.zip
$mail->addAttachment('/path/to/example.zip');
// прикладывает файл /path/to/example.zip под именем info.zip
$mail->addAttachment('info.zip', file_get_contents('/path/to/example.zip'));
// прикладывает файл example.txt с содержимым "Hello John!"
$mail->addAttachment('example.txt', 'Hello John!');
Файл можно встроить прямо в HTML-тело методом addEmbeddedFile(). Он
возвращает созданную MIME-часть, на Content-ID которой вы ссылаетесь в
HTML (именно этот механизм автоматическое встраивание изображений
использует внутри):
$file = $mail->addEmbeddedFile('/path/to/logo.png');
$mail->setHtmlBody('<img src="cid:' . trim($file->getHeader('Content-ID'), '<>') . '">');
Шаблоны
Если вы отправляете HTML-письма, писать их в шаблонизаторе Latte – отличный вариант. Как это сделать?
$latte = new Latte\Engine;
$params = [
'orderId' => 123,
];
$mail = new Nette\Mail\Message;
$mail->setFrom('John <john@example.com>')
->addTo('jack@example.com')
->setHtmlBody(
$latte->renderToString('/path/to/email.latte', $params),
'/path/to/images',
);
Файл email.latte:
<html>
<head>
<meta charset="utf-8">
<title>Order Confirmation</title>
<style>
body {
background: url("background.png")
}
</style>
</head>
<body>
<p>Hello,</p>
<p>Your order number {$orderId} has been accepted.</p>
</body>
</html>
Nette автоматически встроит все изображения, задаст тему по элементу
<title> и породит текстовую альтернативу к HTML.
Использование в Nette Application
Если вы используете письма вместе с Nette Application, то есть с презентерами,
вам может понадобиться создавать в шаблонах ссылки атрибутом
n:href или тегом {link}. Latte по умолчанию их не знает, но
добавить их очень легко. Ссылки умеет создавать объект
Nette\Application\LinkGenerator, и получить его можно, передав его через внедрение зависимостей:
use Nette;
class MailSender
{
public function __construct(
private Nette\Application\LinkGenerator $linkGenerator,
private Nette\Bridges\ApplicationLatte\TemplateFactory $templateFactory,
) {
}
private function createTemplate(): Nette\Application\UI\Template
{
$template = $this->templateFactory->createTemplate();
$template->getLatte()->addProvider('uiControl', $this->linkGenerator);
return $template;
}
public function createEmail(): Nette\Mail\Message
{
$template = $this->createTemplate();
$html = $template->renderToString('/path/to/email.latte', $params);
$mail = new Nette\Mail\Message;
$mail->setHtmlBody($html);
// ...
return $mail;
}
}
В шаблоне вы затем создаёте ссылки как привыкли. Все ссылки, созданные через LinkGenerator, будут абсолютными.
<a n:href="Presenter:action">Link</a>
Встраивание CSS
Nette\Mail\CssInliner преобразует
правила CSS в inline-атрибуты style, чтобы письма одинаково
отображались во всех клиентах. Он также порождает HTML-атрибуты ради
совместимости с Outlook.
Требует PHP 8.4 или новее и расширения dom.
У большинства почтовых клиентов поддержка тегов <style>
ограничена или они их вовсе игнорируют. Чтобы отображение было
правильным, правила CSS нужно перенести в inline-атрибуты style
отдельных элементов. Просто прогоните свой HTML через inline():
$inliner = new Nette\Mail\CssInliner;
$html = $inliner->inline($html);
Например, если HTML содержит:
<style>
p { margin: 0; color: #333; }
a { color: #a0704e; }
</style>
<p>Hello <a href="#">world</a></p>
Результатом встраивания будет (тег <style> сохраняется, но
здесь для краткости опущен):
<p style="margin: 0; color: #333">Hello <a href="#" style="color: #a0704e">world</a></p>
Тег <style> в выводе сохраняется всегда, так что запросы
@media и другие правила, которые встроить нельзя, продолжают
работать.
Кроме извлечения стилей из тегов <style>, CSS можно
предоставить и методом addCss(). Встраивать CSS нужно до передачи HTML
в setHtmlBody():
$latte = new Latte\Engine;
$params = [
'orderId' => 123,
];
$html = $latte->renderToString('/path/to/email.latte', $params);
$html = (new Nette\Mail\CssInliner)
->addCss(file_get_contents('/path/to/email.css'))
->inline($html);
$mail = new Nette\Mail\Message;
$mail->setHtmlBody($html);
Когда одно и то же свойство элемента задают несколько
правил, победителя определяет каскад CSS, точно как в браузере:
объявления с !important побеждают обычные, существующий inline-атрибут
style побеждает любой селектор, более специфичный селектор
побеждает менее специфичный, а при равенстве побеждает более позднее
правило. Правила из тегов <style> обрабатываются раньше
добавленных через addCss(), и записывается только победившее
значение.
At-правила вроде @media или @font-face при встраивании
пропускаются. Учтите, что псевдоклассы вроде :hover осмысленно
встроить нельзя, потому что inline-стили не поддерживают динамических
состояний.
HTML-атрибуты для Outlook
Настольные версии Microsoft Outlook используют движок отображения Word,
который многих свойств CSS не понимает. Ради совместимости CssInliner
автоматически порождает из правил CSS соответствующие HTML-атрибуты
вместе с inline-стилями:
| Свойство CSS | HTML-атрибут | Применяется к |
|---|---|---|
background-color |
bgcolor |
<table>, <td>, <th>,
<body>, <tr> |
width |
width |
<table>, <td>, <th>, <img> |
height |
height |
<table>, <td>, <th>, <img> |
border-spacing |
cellspacing |
<table> |
У width, height и cellspacing единица px
автоматически убирается (например, width: 600px становится
width="600"), проценты сохраняют свой %, а значения, которые
атрибут выразить не может, например auto или calc(), не
порождают атрибута вовсе. Inline-стиль и HTML-атрибут задаются вместе, так
что письмо правильно отображается и в современных клиентах, и в Outlook.
HTML-атрибуты порождаются только из правил CSS, обработанных
CssInliner, а не из атрибутов style, уже присутствующих в
исходном HTML.
Отправка писем
Mailer – класс, отвечающий за отправку писем. Он реализует интерфейс Nette\Mail\Mailer, и доступно несколько готовых mailer'ов, которые мы представим.
Фреймворк автоматически добавляет в DI-контейнер сервис
Nette\Mail\Mailer согласно конфигурации, и получить
его можно, передав его через внедрение зависимостей.
SendmailMailer
Mailer по умолчанию – SendmailMailer, который использует PHP-функцию mail. Пример использования:
$mailer = new Nette\Mail\SendmailMailer;
$mailer->send($mail);
Если вы хотите задать returnPath, а ваш сервер всё равно его
перезаписывает, используйте $mailer->commandArgs = '-fmy@email.com'.
По умолчанию SendmailMailer передаёт адрес отправителя функции
mail() как отправителя конверта (аргумент -f). Отключить это
можно через $mailer->setEnvelopeSender(false).
SmtpMailer
Чтобы отправлять почту через SMTP-сервер, используйте SmtpMailer.
$mailer = new Nette\Mail\SmtpMailer(
host: 'smtp.gmail.com',
username: 'john@gmail.com',
password: '*****', // ваш пароль
encryption: 'ssl', // либо 'tls'
);
$mailer->send($mail);
Конструктору можно передать следующие дополнительные параметры:
port– если не задан, используется значение по умолчанию: 465 дляssl, 587 дляtls, иначе 25timeout– тайм-аут SMTP-соединенияpersistent– использовать постоянное соединениеclientHost– задаёт заголовок host клиентаstreamOptions– позволяет задать соединению параметры контекста SSL
Аутентификация OAuth 2.0
Gmail и Microsoft 365 отказываются от аутентификации по паролю для SMTP и
требуют вместо неё токен доступа OAuth 2.0 (механизм XOAUTH2). Передайте токен
методом setAccessToken(); имя пользователя остаётся, а пароль
оставляется пустым:
$mailer = new Nette\Mail\SmtpMailer(
host: 'smtp.gmail.com',
username: 'john@gmail.com',
password: '',
encryption: 'tls',
);
$mailer->setAccessToken($accessToken);
Поскольку срок действия токенов доступа истекает, вместо токена можно передать callback; он вызывается при каждом соединении, так что всегда может выдать свежий токен. Получение и обновление токена остаётся на вас или на вашей OAuth-библиотеке:
$mailer->setAccessToken(fn() => $oauthProvider->getFreshToken());
FallbackMailer
Этот mailer писем напрямую не отправляет, а посредничает при отправке через набор mailer'ов. Если один mailer даёт сбой, он пробует следующий. Если сбой даёт последний, он начинает снова с первого.
$mailer = new Nette\Mail\FallbackMailer([
$smtpMailer,
$backupSmtpMailer,
$sendmailMailer,
]);
$mailer->send($mail);
Прочие параметры конструктора – количество попыток (по умолчанию
3) и время ожидания между ними в миллисекундах (по умолчанию
1000). Если все mailer'ы дают сбой при каждой попытке, выбрасывается
Nette\Mail\FallbackMailerException, в свойстве $failures которого находятся
собранные исключения.
Mailer, сбой которого постоянен, например когда SMTP-сервер отвергает учётные данные, из дальнейших попыток исключается: повторение результата не изменит.
Позже можно добавить ещё один mailer через addMailer() и
зарегистрировать событие $onFailure, которое вызывается после
каждой неудачной попытки:
$mailer->onFailure[] = function ($mailer, $exception, $failedMailer, $mail) {
// например, записать неудачную попытку в лог
};
FileMailer
Этот mailer ничего не отправляет: он записывает каждое сообщение как
файл .eml в заданный каталог. Файлы открываются в любом почтовом
клиенте, так что вы можете точно проверить, что было бы отправлено, –
удобно в тестах и при разработке.
$mailer = new Nette\Mail\FileMailer('/path/to/mails');
$mailer->send($mail);
Отладка писем
При разработке или на промежуточном сервере вы не хотите, чтобы тестовое письмо утекло к реальному клиенту. Есть два способа гарантировать, что этого никогда не произойдёт.
Рекомендуемая локальная схема – запустить у себя на машине лёгкий
SMTP-перехватчик вроде Mailpit или MailHog. Они принимают каждое сообщение,
показывают его в веб-интерфейсе и никогда никуда не пересылают – вы
просто направляете Nette Mail на 127.0.0.1:1025:
mail:
smtp: true
host: 127.0.0.1
port: 1025
Для промежуточных серверов или окружений, где локальный перехватчик
не запустить, в Nette Mail есть встроенное перенаправление. Задайте цель в
конфигурации, и каждый получатель To, Cc и Bcc будет
заменён на неё. Nette Mail сохраняет исходные в заголовках X-Original-*,
так что вы видите, кому письмо предназначалось, и может дописать в
начало темы метку:
mail:
redirect:
to: dev@example.com
subjectPrefix: '[debug]' # необязательно
Сокращённая форма redirect: dev@example.com подходит, когда приставка к
теме вам не нужна. В режиме отладки автоматически подключается панель
Tracy Bar со списком всех отправленных писем.
Внутри этим занимается Nette\Mail\Interceptor, который
предоставляет и событие $onSent для собственных слушателей
(журналы аудита, метрики, …).
DKIM
DKIM (DomainKeys Identified Mail) – технология повышения доверия к письмам, которая помогает и обнаруживать подделанные сообщения. Отправляемое сообщение подписывается закрытым ключом домена отправителя, и эта подпись сохраняется в заголовке письма. Сервер получателя сравнивает эту подпись с открытым ключом, хранящимся в DNS-записях домена. Если подпись совпадает, это доказывает, что письмо действительно происходит из домена отправителя и что при передаче сообщение не изменялось.
Настроить mailer на подписывание писем можно прямо в конфигурации. Если вы не используете внедрение зависимостей, применяется это так:
$signer = new Nette\Mail\DkimSigner(
domain: 'yourdomain.com',
selector: 'dkim', // селектор из DNS-записи
privateKey: file_get_contents('/path/to/dkim.key'), // путь к вашему закрытому ключу
passPhrase: 'your_passphrase', // парольная фраза к закрытому ключу, если есть
);
$mailer = new Nette\Mail\SendmailMailer; // либо SmtpMailer
$mailer->setSigner($signer);
$mailer->send($mail);
Закрытым ключом может быть либо RSA-ключ в формате PEM, либо
ключ Ed25519 (RFC 8463) в виде сырых байтов в
base64; тип определяется по самому ключу. Подписывание Ed25519 требует
расширения sodium.
В параметре oversignHeaders можно перечислить заголовки,
которые нужно защитить от дописывания второй копии к уже подписанному
сообщению – этот трюк используют подделанные письма; обычный
кандидат – From.
Конфигурация
Обзор параметров конфигурации Nette Mail. Если вы используете не весь фреймворк, а только эту библиотеку, прочитайте, как загрузить конфигурацию.
По умолчанию для отправки писем используется Nette\Mail\SendmailMailer,
который не требует дальнейшей настройки. Однако мы можем
переключиться на Nette\Mail\SmtpMailer:
mail:
# использовать SmtpMailer
smtp: true # (bool) по умолчанию false
host: ... # (string) имя хоста SMTP-сервера
port: ... # (int) порт SMTP-сервера
username: ... # (string) имя пользователя для SMTP-аутентификации
password: ... # (string) пароль для SMTP-аутентификации
timeout: ... # (int) тайм-аут SMTP-соединения
encryption: ... # (ssl|tls|null) по умолчанию null (синоним 'secure')
clientHost: ... # (string) имя хоста клиента, по умолчанию $_SERVER['HTTP_HOST'] либо 'localhost'
persistent: ... # (bool) использовать постоянное соединение, по умолчанию false
# параметры контекста потока для SMTP-соединения, по умолчанию stream_context_get_default()
context:
ssl: # все параметры на https://www.php.net/manual/en/context.ssl.php
allow_self_signed: ...
...
http: # список параметров на https://www.php.net/manual/en/context.http.php
header: ...
...
Отключить проверку SSL-сертификата можно параметром
context › ssl › verify_peer: false. Мы настоятельно не рекомендуем этого
делать, потому что это делает приложение уязвимым. Вместо этого добавьте сертификаты в хранилище
доверия.
Чтобы повысить доверие, мы можем подписывать письма по технологии DKIM:
mail:
dkim:
domain: myweb.com # ваш домен
selector: lovenette # селектор DKIM
privateKey: %appDir%/cert/dkim.key # путь к файлу вашего закрытого ключа
passPhrase: ... # парольная фраза к закрытому ключу, если нужна
Параметры перенаправления всех писем и включения панели отладки описаны в разделе Отладка писем:
mail:
# перенаправляет все письма на один адрес
redirect: dev@example.com
# включает (true) или выключает (false) панель Tracy и перехват писем
debugger: ... # (bool) по умолчанию null, то есть auto в режиме отладки
Сервисы DI
Эти сервисы добавляются в DI-контейнер:
| Имя | Тип | Описание |
|---|---|---|
mail.mailer |
Nette\Mail\Mailer | класс отправки писем |
mail.signer |
Nette\Mail\Signer | подписывание DKIM |
Если вы переходите на более новую версию, посмотрите страницу обновления.