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

Для обычных уведомлений в Битрикс используется \Bitrix\Main\Mail\Event::send(): метод ставит событие в очередь. \Bitrix\Main\Mail\Event::sendImmediate() обрабатывает его сразу, без записи в очередь. Разберу оба способа, настройку отправителя и проверку статусов, по которым можно найти место сбоя.

Связать поля события с почтовым шаблоном

Почтовое событие передаёт данные, а шаблон определяет отправителя, получателя, тему и содержимое письма. В примерах используется тип события PROJECT_REQUEST_CREATED с полями EMAIL_TO, REQUEST_ID и CLIENT_NAME. Ему должен соответствовать активный шаблон, привязанный к сайту s1. Шаблон в текстовом формате:

От кого: #DEFAULT_EMAIL_FROM#
Кому: #EMAIL_TO#
Тема: Заявка №#REQUEST_ID#

Получена заявка №#REQUEST_ID#.
Клиент: #CLIENT_NAME#.

Значения для этих макросов передаются в C_FIELDS. DEFAULT_EMAIL_FROM заполняется штатно. Если шаблон привязан к другому сайту или неактивен, одного корректного вызова API недостаточно.

Одному типу события могут соответствовать несколько шаблонов. При обычном выборе Битрикс обработает подходящие активные шаблоны для указанного сайта и языка. Проверьте их количество, если вместо одного уведомления уходят несколько писем.

Поставить уведомление в очередь через Event::send()

Очередь подходит для уведомлений, результат которых не требуется получать прямо в пользовательском запросе. В обработчике после сохранения заявки:

use Bitrix\Main\Mail\Event;

$result = Event::send([
    'EVENT_NAME' => 'PROJECT_REQUEST_CREATED',
    'LID' => 's1',
    'LANGUAGE_ID' => 'ru',
    'DUPLICATE' => 'N',
    'C_FIELDS' => [
        'EMAIL_TO' => 'manager@example.com',
        'REQUEST_ID' => '124',
        'CLIENT_NAME' => 'Анна',
    ],
]);

if (!$result->isSuccess()) {
    error_log('[mail-queue] ' . implode('; ', $result->getErrorMessages()));
} else {
    $eventId = (int)$result->getId();
    error_log('[mail-queue] Создано почтовое событие ID=' . $eventId);
}

Код выполняется в загруженном окружении Битрикс. Замените сайт, язык и адрес на значения своего проекта. Адрес менеджера берётся из настроек приложения; не позволяйте посетителю произвольно задавать получателя служебного уведомления. Если письмо адресовано самому пользователю, предварительно проверьте его email и условия отправки.

isSuccess() сообщает об успешном добавлении события, а getId() возвращает ID записи в b_event. Это ещё не результат отправки: шаблоны будут обработаны позднее. Сохранённый ID позволяет найти конкретное событие, не ориентируясь только на время запроса.

DUPLICATE => 'N' отключает отправку на штатный адрес дублирования писем. Этот параметр не предотвращает повторную постановку события и не ограничивает число подходящих шаблонов. Если обработчик заявки может сработать повторно, защиту от повторного уведомления нужно связать с состоянием самой заявки.

Для выбора конкретного почтового шаблона существует MESSAGE_ID. Используйте ID действующего шаблона своего проекта и проверяйте его настройки: неверный ID не стоит считать гарантированным запретом отправки по другим шаблонам.

Обработать событие сразу через Event::sendImmediate()

Немедленная обработка полезна для разовой диагностики шаблона и в сценарии, где результат вызова транспорта нужен до завершения текущего запроса. Она увеличивает время ответа и не исправляет неисправный SMTP или неверный шаблон.

use Bitrix\Main\Mail\Event;

$status = Event::sendImmediate([
    'EVENT_NAME' => 'PROJECT_REQUEST_CREATED',
    'LID' => 's1',
    'LANGUAGE_ID' => 'ru',
    'DUPLICATE' => 'N',
    'C_FIELDS' => [
        'EMAIL_TO' => 'manager@example.com',
        'REQUEST_ID' => '124',
        'CLIENT_NAME' => 'Анна',
    ],
]);

if ($status === Event::SEND_RESULT_SUCCESS) {
    error_log('[mail-immediate] Шаблоны обработаны, транспорт вернул успех');
} else {
    error_log('[mail-immediate] Результат обработки: ' . $status);
}

