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

16 ноября 2015 Антон Кулешов 7547 0

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

Реклама

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

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

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

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

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

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

perehodim-v-svoyu-pochtu

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

pochtovye-programmy

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

imap-i-pop3

Тут многие увидят картину как на изображение выше, но я сталкивался, и не один раз, когда доступы отключены. Поэтому если у вас настройки другие, ставим галочки как на скриншоте, для нас главное разрешить доступ через протокол IMAP.

Структура приложения и её кодировка

В этом примере мы не будем придумывать сложную структуру приложения, так как она не нужна, а добавим только то, что необходимо (я работаю в редакторе Sublime Text):

struktura-prilozheniya-dlya-pochty
  • 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

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

adres-pochtovogo-servera-yandeks-pochty

Теперь можно переходить непосредственно к самому коду:

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. Вот пример одного из писем:

obekt-pisma-yandeks-pochty

На этом скриншоте видно, что все данные дублируются, но это особой роли не играет, тянем, то, что удобнее. Намного важнее - кодировка темы письма. Она может быть какой угодно и этот момент надо контролировать. Такая же ситуация и с телом письма и с вложенными файлами.

$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 находится структура письма – это объект, в котором можно найти массу полезной информации, пример части этого объекта представлен ниже:

struktura-pisma-yandeks-pochty

Что важно для решения нашей задачи:

  • 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, и для визуализации мы уже будем работать непосредственно с ним. В этой статье я использовал тестовое письмо, которое лежало у меня почте, давайте глянем, что у нас получилось в итоге:

massiv-s-dannymi-pisma

Вот какого вида примерно должен у вас получиться массив, увы, пришлось скрыть содержание файла по личным причинам. Теперь переходим к нашей 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>

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

rezultat-v-html-dlya-yandeks-prilozheniya

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

Заключение

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

Реклама
Комментариев еще не оставлено
no_avatar
Читайте далее

Эффект падающего снега на jQuery

13 декабря 2014 Антон Кулешов

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

Кнопки социальных сетей

28 мая 2016 Антон Кулешов

Приветствую, читатели falbar, сегодня мы рассмотрим одну простенькую JavaScript библиотеку sharer для добавления кнопок социальных сетей. Большой плюс этого скрипта заключается в том, что в нём реализована функция «поделиться» более чем для 20 социальных сетей.

Табы для сайта на jQuery

21 октября 2014 Антон Кулешов

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