понедельник, 19 сентября 2011 г.

Как узнать ссылку на цепочку gmail по определённому запросу с помощью php через imap протокол

В gmail можно делать закладки на определенные письма/цепочки писем.

Обычно ссылка имеет вид
https://mail.google.com/mail/#all/124e3d1d452af489
,где "124e3d1d452af489" - уникальный id писма/цепочки писем.

Например, ваши заказы хранятся в таблице и было-бы здорово сделать ссылку на gmail цепочку по каждой заявке.

Пока что я написал только часть скрипта. У меня база хранится в google spreadsheets, ну а у вас это может быть mySql или что-то еще.

Как вытащить данные из таблиц гугл вы можете посмотреть тут (на английском, но всё очень подробно и понятно).

Да и еще, пока я ковырялся с php, в google apps scripts появилась возможность работать с gmail, так что скорее всего Я не буду дописывать этот скрипт (ну там как всегда чего-то не работает, чего-то не хватает - нафиг - буду продолжать этот скрипт). Но если что-то нужно - спрашивайте.

Итак Я использую ZEND framework, а именно класс Zend/Mail/Protocol/Imap.php (версия, с которой Я работал) и дополняющий класс, который написал сам - shkurIMAPclassExtendsZendProtocolImap.php

И собственно сам скрипт: (есть версия посвежее - если надо спрашивайте - выложу)

<?php
echo 'Не забудьте включить IMAP в настройках почты!';
$login = "user@exmple.com"; //обязательно вида user@domain.com
$pass = "pass";
//require_once "Zend/Mail/Storage/Imap.php"; //кагбэ не нужен
require_once "Zend/Mail/Protocol/Imap.php";
//require_once "Zend/Registry.php"; //кагбэ не нужен
require_once "shkurIMAPclassExtendsZendProtocolImap.php"; // достаточно инициализировать только дочерний класс
//$protocol = new Zend_Mail_Protocol_Imap('imap.gmail.com', 993, true);
$protocol = new shkur_IMAP_class_Extends_Zend_Mail_Protocol_Imap('imap.gmail.com', 993, true);
$protocol->login($login, $pass);
// попробую написать чтобы скрипт сам выбирал папку All mail независимо от локлизации. utf-7 imap кривая кодировка
$xlist = $protocol->xlist();
//print_r($xlist);
$found = array_keys($protocol->getParentStack('\AllMail', $xlist));
$allMail = $found[0];
//$storage = new Zend_Mail_Storage_Imap($protocol); //кагбэ не нужен
//$protocol->select("INBOX"); нам нужна вся почта а это чуть ниже абракадабра
$protocol->select($allMail); // select надо вызывать после $storage = new Zend_Mail_Storage_Imap($protocol); иначе поиск смотрит в INBOX; метод echo $storage->getCurrentFolder(); в любом случае будет выводить INBOX - убиваем перфекциониста - идём по простому пути - работает и х_й с ним. 
//echo $storage->getCurrentFolder(); //ret INBOX в любом случае - неправильно что-то ну хрен с ним
//echo '<br>'; echo "<pre>"; print_r($protocol->listMailbox());  echo "</pre>"; echo '<br>'; массив ящики
//echo $protocol->fetch('X-GM-MSGID', $storage->getUniqueId(1)); // ret  1327644189674155015   то что надо! ура!!!
$findthis = "test"; //  qwertyuiop  живалов&test
echo 'ищщем → <b>'.$findthis.'</b><br>';
//$search = $protocol->search(array("charset utf-8 text", $findthis)); // вот так работает - это поиск через imap протокол низзя пробелы - можно & или |
$search = $protocol->search(array("charset utf-8 X-GM-RAW", $findthis)); //более жадный поиск, например по запорсу живалов|test (это разные цепочки и разные письма) imap ни чего не найдёт, а гугло поиск даст и те и другие письма!
echo "\nкол-во найденных писем по запросу $findthis = ".count($search)."<br>";
if (count($search) < 1 ){ exit('ни чего не найдено в почте. exit');} // или break, exit может прервать выполнение всего скрипта.
//echo "\nsearch= "; print_r($search);
//echo $decID = $protocol->fetch('X-GM-MSGID', $storage->getUniqueId($search[0])-3); //почему-то ставит 50 вместо 47
//echo $decID = $protocol->fetch('X-GM-MSGID', $search[0]); //вот так работает и без всяких getUniqueId
$decID = $protocol->fetch('X-GM-THRID', $search);
$decID = array_unique($decID); // убрать повторяющиеся значения - несколько писем в одной цепочке.
//if ($decID) {} else {echo 'переменной нет';}
//echo "\n$decID= "; print_r($decID);
if ($decID){ // проверка на существование массива/переменной, т.к. если ни чего нет, то будет ошибка foreach
 foreach ($decID as $k => $v) {
  $hexID[]=$protocol->dec2hex($v); 
 }
}

