Bitrix D7 Mail\Event: send, sendImmediate и типичные ошибки

В модулях до сих пор вызывают CEvent::Send «на удачу»: без проверки результата, с чужим LID и ожиданием, что письмо уйдёт внутри того же хита. Штатный D7-контракт — Bitrix\Main\Mail\Event: постановка в очередь через send или синхронная отправка через sendImmediate.

Рядом по теме: Result и Error, EventManager (это другие события), cron в Linux, Logger.

Если нужна помощь с модулем или отладкой почты на проде — услуги и контакты на karoche.ru.

Чем Mail\Event отличается от EventManager

Bitrix\Main\EventManager подписывает обработчики на события модулей (OnAfterUserAdd и т.п.). Bitrix\Main\Mail\Event работает с почтовыми событиями: тип из «Почтовые события», шаблон письма, поля #EMAIL#, очередь b_event.

Путаница в названии дорого стоит: подписались на ядерное событие и ждут письмо, либо наоборот вызывают Mail\Event там, где нужен хук бизнес-логики. Это два разных механизма.

Минимальный вызов Event::send

send создаёт запись почтового события и возвращает \Bitrix\Main\Entity\AddResult (аналог старого CEvent::Send). Фактическая отправка SMTP обычно происходит позже — агентом при обработке очереди.

<?php
use Bitrix\Main\Mail\Event;
use Bitrix\Main\Loader;

Loader::includeModule('main');

$result = Event::send([
    'EVENT_NAME' => 'NEW_USER',
    'LID' => 's1',
    'C_FIELDS' => [
        'EMAIL' => 'user@example.com',
        'USER_ID' => 42,
    ],
]);

if (!$result->isSuccess()) {
    // не попали в очередь / ошибка добавления
    $errors = $result->getErrorMessages();
    // залогировать через Diag\Logger или свой канал
}

Успешный isSuccess() значит: событие попало в очередь. Это ещё не «письмо в инбоксе». Если агенты крутятся только от хитов и ночью трафика нет, очередь копится часами.

Когда нужен sendImmediate

sendImmediate пытается отправить письмо в текущем запросе и не создаёт запись в b_event (по контракту legacy CEvent::SendImmediate; D7-метод — заявленный аналог с оговорками).

Типичные случаи:

  1. Код восстановления пароля / одноразовый код, где задержка очереди неприемлема.
  2. Фоновый CLI/агент, где вы уже «внутри крона» и не хотите второй слой очереди.
  3. Диагностика: проверить SMTP «здесь и сейчас», без ожидания агента.
<?php
use Bitrix\Main\Mail\Event;

$flag = Event::sendImmediate([
    'EVENT_NAME' => 'USER_PASS_REQUEST',
    'LID' => 's1',
    'C_FIELDS' => [
        'EMAIL' => 'user@example.com',
        'CODE' => 'Af329',
    ],
]);

// строковые коды (как у CEvent::SendImmediate), не объект Result
if ($flag === Event::SEND_RESULT_SUCCESS) { // 'Y'
    // ушло из PHP-процесса
} elseif ($flag === Event::SEND_RESULT_TEMPLATE_NOT_FOUND) { // '0'
    // нет активного шаблона под EVENT_NAME + LID
} elseif ($flag === Event::SEND_RESULT_ERROR) { // 'F'
    // сбой отправки
}

Константы результата (из документации CEvent::SendImmediate): SEND_RESULT_NONE = 'N', SUCCESS = 'Y', ERROR = 'F', PARTLY = 'P', TEMPLATE_NOT_FOUND = '0'.

Важная оговорка сообщества на странице документации: Bitrix\Main\Mail\Event::sendImmediate не вызывает обработчики OnBeforeEventAdd, тогда как CEvent::SendImmediate их выполняет. Если ваш код полагается на этот хук (подмена полей, стоп-лист) — проверьте поведение на стенде перед заменой вызова.

Чем отличаются send и sendImmediate

КритерийEvent::sendEvent::sendImmediate
Очередь b_eventДа, запись создаётсяНет (по контракту immediate)
Когда уходит SMTPПозже, агентом / cronВ текущем хите/процессе
Тип возвратаEntity\AddResult (isSuccess, ошибки)Строковый код Y/F/P/N/0
Нагрузка на хитНизкая (запись в БД)Выше (сеть SMTP внутри запроса)
Типичный сценарийЗаказ, регистрация, фоновые уведомленияOTP, срочный алерт, отладка

Для пользовательского HTTP-запроса по умолчанию берите send. sendImmediate в тяжёлом SMTP на каждом клике тормозит ответ и сильнее зависит от доступности почтового сервера.

LID, MESSAGE_ID, FILE и C_FIELDS

