Configurando a API do Gmail para substituir a extensão PHP IMAP e trabalhar com o protocolo OAuth2

Uma vez que um dos sortudos, não estou preparado para o fato de que a partir de 15 de fevereiro de 2021 a Autorização para Gmail e outros produtos só rodarão através do OAuth, li o artigo " o Google enterra a extensão PHP IMAP " e infelizmente comecei a agir na substituição da extensão PHP IMAP seu projeto na API do Google. Havia mais perguntas do que respostas, então rabisquei um manual ao mesmo tempo.



Tenho PHP IMAP usado para as seguintes tarefas:



  1. Removendo cartas antigas das caixas de correio . Infelizmente, no painel de controle da conta corporativa do G Suite, você só pode configurar o período de exclusão de mensagens de todas as caixas de correio da organização após N dias após o recebimento. No entanto, preciso excluir as cartas apenas em caixas de correio especificadas e após um número diferente de dias especificado após o recebimento.
  2. Filtragem, análise e marcação de letras . Muitas cartas são enviadas do nosso site em modo automático, algumas das quais não chegam aos destinatários, sobre os quais, consequentemente, chegam relatórios. É necessário pegar esses relatórios, desmontar, localizar um cliente por e-mail e formar uma carta legível para o gerente, para que ele possa entrar em contato com o cliente e esclarecer a relevância do endereço de e-mail.


Resolveremos essas duas tarefas usando a API do Gmail neste artigo (e ao mesmo tempo desabilitaremos o acesso de aplicativos não seguros nas configurações da caixa de correio, que foi habilitado para o funcionamento do PHP IMAP e, de fato, deixará de funcionar em um dia terrível de fevereiro de 2021). Utilizaremos a chamada conta de serviço do aplicativo Gmail, que, se configurada adequadamente, permite conectar-se a todas as caixas de correio da organização e realizar qualquer ação nelas.



1. Crie um projeto no console de desenvolvedor de API do Google



Com a ajuda deste projeto, faremos a interação da API com o Gmail, e nela criaremos a mesma conta de serviço.



Para criar um projeto:



  1. Vá para o console do desenvolvedor de API do Google e faça login como administrador do G Suite (bem, ou quem é seu usuário com todos os direitos)
  2. Estamos procurando o botão "Criar Projeto".



    Eu encontrei aqui:
    image



    E então aqui:
    image



    Preencha o nome do projeto e salve:



    Criação de projeto
    image



  3. Vá para o projeto e clique no botão "Ativar API e serviços":



    Ativar API e serviços
    image



    Escolhendo a API do Gmail



2. Crie e configure uma conta de serviço



Para fazer isso, você pode usar o manual oficial ou continuar lendo:



  1. Vá para a API do Gmail adicionada, clique no botão "Criar credenciais" e selecione "Conta de serviço":



    Criação de conta de serviço
    image



    Preencha algo e clique em "Criar":



    Detalhes da conta de serviço
    image



    Todo o resto pode ser deixado em branco:



    Direitos de acesso para a conta de serviço
    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



    E também preencha o nome do seu produto no campo abaixo.

  5. Agora você precisa criar uma chave de conta de serviço: este é um arquivo que deve estar disponível em seu aplicativo. Ele, de fato, será usado para autorização.



    Para fazer isso, na página "Credenciais" do seu projeto, acesse o link "Gerenciar contas de serviço":



    Credenciais
    image



    e selecione "Ações - Criar chave", digite: JSON:



    Gerenciamento de contas de serviço
    image



    Depois disso, um arquivo de chave será gerado e baixado para seu computador, que deve ser colocado em seu projeto e ter acesso quando você chamar a API do Gmail.



Isso completa a configuração da API do Gmail, então haverá um pouco do meu código cacau, de fato, implementando as funções que até agora foram resolvidas pela extensão IMAP do PHP.



3. Escrevendo o código



Existe uma documentação oficial bastante boa ( clique e clique ) para a API do Gmail , que usei. Mas desde que comecei a escrever um manual detalhado, irei anexar meu próprio código cacau.



Então, em primeiro lugar, instalamos a biblioteca cliente do Google (apiclient) usando o compositer:



composer require google/apiclient



(No início, como um verdadeiro especialista em literatura, instalei a versão 2.0 do cliente api, como indicado no PHP Quickstart , mas no primeiro início, todos os tipos de amanheceres e alarmes caíram no PHP 7.4 , portanto, não aconselho você a fazer o mesmo)



Então, com base em exemplos da documentação oficial, escrevemos nossa própria classe para trabalhar com o Gmail, não esquecendo de especificar o arquivo de chave da conta de serviço:



Aula para trabalhar com o Gmail
<?php
//     Gmail
class 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());
        }
    }
    // ---------------------------------------------------------------------------------------------

}




Sempre que interagimos com o Gmail, a primeira coisa que fazemos é chamar a função getService ($ strEmail) da classe GmailAPI, que retorna um objeto "autorizado" para trabalhar com a caixa de correio $ strEmail. Além disso, este objeto já é passado para qualquer outra função para realizar diretamente as ações de que precisamos. Todas as outras funções na classe GmailAPI já executam tarefas específicas:



  • listMessageIDs - encontra mensagens pelos critérios especificados e retorna seus IDs (a string de pesquisa passada para a função listUsersMessages da API do Gmail deve ser semelhante à string de pesquisa na interface da web da caixa de correio),
  • deleteMessages - exclui mensagens com IDs passados ​​para ela (a função batchDelete API do Gmail exclui no máximo 1000 mensagens em uma passagem, então eu tive que dividir a matriz de IDs passados ​​para a função em várias matrizes de 999 letras cada e executar a exclusão várias vezes),
  • getMessage - obtém todas as informações sobre a mensagem com o ID passado a ela,
  • listLabels - retorna uma lista de sinalizadores na caixa de correio (eu usei para obter o ID do sinalizador que foi criado originalmente na interface da web da caixa de correio e é atribuído às mensagens desejadas)
  • modifyLabels - adiciona ou remove sinalizadores para a mensagem


A seguir, temos a tarefa de deletar cartas antigas em várias caixas de correio. Ao mesmo tempo, consideramos as cartas antigas recebidas pelo número de dias atrás para cada caixa de correio. Para realizar essa tarefa, escrevemos o seguinte script, que é executado diariamente pelo cron:



Removendo e-mails antigos
<?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);
}




O script se conecta a cada caixa de correio especificada, seleciona cartas antigas e as exclui.



A tarefa de gerar relatórios para o gerente sobre emails não entregues com base em relatórios automáticos é resolvida pelo seguinte script:



Filtrar e marcar emails
<?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);
}




Este script, como o primeiro, se conecta à caixa de correio especificada, seleciona as cartas necessárias dela (relatórios sobre mensagens não entregues) sem um sinalizador, encontra na carta o endereço de e-mail no qual a carta foi enviada e marca esta carta com o sinalizador "Processado" ... Em seguida, as manipulações são realizadas com o endereço de e-mail encontrado, em que uma carta legível é enviada ao funcionário responsável.



As fontes estão disponíveis no GitHub .



Isso é tudo que eu queria dizer neste artigo. Obrigado por ler! Se meu código doeu em seus olhos, basta enrolar o spoiler ou escrever seus comentários - ficarei feliz por críticas construtivas.



All Articles