alecpl
2012-01-05 1c4f23d6e58e12f93d8de2c3ae416df575e8ad85
commit | author | age
4e17e6 1 <?php
T 2
3 /*
4  +-----------------------------------------------------------------------+
47124c 5  | program/include/rcube_imap.php                                        |
4e17e6 6  |                                                                       |
e019f2 7  | This file is part of the Roundcube Webmail client                     |
1a716d 8  | Copyright (C) 2005-2011, The Roundcube Dev Team                       |
T 9  | Copyright (C) 2011, Kolab Systems AG                                  |
30233b 10  | Licensed under the GNU GPL                                            |
4e17e6 11  |                                                                       |
T 12  | PURPOSE:                                                              |
59c216 13  |   IMAP Engine                                                         |
4e17e6 14  |                                                                       |
T 15  +-----------------------------------------------------------------------+
16  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
59c216 17  | Author: Aleksander Machniak <alec@alec.pl>                            |
4e17e6 18  +-----------------------------------------------------------------------+
T 19
20  $Id$
21
22 */
23
24
15a9d1 25 /**
T 26  * Interface class for accessing an IMAP server
27  *
6d969b 28  * @package    Mail
15a9d1 29  * @author     Thomas Bruederli <roundcube@gmail.com>
c43517 30  * @author     Aleksander Machniak <alec@alec.pl>
d062db 31  * @version    2.0
15a9d1 32  */
4e17e6 33 class rcube_imap
6d969b 34 {
59c216 35     public $skip_deleted = false;
A 36     public $page_size = 10;
37     public $list_page = 1;
38     public $threading = false;
39     public $fetch_add_headers = '';
103ddc 40     public $get_all_headers = false;
f52c93 41
5c461b 42     /**
A 43      * Instance of rcube_imap_generic
44      *
45      * @var rcube_imap_generic
46      */
47     public $conn;
48
49     /**
80152b 50      * Instance of rcube_imap_cache
5c461b 51      *
80152b 52      * @var rcube_imap_cache
5c461b 53      */
80152b 54     private $mcache;
5cf5ee 55
A 56     /**
57      * Instance of rcube_cache
58      *
59      * @var rcube_cache
60      */
61     private $cache;
80152b 62
A 63     /**
64      * Internal (in-memory) cache
65      *
66      * @var array
67      */
68     private $icache = array();
69
59c216 70     private $mailbox = 'INBOX';
00290a 71     private $delimiter = NULL;
A 72     private $namespace = NULL;
59c216 73     private $sort_field = '';
A 74     private $sort_order = 'DESC';
75     private $default_charset = 'ISO-8859-1';
76     private $struct_charset = NULL;
77     private $default_folders = array('INBOX');
78     private $uid_id_map = array();
79     private $msg_headers = array();
80     public  $search_set = NULL;
81     public  $search_string = '';
82     private $search_charset = '';
83     private $search_sort_field = '';
84     private $search_threads = false;
c60978 85     private $search_sorted = false;
59c216 86     private $options = array('auth_method' => 'check');
A 87     private $host, $user, $pass, $port, $ssl;
341d96 88     private $caching = false;
80152b 89     private $messages_caching = false;
103ddc 90
A 91     /**
92      * All (additional) headers used (in any way) by Roundcube
8bce65 93      * Not listed here: DATE, FROM, TO, CC, REPLY-TO, SUBJECT, CONTENT-TYPE, LIST-POST
103ddc 94      * (used for messages listing) are hardcoded in rcube_imap_generic::fetchHeaders()
A 95      *
96      * @var array
97      * @see rcube_imap::fetch_add_headers
98      */
99     private $all_headers = array(
100         'IN-REPLY-TO',
101         'BCC',
102         'MESSAGE-ID',
103         'CONTENT-TRANSFER-ENCODING',
104         'REFERENCES',
105         'X-DRAFT-INFO',
106         'MAIL-FOLLOWUP-TO',
107         'MAIL-REPLY-TO',
108         'RETURN-PATH',
109     );
4e17e6 110
90f81a 111     const UNKNOWN       = 0;
A 112     const NOPERM        = 1;
113     const READONLY      = 2;
114     const TRYCREATE     = 3;
115     const INUSE         = 4;
116     const OVERQUOTA     = 5;
117     const ALREADYEXISTS = 6;
118     const NONEXISTENT   = 7;
119     const CONTACTADMIN  = 8;
120
4e17e6 121
59c216 122     /**
5cf5ee 123      * Object constructor.
59c216 124      */
5cf5ee 125     function __construct()
4e17e6 126     {
59c216 127         $this->conn = new rcube_imap_generic();
68070e 128
A 129         // Set namespace and delimiter from session,
130         // so some methods would work before connection
131         if (isset($_SESSION['imap_namespace']))
132             $this->namespace = $_SESSION['imap_namespace'];
133         if (isset($_SESSION['imap_delimiter']))
134             $this->delimiter = $_SESSION['imap_delimiter'];
4e17e6 135     }
T 136
30b302 137
59c216 138     /**
A 139      * Connect to an IMAP server
140      *
5c461b 141      * @param  string   $host    Host to connect
A 142      * @param  string   $user    Username for IMAP account
143      * @param  string   $pass    Password for IMAP account
144      * @param  integer  $port    Port to connect to
145      * @param  string   $use_ssl SSL schema (either ssl or tls) or null if plain connection
59c216 146      * @return boolean  TRUE on success, FALSE on failure
A 147      * @access public
148      */
149     function connect($host, $user, $pass, $port=143, $use_ssl=null)
4e17e6 150     {
175d8e 151         // check for OpenSSL support in PHP build
59c216 152         if ($use_ssl && extension_loaded('openssl'))
A 153             $this->options['ssl_mode'] = $use_ssl == 'imaps' ? 'ssl' : $use_ssl;
154         else if ($use_ssl) {
155             raise_error(array('code' => 403, 'type' => 'imap',
156                 'file' => __FILE__, 'line' => __LINE__,
157                 'message' => "OpenSSL not available"), true, false);
158             $port = 143;
159         }
4e17e6 160
59c216 161         $this->options['port'] = $port;
f28124 162
890eae 163         if ($this->options['debug']) {
7f1da4 164             $this->conn->setDebug(true, array($this, 'debug_handler'));
A 165
890eae 166             $this->options['ident'] = array(
A 167                 'name' => 'Roundcube Webmail',
168                 'version' => RCMAIL_VERSION,
169                 'php' => PHP_VERSION,
170                 'os' => PHP_OS,
171                 'command' => $_SERVER['REQUEST_URI'],
172             );
173         }
174
59c216 175         $attempt = 0;
A 176         do {
177             $data = rcmail::get_instance()->plugins->exec_hook('imap_connect',
e7e794 178                 array_merge($this->options, array('host' => $host, 'user' => $user,
A 179                     'attempt' => ++$attempt)));
c43517 180
59c216 181             if (!empty($data['pass']))
A 182                 $pass = $data['pass'];
b026c3 183
e7e794 184             $this->conn->connect($data['host'], $data['user'], $pass, $data);
59c216 185         } while(!$this->conn->connected() && $data['retry']);
80fbda 186
59c216 187         $this->host = $data['host'];
A 188         $this->user = $data['user'];
189         $this->pass = $pass;
190         $this->port = $port;
191         $this->ssl  = $use_ssl;
520c36 192
59c216 193         if ($this->conn->connected()) {
00290a 194             // get namespace and delimiter
A 195             $this->set_env();
94a6c6 196             return true;
59c216 197         }
A 198         // write error log
199         else if ($this->conn->error) {
ad399a 200             if ($pass && $user) {
A 201                 $message = sprintf("Login failed for %s from %s. %s",
202                     $user, rcmail_remote_ip(), $this->conn->error);
203
b36491 204                 raise_error(array('code' => 403, 'type' => 'imap',
A 205                     'file' => __FILE__, 'line' => __LINE__,
ad399a 206                     'message' => $message), true, false);
A 207             }
59c216 208         }
A 209
94a6c6 210         return false;
4e17e6 211     }
T 212
30b302 213
59c216 214     /**
A 215      * Close IMAP connection
216      * Usually done on script shutdown
217      *
218      * @access public
219      */
220     function close()
c43517 221     {
e232ac 222         $this->conn->closeConnection();
80152b 223         if ($this->mcache)
A 224             $this->mcache->close();
4e17e6 225     }
T 226
30b302 227
59c216 228     /**
A 229      * Close IMAP connection and re-connect
230      * This is used to avoid some strange socket errors when talking to Courier IMAP
231      *
232      * @access public
233      */
234     function reconnect()
520c36 235     {
dd8354 236         $this->conn->closeConnection();
A 237         $connected = $this->connect($this->host, $this->user, $this->pass, $this->port, $this->ssl);
c43517 238
59c216 239         // issue SELECT command to restore connection status
dd8354 240         if ($connected && strlen($this->mailbox))
59c216 241             $this->conn->select($this->mailbox);
520c36 242     }
T 243
8fcc3e 244
A 245     /**
246      * Returns code of last error
247      *
248      * @return int Error code
249      */
250     function get_error_code()
251     {
7f1da4 252         return $this->conn->errornum;
8fcc3e 253     }
A 254
255
256     /**
257      * Returns message of last error
258      *
259      * @return string Error message
260      */
261     function get_error_str()
262     {
7f1da4 263         return $this->conn->error;
90f81a 264     }
A 265
266
267     /**
268      * Returns code of last command response
269      *
270      * @return int Response code
271      */
272     function get_response_code()
273     {
274         switch ($this->conn->resultcode) {
275             case 'NOPERM':
276                 return self::NOPERM;
277             case 'READ-ONLY':
278                 return self::READONLY;
279             case 'TRYCREATE':
280                 return self::TRYCREATE;
281             case 'INUSE':
282                 return self::INUSE;
283             case 'OVERQUOTA':
284                 return self::OVERQUOTA;
285             case 'ALREADYEXISTS':
286                 return self::ALREADYEXISTS;
287             case 'NONEXISTENT':
288                 return self::NONEXISTENT;
289             case 'CONTACTADMIN':
290                 return self::CONTACTADMIN;
291             default:
292                 return self::UNKNOWN;
293         }
294     }
295
296
297     /**
298      * Returns last command response
299      *
300      * @return string Response
301      */
302     function get_response_str()
303     {
7f1da4 304         return $this->conn->result;
8fcc3e 305     }
30b302 306
8fcc3e 307
59c216 308     /**
A 309      * Set options to be used in rcube_imap_generic::connect()
5c461b 310      *
A 311      * @param array $opt Options array
59c216 312      */
A 313     function set_options($opt)
4e17e6 314     {
59c216 315         $this->options = array_merge($this->options, (array)$opt);
17b5fb 316     }
T 317
30b302 318
59c216 319     /**
06744d 320      * Activate/deactivate debug mode
T 321      *
322      * @param boolean $dbg True if IMAP conversation should be logged
323      * @access public
324      */
325     function set_debug($dbg = true)
326     {
327         $this->options['debug'] = $dbg;
328         $this->conn->setDebug($dbg, array($this, 'debug_handler'));
329     }
330
331
332     /**
59c216 333      * Set default message charset
A 334      *
335      * This will be used for message decoding if a charset specification is not available
336      *
5c461b 337      * @param  string $cs Charset string
59c216 338      * @access public
A 339      */
340     function set_charset($cs)
17b5fb 341     {
59c216 342         $this->default_charset = $cs;
4e17e6 343     }
T 344
30b302 345
59c216 346     /**
A 347      * This list of folders will be listed above all other folders
348      *
5c461b 349      * @param  array $arr Indexed list of folder names
59c216 350      * @access public
A 351      */
352     function set_default_mailboxes($arr)
4e17e6 353     {
59c216 354         if (is_array($arr)) {
A 355             $this->default_folders = $arr;
fa4cd2 356
59c216 357             // add inbox if not included
A 358             if (!in_array('INBOX', $this->default_folders))
359                 array_unshift($this->default_folders, 'INBOX');
360         }
4e17e6 361     }
T 362
30b302 363
59c216 364     /**
A 365      * Set internal mailbox reference.
366      *
367      * All operations will be perfomed on this mailbox/folder
368      *
d08333 369      * @param  string $mailbox Mailbox/Folder name
59c216 370      * @access public
A 371      */
d08333 372     function set_mailbox($mailbox)
4e17e6 373     {
59c216 374         if ($this->mailbox == $mailbox)
A 375             return;
4e17e6 376
59c216 377         $this->mailbox = $mailbox;
4e17e6 378
59c216 379         // clear messagecount cache for this mailbox
A 380         $this->_clear_messagecount($mailbox);
4e17e6 381     }
T 382
30b302 383
A 384     /**
59c216 385      * Set internal list page
A 386      *
5c461b 387      * @param  number $page Page number to list
59c216 388      * @access public
A 389      */
390     function set_page($page)
4e17e6 391     {
59c216 392         $this->list_page = (int)$page;
4e17e6 393     }
T 394
30b302 395
59c216 396     /**
A 397      * Set internal page size
398      *
5c461b 399      * @param  number $size Number of messages to display on one page
59c216 400      * @access public
A 401      */
402     function set_pagesize($size)
4e17e6 403     {
59c216 404         $this->page_size = (int)$size;
4e17e6 405     }
c43517 406
30b302 407
59c216 408     /**
1c4f23 409      * Save a search result for future message listing methods
59c216 410      *
1c4f23 411      * @param  array  $set  Search set, result from rcube_imap::get_search_set():
A 412      *                      0 - searching criteria, string
413      *                      1 - search result, rcube_result_index|rcube_result_thread
414      *                      2 - searching character set, string
415      *                      3 - sorting field, string
416      *                      4 - true if sorted, bool
59c216 417      */
1c4f23 418     function set_search_set($set)
04c618 419     {
1c4f23 420         $set = (array)$set;
f52c93 421
1c4f23 422         $this->search_string     = $set[0];
A 423         $this->search_set        = $set[1];
424         $this->search_charset    = $set[2];
425         $this->search_sort_field = $set[3];
426         $this->search_sorted     = $set[4];
40c45e 427         $this->search_threads    = is_a($this->search_set, 'rcube_result_thread');
04c618 428     }
T 429
30b302 430
59c216 431     /**
A 432      * Return the saved search set as hash array
40c45e 433      *
59c216 434      * @return array Search set
A 435      */
436     function get_search_set()
04c618 437     {
40c45e 438         return array(
A 439             $this->search_string,
59c216 440             $this->search_set,
A 441             $this->search_charset,
442             $this->search_sort_field,
c60978 443             $this->search_sorted,
59c216 444         );
04c618 445     }
4e17e6 446
30b302 447
59c216 448     /**
A 449      * Returns the currently used mailbox name
450      *
451      * @return  string Name of the mailbox/folder
452      * @access  public
453      */
454     function get_mailbox_name()
4e17e6 455     {
46cdbf 456         return $this->mailbox;
1cded8 457     }
T 458
30b302 459
59c216 460     /**
A 461      * Returns the IMAP server's capability
462      *
5c461b 463      * @param   string  $cap Capability name
59c216 464      * @return  mixed   Capability value or TRUE if supported, FALSE if not
A 465      * @access  public
466      */
467     function get_capability($cap)
1cded8 468     {
59c216 469         return $this->conn->getCapability(strtoupper($cap));
f52c93 470     }
T 471
30b302 472
59c216 473     /**
A 474      * Sets threading flag to the best supported THREAD algorithm
475      *
5c461b 476      * @param  boolean  $enable TRUE to enable and FALSE
59c216 477      * @return string   Algorithm or false if THREAD is not supported
A 478      * @access public
479      */
480     function set_threading($enable=false)
f52c93 481     {
59c216 482         $this->threading = false;
c43517 483
600bb1 484         if ($enable && ($caps = $this->get_capability('THREAD'))) {
A 485             if (in_array('REFS', $caps))
59c216 486                 $this->threading = 'REFS';
600bb1 487             else if (in_array('REFERENCES', $caps))
59c216 488                 $this->threading = 'REFERENCES';
600bb1 489             else if (in_array('ORDEREDSUBJECT', $caps))
59c216 490                 $this->threading = 'ORDEREDSUBJECT';
A 491         }
492
493         return $this->threading;
494     }
495
30b302 496
59c216 497     /**
A 498      * Checks the PERMANENTFLAGS capability of the current mailbox
499      * and returns true if the given flag is supported by the IMAP server
500      *
5c461b 501      * @param   string  $flag Permanentflag name
29983c 502      * @return  boolean True if this flag is supported
59c216 503      * @access  public
A 504      */
505     function check_permflag($flag)
506     {
507         $flag = strtoupper($flag);
508         $imap_flag = $this->conn->flags[$flag];
a2e8cb 509         return (in_array_nocase($imap_flag, $this->conn->data['PERMANENTFLAGS']));
59c216 510     }
A 511
30b302 512
59c216 513     /**
A 514      * Returns the delimiter that is used by the IMAP server for folder separation
515      *
516      * @return  string  Delimiter string
517      * @access  public
518      */
519     function get_hierarchy_delimiter()
520     {
521         return $this->delimiter;
00290a 522     }
A 523
524
525     /**
526      * Get namespace
527      *
d08333 528      * @param string $name Namespace array index: personal, other, shared, prefix
A 529      *
00290a 530      * @return  array  Namespace data
A 531      * @access  public
532      */
d08333 533     function get_namespace($name=null)
00290a 534     {
d08333 535         $ns = $this->namespace;
A 536
537         if ($name) {
538             return isset($ns[$name]) ? $ns[$name] : null;
539         }
540
541         unset($ns['prefix']);
542         return $ns;
00290a 543     }
A 544
545
546     /**
547      * Sets delimiter and namespaces
548      *
549      * @access private
550      */
551     private function set_env()
552     {
553         if ($this->delimiter !== null && $this->namespace !== null) {
554             return;
555         }
556
557         $config = rcmail::get_instance()->config;
558         $imap_personal  = $config->get('imap_ns_personal');
559         $imap_other     = $config->get('imap_ns_other');
560         $imap_shared    = $config->get('imap_ns_shared');
561         $imap_delimiter = $config->get('imap_delimiter');
562
7f1da4 563         if (!$this->conn->connected())
00290a 564             return;
A 565
566         $ns = $this->conn->getNamespace();
567
02491a 568         // Set namespaces (NAMESPACE supported)
00290a 569         if (is_array($ns)) {
A 570             $this->namespace = $ns;
571         }
02491a 572         else {
00290a 573             $this->namespace = array(
A 574                 'personal' => NULL,
575                 'other'    => NULL,
576                 'shared'   => NULL,
577             );
02491a 578         }
00290a 579
02491a 580         if ($imap_delimiter) {
A 581             $this->delimiter = $imap_delimiter;
582         }
583         if (empty($this->delimiter)) {
584             $this->delimiter = $this->namespace['personal'][0][1];
585         }
586         if (empty($this->delimiter)) {
587             $this->delimiter = $this->conn->getHierarchyDelimiter();
588         }
589         if (empty($this->delimiter)) {
590             $this->delimiter = '/';
591         }
592
593         // Overwrite namespaces
594         if ($imap_personal !== null) {
595             $this->namespace['personal'] = NULL;
596             foreach ((array)$imap_personal as $dir) {
597                 $this->namespace['personal'][] = array($dir, $this->delimiter);
598             }
599         }
600         if ($imap_other !== null) {
601             $this->namespace['other'] = NULL;
602             foreach ((array)$imap_other as $dir) {
603                 if ($dir) {
604                     $this->namespace['other'][] = array($dir, $this->delimiter);
00290a 605                 }
A 606             }
02491a 607         }
A 608         if ($imap_shared !== null) {
609             $this->namespace['shared'] = NULL;
610             foreach ((array)$imap_shared as $dir) {
611                 if ($dir) {
612                     $this->namespace['shared'][] = array($dir, $this->delimiter);
00290a 613                 }
A 614             }
615         }
616
d08333 617         // Find personal namespace prefix for mod_mailbox()
A 618         // Prefix can be removed when there is only one personal namespace
619         if (is_array($this->namespace['personal']) && count($this->namespace['personal']) == 1) {
620             $this->namespace['prefix'] = $this->namespace['personal'][0][0];
621         }
622
00290a 623         $_SESSION['imap_namespace'] = $this->namespace;
A 624         $_SESSION['imap_delimiter'] = $this->delimiter;
59c216 625     }
A 626
30b302 627
59c216 628     /**
A 629      * Get message count for a specific mailbox
630      *
d08333 631      * @param  string  $mailbox Mailbox/folder name
A 632      * @param  string  $mode    Mode for count [ALL|THREADS|UNSEEN|RECENT]
633      * @param  boolean $force   Force reading from server and update cache
634      * @param  boolean $status  Enables storing folder status info (max UID/count),
635      *                          required for mailbox_status()
a03c98 636      * @return int     Number of messages
A 637      * @access public
59c216 638      */
d08333 639     function messagecount($mailbox='', $mode='ALL', $force=false, $status=true)
59c216 640     {
d08333 641         if (!strlen($mailbox)) {
A 642             $mailbox = $this->mailbox;
643         }
644
488074 645         return $this->_messagecount($mailbox, $mode, $force, $status);
59c216 646     }
A 647
30b302 648
59c216 649     /**
A 650      * Private method for getting nr of messages
651      *
5c461b 652      * @param string  $mailbox Mailbox name
A 653      * @param string  $mode    Mode for count [ALL|THREADS|UNSEEN|RECENT]
654      * @param boolean $force   Force reading from server and update cache
655      * @param boolean $status  Enables storing folder status info (max UID/count),
656      *                         required for mailbox_status()
657      * @return int Number of messages
59c216 658      * @access  private
A 659      * @see     rcube_imap::messagecount()
660      */
d08333 661     private function _messagecount($mailbox, $mode='ALL', $force=false, $status=true)
59c216 662     {
A 663         $mode = strtoupper($mode);
664
665         // count search set
666         if ($this->search_string && $mailbox == $this->mailbox && ($mode == 'ALL' || $mode == 'THREADS') && !$force) {
40c45e 667             if ($mode == 'ALL')
A 668                 return $this->search_set->countMessages();
59c216 669             else
40c45e 670                 return $this->search_set->count();
59c216 671         }
c43517 672
59c216 673         $a_mailbox_cache = $this->get_cache('messagecount');
c43517 674
59c216 675         // return cached value
A 676         if (!$force && is_array($a_mailbox_cache[$mailbox]) && isset($a_mailbox_cache[$mailbox][$mode]))
677             return $a_mailbox_cache[$mailbox][$mode];
678
679         if (!is_array($a_mailbox_cache[$mailbox]))
680             $a_mailbox_cache[$mailbox] = array();
681
682         if ($mode == 'THREADS') {
40c45e 683             $res   = $this->fetch_threads($mailbox, $force);
A 684             $count = $res->count();
c26b39 685
488074 686             if ($status) {
40c45e 687                 $msg_count = $res->countMessages();
A 688                 $this->set_folder_stats($mailbox, 'cnt', $msg_count);
689                 $this->set_folder_stats($mailbox, 'maxuid', $msg_count ? $this->id2uid($msg_count, $mailbox) : 0);
488074 690             }
59c216 691         }
A 692         // RECENT count is fetched a bit different
693         else if ($mode == 'RECENT') {
e232ac 694             $count = $this->conn->countRecent($mailbox);
59c216 695         }
A 696         // use SEARCH for message counting
697         else if ($this->skip_deleted) {
698             $search_str = "ALL UNDELETED";
659cf1 699             $keys       = array('COUNT');
59c216 700
659cf1 701             if ($mode == 'UNSEEN') {
59c216 702                 $search_str .= " UNSEEN";
659cf1 703             }
9ae29c 704             else {
5cf5ee 705                 if ($this->messages_caching) {
9ae29c 706                     $keys[] = 'ALL';
A 707                 }
708                 if ($status) {
709                     $keys[]   = 'MAX';
710                 }
659cf1 711             }
c43517 712
40c45e 713             // @TODO: if $force==false && $mode == 'ALL' we could try to use cache index here
A 714
659cf1 715             // get message count using (E)SEARCH
A 716             // not very performant but more precise (using UNDELETED)
40c45e 717             $index = $this->conn->search($mailbox, $search_str, true, $keys);
A 718             $count = $index->count();
59c216 719
9ae29c 720             if ($mode == 'ALL') {
40c45e 721                 // Cache index data, will be used in message_index_direct()
A 722                 $this->icache['undeleted_idx'] = $index;
723
9ae29c 724                 if ($status) {
A 725                     $this->set_folder_stats($mailbox, 'cnt', $count);
40c45e 726                     $this->set_folder_stats($mailbox, 'maxuid', $index->max());
9ae29c 727                 }
488074 728             }
59c216 729         }
A 730         else {
731             if ($mode == 'UNSEEN')
732                 $count = $this->conn->countUnseen($mailbox);
733             else {
734                 $count = $this->conn->countMessages($mailbox);
488074 735                 if ($status) {
A 736                     $this->set_folder_stats($mailbox,'cnt', $count);
80152b 737                     $this->set_folder_stats($mailbox, 'maxuid', $count ? $this->id2uid($count, $mailbox) : 0);
488074 738                 }
59c216 739             }
A 740         }
741
742         $a_mailbox_cache[$mailbox][$mode] = (int)$count;
743
744         // write back to cache
745         $this->update_cache('messagecount', $a_mailbox_cache);
746
747         return (int)$count;
4e17e6 748     }
T 749
750
59c216 751     /**
A 752      * Public method for listing headers
753      * convert mailbox name with root dir first
754      *
d08333 755      * @param   string   $mailbox    Mailbox/folder name
5c461b 756      * @param   int      $page       Current page to list
A 757      * @param   string   $sort_field Header field to sort by
758      * @param   string   $sort_order Sort order [ASC|DESC]
759      * @param   int      $slice      Number of slice items to extract from result array
40c45e 760      *
59c216 761      * @return  array    Indexed array with message header objects
c43517 762      * @access  public
59c216 763      */
40c45e 764     public function list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
4e17e6 765     {
d08333 766         if (!strlen($mailbox)) {
A 767             $mailbox = $this->mailbox;
768         }
769
609d39 770         return $this->_list_headers($mailbox, $page, $sort_field, $sort_order, $slice);
4e17e6 771     }
T 772
f13baa 773
59c216 774     /**
A 775      * Private method for listing message headers
776      *
5c461b 777      * @param   string   $mailbox    Mailbox name
A 778      * @param   int      $page       Current page to list
779      * @param   string   $sort_field Header field to sort by
780      * @param   string   $sort_order Sort order [ASC|DESC]
781      * @param   int      $slice      Number of slice items to extract from result array
80152b 782      *
5c461b 783      * @return  array    Indexed array with message header objects
59c216 784      * @see     rcube_imap::list_headers
A 785      */
80152b 786     private function _list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
4e17e6 787     {
40c45e 788         if (!strlen($mailbox)) {
59c216 789             return array();
40c45e 790         }
04c618 791
40c45e 792         $this->set_sort_order($sort_field, $sort_order);
80152b 793         $page = $page ? $page : $this->list_page;
1cded8 794
40c45e 795         // use saved message set
A 796         if ($this->search_string && $mailbox == $this->mailbox) {
797             return $this->_list_header_set($mailbox, $page, $slice);
a96183 798         }
2dd7ee 799
40c45e 800         if ($this->threading) {
A 801             return $this->_list_thread_headers($mailbox, $page, $slice);
802         }
803
804         // get UIDs of all messages in the folder, sorted
805         $index = $this->message_index($mailbox, $this->sort_field, $this->sort_order);
806
807         if ($index->isEmpty()) {
59c216 808             return array();
40c45e 809         }
c43517 810
40c45e 811         $from = ($page-1) * $this->page_size;
A 812         $to   = $from + $this->page_size;
1cded8 813
40c45e 814         $index->slice($from, $to - $from);
A 815
816         if ($slice)
817             $index->slice(-$slice, $slice);
818
819         // fetch reqested messages headers
820         $a_index = $index->get();
821         $a_msg_headers = $this->fetch_headers($mailbox, $a_index);
1cded8 822
59c216 823         return array_values($a_msg_headers);
4e17e6 824     }
7e93ff 825
f13baa 826
59c216 827     /**
A 828      * Private method for listing message headers using threads
829      *
5c461b 830      * @param   string   $mailbox    Mailbox/folder name
A 831      * @param   int      $page       Current page to list
832      * @param   int      $slice      Number of slice items to extract from result array
80152b 833      *
5c461b 834      * @return  array    Indexed array with message header objects
59c216 835      * @see     rcube_imap::list_headers
A 836      */
40c45e 837     private function _list_thread_headers($mailbox, $page, $slice=0)
f52c93 838     {
80152b 839         // get all threads (not sorted)
40c45e 840         if ($mcache = $this->get_mcache_engine())
A 841             $threads = $mcache->get_thread($mailbox);
80152b 842         else
40c45e 843             $threads = $this->fetch_threads($mailbox);
f52c93 844
40c45e 845         return $this->_fetch_thread_headers($mailbox, $threads, $page, $slice);
f52c93 846     }
T 847
848
59c216 849     /**
80152b 850      * Method for fetching threads data
59c216 851      *
80152b 852      * @param  string $mailbox  Folder name
A 853      * @param  bool   $force    Use IMAP server, no cache
854      *
40c45e 855      * @return rcube_imap_thread Thread data object
59c216 856      */
80152b 857     function fetch_threads($mailbox, $force = false)
f52c93 858     {
80152b 859         if (!$force && ($mcache = $this->get_mcache_engine())) {
A 860             // don't store in self's internal cache, cache has it's own internal cache
861             return $mcache->get_thread($mailbox);
862         }
863
59c216 864         if (empty($this->icache['threads'])) {
A 865             // get all threads
e9a974 866             $result = $this->conn->thread($mailbox, $this->threading,
40c45e 867                 $this->skip_deleted ? 'UNDELETED' : '', true);
c43517 868
59c216 869             // add to internal (fast) cache
40c45e 870             $this->icache['threads'] = $result;
f52c93 871         }
T 872
40c45e 873         return $this->icache['threads'];
f52c93 874     }
T 875
876
59c216 877     /**
A 878      * Private method for fetching threaded messages headers
879      *
40c45e 880      * @param string              $mailbox    Mailbox name
A 881      * @param rcube_result_thread $threads    Threads data object
882      * @param int                 $page       List page number
883      * @param int                 $slice      Number of threads to slice
80152b 884      *
29983c 885      * @return array  Messages headers
59c216 886      * @access  private
A 887      */
40c45e 888     private function _fetch_thread_headers($mailbox, $threads, $page, $slice=0)
4647e1 889     {
40c45e 890         // Sort thread structure
A 891         $this->sort_threads($threads);
892
893         $from = ($page-1) * $this->page_size;
894         $to   = $from + $this->page_size;
895
896         $threads->slice($from, $to - $from);
4647e1 897
0b6e97 898         if ($slice)
40c45e 899             $threads->slice(-$slice, $slice);
59c216 900
40c45e 901         // Get UIDs of all messages in all threads
A 902         $a_index = $threads->get();
59c216 903
A 904         // fetch reqested headers from server
40c45e 905         $a_msg_headers = $this->fetch_headers($mailbox, $a_index);
0803fb 906
40c45e 907         unset($a_index);
84b884 908
59c216 909         // Set depth, has_children and unread_children fields in headers
40c45e 910         $this->_set_thread_flags($a_msg_headers, $threads);
59c216 911
84b884 912         return array_values($a_msg_headers);
59c216 913     }
84b884 914
d15d59 915
59c216 916     /**
A 917      * Private method for setting threaded messages flags:
918      * depth, has_children and unread_children
919      *
40c45e 920      * @param  array             $headers Reference to headers array indexed by message UID
A 921      * @param  rcube_imap_result $threads Threads data object
922      *
923      * @return array Message headers array indexed by message UID
59c216 924      * @access private
A 925      */
40c45e 926     private function _set_thread_flags(&$headers, $threads)
59c216 927     {
A 928         $parents = array();
0b6e97 929
40c45e 930         list ($msg_depth, $msg_children) = $threads->getThreadData();
A 931
932         foreach ($headers as $uid => $header) {
933             $depth = $msg_depth[$uid];
59c216 934             $parents = array_slice($parents, 0, $depth);
A 935
936             if (!empty($parents)) {
40c45e 937                 $headers[$uid]->parent_uid = end($parents);
609d39 938                 if (empty($header->flags['SEEN']))
59c216 939                     $headers[$parents[0]]->unread_children++;
A 940             }
40c45e 941             array_push($parents, $uid);
59c216 942
40c45e 943             $headers[$uid]->depth = $depth;
A 944             $headers[$uid]->has_children = $msg_children[$uid];
84b884 945         }
f52c93 946     }
T 947
948
59c216 949     /**
A 950      * Private method for listing a set of message headers (search results)
951      *
40c45e 952      * @param   string   $mailbox  Mailbox/folder name
A 953      * @param   int      $page     Current page to list
954      * @param   int      $slice    Number of slice items to extract from result array
955      *
59c216 956      * @return  array    Indexed array with message header objects
A 957      * @access  private
958      */
40c45e 959     private function _list_header_set($mailbox, $page, $slice=0)
f52c93 960     {
40c45e 961         if (!strlen($mailbox) || empty($this->search_set) || $this->search_set->isEmpty()) {
59c216 962             return array();
40c45e 963         }
f52c93 964
59c216 965         // use saved messages from searching
40c45e 966         if ($this->threading) {
A 967             return $this->_list_thread_header_set($mailbox, $page, $slice);
968         }
f52c93 969
59c216 970         // search set is threaded, we need a new one
f22b54 971         if ($this->search_threads) {
40c45e 972             $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
f22b54 973         }
c43517 974
40c45e 975         $index = clone $this->search_set;
A 976         $from  = ($page-1) * $this->page_size;
977         $to    = $from + $this->page_size;
a96183 978
40c45e 979         // return empty array if no messages found
A 980         if ($index->isEmpty())
981             return array();
a96183 982
59c216 983         // quickest method (default sorting)
A 984         if (!$this->search_sort_field && !$this->sort_field) {
40c45e 985             $got_index = true;
A 986         }
987         // sorted messages, so we can first slice array and then fetch only wanted headers
988         else if ($this->search_sorted) { // SORT searching result
989             $got_index = true;
990             // reset search set if sorting field has been changed
991             if ($this->sort_field && $this->search_sort_field != $this->sort_field) {
992                 $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
993
994                 $index = clone $this->search_set;
995
996                 // return empty array if no messages found
997                 if ($index->isEmpty())
998                     return array();
999             }
1000         }
1001
1002         if ($got_index) {
1003             if ($this->sort_order != $index->getParameters('ORDER')) {
1004                 $index->revert();
1005             }
df0da2 1006
59c216 1007             // get messages uids for one page
40c45e 1008             $index->slice($from, $to-$from);
e538b3 1009
59c216 1010             if ($slice)
40c45e 1011                 $index->slice(-$slice, $slice);
84b884 1012
59c216 1013             // fetch headers
40c45e 1014             $a_index       = $index->get();
A 1015             $a_msg_headers = $this->fetch_headers($mailbox, $a_index);
59c216 1016
A 1017             return array_values($a_msg_headers);
1018         }
1019
40c45e 1020         // SEARCH result, need sorting
A 1021         $cnt = $index->count();
1022
1023         // 300: experimantal value for best result
1024         if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) {
1025             // use memory less expensive (and quick) method for big result set
1026             $index = clone $this->message_index('', $this->sort_field, $this->sort_order);
1027             // get messages uids for one page...
1028             $index->slice($start_msg, min($cnt-$from, $this->page_size));
1029
1030             if ($slice)
1031                 $index->slice(-$slice, $slice);
1032
1033             // ...and fetch headers
1034             $a_index       = $index->get();
1035             $a_msg_headers = $this->fetch_headers($mailbox, $a_index);
1036
1037             return array_values($a_msg_headers);
1038         }
1039         else {
1040             // for small result set we can fetch all messages headers
1041             $a_index       = $index->get();
1042             $a_msg_headers = $this->fetch_headers($mailbox, $a_index, false);
59c216 1043
A 1044             // return empty array if no messages found
40c45e 1045             if (!is_array($a_msg_headers) || empty($a_msg_headers))
59c216 1046                 return array();
A 1047
40c45e 1048             // if not already sorted
A 1049             $a_msg_headers = $this->conn->sortHeaders(
1050                 $a_msg_headers, $this->sort_field, $this->sort_order);
59c216 1051
40c45e 1052             // only return the requested part of the set
A 1053             $a_msg_headers = array_slice(array_values($a_msg_headers),
1054                 $from, min($cnt-$to, $this->page_size));
59c216 1055
A 1056             if ($slice)
40c45e 1057                 $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
59c216 1058
40c45e 1059             return $a_msg_headers;
59c216 1060         }
df0da2 1061     }
4e17e6 1062
T 1063
59c216 1064     /**
A 1065      * Private method for listing a set of threaded message headers (search results)
1066      *
5c461b 1067      * @param   string   $mailbox    Mailbox/folder name
A 1068      * @param   int      $page       Current page to list
1069      * @param   int      $slice      Number of slice items to extract from result array
40c45e 1070      *
59c216 1071      * @return  array    Indexed array with message header objects
A 1072      * @access  private
1073      * @see     rcube_imap::list_header_set()
1074      */
40c45e 1075     private function _list_thread_header_set($mailbox, $page, $slice=0)
59c216 1076     {
A 1077         // update search_set if previous data was fetched with disabled threading
f22b54 1078         if (!$this->search_threads) {
40c45e 1079             if ($this->search_set->isEmpty())
f22b54 1080                 return array();
40c45e 1081             $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
f22b54 1082         }
A 1083
40c45e 1084         return $this->_fetch_thread_headers($mailbox, clone $this->search_set, $page, $slice);
59c216 1085     }
A 1086
1087
1088     /**
40c45e 1089      * Fetches messages headers (by UID)
59c216 1090      *
80152b 1091      * @param  string  $mailbox  Mailbox name
40c45e 1092      * @param  array   $msgs     Message UIDs
A 1093      * @param  bool    $sort     Enables result sorting by $msgs
80152b 1094      * @param  bool    $force    Disables cache use
A 1095      *
1096      * @return array Messages headers indexed by UID
59c216 1097      * @access private
A 1098      */
40c45e 1099     function fetch_headers($mailbox, $msgs, $sort = true, $force = false)
59c216 1100     {
80152b 1101         if (empty($msgs))
A 1102             return array();
1103
1104         if (!$force && ($mcache = $this->get_mcache_engine())) {
40c45e 1105             $headers = $mcache->get_messages($mailbox, $msgs);
A 1106         }
1107         else {
1108             // fetch reqested headers from server
1109             $headers = $this->conn->fetchHeaders(
1110                 $mailbox, $msgs, true, false, $this->get_fetch_headers());
80152b 1111         }
A 1112
40c45e 1113         if (empty($headers))
80152b 1114             return array();
59c216 1115
40c45e 1116         foreach ($headers as $h) {
A 1117             $a_msg_headers[$h->uid] = $h;
1118         }
1119
1120         if ($sort) {
1121             // use this class for message sorting
1122             $sorter = new rcube_header_sorter();
1123             $sorter->set_index($msgs);
1124             $sorter->sort_headers($a_msg_headers);
eacce9 1125         }
A 1126
80152b 1127         return $a_msg_headers;
59c216 1128     }
c43517 1129
488074 1130
59c216 1131     /**
488074 1132      * Returns current status of mailbox
59c216 1133      *
A 1134      * We compare the maximum UID to determine the number of
1135      * new messages because the RECENT flag is not reliable.
1136      *
d08333 1137      * @param string $mailbox Mailbox/folder name
488074 1138      * @return int   Folder status
59c216 1139      */
40c45e 1140     public function mailbox_status($mailbox = null)
59c216 1141     {
d08333 1142         if (!strlen($mailbox)) {
A 1143             $mailbox = $this->mailbox;
1144         }
488074 1145         $old = $this->get_folder_stats($mailbox);
A 1146
c43517 1147         // refresh message count -> will update
59c216 1148         $this->_messagecount($mailbox, 'ALL', true);
488074 1149
A 1150         $result = 0;
1cb0d6 1151
A 1152         if (empty($old)) {
1153             return $result;
1154         }
1155
488074 1156         $new = $this->get_folder_stats($mailbox);
A 1157
1158         // got new messages
1159         if ($new['maxuid'] > $old['maxuid'])
1160             $result += 1;
1161         // some messages has been deleted
1162         if ($new['cnt'] < $old['cnt'])
1163             $result += 2;
1164
1165         // @TODO: optional checking for messages flags changes (?)
1166         // @TODO: UIDVALIDITY checking
1167
1168         return $result;
59c216 1169     }
488074 1170
A 1171
1172     /**
1173      * Stores folder statistic data in session
1174      * @TODO: move to separate DB table (cache?)
1175      *
d08333 1176      * @param string $mailbox Mailbox name
A 1177      * @param string $name    Data name
1178      * @param mixed  $data    Data value
488074 1179      */
d08333 1180     private function set_folder_stats($mailbox, $name, $data)
488074 1181     {
d08333 1182         $_SESSION['folders'][$mailbox][$name] = $data;
488074 1183     }
A 1184
1185
1186     /**
1187      * Gets folder statistic data
1188      *
d08333 1189      * @param string $mailbox Mailbox name
A 1190      *
488074 1191      * @return array Stats data
A 1192      */
d08333 1193     private function get_folder_stats($mailbox)
488074 1194     {
d08333 1195         if ($_SESSION['folders'][$mailbox])
A 1196             return (array) $_SESSION['folders'][$mailbox];
488074 1197         else
A 1198             return array();
1199     }
1200
1201
59c216 1202     /**
40c45e 1203      * Return sorted list of message UIDs
59c216 1204      *
d08333 1205      * @param string $mailbox    Mailbox to get index from
5c461b 1206      * @param string $sort_field Sort column
A 1207      * @param string $sort_order Sort order [ASC, DESC]
40c45e 1208      *
A 1209      * @return rcube_result_index|rcube_result_thread List of messages (UIDs)
59c216 1210      */
40c45e 1211     public function message_index($mailbox='', $sort_field=NULL, $sort_order=NULL)
59c216 1212     {
A 1213         if ($this->threading)
d08333 1214             return $this->thread_index($mailbox, $sort_field, $sort_order);
59c216 1215
40c45e 1216         $this->set_sort_order($sort_field, $sort_order);
59c216 1217
d08333 1218         if (!strlen($mailbox)) {
A 1219             $mailbox = $this->mailbox;
1220         }
59c216 1221
A 1222         // we have a saved search result, get index from there
40c45e 1223         if ($this->search_string) {
A 1224             if ($this->search_threads) {
1225                 $this->search($mailbox, $this->search_string, $this->search_charset, $this->sort_field);
59c216 1226             }
A 1227
40c45e 1228             // use message index sort as default sorting
A 1229             if (!$this->sort_field || $this->search_sorted) {
1230                 if ($this->sort_field && $this->search_sort_field != $this->sort_field) {
1231                     $this->search($mailbox, $this->search_string, $this->search_charset, $this->sort_field);
1232                 }
1233                 $index = $this->search_set;
59c216 1234             }
A 1235             else {
40c45e 1236                 $index = $this->conn->index($mailbox, $this->search_set->get(),
A 1237                     $this->sort_field, $this->skip_deleted, true, true);
59c216 1238             }
A 1239
40c45e 1240             if ($this->sort_order != $index->getParameters('ORDER')) {
A 1241                 $index->revert();
1242             }
1243
1244             return $index;
1245         }
59c216 1246
A 1247         // check local cache
80152b 1248         if ($mcache = $this->get_mcache_engine()) {
40c45e 1249             $index = $mcache->get_index($mailbox, $this->sort_field, $this->sort_order);
80152b 1250         }
A 1251         // fetch from IMAP server
1252         else {
40c45e 1253             $index = $this->message_index_direct(
80152b 1254                 $mailbox, $this->sort_field, $this->sort_order);
59c216 1255         }
A 1256
40c45e 1257         return $index;
80152b 1258     }
A 1259
1260
1261     /**
40c45e 1262      * Return sorted list of message UIDs ignoring current search settings.
A 1263      * Doesn't uses cache by default.
80152b 1264      *
A 1265      * @param string $mailbox    Mailbox to get index from
1266      * @param string $sort_field Sort column
1267      * @param string $sort_order Sort order [ASC, DESC]
40c45e 1268      * @param bool   $skip_cache Disables cache usage
80152b 1269      *
40c45e 1270      * @return rcube_result_index Sorted list of message UIDs
80152b 1271      */
40c45e 1272     public function message_index_direct($mailbox, $sort_field = null, $sort_order = null, $skip_cache = true)
80152b 1273     {
40c45e 1274         if (!$skip_cache && ($mcache = $this->get_mcache_engine())) {
A 1275             $index = $mcache->get_index($mailbox, $sort_field, $sort_order);
1276         }
59c216 1277         // use message index sort as default sorting
40c45e 1278         else if (!$sort_field) {
A 1279             if ($this->skip_deleted && !empty($this->icache['undeleted_idx'])
1280                 && $this->icache['undeleted_idx']->getParameters('MAILBOX') == $mailbox
1281             ) {
1282                 $index = $this->icache['undeleted_idx'];
59c216 1283             }
40c45e 1284             else {
A 1285                 $index = $this->conn->search($mailbox,
1286                     'ALL' .($this->skip_deleted ? ' UNDELETED' : ''), true);
1287             }
59c216 1288         }
A 1289         // fetch complete message index
40c45e 1290         else {
A 1291             if ($this->get_capability('SORT')) {
1292                 $index = $this->conn->sort($mailbox, $sort_field,
1293                     $this->skip_deleted ? 'UNDELETED' : '', true);
1294             }
c43517 1295
40c45e 1296             if (empty($index) || $index->isError()) {
A 1297                 $index = $this->conn->index($mailbox, "1:*", $sort_field,
1298                     $this->skip_deleted, false, true);
1299             }
59c216 1300         }
31b2ce 1301
40c45e 1302         if ($sort_order != $index->getParameters('ORDER')) {
A 1303             $index->revert();
1304         }
1305
1306         return $index;
4e17e6 1307     }
T 1308
1309
59c216 1310     /**
40c45e 1311      * Return index of threaded message UIDs
59c216 1312      *
d08333 1313      * @param string $mailbox    Mailbox to get index from
5c461b 1314      * @param string $sort_field Sort column
A 1315      * @param string $sort_order Sort order [ASC, DESC]
40c45e 1316      *
A 1317      * @return rcube_result_thread Message UIDs
59c216 1318      */
d08333 1319     function thread_index($mailbox='', $sort_field=NULL, $sort_order=NULL)
f52c93 1320     {
d08333 1321         if (!strlen($mailbox)) {
A 1322             $mailbox = $this->mailbox;
1323         }
f52c93 1324
59c216 1325         // we have a saved search result, get index from there
40c45e 1326         if ($this->search_string && $this->search_threads && $mailbox == $this->mailbox) {
A 1327             $threads = $this->search_set;
1328         }
1329         else {
1330             // get all threads (default sort order)
1331             $threads = $this->fetch_threads($mailbox);
59c216 1332         }
f52c93 1333
40c45e 1334         $this->set_sort_order($sort_field, $sort_order);
A 1335         $this->sort_threads($threads);
f52c93 1336
40c45e 1337         return $threads;
f52c93 1338     }
T 1339
1340
59c216 1341     /**
40c45e 1342      * Sort threaded result, using THREAD=REFS method
59c216 1343      *
40c45e 1344      * @param rcube_result_thread $threads  Threads result set
59c216 1345      */
40c45e 1346     private function sort_threads($threads)
f52c93 1347     {
40c45e 1348         if ($threads->isEmpty()) {
A 1349             return;
59c216 1350         }
f52c93 1351
40c45e 1352         // THREAD=ORDEREDSUBJECT: sorting by sent date of root message
A 1353         // THREAD=REFERENCES:     sorting by sent date of root message
1354         // THREAD=REFS:           sorting by the most recent date in each thread
1355
1356         if ($this->sort_field && ($this->sort_field != 'date' || $this->get_capability('THREAD') != 'REFS')) {
1357             $index = $this->message_index_direct($this->mailbox, $this->sort_field, $this->sort_order, false);
1358
1359             if (!$index->isEmpty()) {
1360                 $threads->sort($index);
1361             }
1362         }
1363         else {
1364             if ($this->sort_order != $threads->getParameters('ORDER')) {
1365                 $threads->revert();
1366             }
1367         }
59c216 1368     }
A 1369
1370
1371     /**
1372      * Invoke search request to IMAP server
1373      *
d08333 1374      * @param  string  $mailbox    Mailbox name to search in
29983c 1375      * @param  string  $str        Search criteria
A 1376      * @param  string  $charset    Search charset
1377      * @param  string  $sort_field Header field to sort by
59c216 1378      * @access public
82f482 1379      * @todo: Search criteria should be provided in non-IMAP format, eg. array
59c216 1380      */
40c45e 1381     function search($mailbox='', $str='ALL', $charset=NULL, $sort_field=NULL)
5df0ad 1382     {
59c216 1383         if (!$str)
40c45e 1384             $str = 'ALL';
c43517 1385
d08333 1386         if (!strlen($mailbox)) {
A 1387             $mailbox = $this->mailbox;
1388         }
5df0ad 1389
59c216 1390         $results = $this->_search_index($mailbox, $str, $charset, $sort_field);
A 1391
1c4f23 1392         $this->set_search_set(array($str, $results, $charset, $sort_field,
A 1393             $this->threading || $this->search_sorted ? true : false));
5df0ad 1394     }
b20bca 1395
A 1396
59c216 1397     /**
A 1398      * Private search method
1399      *
5c461b 1400      * @param string $mailbox    Mailbox name
A 1401      * @param string $criteria   Search criteria
1402      * @param string $charset    Charset
1403      * @param string $sort_field Sorting field
80152b 1404      *
40c45e 1405      * @return rcube_result_index|rcube_result_thread  Search results (UIDs)
59c216 1406      * @see rcube_imap::search()
A 1407      */
1408     private function _search_index($mailbox, $criteria='ALL', $charset=NULL, $sort_field=NULL)
b20bca 1409     {
59c216 1410         $orig_criteria = $criteria;
A 1411
1412         if ($this->skip_deleted && !preg_match('/UNDELETED/', $criteria))
1413             $criteria = 'UNDELETED '.$criteria;
1414
1415         if ($this->threading) {
40c45e 1416             $threads = $this->conn->thread($mailbox, $this->threading, $criteria, true, $charset);
59c216 1417
ffd3e2 1418             // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
A 1419             // but I've seen that Courier doesn't support UTF-8)
40c45e 1420             if ($threads->isError() && $charset && $charset != 'US-ASCII')
A 1421                 $threads = $this->conn->thread($mailbox, $this->threading,
1422                     $this->convert_criteria($criteria, $charset), true, 'US-ASCII');
ffd3e2 1423
40c45e 1424             return $threads;
59c216 1425         }
e9a974 1426
c60978 1427         if ($sort_field && $this->get_capability('SORT')) {
40c45e 1428             $charset  = $charset ? $charset : $this->default_charset;
A 1429             $messages = $this->conn->sort($mailbox, $sort_field, $criteria, true, $charset);
59c216 1430
ffd3e2 1431             // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
80152b 1432             // but I've seen Courier with disabled UTF-8 support)
40c45e 1433             if ($messages->isError() && $charset && $charset != 'US-ASCII')
A 1434                 $messages = $this->conn->sort($mailbox, $sort_field,
1435                     $this->convert_criteria($criteria, $charset), true, 'US-ASCII');
c60978 1436
40c45e 1437             if (!$messages->isError()) {
c60978 1438                 $this->search_sorted = true;
40c45e 1439                 return $messages;
c60978 1440             }
A 1441         }
1442
40c45e 1443         $messages = $this->conn->search($mailbox,
A 1444             ($charset ? "CHARSET $charset " : '') . $criteria, true);
c60978 1445
40c45e 1446         // Error, try with US-ASCII (some servers may support only US-ASCII)
A 1447         if ($messages->isError() && $charset && $charset != 'US-ASCII')
1448             $messages = $this->conn->search($mailbox,
c886de 1449                 $this->convert_criteria($criteria, $charset), true);
59c216 1450
c60978 1451         $this->search_sorted = false;
c43517 1452
40c45e 1453         return $messages;
59c216 1454     }
c43517 1455
6f31b3 1456
A 1457     /**
1458      * Direct (real and simple) SEARCH request to IMAP server,
1459      * without result sorting and caching
1460      *
d08333 1461      * @param  string  $mailbox Mailbox name to search in
A 1462      * @param  string  $str     Search string
1463      * @param  boolean $ret_uid True if UIDs should be returned
80152b 1464      *
40c45e 1465      * @return rcube_result_index  Search result (UIDs)
6f31b3 1466      */
40c45e 1467     function search_once($mailbox='', $str='ALL')
6f31b3 1468     {
A 1469         if (!$str)
40c45e 1470             return 'ALL';
c43517 1471
d08333 1472         if (!strlen($mailbox)) {
A 1473             $mailbox = $this->mailbox;
1474         }
6f31b3 1475
40c45e 1476         $index = $this->conn->search($mailbox, $str, true);
A 1477
1478         return $index;
6f31b3 1479     }
A 1480
c43517 1481
59c216 1482     /**
ffd3e2 1483      * Converts charset of search criteria string
A 1484      *
5c461b 1485      * @param  string  $str          Search string
A 1486      * @param  string  $charset      Original charset
1487      * @param  string  $dest_charset Destination charset (default US-ASCII)
ffd3e2 1488      * @return string  Search string
A 1489      * @access private
1490      */
1491     private function convert_criteria($str, $charset, $dest_charset='US-ASCII')
1492     {
1493         // convert strings to US_ASCII
1494         if (preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) {
1495             $last = 0; $res = '';
1496             foreach ($matches[1] as $m) {
1497                 $string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n
1498                 $string = substr($str, $string_offset - 1, $m[0]);
1499                 $string = rcube_charset_convert($string, $charset, $dest_charset);
82f482 1500                 if ($string === false)
ffd3e2 1501                     continue;
82f482 1502                 $res .= substr($str, $last, $m[1] - $last - 1) . rcube_imap_generic::escape($string);
ffd3e2 1503                 $last = $m[0] + $string_offset - 1;
A 1504             }
1505             if ($last < strlen($str))
1506                 $res .= substr($str, $last, strlen($str)-$last);
1507         }
1508         else // strings for conversion not found
1509             $res = $str;
1510
1511         return $res;
1512     }
1513
1514
1515     /**
59c216 1516      * Refresh saved search set
A 1517      *
1518      * @return array Current search set
1519      */
1520     function refresh_search()
1521     {
40c45e 1522         if (!empty($this->search_string)) {
A 1523             $this->search('', $this->search_string, $this->search_charset, $this->search_sort_field);
1524         }
59c216 1525
A 1526         return $this->get_search_set();
1527     }
c43517 1528
A 1529
59c216 1530     /**
A 1531      * Return message headers object of a specific message
1532      *
80152b 1533      * @param int     $id       Message sequence ID or UID
d08333 1534      * @param string  $mailbox  Mailbox to read from
80152b 1535      * @param bool    $force    True to skip cache
A 1536      *
1537      * @return rcube_mail_header Message headers
59c216 1538      */
80152b 1539     function get_headers($uid, $mailbox = null, $force = false)
59c216 1540     {
d08333 1541         if (!strlen($mailbox)) {
A 1542             $mailbox = $this->mailbox;
1543         }
59c216 1544
A 1545         // get cached headers
80152b 1546         if (!$force && $uid && ($mcache = $this->get_mcache_engine())) {
A 1547             $headers = $mcache->get_message($mailbox, $uid);
1548         }
1549         else {
1550             $headers = $this->conn->fetchHeader(
1551                 $mailbox, $uid, true, true, $this->get_fetch_headers());
59c216 1552         }
A 1553
1554         return $headers;
1555     }
1556
1557
1558     /**
80152b 1559      * Fetch message headers and body structure from the IMAP server and build
59c216 1560      * an object structure similar to the one generated by PEAR::Mail_mimeDecode
A 1561      *
80152b 1562      * @param int     $uid      Message UID to fetch
A 1563      * @param string  $mailbox  Mailbox to read from
1564      *
1565      * @return object rcube_mail_header Message data
59c216 1566      */
80152b 1567     function get_message($uid, $mailbox = null)
59c216 1568     {
80152b 1569         if (!strlen($mailbox)) {
A 1570             $mailbox = $this->mailbox;
59c216 1571         }
A 1572
80152b 1573         // Check internal cache
A 1574         if (!empty($this->icache['message'])) {
1575             if (($headers = $this->icache['message']) && $headers->uid == $uid) {
1576                 return $headers;
1577             }
70318e 1578         }
59c216 1579
80152b 1580         $headers = $this->get_headers($uid, $mailbox);
A 1581
1ae119 1582         // message doesn't exist?
A 1583         if (empty($headers))
40c45e 1584             return null;
1ae119 1585
80152b 1586         // structure might be cached
A 1587         if (!empty($headers->structure))
1588             return $headers;
1589
1590         $this->_msg_uid = $uid;
1591
1592         if (empty($headers->bodystructure)) {
1593             $headers->bodystructure = $this->conn->getStructure($mailbox, $uid, true);
1594         }
1595
1596         $structure = $headers->bodystructure;
1597
1598         if (empty($structure))
1599             return $headers;
59c216 1600
A 1601         // set message charset from message headers
1602         if ($headers->charset)
1603             $this->struct_charset = $headers->charset;
1604         else
1605             $this->struct_charset = $this->_structure_charset($structure);
1606
3c9d9a 1607         $headers->ctype = strtolower($headers->ctype);
A 1608
c43517 1609         // Here we can recognize malformed BODYSTRUCTURE and
59c216 1610         // 1. [@TODO] parse the message in other way to create our own message structure
A 1611         // 2. or just show the raw message body.
1612         // Example of structure for malformed MIME message:
3c9d9a 1613         // ("text" "plain" NIL NIL NIL "7bit" 2154 70 NIL NIL NIL)
A 1614         if ($headers->ctype && !is_array($structure[0]) && $headers->ctype != 'text/plain'
1615             && strtolower($structure[0].'/'.$structure[1]) == 'text/plain') {
1616             // we can handle single-part messages, by simple fix in structure (#1486898)
ecc28c 1617             if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) {
3c9d9a 1618                 $structure[0] = $m[1];
A 1619                 $structure[1] = $m[2];
1620             }
1621             else
80152b 1622                 return $headers;
59c216 1623         }
A 1624
1c4f23 1625         $struct = $this->_structure_part($structure, 0, '', $headers);
59c216 1626
A 1627         // don't trust given content-type
71f72f 1628         if (empty($struct->parts) && !empty($headers->ctype)) {
59c216 1629             $struct->mime_id = '1';
71f72f 1630             $struct->mimetype = strtolower($headers->ctype);
59c216 1631             list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype);
A 1632         }
1633
80152b 1634         $headers->structure = $struct;
59c216 1635
80152b 1636         return $this->icache['message'] = $headers;
59c216 1637     }
A 1638
c43517 1639
59c216 1640     /**
A 1641      * Build message part object
1642      *
5c461b 1643      * @param array  $part
A 1644      * @param int    $count
1645      * @param string $parent
59c216 1646      * @access private
A 1647      */
1c4f23 1648     private function _structure_part($part, $count=0, $parent='', $mime_headers=null)
59c216 1649     {
A 1650         $struct = new rcube_message_part;
1651         $struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
1652
1653         // multipart
1654         if (is_array($part[0])) {
1655             $struct->ctype_primary = 'multipart';
c43517 1656
95fd49 1657         /* RFC3501: BODYSTRUCTURE fields of multipart part
A 1658             part1 array
1659             part2 array
1660             part3 array
1661             ....
1662             1. subtype
1663             2. parameters (optional)
1664             3. description (optional)
1665             4. language (optional)
1666             5. location (optional)
1667         */
1668
59c216 1669             // find first non-array entry
A 1670             for ($i=1; $i<count($part); $i++) {
1671                 if (!is_array($part[$i])) {
1672                     $struct->ctype_secondary = strtolower($part[$i]);
1673                     break;
1674                 }
1675             }
c43517 1676
59c216 1677             $struct->mimetype = 'multipart/'.$struct->ctype_secondary;
A 1678
1679             // build parts list for headers pre-fetching
95fd49 1680             for ($i=0; $i<count($part); $i++) {
A 1681                 if (!is_array($part[$i]))
1682                     break;
1683                 // fetch message headers if message/rfc822
1684                 // or named part (could contain Content-Location header)
1685                 if (!is_array($part[$i][0])) {
1686                     $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
1687                     if (strtolower($part[$i][0]) == 'message' && strtolower($part[$i][1]) == 'rfc822') {
1688                         $mime_part_headers[] = $tmp_part_id;
1689                     }
733ed0 1690                     else if (in_array('name', (array)$part[$i][2]) && empty($part[$i][3])) {
95fd49 1691                         $mime_part_headers[] = $tmp_part_id;
59c216 1692                     }
A 1693                 }
1694             }
c43517 1695
59c216 1696             // pre-fetch headers of all parts (in one command for better performance)
A 1697             // @TODO: we could do this before _structure_part() call, to fetch
1698             // headers for parts on all levels
1699             if ($mime_part_headers) {
1700                 $mime_part_headers = $this->conn->fetchMIMEHeaders($this->mailbox,
80152b 1701                     $this->_msg_uid, $mime_part_headers);
59c216 1702             }
95fd49 1703
59c216 1704             $struct->parts = array();
A 1705             for ($i=0, $count=0; $i<count($part); $i++) {
95fd49 1706                 if (!is_array($part[$i]))
A 1707                     break;
1708                 $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
1709                 $struct->parts[] = $this->_structure_part($part[$i], ++$count, $struct->mime_id,
253768 1710                     $mime_part_headers[$tmp_part_id]);
59c216 1711             }
A 1712
1713             return $struct;
1714         }
95fd49 1715
A 1716         /* RFC3501: BODYSTRUCTURE fields of non-multipart part
1717             0. type
1718             1. subtype
1719             2. parameters
1720             3. id
1721             4. description
1722             5. encoding
1723             6. size
1724           -- text
1725             7. lines
1726           -- message/rfc822
1727             7. envelope structure
1728             8. body structure
1729             9. lines
1730           --
1731             x. md5 (optional)
1732             x. disposition (optional)
1733             x. language (optional)
1734             x. location (optional)
1735         */
59c216 1736
A 1737         // regular part
1738         $struct->ctype_primary = strtolower($part[0]);
1739         $struct->ctype_secondary = strtolower($part[1]);
1740         $struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary;
1741
1742         // read content type parameters
1743         if (is_array($part[2])) {
1744             $struct->ctype_parameters = array();
1745             for ($i=0; $i<count($part[2]); $i+=2)
1746                 $struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i+1];
c43517 1747
59c216 1748             if (isset($struct->ctype_parameters['charset']))
A 1749                 $struct->charset = $struct->ctype_parameters['charset'];
1750         }
c43517 1751
824144 1752         // #1487700: workaround for lack of charset in malformed structure
A 1753         if (empty($struct->charset) && !empty($mime_headers) && $mime_headers->charset) {
1754             $struct->charset = $mime_headers->charset;
1755         }
1756
59c216 1757         // read content encoding
733ed0 1758         if (!empty($part[5])) {
59c216 1759             $struct->encoding = strtolower($part[5]);
A 1760             $struct->headers['content-transfer-encoding'] = $struct->encoding;
1761         }
c43517 1762
59c216 1763         // get part size
733ed0 1764         if (!empty($part[6]))
59c216 1765             $struct->size = intval($part[6]);
A 1766
1767         // read part disposition
95fd49 1768         $di = 8;
A 1769         if ($struct->ctype_primary == 'text') $di += 1;
1770         else if ($struct->mimetype == 'message/rfc822') $di += 3;
1771
1772         if (is_array($part[$di]) && count($part[$di]) == 2) {
59c216 1773             $struct->disposition = strtolower($part[$di][0]);
A 1774
1775             if (is_array($part[$di][1]))
1776                 for ($n=0; $n<count($part[$di][1]); $n+=2)
1777                     $struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1];
1778         }
c43517 1779
95fd49 1780         // get message/rfc822's child-parts
59c216 1781         if (is_array($part[8]) && $di != 8) {
A 1782             $struct->parts = array();
95fd49 1783             for ($i=0, $count=0; $i<count($part[8]); $i++) {
A 1784                 if (!is_array($part[8][$i]))
1785                     break;
1786                 $struct->parts[] = $this->_structure_part($part[8][$i], ++$count, $struct->mime_id);
1787             }
59c216 1788         }
A 1789
1790         // get part ID
733ed0 1791         if (!empty($part[3])) {
59c216 1792             $struct->content_id = $part[3];
A 1793             $struct->headers['content-id'] = $part[3];
c43517 1794
59c216 1795             if (empty($struct->disposition))
A 1796                 $struct->disposition = 'inline';
1797         }
c43517 1798
59c216 1799         // fetch message headers if message/rfc822 or named part (could contain Content-Location header)
A 1800         if ($struct->ctype_primary == 'message' || ($struct->ctype_parameters['name'] && !$struct->content_id)) {
1801             if (empty($mime_headers)) {
1802                 $mime_headers = $this->conn->fetchPartHeader(
80152b 1803                     $this->mailbox, $this->_msg_uid, true, $struct->mime_id);
59c216 1804             }
d755ea 1805
T 1806             if (is_string($mime_headers))
1c4f23 1807                 $struct->headers = rcube_mime::parse_headers($mime_headers) + $struct->headers;
d755ea 1808             else if (is_object($mime_headers))
T 1809                 $struct->headers = get_object_vars($mime_headers) + $struct->headers;
59c216 1810
253768 1811             // get real content-type of message/rfc822
59c216 1812             if ($struct->mimetype == 'message/rfc822') {
253768 1813                 // single-part
A 1814                 if (!is_array($part[8][0]))
1815                     $struct->real_mimetype = strtolower($part[8][0] . '/' . $part[8][1]);
1816                 // multi-part
1817                 else {
1818                     for ($n=0; $n<count($part[8]); $n++)
1819                         if (!is_array($part[8][$n]))
1820                             break;
1821                     $struct->real_mimetype = 'multipart/' . strtolower($part[8][$n]);
c43517 1822                 }
59c216 1823             }
A 1824
253768 1825             if ($struct->ctype_primary == 'message' && empty($struct->parts)) {
A 1826                 if (is_array($part[8]) && $di != 8)
1827                     $struct->parts[] = $this->_structure_part($part[8], ++$count, $struct->mime_id);
1828             }
59c216 1829         }
A 1830
1831         // normalize filename property
1832         $this->_set_part_filename($struct, $mime_headers);
1833
1834         return $struct;
1835     }
c43517 1836
59c216 1837
A 1838     /**
c43517 1839      * Set attachment filename from message part structure
59c216 1840      *
5c461b 1841      * @param  rcube_message_part $part    Part object
A 1842      * @param  string             $headers Part's raw headers
29983c 1843      * @access private
59c216 1844      */
A 1845     private function _set_part_filename(&$part, $headers=null)
1846     {
1847         if (!empty($part->d_parameters['filename']))
1848             $filename_mime = $part->d_parameters['filename'];
1849         else if (!empty($part->d_parameters['filename*']))
1850             $filename_encoded = $part->d_parameters['filename*'];
1851         else if (!empty($part->ctype_parameters['name*']))
1852             $filename_encoded = $part->ctype_parameters['name*'];
1853         // RFC2231 value continuations
1854         // TODO: this should be rewrited to support RFC2231 4.1 combinations
1855         else if (!empty($part->d_parameters['filename*0'])) {
1856             $i = 0;
1857             while (isset($part->d_parameters['filename*'.$i])) {
1858                 $filename_mime .= $part->d_parameters['filename*'.$i];
1859                 $i++;
1860             }
1861             // some servers (eg. dovecot-1.x) have no support for parameter value continuations
1862             // we must fetch and parse headers "manually"
1863             if ($i<2) {
1864                 if (!$headers) {
1865                     $headers = $this->conn->fetchPartHeader(
80152b 1866                         $this->mailbox, $this->_msg_uid, true, $part->mime_id);
59c216 1867                 }
A 1868                 $filename_mime = '';
1869                 $i = 0;
1870                 while (preg_match('/filename\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
1871                     $filename_mime .= $matches[1];
1872                     $i++;
1873                 }
1874             }
1875         }
1876         else if (!empty($part->d_parameters['filename*0*'])) {
1877             $i = 0;
1878             while (isset($part->d_parameters['filename*'.$i.'*'])) {
1879                 $filename_encoded .= $part->d_parameters['filename*'.$i.'*'];
1880                 $i++;
1881             }
1882             if ($i<2) {
1883                 if (!$headers) {
1884                     $headers = $this->conn->fetchPartHeader(
80152b 1885                             $this->mailbox, $this->_msg_uid, true, $part->mime_id);
59c216 1886                 }
A 1887                 $filename_encoded = '';
1888                 $i = 0; $matches = array();
1889                 while (preg_match('/filename\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
1890                     $filename_encoded .= $matches[1];
1891                     $i++;
1892                 }
1893             }
1894         }
1895         else if (!empty($part->ctype_parameters['name*0'])) {
1896             $i = 0;
1897             while (isset($part->ctype_parameters['name*'.$i])) {
1898                 $filename_mime .= $part->ctype_parameters['name*'.$i];
1899                 $i++;
1900             }
1901             if ($i<2) {
1902                 if (!$headers) {
1903                     $headers = $this->conn->fetchPartHeader(
80152b 1904                         $this->mailbox, $this->_msg_uid, true, $part->mime_id);
59c216 1905                 }
A 1906                 $filename_mime = '';
1907                 $i = 0; $matches = array();
1908                 while (preg_match('/\s+name\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
1909                     $filename_mime .= $matches[1];
1910                     $i++;
1911                 }
1912             }
1913         }
1914         else if (!empty($part->ctype_parameters['name*0*'])) {
1915             $i = 0;
1916             while (isset($part->ctype_parameters['name*'.$i.'*'])) {
1917                 $filename_encoded .= $part->ctype_parameters['name*'.$i.'*'];
1918                 $i++;
1919             }
1920             if ($i<2) {
1921                 if (!$headers) {
1922                     $headers = $this->conn->fetchPartHeader(
80152b 1923                         $this->mailbox, $this->_msg_uid, true, $part->mime_id);
59c216 1924                 }
A 1925                 $filename_encoded = '';
1926                 $i = 0; $matches = array();
1927                 while (preg_match('/\s+name\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
1928                     $filename_encoded .= $matches[1];
1929                     $i++;
1930                 }
1931             }
1932         }
1933         // read 'name' after rfc2231 parameters as it may contains truncated filename (from Thunderbird)
1934         else if (!empty($part->ctype_parameters['name']))
1935             $filename_mime = $part->ctype_parameters['name'];
1936         // Content-Disposition
1937         else if (!empty($part->headers['content-description']))
1938             $filename_mime = $part->headers['content-description'];
1939         else
1940             return;
1941
1942         // decode filename
1943         if (!empty($filename_mime)) {
7e50b4 1944             if (!empty($part->charset))
A 1945                 $charset = $part->charset;
1946             else if (!empty($this->struct_charset))
1947                 $charset = $this->struct_charset;
1948             else
1949                 $charset = rc_detect_encoding($filename_mime, $this->default_charset);
1950
1c4f23 1951             $part->filename = rcube_mime::decode_mime_string($filename_mime, $charset);
c43517 1952         }
59c216 1953         else if (!empty($filename_encoded)) {
A 1954             // decode filename according to RFC 2231, Section 4
1955             if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) {
1956                 $filename_charset = $fmatches[1];
1957                 $filename_encoded = $fmatches[2];
1958             }
7e50b4 1959
59c216 1960             $part->filename = rcube_charset_convert(urldecode($filename_encoded), $filename_charset);
A 1961         }
1962     }
1963
1964
1965     /**
1966      * Get charset name from message structure (first part)
1967      *
5c461b 1968      * @param  array $structure Message structure
59c216 1969      * @return string Charset name
29983c 1970      * @access private
59c216 1971      */
29983c 1972     private function _structure_charset($structure)
59c216 1973     {
A 1974         while (is_array($structure)) {
1975             if (is_array($structure[2]) && $structure[2][0] == 'charset')
1976                 return $structure[2][1];
1977             $structure = $structure[0];
1978         }
c43517 1979     }
b20bca 1980
A 1981
59c216 1982     /**
A 1983      * Fetch message body of a specific message from the server
1984      *
5c461b 1985      * @param  int                $uid    Message UID
A 1986      * @param  string             $part   Part number
1987      * @param  rcube_message_part $o_part Part object created by get_structure()
1988      * @param  mixed              $print  True to print part, ressource to write part contents in
1989      * @param  resource           $fp     File pointer to save the message part
8abc17 1990      * @param  boolean            $skip_charset_conv Disables charset conversion
A 1991      *
59c216 1992      * @return string Message/part body if not printed
A 1993      */
1c4f23 1994     function get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false)
8d4bcd 1995     {
8a6503 1996         // get part data if not provided
59c216 1997         if (!is_object($o_part)) {
80152b 1998             $structure = $this->conn->getStructure($this->mailbox, $uid, true);
8a6503 1999             $part_data = rcube_imap_generic::getStructurePartData($structure, $part);
5f571e 2000
59c216 2001             $o_part = new rcube_message_part;
8a6503 2002             $o_part->ctype_primary = $part_data['type'];
A 2003             $o_part->encoding      = $part_data['encoding'];
2004             $o_part->charset       = $part_data['charset'];
2005             $o_part->size          = $part_data['size'];
59c216 2006         }
c43517 2007
1ae119 2008         if ($o_part && $o_part->size) {
A 2009             $body = $this->conn->handlePartBody($this->mailbox, $uid, true,
2010                 $part ? $part : 'TEXT', $o_part->encoding, $print, $fp);
9840ab 2011         }
5f571e 2012
9840ab 2013         if ($fp || $print) {
59c216 2014             return true;
9840ab 2015         }
8d4bcd 2016
8abc17 2017         // convert charset (if text or message part)
eeae0d 2018         if ($body && preg_match('/^(text|message)$/', $o_part->ctype_primary)) {
e4a4ca 2019             // Remove NULL characters if any (#1486189)
A 2020             if (strpos($body, "\x00") !== false) {
2021                 $body = str_replace("\x00", '', $body);
2022             }
eeae0d 2023
e4a4ca 2024             if (!$skip_charset_conv) {
eeae0d 2025                 if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') {
dfc79b 2026                     // try to extract charset information from HTML meta tag (#1488125)
b9787b 2027                     if ($o_part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m))
dfc79b 2028                         $o_part->charset = strtoupper($m[1]);
T 2029                     else
2030                         $o_part->charset = $this->default_charset;
eeae0d 2031                 }
A 2032                 $body = rcube_charset_convert($body, $o_part->charset);
8abc17 2033             }
59c216 2034         }
c43517 2035
59c216 2036         return $body;
4e17e6 2037     }
T 2038
2039
59c216 2040     /**
A 2041      * Fetch message body of a specific message from the server
2042      *
5c461b 2043      * @param  int    $uid  Message UID
A 2044      * @return string $part Message/part body
59c216 2045      * @see    rcube_imap::get_message_part()
A 2046      */
1c4f23 2047     function get_body($uid, $part=1)
8d4bcd 2048     {
59c216 2049         $headers = $this->get_headers($uid);
A 2050         return rcube_charset_convert($this->get_message_part($uid, $part, NULL),
2051             $headers->charset ? $headers->charset : $this->default_charset);
8d4bcd 2052     }
T 2053
2054
59c216 2055     /**
a208a4 2056      * Returns the whole message source as string (or saves to a file)
59c216 2057      *
a208a4 2058      * @param int      $uid Message UID
A 2059      * @param resource $fp  File pointer to save the message
2060      *
59c216 2061      * @return string Message source string
A 2062      */
1c4f23 2063     function get_raw_body($uid, $fp=null)
4e17e6 2064     {
a208a4 2065         return $this->conn->handlePartBody($this->mailbox, $uid,
A 2066             true, null, null, false, $fp);
4e17e6 2067     }
e5686f 2068
A 2069
59c216 2070     /**
A 2071      * Returns the message headers as string
2072      *
5c461b 2073      * @param int $uid  Message UID
59c216 2074      * @return string Message headers string
A 2075      */
1c4f23 2076     function get_raw_headers($uid)
e5686f 2077     {
59c216 2078         return $this->conn->fetchPartHeader($this->mailbox, $uid, true);
e5686f 2079     }
c43517 2080
8d4bcd 2081
59c216 2082     /**
A 2083      * Sends the whole message source to stdout
2084      *
5c461b 2085      * @param int $uid Message UID
c43517 2086      */
59c216 2087     function print_raw_body($uid)
8d4bcd 2088     {
59c216 2089         $this->conn->handlePartBody($this->mailbox, $uid, true, NULL, NULL, true);
8d4bcd 2090     }
4e17e6 2091
T 2092
59c216 2093     /**
A 2094      * Set message flag to one or several messages
2095      *
5c461b 2096      * @param mixed   $uids       Message UIDs as array or comma-separated string, or '*'
A 2097      * @param string  $flag       Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
d08333 2098      * @param string  $mailbox    Folder name
5c461b 2099      * @param boolean $skip_cache True to skip message cache clean up
d08333 2100      *
c309cd 2101      * @return boolean  Operation status
59c216 2102      */
d08333 2103     function set_flag($uids, $flag, $mailbox=null, $skip_cache=false)
4e17e6 2104     {
d08333 2105         if (!strlen($mailbox)) {
A 2106             $mailbox = $this->mailbox;
2107         }
48958e 2108
59c216 2109         $flag = strtoupper($flag);
A 2110         list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
cff886 2111
59c216 2112         if (strpos($flag, 'UN') === 0)
A 2113             $result = $this->conn->unflag($mailbox, $uids, substr($flag, 2));
cff886 2114         else
59c216 2115             $result = $this->conn->flag($mailbox, $uids, $flag);
fa4cd2 2116
c309cd 2117         if ($result) {
59c216 2118             // reload message headers if cached
80152b 2119             // @TODO: update flags instead removing from cache
A 2120             if (!$skip_cache && ($mcache = $this->get_mcache_engine())) {
2121                 $status = strpos($flag, 'UN') !== 0;
2122                 $mflag  = preg_replace('/^UN/', '', $flag);
2123                 $mcache->change_flag($mailbox, $all_mode ? null : explode(',', $uids),
2124                     $mflag, $status);
15e00b 2125             }
c309cd 2126
A 2127             // clear cached counters
2128             if ($flag == 'SEEN' || $flag == 'UNSEEN') {
2129                 $this->_clear_messagecount($mailbox, 'SEEN');
2130                 $this->_clear_messagecount($mailbox, 'UNSEEN');
2131             }
2132             else if ($flag == 'DELETED') {
2133                 $this->_clear_messagecount($mailbox, 'DELETED');
2134             }
4e17e6 2135         }
T 2136
59c216 2137         return $result;
4e17e6 2138     }
T 2139
fa4cd2 2140
59c216 2141     /**
A 2142      * Remove message flag for one or several messages
2143      *
d08333 2144      * @param mixed  $uids    Message UIDs as array or comma-separated string, or '*'
A 2145      * @param string $flag    Flag to unset: SEEN, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
2146      * @param string $mailbox Folder name
2147      *
59c216 2148      * @return int   Number of flagged messages, -1 on failure
A 2149      * @see set_flag
2150      */
d08333 2151     function unset_flag($uids, $flag, $mailbox=null)
fa4cd2 2152     {
d08333 2153         return $this->set_flag($uids, 'UN'.$flag, $mailbox);
15e00b 2154     }
A 2155
2156
59c216 2157     /**
A 2158      * Append a mail message (source) to a specific mailbox
2159      *
d08333 2160      * @param string  $mailbox Target mailbox
A 2161      * @param string  $message The message source string or filename
2162      * @param string  $headers Headers string if $message contains only the body
2163      * @param boolean $is_file True if $message is a filename
59c216 2164      *
765fde 2165      * @return int|bool Appended message UID or True on success, False on error
59c216 2166      */
d08333 2167     function save_message($mailbox, &$message, $headers='', $is_file=false)
15e00b 2168     {
d08333 2169         if (!strlen($mailbox)) {
A 2170             $mailbox = $this->mailbox;
2171         }
4e17e6 2172
59c216 2173         // make sure mailbox exists
d08333 2174         if ($this->mailbox_exists($mailbox)) {
272a7e 2175             if ($is_file)
A 2176                 $saved = $this->conn->appendFromFile($mailbox, $message, $headers);
59c216 2177             else
A 2178                 $saved = $this->conn->append($mailbox, $message);
4e17e6 2179         }
f8a846 2180
59c216 2181         if ($saved) {
A 2182             // increase messagecount of the target mailbox
2183             $this->_set_messagecount($mailbox, 'ALL', 1);
8d4bcd 2184         }
59c216 2185
A 2186         return $saved;
8d4bcd 2187     }
T 2188
2189
59c216 2190     /**
A 2191      * Move a message from one mailbox to another
2192      *
5c461b 2193      * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
A 2194      * @param string $to_mbox   Target mailbox
2195      * @param string $from_mbox Source mailbox
59c216 2196      * @return boolean True on success, False on error
A 2197      */
2198     function move_message($uids, $to_mbox, $from_mbox='')
4e17e6 2199     {
d08333 2200         if (!strlen($from_mbox)) {
A 2201             $from_mbox = $this->mailbox;
2202         }
c58c0a 2203
d08333 2204         if ($to_mbox === $from_mbox) {
af3c04 2205             return false;
d08333 2206         }
af3c04 2207
59c216 2208         list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
41fa0b 2209
59c216 2210         // exit if no message uids are specified
A 2211         if (empty($uids))
2212             return false;
2213
2214         // make sure mailbox exists
d08333 2215         if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
a561cd 2216             if (in_array($to_mbox, $this->default_folders)) {
A 2217                 if (!$this->create_mailbox($to_mbox, true)) {
2218                     return false;
2219                 }
2220             }
2221             else {
59c216 2222                 return false;
a561cd 2223             }
59c216 2224         }
A 2225
2226         $config = rcmail::get_instance()->config;
d08333 2227         $to_trash = $to_mbox == $config->get('trash_mbox');
A 2228
2229         // flag messages as read before moving them
2230         if ($to_trash && $config->get('read_when_deleted')) {
59c216 2231             // don't flush cache (4th argument)
d08333 2232             $this->set_flag($uids, 'SEEN', $from_mbox, true);
59c216 2233         }
A 2234
2235         // move messages
93272e 2236         $moved = $this->conn->move($uids, $from_mbox, $to_mbox);
59c216 2237
A 2238         // send expunge command in order to have the moved message
2239         // really deleted from the source mailbox
2240         if ($moved) {
2241             $this->_expunge($from_mbox, false, $uids);
2242             $this->_clear_messagecount($from_mbox);
2243             $this->_clear_messagecount($to_mbox);
2244         }
2245         // moving failed
d08333 2246         else if ($to_trash && $config->get('delete_always', false)) {
A 2247             $moved = $this->delete_message($uids, $from_mbox);
59c216 2248         }
9e81b5 2249
59c216 2250         if ($moved) {
A 2251             // unset threads internal cache
2252             unset($this->icache['threads']);
2253
2254             // remove message ids from search set
2255             if ($this->search_set && $from_mbox == $this->mailbox) {
2256                 // threads are too complicated to just remove messages from set
2257                 if ($this->search_threads || $all_mode)
2258                     $this->refresh_search();
40c45e 2259                 else
A 2260                     $this->search_set->filter(explode(',', $uids));
59c216 2261             }
A 2262
80152b 2263             // remove cached messages
A 2264             // @TODO: do cache update instead of clearing it
2265             $this->clear_message_cache($from_mbox, $all_mode ? null : explode(',', $uids));
59c216 2266         }
A 2267
2268         return $moved;
2269     }
2270
2271
2272     /**
2273      * Copy a message from one mailbox to another
2274      *
5c461b 2275      * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
A 2276      * @param string $to_mbox   Target mailbox
2277      * @param string $from_mbox Source mailbox
59c216 2278      * @return boolean True on success, False on error
A 2279      */
2280     function copy_message($uids, $to_mbox, $from_mbox='')
2281     {
d08333 2282         if (!strlen($from_mbox)) {
A 2283             $from_mbox = $this->mailbox;
2284         }
59c216 2285
A 2286         list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
2287
2288         // exit if no message uids are specified
93272e 2289         if (empty($uids)) {
59c216 2290             return false;
93272e 2291         }
59c216 2292
A 2293         // make sure mailbox exists
d08333 2294         if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
a561cd 2295             if (in_array($to_mbox, $this->default_folders)) {
A 2296                 if (!$this->create_mailbox($to_mbox, true)) {
2297                     return false;
2298                 }
2299             }
2300             else {
59c216 2301                 return false;
a561cd 2302             }
59c216 2303         }
A 2304
2305         // copy messages
93272e 2306         $copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
59c216 2307
A 2308         if ($copied) {
2309             $this->_clear_messagecount($to_mbox);
2310         }
2311
2312         return $copied;
2313     }
2314
2315
2316     /**
2317      * Mark messages as deleted and expunge mailbox
2318      *
d08333 2319      * @param mixed  $uids    Message UIDs as array or comma-separated string, or '*'
A 2320      * @param string $mailbox Source mailbox
2321      *
59c216 2322      * @return boolean True on success, False on error
A 2323      */
d08333 2324     function delete_message($uids, $mailbox='')
59c216 2325     {
d08333 2326         if (!strlen($mailbox)) {
A 2327             $mailbox = $this->mailbox;
2328         }
59c216 2329
A 2330         list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2331
2332         // exit if no message uids are specified
2333         if (empty($uids))
2334             return false;
2335
e361bf 2336         $deleted = $this->conn->flag($mailbox, $uids, 'DELETED');
59c216 2337
A 2338         if ($deleted) {
2339             // send expunge command in order to have the deleted message
2340             // really deleted from the mailbox
2341             $this->_expunge($mailbox, false, $uids);
2342             $this->_clear_messagecount($mailbox);
2343             unset($this->uid_id_map[$mailbox]);
2344
2345             // unset threads internal cache
2346             unset($this->icache['threads']);
c43517 2347
59c216 2348             // remove message ids from search set
A 2349             if ($this->search_set && $mailbox == $this->mailbox) {
2350                 // threads are too complicated to just remove messages from set
2351                 if ($this->search_threads || $all_mode)
2352                     $this->refresh_search();
40c45e 2353                 else
A 2354                     $this->search_set->filter(explode(',', $uids));
59c216 2355             }
A 2356
80152b 2357             // remove cached messages
A 2358             $this->clear_message_cache($mailbox, $all_mode ? null : explode(',', $uids));
59c216 2359         }
A 2360
2361         return $deleted;
2362     }
2363
2364
2365     /**
2366      * Clear all messages in a specific mailbox
2367      *
d08333 2368      * @param string $mailbox Mailbox name
A 2369      *
59c216 2370      * @return int Above 0 on success
A 2371      */
d08333 2372     function clear_mailbox($mailbox=null)
59c216 2373     {
d08333 2374         if (!strlen($mailbox)) {
A 2375             $mailbox = $this->mailbox;
2376         }
c43517 2377
01bdfd 2378         // SELECT will set messages count for clearFolder()
A 2379         if ($this->conn->select($mailbox)) {
2380             $cleared = $this->conn->clearFolder($mailbox);
4e17e6 2381         }
c43517 2382
80152b 2383         // make sure the cache is cleared as well
59c216 2384         if ($cleared) {
80152b 2385             $this->clear_message_cache($mailbox);
59c216 2386             $a_mailbox_cache = $this->get_cache('messagecount');
A 2387             unset($a_mailbox_cache[$mailbox]);
2388             $this->update_cache('messagecount', $a_mailbox_cache);
2389         }
c43517 2390
59c216 2391         return $cleared;
A 2392     }
2393
2394
2395     /**
2396      * Send IMAP expunge command and clear cache
2397      *
d08333 2398      * @param string  $mailbox     Mailbox name
5c461b 2399      * @param boolean $clear_cache False if cache should not be cleared
d08333 2400      *
59c216 2401      * @return boolean True on success
A 2402      */
d08333 2403     function expunge($mailbox='', $clear_cache=true)
59c216 2404     {
d08333 2405         if (!strlen($mailbox)) {
A 2406             $mailbox = $this->mailbox;
2407         }
2408
59c216 2409         return $this->_expunge($mailbox, $clear_cache);
A 2410     }
2411
2412
2413     /**
2414      * Send IMAP expunge command and clear cache
2415      *
5c461b 2416      * @param string  $mailbox     Mailbox name
A 2417      * @param boolean $clear_cache False if cache should not be cleared
2418      * @param mixed   $uids        Message UIDs as array or comma-separated string, or '*'
59c216 2419      * @return boolean True on success
A 2420      * @access private
2421      * @see rcube_imap::expunge()
2422      */
2423     private function _expunge($mailbox, $clear_cache=true, $uids=NULL)
2424     {
c43517 2425         if ($uids && $this->get_capability('UIDPLUS'))
80152b 2426             list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
59c216 2427         else
80152b 2428             $uids = null;
59c216 2429
90f81a 2430         // force mailbox selection and check if mailbox is writeable
A 2431         // to prevent a situation when CLOSE is executed on closed
2432         // or EXPUNGE on read-only mailbox
2433         $result = $this->conn->select($mailbox);
2434         if (!$result) {
2435             return false;
2436         }
2437         if (!$this->conn->data['READ-WRITE']) {
2438             $this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Mailbox is read-only");
2439             return false;
2440         }
2441
e232ac 2442         // CLOSE(+SELECT) should be faster than EXPUNGE
80152b 2443         if (empty($uids) || $all_mode)
e232ac 2444             $result = $this->conn->close();
A 2445         else
80152b 2446             $result = $this->conn->expunge($mailbox, $uids);
59c216 2447
854cf2 2448         if ($result && $clear_cache) {
80152b 2449             $this->clear_message_cache($mailbox, $all_mode ? null : explode(',', $uids));
59c216 2450             $this->_clear_messagecount($mailbox);
A 2451         }
c43517 2452
59c216 2453         return $result;
A 2454     }
2455
2456
2457     /**
2458      * Parse message UIDs input
2459      *
5c461b 2460      * @param mixed  $uids    UIDs array or comma-separated list or '*' or '1:*'
A 2461      * @param string $mailbox Mailbox name
c43517 2462      * @return array Two elements array with UIDs converted to list and ALL flag
59c216 2463      * @access private
A 2464      */
2465     private function _parse_uids($uids, $mailbox)
2466     {
2467         if ($uids === '*' || $uids === '1:*') {
2468             if (empty($this->search_set)) {
2469                 $uids = '1:*';
2470                 $all = true;
2471             }
2472             // get UIDs from current search set
2473             else {
40c45e 2474                 $uids = join(',', $this->search_set->get());
59c216 2475             }
A 2476         }
2477         else {
2478             if (is_array($uids))
2479                 $uids = join(',', $uids);
2480
2481             if (preg_match('/[^0-9,]/', $uids))
2482                 $uids = '';
2483         }
2484
2485         return array($uids, (bool) $all);
2486     }
2487
2488
2489     /* --------------------------------
2490      *        folder managment
2491      * --------------------------------*/
2492
2493     /**
6f4e7d 2494      * Public method for listing subscribed folders
59c216 2495      *
888176 2496      * @param   string  $root      Optional root folder
A 2497      * @param   string  $name      Optional name pattern
2498      * @param   string  $filter    Optional filter
2499      * @param   string  $rights    Optional ACL requirements
2500      * @param   bool    $skip_sort Enable to return unsorted list (for better performance)
d08333 2501      *
d342f8 2502      * @return  array   List of folders
59c216 2503      * @access  public
A 2504      */
888176 2505     function list_mailboxes($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
59c216 2506     {
d342f8 2507         $cache_key = $root.':'.$name;
A 2508         if (!empty($filter)) {
2509             $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
2510         }
2511         $cache_key .= ':'.$rights;
2512         $cache_key = 'mailboxes.'.md5($cache_key);
2513
2514         // get cached folder list
2515         $a_mboxes = $this->get_cache($cache_key);
2516         if (is_array($a_mboxes)) {
2517             return $a_mboxes;
2518         }
2519
e750d1 2520         $a_mboxes = $this->_list_mailboxes($root, $name, $filter, $rights);
d342f8 2521
A 2522         if (!is_array($a_mboxes)) {
2523             return array();
2524         }
2525
2526         // filter folders list according to rights requirements
2527         if ($rights && $this->get_capability('ACL')) {
2528             $a_mboxes = $this->filter_rights($a_mboxes, $rights);
2529         }
59c216 2530
A 2531         // INBOX should always be available
94bdcc 2532         if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
d08333 2533             array_unshift($a_mboxes, 'INBOX');
94bdcc 2534         }
59c216 2535
d342f8 2536         // sort mailboxes (always sort for cache)
A 2537         if (!$skip_sort || $this->cache) {
888176 2538             $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
A 2539         }
d342f8 2540
A 2541         // write mailboxlist to cache
2542         $this->update_cache($cache_key, $a_mboxes);
59c216 2543
d08333 2544         return $a_mboxes;
59c216 2545     }
A 2546
2547
2548     /**
89dcf5 2549      * Private method for mailbox listing (LSUB)
59c216 2550      *
5c461b 2551      * @param   string  $root   Optional root folder
94bdcc 2552      * @param   string  $name   Optional name pattern
A 2553      * @param   mixed   $filter Optional filter
e750d1 2554      * @param   string  $rights Optional ACL requirements
94bdcc 2555      *
89dcf5 2556      * @return  array   List of subscribed folders
59c216 2557      * @see     rcube_imap::list_mailboxes()
A 2558      * @access  private
2559      */
e750d1 2560     private function _list_mailboxes($root='', $name='*', $filter=null, $rights=null)
59c216 2561     {
6f4e7d 2562         $a_defaults = $a_out = array();
A 2563
59c216 2564         // Give plugins a chance to provide a list of mailboxes
e6ce00 2565         $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
94bdcc 2566             array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB'));
c43517 2567
59c216 2568         if (isset($data['folders'])) {
A 2569             $a_folders = $data['folders'];
2570         }
20ed37 2571         else if (!$this->conn->connected()) {
d342f8 2572            return null;
20ed37 2573         }
59c216 2574         else {
3870be 2575             // Server supports LIST-EXTENDED, we can use selection options
f75f65 2576             $config = rcmail::get_instance()->config;
A 2577             // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
2578             if (!$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED')) {
3870be 2579                 // This will also set mailbox options, LSUB doesn't do that
94bdcc 2580                 $a_folders = $this->conn->listMailboxes($root, $name,
3870be 2581                     NULL, array('SUBSCRIBED'));
A 2582
189a0a 2583                 // unsubscribe non-existent folders, remove from the list
A 2584                 if (is_array($a_folders) && $name == '*') {
3870be 2585                     foreach ($a_folders as $idx => $folder) {
A 2586                         if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
2587                             && in_array('\\NonExistent', $opts)
2588                         ) {
189a0a 2589                             $this->conn->unsubscribe($folder);
3870be 2590                             unset($a_folders[$idx]);
189a0a 2591                         }
3870be 2592                     }
A 2593                 }
2594             }
2595             // retrieve list of folders from IMAP server using LSUB
2596             else {
94bdcc 2597                 $a_folders = $this->conn->listSubscribed($root, $name);
189a0a 2598
A 2599                 // unsubscribe non-existent folders, remove from the list
2600                 if (is_array($a_folders) && $name == '*') {
2601                     foreach ($a_folders as $idx => $folder) {
2602                         if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
2603                             && in_array('\\Noselect', $opts)
2604                         ) {
2605                             // Some servers returns \Noselect for existing folders
2606                             if (!$this->mailbox_exists($folder)) {
2607                                 $this->conn->unsubscribe($folder);
2608                                 unset($a_folders[$idx]);
2609                             }
2610                         }
2611                     }
2612                 }
3870be 2613             }
59c216 2614         }
c43517 2615
94bdcc 2616         if (!is_array($a_folders) || !sizeof($a_folders)) {
59c216 2617             $a_folders = array();
94bdcc 2618         }
c43517 2619
59c216 2620         return $a_folders;
A 2621     }
2622
2623
2624     /**
2625      * Get a list of all folders available on the IMAP server
c43517 2626      *
888176 2627      * @param string  $root      IMAP root dir
A 2628      * @param string  $name      Optional name pattern
2629      * @param mixed   $filter    Optional filter
2630      * @param string  $rights    Optional ACL requirements
2631      * @param bool    $skip_sort Enable to return unsorted list (for better performance)
94bdcc 2632      *
59c216 2633      * @return array Indexed array with folder names
A 2634      */
888176 2635     function list_unsubscribed($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
59c216 2636     {
aa07b2 2637         $cache_key = $root.':'.$name;
A 2638         if (!empty($filter)) {
2639             $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
2640         }
2641         $cache_key .= ':'.$rights;
2642         $cache_key = 'mailboxes.list.'.md5($cache_key);
2643
2644         // get cached folder list
2645         $a_mboxes = $this->get_cache($cache_key);
2646         if (is_array($a_mboxes)) {
2647             return $a_mboxes;
2648         }
2649
6f4e7d 2650         // Give plugins a chance to provide a list of mailboxes
e6ce00 2651         $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
94bdcc 2652             array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
c43517 2653
6f4e7d 2654         if (isset($data['folders'])) {
A 2655             $a_mboxes = $data['folders'];
2656         }
2657         else {
2658             // retrieve list of folders from IMAP server
89dcf5 2659             $a_mboxes = $this->_list_unsubscribed($root, $name);
6f4e7d 2660         }
64e3e8 2661
d08333 2662         if (!is_array($a_mboxes)) {
6f4e7d 2663             $a_mboxes = array();
59c216 2664         }
f0485a 2665
A 2666         // INBOX should always be available
94bdcc 2667         if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
d08333 2668             array_unshift($a_mboxes, 'INBOX');
94bdcc 2669         }
59c216 2670
aa07b2 2671         // cache folder attributes
A 2672         if ($root == '' && $name == '*' && empty($filter)) {
2673             $this->update_cache('mailboxes.attributes', $this->conn->data['LIST']);
2674         }
2675
e750d1 2676         // filter folders list according to rights requirements
T 2677         if ($rights && $this->get_capability('ACL')) {
2678             $a_folders = $this->filter_rights($a_folders, $rights);
2679         }
2680
59c216 2681         // filter folders and sort them
888176 2682         if (!$skip_sort) {
A 2683             $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
2684         }
aa07b2 2685
A 2686         // write mailboxlist to cache
2687         $this->update_cache($cache_key, $a_mboxes);
d08333 2688
A 2689         return $a_mboxes;
59c216 2690     }
A 2691
2692
2693     /**
89dcf5 2694      * Private method for mailbox listing (LIST)
A 2695      *
2696      * @param   string  $root   Optional root folder
2697      * @param   string  $name   Optional name pattern
2698      *
2699      * @return  array   List of folders
2700      * @see     rcube_imap::list_unsubscribed()
2701      */
2702     private function _list_unsubscribed($root='', $name='*')
2703     {
2704         $result = $this->conn->listMailboxes($root, $name);
2705
2706         if (!is_array($result)) {
2707             return array();
2708         }
2709
2710         // #1486796: some server configurations doesn't
2711         // return folders in all namespaces, we'll try to detect that situation
2712         // and ask for these namespaces separately
4f1c0a 2713         if ($root == '' && $name == '*') {
89dcf5 2714             $delim     = $this->get_hierarchy_delimiter();
A 2715             $namespace = $this->get_namespace();
2716             $search    = array();
2717
2718             // build list of namespace prefixes
2719             foreach ((array)$namespace as $ns) {
2720                 if (is_array($ns)) {
2721                     foreach ($ns as $ns_data) {
305b36 2722                         if (strlen($ns_data[0])) {
e37e7d 2723                             $search[] = $ns_data[0];
89dcf5 2724                         }
A 2725                     }
2726                 }
2727             }
2728
2729             if (!empty($search)) {
2730                 // go through all folders detecting namespace usage
305b36 2731                 foreach ($result as $folder) {
A 2732                     foreach ($search as $idx => $prefix) {
2733                         if (strpos($folder, $prefix) === 0) {
89dcf5 2734                             unset($search[$idx]);
A 2735                         }
2736                     }
2737                     if (empty($search)) {
2738                         break;
2739                     }
2740                 }
2741
2742                 // get folders in hidden namespaces and add to the result
305b36 2743                 foreach ($search as $prefix) {
A 2744                     $list = $this->conn->listMailboxes($prefix, $name);
89dcf5 2745
A 2746                     if (!empty($list)) {
2747                         $result = array_merge($result, $list);
2748                     }
2749                 }
2750             }
2751         }
2752
2753         return $result;
2754     }
2755
2756
2757     /**
e750d1 2758      * Filter the given list of folders according to access rights
T 2759      */
2760     private function filter_rights($a_folders, $rights)
2761     {
2762         $regex = '/('.$rights.')/';
2763         foreach ($a_folders as $idx => $folder) {
2764             $myrights = join('', (array)$this->my_rights($folder));
2765             if ($myrights !== null && !preg_match($regex, $myrights))
2766                 unset($a_folders[$idx]);
2767         }
2768
2769         return $a_folders;
2770     }
2771
2772
2773     /**
59c216 2774      * Get mailbox quota information
A 2775      * added by Nuny
c43517 2776      *
59c216 2777      * @return mixed Quota info or False if not supported
A 2778      */
2779     function get_quota()
2780     {
2781         if ($this->get_capability('QUOTA'))
2782             return $this->conn->getQuota();
c43517 2783
59c216 2784         return false;
A 2785     }
2786
2787
2788     /**
af3c04 2789      * Get mailbox size (size of all messages in a mailbox)
A 2790      *
d08333 2791      * @param string $mailbox Mailbox name
A 2792      *
af3c04 2793      * @return int Mailbox size in bytes, False on error
A 2794      */
d08333 2795     function get_mailbox_size($mailbox)
af3c04 2796     {
A 2797         // @TODO: could we try to use QUOTA here?
d08333 2798         $result = $this->conn->fetchHeaderIndex($mailbox, '1:*', 'SIZE', false);
af3c04 2799
A 2800         if (is_array($result))
2801             $result = array_sum($result);
2802
2803         return $result;
2804     }
2805
2806
2807     /**
59c216 2808      * Subscribe to a specific mailbox(es)
A 2809      *
5c461b 2810      * @param array $a_mboxes Mailbox name(s)
59c216 2811      * @return boolean True on success
c43517 2812      */
59c216 2813     function subscribe($a_mboxes)
A 2814     {
2815         if (!is_array($a_mboxes))
2816             $a_mboxes = array($a_mboxes);
2817
2818         // let this common function do the main work
2819         return $this->_change_subscription($a_mboxes, 'subscribe');
2820     }
2821
2822
2823     /**
2824      * Unsubscribe mailboxes
2825      *
5c461b 2826      * @param array $a_mboxes Mailbox name(s)
59c216 2827      * @return boolean True on success
A 2828      */
2829     function unsubscribe($a_mboxes)
2830     {
2831         if (!is_array($a_mboxes))
2832             $a_mboxes = array($a_mboxes);
2833
2834         // let this common function do the main work
2835         return $this->_change_subscription($a_mboxes, 'unsubscribe');
2836     }
2837
2838
2839     /**
2840      * Create a new mailbox on the server and register it in local cache
2841      *
d08333 2842      * @param string  $mailbox   New mailbox name
5c461b 2843      * @param boolean $subscribe True if the new mailbox should be subscribed
d08333 2844      *
A 2845      * @return boolean True on success
59c216 2846      */
d08333 2847     function create_mailbox($mailbox, $subscribe=false)
59c216 2848     {
d08333 2849         $result = $this->conn->createFolder($mailbox);
59c216 2850
A 2851         // try to subscribe it
392589 2852         if ($result) {
A 2853             // clear cache
ccc059 2854             $this->clear_cache('mailboxes', true);
392589 2855
A 2856             if ($subscribe)
2857                 $this->subscribe($mailbox);
2858         }
59c216 2859
af3c04 2860         return $result;
59c216 2861     }
A 2862
2863
2864     /**
2865      * Set a new name to an existing mailbox
2866      *
d08333 2867      * @param string $mailbox  Mailbox to rename
A 2868      * @param string $new_name New mailbox name
0e1194 2869      *
af3c04 2870      * @return boolean True on success
59c216 2871      */
d08333 2872     function rename_mailbox($mailbox, $new_name)
59c216 2873     {
d08333 2874         if (!strlen($new_name)) {
A 2875             return false;
2876         }
59c216 2877
d08333 2878         $delm = $this->get_hierarchy_delimiter();
c43517 2879
0e1194 2880         // get list of subscribed folders
A 2881         if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false)) {
d08333 2882             $a_subscribed = $this->_list_mailboxes('', $mailbox . $delm . '*');
A 2883             $subscribed   = $this->mailbox_exists($mailbox, true);
0e1194 2884         }
A 2885         else {
2886             $a_subscribed = $this->_list_mailboxes();
2887             $subscribed   = in_array($mailbox, $a_subscribed);
2888         }
59c216 2889
d08333 2890         $result = $this->conn->renameFolder($mailbox, $new_name);
59c216 2891
A 2892         if ($result) {
0e1194 2893             // unsubscribe the old folder, subscribe the new one
A 2894             if ($subscribed) {
2895                 $this->conn->unsubscribe($mailbox);
d08333 2896                 $this->conn->subscribe($new_name);
0e1194 2897             }
677e1f 2898
59c216 2899             // check if mailbox children are subscribed
0e1194 2900             foreach ($a_subscribed as $c_subscribed) {
3d4e8a 2901                 if (strpos($c_subscribed, $mailbox.$delm) === 0) {
59c216 2902                     $this->conn->unsubscribe($c_subscribed);
A 2903                     $this->conn->subscribe(preg_replace('/^'.preg_quote($mailbox, '/').'/',
d08333 2904                         $new_name, $c_subscribed));
80152b 2905
A 2906                     // clear cache
2907                     $this->clear_message_cache($c_subscribed);
59c216 2908                 }
0e1194 2909             }
59c216 2910
A 2911             // clear cache
80152b 2912             $this->clear_message_cache($mailbox);
ccc059 2913             $this->clear_cache('mailboxes', true);
59c216 2914         }
A 2915
af3c04 2916         return $result;
59c216 2917     }
A 2918
2919
2920     /**
0e1194 2921      * Remove mailbox from server
59c216 2922      *
d08333 2923      * @param string $mailbox Mailbox name
0e1194 2924      *
59c216 2925      * @return boolean True on success
A 2926      */
d08333 2927     function delete_mailbox($mailbox)
59c216 2928     {
d08333 2929         $delm = $this->get_hierarchy_delimiter();
59c216 2930
0e1194 2931         // get list of folders
A 2932         if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false))
2933             $sub_mboxes = $this->list_unsubscribed('', $mailbox . $delm . '*');
2934         else
2935             $sub_mboxes = $this->list_unsubscribed();
59c216 2936
0e1194 2937         // send delete command to server
A 2938         $result = $this->conn->deleteFolder($mailbox);
59c216 2939
0e1194 2940         if ($result) {
A 2941             // unsubscribe mailbox
2942             $this->conn->unsubscribe($mailbox);
59c216 2943
0e1194 2944             foreach ($sub_mboxes as $c_mbox) {
3d4e8a 2945                 if (strpos($c_mbox, $mailbox.$delm) === 0) {
0e1194 2946                     $this->conn->unsubscribe($c_mbox);
A 2947                     if ($this->conn->deleteFolder($c_mbox)) {
80152b 2948                         $this->clear_message_cache($c_mbox);
59c216 2949                     }
A 2950                 }
2951             }
0e1194 2952
A 2953             // clear mailbox-related cache
80152b 2954             $this->clear_message_cache($mailbox);
ccc059 2955             $this->clear_cache('mailboxes', true);
59c216 2956         }
A 2957
0e1194 2958         return $result;
59c216 2959     }
A 2960
2961
2962     /**
2963      * Create all folders specified as default
2964      */
2965     function create_default_folders()
2966     {
2967         // create default folders if they do not exist
2968         foreach ($this->default_folders as $folder) {
2969             if (!$this->mailbox_exists($folder))
2970                 $this->create_mailbox($folder, true);
2971             else if (!$this->mailbox_exists($folder, true))
2972                 $this->subscribe($folder);
2973         }
2974     }
2975
2976
2977     /**
2978      * Checks if folder exists and is subscribed
2979      *
d08333 2980      * @param string   $mailbox      Folder name
5c461b 2981      * @param boolean  $subscription Enable subscription checking
d08333 2982      *
59c216 2983      * @return boolean TRUE or FALSE
A 2984      */
d08333 2985     function mailbox_exists($mailbox, $subscription=false)
59c216 2986     {
d08333 2987         if ($mailbox == 'INBOX') {
448409 2988             return true;
d08333 2989         }
59c216 2990
448409 2991         $key  = $subscription ? 'subscribed' : 'existing';
ad5881 2992
d08333 2993         if (is_array($this->icache[$key]) && in_array($mailbox, $this->icache[$key]))
448409 2994             return true;
16378f 2995
448409 2996         if ($subscription) {
d08333 2997             $a_folders = $this->conn->listSubscribed('', $mailbox);
448409 2998         }
A 2999         else {
d08333 3000             $a_folders = $this->conn->listMailboxes('', $mailbox);
af3c04 3001         }
c43517 3002
d08333 3003         if (is_array($a_folders) && in_array($mailbox, $a_folders)) {
A 3004             $this->icache[$key][] = $mailbox;
448409 3005             return true;
59c216 3006         }
A 3007
3008         return false;
3009     }
3010
3011
3012     /**
bbce3e 3013      * Returns the namespace where the folder is in
A 3014      *
d08333 3015      * @param string $mailbox Folder name
bbce3e 3016      *
A 3017      * @return string One of 'personal', 'other' or 'shared'
3018      * @access public
3019      */
d08333 3020     function mailbox_namespace($mailbox)
bbce3e 3021     {
d08333 3022         if ($mailbox == 'INBOX') {
bbce3e 3023             return 'personal';
A 3024         }
3025
3026         foreach ($this->namespace as $type => $namespace) {
3027             if (is_array($namespace)) {
3028                 foreach ($namespace as $ns) {
305b36 3029                     if ($len = strlen($ns[0])) {
A 3030                         if (($len > 1 && $mailbox == substr($ns[0], 0, -1))
d08333 3031                             || strpos($mailbox, $ns[0]) === 0
bbce3e 3032                         ) {
A 3033                             return $type;
3034                         }
3035                     }
3036                 }
3037             }
3038         }
3039
3040         return 'personal';
3041     }
3042
3043
3044     /**
d08333 3045      * Modify folder name according to namespace.
A 3046      * For output it removes prefix of the personal namespace if it's possible.
3047      * For input it adds the prefix. Use it before creating a folder in root
3048      * of the folders tree.
59c216 3049      *
d08333 3050      * @param string $mailbox Folder name
A 3051      * @param string $mode    Mode name (out/in)
3052      *
59c216 3053      * @return string Folder name
A 3054      */
d08333 3055     function mod_mailbox($mailbox, $mode = 'out')
59c216 3056     {
d08333 3057         if (!strlen($mailbox)) {
A 3058             return $mailbox;
3059         }
59c216 3060
d08333 3061         $prefix     = $this->namespace['prefix']; // see set_env()
A 3062         $prefix_len = strlen($prefix);
3063
3064         if (!$prefix_len) {
3065             return $mailbox;
3066         }
3067
3068         // remove prefix for output
3069         if ($mode == 'out') {
3070             if (substr($mailbox, 0, $prefix_len) === $prefix) {
3071                 return substr($mailbox, $prefix_len);
00290a 3072             }
A 3073         }
d08333 3074         // add prefix for input (e.g. folder creation)
00290a 3075         else {
d08333 3076             return $prefix . $mailbox;
59c216 3077         }
c43517 3078
d08333 3079         return $mailbox;
59c216 3080     }
A 3081
3082
103ddc 3083     /**
aa07b2 3084      * Gets folder attributes from LIST response, e.g. \Noselect, \Noinferiors
a5a4bf 3085      *
d08333 3086      * @param string $mailbox Folder name
aa07b2 3087      * @param bool   $force   Set to True if attributes should be refreshed
a5a4bf 3088      *
A 3089      * @return array Options list
3090      */
aa07b2 3091     function mailbox_attributes($mailbox, $force=false)
a5a4bf 3092     {
aa07b2 3093         // get attributes directly from LIST command
A 3094         if (!empty($this->conn->data['LIST']) && is_array($this->conn->data['LIST'][$mailbox])) {
3095             $opts = $this->conn->data['LIST'][$mailbox];
3096         }
3097         // get cached folder attributes
3098         else if (!$force) {
3099             $opts = $this->get_cache('mailboxes.attributes');
3100             $opts = $opts[$mailbox];
a5a4bf 3101         }
A 3102
aa07b2 3103         if (!is_array($opts)) {
A 3104             $this->conn->listMailboxes('', $mailbox);
3105             $opts = $this->conn->data['LIST'][$mailbox];
a5a4bf 3106         }
A 3107
3108         return is_array($opts) ? $opts : array();
80152b 3109     }
A 3110
3111
3112     /**
3113      * Gets connection (and current mailbox) data: UIDVALIDITY, EXISTS, RECENT,
3114      * PERMANENTFLAGS, UIDNEXT, UNSEEN
3115      *
3116      * @param string $mailbox Folder name
3117      *
3118      * @return array Data
3119      */
3120     function mailbox_data($mailbox)
3121     {
3122         if (!strlen($mailbox))
3123             $mailbox = $this->mailbox !== null ? $this->mailbox : 'INBOX';
3124
3125         if ($this->conn->selected != $mailbox) {
3126             if ($this->conn->select($mailbox))
3127                 $this->mailbox = $mailbox;
609d39 3128             else
A 3129                 return null;
80152b 3130         }
A 3131
3132         $data = $this->conn->data;
3133
3134         // add (E)SEARCH result for ALL UNDELETED query
40c45e 3135         if (!empty($this->icache['undeleted_idx'])
A 3136             && $this->icache['undeleted_idx']->getParameters('MAILBOX') == $mailbox
3137         ) {
3138             $data['UNDELETED'] = $this->icache['undeleted_idx'];
80152b 3139         }
A 3140
3141         return $data;
a5a4bf 3142     }
A 3143
3144
3145     /**
25e6a0 3146      * Returns extended information about the folder
A 3147      *
3148      * @param string $mailbox Folder name
3149      *
3150      * @return array Data
3151      */
3152     function mailbox_info($mailbox)
3153     {
2ce8e5 3154         if ($this->icache['options'] && $this->icache['options']['name'] == $mailbox) {
A 3155             return $this->icache['options'];
3156         }
3157
25e6a0 3158         $acl       = $this->get_capability('ACL');
A 3159         $namespace = $this->get_namespace();
3160         $options   = array();
3161
3162         // check if the folder is a namespace prefix
3163         if (!empty($namespace)) {
3164             $mbox = $mailbox . $this->delimiter;
3165             foreach ($namespace as $ns) {
68070e 3166                 if (!empty($ns)) {
A 3167                     foreach ($ns as $item) {
3168                         if ($item[0] === $mbox) {
3169                             $options['is_root'] = true;
922016 3170                             break 2;
68070e 3171                         }
25e6a0 3172                     }
A 3173                 }
3174             }
3175         }
922016 3176         // check if the folder is other user virtual-root
A 3177         if (!$options['is_root'] && !empty($namespace) && !empty($namespace['other'])) {
3178             $parts = explode($this->delimiter, $mailbox);
3179             if (count($parts) == 2) {
3180                 $mbox = $parts[0] . $this->delimiter;
3181                 foreach ($namespace['other'] as $item) {
3182                     if ($item[0] === $mbox) {
3183                         $options['is_root'] = true;
3184                         break;
3185                     }
3186                 }
3187             }
3188         }
25e6a0 3189
aa07b2 3190         $options['name']       = $mailbox;
A 3191         $options['attributes'] = $this->mailbox_attributes($mailbox, true);
3192         $options['namespace']  = $this->mailbox_namespace($mailbox);
3193         $options['rights']     = $acl && !$options['is_root'] ? (array)$this->my_rights($mailbox) : array();
3194         $options['special']    = in_array($mailbox, $this->default_folders);
25e6a0 3195
1cd362 3196         // Set 'noselect' and 'norename' flags
aa07b2 3197         if (is_array($options['attributes'])) {
A 3198             foreach ($options['attributes'] as $attrib) {
3199                 $attrib = strtolower($attrib);
3200                 if ($attrib == '\noselect' || $attrib == '\nonexistent') {
25e6a0 3201                     $options['noselect'] = true;
A 3202                 }
3203             }
3204         }
3205         else {
3206             $options['noselect'] = true;
3207         }
3208
3209         if (!empty($options['rights'])) {
30f505 3210             $options['norename'] = !in_array('x', $options['rights']) && !in_array('d', $options['rights']);
A 3211
25e6a0 3212             if (!$options['noselect']) {
A 3213                 $options['noselect'] = !in_array('r', $options['rights']);
3214             }
3215         }
1cd362 3216         else {
A 3217             $options['norename'] = $options['is_root'] || $options['namespace'] != 'personal';
3218         }
25e6a0 3219
2ce8e5 3220         $this->icache['options'] = $options;
A 3221
25e6a0 3222         return $options;
A 3223     }
3224
3225
3226     /**
609d39 3227      * Synchronizes messages cache.
A 3228      *
3229      * @param string $mailbox Folder name
3230      */
3231     public function mailbox_sync($mailbox)
3232     {
3233         if ($mcache = $this->get_mcache_engine()) {
3234             $mcache->synchronize($mailbox);
3235         }
3236     }
3237
3238
3239     /**
103ddc 3240      * Get message header names for rcube_imap_generic::fetchHeader(s)
A 3241      *
3242      * @return string Space-separated list of header names
3243      */
3244     private function get_fetch_headers()
3245     {
3246         $headers = explode(' ', $this->fetch_add_headers);
3247         $headers = array_map('strtoupper', $headers);
3248
5cf5ee 3249         if ($this->messages_caching || $this->get_all_headers)
103ddc 3250             $headers = array_merge($headers, $this->all_headers);
A 3251
3252         return implode(' ', array_unique($headers));
3253     }
3254
3255
8b6eff 3256     /* -----------------------------------------
A 3257      *   ACL and METADATA/ANNOTATEMORE methods
3258      * ----------------------------------------*/
3259
3260     /**
3261      * Changes the ACL on the specified mailbox (SETACL)
3262      *
3263      * @param string $mailbox Mailbox name
3264      * @param string $user    User name
3265      * @param string $acl     ACL string
3266      *
3267      * @return boolean True on success, False on failure
3268      *
3269      * @access public
3270      * @since 0.5-beta
3271      */
3272     function set_acl($mailbox, $user, $acl)
3273     {
3274         if ($this->get_capability('ACL'))
3275             return $this->conn->setACL($mailbox, $user, $acl);
3276
3277         return false;
3278     }
3279
3280
3281     /**
3282      * Removes any <identifier,rights> pair for the
3283      * specified user from the ACL for the specified
3284      * mailbox (DELETEACL)
3285      *
3286      * @param string $mailbox Mailbox name
3287      * @param string $user    User name
3288      *
3289      * @return boolean True on success, False on failure
3290      *
3291      * @access public
3292      * @since 0.5-beta
3293      */
3294     function delete_acl($mailbox, $user)
3295     {
3296         if ($this->get_capability('ACL'))
3297             return $this->conn->deleteACL($mailbox, $user);
3298
3299         return false;
3300     }
3301
3302
3303     /**
3304      * Returns the access control list for mailbox (GETACL)
3305      *
3306      * @param string $mailbox Mailbox name
3307      *
3308      * @return array User-rights array on success, NULL on error
3309      * @access public
3310      * @since 0.5-beta
3311      */
3312     function get_acl($mailbox)
3313     {
3314         if ($this->get_capability('ACL'))
3315             return $this->conn->getACL($mailbox);
3316
3317         return NULL;
3318     }
3319
3320
3321     /**
3322      * Returns information about what rights can be granted to the
3323      * user (identifier) in the ACL for the mailbox (LISTRIGHTS)
3324      *
3325      * @param string $mailbox Mailbox name
3326      * @param string $user    User name
3327      *
3328      * @return array List of user rights
3329      * @access public
3330      * @since 0.5-beta
3331      */
3332     function list_rights($mailbox, $user)
3333     {
3334         if ($this->get_capability('ACL'))
3335             return $this->conn->listRights($mailbox, $user);
3336
3337         return NULL;
3338     }
3339
3340
3341     /**
3342      * Returns the set of rights that the current user has to
3343      * mailbox (MYRIGHTS)
3344      *
3345      * @param string $mailbox Mailbox name
3346      *
3347      * @return array MYRIGHTS response on success, NULL on error
3348      * @access public
3349      * @since 0.5-beta
3350      */
3351     function my_rights($mailbox)
3352     {
3353         if ($this->get_capability('ACL'))
3354             return $this->conn->myRights($mailbox);
3355
3356         return NULL;
3357     }
3358
3359
3360     /**
3361      * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3362      *
3363      * @param string $mailbox Mailbox name (empty for server metadata)
3364      * @param array  $entries Entry-value array (use NULL value as NIL)
3365      *
3366      * @return boolean True on success, False on failure
3367      * @access public
3368      * @since 0.5-beta
3369      */
3370     function set_metadata($mailbox, $entries)
3371     {
448409 3372         if ($this->get_capability('METADATA') ||
A 3373             (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
8b6eff 3374         ) {
A 3375             return $this->conn->setMetadata($mailbox, $entries);
3376         }
3377         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
e22740 3378             foreach ((array)$entries as $entry => $value) {
8b6eff 3379                 list($ent, $attr) = $this->md2annotate($entry);
A 3380                 $entries[$entry] = array($ent, $attr, $value);
3381             }
3382             return $this->conn->setAnnotation($mailbox, $entries);
3383         }
3384
3385         return false;
3386     }
3387
3388
3389     /**
3390      * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3391      *
3392      * @param string $mailbox Mailbox name (empty for server metadata)
3393      * @param array  $entries Entry names array
3394      *
3395      * @return boolean True on success, False on failure
3396      *
3397      * @access public
3398      * @since 0.5-beta
3399      */
3400     function delete_metadata($mailbox, $entries)
3401     {
3402         if ($this->get_capability('METADATA') || 
448409 3403             (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
8b6eff 3404         ) {
A 3405             return $this->conn->deleteMetadata($mailbox, $entries);
3406         }
3407         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
e22740 3408             foreach ((array)$entries as $idx => $entry) {
8b6eff 3409                 list($ent, $attr) = $this->md2annotate($entry);
A 3410                 $entries[$idx] = array($ent, $attr, NULL);
3411             }
3412             return $this->conn->setAnnotation($mailbox, $entries);
3413         }
3414
3415         return false;
3416     }
3417
3418
3419     /**
3420      * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
3421      *
3422      * @param string $mailbox Mailbox name (empty for server metadata)
3423      * @param array  $entries Entries
3424      * @param array  $options Command options (with MAXSIZE and DEPTH keys)
3425      *
3426      * @return array Metadata entry-value hash array on success, NULL on error
3427      *
3428      * @access public
3429      * @since 0.5-beta
3430      */
3431     function get_metadata($mailbox, $entries, $options=array())
3432     {
3433         if ($this->get_capability('METADATA') || 
e22740 3434             (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
8b6eff 3435         ) {
A 3436             return $this->conn->getMetadata($mailbox, $entries, $options);
3437         }
3438         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3439             $queries = array();
3440             $res     = array();
3441
3442             // Convert entry names
e22740 3443             foreach ((array)$entries as $entry) {
8b6eff 3444                 list($ent, $attr) = $this->md2annotate($entry);
A 3445                 $queries[$attr][] = $ent;
3446             }
3447
3448             // @TODO: Honor MAXSIZE and DEPTH options
3449             foreach ($queries as $attrib => $entry)
3450                 if ($result = $this->conn->getAnnotation($mailbox, $entry, $attrib))
00d424 3451                     $res = array_merge_recursive($res, $result);
8b6eff 3452
A 3453             return $res;
3454         }
3455
3456         return NULL;
3457     }
3458
3459
3460     /**
3461      * Converts the METADATA extension entry name into the correct
3462      * entry-attrib names for older ANNOTATEMORE version.
3463      *
e22740 3464      * @param string $entry Entry name
8b6eff 3465      *
A 3466      * @return array Entry-attribute list, NULL if not supported (?)
3467      */
e22740 3468     private function md2annotate($entry)
8b6eff 3469     {
A 3470         if (substr($entry, 0, 7) == '/shared') {
3471             return array(substr($entry, 7), 'value.shared');
3472         }
3473         else if (substr($entry, 0, 8) == '/private') {
3474             return array(substr($entry, 8), 'value.priv');
3475         }
3476
3477         // @TODO: log error
3478         return NULL;
3479     }
3480
3481
59c216 3482     /* --------------------------------
A 3483      *   internal caching methods
3484      * --------------------------------*/
3485
3486     /**
5cf5ee 3487      * Enable or disable indexes caching
5c461b 3488      *
682819 3489      * @param string $type Cache type (@see rcmail::get_cache)
59c216 3490      * @access public
A 3491      */
5cf5ee 3492     function set_caching($type)
59c216 3493     {
5cf5ee 3494         if ($type) {
682819 3495             $this->caching = $type;
5cf5ee 3496         }
A 3497         else {
3498             if ($this->cache)
3499                 $this->cache->close();
80152b 3500             $this->cache   = null;
341d96 3501             $this->caching = false;
5cf5ee 3502         }
59c216 3503     }
A 3504
341d96 3505     /**
A 3506      * Getter for IMAP cache object
3507      */
3508     private function get_cache_engine()
3509     {
3510         if ($this->caching && !$this->cache) {
3511             $rcmail = rcmail::get_instance();
682819 3512             $this->cache = $rcmail->get_cache('IMAP', $this->caching);
341d96 3513         }
A 3514
3515         return $this->cache;
3516     }
29983c 3517
59c216 3518     /**
5c461b 3519      * Returns cached value
A 3520      *
3521      * @param string $key Cache key
3522      * @return mixed
59c216 3523      * @access public
A 3524      */
3525     function get_cache($key)
3526     {
341d96 3527         if ($cache = $this->get_cache_engine()) {
A 3528             return $cache->get($key);
59c216 3529         }
A 3530     }
29983c 3531
59c216 3532     /**
5c461b 3533      * Update cache
A 3534      *
3535      * @param string $key  Cache key
3536      * @param mixed  $data Data
1f385b 3537      * @access public
59c216 3538      */
1f385b 3539     function update_cache($key, $data)
59c216 3540     {
341d96 3541         if ($cache = $this->get_cache_engine()) {
A 3542             $cache->set($key, $data);
59c216 3543         }
A 3544     }
3545
3546     /**
5c461b 3547      * Clears the cache.
A 3548      *
ccc059 3549      * @param string  $key         Cache key name or pattern
A 3550      * @param boolean $prefix_mode Enable it to clear all keys starting
3551      *                             with prefix specified in $key
59c216 3552      * @access public
A 3553      */
ccc059 3554     function clear_cache($key=null, $prefix_mode=false)
59c216 3555     {
341d96 3556         if ($cache = $this->get_cache_engine()) {
A 3557             $cache->remove($key, $prefix_mode);
59c216 3558         }
A 3559     }
3560
3561
3562     /* --------------------------------
3563      *   message caching methods
3564      * --------------------------------*/
5cf5ee 3565
A 3566     /**
3567      * Enable or disable messages caching
3568      *
3569      * @param boolean $set Flag
3570      */
3571     function set_messages_caching($set)
3572     {
80152b 3573         if ($set) {
5cf5ee 3574             $this->messages_caching = true;
A 3575         }
3576         else {
80152b 3577             if ($this->mcache)
A 3578                 $this->mcache->close();
3579             $this->mcache = null;
5cf5ee 3580             $this->messages_caching = false;
A 3581         }
3582     }
c43517 3583
1c4f23 3584
59c216 3585     /**
80152b 3586      * Getter for messages cache object
A 3587      */
3588     private function get_mcache_engine()
3589     {
3590         if ($this->messages_caching && !$this->mcache) {
3591             $rcmail = rcmail::get_instance();
3592             if ($dbh = $rcmail->get_dbh()) {
3593                 $this->mcache = new rcube_imap_cache(
3594                     $dbh, $this, $rcmail->user->ID, $this->skip_deleted);
3595             }
3596         }
3597
3598         return $this->mcache;
3599     }
3600
1c4f23 3601
80152b 3602     /**
A 3603      * Clears the messages cache.
59c216 3604      *
80152b 3605      * @param string $mailbox Folder name
A 3606      * @param array  $uids    Optional message UIDs to remove from cache
59c216 3607      */
80152b 3608     function clear_message_cache($mailbox = null, $uids = null)
59c216 3609     {
80152b 3610         if ($mcache = $this->get_mcache_engine()) {
A 3611             $mcache->clear($mailbox, $uids);
59c216 3612         }
A 3613     }
3614
3615
3616     /* --------------------------------
3617      *         private methods
3618      * --------------------------------*/
3619
3620     /**
3621      * Validate the given input and save to local properties
5c461b 3622      *
A 3623      * @param string $sort_field Sort column
3624      * @param string $sort_order Sort order
59c216 3625      * @access private
A 3626      */
40c45e 3627     private function set_sort_order($sort_field, $sort_order)
59c216 3628     {
A 3629         if ($sort_field != null)
3630             $this->sort_field = asciiwords($sort_field);
3631         if ($sort_order != null)
3632             $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
3633     }
3634
29983c 3635
59c216 3636     /**
A 3637      * Sort mailboxes first by default folders and then in alphabethical order
5c461b 3638      *
A 3639      * @param array $a_folders Mailboxes list
59c216 3640      * @access private
A 3641      */
3642     private function _sort_mailbox_list($a_folders)
3643     {
3644         $a_out = $a_defaults = $folders = array();
3645
3646         $delimiter = $this->get_hierarchy_delimiter();
3647
3648         // find default folders and skip folders starting with '.'
3649         foreach ($a_folders as $i => $folder) {
3650             if ($folder[0] == '.')
3651                 continue;
3652
3653             if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p])
3654                 $a_defaults[$p] = $folder;
3655             else
a44682 3656                 $folders[$folder] = rcube_charset_convert($folder, 'UTF7-IMAP');
59c216 3657         }
A 3658
3659         // sort folders and place defaults on the top
3660         asort($folders, SORT_LOCALE_STRING);
3661         ksort($a_defaults);
3662         $folders = array_merge($a_defaults, array_keys($folders));
3663
c43517 3664         // finally we must rebuild the list to move
59c216 3665         // subfolders of default folders to their place...
A 3666         // ...also do this for the rest of folders because
3667         // asort() is not properly sorting case sensitive names
3668         while (list($key, $folder) = each($folders)) {
c43517 3669             // set the type of folder name variable (#1485527)
59c216 3670             $a_out[] = (string) $folder;
A 3671             unset($folders[$key]);
c43517 3672             $this->_rsort($folder, $delimiter, $folders, $a_out);
59c216 3673         }
A 3674
3675         return $a_out;
3676     }
3677
3678
3679     /**
3680      * @access private
3681      */
3682     private function _rsort($folder, $delimiter, &$list, &$out)
3683     {
3684         while (list($key, $name) = each($list)) {
3685             if (strpos($name, $folder.$delimiter) === 0) {
c43517 3686                 // set the type of folder name variable (#1485527)
59c216 3687                 $out[] = (string) $name;
A 3688                 unset($list[$key]);
3689                 $this->_rsort($name, $delimiter, $list, $out);
3690             }
3691         }
c43517 3692         reset($list);
59c216 3693     }
A 3694
3695
3696     /**
80152b 3697      * Find UID of the specified message sequence ID
A 3698      *
3699      * @param int    $id       Message (sequence) ID
3700      * @param string $mailbox  Mailbox name
d08333 3701      *
29983c 3702      * @return int Message UID
59c216 3703      */
1c4f23 3704     private function id2uid($id, $mailbox = null)
59c216 3705     {
d08333 3706         if (!strlen($mailbox)) {
A 3707             $mailbox = $this->mailbox;
3708         }
59c216 3709
d08333 3710         if ($uid = array_search($id, (array)$this->uid_id_map[$mailbox])) {
59c216 3711             return $uid;
d08333 3712         }
59c216 3713
40c45e 3714         $uid = $this->conn->ID2UID($mailbox, $id);
29983c 3715
d08333 3716         $this->uid_id_map[$mailbox][$uid] = $id;
c43517 3717
59c216 3718         return $uid;
A 3719     }
3720
3721
3722     /**
3723      * Subscribe/unsubscribe a list of mailboxes and update local cache
3724      * @access private
3725      */
3726     private function _change_subscription($a_mboxes, $mode)
3727     {
3728         $updated = false;
3729
3730         if (is_array($a_mboxes))
d08333 3731             foreach ($a_mboxes as $i => $mailbox) {
59c216 3732                 $a_mboxes[$i] = $mailbox;
A 3733
d08333 3734                 if ($mode == 'subscribe')
59c216 3735                     $updated = $this->conn->subscribe($mailbox);
d08333 3736                 else if ($mode == 'unsubscribe')
59c216 3737                     $updated = $this->conn->unsubscribe($mailbox);
A 3738             }
3739
3253b2 3740         // clear cached mailbox list(s)
59c216 3741         if ($updated) {
ccc059 3742             $this->clear_cache('mailboxes', true);
59c216 3743         }
A 3744
3745         return $updated;
3746     }
3747
3748
3749     /**
3750      * Increde/decrese messagecount for a specific mailbox
3751      * @access private
3752      */
d08333 3753     private function _set_messagecount($mailbox, $mode, $increment)
59c216 3754     {
A 3755         $mode = strtoupper($mode);
3756         $a_mailbox_cache = $this->get_cache('messagecount');
c43517 3757
59c216 3758         if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
A 3759             return false;
c43517 3760
59c216 3761         // add incremental value to messagecount
A 3762         $a_mailbox_cache[$mailbox][$mode] += $increment;
c43517 3763
59c216 3764         // there's something wrong, delete from cache
A 3765         if ($a_mailbox_cache[$mailbox][$mode] < 0)
3766             unset($a_mailbox_cache[$mailbox][$mode]);
3767
3768         // write back to cache
3769         $this->update_cache('messagecount', $a_mailbox_cache);
c43517 3770
59c216 3771         return true;
A 3772     }
3773
3774
3775     /**
3776      * Remove messagecount of a specific mailbox from cache
3777      * @access private
3778      */
d08333 3779     private function _clear_messagecount($mailbox, $mode=null)
59c216 3780     {
A 3781         $a_mailbox_cache = $this->get_cache('messagecount');
3782
3783         if (is_array($a_mailbox_cache[$mailbox])) {
c309cd 3784             if ($mode) {
A 3785                 unset($a_mailbox_cache[$mailbox][$mode]);
3786             }
3787             else {
3788                 unset($a_mailbox_cache[$mailbox]);
3789             }
59c216 3790             $this->update_cache('messagecount', $a_mailbox_cache);
A 3791         }
6c68cb 3792     }
A 3793
3794
3795     /**
7f1da4 3796      * This is our own debug handler for the IMAP connection
A 3797      * @access public
3798      */
3799     public function debug_handler(&$imap, $message)
3800     {
3801         write_log('imap', $message);
3802     }
3803
6d969b 3804 }  // end class rcube_imap
4e17e6 3805
8d4bcd 3806
T 3807 /**
3808  * Class representing a message part
6d969b 3809  *
T 3810  * @package Mail
8d4bcd 3811  */
T 3812 class rcube_message_part
3813 {
59c216 3814     var $mime_id = '';
A 3815     var $ctype_primary = 'text';
3816     var $ctype_secondary = 'plain';
3817     var $mimetype = 'text/plain';
3818     var $disposition = '';
3819     var $filename = '';
3820     var $encoding = '8bit';
3821     var $charset = '';
3822     var $size = 0;
3823     var $headers = array();
3824     var $d_parameters = array();
3825     var $ctype_parameters = array();
8d4bcd 3826
59c216 3827     function __clone()
A 3828     {
3829         if (isset($this->parts))
3830             foreach ($this->parts as $idx => $part)
3831                 if (is_object($part))
3832                     $this->parts[$idx] = clone $part;
3833     }
8d4bcd 3834 }
4e17e6 3835
T 3836
7e93ff 3837 /**
59c216 3838  * Class for sorting an array of rcube_mail_header objects in a predetermined order.
7e93ff 3839  *
6d969b 3840  * @package Mail
7e93ff 3841  * @author Eric Stadtherr
T 3842  */
3843 class rcube_header_sorter
3844 {
80152b 3845     private $uids = array();
A 3846
c43517 3847
59c216 3848     /**
A 3849      * Set the predetermined sort order.
3850      *
40c45e 3851      * @param array $index  Numerically indexed array of IMAP UIDs
59c216 3852      */
40c45e 3853     function set_index($index)
59c216 3854     {
80152b 3855         $index = array_flip($index);
A 3856
40c45e 3857         $this->uids = $index;
59c216 3858     }
A 3859
3860     /**
3861      * Sort the array of header objects
3862      *
5c461b 3863      * @param array $headers Array of rcube_mail_header objects indexed by UID
59c216 3864      */
A 3865     function sort_headers(&$headers)
3866     {
40c45e 3867         uksort($headers, array($this, "compare_uids"));
80152b 3868     }
A 3869
3870     /**
3871      * Sort method called by uksort()
3872      *
3873      * @param int $a Array key (UID)
3874      * @param int $b Array key (UID)
3875      */
3876     function compare_uids($a, $b)
3877     {
3878         // then find each sequence number in my ordered list
3879         $posa = isset($this->uids[$a]) ? intval($this->uids[$a]) : -1;
3880         $posb = isset($this->uids[$b]) ? intval($this->uids[$b]) : -1;
c43517 3881
59c216 3882         // return the relative position as the comparison value
A 3883         return $posa - $posb;
3884     }
7e93ff 3885 }