Русский
Русский
English
Статистика
Реклама

Настройка Gmail API для замены расширения PHP IMAP и работы по протоколу OAuth2

Оказавшись одним из счастливчиков, совершенно не готовым к тому, что с 15 февраля 2021 года авторизация в Gmail и других продуктах будет работать только через OAuth, я прочитал статью "Google хоронит расширение PHP IMAP" и загрустил начал предпринимать действия по замене расширения PHP IMAP в своём проекте на API Google. Вопросов было больше, чем ответов, поэтому заодно нацарапал мануал.

У меня PHP IMAP используется для следующих задач:
  1. Удаление старых писем из почтовых ящиков. К сожалению, в панели управления корпоративным аккаунтом G Suite можно настроить только срок удаления писем из всех почтовых ящиков организации через N дней после получения. Мне же требуется удалять письма только в заданных почтовых ящиках и через заданное разное количество дней после получения.
  2. Фильтрация, разбор и маркировка писем. С нашего сайта в автоматическом режиме отправляется множество писем, некоторые из которых не доходят до адресатов, о чём, соответственно, приходят отчёты. Нужно эти отчёты отлавливать, разбирать, находить клиента по email и формировать человекочитаемое письмо для менеджера, чтобы тот связался с клиентом и уточнил актуальность адреса электронной почты.


Эти две задачи мы и будем решать при помощи API Gmail в данной статье (а заодно и отключим в настройках почтовых ящиков доступ для небезопасных приложений, который был включён для работы PHP IMAP, и, собственно, перестанет работать в страшный день в феврале 2021). Использовать будем так называемый сервисный аккаунт приложения Gmail, который при соответствующей настройке даёт возможность подключения ко всем почтовым ящикам организации и выполнения в них любых действий.

1. Создаём проект в консоли разработчика Google API


При помощи этого проекта мы и будем осуществлять API-взаимодействие с Gmail, и в нём же создадим тот самый сервисный аккаунт.
Для создания проекта:
  1. Переходим в консоль разработчика Google API и логинимся по администратором G Suite (ну или кто у Вас там пользователь со всеми правами)
  2. Ищем кнопку Создать проект.
    Я нашёл здесь:
    image

    И затем здесь:
    image


    Заполняем имя проекта и сохраняем
    Создание проекта
    image

  3. Переходим к проекту и нажимаем кнопку Включить API и сервисы
    Включить API и сервисы
    image

    Выбираем Gmail API


2. Создаём и настраиваем сервисный аккаунт


Для этого можно воспользоваться официальным мануалом или продолжить чтение:
  1. Переходим в наш добавленный Gmail API, нажимаем кнопку Создать учётные данные и выбираем Сервисный аккаунт
    Создание сервисного аккаунта
    image


    Что-нибудь заполняем и нажимаем Создать
    Сведения о сервисном аккаунте
    image


    Всё остальное можно не заполнять
    Права доступа для сервисного аккаунта
    image

    Предоставление пользователям доступа к сервисному аккаунту
    image

  2. Далее, сервисному аккаунту нужно дать права на чтение или управление почтовыми ящиками. Для этого переходим в консоль администрирования G Suite, открываем главное меню и переходим в пункт Безопасность Управление API.
    Управление API
    image
    image

  3. Прокручиваем страницу вниз и выбираем пункт Настроить делегирование доступа к данным в домене
    Делегирование доступа к данным в домене
    image

    Нажимаем Добавить, в поле Идентификатор клиента копируем соответствующую строку из карточки сервисного аккаунта, а поле Области действия OAuth вставляем права одно или несколько из следующих значений через запятую:

    - https://mail.google.com/ - для полного доступа
    - https://www.googleapis.com/auth/gmail.modify - для редактирования меток
    - https://www.googleapis.com/auth/gmail.readonly - для чтения
    - https://www.googleapis.com/auth/gmail.metadata - для доступа к метаданным


    Сведения о сервисном аккаунте
    image
    image

  4. Возвращаемся к карточке сервисного аккаунта и включаем ещё одну разрешающую галку Включить делегирование доступа к данным в домене G Suite:
    Статус сервисного аккаунта
    image

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

    Для этого со страницы Учётные данные Вашего проекта переходим по ссылке Управление сервисными аккаунтами
    Учётные данные
    image


    и выбираем Действия Создать ключ, тип: JSON
    Управление сервисными аккаунтами
    image


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

На этом настройка API Gmail закончена, далее будет немного моего кака-кода, собственно, реализующего функции, которые до сих пор решались расширением IMAP PHP.

3. Пишем код


