Отправка почты на PHP. Аутентификация

mailer_logoВ предыдущих статьях по отправке почты мы рассмотрели теоретическую часть по отправке почты, а также рассмотрели небольшой практический пример. В этой статье речь пойдет об аутентификации.
Давным-давно, когда на свете жили динозавры (т.е. компы на процессоре i386), а о почте, а тем более о протоколе знали только избранные, проблем с почтой практически не возникало. Пользователи, зачастую вручную, посредством ввода команд, просили сервер отправить почту кому-то на чей-то компьютер.
Время шло, времена менялись. И, в конце концов, Интернетом заинтересовались бизнесмены. Здесь начали возникать магазины и другие платные службы, которые начали приносить не плохой доход. Бизнес требовал все больше и больше клиентов. Именно в этот момент и началась активная рассылка писем рекламного содержания через бесплатные SMTP-сервера. Поток спама увеличивался и увеличивался, пока практически все бесплатные почтовики не ввели аутентификацию на отправку почты.
На данный момент для протокола ESMTP (именно ESMTP, поскольку SMTP не умеет работать с аутентификацией) существует несколько методов аутентификации.
Методика аутентификации проста. После успешного отклика сервера на команду EHLO, клиент обязан отослать команду AUTH method, где method – наименование метода аутентификации. Если сервер поддерживает выбранный метод аутентификации, то он должен ответить кодом 334. В этой небольшой статье рассмотрим три самых популярных метода аутентификации: PLAIN, LOGIN и CRAM-MD5.

PLAIN
Самым простым и наименее криптостойким способом аутентификации является способ PLAIN. В этом случае логин и пароль передаются незашифрованными, но обязательно кодируются в base64. Создается строка, которая состоит из логина, повторенного дважды и пароля разделенных нуль-символами (\0). Логин повторяется дважды потому, что в данном методе аутентификации принимают участие два идентификатора пользователя. Но поскольку логин обычно один, то его повторяют дважды. Получается такая строка
логин\0логин\0пароль
После эта строка кодируется в base64 и отправляется на сервер. В случае успешно аутентификации, сервер должен ответить кодом 235.
Теперь внедрим аутентификацию в библиотеку WJMailer. Создадим новый транспортный класс, наследуя его от WJTransport

transports/wj_transport_auth_plain.class.php