Метод возвращает строковый статус, а не AddResult. Запись в b_event при таком вызове не создаётся, поэтому искать результат в очереди бессмысленно. Сравнивайте статус явно с константой: проверка if ($status) примет непустую строку F за истину.

Код Y означает успешный результат отправки по обработанным шаблонам на уровне используемого транспорта. Он не подтверждает, что письмо попало во входящие получателя. После принятия письма возможны отказ принимающего сервера, фильтрация или попадание в спам.

Event::sendImmediate() вызывает OnBeforeEventAdd до выбора и обработки шаблонов. Если обработчик возвращает false, метод сразу возвращает Event::SEND_RESULT_NONE (N). Записи в очереди при этом нет: название события OnBeforeEventAdd не означает, что оно вызывается только перед записью в b_event.

Выбрать адрес отправителя: DEFAULT_EMAIL_FROM

#DEFAULT_EMAIL_FROM# — макрос почтового шаблона, а не PHP-константа, которую нужно объявлять в init.php. Битрикс берёт адрес из поля EMAIL выбранного сайта. Если оно пустое, используется настройка главного модуля email_from.

Проверить оба значения в PHP-коде с загруженным ядром:

$site = \Bitrix\Main\SiteTable::getById('s1')->fetch();
$siteFrom = trim((string)($site['EMAIL'] ?? ''));
$moduleFrom = \Bitrix\Main\Config\Option::get('main', 'email_from', '');

var_export([
    'site_email' => $siteFrom,
    'main_email_from' => $moduleFrom,
]);

Проверьте также EMAIL_FROM самого шаблона: если там указан буквальный адрес, изменение DEFAULT_EMAIL_FROM его не заменит. Используйте отправителя, разрешённого вашим почтовым сервисом. Передача email посетителя в From может конфликтовать с настройками сервиса и домена; для ответа посетителю предназначен Reply-To.

Найти событие и прочитать SUCCESS_EXEC в b_event

Очередь хранится в таблице b_event, в единственном числе. Запрос для конкретного ID события:

SELECT ID, EVENT_NAME, LID, MESSAGE_ID,
       DATE_INSERT, DATE_EXEC, SUCCESS_EXEC
FROM b_event
WHERE ID = 124;

Замените 124 на ID из результата Event::send(). Не путайте его с номером заявки, который передаётся внутри C_FIELDS.

Основные статусы:

  • N — событие остаётся в очереди. Новая запись получает этот статус до обработки; длительное ожидание требует проверки запуска очереди. Обработчики также могут намеренно отложить событие.
  • Y — отправка по обработанным шаблонам вернула успех. Это не отчёт о доставке получателю.
  • F — обработанные шаблоны завершились ошибкой. Причина может быть при формировании письма или в транспорте, а не только в PHP mail().
  • P — часть попыток по шаблонам успешна, часть завершилась ошибкой. Проверяйте каждый подходящий шаблон и его адреса.
  • 0 — отправка по шаблонам не состоялась. Сначала проверяйте их активность и привязку к событию и сайту; такой результат возможен также при пропуске шаблонов обработчиком.
  • E — обработка события прервана исключением. Ищите запись в журнале исключений Битрикс.

DATE_EXEC помогает отличить свежую запись от уже обработанной. Не меняйте статус вручную на Y: это только скроет проблему. Возврат старых событий в N может вызвать повторные уведомления.

Для оценки накопления очереди:

SELECT EVENT_NAME, COUNT(*) AS PENDING,
       MIN(DATE_INSERT) AS OLDEST
FROM b_event
WHERE SUCCESS_EXEC = 'N'
GROUP BY EVENT_NAME
ORDER BY OLDEST;

Если события разных типов копятся одновременно, сначала проверяйте обработчик очереди. Если проблема только у одного типа, сравните его шаблоны и обработчики с работающими событиями.

Разобраться с N: обработка на хитах или по cron

CEvent::CheckEvents() запускает проверку почтовой очереди. При BX_CRONTAB_SUPPORT === true обычный запрос без BX_CRONTAB === true пропускает эту проверку: предполагается запуск из cron. DisableEventsCheck === true также запрещает проверку.

