Добро пожаловать!
Здесь вы можете найти ответ на интересующий вас вопрос в отрасли сайтостроения, познакомится ближе с web технологиями и web стандартами.

Отправка писем с использованием сокетов

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

Вообще говоря, есть несколько способов отправки письма с формы, простых и не очень. Начнём с самого простого (и обычно не работающего).

PHP функция mail()

Описание функции следующее:

bool mail (string to, string subject, string message)

т.е. она для работы требует несколько параметров (они подробно рассматриваются в мануале), в том числе строка с адресом почты "кому".

А вообще-то для отправки письма нам необходимо:

  • сервер/программа("A") откуда наше письмо начнёт путь
  • сервер("B") куда оно придёт
  • почтовый ящик на сервере "B", владельцу которого, собственно, это письмо и предназначалось.

А у нас имеется только последние два, где же прячется ещё один параметр? А прячется он в конфигурационном файле php.ini. Под операционной системой Windows актуальны следующие параметры:

  • SMTP string - DNS имя smtp-сервера исходящих сообщений или его IP-адрес, например smtp.mail.ru или smtp.zelnet.ru
  • smtp_port int - порт который слушает сервер для приёма исходящих сообщений, обычно 25.

Для *nix систем параметры php.ini следующие:

  • sendmail_path string - определяет путь к программе sendmail, обычно /usr/sbin/sendmail или /usr/lib/sendmail.

Так вот мы нашли "точку входа" нашего письма. Под Windows это обязательно некий почтовый сервер, который, словно металический почтовый ящик, что стоит на почте, умеет принимать письма и отправлять их адресату. Под юникс это программа sendmail, которая сама, без помощи сторонних серверов, умеет доставлять сообщения до сервера "B".

Тут немного разгребли, теперь составлю общий план:

  • в html форме составляем письмо, указав поле "Кому:"
  • в скрипте, что обрабатывает форму, используем функцию mail()
  • функция mail() воспользуется указанными нами и параметрами php.ini и передаст письмо либо почтовому серверу "A", либо программе sendmail
  • далее sendmail или сервер исходящих определяют сервер назначения "B" (он обслуживает почтовый ящик адресанта) и передаёт письмо ему
  • почтовый сервер "B" хранит письмо, пока адресат его не заберёт с сервера.

Тогда начинаем работать по плану, для начала накидаем форму в каком-нибудь редакторе визуальном или не очень:

  <FORM METHOD="POST" ACTION="mail.php">
  <INPUT NAME="to" TYPE="text"> Адрес получателя<BR>
  <INPUT NAME="subj" TYPE="text"> Тема сообщения<BR>
  <TEXTAREA NAME="mess" ROWS="3" COLS="20">Сообщение</TEXTAREA><BR>
  <INPUT TYPE="submit" NAME="send">
  </FORM>

Пишем скрипт:

<?php
if ( $_POST['to'] != '' ) {
  if ( 
mail($_POST['to'], $_POST['subj'], $_POST['mess']) ) 
    echo 
'Ваше письмо успешно отправлено';
  else 
    echo 
'Возникла ошибка при отправке письма';
}
?>

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

Warning: mail(): SMTP server response: 575 login@zelnet.ru 
sender requires authentication in test_mail.php on line 9

Более чем уверен, что это не совсем то, что требовалось.

Используем сокеты

Вопрос: как обойти проблемы с обязательной smtp авторизацией (на исходящие сообщения)? Решение: если функция mail() не хочет работать так, как нам того хочется, напишем собственную, используя сокеты. Для этого необходим почтовый ящик, зарегистрированный на каком-либо сервере, как пример: mail.ru или yandex.ru, соответственно логин и пароль от этого ящика. Надо отметить что именно с этого адреса будут посылаться ваши "форменные" сообщения.

О том, что такое сокеты чуть позже, а пока жмите: Пуск->Выполнить->cmd, пишем:

telnet smtp.zelnet.ru 25

Подождём с ответа и что видим?

220 mail.zelnet.ru ESMTP CommuniGate Pro 4.3.4

SMTP-сервер нам что-то ответил. А ну-ка напишем мы ему следующую команду:

HELO 127.0.0.1

Вместо 127.0.0.1 подставите свой IP адрес, на что нам ответят:

250 mail.zelnet.ru is pleased to meet you

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

Для начала стоит рассказать, что собственно этому серверу посылать-то, какие команды... Набросаю кратко план действий.

После соединения необходимо забрать строку приветствия сервера, в нашем случае "220 mail.zelnet.ru ESMTP CommuniGate Pro 4.3.4". Цифра 220 в начале значит что сервер работает и готов принимать наши команды, о других цифрах чуть позже.

Далее отправляем:

HELO host
где вместо host ваш IP-адрес. После нужно уведомить smtp-сервер о том, что мы хотим авторизоваться. Делается это командой:
AUTH LOGIN
далее последовательно посылаются логин и пароль (тут есть тонкость, о ней чуть позже). И если сервер ответил что, вы авторизованы, уведомляем сервер от кого письмо:
MAIL FROM:vasya_pupkin@mail.ru
И кому:
RCPT TO:vasha_masha@gmail.com
Далее необходимо уведомить сервер о том, что сейчас будем посылать данные:
DATA
И если сервер готов принять посылается сам текст письма:
Приветсвую, Маня!
Как у тебя дела?

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

.

Всё собственно. Больше нам сервер не нужен (мы же пока не собираемся ещё и принимать письма, ведь так?), так что закрываем соединение:

QUIT

Это и есть план подключения.

