Отправка почты на PHP: как написать правильно

Отправка почты из скриптов на PHP - вещь, которая очень часто встречается в веб-приложениях. К сожалению, как показывает практика, большинство разработчиков используют эту функцию неправильно, допуская в своих скриптах одни и те же ошибки. В результате оказывается, что письмо получателю пришло в неверной кодировке, просто не дошло, или вообще неумело написанная форма отправки сообщения была использована для рассылки спама.

Формат почтового сообщения

Для того, чтобы быть уверенным, что ваше сообщение отправляется действительно верно, необходимо иметь по меньшей мере базовые представления о формате почтового сообщения. Формат почтового сообщения описан в нескольких стандартизирующих документах, основными из которых являются RFC 822 (описывает формат передачи простого текста на английском языке http://ietf.org/rfc/rfc822.txt) и RFC 2045 (описывает расширения этого формата для передачи произвольных данных http://ietf.org/rfc/rfc2045.txt).

Ниже приведен самый простой пример текстового сообщения, составленного в соответствии с приведенными выше стандартами и готового к отправке.

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) и другая информация. В самом простом случае каждая строка содержит пару "ИмяЗаголовка: ЗначениеЗаголовка". Особенно необходимо подчеркнуть, что, согласно стандартам, в заголовках ни при каких обстоятельствах не должны содержаться русские символы.

Грамотное использование русских символов в заголовках почтового сообщения

Итак, в явном виде русский текст в заголовке присутствовать не должен, поэтому для того, чтобы включить его туда, этот текст предварительно нужно закодировать. Стандарты описывают способ кодирования "запрещенных" символов. Общий формат выглядит так:

=?кодировка?способ кодирования?закодированный текст?=

Кодировка может быть любой из списка "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"] . ">";

Функция mail()

Отправка почтовых сообщений в большинстве скриптов выполняется системной функцией mail(). Описание синтаксиса функции mail всегда можно найти в оригинальном руководстве. Эта функция принимает всего 5 параметров, из которых три являются обязательными, а остальные два - опциональными.

На самом деле функция mail() просто формирует строку следующего вида:

$additional_headers
To: $to
Subject: $subject

$message

а затем передает ее на обработку стандартной UNIX'овой команде отправки почты, которая носит имя sendmail. Сравнивая то, что выдает mail() с тем, что должно получиться (см. пример выше), можно составить точный план подготовки переменных для передачи в функцию:

  1. Закодировать поле Subject с использованием base64, как в примере выше.
  2. Закодировать поле To.
  3. В случае необходимости создать $additional_headers, самостоятельно сформировав все необходимые заголовки (например, добавив поле "From:") и разделяя эти заголовки парой символов "\r\n".

После этого сформирвоанный набор параметров можно передавать на вход функции mail().

Работа над ошибками с помощью additional_parameters

Как уже упоминалось выше, функция mail() для отправки почты просто вызывает стандартную команду sendmail, при необходимости передавая ей дополнительные параметры. В каком случае может возникнуть необходимость их использования? Самый часто встречающийся случай - проблема с адресом отправителя. Этот адрес нередко изменяется функцией sendmail на адрес вида "имя_пользователя_на_сервере_хостинга@адрес_сервера_хостинга.ru". Чтобы избежать такого поведения, необходимо передать команде параметр "-fадрес отправителя <email@отправителя.ru>", также предварительно его закодировав (обратите внимание на формат строки: "минус-эф", а затем без пробелов электронный адрес).

mail('sample@mail.ru', 'test subj', 'test msg', null, '-finfo@mycompany.ru')

Защита от спама

Спамеры очень часто для рассылки используют плохо написанные формы отправки почты на PHP. Дело в том, что стандарт допускает использование нескольких получателей, которые могут быть перечислены через запятую в заголовке "To", а также использование произвольного количества специальных заголовков с именем "Сс" (копия) и "Bcc" (скрытая копия). Предположим, что в вашей форме есть поле темы, которое без изменений транслируется в заголовок "Subject". Для того, чтобы отправить произвольное сообщение на тысячу адресов, спамеру достаточно в поле Subject вписать примерно такой текст:

Hello
Cc: a@yahoo.com
Cc: b@yahoo.com
...
Cc: zzzz@google.com

