Добро пожаловать!
Здесь вы можете найти ответ на интересующий вас вопрос в отрасли сайтостроения, познакомится ближе с web технологиями и web стандартами.

Статьи

Чтение почты с помощью 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_conn1024); ?>

Начинаем коннект к POP-серверу, на примере бесплатного почтовика bk.ru:

<?php
$pop_conn 
fsockopen("pop.bk.ru"110$errno$errstr10);
print 
fgets($pop_conn1024);
?>

Если все нормально, ответ будет

+OK

Теперь нужно авторизоваться. Посылаем команду USER с указанием имени пользователя

<?php
fputs
($pop_conn"USER webi\r\n");
print 
fgets($pop_conn1024);
?>

Ответ

+OK Password required for user webi

Вводим пароль с помощью команды PASS

<?php
fputs
($pop_conn"PASS password\r\n");
print 
fgets($pop_conn1024);
?>

+OK webi@bk.ru maildrop has 2 messages (8192 octets)

В данном ответе говорится, что в ящике имеется 2 письма общим размером 8192 байт. Сейчас авторизация пройдена и можно начинать получать информацию. Например, команда STAT

<?php
fputs
($pop_conn"STAT\r\n");
print 
fgets($pop_conn1024);
?>

покажет сколько писем в ящике и их общий размер

+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+2strlen($string));
    
$intpos strpos($string"?");
    
$charset substr($string0$intpos);
    
$enctype strtolower(substr($string$intpos+11));
    
$string substr($string$intpos+3strlen($string));
    
$endpos strpos($string"?=");
    
$mystring substr($string0$endpos);
    
$string substr($string$endpos+2strlen($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($email0strpos($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($thisheader0$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 chopfgets($pop_conn1024) );
    
$data .= "$buffer\r\n";
    if(
trim($buffer) == ".") break;
  }
  return 
$data;
}
?>

Вот имея эти функции можно уже начать работать с почтой. Предположим, читаем первое письмо

<?php
$pop_conn 
fsockopen("pop.bk.ru"110,$errno$errstr10);
$code fgets($pop_conn1024);
fputs($pop_conn,"USER webi\r\n");
$code fgets($pop_conn1024);
fputs($pop_conn,"PASS password\r\n");
$code fgets($pop_conn1024);

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