mixkorshun
Доброго времени суток, коллеги. В этой статье я расскажу об опыте использовании Gmail API. Как оказалось, данная тема не очень освещена в интернете, да и документация далека от идеала.
Недавно у меня появилась задача: написать PHP приложение для поиска сообщений на Gmail ящике пользователя. Притом не просто поиск, а поиск по параметрам, благо Gmail имеет неплохую строку поиска, позволяющую написать что то вида "is:sent after:2012/08/10". Да и в API есть расширения IMAP протокола X-GM-*
Итак, нам требуется реализовать интерфейс для авторизации пользователей и поиска сообщений. Для данных целей я использовал Zend Framework, так как проект написан на Zend Framework, да и Google рекомендует его использовать для работы с API.
Обрисуем интерфейс:
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);
}
Что делает каждый метод я написал в комментариях.
Примечание: да я знаю что такое синглтон и что этот класс стоит так реализовать, но суть не в этом!
Итак, начнем:
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);
}
}
Все довольно просто: Запускаем сессию, перекидываем на Google для нажатия кнопки Grant access и получаем Access Token, с помощью переданного нам Request Token"а
Главное не забыть сделать блок try-catch, т.к. если, к примеру, пользователь нажмёт назад, то больше, пока сессия не будет очищена, он авторизоваться не сможет (Request Token сохраняется на первом шаге)!
Ну и чуть не забыл конфиги:
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;
}
Этот метод есть в примере использования у Google, он документирован и работает "как есть". К тому же он довольно простой.
Ну и переходим к самому интересному:
searchMessages
Вначале алгоритм действий:
- Выстраиваем на основе параметров строку поиска
- Находим ID сообщений удовлетворяющих условиям
- Преобразуем их в зависимости от $mode
- PROFIT! :)
Пункт 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) . '"';
Просто проходим по массиву с параметрами и преобразуем их в строку. Исключения составляют лишь даты, которые мы будем преобразовывать сами.
Пункт 2:
$messages = $imapConnection -> search(array($searchString));
Просто, правда? Но как оказалось это решение не работает вообще. Сервер выдаст ошибку, т.к. мы не выполнили команду EXAMINE "INBOX". Ну ладно:
if (isset($params['in'])){
$imapConnection->examine(strtoupper(($params['in'])));
} else {
$imapConnection->examine("INBOX");
}
$messages = $imapConnection -> search(array($searchString));
Это решение уже работает, и почти правильно работает. Но, как только придется искать в исходящих(in:sent), мы получим неверный ответ. Я потратил много времени копаясь с этой проблемой, и ответ был найден.
Оказалось что у Gmail папки называются не SENT, INBOX, ..., а имеют названия зависящие от локали (оО). Пришлось сделать простой метод преобразования названий папок:
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];
}
Просто узнаем список папок и найдем нужную. Но на этом, как оказалось, не все. EXAMINE от проблемы все равно не спасает, а вызывать нужно метод select для выбора папки перед поиском.
if (isset($params['in']))
$imapConnection -> select($this -> getFolder($imapConnection, strtoupper($params['in'])));
$messages = $imapConnection -> search(array($searchString));
Теперь у нас есть ID найденых сообщений, дело за малым - преобразовать к виду сообщений.
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
}
В 1ом случае так и вернем массив идентификаторов, во втором получим сами сообщения, но самый интересный 3ий случай.
Здесь мы используем Zend_Mail_Storage_Imap для получения сообщений в виде Zend_Mail_Message.
Не стоит забывать что Zend_Mail_Storage_Imap ничего не знает о выбранной нам папке(у нас стала другая нумерация сообщений), по этому не забудем вызвать метод selectFolder.
Процесс преобразования простой: получим тред сообщения, преобразуем к виду: [все сообщения, мои сообщения]. Дальше выбираем последнее сообщение треда и формируем результат.
Также не забудем что результат нужно перевернуть, т.к. нумерация на сервере идет от старых к новым, ну а мы привыкли наоборот.
Вот и все! Спасибо всем за внимание. Надеюсь, что статья окажется вам полезной.