POP3: Проверка почты своими руками
Сегодня я бы хотел еще раз затронуть тему сокетов и рассказать про то, как я изучал протокол POP3 для проверки почтового ящика.
Аналогичную мою статью про SMTP Вы можете почитать здесь. |
Я расскажу, как можно сделать проверку почтового ящика двумя методами в PHP:
- Написать свой простой POP3-клиент
- Использовать готовый модуль IMAP для PHP
Пишем простой POP3-клиент
Поставим себе задачу: написать скрипт, который будет проверять заданный почтовый ящик и выводить список всех писем, в котором будут тема сообщения, дата и размер в килобайтах.
На самом деле протокол POP3 гораздо проще SMTP. Сначала рассмотрим сам процесс общения с сервером.
Я говорил в статье про отправку SMTP с авторизацией, что у меня есть Windows с настроенными SMTP- и POP3-серверами. Я решил пообщаться со своим локальным сервером через telnet, набрав в консоли команду:
telnet localhost 110
И далее приведен процесс общения (S - ответы сервера, C - мои команды):
<b>C:</b> user novice@localhost.ru<br />
<b>S:</b> +OK<br />
<b>C:</b> pass 123456<br />
<b>S:</b> +OK User successfully logged on<br />
<b>C:</b> stat<br />
<b>S:</b> +OK 1 774<br />
<b>C:</b> top 1 0<br />
<b>S:</b> +OK 774 octects<br />
<b>S:</b> Received: from novicemachine ([127.0.0.1]) by novicemachine with Microsoft SMTPSVC<br />
<b>S:</b> (6.0.3790.3959);<br />
<b>S:</b> Thu, 21 Aug 12:54:17 +0400<br />
<b>S:</b> Message-ID: <04D9EF87ADC14A2497BBCD509DE68973@novicemachine><br />
<b>S:</b> From: "Novice" <novice@localhost.ru><br />
<b>S:</b> To: <novice@localhost.ru><br />
<b>S:</b> Subject: =?koi8-r?B?9MXNwSDTz8/C3cXOydE=?=<br />
<b>S:</b> Date: Thu, 21 Aug 12:54:17 +0400<br />
<b>S:</b> MIME-Version: 1.0<br />
<b>S:</b> Content-Type: text/plain;<br />
<b>S:</b> format=flowed;<br />
<b>S:</b> charset="koi8-r";<br />
<b>S:</b> reply-type=original<br />
<b>S:</b> Content-Transfer-Encoding: 8bit<br />
<b>S:</b> X-Priority: 3<br />
<b>S:</b> X-MSMail-Priority: Normal<br />
<b>S:</b> X-Mailer: Microsoft Outlook Express 6.00.3790.3959<br />
<b>S:</b> X-MimeOLE: Produced By Microsoft MimeOLE V6.00.3790.3959<br />
<b>S:</b> Return-Path: novice@localhost.ru<br />
<b>S:</b> X-OriginalArrivalTime: 21 Aug 08:54:17.0434 (UTC) FILETIME=[7E6EBBA0:01C9036B]<br />
<b>S:</b><br />
<b>S:</b><br />
<b>S:</b> .<br />
<b>C:</b> list 1<br />
<b>S:</b> +OK 1 774<br />
<b>C:</b> quit<br />
<b>S:</b> +OK Microsoft Windows POP3 Service Version 1.0 <474514105@novicemachine> signing off.
Что мы тут сделали. Мы залогинились на сервер (команды USER и PASS), выяснили кол-во сообщений в почтовом ящике и их общий размер (команда STAT), дали команду серверу вернуть нам все заголовки первого сообщения (команда TOP), вернуть размер первого сообщения (команда LIST) и отсоединились от сервера (команда QUIT).
Это все команды, которые нам нужны для поставленной задачи.
Кстати, мы залогинились самым простым способом - открыто передали пароль. Но можно логиниться и более безопасно. Для этого существуют команды APOP и AUTH, которые выходят за рамки нашей статьи.
Заметим, что в ответах POP3-сервера нет всяких цифровых кодов, как в SMTP. Тут есть просто плюсы и минусы. Если сервер ответил Вам строкой, которая начинается со знаком + (чаще всего +OK), то Ваш запрос был верным. В противном случае знак минус (-ERR) показывает, что Ваш запрос был задан неверно или с неверными параметрами (например, неправильный пароль для почтового ящика).
Рассмотрим использованные команды более подробно.
USER
Формат: USER <имя_пользователя>
Назначение: сообщает имя пользователя серверу, чтобы залогиниться
PASS
Формат: PASS <пароль>
Назначение: сообщает пароль серверу, чтобы залогиниться
STAT
Формат: STAT
Назначение: вернуть кол-во писем в почтовом ящике и их общий размер в байтах
Возвращаемое значение: +OK <кол-во_писем> <размер_в_байтах>
TOP
Формат: TOP <номер_сообщения> <кол-во_строк_тела_сообщения>
Назначение: вернуть все заголовки сообщения и заданное кол-во строк тела сообщения (может быть нулем)
LIST
Формат: LIST [номер_сообщения]
Назначение: определить размер заданного сообщения (если номер не задан - возвращает размер для каждого сообщения)
Возвращаемое значение: +OK <номер_сообщения> <размер_в_байтах>
QUIT
Формат: QUIT
Назначение: разъединение с сервером
Это не все команды, которые поддерживаются POP3-сервером. Есть еще и другие, о которых Вы можете узнать, например, здесь.
Итак, из поставленной задачи нам нужно определить тему каждого сообщения, его дату и размер. С размером все понятно. Но как быть с темой и датой?
Дата хранится в заголовке:
Date: Thu, 21 Aug 12:54:17 +0400
Чтобы привести ее в другой вид, можно использовать связку функций date-strtotime:
echo date(‘d.m.Y H:i:s’, strtotime(‘Thu, 21 Aug 12:54:17 +0400′));
Выведет дату и время в привычном нам виде: 21.08 12:54:17
С датой все понятно. Осталось разобраться с темой, и можно писать скрипт. Тут есть небольшая проблемка, потому что тема хранится зашифрованная в base64:
Subject: =?koi8-r?B?9MXNwSDTz8/C3cXOydE=?=
Да и еще к тому же в кодировке KOI-8 в нашем случае, хотя могла бы быть и в windows-1251 и любой другой. Как же ее раскодировать? На самом деле это проблема и для этого существуют даже отдельные библиотеки.
Одну из них я нашел на [ссылка], и называется она «MIME E-mail message parser». Ее-то я и использовал, чтобы узнать, что за тема в сообщении закодирована. Оказалось вот что: «Тема сообщения»
Ну, не буду испытывать Ваше терпение, а приведу здесь код нашего скрипта:
index1.php
<? // Включаем библиотеку mime parser require_once('rfc822_addresses.php'); require_once('mime_parser.php'); $mime = new mime_parser_class; header('Content-Type: text/plain;'); error_reporting(E_ALL ^ E_WARNING); ob_implicit_flush(); $address = 'localhost'; // адрес pop3-сервера $port = 110; // порт (стандартный pop3 - 110) $login = 'novice'; // логин к ящику $pwd = 'novice'; // пароль к ящику try { // Создаем и соединяем сокет к серверу echo 'Connect to \''.$address.':'.$port.'\' ... '; $socket = fsockopen($address, $port, $errno, $errstr); if (!$socket) { throw new Exception('fsockopen() failed: '.$errstr."\n"); } echo "OK\n"; // Читаем информацию о сервере read_pop3_answer($socket); // Делаем авторизацию echo 'Authentication ... '; write_pop3_response($socket, 'USER '.$login); read_pop3_answer($socket); // ответ сервера write_pop3_response($socket, 'PASS '.$pwd); read_pop3_answer($socket); // ответ сервера echo "OK\n"; // Определяем кол-во сообщений в ящике и общий размер write_pop3_response($socket, 'STAT'); $answer = read_pop3_answer($socket); // ответ сервера preg_match('!([0-9]+)[[:space:]]([0-9]+)!is', $answer, $matches); $total_count = $matches[1]; echo "\n".'Total messages: '.$total_count."\n"; if ($total_count > 0) { echo 'Total size, Kb: '.ceil($matches[2] / 1024)."\n\n"; } // Просматриваем параметры каждого сообщения for ($i = 1; $i <= $total_count; $i++) { write_pop3_response($socket, 'TOP '.$i.' 0'); $answer = read_pop3_answer($socket, true); write_pop3_response($socket, 'LIST '.$i); $answer2 = read_pop3_answer($socket); // Определяем размер сообщения preg_match('!^\+[A-Za-z]+[[:space:]]+[0-9]+[[:space:]]+([0-9]+)!is', $answer2, $matches); echo 'Message '.$i.' size, Kb: '.ceil($matches[1] / 1024)."\n"; // Определяем дату сообщения preg_match('!Date:[[:space:]]+(.*?)\n+.*!is', $answer, $matches); echo 'Message '.$i.' date: '.date('d.m.Y H:i:s', strtotime($matches[1]))."\n"; // Определяем тему сообщения preg_match('!Subject:[[:space:]]+(.*?)\n+.*!is', $answer, $matches); $msg_subject = ''; if ($mime->Decode(Array('Data' => $answer), $decoded)) { if ($mime->Analyze($decoded[0], $results)) { $msg_subject = $results['Subject']; } } if (!empty($msg_subject)) { echo 'Message '.$i.' subject: '.$msg_subject."\n"; } } // Отсоединяемся от сервера echo "\n".'Close connection ... '; write_pop3_response($socket, 'QUIT'); read_pop3_answer($socket); // ответ сервера echo "OK\n"; } catch (Exception $e) { echo "\nError: ".$e->getMessage(); } if (isset($socket)) { fclose($socket); } // Функция для чтения ответа сервера. Выбрасывает исключение в случае ошибки function read_pop3_answer($socket, $top = false) { $read = fgets($socket); if ($top) { // Если читаем заголовки $line = $read; while (!ereg("^\.\r\n", $line)) { $line = fgets($socket); $read .= $line; } } if ($read{0} != '+') { if (!empty($read)) { throw new Exception('POP3 failed: '.$read."\n"); } else { throw new Exception('Unknown error'."\n"); } } return $read; } // Функция для отправки запроса серверу function write_pop3_response($socket, $msg) { $msg = $msg."\r\n"; fwrite($socket, $msg); } ?>
Скрипт вывел в окно браузера:
Authentication ... OK<br />
<br />
Total messages: 1<br />
Total size, Kb: 1<br />
<br />
Message 1 size, Kb: 1<br />
Message 1 date: 21.08 12:54:17<br />
Message 1 subject: Тема сообщения<br />
<br />
Close connection ... OK
Т.е. мы видим, что у нас в ящике лежит одно сообщение с темой «Тема сообщения», размером 1 Кб и датой от 21 августа года.
Это мы написали POP3-клиента вручную. Теперь же не будем изобретать велосипед, а попробуем использовать IMAP - сделаем абсолютно то же самое, но с меньшими затратами сил. Только перед этим нужно убедиться, что расширение IMAP подключено к PHP.
Юзаем IMAP
index2.php
<? // Включаем библиотеку mime parser require_once('rfc822_addresses.php'); require_once('mime_parser.php'); $mime = new mime_parser_class; header('Content-Type: text/plain;'); error_reporting(E_ALL ^ E_WARNING ^ E_NOTICE); ob_implicit_flush(); $address = 'localhost'; // адрес pop3-сервера $port = 110; // порт (стандартный pop3 - 110) $login = 'novice'; // логин к ящику $pwd = 'novice'; // пароль к ящику try { // Соединяемся с сервером и делаем авторизацию echo 'Connect to \''.$address.':'.$port.'\' ... '; $mbox = imap_open('{'.$address.':'.$port.'/pop3}INBOX', $login, $pwd); if (!$mbox) { throw new Exception('imap_open() failed: '.imap_last_error()."\n"); } echo "OK\n"; echo 'Authentication ... OK'."\n"; // Определяем кол-во сообщений в ящике и общий размер $total_count = imap_num_msg($mbox); echo "\n".'Total messages: '.$total_count."\n"; if ($total_count > 0) { $minfo = imap_mailboxmsginfo($mbox); $total_size = ceil($minfo->Size / 1024); echo 'Total size, Kb: '.$total_size."\n\n"; } // Просматриваем параметры каждого сообщения for ($i = 1; $i <= $total_count; $i++) { // Определяем размер сообщения $msg_size = imap_headerinfo($mbox, $i)->Size; echo 'Message '.$i.' size, Kb: '.ceil($msg_size / 1024)."\n"; // Определяем дату сообщения $msg_date = imap_headerinfo($mbox, $i)->udate; echo 'Message '.$i.' date: '.date('d.m.Y H:i:s', $msg_date)."\n"; // Определяем тему сообщения $msg_subject = imap_headerinfo($mbox, $i)->subject; if ($mime->Decode(Array('Data' => 'Subject: '.$msg_subject), $decoded)) { if ($mime->Analyze($decoded[0], $results)) { $msg_subject = $results['Subject']; } } if (!empty($msg_subject)) { echo 'Message '.$i.' subject: '.$msg_subject."\n"; } echo "\n".'Close connection ... '; echo "OK\n"; } } catch (Exception $e) { echo "\nError: ".$e->getMessage(); } if (isset($mbox)) { imap_close($mbox); } ?>
Как видим, с помощью IMAP получить доступ к своим письмам гораздо легче.
Эх, я же забыл в списке писем выводить адрес отправителя. Ну это, я думаю, Вы сделаете сами
Приведенные примеры с библиотекой для распознавания mime-заголовков можете скачать здесь.
Пожалуй, на этом все. Желаю удачи в написании собственных POP3-клиентов на PHP!
спасибо.
быстро попробывал.
пока правда Notice: Undefined offset: 0 in /mime_parser.php on line 2197
но думаю это поправимо:)
А если надо проверить много почты?
Как быть тогда?
Скорость заметна будет?
хм.. а не проще юзать стандартные функции декодирования из ASCII в KOI8-R, а KOI8-R в WIN-1251 или UTF-8, не прибегая к такому гигантскому классу?
$text_win = convert_cyr_string(base64_decode($text), ‘k’, ‘w’);
Лично я делаю так с декодированием:
$from = imap_mime_header_decode($mes->fromaddress);
if($from[0]->charset != “default”)
$from = iconv($from[0]->charset, “UTF-8″, $from[0]->text);
Здравствуйте !
использовл ваш скрипт , но в том месте где он считает количество сообщений … выводит почему то 0, хотя на почте хранится 2 письма… в чем причина ?
А как выбрать письма только за два последних дня? не прибегая к заносу их всех в массив?
их ведь м.б. и 10000+1???
поробовал код. есть ошибка на 77 строке, это где выводиться сама тема
$msg_subject = $results[‘Subject’];
А пишет что
Undefined index:
Это ошибка в подключаемом файле или вывод с ошибкой.
Буду разбираться.
Всё равно спасибо за статью. Очень грамотно написана.