POP3: Проверка почты своими руками

Сегодня я бы хотел еще раз затронуть тему сокетов и рассказать про то, как я изучал протокол POP3 для проверки почтового ящика.

Аналогичную мою статью про SMTP Вы можете почитать здесь.

Я расскажу, как можно сделать проверку почтового ящика двумя методами в PHP:

  • Написать свой простой POP3-клиент
  • Использовать готовый модуль IMAP для PHP

Пишем простой POP3-клиент

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

На самом деле протокол POP3 гораздо проще SMTP. Сначала рассмотрим сам процесс общения с сервером.

Я говорил в статье про отправку SMTP с авторизацией, что у меня есть Windows с настроенными SMTP- и POP3-серверами. Я решил пообщаться со своим локальным сервером через telnet, набрав в консоли команду:

telnet localhost 110

И далее приведен процесс общения (S - ответы сервера, C - мои команды):

<b>S:</b> +OK Microsoft Windows POP3 Service Version 1.0 <474514105@novicemachine> ready.<br />
<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);
	}
?>

Скрипт вывел в окно браузера:

Connect to 'localhost:110' ... OK<br />
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! :)





Читайте также:



7 Ответов на “POP3: Проверка почты своими руками”

  1. спасибо.

    быстро попробывал.
    пока правда Notice: Undefined offset: 0 in /mime_parser.php on line 2197

    но думаю это поправимо:)

  2. Nurbek

    А если надо проверить много почты?

    Как быть тогда?

    Скорость заметна будет?

  3. Сергей

    хм.. а не проще юзать стандартные функции декодирования из ASCII в KOI8-R, а KOI8-R в WIN-1251 или UTF-8, не прибегая к такому гигантскому классу?

    $text_win = convert_cyr_string(base64_decode($text), ‘k’, ‘w’);

  4. Лично я делаю так с декодированием:

    $from = imap_mime_header_decode($mes->fromaddress);
    if($from[0]->charset != “default”)
    $from = iconv($from[0]->charset, “UTF-8″, $from[0]->text);

  5. den

    Здравствуйте !
    использовл ваш скрипт , но в том месте где он считает количество сообщений … выводит почему то 0, хотя на почте хранится 2 письма… в чем причина ?

  6. А как выбрать письма только за два последних дня? не прибегая к заносу их всех в массив?
    их ведь м.б. и 10000+1???

  7. Nevredimiy

    поробовал код. есть ошибка на 77 строке, это где выводиться сама тема
    $msg_subject = $results[‘Subject’];
    А пишет что
    Undefined index:
    Это ошибка в подключаемом файле или вывод с ошибкой.
    Буду разбираться.
    Всё равно спасибо за статью. Очень грамотно написана.


© Copyright. . I-Novice. All Rights Reserved. Terms | Site Map