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

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