Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]

   1  <?php
   2  /**
   3   * Copyright 2008-2017 Horde LLC (http://www.horde.org/)
   4   *
   5   * See the enclosed file LICENSE for license information (LGPL). If you
   6   * did not receive this file, see http://www.horde.org/licenses/lgpl21.
   7   *
   8   * @category  Horde
   9   * @copyright 2008-2017 Horde LLC
  10   * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
  11   * @package   Imap_Client
  12   */
  13  
  14  /**
  15   * Abstraction of the IMAP4rev1 search criteria (see RFC 3501 [6.4.4]).
  16   * Allows translation between abstracted search criteria and a generated IMAP
  17   * search criteria string suitable for sending to a remote IMAP server.
  18   *
  19   * @author    Michael Slusarz <slusarz@horde.org>
  20   * @category  Horde
  21   * @copyright 2008-2017 Horde LLC
  22   * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
  23   * @package   Imap_Client
  24   */
  25  class Horde_Imap_Client_Search_Query implements Serializable
  26  {
  27      /**
  28       * Serialized version.
  29       */
  30      const VERSION = 3;
  31  
  32      /**
  33       * Constants for dateSearch()
  34       */
  35      const DATE_BEFORE = 'BEFORE';
  36      const DATE_ON = 'ON';
  37      const DATE_SINCE = 'SINCE';
  38  
  39      /**
  40       * Constants for intervalSearch()
  41       */
  42      const INTERVAL_OLDER = 'OLDER';
  43      const INTERVAL_YOUNGER = 'YOUNGER';
  44  
  45      /**
  46       * The charset of the search strings.  All text strings must be in
  47       * this charset. By default, this is 'US-ASCII' (see RFC 3501 [6.4.4]).
  48       *
  49       * @var string
  50       */
  51      protected $_charset = null;
  52  
  53      /**
  54       * The list of search params.
  55       *
  56       * @var array
  57       */
  58      protected $_search = array();
  59  
  60      /**
  61       * String representation: The IMAP search string.
  62       */
  63      public function __toString()
  64      {
  65          try {
  66              $res = $this->build(null);
  67              return $res['query']->escape();
  68          } catch (Exception $e) {
  69              return '';
  70          }
  71      }
  72  
  73      /**
  74       * Sets the charset of the search text.
  75       *
  76       * @param string $charset   The charset to use for the search.
  77       * @param boolean $convert  Convert existing text values?
  78       *
  79       * @throws Horde_Imap_Client_Exception_SearchCharset
  80       */
  81      public function charset($charset, $convert = true)
  82      {
  83          $oldcharset = $this->_charset;
  84          $this->_charset = Horde_String::upper($charset);
  85  
  86          if (!$convert || ($oldcharset == $this->_charset)) {
  87              return;
  88          }
  89  
  90          foreach (array('and', 'or') as $item) {
  91              if (isset($this->_search[$item])) {
  92                  foreach ($this->_search[$item] as &$val) {
  93                      $val->charset($charset, $convert);
  94                  }
  95              }
  96          }
  97  
  98          // Unset the reference to avoid corrupting $this->_search below.
  99          unset($val);
 100  
 101          foreach (array('header', 'text') as $item) {
 102              if (isset($this->_search[$item])) {
 103                  foreach ($this->_search[$item] as $key => $val) {
 104                      $new_val = Horde_String::convertCharset($val['text'], $oldcharset, $this->_charset);
 105                      if (Horde_String::convertCharset($new_val, $this->_charset, $oldcharset) != $val['text']) {
 106                          throw new Horde_Imap_Client_Exception_SearchCharset($this->_charset);
 107                      }
 108                      $this->_search[$item][$key]['text'] = $new_val;
 109                  }
 110              }
 111          }
 112      }
 113  
 114      /**
 115       * Builds an IMAP4rev1 compliant search string.
 116       *
 117       * @todo  Change default of $exts to null.
 118       *
 119       * @param Horde_Imap_Client_Base $exts  The server object this query will
 120       *                                      be run on (@since 2.24.0), a
 121       *                                      Horde_Imap_Client_Data_Capability
 122       *                                      object (@since 2.24.0), or the
 123       *                                      list of extensions present
 124       *                                      on the server (@deprecated).
 125       *                                      If null, all extensions are
 126       *                                      assumed to be available.
 127       *
 128       * @return array  An array with these elements:
 129       *   - charset: (string) The charset of the search string. If null, no
 130       *              text strings appear in query.
 131       *   - exts: (array) The list of IMAP extensions used to create the
 132       *           string.
 133       *   - query: (Horde_Imap_Client_Data_Format_List) The IMAP search
 134       *            command.
 135       *
 136       * @throws Horde_Imap_Client_Data_Format_Exception
 137       * @throws Horde_Imap_Client_Exception_NoSupportExtension
 138       */
 139      public function build($exts = array())
 140      {
 141          /* @todo: BC */
 142          if (is_array($exts)) {
 143              $tmp = new Horde_Imap_Client_Data_Capability_Imap();
 144              foreach ($exts as $key => $val) {
 145                  $tmp->add($key, is_array($val) ? $val : null);
 146              }
 147              $exts = $tmp;
 148          } elseif (!is_null($exts)) {
 149              if ($exts instanceof Horde_Imap_Client_Base) {
 150                  $exts = $exts->capability;
 151              } elseif (!($exts instanceof Horde_Imap_Client_Data_Capability)) {
 152                  throw new InvalidArgumentException('Incorrect $exts parameter');
 153              }
 154          }
 155  
 156          $temp = array(
 157              'cmds' => new Horde_Imap_Client_Data_Format_List(),
 158              'exts' => $exts,
 159              'exts_used' => array()
 160          );
 161          $cmds = &$temp['cmds'];
 162          $charset = $charset_cname = null;
 163          $default_search = true;
 164          $exts_used = &$temp['exts_used'];
 165          $ptr = &$this->_search;
 166  
 167          $charset_get = function ($c) use (&$charset, &$charset_cname) {
 168              $charset = is_null($c)
 169                  ? 'US-ASCII'
 170                  : strval($c);
 171              $charset_cname = ($charset === 'US-ASCII')
 172                  ? 'Horde_Imap_Client_Data_Format_Astring'
 173                  : 'Horde_Imap_Client_Data_Format_Astring_Nonascii';
 174          };
 175          $create_return = function ($charset, $exts_used, $cmds) {
 176              return array(
 177                  'charset' => $charset,
 178                  'exts' => array_keys(array_flip($exts_used)),
 179                  'query' => $cmds
 180              );
 181          };
 182  
 183          /* Do IDs check first. If there is an empty ID query (without a NOT
 184           * qualifier), the rest of this query is irrelevant since we already
 185           * know the search will return no results. */
 186          if (isset($ptr['ids'])) {
 187              if (!count($ptr['ids']['ids']) && !$ptr['ids']['ids']->special) {
 188                  if (empty($ptr['ids']['not'])) {
 189                      /* This is a match on an empty list of IDs. We do need to
 190                       * process any OR queries that may exist, since they are
 191                       * independent of this result. */
 192                      if (isset($ptr['or'])) {
 193                          $this->_buildAndOr(
 194                              'OR', $ptr['or'], $charset, $exts_used, $cmds
 195                          );
 196                      }
 197                      return $create_return($charset, $exts_used, $cmds);
 198                  }
 199  
 200                  /* If reached here, this a NOT search of an empty list. We can
 201                   * safely discard this from the output. */
 202              } else {
 203                  $this->_addFuzzy(!empty($ptr['ids']['fuzzy']), $temp);
 204                  if (!empty($ptr['ids']['not'])) {
 205                      $cmds->add('NOT');
 206                  }
 207                  if (!$ptr['ids']['ids']->sequence) {
 208                      $cmds->add('UID');
 209                  }
 210                  $cmds->add(strval($ptr['ids']['ids']));
 211              }
 212          }
 213  
 214          if (isset($ptr['new'])) {
 215              $this->_addFuzzy(!empty($ptr['newfuzzy']), $temp);
 216              if ($ptr['new']) {
 217                  $cmds->add('NEW');
 218                  unset($ptr['flag']['UNSEEN']);
 219              } else {
 220                  $cmds->add('OLD');
 221              }
 222              unset($ptr['flag']['RECENT']);
 223          }
 224  
 225          if (!empty($ptr['flag'])) {
 226              foreach ($ptr['flag'] as $key => $val) {
 227                  $this->_addFuzzy(!empty($val['fuzzy']), $temp);
 228  
 229                  $tmp = '';
 230                  if (empty($val['set'])) {
 231                      // This is a 'NOT' search.  All system flags but \Recent
 232                      // have 'UN' equivalents.
 233                      if ($key == 'RECENT') {
 234                          $cmds->add('NOT');
 235                      } else {
 236                          $tmp = 'UN';
 237                      }
 238                  }
 239  
 240                  if ($val['type'] == 'keyword') {
 241                      $cmds->add(array(
 242                          $tmp . 'KEYWORD',
 243                          $key
 244                      ));
 245                  } else {
 246                      $cmds->add($tmp . $key);
 247                  }
 248              }
 249          }
 250  
 251          if (!empty($ptr['header'])) {
 252              /* The list of 'system' headers that have a specific search
 253               * query. */
 254              $systemheaders = array(
 255                  'BCC', 'CC', 'FROM', 'SUBJECT', 'TO'
 256              );
 257  
 258              foreach ($ptr['header'] as $val) {
 259                  $this->_addFuzzy(!empty($val['fuzzy']), $temp);
 260  
 261                  if (!empty($val['not'])) {
 262                      $cmds->add('NOT');
 263                  }
 264  
 265                  if (in_array($val['header'], $systemheaders)) {
 266                      $cmds->add($val['header']);
 267                  } else {
 268                      $cmds->add(array(
 269                          'HEADER',
 270                          new Horde_Imap_Client_Data_Format_Astring($val['header'])
 271                      ));
 272                  }
 273  
 274                  $charset_get($this->_charset);
 275                  $cmds->add(
 276                      new $charset_cname(isset($val['text']) ? $val['text'] : '')
 277                  );
 278              }
 279          }
 280  
 281          if (!empty($ptr['text'])) {
 282              foreach ($ptr['text'] as $val) {
 283                  $this->_addFuzzy(!empty($val['fuzzy']), $temp);
 284  
 285                  if (!empty($val['not'])) {
 286                      $cmds->add('NOT');
 287                  }
 288  
 289                  $charset_get($this->_charset);
 290                  $cmds->add(array(
 291                      $val['type'],
 292                      new $charset_cname($val['text'])
 293                  ));
 294              }
 295          }
 296  
 297          if (!empty($ptr['size'])) {
 298              foreach ($ptr['size'] as $key => $val) {
 299                  $this->_addFuzzy(!empty($val['fuzzy']), $temp);
 300                  if (!empty($val['not'])) {
 301                      $cmds->add('NOT');
 302                  }
 303                  $cmds->add(array(
 304                      $key,
 305                      new Horde_Imap_Client_Data_Format_Number(
 306                          empty($val['size']) ? 0 : $val['size']
 307                      )
 308                  ));
 309              }
 310          }
 311  
 312          if (!empty($ptr['date'])) {
 313              foreach ($ptr['date'] as $val) {
 314                  $this->_addFuzzy(!empty($val['fuzzy']), $temp);
 315  
 316                  if (!empty($val['not'])) {
 317                      $cmds->add('NOT');
 318                  }
 319  
 320                  if (empty($val['header'])) {
 321                      $cmds->add($val['range']);
 322                  } else {
 323                      $cmds->add('SENT' . $val['range']);
 324                  }
 325                  $cmds->add($val['date']);
 326              }
 327          }
 328  
 329          if (!empty($ptr['within'])) {
 330              if (is_null($exts) || $exts->query('WITHIN')) {
 331                  $exts_used[] = 'WITHIN';
 332              }
 333  
 334              foreach ($ptr['within'] as $key => $val) {
 335                  $this->_addFuzzy(!empty($val['fuzzy']), $temp);
 336                  if (!empty($val['not'])) {
 337                      $cmds->add('NOT');
 338                  }
 339  
 340                  if (is_null($exts) || $exts->query('WITHIN')) {
 341                      $cmds->add(array(
 342                          $key,
 343                          new Horde_Imap_Client_Data_Format_Number($val['interval'])
 344                      ));
 345                  } else {
 346                      // This workaround is only accurate to within 1 day, due
 347                      // to limitations with the IMAP4rev1 search commands.
 348                      $cmds->add(array(
 349                          ($key == self::INTERVAL_OLDER) ? self::DATE_BEFORE : self::DATE_SINCE,
 350                          new Horde_Imap_Client_Data_Format_Date('now -' . $val['interval'] . ' seconds')
 351                      ));
 352                  }
 353              }
 354          }
 355  
 356          if (!empty($ptr['modseq'])) {
 357              if (!is_null($exts) && !$exts->query('CONDSTORE')) {
 358                  throw new Horde_Imap_Client_Exception_NoSupportExtension('CONDSTORE');
 359              }
 360  
 361              $exts_used[] = 'CONDSTORE';
 362  
 363              $this->_addFuzzy(!empty($ptr['modseq']['fuzzy']), $temp);
 364  
 365              if (!empty($ptr['modseq']['not'])) {
 366                  $cmds->add('NOT');
 367              }
 368              $cmds->add('MODSEQ');
 369              if (isset($ptr['modseq']['name'])) {
 370                  $cmds->add(array(
 371                      new Horde_Imap_Client_Data_Format_String($ptr['modseq']['name']),
 372                      $ptr['modseq']['type']
 373                  ));
 374              }
 375              $cmds->add(new Horde_Imap_Client_Data_Format_Number($ptr['modseq']['value']));
 376          }
 377  
 378          if (isset($ptr['prevsearch'])) {
 379              if (!is_null($exts) && !$exts->query('SEARCHRES')) {
 380                  throw new Horde_Imap_Client_Exception_NoSupportExtension('SEARCHRES');
 381              }
 382  
 383              $exts_used[] = 'SEARCHRES';
 384  
 385              $this->_addFuzzy(!empty($ptr['prevsearchfuzzy']), $temp);
 386  
 387              if (!$ptr['prevsearch']) {
 388                  $cmds->add('NOT');
 389              }
 390              $cmds->add('$');
 391          }
 392  
 393          // Add AND'ed queries
 394          if (!empty($ptr['and'])) {
 395              $default_search = $this->_buildAndOr(
 396                  'AND', $ptr['and'], $charset, $exts_used, $cmds
 397              );
 398          }
 399  
 400          // Add OR'ed queries
 401          if (!empty($ptr['or'])) {
 402              $default_search = $this->_buildAndOr(
 403                  'OR', $ptr['or'], $charset, $exts_used, $cmds
 404              );
 405          }
 406  
 407          // Default search is 'ALL'
 408          if ($default_search && !count($cmds)) {
 409              $cmds->add('ALL');
 410          }
 411  
 412          return $create_return($charset, $exts_used, $cmds);
 413      }
 414  
 415      /**
 416       * Builds the AND/OR query.
 417       *
 418       * @param string $type                               'AND' or 'OR'.
 419       * @param array $data                                Query data.
 420       * @param string &$charset                           Search charset.
 421       * @param array &$exts_used                          IMAP extensions used.
 422       * @param Horde_Imap_Client_Data_Format_List &$cmds  Command list.
 423       *
 424       * @return boolean  True if query might return results.
 425       */
 426      protected function _buildAndOr($type, $data, &$charset, &$exts_used,
 427                                     &$cmds)
 428      {
 429          $results = false;
 430  
 431          foreach ($data as $val) {
 432              $ret = $val->build();
 433  
 434              /* Empty sub-query. */
 435              if (!count($ret['query'])) {
 436                  switch ($type) {
 437                  case 'AND':
 438                      /* Any empty sub-query means that the query MUST return
 439                       * no results. */
 440                      $cmds = new Horde_Imap_Client_Data_Format_List();
 441                      $exts_used = array();
 442                      return false;
 443  
 444                  case 'OR':
 445                      /* Skip this query. */
 446                      continue 2;
 447                  }
 448              }
 449  
 450              $results = true;
 451  
 452              if (!is_null($ret['charset']) && ($ret['charset'] != 'US-ASCII')) {
 453                  if (!is_null($charset) &&
 454                      ($charset != 'US-ASCII') &&
 455                      ($charset != $ret['charset'])) {
 456                      throw new InvalidArgumentException(
 457                          'AND/OR queries must all have the same charset.'
 458                      );
 459                  }
 460                  $charset = $ret['charset'];
 461              }
 462  
 463              $exts_used = array_merge($exts_used, $ret['exts']);
 464  
 465              switch ($type) {
 466              case 'AND':
 467                  $cmds->add($ret['query'], true);
 468                  break;
 469  
 470              case 'OR':
 471                  // First OR'd query
 472                  if (count($cmds)) {
 473                      $new_cmds = new Horde_Imap_Client_Data_Format_List();
 474                      $new_cmds->add(array(
 475                          'OR',
 476                          $ret['query'],
 477                          $cmds
 478                      ));
 479                      $cmds = $new_cmds;
 480                  } else {
 481                      $cmds = $ret['query'];
 482                  }
 483                  break;
 484              }
 485          }
 486  
 487          return $results;
 488      }
 489  
 490      /**
 491       * Adds fuzzy modifier to search keys.
 492       *
 493       * @param boolean $add  Add the fuzzy modifier?
 494       * @param array $temp   Temporary build data.
 495       *
 496       * @throws Horde_Imap_Client_Exception_NoSupport_Extension
 497       */
 498      protected function _addFuzzy($add, &$temp)
 499      {
 500          if ($add) {
 501              if (!$temp['exts']->query('SEARCH', 'FUZZY')) {
 502                  throw new Horde_Imap_Client_Exception_NoSupportExtension('SEARCH=FUZZY');
 503              }
 504              $temp['cmds']->add('FUZZY');
 505              $temp['exts_used'][] = 'SEARCH=FUZZY';
 506          }
 507      }
 508  
 509      /**
 510       * Search for a flag/keywords.
 511       *
 512       * @param string $name  The flag or keyword name.
 513       * @param boolean $set  If true, search for messages that have the flag
 514       *                      set.  If false, search for messages that do not
 515       *                      have the flag set.
 516       * @param array $opts   Additional options:
 517       *   - fuzzy: (boolean) If true, perform a fuzzy search. The IMAP server
 518       *            MUST support RFC 6203.
 519       */
 520      public function flag($name, $set = true, array $opts = array())
 521      {
 522          $name = Horde_String::upper(ltrim($name, '\\'));
 523          if (!isset($this->_search['flag'])) {
 524              $this->_search['flag'] = array();
 525          }
 526  
 527          /* The list of defined system flags (see RFC 3501 [2.3.2]). */
 528          $systemflags = array(
 529              'ANSWERED', 'DELETED', 'DRAFT', 'FLAGGED', 'RECENT', 'SEEN'
 530          );
 531  
 532          $this->_search['flag'][$name] = array_filter(array(
 533              'fuzzy' => !empty($opts['fuzzy']),
 534              'set' => $set,
 535              'type' => in_array($name, $systemflags) ? 'flag' : 'keyword'
 536          ));
 537      }
 538  
 539      /**
 540       * Determines if flags are a part of the search.
 541       *
 542       * @return boolean  True if search query involves flags.
 543       */
 544      public function flagSearch()
 545      {
 546          return !empty($this->_search['flag']);
 547      }
 548  
 549      /**
 550       * Search for either new messages (messages that have the '\Recent' flag
 551       * but not the '\Seen' flag) or old messages (messages that do not have
 552       * the '\Recent' flag).  If new messages are searched, this will clear
 553       * any '\Recent' or '\Unseen' flag searches.  If old messages are searched,
 554       * this will clear any '\Recent' flag search.
 555       *
 556       * @param boolean $newmsgs  If true, searches for new messages.  Else,
 557       *                          search for old messages.
 558       * @param array $opts       Additional options:
 559       *   - fuzzy: (boolean) If true, perform a fuzzy search. The IMAP server
 560       *            MUST support RFC 6203.
 561       */
 562      public function newMsgs($newmsgs = true, array $opts = array())
 563      {
 564          $this->_search['new'] = $newmsgs;
 565          if (!empty($opts['fuzzy'])) {
 566              $this->_search['newfuzzy'] = true;
 567          }
 568      }
 569  
 570      /**
 571       * Search for text in the header of a message.
 572       *
 573       * @param string $header  The header field.
 574       * @param string $text    The search text.
 575       * @param boolean $not    If true, do a 'NOT' search of $text.
 576       * @param array $opts     Additional options:
 577       *   - fuzzy: (boolean) If true, perform a fuzzy search. The IMAP server
 578       *            MUST support RFC 6203.
 579       */
 580      public function headerText($header, $text, $not = false,
 581                                  array $opts = array())
 582      {
 583          if (!isset($this->_search['header'])) {
 584              $this->_search['header'] = array();
 585          }
 586          $this->_search['header'][] = array_filter(array(
 587              'fuzzy' => !empty($opts['fuzzy']),
 588              'header' => Horde_String::upper($header),
 589              'text' => $text,
 590              'not' => $not
 591          ));
 592      }
 593  
 594      /**
 595       * Search for text in either the entire message, or just the body.
 596       *
 597       * @param string $text      The search text.
 598       * @param string $bodyonly  If true, only search in the body of the
 599       *                          message. If false, also search in the headers.
 600       * @param boolean $not      If true, do a 'NOT' search of $text.
 601       * @param array $opts       Additional options:
 602       *   - fuzzy: (boolean) If true, perform a fuzzy search. The IMAP server
 603       *            MUST support RFC 6203.
 604       */
 605      public function text($text, $bodyonly = true, $not = false,
 606                           array $opts = array())
 607      {
 608          if (!isset($this->_search['text'])) {
 609              $this->_search['text'] = array();
 610          }
 611  
 612          $this->_search['text'][] = array_filter(array(
 613              'fuzzy' => !empty($opts['fuzzy']),
 614              'not' => $not,
 615              'text' => $text,
 616              'type' => $bodyonly ? 'BODY' : 'TEXT'
 617          ));
 618      }
 619  
 620      /**
 621       * Search for messages smaller/larger than a certain size.
 622       *
 623       * @todo: Remove $not for 3.0
 624       *
 625       * @param integer $size    The size (in bytes).
 626       * @param boolean $larger  Search for messages larger than $size?
 627       * @param boolean $not     If true, do a 'NOT' search of $text.
 628       * @param array $opts      Additional options:
 629       *   - fuzzy: (boolean) If true, perform a fuzzy search. The IMAP server
 630       *            MUST support RFC 6203.
 631       */
 632      public function size($size, $larger = false, $not = false,
 633                           array $opts = array())
 634      {
 635          if (!isset($this->_search['size'])) {
 636              $this->_search['size'] = array();
 637          }
 638          $this->_search['size'][$larger ? 'LARGER' : 'SMALLER'] = array_filter(array(
 639              'fuzzy' => !empty($opts['fuzzy']),
 640              'not' => $not,
 641              'size' => (float)$size
 642          ));
 643      }
 644  
 645      /**
 646       * Search for messages within a given UID range. Only one message range
 647       * can be specified per query.
 648       *
 649       * @param Horde_Imap_Client_Ids $ids  The list of UIDs to search.
 650       * @param boolean $not                If true, do a 'NOT' search of the
 651       *                                    UIDs.
 652       * @param array $opts                 Additional options:
 653       *   - fuzzy: (boolean) If true, perform a fuzzy search. The IMAP server
 654       *            MUST support RFC 6203.
 655       */
 656      public function ids(Horde_Imap_Client_Ids $ids, $not = false,
 657                          array $opts = array())
 658      {
 659          $this->_search['ids'] = array_filter(array(
 660              'fuzzy' => !empty($opts['fuzzy']),
 661              'ids' => $ids,
 662              'not' => $not
 663          ));
 664      }
 665  
 666      /**
 667       * Search for messages within a date range.
 668       *
 669       * @param mixed $date    DateTime or Horde_Date object.
 670       * @param string $range  Either:
 671       *   - Horde_Imap_Client_Search_Query::DATE_BEFORE
 672       *   - Horde_Imap_Client_Search_Query::DATE_ON
 673       *   - Horde_Imap_Client_Search_Query::DATE_SINCE
 674       * @param boolean $header  If true, search using the date in the message
 675       *                         headers. If false, search using the internal
 676       *                         IMAP date (usually arrival time).
 677       * @param boolean $not     If true, do a 'NOT' search of the range.
 678       * @param array $opts      Additional options:
 679       *   - fuzzy: (boolean) If true, perform a fuzzy search. The IMAP server
 680       *            MUST support RFC 6203.
 681       */
 682      public function dateSearch($date, $range, $header = true, $not = false,
 683                                 array $opts = array())
 684      {
 685          if (!isset($this->_search['date'])) {
 686              $this->_search['date'] = array();
 687          }
 688  
 689          // We should really be storing the raw DateTime object as data,
 690          // but all versions of the query object have converted at this stage.
 691          $ob = new Horde_Imap_Client_Data_Format_Date($date);
 692  
 693          $this->_search['date'][] = array_filter(array(
 694              'date' => $ob->escape(),
 695              'fuzzy' => !empty($opts['fuzzy']),
 696              'header' => $header,
 697              'range' => $range,
 698              'not' => $not
 699          ));
 700      }
 701  
 702      /**
 703       * Search for messages within a date and time range.
 704       *
 705       * @param mixed $date    DateTime or Horde_Date object.
 706       * @param string $range  Either:
 707       *   - Horde_Imap_Client_Search_Query::DATE_BEFORE
 708       *   - Horde_Imap_Client_Search_Query::DATE_ON
 709       *   - Horde_Imap_Client_Search_Query::DATE_SINCE
 710       * @param boolean $header  If true, search using the date in the message
 711       *                         headers. If false, search using the internal
 712       *                         IMAP date (usually arrival time).
 713       * @param boolean $not     If true, do a 'NOT' search of the range.
 714       * @param array $opts      Additional options:
 715       *   - fuzzy: (boolean) If true, perform a fuzzy search. The IMAP server
 716       *            MUST support RFC 6203.
 717       */
 718      public function dateTimeSearch($date, $range, $header = true, $not = false,
 719                                     array $opts = array())
 720      {
 721          if (!isset($this->_search['date'])) {
 722              $this->_search['date'] = array();
 723          }
 724  
 725          // We should really be storing the raw DateTime object as data,
 726          // but all versions of the query object have converted at this stage.
 727          $ob = new Horde_Imap_Client_Data_Format_DateTime($date);
 728  
 729          $this->_search['date'][] = array_filter(array(
 730              'date' => $ob->escape(),
 731              'fuzzy' => !empty($opts['fuzzy']),
 732              'header' => $header,
 733              'range' => $range,
 734              'not' => $not
 735          ));
 736      }
 737  
 738      /**
 739       * Search for messages within a given interval. Only one interval of each
 740       * type can be specified per search query. If the IMAP server supports
 741       * the WITHIN extension (RFC 5032), it will be used.  Otherwise, the
 742       * search query will be dynamically created using IMAP4rev1 search
 743       * terms.
 744       *
 745       * @param integer $interval  Seconds from the present.
 746       * @param string $range      Either:
 747       *   - Horde_Imap_Client_Search_Query::INTERVAL_OLDER
 748       *   - Horde_Imap_Client_Search_Query::INTERVAL_YOUNGER
 749       * @param boolean $not       If true, do a 'NOT' search.
 750       * @param array $opts        Additional options:
 751       *   - fuzzy: (boolean) If true, perform a fuzzy search. The IMAP server
 752       *            MUST support RFC 6203.
 753       */
 754      public function intervalSearch($interval, $range, $not = false,
 755                                     array $opts = array())
 756      {
 757          if (!isset($this->_search['within'])) {
 758              $this->_search['within'] = array();
 759          }
 760          $this->_search['within'][$range] = array(
 761              'fuzzy' => !empty($opts['fuzzy']),
 762              'interval' => $interval,
 763              'not' => $not
 764          );
 765      }
 766  
 767      /**
 768       * AND queries - the contents of this query will be AND'ed (in its
 769       * entirety) with the contents of EACH of the queries passed in.  All
 770       * AND'd queries must share the same charset as this query.
 771       *
 772       * @param mixed $queries  A query, or an array of queries, to AND with the
 773       *                        current query.
 774       */
 775      public function andSearch($queries)
 776      {
 777          if (!isset($this->_search['and'])) {
 778              $this->_search['and'] = array();
 779          }
 780  
 781          if ($queries instanceof Horde_Imap_Client_Search_Query) {
 782              $queries = array($queries);
 783          }
 784  
 785          $this->_search['and'] = array_merge($this->_search['and'], $queries);
 786      }
 787  
 788      /**
 789       * OR a query - the contents of this query will be OR'ed (in its entirety)
 790       * with the contents of EACH of the queries passed in.  All OR'd queries
 791       * must share the same charset as this query.  All contents of any single
 792       * query will be AND'ed together.
 793       *
 794       * @param mixed $queries  A query, or an array of queries, to OR with the
 795       *                        current query.
 796       */
 797      public function orSearch($queries)
 798      {
 799          if (!isset($this->_search['or'])) {
 800              $this->_search['or'] = array();
 801          }
 802  
 803          if ($queries instanceof Horde_Imap_Client_Search_Query) {
 804              $queries = array($queries);
 805          }
 806  
 807          $this->_search['or'] = array_merge($this->_search['or'], $queries);
 808      }
 809  
 810      /**
 811       * Search for messages modified since a specific moment. The IMAP server
 812       * must support the CONDSTORE extension (RFC 7162) for this query to be
 813       * used.
 814       *
 815       * @param integer $value  The mod-sequence value.
 816       * @param string $name    The entry-name string.
 817       * @param string $type    Either 'shared', 'priv', or 'all'. Defaults to
 818       *                        'all'
 819       * @param boolean $not    If true, do a 'NOT' search.
 820       * @param array $opts     Additional options:
 821       *   - fuzzy: (boolean) If true, perform a fuzzy search. The IMAP server
 822       *            MUST support RFC 6203.
 823       */
 824      public function modseq($value, $name = null, $type = null, $not = false,
 825                             array $opts = array())
 826      {
 827          if (!is_null($type)) {
 828              $type = Horde_String::lower($type);
 829              if (!in_array($type, array('shared', 'priv', 'all'))) {
 830                  $type = 'all';
 831              }
 832          }
 833  
 834          $this->_search['modseq'] = array_filter(array(
 835              'fuzzy' => !empty($opts['fuzzy']),
 836              'name' => $name,
 837              'not' => $not,
 838              'type' => (!is_null($name) && is_null($type)) ? 'all' : $type,
 839              'value' => $value
 840          ));
 841      }
 842  
 843      /**
 844       * Use the results from the previous SEARCH command. The IMAP server must
 845       * support the SEARCHRES extension (RFC 5182) for this query to be used.
 846       *
 847       * @param boolean $not  If true, don't match the previous query.
 848       * @param array $opts   Additional options:
 849       *   - fuzzy: (boolean) If true, perform a fuzzy search. The IMAP server
 850       *            MUST support RFC 6203.
 851       */
 852      public function previousSearch($not = false, array $opts = array())
 853      {
 854          $this->_search['prevsearch'] = $not;
 855          if (!empty($opts['fuzzy'])) {
 856              $this->_search['prevsearchfuzzy'] = true;
 857          }
 858      }
 859  
 860      /* Serializable methods. */
 861  
 862      /**
 863       * Serialization.
 864       *
 865       * @return string  Serialized data.
 866       */
 867      public function serialize()
 868      {
 869          $data = array(
 870              // Serialized data ID.
 871              self::VERSION,
 872              $this->_search
 873          );
 874  
 875          if (!is_null($this->_charset)) {
 876              $data[] = $this->_charset;
 877          }
 878  
 879          return serialize($data);
 880      }
 881  
 882      /**
 883       * Unserialization.
 884       *
 885       * @param string $data  Serialized data.
 886       *
 887       * @throws Exception
 888       */
 889      public function unserialize($data)
 890      {
 891          $data = @unserialize($data);
 892          if (!is_array($data) ||
 893              !isset($data[0]) ||
 894              ($data[0] != self::VERSION)) {
 895              throw new Exception('Cache version change');
 896          }
 897  
 898          $this->_search = $data[1];
 899          if (isset($data[2])) {
 900              $this->_charset = $data[2];
 901          }
 902      }
 903  
 904  }