По API Gmail есть вполне себе неплохая официальная документация (клик и клик), которой я и пользовался. Но раз уж взялся написать подробный мануал, то приложу и свой собственный кака-код.

Итак, первым делом устанавливаем Google Client Library (apiclient) при помощи composer:

composer require google/apiclient

(Сначала я, как истинный буквоед, установил именно версию 2.0 api-клиента, как указано в PHP Quickstart, но при первом же запуске на PHP 7.4 посыпались всякие ворнинги и алармы, поэтому Вам так же делать не советую)

Затем на основе примеров из официальной документации пишем свой класс для работы с Gmail, не забывая указать файл ключа сервисного аккаунта:
Класс для работы с Gmail
<?php// Класс для работы с Gmailclass GmailAPI{    private $credentials_file = __DIR__ . '/../Gmail/credentials.json'; // Ключ сервисного аккаунта    // ---------------------------------------------------------------------------------------------    /**     * Функция возвращает Google_Service_Gmail Authorized Gmail API instance     *     * @param  string $strEmail Почта пользователя     * @return Google_Service_Gmail Authorized Gmail API instance     * @throws Exception     */    function getService(string $strEmail){        // Подключаемся к почтовому ящику        try{            $client = new Google_Client();            $client->setAuthConfig($this->credentials_file);            $client->setApplicationName('My Super Project');            $client->setScopes(Google_Service_Gmail::MAIL_GOOGLE_COM);            $client->setSubject($strEmail);            $service = new Google_Service_Gmail($client);        }catch (Exception $e) {            throw new \Exception('Исключение в функции getService: '.$e->getMessage());        }        return $service;    }    // ---------------------------------------------------------------------------------------------    /**     * Функция возвращает массив ID сообщений в ящике пользователя     *     * @param  Google_Service_Gmail $service Authorized Gmail API instance.     * @param  string $strEmail Почта пользователя     * @param  array $arrOptionalParams любые дополнительные параметры для выборки писем     * Из них мы сделаем стандартную строку поиска в Gmail вида after: 2020/08/20 in:inbox label:     * и запишем её в переменную q массива $opt_param     * @return array Массив ID писем или массив ошибок array('arrErrors' => $arrErrors), если они есть     * @throws Exception     */    function listMessageIDs(Google_Service_Gmail $service, string $strEmail, array $arrOptionalParams = array()) {        $arrIDs = array(); // Массив ID писем        $pageToken = NULL; // Токен страницы в почтовом ящике        $messages = array(); // Массив писем в ящике        // Параметры выборки        $opt_param = array();        // Если параметры выборки есть, делаем из них строку поиска в Gmail и записываем её в переменную q        if (count($arrOptionalParams)) $opt_param['q'] = str_replace('=', ':', http_build_query($arrOptionalParams, null, ' '));        // Получаем массив писем, соответствующих условию выборки, со всех страниц почтового ящика        do {            try {                if ($pageToken) {                    $opt_param['pageToken'] = $pageToken;                }                $messagesResponse = $service->users_messages->listUsersMessages($strEmail, $opt_param);                if ($messagesResponse->getMessages()) {                    $messages = array_merge($messages, $messagesResponse->getMessages());                    $pageToken = $messagesResponse->getNextPageToken();                }            } catch (Exception $e) {                throw new \Exception('Исключение в функции listMessageIDs: '.$e->getMessage());            }        } while ($pageToken);        // Получаем массив ID этих писем        if (count($messages)) {            foreach ($messages as $message) {                $arrIDs[] = $message->getId();            }        }        return $arrIDs;    }    // ---------------------------------------------------------------------------------------------    /**     * Удаляем сообщения из массива их ID функцией batchDelete     *     * @param  Google_Service_Gmail $service Authorized Gmail API instance.     * @param  string $strEmail Почта пользователя     * @param  array $arrIDs массив ID писем для удаления из функции listMessageIDs     * @throws Exception     */    function deleteMessages(Google_Service_Gmail $service, string $strEmail, array $arrIDs){        // Разбиваем массив на части по 1000 элементов, так как столько поддерживает метод batchDelete        $arrParts = array_chunk($arrIDs, 999);        if (count($arrParts)){            foreach ($arrParts as $arrPartIDs){                try{                    // Получаем объект запроса удаляемых писем                    $objBatchDeleteMessages = new Google_Service_Gmail_BatchDeleteMessagesRequest();                    // Назначаем удаляемые письма                    $objBatchDeleteMessages->setIds($arrPartIDs);                    // Удаляем их                    $service->users_messages->batchDelete($strEmail,$objBatchDeleteMessages);                }catch (Exception $e) {                    throw new \Exception('Исключение в функции deleteMessages: '.$e->getMessage());                }            }        }    }    // ---------------------------------------------------------------------------------------------    /**     * Получаем содержиме сообщения функцией get     *     * @param  Google_Service_Gmail $service Authorized Gmail API instance.     * @param  string $strEmail Почта пользователя     * @param  string $strMessageID ID письма     * @param  string $strFormat The format to return the message in.     * Acceptable values are:     * "full": Returns the full email message data with body content parsed in the payload field; the raw field is not used. (default)     * "metadata": Returns only email message ID, labels, and email headers.     * "minimal": Returns only email message ID and labels; does not return the email headers, body, or payload.     * "raw": Returns the full email message data with body content in the raw field as a base64url encoded string; the payload field is not used.     * @param  array $arrMetadataHeaders When given and format is METADATA, only include headers specified.     * @return  object Message     * @throws Exception     */    function getMessage(Google_Service_Gmail $service, string $strEmail, string $strMessageID, string $strFormat = 'full', array $arrMetadataHeaders = array()){        $arrOptionalParams = array(            'format' => $strFormat // Формат, в котором возвращаем письмо        );        // Если формат - metadata, перечисляем только нужные нам заголовки        if (($strFormat == 'metadata') and count($arrMetadataHeaders))            $arrOptionalParams['metadataHeaders'] = implode(',',$arrMetadataHeaders);        try{            $objMessage = $service->users_messages->get($strEmail, $strMessageID,$arrOptionalParams);            return $objMessage;        }catch (Exception $e) {            throw new \Exception('Исключение в функции getMessage: '.$e->getMessage());        }    }    // ---------------------------------------------------------------------------------------------    /**     * Выводим список меток, имеющихся в почтовом ящике     *     * @param  Google_Service_Gmail $service Authorized Gmail API instance.     * @param  string $strEmail Почта пользователя     * @return  object $objLabels - объект - список меток     * @throws Exception     */    function listLabels(Google_Service_Gmail $service, string $strEmail){        try{            $objLabels = $service->users_labels->listUsersLabels($strEmail);            return $objLabels;        }catch (Exception $e) {            throw new \Exception('Исключение в функции listLabels: '.$e->getMessage());        }    }    // ---------------------------------------------------------------------------------------------    /**     * Добавляем или удаляем метку (флаг) к письму     *     * @param  Google_Service_Gmail $service Authorized Gmail API instance.     * @param  string $strEmail Почта пользователя     * @param  string $strMessageID ID письма     * @param  array $arrAddLabelIds Массив ID меток, которые мы добавляем к письму     * @param  array $arrRemoveLabelIds Массив ID меток, которые мы удаляем в письме     * @return  object Message - текущее письмо     * @throws Exception     */    function modifyLabels(Google_Service_Gmail $service, string $strEmail, string $strMessageID, array $arrAddLabelIds = array(), array $arrRemoveLabelIds = array()){        try{            $objPostBody = new Google_Service_Gmail_ModifyMessageRequest();            $objPostBody->setAddLabelIds($arrAddLabelIds);            $objPostBody->setRemoveLabelIds($arrRemoveLabelIds);            $objMessage = $service->users_messages->modify($strEmail,$strMessageID,$objPostBody);            return $objMessage;        }catch (Exception $e) {            throw new \Exception('Исключение в функции modifyLabels: '.$e->getMessage());        }    }    // ---------------------------------------------------------------------------------------------}