Таким образом отправляемое письмо будет дополнено заголовками "Cc", и почтовая программа, обнаружив все эти заголовки в тексте письма, примется рассылать его всем получателям из списка. Аналогичная ситуация и с переменными $additional_headers и $to - любое из них при удачном стечении обстоятельств может быть использовано злоумышленниками в своих интересах.

Борьба с этим явлением может вестись двумя способами:

1. Проверка всего пользовательского ввода и фильтрация опасных символов (т.е символов "\r" и "\n"). Эти символы нужно либо заменять на пробелы, либо отказываться обрабатывать некорректно введенные параметры. Например, замену можно выполнить так:

$from = preg_replace("/[\r\n]/", " ", $_POST["sender"]);

2. Выполнение кодирования Base64, помимо прочего, защищает и от спамеров, т.к. "вытягивает" закодированный текст в одну строку

Как все это сделать проще

Существует по меньшей мере три варианта упрощения жизни при работе с формой отправки почты.

Способ первый

Использование готовых решений для отправки почты. В качестве примера можно привести класс PHPMailer, который легко позволяет реализовать все возможности, описанные выше, и еще целый ряд того, о чем не было упомянуто. Здесь нужно упомянуть об одном подводном камне, на который легко натолкнуться. Уверены ли вы в безопасности полутора тысяч строк чужого кода настолько, насколько можете быть уверены в нескольких строках своего? 11 июня 2007 года в PHPMailer была обнаружена уязвимость, не устраненная в официальной версии до сих пор (т.е. существующая уже почти два месяца).

Способ второй

Использование конструктора сайтов. Более подробную информацию о конструкторе можно найти по ссылке. Если говорить коротко, то конструктор позволяет без особого труда создать простой сайт, в который в числе прочего, будет включена грамотная реализация формы отправки сообщений.

Способ третий

Установка приложений для всех клиентов хостинга. Все клиенты нашего хостинга имеют возможность бесплатной установки ряда приложений на свои сайты. Установка приложений выполняется буквально в два клика, после чего вы получаете в све распорядение уже настроенный и полностью готовый к работе модуль. Точно также, в два клика, можно установить и написанную нами форму отправки сообщений.

Отправка писем при помощи PHP

Общие возможности

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

Во всех этих случаях необходима именно автоматическая отправка писем минуя разнообразные почтовые программы и утилиты. Сделать это можно при помощи функции mail(), которую мы сейчас и будем изучать.

Синтаксис функции mail():

bool mail(string $to, string $subject, string $msg [, string $header]);

Функция mail() посылает письмо с темой $subject и содержанием $msg по адресу $to.

Если Вы хотите, чтобы письмо ушло по нескольким адресам, разделите их пробелами.

Само сообщение может быть многострочным. Для перехода на новую строку поставьте знак перехода на новую строку "\n" в конце каждой строки.

