Первая часть текста взята из инструкции хостинг-провайдера Netangels. Вторая - авторская.
Отправка почты из скриптов на PHP - вещь, которая очень часто встречается в веб-приложениях. К сожалению, как показывает практика, большинство разработчиков используют эту функцию неправильно, допуская в своих скриптах одни и те же ошибки. В результате оказывается, что письмо получателю пришло в неверной кодировке, просто не дошло, или дошло, но отображается совсем не так, как этого хотел автор.
Для того, чтобы быть уверенным, что ваше сообщение отправляется действительно верно, необходимо иметь по меньшей мере базовые представления о формате почтового сообщения. Формат почтового сообщения описан в нескольких стандартизирующих документах, основными из которых являются
RFC 822 (описывает формат передачи простого текста на английском языке) и
RFC 2045 и далее (описывает расширения этого формата для передачи произвольных данных).
Формат почтового сообщения
Ниже приведен самый простой пример текстового сообщения, составленного в соответствии с приведенными выше стандартами и готового к отправке.
From: =?windows-1251?b?0J7RgtC/0YDQsNCy0LjRgtC10LvRjD89?= <putin@kremlin.ru>
To: =?windows-1251?b?0J/QvtC70YPRh9Cw0YLQtdC70Yw/PQ==?= <info@netangels.ru>
Subject: =?windows-1251?b?0Y3RgtC+INGC0LXQvNCwINGB0L7QvtCx0YnQtdC90LjRjz89?=
Content-Type: text/plain; charset="windows-1251"
Content-Transfer-Encoding: 8bit
Это почтовое сообщение на русском языке
Содержит несколько строк
Именно в таком формате клиент для отправки почты (MS Outlook или Mozilla Thunderbird) подготавливает сообщение, а затем отправляет его получателю (кстати, большинство почтовых клиентов позволяют просмотреть исходный код сообщения, в Mozilla Thunderbird, например, для этого служит комбинация клавиш Ctrl+U). Задача нашего скрипта языке PHP - добиться точно такого же формата письма.
Как видно из приведенного выше примера, электронное письмо содержит две части: в одной (верхней) размещаются заголовки, а в другой (нижней) собствено текст письма. Отделены эти части друг от друга пустой строкой. Заголовки состоят из строк, в которых содержится тема письма (Subject), имя и адрес отправителя (From), получателя (To) и другая информация. В самом простом случае каждая строка содержит пару "ИмяЗаголовка: ЗначениеЗаголовка". Особенно необходимо подчеркнуть, что, согласно стандартам, в заголовках ни при каких обстоятельствах не должны содержаться символы, не присутствующие в ASCII таблице - латинские буквы, цифры, знаки пунктуации и псевдографики.
Грамотное использование русских символов в заголовках почтового сообщения
Итак, в явном виде русский текст в заголовке присутствовать не должен, поэтому для того, чтобы включить его туда, этот текст предварительно нужно закодировать. Стандарты описывают способ кодирования "запрещенных" символов. Общий формат выглядит так:
=?кодировка?способ кодирования?закодированный текст?=
Кодировка может быть любой из списка "windows-1251", "koi8-r", "utf-8" и т.д. Во всех случаях, как правило, кодировка сообщения будет совпадать с кодировкой в которой работает сайт. То есть в большинстве случаев это будет "windows-1251", реже - "utf-8".
Способ кодирования указывает на то, каким именно образом русские символы будут преобразованы в безопасный набор. Способа определяется два: так называемый "Q-encoding" (обозначается одной буквой "Q") и "Base64" (обозначается одной буквой "B").
К сожалению, штатной функции, которая бы могла бы обычную строку преобразовать в Q-encoded текст, в PHP нет, зато есть функция, которая умеет выполнять аналогичное преобразование в Base64. Итак, PHP код правильного создания заголовка темы почтового сообщения может выглядеть следующим образом:
$subject = "=?windows-1251?b?" . base64_encode($_POST["subject"]) . "?=";
Здесь предполагается, что в переменной $_POST["subject"] у вас содержится тема почтового сообщения, записанная по-русски в кодировке windows-1251.
Адрес отправителя или получателя может быть записан в виде "
user@example.com" или в виде "Имя пользователя <
user@example.com>". Во втором случае имя пользователя необходимо преобразовать так же, как в предыдущем примере. Ниже приведен пример, в котором предполагается, что в переменной $_POST["username"] содержится имя пользователя, а в переменной $_POST["email"] его электронный адрес:
$sender = "=?windows-1251?B?" . base64_encode($_POST["username"]) . "?= <" . $_POST["email"] . ">";
Content-type: multipart/???
С этим заголовком знаком любой разработчик, которому доводилось решать проблемы отправки писем с вложениями или HTML письмами. И зачастую письма, сформированные без использования библиотек вроде
PEAR::Mail_mime отображаются не очень корректно. Практика показывает, что если при формировании письма жестко придерживаться стандарта, которы задается в RFC (в частности -
RFC 2046) - подавляющее большинство клиентских программ (включая таких любителей придерживаться стандартов, как Mozilla Thunderbird) отображает письмо корректно. Далее мы будем исходить из того, что читатель этого документа представляет себе основной синтаксис команд и понимает, что таке boundary и почему необходимо указывать Content-type для каждой из частей письма. Постараемся отметить основные ошибки.
Ошибка первая - неверный subtype
Тип multipart имеет три субтипа - mixed, alternative и related, которые используются синтаксически одинаково, но имеют разное предназначение
- mixed - используется, когда в рамках одного почтового сообщения имеется несколько независимых друг от друга, и равнозначных частей. Самый простой пример такого письма - сообщение с вложением.
- alternative - используется, когда в одном почтовом сообщении содержится несколько частей, содержащих одну и ту же информацию, предназначенную для отображения на различном клиентском ПО - например текстовая и HTML версия одного и того же письма.
- related - используется, когда в одном почтовом сообщении содержится несколько частей, формирующих один итоговый документ. Яркий пример - HTML письмо с картинками. Запомните, по стандарту только в этом случае должны работать ссылки на Contend-id элементов (вида <img src="cid:image">).
Помните и применяйте по назначению.
Ошибка вторая - неверный порядок частей
Порядок частей, в котором они указаны в письме, зачастую имеет ключевое значения для того, как будет отображаться сообщение у клиента.
- mixed - порядок частей для наших задач не имеет значения.
- alternative - части должны быть расставлены по порядку, от более простых к более сложным. RFC регламентирует процесс выбора одной из версий письма клиентом пользователя примерно так: "В общем случае, почтовый клиент должен отображать последнюю доступную ему версию документа". Т.е. при формировании текстовой и HTML-версий письма необходимо вперед поставить текстовую.
- related - первой в очереди должна идти основная часть (HTML документ, например). Следом - все остальные. По большому счету, стандартом регламентирован специальный параметр "start", который указывает на основную часть документа, но этим лучше не злоупотреблять.
Ошибка третья - выбор только одного субтипа
Зачастую разработчик, формирующий из программы письмо забывает, что любая из частей письма может так же иметь Content-type: multipart, а значит можно выстроить некоторое подобие древовидной структуры, гарантирующей, что каждая из частей письма займет правильное место. Вот как примерно может выглядеть структура письма, имеющего текстовую и HTML версию (HTML с картинками), а так же приложенный документ MS Word:
- Content-type: multipart/mixed
- Content-type: multipart/alternative
- Content-type: text/plain
- Content-type: multipart/related
- Content-type: text/html
- Content-type: image/jpeg
- Content-type: image/jpeg
- Content-type: application/msword
И напоследок - еще пара рекомендаций
- Всегда делайте text/plain вариант письма - никто не может предсказать, как именно будут читать Ваше письмо.
- Не ленитесь и придерживайтесь стандартов.
- Если интересно - http://people.dsv.su.se/~jpalme/ietf/mht… тут есть несколько примеров.
комментарии (72)
Меня реально не устраивает ни один скрипт. при том что я вообще не программист, я написал свой по мотивам нескольких. Задал проверку полей, выучился маленько регэкспам для распознавания теоретически верного и очень сложного мыла, и то не идеально (сейчас переделываю). Готовые и отлаженные пригодны для безошибочной отправки конкретного типа сообщения. А мне нужно, чтобы совсем разные по формату сообщения проходили.
в показательных примерах не стоит делать привязки )
В итоге от имени вашего сервера на все эти адреса будут отосланы сообщения с содержимым, которое обычно так же вводит клиент в браузере.
base64_encode закодирует в буквы и перевод строки, так что ничего подобного не случится.
base64_encode("subject\ncc:много, много email-адресов")
даёт в результате строку
c3ViamVjdApjYzrNzs/Hzywgzc7Px88gZW1haWwtwcTSxdPP1w==
В результате подстановки данной строки в заголовки ровным счётом ничего плохого не случится.
"В примерах используется прямая вставка из $_POST, но не описано что такое mail-инъекция и способы борьбы с ней. ИМХО, это гораздо важнее для "грамотного отправления почты", чем указание субтипов." :)
Во-вторых, $_POST['email'] вставляется в примере напрямую.
$subject = "=?windows-1251?b?" . base64_encode($_POST["subject"]) . "?=";
03-May-2007
Fixed a header injection via Subject and To parameters to the mail() function (MOPB-34 by Stefan Esser) (Ilia)»
или например отсутствие конфига какого-нибудь? который по-любому там где-нибудь в начале скрипта по-любому подключается, или например название файла
или редактора в котором писался текст или ещё чего-нибудь абсолютно неуместного ? Нет ?
Этот код ДЛЯ НАГЛЯДНОСТИ !!!
Извини конечно, но ИМХО зря такие комментарии делаются, просто ни к чему.
P.S. Ramm, я не на твоём месте, но будья на твоём месте я бы не распылясля
отвечать на подобные вещи, не напряшайся, эти каменты
на мой взгляд - просто желание вставить свои 5 копеек. А за статью большое спасибо.
Потом подавляющее число читателей берет подобный код "для наглядности" и вставляет не думая.
И говорил я не именно про какой-то код, а про то, что отслеживание подобных вещей напрямую относится к вопросу о том "как грамотно отправлять почту из скриптов".
К чему вас вдруг понесло? Я написал один комментарий, а с вашей помощью развели уже на страницу.
Метод кодирования заголовков пожалуй стоит запомнить, но остальным пусть занимаются библиотеки... Уж если надо письма с вложениями отправлять - вникать в это просто некогда.
Взять тот же самый ezComponents::Mail. По-умолчанию и, вроде как, по стандарту, разделитель заголовков - "\r\n". В реальности, нужно использовать "\n", иначе половина почтовых служб отображает письмо как Б-г на душу положит. :)
Ну а если ты разработчик клиента для чтения почты, то тебе нужно учитывать тот факт, что разделение может быть и \r\n и \n и собственно просто \r (как у Маков кажись старых).
MIME-Version: 1.0
Без него получающая система не будет обязана интерпретировать ни квотед-принтабл, ни указание на чарсет в заголовках. Кроме того, не провентилированным остался вопрос, как именно мы отправляем почту:
1) вызываем /usr/sbin/sendmail,
2) пытаемтся послать на порт 25/tcp на локальный хост,
3) пытаемтся послать на смартхост, где-то описанный,
4) или может (о ужас), самостоятельно ищем MXы.
Во всех случаях технология может несколько разниться, особенно в части формирования заголовков. В частности, в первом лучше передавать их в команде:
$stat = mail (null, 'тема сообщения', 'тело сообщения',
"To: Адресат addressee@email.address>\r\nFrom: Отправитель sender@email.address>", '-t');
(русские слова там для примера, должно быть или ASCII, или майм-кодирование)
Кроме того, статья предлагает использовать однобайтную кодировку CP-1251, что никак не может быть признано хорошей рекомендаций в нынешних реалиях. В качестве примера тот же хабр, как люди мучаются с вводом символов за пределами этой кодировки.
Т.е. это такие вещи, проблемы с которыми приведут к очевидным проблемам еще на этапе тестирования - разработчик заметит и займется.
Описанные мной проблемы могут скрываться очень долго. Особенно если и программист и тот, кото принимает его работу пользуются чем-то вроде The Bat!, который при отображении писем вообще на RFC с высокой колокольни плюет.
И будут проблемы типа "а чо, у меня-то работает все как надо, это у вас кривой клиент, ставьте The Bat!"
Мда, кажется я заговариваюсь. Извините. :)
кажется, вы здесь что-то напутали )
а за статью большое спасибо
только не хватает принципов формирования частей письма, назначения boundary и т.п.
Про boundary они знают.
Впрочем, возможно Вы правы - в следующий раз буду дописывать в подобные материалы базовые вещи.
по поводу готовых классов: порой задача очень проста (форма контактов на простеньком сайте) и внедрять туда код размером в 10kb нет желания, достаточно просто функции mail(), но и ее надо использовать грамотно, в чем эта статья и помогает
кстати, если отправлять письмо с Content-type: text/html, желательно не забывать про теги , многие фильтры дают за их отсутствие много спам-баллов
http://pear.php.net/package/Mail_Mime