/program/include/crystal_imap.php
PHP | 3834 lines | 2147 code | 627 blank | 1060 comment | 635 complexity | 26e1820e60feb938b38e495d51ff5ca5 MD5 | raw file
Possible License(s): AGPL-1.0
Large files files are truncated, but you can click here to view the full file
- <?php
- /*
- +-----------------------------------------------------------------------+
- | program/include/crystal_imap.php |
- | |
- | This file is part of the RoundCube Webmail client |
- | Copyright (C) 2005-2010, RoundCube Dev. - Switzerland |
- | Licensed under the GNU GPL |
- | |
- | PURPOSE: |
- | IMAP Engine |
- | |
- +-----------------------------------------------------------------------+
- | Author: Thomas Bruederli <crystalmail@gmail.com> |
- | Author: Aleksander Machniak <alec@alec.pl> |
- +-----------------------------------------------------------------------+
- $Id: crystal_imap.php 3494 2010-04-15 12:21:03Z alec $
- */
- /**
- * Interface class for accessing an IMAP server
- *
- * @package Mail
- * @author Thomas Bruederli <crystalmail@gmail.com>
- * @version 1.6
- */
- class crystal_imap
- {
- public $debug_level = 1;
- public $error_code = 0;
- public $skip_deleted = false;
- public $root_dir = '';
- public $page_size = 10;
- public $list_page = 1;
- public $delimiter = NULL;
- public $threading = false;
- public $fetch_add_headers = '';
- public $conn; // crystal_imap_generic object
- private $db;
- private $root_ns = '';
- private $mailbox = 'INBOX';
- private $sort_field = '';
- private $sort_order = 'DESC';
- private $caching_enabled = false;
- private $default_charset = 'ISO-8859-1';
- private $struct_charset = NULL;
- private $default_folders = array('INBOX');
- private $icache = array();
- private $cache = array();
- private $cache_keys = array();
- private $cache_changes = array();
- private $uid_id_map = array();
- private $msg_headers = array();
- public $search_set = NULL;
- public $search_string = '';
- private $search_charset = '';
- private $search_sort_field = '';
- private $search_threads = false;
- private $db_header_fields = array('idx', 'uid', 'subject', 'from', 'to', 'cc', 'date', 'size');
- private $options = array('auth_method' => 'check');
- private $host, $user, $pass, $port, $ssl;
- /**
- * Object constructor
- *
- * @param object DB Database connection
- */
- function __construct($db_conn)
- {
- $this->db = $db_conn;
- $this->conn = new crystal_imap_generic();
- }
- /**
- * Connect to an IMAP server
- *
- * @param string Host to connect
- * @param string Username for IMAP account
- * @param string Password for IMAP account
- * @param number Port to connect to
- * @param string SSL schema (either ssl or tls) or null if plain connection
- * @return boolean TRUE on success, FALSE on failure
- * @access public
- */
- function connect($host, $user, $pass, $port=143, $use_ssl=null)
- {
- // check for Open-SSL support in PHP build
- if ($use_ssl && extension_loaded('openssl'))
- $this->options['ssl_mode'] = $use_ssl == 'imaps' ? 'ssl' : $use_ssl;
- else if ($use_ssl) {
- raise_error(array('code' => 403, 'type' => 'imap',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "OpenSSL not available"), true, false);
- $port = 143;
- }
- $this->options['port'] = $port;
- $attempt = 0;
- do {
- $data = cmail::get_instance()->plugins->exec_hook('imap_connect',
- array('host' => $host, 'user' => $user, 'attempt' => ++$attempt));
-
- if (!empty($data['pass']))
- $pass = $data['pass'];
- $this->conn->connect($data['host'], $data['user'], $pass, $this->options);
- } while(!$this->conn->connected() && $data['retry']);
- $this->host = $data['host'];
- $this->user = $data['user'];
- $this->pass = $pass;
- $this->port = $port;
- $this->ssl = $use_ssl;
- // print trace messages
- if ($this->conn->connected()) {
- if ($this->conn->message && ($this->debug_level & 8)) {
- console($this->conn->message);
- }
- // get server properties
- if (!empty($this->conn->rootdir))
- $this->set_rootdir($this->conn->rootdir);
- if (empty($this->delimiter))
- $this->get_hierarchy_delimiter();
- return true;
- }
- // write error log
- else if ($this->conn->error) {
- $this->error_code = $this->conn->errornum;
- raise_error(array('code' => 403, 'type' => 'imap',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => $this->conn->error), true, false);
- }
- return false;
- }
- /**
- * Close IMAP connection
- * Usually done on script shutdown
- *
- * @access public
- */
- function close()
- {
- if ($this->conn && $this->conn->connected())
- $this->conn->close();
- $this->write_cache();
- }
- /**
- * Close IMAP connection and re-connect
- * This is used to avoid some strange socket errors when talking to Courier IMAP
- *
- * @access public
- */
- function reconnect()
- {
- $this->close();
- $this->connect($this->host, $this->user, $this->pass, $this->port, $this->ssl);
-
- // issue SELECT command to restore connection status
- if ($this->mailbox)
- $this->conn->select($this->mailbox);
- }
- /**
- * Set options to be used in crystal_imap_generic::connect()
- */
- function set_options($opt)
- {
- $this->options = array_merge($this->options, (array)$opt);
- }
- /**
- * Set a root folder for the IMAP connection.
- *
- * Only folders within this root folder will be displayed
- * and all folder paths will be translated using this folder name
- *
- * @param string Root folder
- * @access public
- */
- function set_rootdir($root)
- {
- if (preg_match('/[.\/]$/', $root)) //(substr($root, -1, 1)==='/')
- $root = substr($root, 0, -1);
- $this->root_dir = $root;
- $this->options['rootdir'] = $root;
-
- if (empty($this->delimiter))
- $this->get_hierarchy_delimiter();
- }
- /**
- * Set default message charset
- *
- * This will be used for message decoding if a charset specification is not available
- *
- * @param string Charset string
- * @access public
- */
- function set_charset($cs)
- {
- $this->default_charset = $cs;
- }
- /**
- * This list of folders will be listed above all other folders
- *
- * @param array Indexed list of folder names
- * @access public
- */
- function set_default_mailboxes($arr)
- {
- if (is_array($arr)) {
- $this->default_folders = $arr;
- // add inbox if not included
- if (!in_array('INBOX', $this->default_folders))
- array_unshift($this->default_folders, 'INBOX');
- }
- }
- /**
- * Set internal mailbox reference.
- *
- * All operations will be perfomed on this mailbox/folder
- *
- * @param string Mailbox/Folder name
- * @access public
- */
- function set_mailbox($new_mbox)
- {
- $mailbox = $this->mod_mailbox($new_mbox);
- if ($this->mailbox == $mailbox)
- return;
- $this->mailbox = $mailbox;
- // clear messagecount cache for this mailbox
- $this->_clear_messagecount($mailbox);
- }
- /**
- * Set internal list page
- *
- * @param number Page number to list
- * @access public
- */
- function set_page($page)
- {
- $this->list_page = (int)$page;
- }
- /**
- * Set internal page size
- *
- * @param number Number of messages to display on one page
- * @access public
- */
- function set_pagesize($size)
- {
- $this->page_size = (int)$size;
- }
-
- /**
- * Save a set of message ids for future message listing methods
- *
- * @param string IMAP Search query
- * @param array List of message ids or NULL if empty
- * @param string Charset of search string
- * @param string Sorting field
- */
- function set_search_set($str=null, $msgs=null, $charset=null, $sort_field=null, $threads=false)
- {
- if (is_array($str) && $msgs == null)
- list($str, $msgs, $charset, $sort_field, $threads) = $str;
- if ($msgs != null && !is_array($msgs))
- $msgs = explode(',', $msgs);
- $this->search_string = $str;
- $this->search_set = $msgs;
- $this->search_charset = $charset;
- $this->search_sort_field = $sort_field;
- $this->search_threads = $threads;
- }
- /**
- * Return the saved search set as hash array
- * @return array Search set
- */
- function get_search_set()
- {
- return array($this->search_string,
- $this->search_set,
- $this->search_charset,
- $this->search_sort_field,
- $this->search_threads,
- );
- }
- /**
- * Returns the currently used mailbox name
- *
- * @return string Name of the mailbox/folder
- * @access public
- */
- function get_mailbox_name()
- {
- return $this->conn->connected() ? $this->mod_mailbox($this->mailbox, 'out') : '';
- }
- /**
- * Returns the IMAP server's capability
- *
- * @param string Capability name
- * @return mixed Capability value or TRUE if supported, FALSE if not
- * @access public
- */
- function get_capability($cap)
- {
- return $this->conn->getCapability(strtoupper($cap));
- }
- /**
- * Sets threading flag to the best supported THREAD algorithm
- *
- * @param boolean TRUE to enable and FALSE
- * @return string Algorithm or false if THREAD is not supported
- * @access public
- */
- function set_threading($enable=false)
- {
- $this->threading = false;
-
- if ($enable) {
- if ($this->get_capability('THREAD=REFS'))
- $this->threading = 'REFS';
- else if ($this->get_capability('THREAD=REFERENCES'))
- $this->threading = 'REFERENCES';
- else if ($this->get_capability('THREAD=ORDEREDSUBJECT'))
- $this->threading = 'ORDEREDSUBJECT';
- }
- return $this->threading;
- }
- /**
- * Checks the PERMANENTFLAGS capability of the current mailbox
- * and returns true if the given flag is supported by the IMAP server
- *
- * @param string Permanentflag name
- * @return mixed True if this flag is supported
- * @access public
- */
- function check_permflag($flag)
- {
- $flag = strtoupper($flag);
- $imap_flag = $this->conn->flags[$flag];
- return (in_array_nocase($imap_flag, $this->conn->permanentflags));
- }
- /**
- * Returns the delimiter that is used by the IMAP server for folder separation
- *
- * @return string Delimiter string
- * @access public
- */
- function get_hierarchy_delimiter()
- {
- if ($this->conn && empty($this->delimiter))
- $this->delimiter = $this->conn->getHierarchyDelimiter();
- if (empty($this->delimiter))
- $this->delimiter = '/';
- return $this->delimiter;
- }
- /**
- * Get message count for a specific mailbox
- *
- * @param string Mailbox/folder name
- * @param string Mode for count [ALL|THREADS|UNSEEN|RECENT]
- * @param boolean Force reading from server and update cache
- * @return int Number of messages
- * @access public
- */
- function messagecount($mbox_name='', $mode='ALL', $force=false)
- {
- $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
- return $this->_messagecount($mailbox, $mode, $force);
- }
- /**
- * Private method for getting nr of messages
- *
- * @access private
- * @see crystal_imap::messagecount()
- */
- private function _messagecount($mailbox='', $mode='ALL', $force=false)
- {
- $mode = strtoupper($mode);
- if (empty($mailbox))
- $mailbox = $this->mailbox;
- // count search set
- if ($this->search_string && $mailbox == $this->mailbox && ($mode == 'ALL' || $mode == 'THREADS') && !$force) {
- if ($this->search_threads)
- return $mode == 'ALL' ? count((array)$this->search_set['depth']) : count((array)$this->search_set['tree']);
- else
- return count((array)$this->search_set);
- }
-
- $a_mailbox_cache = $this->get_cache('messagecount');
-
- // return cached value
- if (!$force && is_array($a_mailbox_cache[$mailbox]) && isset($a_mailbox_cache[$mailbox][$mode]))
- return $a_mailbox_cache[$mailbox][$mode];
- if (!is_array($a_mailbox_cache[$mailbox]))
- $a_mailbox_cache[$mailbox] = array();
- if ($mode == 'THREADS') {
- $count = $this->_threadcount($mailbox, $msg_count);
- $_SESSION['maxuid'][$mailbox] = $msg_count ? $this->_id2uid($msg_count) : 0;
- }
- // RECENT count is fetched a bit different
- else if ($mode == 'RECENT') {
- $count = $this->conn->checkForRecent($mailbox);
- }
- // use SEARCH for message counting
- else if ($this->skip_deleted) {
- $search_str = "ALL UNDELETED";
- // get message count and store in cache
- if ($mode == 'UNSEEN')
- $search_str .= " UNSEEN";
- // get message count using SEARCH
- // not very performant but more precise (using UNDELETED)
- // disable THREADS for this request
- $threads = $this->threading;
- $this->threading = false;
- $index = $this->_search_index($mailbox, $search_str);
- $this->threading = $threads;
-
- $count = is_array($index) ? count($index) : 0;
- if ($mode == 'ALL')
- $_SESSION['maxuid'][$mailbox] = $index ? $this->_id2uid(max($index)) : 0;
- }
- else {
- if ($mode == 'UNSEEN')
- $count = $this->conn->countUnseen($mailbox);
- else {
- $count = $this->conn->countMessages($mailbox);
- $_SESSION['maxuid'][$mailbox] = $count ? $this->_id2uid($count) : 0;
- }
- }
- $a_mailbox_cache[$mailbox][$mode] = (int)$count;
- // write back to cache
- $this->update_cache('messagecount', $a_mailbox_cache);
- return (int)$count;
- }
- /**
- * Private method for getting nr of threads
- *
- * @access private
- * @see crystal_imap::messagecount()
- */
- private function _threadcount($mailbox, &$msg_count)
- {
- if (!empty($this->icache['threads']))
- return count($this->icache['threads']['tree']);
-
- list ($thread_tree, $msg_depth, $has_children) = $this->_fetch_threads($mailbox);
-
- $msg_count = count($msg_depth);
- // $this->update_thread_cache($mailbox, $thread_tree, $msg_depth, $has_children);
- return count($thread_tree);
- }
- /**
- * Public method for listing headers
- * convert mailbox name with root dir first
- *
- * @param string Mailbox/folder name
- * @param int Current page to list
- * @param string Header field to sort by
- * @param string Sort order [ASC|DESC]
- * @param boolean Number of slice items to extract from result array
- * @return array Indexed array with message header objects
- * @access public
- */
- function list_headers($mbox_name='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
- {
- $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
- return $this->_list_headers($mailbox, $page, $sort_field, $sort_order, false, $slice);
- }
- /**
- * Private method for listing message headers
- *
- * @access private
- * @see crystal_imap::list_headers
- */
- private function _list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $recursive=false, $slice=0)
- {
- if (!strlen($mailbox))
- return array();
- // use saved message set
- if ($this->search_string && $mailbox == $this->mailbox)
- return $this->_list_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
- if ($this->threading)
- return $this->_list_thread_headers($mailbox, $page, $sort_field, $sort_order, $recursive, $slice);
- $this->_set_sort_order($sort_field, $sort_order);
- $page = $page ? $page : $this->list_page;
- $cache_key = $mailbox.'.msg';
- $cache_status = $this->check_cache_status($mailbox, $cache_key);
- // cache is OK, we can get all messages from local cache
- if ($cache_status>0) {
- $start_msg = ($page-1) * $this->page_size;
- $a_msg_headers = $this->get_message_cache($cache_key, $start_msg,
- $start_msg+$this->page_size, $this->sort_field, $this->sort_order);
- $result = array_values($a_msg_headers);
- if ($slice)
- $result = array_slice($result, -$slice, $slice);
- return $result;
- }
- // cache is dirty, sync it
- else if ($this->caching_enabled && $cache_status==-1 && !$recursive) {
- $this->sync_header_index($mailbox);
- return $this->_list_headers($mailbox, $page, $this->sort_field, $this->sort_order, true, $slice);
- }
- // retrieve headers from IMAP
- $a_msg_headers = array();
- // use message index sort as default sorting (for better performance)
- if (!$this->sort_field) {
- if ($this->skip_deleted) {
- // @TODO: this could be cached
- if ($msg_index = $this->_search_index($mailbox, 'ALL UNDELETED')) {
- $max = max($msg_index);
- list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
- $msg_index = array_slice($msg_index, $begin, $end-$begin);
- }
- }
- else if ($max = $this->conn->countMessages($mailbox)) {
- list($begin, $end) = $this->_get_message_range($max, $page);
- $msg_index = range($begin+1, $end);
- }
- else
- $msg_index = array();
- if ($slice)
- $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
- // fetch reqested headers from server
- if ($msg_index)
- $this->_fetch_headers($mailbox, join(",", $msg_index), $a_msg_headers, $cache_key);
- }
- // use SORT command
- else if ($this->get_capability('SORT')) {
- if ($msg_index = $this->conn->sort($mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')) {
- list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
- $max = max($msg_index);
- $msg_index = array_slice($msg_index, $begin, $end-$begin);
- if ($slice)
- $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
- // fetch reqested headers from server
- $this->_fetch_headers($mailbox, join(',', $msg_index), $a_msg_headers, $cache_key);
- }
- }
- // fetch specified header for all messages and sort
- else if ($a_index = $this->conn->fetchHeaderIndex($mailbox, "1:*", $this->sort_field, $this->skip_deleted)) {
- asort($a_index); // ASC
- $msg_index = array_keys($a_index);
- $max = max($msg_index);
- list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
- $msg_index = array_slice($msg_index, $begin, $end-$begin);
- if ($slice)
- $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
- // fetch reqested headers from server
- $this->_fetch_headers($mailbox, join(",", $msg_index), $a_msg_headers, $cache_key);
- }
- // delete cached messages with a higher index than $max+1
- // Changed $max to $max+1 to fix this bug : #1484295
- $this->clear_message_cache($cache_key, $max + 1);
- // kick child process to sync cache
- // ...
- // return empty array if no messages found
- if (!is_array($a_msg_headers) || empty($a_msg_headers))
- return array();
-
- // use this class for message sorting
- $sorter = new crystal_header_sorter();
- $sorter->set_sequence_numbers($msg_index);
- $sorter->sort_headers($a_msg_headers);
- if ($this->sort_order == 'DESC')
- $a_msg_headers = array_reverse($a_msg_headers);
- return array_values($a_msg_headers);
- }
- /**
- * Private method for listing message headers using threads
- *
- * @access private
- * @see crystal_imap::list_headers
- */
- private function _list_thread_headers($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $recursive=false, $slice=0)
- {
- $this->_set_sort_order($sort_field, $sort_order);
- $page = $page ? $page : $this->list_page;
- // $cache_key = $mailbox.'.msg';
- // $cache_status = $this->check_cache_status($mailbox, $cache_key);
- // get all threads (default sort order)
- list ($thread_tree, $msg_depth, $has_children) = $this->_fetch_threads($mailbox);
- if (empty($thread_tree))
- return array();
- $msg_index = $this->_sort_threads($mailbox, $thread_tree);
- return $this->_fetch_thread_headers($mailbox,
- $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice);
- }
- /**
- * Private method for fetching threads data
- *
- * @param string Mailbox/folder name
- * @return array Array with thread data
- * @access private
- */
- private function _fetch_threads($mailbox)
- {
- if (empty($this->icache['threads'])) {
- // get all threads
- list ($thread_tree, $msg_depth, $has_children) = $this->conn->thread(
- $mailbox, $this->threading, $this->skip_deleted ? 'UNDELETED' : '');
-
- // add to internal (fast) cache
- $this->icache['threads'] = array();
- $this->icache['threads']['tree'] = $thread_tree;
- $this->icache['threads']['depth'] = $msg_depth;
- $this->icache['threads']['has_children'] = $has_children;
- }
- return array(
- $this->icache['threads']['tree'],
- $this->icache['threads']['depth'],
- $this->icache['threads']['has_children'],
- );
- }
- /**
- * Private method for fetching threaded messages headers
- *
- * @access private
- */
- private function _fetch_thread_headers($mailbox, $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0)
- {
- $cache_key = $mailbox.'.msg';
- // now get IDs for current page
- list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
- $msg_index = array_slice($msg_index, $begin, $end-$begin);
- if ($slice)
- $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
- if ($this->sort_order == 'DESC')
- $msg_index = array_reverse($msg_index);
- // flatten threads array
- // @TODO: fetch children only in expanded mode (?)
- $all_ids = array();
- foreach($msg_index as $root) {
- $all_ids[] = $root;
- if (!empty($thread_tree[$root]))
- $all_ids = array_merge($all_ids, array_keys_recursive($thread_tree[$root]));
- }
- // fetch reqested headers from server
- $this->_fetch_headers($mailbox, $all_ids, $a_msg_headers, $cache_key);
- // return empty array if no messages found
- if (!is_array($a_msg_headers) || empty($a_msg_headers))
- return array();
-
- // use this class for message sorting
- $sorter = new crystal_header_sorter();
- $sorter->set_sequence_numbers($all_ids);
- $sorter->sort_headers($a_msg_headers);
- // Set depth, has_children and unread_children fields in headers
- $this->_set_thread_flags($a_msg_headers, $msg_depth, $has_children);
- return array_values($a_msg_headers);
- }
- /**
- * Private method for setting threaded messages flags:
- * depth, has_children and unread_children
- *
- * @param array Reference to headers array indexed by message ID
- * @param array Array of messages depth indexed by message ID
- * @param array Array of messages children flags indexed by message ID
- * @return array Message headers array indexed by message ID
- * @access private
- */
- private function _set_thread_flags(&$headers, $msg_depth, $msg_children)
- {
- $parents = array();
- foreach ($headers as $idx => $header) {
- $id = $header->id;
- $depth = $msg_depth[$id];
- $parents = array_slice($parents, 0, $depth);
- if (!empty($parents)) {
- $headers[$idx]->parent_uid = end($parents);
- if (!$header->seen)
- $headers[$parents[0]]->unread_children++;
- }
- array_push($parents, $header->uid);
- $headers[$idx]->depth = $depth;
- $headers[$idx]->has_children = $msg_children[$id];
- }
- }
- /**
- * Private method for listing a set of message headers (search results)
- *
- * @param string Mailbox/folder name
- * @param int Current page to list
- * @param string Header field to sort by
- * @param string Sort order [ASC|DESC]
- * @param boolean Number of slice items to extract from result array
- * @return array Indexed array with message header objects
- * @access private
- * @see crystal_imap::list_header_set()
- */
- private function _list_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
- {
- if (!strlen($mailbox) || empty($this->search_set))
- return array();
- // use saved messages from searching
- if ($this->threading)
- return $this->_list_thread_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
- // search set is threaded, we need a new one
- if ($this->search_threads)
- $this->search('', $this->search_string, $this->search_charset, $sort_field);
-
- $msgs = $this->search_set;
- $a_msg_headers = array();
- $page = $page ? $page : $this->list_page;
- $start_msg = ($page-1) * $this->page_size;
- $this->_set_sort_order($sort_field, $sort_order);
- // quickest method (default sorting)
- if (!$this->search_sort_field && !$this->sort_field) {
- if ($sort_order == 'DESC')
- $msgs = array_reverse($msgs);
- // get messages uids for one page
- $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
- if ($slice)
- $msgs = array_slice($msgs, -$slice, $slice);
- // fetch headers
- $this->_fetch_headers($mailbox, join(',',$msgs), $a_msg_headers, NULL);
- // I didn't found in RFC that FETCH always returns messages sorted by index
- $sorter = new crystal_header_sorter();
- $sorter->set_sequence_numbers($msgs);
- $sorter->sort_headers($a_msg_headers);
- return array_values($a_msg_headers);
- }
- // sorted messages, so we can first slice array and then fetch only wanted headers
- if ($this->get_capability('SORT')) { // SORT searching result
- // reset search set if sorting field has been changed
- if ($this->sort_field && $this->search_sort_field != $this->sort_field)
- $msgs = $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
- // return empty array if no messages found
- if (empty($msgs))
- return array();
- if ($sort_order == 'DESC')
- $msgs = array_reverse($msgs);
- // get messages uids for one page
- $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
- if ($slice)
- $msgs = array_slice($msgs, -$slice, $slice);
- // fetch headers
- $this->_fetch_headers($mailbox, join(',',$msgs), $a_msg_headers, NULL);
- $sorter = new crystal_header_sorter();
- $sorter->set_sequence_numbers($msgs);
- $sorter->sort_headers($a_msg_headers);
- return array_values($a_msg_headers);
- }
- else { // SEARCH result, need sorting
- $cnt = count($msgs);
- // 300: experimantal value for best result
- if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) {
- // use memory less expensive (and quick) method for big result set
- $a_index = $this->message_index('', $this->sort_field, $this->sort_order);
- // get messages uids for one page...
- $msgs = array_slice($a_index, $start_msg, min($cnt-$start_msg, $this->page_size));
- if ($slice)
- $msgs = array_slice($msgs, -$slice, $slice);
- // ...and fetch headers
- $this->_fetch_headers($mailbox, join(',', $msgs), $a_msg_headers, NULL);
- // return empty array if no messages found
- if (!is_array($a_msg_headers) || empty($a_msg_headers))
- return array();
- $sorter = new crystal_header_sorter();
- $sorter->set_sequence_numbers($msgs);
- $sorter->sort_headers($a_msg_headers);
- return array_values($a_msg_headers);
- }
- else {
- // for small result set we can fetch all messages headers
- $this->_fetch_headers($mailbox, join(',', $msgs), $a_msg_headers, NULL);
-
- // return empty array if no messages found
- if (!is_array($a_msg_headers) || empty($a_msg_headers))
- return array();
- // if not already sorted
- $a_msg_headers = $this->conn->sortHeaders(
- $a_msg_headers, $this->sort_field, $this->sort_order);
- // only return the requested part of the set
- $a_msg_headers = array_slice(array_values($a_msg_headers),
- $start_msg, min($cnt-$start_msg, $this->page_size));
- if ($slice)
- $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
- return $a_msg_headers;
- }
- }
- }
- /**
- * Private method for listing a set of threaded message headers (search results)
- *
- * @param string Mailbox/folder name
- * @param int Current page to list
- * @param string Header field to sort by
- * @param string Sort order [ASC|DESC]
- * @param boolean Number of slice items to extract from result array
- * @return array Indexed array with message header objects
- * @access private
- * @see crystal_imap::list_header_set()
- */
- private function _list_thread_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
- {
- // update search_set if previous data was fetched with disabled threading
- if (!$this->search_threads)
- $this->search('', $this->search_string, $this->search_charset, $sort_field);
- $thread_tree = $this->search_set['tree'];
- $msg_depth = $this->search_set['depth'];
- $has_children = $this->search_set['children'];
- $a_msg_headers = array();
- $page = $page ? $page : $this->list_page;
- $start_msg = ($page-1) * $this->page_size;
- $this->_set_sort_order($sort_field, $sort_order);
- $msg_index = $this->_sort_threads($mailbox, $thread_tree, array_keys($msg_depth));
- return $this->_fetch_thread_headers($mailbox,
- $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0);
- }
- /**
- * Helper function to get first and last index of the requested set
- *
- * @param int message count
- * @param mixed page number to show, or string 'all'
- * @return array array with two values: first index, last index
- * @access private
- */
- private function _get_message_range($max, $page)
- {
- $start_msg = ($page-1) * $this->page_size;
-
- if ($page=='all') {
- $begin = 0;
- $end = $max;
- }
- else if ($this->sort_order=='DESC') {
- $begin = $max - $this->page_size - $start_msg;
- $end = $max - $start_msg;
- }
- else {
- $begin = $start_msg;
- $end = $start_msg + $this->page_size;
- }
- if ($begin < 0) $begin = 0;
- if ($end < 0) $end = $max;
- if ($end > $max) $end = $max;
-
- return array($begin, $end);
- }
-
- /**
- * Fetches message headers
- * Used for loop
- *
- * @param string Mailbox name
- * @param string Message index to fetch
- * @param array Reference to message headers array
- * @param array Array with cache index
- * @return int Messages count
- * @access private
- */
- private function _fetch_headers($mailbox, $msgs, &$a_msg_headers, $cache_key)
- {
- // fetch reqested headers from server
- $a_header_index = $this->conn->fetchHeaders(
- $mailbox, $msgs, false, false, $this->fetch_add_headers);
- if (empty($a_header_index))
- return 0;
- // cache is incomplete
- $cache_index = $this->get_message_cache_index($cache_key);
- foreach ($a_header_index as $i => $headers) {
- if ($this->caching_enabled && $cache_index[$headers->id] != $headers->uid) {
- // prevent index duplicates
- if ($cache_index[$headers->id]) {
- $this->remove_message_cache($cache_key, $headers->id, true);
- unset($cache_index[$headers->id]);
- }
- // add message to cache
- $this->add_message_cache($cache_key, $headers->id, $headers, NULL,
- !in_array($headers->uid, $cache_index));
- }
- $a_msg_headers[$headers->uid] = $headers;
- }
- return count($a_msg_headers);
- }
-
- /**
- * Fetches IDS of pseudo recent messages.
- *
- * We compare the maximum UID to determine the number of
- * new messages because the RECENT flag is not reliable.
- *
- * @param string Mailbox/folder name
- * @return array List of recent message UIDs
- */
- function recent_uids($mbox_name = null, $nofetch = false)
- {
- $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
- $old_maxuid = intval($_SESSION['maxuid'][$mailbox]);
-
- // refresh message count -> will update $_SESSION['maxuid'][$mailbox]
- $this->_messagecount($mailbox, 'ALL', true);
-
- if ($_SESSION['maxuid'][$mailbox] > $old_maxuid) {
- $maxuid = max(1, $old_maxuid+1);
- return array_values((array)$this->conn->fetchHeaderIndex(
- $mailbox, "$maxuid:*", 'UID', $this->skip_deleted, true));
- }
-
- return array();
- }
-
- /**
- * Return sorted array of message IDs (not UIDs)
- *
- * @param string Mailbox to get index from
- * @param string Sort column
- * @param string Sort order [ASC, DESC]
- * @return array Indexed array with message ids
- */
- function message_index($mbox_name='', $sort_field=NULL, $sort_order=NULL)
- {
- if ($this->threading)
- return $this->thread_index($mbox_name, $sort_field, $sort_order);
- $this->_set_sort_order($sort_field, $sort_order);
- $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
- $key = "{$mailbox}:{$this->sort_field}:{$this->sort_order}:{$this->search_string}.msgi";
- // we have a saved search result, get index from there
- if (!isset($this->cache[$key]) && $this->search_string
- && !$this->search_threads && $mailbox == $this->mailbox) {
- // use message index sort as default sorting
- if (!$this->sort_field) {
- $msgs = $this->search_set;
- if ($this->search_sort_field != 'date')
- sort($msgs);
- if ($this->sort_order == 'DESC')
- $this->cache[$key] = array_reverse($msgs);
- else
- $this->cache[$key] = $msgs;
- }
- // sort with SORT command
- else if ($this->get_capability('SORT')) {
- if ($this->sort_field && $this->search_sort_field != $this->sort_field)
- $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
- if ($this->sort_order == 'DESC')
- $this->cache[$key] = array_reverse($this->search_set);
- else
- $this->cache[$key] = $this->search_set;
- }
- else {
- $a_index = $this->conn->fetchHeaderIndex($mailbox,
- join(',', $this->search_set), $this->sort_field, $this->skip_deleted);
-
- if (is_array($a_index)) {
- if ($this->sort_order=="ASC")
- asort($a_index);
- else if ($this->sort_order=="DESC")
- arsort($a_index);
- $this->cache[$key] = array_keys($a_index);
- }
- else {
- $this->cache[$key] = array();
- }
- }
- }
- // have stored it in RAM
- if (isset($this->cache[$key]))
- return $this->cache[$key];
- // check local cache
- $cache_key = $mailbox.'.msg';
- $cache_status = $this->check_cache_status($mailbox, $cache_key);
- // cache is OK
- if ($cache_status>0) {
- $a_index = $this->get_message_cache_index($cache_key,
- true, $this->sort_field, $this->sort_order);
- return array_keys($a_index);
- }
- // use message index sort as default sorting
- if (!$this->sort_field) {
- if ($this->skip_deleted) {
- $a_index = $this->_search_index($mailbox, 'ALL');
- } else if ($max = $this->_messagecount($mailbox)) {
- $a_index = range(1, $max);
- }
- if ($this->sort_order == 'DESC')
- $a_index = array_reverse($a_index);
- $this->cache[$key] = $a_index;
- }
- // fetch complete message index
- else if ($this->get_capability('SORT')) {
- if ($a_index = $this->conn->sort($mailbox,
- $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')) {
- if ($this->sort_order == 'DESC')
- $a_index = array_reverse($a_index);
-
- $this->cache[$key] = $a_index;
- }
- }
- else if ($a_index = $this->conn->fetchHeaderIndex(
- $mailbox, "1:*", $this->sort_field, $this->skip_deleted)) {
- if ($this->sort_order=="ASC")
- asort($a_index);
- else if ($this->sort_order=="DESC")
- arsort($a_index);
-
- $this->cache[$key] = array_keys($a_index);
- }
- return $this->cache[$key];
- }
- /**
- * Return sorted array of threaded message IDs (not UIDs)
- *
- * @param string Mailbox to get index from
- * @param string Sort column
- * @param string Sort order [ASC, DESC]
- * @return array Indexed array with message IDs
- */
- function thread_index($mbox_name='', $sort_field=NULL, $sort_order=NULL)
- {
- $this->_set_sort_order($sort_field, $sort_order);
- $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
- $key = "{$mailbox}:{$this->sort_field}:{$this->sort_order}:{$this->search_string}.thi";
- // we have a saved search result, get index from there
- if (!isset($this->cache[$key]) && $this->search_string
- && $this->search_threads && $mailbox == $this->mailbox) {
- // use message IDs for better performance
- $ids = array_keys_recursive($this->search_set['tree']);
- $this->cache[$key] = $this->_flatten_threads($mailbox, $this->search_set['tree'], $ids);
- }
- // have stored it in RAM
- if (isset($this->cache[$key]))
- return $this->cache[$key];
- /*
- // check local cache
- $cache_key = $mailbox.'.msg';
- $cache_status = $this->check_cache_status($mailbox, $cache_key);
- // cache is OK
- if ($cache_status>0) {
- $a_index = $this->get_message_cache_index($cache_key, true, $this->sort_field, $this->sort_order);
- return array_keys($a_index);
- }
- */
- // get all threads (default sort order)
- list ($thread_tree) = $this->_fetch_threads($mailbox);
- $this->cache[$key] = $this->_flatten_threads($mailbox, $thread_tree);
-
- return $this->cache[$key];
- }
- /**
- * Return array of threaded messages (all, not only roots)
- *
- * @param string Mailbox to get index from
- * @param array Threaded messages array (see _fetch_threads())
- * @param array Message IDs if we know what we need (e.g. search result)
- * for better performance
- * @return array Indexed array with message IDs
- *
- * @access private
- */
- private function _flatten_threads($mailbox, $thread_tree, $ids=null)
- {
- if (empty($thread_tree))
- return array();
- $msg_index = $this->_sort_threads($mailbox, $thread_tree, $ids);
- if ($this->sort_order == 'DESC')
- $msg_index = array_reverse($msg_index);
-
- // flatten threads array
- $all_ids = array();
- foreach($msg_index as $root) {
- $all_ids[] = $root;
- if (!empty($thread_tree[$root]))
- $all_ids = array_merge($all_ids, array_keys_recursive($thread_tree[$root]));
- }
- return $all_ids;
- }
- /**
- * @access private
- */
- private function sync_header_index($mailbox)
- {
- $cache_key = $mailbox.'.msg';
- $cache_index = $this->get_message_cache_index($cache_key);
- // fetch complete message index
- $a_message_index = $this->conn->fetchHeaderIndex($mailbox, "1:*", 'UID', $this->skip_deleted);
-
- if ($a_message_index === false)
- return false;
-
- foreach ($a_message_index as $id => $uid) {
- // message in cache at correct position
- if ($cache_index[$id] == $uid) {
- unset($cache_index[$id]);
- continue;
- }
- // message in cache but in wrong position
- if (in_array((string)$uid, $cache_index, true)) {
- unset($cache_index[$id]);
- }
-
- // other message at this position
- if (isset($cache_index[$id])) {
- $for_remove[] = $cache_index[$id];
- unset($cache_index[$id]);
- }
-
- $for_update[] = $id;
- }
- // clear messages at wrong positions and those deleted that are still in cache_index
- if (!empty($for_remove))
- $cache_index = array_merge($cache_index, $for_remove);
-
- if (!empty($cache_index))
- $this->remove_message_cache($cache_key, $cache_index);
- // fetch complete headers and add to cache
- if (!empty($for_update)) {
- if ($headers = $this->conn->fetchHeader($mailbox,
- join(',', $for_update), false, $this->fetch_add_headers)) {
- foreach ($headers as $header) {
- $this->add_message_cache($cache_key, $header->id, $header, NULL,
- in_array($header->uid, (array)$for_remove));
- }
- }
- }
- }
- /**
- * Invoke search request to IMAP server
- *
- * @param string mailbox name to search in
- * @param string search string
- * @param string search string charset
- * @param string header field to sort by
- * @return array search results as list of message ids
- * @access public
- */
- function search($mbox_name='', $str=NULL, $charset=NULL, $sort_field=NULL)
- {
- if (!$str)
- return false;
-
- $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
- $results = $this->_search_index($mailbox, $str, $charset, $sort_field);
- // try search with US-ASCII charset (should be supported by server)
- // only if UTF-8 search is not supported
- if (empty($results) && !is_array($results) && !empty($charset) && $charset != 'US-ASCII')
- {
- // convert strings to US_ASCII
- if(preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) {
- $last = 0; $res = '';
- foreach($matches[1] as $m)
- {
- $string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n
- $string = substr($str, $string_offset - 1, $m[0]);
- $string = crystal_charset_convert($string, $charset, 'US-ASCII');
- if (!$string)
- continue;
- $res .= sprintf("%s{%d}\r\n%s", substr($str, $last, $m[1] - $last - 1), strlen($string), $string);
- $last = $m[0] + $string_offset - 1;
- }
- if ($last < strlen($str))
- $res .= substr($str, $last, strlen($str)-$last);
- }
- else // strings for conversion not found
- $res = $str;
- $results = $this->search($mbox_name, $res, NULL, $sort_field);
- }
- $this->set_search_set($str, $results, $charset, $sort_field, (bool)$this->threading);
- return $results;
- }
- /**
- * Private search method
- *
- * @return array search results as list of message ids
- * @access private
- * @see crystal_imap::search()
- */
- private function _search_index($mailbox, $criteria='ALL', $charset=NULL, $sort_field=NULL)
- {
- $orig_criteria = $criteria;
- if ($this->skip_deleted && !preg_match('/UNDELETED/', $criteria))
- $criteria = 'UNDELETED '.$criteria;
- if ($this->threading) {
- list ($thread_tree, $msg_depth, $has_children) = $this->conn->thread(
- $mailbox, $this->threading, $criteria, $charset);
- $a_messages = array(
- 'tree' => $thread_tree,
- 'depth' => $msg_depth,
- 'children' => $has_children
- );
- }
- else if ($sort_field && $this->get_capability('SORT')) {
- $charset = $charset ? $charset : $this->default_charset;
- $a_messages = $this->conn->sort($mailbox, $sort_field, $criteria, false, $charset);
- if (!$a_messages)
- return array();
- }
- else {
- if ($orig_criteria == 'ALL') {
- $max = $this->_messagecount($mailbox);
- $a_messages = $max ? range(1, $max) : array();
- }
- else {
- $a_messages = $this->conn->search($mailbox,
- ($charset ? "CHARSET $charset " : '') . $criteria);
- if (!$a_messages)
- return array();
- // I didn't found that SEARCH always returns sorted IDs
- if (!$this->sort_field)
- sort($a_messages);
- }
- }
- // update messagecount cache ?
- // $a_mailbox_cache = get_cache('messagecount');
- // $a_mailbox_cache[$mailbox][$criteria] = sizeof($a_messages);
- // $this->update_cache('messagecount', $a_mailbox_cache);
-
- return $a_messages;
- }
-
- /**
- * Direct (real and simple) SEARCH request to IMAP server,
- * without result sorting and caching
- *
- * @param string Mailbox name to search in
- * @param string Search string
- * @param boolean True if UIDs should be returned
- * @return array Search results as list of message IDs or UIDs
- * @access public
- */
- function search_once($mbox_name='', $str=NULL, $ret_uid=false)
- {
- if (!$str)
- return false;
-
- $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
- return $this->conn->search($mailbox, $str, $ret_uid);
- }
-
- /**
- * Sort thread
- *
- * @param string Mailbox name
- * @param array Unsorted thread tree (crystal_imap_generic::thread() result)
- * @param array Message IDs if we know what we need (e.g. search result)
- * @return array Sorted roots IDs
- * @access private
- */
- private function _sort_threads($mailbox, $thread_tree, $ids=NULL)
- …
Large files files are truncated, but you can click here to view the full file