<?php
include_once 'wj_transport.class.php';
class WJTransportAuthPlain extends WJTransport{
//логин и пароль
private $login,
$password;
/**
* WJTransportAuthPlain::setLogin()
* метод для установки логина и пароля
* @access public
* @return void
* @param string $login
* @param string $password
**/
public function setLogin($login, $password){
$this->login = $login;
$this->password = $password;
}

Одно из обязательных условий аутентификации – это то, что аутентификация поддерживается протоколом ESMTP. Поэтому для уверенности, что будет послана верная команда (EHLO) при приветствии, нужно переопределить метод hello

transports/wj_transport_auth_plain.class.php

/**
* WJTransportAuthPlain::hello()
* метод для отправки приветствия серверу. Обязательно должен отправить EHLO
* @access public
* @param string $name
* @return bool
**/
public function hello($name){
if(!is_resource($this->_socket))throw new
Exception('Not connected to the server');
$res = @socket_write($this->_socket, 'EHLO '.$name."\r\n");
if(!$res){
throw new Exception('Cannot write into socket');
}
$this->responce();
if($this->getCode()==250){
$this->_extended = true;
return true;
}else{
$this->_error = 'Error with code '.$this->getCode();
return false;
}
}

Ну и, наконец, метод auth, который производит аутентификацию
transports/wj_transport_auth_plain.class.php

/**
* WJTransportAuthPlain::auth() - метод для
аутентификации по методу PLAIN
* @access public
* @return bool
* @throws Exception
**/
public function auth(){
if(!is_resource($this->_socket))throw new
Exception('Not connected to the server');
if(!$this->_extended)throw new
Exception('Server not support authenticate');
/*
шлем логин и пароль в виде
login\0login\0password
закодированные в base64
*/
$login = base64_encode("{$this->login}\0{$this->login}\0".
"{$this->password}");
$res = @socket_write($this->_socket, "AUTH PLAIN $login\r\n");
if(!$res){
throw new Exception('Cannot write into socket');
}
$this->responce();
if($this->getCode()==235){
return true;
}else{
$this->_error = 'Error with code '.$this->getCode();
return false;
}
}

LOGIN
Метод SMTP-аутентификации LOGIN тоже относится к простым незашифрованным способам авторизации. Он отличается от PLAIN только способом передачи логина и пароля. После приветствия, клиент посылает команду AUTH LOGIN. Если сервер поддерживает этот метод аутентификации, он должен ответить кодом 334. Тогда клиент должен послать логин, закодированный в base64. Если все в порядке, то сервер должен ответить кодом 334. После этого клиент должен послать пароль, закодированный в base64. Если авторизация прошла успешно, то сервер должен ответить кодом 235.
Для аутентификации по методу LOGIN в библиотеке WJMailer создадим новый транспортный класс WJTransportAuthLogin, который наследует WJTransport.

transports/wj_transport_auth_login.class.php

<?php
include_once 'wj_transport.class.php';
class WJTransportAuthLogin extends WJTransport{
//приватные свойства для хранения логина и пароля
private $login,
$password;

Методы hello и setLogin идентичны аналогичным методам класса WJTransportAuthPlain (см. выше). Возможно стоило бы сделать наследование не от WJTransport, а от WJTransportAuthPlain или сделать отдельный базовый класс для аутентификации. Но я не стал делать. Пять строчек кода в трех классах – это еще не повод выносить всё в отдельный и делать запутанную иерархию наследования.
Далее метод для аутентификации.

transports/wj_transport_auth_login.class.php

/**
* WJTransportAuthLogin::auth() - метод для аутентификации по методу LOGIN
* @access public
* @return bool
* @throws Exception
**/
public function auth(){
if(!is_resource($this->_socket))throw new 
Exception('Not connected to the server');
if(!$this->_extended)throw new 
Exception('Server not support authenticate');
$res = @socket_write($this->_socket, "AUTH LOGIN\r\n");
if(!$res){
throw new Exception('Cannot write into socket');
}
$this->responce();
if($this->getCode()==334){
$code = base64_encode($this->login);
$res = @socket_write($this->_socket, "$code\r\n");
if(!$res){
throw new Exception('Cannot write into socket');
}
$this->responce();
if($this->getCode()==334){
$code = base64_encode($this->password);
$res = @socket_write($this->_socket, "$code\r\n");
if(!$res){
throw new Exception('Cannot write into socket');
}
$this->responce();
if($this->getCode()==235){
return true;
}else{
$this->_error = 'Error with code '.$this->getCode();
return false;
}
}else{
$this->_error = 'Error with code '.$this->getCode();
return false;
}
}else{
$this->_error = 'Error with code '.$this->getCode();
return false;
}
}

CRAM-MD5
Самый криптостойкий алгоритм из всех трех, рассматриваемых алгоритмов – это CRAM-MD5. Этот метод использует шифрование пароля. Авторизация проходит следующим образом. После того, как клиент отсылает команду AUTH CRAM-MD5, сервер шлет в ответ код 334, а вместе с кодом через пробел шлет секретный ключ, закодированный в base64. Клиент берет ключ, раскодирывает и с помощью него вычисляет хэш для пароля по методу HMAC-MD5. После этого клиент создает строку для ответа. Записывает логин, через пробел получившийся хэш, после чего кодирует в base64 и отправляет серверу. Сервер в случае успеха должен ответить кодом 235.
Напишем еще один транспортный класс WJTransportAuthCram

transports/wj_transport_auth_cram.class.php

<?php
include_once 'wj_transport.class.php';
class WJTransportAuthCram extends WJTransport{
private $login,
$password;

Методы hello и setLogin такие же как и в классе WJTransportPlain.
Прежде чем писать метод для аутентификации, задумаемся над вычислением хэша. По методу HMAC-MD5 могут шифровать функции mhash и hash_hmac. Первая находится в расширении MHASH, а вторая идет с PHP начиная с версии 5.1.2. Поэтому в последних версиях php можно использовать hash_hmac. Специально для версий ниже 5.1.2 напишем небольшой метод, который вернет HMAC-MD5.

transports/wj_transport_auth_cram.class.php

public static function hmac($key, $data, $block = 64)
{
if (strlen($key) > 64) {
$key = pack('H32', md5($key));
} elseif (strlen($key) < 64) {
$key = str_pad($key, $block, chr(0));
}
$k_ipad = substr($key, 0, 64) ^ str_repeat(chr(0x36), 64);
$k_opad = substr($key, 0, 64) ^ str_repeat(chr(0x5C), 64);
$inner = pack('H32', md5($k_ipad . $data));
$digest = md5($k_opad . $inner);
return $digest;
}

Теперь можно приступать к авторизации способом, описанным выше.

transports/wj_transport_auth_cram.class.php

/**
* WJTransportAuthCram::auth() - метод для
аутентификации по методу CRAM-MD5
* @access public
* @return bool
* @throws Exception
**/
public function auth(){
if(!is_resource($this->_socket))throw new
Exception('Not connected to the server');
if(!$this->_extended)throw new
Exception('Server not support authenticate');
$res = @socket_write($this->_socket, "AUTH CRAM-MD5\r\n");
if(!$res){
throw new Exception('Cannot write into socket');
}
$this->responce();
if($this->getCode()==334){
$s = explode(" ", $this->_buffer);
if(!isset($s[1])||empty($s[1])||trim(strlen($s[1]))==0){$this->_error = 'Cannot find salt';return false;}
$salt = trim($s[1]);
$hmac = self::hmac($this->password, base64_decode($salt));
$res = @socket_write($this->_socket,
base64_encode($this->login.' '.$hmac)."\r\n");
if(!$res){
throw new Exception('Cannot write into socket');
}
$this->responce();
if($this->getCode()==235){
return true;
}else{
$this->_error = 'Error with code '.$this->getCode();
return false;
}
}else{
$this->_error = 'Error with code '.$this->getCode();
return false;
}
}

Тестирование
Для тестирования напишем небольшой тестовый скрипт, в котором попытаемся отправить почту по одному из вышеперечисленных методов.

test.php

<?php
/*
подключаем файлы с классами:
wj_mailer.class.php - WJMailer - основной класс
transports/wj_transport_auth_cram.class.php - WJTransportAuthCram - транспортный класс
для аутентификации по методу CRAM-MD5
*/
include_once 'wj_mailer.class.php';
include_once 'transports/wj_transport_auth_cram.class.php';
$mailer = new WJMailer('windows-1251');
//устанавливаем от кого
$mailer->addFrom('Веб-джуниор','admin@web-junior.net');
//устанавливаем поле Отправитель
$mailer->addSender('Секретарь','secretar@web-junior.net');
//добавляем получателя
$mailer->addTo('Вася петичкин','user@example.loc');
//устанавливаем тему
$mailer->setSubject('Почему не пишешь?');
//устанавливаем текст
$mailer->setText('По какой причине, дорогой '.
'мой Вася ты мне не пишешь????','text/plain');
//добавляем файл
$mailer->addFile('wj_mailer.class.php', 'text/php');
//создаем класс транспорта, передаем ему имя сервера и порт
$tr = new WJTransportAuthCram('example.loc', 25);
//устанавливаем логин и пароль
$tr->setLogin('user', 'password');
//устанавливаем транспорт
$mailer->setTransport($tr);
//отправляем сообщение
if($mailer->send()){
echo 'Сообщение успешно отправлено';
}else{
echo $mailer->getError();
}
?>

Cкачать все транспортные классы вместе с WJMailer можно здесь.

Скачать

Читайте продолжение статьи

Популярность: 5%


Интересное из других блогов:

2leep.com

И не забывайте комментировать статью.

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

Отзывов: 11 на «Отправка почты на PHP. Аутентификация»

