Happy New Year
[squirrelmail.git] / functions / imap_asearch.php
CommitLineData
cd33ec11 1<?php
2
3/**
0e1a248b 4 * imap_search.php
5 *
0e1a248b 6 * IMAP asearch routines
7 *
8 * Subfolder search idea from Patch #806075 by Thomas Pohl xraven at users.sourceforge.net. Thanks Thomas!
9 *
4b4abf93 10 * @author Alex Lemaresquier - Brainstorm <alex at brainstorm.fr>
c4faef33 11 * @copyright 1999-2020 The SquirrelMail Project Team
4b4abf93 12 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
0e1a248b 13 * @version $Id$
14 * @package squirrelmail
15 * @subpackage imap
16 * @see search.php
17 * @link http://www.ietf.org/rfc/rfc3501.txt
0e1a248b 18 */
cd33ec11 19
0e218c3b 20/** This functionality requires the IMAP and date functions
0e1a248b 21 */
202bcbcc 22//require_once(SM_PATH . 'functions/imap_general.php');
23//require_once(SM_PATH . 'functions/date.php');
cd33ec11 24
0e1a248b 25/** Set to TRUE to dump the IMAP dialogue
26 * @global bool $imap_asearch_debug_dump
27 */
cd33ec11 28$imap_asearch_debug_dump = FALSE;
29
0e1a248b 30/** IMAP SEARCH keys
31 * @global array $imap_asearch_opcodes
32 */
17a7913a 33global $imap_asearch_opcodes;
cd33ec11 34$imap_asearch_opcodes = array(
91e0dccc 35/* <sequence-set> => 'asequence', */ // Special handling, @see sqimap_asearch_build_criteria()
cd33ec11 36/*'ALL' is binary operator */
f7027a32 37 'ANSWERED' => '',
38 'BCC' => 'astring',
39 'BEFORE' => 'adate',
40 'BODY' => 'astring',
41 'CC' => 'astring',
42 'DELETED' => '',
43 'DRAFT' => '',
44 'FLAGGED' => '',
45 'FROM' => 'astring',
91e0dccc 46 'HEADER' => 'afield', // Special syntax for this one, @see sqimap_asearch_build_criteria()
f7027a32 47 'KEYWORD' => 'akeyword',
48 'LARGER' => 'anum',
49 'NEW' => '',
cd33ec11 50/*'NOT' is unary operator */
f7027a32 51 'OLD' => '',
52 'ON' => 'adate',
cd33ec11 53/*'OR' is binary operator */
f7027a32 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' => ''
cd33ec11 71);
72
0e1a248b 73/** IMAP SEARCH month names encoding
74 * @global array $imap_asearch_months
75 */
cd33ec11 76$imap_asearch_months = array(
f7027a32 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'
cd33ec11 89);
90
00b05f03 91/**
0e1a248b 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 */
00b05f03 101//@global array color sm colors array
40fbe929 102function sqimap_asearch_error_box($response, $query, $message, $link = '')
ff6f916c 103{
0daff8c9 104 global $color;
0e1a248b 105 // Error message titles according to IMAP server returned code
0daff8c9 106 $imap_error_titles = array(
107 'OK' => '',
0e1a248b 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.")
0daff8c9 112 );
113
f7027a32 114
115 if (!array_key_exists($response, $imap_error_titles))
0e1a248b 116 $title = _("ERROR: Unknown IMAP response.");
f7027a32 117 else
118 $title = $imap_error_titles[$response];
119 if ($link == '')
0e1a248b 120 $message_title = _("Reason Given:");
f7027a32 121 else
0e1a248b 122 $message_title = _("Possible reason:");
123 $message_title .= ' ';
582d0621 124 sqimap_error_box($title, $query, $message_title, $message, $link);
ff6f916c 125}
126
48af4b64 127/**
0e1a248b 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 */
2c300e0b 133function asearch_nz(&$var, $def = '')
cd33ec11 134{
f7027a32 135 if (isset($var))
136 return $var;
137 return $def;
cd33ec11 138}
139
48af4b64 140/**
0e1a248b 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 */
cd33ec11 146function asearch_unhtmlentities($string) {
f7027a32 147 $trans_tbl = array_flip(get_html_translation_table(HTML_ENTITIES));
91e0dccc 148 for ($i=127; $i<255; $i++) /* Add &#<dec>; entities */
f7027a32 149 $trans_tbl['&#' . $i . ';'] = chr($i);
150 return strtr($string, $trans_tbl);
cd33ec11 151/* I think the one above is quicker, though it should be benchmarked
f7027a32 152 $string = strtr($string, array_flip(get_html_translation_table(HTML_ENTITIES)));
153 return preg_replace("/&#([0-9]+);/E", "chr('\\1')", $string);
0e1a248b 154 */
cd33ec11 155}
156
00b05f03 157/** Encode a string to quoted or literal as defined in rfc 3501
0e1a248b 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 */
f945228f 168function sqimap_asearch_encode_string($what, $charset)
cd33ec11 169{
91e0dccc 170 if (strtoupper($charset) == 'ISO-2022-JP') // This should be now handled in imap_utf7_local?
f7027a32 171 $what = mb_convert_encoding($what, 'JIS', 'auto');
172 if (preg_match('/["\\\\\r\n\x80-\xff]/', $what))
91e0dccc 173 return '{' . strlen($what) . "}\r\n" . $what; // 4.3 literal form
174 return '"' . $what . '"'; // 4.3 quoted string form
cd33ec11 175}
176
48af4b64 177/**
0e1a248b 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 */
cd33ec11 188function sqimap_asearch_parse_date($what)
189{
f7027a32 190 global $imap_asearch_months;
191
192 $what = trim($what);
b7910e12 193 $what = preg_replace('/[ \/\\.,]+/', '-', $what);
f7027a32 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]));
91e0dccc 198/* if (!in_array($what_month, $imap_asearch_months)) {*/
f7027a32 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 }
91e0dccc 210/* }*/
f7027a32 211 }
212 }
213 else
214 $what_parts = array();
215 return $what_parts;
cd33ec11 216}
217
00b05f03 218/**
0e1a248b 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 */
f945228f 226function sqimap_asearch_build_criteria($opcode, $what, $charset)
cd33ec11 227{
f7027a32 228 global $imap_asearch_opcodes;
229
230 $criteria = '';
231 switch ($imap_asearch_opcodes[$opcode]) {
232 default:
233 case 'anum':
234 $what = str_replace(' ', '', $what);
b7910e12 235 $what = preg_replace('/[^0-9]+[^KMG]$/', '', strtoupper($what));
f7027a32 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;
91e0dccc 251 case '': //aflag
f7027a32 252 $criteria = $opcode . ' ';
253 break;
91e0dccc 254 case 'afield': /* HEADER field-name: field-body */
f7027a32 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':
b7910e12 271 $what = preg_replace('/[^0-9:()]+/', '', $what);
f7027a32 272 if ($what != '')
273 $criteria = $opcode . ' ' . $what . ' ';
274 break;
275 }
276 return $criteria;
cd33ec11 277}
278
00b05f03 279/**
0e1a248b 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 */
d2f031ed 285function sqimap_array_merge_unique(&$to, $from)
75d24fd2 286{
f7027a32 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;
75d24fd2 295}
296
00b05f03 297/**
0e1a248b 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 */
cd33ec11 306function sqimap_run_search($imapConnection, $search_string, $search_charset)
307{
f7027a32 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;
f171f05a 316 $readin = sqimap_run_command_list($imapConnection, $query, false, $response, $message, TRUE);
f7027a32 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;
f171f05a 321 $readin = sqimap_run_command_list($imapConnection, $query, false, $response, $message, TRUE);
f7027a32 322 }
323 if (strtoupper($response) != 'OK') {
324 sqimap_asearch_error_box($response, $query, $message);
325 return array();
326 }
324ac3c5 327 $messagelist = parseUidList($readin,'SEARCH');
cd33ec11 328
91e0dccc 329 if (empty($messagelist)) //Empty search response, ie '* SEARCH'
f7027a32 330 return array();
3f075f6c 331
f7027a32 332 $cnt = count($messagelist);
333 for ($q = 0; $q < $cnt; $q++)
334 $id[$q] = trim($messagelist[$q]);
335 return $id;
cd33ec11 336}
337
00b05f03 338/**
0e1a248b 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 */
f945228f 344function sqimap_asearch_get_charset()
345{
f7027a32 346 global $allow_charset_search, $languages, $squirrelmail_language;
f945228f 347
f7027a32 348 if ($allow_charset_search)
349 return $languages[$squirrelmail_language]['CHARSET'];
350 return '';
f945228f 351}
352
00b05f03 353/**
0e1a248b 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 */
c2d47d51 364function sqimap_asearch_get_sort_criteria($mailbox, $sort_by)
365{
f7027a32 366 global $internal_date_sort, $sent_folder;
c2d47d51 367
f7027a32 368 $sort_opcodes = array ('DATE', 'FROM', 'SUBJECT', 'SIZE');
369 if ($internal_date_sort == true)
370 $sort_opcodes[0] = 'ARRIVAL';
be519908 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
e50f5ac2 372// if (handleAsSent($mailbox))
373// if (isSentFolder($mailbox))
f7027a32 374 if ($mailbox == $sent_folder)
375 $sort_opcodes[1] = 'TO';
376 return (($sort_by % 2) ? '' : 'REVERSE ') . $sort_opcodes[($sort_by >> 1) & 3];
c2d47d51 377}
378
00b05f03 379/**
0e1a248b 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 */
0e218c3b 384function sqimap_asearch_get_sub_mailboxes($cur_mailbox, &$mboxes_array)
40fbe929 385{
f7027a32 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;
40fbe929 393}
394
395/**
0e1a248b 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 */
0e218c3b 408function sqimap_asearch($imapConnection, &$mailbox_array, &$biop_array, &$unop_array, &$where_array, &$what_array, &$exclude_array, &$sub_array, &$mboxes_array)
cd33ec11 409{
c2d47d51 410
f7027a32 411 $search_charset = sqimap_asearch_get_charset();
324ac3c5 412 $mbox_search = array();
f7027a32 413 $search_string = '';
414 $cur_mailbox = $mailbox_array[0];
91e0dccc 415 $cur_biop = ''; /* Start with ALL */
f7027a32 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) {
91e0dccc 421 $search_string = trim($search_string); /* Trim out last space */
f7027a32 422 if ($cur_mailbox == 'All Folders')
696155b5 423 $search_mboxes = $mboxes_array;
f7027a32 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) {
324ac3c5 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 }
f7027a32 436 $cur_mailbox = $next_mailbox;
437 $search_string = '';
f7027a32 438 }
439 if (isset($where_array[$cur_crit]) && empty($exclude_array[$cur_crit])) {
fdbadf16 440 $aCriteria = array();
f7027a32 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 }
324ac3c5 451 }
f7027a32 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];
696155b5 463 } else { // OR only supports 2 search keys so we need to create a parenthesized list
f7027a32 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) == ')') {
696155b5 473 $last = substr($last,0,-1);
474 $sEnd .= ')';
324ac3c5 475 }
f7027a32 476 $aSearch[$i-1] = "(OR $last";
477 $aSearch[] = $aCriteria[$i][1].$sEnd.')';
478 }
479 } else {
480 $aSearch[] = $aCriteria[$i][1];
481 }
482 }
f7027a32 483 }
484 $search_string .= implode(' ',$aSearch);
485 }
f7027a32 486 }
487 }
324ac3c5 488 return ($mbox_search);
cd33ec11 489}