Чтение почты с помощью php через pop302.09.2007 сайт автора: http://webi.ru
публикация данной статьи разрешена только со ссылкой на сайт автора статьиДополнительная документация на русском:
Почтовый стандарт "MIME" (RFC1521) - поможет разобраться с заголовками и с закодированным содержанием писем. Описание почтового протокола POP3 - список команд и описание.
Обязательно ознакомьтесь с этой документацией, я приведу только не большой пример работы с почтой.
В своей предыдущей статье я писал про отправку почты через SMTP с помощью php, так вот получение почты очень похоже, тоже отправлять команды и получать ответы от сервера. Вот только анализировать полученную информацию не так просто... В этой статье я постараюсь рассказать, как правильно обрабатывать полученную информацию от сервера. Общение клиент-сервер происходит точно так же, как и при общении с smtp сервером, только команды другие.
Для начала сделаем функцию, для получения данных от сервера.
<?
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;
}
?>
Эта функция будет получать данные от сервера, которые при выводе занимают более одной строки.
Для однострочных ответов будем использовать просто
<? fgets($pop_conn,1024); ?>
Начинаем коннект к pop - серверу, на примере бесплатного почтовика bk.ru от mail.ru
<? $pop_conn = fsockopen("pop.bk.ru", 110,$errno, $errstr, 10);
print fgets($pop_conn,1024); ?>
Если все нормально, ответ будет
+OK
Теперь нужно авторизоваться. Посылаем команду USER с указанием имени пользователя
<? fputs($pop_conn,"USER webi\r\n"); print fgets($pop_conn,1024); ?>
Ответ
+OK Password required for user webi
Вводим пароль с помощью команды PASS
<? 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
<? fputs($pop_conn,"STAT\r\n"); print fgets($pop_conn,1024); ?>
Покажет сколько писем в ящике и их общий размер
+OK 2 8192
Можно получить более подробную информацию по каждому письму с помощью команды LIST. Вариант команды без параметров В данном примере уже используется функция получения данных, так как ответ будет уже больше одной строки.
<? fputs($pop_conn,"LIST\r\n"); print get_data($pop_conn); ?>
Покажет такой ответ
+OK 2 messages (8192 octets) 1 6654 2 1372 .
Показывает общую информацию и показывает размер каждого письма. Цифры 1 и 2 в данном случае номера писем, именно по этим номерам и можно будет обращаться к письмам. Заканчивается многострочный ответ точкой.
С помощью команды LIST можно получить информацию о размере любого письма, например второго.
<? fputs($pop_conn,"LIST 2\r\n"); print fgets($pop_conn); ?>
Вот такой ответ
+OK 2 1372
Второе письмо 1372 байта.
Теперь рассмотрим команду TOP. Она показывает заголовки письма и несколько строк самого письма.
<? 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). И обратите внимание, что многострочный ответ заканчивается точкой...
Сейчас рассмотрим удаление писем.
<? fputs($pop_conn,"DELE 2\r\n"); print fgets($pop_conn); ?>
Ответ
+OK message 2 deleted
В данном случае удалилось второе письмо. Точнее сказать не удалилось, а пометилось для удаления. Помеченные письма удалятся только после завершения работы с сервером, т.е. после того, как будет отправлена команда QUIT. Если есть необходимость отменить пометки об удалении, можно воспользоваться такой командой
<? fputs($pop_conn,"RSET\r\n"); print fgets($pop_conn); ?>
Ответ +OK maildrop has 2 messages (8192 octets)
Все основные команды рассмотрены, остались две, это чтение письма и закрытие сессии. Закрывать соединение нужно обязательно, чтобы помеченные к удалению письма удалились.
<? fputs($pop_conn,"RETR 1\r\n"); print get_data($pop_conn); fputs($pop_conn,"QUIT\r\n"); ?>
Ответ на эту команду будет естественно вывод первого письма, всего полностью, вместе со всеми заголовками и после этого идет команда выхода. И вот тут то и начинается самое интересное. Как видно из примеров, получить письмо, это дело не хитрое, а теперь ведь нужно его разобрать, если в нем только текст, то достаточно отбросить заголовки и оставить тело. Но не все предусматривают письма, закодированные 7bit, 8 bit, а еще вложенные файлы... Попробую продемонстрировать разбор заголовков на примерах, ну а дальше уже все будет понятно. Обязательно почитайте Почтовый стандарт "MIME" (RFC1521) на нем все основано, и про заголовки и про кодирование тут все написано. Я приведу только не большой пример, чтобы все предусмотреть, нужно знать стандарт.
Вот минимальный набор функций, которые пригодятся. Расписывать все действия внутри функций не буду, только опишу кратко предназначение каждой функции.
<?
// При отправке почты, все не латинские символы в заголовках кодируется,
// например тема письма может выглядеть так =?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);
elseif(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;
}
?>
Вот имея эти функции можно уже начать работать с почтой. Предположим, читаем первое письмо
<?
$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 сейчас все письмо вместе с заголовками.
// разделяем письмо на заголовки и тело, еще раз советую почитать Почтовый стандарт "MIME" (RFC1521) (http://webi.ru/webi_files/26_15_f.html)
$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 и к нему же можно добавить альтернативное текстовое содержание.
// подробнее читайте про этот подтип в Почтовом стандарте "MIME" (RFC1521) (http://webi.ru/webi_files/26_15_f.html)
elseif($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;
}
// если это html
elseif($rctype == "text/html" && !$is_download) {
$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, то лучше сохранять письмо в файл и открыв в текстовом редакторе уже его изучать. Так как БАТ скрывает заголовки писем состоящих из нескольких частей, то есть показывает только основной заголовок письма.
Если заметили неточность или опечатку или хотите дополнить, пишите, поправлю, добавлю...
Комментарии
RSS комментарии
19.10.2007 Станислав
Спасибо, человечище!
26.11.2007 Алексей
Огромное спасибо!!!!! Сегодня целый день в нете в поисках примера... везде только все не то попадалось
04.12.2007 Игорь
В скрипте опечатка небольшая
вместо
if(strtolower($enctype) == "base64")
надо
if(strtolower($enctype) == "base64;")
хотя автору все равно спасибо
15.12.2007 Админ
А помоему не надо там точки с запятой.
20.12.2007 Александр
Спасибо большое! А то ничего похожево не найдешь в инете!
18.01.2008 Лекс
Хм. У меня аттачи Outlook не хочет понимать...
27.01.2008 artem2005
// теперь читаем файл в переменную.
$body = compile_body($body,$headers["content-transfer-encoding"],$ctype);
Вместо $ctype в этой строчке здесь нужно подставить $ctype2.
И - спасибо огромное за статью!
31.01.2008 samodelkin(at)gmail.com
Спасибо большое за статью!
Взял за основу, использовал модуль imap + преобразование всего в utf-8, слегка модифицировав ваши функции.
05.03.2008 Олег
Супер Большое спасибо
13.06.2008 Виталий
Супер статья! Большое спасибо!
30.06.2008 Константин
Аффтару уважишище!!! Весь день искал, единственное что находил было завязано на имапе, а отдельный пакет ставить не хотелось, а тут бац и все сразу сходу рабочее =) И переобразование кодировок так же заслуживает похвалы) Спасибо =)
Кстати, насчет строчки с base64 - там точка с запятой не нужна - она коверкает файлы приаттаченные. Правда, с другой стороны убирает точку в конце))
17.07.2008 Александр
одно плохо если сервак использует tsl от этого скрипта толку 0
06.10.2008 Владимир
Спасибо, отличная статья, но у меня возникла ошибка если данные с сервера загружаются дольше чем за 30 секунд, то возникает ошибка
Maximum execution time of 30 seconds exceeded in test3.php on line 103
21.10.2008 админ
Это уже настройки php.
По умолчанию php скрипт способен работать не более 30 секунд.
Если есть доступ к php.ini можете увеличить время работы скрипта, параметр max_execution_time.
Использовать примерно так:
max_execution_time = 180;
Установлено на 180 секунд.
18.11.2008 lux
Ничего особенного когда знаешь, но попробуй найти информацию. Особенно когда она нужна срочно.
Отлично.
Спасибо
20.11.2008 Kamerad Rover
Молодец!!!
10.01.2009 Виталий
Скрипт - шедевр!!!!
Автору заслуживает невероятной благодарности,за то, что делиться такой работой с общественностью!!!
Огромное спасибо!
27.01.2009 Владимир
Отлично
29.01.2009 Me
супер! то что нужно! молодец +1
06.02.2009 Алексей
Сохраняет файлы не в полном объеме, на 98% примерно. Ни у кого не было такой проблемы?
Добавить свой комментарий
|