Как настроить отправку писем с формы обратной связи на сайте
Практика3 минРедакция Innovialabs
Коротко
Надёжная форма сначала сохраняет заявку (в базу, CRM или Telegram), а уже потом отправляет письмо через транзакционный сервис с домена, настроенного по SPF, DKIM и DMARC; спам отсекают honeypot и лимит запросов.
Короткий ответ
Форма обратной связи не должна полагаться только на почту. Рабочая схема выглядит так:
- Браузер отправляет данные на ваш серверный обработчик (API-маршрут или serverless-функцию).
- Обработчик проверяет данные и отсекает спам.
- Заявка сохраняется: база данных, CRM, таблица или сообщение в Telegram.
- Только после этого отправляется письмо через SMTP или транзакционный сервис.
- Пользователь видит «Спасибо», когда заявка сохранена, даже если письмо задержалось.
Функция mail() на хостинге и отправка «от имени» Gmail без настроек — частые причины, по которым письма уходят в спам или теряются.
Способы отправки
| Способ | Плюсы | Минусы |
|---|---|---|
| SMTP вашей почты (Google Workspace, Яндекс 360 и др.) | просто, уже есть ящик | лимиты на отправку, пароль приложения в настройках сервера |
| Транзакционный сервис (SendGrid, Mailgun, Postmark, Amazon SES, Resend) | хорошая доставляемость, логи, вебхуки о недоставке | нужна настройка DNS, платные тарифы при росте |
| Serverless-обработчик (Vercel, Netlify, Cloudflare) | не нужен свой сервер | внутри всё равно нужен SMTP или API почтового сервиса |
| Готовые сервисы форм | ноль кода | данные у третьей стороны, меньше контроля |
Для большинства сайтов разумный выбор — транзакционный сервис через API, вызываемый из серверного обработчика. Ключ API хранится в переменных окружения сервера и никогда не попадает в браузер.
Пример обработчика
Пример маршрута на Node.js с Nodemailer. Логика одинакова для любого фреймворка.
import nodemailer from "nodemailer";
const transport = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: 587,
auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS }
});
export async function handleLead(body, ip) {
if (body.website) return { ok: true }; // honeypot: бот заполнил скрытое поле
if (!body.name || !body.contact) return { ok: false, error: "invalid" };
if (await tooManyRequests(ip)) return { ok: false, error: "rate_limit" };
const lead = await saveLead(body); // сначала сохраняем
try {
await transport.sendMail({
from: "Сайт <no-reply@example.com>",
to: "sales@example.com",
replyTo: body.email,
subject: `Новая заявка: ${body.name}`,
text: `${body.name}\n${body.contact}\n\n${body.message ?? ""}`
});
} catch (err) {
await markForRetry(lead.id, err); // письмо не ушло, заявка цела
}
return { ok: true };
}
Обратите внимание: From — адрес вашего домена, а email посетителя ставится в Reply-To. Если подставить адрес посетителя в From, почтовые системы почти наверняка отклонят письмо как подделку.
Защита от спама
- Honeypot. Скрытое поле (например,
website), невидимое человеку. Боты его заполняют — такие запросы молча игнорируйте. - Лимит запросов. Ограничьте число отправок с одного IP за промежуток времени.
- Проверка времени. Форма, отправленная через секунду после загрузки страницы, скорее всего от бота.
- Капча (Cloudflare Turnstile, reCAPTCHA) — когда honeypot и лимиты уже не справляются.
- Валидация на сервере. Проверки в браузере легко обойти, повторяйте их на сервере.
Доставляемость
- Отправляйте с собственного домена, а не с бесплатного ящика.
- Настройте SPF (кто имеет право слать с домена), DKIM (подпись писем) и DMARC (что делать с письмами, не прошедшими проверку). Транзакционный сервис даёт готовые DNS-записи.
- Пишите простые письма: понятная тема, текстовая версия, без сокращённых ссылок.
- Следите за вебхуками о недоставке, если сервис их поддерживает.
Как не терять заявки
- Сохраняйте заявку до отправки письма. Почта — уведомление, а не хранилище.
- Дублируйте канал: письмо плюс сообщение в Telegram-чат или запись в CRM.
- Повторы: неудачные отправки ставьте в очередь и пробуйте снова.
- Мониторинг: раз в неделю отправляйте тестовую заявку или настройте алерт на ошибки обработчика.
- Честный ответ пользователю: если сохранить не удалось, покажите ошибку и альтернативный контакт, а не «Спасибо».