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