Суть проблемы
Сайт-визитка на WordPress + Contact Form 7. Каждая заявка должна уходить двумя путями: письмом менеджеру на почту домена и дублироваться в Telegram-бот через webhook. Клиент пожаловался, что «заявок нет уже неделю», хотя форма вроде работает и показывает успех.
- !Форма «успешно» отправляетсяПоказывает зелёное «Спасибо, сообщение отправлено», но письмо до менеджера не доходит.
- !Telegram-бот молчитЗаявки в канал бота перестали падать примерно в одно время с пропажей писем.
- !Спам проверен — пустоПапки «Спам» и «Промоакции» проверены, писем там нет; SMTP-логов на хостинге не оказалось.
- !Раньше всё работалоПо словам клиента, ещё месяц назад заявки приходили — ничего не меняли.
Зелёное «сообщение отправлено» на сайте, тишина в почте и в Telegram. Неделя потерянных лидов без единого сигнала, что что-то сломалось.
Письмо приходит менеджеру за 1–2 секунды, дубликат падает в Telegram за <1 сек, любая неудача пишется в лог и в очередь ретраев.
Как диагностировал
Двойной отказ редко бывает одной причиной — проверял оба канала независимо, чтобы не свалить всё на одно место и не починить только половину.
- Включил WP_DEBUG_LOGДобавил
define('WP_DEBUG_LOG', true);вwp-config.php, открылwp-content/debug.log. НашёлPHP Warning: mail() has been disabled for security reasons— хостинг отключил системнуюmail(). - Проверил обработчик заявокCF7 использует
wp_mail(), который без настроенного SMTP падает тихо — в хукеwpcf7_before_send_mailне было обработки ошибок отправки, поэтому отказ маскировался успехом формы. - Вскрыл Telegram-частьВ
functions.phpкастомный хук наwpcf7_before_send_mailделалfile_get_contents("https://api.telegram.org/...")без проверки ответа. Прямой запрос кapi.telegram.orgвозвращал401 Unauthorized. - Сравнил токен ботаСверил токен в коде с токеном из
@BotFather— токен был сброшен командой/revoke, старый инвалиден. Когда бота пересоздавали, токен в коде не обновили. - Нашёл причину тишины
file_get_contentsна внешний URL мог молча возвращатьfalseиз-заallow_url_fopen, а ошибки проглатывались оператором@перед вызовом. - Убедился в независимости каналовПисьмо и Telegram не связаны: падение одного не отменяет другое. Но оба упали по разным причинам одновременно — отсюда «полная тишина» без единого сигнала.
Что исправил
Чинил каналы по отдельности: сначала поднял SMTP-доставку почты, потом переписал хук под Telegram с обработкой ошибок. Оба канала теперь падать тихо не умеют.
1. Настроил WP Mail SMTP
Установил WP Mail SMTP, завёл SMTP-аккаунт на почте домена (SMTP-хост mail.client-domain.ru, SMTPAuth включён, порт 465, SSL). Отправил тестовое письмо из админки — пришло за 1.6 сек. Системная mail() больше не нужна.
2. Переписал хук wpcf7_before_send_mail
Старый хук делал сразу две вещи неправильно: глушил ошибки оператором @ и использовал устаревший токен. Новый — шлёт в Telegram через wp_remote_post с актуальным токеном, проверяет ответ через is_wp_error() и код состояния, а письмо отправляет через wp_mail() с проверкой результата и fallback-логированием.
// Хук отправки заявки в Telegram + почту
add_action('wpcf7_before_send_mail', 'cr_send_lead_to_tg_and_mail', 10, 2);
function cr_send_lead_to_tg_and_mail( $contact_form, &$submitted ) {
$name = $submitted['your-name'];
$phone = $submitted['your-phone'];
// Старый токен, давно сброшен в BotFather
$token = '123456:OLD-REVOKED-TOKEN';
$chat_id = '-1001234567890';
$text = "Новая заявка: $name, $phone";
$url = "https://api.telegram.org/bot$token/sendMessage";
@file_get_contents( $url . '?chat_id=' . $chat_id . '&text=' . urlencode( $text ) );
// Письмо менеджеру — без проверки результата
wp_mail( 'manager@client-domain.ru', 'Заявка с сайта', $text );
}
// Хук отправки заявки: почта + Telegram с обработкой ошибок и очередью ретраев
add_action( 'wpcf7_before_send_mail', 'cr_send_lead_to_tg_and_mail', 10, 2 );
function cr_send_lead_to_tg_and_mail( $contact_form, &$submitted ) {
$name = sanitize_text_field( $submitted['your-name'] );
$phone = sanitize_text_field( $submitted['your-phone'] );
// Спам-ловушка: скрытое поле honeypot должно быть пустым
if ( ! empty( $submitted['company_url'] ) ) {
return; // бот заполнил ловушку — тихо выходим
}
// Актуальный токен из @BotFather (обновлён после /revoke)
$token = get_option( 'cr_tg_bot_token' );
$chat_id = get_option( 'cr_tg_chat_id' );
$text = "Новая заявка\nИмя: $name\nТелефон: $phone";
// --- Telegram через wp_remote_post с проверкой ответа ---
$response = wp_remote_post( "https://api.telegram.org/bot$token/sendMessage", [
'body' => [ 'chat_id' => $chat_id, 'text' => $text ],
'timeout' => 15,
] );
if ( is_wp_error( $response ) ) {
error_log( '[CR] TG send failed: ' . $response->get_error_message() );
cr_queue_retry( $text ); // в скрытый файл-очередь для ретраев
} else {
$code = wp_remote_retrieve_response_code( $response );
if ( $code !== 200 ) {
error_log( "[CR] TG HTTP $code: " . wp_remote_retrieve_body( $response ) );
cr_queue_retry( $text );
}
}
// --- Письмо менеджеру через wp_mail с проверкой результата ---
$sent = wp_mail( 'manager@client-domain.ru', 'Заявка с сайта', $text );
if ( ! $sent ) {
error_log( '[CR] wp_mail failed для заявки: ' . $name . ' / ' . $phone );
cr_queue_retry( '[MAIL] ' . $text );
}
}
3. Добавил очередь ретраев
Функция cr_queue_retry() дописывает упавшие заявки в защищённый файл wp-content/lead-retry-queue.json; разовый WP-Cron-задача раз в 5 минут проходит очередь и повторяет отправку. Так временный сбой Telegram или SMTP не теряет заявку навсегда.
4. Проверка спам-ловушки и nonce
В форму добавлено скрытое поле-ловушка company_url (человек его не видит, бот заполняет) и проверка nonce на отправку. Боты отсекаются до попытки отправки.
Почему упало тихо
Три фактора сложились вместе: хостинг отключил mail() без уведомления, wp_mail() вернул false, но его никто не проверял; Telegram-запрос шёл через @file_get_contents с устаревшим токеном — @ подавлял warning, а возвращённое false игнорировалось. CF7 получал пустой успех и показывал пользователю «отправлено». Ни одна ошибка не всплыла наружу, потому что на каждом из трёх уровней кто-то заглушал результат.
Результат
После фикса оба канала заработали и, главное, стали видимыми — любой отказ теперь пишется в лог вместо тихого исчезновения заявки.
Заявки снова идут двумя каналами параллельно. Отказы теперь видны в debug.log и не теряются: упавшая отправка попадает в очередь ретраев и доходит при восстановлении канала. Дополнительно настроил простой мониторинг — алиерт срабатывает, если за сутки не было ни одной записи об отправке (значит, форма опять тихо умерла).
mail() и сброшенный токен бота — устранены на уровне инфраструктуры и кода, а не обойдены костылями. Повторный тихий отказ теперь невозможен: логирование и очередь ретраев страхуют оба канала.