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