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