Поэтому удаление cron-констант — не универсальное исправление. Сначала определите, где очередь должна обрабатываться, и проверьте реальный запуск этого процесса. Состояние констант в текущем окружении можно посмотреть так:

foreach (['BX_CRONTAB_SUPPORT', 'BX_CRONTAB', 'DisableEventsCheck'] as $name) {
    printf("%s = %s\n", $name,
        defined($name) ? var_export(constant($name), true) : 'не определена');
}

Веб-запрос и CLI могут загружать разные настройки. Проверьте задачу пользователя, от которого запускается сайт, путь к PHP, рабочий каталог и журнал выполнения. Убедитесь, что init.php не делает в CLI редирект и не рассчитывает на заполненные HTTP_HOST или другие данные браузерного запроса.

Для отдельного обработчика почты можно использовать /local/cli/process-mail.php:

<?php
if (PHP_SAPI !== 'cli') { exit; }

$_SERVER['DOCUMENT_ROOT'] = dirname(__DIR__, 2);
define('NO_KEEP_STATISTIC', true);
define('BX_CRONTAB', true);

require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';

\CEvent::CheckEvents();

BX_CRONTAB здесь задаётся только для процесса обработки. Если в проекте уже есть рабочий обработчик очереди, исправьте его запуск, а не добавляйте второй независимо от первого.

Разовый запуск выполняется от того же пользователя, которому доступны файлы и конфигурация сайта. Например, при корне сайта /var/www/site и пользователе www-data:

sudo -u www-data /usr/bin/php /var/www/site/local/cli/process-mail.php

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

Если ручной запуск работает, а плановый — нет, причина находится в расписании или окружении cron. Если статусы остаются N, проверьте запрещающие константы, ошибки выполнения и блокировку обработчика. Ещё одна возможная причина пропуска проверки — маркер пустой очереди в managed cache.

Как managed cache влияет на проверку очереди

Битрикс кеширует не сами письма, а признак того, что ожидающих событий нет. Он хранится под ключом events и позволяет не обращаться к b_event при каждом запуске проверки.

Последовательность такая:

  1. Обработчик ищет записи с SUCCESS_EXEC = 'N'.
  2. Если записей нет и кеширование очереди включено, он сохраняет маркер events.
  3. При следующем вызове CEvent::CheckEvents() наличие актуального маркера позволяет завершить проверку без выборки писем из базы.
  4. При добавлении нового события Event::send() сбрасывает этот маркер, чтобы очередь снова проверялась.

За использование маркера отвечает CACHED_b_event: значение false отключает эту проверку кеша. При штатном добавлении событий через API вручную очищать кеш после каждой отправки не требуется.

Рассматривать кеш как причину стоит при конкретном расхождении: в b_event есть ожидающие записи, запуск обработчика подтверждён, запрещающих констант нет, но до выборки очереди выполнение не доходит. Например, прямое добавление записей SQL-запросом обходит сброс маркера, который делает Event::send().

Для проверки этой гипотезы сбросьте только маркер очереди в окружении Битрикс:

\Bitrix\Main\Application::getInstance()->getManagedCache()->clean('events');

Затем повторите запуск обработчика и сравните статусы тех же событий. Очистка маркера сама письма не отправляет; следующий проход очереди уже может их обработать. Если поведение не изменилось, причина не подтверждена: вернитесь к запуску процесса, его ошибкам и блокировке.

Если сброс помогает, но проблема возвращается, проверьте путь создания событий и ошибки доступа к хранилищу managed cache от пользователя PHP или cron. Очистка всего каталога /bitrix/managed_cache/ затронет другие данные и не нужна для проверки именно этого маркера.

Разобраться с 0: шаблон не выбран или пропущен

Начните с сочетания EVENT_NAME, LID и LANGUAGE_ID из вызова. Затем проверьте, что нужный шаблон активен, относится к этому типу события и привязан к сайту. Если передаётся MESSAGE_ID, проверьте существование и активность именно этого шаблона.

Просмотрите OnBeforeEventAdd и OnBeforeEventSend. Первый может отменить создание или немедленную обработку события, второй — пропустить отдельный шаблон. Если все шаблоны пропущены, результат может быть 0 даже при наличии активных шаблонов в базе.

