Показать Меню
falbar Протокол IMAP

Протокол IMAP, или о том, как мы данные из писем вытаскивали

Протокол IMAP

Электронная почта – это важнейший инструмент для обмена информацией и если Вы её используете для работы, то наверняка сталкивались с ситуацией: на почту приходит письмо, в котором содержатся данные необходимые для обработки скрипом. Говорить мы будем о Яндекс почте – в этой статье я поделюсь с Вами, дорогие читатели, опытом как достать письма из ящика, так же мы разберем вариант, когда в письме есть вложенный файл - как его обнаружить и в итоге скачать для дальнейших манипуляций над ним.

Сам я с этой задачей столкнулся достаточно давно, и тогда при наличии малого опыта работы c почтовыми программами Яндекса потратил массу времени и нервов для достижения требуемого результата. Первая моя ошибка заключалась в том, что, как многие веб-разработчики, я начел интенсивно искать похожие примеры в сети, но не воспользовался самой справкой (помощью) Яндекс. Да, там есть полезная информация, хотя её и очень мало, но она достаточно важная для написания такого рода скрипта (об этом будет ниже). На то время требовалось написать скрипт, суть которого сводилось: на Яндекс почту заказчика приходило письмо с прайсом товаров в .xls формате раз в сутки, его необходимо было обработать (распарсить и сравнить с данными из БД интернет магазина и, в зависимости от результата, что-то обновить где-то, отключить или включить).

И первое, что мы сделаем перед написанием скрипта – это наметим наш план действий, который будет состоять из девяти пунктов:

  1. Настроим почту для получения доступа через почтовые протоколы;
  2. Наметим саму структуру PHP приложения и определимся с кодировкой файлов;
  3. Познакомимся с почтовым протоколом IMAP и его возможностями;
  4. Подключимся к Яндекс почте через логин и пароль аккаунта и отследим ошибки на этом этапе;
  5. Обработаем шапку письма;
  6. Получим и обработаем тела письма;
  7. Получим и сохраним вложенные файлы;
  8. Визуализируем проделанную работу;
  9. Сделаем выводы.

Тема довольно объёмная, но я постараюсь изложить всё максимально компактно и понятно. Пожалуй, приступим.

Настройка почты

Переходим в свою почту и заходим в настройки, как показано ниже на скриншоте:

Переходим в свою почту

Далее в нижнем правом углу находим ссылку «Почтовые программы» и кликаем по ней:

Почтовые программы

Теперь мы попали в настройки работы почты через протоколы IMAP и POP3:

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>

Стили я не буду тут добавлять, так как они особой роли не играют, в итоге:

Результат в HTML

А на сервере в папке tmp у Вас появится файл.

Заключение

Проделав все этапы из статьи, Вы достигните должного результата, но всё не так просто, как может показаться – есть подводные камни, которые необходимо учитывать. При написании скрипта под конкретную задачу необходимо следить за кодировкой на всех этапах, письма могут идти с различных почт, у каждой из которых могут быть свои нюансы. Так же немаловажным будет учитывать, что Яндекс почта и их документация периодический обновляется, поэтому могут появиться различные подпункты для работы с почтовыми программами. На этом у меня всё, надеюсь Вам пригодиться данная статья при работе с более низкоуровневым вариантом Яндекс почты.

Подписаться на обновления

Комментариев еще не оставлено