Заметьте, после каждой команды сервер вернёт некую строчку - она начинается с трёхзначного кода и строки пояснения после пробела. Какой код означает удачу/неудачу, какой код после какой команды вообще может быть возвращён - смотрите в RFC821.

Вопрос ещё не родился у вас в светлых головах? А как эти команды посылать с помощью PHP? СОКЕТЫ!!! Так, сейчас расскажу. Для начала: сокеты - это некий программный интерфейс над реализацией стека TCP/IP. По сути это функции с помощью которых можно работать с сетью на низком уровне: соединиться с каким-то адрессом:портом, послать байт, принять байт и т.п. А ещё сокет - это связка адрес-порт. Например, файловый сервер городской сети открыт по адресу ftp1.zelnet.ru и порту 21. Так вот связка ftp1.zelnet.ru:21 будет точно указывать именно на этот файловый сервер (можно на ftp1.zelnet.ru открыть ещё один файловый сервер просто на другом порту и это будет уже другой сервер), её и можно назвать сокетом.

Чтобы открыть соединение используем функцию fsockopen():

resource fsockopen ( string target, int port )

Здесь

  • target - DNS имя или IP адресс сервера
  • port - порт на котором нас ждёт этот сервис. Smtp-серверы обычно работают на 25м порту

Чтение/запись через сокет, открытый этой функцией, производится с помощью fgets() и fputs() соответственно.

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

<?php
function smtp_send_cmd($smtp_socket$cmd) {
  
$smtp_msg  "";
  
$smtp_code "";
  
fputs$smtp_socket$cmd."\r\n" ); 
  while (
$line fgets($smtp_socket515)) {
    
$smtp_msg .= $line;
    if (
substr($line31) == " ") break;
  }
  
$smtp_code substr$smtp_msg0); 
  return 
$smtp_code=="" false $smtp_code;

?>

Каждая команда SMTP-серверу должна завершаться символом перевода строки <CRLF> или \r\n. Для того чтобы каждый раз не писать \r\n после команды, мы дописываем это в этой функции, перед тем как послать команду серверу. После в цикле мы принимаем ответ сервера и выбираем из него первые три символа, которые и возвращаем из функции.

Всё собственно, можно писать код (пояснения в комментариях и ниже):

<?php
// соединяемся с сервером $smtp_host на порт $smtp_port
$smtp_socket fsockopen($smtp_host$smtp_port) or die ('Не могу соединиться');  
while (
$line fgets($smtp_socket515)) {
$smtp_msg .= $line;
  if (
substr($line31) == " ") break;
}
// приняли ответ сервера, если он начинается на 220 - значит всё ок, сервер работает
$answer substr($smtp_msg03);
if(
$answer != '220') die (); 
// посылаем серверу приветствие и свой адрес
$answer smtp_send_cmd($smtp_socket'HELO '.$localhost);  
if(
$answer != '250') die (); 
// если всё ок, скажем серверу что хотим авторизоваться
$answer smtp_send_cmd($smtp_socket'AUTH LOGIN'); 
if(
$answer != '334') die ();
// если сервер работает через smtp авторизацию на исходящие, посылаем ему логин от ящика $smtp_user
$answer smtp_send_cmd($smtp_socketbase64_encode($smtp_user)); 
if(
$answer != '334') die ();
// и пароль $smtp_pass
$answer smtp_send_cmd($smtp_socketbase64_encode($smtp_pass)); 
if(
$answer != '235') die ();
// говорим от кого
$answer smtp_send_cmd($smtp_socket'MAIL FROM:'.$from); 
if(
$answer != '250') die ();
// и кому посылаем
$answer smtp_send_cmd($smtp_socket'RCPT TO:'.$to); 
if(
$answer != '250') die ();
// сообщаем, что начинаем вводить данные:
$answer smtp_send_cmd($smtp_socket"DATA");
if(
$answer != '354') die ();
// собственно сам процесс введения данных:
fputs($smtp_socket$data."\r\n"); 
// говорим, что закончили посылать данные:
$answer smtp_send_cmd($smtp_socket".");
if(
$answer != '250') die ();
// если всё ок, закрываем соединение с севером
$answer smtp_send_cmd($smtp_socket"QUIT");
if(
$answer != '221') die ();
fclose($smtp_socket);
?>

Теперь немного пояснений.

Почему логин и пароль кодируются с помощью base64_encode() не знаю. Мне это не интересно, для результата знание этого не важно - так что примите на веру.

Вот переменные которые должны быть определены до того как вызовётся этот кусок кода:

  $localhost
  $smtp_user
  $smtp_pass
  $to
  $data
  $smtp_host
  $smtp_port

и вроде бы имена говорящие.

Единственное с чем есть загвоздка это $data. Эта строковая переменная должна содержать письмо, в определённом формате. В примитивном случаем это примерно вот так:

заголовки\r\n
заголовки\r\n
заголовки\r\n
\r\n
само послание

Как пример:

From: vasya@nc.ru
To: masha@mail.ru
Subject: Вот такое хреновое лето
Content-Type: text/plain; charset="koi8-r"
Content-Transfer-Encoding: 8bit

Привет. Как ваша жизнь?

Какие бывают заголовки можно посмотреть, открыв в почтовом клиенте любое письмо и в менюшках щёлкнув "Показать заголовки RFC822" или что-нибудь вроде того, либо проштудировав данный RFC.

Кому-то то, что я написал, может не понравится, потому что сочтут что воспользоваться веб-интерфейсом какого-нить mail.ru проще. А вообще, именно так, в абсолютном большинстве случаев, и отправляют почту, используя PHP.