|
Статьи
Чтение почты с помощью PHP через POP3
В своей предыдущей статье я писал про отправку почты через SMTP с помощью php, так вот получение почты очень похоже, тоже отправлять команды и получать ответы от сервера. Вот только анализировать полученную информацию не так просто. В этой статье я постараюсь рассказать, как правильно обрабатывать полученную информацию от сервера. Общение клиент-сервер происходит точно так же, как и при общении с SMTP сервером, только команды другие.
Для начала сделаем функцию, для получения данных от сервера.
<?php
function get_data($pop_conn)
{
$data = "";
while ( !feof($pop_conn) ) {
$buffer = chop(fgets($pop_conn,1024));
$data .= $buffer."\r\n";
if( trim($buffer) == "." ) break;
}
return $data;
}
?>
Эта функция будет получать данные от сервера, которые при выводе занимают более одной строки. Для однострочных ответов будем использовать просто
<?php fgets($pop_conn, 1024); ?>
Начинаем коннект к POP-серверу, на примере бесплатного почтовика bk.ru:
<?php
$pop_conn = fsockopen("pop.bk.ru", 110, $errno, $errstr, 10);
print fgets($pop_conn, 1024);
?>
Если все нормально, ответ будет
+OK
Теперь нужно авторизоваться. Посылаем команду USER с указанием имени пользователя
<?php
fputs($pop_conn, "USER webi\r\n");
print fgets($pop_conn, 1024);
?>
Ответ
+OK Password required for user webi
Вводим пароль с помощью команды PASS
<?php
fputs($pop_conn, "PASS password\r\n");
print fgets($pop_conn, 1024);
?>
+OK webi@bk.ru maildrop has 2 messages (8192 octets)
В данном ответе говорится, что в ящике имеется 2 письма общим размером 8192 байт. Сейчас авторизация пройдена и можно начинать получать информацию. Например, команда STAT
<?php
fputs($pop_conn, "STAT\r\n");
print fgets($pop_conn, 1024);
?>
покажет сколько писем в ящике и их общий размер
+OK 2 8192
Можно получить более подробную информацию по каждому письму с помощью команды LIST :
<?php
fputs($pop_conn, "LIST\r\n");
print get_data($pop_conn);
?>
В данном примере уже используется функция получения данных, так как ответ будет уже больше одной строки. Получим такой ответ
+OK 2 messages (8192 octets)
1 6654
2 1372
.
Показывает общую информацию и показывает размер каждого письма. Цифры 1 и 2 в данном случае номера писем, именно по этим номерам и можно будет обращаться к письмам. Заканчивается многострочный ответ точкой.
С помощью команды LIST можно получить информацию о размере любого письма, например второго.
<?php
fputs($pop_conn, "LIST 2\r\n");
print fgets($pop_conn);
?>
Вот такой ответ
+OK 2 1372
Второе письмо 1372 байта.
Теперь рассмотрим команду TOP . Она показывает заголовки письма и несколько строк самого письма.
<?php
fputs($pop_conn, "TOP 1 3\r\n");
print get_data($pop_conn);
?>
Ответ
+OK
Received: from [212.164.71.38] (port=43490 helo=imx2.ngs.ru)
by mx26.mail.ru with esmtp
Date: Mon, 27 Aug 2007 18:08:00 +0700
X-Mailer: The Bat! (v3.99.3) Professional
X-Priority: 3 (Normal)
X-Spam: Not detected
[...тут идут дальше заголовки, я их вырезал, в целях экономии места...]
Здравствуйте.
это идет текст письма...
.
В данном примере я вырезал часть заголовков, чтобы не засорять лишними текстами страницу. Здесь показаны три строки первого письма (TOP 1 3 ). И обратите внимание, что многострочный ответ заканчивается точкой.
Сейчас рассмотрим удаление писем.
<?php
fputs($pop_conn, "DELE 2\r\n");
print fgets($pop_conn);
?>
Ответ
+OK message 2 deleted
В данном случае удалилось второе письмо. Точнее сказать не удалилось, а пометилось для удаления. Помеченные письма удалятся только после завершения работы с сервером, т.е. после того, как будет отправлена команда QUIT.
Если есть необходимость отменить пометки об удалении, можно воспользоваться такой командой RSET
<?php
fputs($pop_conn, "RSET\r\n");
print fgets($pop_conn);
?>
Ответ
+OK maildrop has 2 messages (8192 octets)
Все основные команды рассмотрены, остались две, это чтение письма и закрытие сессии. Закрывать соединение нужно обязательно, чтобы помеченные к удалению письма удалились.
<?php
fputs($pop_conn, "RETR 1\r\n");
print get_data($pop_conn);
fputs($pop_conn, "QUIT\r\n");
?>
Ответ на эту команду будет естественно вывод первого письма, всего полностью, вместе со всеми заголовками и после этого идет команда выхода. И вот тут то и начинается самое интересное. Как видно из примеров, получить письмо, это дело не хитрое, а теперь ведь нужно его разобрать, если в нем только текст, то достаточно отбросить заголовки и оставить тело. Но не все предусматривают письма, закодированные 7bit, 8 bit, а еще вложенные файлы...
Попробую продемонстрировать разбор заголовков на примерах, ну а дальше уже все будет понятно. Обязательно почитайте Почтовый стандарт "MIME" (RFC1521) на нем все основано, и про заголовки и про кодирование тут все написано. Я приведу только небольшой пример, чтобы все предусмотреть, нужно знать стандарт.
Вот минимальный набор функций, которые пригодятся. Расписывать все действия внутри функций не буду, только опишу кратко предназначение каждой функции.
<?php
// При отправке почты, все не латинские символы в заголовках кодируется,
// например тема письма может выглядеть так =?windows-1251?B?7/Du4uXw6uA=?=
// вот такие тексты и будет преобразовывать эта функция
function decode_mime_string($subject) {
$string = $subject;
if( ($pos = strpos($string,"=?")) === false ) return $string;
while(!($pos === false)) {
$newresult .= substr($string,0,$pos);
$string = substr($string, $pos+2, strlen($string));
$intpos = strpos($string, "?");
$charset = substr($string, 0, $intpos);
$enctype = strtolower(substr($string, $intpos+1, 1));
$string = substr($string, $intpos+3, strlen($string));
$endpos = strpos($string, "?=");
$mystring = substr($string, 0, $endpos);
$string = substr($string, $endpos+2, strlen($string));
if($enctype == "q")
$mystring = quoted_printable_decode(ereg_replace("_", " ", $mystring));
else if ($enctype == "b")
$mystring = base64_decode($mystring);
$newresult .= $mystring;
$pos = strpos($string, "=?");
}
$result = $newresult.$string;
if(ereg("koi8", $subject)) $result = convert_cyr_string($result, "k", "w");
if(ereg("KOI8", $subject)) $result = convert_cyr_string($result, "k", "w");
return $result;
}
// Перекодировщик тела письма. Само письмо может быть закодировано
// и данная функция приводит тело письма в нормальный вид.
// Так же и вложенные файлы будут перекодироваться этой функцией.
function compile_body($body, $enctype, $ctype) {
$enctype = explode(" ", $enctype);
$enctype = $enctype[0];
if(strtolower($enctype) == "base64")
$body = base64_decode($body);
else if(strtolower($enctype) == "quoted-printable")
$body = quoted_printable_decode($body);
if(ereg("koi8", $ctype)) $body = convert_cyr_string($body, "k", "w");
return $body;
}
// Функция для выдергивания метки boundary из заголовка Content-Type
// boundary это разделитель между разным содержимым в письме,
// например, чтобы отделить файл от текста письма
function get_boundary($ctype){
if(preg_match('/boundary[ ]?=[ ]?(["]?.*)/i', $ctype, $regs)) {
$boundary = preg_replace('/^\"(.*)\"$/', "\\1", $regs[1]);
return trim("--$boundary");
}
}
// Если письмо будет состоять из нескольких частей (текст, файлы и т.д.) то эта
// функция разобьет такое письмо на части (в массив), согласно разделителю boundary
function split_parts($boundary, $body) {
$startpos = strpos($body, $boundary)+strlen($boundary)+2;
$lenbody = strpos($body, "\r\n$boundary--") - $startpos;
$body = substr($body,$startpos,$lenbody);
return explode($boundary."\r\n", $body);
}
// Эта функция отделяет заголовки от тела.
// и возвращает массив с заголовками и телом
function fetch_structure($email) {
$ARemail = Array();
$separador = "\r\n\r\n";
$header = trim(substr($email, 0, strpos($email, $separador)));
$bodypos = strlen($header)+strlen($separador);
$body = substr($email,$bodypos,strlen($email)-$bodypos);
$ARemail["header"] = $header;
$ARemail["body"] = $body;
return $ARemail;
}
// Разбирает все заголовки и выводит массив, в котором
// каждый элемент является соответсвующим заголовком
function decode_header($header) {
$headers = explode("\r\n", $header);
$decodedheaders = Array();
for($i=0; $i<count($headers); $i++) {
$thisheader = trim($headers[$i]);
if(!empty($thisheader)) {
if(!ereg("^[A-Z0-9a-z_-]+:",$thisheader)) {
$decodedheaders[$lasthead] .= " $thisheader";
} else {
$dbpoint = strpos($thisheader, ":");
$headname = strtolower(substr($thisheader, 0, $dbpoint));
$headvalue = trim(substr($thisheader, $dbpoint+1));
if($decodedheaders[$headname] != "")
$decodedheaders[$headname] .= "; $headvalue";
else
$decodedheaders[$headname] = $headvalue;
$lasthead = $headname;
}
}
}
return $decodedheaders;
}
// Эта функция нам уже знакома. она получает данные и реагирует
// на точку, которая ставится сервером в конце вывода.
function get_data($pop_conn)
{
$data = "";
while ( !feof($pop_conn) ) {
$buffer = chop( fgets($pop_conn, 1024) );
$data .= "$buffer\r\n";
if(trim($buffer) == ".") break;
}
return $data;
}
?>
Вот имея эти функции можно уже начать работать с почтой. Предположим, читаем первое письмо
<?php
$pop_conn = fsockopen("pop.bk.ru", 110,$errno, $errstr, 10);
$code = fgets($pop_conn, 1024);
fputs($pop_conn,"USER webi\r\n");
$code = fgets($pop_conn, 1024);
fputs($pop_conn,"PASS password\r\n");
$code = fgets($pop_conn, 1024);
fputs($pop_conn, "RETR 1\r\n");
$text .= get_data($pop_conn);
// в переменной $text сейчас все письмо вместе с заголовками.
// разделяем письмо на заголовки и тело
$struct = fetch_structure($text);
// Теперь раскладываем заголовки по полочками получаем
// удобный ассоциативный массив с удобным обращением к любому заголовку.
// Например $mass_header['subject'] == "=?windows-1251?B?7/Do4uXy?="
$mass_header=decode_header($struct['header']);
// Чтобы воспользоваться заголовком, который может содержать не латинские символы,
// например тема письма, нужно прогнать заголовок через функцию декодирования.
$mass_header["subject"] = decode_mime_string($mass_header["subject"]);
// теперь можно использовать тему, теперь тут обычный читаемый текст
// Сейчас разберем заголовок Content-Type, это тип содержимого.
// Определим, что в письме, только текст или еще и файлы.
// Content-Type: text/plain; charset=Windows-1251 - это обычное текстовое письмо
// Content-Type: multipart/mixed; boundary="_----------=_118224799143839" -
// это составное письмо из нескольких частей, с вложенными файлами.
$type = $ctype = $mass_header['content-type'];
$ctype = split(";",$ctype);
$types = split("/",$ctype[0]);
$maintype = trim(strtolower($types[0])); // text или multipart
$subtype = trim(strtolower($types[1])); // а это подтип (plain, html, mixed)
// Сейчас проверяем тип содержимого письма:
// если это обычное текстовое содержимое (текст или html) без вложений
if($maintype=="text")
{
// $subtype можно использовать эту переменную для определения текстовое письмо или html
// эту проверку можете поставить сами
// Передаем тело письма в функцию, на перекодирование. И так же посылаем заголовки,
// информирующие о том, как было закодировано письмо.
$body = compile_body($struct['body'], $mass_header["content-transfer-encoding"], $mass_header["content-type"]);
print $body;
}
// Теперь рассмотрим вариант, если письмо имеет несколько разных частей.
// Тут рассматриваю подтипы signed,mixed,related, но есть еще подтип alternative,
// который служит для альтернативного отображения письма.
// Например, письмо в html и к нему же можно добавить альтернативное текстовое содержание.
else if($maintype=="multipart" and ereg($subtype,"signed,mixed,related"))
{
// получаем метку-разделитель частей письма
$boundary=get_boundary($mass_header['content-type']);
// на основе этого разделителя разбиваем письмо на части
$part = split_parts($boundary,$struct['body']);
// теперь обрабатываем каждую часть письма
for($i=0;$i<count($part);$i++)
{
// разбиваем текущую часть на тело и заголовки
$email = fetch_structure($part[$i]);
$header = $email["header"];
$body = $email["body"];
// разбираем заголовки на массив
$headers = decode_header($header);
$ctype = $headers["content-type"];
$cid = $headers["content-id"];
$Actype = split(";", $headers["content-type"]);
$types = split("/",$Actype[0]);
$rctype = strtolower($Actype[0]);
// теперь проверяем, является ли эта часть прикрепленным файлом
$is_download = ( ereg("name=",$headers["content-disposition"].$headers["content-type"]) ||
$headers["content-id"] != "" || $rctype == "message/rfc822" );
// теперь читаем и выводим само тело части
if($rctype == "text/plain" && !$is_download) { // если это обычный текст
$body = compile_body($body,$headers["content-transfer-encoding"],$headers["content-type"]);
print $body;
}
else if($rctype == "text/html" && !$is_download) // если это html
{
$body = compile_body($body,$headers["content-transfer-encoding"],$headers["content-type"]);
print $body;
}
elseif($is_download) // и наконец, если это файл
{
// Имя файла можно выдернуть из заголовков Content-Type или Content-Disposition
$cdisp = $headers["content-disposition"];
$ctype = $headers["content-type"];
$ctype2 = explode(";",$ctype);
$ctype2 = $ctype2[0];
$Atype = split("/", $ctype);
$Acdisp = split(";", $cdisp);
$fname = $Acdisp[1];
if(ereg("filename=(.*)", $fname, $regs)) $filename = $regs[1];
if($filename == "" && ereg("name=(.*)",$ctype,$regs)) $filename = $regs[1];
$filename = ereg_replace("\"(.*)\"", "\\1", $filename);
// как получили имя файла, теперь его нужно декодировать
$filename = trim(decode_mime_string($filename));
// теперь читаем файл в переменную.
$body = compile_body($body, $headers["content-transfer-encoding"], $ctype);
// содержимое файла теперь в переменной $body и сейчас можно отдать содержимое
// файла в браузер или например сохранить на диске
$ft = fopen($filename, "wb");
fwrite($ft, $body);
fclose($ft);
}
}
}
?>
Вот такой вот полностью рабочий пример работы с почтой. Конечно, я не стал предусматривать все возможные варианты писем, существуют еще типы multipart/alternative , multipart/digest . Возможно что-то еще упустил... Вам достаточно даже бегло ознакомиться с почтовым стандартом, поизучать заголовки писем и сможете обрабатывать письма любых форматов.
Хочу предупредить, если вы будете разглядывать заголовки писем через The Bat, то лучше сохранять письмо в файл и открыв в текстовом редакторе уже его изучать. Так как The Bat скрывает заголовки писем состоящих из нескольких частей, то есть показывает только основной заголовок письма.
Источник: http://webi.ru
|
|