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