source: subversion/trunk/roundcubemail/program/include/rcube_imap.php @ 2861

Last change on this file since 2861 was 2861, checked in by thomasb, 4 years ago

Changed imap_connect hook according to suggestions in #1485956

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