  1. Автор: Сергей, 30 мая 2010 в 07:43

    использует Firefox 3.6.3 Firefox 3.6.3 на Windows XP Windows XP

    Детально разбираюсь с кодом. Маленький вопрос. В классе WJMailer такая вот функция-
    public function setTransport(WJTransport $tr){
    $this->_transport = $tr;
    }
    Здесь $tr – обект класса WJTransportAuthLogin, а параметр функции имеет класс WJTransport. Хотя, как показывает распечатка, в класс WJMailer передается объект именно класса WJTransportAuthLogin. Как-то можно это прокомментировать ?

    С уважением – Новичек.

    • Автор: web-junior, 30 мая 2010 в 10:23

      использует Firefox 3.5.9 Firefox 3.5.9 на Windows XP Windows XP

      Добрый день!

      WJTransportAuthLogin является потомком класса WJTransport. Из-за крепких родственных связей они заменяемы при передаче через параметр. Хотя заменяемость только односторонняя: потомки могут заменять родителей, но не наоборот.
      Таким образом реализуется контроль типов в php http://www.php.net/manual/en/language.oop5.typehinting.php

      • Автор: Сергей, 30 мая 2010 в 16:57

        использует Firefox 3.6.3 Firefox 3.6.3 на Windows XP Windows XP

