Зачем всё это нужно
Иногда PHP-проект необходимо интегрировать с внешними информационными системами, которые не имеют программного интерфейса или не дают к нему доступ. Бывает наоборот: ваша политика информационной безопасности не позволяет организовать подключение к этим внешним системам.
Таким примером может быть служба поддержки какого-то внешнего сервиса, которая работает со своими клиентами исключительно через электронную почту.
Допустим, у вас есть закрытый интранет-портал на Битриксе или другой PHP-платформе. Сотрудники бронируют на портале переговорные и в некоторых случаях отмечают в заявке, что им требуется, кроме переговорной, еще организация видео-конференц-связи (ВКС). У компании заключен договор на поддержку ВКС, но по условиям заявка на создание сессии видеосвязи должна подаваться только от нескольких согласованных ответственных лиц, которые модерируют бронирование переговорных, и только с их адреса электронной почты. При такой схеме ответственные лица, помимо модерации бронирования переговорных, вынуждены решать рутинные задачи: оформлять заявки подрядчику ВКС, получать ссылки на подключение и пересылать их сотрудникам.
Рутинные задачи из примера можно автоматизировать, используя PHP-библиотеку php-imap. Интранет-портал при бронировании переговорной с ВКС будет отправлять заявку по форме подрядчику ВКС, а потом через некоторые интервалы проверять согласованный почтовый ящик, который парсит новые email-адреса. А при появлении письма от подрядчика ВКС найдет в нем ссылку для подключения и разошлет ее всем участникам встречи.
Этот способ при некоторой доработке подойдет и для других задач, например для оформления командировок, заказа билетов, пропусков или парковочных мест с оповещением постов охраны. Электронная почта в таком случае выступает как брокер сообщений, задача разработчика — обеспечить интерфейс связи информационной системы с системой электронной почты SMTP- и IMAP- или POP-серверам. Тогда процессы можно автоматизировать.
Интеграция портала на базе Битрикса с IMAP-сервером
Рассмотрим пример, когда нужно интегрировать портал на базе Битрикса с IMAP-сервером электронной почты и настроить парсинг почтовых сообщений от поддержки сервиса ВКС. Предположим, в портале есть сущность «встреча» в виде информационного блока Битрикса (IBLOCK). В свойствах этого блока будут храниться параметры встречи и то, что для примера важно, — ссылка на ВКС.
Для регулярных запусков нашего кода воспользуемся встроенным механизмом Битрикса — создадим агента с функцией VKSLink(), выберем дату запуска и желаемый интервал запуска, то есть то, как часто будет проверяться почта. Например, 600 секунд (10 минут).
Чтобы функция VKSLink() была видна ядру Битрикса, необходимо подключить файл с агентом. Для этого воспользуемся файлом init.php.
Листинг файла init.php
<?php
// Подключаем код агента VKSLink
include 'vkslink.php';
В Битриксе файл init.php используется для инициализации обработчиков событий или подключения дополнительных функций, потому что он вызывается при каждом обращении к сайту. В моей инсталляции Битрикса init.php находится в /local/php_interface/init.php. Как видно из кода, файл с агентом должен располагаться рядом в /local/php_interface/vkslink.php. Получается следующая файловая структура.
Рассмотрим письма, которые отправляются в службу поддержки сервиса ВКС и возвращаются. Письма отправляются с единого почтового ящика vks@test.ru, и ответ приходит на тот же ящик.
Примерное письмо в службу поддержки сервиса ВКС
Тема: Заявка на создание ВКС #intranet_561#
Здравствуйте!
ООО «Тест.ру» запрашивает ВКС для проведения онлайн-встречи. Чтобы участники получили ссылку на ВКС, пожалуйста, при ответе не удаляйте из темы письма номер заявки, заключенный в решетки (#) intranet_561. Это позволит автоматически обработать ваше письмо и направить ссылку на ВКС участникам мероприятия.
Лучше всего отправить ссылку на ВКС, нажав в почтовой программе кнопку «Ответить» на это письмо, тогда тема будет дополнена фразой Re: или «Ответ:», но первоначальная тема письма сохранится.
Дата и время ВКС: 19.06.2024 09:30:00.
Длительность: 1 час 30 минут.
Тема мероприятия: Встреча по проектам.
Кол-во участников: 20.
Ведущий: Иванов Иван.
Запись: не требуется.
С уважением, ответственный за ВКС
Пётр Петров
Письмо сформировано автоматически
Письмо из службы поддержки сервиса ВКС
Тема: Re: Заявка на создание ВКС #intranet_561#
Здравствуйте!
Направляем ссылку на подключение к видео-конференц-связи:
https://vksservice.ru/meeting/aRiaF0vwcO1crGbrddk
Инструкция по подключению доступна по ссылке https://vksservice.ru/manual.pdf
В письмах обратите внимание на метку #intranet_561#. 561 — это ID «встречи» (элемента IBLOCK Битрикса) в интранет-портале, а intranet_ — это префикс, введенный, чтобы ITSM-система поддержки ВКС не распарсила наш внутренний номер как свой и не привязала бы наше письмо к другому сообщению или заявке. ID с префиксом заключены в решетки # для удобства поиска в заголовках письма. С помощью метки #intranet_561# почтовые сообщения связываются с конкретной заявкой и «встречей» в базе данных портала. Это будет видно дальше из кода в файле vsklink.php.
Итак, у нас уже есть:
- Почтовый сервер IMAP и доступ к нему.
- На почтовом сервере лежит письмо из службы поддержки ВКС со ссылкой вида https://vksservice.ru/meeting/_hash_ и меткой в теме сообщения вида #intranet_NNN#, где NNN — это ID элементов IBLOCK или встреч в базе данных интранет-портала.
- В Битриксе создан агент для регулярного запуска кода функции VKSLink().
- PHP на сервере запускается с подключенным модулем php-imap.
Рассмотрим код самого агента — функции VKSLink().
Листинг 2. Код агента Битрикса VKSLink
<?php
function VKSLink()
{
// Данные доступа к IMAP-серверу
$server = '{mail.test.ru:993/imap/ssl}INBOX';
$username = 'vks@test.ru';
$password = '0ZOwNytIkqYecS4Olxw';
// Подключение к IMAP-серверу
$mailbox = imap_open($server, $username, $password);
if ($mailbox) {
// Просмотр непрочитанных сообщений
$search_query = 'UNSEEN TEXT "https://vksservice.ru"';
$emails = imap_search($mailbox, $search_query, null, 'UTF-8');
// Объявляем массив для URL, содержащих https://vksservice
$found_values = [];
if ($emails) { // Если в почтовом ящике есть письма, соответствующие условию, смотрим их
// Объявляем массив для хранения последних писем на каждый ID встречи,
// так как нас интересуют только последние пришедшие и не интересуют старые, которые,
// например, могли быть ошибочно отправлены
$latest_emails = [];
// Перебираем письма в почтовом ящике
foreach ($emails as $email_id) {
// Получаем заголовки письма
$header = imap_headerinfo($mailbox, $email_id);
// Получаем тему письма и в ней ищем нашу метку #intranet_id#
$subject = imap_utf8($header->subject);
preg_match('/#intranet_(d+)#/', $subject, $matches);
$intranet_id = isset($matches[1]) ? $matches[1] : '';
// Получаем дату письма
$email_date = strtotime($header->date);
// Получаем тело письма
$body = imap_fetchbody($mailbox, $email_id, 1);
// Ищем в теле письма ссылку на ВКС
preg_match('/https://vksservice.[^s]+/', $body, $vksservice_url_matches);
$vksservice_url = isset($vksservice_url_matches[0]) ? $vksservice_url_matches[0] : '';
// Выбираем только те письма, где в теме есть метка #intranet_N# и в теле — искомая ссылка на ВКС https://vksservice.
if (!empty($intranet_id) && !empty($vksservice_url)) {
// Проверяем массив найденных писем, не было ли найдено писем с такой же меткой
if (isset($latest_emails[$intranet_id])) {
// Если обрабатываемое письмо новее ранее найденного, заменяем предыдущее на новое
if ($email_date > $latest_emails[$intranet_id]['date']) {
$latest_emails[$intranet_id] = [
'id' => $email_id,
'date' => $email_date,
];
}
} else {
// Если писем с одинаковыми метками не найдено, сохраняем в массив текущее
$latest_emails[$intranet_id] = [
'id' => $email_id,
'date' => $email_date,
];
}
}
}
// Обрабатываем последние пришедшие письма с уникальными метками, ищем в них ссылки https://vksservice
// и сохраняем в массив $found_values array
foreach ($latest_emails as $intranet_id => $email_info) {
$email_id = $email_info['id'];
// Получаем тему письма
$header = imap_headerinfo($mailbox, $email_id);
$subject = imap_utf8($header->subject);
// Получаем тело письма
$body = imap_fetchbody($mailbox, $email_id, 1);
// Извлекаем ссылку на ВКС https://vksservice из тела письма
preg_match('/https://vksservice.[^s]+/', $body, $vksservice_url_matches);
$vksservice_url = isset($vksservice_url_matches[0]) ? $vksservice_url_matches[0] : '';
// Сохраняем полученные результаты в массив
$found_values[$email_id] = [
'intranet_id' => $intranet_id,
'subject' => $subject,
'url' => $vksservice_url,
];
}
}
// Закрываем IMAP-соединение
imap_close($mailbox);
// Обрабатываем полученный массив
foreach ($found_values as $key => $value){
// Код, отвечающий за сохранение ссылки на ВКС в БД и рассылку уведомлений участникам ВКС
$arSelect = array( // Формируем список важных нам полей для запроса к БД
"ID",
"IBLOCK_SECTION_ID",
"IBLOCK_ID",
"IBLOCK_ELEMENT_ID",
"NAME",
"CREATED_BY",
"DATE_ACTIVE_FROM",
"DATE_ACTIVE_TO",
"DETAIL_TEXT",
"PROPERTY_UF_USER",
"PROPERTY_UF_SPEAKER",
"PROPERTY_UF_PARTICIPANTS_COUNT",
"PROPERTY_UF_EVENT_LENGTH",
"PROPERTY_UF_STATUS",
"PROPERTY_UF_VKSREQUEST_SENT",
"PROPERTY_UF_VKSEMAIL_IDS",
"PROPERTY_UF_VKSLINK",
"PROPERTY_UF_ROOM_ID",
);
// Делаем запрос в БД о записи с ID из метки #intranet_ID#
$arFilter = Array("IBLOCK_ID"=>IntVal(IBLOCK_ID), "ID"=>IntVal($value['intranet_id']));
$res = CIBlockElement::GetList(Array(), $arFilter, false, Array("nPageSize"=>50), $arSelect);
$results = $res->arResult;
// Чтобы не удалились старые записи, собираем весь массив свойств, подставляя значение UF_VKSLINK, // иначе метод $iblockElementObject->Update затрет старые значения
$arFields = array(
"NAME" => $results[0]["NAME"],
"DATE_ACTIVE_FROM" => $results[0]["DATE_ACTIVE_FROM"],
"DATE_ACTIVE_TO" => $results[0]["DATE_ACTIVE_TO"],
"CREATED_BY" => $results[0]["CREATED_BY"],
"DETAIL_TEXT" => $results[0]["DETAIL_TEXT"],
"PROPERTY_VALUES" => array(
'UF_USER' => array(
$results[0]["PROPERTY_UF_USER_VALUE"]
),
'UF_SPEAKER' => array(
$results[0]["PROPERTY_UF_SPEAKER_VALUE"]
),
'UF_PARTICIPANTS_COUNT' => array(
$results[0]["PROPERTY_UF_PARTICIPANTS_COUNT_VALUE"]
),
'UF_EVENT_LENGTH' => array(
$results[0]["PROPERTY_UF_EVENT_LENGTH_VALUE"]
),
'UF_STATUS' => array(
$results[0]["PROPERTY_UF_STATUS_ENUM_ID"]
),
'UF_VKSREQUEST_SENT' => array(
$results[0]["PROPERTY_UF_VKSREQUEST_SENT_VALUE"]
),
'UF_VKSEMAIL_IDS' => array(
is_null($results[0]["PROPERTY_UF_VKSEMAIL_IDS_VALUE"]) ? $key : $results[0]["PROPERTY_UF_VKSEMAIL_IDS_VALUE"] .', ' . $key
),
'UF_VKSLINK' => array(
$value['url']
),
'UF_ROOM_ID' => array(
$results[0]["PROPERTY_UF_ROOM_ID_VALUE"]
),
),
"ACTIVE" => "Y",
);
// Обновляем "встречу" с ID из письма в БД — добавляем туда ссылку на ВКС
$eventId = $value['intranet_id'];
$iblockElementObject = new CIBlockElement;
$res = $iblockElementObject->Update($eventId, $arFields);
// Вычисляем ID пользователя-заявителя и далее получаем данные о нем для отправки письма со ссылкой на ВКС
if ($user[0] === 'U') { // Если пользователь корректно занесен в формате компонента main.user.link, вычисляем его ID
$user_id = substr($results[0]["PROPERTY_UF_USER"], 1);
// Запрашиваем данные пользователя из БД
$dbUser = CUser::GetByID($user_id);
if ($arUser = $dbUser->GetNext()) {
// Отправляем письмо со ссылкой на ВКС пользователю-заявителю
// Формируем переменные для почтового шаблона
$letterFields['EVENT_ID'] = $results[0]["ID"];
$letterFields['USER_EMAIL'] = $arUser['EMAIL'];
$letterFields['DATE_FROM'] = $results[0]["DATE_ACTIVE_FROM"];
$letterFields['VKSLINK'] = $vks_link;
$msg = CEvent::sendImmediate('VKSLINK', SITE_ID, $letterFields, "N", EMAIL_TEMPLATE);
}
}
}
} else {
// Если подключение к IMAP-серверу не удалось, запишем в лог сообщение
AddMessage2Log("Не удалось подключиться к IMAP-серверу.", "VKSLink");
}
// Обязательная часть агента Битрикса — функция агента должна возвращать свое имя или, точнее, PHP-код своего запуска
return "VKSLink();";
}
Файл vkslink.php подробно откомментирован, но на отдельные его части стоит обратить внимание.
После успешного подключения к IMAP-серверу нужно сформировать запрос, чтобы почтовый сервер вернул нам определенные сообщения, а не все. Для этого используется PHP-функция imap_search(), в ней нас интересует поисковая строка $search_query.
В примере она задана как UNSEEN TEXT https://vksservice.ru, что означает непрочитанные сообщения, содержащие текст https://vksservice.ru. Такой выбор гарантирует, что не будут обрабатываться письма, которые уже были в выдаче. Параметры поиска, возможно, придется скорректировать под конкретную задачу и ваш почтовый сервер. Например, Microsoft Exchange не поддерживает все стандарты IMAP и параметры могут не работать или называться иначе. Все возможные ключевые слова поисковой строки можно посмотреть в документации PHP по функции imap_search(). Приведу наиболее интересные из них в таблице.
При работе с почтовыми сообщениями на IMAP-сервере учитывайте, что сообщения передаются в разных кодировках и могут быть наслоения MIME, UTF и других способов передачи сложного текста, особенно кириллицы. Иногда письма содержат много дополнительной нагрузки и отличаются от писем из примеров выше. В приведенном файле vkslink.php данные проблемы решаются совместным использованием функций imap_headerinfo() вместо распространенной imap_fetchheader()) и imap_utf8.
Если возникнут проблемы, когда нужные метки не находятся или поиск не выдает нужные письма, поэтапно просмотрите приходящие с почтового сервера ответы и данные.
Пробуйте и дорабатывайте систему
Для некоторых интеграций вполне подойдут забытые, но по-прежнему эффективные способы взаимодействия между системами, где в качестве брокера сообщений может выступать электронная почта.
Битрикс-агент — это просто PHP-функция, подключение к серверу IMAP и парсинг почтовых сообщений можно легко перенести на любой другой PHP-фреймворк или CMS.