При любом взаимодействии с Gmail первым делом мы вызываем функцию getService($strEmail) класса GmailAPI, которая возвращает авторизованный объект для работы с почтовым ящиком $strEmail. Далее этот объект уже передаётся в любую другую функцию для уже непосредственно выполнения нужных нам действий. Все остальные функции в классе GmailAPI уже выполняют конкретные задачи:

  • listMessageIDs находит письма по заданным критериям и возвращает их ID (передаваемая в функцию listUsersMessages Gmail API строка поиска писем должна быть аналогична строке поиска в веб-интерфейсе почтового ящика),
  • deleteMessages удаляет письма с переданными в неё ID (функция batchDelete API Gmail удаляет не более 1000 писем за один проход, поэтому пришлось разбить массив переданных в функцию ID на несколько массивов по 999 писем и выполнить удаление несколько раз),
  • getMessage получает всю информацию о сообщении с переданным в неё ID,
  • listLabels возвращает список флагов в почтовом ящике (я использовал её, чтобы получить ID флага, который изначально был создан в веб-интерфейсе ящика, и присваивается нужным сообщениям)
  • modifyLabels добавляет или удаляет флаги к сообщению


Далее, у нас есть задача удаления старых писем в различных почтовых ящиках. При этом старыми мы считаем письма, полученные своё количество дней назад для каждого почтового ящика. Для реализации этой задачи пишем следующий скрипт, ежедневно запускаемый cron'ом:
Удаление старых писем
<?php/** * Удаляем письма в почтовых ящиках Gmail * Используем сервисный аккаунт и его ключ */require __DIR__ .'/../general/config/config.php'; // Общий файл конфигурацииrequire __DIR__ .'/../vendor/autoload.php'; // Загрузчик внешних компонент// Задаём количества дней хранения почты в ящиках$arrMailBoxesForClean = array(    'a@domain.com' => 30,    'b@domain.com' => 30,    'c@domain.com' => 7,    'd@domain.com' => 7,    'e@domain.com' => 7,    'f@domain.com' => 1);$arrErrors = array(); // Массив ошибок$objGmailAPI = new GmailAPI(); // Класс для работы с GMail// Проходим по списку почтовых ящиков, из которых нужно удалить старые письмаforeach ($arrMailBoxesForClean as $strEmail => $intDays) {    try{        // Подключаемся к почтовому ящику        $service = $objGmailAPI->getService($strEmail);        // Указываем условие выборки писем в почтовом ящике        $arrParams = array('before' => date('Y/m/d', (time() - 60 * 60 * 24 * $intDays)));        // Получаем массив писем, подходящих для удаления        $arrIDs = $objGmailAPI->listMessageIDs($service,$strEmail,$arrParams);        // Удаляем письма по их ID в массиве $arrIDs        if (count($arrIDs)) $objGmailAPI->deleteMessages($service,$strEmail,$arrIDs);        // Удаляем все использованные переменные        unset($service,$arrIDs);    }catch (Exception $e) {        $arrErrors[] = $e->getMessage();    }}if (count($arrErrors)){    $strTo = 'my_email@domain.com';    $strSubj = 'Ошибка при удалении старых писем из почтовых ящиков';    $strMessage = 'При удалении старых писем из почтовых ящиков возникли следующие ошибки:'.        '<ul><li>'.implode('</li><li>',$arrErrors).'</li></ul>'.        '<br/>URL: '.filter_input(INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_URL);    $objMailSender = new mailSender();    $objMailSender->sendMail($strTo,$strSubj,$strMessage);}


