PageRenderTime 52ms CodeModel.GetById 34ms app.highlight 13ms RepoModel.GetById 1ms app.codeStats 0ms

/trunk/squirrelmail/functions/imap_asearch.php

#
PHP | 489 lines | 311 code | 27 blank | 151 comment | 87 complexity | 6dd12d7a4484892b09464662c7ff4822 MD5 | raw file
  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-2012 The SquirrelMail Project Team
 12 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
 13 * @version $Id: imap_asearch.php 14249 2012-01-02 02:09:17Z 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 */
 33global $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
102function 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 */
133function 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 */
146function 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 */
168function 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 */
188function 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 */
226function 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($what, 0, -1) << 30;
240                    break;
241                    case 'M':
242                        $what = substr($what, 0, -1) << 20;
243                    break;
244                    case 'K':
245                        $what = substr($what, 0, -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 */
285function 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 */
306function 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, $query, false, $response, $message, TRUE);
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, $query, false, $response, $message, TRUE);
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[$q] = trim($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 */
344function sqimap_asearch_get_charset()
345{
346    global $allow_charset_search, $languages, $squirrelmail_language;
347
348    if ($allow_charset_search)
349        return $languages[$squirrelmail_language]['CHARSET'];
350    return '';
351}
352
353/**
354 * Convert SquirrelMail internal sort to IMAP sort taking care of:
355 * - user defined date sorting (ARRIVAL vs DATE)
356 * - if the searched mailbox is the sent folder then TO is being used instead of FROM
357 * - reverse order by using REVERSE
358 * @param string $mailbox mailbox name to sort
359 * @param integer $sort_by sm sort criteria index
360 * @global bool internal_date_sort sort by arrival date instead of message date
361 * @global string sent_folder sent folder name
362 * @return string imap sort criteria
363 */
364function sqimap_asearch_get_sort_criteria($mailbox, $sort_by)
365{
366    global $internal_date_sort, $sent_folder;
367
368    $sort_opcodes = array ('DATE', 'FROM', 'SUBJECT', 'SIZE');
369    if ($internal_date_sort == true)
370        $sort_opcodes[0] = 'ARRIVAL';
371// 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
372//        if (handleAsSent($mailbox))
373//        if (isSentFolder($mailbox))
374    if ($mailbox == $sent_folder)
375        $sort_opcodes[1] = 'TO';
376    return (($sort_by % 2) ? '' : 'REVERSE ') . $sort_opcodes[($sort_by >> 1) & 3];
377}
378
379/**
380 * @param string $cur_mailbox unformatted mailbox name
381 * @param array $boxes_unformatted selectable mailbox unformatted names array (reference)
382 * @return array sub mailboxes unformatted names
383 */
384function sqimap_asearch_get_sub_mailboxes($cur_mailbox, &$mboxes_array)
385{
386    $sub_mboxes_array = array();
387    $boxcount = count($mboxes_array);
388    for ($boxnum=0; $boxnum < $boxcount; $boxnum++) {
389        if (isBoxBelow($mboxes_array[$boxnum], $cur_mailbox))
390            $sub_mboxes_array[] = $mboxes_array[$boxnum];
391    }
392    return $sub_mboxes_array;
393}
394
395/**
396 * Create the search query strings for all given criteria and merge results for every mailbox
397 * @param resource $imapConnection
398 * @param array $mailbox_array (reference)
399 * @param array $biop_array (reference)
400 * @param array $unop_array (reference)
401 * @param array $where_array (reference)
402 * @param array $what_array (reference)
403 * @param array $exclude_array (reference)
404 * @param array $sub_array (reference)
405 * @param array $mboxes_array selectable unformatted mailboxes names (reference)
406 * @return array array(mailbox => array(UIDs))
407 */
408function sqimap_asearch($imapConnection, &$mailbox_array, &$biop_array, &$unop_array, &$where_array, &$what_array, &$exclude_array, &$sub_array, &$mboxes_array)
409{
410
411    $search_charset = sqimap_asearch_get_charset();
412    $mbox_search = array();
413    $search_string = '';
414    $cur_mailbox = $mailbox_array[0];
415    $cur_biop = '';    /* Start with ALL */
416    /* We loop one more time than the real array count, so the last search gets fired */
417    for ($cur_crit=0,$iCnt=count($where_array); $cur_crit <= $iCnt; ++$cur_crit) {
418        if (empty($exclude_array[$cur_crit])) {
419            $next_mailbox = (isset($mailbox_array[$cur_crit])) ? $mailbox_array[$cur_crit] : false;
420            if ($next_mailbox != $cur_mailbox) {
421                $search_string = trim($search_string);    /* Trim out last space */
422                if ($cur_mailbox == 'All Folders')
423                    $search_mboxes = $mboxes_array;
424                else if ((!empty($sub_array[$cur_crit - 1])) || (!in_array($cur_mailbox, $mboxes_array)))
425                    $search_mboxes = sqimap_asearch_get_sub_mailboxes($cur_mailbox, $mboxes_array);
426                else
427                    $search_mboxes = array($cur_mailbox);
428                foreach ($search_mboxes as $cur_mailbox) {
429                    if (isset($mbox_search[$cur_mailbox])) {
430                        $mbox_search[$cur_mailbox]['search'] .= ' ' . $search_string;
431                    } else {
432                        $mbox_search[$cur_mailbox]['search'] = $search_string;
433                    }
434                    $mbox_search[$cur_mailbox]['charset'] = $search_charset;
435                }
436                $cur_mailbox = $next_mailbox;
437                $search_string = '';
438            }
439            if (isset($where_array[$cur_crit]) && empty($exclude_array[$cur_crit])) {
440                $aCriteria = array();
441                for ($crit = $cur_crit; $crit < count($where_array); $crit++) {
442                    $criteria = trim(sqimap_asearch_build_criteria($where_array[$crit], $what_array[$crit], $search_charset));
443                    if (!empty($criteria) && empty($exclude_array[$crit])) {
444                        if (asearch_nz($mailbox_array[$crit]) == $cur_mailbox) {
445                            $unop = $unop_array[$crit];
446                            if (!empty($unop)) {
447                                $criteria = $unop . ' ' . $criteria;
448                            }
449                            $aCriteria[] = array($biop_array[$crit], $criteria);
450                        }
451                    }
452                    // unset something
453                    $exclude_array[$crit] = true;
454                }
455                $aSearch = array();
456                for($i=0,$iCnt=count($aCriteria);$i<$iCnt;++$i) {
457                    $cur_biop = $aCriteria[$i][0];
458                    $next_biop = (isset($aCriteria[$i+1][0])) ? $aCriteria[$i+1][0] : false;
459                    if ($next_biop != $cur_biop && $next_biop == 'OR') {
460                        $aSearch[] = 'OR '.$aCriteria[$i][1];
461                    } else if ($cur_biop != 'OR') {
462                        $aSearch[] = 'ALL '.$aCriteria[$i][1];
463                    } else { // OR only supports 2 search keys so we need to create a parenthesized list
464                        $prev_biop = (isset($aCriteria[$i-1][0])) ? $aCriteria[$i-1][0] : false;
465                        if ($prev_biop == $cur_biop) {
466                            $last = $aSearch[$i-1];
467                            if (!substr($last,-1) == ')') {
468                                $aSearch[$i-1] = "(OR $last";
469                                $aSearch[] = $aCriteria[$i][1].')';
470                            } else {
471                                $sEnd = '';
472                                while ($last && substr($last,-1) == ')') {
473                                    $last = substr($last,0,-1);
474                                    $sEnd .= ')';
475                                }
476                                $aSearch[$i-1] = "(OR $last";
477                                $aSearch[] = $aCriteria[$i][1].$sEnd.')';
478                            }
479                        } else {
480                            $aSearch[] = $aCriteria[$i][1];
481                        }
482                    }
483                }
484                $search_string .= implode(' ',$aSearch);
485            }
486        }
487    }
488    return ($mbox_search);
489}