Skip to content

📧 Mail — Отправка писем в PageBlocks ​

Класс Mail предназначен для формирования и отправки писем с использованием стандартного сервиса modMail в MODX. В PageBlocks он упрощает работу с почтой и поддерживает Fenom-шаблоны для генерации содержимого.

📌 Назначение ​

Mail оборачивает работу с modMail и позволяет настраивать все параметры письма через цепочку методов. Тело письма можно задавать напрямую или генерировать через Fenom-шаблон.

⚙️ Дефолтные значения ​

При создании объекта Mail автоматически подтягиваются некоторые параметры из системных настроек MODX:

СвойствоЗначение по умолчанию
fromemailsender из системных настроек MODX
fromNamesite_name из системных настроек MODX
senderemailsender из системных настроек MODX
isHtmltrue — письма отправляются в формате HTML

Пример:

php
$mail = Mail::to('user@example.com');
// $mail->from будет равен $modx->getOption('emailsender')

✅ Быстрый пример ​

php
use Boshnik\PageBlocks\Support\Mail;

Mail::to('recipient@example.com')
    ->subject('Добро пожаловать')
    ->message('<p>Спасибо за регистрацию!</p>')
    ->send();

🗂️ Методы класса ​

➡️ Mail::to(string $email, string $name = ''): Mail ​

Указывает адрес получателя и, при необходимости, его имя.

➡️ subject(string $text): Mail ​

Устанавливает тему письма.

➡️ message(string $html): Mail ​

Задаёт тело письма напрямую (HTML или текст).

➡️ view(string $tpl, array $data = []): Mail ​

Рендерит письмо через Fenom-шаблон и переданные данные.

Пример:

php
Mail::to('user@example.com')
    ->subject('Ваш заказ')
    ->view('file:emails/order', ['order' => $order])
    ->send();

➡️ attach($file, string $as = ''): Mail ​

Прикрепляет файл к письму. Принимает либо строку с путём, либо массив, где путь лежит в ключе url или path, плюс необязательное name — имя, которое увидит получатель. Со строкой имя для получателя задаёт второй аргумент; без него берётся имя самого файла.

php
->attach(MODX_BASE_PATH . 'assets/docs/contract.pdf');

->attach([
    'url' => MODX_BASE_PATH . 'assets/docs/contract.pdf',
    'name' => 'Договор.pdf'
]);

Файл, который к моменту отправки пропал, письмо не отменяет: оно уйдёт без вложения, а факт попадёт в лог.

➡️ replyTo(string $email): Mail ​

Устанавливает адрес для ответа.

➡️ cc(string $email): Mail ​

Добавляет адресата в копию.

➡️ bcc(string $email): Mail ​

Добавляет скрытую копию.

➡️ from(string $email, string $name = ''): Mail ​

Изменяет отправителя и имя.

➡️ sender(string $email): Mail ​

Изменяет MAIL_SENDER.

➡️ setHTML(bool $html = true): Mail ​

Указывает, в каком формате отправлять письмо (HTML или plain text).

➡️ header(string $name, string $value): Mail ​

Добавляет свой заголовок — например, List-Unsubscribe в рассылке: почтовые службы понижают репутацию пачке писем, которая уходит без него.

➡️ send(): bool ​

Отправляет письмо сейчас и возвращает, ушло ли оно в этот момент. Адрес, тему и тело берёт из цепочки — аргументов у метода нет.

Неудачный send() не теряется: письмо остаётся в очереди, и попытка повторится. См. раздел «Повтор при сбое».

➡️ queue(?DateTimeInterface $at = null): bool ​

Записывает письмо и возвращает управление, не трогая SMTP. См. раздел «Очередь».

➡️ context(string $key): Mail ​

Помечает письмо для журнала. См. раздел «context — метка, а не украшение».

🗂️ Пример: Fenom + вложение + CC/BCC ​

php
Mail::to('user@example.com')
    ->subject('Ваш заказ принят')
    ->view('emails/order', ['order' => $order])
    ->attach([
        'url' => MODX_BASE_PATH . 'assets/invoices/invoice.pdf',
        'name' => 'Счет-фактура.pdf'
    ])
    ->cc('manager@example.com')
    ->bcc('admin@example.com')
    ->send();

📝 Логирование ошибок ​

Если письмо не удалось отправить, в системный лог MODX (core/cache/logs/error.log) будет записана ошибка с уровнем ERROR и префиксом [Mail].

Журнал ​

Каждое письмо попадает в pb_mails до того, как будет отдано SMTP. В записи видно, когда письмо собрали, сколько раз пытались отправить, что ответил сервер и ушло ли оно в итоге.

Это не удобство. Пока журнала не было, от отправки оставался ровно один след — строка в логе MODX, и только при неудаче. Вопрос «клиент говорит, что ключ так и не пришёл» так не закрывался: лог ротируется, успешная отправка в него не пишется вовсе, а «ушло, проверьте спам» и «не ушло» выглядели одинаково — как ничего.

Тело письма тоже сохраняется: без него нечего повторять.

Очередь ​

php
Mail::to($email)->subject($subject)->message($html)->context('app.register')->queue();

// Отложить — например, напоминание о брошенной корзине.
Mail::to($email)->subject($subject)->message($html)->queue(now()->addHours(3));

