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

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