В D7 параметр LID — строка. Несколько сайтов передают через запятую ("s1,s2"), не массивом как в части старых вызовов CEvent.

Частая ловушка: в LID попадает языковой код или константа вроде ru, а шаблоны привязаны к сайту s1. Результат — TEMPLATE_NOT_FOUND / пустая отправка при живом шаблоне в админке.

Поля:

  • EVENT_NAME — символьный код типа почтового события.
  • C_FIELDS — значения для макросов шаблона (#EMAIL#, #ORDER_ID# …). Ключи должны совпадать с полями типа события.
  • MESSAGE_ID (int) — конкретный шаблон, если на тип повешено несколько.
  • FILE — массив id файлов CFile или абсолютных путей для вложений. Файл сначала сохраняют (CFile::SaveFile), потом передают id; временные файлы после отправки можно удалить.
<?php
use Bitrix\Main\Mail\Event;

$fileId = \CFile::SaveFile($_FILES['attach'], 'mailatt');

Event::send([
    'EVENT_NAME' => 'LOCAL_FORM_FEEDBACK',
    'LID' => 's1',
    'MESSAGE_ID' => 64,
    'C_FIELDS' => [
        'EMAIL' => $email,
        'MESSAGE' => $text,
    ],
    'FILE' => $fileId ? [$fileId] : [],
]);

Пользовательский текст из формы не кладите в шаблон без очистки: HTML из комментария легко уедет в письмо как разметка.

Как проверить, что письмо реально ушло

  1. Проверить возврат вызова: AddResult для send, строковый код для sendImmediate.
  2. В админке: Сервисы → Почтовые события → просмотр очереди/журнала. Нет записи после send — проблема до SMTP (код, EVENT_NAME, права, исключения раньше вызова).
  3. Статус в журнале: ожидает / отправлено / ошибка. Текст ошибки SMTP часто прямо указывает на порт, relay или отказ получателя.
  4. Режим агентов: на проде очередь должна крутиться системным cron (cron_events.php / аналог), не только «от хитов». Иначе ночные письма стоят до первого утреннего визита. База по crontab — в гайде про cron для PHP.
  5. Если в b_event статус «отправлено», а в ящике пусто — смотреть SPF/DKIM, спам, блокировку исходящего порта на хостинге. Это уже слой MTA, не API Mail\Event.

Для отладки своего модуля удобно писать исход вызова в Diag\Logger: EVENT_NAME, LID, isSuccess / код immediate, id записи если доступен.

Типичные ошибки

Опечатка в EVENT_NAME. Шаблон есть, код события в вызове другой на одну букву. Immediate вернёт TEMPLATE_NOT_FOUND (0); у send смотрите ошибки AddResult и отсутствие нормальной обработки шаблона в журнале.

Неверный LID. Классика с мультиязычностью и копипастой SITE_ID / языка. Сверьте идентификатор сайта в настройках сайта и привязку шаблона.

Ждут мгновенной доставки от send. Метод честно положил задачу в очередь. Без cron письмо «висит» до агента.

Не проверяют результат. Ошибка шаблона всплывает только когда клиент пишет в поддержку.

Путают с EventManager. Подписка на EventManager::getInstance()->addEventHandler(...) письмо сама не отправит — нужен явный Mail\Event::send*.

sendImmediate в каждом хите формы. Таймаут SMTP = долгий TTFB и ложные «сайт лежит». Для формы обратной связи обычно достаточно send + рабочий cron.

Вложение без SaveFile. В FILE передали путь из $_FILES['tmp_name'] без сохранения — на следующем шаге файла уже нет.

Смена API без проверки хуков. Переезд с CEvent::SendImmediate на D7 Event::sendImmediate может обойти OnBeforeEventAdd — см. оговорку выше.

Чеклист перед продом

  1. Тип почтового события и активный шаблон на нужном LID.
  2. В шаблоне есть все макросы, которые вы кладёте в C_FIELDS (и наоборот — нет обязательных пустых).
  3. Вызов через Bitrix\Main\Mail\Event, результат проверяется.
  4. Для обычных уведомлений — send; для OTP/срочного — осознанный sendImmediate.
  5. На проде агенты на системном cron, лог крона пишется, тестовое письмо появляется в журнале событий.
  6. Исходящий SMTP/порт разрешён хостингом; после успешной отправки из журнала при необходимости проверены DNS (SPF/DKIM).
  7. Нет секретов в теле письма (токены с длинным TTL, пароли в открытом виде).

Если разбираете связку контроллер → сервис → письмо, смотрите также Engine\Controller и контракт Result.

Источники

Нужна помощь с модулем Битрикс или почтовыми событиями на проекте — karoche.ru.