Разбор 01

Не работают формы обратной связи

Форма показывала «сообщение отправлено», но письма не приходили, а Telegram-бот перестал получать заявки. Тихий двойной отказ — клиент терял лиды неделями.

PHP WordPress Contact Form 7 WP Mail SMTP Telegram Bot API wpcf7_before_send_mail
Клиент
Услуговая компания, сайт-визитка на WordPress
Стек
PHP 7.4, WordPress 6.3, Contact Form 7, WP Mail SMTP, Telegram Bot API
Срок
~3 часа (диагностика + фикс)
Итог
Заявки идут двумя каналами, отказы логируются

Суть проблемы

Сайт-визитка на WordPress + Contact Form 7. Каждая заявка должна уходить двумя путями: письмом менеджеру на почту домена и дублироваться в Telegram-бот через webhook. Клиент пожаловался, что «заявок нет уже неделю», хотя форма вроде работает и показывает успех.

Что видел клиент До

Зелёное «сообщение отправлено» на сайте, тишина в почте и в Telegram. Неделя потерянных лидов без единого сигнала, что что-то сломалось.

Что стало после фикса После

Письмо приходит менеджеру за 1–2 секунды, дубликат падает в Telegram за <1 сек, любая неудача пишется в лог и в очередь ретраев.

Как диагностировал

Двойной отказ редко бывает одной причиной — проверял оба канала независимо, чтобы не свалить всё на одно место и не починить только половину.

  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().
  2. Проверил обработчик заявокCF7 использует wp_mail(), который без настроенного SMTP падает тихо — в хуке wpcf7_before_send_mail не было обработки ошибок отправки, поэтому отказ маскировался успехом формы.
  3. Вскрыл Telegram-частьВ functions.php кастомный хук на wpcf7_before_send_mail делал file_get_contents("https://api.telegram.org/...") без проверки ответа. Прямой запрос к api.telegram.org возвращал 401 Unauthorized.
  4. Сравнил токен ботаСверил токен в коде с токеном из @BotFather — токен был сброшен командой /revoke, старый инвалиден. Когда бота пересоздавали, токен в коде не обновили.
  5. Нашёл причину тишиныfile_get_contents на внешний URL мог молча возвращать false из-за allow_url_fopen, а ошибки проглатывались оператором @ перед вызовом.
  6. Убедился в независимости каналовПисьмо и 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-логированием.

functions.phpбыло
// Хук отправки заявки в 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 );
}
functions.phpстало
// Хук отправки заявки: почта + 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 получал пустой успех и показывал пользователю «отправлено». Ни одна ошибка не всплыла наружу, потому что на каждом из трёх уровней кто-то заглушал результат.

Результат

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

0% → 100%
100%
Доставка письма менеджеру (до 2 сек)
0% → 100%
100%
Доставка в Telegram (до 1 сек)
100% → 0%
0%
Тихих отказов — теперь логируются
→ 3 ч
~3 ч
Время цикла фикса

Заявки снова идут двумя каналами параллельно. Отказы теперь видны в debug.log и не теряются: упавшая отправка попадает в очередь ретраев и доходит при восстановлении канала. Дополнительно настроил простой мониторинг — алиерт срабатывает, если за сутки не было ни одной записи об отправке (значит, форма опять тихо умерла).

Корневые причины — отключённая mail() и сброшенный токен бота — устранены на уровне инфраструктуры и кода, а не обойдены костылями. Повторный тихий отказ теперь невозможен: логирование и очередь ретраев страхуют оба канала.
Есть похожая проблема?

Сначала найдём причину. Потом решим, что действительно нужно исправить.

Опишите симптом и что из-за него перестало работать. Я посмотрю, где искать причину и насколько задача похожа на точечный фикс.

Описать проблему ↗