Везде, где письма ждёт живой человек, правильный выбор — queue(): недоступный почтовый сервер превращает синхронную отправку в страницу, которая висит до TCP-таймаута и затем отдаёт ошибку, причём письмо теряется в любом случае.

Если таблицы журнала нет, queue() отправит письмо сразу

Отложенная отправка — оптимизация, а не обязательное условие: письмо не должно исчезать из-за того, что миграция не применена.

Повтор при сбое ​

Просить о повторах не нужно: неудачный send() остаётся в очереди. Вызывающему коду не обязательно знать, что очередь вообще существует.

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

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

Состояния записи ​

СостояниеЧто означает
queuedждёт обработчика: либо ещё не пытались, либо выжидает паузу
sendingпрямо сейчас взято обработчиком
sentушло
failedпопытки исчерпаны
skippedзадержано режимом разработки

После неудачи запись возвращается в queued, а не остаётся в failed: состояние должно говорить, что будет дальше, а дальше будет ещё одна попытка. failed означает «само больше ничего не произойдёт».

context — метка, а не украшение ​

Произвольная строка: shop.invoice_copy, app.register, repository.license. Журнал фильтруется по ней. Без метки на вопрос «ушли ли вчерашние счета» отвечают, читая темы писем одну за другой; с меткой — одним фильтром. Стоит задавать всюду, где письма уходят пачкой.

Разбор очереди: cron ​

Без cron очередь двигается только тогда, когда кто-то нажмёт «Отправить сейчас» в менеджере, — то есть письма уходят, когда кто-то смотрит в экран.

bash
* * * * * cd /путь/к/сайту && /usr/bin/php core/components/pageblocks/bin/mail-queue.php >> /dev/null 2>&1
30 4 * * * cd /путь/к/сайту && /usr/bin/php core/components/pageblocks/bin/mail-queue.php --purge

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

Флаги: --limit=N, --context=KEY, --purge, --dry (показать очередь, ничего не отправляя).

Запускать от пользователя веб-сервера, а не от root

Иначе получим root-овские файлы кэша, которые сайт потом не сможет перезаписать.

Два обработчика одновременно ​

Запись забирается условным UPDATE: оба процесса видят её свободной, оба пытаются взять, база отдаёт ровно одному. Проигравший получает ноль затронутых строк и идёт дальше. Без этого cron раз в минуту при проходе длиной полторы минуты отправлял бы всё дважды.

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

Журнал в менеджере ​

Пункт меню Почта — отдельная страница компонента, рядом с «Многоразовыми блоками» и «Корзиной». Вкладкой конструктора журнал был раньше: в конструкторе строят схему, а журнал смотрят, когда письмо не пришло, — и спрашивает об этом не интегратор, а тот, кто ведёт сайт.

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

Писать в журнал руками нельзя — ни создать запись, ни отредактировать. Журнал, в который можно вписать что угодно, отвечает на вопрос «отправлялось ли это?» последней правкой оператора, а это хуже, чем отсутствие журнала.

Доступны: фильтр по состоянию (включая Требуют внимания — неудачные плюс те, что уже один раз промахнулись), фильтр по источнику, поиск, просмотр письма целиком, Отправить снова, Отправить сейчас и Удалить старые записи.

Тело письма со списком не передаётся: двадцать строк HTML-счёта на запись — это мегабайты по сети ради списка тем. Просмотрщик запрашивает одну запись отдельно и показывает тело в iframe с пустым sandbox — это чужая разметка, и ей не место в DOM менеджера ни по соображениям безопасности, ни по вёрстке.

Настройки ​

НастройкаПо умолчаниюЧто делает
pageblocks_mail_max_attempts5сколько раз пытаться
pageblocks_mail_batch50писем за один проход cron
pageblocks_mail_throttle250000пауза между письмами, в микросекундах
pageblocks_mail_keep_days90сколько хранить завершённые записи; 0 — не чистить никогда
pageblocks_site_modeproddev включает предохранитель
pageblocks_admin_email—кому всё ещё можно писать в dev
pageblocks_dev_mail_groupsAdministratorчьим группам всё ещё можно писать в dev

Предохранитель для разработки ​

На сайте в режиме dev Mail пропускает только письма на адрес pageblocks_admin_email и в группы из pageblocks_dev_mail_groups. Остальным не уходит ничего, запись помечается skipped, а send() возвращает true.

Так и задумано: вызывающий код считает false ошибкой доставки, и рассылка из-за режима площадки отписала бы живых подписчиков.

Залили дамп боевой базы на dev — первым делом поставьте pageblocks_site_mode = dev

Из дампа приедет prod, и предохранитель окажется выключен. Отсутствие настройки тоже читается как prod.

Хранение и очистка ​

В pb_mails лежат тела писем, из-за чего это самая тяжёлая таблица компонента. --purge удаляет записи, которые свой срок отслужили.

Очистка трогает только завершённые записи: отправленные, пропущенные и окончательно неудачные. Письмо, которое всё ещё ждёт в очереди, не становится устаревшим только потому, что его собрали две недели назад, — удалить его значило бы молча потерять почту, а ровно для этого журнал и существует.

📚 Итог ​

  • Mail — удобный класс для отправки писем с Fenom-шаблонами и вложениями.
  • Использует настройки MODX для значений from, fromName и sender.
  • Позволяет легко настраивать адреса, копии и формат отправки.
  • Каждое письмо попадает в журнал, а сбой повторяется с растущей задержкой.

© PageBlocks 2019-present