        Спасибо за оперативный ответ.
        С уважением – Новичек.

  2. Автор: Сергей, 31 мая 2010 в 13:39

    использует Firefox 3.6.3 Firefox 3.6.3 на Windows XP Windows XP

    На многие вещи Ваша статья открыла мне глаза. Коль скоро мы научились отправлять почту, хотелось бы научиться отправлять sms-сообщения. Если у Вас нет времени, может быть намекнете, где копать ?

    С уважением – Новичек.

    • Автор: web-junior, 3 июня 2010 в 17:49

      использует Firefox 3.5.9 Firefox 3.5.9 на Windows XP Windows XP

      Добрый день!

      Для отправки смс можно использовать специально разработанные api(например, этот) или использовать специальные шлюзы(http://sms.cod3sun.com/sms-shlyuzy.php)

  3. Автор: Сергей, 9 июня 2010 в 02:37

    использует Firefox 3.6.3 Firefox 3.6.3 на Windows XP Windows XP

    Добрый день !!!
    Разбираюсь по прежнему с Вашей программой. Такая вот возникла проблема:
    ваш скрипт работает нормально на одной из моих машин , а вот на другой, “точно такой-же”, получаю ошибку от Апачи – 500: внутренняя ошибка сервера. Сравнил все конфиги (Apache и PHP) – все один к одному…Причем на этой другой машине, другие скрипты выполняются нормально….Походил по интернету – ошибка, конечно, частая и рецепты самые разные, но мне сдается, все же, что дело все в самом скрипте…
    Может быть намекнете где копать ?

    С уважением – Новичек.

  4. Автор: Сергей, 10 июня 2010 в 14:16

    использует Firefox 3.6.3 Firefox 3.6.3 на Windows XP Windows XP

    Добрый Вам день !
    С предыдущим вопросом разобрался – были противоречивые настройки Apache и PHP.

    А вот такой вопрос. SMTP-сервер получил от нас, в конечном счете, пакет
    Wjmailer -> _data, и что дальше ? Меня интересует – нельзя ли где-то, по пути следования этого пакета на POP3-сервер, перехватить его и “выкусить” из него нежелательную для нас информацию об отправителе ?

    С уважением – Новичек

  5. Автор: Сергей, 14 июля 2010 в 00:55

    использует Firefox 3.6.6 Firefox 3.6.6 на Windows XP Windows XP

    Здравствуйте !
    Столкнулся с проблемой !
    Пытаюсь вашей программой работать с сервером smtp.gmail.com,
    который требует SSL – аутентификации…
    Как тут быть ?

    • Автор: web-junior, 22 июля 2010 в 13:49

      использует Firefox 3.5.9 Firefox 3.5.9 на Windows XP Windows XP

      Добрый день!
      К сожалению, пока что SSL-шифрование библиотека не поддерживает.

  6. Автор: Борис, 27 мая 2011 в 00:34

    использует Firefox 3.5.9 Firefox 3.5.9 на Windows Vista Windows Vista

    <?php
    /*
    подключаем файлы с классами:
    wj_mailer.class.php – WJMailer – основной класс
    transport/wj_transport_auth_cram.class.php – WJTransportAuthCram – транспортный класс
    для аутентификации по методу CRAM-MD5
    */

    а должно быть

    <?php
    /*
    подключаем файлы с классами:
    wj_mailer.class.php – WJMailer – основной класс
    transportS/wj_transport_auth_cram.class.php – WJTransportAuthCram – транспортный класс
    для аутентификации по методу CRAM-MD5
    */

    • Автор: web-junior, 15 июля 2011 в 16:34

      использует Google Chrome 12.0.742.82 Google Chrome 12.0.742.82 на GNU/Linux x64 GNU/Linux x64

      Здравствуйте.
      Да, действительно, так и должно быть. Все исправил.
      Спасибо, что помогли устранить недочет.

RSS-лента комментариев. Адрес для трекбека

Ваш отзыв

Вы можете использовать следующие теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">

Нажимая на кнопку "Добавить" вы принимаете правила комментирования