Скрипт подключается к каждому заданному почтовому ящику, выбирает старые письма и удаляет их.

Задача формирования отчётов для менеджера о недоставленных письмах на основании автоматических отчётов решается следующим скриптом:
Фильтрация и маркировка писем
<?php/* * Подключаемся к ящику a@domain.com * Берём с него письма о том, что наши письма не доставлены: отправитель: mailer-daemon@googlemail.com * Проверяем почтовые ящики в этих письмах. Если они есть у клиентов на нашем сайте, отправляем на b@domain.com * письмо об этом */require __DIR__ .'/../general/config/config.php'; // Общий файл конфигурацииrequire __DIR__ .'/../vendor/autoload.php'; // Загрузчик внешних компонент$strEmail = 'a@domain.com';$strLabelID = 'Label_2399611988534712153'; // Флаг reportProcessed - устанавливаем при обработке письма// Параметры выборки$arrParams = array(    'from' => 'mailer-daemon@googlemail.com', // Письма об ошибках приходят с этого адреса    'in' => 'inbox', // Во входящих    'after' => date('Y/m/d', (time() - 60 * 60 * 24)), // За последние сутки    'has' => 'nouserlabels' // Без флага);$arrErrors = array(); // Массив ошибок$objGmailAPI = new GmailAPI(); // Класс для работы с GMail$arrClientEmails = array(); // Массив адресов электронной почты, на которые не удалось отправить сообщениеtry{    // Подключаемся к почтовому ящику    $service = $objGmailAPI->getService($strEmail);    // Находим в нём отчёты за последние сутки о том, что письма не доставлены    $arrIDs = $objGmailAPI->listMessageIDs($service,$strEmail, $arrParams);    // Для найденных писем получаем заголовок 'X-Failed-Recipients', в котором содержится адрес, на который пыталось быть отправлено письмо    if (count($arrIDs)){        foreach ($arrIDs as $strMessageID){            // Получаем метаданные письма            $objMessage = $objGmailAPI->getMessage($service,$strEmail,$strMessageID,'metadata',array('X-Failed-Recipients'));            // Заголовки письма            $arrHeaders = $objMessage->getPayload()->getHeaders();            // Находим нужный            foreach ($arrHeaders as $objMessagePartHeader){                if ($objMessagePartHeader->getName() == 'X-Failed-Recipients'){                    $strClientEmail = mb_strtolower(trim($objMessagePartHeader->getValue()), 'UTF-8');                    if (!empty($strClientEmail)) {                        if (!in_array($strClientEmail, $arrClientEmails)) $arrClientEmails[] = $strClientEmail;                    }                    // Помечаем письмо флагом reportProcessed, чтобы не выбирать его в следующий раз                    $objGmailAPI->modifyLabels($service,$strEmail,$strMessageID,array($strLabelID));                }            }        }    }    unset($service,$arrIDs,$strMessageID);}catch (Exception $e) {    $arrErrors[] = $e->getMessage();}// Если найдены адреса электронной почты, на которые не удалось доставить сообщения, проверяем их в базеif (count($arrClientEmails)) {    $objClients = new clients();    // Получаем все email всех клиентов    $arrAllClientsEmails = $objClients->getAllEmails();    foreach ($arrClientEmails as $strClientEmail){        $arrUsages = array();        foreach ($arrAllClientsEmails as $arrRow){            if (strpos($arrRow['email'], $strClientEmail) !== false) {                $arrUsages[] = 'как основной email клиентом "<a href="'.MANAGEURL.'?m=admin&sm=clients&edit='.$arrRow['s_users_id'].'">'.$arrRow['name'].'</a>"';            }            if (strpos($arrRow['email2'], $strClientEmail) !== false) {                $arrUsages[] = 'как дополнительный email клиентом "<a href="'.MANAGEURL.'?m=admin&sm=clients&edit='.$arrRow['s_users_id'].'">'.$arrRow['name'].'</a>"';            }            if (strpos($arrRow['site_user_settings_contact_email'], $strClientEmail) !== false) {                $arrUsages[] = 'как контактный email клиентом "<a href="'.MANAGEURL.'?m=admin&sm=clients&edit='.$arrRow['s_users_id'].'">'.$arrRow['name'].'</a>"';            }        }        $intUsagesCnt = count($arrUsages);        if ($intUsagesCnt > 0){            $strMessage = 'Не удалось доставить письмо с сайта по адресу электронной почты <span style="color: #000099;">'.$strClientEmail.'</span><br/>                Этот адрес используется';            if ($intUsagesCnt == 1){                $strMessage .= ' '.$arrUsages[0].'<br/>';            }else{                $strMessage .= ':<ul>';                foreach ($arrUsages as $strUsage){                    $strMessage .= '<li>'.$strUsage.'</li>';                }                $strMessage .= '</ul>';            }            $strMessage .= '<br/>Пожалуйста, уточните у клиента актуальность этого адреса электронной почты.<br/><br/>                Это письмо было отправлено автоматически, не отвечайте на него';            if (empty($objMailSender)) $objMailSender = new mailSender();            $objMailSender->sendMail('b@domain.com','Проверьте email клиента',$strMessage);        }    }}if (count($arrErrors)){    $strTo = 'my_email@domain.com';    $strSubj = 'Ошибка при обработке отчётов о недоставленных письмах';    $strMessage = 'При обработке отчётов о недоставленных письмах возникли следующие ошибки:'.        '<ul><li>'.implode('</li><li>',$arrErrors).'</li></ul>'.        '<br/>URL: '.filter_input(INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_URL);    if (empty($objMailSender)) $objMailSender = new mailSender();    $objMailSender->sendMail($strTo,$strSubj,$strMessage);}


Этот скрипт так же, как и первый, подключается к заданному почтовому ящику, выбирает из него нужные письма (отчёты о недоставленных сообщениях) без флага, находит в письме адрес электронной почты, на которой пыталось быть отправлено письмо и маркирует это письмо флагом Обработано. Затем уже с найденным адресом электронной почты производятся манипуляции, в результате которых формируется человекочитаемое письмо ответственному сотруднику.

Исходники доступны на GitHub.

Вот и всё, что я хотел поведать в этой статье. Спасибо за прочтение! Если у Вас защипало в глазах от моего кода, просто сверните спойлер или напишите свои замечания буду рад конструктивной критике.
Источник: habr.com
К списку статей
Опубликовано: 25.08.2020 12:13:42
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Разработка веб-сайтов

Php

Программирование

Проектирование и рефакторинг

Google api

Imap

Oauth2

Gmail api

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2023, personeltest.ru