mail("name@mail.ru", "my subject", "stroka1
nstroka2
nstroka3");

Или тотже самое можно записать так:

mail("name@mail.ru", "my subject", "stroka1stroka2stroka3");

В четвертом необязательном параметре $header можно указать заголовки нашего сообщения.

Под заголовками я подразумеваю информацию, передаваемую вместе с письмом почтовому клиенту, которая будет содержать некоторые технические данные, такие как: кодировка письма, имя отправителя, обратный адрес отправителя и т.д. Это схоже с использованием тега <META> в HTML.

Чтобы было более понятно, что из себя представляют заголовки, и где они находятся в письме, откройте почтовый клиент и посмотрите на любое письмо в том виде, как оно пришло (Для Outlook это можно сделать нажав правой клавишей мыши на сообщение->Свойства->Подробно->Исходное сообщение).

Как мы видим, письмо представляет простой текстовой файл, состоящий из двух общих разделов: Вверху идут заголовки письма, затем через две строки идет само письмо.

Заголовков письма достаточно много, но не все они используются при написании скрипта отправки почты.

Вот некоторые наиболее часто используемые:

From: "Имя пользователя отправителя" <Обратный адрес отправителя> 
To: <Адрес, по которому отправляют письмо> 
Subject: Тема письма

Отправка письма в HTML-виде

Для отправки письма в HTML-виде достаточно в заголовке Content-type указать тип документа не text/plain (простой текст), а text/html (html-текст).

А само письмо записать в html-виде:

$subject = "Тема письма";
$header = "Content-type: text/html; charset=\"windows-1251\"";
$header .= "From: \"Evgen\" <evgen@mail.ru>";
$header .= "Subject: $subject";
$header .= "Content-type: text/html; charset=\"windows-1251\"";
$msg = "<body><li>Сторака 1<li>Сторака 2<li>Сторака 3</body>";
mail("name@mail.ru", $subject, $msg, $header);

Отправка писем при помощи PHP: Как вставить в письмо картинку Допустим, у нас задача - вставить в письмо, отправляемое с сайта пользователю, свой баннер (кнопку).

Сделать это можно двумя способами:

Первый - в HTML-коде письма указываем адрес картинки как на простой страничке <IMG src="http://spravkaweb.ru/img/88x31.gif"> В этом случае, если пользователь читает ваше письмо online, картинка благополучно загрузится с указанного сервера и отобразится в письме. Но если пользователь не в online, картинка загрузиться не сможет.

Второй способ - это поместить картинку в само письмо как прикрепленный файл, присвоить этому файлу уникальный идентификатор, а затем в теле письма при указании адреса картинки сослаться на этот идентификатор. Таким образом, в письмо можно вставлять не только изображения, но и flash-ролики, музыку, элементы ActiveX.

Конечно, при этом размер письма увеличится, но зато мы будем уверены, что пользователь точно увидит выставляемую картинку (если конечно в его почтовой программе не отключено отображение изображений).

Чтобы присвоить идентификатор картинке, надо в разделе письма, где она располагается, поместить следующий заголовок:

Content-ID: <идентификатор> где идентификатор является строкой, которая будет уникальной для данного письма (по примеру параметра boundary заголовка Content-Type).

Теперь в самом письме можно в адресе картинки подставлять ее идентификатор.

<IMG src="cid:идентификатор">

Почтовая программа проанализирует его, извлечет из соответствующей секции картинку и покажет ее.

Вот как может выглядеть письмо:

Date: Sat, 13 Mar 2004 09:56:31 -0300
Subject: Отправка изображения
From: "Evgen" <admin@spravkaweb.ru>
To: admin@localhost.ru
Subject: Отправка изображения
Mime-Version: 1.0
Content-Type: multipart/alternative; boundary="spravkaweb-1234"

--spravkaweb-1234
Content-type: text/html; charset="windows-1251"
Content-Transfer-Encoding: 8bit

<h3>Привет</h3>
Это проба отправки письма с прикрепленной картинкой.<BR>
А вот и сама картинка:<BR>
<img src="cid:spravkaweb_img_1">

--spravkaweb-1234
Content-Type: image/jpeg; name="5.jpg"
Content-Transfer-Encoding:base64
Content-ID: <spravkaweb_img_1>

/9j/4AAQSkZJRgABAQAAAQABAAD/2wBD ....
--spravkaweb-1234--

Как и в случае с прикрепляемыми файлами, если нам заранее неизвестно, какой тип картинки будет прикреплен, заголовку Content-Type можно присвоить значение application/octet-stream.

Для закрепления этой темы напишем программу, которая отправляет письмо с картинкой:

<?php
/*Зададим в переменной $file_name путь до вставляемой картинки.
В нашем случае она находится в том же каталоге, что и файл отправки письма. 
Но вместо этого сюда можно подставитьфайл, 
полученный сценарием из <INPUT type=file name=file_name>.*/
$file_name = "5.jpg";
$subj = "Отправка изображения";
$bound = "spravkaweb-1234";
$headers = "From: \"Evgen\" <admin@spravkaweb.ru>\n";
$headers .= "To: admin@localhost.ru\n";
$headers .= "Subject: $subj\n";
$headers .= "Mime-Version: 1.0\n";
$headers .= "Content-Type: multipart/alternative; boundary=\"$bound\"\n";
$body = "--$bound\n";
$body .= "Content-type: text/html; charset="windows-1251"\n";
$body .= "Content-Transfer-Encoding: 8bit\n\n";
$body .= "<h3>Привет</h3>Это проба отправки письма с прикрепленной картинкой.<BR>А вот и сама 
картинка:<BR><img src=\"cid:spravkaweb_img_1\">";
$body .= "\n\n--$bound\n";
$body .= "Content-Type: image/jpeg; name=\"".basename($file_name)."\"\n";
$body .= "Content-Transfer-Encoding:base64\n";
$body .= "Content-ID: <spravkaweb_img_1>\n\n";
$f = fopen($file_name,"rb");
$body .= base64_encode(fread($f,filesize($file_name)))."\n";
$body .= "--$bound--\n\n";
mail("admin@localhost.ru", $subj, $body, $headers);
?>

Хочу отметить, что аналогичным образом в письмо можно вставлять не только картинки, но, например, flash-ролики, звук, и другие элементы, которые должны погружаться к странице из файлов.

Как отправить письмо средствами PHP

Самый простой способ отправить письмо при помощи PHP - воспользоваться стандартной функцией mail. Она имеет следующий синтаксис:

bool mail ( string to, string subject, string message [, string additional_headers [, string additional_parameters]])

Обязательные параметры:

E-mail получателя 
Заголовок письма 
Текст письма

Необязательные параметры:

Дополнительные заголовки письма 
Дополнительные параметры командной строки

Возвращаемое значение:

true, если письмо было принято к доставке 
false, в противном случае

Простейший пример ее использования выглядит так:

<?php 
mail("joecool@example.com", "My Subject", "Line 1 Line 2 Line 3"); 
?>

Если у Вас на экране появилась ошибка "Fatal error: Call to undefined function: mail()", это значит, что либо PHP собран без поддержки функции mail, либо она запрещена настройками сервера. Такая практика в последнее время широко распространена на бесплатных хостинговых серверах. Если Вы столкнулись с такой проблемой, воспользуйтесь возможностью отправки писем при помощи сокетов (sockets), детально описанной в разделе "альтернативные способы отправки писем". В случае, если Вы администратор системы, обратитесь к разделу "Как настроить сервер" и попробуйте устранить эту проблему как таковую.

Дополнительные заголовки письма (additional headers) могут использоваться для указания кодировки письма, адреса отправителя, обратного адреса и множества других опций. Они должны быть разделены переводом строк: комбинацией " ". Например, так:

<?php 
mail("nobody@example.com", "the subject", $message, 
"From: webmaster@ example.com " 
."X-Mailer: PHP/" . phpversion()); 
?>

Перейдем к более сложному примеру. Предыдущие скрипты работали с форматом text/plain, теперь же попробуем отправить почту в формате HTML нескольким адресатам с указанием кодировки:

<?php 
$to = "Mary <mary@example.com>, "; 
$to .= "Kelly <kelly@example.com>"; 

$subject = "Birthday Reminders for August"; 

$message = " 
<html> 
<head> 
<title>Birthday Reminders for August</title> 
</head> 
<body> 
<p>Here are the birthdays upcoming in August!</p> 
</body> 
</html>"; 

$headers = "Content-type: text/html; charset=windows-1251 "; 
$headers .= "From: Birthday Reminder <birthday@example.com> "; 
$headers .= "Bcc: birthday-archive@example.com "; 

mail($to, $subject, $message, $headers); 
?>

Комментарии к примеру: вначале мы определяем, кому адресовано письмо. Если получателей несколько, их адреса указываются в одной строке и разделяются запятыми. При задании заголовка и тела письма следите, чтобы кодировка, в которой они действительно написаны, совпадала с заявленной в заголовке charset.

В нашем примере переменная $headers состоит из четырех строк: в первых двух мы указываем тип отправляемого письма - HTML и его кодировку. В следующих двух строках мы указываем адрес отправителя и адрес, по которому следует отправить скрытую копию письма.

Одна из часто возникающих проблем при отправке почты в koi8 - это формирование заголовка письма. Для ее решения необходимо воспользоваться следующим кодом, который переводит строку в кодировке win-1251 в понятный большинству почтовых клиентов koi8 заголовок.

<?php
$subject = '=?koi8-r?B?'.base64_encode(convert_cyr_string($subject, "w","k")).'?='; 
?>

К примеру, заголовок "Почтовое уведомление" будет выглядеть как

=?koi8-r?B?8M/e1M/Xz8Ug1dfFxM/NzMXOycU=?=

Если Вы все сделали правильно, а получатель письмо не получает (помните, что временные издержки на доставку письма зависят от многих факторов и могут колебаться от нескольких минут до нескольких часов), убедитесь в том, что оно действительно отправилось. Это необходимо сделать в 2 этапа. Вначале попробуйте выполнить код:

<?php
if (mail("nobody@example.com", "the subject", "Example message", 
"From: webmaster@example.com ")) { 
echo "messege acepted for delivery"; 
} else { 
echo "some error happen"; 
} 
?>

В случае если уже на этом шаге Вы получили ошибку, это может означать, что у Вас либо не запущен sendmail (или другой транспортный агент), либо он неправильно настроен, либо существуют ошибки в php.ini. К примеру, в последнее время распространено правило не принимать письма, в которых не указан правильный заголовок Mail-from.

В случае, если сообщение было принято к отправке, попробуйте посмотреть файл /var/log/mail или попросить об этом Вашего администратора, так как для этого требуются права суперюзера (root). Это можно сделать при помощи команды tail /var/log/mail. В случае успешной отправки письма в log-файле должны появится строки нижеприведенного типа либо сообщение об ошибке:

Oct 2 00:21:02 l72 sendmail[131]: h91LL1DG000131: 
   to=root, ctladdr=root (0/0), delay=00:00:01, 
   xdelay=00:00:01, mailer=relay, pri=30225, 
   relay=[127.0.0.1] [127.0.0.1], dsn=2.0.0, 
   stat=Sent (h91LL1g1000134 Message accepted for delivery) 
Oct 2 00:21:18 l72 sendmail[137]: h91LL1g1000134: 
   to=, ctladdr= (0/0), delay=00:00:17, xdelay=00:00:16, 
   mailer=local, pri=30774, dsn=2.0.0, stat=Sent

В этом случае попробуйте настроить сервер.

Content-type: multipart/???

С этим заголовком знаком любой разработчик, которому доводилось решать проблемы отправки писем с вложениями или HTML письмами. И зачастую письма, сформированные без использования библиотек вроде PEAR::Mail_mime отображаются не очень корректно. Практика показывает, что если при формировании письма жестко придерживаться стандарта, которы задается в RFC (в частности - RFC 2046 http://ietf.org/rfc/rfc2046.txt) - подавляющее большинство клиентских программ (включая таких любителей придерживаться стандартов, как Mozilla Thunderbird) отображает письмо корректно. Далее мы будем исходить из того, что читатель этого документа представляет себе основной синтаксис команд и понимает, что таке boundary и почему необходимо указывать Content-type для каждой из частей письма. Постараемся отметить основные ошибки.

Ошибка первая - неверный subtype

Тип multipart имеет три субтипа - mixed, alternative и related, которые используются синтаксически одинаково, но имеют разное предназначение

Помните и применяйте по назначению.

Ошибка вторая - неверный порядок частей

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

Ошибка третья - выбор только одного субтипа

Зачастую разработчик, формирующий из программы письмо забывает, что любая из частей письма может так же иметь Content-type: multipart, а значит можно выстроить некоторое подобие древовидной структуры, гарантирующей, что каждая из частей письма займет правильное место. Вот как примерно может выглядеть структура письма, имеющего текстовую и HTML версию (HTML с картинками), а так же приложенный документ MS Word:

И напоследок - еще пара рекомендаций

Разделитель

Что указывать \n или \r\n в качестве разделителя, можно определять следующим образом:

<?php 
$CRLF = substr(PHP_OS, 0, 3) != "WIN" ? "\n" : "\r\n"; 
?>

Проблемы при отправке почты

Появление символов !

Иногда при отправки почты через функцию mail(), в письме в разных местах получается разделение в виде "! " восклицательного знака и пробела. Всё дело в том, что это штатное ограничение количества символов в одной строке от sendmail. По умолчанию такой разграничитель строки вставляется через каждые 1015 символов, т.е. между 1015 и 1016 символами и т.д.

Если Ваша строка намного длиннее, и Вам нужно поправить это, у Вас 2 варианта:

1. Обратиться в техподдержку хостинга и попросить увеличить длинну строки.
2. Выставлять переносы строк в скрипте, чаще 1015 символов.