alecpl
2009-01-08 7a229b9e33f7955db3cd6725d357f01735293216
program/include/rcube_imap.php
@@ -59,14 +59,14 @@
  var $cache_changes = array();
  var $uid_id_map = array();
  var $msg_headers = array();
  var $capabilities = array();
  var $skip_deleted = FALSE;
  var $search_set = NULL;
  var $search_subject = '';
  var $search_string = '';
  var $search_charset = '';
  var $search_sort_field = '';
  var $debug_level = 1;
  var $error_code = 0;
  var $options = array('imap' => 'check');
  /**
@@ -77,17 +77,6 @@
  function __construct($db_conn)
    {
    $this->db = $db_conn;
    }
  /**
   * PHP 4 object constructor
   *
   * @see  rcube_imap::__construct
   */
  function rcube_imap($db_conn)
    {
    $this->__construct($db_conn);
    }
@@ -118,8 +107,8 @@
    $ICL_PORT = $port;
    $IMAP_USE_INTERNAL_DATE = false;
    $this->conn = iil_Connect($host, $user, $pass, array('imap' => 'check'));
    $this->conn = iil_Connect($host, $user, $pass, $this->options);
    $this->host = $host;
    $this->user = $user;
    $this->pass = $pass;
@@ -142,8 +131,6 @@
    // get server properties
    if ($this->conn)
      {
      $this->_parse_capability($this->conn->capability);
      if (!empty($this->conn->delimiter))
        $this->delimiter = $this->conn->delimiter;
      if (!empty($this->conn->rootdir))
@@ -180,8 +167,19 @@
    {
    $this->close();
    $this->connect($this->host, $this->user, $this->pass, $this->port, $this->ssl);
    // issue SELECT command to restore connection status
    if ($this->mailbox)
      iil_C_Select($this->conn, $this->mailbox);
    }
  /**
   * Set options to be used in iil_Connect()
   */
  function set_options($opt)
  {
    $this->options = array_merge($this->options, (array)$opt);
  }
  /**
   * Set a root folder for the IMAP connection.
@@ -198,6 +196,7 @@
      $root = substr($root, 0, -1);
    $this->root_dir = $root;
    $this->options['rootdir'] = $root;
    
    if (empty($this->delimiter))
      $this->get_hierarchy_delimiter();
@@ -291,21 +290,22 @@
  /**
   * Save a set of message ids for future message listing methods
   *
   * @param  array  List of IMAP fields to search in
   * @param  string Search string
   * @param  array  List of message ids or NULL if empty
   * @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($subject, $str=null, $msgs=null, $charset=null)
  function set_search_set($str=null, $msgs=null, $charset=null, $sort_field=null)
    {
    if (is_array($subject) && $str == null && $msgs == null)
      list($subject, $str, $msgs, $charset) = $subject;
    if (is_array($str) && $msgs == null)
      list($str, $msgs, $charset, $sort_field) = $str;
    if ($msgs != null && !is_array($msgs))
      $msgs = split(',', $msgs);
      
    $this->search_subject = $subject;
    $this->search_string = $str;
    $this->search_set = (array)$msgs;
    $this->search_set = $msgs;
    $this->search_charset = $charset;
    $this->search_sort_field = $sort_field;
    }
@@ -315,7 +315,7 @@
   */
  function get_search_set()
    {
    return array($this->search_subject, $this->search_string, $this->search_set, $this->search_charset);
    return array($this->search_string, $this->search_set, $this->search_charset, $this->search_sort_field);
    }
@@ -340,8 +340,22 @@
   */
  function get_capability($cap)
    {
    $cap = strtoupper($cap);
    return $this->capabilities[$cap];
    return iil_C_GetCapability($this->conn, strtoupper($cap));
    }
  /**
   * 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)
    {
    $flagsmap = $GLOBALS['IMAP_FLAGS'];
    return (($imap_flag = $flagsmap[strtoupper($flag)]) && in_array_nocase($imap_flag, $this->conn->permanentflags));
    }
@@ -540,7 +554,7 @@
    // use saved message set
    if ($this->search_string && $mailbox == $this->mailbox)
      return $this->_list_header_set($mailbox, $this->search_set, $page, $sort_field, $sort_order);
      return $this->_list_header_set($mailbox, $page, $sort_field, $sort_order);
    $this->_set_sort_order($sort_field, $sort_order);
@@ -573,10 +587,9 @@
      {
      // retrieve headers from IMAP
      if ($this->get_capability('sort') && ($msg_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')))
        {
        $mymsgidx = array_slice ($msg_index, $begin, $end-$begin, true);
        {
        $mymsgidx = array_slice ($msg_index, $begin, $end-$begin);
        $msgs = join(",", $mymsgidx);
        $headers_sorted = true;
        }
      else
        {
@@ -588,18 +601,13 @@
      // fetch reuested headers from server
      $a_msg_headers = array();
      $deleted_count = $this->_fetch_headers($mailbox, $msgs, $a_msg_headers, $cache_key);
      if ($this->sort_order == 'DESC' && $headers_sorted) {
        //since the sort order is not used in the iil_c_sort function we have to do it here
        $a_msg_headers = array_reverse($a_msg_headers);
      }
      // 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
@@ -623,62 +631,91 @@
    }
  /**
   * Public method for listing a specific set of headers
   * convert mailbox name with root dir first
   * Private method for listing a set of message headers (search results)
   *
   * @param   string   Mailbox/folder name
   * @param   array    List of message ids to list
   * @param   int      Current page to list
   * @param   string   Header field to sort by
   * @param   string   Sort order [ASC|DESC]
   * @return  array    Indexed array with message header objects
   * @access  public
   */
  function list_header_set($mbox_name='', $msgs, $page=NULL, $sort_field=NULL, $sort_order=NULL)
    {
    $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
    return $this->_list_header_set($mailbox, $msgs, $page, $sort_field, $sort_order);
    }
  /**
   * Private method for listing a set of message headers
   *
   * @access  private
   * @see     rcube_imap::list_header_set()
   */
  function _list_header_set($mailbox, $msgs, $page=NULL, $sort_field=NULL, $sort_order=NULL)
  function _list_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL)
    {
    if (!strlen($mailbox) || empty($msgs))
    if (!strlen($mailbox) || empty($this->search_set))
      return array();
    // also accept a comma-separated list of message ids
    if (is_array ($msgs)) {
      $max = count ($msgs);
      $msgs = join (',', $msgs);
    } else {
      $max = count(split(',', $msgs));
    }
    $msgs = $this->search_set;
    $a_msg_headers = array();
    $start_msg = ($this->list_page-1) * $this->page_size;
    $this->_set_sort_order($sort_field, $sort_order);
    $start_msg = ($this->list_page-1) * $this->page_size;
    // 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);
        }
    // fetch reuested headers from server
    $a_msg_headers = array();
    $this->_fetch_headers($mailbox, $msgs, $a_msg_headers, NULL);
      // return empty array if no messages found
      if (empty($msgs))
        return array();
    // return empty array if no messages found
    if (!is_array($a_msg_headers) || empty($a_msg_headers))
      return array();
      if ($sort_order == 'DESC')
        $msgs = array_reverse($msgs);
    // if not already sorted
    $a_msg_headers = iil_SortHeaders($a_msg_headers, $this->sort_field, $this->sort_order);
      // get messages uids for one page
      $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
    // only return the requested part of the set
    return array_slice(array_values($a_msg_headers), $start_msg, min($max-$start_msg, $this->page_size));
      // fetch headers
      $this->_fetch_headers($mailbox, join(',',$msgs), $a_msg_headers, NULL);
      $sorter = new rcube_header_sorter();
      $sorter->set_sequence_numbers($msgs);
      $sorter->sort_headers($a_msg_headers);
      return array_values($a_msg_headers);
      }
    else { // SEARCH searching result, need sorting
      $cnt = count($msgs);
      if ($cnt > 300 && $cnt > $this->page_size) { // experimantal value for best result
        // use memory less expensive (and quick) method for big result set
   $a_index = $this->message_index($mailbox, $this->sort_field, $this->sort_order);
        // get messages uids for one page...
        $msgs = array_slice(array_keys($a_index), $start_msg, min($cnt-$start_msg, $this->page_size));
   // ...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 rcube_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 = iil_SortHeaders($a_msg_headers, $this->sort_field, $this->sort_order);
        // only return the requested part of the set
        return array_slice(array_values($a_msg_headers), $start_msg, min($cnt-$start_msg, $this->page_size));
        }
      }
    }
