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