source: subversion/branches/devel-threads/program/include/rcube_imap.php @ 3118

Last change on this file since 3118 was 3118, checked in by alec, 4 years ago
  • use underlined subject for parent with unread children + some fixes
  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 103.7 KB
Line 
1<?php
2
3/*
4 +-----------------------------------------------------------------------+
5 | program/include/rcube_imap.php                                        |
6 |                                                                       |
7 | This file is part of the RoundCube Webmail client                     |
8 | Copyright (C) 2005-2009, RoundCube Dev. - Switzerland                 |
9 | Licensed under the GNU GPL                                            |
10 |                                                                       |
11 | PURPOSE:                                                              |
12 |   IMAP wrapper that implements the Iloha IMAP Library (IIL)           |
13 |   See http://ilohamail.org/ for details                               |
14 |                                                                       |
15 +-----------------------------------------------------------------------+
16 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
17 +-----------------------------------------------------------------------+
18
19 $Id$
20
21*/
22
23
24/*
25 * Obtain classes from the Iloha IMAP library
26 */
27require_once('lib/imap.inc');
28require_once('lib/mime.inc');
29require_once('lib/tnef_decoder.inc');
30
31
32/**
33 * Interface class for accessing an IMAP server
34 *
35 * This is a wrapper that implements the Iloha IMAP Library (IIL)
36 *
37 * @package    Mail
38 * @author     Thomas Bruederli <roundcube@gmail.com>
39 * @version    1.5
40 * @link       http://ilohamail.org
41 */
42class rcube_imap
43{
44  var $db;
45  var $conn;
46  var $root_ns = '';
47  var $root_dir = '';
48  var $mailbox = 'INBOX';
49  var $list_page = 1;
50  var $page_size = 10;
51  var $sort_field = '';
52  var $sort_order = 'DESC';
53  var $delimiter = NULL;
54  var $threading = false;
55  var $caching_enabled = false;
56  var $default_charset = 'ISO-8859-1';
57  var $struct_charset = NULL;
58  var $default_folders = array('INBOX');
59  var $default_folders_lc = array('inbox');
60  var $fetch_add_headers = '';
61  var $cache = array();
62  var $cache_keys = array(); 
63  var $cache_changes = array();
64  var $uid_id_map = array();
65  var $msg_headers = array();
66  var $skip_deleted = false;
67  var $search_set = NULL;
68  var $search_string = '';
69  var $search_charset = '';
70  var $search_sort_field = '';
71  var $search_threads = false;
72  var $debug_level = 1;
73  var $error_code = 0;
74  var $db_header_fields = array('idx', 'uid', 'subject', 'from', 'to', 'cc', 'date', 'size');
75  var $options = array('auth_method' => 'check');
76 
77  private $host, $user, $pass, $port, $ssl;
78
79
80  /**
81   * Object constructor
82   *
83   * @param object DB Database connection
84   */
85  function __construct($db_conn)
86    {
87    $this->db = $db_conn;
88    }
89
90
91  /**
92   * Connect to an IMAP server
93   *
94   * @param  string   Host to connect
95   * @param  string   Username for IMAP account
96   * @param  string   Password for IMAP account
97   * @param  number   Port to connect to
98   * @param  string   SSL schema (either ssl or tls) or null if plain connection
99   * @return boolean  TRUE on success, FALSE on failure
100   * @access public
101   */
102  function connect($host, $user, $pass, $port=143, $use_ssl=null)
103    {
104    global $ICL_SSL, $ICL_PORT, $IMAP_USE_INTERNAL_DATE;
105   
106    // check for Open-SSL support in PHP build
107    if ($use_ssl && extension_loaded('openssl'))
108      $ICL_SSL = $use_ssl == 'imaps' ? 'ssl' : $use_ssl;
109    else if ($use_ssl) {
110      raise_error(array('code' => 403, 'type' => 'imap', 'file' => __FILE__,
111                        'message' => 'Open SSL not available;'), TRUE, FALSE);
112      $port = 143;
113    }
114
115    $ICL_PORT = $port;
116    $IMAP_USE_INTERNAL_DATE = false;
117
118    $attempt = 0;
119    do {
120      $data = rcmail::get_instance()->plugins->exec_hook('imap_connect', array('host' => $host, 'user' => $user, 'attempt' => ++$attempt));
121      if (!empty($data['pass']))
122        $pass = $data['pass'];
123
124      $this->conn = iil_Connect($data['host'], $data['user'], $pass, $this->options);
125    } while(!$this->conn && $data['retry']);
126
127    $this->host = $data['host'];
128    $this->user = $data['user'];
129    $this->pass = $pass;
130    $this->port = $port;
131    $this->ssl = $use_ssl;
132   
133    // print trace messages
134    if ($this->conn && ($this->debug_level & 8))
135      console($this->conn->message);
136   
137    // write error log
138    else if (!$this->conn && $GLOBALS['iil_error'])
139      {
140      $this->error_code = $GLOBALS['iil_errornum'];
141      raise_error(array('code' => 403,
142                       'type' => 'imap',
143                       'message' => $GLOBALS['iil_error']), TRUE, FALSE);
144      }
145
146    // get server properties
147    if ($this->conn)
148      {
149      if (!empty($this->conn->rootdir))
150        {
151        $this->set_rootdir($this->conn->rootdir);
152        $this->root_ns = preg_replace('/[.\/]$/', '', $this->conn->rootdir);
153        }
154      if (empty($this->delimiter))
155        $this->get_hierarchy_delimiter();
156      }
157
158    return $this->conn ? TRUE : FALSE;
159    }
160
161
162  /**
163   * Close IMAP connection
164   * Usually done on script shutdown
165   *
166   * @access public
167   */
168  function close()
169    {   
170    if ($this->conn)
171      iil_Close($this->conn);
172    }
173
174
175  /**
176   * Close IMAP connection and re-connect
177   * This is used to avoid some strange socket errors when talking to Courier IMAP
178   *
179   * @access public
180   */
181  function reconnect()
182    {
183    $this->close();
184    $this->connect($this->host, $this->user, $this->pass, $this->port, $this->ssl);
185   
186    // issue SELECT command to restore connection status
187    if ($this->mailbox)
188      iil_C_Select($this->conn, $this->mailbox);
189    }
190
191  /**
192   * Set options to be used in iil_Connect()
193   */
194  function set_options($opt)
195  {
196    $this->options = array_merge($this->options, (array)$opt);
197  }
198
199  /**
200   * Set a root folder for the IMAP connection.
201   *
202   * Only folders within this root folder will be displayed
203   * and all folder paths will be translated using this folder name
204   *
205   * @param  string   Root folder
206   * @access public
207   */
208  function set_rootdir($root)
209    {
210    if (preg_match('/[.\/]$/', $root)) //(substr($root, -1, 1)==='/')
211      $root = substr($root, 0, -1);
212
213    $this->root_dir = $root;
214    $this->options['rootdir'] = $root;
215   
216    if (empty($this->delimiter))
217      $this->get_hierarchy_delimiter();
218    }
219
220
221  /**
222   * Set default message charset
223   *
224   * This will be used for message decoding if a charset specification is not available
225   *
226   * @param  string   Charset string
227   * @access public
228   */
229  function set_charset($cs)
230    {
231    $this->default_charset = $cs;
232    }
233
234
235  /**
236   * This list of folders will be listed above all other folders
237   *
238   * @param  array  Indexed list of folder names
239   * @access public
240   */
241  function set_default_mailboxes($arr)
242    {
243    if (is_array($arr))
244      {
245      $this->default_folders = $arr;
246      $this->default_folders_lc = array();
247
248      // add inbox if not included
249      if (!in_array_nocase('INBOX', $this->default_folders))
250        array_unshift($this->default_folders, 'INBOX');
251
252      // create a second list with lower cased names
253      foreach ($this->default_folders as $mbox)
254        $this->default_folders_lc[] = strtolower($mbox);
255      }
256    }
257
258
259  /**
260   * Set internal mailbox reference.
261   *
262   * All operations will be perfomed on this mailbox/folder
263   *
264   * @param  string  Mailbox/Folder name
265   * @access public
266   */
267  function set_mailbox($new_mbox)
268    {
269    $mailbox = $this->mod_mailbox($new_mbox);
270
271    if ($this->mailbox == $mailbox)
272      return;
273
274    $this->mailbox = $mailbox;
275
276    // clear messagecount cache for this mailbox
277    $this->_clear_messagecount($mailbox);
278    }
279
280
281  /**
282   * Set internal list page
283   *
284   * @param  number  Page number to list
285   * @access public
286   */
287  function set_page($page)
288    {
289    $this->list_page = (int)$page;
290    }
291
292
293  /**
294   * Set internal page size
295   *
296   * @param  number  Number of messages to display on one page
297   * @access public
298   */
299  function set_pagesize($size)
300    {
301    $this->page_size = (int)$size;
302    }
303   
304
305  /**
306   * Save a set of message ids for future message listing methods
307   *
308   * @param  string  IMAP Search query
309   * @param  array   List of message ids or NULL if empty
310   * @param  string  Charset of search string
311   * @param  string  Sorting field
312   */
313  function set_search_set($str=null, $msgs=null, $charset=null, $sort_field=null, $threads=false)
314    {
315    if (is_array($str) && $msgs == null)
316      list($str, $msgs, $charset, $sort_field) = $str;
317    if ($msgs != null && !is_array($msgs))
318      $msgs = explode(',', $msgs);
319     
320    $this->search_string = $str;
321    $this->search_set = $msgs;
322    $this->search_charset = $charset;
323    $this->search_sort_field = $sort_field;
324    $this->search_threads = $threads;
325    }
326
327
328  /**
329   * Return the saved search set as hash array
330   * @return array Search set
331   */
332  function get_search_set()
333    {
334    return array($this->search_string,
335        $this->search_set,
336        $this->search_charset,
337        $this->search_sort_field,
338        $this->search_threads,
339        );
340    }
341
342
343  /**
344   * Returns the currently used mailbox name
345   *
346   * @return  string Name of the mailbox/folder
347   * @access  public
348   */
349  function get_mailbox_name()
350    {
351    return $this->conn ? $this->mod_mailbox($this->mailbox, 'out') : '';
352    }
353
354
355  /**
356   * Returns the IMAP server's capability
357   *
358   * @param   string  Capability name
359   * @return  mixed   Capability value or TRUE if supported, FALSE if not
360   * @access  public
361   */
362  function get_capability($cap)
363    {
364    return iil_C_GetCapability($this->conn, strtoupper($cap));
365    }
366
367
368  /**
369   * Sets threading flag to the best supported THREAD algorithm
370   *
371   * @param  boolean  TRUE to enable and FALSE
372   * @return string   Algorithm or false if THREAD is not supported
373   * @access public
374   */
375  function set_threading($enable=false)
376    {
377    $this->threading = false;
378   
379    if ($enable) {
380      if ($this->get_capability('THREAD=REFS'))
381        $this->threading = 'REFS';
382      else if ($this->get_capability('THREAD=REFERENCES'))
383        $this->threading = 'REFERENCES';
384      else if ($this->get_capability('THREAD=ORDEREDSUBJECT'))
385        $this->threading = 'ORDEREDSUBJECT';
386      }
387     
388    return $this->threading;
389    }
390
391
392  /**
393   * Checks the PERMANENTFLAGS capability of the current mailbox
394   * and returns true if the given flag is supported by the IMAP server
395   *
396   * @param   string  Permanentflag name
397   * @return  mixed   True if this flag is supported
398   * @access  public
399   */
400  function check_permflag($flag)
401    {
402    $flag = strtoupper($flag);
403    $imap_flag = $GLOBALS['IMAP_FLAGS'][$flag];
404    return (in_array_nocase($imap_flag, $this->conn->permanentflags));
405    }
406
407
408  /**
409   * Returns the delimiter that is used by the IMAP server for folder separation
410   *
411   * @return  string  Delimiter string
412   * @access  public
413   */
414  function get_hierarchy_delimiter()
415    {
416    if ($this->conn && empty($this->delimiter))
417      $this->delimiter = iil_C_GetHierarchyDelimiter($this->conn);
418
419    if (empty($this->delimiter))
420      $this->delimiter = '/';
421
422    return $this->delimiter;
423    }
424
425
426  /**
427   * Public method for mailbox listing.
428   *
429   * Converts mailbox name with root dir first
430   *
431   * @param   string  Optional root folder
432   * @param   string  Optional filter for mailbox listing
433   * @return  array   List of mailboxes/folders
434   * @access  public
435   */
436  function list_mailboxes($root='', $filter='*')
437    {
438    $a_out = array();
439    $a_mboxes = $this->_list_mailboxes($root, $filter);
440
441    foreach ($a_mboxes as $mbox_row)
442      {
443      $name = $this->mod_mailbox($mbox_row, 'out');
444      if (strlen($name))
445        $a_out[] = $name;
446      }
447
448    // INBOX should always be available
449    if (!in_array_nocase('INBOX', $a_out))
450      array_unshift($a_out, 'INBOX');
451
452    // sort mailboxes
453    $a_out = $this->_sort_mailbox_list($a_out);
454
455    return $a_out;
456    }
457
458
459  /**
460   * Private method for mailbox listing
461   *
462   * @return  array   List of mailboxes/folders
463   * @see     rcube_imap::list_mailboxes()
464   * @access  private
465   */
466  private function _list_mailboxes($root='', $filter='*')
467    {
468    $a_defaults = $a_out = array();
469   
470    // get cached folder list   
471    $a_mboxes = $this->get_cache('mailboxes');
472    if (is_array($a_mboxes))
473      return $a_mboxes;
474
475    // Give plugins a chance to provide a list of mailboxes
476    $data = rcmail::get_instance()->plugins->exec_hook('list_mailboxes',array('root'=>$root,'filter'=>$filter));
477    if (isset($data['folders'])) {
478        $a_folders = $data['folders'];
479    }
480    else{
481        // retrieve list of folders from IMAP server
482        $a_folders = iil_C_ListSubscribed($this->conn, $this->mod_mailbox($root), $filter);
483    }
484
485   
486    if (!is_array($a_folders) || !sizeof($a_folders))
487      $a_folders = array();
488
489    // write mailboxlist to cache
490    $this->update_cache('mailboxes', $a_folders);
491   
492    return $a_folders;
493    }
494
495
496  /**
497   * Get message count for a specific mailbox
498   *
499   * @param   string   Mailbox/folder name
500   * @param   string   Mode for count [ALL|UNSEEN|RECENT]
501   * @param   boolean  Force reading from server and update cache
502   * @return  int      Number of messages
503   * @access  public
504   */
505  function messagecount($mbox_name='', $mode='ALL', $force=FALSE)
506    {
507    $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
508    return $this->_messagecount($mailbox, $mode, $force);
509    }
510
511
512  /**
513   * Private method for getting nr of messages
514   *
515   * @access  private
516   * @see     rcube_imap::messagecount()
517   */
518  private function _messagecount($mailbox='', $mode='ALL', $force=FALSE)
519    {
520    $mode = strtoupper($mode);
521
522    if (empty($mailbox))
523      $mailbox = $this->mailbox;
524
525    // count search set
526    if ($this->search_string && $mailbox == $this->mailbox && ($mode == 'ALL' || $mode == 'THREADS') && !$force) {
527      if ($this->search_threads)
528        return $mode == 'ALL' ? count((array)$this->search_set['depth']) : count((array)$this->search_set['tree']);
529      else
530        return count((array)$this->search_set);
531      }
532   
533    $a_mailbox_cache = $this->get_cache('messagecount');
534   
535    // return cached value
536    if (!$force && is_array($a_mailbox_cache[$mailbox]) && isset($a_mailbox_cache[$mailbox][$mode]))
537      return $a_mailbox_cache[$mailbox][$mode];
538
539    if ($mode == 'THREADS')
540      $count = $this->_threadcount($mailbox);
541
542    // RECENT count is fetched a bit different
543    else if ($mode == 'RECENT')
544       $count = iil_C_CheckForRecent($this->conn, $mailbox);
545
546    // use SEARCH for message counting
547    else if ($this->skip_deleted)
548      {
549      $search_str = "ALL UNDELETED";
550
551      // get message count and store in cache
552      if ($mode == 'UNSEEN')
553        $search_str .= " UNSEEN";
554
555      // get message count using SEARCH
556      // not very performant but more precise (using UNDELETED)
557      $index = $this->_search_index($mailbox, $search_str);
558      $count = is_array($index) ? count($index) : 0;
559      }
560    else
561      {
562      if ($mode == 'UNSEEN')
563        $count = iil_C_CountUnseen($this->conn, $mailbox);
564      else
565        $count = iil_C_CountMessages($this->conn, $mailbox);
566      }
567
568    if (!is_array($a_mailbox_cache[$mailbox]))
569      $a_mailbox_cache[$mailbox] = array();
570     
571    $a_mailbox_cache[$mailbox][$mode] = (int)$count;
572
573    // write back to cache
574    $this->update_cache('messagecount', $a_mailbox_cache);
575
576    return (int)$count;
577    }
578
579
580  /**
581   * Private method for getting nr of threads
582   *
583   * @access  private
584   * @see     rcube_imap::messagecount()
585   */
586  private function _threadcount($mailbox)
587    {
588    if (!empty($this->cache['__threads']))
589      return count($this->cache['__threads']['tree']);
590   
591    list ($thread_tree, $msg_depth, $has_children) = $this->_fetch_threads($mailbox);
592
593//    $this->update_thread_cache($mailbox, $thread_tree, $msg_depth, $has_children);
594    return count($thread_tree); 
595    }
596
597
598  /**
599   * Public method for listing headers
600   * convert mailbox name with root dir first
601   *
602   * @param   string   Mailbox/folder name
603   * @param   int      Current page to list
604   * @param   string   Header field to sort by
605   * @param   string   Sort order [ASC|DESC]
606   * @param   boolean  Number of slice items to extract from result array
607   * @return  array    Indexed array with message header objects
608   * @access  public   
609   */
610  function list_headers($mbox_name='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
611    {
612    $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
613    return $this->_list_headers($mailbox, $page, $sort_field, $sort_order, false, $slice);
614    }
615
616
617  /**
618   * Private method for listing message headers
619   *
620   * @access  private
621   * @see     rcube_imap::list_headers
622   */
623  private function _list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $recursive=FALSE, $slice=0)
624    {
625    if (!strlen($mailbox))
626      return array();
627
628    // use saved message set
629    if ($this->search_string && $mailbox == $this->mailbox)
630      return $this->_list_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
631
632    if ($this->threading)
633      return $this->_list_thread_headers($mailbox, $page, $sort_field, $sort_order, $recursive, $slice);
634
635    $this->_set_sort_order($sort_field, $sort_order);
636
637    $page = $page ? $page : $this->list_page;
638    $cache_key = $mailbox.'.msg';
639    $cache_status = $this->check_cache_status($mailbox, $cache_key);
640
641    // cache is OK, we can get all messages from local cache
642    if ($cache_status>0)
643      {
644      $start_msg = ($page-1) * $this->page_size;
645      $a_msg_headers = $this->get_message_cache($cache_key, $start_msg, $start_msg+$this->page_size, $this->sort_field, $this->sort_order);
646      $result = array_values($a_msg_headers);
647      if ($slice)
648        $result = array_slice($result, -$slice, $slice);
649      return $result;
650      }
651    // cache is dirty, sync it
652    else if ($this->caching_enabled && $cache_status==-1 && !$recursive)
653      {
654      $this->sync_header_index($mailbox);
655      return $this->_list_headers($mailbox, $page, $this->sort_field, $this->sort_order, TRUE, $slice);
656      }
657
658    // retrieve headers from IMAP
659    $a_msg_headers = array();
660
661    // use message index sort as default sorting (for better performance)
662    if (!$this->sort_field)
663      {
664        if ($this->skip_deleted) {
665          // @TODO: this could be cached
666          if ($msg_index = $this->_search_index($mailbox, 'ALL UNDELETED')) {
667            $max = max($msg_index);
668            list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
669            $msg_index = array_slice($msg_index, $begin, $end-$begin);
670            }
671        } else if ($max = iil_C_CountMessages($this->conn, $mailbox)) {
672          list($begin, $end) = $this->_get_message_range($max, $page);
673          $msg_index = range($begin+1, $end);
674        } else
675          $msg_index = array();
676
677        if ($slice)
678          $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
679
680        // fetch reqested headers from server
681        if ($msg_index)
682          $this->_fetch_headers($mailbox, join(",", $msg_index), $a_msg_headers, $cache_key);
683      }
684    // use SORT command
685    else if ($this->get_capability('sort') && ($msg_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')))
686      {
687      list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
688      $max = max($msg_index);
689      $msg_index = array_slice($msg_index, $begin, $end-$begin);
690
691      if ($slice)
692        $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
693
694      // fetch reqested headers from server
695      $this->_fetch_headers($mailbox, join(',', $msg_index), $a_msg_headers, $cache_key);
696      }
697    // fetch specified header for all messages and sort
698    else if ($a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, "1:*", $this->sort_field, $this->skip_deleted))
699      {
700      asort($a_index); // ASC
701      $msg_index = array_keys($a_index);
702      $max = max($msg_index);
703      list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
704      $msg_index = array_slice($msg_index, $begin, $end-$begin);
705
706      if ($slice)
707        $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
708
709      // fetch reqested headers from server
710      $this->_fetch_headers($mailbox, join(",", $msg_index), $a_msg_headers, $cache_key);
711      }
712
713    // delete cached messages with a higher index than $max+1
714    // Changed $max to $max+1 to fix this bug : #1484295
715    $this->clear_message_cache($cache_key, $max + 1);
716
717    // kick child process to sync cache
718    // ...
719
720    // return empty array if no messages found
721    if (!is_array($a_msg_headers) || empty($a_msg_headers))
722      return array();
723   
724    // use this class for message sorting
725    $sorter = new rcube_header_sorter();
726    $sorter->set_sequence_numbers($msg_index);
727    $sorter->sort_headers($a_msg_headers);
728
729    if ($this->sort_order == 'DESC')
730      $a_msg_headers = array_reverse($a_msg_headers);       
731
732    return array_values($a_msg_headers);
733    }
734
735
736  /**
737   * Private method for listing message headers using threads
738   *
739   * @access  private
740   * @see     rcube_imap::list_headers
741   */
742  private function _list_thread_headers($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $recursive=FALSE, $slice=0)
743    {
744    $this->_set_sort_order($sort_field, $sort_order);
745
746    $page = $page ? $page : $this->list_page;
747//    $cache_key = $mailbox.'.msg';
748//    $cache_status = $this->check_cache_status($mailbox, $cache_key);
749
750    // get all threads (default sort order)
751    list ($thread_tree, $msg_depth, $has_children) = $this->_fetch_threads($mailbox);
752
753    if (empty($thread_tree))
754      return array();
755
756    $msg_index = $this->_sort_threads($mailbox, $thread_tree);
757
758    return $this->_fetch_thread_headers($mailbox, $thread_tree, $msg_depth, $has_children,
759        $msg_index, $page, $lice);
760    }
761
762
763  /**
764   * Private method for fetching threads data
765   *
766   * @param   string   Mailbox/folder name
767   * @return  array    Array with thread data
768   * @access  private
769   */
770  private function _fetch_threads($mailbox)
771    {
772    if (empty($this->cache['__threads'])) {
773      // get all threads
774      list ($thread_tree, $msg_depth, $has_children) = iil_C_Thread($this->conn,
775        $mailbox, $this->threading, $this->skip_deleted ? 'UNDELETED' : '');
776   
777      // add to internal (fast) cache
778      $this->cache['__threads'] = array();
779      $this->cache['__threads']['tree'] = $thread_tree;
780      $this->cache['__threads']['depth'] = $msg_depth;
781      $this->cache['__threads']['has_children'] = $has_children;
782      }
783
784    return array(
785      $this->cache['__threads']['tree'],
786      $this->cache['__threads']['depth'],
787      $this->cache['__threads']['has_children'],
788      );
789    }
790
791
792  /**
793   * Private method for fetching threaded messages headers
794   *
795   * @access  private
796   */
797  private function _fetch_thread_headers($mailbox, $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0)
798    {
799    $cache_key = $mailbox.'.msg';
800    // now get IDs for current page
801    $max = max($msg_index);
802    list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
803    $msg_index = array_slice($msg_index, $begin, $end-$begin);
804
805    if ($slice)
806      $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
807
808    if ($this->sort_order == 'DESC')
809      $msg_index = array_reverse($msg_index);
810
811    // flatten threads array
812    // @TODO: fetch children only in expanded mode
813    $all_ids = array();
814    foreach($msg_index as $root) {
815      $all_ids[] = $root;
816      if (!empty($thread_tree[$root]))
817        $all_ids = array_merge($all_ids, array_keys_recursive($thread_tree[$root]));
818      }
819
820    // fetch reqested headers from server
821    $this->_fetch_headers($mailbox, $all_ids, $a_msg_headers, $cache_key);
822
823    // return empty array if no messages found
824    if (!is_array($a_msg_headers) || empty($a_msg_headers))
825      return array();
826   
827    // use this class for message sorting
828    $sorter = new rcube_header_sorter();
829    $sorter->set_sequence_numbers($all_ids);
830    $sorter->sort_headers($a_msg_headers);
831
832    // Set depth, has_children and unread_children fields in headers
833    $this->_set_thread_flags($a_msg_headers, $msg_depth, $has_children);
834
835    return array_values($a_msg_headers);
836    }
837
838
839  /**
840   * Private method for setting threaded messages flags:
841   * depth, has_children and unread_children
842   *
843   * @param  array   Reference to headers array indexed by message ID
844   * @param  array   Array of messages depth indexed by message ID
845   * @param  array   Array of messages children flags indexed by message ID
846   * @return array   Message headers array indexed by message ID
847   * @access private
848   */
849  private function _set_thread_flags(&$headers, $msg_depth, $msg_children)
850    {
851    $parents = array();
852
853    foreach ($headers as $idx => $header) {
854      $id = $header->id;
855      $depth = $msg_depth[$id];
856      $parents = array_slice($parents, 0, $depth);
857
858      if (!empty($parents)) {
859        $headers[$idx]->parent_uid = end($parents);
860        if (!$header->seen)
861          $headers[$parents[0]]->unread_children++;
862        }
863      array_push($parents, $header->uid);
864
865      $headers[$idx]->depth = $depth;
866      $headers[$idx]->has_children = $msg_children[$id];
867      }
868    }
869
870
871  /**
872   * Private method for listing a set of message headers (search results)
873   *
874   * @param   string   Mailbox/folder name
875   * @param   int      Current page to list
876   * @param   string   Header field to sort by
877   * @param   string   Sort order [ASC|DESC]
878   * @param   boolean  Number of slice items to extract from result array
879   * @return  array    Indexed array with message header objects
880   * @access  private
881   * @see     rcube_imap::list_header_set()
882   */
883  private function _list_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
884    {
885    if (!strlen($mailbox) || empty($this->search_set))
886      return array();
887
888    // use saved messages from searching
889    if ($this->threading)
890      return $this->_list_thread_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
891
892    $msgs = $this->search_set;
893    $a_msg_headers = array();
894    $page = $page ? $page : $this->list_page;
895    $start_msg = ($page-1) * $this->page_size;
896
897    $this->_set_sort_order($sort_field, $sort_order);
898
899    // quickest method (default sorting)
900    if (!$this->search_sort_field && !$this->sort_field)
901      {
902      if ($sort_order == 'DESC')
903        $msgs = array_reverse($msgs);
904
905      // get messages uids for one page
906      $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
907
908      if ($slice)
909        $msgs = array_slice($msgs, -$slice, $slice);
910
911      // fetch headers
912      $this->_fetch_headers($mailbox, join(',',$msgs), $a_msg_headers, NULL);
913
914      // I didn't found in RFC that FETCH always returns messages sorted by index
915      $sorter = new rcube_header_sorter();
916      $sorter->set_sequence_numbers($msgs);
917      $sorter->sort_headers($a_msg_headers);
918
919      return array_values($a_msg_headers);
920      }
921
922    // sorted messages, so we can first slice array and then fetch only wanted headers
923    if ($this->get_capability('sort')) // SORT searching result
924      {
925      // reset search set if sorting field has been changed
926      if ($this->sort_field && $this->search_sort_field != $this->sort_field)
927        $msgs = $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
928
929      // return empty array if no messages found
930      if (empty($msgs))
931        return array();
932
933      if ($sort_order == 'DESC')
934        $msgs = array_reverse($msgs);
935
936      // get messages uids for one page
937      $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
938
939      if ($slice)
940        $msgs = array_slice($msgs, -$slice, $slice);
941
942      // fetch headers
943      $this->_fetch_headers($mailbox, join(',',$msgs), $a_msg_headers, NULL);
944
945      $sorter = new rcube_header_sorter();
946      $sorter->set_sequence_numbers($msgs);
947      $sorter->sort_headers($a_msg_headers);
948
949      return array_values($a_msg_headers);
950      }
951    else { // SEARCH result, need sorting
952      $cnt = count($msgs);
953      // 300: experimantal value for best result
954      if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) {
955        // use memory less expensive (and quick) method for big result set
956        $a_index = $this->message_index('', $this->sort_field, $this->sort_order);
957        // get messages uids for one page...
958        $msgs = array_slice($a_index, $start_msg, min($cnt-$start_msg, $this->page_size));
959        if ($slice)
960          $msgs = array_slice($msgs, -$slice, $slice);
961        // ...and fetch headers
962        $this->_fetch_headers($mailbox, join(',', $msgs), $a_msg_headers, NULL);
963
964        // return empty array if no messages found
965        if (!is_array($a_msg_headers) || empty($a_msg_headers))
966          return array();
967
968        $sorter = new rcube_header_sorter();
969        $sorter->set_sequence_numbers($msgs);
970        $sorter->sort_headers($a_msg_headers);
971
972        return array_values($a_msg_headers);
973        }
974      else {
975        // for small result set we can fetch all messages headers
976        $this->_fetch_headers($mailbox, join(',', $msgs), $a_msg_headers, NULL);
977   
978        // return empty array if no messages found
979        if (!is_array($a_msg_headers) || empty($a_msg_headers))
980          return array();
981
982        // if not already sorted
983        $a_msg_headers = iil_SortHeaders($a_msg_headers, $this->sort_field, $this->sort_order);
984     
985        // only return the requested part of the set
986        $a_msg_headers = array_slice(array_values($a_msg_headers), $start_msg, min($cnt-$start_msg, $this->page_size));
987        if ($slice)
988          $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
989
990        return $a_msg_headers;
991        }
992      }
993    }
994
995
996  /**
997   * Private method for listing a set of threaded message headers (search results)
998   *
999   * @param   string   Mailbox/folder name
1000   * @param   int      Current page to list
1001   * @param   string   Header field to sort by
1002   * @param   string   Sort order [ASC|DESC]
1003   * @param   boolean  Number of slice items to extract from result array
1004   * @return  array    Indexed array with message header objects
1005   * @access  private
1006   * @see     rcube_imap::list_header_set()
1007   */
1008  private function _list_thread_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
1009    {
1010    $thread_tree = $this->search_set['tree'];
1011    $msg_depth = $this->search_set['depth'];
1012    $has_children = $this->search_set['children'];
1013    $a_msg_headers = array();
1014
1015    $page = $page ? $page : $this->list_page;
1016    $start_msg = ($page-1) * $this->page_size;
1017
1018    $this->_set_sort_order($sort_field, $sort_order);
1019
1020    $msg_index = $this->_sort_threads($mailbox, $thread_tree, array_keys($msg_depth));
1021
1022    return $this->_fetch_thread_headers($mailbox, $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0);
1023    }
1024
1025
1026  /**
1027   * Helper function to get first and last index of the requested set
1028   *
1029   * @param  int     message count
1030   * @param  mixed   page number to show, or string 'all'
1031   * @return array   array with two values: first index, last index
1032   * @access private
1033   */
1034  private function _get_message_range($max, $page)
1035    {
1036    $start_msg = ($page-1) * $this->page_size;
1037   
1038    if ($page=='all')
1039      {
1040      $begin = 0;
1041      $end = $max;
1042      }
1043    else if ($this->sort_order=='DESC')
1044      {
1045      $begin = $max - $this->page_size - $start_msg;
1046      $end =   $max - $start_msg;
1047      }
1048    else
1049      {
1050      $begin = $start_msg;
1051      $end   = $start_msg + $this->page_size;
1052      }
1053
1054    if ($begin < 0) $begin = 0;
1055    if ($end < 0) $end = $max;
1056    if ($end > $max) $end = $max;
1057   
1058    return array($begin, $end);
1059    }
1060   
1061
1062  /**
1063   * Fetches message headers
1064   * Used for loop
1065   *
1066   * @param  string  Mailbox name
1067   * @param  string  Message index to fetch
1068   * @param  array   Reference to message headers array
1069   * @param  array   Array with cache index
1070   * @return int     Messages count
1071   * @access private
1072   */
1073  private function _fetch_headers($mailbox, $msgs, &$a_msg_headers, $cache_key)
1074    {
1075    // fetch reqested headers from server
1076    $a_header_index = iil_C_FetchHeaders($this->conn, $mailbox, $msgs, false, false, $this->fetch_add_headers);
1077
1078    if (!empty($a_header_index))
1079      {
1080      // cache is incomplete
1081      $cache_index = $this->get_message_cache_index($cache_key);
1082
1083      foreach ($a_header_index as $i => $headers) {
1084        if ($this->caching_enabled && $cache_index[$headers->id] != $headers->uid) {
1085          // prevent index duplicates
1086          if ($cache_index[$headers->id]) {
1087            $this->remove_message_cache($cache_key, $headers->id, true);
1088            unset($cache_index[$headers->id]);
1089            }
1090          // add message to cache
1091          $this->add_message_cache($cache_key, $headers->id, $headers, NULL,
1092            !in_array($headers->uid, $cache_index));
1093          }
1094
1095        $a_msg_headers[$headers->uid] = $headers;
1096        }
1097      }
1098
1099    return count($a_msg_headers);
1100    }
1101   
1102 
1103  /**
1104   * Return sorted array of message IDs (not UIDs)
1105   *
1106   * @param string Mailbox to get index from
1107   * @param string Sort column
1108   * @param string Sort order [ASC, DESC]
1109   * @return array Indexed array with message ids
1110   */
1111  function message_index($mbox_name='', $sort_field=NULL, $sort_order=NULL)
1112    {
1113    $this->_set_sort_order($sort_field, $sort_order);
1114
1115    $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
1116    $key = "{$mailbox}:{$this->sort_field}:{$this->sort_order}:{$this->search_string}.msgi";
1117
1118    // we have a saved search result, get index from there
1119    if (!isset($this->cache[$key]) && $this->search_string && $mailbox == $this->mailbox)
1120    {
1121      $this->cache[$key] = array();
1122     
1123      // use message index sort as default sorting
1124      if (!$this->sort_field)
1125        {
1126        $msgs = $this->search_set;
1127       
1128        if ($this->search_sort_field != 'date')
1129          sort($msgs);
1130       
1131        if ($this->sort_order == 'DESC')
1132          $this->cache[$key] = array_reverse($msgs);
1133        else
1134          $this->cache[$key] = $msgs;
1135        }
1136      // sort with SORT command
1137      else if ($this->get_capability('sort'))
1138        {
1139        if ($this->sort_field && $this->search_sort_field != $this->sort_field)
1140          $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
1141
1142        if ($this->sort_order == 'DESC')
1143          $this->cache[$key] = array_reverse($this->search_set);
1144        else
1145          $this->cache[$key] = $this->search_set;
1146        }
1147      else
1148        {
1149        $a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, join(',', $this->search_set), $this->sort_field, $this->skip_deleted);
1150
1151        if ($this->sort_order=="ASC")
1152          asort($a_index);
1153        else if ($this->sort_order=="DESC")
1154          arsort($a_index);
1155
1156        $this->cache[$key] = array_keys($a_index);
1157        }
1158    }
1159
1160    // have stored it in RAM
1161    if (isset($this->cache[$key]))
1162      return $this->cache[$key];
1163
1164    // check local cache
1165    $cache_key = $mailbox.'.msg';
1166    $cache_status = $this->check_cache_status($mailbox, $cache_key);
1167
1168    // cache is OK
1169    if ($cache_status>0)
1170      {
1171      $a_index = $this->get_message_cache_index($cache_key, TRUE, $this->sort_field, $this->sort_order);
1172      return array_keys($a_index);
1173      }
1174
1175    // use message index sort as default sorting
1176    if (!$this->sort_field)
1177      {
1178      if ($this->skip_deleted) {
1179        $a_index = $this->_search_index($mailbox, 'ALL');
1180      } else if ($max = $this->_messagecount($mailbox)) {
1181        $a_index = range(1, $max);
1182      }
1183
1184      if ($this->sort_order == 'DESC')
1185        $a_index = array_reverse($a_index);
1186
1187      $this->cache[$key] = $a_index;
1188      }
1189    // fetch complete message index
1190    else if ($this->get_capability('sort') && ($a_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')))
1191      {
1192      if ($this->sort_order == 'DESC')
1193        $a_index = array_reverse($a_index);
1194
1195      $this->cache[$key] = $a_index;
1196      }
1197    else
1198      {
1199      $a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, "1:*", $this->sort_field, $this->skip_deleted);
1200
1201      if ($this->sort_order=="ASC")
1202        asort($a_index);
1203      else if ($this->sort_order=="DESC")
1204        arsort($a_index);
1205       
1206      $this->cache[$key] = array_keys($a_index);
1207      }
1208
1209    return $this->cache[$key];
1210    }
1211
1212
1213  /**
1214   * @access private
1215   */
1216  function sync_header_index($mailbox)
1217    {
1218    $cache_key = $mailbox.'.msg';
1219    $cache_index = $this->get_message_cache_index($cache_key);
1220
1221    // fetch complete message index
1222    $a_message_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, "1:*", 'UID', $this->skip_deleted);
1223   
1224    if ($a_message_index === false)
1225      return false;
1226       
1227    foreach ($a_message_index as $id => $uid)
1228      {
1229      // message in cache at correct position
1230      if ($cache_index[$id] == $uid)
1231        {
1232        unset($cache_index[$id]);
1233        continue;
1234        }
1235       
1236      // message in cache but in wrong position
1237      if (in_array((string)$uid, $cache_index, TRUE))
1238        {
1239        unset($cache_index[$id]);
1240        }
1241     
1242      // other message at this position
1243      if (isset($cache_index[$id]))
1244        {
1245        $for_remove[] = $cache_index[$id];
1246        unset($cache_index[$id]);
1247        }
1248       
1249        $for_update[] = $id;
1250      }
1251
1252    // clear messages at wrong positions and those deleted that are still in cache_index     
1253    if (!empty($for_remove))
1254      $cache_index = array_merge($cache_index, $for_remove);
1255   
1256    if (!empty($cache_index))
1257      $this->remove_message_cache($cache_key, $cache_index);
1258
1259    // fetch complete headers and add to cache
1260    if (!empty($for_update)) {
1261      if ($headers = iil_C_FetchHeader($this->conn, $mailbox, join(',', $for_update), false, $this->fetch_add_headers))
1262        foreach ($headers as $header)
1263          $this->add_message_cache($cache_key, $header->id, $header, NULL,
1264                in_array($header->uid, (array)$for_remove));
1265      }
1266    }
1267
1268
1269  /**
1270   * Invoke search request to IMAP server
1271   *
1272   * @param  string  mailbox name to search in
1273   * @param  string  search string
1274   * @param  string  search string charset
1275   * @param  string  header field to sort by
1276   * @return array   search results as list of message ids
1277   * @access public
1278   */
1279  function search($mbox_name='', $str=NULL, $charset=NULL, $sort_field=NULL)
1280    {
1281    if (!$str)
1282      return false;
1283   
1284    $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
1285
1286    $results = $this->_search_index($mailbox, $str, $charset, $sort_field);
1287
1288    // try search with US-ASCII charset (should be supported by server)
1289    // only if UTF-8 search is not supported
1290    if (empty($results) && !is_array($results) && !empty($charset) && $charset != 'US-ASCII')
1291      {
1292        // convert strings to US_ASCII
1293        if(preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE))
1294          {
1295          $last = 0; $res = '';
1296          foreach($matches[1] as $m)
1297            {
1298            $string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n
1299            $string = substr($str, $string_offset - 1, $m[0]);
1300            $string = rcube_charset_convert($string, $charset, 'US-ASCII');
1301            if (!$string) continue;
1302            $res .= sprintf("%s{%d}\r\n%s", substr($str, $last, $m[1] - $last - 1), strlen($string), $string);
1303            $last = $m[0] + $string_offset - 1;
1304            }
1305            if ($last < strlen($str))
1306              $res .= substr($str, $last, strlen($str)-$last);
1307          }
1308        else // strings for conversion not found
1309          $res = $str;
1310         
1311        $results = $this->search($mbox_name, $res, NULL, $sort_field);
1312      }
1313
1314    $this->set_search_set($str, $results, $charset, $sort_field, (bool) $this->threading);
1315
1316    return $results;
1317    }
1318
1319
1320  /**
1321   * Private search method
1322   *
1323   * @return array   search results as list of message ids
1324   * @access private
1325   * @see rcube_imap::search()
1326   */
1327  private function _search_index($mailbox, $criteria='ALL', $charset=NULL, $sort_field=NULL)
1328    {
1329    $orig_criteria = $criteria;
1330
1331    if ($this->skip_deleted && !preg_match('/UNDELETED/', $criteria))
1332      $criteria = 'UNDELETED '.$criteria;
1333
1334    if ($this->threading) {
1335      list ($thread_tree, $msg_depth, $has_children) = iil_C_Thread($this->conn,
1336            $mailbox, $this->threading, $criteria, $charset);
1337
1338      $a_messages = array(
1339        'tree'  => $thread_tree,
1340        'depth' => $msg_depth,
1341        'children' => $has_children
1342        );
1343      }
1344    else if ($sort_field && $this->get_capability('sort')) {
1345      $charset = $charset ? $charset : $this->default_charset;
1346      $a_messages = iil_C_Sort($this->conn, $mailbox, $sort_field, $criteria, FALSE, $charset);
1347      }
1348    else {
1349      if ($orig_criteria == 'ALL') {
1350        $max = $this->_messagecount($mailbox);
1351        $a_messages = $max ? range(1, $max) : array();
1352        }
1353      else {
1354        $a_messages = iil_C_Search($this->conn, $mailbox, ($charset ? "CHARSET $charset " : '') . $criteria);
1355   
1356        // I didn't found that SEARCH always returns sorted IDs
1357        if (!$this->sort_field)
1358          sort($a_messages);
1359        }
1360      }
1361    // update messagecount cache ?
1362//    $a_mailbox_cache = get_cache('messagecount');
1363//    $a_mailbox_cache[$mailbox][$criteria] = sizeof($a_messages);
1364//    $this->update_cache('messagecount', $a_mailbox_cache);
1365       
1366    return $a_messages;
1367    }
1368   
1369 
1370  /**
1371   * Sort thread
1372   *
1373   * @param  array Unsorted thread tree (iil_C_Thread() result)
1374   * @param  array Message IDs if we know what we need (e.g. search result)
1375   * @return array Sorted roots IDs
1376   * @access private
1377   */
1378  private function _sort_threads($mailbox, $thread_tree, $ids=NULL)
1379    {
1380    // THREAD=ORDEREDSUBJECT:   sorting by sent date of root message
1381    // THREAD=REFERENCES:       sorting by sent date of root message
1382    // THREAD=REFS:             sorting by the most recent date in each thread
1383    // default sorting
1384    if (!$this->sort_field || ($this->sort_field == 'date' && $this->threading == 'REFS')) {
1385        return array_keys($thread_tree);
1386      }
1387    // here we'll implement REFS sorting, for performance reason
1388    else { // ($sort_field == 'date' && $this->threading != 'REFS')
1389      // use SORT command
1390      if ($this->get_capability('sort')) {
1391        $a_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field,
1392            !empty($ids) ? $ids : ($this->skip_deleted ? 'UNDELETED' : ''));
1393        }
1394      else {
1395        // fetch specified headers for all messages and sort them
1396        $a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, !empty($ids) ? $ids : "1:*",
1397            $this->sort_field, $this->skip_deleted);
1398        asort($a_index); // ASC
1399        $a_index = array_values($a_index);
1400        }
1401
1402        return $this->_sort_thread_refs($thread_tree, $a_index);
1403      }
1404/*
1405    // other sorting, we'll sort roots only
1406    else {
1407      // use SORT command for root messages sorting
1408      if ($this->get_capability('sort')) {
1409        $msg_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, array_keys($thread_tree));
1410        }
1411      else {
1412        // fetch specified headers for all root messages and sort
1413        $a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox,
1414            array_keys($thread_tree), $this->sort_field, $this->skip_deleted);
1415        asort($a_index); // ASC
1416        $msg_index = array_keys($a_index);
1417        }
1418      }
1419*/
1420    return array();
1421    }
1422
1423
1424  /**
1425   * THREAD=REFS sorting implementation
1426   *
1427   * @param  array   Thread tree array (message identifiers as keys)
1428   * @param  array   Array of sorted message identifiers
1429   * @return array   Array of sorted roots messages
1430   * @access private
1431   */
1432  private function _sort_thread_refs($tree, $index)
1433    {
1434    if (empty($tree))
1435      return array();
1436   
1437    $index = array_combine(array_values($index), $index);
1438
1439    // assign roots
1440    foreach ($tree as $idx => $val) {
1441      $index[$idx] = $idx;
1442      if (!empty($val)) {
1443        $idx_arr = array_keys_recursive($tree[$idx]);
1444        foreach ($idx_arr as $subidx)
1445          $index[$subidx] = $idx;
1446        }
1447      }
1448
1449    $index = array_values($index); 
1450
1451    // create sorted array of roots
1452    $msg_index = array();
1453    if ($this->sort_order != 'DESC') {
1454      foreach ($index as $idx)
1455        if (!isset($msg_index[$idx]))
1456          $msg_index[$idx] = $idx;
1457      $msg_index = array_values($msg_index);
1458      }
1459    else {
1460      for ($x=count($index)-1; $x>=0; $x--)
1461        if (!isset($msg_index[$index[$x]]))
1462          $msg_index[$index[$x]] = $index[$x];
1463      $msg_index = array_reverse($msg_index);
1464      }
1465
1466    return $msg_index;
1467    }
1468
1469
1470  /**
1471   * Refresh saved search set
1472   *
1473   * @return array Current search set
1474   */
1475  function refresh_search()
1476    {
1477    if (!empty($this->search_string))
1478      $this->search_set = $this->search('', $this->search_string, $this->search_charset,
1479            $this->search_sort_field, $this->search_threads);
1480     
1481    return $this->get_search_set();
1482    }
1483 
1484 
1485  /**
1486   * Check if the given message ID is part of the current search set
1487   *
1488   * @return boolean True on match or if no search request is stored
1489   */
1490  function in_searchset($msgid)
1491  {
1492    if (!empty($this->search_string))
1493      return in_array("$msgid", (array)$this->search_set, true);
1494    else
1495      return true;
1496  }
1497
1498
1499  /**
1500   * Return message headers object of a specific message
1501   *
1502   * @param int     Message ID
1503   * @param string  Mailbox to read from
1504   * @param boolean True if $id is the message UID
1505   * @param boolean True if we need also BODYSTRUCTURE in headers
1506   * @return object Message headers representation
1507   */
1508  function get_headers($id, $mbox_name=NULL, $is_uid=TRUE, $bodystr=FALSE)
1509    {
1510    $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
1511    $uid = $is_uid ? $id : $this->_id2uid($id);
1512
1513    // get cached headers
1514    if ($uid && ($headers = &$this->get_cached_message($mailbox.'.msg', $uid)))
1515      return $headers;
1516
1517    $headers = iil_C_FetchHeader($this->conn, $mailbox, $id, $is_uid, $bodystr, $this->fetch_add_headers);
1518
1519    // write headers cache
1520    if ($headers)
1521      {
1522      if ($headers->uid && $headers->id)
1523        $this->uid_id_map[$mailbox][$headers->uid] = $headers->id;
1524
1525      $this->add_message_cache($mailbox.'.msg', $headers->id, $headers, NULL, true);
1526      }
1527
1528    return $headers;
1529    }
1530
1531
1532  /**
1533   * Fetch body structure from the IMAP server and build
1534   * an object structure similar to the one generated by PEAR::Mail_mimeDecode
1535   *
1536   * @param int Message UID to fetch
1537   * @param string Message BODYSTRUCTURE string (optional)
1538   * @return object rcube_message_part Message part tree or False on failure
1539   */
1540  function &get_structure($uid, $structure_str='')
1541    {
1542    $cache_key = $this->mailbox.'.msg';
1543    $headers = &$this->get_cached_message($cache_key, $uid);
1544
1545    // return cached message structure
1546    if (is_object($headers) && is_object($headers->structure)) {
1547      return $headers->structure;
1548    }
1549
1550    if (!$structure_str)
1551      $structure_str = iil_C_FetchStructureString($this->conn, $this->mailbox, $uid, true);
1552    $structure = iml_GetRawStructureArray($structure_str);
1553    $struct = false;
1554
1555    // parse structure and add headers
1556    if (!empty($structure))
1557      {
1558      $headers = $this->get_headers($uid);
1559      $this->_msg_id = $headers->id;
1560
1561      // set message charset from message headers
1562      if ($headers->charset)
1563        $this->struct_charset = $headers->charset;
1564      else
1565        $this->struct_charset = $this->_structure_charset($structure);
1566
1567      // Here we can recognize malformed BODYSTRUCTURE and
1568      // 1. [@TODO] parse the message in other way to create our own message structure
1569      // 2. or just show the raw message body.
1570      // Example of structure for malformed MIME message:
1571      // ("text" "plain" ("charset" "us-ascii") NIL NIL "7bit" 2154 70 NIL NIL NIL)
1572      if ($headers->ctype && $headers->ctype != 'text/plain'
1573          && $structure[0] == 'text' && $structure[1] == 'plain') {
1574        return false; 
1575        }
1576
1577      $struct = &$this->_structure_part($structure);
1578      $struct->headers = get_object_vars($headers);
1579
1580      // don't trust given content-type
1581      if (empty($struct->parts) && !empty($struct->headers['ctype']))
1582        {
1583        $struct->mime_id = '1';
1584        $struct->mimetype = strtolower($struct->headers['ctype']);
1585        list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype);
1586        }
1587
1588      // write structure to cache
1589      if ($this->caching_enabled)
1590        $this->add_message_cache($cache_key, $this->_msg_id, $headers, $struct);
1591      }
1592
1593    return $struct;
1594    }
1595
1596 
1597  /**
1598   * Build message part object
1599   *
1600   * @access private
1601   */
1602  function &_structure_part($part, $count=0, $parent='', $raw_headers=null)
1603    {
1604    $struct = new rcube_message_part;
1605    $struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
1606
1607    // multipart
1608    if (is_array($part[0]))
1609      {
1610      $struct->ctype_primary = 'multipart';
1611     
1612      // find first non-array entry
1613      for ($i=1; $i<count($part); $i++)
1614        if (!is_array($part[$i]))
1615          {
1616          $struct->ctype_secondary = strtolower($part[$i]);
1617          break;
1618          }
1619         
1620      $struct->mimetype = 'multipart/'.$struct->ctype_secondary;
1621
1622      // build parts list for headers pre-fetching
1623      for ($i=0, $count=0; $i<count($part); $i++)
1624        if (is_array($part[$i]) && count($part[$i]) > 3)
1625          // fetch message headers if message/rfc822 or named part (could contain Content-Location header)
1626          if (strtolower($part[$i][0]) == 'message' ||
1627            (in_array('name', (array)$part[$i][2]) && (empty($part[$i][3]) || $part[$i][3]=='NIL'))) {
1628            $part_headers[] = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
1629            }
1630
1631      // pre-fetch headers of all parts (in one command for better performance)
1632      if ($part_headers)
1633        $part_headers = iil_C_FetchMIMEHeaders($this->conn, $this->mailbox, $this->_msg_id, $part_headers);
1634
1635      $struct->parts = array();
1636      for ($i=0, $count=0; $i<count($part); $i++)
1637        if (is_array($part[$i]) && count($part[$i]) > 3) {
1638          $struct->parts[] = $this->_structure_part($part[$i], ++$count, $struct->mime_id,
1639                $part_headers[$struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1]);
1640        }
1641
1642      return $struct;
1643      }
1644   
1645   
1646    // regular part
1647    $struct->ctype_primary = strtolower($part[0]);
1648    $struct->ctype_secondary = strtolower($part[1]);
1649    $struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary;
1650
1651    // read content type parameters
1652    if (is_array($part[2]))
1653      {
1654      $struct->ctype_parameters = array();
1655      for ($i=0; $i<count($part[2]); $i+=2)
1656        $struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i+1];
1657       
1658      if (isset($struct->ctype_parameters['charset']))
1659        $struct->charset = $struct->ctype_parameters['charset'];
1660      }
1661   
1662    // read content encoding
1663    if (!empty($part[5]) && $part[5]!='NIL')
1664      {
1665      $struct->encoding = strtolower($part[5]);
1666      $struct->headers['content-transfer-encoding'] = $struct->encoding;
1667      }
1668   
1669    // get part size
1670    if (!empty($part[6]) && $part[6]!='NIL')
1671      $struct->size = intval($part[6]);
1672
1673    // read part disposition
1674    $di = count($part) - 2;
1675    if ((is_array($part[$di]) && count($part[$di]) == 2 && is_array($part[$di][1])) ||
1676        (is_array($part[--$di]) && count($part[$di]) == 2))
1677      {
1678      $struct->disposition = strtolower($part[$di][0]);
1679
1680      if (is_array($part[$di][1]))
1681        for ($n=0; $n<count($part[$di][1]); $n+=2)
1682          $struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1];
1683      }
1684     
1685    // get child parts
1686    if (is_array($part[8]) && $di != 8)
1687      {
1688      $struct->parts = array();
1689      for ($i=0, $count=0; $i<count($part[8]); $i++)
1690        if (is_array($part[8][$i]) && count($part[8][$i]) > 5)
1691          $struct->parts[] = $this->_structure_part($part[8][$i], ++$count, $struct->mime_id);
1692      }
1693
1694    // get part ID
1695    if (!empty($part[3]) && $part[3]!='NIL')
1696      {
1697      $struct->content_id = $part[3];
1698      $struct->headers['content-id'] = $part[3];
1699   
1700      if (empty($struct->disposition))
1701        $struct->disposition = 'inline';
1702      }
1703   
1704    // fetch message headers if message/rfc822 or named part (could contain Content-Location header)
1705    if ($struct->ctype_primary == 'message' || ($struct->ctype_parameters['name'] && !$struct->content_id)) {
1706      if (empty($raw_headers))
1707        $raw_headers = iil_C_FetchPartHeader($this->conn, $this->mailbox, $this->_msg_id, false, $struct->mime_id);
1708      $struct->headers = $this->_parse_headers($raw_headers) + $struct->headers;
1709    }
1710
1711    if ($struct->ctype_primary=='message') {
1712      if (is_array($part[8]) && empty($struct->parts))
1713        $struct->parts[] = $this->_structure_part($part[8], ++$count, $struct->mime_id);
1714    }
1715
1716    // normalize filename property
1717    $this->_set_part_filename($struct, $raw_headers);
1718
1719    return $struct;
1720    }
1721   
1722
1723  /**
1724   * Set attachment filename from message part structure
1725   *
1726   * @access private
1727   * @param  object rcube_message_part Part object
1728   * @param  string Part's raw headers
1729   */
1730  private function _set_part_filename(&$part, $headers=null)
1731    {
1732    if (!empty($part->d_parameters['filename']))
1733      $filename_mime = $part->d_parameters['filename'];
1734    else if (!empty($part->d_parameters['filename*']))
1735      $filename_encoded = $part->d_parameters['filename*'];
1736    else if (!empty($part->ctype_parameters['name*']))
1737      $filename_encoded = $part->ctype_parameters['name*'];
1738    // RFC2231 value continuations
1739    // TODO: this should be rewrited to support RFC2231 4.1 combinations
1740    else if (!empty($part->d_parameters['filename*0'])) {
1741      $i = 0;
1742      while (isset($part->d_parameters['filename*'.$i])) {
1743        $filename_mime .= $part->d_parameters['filename*'.$i];
1744        $i++;
1745      }
1746      // some servers (eg. dovecot-1.x) have no support for parameter value continuations
1747      // we must fetch and parse headers "manually"
1748      if ($i<2) {
1749        if (!$headers)
1750          $headers = iil_C_FetchPartHeader($this->conn, $this->mailbox, $this->_msg_id, false, $part->mime_id);
1751        $filename_mime = '';
1752        $i = 0;
1753        while (preg_match('/filename\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
1754          $filename_mime .= $matches[1];
1755          $i++;
1756        }
1757      }
1758    }
1759    else if (!empty($part->d_parameters['filename*0*'])) {
1760      $i = 0;
1761      while (isset($part->d_parameters['filename*'.$i.'*'])) {
1762        $filename_encoded .= $part->d_parameters['filename*'.$i.'*'];
1763        $i++;
1764      }
1765      if ($i<2) {
1766        if (!$headers)
1767          $headers = iil_C_FetchPartHeader($this->conn, $this->mailbox, $this->_msg_id, false, $part->mime_id);
1768        $filename_encoded = '';
1769        $i = 0; $matches = array();
1770        while (preg_match('/filename\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
1771          $filename_encoded .= $matches[1];
1772          $i++;
1773        }
1774      }
1775    }
1776    else if (!empty($part->ctype_parameters['name*0'])) {
1777      $i = 0;
1778      while (isset($part->ctype_parameters['name*'.$i])) {
1779        $filename_mime .= $part->ctype_parameters['name*'.$i];
1780        $i++;
1781      }
1782      if ($i<2) {
1783        if (!$headers)
1784          $headers = iil_C_FetchPartHeader($this->conn, $this->mailbox, $this->_msg_id, false, $part->mime_id);
1785        $filename_mime = '';
1786        $i = 0; $matches = array();
1787        while (preg_match('/\s+name\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
1788          $filename_mime .= $matches[1];
1789          $i++;
1790        }
1791      }
1792    }
1793    else if (!empty($part->ctype_parameters['name*0*'])) {
1794      $i = 0;
1795      while (isset($part->ctype_parameters['name*'.$i.'*'])) {
1796        $filename_encoded .= $part->ctype_parameters['name*'.$i.'*'];
1797        $i++;
1798      }
1799      if ($i<2) {
1800        if (!$headers)
1801          $headers = iil_C_FetchPartHeader($this->conn, $this->mailbox, $this->_msg_id, false, $part->mime_id);
1802        $filename_encoded = '';
1803        $i = 0; $matches = array();
1804        while (preg_match('/\s+name\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
1805          $filename_encoded .= $matches[1];
1806          $i++;
1807        }
1808      }
1809    }
1810    // read 'name' after rfc2231 parameters as it may contains truncated filename (from Thunderbird)
1811    else if (!empty($part->ctype_parameters['name']))
1812      $filename_mime = $part->ctype_parameters['name'];
1813    // Content-Disposition
1814    else if (!empty($part->headers['content-description']))
1815      $filename_mime = $part->headers['content-description'];
1816    else
1817      return;
1818
1819    // decode filename
1820    if (!empty($filename_mime)) {
1821      $part->filename = rcube_imap::decode_mime_string($filename_mime, 
1822        $part->charset ? $part->charset : $this->struct_charset ? $this->struct_charset :
1823            rc_detect_encoding($filename_mime, $this->default_charset));
1824      } 
1825    else if (!empty($filename_encoded)) {
1826      // decode filename according to RFC 2231, Section 4
1827      if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) {
1828        $filename_charset = $fmatches[1];
1829        $filename_encoded = $fmatches[2];
1830        }
1831      $part->filename = rcube_charset_convert(urldecode($filename_encoded), $filename_charset);
1832      }
1833    }
1834
1835
1836  /**
1837   * Get charset name from message structure (first part)
1838   *
1839   * @access private
1840   * @param  array  Message structure
1841   * @return string Charset name
1842   */
1843  function _structure_charset($structure)
1844    {
1845      while (is_array($structure)) {
1846        if (is_array($structure[2]) && $structure[2][0] == 'charset')
1847          return $structure[2][1];
1848        $structure = $structure[0];
1849        }
1850    } 
1851
1852
1853  /**
1854   * Fetch message body of a specific message from the server
1855   *
1856   * @param  int    Message UID
1857   * @param  string Part number
1858   * @param  object rcube_message_part Part object created by get_structure()
1859   * @param  mixed  True to print part, ressource to write part contents in
1860   * @param  resource File pointer to save the message part
1861   * @return string Message/part body if not printed
1862   */
1863  function &get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL)
1864    {
1865    // get part encoding if not provided
1866    if (!is_object($o_part))
1867      {
1868      $structure_str = iil_C_FetchStructureString($this->conn, $this->mailbox, $uid, true); 
1869      $structure = iml_GetRawStructureArray($structure_str);
1870      // error or message not found
1871      if (empty($structure))
1872        return false;
1873
1874      $part_type = iml_GetPartTypeCode($structure, $part);
1875      $o_part = new rcube_message_part;
1876      $o_part->ctype_primary = $part_type==0 ? 'text' : ($part_type==2 ? 'message' : 'other');
1877      $o_part->encoding = strtolower(iml_GetPartEncodingString($structure, $part));
1878      $o_part->charset = iml_GetPartCharset($structure, $part);
1879      }
1880     
1881    // TODO: Add caching for message parts
1882
1883    if (!$part) $part = 'TEXT';
1884
1885    $body = iil_C_HandlePartBody($this->conn, $this->mailbox, $uid, true, $part,
1886        $o_part->encoding, $print, $fp);
1887
1888    if ($fp || $print)
1889      return true;
1890
1891    // convert charset (if text or message part)
1892    if ($o_part->ctype_primary=='text' || $o_part->ctype_primary=='message') {
1893      // assume default if no charset specified
1894      if (empty($o_part->charset) || strtolower($o_part->charset) == 'us-ascii')
1895        $o_part->charset = $this->default_charset;
1896
1897      $body = rcube_charset_convert($body, $o_part->charset);
1898      }
1899   
1900    return $body;
1901    }
1902
1903
1904  /**
1905   * Fetch message body of a specific message from the server
1906   *
1907   * @param  int    Message UID
1908   * @return string Message/part body
1909   * @see    rcube_imap::get_message_part()
1910   */
1911  function &get_body($uid, $part=1)
1912    {
1913    $headers = $this->get_headers($uid);
1914    return rcube_charset_convert($this->get_message_part($uid, $part, NULL),
1915      $headers->charset ? $headers->charset : $this->default_charset);
1916    }
1917
1918
1919  /**
1920   * Returns the whole message source as string
1921   *
1922   * @param int  Message UID
1923   * @return string Message source string
1924   */
1925  function &get_raw_body($uid)
1926    {
1927    return iil_C_HandlePartBody($this->conn, $this->mailbox, $uid, true);
1928    }
1929
1930
1931  /**
1932   * Returns the message headers as string
1933   *
1934   * @param int  Message UID
1935   * @return string Message headers string
1936   */
1937  function &get_raw_headers($uid)
1938    {
1939    return iil_C_FetchPartHeader($this->conn, $this->mailbox, $uid, true);
1940    }
1941   
1942
1943  /**
1944   * Sends the whole message source to stdout
1945   *
1946   * @param int  Message UID
1947   */ 
1948  function print_raw_body($uid)
1949    {
1950    iil_C_HandlePartBody($this->conn, $this->mailbox, $uid, true, NULL, NULL, true);
1951    }
1952
1953
1954  /**
1955   * Set message flag to one or several messages
1956   *
1957   * @param mixed  Message UIDs as array or as comma-separated string
1958   * @param string Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
1959   * @param string Folder name
1960   * @param boolean True to skip message cache clean up
1961   * @return boolean True on success, False on failure
1962   */
1963  function set_flag($uids, $flag, $mbox_name=NULL, $skip_cache=false)
1964    {
1965    $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
1966
1967    $flag = strtoupper($flag);
1968    if (!is_array($uids))
1969      $uids = explode(',',$uids);
1970     
1971    if (strpos($flag, 'UN') === 0)
1972      $result = iil_C_UnFlag($this->conn, $mailbox, join(',', $uids), substr($flag, 2));
1973    else
1974      $result = iil_C_Flag($this->conn, $mailbox, join(',', $uids), $flag);
1975
1976    // reload message headers if cached
1977    if ($this->caching_enabled && !$skip_cache) {
1978      $cache_key = $mailbox.'.msg';
1979      $this->remove_message_cache($cache_key, $uids);
1980      }
1981
1982    // set nr of messages that were flaged
1983    $count = count($uids);
1984
1985    // clear message count cache
1986    if ($result && $flag=='SEEN')
1987      $this->_set_messagecount($mailbox, 'UNSEEN', $count*(-1));
1988    else if ($result && $flag=='UNSEEN')
1989      $this->_set_messagecount($mailbox, 'UNSEEN', $count);
1990    else if ($result && $flag=='DELETED')
1991      $this->_set_messagecount($mailbox, 'ALL', $count*(-1));
1992
1993    return $result;
1994    }
1995
1996
1997  /**
1998   * Remove message flag for one or several messages
1999   *
2000   * @param mixed  Message UIDs as array or as comma-separated string
2001   * @param string Flag to unset: SEEN, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
2002   * @param string Folder name
2003   * @return boolean True on success, False on failure
2004   * @see set_flag
2005   */
2006  function unset_flag($uids, $flag, $mbox_name=NULL)
2007    {
2008    return $this->set_flag($uids, 'UN'.$flag, $mbox_name);
2009    }
2010
2011
2012  /**
2013   * Append a mail message (source) to a specific mailbox
2014   *
2015   * @param string Target mailbox
2016   * @param string Message source
2017   * @return boolean True on success, False on error
2018   */
2019  function save_message($mbox_name, &$message)
2020    {
2021    $mailbox = $this->mod_mailbox($mbox_name);
2022
2023    // make sure mailbox exists
2024    if (($mailbox == 'INBOX') || in_array($mailbox, $this->_list_mailboxes()))
2025      $saved = iil_C_Append($this->conn, $mailbox, $message);
2026
2027    if ($saved)
2028      {
2029      // increase messagecount of the target mailbox
2030      $this->_set_messagecount($mailbox, 'ALL', 1);
2031      }
2032         
2033    return $saved;
2034    }
2035
2036
2037  /**
2038   * Move a message from one mailbox to another
2039   *
2040   * @param string List of UIDs to move, separated by comma
2041   * @param string Target mailbox
2042   * @param string Source mailbox
2043   * @return boolean True on success, False on error
2044   */
2045  function move_message($uids, $to_mbox, $from_mbox='')
2046    {
2047    $fbox = $from_mbox;
2048    $tbox = $to_mbox;
2049    $to_mbox = $this->mod_mailbox($to_mbox);
2050    $from_mbox = $from_mbox ? $this->mod_mailbox($from_mbox) : $this->mailbox;
2051
2052    // make sure mailbox exists
2053    if ($to_mbox != 'INBOX' && !in_array($to_mbox, $this->_list_mailboxes()))
2054      {
2055      if (in_array($to_mbox_in, $this->default_folders))
2056        $this->create_mailbox($to_mbox_in, TRUE);
2057      else
2058        return FALSE;
2059      }
2060
2061    // convert the list of uids to array
2062    $a_uids = is_string($uids) ? explode(',', $uids) : (is_array($uids) ? $uids : NULL);
2063
2064    // exit if no message uids are specified
2065    if (!is_array($a_uids) || empty($a_uids))
2066      return false;
2067
2068    // flag messages as read before moving them
2069    $config = rcmail::get_instance()->config;
2070    if ($config->get('read_when_deleted') && $tbox == $config->get('trash_mbox')) {
2071      // don't flush cache (4th argument)
2072      $this->set_flag($uids, 'SEEN', $fbox, true);
2073      }
2074
2075    // move messages
2076    $iil_move = iil_C_Move($this->conn, join(',', $a_uids), $from_mbox, $to_mbox);
2077    $moved = !($iil_move === false || $iil_move < 0);
2078   
2079    // send expunge command in order to have the moved message
2080    // really deleted from the source mailbox
2081    if ($moved) {
2082      $this->_expunge($from_mbox, FALSE, $a_uids);
2083      $this->_clear_messagecount($from_mbox);
2084      $this->_clear_messagecount($to_mbox);
2085    }
2086    // moving failed
2087    else if (rcmail::get_instance()->config->get('delete_always', false)) {
2088      return iil_C_Delete($this->conn, $from_mbox, join(',', $a_uids));
2089    }
2090
2091    // remove message ids from search set
2092    if ($moved && $this->search_set && $from_mbox == $this->mailbox) {
2093      foreach ($a_uids as $uid)
2094        $a_mids[] = $this->_uid2id($uid, $from_mbox);
2095      $this->search_set = array_diff($this->search_set, $a_mids);
2096    }
2097
2098    // update cached message headers
2099    $cache_key = $from_mbox.'.msg';
2100    if ($moved && $start_index = $this->get_message_cache_index_min($cache_key, $a_uids)) {
2101      // clear cache from the lowest index on
2102      $this->clear_message_cache($cache_key, $start_index);
2103      }
2104
2105    return $moved;
2106    }
2107
2108
2109  /**
2110   * Mark messages as deleted and expunge mailbox
2111   *
2112   * @param string List of UIDs to move, separated by comma
2113   * @param string Source mailbox
2114   * @return boolean True on success, False on error
2115   */
2116  function delete_message($uids, $mbox_name='')
2117    {
2118    $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
2119
2120    // convert the list of uids to array
2121    $a_uids = is_string($uids) ? explode(',', $uids) : (is_array($uids) ? $uids : NULL);
2122   
2123    // exit if no message uids are specified
2124    if (!is_array($a_uids) || empty($a_uids))
2125      return false;
2126
2127    $deleted = iil_C_Delete($this->conn, $mailbox, join(',', $a_uids));
2128
2129    // send expunge command in order to have the deleted message
2130    // really deleted from the mailbox
2131    if ($deleted)
2132      {
2133      $this->_expunge($mailbox, FALSE, $a_uids);
2134      $this->_clear_messagecount($mailbox);
2135      unset($this->uid_id_map[$mailbox]);
2136      }
2137
2138    // remove message ids from search set
2139    if ($deleted && $this->search_set && $mailbox == $this->mailbox) {
2140      foreach ($a_uids as $uid)
2141        $a_mids[] = $this->_uid2id($uid, $mailbox);
2142      $this->search_set = array_diff($this->search_set, $a_mids);
2143    }
2144
2145    // remove deleted messages from cache
2146    $cache_key = $mailbox.'.msg';
2147    if ($deleted && $start_index = $this->get_message_cache_index_min($cache_key, $a_uids)) {
2148      // clear cache from the lowest index on
2149      $this->clear_message_cache($cache_key, $start_index);
2150      }
2151
2152    return $deleted;
2153    }
2154
2155
2156  /**
2157   * Clear all messages in a specific mailbox
2158   *
2159   * @param string Mailbox name
2160   * @return int Above 0 on success
2161   */
2162  function clear_mailbox($mbox_name=NULL)
2163    {
2164    $mailbox = !empty($mbox_name) ? $this->mod_mailbox($mbox_name) : $this->mailbox;
2165    $msg_count = $this->_messagecount($mailbox, 'ALL');
2166   
2167    if ($msg_count>0)
2168      {
2169      $cleared = iil_C_ClearFolder($this->conn, $mailbox);
2170     
2171      // make sure the message count cache is cleared as well
2172      if ($cleared)
2173        {
2174        $this->clear_message_cache($mailbox.'.msg');     
2175        $a_mailbox_cache = $this->get_cache('messagecount');
2176        unset($a_mailbox_cache[$mailbox]);
2177        $this->update_cache('messagecount', $a_mailbox_cache);
2178        }
2179       
2180      return $cleared;
2181      }
2182    else
2183      return 0;
2184    }
2185
2186
2187  /**
2188   * Send IMAP expunge command and clear cache
2189   *
2190   * @param string Mailbox name
2191   * @param boolean False if cache should not be cleared
2192   * @return boolean True on success
2193   */
2194  function expunge($mbox_name='', $clear_cache=TRUE)
2195    {
2196    $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
2197    return $this->_expunge($mailbox, $clear_cache);
2198    }
2199
2200
2201  /**
2202   * Send IMAP expunge command and clear cache
2203   *
2204   * @see rcube_imap::expunge()
2205   * @param string      Mailbox name
2206   * @param boolean     False if cache should not be cleared
2207   * @param string      List of UIDs to remove, separated by comma
2208   * @return boolean True on success
2209   * @access private
2210   */
2211  private function _expunge($mailbox, $clear_cache=TRUE, $uids=NULL)
2212    {
2213    if ($uids && $this->get_capability('UIDPLUS')) 
2214      $a_uids = is_array($uids) ? join(',', $uids) : $uids;
2215    else
2216      $a_uids = NULL;
2217
2218    $result = iil_C_Expunge($this->conn, $mailbox, $a_uids);
2219
2220    if ($result>=0 && $clear_cache)
2221      {
2222      $this->clear_message_cache($mailbox.'.msg');
2223      $this->_clear_messagecount($mailbox);
2224      }
2225     
2226    return $result;
2227    }
2228
2229
2230  /* --------------------------------
2231   *        folder managment
2232   * --------------------------------*/
2233
2234
2235  /**
2236   * Get a list of all folders available on the IMAP server
2237   *
2238   * @param string IMAP root dir
2239   * @return array Indexed array with folder names
2240   */
2241  function list_unsubscribed($root='')
2242    {
2243    static $sa_unsubscribed;
2244   
2245    if (is_array($sa_unsubscribed))
2246      return $sa_unsubscribed;
2247     
2248    // retrieve list of folders from IMAP server
2249    $a_mboxes = iil_C_ListMailboxes($this->conn, $this->mod_mailbox($root), '*');
2250
2251    // modify names with root dir
2252    foreach ($a_mboxes as $mbox_name)
2253      {
2254      $name = $this->mod_mailbox($mbox_name, 'out');
2255      if (strlen($name))
2256        $a_folders[] = $name;
2257      }
2258
2259    // filter folders and sort them
2260    $sa_unsubscribed = $this->_sort_mailbox_list($a_folders);
2261    return $sa_unsubscribed;
2262    }
2263
2264
2265  /**
2266   * Get mailbox quota information
2267   * added by Nuny
2268   *
2269   * @return mixed Quota info or False if not supported
2270   */
2271  function get_quota()
2272    {
2273    if ($this->get_capability('QUOTA'))
2274      return iil_C_GetQuota($this->conn);
2275       
2276    return FALSE;
2277    }
2278
2279
2280  /**
2281   * Subscribe to a specific mailbox(es)
2282   *
2283   * @param array Mailbox name(s)
2284   * @return boolean True on success
2285   */ 
2286  function subscribe($a_mboxes)
2287    {
2288    if (!is_array($a_mboxes))
2289      $a_mboxes = array($a_mboxes);
2290
2291    // let this common function do the main work
2292    return $this->_change_subscription($a_mboxes, 'subscribe');
2293    }
2294
2295
2296  /**
2297   * Unsubscribe mailboxes
2298   *
2299   * @param array Mailbox name(s)
2300   * @return boolean True on success
2301   */
2302  function unsubscribe($a_mboxes)
2303    {
2304    if (!is_array($a_mboxes))
2305      $a_mboxes = array($a_mboxes);
2306
2307    // let this common function do the main work
2308    return $this->_change_subscription($a_mboxes, 'unsubscribe');
2309    }
2310
2311
2312  /**
2313   * Create a new mailbox on the server and register it in local cache
2314   *
2315   * @param string  New mailbox name (as utf-7 string)
2316   * @param boolean True if the new mailbox should be subscribed
2317   * @param string  Name of the created mailbox, false on error
2318   */
2319  function create_mailbox($name, $subscribe=FALSE)
2320    {
2321    $result = FALSE;
2322   
2323    // reduce mailbox name to 100 chars
2324    $name = substr($name, 0, 100);
2325
2326    $abs_name = $this->mod_mailbox($name);
2327    $a_mailbox_cache = $this->get_cache('mailboxes');
2328
2329    if (strlen($abs_name) && (!is_array($a_mailbox_cache) || !in_array($abs_name, $a_mailbox_cache)))
2330      $result = iil_C_CreateFolder($this->conn, $abs_name);
2331
2332    // try to subscribe it
2333    if ($result && $subscribe)
2334      $this->subscribe($name);
2335
2336    return $result ? $name : FALSE;
2337    }
2338
2339
2340  /**
2341   * Set a new name to an existing mailbox
2342   *
2343   * @param string Mailbox to rename (as utf-7 string)
2344   * @param string New mailbox name (as utf-7 string)
2345   * @return string Name of the renames mailbox, False on error
2346   */
2347  function rename_mailbox($mbox_name, $new_name)
2348    {
2349    $result = FALSE;
2350
2351    // encode mailbox name and reduce it to 100 chars
2352    $name = substr($new_name, 0, 100);
2353
2354    // make absolute path
2355    $mailbox = $this->mod_mailbox($mbox_name);
2356    $abs_name = $this->mod_mailbox($name);
2357   
2358    // check if mailbox is subscribed
2359    $a_subscribed = $this->_list_mailboxes();
2360    $subscribed = in_array($mailbox, $a_subscribed);
2361   
2362    // unsubscribe folder
2363    if ($subscribed)
2364      iil_C_UnSubscribe($this->conn, $mailbox);
2365
2366    if (strlen($abs_name))
2367      $result = iil_C_RenameFolder($this->conn, $mailbox, $abs_name);
2368
2369    if ($result)
2370      {
2371      $delm = $this->get_hierarchy_delimiter();
2372     
2373      // check if mailbox children are subscribed
2374      foreach ($a_subscribed as $c_subscribed)
2375        if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_subscribed))
2376          {
2377          iil_C_UnSubscribe($this->conn, $c_subscribed);
2378          iil_C_Subscribe($this->conn, preg_replace('/^'.preg_quote($mailbox, '/').'/', $abs_name, $c_subscribed));
2379          }
2380
2381      // clear cache
2382      $this->clear_message_cache($mailbox.'.msg');
2383      $this->clear_cache('mailboxes');     
2384      }
2385
2386    // try to subscribe it
2387    if ($result && $subscribed)
2388      iil_C_Subscribe($this->conn, $abs_name);
2389
2390    return $result ? $name : FALSE;
2391    }
2392
2393
2394  /**
2395   * Remove mailboxes from server
2396   *
2397   * @param string Mailbox name(s) string/array
2398   * @return boolean True on success
2399   */
2400  function delete_mailbox($mbox_name)
2401    {
2402    $deleted = FALSE;
2403
2404    if (is_array($mbox_name))
2405      $a_mboxes = $mbox_name;
2406    else if (is_string($mbox_name) && strlen($mbox_name))
2407      $a_mboxes = explode(',', $mbox_name);
2408
2409    $all_mboxes = iil_C_ListMailboxes($this->conn, $this->mod_mailbox($root), '*');
2410
2411    if (is_array($a_mboxes))
2412      foreach ($a_mboxes as $mbox_name)
2413        {
2414        $mailbox = $this->mod_mailbox($mbox_name);
2415
2416        // unsubscribe mailbox before deleting
2417        iil_C_UnSubscribe($this->conn, $mailbox);
2418
2419        // send delete command to server
2420        $result = iil_C_DeleteFolder($this->conn, $mailbox);
2421        if ($result >= 0) {
2422          $deleted = TRUE;
2423          $this->clear_message_cache($mailbox.'.msg');
2424          }
2425         
2426        foreach ($all_mboxes as $c_mbox)
2427          {
2428          $regex = preg_quote($mailbox . $this->delimiter, '/');
2429          $regex = '/^' . $regex . '/';
2430          if (preg_match($regex, $c_mbox))
2431            {
2432            iil_C_UnSubscribe($this->conn, $c_mbox);
2433            $result = iil_C_DeleteFolder($this->conn, $c_mbox);
2434            if ($result >= 0) {
2435              $deleted = TRUE;
2436              $this->clear_message_cache($c_mbox.'.msg');
2437              }
2438            }
2439          }
2440        }
2441
2442    // clear mailboxlist cache
2443    if ($deleted)
2444      $this->clear_cache('mailboxes');
2445
2446    return $deleted;
2447    }
2448
2449
2450  /**
2451   * Create all folders specified as default
2452   */
2453  function create_default_folders()
2454    {
2455    $a_folders = iil_C_ListMailboxes($this->conn, $this->mod_mailbox(''), '*');
2456    $a_subscribed = iil_C_ListSubscribed($this->conn, $this->mod_mailbox(''), '*');
2457   
2458    // create default folders if they do not exist
2459    foreach ($this->default_folders as $folder)
2460      {
2461      $abs_name = $this->mod_mailbox($folder);
2462      if (!in_array_nocase($abs_name, $a_folders))
2463        $this->create_mailbox($folder, TRUE);
2464      else if (!in_array_nocase($abs_name, $a_subscribed))
2465        $this->subscribe($folder);
2466      }
2467    }
2468
2469
2470
2471  /* --------------------------------
2472   *   internal caching methods
2473   * --------------------------------*/
2474
2475  /**
2476   * @access private
2477   */
2478  function set_caching($set)
2479    {
2480    if ($set && is_object($this->db))
2481      $this->caching_enabled = TRUE;
2482    else
2483      $this->caching_enabled = FALSE;
2484    }
2485
2486  /**
2487   * @access private
2488   */
2489  function get_cache($key)
2490    {
2491    // read cache (if it was not read before)
2492    if (!count($this->cache) && $this->caching_enabled)
2493      {
2494      return $this->_read_cache_record($key);
2495      }
2496   
2497    return $this->cache[$key];
2498    }
2499
2500  /**
2501   * @access private
2502   */
2503  function update_cache($key, $data)
2504    {
2505    $this->cache[$key] = $data;
2506    $this->cache_changed = TRUE;
2507    $this->cache_changes[$key] = TRUE;
2508    }
2509
2510  /**
2511   * @access private
2512   */
2513  function write_cache()
2514    {
2515    if ($this->caching_enabled && $this->cache_changed)
2516      {
2517      foreach ($this->cache as $key => $data)
2518        {
2519        if ($this->cache_changes[$key])
2520          $this->_write_cache_record($key, serialize($data));
2521        }
2522      }   
2523    }
2524
2525  /**
2526   * @access private
2527   */
2528  function clear_cache($key=NULL)
2529    {
2530    if (!$this->caching_enabled)
2531      return;
2532   
2533    if ($key===NULL)
2534      {
2535      foreach ($this->cache as $key => $data)
2536        $this->_clear_cache_record($key);
2537
2538      $this->cache = array();
2539      $this->cache_changed = FALSE;
2540      $this->cache_changes = array();
2541      }
2542    else
2543      {
2544      $this->_clear_cache_record($key);
2545      $this->cache_changes[$key] = FALSE;
2546      unset($this->cache[$key]);
2547      }
2548    }
2549
2550  /**
2551   * @access private
2552   */
2553  private function _read_cache_record($key)
2554    {
2555    if ($this->db)
2556      {
2557      // get cached data from DB
2558      $sql_result = $this->db->query(
2559        "SELECT cache_id, data, cache_key
2560         FROM ".get_table_name('cache')."
2561         WHERE  user_id=?
2562         AND cache_key LIKE 'IMAP.%'",
2563        $_SESSION['user_id']);
2564
2565      while ($sql_arr = $this->db->fetch_assoc($sql_result))
2566        {
2567        $sql_key = preg_replace('/^IMAP\./', '', $sql_arr['cache_key']);
2568        $this->cache_keys[$sql_key] = $sql_arr['cache_id'];
2569        if (!isset($this->cache[$sql_key]))
2570          $this->cache[$sql_key] = $sql_arr['data'] ? unserialize($sql_arr['data']) : FALSE;
2571        }
2572      }
2573
2574    return $this->cache[$key];
2575    }
2576
2577  /**
2578   * @access private
2579   */
2580  private function _write_cache_record($key, $data)
2581    {
2582    if (!$this->db)
2583      return FALSE;
2584
2585    // update existing cache record
2586    if ($this->cache_keys[$key])
2587      {
2588      $this->db->query(
2589        "UPDATE ".get_table_name('cache')."
2590         SET    created=". $this->db->now().", data=?
2591         WHERE  user_id=?
2592         AND    cache_key=?",
2593        $data,
2594        $_SESSION['user_id'],
2595        'IMAP.'.$key);
2596      }
2597    // add new cache record
2598    else
2599      {
2600      $this->db->query(
2601        "INSERT INTO ".get_table_name('cache')."
2602         (created, user_id, cache_key, data)
2603         VALUES (".$this->db->now().", ?, ?, ?)",
2604        $_SESSION['user_id'],
2605        'IMAP.'.$key,
2606        $data);
2607
2608      // get cache entry ID for this key
2609      $sql_result = $this->db->query(
2610        "SELECT cache_id
2611         FROM ".get_table_name('cache')."
2612         WHERE  user_id=?
2613         AND    cache_key=?",
2614        $_SESSION['user_id'],
2615        'IMAP.'.$key);
2616                                     
2617        if ($sql_arr = $this->db->fetch_assoc($sql_result))
2618          $this->cache_keys[$key] = $sql_arr['cache_id'];
2619      }
2620    }
2621
2622  /**
2623   * @access private
2624   */
2625  private function _clear_cache_record($key)
2626    {
2627    $this->db->query(
2628      "DELETE FROM ".get_table_name('cache')."
2629       WHERE  user_id=?
2630       AND    cache_key=?",
2631      $_SESSION['user_id'],
2632      'IMAP.'.$key);
2633     
2634    unset($this->cache_keys[$key]);
2635    }
2636
2637
2638
2639  /* --------------------------------
2640   *   message caching methods
2641   * --------------------------------*/
2642   
2643
2644  /**
2645   * Checks if the cache is up-to-date
2646   *
2647   * @param string Mailbox name
2648   * @param string Internal cache key
2649   * @return int   Cache status: -3 = off, -2 = incomplete, -1 = dirty
2650   */
2651  private function check_cache_status($mailbox, $cache_key)
2652  {
2653    if (!$this->caching_enabled)
2654      return -3;
2655
2656    $cache_index = $this->get_message_cache_index($cache_key);
2657    $msg_count = $this->_messagecount($mailbox);
2658    $cache_count = count($cache_index);
2659
2660    // empty mailbox
2661    if (!$msg_count)
2662      return $cache_count ? -2 : 1;
2663
2664    // @TODO: We've got one big performance problem in cache status checking method
2665    // E.g. mailbox contains 1000 messages, in cache table we've got first 100
2666    // of them. Now if we want to display only that 100 (which we've got)
2667    // check_cache_status returns 'incomplete' and messages are fetched
2668    // from IMAP instead of DB.
2669
2670    if ($cache_count==$msg_count) {
2671      if ($this->skip_deleted) {
2672        $h_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, "1:*", 'UID', $this->skip_deleted);
2673
2674        if (sizeof($h_index) == $cache_count) {
2675          $cache_index = array_flip($cache_index);
2676          foreach ($h_index as $idx => $uid)
2677            unset($cache_index[$uid]);
2678
2679          if (empty($cache_index))
2680            return 1;
2681        }
2682        return -2;
2683      } else {
2684        // get UID of message with highest index
2685        $uid = iil_C_ID2UID($this->conn, $mailbox, $msg_count);
2686        $cache_uid = array_pop($cache_index);
2687     
2688        // uids of highest message matches -> cache seems OK
2689        if ($cache_uid == $uid)
2690          return 1;
2691      }
2692      // cache is dirty
2693      return -1;
2694    }
2695    // if cache count differs less than 10% report as dirty
2696    else if (abs($msg_count - $cache_count) < $msg_count/10)
2697      return -1;
2698    else
2699      return -2;
2700  }
2701
2702  /**
2703   * @access private
2704   */
2705  private function get_message_cache($key, $from, $to, $sort_field, $sort_order)
2706    {
2707    $cache_key = "$key:$from:$to:$sort_field:$sort_order";
2708   
2709    $config = rcmail::get_instance()->config;
2710
2711    // use idx sort as default sorting
2712    if (!$sort_field || !in_array($sort_field, $this->db_header_fields)) {
2713      $sort_field = 'idx';
2714      }
2715   
2716    if ($this->caching_enabled && !isset($this->cache[$cache_key]))
2717      {
2718      $this->cache[$cache_key] = array();
2719      $sql_result = $this->db->limitquery(
2720        "SELECT idx, uid, headers
2721         FROM ".get_table_name('messages')."
2722         WHERE  user_id=?
2723         AND    cache_key=?
2724         ORDER BY ".$this->db->quoteIdentifier($sort_field)." ".strtoupper($sort_order),
2725        $from,
2726        $to-$from,
2727        $_SESSION['user_id'],
2728        $key);
2729
2730      while ($sql_arr = $this->db->fetch_assoc($sql_result))
2731        {
2732        $uid = $sql_arr['uid'];
2733        $this->cache[$cache_key][$uid] =  $this->db->decode(unserialize($sql_arr['headers']));
2734
2735        // featch headers if unserialize failed
2736        if (empty($this->cache[$cache_key][$uid]))
2737          $this->cache[$cache_key][$uid] = iil_C_FetchHeader($this->conn, preg_replace('/.msg$/', '', $key), $uid, true, $this->fetch_add_headers);
2738        }
2739      }
2740
2741    return $this->cache[$cache_key];
2742    }
2743
2744  /**
2745   * @access private
2746   */
2747  private function &get_cached_message($key, $uid)
2748    {
2749    $internal_key = '__single_msg';
2750   
2751    if ($this->caching_enabled && !isset($this->cache[$internal_key][$uid]))
2752      {
2753      $sql_result = $this->db->query(
2754        "SELECT idx, headers, structure
2755         FROM ".get_table_name('messages')."
2756         WHERE  user_id=?
2757         AND    cache_key=?
2758         AND    uid=?",
2759        $_SESSION['user_id'],
2760        $key,
2761        $uid);
2762
2763      if ($sql_arr = $this->db->fetch_assoc($sql_result))
2764        {
2765        $this->uid_id_map[preg_replace('/\.msg$/', '', $key)][$uid] = $sql_arr['idx'];
2766        $this->cache[$internal_key][$uid] = $this->db->decode(unserialize($sql_arr['headers']));
2767        if (is_object($this->cache[$internal_key][$uid]) && !empty($sql_arr['structure']))
2768          $this->cache[$internal_key][$uid]->structure = $this->db->decode(unserialize($sql_arr['structure']));
2769        }
2770      }
2771
2772    return $this->cache[$internal_key][$uid];
2773    }
2774
2775  /**
2776   * @access private
2777   */ 
2778  private function get_message_cache_index($key, $force=FALSE, $sort_field='idx', $sort_order='ASC')
2779    {
2780    static $sa_message_index = array();
2781   
2782    // empty key -> empty array
2783    if (!$this->caching_enabled || empty($key))
2784      return array();
2785   
2786    if (!empty($sa_message_index[$key]) && !$force)
2787      return $sa_message_index[$key];
2788
2789    // use idx sort as default
2790    if (!$sort_field || !in_array($sort_field, $this->db_header_fields))
2791      $sort_field = 'idx';
2792   
2793    $sa_message_index[$key] = array();
2794    $sql_result = $this->db->query(
2795      "SELECT idx, uid
2796       FROM ".get_table_name('messages')."
2797       WHERE  user_id=?
2798       AND    cache_key=?
2799       ORDER BY ".$this->db->quote_identifier($sort_field)." ".$sort_order,
2800      $_SESSION['user_id'],
2801      $key);
2802
2803    while ($sql_arr = $this->db->fetch_assoc($sql_result))
2804      $sa_message_index[$key][$sql_arr['idx']] = $sql_arr['uid'];
2805     
2806    return $sa_message_index[$key];
2807    }
2808
2809  /**
2810   * @access private
2811   */
2812  private function add_message_cache($key, $index, $headers, $struct=null, $force=false)
2813    {
2814    if (empty($key) || !is_object($headers) || empty($headers->uid))
2815        return;
2816
2817    // add to internal (fast) cache
2818    $this->cache['__single_msg'][$headers->uid] = clone $headers;
2819    $this->cache['__single_msg'][$headers->uid]->structure = $struct;
2820
2821    // no further caching
2822    if (!$this->caching_enabled)
2823      return;
2824   
2825    // check for an existing record (probly headers are cached but structure not)
2826    if (!$force) {
2827      $sql_result = $this->db->query(
2828        "SELECT message_id
2829         FROM ".get_table_name('messages')."
2830         WHERE  user_id=?
2831         AND    cache_key=?
2832         AND    uid=?",
2833        $_SESSION['user_id'],
2834        $key,
2835        $headers->uid);
2836      if ($sql_arr = $this->db->fetch_assoc($sql_result))
2837        $message_id = $sql_arr['message_id'];
2838      }
2839
2840    // update cache record
2841    if ($message_id)
2842      {
2843      $this->db->query(
2844        "UPDATE ".get_table_name('messages')."
2845         SET   idx=?, headers=?, structure=?
2846         WHERE message_id=?",
2847        $index,
2848        serialize($this->db->encode(clone $headers)),
2849        is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL,
2850        $message_id
2851        );
2852      }
2853    else  // insert new record
2854      {
2855      $this->db->query(
2856        "INSERT INTO ".get_table_name('messages')."
2857         (user_id, del, cache_key, created, idx, uid, subject, ".$this->db->quoteIdentifier('from').", ".$this->db->quoteIdentifier('to').", cc, date, size, headers, structure)
2858         VALUES (?, 0, ?, ".$this->db->now().", ?, ?, ?, ?, ?, ?, ".$this->db->fromunixtime($headers->timestamp).", ?, ?, ?)",
2859        $_SESSION['user_id'],
2860        $key,
2861        $index,
2862        $headers->uid,
2863        (string)mb_substr($this->db->encode($this->decode_header($headers->subject, TRUE)), 0, 128),
2864        (string)mb_substr($this->db->encode($this->decode_header($headers->from, TRUE)), 0, 128),
2865        (string)mb_substr($this->db->encode($this->decode_header($headers->to, TRUE)), 0, 128),
2866        (string)mb_substr($this->db->encode($this->decode_header($headers->cc, TRUE)), 0, 128),
2867        (int)$headers->size,
2868        serialize($this->db->encode(clone $headers)),
2869        is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL
2870        );
2871      }
2872    }
2873   
2874  /**
2875   * @access private
2876   */
2877  private function remove_message_cache($key, $ids, $idx=false)
2878    {
2879    if (!$this->caching_enabled)
2880      return;
2881   
2882    $this->db->query(
2883      "DELETE FROM ".get_table_name('messages')."
2884      WHERE user_id=?
2885      AND cache_key=?
2886      AND ".($idx ? "idx" : "uid")." IN (".$this->db->array2list($ids, 'integer').")",
2887      $_SESSION['user_id'],
2888      $key);
2889    }
2890
2891  /**
2892   * @access private
2893   */
2894  private function clear_message_cache($key, $start_index=1)
2895    {
2896    if (!$this->caching_enabled)
2897      return;
2898   
2899    $this->db->query(
2900      "DELETE FROM ".get_table_name('messages')."
2901       WHERE user_id=?
2902       AND cache_key=?
2903       AND idx>=?",
2904      $_SESSION['user_id'], $key, $start_index);
2905    }
2906
2907  /**
2908   * @access private
2909   */
2910  private function get_message_cache_index_min($key, $uids=NULL)
2911    {
2912    if (!$this->caching_enabled)
2913      return;
2914   
2915    $sql_result = $this->db->query(
2916      "SELECT MIN(idx) AS minidx
2917      FROM ".get_table_name('messages')."
2918      WHERE  user_id=?
2919      AND    cache_key=?"
2920      .(!empty($uids) ? " AND uid IN (".$this->db->array2list($uids, 'integer').")" : ''),
2921      $_SESSION['user_id'],
2922      $key);
2923
2924    if ($sql_arr = $this->db->fetch_assoc($sql_result))
2925      return $sql_arr['minidx'];
2926    else
2927      return 0; 
2928    }
2929
2930
2931  /* --------------------------------
2932   *   encoding/decoding methods
2933   * --------------------------------*/
2934
2935  /**
2936   * Split an address list into a structured array list
2937   *
2938   * @param string  Input string
2939   * @param int     List only this number of addresses
2940   * @param boolean Decode address strings
2941   * @return array  Indexed list of addresses
2942   */
2943  function decode_address_list($input, $max=null, $decode=true)
2944    {
2945    $a = $this->_parse_address_list($input, $decode);
2946    $out = array();
2947    // Special chars as defined by RFC 822 need to in quoted string (or escaped).
2948    $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
2949   
2950    if (!is_array($a))
2951      return $out;
2952
2953    $c = count($a);
2954    $j = 0;
2955
2956    foreach ($a as $val)
2957      {
2958      $j++;
2959      $address = $val['address'];
2960      $name = preg_replace(array('/^[\'"]/', '/[\'"]$/'), '', trim($val['name']));
2961      if ($name && $address && $name != $address)
2962        $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
2963      else if ($address)
2964        $string = $address;
2965      else if ($name)
2966        $string = $name;
2967     
2968      $out[$j] = array('name' => $name,
2969                       'mailto' => $address,
2970                       'string' => $string);
2971             
2972      if ($max && $j==$max)
2973        break;
2974      }
2975   
2976    return $out;
2977    }
2978 
2979 
2980  /**
2981   * Decode a Microsoft Outlook TNEF part (winmail.dat)
2982   *
2983   * @param object rcube_message_part Message part to decode
2984   * @param string UID of the message
2985   * @return array List of rcube_message_parts extracted from windmail.dat
2986   */
2987  function tnef_decode(&$part, $uid)
2988  {
2989    if (!isset($part->body))
2990      $part->body = $this->get_message_part($uid, $part->mime_id, $part);
2991
2992    $pid = 0;
2993    $tnef_parts = array();
2994    $tnef_arr = tnef_decode($part->body);
2995    foreach ($tnef_arr as $winatt) {
2996      $tpart = new rcube_message_part;
2997      $tpart->filename = $winatt["name"];
2998      $tpart->encoding = 'stream';
2999      $tpart->ctype_primary = $winatt["type0"];
3000      $tpart->ctype_secondary = $winatt["type1"];
3001      $tpart->mimetype = strtolower($winatt["type0"] . "/" . $winatt["type1"]);
3002      $tpart->mime_id = "winmail." . $part->mime_id . ".$pid";
3003      $tpart->size = $winatt["size"];
3004      $tpart->body = $winatt['stream'];
3005     
3006      $tnef_parts[] = $tpart;
3007      $pid++;
3008    }
3009
3010    return $tnef_parts;
3011  }
3012
3013
3014  /**
3015   * Decode a message header value
3016   *
3017   * @param string  Header value
3018   * @param boolean Remove quotes if necessary
3019   * @return string Decoded string
3020   */
3021  function decode_header($input, $remove_quotes=FALSE)
3022    {
3023    $str = rcube_imap::decode_mime_string((string)$input, $this->default_charset);
3024    if ($str{0}=='"' && $remove_quotes)
3025      $str = str_replace('"', '', $str);
3026   
3027    return $str;
3028    }
3029
3030
3031  /**
3032   * Decode a mime-encoded string to internal charset
3033   *
3034   * @param string $input    Header value
3035   * @param string $fallback Fallback charset if none specified
3036   *
3037   * @return string Decoded string
3038   * @static
3039   */
3040  public static function decode_mime_string($input, $fallback=null)
3041    {
3042    // Initialize variable
3043    $out = '';
3044
3045    // Iterate instead of recursing, this way if there are too many values we don't have stack overflows
3046    // rfc: all line breaks or other characters not found
3047    // in the Base64 Alphabet must be ignored by decoding software
3048    // delete all blanks between MIME-lines, differently we can
3049    // receive unnecessary blanks and broken utf-8 symbols
3050    $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
3051
3052    // Check if there is stuff to decode
3053    if (strpos($input, '=?') !== false) {
3054      // Loop through the string to decode all occurences of =? ?= into the variable $out
3055      while(($pos = strpos($input, '=?')) !== false) {
3056        // Append everything that is before the text to be decoded
3057        $out .= substr($input, 0, $pos);
3058
3059        // Get the location of the text to decode
3060        $end_cs_pos = strpos($input, "?", $pos+2);
3061        $end_en_pos = strpos($input, "?", $end_cs_pos+1);
3062        $end_pos = strpos($input, "?=", $end_en_pos+1);
3063
3064        // Extract the encoded string
3065        $encstr = substr($input, $pos+2, ($end_pos-$pos-2));
3066        // Extract the remaining string
3067        $input = substr($input, $end_pos+2);
3068
3069        // Decode the string fragement
3070        $out .= rcube_imap::_decode_mime_string_part($encstr);
3071      }
3072
3073      // Deocde the rest (if any)
3074      if (strlen($input) != 0)
3075         $out .= rcube_imap::decode_mime_string($input, $fallback);
3076
3077       // return the results
3078      return $out;
3079    }
3080
3081    // no encoding information, use fallback
3082    return rcube_charset_convert($input, 
3083      !empty($fallback) ? $fallback : rcmail::get_instance()->config->get('default_charset', 'ISO-8859-1'));
3084    }
3085
3086
3087  /**
3088   * Decode a part of a mime-encoded string
3089   *
3090   * @access private
3091   */
3092  private function _decode_mime_string_part($str)
3093    {
3094    $a = explode('?', $str);
3095    $count = count($a);
3096
3097    // should be in format "charset?encoding?base64_string"
3098    if ($count >= 3)
3099      {
3100      for ($i=2; $i<$count; $i++)
3101        $rest.=$a[$i];
3102
3103      if (($a[1]=="B")||($a[1]=="b"))
3104        $rest = base64_decode($rest);
3105      else if (($a[1]=="Q")||($a[1]=="q"))
3106        {
3107        $rest = str_replace("_", " ", $rest);
3108        $rest = quoted_printable_decode($rest);
3109        }
3110
3111      return rcube_charset_convert($rest, $a[0]);
3112      }
3113    else
3114      return $str;    // we dont' know what to do with this 
3115    }
3116
3117
3118  /**
3119   * Decode a mime part
3120   *
3121   * @param string Input string
3122   * @param string Part encoding
3123   * @return string Decoded string
3124   */
3125  function mime_decode($input, $encoding='7bit')
3126    {
3127    switch (strtolower($encoding))
3128      {
3129      case 'quoted-printable':
3130        return quoted_printable_decode($input);
3131        break;
3132     
3133      case 'base64':
3134        return base64_decode($input);
3135        break;
3136
3137      case 'x-uuencode':
3138      case 'x-uue':
3139      case 'uue':
3140      case 'uuencode':
3141        return convert_uudecode($input);
3142        break;
3143                                                     
3144      case '7bit':
3145      default:
3146        return $input;
3147      }
3148    }
3149
3150
3151  /**
3152   * Convert body charset to RCMAIL_CHARSET according to the ctype_parameters
3153   *
3154   * @param string Part body to decode
3155   * @param string Charset to convert from
3156   * @return string Content converted to internal charset
3157   */
3158  function charset_decode($body, $ctype_param)
3159    {
3160    if (is_array($ctype_param) && !empty($ctype_param['charset']))
3161      return rcube_charset_convert($body, $ctype_param['charset']);
3162
3163    // defaults to what is specified in the class header
3164    return rcube_charset_convert($body,  $this->default_charset);
3165    }
3166
3167
3168  /**
3169   * Translate UID to message ID
3170   *
3171   * @param int    Message UID
3172   * @param string Mailbox name
3173   * @return int   Message ID
3174   */
3175  function get_id($uid, $mbox_name=NULL) 
3176    {
3177      $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
3178      return $this->_uid2id($uid, $mailbox);
3179    }
3180
3181
3182  /**
3183   * Translate message number to UID
3184   *
3185   * @param int    Message ID
3186   * @param string Mailbox name
3187   * @return int   Message UID
3188   */
3189  function get_uid($id,$mbox_name=NULL)
3190    {
3191      $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
3192      return $this->_id2uid($id, $mailbox);
3193    }
3194
3195
3196  /**
3197   * Modify folder name for input/output according to root dir and namespace
3198   *
3199   * @param string  Folder name
3200   * @param string  Mode
3201   * @return string Folder name
3202   */
3203  function mod_mailbox($mbox_name, $mode='in')
3204    {
3205    if ((!empty($this->root_ns) && $this->root_ns == $mbox_name) || $mbox_name == 'INBOX')
3206      return $mbox_name;
3207
3208    if (!empty($this->root_dir) && $mode=='in') 
3209      $mbox_name = $this->root_dir.$this->delimiter.$mbox_name;
3210    else if (strlen($this->root_dir) && $mode=='out') 
3211      $mbox_name = substr($mbox_name, strlen($this->root_dir)+1);
3212
3213    return $mbox_name;
3214    }
3215
3216
3217  /* --------------------------------
3218   *         private methods
3219   * --------------------------------*/
3220
3221  /**
3222   * Validate the given input and save to local properties
3223   * @access private
3224   */
3225  private function _set_sort_order($sort_field, $sort_order)
3226  {
3227    if ($sort_field != null)
3228      $this->sort_field = asciiwords($sort_field);
3229    if ($sort_order != null)
3230      $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
3231  }
3232
3233  /**
3234   * Sort mailboxes first by default folders and then in alphabethical order
3235   * @access private
3236   */
3237  private function _sort_mailbox_list($a_folders)
3238    {
3239    $a_out = $a_defaults = $folders = array();
3240
3241    $delimiter = $this->get_hierarchy_delimiter();
3242
3243    // find default folders and skip folders starting with '.'
3244    foreach ($a_folders as $i => $folder)
3245      {
3246      if ($folder{0}=='.')
3247        continue;
3248
3249      if (($p = array_search(strtolower($folder), $this->default_folders_lc)) !== false && !$a_defaults[$p])
3250        $a_defaults[$p] = $folder;
3251      else
3252        $folders[$folder] = mb_strtolower(rcube_charset_convert($folder, 'UTF7-IMAP'));
3253      }
3254
3255    // sort folders and place defaults on the top
3256    asort($folders, SORT_LOCALE_STRING);
3257    ksort($a_defaults);
3258    $folders = array_merge($a_defaults, array_keys($folders));
3259
3260    // finally we must rebuild the list to move
3261    // subfolders of default folders to their place...
3262    // ...also do this for the rest of folders because
3263    // asort() is not properly sorting case sensitive names
3264    while (list($key, $folder) = each($folders)) {
3265      // set the type of folder name variable (#1485527)
3266      $a_out[] = (string) $folder;
3267      unset($folders[$key]);
3268      $this->_rsort($folder, $delimiter, $folders, $a_out);     
3269      }
3270
3271    return $a_out;
3272    }
3273
3274
3275  /**
3276   * @access private
3277   */
3278  private function _rsort($folder, $delimiter, &$list, &$out)
3279    {
3280      while (list($key, $name) = each($list)) {
3281        if (strpos($name, $folder.$delimiter) === 0) {
3282          // set the type of folder name variable (#1485527)
3283          $out[] = (string) $name;
3284          unset($list[$key]);
3285          $this->_rsort($name, $delimiter, $list, $out);
3286          }
3287        }
3288      reset($list);     
3289    }
3290
3291
3292  /**
3293   * @access private
3294   */
3295  private function _uid2id($uid, $mbox_name=NULL)
3296    {
3297    if (!$mbox_name)
3298      $mbox_name = $this->mailbox;
3299     
3300    if (!isset($this->uid_id_map[$mbox_name][$uid]))
3301      $this->uid_id_map[$mbox_name][$uid] = iil_C_UID2ID($this->conn, $mbox_name, $uid);
3302
3303    return $this->uid_id_map[$mbox_name][$uid];
3304    }
3305
3306  /**
3307   * @access private
3308   */
3309  private function _id2uid($id, $mbox_name=NULL)
3310    {
3311    if (!$mbox_name)
3312      $mbox_name = $this->mailbox;
3313     
3314    $index = array_flip((array)$this->uid_id_map[$mbox_name]);
3315    if (isset($index[$id]))
3316      $uid = $index[$id];
3317    else
3318      {
3319      $uid = iil_C_ID2UID($this->conn, $mbox_name, $id);
3320      $this->uid_id_map[$mbox_name][$uid] = $id;
3321      }
3322   
3323    return $uid;
3324    }
3325
3326
3327  /**
3328   * Subscribe/unsubscribe a list of mailboxes and update local cache
3329   * @access private
3330   */
3331  private function _change_subscription($a_mboxes, $mode)
3332    {
3333    $updated = FALSE;
3334
3335    if (is_array($a_mboxes))
3336      foreach ($a_mboxes as $i => $mbox_name)
3337        {
3338        $mailbox = $this->mod_mailbox($mbox_name);
3339        $a_mboxes[$i] = $mailbox;
3340
3341        if ($mode=='subscribe')
3342          $updated = iil_C_Subscribe($this->conn, $mailbox);
3343        else if ($mode=='unsubscribe')
3344          $updated = iil_C_UnSubscribe($this->conn, $mailbox);
3345        }
3346
3347    // get cached mailbox list
3348    if ($updated)
3349      {
3350      $a_mailbox_cache = $this->get_cache('mailboxes');
3351      if (!is_array($a_mailbox_cache))
3352        return $updated;
3353
3354      // modify cached list
3355      if ($mode=='subscribe')
3356        $a_mailbox_cache = array_merge($a_mailbox_cache, $a_mboxes);
3357      else if ($mode=='unsubscribe')
3358        $a_mailbox_cache = array_diff($a_mailbox_cache, $a_mboxes);
3359
3360      // write mailboxlist to cache
3361      $this->update_cache('mailboxes', $this->_sort_mailbox_list($a_mailbox_cache));
3362      }
3363
3364    return $updated;
3365    }
3366
3367
3368  /**
3369   * Increde/decrese messagecount for a specific mailbox
3370   * @access private
3371   */
3372  private function _set_messagecount($mbox_name, $mode, $increment)
3373    {
3374    $a_mailbox_cache = FALSE;
3375    $mailbox = $mbox_name ? $mbox_name : $this->mailbox;
3376    $mode = strtoupper($mode);
3377
3378    $a_mailbox_cache = $this->get_cache('messagecount');
3379   
3380    if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
3381      return FALSE;
3382   
3383    // add incremental value to messagecount
3384    $a_mailbox_cache[$mailbox][$mode] += $increment;
3385   
3386    // there's something wrong, delete from cache
3387    if ($a_mailbox_cache[$mailbox][$mode] < 0)
3388      unset($a_mailbox_cache[$mailbox][$mode]);
3389
3390    // write back to cache
3391    $this->update_cache('messagecount', $a_mailbox_cache);
3392   
3393    return TRUE;
3394    }
3395
3396
3397  /**
3398   * Remove messagecount of a specific mailbox from cache
3399   * @access private
3400   */
3401  private function _clear_messagecount($mbox_name='')
3402    {
3403    $a_mailbox_cache = FALSE;
3404    $mailbox = $mbox_name ? $mbox_name : $this->mailbox;
3405
3406    $a_mailbox_cache = $this->get_cache('messagecount');
3407
3408    if (is_array($a_mailbox_cache[$mailbox]))
3409      {
3410      unset($a_mailbox_cache[$mailbox]);
3411      $this->update_cache('messagecount', $a_mailbox_cache);
3412      }
3413    }
3414
3415
3416  /**
3417   * Split RFC822 header string into an associative array
3418   * @access private
3419   */
3420  private function _parse_headers($headers)
3421    {
3422    $a_headers = array();
3423    $lines = explode("\n", $headers);
3424    $c = count($lines);
3425    for ($i=0; $i<$c; $i++)
3426      {
3427      if ($p = strpos($lines[$i], ': '))
3428        {
3429        $field = strtolower(substr($lines[$i], 0, $p));
3430        $value = trim(substr($lines[$i], $p+1));
3431        if (!empty($value))
3432          $a_headers[$field] = $value;
3433        }
3434      }
3435   
3436    return $a_headers;
3437    }
3438
3439
3440  /**
3441   * @access private
3442   */
3443  private function _parse_address_list($str, $decode=true)
3444    {
3445    // remove any newlines and carriage returns before
3446    $a = rcube_explode_quoted_string('[,;]', preg_replace( "/[\r\n]/", " ", $str));
3447    $result = array();
3448
3449    foreach ($a as $key => $val)
3450      {
3451      $val = preg_replace("/([\"\w])</", "$1 <", $val);
3452      $sub_a = rcube_explode_quoted_string(' ', $decode ? $this->decode_header($val) : $val);
3453      $result[$key]['name'] = '';
3454
3455      foreach ($sub_a as $k => $v)
3456        {
3457        // use angle brackets in regexp to not handle names with @ sign
3458        if (preg_match('/^<\S+@\S+>$/', $v))
3459          $result[$key]['address'] = trim($v, '<>');
3460        else
3461          $result[$key]['name'] .= (empty($result[$key]['name'])?'':' ').str_replace("\"",'',stripslashes($v));
3462        }
3463       
3464      if (empty($result[$key]['name']))
3465        $result[$key]['name'] = $result[$key]['address'];       
3466      elseif (empty($result[$key]['address']))
3467        $result[$key]['address'] = $result[$key]['name'];
3468      }
3469   
3470    return $result;
3471    }
3472
3473}  // end class rcube_imap
3474
3475
3476/**
3477 * Class representing a message part
3478 *
3479 * @package Mail
3480 */
3481class rcube_message_part
3482{
3483  var $mime_id = '';
3484  var $ctype_primary = 'text';
3485  var $ctype_secondary = 'plain';
3486  var $mimetype = 'text/plain';
3487  var $disposition = '';
3488  var $filename = '';
3489  var $encoding = '8bit';
3490  var $charset = '';
3491  var $size = 0;
3492  var $headers = array();
3493  var $d_parameters = array();
3494  var $ctype_parameters = array();
3495
3496  function __clone()
3497  {
3498    if (isset($this->parts))
3499      foreach ($this->parts as $idx => $part)
3500        if (is_object($part))
3501          $this->parts[$idx] = clone $part;
3502  }                             
3503}
3504
3505
3506/**
3507 * Class for sorting an array of iilBasicHeader objects in a predetermined order.
3508 *
3509 * @package Mail
3510 * @author Eric Stadtherr
3511 */
3512class rcube_header_sorter
3513{
3514   var $sequence_numbers = array();
3515   
3516   /**
3517    * Set the predetermined sort order.
3518    *
3519    * @param array Numerically indexed array of IMAP message sequence numbers
3520    */
3521   function set_sequence_numbers($seqnums)
3522   {
3523      $this->sequence_numbers = array_flip($seqnums);
3524   }
3525 
3526   /**
3527    * Sort the array of header objects
3528    *
3529    * @param array Array of iilBasicHeader objects indexed by UID
3530    */
3531   function sort_headers(&$headers)
3532   {
3533      /*
3534       * uksort would work if the keys were the sequence number, but unfortunately
3535       * the keys are the UIDs.  We'll use uasort instead and dereference the value
3536       * to get the sequence number (in the "id" field).
3537       *
3538       * uksort($headers, array($this, "compare_seqnums"));
3539       */
3540       uasort($headers, array($this, "compare_seqnums"));
3541   }
3542 
3543   /**
3544    * Sort method called by uasort()
3545    */
3546   function compare_seqnums($a, $b)
3547   {
3548      // First get the sequence number from the header object (the 'id' field).
3549      $seqa = $a->id;
3550      $seqb = $b->id;
3551     
3552      // then find each sequence number in my ordered list
3553      $posa = isset($this->sequence_numbers[$seqa]) ? intval($this->sequence_numbers[$seqa]) : -1;
3554      $posb = isset($this->sequence_numbers[$seqb]) ? intval($this->sequence_numbers[$seqb]) : -1;
3555     
3556      // return the relative position as the comparison value
3557      return $posa - $posb;
3558   }
3559}
Note: See TracBrowser for help on using the repository browser.