Краткое резюме
Главная причина, по которой уведомление с формы обратной связи «вроде бы отправляется успешно, но не доходит», — не в самом соединении SMTP, а в том, что видимый отправитель (From:) не совпадает с отправителем, который фактически проходит аутентификацию (MAIL FROM в SPF / d= в DKIM). DMARC ориентируется на RFC5322.From и проверяет, проходит ли SPF или DKIM с выравниванием (alignment). Иными словами, дизайн, при котором Gmail-адрес или корпоративный адрес отправителя формы подставляется напрямую в From:, выглядит удобным для ответа, но на практике легко приводит к сбоям.
Практический вывод прост. Базовое правило — закрепить From: уведомлений об обращении за доменом собственного сайта, а адрес пользователя формы указывать в Reply-To:. Sender: используется только тогда, когда «автор письма и фактический отправитель различаются», а Return-Path не прописывается вручную в заголовке — его задают как адрес конверта (envelope sender) на стороне MTA или почтового сервиса.
Кроме того, пересылка легко ломает SPF, а если список рассылки или ретранслирующий сервер изменяет тело письма или заголовки, ломается и DKIM. Именно поэтому для форм обратной связи безопасно не полагаться только на SPF, обязательно включить DKIM и начинать DMARC с наблюдения в режиме p=none, прежде чем переходить к quarantine / reject.
Кроме того, для писем, отправляемых в Gmail, Google требует настроить на домене отправки SPF или DKIM, при отправке в большом объёме — иметь все три механизма (SPF, DKIM, DMARC), а также требует, чтобы домен в заголовке From: совпадал с доменом, аутентифицированным через SPF или DKIM. Настоящая причина проблем с доставкой обычно кроется не в «коде отправки писем», а в «дизайне отправителя».
Роли SPF, DKIM, DMARC и заголовка From
При доставке почты нужно разделять конверт (envelope) и заголовки. SPF в первую очередь проверяет MAIL FROM в рамках SMTP-сессии — это отправитель на уровне доставки. При финальной доставке обратный путь должен сохраняться как единственный Return-Path, и отправляющая SMTP-система изначально не должна формировать сообщение с уже прописанным заголовком Return-Path. В то же время From: на стороне заголовков сообщения показывает «от чьего имени выглядит письмо», Reply-To: задаёт адрес для ответа, а Sender: обозначает субъекта, который фактически отправил письмо.
RFC 5322 определяет From: как автора сообщения, а Sender: как фактического отправителя. Если автор и отправитель совпадают, Sender: использовать не следует; если нужно указать адрес для ответа, отличный от автора, правильный инструмент — Reply-To:. Кроме того, RFC 5322 прямо указывает, что в From: не должен стоять адрес, не принадлежащий автору. Уведомление с формы обратной связи обычно отправляет не сам пользователь из почтового клиента (MUA), а система сайта, поэтому дизайн, при котором в From: помещают адрес пользователя, легко расходится и со смыслом спецификации.
DKIM ставит подпись на часть заголовков и тело сообщения, а принимающая сторона проверяет её открытым ключом из DNS. Домен подписи указан в d= внутри DKIM-Signature, а открытый ключ ищется по селектору, например selector._domainkey.example.com. DKIM используется как аутентификация, которая «относительно неплохо переживает пересылку», но если тело письма или подписанные заголовки изменяются по пути, проверка хеша тела bh= или самой подписи не проходит.
DMARC смотрит не только на то, прошли ли SPF и DKIM, но и на то, согласуются ли аутентифицированные ими домены с доменом From:. В этом суть. Даже если SPF проходит, но при MAIL FROM=bounces.vendor.net стоит From: contact@example.com, выравнивание (alignment) SPF не проходит. Если DKIM проходит с d=example.com, DMARC может пройти, но если нет и DKIM, DMARC терпит неудачу. В DMARC определены режимы выравнивания strict / relaxed через adkim / aspf, политики p=none|quarantine|reject и адреса для отчётов rua / ruf.
Недавние рекомендации Gmail напрямую превращают эту идею в эксплуатационные требования. Google прямо указывает: «не подделывайте заголовок From:» и «для прямой почты домен в заголовке From: должен совпадать с доменом SPF или доменом DKIM». Уведомление с формы обратной связи — не маркетинговое письмо, но базовая логика фильтров на стороне получателя та же самая, поэтому если игнорировать эту логику, доставляемость снижается.
sequenceDiagram
participant User as Пользователь формы
participant App as Веб-приложение
participant SMTP as Отправляющий MTA / SMTP-сервис
participant DNS as DNS
participant MX as Принимающий MX
User->>App: Отправка формы
App->>App: Определение From / Reply-To / Sender
App->>SMTP: Запрос на отправку по SMTP
SMTP->>SMTP: Наложение подписи DKIM
SMTP->>MX: MAIL FROM / RCPT TO / DATA
MX->>DNS: Запрос SPF (MAIL FROM)
MX->>DNS: Запрос открытого ключа DKIM (selector._domainkey)
MX->>DNS: Запрос DMARC (_dmarc + домен From)
MX->>MX: Определение Alignment
MX-->>App: Доставлено / спам / отклонено / возврат
В этом потоке важно, что критерий оценки DMARC до самого конца остаётся сосредоточен на домене From:. Поскольку From: настраивается на стороне приложения, MAIL FROM — на стороне SMTP, а SPF/DKIM/DMARC — на стороне DNS, и все они трогаются по отдельности, исправление только одного из них не решает проблему недоставки.
Типичные сценарии сбоев в формах обратной связи
Чаще всего встречается случай, когда в From: подставляют адрес пользователя формы. Например, если письмо отправляется через SMTP сайта, а в From: указан taro@gmail.com, то SPF и DKIM обычно проходят для домена сайта, а не для домена Gmail. В результате возникает рассогласование: From: показывает Gmail, а аутентифицированным оказывается домен example.com — и выравнивание DMARC не проходит. Сам Google требует избегать подделки From: и добиваться совпадения From: с доменами SPF/DKIM.
Следующий по частоте случай — SPF ломается при пересылке. При пересылке почты IP-адрес источника, который видит конечный получатель, часто оказывается «ретранслирующим сервером, не указанным в SPF исходного домена отправки», из-за чего SPF не проходит даже для легитимного письма. Google тоже советует: «пересланная почта легко проваливает SPF, поэтому обязательно используйте DKIM». Более того, если ретранслятор изменяет тело письма, добавляет префикс к теме или подпись в конце, ломается и DKIM.
Типична также неполная первоначальная настройка внешнего SMTP-сервиса. В SendGrid бывают случаи, когда отправка невозможна без настройки Domain Authentication; при включении Automated Security генерируются записи аутентификации на базе CNAME, а при необходимости можно настроить Custom Return Path и Custom DKIM Selector. В SES по умолчанию используется MAIL FROM на amazonses.com, и SPF проходит неявно, но если нужно выравнивание SPF с доменом сайта, требуется настроить кастомный MAIL FROM. В Mailgun тоже: пока не настроены SPF/DKIM домена отправки и необходимые записи MX, корректная подпись отправки не формируется.
Локальный MTA на общем хостинге — легко упускаемая из виду мина. Даже если сайт аутентифицирован, если исходящий IP оказывается общим и с плохой репутацией, у него нет записи PTR, или хостинг не настроил DKIM, доставляемость падает. Google требует наличия PTR у исходящего IP и предупреждает, что плохая репутация общего IP может быть причиной ошибок серии 5.7.1.
Способ использования mail() в PHP или сторонних библиотек отправки тоже легко приводит к сбоям. В руководстве по PHP объясняется, что mail() требует заголовок From, а через дополнительный параметр можно указать адрес конверта через sendmail -f. Иными словами, правильный подход — передавать адрес конверта в MTA, а не собирать заголовок Return-Path: вручную. Если это неправильно понять, отправитель, используемый для проверки SPF, разойдётся с отправителем, который предполагает приложение.
Наконец, встречается случай, когда вместо Reply-To начинают возиться с Sender или From в ситуациях, где нужен именно Reply-To. Если единственная цель — направить ответ пользователю, достаточно Reply-To. Sender — заголовок, который используют, чтобы явно показать, что «автор и фактический отправитель различаются»; в форме обратной связи его не применяют постоянно. Если чётко разделять цель дизайна — «хотим направить ответ пользователю» или «хотим показать ответственного за отправку субъекта», — количество сбоев снижается.
Порядок диагностики и команды
Первым делом нужно посмотреть на исходные заголовки письма, прежде чем смотреть код. В Gmail это делается через «Показать оригинал», в Outlook — через «Сведения о сообщении» или «Заголовки интернета»: там можно проверить Authentication-Results, Return-Path, From, Reply-To, DKIM-Signature и Received. Это позволяет довольно точно разграничить, в чём проблема — в том, что письмо «не отправляется», или в том, что «нарушена аутентификация и согласованность».
На что смотреть в заголовках в первую очередь
В первую очередь важны эти пять пунктов:
- Какой домен указан в
From: - Какой домен указан в
Return-Path: - Показывает ли
Authentication-Results:spf=pass/dkim=pass/dmarc=pass - При
dkim=pass— какой домен указан вheader.i=илиd= - При
dmarc=fail— в чём причина: в ошибке аутентификации или в ошибке выравнивания (alignment failure)
В материалах Google по устранению неполадок DMARC тоже прямо указано, что даже если сообщение проходит другие проверки аутентификации, оно всё равно провалит DMARC, если заголовки не выровнены.
Команды для проверки DNS
dig — стандартный инструмент для диагностики DNS: в руководстве по BIND он описан как инструмент DNS-запросов с гибким и понятным выводом. nslookup — более лёгкая команда для проверки, которую можно использовать и в неинтерактивном режиме. При проверке аутентификации формы обратной связи запрашивают как минимум три записи — SPF, DKIM и DMARC.
# SPF
dig +short TXT example.com
# DKIM
dig +short TXT form2026._domainkey.example.com
# DMARC
dig +short TXT _dmarc.example.com
# В Windows
nslookup -type=txt example.com
nslookup -type=txt _dmarc.example.com
nslookup -type=txt form2026._domainkey.example.com
Ожидаемый вид вывода выглядит примерно так.
"v=spf1 include:sendgrid.net include:mailgun.org ip4:203.0.113.10 -all"
"v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ..."
"v=DMARC1; p=none; rua=mailto:dmarc-agg@example.com; ruf=mailto:dmarc-afrf@example.com; adkim=r; aspf=r; fo=1"
Здесь нужно проверить: объединена ли SPF-запись в одну, резолвится ли открытый ключ DKIM и есть ли в DMARC параметр p=. RFC 7208 предписывает ограничивать число механизмов SPF, вызывающих DNS-запросы, суммарно десятью; превышение этого лимита приводит к permerror.
Проверка SMTP-соединения и TLS
openssl s_client — универсальный клиент SSL/TLS из состава OpenSSL, удобный для проверки STARTTLS SMTP-сервера и цепочки сертификатов. Флаг -starttls smtp запускает STARTTLS для SMTP, а -showcerts показывает список сертификатов, возвращённых сервером.
printf 'QUIT\r\n' | openssl s_client \
-connect smtp.example.com:587 \
-starttls smtp \
-servername smtp.example.com \
-showcerts \
-brief
Пример вывода.
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Verification: OK
250 CHUNKING
Так можно как минимум проверить, доступен ли SMTP-сервер, включён ли STARTTLS и нет ли явных аномалий при проверке сертификата. Google требует TLS от отправителей, превышающих определённый объём, и отсутствие TLS может стать причиной ошибки 5.7.29.
Тест с реальной отправкой для воспроизведения проблемы
swaks — специализированный практический инструмент для тестирования SMTP, который гибко воспроизводит отправку, включая TLS, аутентификацию и расширения SMTP. При расследовании недоставки с формы обратной связи эффективно отправить одно письмо «в обход приложения — с тем же SMTP, тем же From, тем же Reply-To и тем же получателем» и воспроизвести проблему.
swaks \
--server smtp.example.com \
--port 587 \
--tls \
--auth LOGIN \
--auth-user contact@example.com \
--auth-password '********' \
--from bounce@example.com \
--to yourtest@gmail.com \
--h-From "Уведомления сайта <contact@example.com>" \
--h-Reply-To "Таро Ямада <visitor@gmail.com>" \
--header "Subject: swaks test" \
--body "This is a test"
Типичный результат при успешной отправке выглядит так.
=== Trying smtp.example.com:587...
=== Connected to smtp.example.com.
<- 250-STARTTLS
<- 250-AUTH LOGIN PLAIN
-> STARTTLS
<- 220 Ready to start TLS
...
<- 250 2.0.0 Ok: queued as ABC123DEF
Если отправка не проходит через приложение, но проходит через swaks, причина, скорее всего, кроется в сборке заголовков библиотекой или в настройке адреса конверта. И наоборот, если и swaks даёт такой же сбой, проблему можно сузить до DNS, SMTP или политики на стороне получателя.
Внешняя диагностика через mail-tester
mail-tester — сервис, который предлагает отправить письмо на случайно сгенерированный тестовый адрес, а затем анализирует само сообщение, отправляющий сервер и IP-адрес отправки, возвращая подробный отчёт. Он хорошо подходит для первичной диагностики случаев «письмо почему-то не доходит» на локальном MTA или общем сервере.
Использование простое.
- Получить тестовый адрес, выданный mail-tester
- Отправить одно письмо тем же путём, что и с формы обратной связи
- Изучить оценку и замечания по SPF / DKIM / DMARC / обратной зоне / чёрным спискам / структуре тела письма
Оценивать боевую доставляемость только по оценке mail-tester нельзя, но с её помощью можно быстро обнаружить такие проблемы, как «SPF вообще не виден», «открытый ключ DKIM не резолвится» или «тело письма или структура отправителя выглядят неестественно».
Примеры разбора заголовков
Пример неудачи.
Return-Path: <bounce-123@vendor.example.net>
Authentication-Results: mx.google.com;
spf=pass (google.com: domain of bounce-123@vendor.example.net designates 198.51.100.10 as permitted sender) smtp.mailfrom=vendor.example.net;
dkim=none;
dmarc=fail (p=quarantine sp=quarantine dis=none) header.from=gmail.com
From: Таро Ямада <visitor@gmail.com>
Reply-To: Таро Ямада <visitor@gmail.com>
Subject: Обращение
Это письмо проходит сам SPF, но проваливает DMARC, потому что From: указывает на gmail.com. Это типичная поломка, возникающая, когда в форме обратной связи в From: подставляют адрес пользователя.
Пример успеха выглядит так.
Return-Path: <bounce@example.com>
Authentication-Results: mx.google.com;
spf=pass smtp.mailfrom=example.com;
dkim=pass header.i=@example.com header.s=form2026;
dmarc=pass header.from=example.com
From: Example Site <contact@example.com>
Reply-To: Таро Ямада <visitor@gmail.com>
Subject: Уведомление об обращении
В таком виде удобство ответа пользователю сохраняется через Reply-To:, а поскольку и видимый, и аутентифицированный отправитель совпадают на example.com, доставляемость становится намного стабильнее.
Рекомендуемые схемы построения From
Незыблемый принцип для формы обратной связи — совместить «домен, используемый для аутентификации» с «доменом From, который видит получатель». При этом адрес для ответа выносится отдельно, в Reply-To:. Дизайн становится устойчивым, если разделить его на три уровня: Sender: — только когда это действительно нужно, а Return-Path задаётся через конверт.
| Схема | Пример заголовков | Когда подходит | Преимущества | На что обратить внимание |
|---|---|---|---|---|
| Рекомендуемая схема | From: contact@example.comReply-To: visitor@gmail.comReturn-Path: bounce@example.com |
Практически для любой формы обратной связи | Легко пройти DMARC / удобно отвечать / просто реализовать | Если забыть Reply-To, ответы будут уходить на сторону сайта |
| Разделение по субдоменам | From: contact@form.example.comReply-To: visitor@gmail.comReturn-Path: bounce.form.example.com |
Когда нужно отделить уведомления формы от основной почты | Легче разделять репутацию / удобнее управлять | Нужно настроить SPF/DKIM/DMARC и на субдомене тоже |
С явным Sender |
From: contact@example.comSender: mailer@example.comReply-To: visitor@gmail.com |
Особые требования, где нужно явно указать отправителя | Показывает субъекта, ответственного за эксплуатацию | Обычно не нужно. Избыточно, если автор и отправитель совпадают |
| Антипаттерн | From: visitor@gmail.comReply-To: visitor@gmail.com |
Реализации, где просто хотят выделить адрес для ответа | Выглядит естественно только внешне | Частая причина сбоя DMARC. Следует избегать в уведомлениях об обращениях |
Основанием для этой таблицы служат семантика From / Sender / Reply-To из RFC 5322, обращение с Return-Path из RFC 5321, а также сама спецификация, согласно которой DMARC оценивает согласованность относительно From:. По умолчанию для уведомлений с формы достаточно «рекомендуемой схемы» из первой строки. Даже когда хочется поместить адрес пользователя в From:, цель достигается, если направить его в Reply-To:.
Особенно важно запомнить, что Return-Path — это не «заголовок, который редактируют», а «результат работы адреса конверта, используемого при доставке». В PHP mail() это делается через -f, а в SMTP-сервисах — через настройки вроде Custom MAIL FROM / Return Path / bounce domain; именно так выглядит правильная реализация.
Руководство по настройке для разных конфигураций
Далее разберём логику настройки для трёх наиболее распространённых на практике конфигураций. Как исходное условие: точные значения для DNS всегда берите из панели управления соответствующего сервиса. Приведённые ниже примеры записей — типовые, чтобы понять структуру.
Если сайт использует внешний SMTP
Самое важное при использовании внешнего SMTP — сначала завершить аутентификацию собственного домена. Google тоже рекомендует: при использовании провайдера почтовых услуг убедиться, что этот сервис аутентифицирует SPF и DKIM вашего домена.
Рекомендуемая конфигурация
From:—contact@example.comилиcontact@form.example.comReply-To:— адрес пользователя формыReturn-Path/ MAIL FROM — субдомен для баунсов, которым вы управляете сами, напримерbounce.example.com- DKIM подписывается доменом
example.comили субдоменом для отправки - DMARC размещается на видимом домене
From:
Типичная настройка SendGrid
SendGrid предполагает Domain Authentication по умолчанию: при включении Automated Security генерируются три записи CNAME. При отключении генерируются один MX и две TXT, а также можно настроить Custom Return Path и Custom DKIM Selector.
; Пример: SendGrid (используйте значения, сгенерированные в панели управления)
em123.example.com. CNAME u123456.wl.sendgrid.net.
s1._domainkey.example.com. CNAME s1.domainkey.u123456.wl.sendgrid.net.
s2._domainkey.example.com. CNAME s2.domainkey.u123456.wl.sendgrid.net.
_dmarc.example.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc-agg@example.com"
Типичная ловушка в SendGrid — состояние «настроена только SMTP-аутентификация, а Domain Authentication не настроена». В таком состоянии отправка как таковая работает, но со стороны получателя связь между From: и аутентифицированным доменом получается слабой. Базовый подход — пройти Domain Authentication, а затем при необходимости настроить Custom Return Path.
Типичная настройка SES
SES по умолчанию использует MAIL FROM на субдомене amazonses.com, поэтому сам SPF проходит неявно. Однако если нужно выравнивание SPF с доменом сайта, используется кастомный MAIL FROM. В этом случае SES требует для домена кастомного MAIL FROM записи SPF TXT и MX, причём MX должна быть ровно одна. Кроме того, Easy DKIM добавляет в DNS три записи CNAME.
; Пример: SES Easy DKIM
abcde12345._domainkey.example.com. CNAME abcde12345.dkim.amazonses.com.
fghij67890._domainkey.example.com. CNAME fghij67890.dkim.amazonses.com.
klmno54321._domainkey.example.com. CNAME klmno54321.dkim.amazonses.com.
; Пример: SES custom MAIL FROM
bounce.example.com. MX 10 feedback-smtp.ap-northeast-1.amazonses.com.
bounce.example.com. TXT "v=spf1 include:amazonses.com -all"
_dmarc.example.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc-agg@example.com"
В дизайне SES важно, что домен для MAIL FROM должен быть отдельным субдоменом для баунсов, а не тем же доменом, что используется в From: для отправки. AWS тоже рекомендует делать MAIL FROM субдоменом, отличным от домена, с которого фактически отправляется почта.
Типичная настройка Mailgun
При верификации домена отправки в Mailgun требуются TXT для SPF и TXT для DKIM, а также добавляются две записи MX. Если SPF уже есть, не нужно добавлять новую запись SPF — вместо этого в существующую запись добавляется include:mailgun.org. Ключей DKIM может отображаться несколько, но отправка возможна, если в DNS корректно опубликован именно тот ключ, который используется сейчас.
; Пример: Mailgun на субдомене
mg.example.com. TXT "v=spf1 include:mailgun.org ~all"
mx._domainkey.mg.example.com. TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ..."
mg.example.com. MX 10 mxa.mailgun.org.
mg.example.com. MX 10 mxb.mailgun.org.
email.mg.example.com. CNAME mailgun.org.
_dmarc.example.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc-agg@example.com"
Mailgun хорошо сочетается с работой через субдомены, поэтому выделение отдельного субдомена для отправки, например mg.example.com, облегчает управление уведомлениями с форм и транзакционными уведомлениями.
Если сайт отправляет через локальный MTA на общем хостинге
На общем хостинге в первую очередь нужно смотреть не на своё приложение, а на качество инфраструктуры отправки на стороне хостинга. Если слабы PTR исходящего IP, поддержка DKIM, репутация общего IP или видимость логов отправки, такая конфигурация уже находится в невыгодном положении. Google тоже придаёт значение PTR исходящего IP и предупреждает, что плохая репутация общего IP может стать причиной блокировки.
На практике безопаснее двигаться в таком порядке.
- Проверить, может ли хостинг-провайдер настроить SPF/DKIM/PTR через панель управления или поддержку
- Обязательно закрепить
From:за собственным доменом - Включить в SPF исходящие IP хостинга или разрешённый домен отправки
- Включить DKIM через функцию хостинга. Если такой функции нет — перейти на внешний SMTP
- По возможности вынести адрес для баунсов MAIL FROM отдельно, например как
bounce.example.com
Типичный пример самостоятельного создания DKIM на общем хостинге — семейство OpenDKIM, где opendkim-genkey позволяет сгенерировать закрытый ключ и TXT-запись для DNS. Структура, при которой открытый ключ DKIM размещается с селектором по адресу selector._domainkey.example.com, соответствует RFC 6376.
mkdir -p /etc/opendkim/keys/example.com
opendkim-genkey -D /etc/opendkim/keys/example.com -d example.com -s form2026
chown opendkim:opendkim /etc/opendkim/keys/example.com/form2026.private
chmod 600 /etc/opendkim/keys/example.com/form2026.private
Вот как выглядит результат после генерации.
form2026._domainkey.example.com. TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ..."
# /etc/opendkim/KeyTable
form2026._domainkey.example.com example.com:form2026:/etc/opendkim/keys/example.com/form2026.private
# /etc/opendkim/SigningTable
*@example.com form2026._domainkey.example.com
Тем не менее, если на общем хостинге нельзя самостоятельно контролировать PTR или исходящий релей, переход на внешний SMTP — более короткий путь. Даже при небольшом объёме отправки, как у уведомлений с формы обратной связи, локальный MTA со слабой аутентификацией оказывается в невыгодном положении перед Gmail и корпоративной почтой.
Если сайт использует PHP mail() или SMTP-библиотеку
PHP mail() удобна, но с точки зрения аутентификации и доставляемости всё зависит от того, какой MTA стоит за ней. В руководстве по PHP объясняется, что письму нужен заголовок From, а при отправке через sendmail_path адрес конверта можно задать через дополнительный параметр. Иными словами, использование mail() само по себе не приводит SPF/DKIM/DMARC в порядок.
Как минимум стоит спроектировать так.
From:—contact@example.comReply-To:— пользователь формы- Адрес конверта —
bounce@example.com - В теле письма также явно указывается адрес пользователя
- По возможности вместо
mail()использовать аутентифицированный SMTP
Минимальный пример конфигурации mail()
Помещать пользовательский ввод в заголовки напрямую опасно. Если в $name или $email попадёт CR/LF, злоумышленник сможет внедрить дополнительные заголовки вроде Bcc:, превратив форму в спам-ретранслятор. Руководство по PHP тоже рекомендует обязательно проверять/нормализовать внешний ввод, используемый в заголовках. В примере ниже все значения, попадающие в заголовки, — включая аргумент адреса конверта (additional_params), — предварительно очищаются (санитизируются).
<?php
// Возвращает только значения, безопасные для использования в заголовках. Отклоняет всё, что содержит CR/LF/NUL.
function sanitize_header_value(string $value): string {
if (preg_match('/[\r\n\0]/', $value)) {
throw new InvalidArgumentException('Invalid characters in header value');
}
return trim($value);
}
// Проверяет адрес электронной почты по RFC.
function sanitize_email(string $email): string {
$clean = sanitize_header_value($email);
if (!filter_var($clean, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email address');
}
return $clean;
}
$to = 'ops@example.com';
$subject = sanitize_header_value('Уведомление об обращении');
// $name / $email / $message — это ввод из формы. $message идёт в тело письма, поэтому там CR/LF допустим,
// а $name / $email используются в заголовках, поэтому CR/LF там нужно всегда отклонять.
$safeName = sanitize_header_value($name);
$safeEmail = sanitize_email($email);
$body = <<<TEXT
Name: {$safeName}
Email: {$safeEmail}
{$message}
TEXT;
$headers = [
'From' => 'Example Site <contact@example.com>',
'Reply-To' => sprintf('%s <%s>', $safeName, $safeEmail),
'Content-Type' => 'text/plain; charset=UTF-8',
];
// additional_params тоже передаётся в shell, поэтому используются только фиксированные значения — динамический ввод подмешивать нельзя.
mail($to, $subject, $body, $headers, '-fbounce@example.com');
У этого примера два ключевых момента. Первый: Return-Path: не прописывается как заголовок — адрес конверта передаётся через -f в пятом аргументе. Второй: пользовательский ввод, попадающий в такие заголовки, как Reply-To:, проходит через функцию санитизации, отклоняющую CR/LF. Если собирать заголовки без санитизации, злоумышленник сможет внедрить строку вроде \r\nBcc: victim@example.com и добавить лишние заголовки, поэтому руководство по PHP тоже считает проверку внешнего ввода, используемого в заголовках, обязательной. Поскольку additional_params в конечном счёте тоже передаётся в shell, используйте только фиксированные значения и не подмешивайте туда пользовательский ввод.
Пример с использованием SMTP-библиотеки
<?php
$mail->isSMTP();
$mail->Host = 'smtp.example.com';
$mail->Port = 587;
$mail->SMTPAuth = true;
$mail->SMTPSecure = 'tls';
$mail->Username = getenv('SMTP_USER');
$mail->Password = getenv('SMTP_PASS');
$mail->setFrom('contact@example.com', 'Example Site');
$mail->addAddress('ops@example.com');
$mail->addReplyTo($email, $name);
// В некоторых библиотеках Sender / return-path можно настроить отдельно
$mail->Sender = 'bounce@example.com';
$mail->Subject = 'Уведомление об обращении';
$mail->Body = $body;
$mail->send();
Преимущество SMTP-библиотеки в том, что отправителя в заголовке и отправителя конверта легко контролировать отдельно друг от друга. Для формы обратной связи такой подход лучше всего подходит дизайну, где From: закреплён за доменом сайта, а в Reply-To: указывается только адрес для ответа.
Конкретные примеры SPF, DKIM и DMARC
Базовый пример SPF
example.com. TXT "v=spf1 ip4:203.0.113.10 include:sendgrid.net -all"
В SPF нужно включить все источники, которые реально отправляют почту. При использовании стороннего отправителя Google тоже требует убедиться, что этот отправитель аутентифицирован через SPF и DKIM. Кроме того, у SPF есть ограничение на число DNS-запросов, поэтому нужно следить, чтобы не накопилось слишком много include.
Базовый пример DKIM
form2026._domainkey.example.com. TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ..."
Селекторы используются для ротации ключей и позволяют держать несколько открытых ключей на одном домене одновременно. На практике вместо default удобнее давать имена, различимые по назначению или дате, — так проще ориентироваться позже.
Примеры внедрения DMARC
Сначала режим наблюдения.
_dmarc.example.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc-agg@example.com; ruf=mailto:dmarc-afrf@example.com; adkim=r; aspf=r; fo=1"
Затем — когда нужно поместить часть писем в карантин.
_dmarc.example.com. TXT "v=DMARC1; p=quarantine; pct=25; rua=mailto:dmarc-agg@example.com; adkim=r; aspf=r"
И наконец — строгий режим.
_dmarc.example.com. TXT "v=DMARC1; p=reject; sp=reject; rua=mailto:dmarc-agg@example.com; adkim=s; aspf=s"
p=none означает только мониторинг, quarantine — рекомендацию помещать в карантин, а reject — рекомендацию отклонять письма прямо во время SMTP-сессии. adkim и aspf переключают режимы strict/relaxed. Вместо того чтобы сразу переходить к reject, безопаснее сначала понаблюдать за объёмом и легитимными источниками отправки на none, а затем повышать политику поэтапно.
Кроме того, если rua / ruf отправляются во внешний сервис агрегации отчётов, RFC 7489 требует дополнительной DNS-записи на стороне третьей стороны. Например, чтобы отправлять отчёты по example.com на thirdparty.example.net, принимающая сторона должна опубликовать запись example.com._report._dmarc.thirdparty.example.net TXT "v=DMARC1".
Контрольный список для диагностики проблем
В завершение соберём порядок проверки, который можно сразу применять на практике. При недоставке писем с формы обратной связи быстрее всего идти по списку сверху вниз.
Что проверить в первую очередь
- Домен ли в
From:собственный - Указан ли адрес пользователя в
Reply-To: - Есть ли в
Authentication-Resultsspf=passилиdkim=pass, и дополнительноdmarc=pass - Если
dmarc=fail— это ошибка аутентификации или ошибка выравнивания - Объединена ли SPF в одну запись
- Не превышено ли число DNS-запросов SPF
- Резолвится ли открытый ключ DKIM
- Есть ли в DMARC параметр
p= - Адекватны ли PTR и обратная зона исходящего IP
- Не используется ли общий IP, и не ухудшилась ли его репутация
Что смотреть в баунсах и логах
Если приходят письма-баунсы, важны поля Final-Recipient, Status, Action и Diagnostic-Code внутри DSN формата message/delivery-status. RFC 3464 определяет эту машиночитаемую информацию о сбое доставки. Например, строка вроде Diagnostic-Code: smtp; 550 relay not permitted означает отказ на стороне SMTP, а не на уровне приложения.
На стороне сервера как минимум смотрят логи доставки MTA. В Postfix отображаются успех и неудача доставки, застревание в очереди, отказ ретрансляции, ошибки разрешения DNS и предупреждения DKIM milter. В Exim ситуация аналогичная. Приведём типичные команды для проверки.
# Пример: Postfix
journalctl -u postfix -n 200 --no-pager
postqueue -p
# Пример: Exim
exim -bp
Пути к логам и права на команды различаются в зависимости от хостинга, поэтому сначала проверьте, можно ли увидеть именно «логи доставки почты», а не «логи приложения». В окружениях, где это недоступно, анализировать проблемы проще, перейдя на внешний SMTP.
Как смотреть в Gmail и Outlook
В Gmail исходные заголовки можно посмотреть через «Показать оригинал», а в Microsoft Outlook — через «Сведения о сообщении» или «Заголовки интернета». При расследовании недоставки писем с формы обратной связи эффективнее сохранять и сравнивать полный текст заголовков, а не скриншоты.
Как распознавать ошибки на стороне получателя
По ошибкам семейства Gmail причину легко определить по коду.
| Пример ошибки | Значение | Основное решение |
|---|---|---|
5.7.27 |
Провал SPF | Добавить источник отправки в запись SPF |
5.7.30 |
Провал DKIM | Исправить ключ DKIM и настройки подписи |
4.7.32 |
Несогласованность From: с организационным доменом SPF/DKIM |
Пересмотреть дизайн From: |
5.7.25 |
Проблема с PTR / обратной зоной | Настроить обратную зону исходящего IP |
В FAQ Google тоже явно указаны эти ошибки и способы их устранения. В формах обратной связи особенно часто встречается 4.7.32 — несовпадение выравнивания.
Финальный критерий оценки
Если одновременно выполняются следующие три условия, дизайн уведомлений с формы обратной связи можно считать надёжным.
From:находится в доменеexample.comReply-To:— адрес пользователя формыAuthentication-Resultsпоказываетdmarc=pass
Если эти три условия соблюдены, дизайн логичен независимо от того, используется ли SendGrid, SES, Mailgun, общий хостинг или SMTP-библиотека. И наоборот, если не хватает хотя бы одного пункта, быстрее всего начать с проверки дизайна From:.
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
10 главных угроз информационной безопасности 2026 — как читать рейтинг и от чего малому и среднему бизнесу действительно стоит защищаться
В «10 главных угрозах информационной безопасности 2026» IPA атаки программ-вымогателей заняли 1-е место 11-й год подряд, атаки на цепочку...
Чтобы не забыть решить, «за сколько секунд это должно работать» — упорядочиваем нефункциональные требования с помощью «Градации нефункциональных требований» IPA
Причина многих споров вроде «слишком медленно» или «мы не ожидали такого поведения при сбое» — забытые нефункциональные требования. Разби...
Что стоит знать и заказчику сайта — используем «Руководство по созданию безопасного веб-сайта» IPA как чек-лист
По какому критерию проверять безопасность корпоративного сайта? Разбираем 11 уязвимостей и мер противодействия из «Руководства по создани...
С чего начать защиту информации малому и среднему бизнесу — как пользоваться 4-й редакцией «Руководства IPA по информационной безопасности для МСБ»
С чего малому и среднему бизнесу начинать меры по информационной безопасности? На основе 4-й редакции «Руководства по мерам информационно...
Лучшие практики проектирования чат-ботов, которые действительно полезны в бизнесе
Чтобы чат-бот приносил реальную пользу в работе, прежде выбора модели нужно определить назначение, источники знаний, права доступа, услов...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка веб-сайтов
Проектирование формы обратной связи, работа с отправителем уведомлений и настройка Reply-To как часть воронки обращений — тема, которую удобно прорабатывать вместе с созданием сайта.
Технические консультации и ревью дизайна
Выбор между SPF / DKIM / DMARC, внешними SMTP-сервисами (SendGrid / SES / Mailgun), общим хостингом и PHP mail() удобно прорабатывать как ревью архитектуры под текущую конфигурацию и требования.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- В чём самая частая причина недоставки писем с формы обратной связи?
- Дело не в самом SMTP-соединении, а в том, что видимый отправитель (From:) не совпадает с отправителем, который фактически проходит аутентификацию (MAIL FROM в SPF / d= в DKIM). Особенно часто встречается дизайн, при котором в From: подставляют Gmail-адрес пользователя формы напрямую. Если письмо отправляется через SMTP сайта, а в From: указано taro@gmail.com, аутентификацию проходит домен сайта, а From: показывает Gmail — возникает рассогласование, и аутентификация DMARC не проходит.
- Как правильно настроить From у уведомлений с формы обратной связи?
- Базовый принцип — закрепить From: за доменом собственного сайта, а адрес пользователя формы указывать в Reply-To:. Это сохраняет удобство ответа и одновременно выравнивает видимого и аутентифицированного отправителя по одному домену, благодаря чему доставляемость стабилизируется. Sender: используется только тогда, когда автор письма и фактический отправитель различаются, а Return-Path не прописывается вручную в заголовке, а задаётся как адрес конверта на стороне MTA или почтового сервиса.
- С чего начать расследование недоставки почты?
- Прежде чем смотреть код, нужно посмотреть на исходные заголовки полученного письма. В Gmail это делается через «Показать оригинал»: проверяются Authentication-Results, Return-Path, From и DKIM-Signature. В первую очередь смотрят на домен в From:, домен в Return-Path: и на то, проходят ли spf / dkim / dmarc проверку (pass). При dmarc=fail нужно разобраться, это ошибка аутентификации или ошибка выравнивания (alignment failure). На стороне DNS проверяют три записи — SPF, DKIM и DMARC — с помощью dig или nslookup.
- Можно ли сразу настроить DMARC на reject?
- Безопаснее не переключаться сразу на reject, а сначала включить режим наблюдения p=none, оценить объём трафика и легитимные источники отправки, а затем поэтапно повышать политику до quarantine и reject. p=none означает только мониторинг, quarantine — рекомендацию помещать письма в карантин, а reject — рекомендацию отклонять их прямо во время SMTP-сессии. Кроме того, важно не полагаться только на SPF, а обязательно включить DKIM, поскольку пересылка легко ломает SPF, а изменение тела письма или заголовков при ретрансляции ломает и DKIM.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки