📜 ⬆️ ⬇️

Work with Gmail using PHP

Good day, colleagues. In this article I will talk about the experience of using the Gmail API. As it turned out, this topic is not very covered on the Internet, and the documentation is far from ideal.

Recently, I had a task: write a PHP application to search for messages in the user's Gmail box. Moreover, it’s not just a search, but a search by parameters, since Gmail has a good search string that allows you to write something like “is: sent after: 2012/08/10”. Yes, and the API has extensions IMAP protocol X-GM- *

So, we need to implement an interface to authorize users and search for messages. For these purposes, I used the Zend Framework, since the project was written in the Zend Framework, and Google recommends using it to work with the API.

Outline the interface:
')
class Model_OAuth_Gmail { //   OAuth public function Connect( $callback ); //    Access Token (     ) public function getConnection($accessToken); //      const MODE_NONE = 0; const MODE_MESSAGES = 1; const MODE_THREAD = 2; //  :  (  getConnection ),     public function searchMessages($imapConnection, $params, $mode = 0); } 


What makes each method I wrote in the comments.
Note: Yes, I know what a singleton is and that this class should be implemented so, but this is not the point!

So, let's begin:

Connect


  public function Connect( $callback ) { $this -> urls['callbackUrl'] = $callback; $session = new Zend_Session_Namespace('OAuth'); $OAuth_Consumer = new Zend_Oauth_Consumer(array_merge($this->config, $this->urls)); try { if (!isset($session -> accessToken)) { if (!isset($session -> requestToken)) { $session -> requestToken = $OAuth_Consumer -> getRequestToken(array('scope' => $this -> scopes), "GET"); $OAuth_Consumer -> redirect(); } else { $session -> accessToken = $OAuth_Consumer -> getAccessToken($_GET, $session -> requestToken); } } $accessToken = $session -> accessToken; $session -> unsetAll(); unset($session); return $accessToken; } catch( exception $e) { $session -> unsetAll(); throw new Zend_Exception("Error occurred. try to reload this page", 5); } } 


Everything is quite simple: We start the session, transfer it to Google to click the Grant access button and get Access Token, using the Request Token sent to us

The main thing is not to forget to make a try-catch block, since if, for example, the user clicks back, then more, until the session is cleared, he will not be able to log in (Request Token is saved in the first step)!

Well, I almost forgot the configs:

  protected $config = array( 'requestScheme' => Zend_Oauth::REQUEST_SCHEME_HEADER, 'version' => '1.0', 'consumerKey' => 'anonymous', 'signatureMethod' => 'HMAC-SHA1', 'consumerSecret' => 'anonymous', ); protected $urls = array('callbackUrl' => "", 'requestTokenUrl' => 'https://www.google.com/accounts/OAuthGetRequestToken', 'userAuthorizationUrl' => 'https://www.google.com/accounts/OAuthAuthorizeToken', 'accessTokenUrl' => 'https://www.google.com/accounts/OAuthGetAccessToken' ); protected $scopes = 'https://mail.google.com/ https://www.googleapis.com/auth/userinfo#email'; 


getConnection


  public function getConnection($accessToken) { $config = new Zend_Oauth_Config(); $config -> setOptions($this::config); $config -> setToken(unserialize($user::accessToken)); $config -> setRequestMethod('GET'); $url = 'https://mail.google.com/mail/b/' . $user -> email . '/imap/'; $urlWithXoauth = $url . '?xoauth_requestor_id=' . urlencode($user -> email); $httpUtility = new Zend_Oauth_Http_Utility(); /** * Get an unsorted array of oauth params, * including the signature based off those params. */ $params = $httpUtility -> assembleParams($url, $config, array('xoauth_requestor_id' => $user -> email)); /** * Sort parameters based on their names, as required * by OAuth. */ ksort($params); /** * Construct a comma-deliminated,ordered,quoted list of * OAuth params as required by XOAUTH. * * Example: oauth_param1="foo",oauth_param2="bar" */ $first = true; $oauthParams = ''; foreach ($params as $key => $value) { // only include standard oauth params if (strpos($key, 'oauth_') === 0) { if (!$first) { $oauthParams .= ','; } $oauthParams .= $key . '="' . urlencode($value) . '"'; $first = false; } } /** * Generate SASL client request, using base64 encoded * OAuth params */ $initClientRequest = 'GET ' . $urlWithXoauth . ' ' . $oauthParams; $initClientRequestEncoded = base64_encode($initClientRequest); /** * Make the IMAP connection and send the auth request */ $imap = new Zend_Mail_Protocol_Imap('imap.gmail.com', '993', true); $authenticateParams = array('XOAUTH', $initClientRequestEncoded); $imap -> requestAndResponse('AUTHENTICATE', $authenticateParams); return $imap; } 


This method is in the example of use by Google, it is documented and works "as is". Besides, it's pretty simple.

Well, go to the most interesting :

searchMessages


Initially, the algorithm of actions:
  1. Building a search string based on parameters
  2. Find the ID of messages that satisfy the conditions
  3. Convert them according to $ mode
  4. PROFIT! :)


Paragraph 1:

  $searchString = 'X-GM-RAW "'; foreach ($params as $key => $value) switch ($key) { // this is dates case "before" : case "after" : $searchString .= $key . ":" . date("Y/m/d", $value) . " "; break; // this is simple strings default : $searchString .= $key . ":" . $value . " "; break; } $searchString = trim($searchString) . '"'; 

Just go through the array with the parameters and convert them to a string. The only exceptions are the dates that we will convert ourselves.

Point 2:

  $messages = $imapConnection -> search(array($searchString)); 

Just right? But as it turned out, this solution does not work at all. The server will give an error, because we did not execute the EXAMINE “INBOX” command. Okay:

  if (isset($params['in'])){ $imapConnection->examine(strtoupper(($params['in']))); } else { $imapConnection->examine("INBOX"); } $messages = $imapConnection -> search(array($searchString)); 


This solution is already working, and it works almost correctly. But, as soon as we have to search in outgoing (in: sent), we will get the wrong answer. I spent a lot of time digging around this problem, and the answer was found.

It turned out that Gmail folders are not called SENT, INBOX, ..., but have names depending on locale (OO). I had to make a simple method for converting folder names:

  protected function getFolder($imap, $folder) { $response = $imap -> requestAndResponse('XLIST "" "*"'); $folders = array(); foreach ($response AS $item) { if ($item[0] != "XLIST") { continue; } $folders[strtoupper(str_replace('\\', '', end($item[1])))] = $item[3]; } return $folders[$folder]; } 


Just find out the list of folders and find the right one. But on this, as it turned out, not all. EXAMINE still does not save the problem, but you need to call the select method to select a folder before searching.

  if (isset($params['in'])) $imapConnection -> select($this -> getFolder($imapConnection, strtoupper($params['in']))); $messages = $imapConnection -> search(array($searchString)); 

Now we have the ID of the messages found, the case for small - convert to the type of messages.

  switch ( $mode ) { case $this::MODE_NONE : return $messages; case $this::MODE_MESSAGES : // fetching (get content of messages) $messages = $imapConnection -> requestAndResponse("FETCH " . implode(',', $messages) . " (X-GM-THRID)"); return $messages; case $this::MODE_THREAD : $messages = $imapConnection -> requestAndResponse("FETCH " . implode(',', $messages) . " (X-GM-THRID)"); $storage = new Zend_Mail_Storage_Imap($imapConnection); $storage -> selectFolder( $this -> getFolder($imapConnection, strtoupper($params['in'])) ); $threads = array(); if ($messages) foreach ($messages AS $message) { if (isset($message[2][1])) { $thread_id = $message[2][1]; if (!isset($threads[$thread_id])) { $threads[$thread_id] = array('all' => $imapConnection -> requestAndResponse("SEARCH X-GM-THRID $thread_id"), 'my' => array()); unset($threads[$thread_id]['all'][0][0]); } $threads[$thread_id]['my'][] = $message[0]; } } $result = array(); foreach ($threads as $thread) if (!array_slice($thread['all'], array_search(max($thread['my']), $thread['all']) + 1)) $result[$storage -> getUniqueId(max($thread['my']))] = $storage -> getMessage(max($thread['my'])); return array_reverse($result); // for right order } 


In the first case we will return an array of identifiers, in the second we will receive the messages themselves, but the most interesting is the third case.

Here we use Zend_Mail_Storage_Imap to receive messages in the form of Zend_Mail_Message.

Do not forget that Zend_Mail_Storage_Imap does not know anything about the folder we have chosen (we have a different numbering of messages), so let's not forget to call the selectFolder method.

The conversion process is simple: get the message thread, convert to the form: [all messages, my messages]. Next, select the last message thread and form the result.

Also let's not forget that the result needs to be turned over, because numbering on the server goes from old to new, well, we are used to the opposite.

That's all! Thank you all for your attention. I hope that the article will be useful to you.

Source: https://habr.com/ru/post/149748/


All Articles