Source for file imap_asearch.php

Documentation is available at imap_asearch.php

  1. <?php
  2.  
  3. /**
  4.  * imap_search.php
  5.  *
  6.  * IMAP asearch routines
  7.  *
  8.  * Subfolder search idea from Patch #806075 by Thomas Pohl xraven at users.sourceforge.net. Thanks Thomas!
  9.  *
  10.  * @author Alex Lemaresquier - Brainstorm <alex at brainstorm.fr>
  11.  * @copyright 1999-2020 The SquirrelMail Project Team
  12.  * @license http://opensource.org/licenses/gpl-license.php GNU Public License
  13.  * @version $Id: imap_asearch.php 14845 2020-01-07 08:09:34Z pdontthink $
  14.  * @package squirrelmail
  15.  * @subpackage imap
  16.  * @see search.php
  17.  * @link http://www.ietf.org/rfc/rfc3501.txt
  18.  */
  19.  
  20. /** This functionality requires the IMAP and date functions
  21.  */
  22. //require_once(SM_PATH . 'functions/imap_general.php');
  23. //require_once(SM_PATH . 'functions/date.php');
  24.  
  25. /** Set to TRUE to dump the IMAP dialogue
  26.  * @global bool $imap_asearch_debug_dump 
  27.  */
  28. $imap_asearch_debug_dump FALSE;
  29.  
  30. /** IMAP SEARCH keys
  31.  * @global array $imap_asearch_opcodes 
  32.  */
  33. global $imap_asearch_opcodes;
  34. $imap_asearch_opcodes array(
  35. /* <sequence-set> => 'asequence', */    // Special handling, @see sqimap_asearch_build_criteria()
  36. /*'ALL' is binary operator */
  37.     'ANSWERED' => '',
  38.     'BCC' => 'astring',
  39.     'BEFORE' => 'adate',
  40.     'BODY' => 'astring',
  41.     'CC' => 'astring',
  42.     'DELETED' => '',
  43.     'DRAFT' => '',
  44.     'FLAGGED' => '',
  45.     'FROM' => 'astring',
  46.     'HEADER' => 'afield',    // Special syntax for this one, @see sqimap_asearch_build_criteria()
  47.     'KEYWORD' => 'akeyword',
  48.     'LARGER' => 'anum',
  49.     'NEW' => '',
  50. /*'NOT' is unary operator */
  51.     'OLD' => '',
  52.     'ON' => 'adate',
  53. /*'OR' is binary operator */
  54.     'RECENT' => '',
  55.     'SEEN' => '',
  56.     'SENTBEFORE' => 'adate',
  57.     'SENTON' => 'adate',
  58.     'SENTSINCE' => 'adate',
  59.     'SINCE' => 'adate',
  60.     'SMALLER' => 'anum',
  61.     'SUBJECT' => 'astring',
  62.     'TEXT' => 'astring',
  63.     'TO' => 'astring',
  64.     'UID' => 'asequence',
  65.     'UNANSWERED' => '',
  66.     'UNDELETED' => '',
  67.     'UNDRAFT' => '',
  68.     'UNFLAGGED' => '',
  69.     'UNKEYWORD' => 'akeyword',
  70.     'UNSEEN' => ''
  71. );
  72.  
  73. /** IMAP SEARCH month names encoding
  74.  * @global array $imap_asearch_months 
  75.  */
  76. $imap_asearch_months array(
  77.     '01' => 'jan',
  78.     '02' => 'feb',
  79.     '03' => 'mar',
  80.     '04' => 'apr',
  81.     '05' => 'may',
  82.     '06' => 'jun',
  83.     '07' => 'jul',
  84.     '08' => 'aug',
  85.     '09' => 'sep',
  86.     '10' => 'oct',
  87.     '11' => 'nov',
  88.     '12' => 'dec'
  89. );
  90.  
  91. /**
  92.  * Function to display an error related to an IMAP query.
  93.  * We need to do our own error management since we may receive NO responses on purpose (even BAD with SORT or THREAD)
  94.  * so we call sqimap_error_box() if the function exists (sm >= 1.5) or use our own embedded code
  95.  * @global array imap_error_titles
  96.  * @param string $response the imap server response code
  97.  * @param string $query the failed query
  98.  * @param string $message an optional error message
  99.  * @param string $link an optional link to try again
  100.  */
  101. //@global array color sm colors array
  102. function sqimap_asearch_error_box($response$query$message$link '')
  103. {
  104.     global $color;
  105.     // Error message titles according to IMAP server returned code
  106.     $imap_error_titles array(
  107.         'OK' => '',
  108.         'NO' => _("ERROR: Could not complete request."),
  109.         'BAD' => _("ERROR: Bad or malformed request."),
  110.         'BYE' => _("ERROR: IMAP server closed the connection."),
  111.         '' => _("ERROR: Connection dropped by IMAP server.")
  112.     );
  113.  
  114.  
  115.     if (!array_key_exists($response$imap_error_titles))
  116.         $title _("ERROR: Unknown IMAP response.");
  117.     else
  118.         $title $imap_error_titles[$response];
  119.     if ($link == '')
  120.         $message_title _("Reason Given:");
  121.     else
  122.         $message_title _("Possible reason:");
  123.     $message_title .= ' ';
  124.     sqimap_error_box($title$query$message_title$message$link);
  125. }
  126.  
  127. /**
  128.  * This is a convenient way to avoid spreading if (isset(... all over the code
  129.  * @param mixed $var any variable (reference)
  130.  * @param mixed $def default value to return if unset (default is zls (''), pass 0 or array() when appropriate)
  131.  * @return mixed $def if $var is unset, otherwise $var
  132.  */
  133. function asearch_nz(&$var$def '')
  134. {
  135.     if (isset($var))
  136.         return $var;
  137.     return $def;
  138. }
  139.  
  140. /**
  141.  * This should give the same results as PHP 4 >= 4.3.0's html_entity_decode(),
  142.  * except it doesn't handle hex constructs
  143.  * @param string $string string to unhtmlentity()
  144.  * @return string decoded string
  145.  */
  146. function asearch_unhtmlentities($string{
  147.     $trans_tbl array_flip(get_html_translation_table(HTML_ENTITIES));
  148.     for ($i=127$i<255$i++)    /* Add &#<dec>; entities */
  149.         $trans_tbl['&#' $i ';'chr($i);
  150.     return strtr($string$trans_tbl);
  151. /* I think the one above is quicker, though it should be benchmarked
  152.     $string = strtr($string, array_flip(get_html_translation_table(HTML_ENTITIES)));
  153.     return preg_replace("/&#([0-9]+);/E", "chr('\\1')", $string);
  154.  */
  155. }
  156.  
  157. /** Encode a string to quoted or literal as defined in rfc 3501
  158.  *
  159.  * -  4.3 String:
  160.  *        A quoted string is a sequence of zero or more 7-bit characters,
  161.  *         excluding CR and LF, with double quote (<">) characters at each end.
  162.  * -  9. Formal Syntax:
  163.  *        quoted-specials = DQUOTE / "\"
  164.  * @param string $what string to encode
  165.  * @param string $charset search charset used
  166.  * @return string encoded string
  167.  */
  168. function sqimap_asearch_encode_string($what$charset)
  169. {
  170.     if (strtoupper($charset== 'ISO-2022-JP')    // This should be now handled in imap_utf7_local?
  171.         $what mb_convert_encoding($what'JIS''auto');
  172.     if (preg_match('/["\\\\\r\n\x80-\xff]/'$what))
  173.         return '{' strlen($what"}\r\n" $what;    // 4.3 literal form
  174.     return '"' $what '"';    // 4.3 quoted string form
  175. }
  176.  
  177. /**
  178.  * Parses a user date string into an rfc 3501 date string
  179.  * Handles space, slash, backslash, dot and comma as separators (and dash of course ;=)
  180.  * @global array imap_asearch_months
  181.  * @param string user date
  182.  * @return array a preg_match-style array:
  183.  *   - [0] = fully formatted rfc 3501 date string (<day number>-<US month TLA>-<4 digit year>)
  184.  *   - [1] = day
  185.  *   - [2] = month
  186.  *   - [3] = year
  187.  */
  188. function sqimap_asearch_parse_date($what)
  189. {
  190.     global $imap_asearch_months;
  191.  
  192.     $what trim($what);
  193.     $what preg_replace('/[ \/\\.,]+/''-'$what);
  194.     if ($what{
  195.         preg_match('/^([0-9]+)-+([^\-]+)-+([0-9]+)$/'$what$what_parts);
  196.         if (count($what_parts== 4{
  197.             $what_month strtolower(asearch_unhtmlentities($what_parts[2]));
  198. /*                if (!in_array($what_month, $imap_asearch_months)) {*/
  199.                 foreach ($imap_asearch_months as $month_number => $month_code{
  200.                     if (($what_month == $month_number)
  201.                     || ($what_month == $month_code)
  202.                     || ($what_month == strtolower(asearch_unhtmlentities(getMonthName($month_number))))
  203.                     || ($what_month == strtolower(asearch_unhtmlentities(getMonthAbrv($month_number))))
  204.                     {
  205.                         $what_parts[2$month_number;
  206.                         $what_parts[0$what_parts[1'-' $month_code '-' $what_parts[3];
  207.                         break;
  208.                     }
  209.                 }
  210. /*                }*/
  211.         }
  212.     }
  213.     else
  214.         $what_parts array();
  215.     return $what_parts;
  216. }
  217.  
  218. /**
  219.  * Build one criteria sequence
  220.  * @global array imap_asearch_opcodes
  221.  * @param string $opcode search opcode
  222.  * @param string $what opcode argument
  223.  * @param string $charset search charset
  224.  * @return string one full criteria sequence
  225.  */
  226. function sqimap_asearch_build_criteria($opcode$what$charset)
  227. {
  228.     global $imap_asearch_opcodes;
  229.  
  230.     $criteria '';
  231.     switch ($imap_asearch_opcodes[$opcode]{
  232.         default:
  233.         case 'anum':
  234.             $what str_replace(' '''$what);
  235.             $what preg_replace('/[^0-9]+[^KMG]$/'''strtoupper($what));
  236.             if ($what != ''{
  237.                 switch (substr($what-1)) {
  238.                     case 'G':
  239.                         $what substr($what0-1<< 30;
  240.                     break;
  241.                     case 'M':
  242.                         $what substr($what0-1<< 20;
  243.                     break;
  244.                     case 'K':
  245.                         $what substr($what0-1<< 10;
  246.                     break;
  247.                 }
  248.                 $criteria $opcode ' ' $what ' ';
  249.             }
  250.         break;
  251.         case '':    //aflag
  252.             $criteria $opcode ' ';
  253.         break;
  254.         case 'afield':    /* HEADER field-name: field-body */
  255.             preg_match('/^([^:]+):(.*)$/'$what$what_parts);
  256.             if (count($what_parts== 3)
  257.                 $criteria $opcode ' ' .
  258.                     sqimap_asearch_encode_string($what_parts[1]$charset' ' .
  259.                     sqimap_asearch_encode_string($what_parts[2]$charset' ';
  260.         break;
  261.         case 'adate':
  262.             $what_parts sqimap_asearch_parse_date($what);
  263.             if (isset($what_parts[0]))
  264.                 $criteria $opcode ' ' $what_parts[0' ';
  265.         break;
  266.         case 'akeyword':
  267.         case 'astring':
  268.             $criteria $opcode ' ' sqimap_asearch_encode_string($what$charset' ';
  269.         break;
  270.         case 'asequence':
  271.             $what preg_replace('/[^0-9:()]+/'''$what);
  272.             if ($what != '')
  273.                 $criteria $opcode ' ' $what ' ';
  274.         break;
  275.     }
  276.     return $criteria;
  277. }
  278.  
  279. /**
  280.  * Another way to do array_values(array_unique(array_merge($to, $from)));
  281.  * @param array $to to array (reference)
  282.  * @param array $from from array
  283.  * @return array uniquely merged array
  284.  */
  285. function sqimap_array_merge_unique(&$to$from)
  286. {
  287.     if (empty($to))
  288.         return $from;
  289.     $count count($from);
  290.     for ($i 0$i $count$i++{
  291.         if (!in_array($from[$i]$to))
  292.             $to[$from[$i];
  293.     }
  294.     return $to;
  295. }
  296.  
  297. /**
  298.  * Run the IMAP SEARCH command as defined in rfc 3501
  299.  * @link http://www.ietf.org/rfc/rfc3501.txt
  300.  * @param resource $imapConnection the current imap stream
  301.  * @param string $search_string the full search expression eg "ALL RECENT"
  302.  * @param string $search_charset charset to use or zls ('')
  303.  * @return array an IDs or UIDs array of matching messages or an empty array
  304.  * @since 1.5.0
  305.  */
  306. function sqimap_run_search($imapConnection$search_string$search_charset)
  307. {
  308.     //For some reason, this seems to happen and forbids searching servers not allowing OPTIONAL [CHARSET]
  309.     if (strtoupper($search_charset== 'US-ASCII')
  310.         $search_charset '';
  311.     /* 6.4.4 try OPTIONAL [CHARSET] specification first */
  312.     if ($search_charset != '')
  313.         $query 'SEARCH CHARSET "' strtoupper($search_charset'" ' $search_string;
  314.     else
  315.         $query 'SEARCH ' $search_string;
  316.     $readin sqimap_run_command_list($imapConnection$queryfalse$response$messageTRUE);
  317.  
  318.     /* 6.4.4 try US-ASCII charset if we tried an OPTIONAL [CHARSET] and received a tagged NO response (SHOULD be [BADCHARSET]) */
  319.     if (($search_charset != '')  && (strtoupper($response== 'NO')) {
  320.         $query 'SEARCH CHARSET US-ASCII ' $search_string;
  321.         $readin sqimap_run_command_list($imapConnection$queryfalse$response$messageTRUE);
  322.     }
  323.     if (strtoupper($response!= 'OK'{
  324.         sqimap_asearch_error_box($response$query$message);
  325.         return array();
  326.     }
  327.     $messagelist parseUidList($readin,'SEARCH');
  328.  
  329.     if (empty($messagelist))    //Empty search response, ie '* SEARCH'
  330.         return array();
  331.  
  332.     $cnt count($messagelist);
  333.     for ($q 0$q $cnt$q++)
  334.         $id[$qtrim($messagelist[$q]);
  335.     return $id;
  336. }
  337.  
  338. /**
  339.  * @global bool allow_charset_search user setting
  340.  * @global array languages sm languages array
  341.  * @global string squirrelmail_language user language setting
  342.  * @return string the user defined charset if $allow_charset_search is TRUE else zls ('')
  343.  */
  344. {
  345.     global $allow_charset_search$languages$squirrelmail_language;
  346.  
  347.     if ($allow_charset_search)
  348.         return $languages[$squirrelmail_language]['CHARSET'];
  349.     return '';
  350. }
  351.  
  352. /**
  353.  * Convert SquirrelMail internal sort to IMAP sort taking care of:
  354.  * - user defined date sorting (ARRIVAL vs DATE)
  355.  * - if the searched mailbox is the sent folder then TO is being used instead of FROM
  356.  * - reverse order by using REVERSE
  357.  * @param string $mailbox mailbox name to sort
  358.  * @param integer $sort_by sm sort criteria index
  359.  * @global bool internal_date_sort sort by arrival date instead of message date
  360.  * @global string sent_folder sent folder name
  361.  * @return string imap sort criteria
  362.  */
  363. function sqimap_asearch_get_sort_criteria($mailbox$sort_by)
  364. {
  365.     global $internal_date_sort$sent_folder;
  366.  
  367.     $sort_opcodes array ('DATE''FROM''SUBJECT''SIZE');
  368.     if ($internal_date_sort == true)
  369.         $sort_opcodes[0'ARRIVAL';
  370. // FIXME: Why are these commented out?  I have no idea what this code does, but both of these functions sound more robust than the simple string check that's being used now.  Someone who understands this code should either fix this or remove these lines completely or document why they are here commented out
  371. //        if (handleAsSent($mailbox))
  372. //        if (isSentFolder($mailbox))
  373.     if ($mailbox == $sent_folder)
  374.         $sort_opcodes[1'TO';
  375.     return (($sort_by 2'' 'REVERSE '$sort_opcodes[($sort_by >> 13];
  376. }
  377.  
  378. /**
  379.  * @param string $cur_mailbox unformatted mailbox name
  380.  * @param array $boxes_unformatted selectable mailbox unformatted names array (reference)
  381.  * @return array sub mailboxes unformatted names
  382.  */
  383. function sqimap_asearch_get_sub_mailboxes($cur_mailbox&$mboxes_array)
  384. {
  385.     $sub_mboxes_array array();
  386.     $boxcount count($mboxes_array);
  387.     for ($boxnum=0$boxnum $boxcount$boxnum++{
  388.         if (isBoxBelow($mboxes_array[$boxnum]$cur_mailbox))
  389.             $sub_mboxes_array[$mboxes_array[$boxnum];
  390.     }
  391.     return $sub_mboxes_array;
  392. }
  393.  
  394. /**
  395.  * Create the search query strings for all given criteria and merge results for every mailbox
  396.  * @param resource $imapConnection 
  397.  * @param array $mailbox_array (reference)
  398.  * @param array $biop_array (reference)
  399.  * @param array $unop_array (reference)
  400.  * @param array $where_array (reference)
  401.  * @param array $what_array (reference)
  402.  * @param array $exclude_array (reference)
  403.  * @param array $sub_array (reference)
  404.  * @param array $mboxes_array selectable unformatted mailboxes names (reference)
  405.  * @return array array(mailbox => array(UIDs))
  406.  */
  407. function sqimap_asearch($imapConnection&$mailbox_array&$biop_array&$unop_array&$where_array&$what_array&$exclude_array&$sub_array&$mboxes_array)
  408. {
  409.  
  410.     $search_charset sqimap_asearch_get_charset();
  411.     $mbox_search array();
  412.     $search_string '';
  413.     $cur_mailbox $mailbox_array[0];
  414.     $cur_biop '';    /* Start with ALL */
  415.     /* We loop one more time than the real array count, so the last search gets fired */
  416.     for ($cur_crit=0,$iCnt=count($where_array)$cur_crit <= $iCnt++$cur_crit{
  417.         if (empty($exclude_array[$cur_crit])) {
  418.             $next_mailbox (isset($mailbox_array[$cur_crit])) $mailbox_array[$cur_critfalse;
  419.             if ($next_mailbox != $cur_mailbox{
  420.                 $search_string trim($search_string);    /* Trim out last space */
  421.                 if ($cur_mailbox == 'All Folders')
  422.                     $search_mboxes $mboxes_array;
  423.                 else if ((!empty($sub_array[$cur_crit 1])) || (!in_array($cur_mailbox$mboxes_array)))
  424.                     $search_mboxes sqimap_asearch_get_sub_mailboxes($cur_mailbox$mboxes_array);
  425.                 else
  426.                     $search_mboxes array($cur_mailbox);
  427.                 foreach ($search_mboxes as $cur_mailbox{
  428.                     if (isset($mbox_search[$cur_mailbox])) {
  429.                         $mbox_search[$cur_mailbox]['search'.= ' ' $search_string;
  430.                     else {
  431.                         $mbox_search[$cur_mailbox]['search'$search_string;
  432.                     }
  433.                     $mbox_search[$cur_mailbox]['charset'$search_charset;
  434.                 }
  435.                 $cur_mailbox $next_mailbox;
  436.                 $search_string '';
  437.             }
  438.             if (isset($where_array[$cur_crit]&& empty($exclude_array[$cur_crit])) {
  439.                 $aCriteria array();
  440.                 for ($crit $cur_crit$crit count($where_array)$crit++{
  441.                     $criteria trim(sqimap_asearch_build_criteria($where_array[$crit]$what_array[$crit]$search_charset));
  442.                     if (!empty($criteria&& empty($exclude_array[$crit])) {
  443.                         if (asearch_nz($mailbox_array[$crit]== $cur_mailbox{
  444.                             $unop $unop_array[$crit];
  445.                             if (!empty($unop)) {
  446.                                 $criteria $unop ' ' $criteria;
  447.                             }
  448.                             $aCriteria[array($biop_array[$crit]$criteria);
  449.                         }
  450.                     }
  451.                     // unset something
  452.                     $exclude_array[$crittrue;
  453.                 }
  454.                 $aSearch array();
  455.                 for($i=0,$iCnt=count($aCriteria);$i<$iCnt;++$i{
  456.                     $cur_biop $aCriteria[$i][0];
  457.                     $next_biop (isset($aCriteria[$i+1][0])) $aCriteria[$i+1][0false;
  458.                     if ($next_biop != $cur_biop && $next_biop == 'OR'{
  459.                         $aSearch['OR '.$aCriteria[$i][1];
  460.                     else if ($cur_biop != 'OR'{
  461.                         $aSearch['ALL '.$aCriteria[$i][1];
  462.                     else // OR only supports 2 search keys so we need to create a parenthesized list
  463.                         $prev_biop (isset($aCriteria[$i-1][0])) $aCriteria[$i-1][0false;
  464.                         if ($prev_biop == $cur_biop{
  465.                             $last $aSearch[$i-1];
  466.                             if (!substr($last,-1== ')'{
  467.                                 $aSearch[$i-1"(OR $last";
  468.                                 $aSearch[$aCriteria[$i][1].')';
  469.                             else {
  470.                                 $sEnd '';
  471.                                 while ($last && substr($last,-1== ')'{
  472.                                     $last substr($last,0,-1);
  473.                                     $sEnd .= ')';
  474.                                 }
  475.                                 $aSearch[$i-1"(OR $last";
  476.                                 $aSearch[$aCriteria[$i][1].$sEnd.')';
  477.                             }
  478.                         else {
  479.                             $aSearch[$aCriteria[$i][1];
  480.                         }
  481.                     }
  482.                 }
  483.                 $search_string .= implode(' ',$aSearch);
  484.             }
  485.         }
  486.     }
  487.     return ($mbox_search);
  488. }

Documentation generated on Mon, 13 Jan 2020 04:22:43 +0100 by phpDocumentor 1.4.3