POP3: Проверка почты своими руками
![]() |
Сегодня я бы хотел еще раз затронуть тему сокетов и рассказать про то, как я изучал протокол POP3 для проверки почтового ящика.
Аналогичную мою статью про SMTP Вы можете почитать здесь. |
Я расскажу, как можно сделать проверку почтового ящика двумя методами в PHP:
- Написать свой простой POP3-клиент
- Использовать готовый модуль IMAP для PHP
Пишем простой POP3-клиент
Поставим себе задачу: написать скрипт, который будет проверять заданный почтовый ящик и выводить список всех писем, в котором будут тема сообщения, дата и размер в килобайтах.
На самом деле протокол POP3 гораздо проще SMTP. Сначала рассмотрим сам процесс общения с сервером.
Я говорил в статье про отправку SMTP с авторизацией, что у меня есть Windows 2003 с настроенными 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 2008 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 2008 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 2008 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 2008 12:54:17 +0400
Чтобы привести ее в другой вид, можно использовать связку функций date-strtotime:
echo date(‘d.m.Y H:i:s’, strtotime(‘Thu, 21 Aug 2008 12:54:17 +0400′));
Выведет дату и время в привычном нам виде: 21.08.2008 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.2008 12:54:17<br />
Message 1 subject: Тема сообщения<br />
<br />
Close connection ... OK
Т.е. мы видим, что у нас в ящике лежит одно сообщение с темой «Тема сообщения», размером 1 Кб и датой от 21 августа 2008 года.
Это мы написали 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!









Март 10th, 2009
спасибо.
быстро попробывал.
пока правда Notice: Undefined offset: 0 in /mime_parser.php on line 2197
но думаю это поправимо:)
Февраль 8th, 2010
А если надо проверить много почты?
Как быть тогда?
Скорость заметна будет?
Апрель 4th, 2010
хм.. а не проще юзать стандартные функции декодирования из ASCII в KOI8-R, а KOI8-R в WIN-1251 или UTF-8, не прибегая к такому гигантскому классу?
$text_win = convert_cyr_string(base64_decode($text), ‘k’, ‘w’);
Май 15th, 2010
Лично я делаю так с декодированием:
$from = imap_mime_header_decode($mes->fromaddress);
if($from[0]->charset != “default”)
$from = iconv($from[0]->charset, “UTF-8″, $from[0]->text);
Февраль 28th, 2011
Здравствуйте !
использовл ваш скрипт , но в том месте где он считает количество сообщений … выводит почему то 0, хотя на почте хранится 2 письма… в чем причина ?