PageRenderTime 37ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/trunk/squirrelmail/functions/imap_asearch.php

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