@@ -766,7 +803,7 @@
    
  
  /**
   * Return sorted array of message UIDs
   * Return sorted array of message IDs (not UIDs)
   *
   * @param string Mailbox to get index from
   * @param string Sort column
@@ -783,11 +820,29 @@
    // we have a saved search result. get index from there
    if (!isset($this->cache[$key]) && $this->search_string && $mailbox == $this->mailbox)
    {
      $this->cache[$key] = $a_msg_headers = array();
      $this->_fetch_headers($mailbox, join(',', $this->search_set), $a_msg_headers, NULL);
      $this->cache[$key] = array();
      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);
      foreach (iil_SortHeaders($a_msg_headers, $this->sort_field, $this->sort_order) as $i => $msg)
        $this->cache[$key][] = $msg->uid;
   if ($this->sort_order == 'DESC')
          $this->cache[$key] = array_reverse($this->search_set);
   else
     $this->cache[$key] = $this->search_set;
        }
      else
        {
   $a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, join(',', $this->search_set), $this->sort_field);
        if ($this->sort_order=="ASC")
          asort($a_index);
        else if ($this->sort_order=="DESC")
          arsort($a_index);
        $this->cache[$key] = array_keys($a_index);
   }
    }
    // have stored it in RAM
@@ -802,34 +857,28 @@
    if ($cache_status>0)
      {
      $a_index = $this->get_message_cache_index($cache_key, TRUE, $this->sort_field, $this->sort_order);
      return array_values($a_index);
      return array_keys($a_index);
      }
    // fetch complete message index
    $msg_count = $this->_messagecount($mailbox);
    if ($this->get_capability('sort') && ($a_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, '', TRUE)))
    if ($this->get_capability('sort') && ($a_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, '')))
      {
      if ($this->sort_order == 'DESC')
        $a_index = array_reverse($a_index);
      $this->cache[$key] = $a_index;
      }
    else
      {
      $a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, "1:$msg_count", $this->sort_field);
      $a_uids = iil_C_FetchUIDs($this->conn, $mailbox);
      if ($this->sort_order=="ASC")
        asort($a_index);
      else if ($this->sort_order=="DESC")
        arsort($a_index);
        
      $i = 0;
      $this->cache[$key] = array();
      foreach ($a_index as $index => $value)
        $this->cache[$key][$i++] = $a_uids[$index];
      $this->cache[$key] = array_keys($a_index);
      }
    return $this->cache[$key];
@@ -889,42 +938,50 @@
   * Invoke search request to IMAP server
   *
   * @param  string  mailbox name to search in
   * @param  string  search criteria (ALL, TO, FROM, SUBJECT, etc)
   * @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='', $criteria='ALL', $str=NULL, $charset=NULL)
  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;
    // have an array of criterias => execute multiple searches
    if (is_array($criteria) && $str)
      {
      $results = array();
      foreach ($criteria as $crit)
        if ($search_result = $this->search($mbox_name, $crit, $str, $charset))
          $results = array_merge($results, $search_result);
      $results = array_unique($results);
      $this->set_search_set($criteria, $str, $results, $charset);
      return $results;
      }
    else if ($str && $criteria)
      {
      $search = (!empty($charset) ? "CHARSET $charset " : '') . sprintf("%s {%d}\r\n%s", $criteria, strlen($str), $str);
      $results = $this->_search_index($mailbox, $search);
    $results = $this->_search_index($mailbox, $str, $charset, $sort_field);
      // try search with ISO charset (should be supported by server)
      if (empty($results) && !empty($charset) && $charset!='ISO-8859-1')
        $results = $this->search($mbox_name, $criteria, rcube_charset_convert($str, $charset, 'ISO-8859-1'), 'ISO-8859-1');
      $this->set_search_set($criteria, $str, $results, $charset);
      return $results;
    // try search with ISO charset (should be supported by server)
    // only if UTF-8 search is not supported
    if (empty($results) && !is_array($results) && !empty($charset) && $charset!='ISO-8859-1')
      {
   // convert strings to ISO-8859-1
        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 = rcube_charset_convert($string, $charset, 'ISO-8859-1');
       $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, 'ISO-8859-1', $sort_field);
      }
    else
      return $this->_search_index($mailbox, $criteria);
    }
    $this->set_search_set($str, $results, $charset, $sort_field);
    return $results;
    }
  /**
@@ -934,9 +991,16 @@
   * @access private
   * @see rcube_imap::search()
   */
  function _search_index($mailbox, $criteria='ALL')
  function _search_index($mailbox, $criteria='ALL', $charset=NULL, $sort_field=NULL)
    {
    $a_messages = iil_C_Search($this->conn, $mailbox, $criteria);
    if ($sort_field && $this->get_capability('sort'))
      {
      $charset = $charset ? $charset : $this->default_charset;
      $a_messages = iil_C_Sort($this->conn, $mailbox, $sort_field, $criteria, FALSE, $charset);
      }
    else
      $a_messages = iil_C_Search($this->conn, $mailbox, ($charset ? "CHARSET $charset " : '') . $criteria);
    // clean message list (there might be some empty entries)
    if (is_array($a_messages))
      {
@@ -956,8 +1020,8 @@
   */
  function refresh_search()
    {
    if (!empty($this->search_subject) && !empty($this->search_string))
      $this->search_set = $this->search('', $this->search_subject, $this->search_string, $this->search_charset);
    if (!empty($this->search_string))
      $this->search_set = $this->search('', $this->search_string, $this->search_charset, $this->search_sort_field);
      
    return $this->get_search_set();
    }
@@ -983,9 +1047,10 @@
   * @param int     Message ID
   * @param string  Mailbox to read from 
   * @param boolean True if $id is the message UID
   * @param boolean True if we need also BODYSTRUCTURE in headers
   * @return object Message headers representation
   */
  function get_headers($id, $mbox_name=NULL, $is_uid=TRUE)
  function get_headers($id, $mbox_name=NULL, $is_uid=TRUE, $bodystr=FALSE)
    {
    $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
    $uid = $is_uid ? $id : $this->_id2uid($id);
@@ -994,7 +1059,7 @@
    if ($uid && ($headers = &$this->get_cached_message($mailbox.'.msg', $uid)))
      return $headers;
    $headers = iil_C_FetchHeader($this->conn, $mailbox, $id, $is_uid);
    $headers = iil_C_FetchHeader($this->conn, $mailbox, $id, $is_uid, $bodystr);
    // write headers cache
    if ($headers)
@@ -1014,22 +1079,26 @@
   * an object structure similar to the one generated by PEAR::Mail_mimeDecode
   *
   * @param int Message UID to fetch
   * @return object stdClass Message part tree or False on failure
   * @param string Message BODYSTRUCTURE string (optional)
   * @return object rcube_message_part Message part tree or False on failure
   */
  function &get_structure($uid)
  function &get_structure($uid, $structure_str='')
    {
    $cache_key = $this->mailbox.'.msg';
    $headers = &$this->get_cached_message($cache_key, $uid, true);
    // return cached message structure
    if (is_object($headers) && is_object($headers->structure))
    if (is_object($headers) && is_object($headers->structure)) {
      return $headers->structure;
    // resolve message sequence number
    if (!($msg_id = $this->_uid2id($uid)))
      return FALSE;
    }
    $structure_str = iil_C_FetchStructureString($this->conn, $this->mailbox, $msg_id);
    // resolve message sequence number
    if (!($msg_id = $this->_uid2id($uid))) {
      return FALSE;
    }
    if (!$structure_str)
      $structure_str = iil_C_FetchStructureString($this->conn, $this->mailbox, $msg_id);
    $structure = iml_GetRawStructureArray($structure_str);
    $struct = false;
@@ -1054,7 +1123,7 @@
      if ($this->caching_enabled)
        $this->add_message_cache($cache_key, $msg_id, $headers, $struct);
      }
    return $struct;
    }
@@ -1064,7 +1133,7 @@
   *
   * @access private
   */
  function &_structure_part($part, $count=0, $parent='')
  function &_structure_part($part, $count=0, $parent='', $raw_headers=null)
    {
    $struct = new rcube_message_part;
    $struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
@@ -1075,7 +1144,7 @@
      $struct->ctype_primary = 'multipart';
      
      // find first non-array entry
      for ($i=1; count($part); $i++)
      for ($i=1; $i<count($part); $i++)
        if (!is_array($part[$i]))
          {
          $struct->ctype_secondary = strtolower($part[$i]);
@@ -1084,11 +1153,25 @@
          
      $struct->mimetype = 'multipart/'.$struct->ctype_secondary;
      // build parts list for headers pre-fetching
      for ($i=0, $count=0; $i<count($part); $i++)
        if (is_array($part[$i]) && count($part[$i]) > 3)
     // fetch message headers if message/rfc822 or named part (could contain Content-Location header)
     if (strtolower($part[$i][0]) == 'message' ||
       (in_array('name', (array)$part[$i][2]) && (empty($part[$i][3]) || $part[$i][3]=='NIL'))) {
       $part_headers[] = $struct->mime_id ? $struct->mime_id.'.'.$i+1 : $i+1;
       }
      // pre-fetch headers of all parts (in one command for better performance)
      if ($part_headers)
        $part_headers = iil_C_FetchMIMEHeaders($this->conn, $this->mailbox, $this->_msg_id, $part_headers);
      $struct->parts = array();
      for ($i=0, $count=0; $i<count($part); $i++)
        if (is_array($part[$i]) && count($part[$i]) > 5)
          $struct->parts[] = $this->_structure_part($part[$i], ++$count, $struct->mime_id);
        if (is_array($part[$i]) && count($part[$i]) > 3)
          $struct->parts[] = $this->_structure_part($part[$i], ++$count, $struct->mime_id,
      $part_headers[$struct->mime_id ? $struck->mime_id.'.'.$i+1 : $i+1]);
      return $struct;
      }
    
@@ -1150,63 +1233,135 @@
      if (empty($struct->disposition))
        $struct->disposition = 'inline';
      }
    // fetch message headers if message/rfc822 or named part (could contain Content-Location header)
    if ($struct->ctype_primary == 'message' || ($struct->ctype_parameters['name'] && !$struct->content_id)) {
      if (empty($raw_headers))
        $raw_headers = iil_C_FetchPartHeader($this->conn, $this->mailbox, $this->_msg_id, $struct->mime_id);
      $struct->headers = $this->_parse_headers($raw_headers) + $struct->headers;
    }
    // fetch message headers if message/rfc822
    if ($struct->ctype_primary=='message')
      {
      $headers = iil_C_FetchPartBody($this->conn, $this->mailbox, $this->_msg_id, $struct->mime_id.'.HEADER');
      $struct->headers = $this->_parse_headers($headers);
    if ($struct->ctype_primary=='message') {
      if (is_array($part[8]) && empty($struct->parts))
        $struct->parts[] = $this->_structure_part($part[8], ++$count, $struct->mime_id);
      }
    // normalize filename property
    if ($filename_mime = $struct->d_parameters['filename'] ? $struct->d_parameters['filename'] : $struct->ctype_parameters['name'])
      $struct->filename = rcube_imap::decode_mime_string($filename_mime, $this->default_charset);
    else if ($filename_encoded = $struct->d_parameters['filename*'] ? $struct->d_parameters['filename*'] : $struct->ctype_parameters['name*'])
    {
      // decode filename according to RFC 2231, Section 4
      list($filename_charset,, $filename_urlencoded) = split('\'', $filename_encoded);
      $struct->filename = rcube_charset_convert(urldecode($filename_urlencoded), $filename_charset);
    }
    else if (!empty($struct->headers['content-description']))
      $struct->filename = rcube_imap::decode_mime_string($struct->headers['content-description'], $this->default_charset);
    // normalize filename property
    $this->_set_part_filename($struct);
    return $struct;
    }
    
  /**
   * Return a flat array with references to all parts, indexed by part numbers
   *
   * @param object rcube_message_part Message body structure
   * @return Array with part number -> object pairs
   */
  function get_mime_numbers(&$structure)
    {
    $a_parts = array();
    $this->_get_part_numbers($structure, $a_parts);
    return $a_parts;
    }
  /**
   * Helper method for recursive calls
   * Set attachment filename from message part structure
   *
   * @access private
   * @param  object rcube_message_part Part object
   */
  function _get_part_numbers(&$part, &$a_parts)
  function _set_part_filename(&$part)
    {
    if ($part->mime_id)
      $a_parts[$part->mime_id] = &$part;
    if (is_array($part->parts))
      for ($i=0; $i<count($part->parts); $i++)
        $this->_get_part_numbers($part->parts[$i], $a_parts);
    if (!empty($part->d_parameters['filename']))
      $filename_mime = $part->d_parameters['filename'];
    else if (!empty($part->d_parameters['filename*']))
      $filename_encoded = $part->d_parameters['filename*'];
    else if (!empty($part->ctype_parameters['name*']))
      $filename_encoded = $part->ctype_parameters['name*'];
    // RFC2231 value continuations
    // TODO: this should be rewrited to support RFC2231 4.1 combinations
    else if (!empty($part->d_parameters['filename*0'])) {
      $i = 0;
      while (isset($part->d_parameters['filename*'.$i])) {
        $filename_mime .= $part->d_parameters['filename*'.$i];
        $i++;
      }
      // some servers (eg. dovecot-1.x) have no support for parameter value continuations
      // we must fetch and parse headers "manually"
      //TODO: fetching headers for a second time is not effecient, this code should be moved somewhere earlier --tensor
      if ($i<2) {
        // TODO: fetch only Content-Type/Content-Disposition header
        $headers = iil_C_FetchPartHeader($this->conn, $this->mailbox, $this->_msg_id, $part->mime_id);
        $filename_mime = '';
        $i = 0;
        while (preg_match('/filename\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
          $filename_mime .= $matches[1];
          $i++;
        }
      }
    }
    else if (!empty($part->d_parameters['filename*0*'])) {
      $i = 0;
      while (isset($part->d_parameters['filename*'.$i.'*'])) {
        $filename_encoded .= $part->d_parameters['filename*'.$i.'*'];
        $i++;
      }
      if ($i<2) {
        $headers = iil_C_FetchPartHeader($this->conn, $this->mailbox, $this->_msg_id, $part->mime_id);
        $filename_encoded = '';
        $i = 0; $matches = array();
        while (preg_match('/filename\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
          $filename_encoded .= $matches[1];
          $i++;
        }
      }
    }
    else if (!empty($part->ctype_parameters['name*0'])) {
      $i = 0;
      while (isset($part->ctype_parameters['name*'.$i])) {
        $filename_mime .= $part->ctype_parameters['name*'.$i];
        $i++;
      }
      if ($i<2) {
        $headers = iil_C_FetchPartHeader($this->conn, $this->mailbox, $this->_msg_id, $part->mime_id);
        $filename_mime = '';
        $i = 0; $matches = array();
        while (preg_match('/\s+name\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
          $filename_mime .= $matches[1];
          $i++;
        }
      }
    }
    else if (!empty($part->ctype_parameters['name*0*'])) {
      $i = 0;
      while (isset($part->ctype_parameters['name*'.$i.'*'])) {
        $filename_encoded .= $part->ctype_parameters['name*'.$i.'*'];
        $i++;
      }
      if ($i<2) {
        $headers = iil_C_FetchPartHeader($this->conn, $this->mailbox, $this->_msg_id, $part->mime_id);
        $filename_encoded = '';
        $i = 0; $matches = array();
        while (preg_match('/\s+name\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
          $filename_encoded .= $matches[1];
          $i++;
        }
      }
    }
    // read 'name' after rfc2231 parameters as it may contains truncated filename (from Thunderbird)
    else if (!empty($part->ctype_parameters['name']))
      $filename_mime = $part->ctype_parameters['name'];
    // Content-Disposition
    else if (!empty($part->headers['content-description']))
      $filename_mime = $part->headers['content-description'];
    else
      return;
    // decode filename
    if (!empty($filename_mime)) {
      $part->filename = rcube_imap::decode_mime_string($filename_mime,
        $part->charset ? $part->charset : rc_detect_encoding($filename_mime, $this->default_charset));
      }
    else if (!empty($filename_encoded)) {
      // decode filename according to RFC 2231, Section 4
      if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) {
        $filename_charset = $fmatches[1];
        $filename_encoded = $fmatches[2];
        }
      $part->filename = rcube_charset_convert(urldecode($filename_encoded), $filename_charset);
      }
    }
  /**
   * Fetch message body of a specific message from the server
   *
@@ -1214,9 +1369,10 @@
   * @param  string Part number
   * @param  object rcube_message_part Part object created by get_structure()
   * @param  mixed  True to print part, ressource to write part contents in
   * @param  resource File pointer to save the message part
   * @return string Message/part body if not printed
   */
  function &get_message_part($uid, $part=1, $o_part=NULL, $print=NULL)
  function &get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL)
    {
    if (!($msg_id = $this->_uid2id($uid)))
      return FALSE;
@@ -1235,6 +1391,8 @@
      
    // TODO: Add caching for message parts
    if (!$part) $part = 'TEXT';
    if ($print)
      {
      $mode = $o_part->encoding == 'base64' ? 3 : ($o_part->encoding == 'quoted-printable' ? 1 : 2);
@@ -1249,7 +1407,10 @@
      }
    else
      {
      $body = iil_C_HandlePartBody($this->conn, $this->mailbox, $msg_id, $part, 1);
      if ($fp && $o_part->encoding == 'base64')
        return iil_C_HandlePartBody($this->conn, $this->mailbox, $msg_id, $part, 3, $fp);
      else
        $body = iil_C_HandlePartBody($this->conn, $this->mailbox, $msg_id, $part, 1);
      // decode part body
      if ($o_part->encoding)
@@ -1264,8 +1425,14 @@
        $body = rcube_charset_convert($body, $o_part->charset);
        }
      if ($fp)
        {
        fwrite($fp, $body);
        return true;
        }
      }
    return $body;
    }
@@ -1279,7 +1446,10 @@
   */
  function &get_body($uid, $part=1)
    {
    return $this->get_message_part($uid, $part);
    $headers = $this->get_headers($uid);
    return rcube_charset_convert(
      $this->mime_decode($this->get_message_part($uid, $part), 'quoted-printable'),
      $headers->charset ? $headers->charset : $this->default_charset);
    }
@@ -1294,10 +1464,24 @@
    if (!($msg_id = $this->_uid2id($uid)))
      return FALSE;
    $body = iil_C_FetchPartHeader($this->conn, $this->mailbox, $msg_id, NULL);
    $body .= iil_C_HandlePartBody($this->conn, $this->mailbox, $msg_id, NULL, 1);
    return iil_C_HandlePartBody($this->conn, $this->mailbox, $msg_id);
    }
    return $body;
  /**
   * Returns the message headers as string
   *
   * @param int  Message UID
   * @return string Message headers string
   */
  function &get_raw_headers($uid)
    {
    if (!($msg_id = $this->_uid2id($uid)))
      return FALSE;
    $headers = iil_C_FetchPartHeader($this->conn, $this->mailbox, $msg_id, NULL);
    return $headers;
    }
    
@@ -1311,8 +1495,6 @@
    if (!($msg_id = $this->_uid2id($uid)))
      return FALSE;
    print iil_C_FetchPartHeader($this->conn, $this->mailbox, $msg_id, NULL);
    flush();
    iil_C_HandlePartBody($this->conn, $this->mailbox, $msg_id, NULL, 2);
    }
@@ -1339,6 +1521,8 @@
      $result = iil_C_Undelete($this->conn, $this->mailbox, join(',', array_values($msg_ids)));
    else if ($flag=='UNSEEN')
      $result = iil_C_Unseen($this->conn, $this->mailbox, join(',', array_values($msg_ids)));
    else if ($flag=='UNFLAGGED')
      $result = iil_C_UnFlag($this->conn, $this->mailbox, join(',', array_values($msg_ids)), 'FLAGGED');
    else
      $result = iil_C_Flag($this->conn, $this->mailbox, join(',', array_values($msg_ids)), $flag);
@@ -1388,7 +1572,7 @@
    $mailbox = $this->_mod_mailbox($mbox_name);
    // make sure mailbox exists
    if (in_array($mailbox, $this->_list_mailboxes()))
    if (($mailbox == 'INBOX') || in_array($mailbox, $this->_list_mailboxes()))
      $saved = iil_C_Append($this->conn, $mailbox, $message);
    if ($saved)
@@ -1411,16 +1595,16 @@
   */
  function move_message($uids, $to_mbox, $from_mbox='')
    {
    $to_mbox = stripslashes($to_mbox);
    $to_mbox_in = stripslashes($to_mbox);
    $from_mbox = stripslashes($from_mbox);
    $to_mbox = $this->_mod_mailbox($to_mbox);
    $to_mbox = $this->_mod_mailbox($to_mbox_in);
    $from_mbox = $from_mbox ? $this->_mod_mailbox($from_mbox) : $this->mailbox;
    // make sure mailbox exists
    if (!in_array($to_mbox, $this->_list_mailboxes()))
      {
      if (in_array($to_mbox, $this->default_folders))
        $this->create_mailbox($to_mbox, TRUE);
      if (in_array($to_mbox_in, $this->default_folders))
        $this->create_mailbox($to_mbox_in, TRUE);
      else
        return FALSE;
      }
@@ -1442,12 +1626,19 @@
    
    // send expunge command in order to have the moved message
    // really deleted from the source mailbox
    if ($moved)
      {
      $this->_expunge($from_mbox, FALSE);
      $this->_clear_messagecount($from_mbox);
      $this->_clear_messagecount($to_mbox);
      }
    if ($moved) {
      // but only when flag_for_deletion is set to false
      if (!rcmail::get_instance()->config->get('flag_for_deletion', false))
        {
        $this->_expunge($from_mbox, FALSE);
        $this->_clear_messagecount($from_mbox);
        $this->_clear_messagecount($to_mbox);
        }
    }
    // moving failed
    else if (rcmail::get_instance()->config->get('delete_always', false)) {
      return iil_C_Delete($this->conn, $from_mbox, join(',', $a_mids));
    }
      
    // remove message ids from search set
    if ($moved && $this->search_set && $from_mbox == $this->mailbox)
@@ -1491,7 +1682,6 @@
    if (!is_array($a_uids))
      return false;
    // convert uids to message ids
    $a_mids = array();
    foreach ($a_uids as $uid)
@@ -1505,10 +1695,11 @@
      {
      $this->_expunge($mailbox, FALSE);
      $this->_clear_messagecount($mailbox);
      unset($this->uid_id_map[$mailbox]);
      }
    // remove message ids from search set
    if ($moved && $this->search_set && $mailbox == $this->mailbox)
    if ($deleted && $this->search_set && $mailbox == $this->mailbox)
      $this->search_set = array_diff($this->search_set, $a_mids);
    // remove deleted messages from cache
@@ -1903,6 +2094,9 @@
   */
  function clear_cache($key=NULL)
    {
    if (!$this->caching_enabled)
      return;
    if ($key===NULL)
      {
      foreach ($this->cache as $key => $data)
@@ -1978,8 +2172,7 @@
      {
      $this->db->query(
        "UPDATE ".get_table_name('cache')."
         SET    created=".$this->db->now().",
                data=?
         SET    created=". $this->db->now().", data=?
         WHERE  user_id=?
         AND    cache_key=?",
        $data,
@@ -2132,7 +2325,7 @@
  /**
   * @access private
   */  
  function get_message_cache_index($key, $force=FALSE, $sort_col='idx', $sort_order='ASC')
  function get_message_cache_index($key, $force=FALSE, $sort_field='idx', $sort_order='ASC')
    {
    static $sa_message_index = array();
    
@@ -2149,7 +2342,7 @@
       FROM ".get_table_name('messages')."
       WHERE  user_id=?
       AND    cache_key=?
       ORDER BY ".$this->db->quote_identifier($sort_col)." ".$sort_order,
       ORDER BY ".$this->db->quote_identifier($sort_field)." ".$sort_order,
      $_SESSION['user_id'],
      $key);
@@ -2166,7 +2359,7 @@
    {
    if (empty($key) || !is_object($headers) || empty($headers->uid))
        return;
    // add to internal (fast) cache
    $this->cache['__single_msg'][$headers->uid] = $headers;
    $this->cache['__single_msg'][$headers->uid]->structure = $struct;
@@ -2329,42 +2522,56 @@
  /**
   * Decode a mime-encoded string to internal charset
   *
   * @param string  Header value
   * @param string  Fallback charset if none specified
   * @param string $input    Header value
   * @param string $fallback Fallback charset if none specified
   *
   * @return string Decoded string
   * @static
   */
  function decode_mime_string($input, $fallback=null)
  public static function decode_mime_string($input, $fallback=null)
    {
    // Initialize variable
    $out = '';
    $pos = strpos($input, '=?');
    if ($pos !== false)
      {
      // rfc: all line breaks or other characters not found
      // in the Base64 Alphabet must be ignored by decoding software
      // delete all blanks between MIME-lines, differently we can
      // receive unnecessary blanks and broken utf-8 symbols
      $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
    // Iterate instead of recursing, this way if there are too many values we don't have stack overflows
    // rfc: all line breaks or other characters not found
    // in the Base64 Alphabet must be ignored by decoding software
    // delete all blanks between MIME-lines, differently we can
    // receive unnecessary blanks and broken utf-8 symbols
    $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
      $out = substr($input, 0, $pos);
      $end_cs_pos = strpos($input, "?", $pos+2);
      $end_en_pos = strpos($input, "?", $end_cs_pos+1);
      $end_pos = strpos($input, "?=", $end_en_pos+1);
      $encstr = substr($input, $pos+2, ($end_pos-$pos-2));
      $rest = substr($input, $end_pos+2);
    // Check if there is stuff to decode
    if (strpos($input, '=?') !== false) {
      // Loop through the string to decode all occurences of =? ?= into the variable $out
      while(($pos = strpos($input, '=?')) !== false) {
        // Append everything that is before the text to be decoded
        $out .= substr($input, 0, $pos);
      $out .= rcube_imap::_decode_mime_string_part($encstr);
      $out .= rcube_imap::decode_mime_string($rest, $fallback);
        // Get the location of the text to decode
        $end_cs_pos = strpos($input, "?", $pos+2);
        $end_en_pos = strpos($input, "?", $end_cs_pos+1);
        $end_pos = strpos($input, "?=", $end_en_pos+1);
      return $out;
        // Extract the encoded string
        $encstr = substr($input, $pos+2, ($end_pos-$pos-2));
        // Extract the remaining string
        $input = substr($input, $end_pos+2);
        // Decode the string fragement
        $out .= rcube_imap::_decode_mime_string_part($encstr);
      }
      // Deocde the rest (if any)
      if (strlen($input) != 0)
         $out .= rcube_imap::decode_mime_string($input, $fallback);
       // return the results
      return $out;
    }
    // no encoding information, use fallback
    return rcube_charset_convert($input, 
      !empty($fallback) ? $fallback : $GLOBALS['CONFIG']['default_charset']);
      !empty($fallback) ? $fallback : rcmail::get_instance()->config->get('default_charset', 'ISO-8859-1'));
    }
@@ -2514,10 +2721,12 @@
   */
  function _sort_mailbox_list($a_folders)
    {
    $a_out = $a_defaults = array();
    $a_out = $a_defaults = $folders = array();
    $delimiter = $this->get_hierarchy_delimiter();
    // find default folders and skip folders starting with '.'
    foreach($a_folders as $i => $folder)
    foreach ($a_folders as $i => $folder)
      {
      if ($folder{0}=='.')
        continue;
@@ -2525,14 +2734,45 @@
      if (($p = array_search(strtolower($folder), $this->default_folders_lc)) !== false && !$a_defaults[$p])
        $a_defaults[$p] = $folder;
      else
        $a_out[] = $folder;
        $folders[$folder] = rc_strtolower(rcube_charset_convert($folder, 'UTF-7'));
      }
    natcasesort($a_out);
    // sort folders and place defaults on the top
    asort($folders, SORT_LOCALE_STRING);
    ksort($a_defaults);
    return array_merge($a_defaults, $a_out);
    $folders = array_merge($a_defaults, array_keys($folders));
    // finally we must rebuild the list to move
    // subfolders of default folders to their place...
    // ...also do this for the rest of folders because
    // asort() is not properly sorting case sensitive names
    while (list($key, $folder) = each($folders)) {
      // set the type of folder name variable (#1485527)
      $a_out[] = (string) $folder;
      unset($folders[$key]);
      $this->_rsort($folder, $delimiter, $folders, $a_out);
      }
    return $a_out;
    }
  /**
   * @access private
   */
  function _rsort($folder, $delimiter, &$list, &$out)
    {
      while (list($key, $name) = each($list)) {
   if (strpos($name, $folder.$delimiter) === 0) {
     // set the type of folder name variable (#1485527)
         $out[] = (string) $name;
     unset($list[$key]);
     $this->_rsort($name, $delimiter, $list, $out);
     }
        }
      reset($list);
    }
  /**
   * @access private
@@ -2566,36 +2806,6 @@
      }
    
    return $uid;
    }
  /**
   * Parse string or array of server capabilities and put them in internal array
   * @access private
   */
  function _parse_capability($caps)
    {
    if (!is_array($caps))
      $cap_arr = explode(' ', $caps);
    else
      $cap_arr = $caps;
    foreach ($cap_arr as $cap)
      {
      if ($cap=='CAPABILITY')
        continue;
      if (strpos($cap, '=')>0)
        {
        list($key, $value) = explode('=', $cap);
        if (!is_array($this->capabilities[$key]))
          $this->capabilities[$key] = array();
        $this->capabilities[$key][] = $value;
        }
      else
        $this->capabilities[$cap] = TRUE;
      }
    }
@@ -2811,7 +3021,7 @@
    */
   function set_sequence_numbers($seqnums)
   {
      $this->sequence_numbers = $seqnums;
      $this->sequence_numbers = array_flip($seqnums);
   }
 
   /**
@@ -2832,19 +3042,6 @@
   }
 
   /**
    * Get the position of a message sequence number in my sequence_numbers array
    *
    * @param int Message sequence number contained in sequence_numbers
    * @return int Position, -1 if not found
    */
   function position_of($seqnum)
   {
      $pos = array_search($seqnum, $this->sequence_numbers);
      if ($pos === false) return -1;
      return $pos;
   }
   /**
    * Sort method called by uasort()
    */
   function compare_seqnums($a, $b)
@@ -2854,12 +3051,11 @@
      $seqb = $b->id;
      
      // then find each sequence number in my ordered list
      $posa = $this->position_of($seqa);
      $posb = $this->position_of($seqb);
      $posa = isset($this->sequence_numbers[$seqa]) ? intval($this->sequence_numbers[$seqa]) : -1;
      $posb = isset($this->sequence_numbers[$seqb]) ? intval($this->sequence_numbers[$seqb]) : -1;
      
      // return the relative position as the comparison value
      $ret = $posa - $posb;
      return $ret;
      return $posa - $posb;
   }
}