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