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