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

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