/** подготовим почву для ссылки, которая должна быть вида https://mail.google.com/mail/#all/13262ab3255034e5
*  или если это hosted аккаунт, то (/a/ - должно быть!) https://mail.google.com/a/example.com/#all/124e3d1d452af489 
*/
$domain = substr(strtolower(stristr($login, '@')), 1); //голый домен ёпте! без@
if ($domain == 'gmail.com' or $domain == 'googlemail.com'){/*$domain = 'google.com';*/ $preUrl = 'https://mail.google.com/mail/';}
else {$preUrl = 'https://mail.google.com/a/'.$domain;}
if (count($hexID) == 1 ){ 
 echo "\nнайдена одна единственная цепочка писем, это ОЧЕНЬ гуд :) \n";
 $finalUrl = $preUrl.'#all/'.$hexID[0];
    echo "<a href='$finalUrl'>$finalUrl</a>"; 
    return ($hexID ? $finalUrl : 'FAIL → ссылка $hexID');
}

if (count($hexID) > 1 ){ 
 echo "\nнайдено несолько цепочек писем. применяем ярлык. ссылка должна быть на ярлык. ярлык это папка.
    если применить ярлык к письму, то он будет применен ко всей цепочке.
    Можно ли прменить ярлык к цепочке или надо применить его к каждому письму?\n";  
    /* если у всех писем один общий ярлык вида 'z/*' используй его и не добавляй новый ярлык - борьба с дубликатами.
    * именно 'z/*' потому что писма могут быть объеденены например ярлыком статуса, или каким-то другим общим ярлыком.
    * z это основной ярлык-папка с вложенными ярлыками, которые идут через дробь. Обязательно включить функцию вложенных
    * ярлыков в gmail lab (экспериментальные функции) и добавить руками корневой ярлык и (?один вложенный?) на всякий случай.
    */
    $labels = $protocol->fetch('X-GM-LABELS', $search); //вытащить все ярлыки из почты по данному поисковому запросу
    //print_r($labels); echo "\n".count($labels);
    
    foreach ($labels as $k => $v){
        $str[] = '$labels['.$k.']';
    }
    $str = implode(', ', $str); // строка для сравнения массивов
    eval('$sameLabels = array_intersect('.$str.');'); // сравни массивы и покажи ярлыки присутствующие у всех писем. С доп. (_assoc) проверкой не находит все повторения
    ///*это test*/array_push($sameLabels, 'z/9', 'z/14', 'z/15', 'z/18'); //не использовать $array[] = value - кривые ключи!
    //echo '$sameLabels = '; print_r($sameLabels);
    function findZlabel($val) {
        if (($val{0}.$val{1}) == 'z/'){ //ярлыки для объединения начинаются с z/цифра...
            return ($val);
        }
    }
    $existingLabel = array_filter($sameLabels, "findZlabel"); //найди ярлыки вида z/*
    //echo '$existingLabel = '; print_r($existingLabel);
    if ($existingLabel = array_shift($existingLabel)){ // именно = а не == Ключи в массиве идут абы как т.к. использовали функцию array_intersect
        $finalUrl = $preUrl.'#label/'.urlencode($existingLabel); /**   #label/z%2F0     */  
        echo "\nтру → используем существующий ярлык\n";
        echo "<a href='$finalUrl'>$finalUrl</a>";
        return ($existingLabel ? $finalUrl : 'FAIL → используем существующий ярлык');
    } else {
        echo "\nнетру → создаём новый ярлык\n";
        /** найдём какой использовать новый ярлык
        *  TODO: найти неиспользуемый ярлык и либо удалить его, либо использовать. Как это можно сделать: 
        *  получаем список всех ярлыков, и проверям каждый на наличие в нём письма. Если ярлык ни где не испульзуется используем его.
        *  Проблема в том что это будет использовать много ресурсов учитывая то, что ярлыков будет дахуя > 1000
        *  Может быть имеет смысл $newLabel делать не из последнего ярлыка, а с первого, но опять же ресурсы, ярлыков будет дахуя > 1000
        */
        $xlistz = $protocol->xlist("", "z/*");
        $lastLabel = end(array_keys($xlistz));
        $newLabel = $lastLabel;
        while (array_key_exists($newLabel, $xlistz)) {                                                         
            $newLabel = 'z/'.(str_replace('z/', '', $newLabel) + 1);
        }
        echo "\n\$newLabel = ".$newLabel;

        // применить новый ярлык ко всем письмам по данному запросу и выдать URL
        $store = $protocol->gmailLabel(array($newLabel), $search, null, '+', true);
        //echo ($store ? "\ntrue" : "\nfalse");
        $finalUrl = $preUrl.'#label/'.urlencode($newLabel); /**   #label/z%2F0     */  
        echo "\n<a href='$finalUrl'>$finalUrl</a>";
        return ($store ? $finalUrl : 'FAIL → используем новый ярлык');
    }
}
?>
ну и пожалуй выложу класс который я написал:
<?php

