Работа с почтой в Drupal 8: отправка plain text и html писем

В этой статье мы разберем как отправлять plain text и html сообщения из Drupal 8. Для успешной работы с почтой в Drupal нам понядобятся дополнительные модули, которые были рассмотрены в первой части: Работа с почтой в Drupal 8: обзор must have модулей. Также договоримся, что для упрощения процесса отладки, а также по причинам описанным в предыдущей части, мы будем отправлять почту непосредственно через SMTP (никаких Sendmail и PHP Mail).

Отправляем plain text письмо

Отправить обычное текстовое письмо (без форматирования) в Drupal очень просто. Вначале установите модуль SMTP Authentication Support, который идеально подходит для отправки обычных текстовых сообщений, и выполните необходимые настройки в зависимости от того какой SMTP сервер вы используете.

Далее обьявляем hook_mail(). Проще всего сделать это в файле my_module.module. hook_mail() будет выглядеть примерно так:

use Drupal\Core\Render\Markup;

/**
 * Implements hook_mail().
 */
function my_module_mail(string $key, array &$message, array $params) {
  $options = [
    'langcode' => $message['langcode'],
  ];
  switch ($key) {
    case 'important_event':
      $message['subject'] = t('Number of updated nodes: @num', ['@num' => $params['updated_nodes']], $options);
      $message['body'][] = Markup::create($params['message']);
      //$message['body'][] = $params['message'];
      break;
  }
}

Все что нужно сделать в этом хуке — заполнить значениями $message[‘subject’] и $message[‘body’] (обратите внимание что $message[‘body’] это массив, содержащий кусочки письма, а не просто одно большое боди). Не нужно задавать здесь $message[‘from’], $message[‘to’] и другие параметры письма — они все к этому моменту инициализированы значениями взятыми из конфигурации сайта — смотрите файл web/core/lib/Drupal/Core/Mail/MailManager.php. Но если вы хотите «перебить» значения по умолчанию — здесь подходящее место для этого.

Пока мы отправляем обычные текстовые письма, в массив $message[‘body’] можно добавлять обычные строки (см. закомментированную строчку — так тоже допустимо). Но для большей гибкости и надежности — заверните $params[‘message’] в Markup::create(). Этот метод сам решит что правильно вернуть в ответ на переданную строку — строку или объект \Drupal\Component\Render\MarkupInterface.

Несколько слов о ключе $key. В $key у нас приходит строка-идентификатор типа сообщения. Так как hook_mail() будет вызываться при отправке сообщения любого типа, нужно как-то понимать, какое именно сообщение сейчас обрабатывается — уведомление админу о новых пользователях, уведомление пользователю о том что интересующий его материал обновлен, и т.д. Поэтому hook_mail() как правило представляет собой switch-case конструкцию, каждый «case» которой содержит логику для компоновки сообщений своего типа.

Переменная $message передается по ссылке, благодаря чему мы можем как угодно дополнить поля сообщения, и возможно даже прицепить пока не существующие заголовки (мы будем активно это использовать при отправке html сообщений). Обратите внимание как компонуется тело сообщения — вместо того чтобы перетирать старое значение новым при вызове hook_mail(), — мы каждый раз добавляем в массив $message[‘body’] новый элемент с контентом.

Теперь напишем код, который будет вызывать hook_mail(), передавать в него недостающие данные (предпочитаемую локализацию, тело сообщения, идентификатор типа письма), и наконец, отправлять письмо. Этот код может находиться где угодно — в кроне, в плагине-обработчике элемента очереди, в сабмите формы…

$mailManager = \Drupal::service('plugin.manager.mail');
$module = 'my_module';
$key = 'important_event';
$to = \Drupal::currentUser()->getEmail();
$params['message'] = 'Hello! This is a test message from my site!';
$params['updated_nodes'] = 10;
$langcode = \Drupal::currentUser()->getPreferredLangcode();
$send = true;

//Try to send letter:
$result = $mailManager->mail($module, $key, $to, $langcode, $params, NULL, $send);

if ($result['result'] !== true) {
  //Email has not been sent.
}
else {
  //Email sent successfully.
}

Здесь мы инстанциируем объект сервиса plugin.manager.mail, хотя в реальности это правильнее делать через инъекцию зависимости.
Задаем $key — это должен быть один из идентификаторов, для которого есть соответствующий кейс в hook_mail().
В $params[‘message’] при отправке plain text сообщения допустимо запихнуть строку, что мы и делаем в данном случае. Обратите на это внимание, так как при отправке html сообщения, в $params[‘message’] нужно передавать объект типа \Drupal\Component\Render\MarkupInterface, если передать HTML содержимое в виде строки, то оно так и придет получателю — в виде HTML разметки!
$params[‘updated_nodes’] = 10; — просто пример того как передать произвольные переменные в hook_mail().
$send = true; — указывает, должно ли сообщение быть в итоге отправлено. По умолчанию всегда true.

