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