/* 13.09.2011 можно спамить сюда testtesttest73@gmail.com это алиас рабочего ящика
это моя первая попытка написать класc пэхапэ дополняющий Zend_Mail_Protocol_Imap (версия $Id: Imap.php 18977 2009-11-14 14:15:59Z yoshida@zend.co.jp )
т.е. сам по себе этот клас не работает, только вместе с зендом. 
require_once "Zend/Mail/Protocol/Imap.php";
первым делом добавим сюда hex2dec and vice versa
http://code.google.com/intl/ru-RU/apis/gmail/imap/
*/

class shkur_IMAP_class_Extends_Zend_Mail_Protocol_Imap extends Zend_Mail_Protocol_Imap {
    /**
     * set flags label to threads
     *
     * @param  string|array       $flags  flags to set, add or remove - see $mode
     * @param  int         $from   message for items or start message if $to !== null
     * @param  int|null    $to     if null only one message ($from) is fetched, else it's the
     *                             last message, INF means last message avaible
     * @param  string|null $mode   'null' and '+' to add flags, '-' to remove flags, everything else sets the flags as given
     * @param  bool        $silent if false the return values are the new flags for the wanted messages
     * @return bool|array new flags if $silent is false, else true or false depending on success
     * @throws Zend_Mail_Protocol_Exception
     */
    public function gmailLabel($flags, $from, $to = null, $mode = null, $silent = true){
        $item = 'X-GM-LABELS';
        ($mode == null ? $mode = '+' : null);
        if ($mode == '+' || $mode == '-') {
            $item = $mode . $item;
        }
 
        $flags = $this->escapeList($flags);
        if (is_array($from)) {
            $set = implode(',', $from);
        } else if ($to === null) {
            $set = (int)$from;
        } else if ($to === INF) {
            $set = (int)$from . ':*';
        } else {
            $set = (int)$from . ':' . (int)$to;
        }
        
        $result = $this->requestAndResponse('STORE', array($set, $item, $flags), $silent);

        if ($silent) {
            return $result ? true : false;
        }

        $tokens = $result;
        $result = array();
        foreach ($tokens as $token) {
            if ($token[1] != 'FETCH' || $token[2][0] != 'X-GM-LABELS') {
                continue;
            }
            $result[$token[0]] = $token[2][1];
        }

        return $result;
    }
    
 /**
  * Gets the parent stack of a string array element if it is found within the 
  * parent array
  * 
  * This will not search objects within an array, though I suspect you could 
  * tweak it easily enough to do that
  *
  * @param string $child The string array element to search for
  * @param array $stack The stack to search within for the child
  * @return array An array containing the parent stack for the child if found,
  *               false otherwise
  */
 function getParentStack($child, $stack) {
  foreach ($stack as $k => $v) {
   if (is_array($v)) {
    // If the current element of the array is an array, recurse it and capture the return
    $return = $this->getParentStack($child, $v);
    
    // If the return is an array, stack it and return it
    if (is_array($return)) {
     return array($k => $return);
    }
   } else {
    // Since we are not on an array, compare directly
    if ($v == $child) {
     // And if we match, stack it and return it
     return array($k => $child);
    }
   }
  }
  // Return false since there was nothing found
  return false;
 }

