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