Далее вызывается метод mail, который выполнит hook_mail(), и после завершения компоновки, письмо будет отправлено. После отправки мы проверяем $result[‘result’], однако стоит отметить, что к положительному результату не стоит относиться доверительно. Иногда $result[‘result’] содержит true, хотя письмо в действительности даже не выходило за пределы текущего окружения. Поэтому в процессе отладки нужно быть уверенным, что введены правильные настройки SMTP и проверять реальное наличие письма во входящих.

Отправляем HTML письмо

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

Итак, модули которые нам понадобятся (я не люблю ставить лишние модули, поэтому рекомендую только проверенный необходимый минимум):

Установите Mail System и Swift Mailer, сконфигурируйте Mail System так, чтобы за форматирование и отправку писем отвечал модуль Swift Mailer, и не забудьте внести настройки своего SMTP сервера в настройках Swift Mailer. Если вы использовали SMTP Authentication Support для отправки plain text сообщений, отключите его. Это не обязательно, но лучше сокращать количество переменных. Отключить модуль можно в его же настройках, удалять не обязательно.

Чтобы начать отправлять HTML письма необходимо реализовать следующий функционал (4 пункта вместо 2 как в предыдущем случае):

  • Добавить функцию, реализующую hook_theme(), которая будет регистрировать в системе тему (на основе которой будет генерироваться письмо), и содержать информацию о том, какие переменные будут передаваться в шаблон;
  • Добавить twig шаблон;
  • Добавить hook_mail();
  • Добавить код, вызывающий hook_mail();

Реализация hook_theme()

Хук hook_theme() обьявляется в файле my_module.module, и соответственно будет называться my_module_theme(). Этот хук служит для регистрации в системе тем, которые предлагает модуль или тема. Более подробно об этом хуке можно почитать в официальном руководстве, а также, если хотите хорошо разобраться с темами в Drupal 8, есть подробная статья в блоге у Niklan. Реализация hook_theme() будет выглядеть примерно так:

function my_module_theme($existing, $type, $theme, $path) {
  return [
    'email_about_updated_nodes' => [
      'variables' => [
        'user_name' => NULL,
        'updated_nodes' => NULL,
      ],
      //'template' => 'custom-temlate-name',
    ],
  ];
}

Если мы явно не указываем название шаблона с помощью элемента ‘template’ — обратите внимание на закомментированную строку, то будет подразумеваться, что имя шаблона почти такое же как название хука темы, только если нижние подчеркивания заменить на дефисы (email-about-updated-nodes). К получившемуся имени система добавит расширение html.twig и будет искать соответствующий файл в папке templates, которая находится (должна находиться) в папке модуля. Если вы используете ключ ‘template’ чтобы добавить какое-то особенное имя шаблона, правила те же — слова должны быть разделены дефисами, расширение указывать не нужно (система добавит его автоматически). Пример валидного имени шаблона — ‘custom-temlate-name’, как показано в закомментированной строке в примере (twig-файл соотвественно будет называться custom-temlate-name.html.twig).

user_name и updated_nodes это имена переменных, которые будут доступны в шаблоне.

Twig шаблон

Сделаем простенький шаблон. Как мы уже понимаем, наш шаблон должен называться email-about-updated-nodes.html.twig и находиться в папке templates нашего модуля. Еще раз обращаю внимание — в названии файла шаблона слова должны разделяться дефисами, в то время как в хуке темы — символами подчеркивания. Код twig шаблона будет выглядеть примерно так:

Hello, {{ user_name }}!
We just checked, and inform you, that from your last visit {{ updated_nodes }} has been updated!

Примеры максимально примитивные, чтобы за сложностью не проглядеть суть 🙂 Как видите, в шаблоне доступны переменные user_name и updated_nodes которые мы выводим. Если вы еще не знакомы с twig, у него отличная документация, которую можно скачать в PDF в виде справочника.

Реализация hook_mail()

Реализуем hook_mail(). Самое важное отличие от hook_mail() для простого текстового письма — нужно добавить заголовок, указывающий что MIME тип нашего письма — text/html. В остальном все то же самое:

use Drupal\Core\Render\Markup;

/**
 * Implements hook_mail().
 */
function my_module_mail(string $key, array &$message, array $params) {
  $options = [
    'langcode' => $message['langcode'],
  ];
  switch ($key) {
    case 'important_event':
      $message['headers']['Content-Type'] = 'text/html; charset=UTF-8; format=flowed; delsp=yes';
      $message['subject'] = t('Number of updated nodes: @num', array('@num' => $params['updated_nodes']), $options);
      $message['body'][] = Markup::create($params['message']);
      //$message['body'][] = $params['message'];

      break;
  }
}

