Как грамотно отправлять почту из скриптов (в частности — на PHP)

    Первая часть текста взята из инструкции хостинг-провайдера 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/mhtml-test/mhtml.html тут есть несколько примеров.
    Share post

    Comments 76

      0
      Оригинал первой части текста тут: http://www.netangels.ru/support/articles…
        +4
        Google RFC не писан, как оказалось. При разделении заголовков символом CRLF (что соответствует RFC 822) следующая строка считается уже телом письма, т.к. Google и CR и LF воспринимает как перевод строки. В итоге при прохождении письма через smtp.gmail.com получаем кашу из заголовков, текста и b64. Поэтому приходится использовать LF специально для GMail =)
          0
          Наверное вы используете postfix и просто не умеете с ним работать. У google всё в порядке.
            0
            Нет, используется ssmtp и у остальных серверов все в порядке. Можете поставить эксперимент и отправить почту вручную через smtp.gmail.com:465
          +4
          «Q-кодирование» правильно называется «quoted printable», теперь можете найти эту функцию в PHP, она есть в модуле IMAP, но можно написать и свою, на php.net в комментариях есть несложный пример.
            0
            Оп-па, спасибо! Я как раз думал, как передать нормально заголовок письма из формы на сайте.
              0
              Спасибо, очень познавательно..
                0
                Ну за что? За что? Я был так рад прочитать это.. В обозримом будущем собирался писать такой скрипт.. А вы... Эх вы..:(
                +5
                Фигней не занимайтесь. PHPMailer
                  0
                    +2
                    По-моему понимать, как оно работает - лишним быть не может. :)
                      –14
                      Заипал спам на хабре. RTFM
                        0
                        Вы, собственно, о чем?
                          –10
                          Спасибо за минусы, подрачил :D
                        0
                        О! спасибо, использую, радуюсь!
                          0
                          +3
                          Чтобы грамотно отправлять почту из скриптов надо взять готовый отлаженный класс и не изобретать велосипед.
                            +1
                            По-моему понимать, как оно работает - лишним быть не может. :)
                              0
                              Нифига ты не понимаешь в изоюретении велосипедов.
                              Меня реально не устраивает ни один скрипт. при том что я вообще не программист, я написал свой по мотивам нескольких. Задал проверку полей, выучился маленько регэкспам для распознавания теоретически верного и очень сложного мыла, и то не идеально (сейчас переделываю). Готовые и отлаженные пригодны для безошибочной отправки конкретного типа сообщения. А мне нужно, чтобы совсем разные по формату сообщения проходили.
                                0
                                Готовый скрипт - это прекрасно, но все таки полезно знать то, с чем имеешь дело. Меньше подводных камней будет.
                                  0
                                  В этом проявляется разница между веб-мастером и программистом.
                                    0
                                    Изумительно, абзацем выше за те же слова только в другой форме был заминусован хьюиТрикс :)
                                      0
                                      а что изумительного? он сам выбрал эту "другую" форму, вот и "напросился"
                                        0
                                        Посмотрите внимательно, "за те же слова" он получил +5
                                        0
                                        +1
                                        В примерах используется прямая вставка из $_POST, но не описано что такое mail-инъекция и способы борьбы с ней. ИМХО, это гораздо важнее для "грамотного отправления почты", чем указание субтипов.
                                          0
                                          задача стояла другая. совсем. :)
                                            0
                                            тогда стоило указывать просто переменные $subject, $to, etc.
                                            в показательных примерах не стоит делать привязки )
                                            0
                                            Расскажите лучше про mail-инекцию.
                                              0
                                              Всё дело в том, что переменная $_POST["subject"] никак не фильтруется, из-за этого можно сформировать свой заголовок и тело письма.
                                                0
                                                Это я понимаю. То потенциально есть возможность выполнить свой код на сервере?
                                                  0
                                                  На сервере нельзя, но можно в _POST['subject'] отправить строку "subject\ncc:много, много email-адресов". В итоге при простой подстановке данной строки в заголовки, получим:

                                                  Subject: subject
                                                  cc: [много, много адресов]

                                                  В итоге от имени вашего сервера на все эти адреса будут отосланы сообщения с содержимым, которое обычно так же вводит клиент в браузере.
                                                    0
                                                    И будет спам... Спасибо за разъяснения.
                                                      +1
                                                      Ну уж, ну уж!
                                                      base64_encode закодирует в буквы и перевод строки, так что ничего подобного не случится.
                                                        –1
                                                        Сам собой закодирует?
                                                          +1
                                                          Не понял вопроса. Конечно, сам собой.
                                                          base64_encode("subject\ncc:много, много email-адресов")
                                                          даёт в результате строку
                                                          c3ViamVjdApjYzrNzs/Hzywgzc7Px88gZW1haWwtwcTSxdPP1w==
                                                          В результате подстановки данной строки в заголовки ровным счётом ничего плохого не случится.
                                                            –1
                                                            Так откуда этот самый base64_encode возьмется сам собой?
                                                              0
                                                              А он собственно есть в примерах, про которые Вы говорили:
                                                              "В примерах используется прямая вставка из $_POST, но не описано что такое mail-инъекция и способы борьбы с ней. ИМХО, это гораздо важнее для "грамотного отправления почты", чем указание субтипов." :)
                                                                0
                                                                Во-первых, повторюсь, я про это говорил не только для этих примерах, а вообще, о том, что неплохо бы сказать о такой проблеме.
                                                                Во-вторых, $_POST['email'] вставляется в примере напрямую.
                                                                +1
                                                                ??? Я не знаю, что вы подразумеваете по "сам собой", но он есть в тот самом примере, на который вы сослались.
                                                                $subject = "=?windows-1251?b?" . base64_encode($_POST["subject"]) . "?=";
                                                                  0
                                                                  Чуть выше
                                                      0
                                                      «Version 5.2.2
                                                      03-May-2007
                                                      Fixed a header injection via Subject and To parameters to the mail() function (MOPB-34 by Stefan Esser) (Ilia)»
                                                    0
                                                    А тебя не смутило то что нет открывающего
                                                      –2
                                                      А тебя не смутило отсутствие откывающего оператора <?
                                                      или например отсутствие конфига какого-нибудь? который по-любому там где-нибудь в начале скрипта по-любому подключается, или например название файла
                                                      или редактора в котором писался текст или ещё чего-нибудь абсолютно неуместного ? Нет ?
                                                      Этот код ДЛЯ НАГЛЯДНОСТИ !!!
                                                      Извини конечно, но ИМХО зря такие комментарии делаются, просто ни к чему.
                                                      P.S. Ramm, я не на твоём месте, но будья на твоём месте я бы не распылясля
                                                      отвечать на подобные вещи, не напряшайся, эти каменты
                                                      на мой взгляд - просто желание вставить свои 5 копеек. А за статью большое спасибо.
                                                        0
                                                        Остынь, гражданин, и не нервничай.
                                                        Потом подавляющее число читателей берет подобный код "для наглядности" и вставляет не думая.
                                                        И говорил я не именно про какой-то код, а про то, что отслеживание подобных вещей напрямую относится к вопросу о том "как грамотно отправлять почту из скриптов".
                                                          0
                                                          Если кто-то не думая вставит такой код к себе, то это его проблемы, я лишь хотел сказать что не зачем цепляться за такие моменты, потому как цель другая у него (куска кода) другая. А если вот так цепляться к автору за такие моменты, то можно тысячу раз уйти в сторону от темы основной темы.
                                                            0
                                                            Успокойтесь, уже запинаетесь.
                                                            К чему вас вдруг понесло? Я написал один комментарий, а с вашей помощью развели уже на страницу.
                                                      +4
                                                      Понимание RFC лишним не бывает, однако, решать тривиальные задачи самостоятельно - не стоит. Есть масса готовых библиотек: PHPMailer, ezComponents::Mail, PEAR::Mail_Mime.
                                                        0
                                                        + сюда же Zend::Mail
                                                          0
                                                          Баги в библиотеках исправлять всё равно придётся.
                                                            0
                                                            иногда проще изобрести велосипед:) особенно с такими хостингами как Агава и РБК, сейчас с ними к счастью не общаюсь, но не забуду как в письма через Ж вставлялись теги антиспама. Агава исправила за неделю, РБК так и не исправила(последний опыт общения июль этого года)
                                                              0
                                                              О, да, тут я полностью согласен! Взять хотя бы ручное закачивание PEAR-библиотек, отсутствие необходимых модулей PHP или невозможность собрать собственный интерпретатор. Хостинги иногда творят совершенно невообразимые вещи. Я для себя выбрал мастерхост - пусть, иногда поддержка там как в мультике - "выглядят они не слишком привлекательно, но ничего". :) Зато, ещё ни разу не упирался в нерешаемые проблемы с хостингом.
                                                            0
                                                            Согласен с мнением общественности...
                                                            Метод кодирования заголовков пожалуй стоит запомнить, но остальным пусть занимаются библиотеки... Уж если надо письма с вложениями отправлять - вникать в это просто некогда.
                                                              0
                                                              Хотя в студенческие времена - это было так увлекательно... )
                                                                +1
                                                                С мнением общественности не согласен. Если так думать, то кто будет писать и поддерживать библиотеки? Хотя, кто-то правильно выше заметил, что это и есть разница между веб-мастером и программистом. Я так понимаю, на Хабре все-таки большинство веб мастеров.
                                                                  +1
                                                                  Немного не в кассу, но мне кажется, многие библиотеки уже давно пишутся с оглядкой на другие аналогичные библиотеки, потому как сервисы, для которых эти библиотеки пишутся сами по себе реализовываются с отклонениями от RFC. И первично бывает уже не знание RFC, а знание нюансов реализаций этого самого RFC в том или ином случае. :)

                                                                  Взять тот же самый ezComponents::Mail. По-умолчанию и, вроде как, по стандарту, разделитель заголовков - "\r\n". В реальности, нужно использовать "\n", иначе половина почтовых служб отображает письмо как Б-г на душу положит. :)
                                                                    0
                                                                    какая реальность, если клиент не понимает \r\n (правильно по RFC), то это упущение его разработчиков. И если ты собираешь письмо вручную, будь добр делать по RFC.
                                                                    Ну а если ты разработчик клиента для чтения почты, то тебе нужно учитывать тот факт, что разделение может быть и \r\n и \n и собственно просто \r (как у Маков кажись старых).
                                                                      0
                                                                      Если я делаю сайт, мне в некоторых случаях важно не соответствие стандарту, а работоспособность. В частности, gmail плохо ведёт себя с '\r\n' - показывает лишние переносы строк. По моему опыту (точнее, результатам проверки в популярных программах-клиентах, и на почтовых сервисах большинства подписчиков на http://www.mixfight.ru), '\n' понимается в большем числе случаев, чем '\r\n', так что я буду использовать это разделение вопреки стандарту, пусть это и не лучшее решение.
                                                                +1
                                                                10x a lot! Для меня хабра уже превращается в справочник по вебу, и вы только что внесли отличный вклад в его наполнение! :))
                                                                  0
                                                                  спасибо. есть несколько вещей, на поиск которых пришлось бы потратить некоторое время. теперь они есть тут.
                                                                    +1
                                                                    Пропущен весьма существенный заголовок:

                                                                    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, что никак не может быть признано хорошей рекомендаций в нынешних реалиях. В качестве примера тот же хабр, как люди мучаются с вводом символов за пределами этой кодировки.
                                                                      0
                                                                      По-моему, это вещи уровня параметра boundary, о котором я написал.
                                                                      Т.е. это такие вещи, проблемы с которыми приведут к очевидным проблемам еще на этапе тестирования - разработчик заметит и займется.

                                                                      Описанные мной проблемы могут скрываться очень долго. Особенно если и программист и тот, кото принимает его работу пользуются чем-то вроде The Bat!, который при отображении писем вообще на RFC с высокой колокольни плюет.

                                                                      И будут проблемы типа "а чо, у меня-то работает все как надо, это у вас кривой клиент, ставьте The Bat!"
                                                                        0
                                                                        Ох, проблемы приведут к проблемам.
                                                                        Мда, кажется я заговариваюсь. Извините. :)
                                                                          0
                                                                          аха, заговариваетесь ;)
                                                                          не должны содержаться символы, не присутствующие в ASCII таблице - латинские буквы, цифры, знаки пунктуации и псевдографики.
                                                                          кажется, вы здесь что-то напутали )

                                                                          а за статью большое спасибо
                                                                          только не хватает принципов формирования частей письма, назначения boundary и т.п.
                                                                            0
                                                                            На самом деле, статья писалась для работы, когда выяснилось, что подавляющее большинство наших программистов правильно сформировать письмо не могут.

                                                                            Про boundary они знают.

                                                                            Впрочем, возможно Вы правы - в следующий раз буду дописывать в подобные материалы базовые вещи.
                                                                      +1
                                                                      есть также полезная функция mb_encode_mimeheader (http://php.net/manual/en/function.mb-encode-mimeheader.php) которая выполняет пример из статьи одним махом.

                                                                      по поводу готовых классов: порой задача очень проста (форма контактов на простеньком сайте) и внедрять туда код размером в 10kb нет желания, достаточно просто функции mail(), но и ее надо использовать грамотно, в чем эта статья и помогает

                                                                      кстати, если отправлять письмо с Content-type: text/html, желательно не забывать про теги , многие фильтры дают за их отсутствие много спам-баллов
                                                                        0
                                                                        Вы, вероятно, имели в виду html, title и body? :)
                                                                          0
                                                                          именно, даже не заметил что фильтр их потер, хоть досточно html, body
                                                                        –1
                                                                        Pear/Mail
                                                                        http://pear.php.net/package/Mail_Mime
                                                                          0
                                                                          Вы не поверите, но на этот пакет даже ссылка в тексте есть. Зачем Ваш комментарий?
                                                                            0
                                                                            Случайно не туда нажал на отправить а потом забыл дописать. Хотел дополнительно прислать ссылку на дискуссию с моим участием на сайте pear http://pear.php.net/bugs/bug.php?id=30 а проблемах и способах устранения некоторых недочетов в pear/mail
                                                                              0
                                                                              Кстати, если вы пишете от 2045 и далее, желательно написать что это далее заканчивается 2049 :-)
                                                                              0
                                                                                0
                                                                                php composer.phar require swiftmailer/swiftmailer @stable
                                                                              • UFO just landed and posted this here

                                                                                Only users with full accounts can post comments. Log in, please.