 /**
  * Gets the complete parent stack of a string array element if it is found 
  * within the parent array
  * 
  * This will not search objects within an array, though I suspect you could 
  * tweak it easily enough to do that
  *
  * @param string $child The string array element to search for
  * @param array $stack The stack to search within for the child
  * @return array An array containing the parent stack for the child if found,
  *               false otherwise
  */
 function getParentStackComplete($child, $stack){
     $return = array();
  foreach ($stack as $k => $v) {
   if (is_array($v)){
    // If the current element of the array is an array, recurse it 
    // and capture the return stack
    $stack = $this->getParentStackComplete($child, $v);
    
    // If the return stack is an array, add it to the return
    if (is_array($stack) && !empty($stack)){
      $return[$k] = $stack;
    }
   } else {
                // Since we are not on an array, compare directly
    if ($v == $child){
                    // And if we match, stack it and return it
     $return[$k] = $child;
    }
   }
     }
 // Return the stack
 return empty($return) ? false: $return;
 }

     /** list переделанный в Xlist
     * get mailbox list
     *
     * this method can't be named after the IMAP command 'LIST', as list is a reserved keyword
     *
     * @param  string $reference mailbox reference for list
     * @param  string $mailbox   mailbox name match with wildcards
     * @return array mailboxes that matched $mailbox as array(globalName => array('delim' => .., 'flags' => ..))
     * @throws Zend_Mail_Protocol_Exception
     */
    public function xlist($reference = '', $mailbox = '*'){
        $result = array();
        $list = $this->requestAndResponse('XLIST', $this->escapeString($reference, $mailbox));
        if (!$list || $list === true){
            return $result;
        }
        foreach ($list as $item){
            if (count($item) != 4 || $item[0] != 'XLIST'){
                continue;
            }
            $result[$item[3]] = array('delim' => $item[2], 'flags' => $item[1]);
        }
        return $result;
    }

 /* Input: A decimal number as a String.
  Output: The equivalent hexadecimal number as a String.*/
 function dec2hex($number){
  $hexvalues = array('0','1','2','3','4','5','6','7',
        '8','9','a','b','c','d','e','f');
  $hexval = '';
   while($number != '0') {
      $hexval = $hexvalues[bcmod($number,'16')].$hexval;
   $number = bcdiv($number,'16',0);
  }
  return $hexval;
 }

 /* Input: A hexadecimal number as a String.
   Output: The equivalent decimal number as a String. */
 function hex2dec($number){
  $decvalues = array('0' => '0', '1' => '1', '2' => '2',
        '3' => '3', '4' => '4', '5' => '5',
        '6' => '6', '7' => '7', '8' => '8',
        '9' => '9', 'a' => '10', 'b' => '11',
        'c' => '12', 'd' => '13', 'e' => '14',
        'f' => '15');
  $decval = '0';
  $number = strrev($number);
  for($i = 0; $i < strlen($number); $i++){
      $decval = bcadd(bcmul(bcpow('16',$i,0),$decvalues[$number{$i}]), $decval);
  }
  return $decval;
 }  

    /** так как эта функция будет исключена в следующих версиях Zend я пихну её сюда.
     * do a search request
     *
     * This method is currently marked as internal as the API might change and is not
     * safe if you don't take precautions.
     *
     * @internal
     * @return array message ids                        array($this->escapeString($params)) - непонятная херня
     */
    public function search(array $params){
        $response = $this->requestAndResponse('SEARCH', $params);
        if (!$response) {
            return 'gotohell';//$response;
        }

        foreach ($response as $ids) {
            if ($ids[0] == 'SEARCH') {
                array_shift($ids);
                return $ids;
            }
        }
        return array();
    }
}
?>

2 комментария:

  1. Этот комментарий был удален автором.

    ОтветитьУдалить
  2. Там ещё скрипт в google spreadsheets который отправляет данные для поиска...

    ОтветитьУдалить

откомментить