Электронная почта – это важнейший инструмент для обмена информацией и если вы её используете для работы, то наверняка сталкивались с ситуацией: на почту приходит письмо, в котором содержатся данные необходимые для обработки скрипом. Говорить мы будем о Яндекс почте – в этой статье я поделюсь с вами, дорогие читатели, опытом как достать письма из ящика, так же мы разберем вариант, когда в письме есть вложенный файл - как его обнаружить и в итоге скачать для дальнейших манипуляций над ним.
Сам я с этой задачей столкнулся достаточно давно, и тогда при наличии малого опыта работы c почтовыми программами Яндекса потратил массу времени и нервов для достижения требуемого результата. Первая моя ошибка заключалась в том, что, как многие веб-разработчики, я начел интенсивно искать похожие примеры в сети, но не воспользовался самой справкой (помощью) Яндекс. Да, там есть полезная информация, хотя её и очень мало, но она достаточно важная для написания такого рода скрипта (об этом будет ниже). На то время требовалось написать скрипт, суть которого сводилось: на Яндекс почту заказчика приходило письмо с прайсом товаров в xls формате раз в сутки, его необходимо было обработать (распарсить и сравнить с данными из БД интернет магазина и, в зависимости от результата, что-то обновить где-то, отключить или включить).
И первое, что мы сделаем перед написанием скрипта – это наметим наш план действий, который будет состоять из девяти пунктов:
- Настроим почту для получения доступа через почтовые протоколы;
- Наметим саму структуру PHP приложения и определимся с кодировкой файлов;
- Познакомимся с почтовым протоколом IMAP и его возможностями;
- Подключимся к Яндекс почте через логин и пароль аккаунта и отследим ошибки на этом этапе;
- Обработаем шапку письма;
- Получим и обработаем тела письма;
- Получим и сохраним вложенные файлы;
- Визуализируем проделанную работу;
- Сделаем выводы.
Тема довольно объёмная, но я постараюсь изложить всё максимально компактно и понятно. Пожалуй, приступим.
Настройка почты
Переходим в свою почту и заходим в настройки, как показано ниже на скриншоте:
Далее в нижнем правом углу находим ссылку «Почтовые программы» и кликаем по ней:
Теперь мы попали в настройки работы почты через протоколы IMAP и POP3:
Тут многие увидят картину как на изображение выше, но я сталкивался, и не один раз, когда доступы отключены. Поэтому если у вас настройки другие, ставим галочки как на скриншоте, для нас главное разрешить доступ через протокол IMAP.
Структура приложения и её кодировка
В этом примере мы не будем придумывать сложную структуру приложения, так как она не нужна, а добавим только то, что необходимо (я работаю в редакторе Sublime Text):
- tmp – папка в которую будем загружать вложенные файлы из письма, если они есть;
- .htaccess – настройка серверной части, если у вас сервер apache;
- functions.php – сюда будем добавлять наши функции;
- main.css – файл стилей;
- index.php – точка входа приложения;
Кодировку будем использовать UTF-8 и поэтому сразу заполним файл .htaccess следующими строками:
AddDefaultCharset utf-8
AddCharset utf-8 *
<IfModule mod_charset.c>
CharsetSourceEnc utf-8
CharsetDefault utf-8
</IfModule>
Протокол IMAP
Возвращаясь к первому пункту видно, что работать с почтой Яндекс можно также и через протокол POP3. Так почему же именно IMAP? Из двух этих протоков IMAP является более новым и альтернативным POP3, следовательно, у него есть ряд преимуществ (их можно изучить, воспользовавшись википедией), но в нашем случае на выбор повлияло только то, что он более новый. Лично я особой разницы не вижу, что использовать под конкретную задачу получения письма. Если по какой либо причине вам потребуется использовать протокол POP3 то все функции, которые применимы к IMAP будут работать и для него.
Подключаемся к Яндекс почте при помощи протокола IMAP
Для того чтобы подключиться к почте нам нужно знать три параметра: логин почты, её пароль и адрес почтового сервера. Если с двумя параметрами проблем нет, то второй можно найти именно в помощи Яндекс. Об этом (возникшей у меня проблеме) я выше и писал в сети масса примеров, где третий параметр указан не правильно и, представьте себе, что уже на стадии подключения возникают ошибки – это, как минимум, неприятно. Я не буду ходить вокруг да около и сразу дам прямую ссылку на страницу Яндекс – настройка почтовых программ. Вот собственно что нам нужно для подключения:
Теперь можно переходить непосредственно к самому коду:
header("Content-Type: text/html; charset=utf-8");
error_reporting(0);
require_once("functions.php");
$mail_login = "yandex_почта";
$mail_password = "пароль_от_почты";
$mail_imap = "{imap.yandex.ru:993/imap/ssl}";
// Список учитываемых типов файлов
$mail_filetypes = array(
"MSWORD"
);
$connection = imap_open($mail_imap, $mail_login, $mail_password);
if(!$connection){
echo("Ошибка соединения с почтой - ".$mail_login);
exit;
}else{
$msg_num = imap_num_msg($connection);
$mails_data = array();
for($i = 1; $i <= $msg_num; $i++){
/*
Работать с каждым письмом
из IMAP-потока будем тут
*/
}
}
imap_close($connection);
Первым делом дополнительно указываем кодировку UTF-8 при помощи заголовка и отключаем отображение ошибок. Подключаем файл functions.php и указываем настройки, о которых выше была речь. В массиве $mail_filetypes прописываем форматы файлов, которые нам нужны. Так было решено сделать, чтобы отсеять ненужный мусор, и получать конкретные файлы. Соединение с почтой происходит при помощи функции imap_open(), которая при удачном выполнении возвращает IMAP-поток, а при неудачном - false (но если включить отображение ошибок, то это не так). Завершаем работу с потоками при помощи функции imap_close() передав ей индикатор соединения. Между этими двумя функциями идёт обычный условный оператор.
При удачном соединении при помощи imap_num_msg() узнаем число писем на почте и добавляем массив, в который будем помещать все нам необходимые данные из потока. Далее следует цикл, в котором будет обрабатываться каждое письмо по его номеру (нумерация происходит от 1) по отдельности.
Обработка шапки письма
Для получения шапки письма необходимо воспользоваться функцией imap_header(), вторым параметром которой являет номер письма:
// Шапка письма
$msg_header = imap_header($connection, $i);
На данном этапе мы получим объект, из которого и будем вытягивать нужные нам данные, сохраняя в массив $mails_data. Вот пример одного из писем:
На этом скриншоте видно, что все данные дублируются, но это особой роли не играет, тянем, то, что удобнее. Намного важнее - кодировка темы письма. Она может быть какой угодно и этот момент надо контролировать. Такая же ситуация и с телом письма и с вложенными файлами.
$mails_data[$i]["time"] = time($msg_header->MailDate);
$mails_data[$i]["date"] = $msg_header->MailDate;
foreach($msg_header->to as $data){
$mails_data[$i]["to"] = $data->mailbox."@".$data->host;
}
foreach($msg_header->from as $data){
$mails_data[$i]["from"] = $data->mailbox."@".$data->host;
}
Сохраняем в нашем массиве: временную метку, дату получения письма, email получателя и отправителя и переходим к получению темы письма. Для этого нам необходимо вначале добавить три функции в файл functions.php:
function check_utf8($charset){
if(strtolower($charset) != "utf-8"){
return false;
}
return true;
}
function convert_to_utf8($in_charset, $str){
return iconv(strtolower($in_charset), "utf-8", $str);
}
function get_imap_title($str){
$mime = imap_mime_header_decode($str);
$title = "";
foreach($mime as $key => $m){
if(!check_utf8($m->charset)){
$title .= convert_to_utf8($m->charset, $m->text);
}else{
$title .= $m->text;
}
}
return $title;
}
Названия говорящие и, я думаю, стоит пояснить только последнюю функцию. Она принимает закодированную строку и при помощи imap_mime_header_decode() декодирует ее, в результате чего возвращается массив объектов, у каждого из которых есть два свойства charset (кодировка) и text (текст темы). Дальше всё просто: в цикле проверяя кодировку, приводим к UTF-8 и склеиваем тему в единый заголовок и возвращаем его.
Теперь вернёмся в файл index.php и вытянем последний параметр:
$mails_data[$i]["title"] = get_imap_title($msg_header->subject);
На этом обработка шапки письма будет завершена.
Работаем с телом письма
Продолжаем постепенно формировать наш массив с обработанными данными письма и теперь для получения тела нам необходимо воспользоваться двумя функциями:
// Тело письма
$msg_structure = imap_fetchstructure($connection, $i);
$msg_body = imap_fetchbody($connection, $i, 1);
В первой переменной $msg_structure находится структура письма – это объект, в котором можно найти массу полезной информации, пример части этого объекта представлен ниже:
Что важно для решения нашей задачи:
- type – первичный тип тела письма, в зависимости от того, что к нам приходит на почту он может меняться от 0 до 7 (каждой цифре советует свой вид контента который находиться в теле письма);
- encoding – кодировка трансфера тела, меняется от 0 до 5 (0 - 7BIT, 1 - 8BIT, 2 – BINARY, 3 - BASE64, 4 - QUOTED-PRINTABLE, 5 - OTHER);
- parts – массив частей письма, который соответствует структуре объекта уровнем выше.
Немного подробнее рассмотрим свойство parts. Первое, что нужно сказать это то, что в нулевой ячейке этого массива находиться информация, соответствующая именно тексту письма, а начиная с первого – вложенным файлам. Также в каждом объекте указывается type и в parameters кодировка в явном и в не явном виде.
Структура письма может быть сколько угодно вложенной, по крайне мере у меня были случаи, когда доходило до четырёх - пяти уровней, поэтому чтобы её, как говорится, раздраконить нам в дальнейшем потребуется написать рекурсивную функцию.
Вторая функция imap_fetchbody() извлекает определённую часть письма, чаще всего в закодированном виде.
Теперь добавим переменную, в которую будем сохранять обработанную версию тела письма:
$body = "";
Вернёмся в файл functions.php и напишем рекурсивную функцию:
function recursive_search($structure){
$encoding = "";
if($structure->subtype == "HTML" ||
$structure->type == 0){
if($structure->parameters[0]->attribute == "charset"){
$charset = $structure->parameters[0]->value;
}
return array(
"encoding" => $structure->encoding,
"charset" => strtolower($charset),
"subtype" => $structure->subtype
);
}else{
if(isset($structure->parts[0])){
return recursive_search($structure->parts[0]);
}else{
if($structure->parameters[0]->attribute == "charset"){
$charset = $structure->parameters[0]->value;
}
return array(
"encoding" => $structure->encoding,
"charset" => strtolower($charset),
"subtype" => $structure->subtype
);
}
}
}
Функция recursive_search() принимает один параметр – структуру письма, где последовательно проверяет свойства и достает три параметра: encoding, charset, subtype. Точкой выхода из рекурсии является отсутствие свойства parts с нулевой ячейкой. Больше пояснять тут особо нечего, из кода я думаю понятно, что и как происходит.
Добавим ещё одну функцию для переконвертации тела письма, которая нам потребуется в дальнейшем:
function structure_encoding($encoding, $msg_body){
switch((int) $encoding){
case 4:
$body = imap_qprint($msg_body);
break;
case 3:
$body = imap_base64($msg_body);
break;
case 2:
$body = imap_binary($msg_body);
break;
case 1:
$body = imap_8bit($msg_body);
break;
case 0:
$body = $msg_body;
break;
default:
$body = "";
break;
}
return $body;
}
Двигаемся дальше и возвращаемся в файл index.php, где пишем следующий код:
$recursive_data = recursive_search($msg_structure);
if($recursive_data["encoding"] == 0 ||
$recursive_data["encoding"] == 1){
$body = $msg_body;
}
if($recursive_data["encoding"] == 4){
$body = structure_encoding($recursive_data["encoding"], $msg_body);
}
if($recursive_data["encoding"] == 3){
$body = structure_encoding($recursive_data["encoding"], $msg_body);
}
if($recursive_data["encoding"] == 2){
$body = structure_encoding($recursive_data["encoding"], $msg_body);
}
if(!check_utf8($recursive_data["charset"])){
$body = convert_to_utf8($recursive_data["charset"], $msg_body);
}
После того, как мы получили данные из рекурсии, постепенно проверяем кодировку трансфера и, в зависимости от этого, вызываем функцию structure_encoding() с соответствующими параметрами. В последнем условном операторе учитываем, то, что мы работает в UTF-8 и если после всех манипуляций у нас получится отличное от кодировки, перекодируем.
Осталось подвести черту:
$mails_data[$i]["body"] = base64_encode($body);
В теле письма может оказаться, как и обычный текст, так и HTML разметка со своими стилями. Кодируем в BASE64, чтобы при визуализации не поехала уже наша верстка.
Вложенные файлы
Вот, плавно подбираемся к концу написания нашего приложения:
// Вложенные файлы
if(isset($msg_structure->parts)){
for($j = 1, $f = 2; $j < count($msg_structure->parts); $j++, $f++){
if(in_array($msg_structure->parts[$j]->subtype, $mail_filetypes)){
$mails_data[$i]["attachs"][$j]["type"] = $msg_structure->parts[$j]->subtype;
$mails_data[$i]["attachs"][$j]["size"] = $msg_structure->parts[$j]->bytes;
$mails_data[$i]["attachs"][$j]["name"] = get_imap_title($msg_structure->parts[$j]->parameters[0]->value);
$mails_data[$i]["attachs"][$j]["file"] = structure_encoding(
$msg_structure->parts[$j]->encoding,
imap_fetchbody($connection, $i, $f)
);
file_put_contents("tmp/".iconv("utf-8", "cp1251", $mails_data[$i]["attachs"][$j]["name"]), $mails_data[$i]["attachs"][$j]["file"]);
}
}
}
Кусок, отвечающий за обработку вложенного файла гораздо меньше, а теперь - почему именно так. Принцип работы с файлом аналогичен работе с телом письма, только этот этап начинаем с наличия его в массиве свойства parts. Не забываем отсеивать ненужные, сверяясь со списком типов. При помощи нехитрой функции file_put_contents() мы сохраняем наш файл к себе на сервер в папку tmp.
Хочу увидеть результат!
В процессе работы у нас сформировался массив с данными $mails_data, и для визуализации мы уже будем работать непосредственно с ним. В этой статье я использовал тестовое письмо, которое лежало у меня почте, давайте глянем, что у нас получилось в итоге:
Вот какого вида примерно должен у вас получиться массив, увы, пришлось скрыть содержание файла по личным причинам. Теперь переходим к нашей HTML разметке:
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8" />
<title>Яндекс Почта | <?php echo($mail_login);?></title>
<link href="main.css" type="text/css" rel="stylesheet" />
</head>
<body>
<div id="page">
<h1>Яндекс Почта (Входящие) | <?php echo($mail_login);?></h1>
<h2>Число писем: <?php echo(count($mails_data));?></h2>
<?php if(!isset($mails_data)):?>
<div class="empty">писем нет</div>
<?php else:?>
<?php foreach($mails_data as $key => $mail):?>
<div id="mail-<?php echo($key);?>">
<div class="time">
<div class="title">Временная метка:</div>
<div class="data"><?php echo($mail["time"]);?></div>
</div>
<div class="date">
<div class="title">Дата:</div>
<div class="data"><?php echo($mail["date"]);?></div>
</div>
<div class="to">
<div class="title">Кому:</div>
<div class="data"><?php echo($mail["to"]);?></div>
</div>
<div class="from">
<div class="title">От:</div>
<div class="data"><?php echo($mail["from"]);?></div>
</div>
<div class="name">
<div class="title">Тема:</div>
<div class="data"><?php echo($mail["title"]);?></div>
</div>
<div class="body">
<div class="title">Письмо в base64:</div>
<div class="data"><?php echo($mail["body"]);?></div>
</div>
<?php if(isset($mail["attachs"])):?>
<div class="attachs">
<div class="title">Вложенные файлы:</div>
<?php foreach($mail["attachs"] as $k => $attach):?>
<div class="attach">
<div class="attach-type">
Тип: <?php echo($attach["type"]);?>
</div>
<div class="attach-size">
Размер (в байтах): <?php echo($attach["size"]);?>
</div>
<div class="attach-name">
Имя: <?php echo($attach["name"]);?>
</div>
<div class="attach-file">
Тело: <?php echo($attach["file"]);?>
</div>
</div>
<?php endforeach;?>
</div>
<?php endif;?>
</div>
<?php endforeach;?>
<?php endif;?>
</div>
</body>
</html>
Стили я не буду тут добавлять, так как они особой роли не играют, в итоге:
А на сервере в папке tmp у вас появится файл.
Заключение
Проделав все этапы из статьи, вы достигните должного результата, но всё не так просто, как может показаться – есть подводные камни, которые необходимо учитывать. При написании скрипта под конкретную задачу необходимо следить за кодировкой на всех этапах, письма могут идти с различных почт, у каждой из которых могут быть свои нюансы. Так же немаловажным будет учитывать, что Яндекс почта и их документация периодический обновляется, поэтому могут появиться различные подпункты для работы с почтовыми программами. На этом у меня всё, надеюсь вам пригодиться данная статья при работе с более низкоуровневым вариантом Яндекс почты.