Доброго времени суток, коллеги. В этой статье я расскажу об опыте использовании Gmail API. Как оказалось, данная тема не очень освещена в интернете, да и документация далека от идеала.
Недавно у меня появилась задача: написать PHP приложение для поиска сообщений на Gmail ящике пользователя. Притом не просто поиск, а поиск по параметрам, благо Gmail имеет неплохую строку поиска, позволяющую написать что то вида “is:sent after:2012/08/10”. Да и в API есть расширения IMAP протокола X-GM-*
Итак, нам требуется реализовать интерфейс для авторизации пользователей и поиска сообщений. Для данных целей я использовал Zend Framework, так как проект написан на Zend Framework, да и Google рекомендует его использовать для работы с API.
Обрисуем интерфейс:
Что делает каждый метод я написал в комментариях.
Примечание: да я знаю что такое синглтон и что этот класс стоит так реализовать, но суть не в этом!
Итак, начнем:
Все довольно просто: Запускаем сессию, перекидываем на Google для нажатия кнопки Grant access и получаем Access Token, с помощью переданного нам Request Token’а
Главное не забыть сделать блок try-catch, т.к. если, к примеру, пользователь нажмёт назад, то больше, пока сессия не будет очищена, он авторизоваться не сможет (Request Token сохраняется на первом шаге)!
Ну и чуть не забыл конфиги:
Этот метод есть в примере использования у Google, он документирован и работает «как есть». К тому же он довольно простой.
Ну и переходим к самому интересному:
Вначале алгоритм действий:
Просто проходим по массиву с параметрами и преобразуем их в строку. Исключения составляют лишь даты, которые мы будем преобразовывать сами.
Просто, правда? Но как оказалось это решение не работает вообще. Сервер выдаст ошибку, т.к. мы не выполнили команду EXAMINE “INBOX”. Ну ладно:
Это решение уже работает, и почти правильно работает. Но, как только придется искать в исходящих(in:sent), мы получим неверный ответ. Я потратил много времени копаясь с этой проблемой, и ответ был найден.
Оказалось что у Gmail папки называются не SENT, INBOX, ..., а имеют названия зависящие от локали (оО). Пришлось сделать простой метод преобразования названий папок:
Просто узнаем список папок и найдем нужную. Но на этом, как оказалось, не все. EXAMINE от проблемы все равно не спасает, а вызывать нужно метод select для выбора папки перед поиском.
Теперь у нас есть ID найденых сообщений, дело за малым – преобразовать к виду сообщений.
В 1ом случае так и вернем массив идентификаторов, во втором получим сами сообщения, но самый интересный 3ий случай.
Здесь мы используем Zend_Mail_Storage_Imap для получения сообщений в виде Zend_Mail_Message.
Не стоит забывать что Zend_Mail_Storage_Imap ничего не знает о выбранной нам папке(у нас стала другая нумерация сообщений), по этому не забудем вызвать метод selectFolder.
Процесс преобразования простой: получим тред сообщения, преобразуем к виду: [все сообщения, мои сообщения]. Дальше выбираем последнее сообщение треда и формируем результат.
Также не забудем что результат нужно перевернуть, т.к. нумерация на сервере идет от старых к новым, ну а мы привыкли наоборот.
Вот и все! Спасибо всем за внимание. Надеюсь, что статья окажется вам полезной.
Недавно у меня появилась задача: написать 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.
Процесс преобразования простой: получим тред сообщения, преобразуем к виду: [все сообщения, мои сообщения]. Дальше выбираем последнее сообщение треда и формируем результат.
Также не забудем что результат нужно перевернуть, т.к. нумерация на сервере идет от старых к новым, ну а мы привыкли наоборот.
Вот и все! Спасибо всем за внимание. Надеюсь, что статья окажется вам полезной.