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