Ошибка SMTP не объясняет отсутствие выбранного шаблона: до транспорта выполнение могло вообще не дойти. Аналогично пропущенный макрос в тексте письма сам по себе не означает статус 0.

Разобраться с F или P: поля шаблона и почтовый транспорт

Сначала проверьте подставленные адреса отправителя, получателя и копий. Макрос должен получать значение из C_FIELDS или штатных полей. Произвольный #BCC# не заполняется автоматически только потому, что он добавлен в шаблон: всё зависит от данных конкретного события. Ошибка в необязательной строке тела и пустой адрес получателя имеют разные последствия.

Следующий этап — транспорт. Путь отправки проходит через bxmail() из /bitrix/modules/main/tools.php. Ядро может использовать встроенный SMTP, функцию custom_mail() или PHP mail(). При выбранном встроенном SMTP наличие custom_mail() не означает, что вызов дойдёт до неё. Сам файл tools.php для настройки почты не редактируют.

Если проект определяет custom_mail(), проверьте её возвращаемое значение. Обёртка должна вернуть результат реальной передачи письма. Отсутствующий return может дать Битрикс неуспех после фактической отправки, а безусловный return true — скрыть сбой. Прямой вызов PHPMailer проверяет только настроенный для него SMTP-маршрут: он не проверяет почтовый шаблон, очередь и маршрут, выбранный Битрикс.

При ошибке транспорта ищите конкретный ответ: отказ авторизации, недопустимый отправитель, ошибка TLS, недоступность сервера или превышение лимита. Путь к журналу зависит от используемой службы и окружения. /home/bitrix/msmtp_default.log относится к конфигурациям с соответствующим msmtp и не является общим журналом любой установки Битрикс.

Проверяйте доступ к SMTP-конфигурации от пользователя реального процесса. Не меняйте владельца файлов на bitrix:bitrix автоматически, если PHP или cron работают от другого пользователя. Пароли и полные тела писем не должны попадать в публичный каталог сайта или вывод страницы при отладке.

Если статус Y, но письма нет, переходите к журналам почтового сервиса: ищите конкретную отправку, отказ принимающей стороны, ограничения и фильтрацию. Проверьте также папку спама. Повторные вызовы Event::send() не исправляют доставляемость и создают новые уведомления.

Проверить достижение транспорта через OnBeforePhpMail

OnBeforePhpMail вызывается внутри bxmail() перед выбором транспорта. Это D7-событие: обработчик получает объект \Bitrix\Main\Event, а аргументы письма находятся в параметре arguments.

Для временной проверки в /local/php_interface/init.php:

\Bitrix\Main\EventManager::getInstance()->addEventHandler(
    'main',
    'OnBeforePhpMail',
    static function (\Bitrix\Main\Event $event): void {
        $arguments = $event->getParameter('arguments');
        if (!is_object($arguments)) { return; }

        error_log('[mail-transport] Вызван bxmail; окружение=' . PHP_SAPI);
    }
);

Запись подтверждает, что выполнение дошло до bxmail(). Она не содержит результата следующего вызова транспорта и не доказывает отправку. Обработчик срабатывает для всех писем, проходящих через эту функцию; после диагностики его следует убрать.

Через объект arguments можно менять параметры письма, но это не обработчик типа почтового события. Для изменения полей конкретного события или выбора шаблона удобнее OnBeforeEventSend. Не пытайтесь отменить отправку простым return false из OnBeforePhpMail: bxmail() не использует этот результат как команду остановки.

Что проверять при NULL и ошибках окружения

NULL в SUCCESS_EXEC не относится к штатным результатам обработки. Такая запись не попадёт в выборку SUCCESS_EXEC = 'N'. Проверьте код, который её создал, обновления базы и соответствие структуры таблицы установленному ядру.

Начинать с ALTER TABLE или массовой замены NULL на N нельзя: изменение значения по умолчанию не объясняет появление старых записей, а повторная постановка может отправить уже обработанные уведомления. Исправление схемы имеет смысл только после подтверждения конкретного расхождения и определения судьбы существующих данных.

Для проверки исправления создайте одно событие на контролируемый адрес и сохраните его ID. Убедитесь, что очередь его обработала, транспорт принял письмо, а получатель действительно его получил. Так у каждого этапа будет собственное подтверждение — от записи в b_event до доставки.