Иногда можно встретить примеры кода, где для заголовков также устанавливаются:

$message['headers']['MIME-Version'] = '1.0';
$message['headers']['Content-Transfer-Encoding'] = '8Bit';
$message['headers']['X-Mailer'] = 'Drupal';
$message['headers']['reply-to'] = \Drupal::config('system.site')->get('mail');
$message['headers']['from'] = 'sender name <'. \Drupal::config('system.site')->get('mail') .'>';

В этом нет никакой необходимости, так как все это Drupal делает перед тем как вызвать ваш hook_mail(), и перебивать эти значения своими имеет смысл только если нужно изменить значения по умолчанию.

$params[‘message’] завернуто в Markup::create() исключительно для подстраховки. Если вы уверены что в $params[‘message’] будет находиться содержимое правильного типа (см. ниже), в принципе, можно этого и не делать.

Самое главное — указать в заголовках, что Content-Type нашего сообщения ‘text/html’. Иначе по умолчанию система проставит ‘text/plain’.

Вызывающий код

В данном случае под вызывающим кодом будем подразумевать код, который инициирует отправку письма, а не код, который вызывающе выглядит 🙂 Этот код мы можем разместить там, откуда нам нужно отправить письмо — из функции, выполнившейся по крону, из обработчика очереди, или из сабмита формы. Код будет выглядеть примерно так:

$renderable = [
  '#theme' => 'email_about_updated_nodes',
  '#user_name' => $userName,
  '#updated_nodes' => $updatedNodes,
];

/**
 * @var \Drupal\Component\Render\MarkupInterface $markup
 */
$markup = \Drupal::service('renderer')->render($renderable);

$userAccount = \Drupal\user\Entity\User::load($userID);
$to = $userAccount->getEmail();
$userName = $userAccount->getAccountName();
$langcode = $userAccount->getPreferredLangcode();

$module = 'my_module';
$key = 'important_event';

$params['message'] = $markup;
$params['updated_nodes'] = $updatedNodes;

$send = true;

/**
 * @var \Drupal\Core\Mail\MailManager $mailManager
 */
$mailManager = \Drupal::service('plugin.manager.mail');
$result = $mailManager->mail($module, $key, $to, $langcode, $params, NULL, $send);

if ($result['result'] !== true) {
  //Email has not been sent.
}
else {
  //Email sent successfully.
}

Пройдемся по коду. Вначале мы составляем комплект для рендеринга — название темы и значения переменных, которые будут в нее переданы, и сохраняем такой комплект в переменной $renderable. Затем, с помощью сервиса ‘renderer’ рендерим (конечно, этот сервис в реальном примере нужно подключить через инъекцию зависимости). В ответ получаем не просто строку, а объект типа \Drupal\Component\Render\MarkupInterface. Не нужно пытаться преобразовать его в строку — если вы сделаете так, то в теле письма вам придет HTML разметка (то есть вы будете видеть теги). Поэтому оставляем переменную $markup как есть (типа \Drupal\Component\Render\MarkupInterface), а в hook_mail(), по желанию, можно оставить «подстраховочный код», как было показано выше — $message[‘body’][] = Markup::create($params[‘message’]). Подстраховочный код, в случае если на вход поступит обычная строка, преобразует ее в \Drupal\Component\Render\MarkupInterface и письмо отправится в корректном формате (теги будут выполнять свою функцию, а не явно показываться пользователю).

Далее мы получаем информацию о получателе по его $userID, тут особо комментировать не нужно, в вашем случае здесь может быть другая логика.

Заполняем массив $params переменными, которые хотим передать в hook_mail(), в нашем случае это количество обновленных нод (используем при формировании темы письма) и собственно тело письма.

Получаем объект сервиса plugin.manager.mail и отправляем письмо (в реальности почтовый сервис должен быть внедрен через инъекцию зависимости, а не так как в нашем примере).

Напомним, что если $result[‘result’] === true то это не гарантирует на 100% что письмо успешно отправилось, поэтому в процессе отладки также проверяем «Входящие» (и «Спам» также не помешает проверить).

PS. Отправка уведомлений по почте на сегодняшний день не самый эффективный способ коммуникации. Письма иногда будут попадать в спам, как бы вы ни настраивали SPF и DKIM. Gmail подозрительно относится к любому новому отправителю/серверу. Однако email до сих пор используется, и уметь пользоваться этим каналом коммуникации нужно. Конечно, для того чтобы отправляемые письма исправно доходили до получателей пробиваясь сквозь спам фильтры, кроме реализации технической части не лишним будет проконсультироваться с email-маркетологом.

Оставьте первый комментарий

Оставить комментарий