Aleksander Machniak
2012-06-06 d1d0564a91812e3e58569bfa0ef413d36e130d24
commit | author | age
197601 1 <?php
T 2
3 /*
4  +-----------------------------------------------------------------------+
5  | program/include/rcmail.php                                            |
6  |                                                                       |
e019f2 7  | This file is part of the Roundcube Webmail client                     |
0c2596 8  | Copyright (C) 2008-2012, The Roundcube Dev Team                       |
A 9  | Copyright (C) 2011-2012, Kolab Systems AG                             |
7fe381 10  |                                                                       |
T 11  | Licensed under the GNU General Public License version 3 or            |
12  | any later version with exceptions for skins & plugins.                |
13  | See the README file for a full license statement.                     |
197601 14  |                                                                       |
T 15  | PURPOSE:                                                              |
16  |   Application class providing core functions and holding              |
17  |   instances of all 'global' objects like db- and imap-connections     |
18  +-----------------------------------------------------------------------+
19  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
1aceb9 20  | Author: Aleksander Machniak <alec@alec.pl>                            |
197601 21  +-----------------------------------------------------------------------+
T 22 */
23
24
25 /**
e019f2 26  * Application class of Roundcube Webmail
197601 27  * implemented as singleton
T 28  *
29  * @package Core
30  */
0c2596 31 class rcmail extends rcube
197601 32 {
5c461b 33   /**
A 34    * Main tasks.
35    *
36    * @var array
37    */
677e1f 38   static public $main_tasks = array('mail','settings','addressbook','login','logout','utils','dummy');
5c461b 39
A 40   /**
41    * Current task.
42    *
43    * @var string
44    */
9b94eb 45   public $task;
5c461b 46
A 47   /**
48    * Current action.
49    *
50    * @var string
51    */
197601 52   public $action = '';
T 53   public $comm_path = './';
677e1f 54
0501b6 55   private $address_books = array();
68d2d5 56   private $action_map = array();
677e1f 57
A 58
1aceb9 59   const JS_OBJECT_NAME = 'rcmail';
A 60
197601 61   /**
T 62    * This implements the 'singleton' design pattern
63    *
5c461b 64    * @return rcmail The one and only instance
197601 65    */
T 66   static function get_instance()
67   {
0c2596 68     if (!self::$instance || !is_a(self::$instance, 'rcmail')) {
197601 69       self::$instance = new rcmail();
T 70       self::$instance->startup();  // init AFTER object was linked with self::$instance
71     }
72
73     return self::$instance;
74   }
b62a0d 75
A 76
197601 77   /**
T 78    * Initial startup function
79    * to register session, create database and imap connections
80    */
0c2596 81   protected function startup()
197601 82   {
0c2596 83     $this->init(self::INIT_WITH_DB | self::INIT_WITH_PLUGINS);
a90ad2 84
929a50 85     // start session
A 86     $this->session_init();
197601 87
T 88     // create user object
89     $this->set_user(new rcube_user($_SESSION['user_id']));
929a50 90
A 91     // configure session (after user config merge!)
92     $this->session_configure();
197601 93
9b94eb 94     // set task and action properties
1aceb9 95     $this->set_task(rcube_utils::get_input_value('_task', rcube_utils::INPUT_GPC));
A 96     $this->action = asciiwords(rcube_utils::get_input_value('_action', rcube_utils::INPUT_GPC));
9b94eb 97
197601 98     // reset some session parameters when changing task
677e1f 99     if ($this->task != 'utils') {
A 100       if ($this->session && $_SESSION['task'] != $this->task)
101         $this->session->remove('page');
102       // set current task to session
103       $_SESSION['task'] = $this->task;
104     }
197601 105
48bc52 106     // init output class
A 107     if (!empty($_REQUEST['_remote']))
929a50 108       $GLOBALS['OUTPUT'] = $this->json_init();
48bc52 109     else
A 110       $GLOBALS['OUTPUT'] = $this->load_gui(!empty($_REQUEST['_framed']));
111
0c2596 112     // load plugins
A 113     $this->plugins->init($this, $this->task);
114     $this->plugins->load_plugins((array)$this->config->get('plugins', array()), array('filesystem_attachments', 'jqueryui'));
197601 115   }
b62a0d 116
A 117
197601 118   /**
T 119    * Setter for application task
120    *
121    * @param string Task to set
122    */
123   public function set_task($task)
124   {
1c932d 125     $task = asciiwords($task);
9b94eb 126
A 127     if ($this->user && $this->user->ID)
c3be8e 128       $task = !$task ? 'mail' : $task;
9b94eb 129     else
A 130       $task = 'login';
131
132     $this->task = $task;
1c932d 133     $this->comm_path = $this->url(array('task' => $this->task));
b62a0d 134
197601 135     if ($this->output)
1c932d 136       $this->output->set_env('task', $this->task);
197601 137   }
b62a0d 138
A 139
197601 140   /**
T 141    * Setter for system user object
142    *
5c461b 143    * @param rcube_user Current user instance
197601 144    */
T 145   public function set_user($user)
146   {
147     if (is_object($user)) {
148       $this->user = $user;
b62a0d 149
197601 150       // overwrite config with user preferences
b545d3 151       $this->config->set_user_prefs((array)$this->user->get_prefs());
197601 152     }
b62a0d 153
c8ae24 154     $_SESSION['language'] = $this->user->language = $this->language_prop($this->config->get('language', $_SESSION['language']));
531abb 155
197601 156     // set localization
e80f50 157     setlocale(LC_ALL, $_SESSION['language'] . '.utf8', 'en_US.utf8');
14de18 158
b62a0d 159     // workaround for http://bugs.php.net/bug.php?id=18556
A 160     if (in_array($_SESSION['language'], array('tr_TR', 'ku', 'az_AZ')))
161       setlocale(LC_CTYPE, 'en_US' . '.utf8');
197601 162   }
b62a0d 163
A 164
197601 165   /**
ade8e1 166    * Return instance of the internal address book class
T 167    *
3704b7 168    * @param string  Address book identifier
ade8e1 169    * @param boolean True if the address book needs to be writeable
7f7ed2 170    *
5c461b 171    * @return rcube_contacts Address book object
ade8e1 172    */
T 173   public function get_address_book($id, $writeable = false)
174   {
b896b1 175     $contacts    = null;
ade8e1 176     $ldap_config = (array)$this->config->get('ldap_public');
b896b1 177     $abook_type  = strtolower($this->config->get('address_book_type'));
cc97ea 178
f03d89 179     // 'sql' is the alias for '0' used by autocomplete
A 180     if ($id == 'sql')
181         $id = '0';
182
0501b6 183     // use existing instance
9a835c 184     if (isset($this->address_books[$id]) && is_object($this->address_books[$id])
A 185       && is_a($this->address_books[$id], 'rcube_addressbook')
186       && (!$writeable || !$this->address_books[$id]->readonly)
187     ) {
0501b6 188       $contacts = $this->address_books[$id];
T 189     }
cc97ea 190     else if ($id && $ldap_config[$id]) {
c321a9 191       $contacts = new rcube_ldap($ldap_config[$id], $this->config->get('ldap_debug'), $this->config->mail_domain($_SESSION['storage_host']));
cc97ea 192     }
T 193     else if ($id === '0') {
0c2596 194       $contacts = new rcube_contacts($this->db, $this->get_user_id());
ade8e1 195     }
b896b1 196     else {
A 197       $plugin = $this->plugins->exec_hook('addressbook_get', array('id' => $id, 'writeable' => $writeable));
198
199       // plugin returned instance of a rcube_addressbook
200       if ($plugin['instance'] instanceof rcube_addressbook) {
201         $contacts = $plugin['instance'];
202       }
7f7ed2 203       // get first source from the list
5ed119 204       else if (!$id) {
7f7ed2 205         $source = reset($this->get_address_sources($writeable));
A 206         if (!empty($source)) {
207           $contacts = $this->get_address_book($source['id']);
208           if ($contacts)
209             $id = $source['id'];
5ed119 210         }
ade8e1 211       }
5ed119 212     }
A 213
214     if (!$contacts) {
0c2596 215       self::raise_error(array(
782d85 216         'code' => 700, 'type' => 'php',
5ed119 217         'file' => __FILE__, 'line' => __LINE__,
A 218         'message' => "Addressbook source ($id) not found!"),
219         true, true);
ade8e1 220     }
457373 221
438753 222     // set configured sort order
T 223     if ($sort_col = $this->config->get('addressbook_sort_col'))
224         $contacts->set_sort_order($sort_col);
225
457373 226     // add to the 'books' array for shutdown function
14b342 227     $this->address_books[$id] = $contacts;
b62a0d 228
ade8e1 229     return $contacts;
T 230   }
3704b7 231
A 232
233   /**
234    * Return address books list
235    *
236    * @param boolean True if the address book needs to be writeable
5c9d1f 237    *
3704b7 238    * @return array  Address books array
A 239    */
240   public function get_address_sources($writeable = false)
241   {
242     $abook_type = strtolower($this->config->get('address_book_type'));
7fdb9d 243     $ldap_config = $this->config->get('ldap_public');
A 244     $autocomplete = (array) $this->config->get('autocomplete_addressbooks');
3704b7 245     $list = array();
A 246
247     // We are using the DB address book
248     if ($abook_type != 'ldap') {
0501b6 249       if (!isset($this->address_books['0']))
0c2596 250         $this->address_books['0'] = new rcube_contacts($this->db, $this->get_user_id());
3704b7 251       $list['0'] = array(
5c9d1f 252         'id'       => '0',
0c2596 253         'name'     => $this->gettext('personaladrbook'),
5c9d1f 254         'groups'   => $this->address_books['0']->groups,
8c263e 255         'readonly' => $this->address_books['0']->readonly,
d06e57 256         'autocomplete' => in_array('sql', $autocomplete),
T 257         'undelete' => $this->address_books['0']->undelete && $this->config->get('undo_timeout'),
3704b7 258       );
A 259     }
260
7fdb9d 261     if ($ldap_config) {
A 262       $ldap_config = (array) $ldap_config;
08b7b6 263       foreach ($ldap_config as $id => $prop) {
A 264         // handle misconfiguration
265         if (empty($prop) || !is_array($prop)) {
266           continue;
267         }
3704b7 268         $list[$id] = array(
5c9d1f 269           'id'       => $id,
A 270           'name'     => $prop['name'],
271           'groups'   => is_array($prop['groups']),
a61bbb 272           'readonly' => !$prop['writable'],
5c9d1f 273           'hidden'   => $prop['hidden'],
A 274           'autocomplete' => in_array($id, $autocomplete)
3704b7 275         );
08b7b6 276       }
3704b7 277     }
A 278
e6ce00 279     $plugin = $this->plugins->exec_hook('addressbooks_list', array('sources' => $list));
3704b7 280     $list = $plugin['sources'];
A 281
0501b6 282     foreach ($list as $idx => $item) {
T 283       // register source for shutdown function
284       if (!is_object($this->address_books[$item['id']]))
285         $this->address_books[$item['id']] = $item;
286       // remove from list if not writeable as requested
287       if ($writeable && $item['readonly'])
c0297f 288           unset($list[$idx]);
3704b7 289     }
8c263e 290
3704b7 291     return $list;
A 292   }
b62a0d 293
A 294
ade8e1 295   /**
197601 296    * Init output object for GUI and add common scripts.
T 297    * This will instantiate a rcmail_template object and set
298    * environment vars according to the current session and configuration
0ece58 299    *
T 300    * @param boolean True if this request is loaded in a (i)frame
0c2596 301    * @return rcube_output_html Reference to HTML output object
197601 302    */
T 303   public function load_gui($framed = false)
304   {
305     // init output page
0c2596 306     if (!($this->output instanceof rcube_output_html))
A 307       $this->output = new rcube_output_html($this->task, $framed);
197601 308
95d90f 309     // set keep-alive/check-recent interval
bf67d6 310     if ($this->session && ($keep_alive = $this->session->get_keep_alive())) {
929a50 311       $this->output->set_env('keep_alive', $keep_alive);
95d90f 312     }
197601 313
T 314     if ($framed) {
315       $this->comm_path .= '&_framed=1';
316       $this->output->set_env('framed', true);
317     }
318
319     $this->output->set_env('task', $this->task);
320     $this->output->set_env('action', $this->action);
321     $this->output->set_env('comm_path', $this->comm_path);
79c45f 322     $this->output->set_charset(RCMAIL_CHARSET);
197601 323
7f5a84 324     // add some basic labels to client
110360 325     $this->output->add_label('loading', 'servererror', 'requesttimedout');
b62a0d 326
197601 327     return $this->output;
T 328   }
b62a0d 329
A 330
197601 331   /**
T 332    * Create an output object for JSON responses
0ece58 333    *
0c2596 334    * @return rcube_output_json Reference to JSON output object
197601 335    */
929a50 336   public function json_init()
197601 337   {
0c2596 338     if (!($this->output instanceof rcube_output_json))
A 339       $this->output = new rcube_output_json($this->task);
b62a0d 340
197601 341     return $this->output;
929a50 342   }
A 343
344
345   /**
346    * Create session object and start the session.
347    */
348   public function session_init()
349   {
963a10 350     parent::session_init();
929a50 351
A 352     // set initial session vars
cf2da2 353     if (!$_SESSION['user_id'])
929a50 354       $_SESSION['temp'] = true;
40d246 355
T 356     // restore skin selection after logout
357     if ($_SESSION['temp'] && !empty($_SESSION['skin']))
358       $this->config->set('skin', $_SESSION['skin']);
197601 359   }
T 360
361
362   /**
c321a9 363    * Perfom login to the mail server and to the webmail service.
197601 364    * This will also create a new user entry if auto_create_user is configured.
T 365    *
c321a9 366    * @param string Mail storage (IMAP) user name
T 367    * @param string Mail storage (IMAP) password
368    * @param string Mail storage (IMAP) host
fdff34 369    *
197601 370    * @return boolean True on success, False on failure
T 371    */
372   function login($username, $pass, $host=NULL)
373   {
fdff34 374     if (empty($username)) {
A 375       return false;
376     }
377
197601 378     $config = $this->config->all();
T 379
380     if (!$host)
381       $host = $config['default_host'];
382
383     // Validate that selected host is in the list of configured hosts
384     if (is_array($config['default_host'])) {
385       $allowed = false;
386       foreach ($config['default_host'] as $key => $host_allowed) {
387         if (!is_numeric($key))
388           $host_allowed = $key;
389         if ($host == $host_allowed) {
390           $allowed = true;
391           break;
392         }
393       }
394       if (!$allowed)
395         return false;
396       }
1aceb9 397     else if (!empty($config['default_host']) && $host != rcube_utils::parse_host($config['default_host']))
197601 398       return false;
T 399
400     // parse $host URL
401     $a_host = parse_url($host);
402     if ($a_host['host']) {
403       $host = $a_host['host'];
c321a9 404       $ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null;
e99991 405       if (!empty($a_host['port']))
c321a9 406         $port = $a_host['port'];
T 407       else if ($ssl && $ssl != 'tls' && (!$config['default_port'] || $config['default_port'] == 143))
408         $port = 993;
197601 409     }
b62a0d 410
c321a9 411     if (!$port) {
T 412         $port = $config['default_port'];
413     }
197601 414
b62a0d 415     /* Modify username with domain if required
197601 416        Inspired by Marco <P0L0_notspam_binware.org>
T 417     */
418     // Check if we need to add domain
c16fab 419     if (!empty($config['username_domain']) && strpos($username, '@') === false) {
197601 420       if (is_array($config['username_domain']) && isset($config['username_domain'][$host]))
1aceb9 421         $username .= '@'.rcube_utils::parse_host($config['username_domain'][$host], $host);
197601 422       else if (is_string($config['username_domain']))
1aceb9 423         $username .= '@'.rcube_utils::parse_host($config['username_domain'], $host);
197601 424     }
T 425
c321a9 426     // Convert username to lowercase. If storage backend
e17553 427     // is case-insensitive we need to store always the same username (#1487113)
A 428     if ($config['login_lc']) {
429       $username = mb_strtolower($username);
430     }
431
942069 432     // try to resolve email address from virtuser table
e17553 433     if (strpos($username, '@') && ($virtuser = rcube_user::email2user($username))) {
A 434       $username = $virtuser;
435     }
197601 436
f1adbf 437     // Here we need IDNA ASCII
A 438     // Only rcube_contacts class is using domain names in Unicode
1aceb9 439     $host = rcube_utils::idn_to_ascii($host);
f1adbf 440     if (strpos($username, '@')) {
8f94b1 441       // lowercase domain name
A 442       list($local, $domain) = explode('@', $username);
443       $username = $local . '@' . mb_strtolower($domain);
1aceb9 444       $username = rcube_utils::idn_to_ascii($username);
f1adbf 445     }
A 446
197601 447     // user already registered -> overwrite username
T 448     if ($user = rcube_user::query($username, $host))
449       $username = $user->data['username'];
450
1aceb9 451     $storage = $this->get_storage();
48bc52 452
c321a9 453     // try to log in
1aceb9 454     if (!($login = $storage->connect($host, $username, $pass, $port, $ssl))) {
f1adbf 455       // try with lowercase
6d94ab 456       $username_lc = mb_strtolower($username);
e17553 457       if ($username_lc != $username) {
A 458         // try to find user record again -> overwrite username
459         if (!$user && ($user = rcube_user::query($username_lc, $host)))
460           $username_lc = $user->data['username'];
461
1aceb9 462         if ($login = $storage->connect($host, $username_lc, $pass, $port, $ssl))
e17553 463           $username = $username_lc;
A 464       }
6d94ab 465     }
T 466
c321a9 467     // exit if login failed
T 468     if (!$login) {
197601 469       return false;
c321a9 470     }
197601 471
T 472     // user already registered -> update user's record
473     if (is_object($user)) {
d08333 474       // update last login timestamp
197601 475       $user->touch();
T 476     }
477     // create new system user
478     else if ($config['auto_create_user']) {
479       if ($created = rcube_user::create($username, $host)) {
480         $user = $created;
481       }
f879f4 482       else {
0c2596 483         self::raise_error(array(
782d85 484           'code' => 620, 'type' => 'php',
6d94ab 485           'file' => __FILE__, 'line' => __LINE__,
f879f4 486           'message' => "Failed to create a user record. Maybe aborted by a plugin?"
10eedb 487           ), true, false);
f879f4 488       }
197601 489     }
T 490     else {
0c2596 491       self::raise_error(array(
782d85 492         'code' => 621, 'type' => 'php',
10eedb 493         'file' => __FILE__, 'line' => __LINE__,
782d85 494         'message' => "Access denied for new user $username. 'auto_create_user' is disabled"
197601 495         ), true, false);
T 496     }
497
498     // login succeeded
499     if (is_object($user) && $user->ID) {
f53750 500       // Configure environment
197601 501       $this->set_user($user);
c321a9 502       $this->set_storage_prop();
88ca38 503       $this->session_configure();
f53750 504
A 505       // fix some old settings according to namespace prefix
506       $this->fix_namespace_settings($user);
507
508       // create default folders on first login
509       if ($config['create_default_folders'] && (!empty($created) || empty($user->data['last_login']))) {
1aceb9 510         $storage->create_default_folders();
f53750 511       }
197601 512
T 513       // set session vars
c321a9 514       $_SESSION['user_id']      = $user->ID;
T 515       $_SESSION['username']     = $user->data['username'];
516       $_SESSION['storage_host'] = $host;
517       $_SESSION['storage_port'] = $port;
518       $_SESSION['storage_ssl']  = $ssl;
519       $_SESSION['password']     = $this->encrypt($pass);
6a8b4c 520       $_SESSION['login_time']   = time();
f53750 521
b62a0d 522       if (isset($_REQUEST['_timezone']) && $_REQUEST['_timezone'] != '_default_')
c8ae24 523         $_SESSION['timezone'] = floatval($_REQUEST['_timezone']);
65082b 524       if (isset($_REQUEST['_dstactive']) && $_REQUEST['_dstactive'] != '_default_')
T 525         $_SESSION['dst_active'] = intval($_REQUEST['_dstactive']);
197601 526
T 527       // force reloading complete list of subscribed mailboxes
1aceb9 528       $storage->clear_cache('mailboxes', true);
197601 529
T 530       return true;
531     }
532
533     return false;
534   }
535
536
537   /**
1854c4 538    * Auto-select IMAP host based on the posted login information
T 539    *
540    * @return string Selected IMAP host
541    */
542   public function autoselect_host()
543   {
544     $default_host = $this->config->get('default_host');
257f88 545     $host = null;
b62a0d 546
257f88 547     if (is_array($default_host)) {
1aceb9 548       $post_host = rcube_utils::get_input_value('_host', rcube_utils::INPUT_POST);
45dd7c 549       $post_user = rcube_utils::get_input_value('_user', rcube_utils::INPUT_POST);
AM 550
551       list($user, $domain) = explode('@', $post_user);
b62a0d 552
257f88 553       // direct match in default_host array
T 554       if ($default_host[$post_host] || in_array($post_host, array_values($default_host))) {
555         $host = $post_host;
556       }
557       // try to select host by mail domain
45dd7c 558       else if (!empty($domain)) {
c321a9 559         foreach ($default_host as $storage_host => $mail_domains) {
48f04d 560           if (is_array($mail_domains) && in_array_nocase($domain, $mail_domains)) {
c321a9 561             $host = $storage_host;
48f04d 562             break;
T 563           }
564           else if (stripos($storage_host, $domain) !== false || stripos(strval($mail_domains), $domain) !== false) {
565             $host = is_numeric($storage_host) ? $mail_domains : $storage_host;
1854c4 566             break;
T 567           }
568         }
569       }
570
48f04d 571       // take the first entry if $host is still not set
257f88 572       if (empty($host)) {
48f04d 573         list($key, $val) = each($default_host);
T 574         $host = is_numeric($key) ? $val : $key;
257f88 575       }
T 576     }
577     else if (empty($default_host)) {
1aceb9 578       $host = rcube_utils::get_input_value('_host', rcube_utils::INPUT_POST);
1854c4 579     }
eec34e 580     else
1aceb9 581       $host = rcube_utils::parse_host($default_host);
1854c4 582
T 583     return $host;
584   }
585
586
587   /**
588    * Destroy session data and remove cookie
589    */
590   public function kill_session()
591   {
e6ce00 592     $this->plugins->exec_hook('session_destroy');
b62a0d 593
cf2da2 594     $this->session->kill();
40d246 595     $_SESSION = array('language' => $this->user->language, 'temp' => true, 'skin' => $this->config->get('skin'));
1854c4 596     $this->user->reset();
T 597   }
598
599
600   /**
601    * Do server side actions on logout
602    */
603   public function logout_actions()
604   {
1aceb9 605     $config  = $this->config->all();
A 606     $storage = $this->get_storage();
1854c4 607
T 608     if ($config['logout_purge'] && !empty($config['trash_mbox'])) {
1aceb9 609       $storage->clear_folder($config['trash_mbox']);
1854c4 610     }
T 611
612     if ($config['logout_expunge']) {
1aceb9 613       $storage->expunge_folder('INBOX');
1854c4 614     }
40a186 615
A 616     // Try to save unsaved user preferences
617     if (!empty($_SESSION['preferences'])) {
618       $this->user->save_prefs(unserialize($_SESSION['preferences']));
619     }
fec2d8 620   }
T 621
622
623   /**
57f0c8 624    * Generate a unique token to be used in a form request
T 625    *
626    * @return string The request token
627    */
549933 628   public function get_request_token()
57f0c8 629   {
ec045b 630     $sess_id = $_COOKIE[ini_get('session.name')];
c9f2c4 631     if (!$sess_id) $sess_id = session_id();
1aceb9 632
A 633     $plugin = $this->plugins->exec_hook('request_token', array(
634         'value' => md5('RT' . $this->get_user_id() . $this->config->get('des_key') . $sess_id)));
635
ef27a6 636     return $plugin['value'];
57f0c8 637   }
b62a0d 638
A 639
57f0c8 640   /**
T 641    * Check if the current request contains a valid token
642    *
549933 643    * @param int Request method
57f0c8 644    * @return boolean True if request token is valid false if not
T 645    */
1aceb9 646   public function check_request($mode = rcube_utils::INPUT_POST)
57f0c8 647   {
1aceb9 648     $token = rcube_utils::get_input_value('_token', $mode);
ec045b 649     $sess_id = $_COOKIE[ini_get('session.name')];
T 650     return !empty($sess_id) && $token == $this->get_request_token();
57f0c8 651   }
b62a0d 652
A 653
57f0c8 654   /**
1854c4 655    * Create unique authorization hash
T 656    *
657    * @param string Session ID
658    * @param int Timestamp
659    * @return string The generated auth hash
660    */
661   private function get_auth_hash($sess_id, $ts)
662   {
663     $auth_string = sprintf('rcmail*sess%sR%s*Chk:%s;%s',
664       $sess_id,
665       $ts,
666       $this->config->get('ip_check') ? $_SERVER['REMOTE_ADDR'] : '***.***.***.***',
667       $_SERVER['HTTP_USER_AGENT']);
668
669     if (function_exists('sha1'))
670       return sha1($auth_string);
671     else
672       return md5($auth_string);
673   }
674
564741 675
A 676   /**
e019f2 677    * Build a valid URL to this instance of Roundcube
c719f3 678    *
T 679    * @param mixed Either a string with the action or url parameters as key-value pairs
1aceb9 680    *
c719f3 681    * @return string Valid application URL
T 682    */
683   public function url($p)
684   {
685     if (!is_array($p))
fde466 686       $p = array('_action' => @func_get_arg(0));
b62a0d 687
1c932d 688     $task = $p['_task'] ? $p['_task'] : ($p['task'] ? $p['task'] : $this->task);
cc97ea 689     $p['_task'] = $task;
1038a6 690     unset($p['task']);
A 691
cf1777 692     $url = './';
T 693     $delm = '?';
77406b 694     foreach (array_reverse($p) as $key => $val) {
a7321e 695       if ($val !== '' && $val !== null) {
cc97ea 696         $par = $key[0] == '_' ? $key : '_'.$key;
cf1777 697         $url .= $delm.urlencode($par).'='.urlencode($val);
T 698         $delm = '&';
699       }
700     }
c719f3 701     return $url;
T 702   }
cefd1d 703
T 704
705   /**
0c2596 706    * Function to be executed in script shutdown
0501b6 707    */
0c2596 708   public function shutdown()
0501b6 709   {
0c2596 710     parent::shutdown();
0501b6 711
0c2596 712     foreach ($this->address_books as $book) {
A 713       if (is_object($book) && is_a($book, 'rcube_addressbook'))
714         $book->close();
0501b6 715     }
T 716
0c2596 717     // before closing the database connection, write session data
A 718     if ($_SERVER['REMOTE_ADDR'] && is_object($this->session)) {
719       session_write_close();
720     }
f53750 721
0c2596 722     // write performance stats to logs/console
A 723     if ($this->config->get('devel_mode')) {
724       if (function_exists('memory_get_usage'))
1aceb9 725         $mem = $this->show_bytes(memory_get_usage());
0c2596 726       if (function_exists('memory_get_peak_usage'))
1aceb9 727         $mem .= '/'.$this->show_bytes(memory_get_peak_usage());
0c2596 728
A 729       $log = $this->task . ($this->action ? '/'.$this->action : '') . ($mem ? " [$mem]" : '');
730       if (defined('RCMAIL_START'))
731         self::print_timer(RCMAIL_START, $log);
732       else
733         self::console($log);
734     }
cefd1d 735   }
68d2d5 736
A 737   /**
738    * Registers action aliases for current task
739    *
740    * @param array $map Alias-to-filename hash array
741    */
742   public function register_action_map($map)
743   {
744     if (is_array($map)) {
745       foreach ($map as $idx => $val) {
746         $this->action_map[$idx] = $val;
747       }
748     }
749   }
f53750 750
68d2d5 751   /**
A 752    * Returns current action filename
753    *
754    * @param array $map Alias-to-filename hash array
755    */
756   public function get_action_file()
757   {
758     if (!empty($this->action_map[$this->action])) {
759       return $this->action_map[$this->action];
760     }
761
762     return strtr($this->action, '-', '_') . '.inc';
763   }
764
d08333 765   /**
A 766    * Fixes some user preferences according to namespace handling change.
767    * Old Roundcube versions were using folder names with removed namespace prefix.
768    * Now we need to add the prefix on servers where personal namespace has prefix.
769    *
770    * @param rcube_user $user User object
771    */
772   private function fix_namespace_settings($user)
773   {
c321a9 774     $prefix     = $this->storage->get_namespace('prefix');
d08333 775     $prefix_len = strlen($prefix);
A 776
777     if (!$prefix_len)
778       return;
779
f53750 780     $prefs = $this->config->all();
A 781     if (!empty($prefs['namespace_fixed']))
d08333 782       return;
A 783
784     // Build namespace prefix regexp
c321a9 785     $ns     = $this->storage->get_namespace();
d08333 786     $regexp = array();
A 787
788     foreach ($ns as $entry) {
789       if (!empty($entry)) {
790         foreach ($entry as $item) {
791           if (strlen($item[0])) {
792             $regexp[] = preg_quote($item[0], '/');
793           }
794         }
795       }
796     }
797     $regexp = '/^('. implode('|', $regexp).')/';
798
799     // Fix preferences
800     $opts = array('drafts_mbox', 'junk_mbox', 'sent_mbox', 'trash_mbox', 'archive_mbox');
801     foreach ($opts as $opt) {
802       if ($value = $prefs[$opt]) {
803         if ($value != 'INBOX' && !preg_match($regexp, $value)) {
804           $prefs[$opt] = $prefix.$value;
805         }
806       }
807     }
808
c321a9 809     if (!empty($prefs['default_folders'])) {
T 810       foreach ($prefs['default_folders'] as $idx => $name) {
d08333 811         if ($name != 'INBOX' && !preg_match($regexp, $name)) {
c321a9 812           $prefs['default_folders'][$idx] = $prefix.$name;
d08333 813         }
A 814       }
815     }
816
817     if (!empty($prefs['search_mods'])) {
818       $folders = array();
819       foreach ($prefs['search_mods'] as $idx => $value) {
820         if ($idx != 'INBOX' && $idx != '*' && !preg_match($regexp, $idx)) {
821           $idx = $prefix.$idx;
822         }
823         $folders[$idx] = $value;
824       }
825       $prefs['search_mods'] = $folders;
826     }
827
828     if (!empty($prefs['message_threading'])) {
829       $folders = array();
830       foreach ($prefs['message_threading'] as $idx => $value) {
831         if ($idx != 'INBOX' && !preg_match($regexp, $idx)) {
832           $idx = $prefix.$idx;
833         }
834         $folders[$prefix.$idx] = $value;
835       }
836       $prefs['message_threading'] = $folders;
837     }
838
839     if (!empty($prefs['collapsed_folders'])) {
840       $folders     = explode('&&', $prefs['collapsed_folders']);
841       $count       = count($folders);
842       $folders_str = '';
843
844       if ($count) {
845           $folders[0]        = substr($folders[0], 1);
846           $folders[$count-1] = substr($folders[$count-1], 0, -1);
847       }
848
849       foreach ($folders as $value) {
850         if ($value != 'INBOX' && !preg_match($regexp, $value)) {
851           $value = $prefix.$value;
852         }
853         $folders_str .= '&'.$value.'&';
854       }
855       $prefs['collapsed_folders'] = $folders_str;
856     }
857
858     $prefs['namespace_fixed'] = true;
859
860     // save updated preferences and reset imap settings (default folders)
861     $user->save_prefs($prefs);
c321a9 862     $this->set_storage_prop();
d08333 863   }
A 864
0c2596 865
A 866     /**
867      * Overwrite action variable
868      *
869      * @param string New action value
870      */
871     public function overwrite_action($action)
872     {
873         $this->action = $action;
874         $this->output->set_env('action', $action);
875     }
876
877
878     /**
879      * Send the given message using the configured method.
880      *
881      * @param object $message    Reference to Mail_MIME object
882      * @param string $from       Sender address string
883      * @param array  $mailto     Array of recipient address strings
884      * @param array  $smtp_error SMTP error array (reference)
885      * @param string $body_file  Location of file with saved message body (reference),
886      *                           used when delay_file_io is enabled
887      * @param array  $smtp_opts  SMTP options (e.g. DSN request)
888      *
889      * @return boolean Send status.
890      */
891     public function deliver_message(&$message, $from, $mailto, &$smtp_error, &$body_file = null, $smtp_opts = null)
892     {
893         $headers = $message->headers();
894
895         // send thru SMTP server using custom SMTP library
896         if ($this->config->get('smtp_server')) {
897             // generate list of recipients
898             $a_recipients = array($mailto);
899
900             if (strlen($headers['Cc']))
901                 $a_recipients[] = $headers['Cc'];
902             if (strlen($headers['Bcc']))
903                 $a_recipients[] = $headers['Bcc'];
904
905             // clean Bcc from header for recipients
906             $send_headers = $headers;
907             unset($send_headers['Bcc']);
908             // here too, it because txtHeaders() below use $message->_headers not only $send_headers
909             unset($message->_headers['Bcc']);
910
911             $smtp_headers = $message->txtHeaders($send_headers, true);
912
913             if ($message->getParam('delay_file_io')) {
914                 // use common temp dir
915                 $temp_dir = $this->config->get('temp_dir');
916                 $body_file = tempnam($temp_dir, 'rcmMsg');
917                 if (PEAR::isError($mime_result = $message->saveMessageBody($body_file))) {
918                     self::raise_error(array('code' => 650, 'type' => 'php',
919                         'file' => __FILE__, 'line' => __LINE__,
920                         'message' => "Could not create message: ".$mime_result->getMessage()),
921                         TRUE, FALSE);
922                     return false;
923                 }
924                 $msg_body = fopen($body_file, 'r');
925             }
926             else {
927                 $msg_body = $message->get();
928             }
929
930             // send message
931             if (!is_object($this->smtp)) {
932                 $this->smtp_init(true);
933             }
934
935             $sent = $this->smtp->send_mail($from, $a_recipients, $smtp_headers, $msg_body, $smtp_opts);
936             $smtp_response = $this->smtp->get_response();
937             $smtp_error = $this->smtp->get_error();
938
939             // log error
940             if (!$sent) {
941                 self::raise_error(array('code' => 800, 'type' => 'smtp',
942                     'line' => __LINE__, 'file' => __FILE__,
943                     'message' => "SMTP error: ".join("\n", $smtp_response)), TRUE, FALSE);
944             }
945         }
946         // send mail using PHP's mail() function
947         else {
948             // unset some headers because they will be added by the mail() function
949             $headers_enc = $message->headers($headers);
950             $headers_php = $message->_headers;
951             unset($headers_php['To'], $headers_php['Subject']);
952
953             // reset stored headers and overwrite
954             $message->_headers = array();
955             $header_str = $message->txtHeaders($headers_php);
956
957             // #1485779
958             if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
959                 if (preg_match_all('/<([^@]+@[^>]+)>/', $headers_enc['To'], $m)) {
960                     $headers_enc['To'] = implode(', ', $m[1]);
961                 }
962             }
963
964             $msg_body = $message->get();
965
966             if (PEAR::isError($msg_body)) {
967                 self::raise_error(array('code' => 650, 'type' => 'php',
968                     'file' => __FILE__, 'line' => __LINE__,
969                     'message' => "Could not create message: ".$msg_body->getMessage()),
970                     TRUE, FALSE);
971             }
972             else {
973                 $delim   = $this->config->header_delimiter();
974                 $to      = $headers_enc['To'];
975                 $subject = $headers_enc['Subject'];
976                 $header_str = rtrim($header_str);
977
978                 if ($delim != "\r\n") {
979                     $header_str = str_replace("\r\n", $delim, $header_str);
980                     $msg_body   = str_replace("\r\n", $delim, $msg_body);
981                     $to         = str_replace("\r\n", $delim, $to);
982                     $subject    = str_replace("\r\n", $delim, $subject);
983                 }
984
985                 if (ini_get('safe_mode'))
986                     $sent = mail($to, $subject, $msg_body, $header_str);
987                 else
988                     $sent = mail($to, $subject, $msg_body, $header_str, "-f$from");
989             }
990         }
991
992         if ($sent) {
993             $this->plugins->exec_hook('message_sent', array('headers' => $headers, 'body' => $msg_body));
994
995             // remove MDN headers after sending
996             unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']);
997
998             // get all recipients
999             if ($headers['Cc'])
1000                 $mailto .= $headers['Cc'];
1001             if ($headers['Bcc'])
1002                 $mailto .= $headers['Bcc'];
1003             if (preg_match_all('/<([^@]+@[^>]+)>/', $mailto, $m))
1004                 $mailto = implode(', ', array_unique($m[1]));
1005
1006             if ($this->config->get('smtp_log')) {
1007                 self::write_log('sendmail', sprintf("User %s [%s]; Message for %s; %s",
1008                     $this->user->get_username(),
1009                     $_SERVER['REMOTE_ADDR'],
1010                     $mailto,
1011                     !empty($smtp_response) ? join('; ', $smtp_response) : ''));
1012             }
1013         }
1014
1015         if (is_resource($msg_body)) {
1016             fclose($msg_body);
1017         }
1018
1019         $message->_headers = array();
1020         $message->headers($headers);
1021
1022         return $sent;
1023     }
1024
1025
1026     /**
1027      * Unique Message-ID generator.
1028      *
1029      * @return string Message-ID
1030      */
1031     public function gen_message_id()
1032     {
1033         $local_part  = md5(uniqid('rcmail'.mt_rand(),true));
1034         $domain_part = $this->user->get_username('domain');
1035
1036         // Try to find FQDN, some spamfilters doesn't like 'localhost' (#1486924)
1037         if (!preg_match('/\.[a-z]+$/i', $domain_part)) {
1038             foreach (array($_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']) as $host) {
1039                 $host = preg_replace('/:[0-9]+$/', '', $host);
1040                 if ($host && preg_match('/\.[a-z]+$/i', $host)) {
1041                     $domain_part = $host;
1042                 }
1043             }
1044         }
1045
1046         return sprintf('<%s@%s>', $local_part, $domain_part);
1047     }
1048
1049
1050     /**
1051      * Returns RFC2822 formatted current date in user's timezone
1052      *
1053      * @return string Date
1054      */
1055     public function user_date()
1056     {
1057         // get user's timezone
1058         try {
1059             $tz   = new DateTimeZone($this->config->get('timezone'));
1060             $date = new DateTime('now', $tz);
1061         }
1062         catch (Exception $e) {
1063             $date = new DateTime();
1064         }
1065
1066         return $date->format('r');
1067     }
1068
1069
1070     /**
1071      * Write login data (name, ID, IP address) to the 'userlogins' log file.
1072      */
1073     public function log_login()
1074     {
1075         if (!$this->config->get('log_logins')) {
1076             return;
1077         }
1078
1079         $user_name = $this->get_user_name();
1080         $user_id   = $this->get_user_id();
1081
1082         if (!$user_id) {
1083             return;
1084         }
1085
1086         self::write_log('userlogins',
1087             sprintf('Successful login for %s (ID: %d) from %s in session %s',
1aceb9 1088                 $user_name, $user_id, rcube_utils::remote_ip(), session_id()));
0c2596 1089     }
A 1090
1aceb9 1091
A 1092     /**
1093      * Create a HTML table based on the given data
1094      *
1095      * @param  array  Named table attributes
1096      * @param  mixed  Table row data. Either a two-dimensional array or a valid SQL result set
1097      * @param  array  List of cols to show
1098      * @param  string Name of the identifier col
1099      *
1100      * @return string HTML table code
1101      */
1102     public function table_output($attrib, $table_data, $a_show_cols, $id_col)
1103     {
1104         $table = new html_table(/*array('cols' => count($a_show_cols))*/);
1105
1106         // add table header
1107         if (!$attrib['noheader']) {
1108             foreach ($a_show_cols as $col) {
1109                 $table->add_header($col, $this->Q($this->gettext($col)));
1110             }
1111         }
1112
1113         if (!is_array($table_data)) {
1114             $db = $this->get_dbh();
1115             while ($table_data && ($sql_arr = $db->fetch_assoc($table_data))) {
1116                 $table->add_row(array('id' => 'rcmrow' . rcube_utils::html_identifier($sql_arr[$id_col])));
1117
1118                 // format each col
1119                 foreach ($a_show_cols as $col) {
1120                     $table->add($col, $this->Q($sql_arr[$col]));
1121                 }
1122             }
1123         }
1124         else {
1125             foreach ($table_data as $row_data) {
1126                 $class = !empty($row_data['class']) ? $row_data['class'] : '';
1127                 $rowid = 'rcmrow' . rcube_utils::html_identifier($row_data[$id_col]);
1128
1129                 $table->add_row(array('id' => $rowid, 'class' => $class));
1130
1131                 // format each col
1132                 foreach ($a_show_cols as $col) {
1133                     $table->add($col, $this->Q(is_array($row_data[$col]) ? $row_data[$col][0] : $row_data[$col]));
1134                 }
1135             }
1136         }
1137
1138         return $table->show($attrib);
1139     }
1140
1141
1142     /**
1143      * Convert the given date to a human readable form
1144      * This uses the date formatting properties from config
1145      *
1146      * @param mixed  Date representation (string, timestamp or DateTime object)
1147      * @param string Date format to use
1148      * @param bool   Enables date convertion according to user timezone
1149      *
1150      * @return string Formatted date string
1151      */
1152     public function format_date($date, $format = null, $convert = true)
1153     {
1154         if (is_object($date) && is_a($date, 'DateTime')) {
1155             $timestamp = $date->format('U');
1156         }
1157         else {
1158             if (!empty($date)) {
1159                 $timestamp = rcube_strtotime($date);
1160             }
1161
1162             if (empty($timestamp)) {
1163                 return '';
1164             }
1165
1166             try {
1167                 $date = new DateTime("@".$timestamp);
1168             }
1169             catch (Exception $e) {
1170                 return '';
1171             }
1172         }
1173
1174         if ($convert) {
1175             try {
1176                 // convert to the right timezone
1177                 $stz = date_default_timezone_get();
1178                 $tz = new DateTimeZone($this->config->get('timezone'));
1179                 $date->setTimezone($tz);
1180                 date_default_timezone_set($tz->getName());
1181
1182                 $timestamp = $date->format('U');
1183             }
1184             catch (Exception $e) {
1185             }
1186         }
1187
1188         // define date format depending on current time
1189         if (!$format) {
1190             $now         = time();
1191             $now_date    = getdate($now);
1192             $today_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday'], $now_date['year']);
1193             $week_limit  = mktime(0, 0, 0, $now_date['mon'], $now_date['mday']-6, $now_date['year']);
1194             $pretty_date = $this->config->get('prettydate');
1195
1196             if ($pretty_date && $timestamp > $today_limit && $timestamp < $now) {
1197                 $format = $this->config->get('date_today', $this->config->get('time_format', 'H:i'));
1198                 $today  = true;
1199             }
1200             else if ($pretty_date && $timestamp > $week_limit && $timestamp < $now) {
1201                 $format = $this->config->get('date_short', 'D H:i');
1202             }
1203             else {
1204                 $format = $this->config->get('date_long', 'Y-m-d H:i');
1205             }
1206         }
1207
1208         // strftime() format
1209         if (preg_match('/%[a-z]+/i', $format)) {
1210             $format = strftime($format, $timestamp);
1211             if ($stz) {
1212                 date_default_timezone_set($stz);
1213             }
1214             return $today ? ($this->gettext('today') . ' ' . $format) : $format;
1215         }
1216
1217         // parse format string manually in order to provide localized weekday and month names
1218         // an alternative would be to convert the date() format string to fit with strftime()
1219         $out = '';
1220         for ($i=0; $i<strlen($format); $i++) {
1221             if ($format[$i] == "\\") {  // skip escape chars
1222                 continue;
1223             }
1224
1225             // write char "as-is"
1226             if ($format[$i] == ' ' || $format[$i-1] == "\\") {
1227                 $out .= $format[$i];
1228             }
1229             // weekday (short)
1230             else if ($format[$i] == 'D') {
1231                 $out .= $this->gettext(strtolower(date('D', $timestamp)));
1232             }
1233             // weekday long
1234             else if ($format[$i] == 'l') {
1235                 $out .= $this->gettext(strtolower(date('l', $timestamp)));
1236             }
1237             // month name (short)
1238             else if ($format[$i] == 'M') {
1239                 $out .= $this->gettext(strtolower(date('M', $timestamp)));
1240             }
1241             // month name (long)
1242             else if ($format[$i] == 'F') {
1243                 $out .= $this->gettext('long'.strtolower(date('M', $timestamp)));
1244             }
1245             else if ($format[$i] == 'x') {
1246                 $out .= strftime('%x %X', $timestamp);
1247             }
1248             else {
1249                 $out .= date($format[$i], $timestamp);
1250             }
1251         }
1252
1253         if ($today) {
1254             $label = $this->gettext('today');
1255             // replcae $ character with "Today" label (#1486120)
1256             if (strpos($out, '$') !== false) {
1257                 $out = preg_replace('/\$/', $label, $out, 1);
1258             }
1259             else {
1260                 $out = $label . ' ' . $out;
1261             }
1262         }
1263
1264         if ($stz) {
1265             date_default_timezone_set($stz);
1266         }
1267
1268         return $out;
1269     }
1270
1271
1272     /**
1273      * Return folders list in HTML
1274      *
1275      * @param array $attrib Named parameters
1276      *
1277      * @return string HTML code for the gui object
1278      */
1279     public function folder_list($attrib)
1280     {
1281         static $a_mailboxes;
1282
1283         $attrib += array('maxlength' => 100, 'realnames' => false, 'unreadwrap' => ' (%s)');
1284
1285         $rcmail  = rcmail::get_instance();
1286         $storage = $rcmail->get_storage();
1287
1288         // add some labels to client
1289         $rcmail->output->add_label('purgefolderconfirm', 'deletemessagesconfirm');
1290
1291         $type = $attrib['type'] ? $attrib['type'] : 'ul';
1292         unset($attrib['type']);
1293
1294         if ($type == 'ul' && !$attrib['id']) {
1295             $attrib['id'] = 'rcmboxlist';
1296         }
1297
1298         if (empty($attrib['folder_name'])) {
1299             $attrib['folder_name'] = '*';
1300         }
1301
1302         // get current folder
1303         $mbox_name = $storage->get_folder();
1304
1305         // build the folders tree
1306         if (empty($a_mailboxes)) {
1307             // get mailbox list
1308             $a_folders = $storage->list_folders_subscribed(
1309                 '', $attrib['folder_name'], $attrib['folder_filter']);
1310             $delimiter = $storage->get_hierarchy_delimiter();
1311             $a_mailboxes = array();
1312
1313             foreach ($a_folders as $folder) {
1314                 $rcmail->build_folder_tree($a_mailboxes, $folder, $delimiter);
1315             }
1316         }
1317
1318         // allow plugins to alter the folder tree or to localize folder names
1319         $hook = $rcmail->plugins->exec_hook('render_mailboxlist', array(
1320             'list'      => $a_mailboxes,
1321             'delimiter' => $delimiter,
1322             'type'      => $type,
1323             'attribs'   => $attrib,
1324         ));
1325
1326         $a_mailboxes = $hook['list'];
1327         $attrib      = $hook['attribs'];
1328
1329         if ($type == 'select') {
0a1dd5 1330             $attrib['is_escaped'] = true;
1aceb9 1331             $select = new html_select($attrib);
A 1332
1333             // add no-selection option
1334             if ($attrib['noselection']) {
0a1dd5 1335                 $select->add(html::quote($rcmail->gettext($attrib['noselection'])), '');
1aceb9 1336             }
A 1337
1338             $rcmail->render_folder_tree_select($a_mailboxes, $mbox_name, $attrib['maxlength'], $select, $attrib['realnames']);
1339             $out = $select->show($attrib['default']);
1340         }
1341         else {
1342             $js_mailboxlist = array();
1343             $out = html::tag('ul', $attrib, $rcmail->render_folder_tree_html($a_mailboxes, $mbox_name, $js_mailboxlist, $attrib), html::$common_attrib);
1344
1345             $rcmail->output->add_gui_object('mailboxlist', $attrib['id']);
1346             $rcmail->output->set_env('mailboxes', $js_mailboxlist);
1347             $rcmail->output->set_env('unreadwrap', $attrib['unreadwrap']);
1348             $rcmail->output->set_env('collapsed_folders', (string)$rcmail->config->get('collapsed_folders'));
1349         }
1350
1351         return $out;
1352     }
1353
1354
1355     /**
1356      * Return folders list as html_select object
1357      *
1358      * @param array $p  Named parameters
1359      *
1360      * @return html_select HTML drop-down object
1361      */
1362     public function folder_selector($p = array())
1363     {
0a1dd5 1364         $p += array('maxlength' => 100, 'realnames' => false, 'is_escaped' => true);
1aceb9 1365         $a_mailboxes = array();
A 1366         $storage = $this->get_storage();
1367
1368         if (empty($p['folder_name'])) {
1369             $p['folder_name'] = '*';
1370         }
1371
1372         if ($p['unsubscribed']) {
1373             $list = $storage->list_folders('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
1374         }
1375         else {
1376             $list = $storage->list_folders_subscribed('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
1377         }
1378
1379         $delimiter = $storage->get_hierarchy_delimiter();
1380
1381         foreach ($list as $folder) {
1382             if (empty($p['exceptions']) || !in_array($folder, $p['exceptions'])) {
1383                 $this->build_folder_tree($a_mailboxes, $folder, $delimiter);
1384             }
1385         }
1386
1387         $select = new html_select($p);
1388
1389         if ($p['noselection']) {
0a1dd5 1390             $select->add(html::quote($p['noselection']), '');
1aceb9 1391         }
A 1392
1393         $this->render_folder_tree_select($a_mailboxes, $mbox, $p['maxlength'], $select, $p['realnames'], 0, $p);
1394
1395         return $select;
1396     }
1397
1398
1399     /**
1400      * Create a hierarchical array of the mailbox list
1401      */
1402     public function build_folder_tree(&$arrFolders, $folder, $delm = '/', $path = '')
1403     {
1404         // Handle namespace prefix
1405         $prefix = '';
1406         if (!$path) {
1407             $n_folder = $folder;
1408             $folder = $this->storage->mod_folder($folder);
1409
1410             if ($n_folder != $folder) {
1411                 $prefix = substr($n_folder, 0, -strlen($folder));
1412             }
1413         }
1414
1415         $pos = strpos($folder, $delm);
1416
1417         if ($pos !== false) {
1418             $subFolders    = substr($folder, $pos+1);
1419             $currentFolder = substr($folder, 0, $pos);
1420
1421             // sometimes folder has a delimiter as the last character
1422             if (!strlen($subFolders)) {
1423                 $virtual = false;
1424             }
1425             else if (!isset($arrFolders[$currentFolder])) {
1426                 $virtual = true;
1427             }
1428             else {
1429                 $virtual = $arrFolders[$currentFolder]['virtual'];
1430             }
1431         }
1432         else {
1433             $subFolders    = false;
1434             $currentFolder = $folder;
1435             $virtual       = false;
1436         }
1437
1438         $path .= $prefix . $currentFolder;
1439
1440         if (!isset($arrFolders[$currentFolder])) {
1441             $arrFolders[$currentFolder] = array(
1442                 'id' => $path,
1443                 'name' => rcube_charset::convert($currentFolder, 'UTF7-IMAP'),
1444                 'virtual' => $virtual,
1445                 'folders' => array());
1446         }
1447         else {
1448             $arrFolders[$currentFolder]['virtual'] = $virtual;
1449         }
1450
1451         if (strlen($subFolders)) {
1452             $this->build_folder_tree($arrFolders[$currentFolder]['folders'], $subFolders, $delm, $path.$delm);
1453         }
1454     }
1455
1456
1457     /**
1458      * Return html for a structured list &lt;ul&gt; for the mailbox tree
1459      */
1460     public function render_folder_tree_html(&$arrFolders, &$mbox_name, &$jslist, $attrib, $nestLevel = 0)
1461     {
1462         $maxlength = intval($attrib['maxlength']);
1463         $realnames = (bool)$attrib['realnames'];
1464         $msgcounts = $this->storage->get_cache('messagecount');
1465         $collapsed = $this->config->get('collapsed_folders');
1466
1467         $out = '';
1468         foreach ($arrFolders as $key => $folder) {
1469             $title        = null;
1470             $folder_class = $this->folder_classname($folder['id']);
1471             $is_collapsed = strpos($collapsed, '&'.rawurlencode($folder['id']).'&') !== false;
1472             $unread       = $msgcounts ? intval($msgcounts[$folder['id']]['UNSEEN']) : 0;
1473
1474             if ($folder_class && !$realnames) {
1475                 $foldername = $this->gettext($folder_class);
1476             }
1477             else {
1478                 $foldername = $folder['name'];
1479
1480                 // shorten the folder name to a given length
1481                 if ($maxlength && $maxlength > 1) {
1482                     $fname = abbreviate_string($foldername, $maxlength);
1483                     if ($fname != $foldername) {
1484                         $title = $foldername;
1485                     }
1486                     $foldername = $fname;
1487                 }
1488             }
1489
1490             // make folder name safe for ids and class names
1491             $folder_id = rcube_utils::html_identifier($folder['id'], true);
1492             $classes   = array('mailbox');
1493
1494             // set special class for Sent, Drafts, Trash and Junk
1495             if ($folder_class) {
1496                 $classes[] = $folder_class;
1497             }
1498
1499             if ($folder['id'] == $mbox_name) {
1500                 $classes[] = 'selected';
1501             }
1502
1503             if ($folder['virtual']) {
1504                 $classes[] = 'virtual';
1505             }
1506             else if ($unread) {
1507                 $classes[] = 'unread';
1508             }
1509
1510             $js_name = $this->JQ($folder['id']);
1511             $html_name = $this->Q($foldername) . ($unread ? html::span('unreadcount', sprintf($attrib['unreadwrap'], $unread)) : '');
1512             $link_attrib = $folder['virtual'] ? array() : array(
1513                 'href' => $this->url(array('_mbox' => $folder['id'])),
1514                 'onclick' => sprintf("return %s.command('list','%s',this)", rcmail::JS_OBJECT_NAME, $js_name),
1515                 'rel' => $folder['id'],
1516                 'title' => $title,
1517             );
1518
1519             $out .= html::tag('li', array(
1520                 'id' => "rcmli".$folder_id,
1521                 'class' => join(' ', $classes),
1522                 'noclose' => true),
1523                 html::a($link_attrib, $html_name) .
1524                 (!empty($folder['folders']) ? html::div(array(
1525                     'class' => ($is_collapsed ? 'collapsed' : 'expanded'),
1526                     'style' => "position:absolute",
1527                     'onclick' => sprintf("%s.command('collapse-folder', '%s')", rcmail::JS_OBJECT_NAME, $js_name)
1528                 ), '&nbsp;') : ''));
1529
1530             $jslist[$folder_id] = array(
1531                 'id'      => $folder['id'],
1532                 'name'    => $foldername,
1533                 'virtual' => $folder['virtual']
1534             );
1535
1536             if (!empty($folder['folders'])) {
1537                 $out .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
1538                     $this->render_folder_tree_html($folder['folders'], $mbox_name, $jslist, $attrib, $nestLevel+1));
1539             }
1540
1541             $out .= "</li>\n";
1542         }
1543
1544         return $out;
1545     }
1546
1547
1548     /**
1549      * Return html for a flat list <select> for the mailbox tree
1550      */
1551     public function render_folder_tree_select(&$arrFolders, &$mbox_name, $maxlength, &$select, $realnames = false, $nestLevel = 0, $opts = array())
1552     {
1553         $out = '';
1554
1555         foreach ($arrFolders as $key => $folder) {
1556             // skip exceptions (and its subfolders)
1557             if (!empty($opts['exceptions']) && in_array($folder['id'], $opts['exceptions'])) {
1558                 continue;
1559             }
1560
1561             // skip folders in which it isn't possible to create subfolders
1562             if (!empty($opts['skip_noinferiors'])) {
1563                 $attrs = $this->storage->folder_attributes($folder['id']);
1564                 if ($attrs && in_array('\\Noinferiors', $attrs)) {
1565                     continue;
1566                 }
1567             }
1568
1569             if (!$realnames && ($folder_class = $this->folder_classname($folder['id']))) {
1570                 $foldername = $this->gettext($folder_class);
1571             }
1572             else {
1573                 $foldername = $folder['name'];
1574
1575                 // shorten the folder name to a given length
1576                 if ($maxlength && $maxlength > 1) {
1577                     $foldername = abbreviate_string($foldername, $maxlength);
1578                 }
e7ca04 1579             }
1aceb9 1580
0a1dd5 1581             $select->add(str_repeat('&nbsp;', $nestLevel*4) . html::quote($foldername), $folder['id']);
1aceb9 1582
e7ca04 1583             if (!empty($folder['folders'])) {
A 1584                 $out .= $this->render_folder_tree_select($folder['folders'], $mbox_name, $maxlength,
1585                     $select, $realnames, $nestLevel+1, $opts);
1aceb9 1586             }
A 1587         }
1588
1589         return $out;
1590     }
1591
1592
1593     /**
1594      * Return internal name for the given folder if it matches the configured special folders
1595      */
1596     public function folder_classname($folder_id)
1597     {
1598         if ($folder_id == 'INBOX') {
1599             return 'inbox';
1600         }
1601
1602         // for these mailboxes we have localized labels and css classes
1603         foreach (array('sent', 'drafts', 'trash', 'junk') as $smbx)
1604         {
1605             if ($folder_id === $this->config->get($smbx.'_mbox')) {
1606                 return $smbx;
1607             }
1608         }
1609     }
1610
1611
1612     /**
1613      * Try to localize the given IMAP folder name.
1614      * UTF-7 decode it in case no localized text was found
1615      *
1616      * @param string $name  Folder name
1617      *
1618      * @return string Localized folder name in UTF-8 encoding
1619      */
1620     public function localize_foldername($name)
1621     {
1622         if ($folder_class = $this->folder_classname($name)) {
1623             return $this->gettext($folder_class);
1624         }
1625         else {
1626             return rcube_charset::convert($name, 'UTF7-IMAP');
1627         }
1628     }
1629
1630
1631     public function localize_folderpath($path)
1632     {
1633         $protect_folders = $this->config->get('protect_default_folders');
1634         $default_folders = (array) $this->config->get('default_folders');
1635         $delimiter       = $this->storage->get_hierarchy_delimiter();
1636         $path            = explode($delimiter, $path);
1637         $result          = array();
1638
1639         foreach ($path as $idx => $dir) {
1640             $directory = implode($delimiter, array_slice($path, 0, $idx+1));
1641             if ($protect_folders && in_array($directory, $default_folders)) {
1642                 unset($result);
1643                 $result[] = $this->localize_foldername($directory);
1644             }
1645             else {
1646                 $result[] = rcube_charset::convert($dir, 'UTF7-IMAP');
1647             }
1648         }
1649
1650         return implode($delimiter, $result);
1651     }
1652
1653
1654     public static function quota_display($attrib)
1655     {
1656         $rcmail = rcmail::get_instance();
1657
1658         if (!$attrib['id']) {
1659             $attrib['id'] = 'rcmquotadisplay';
1660         }
1661
1662         $_SESSION['quota_display'] = !empty($attrib['display']) ? $attrib['display'] : 'text';
1663
1664         $rcmail->output->add_gui_object('quotadisplay', $attrib['id']);
1665
1666         $quota = $rcmail->quota_content($attrib);
1667
1668         $rcmail->output->add_script('rcmail.set_quota('.rcube_output::json_serialize($quota).');', 'docready');
1669
1670         return html::span($attrib, '');
1671     }
1672
1673
1674     public function quota_content($attrib = null)
1675     {
1676         $quota = $this->storage->get_quota();
1677         $quota = $this->plugins->exec_hook('quota', $quota);
1678
1679         $quota_result = (array) $quota;
1680         $quota_result['type'] = isset($_SESSION['quota_display']) ? $_SESSION['quota_display'] : '';
1681
1682         if (!$quota['total'] && $this->config->get('quota_zero_as_unlimited')) {
1683             $quota_result['title']   = $this->gettext('unlimited');
1684             $quota_result['percent'] = 0;
1685         }
1686         else if ($quota['total']) {
1687             if (!isset($quota['percent'])) {
1688                 $quota_result['percent'] = min(100, round(($quota['used']/max(1,$quota['total']))*100));
1689             }
1690
1691             $title = sprintf('%s / %s (%.0f%%)',
1692                 $this->show_bytes($quota['used'] * 1024), $this->show_bytes($quota['total'] * 1024),
1693                 $quota_result['percent']);
1694
1695             $quota_result['title'] = $title;
1696
1697             if ($attrib['width']) {
1698                 $quota_result['width'] = $attrib['width'];
1699             }
1700             if ($attrib['height']) {
1701                 $quota_result['height']    = $attrib['height'];
1702             }
1703         }
1704         else {
1705             $quota_result['title']   = $this->gettext('unknown');
1706             $quota_result['percent'] = 0;
1707         }
1708
1709         return $quota_result;
1710     }
1711
1712
1713     /**
1714      * Outputs error message according to server error/response codes
1715      *
1716      * @param string $fallback       Fallback message label
1717      * @param array  $fallback_args  Fallback message label arguments
1718      */
1719     public function display_server_error($fallback = null, $fallback_args = null)
1720     {
1721         $err_code = $this->storage->get_error_code();
1722         $res_code = $this->storage->get_response_code();
1723
1724         if ($err_code < 0) {
1725             $this->output->show_message('storageerror', 'error');
1726         }
1727         else if ($res_code == rcube_storage::NOPERM) {
1728             $this->output->show_message('errornoperm', 'error');
1729         }
1730         else if ($res_code == rcube_storage::READONLY) {
1731             $this->output->show_message('errorreadonly', 'error');
1732         }
1733         else if ($err_code && ($err_str = $this->storage->get_error_str())) {
1734             // try to detect access rights problem and display appropriate message
1735             if (stripos($err_str, 'Permission denied') !== false) {
1736                 $this->output->show_message('errornoperm', 'error');
1737             }
1738             else {
1739                 $this->output->show_message('servererrormsg', 'error', array('msg' => $err_str));
1740             }
1741         }
1742         else if ($fallback) {
1743             $this->output->show_message($fallback, 'error', $fallback_args);
1744         }
1745     }
1746
1747
1748     /**
1749      * Output HTML editor scripts
1750      *
1751      * @param string $mode  Editor mode
1752      */
1753     public function html_editor($mode = '')
1754     {
1755         $hook = $this->plugins->exec_hook('html_editor', array('mode' => $mode));
1756
1757         if ($hook['abort']) {
1758             return;
1759         }
1760
1761         $lang = strtolower($_SESSION['language']);
1762
1763         // TinyMCE uses two-letter lang codes, with exception of Chinese
1764         if (strpos($lang, 'zh_') === 0) {
1765             $lang = str_replace('_', '-', $lang);
1766         }
1767         else {
1768             $lang = substr($lang, 0, 2);
1769         }
1770
1771         if (!file_exists(INSTALL_PATH . 'program/js/tiny_mce/langs/'.$lang.'.js')) {
1772             $lang = 'en';
1773         }
1774
1775         $script = json_encode(array(
1776             'mode'       => $mode,
1777             'lang'       => $lang,
1778             'skin_path'  => $this->output->get_skin_path(),
1779             'spellcheck' => intval($this->config->get('enable_spellcheck')),
1780             'spelldict'  => intval($this->config->get('spellcheck_dictionary'))
1781         ));
1782
1783         $this->output->include_script('tiny_mce/tiny_mce.js');
1784         $this->output->include_script('editor.js');
1785         $this->output->add_script("rcmail_editor_init($script)", 'docready');
1786     }
1787
1788
1789     /**
1790      * Replaces TinyMCE's emoticon images with plain-text representation
1791      *
1792      * @param string $html  HTML content
1793      *
1794      * @return string HTML content
1795      */
1796     public static function replace_emoticons($html)
1797     {
1798         $emoticons = array(
1799             '8-)' => 'smiley-cool',
1800             ':-#' => 'smiley-foot-in-mouth',
1801             ':-*' => 'smiley-kiss',
1802             ':-X' => 'smiley-sealed',
1803             ':-P' => 'smiley-tongue-out',
1804             ':-@' => 'smiley-yell',
1805             ":'(" => 'smiley-cry',
1806             ':-(' => 'smiley-frown',
1807             ':-D' => 'smiley-laughing',
1808             ':-)' => 'smiley-smile',
1809             ':-S' => 'smiley-undecided',
1810             ':-$' => 'smiley-embarassed',
1811             'O:-)' => 'smiley-innocent',
1812             ':-|' => 'smiley-money-mouth',
1813             ':-O' => 'smiley-surprised',
1814             ';-)' => 'smiley-wink',
1815         );
1816
1817         foreach ($emoticons as $idx => $file) {
1818             // <img title="Cry" src="http://.../program/js/tiny_mce/plugins/emotions/img/smiley-cry.gif" border="0" alt="Cry" />
1819             $search[]  = '/<img title="[a-z ]+" src="https?:\/\/[a-z0-9_.\/-]+\/tiny_mce\/plugins\/emotions\/img\/'.$file.'.gif"[^>]+\/>/i';
1820             $replace[] = $idx;
1821         }
1822
1823         return preg_replace($search, $replace, $html);
1824     }
1825
1826
1827     /**
1828      * File upload progress handler.
1829      */
1830     public function upload_progress()
1831     {
1832         $prefix = ini_get('apc.rfc1867_prefix');
1833         $params = array(
1834             'action' => $this->action,
1835             'name' => rcube_utils::get_input_value('_progress', rcube_utils::INPUT_GET),
1836         );
1837
1838         if (function_exists('apc_fetch')) {
1839             $status = apc_fetch($prefix . $params['name']);
1840
1841             if (!empty($status)) {
1842                 $status['percent'] = round($status['current']/$status['total']*100);
1843                 $params = array_merge($status, $params);
1844             }
1845         }
1846
1847         if (isset($params['percent']))
1848             $params['text'] = $this->gettext(array('name' => 'uploadprogress', 'vars' => array(
1849                 'percent' => $params['percent'] . '%',
1850                 'current' => $this->show_bytes($params['current']),
1851                 'total'   => $this->show_bytes($params['total'])
1852         )));
1853
1854         $this->output->command('upload_progress_update', $params);
1855         $this->output->send();
1856     }
1857
1858
1859     /**
1860      * Initializes file uploading interface.
1861      */
1862     public function upload_init()
1863     {
1864         // Enable upload progress bar
1865         if (($seconds = $this->config->get('upload_progress')) && ini_get('apc.rfc1867')) {
1866             if ($field_name = ini_get('apc.rfc1867_name')) {
1867                 $this->output->set_env('upload_progress_name', $field_name);
1868                 $this->output->set_env('upload_progress_time', (int) $seconds);
1869             }
1870         }
1871
1872         // find max filesize value
1873         $max_filesize = parse_bytes(ini_get('upload_max_filesize'));
1874         $max_postsize = parse_bytes(ini_get('post_max_size'));
1875         if ($max_postsize && $max_postsize < $max_filesize) {
1876             $max_filesize = $max_postsize;
1877         }
1878
1879         $this->output->set_env('max_filesize', $max_filesize);
1880         $max_filesize = self::show_bytes($max_filesize);
1881         $this->output->set_env('filesizeerror', $this->gettext(array(
1882             'name' => 'filesizeerror', 'vars' => array('size' => $max_filesize))));
1883
1884         return $max_filesize;
1885     }
1886
1887
1888     /**
1889      * Initializes client-side autocompletion.
1890      */
1891     public function autocomplete_init()
1892     {
1893         static $init;
1894
1895         if ($init) {
1896             return;
1897         }
1898
1899         $init = 1;
1900
1901         if (($threads = (int)$this->config->get('autocomplete_threads')) > 0) {
1902             $book_types = (array) $this->config->get('autocomplete_addressbooks', 'sql');
1903             if (count($book_types) > 1) {
1904                 $this->output->set_env('autocomplete_threads', $threads);
1905                 $this->output->set_env('autocomplete_sources', $book_types);
1906             }
1907         }
1908
1909         $this->output->set_env('autocomplete_max', (int)$this->config->get('autocomplete_max', 15));
1910         $this->output->set_env('autocomplete_min_length', $this->config->get('autocomplete_min_length'));
1911         $this->output->add_label('autocompletechars', 'autocompletemore');
1912     }
1913
1914
1915     /**
1916      * Returns supported font-family specifications
1917      *
1918      * @param string $font  Font name
1919      *
1920      * @param string|array Font-family specification array or string (if $font is used)
1921      */
1922     public static function font_defs($font = null)
1923     {
1924         $fonts = array(
1925             'Andale Mono'   => '"Andale Mono",Times,monospace',
1926             'Arial'         => 'Arial,Helvetica,sans-serif',
1927             'Arial Black'   => '"Arial Black","Avant Garde",sans-serif',
1928             'Book Antiqua'  => '"Book Antiqua",Palatino,serif',
1929             'Courier New'   => '"Courier New",Courier,monospace',
1930             'Georgia'       => 'Georgia,Palatino,serif',
1931             'Helvetica'     => 'Helvetica,Arial,sans-serif',
1932             'Impact'        => 'Impact,Chicago,sans-serif',
1933             'Tahoma'        => 'Tahoma,Arial,Helvetica,sans-serif',
1934             'Terminal'      => 'Terminal,Monaco,monospace',
1935             'Times New Roman' => '"Times New Roman",Times,serif',
1936             'Trebuchet MS'  => '"Trebuchet MS",Geneva,sans-serif',
1937             'Verdana'       => 'Verdana,Geneva,sans-serif',
1938         );
1939
1940         if ($font) {
1941             return $fonts[$font];
1942         }
1943
1944         return $fonts;
1945     }
1946
1947
1948     /**
1949      * Create a human readable string for a number of bytes
1950      *
1951      * @param int Number of bytes
1952      *
1953      * @return string Byte string
1954      */
1955     public function show_bytes($bytes)
1956     {
1957         if ($bytes >= 1073741824) {
1958             $gb  = $bytes/1073741824;
1959             $str = sprintf($gb>=10 ? "%d " : "%.1f ", $gb) . $this->gettext('GB');
1960         }
1961         else if ($bytes >= 1048576) {
1962             $mb  = $bytes/1048576;
1963             $str = sprintf($mb>=10 ? "%d " : "%.1f ", $mb) . $this->gettext('MB');
1964         }
1965         else if ($bytes >= 1024) {
1966             $str = sprintf("%d ",  round($bytes/1024)) . $this->gettext('KB');
1967         }
1968         else {
1969             $str = sprintf('%d ', $bytes) . $this->gettext('B');
1970         }
1971
1972         return $str;
1973     }
1974
1975
1976     /**
1977      * Quote a given string.
1978      * Shortcut function for rcube_utils::rep_specialchars_output()
1979      *
1980      * @return string HTML-quoted string
1981      */
1982     public static function Q($str, $mode = 'strict', $newlines = true)
1983     {
1984         return rcube_utils::rep_specialchars_output($str, 'html', $mode, $newlines);
1985     }
1986
1987
1988     /**
1989      * Quote a given string for javascript output.
1990      * Shortcut function for rcube_utils::rep_specialchars_output()
1991      *
1992      * @return string JS-quoted string
1993      */
1994     public static function JQ($str)
1995     {
1996         return rcube_utils::rep_specialchars_output($str, 'js');
1997     }
1998
1999
2000     /************************************************************************
2001      *********          Deprecated methods (to be removed)          *********
2002      ***********************************************************************/
2003
2004     public static function setcookie($name, $value, $exp = 0)
2005     {
2006         rcube_utils::setcookie($name, $value, $exp);
2007     }
38a08c 2008
AM 2009     public function imap_connect()
2010     {
2011         return $this->storage_connect();
2012     }
5a575b 2013
b97f21 2014     public function imap_init()
TB 2015     {
2016         return $this->storage_init();
2017     }
2018
5a575b 2019     /**
AM 2020      * Connect to the mail storage server with stored session data
2021      *
2022      * @return bool True on success, False on error
2023      */
2024     public function storage_connect()
2025     {
2026         $storage = $this->get_storage();
2027
2028         if ($_SESSION['storage_host'] && !$storage->is_connected()) {
2029             $host = $_SESSION['storage_host'];
2030             $user = $_SESSION['username'];
2031             $port = $_SESSION['storage_port'];
2032             $ssl  = $_SESSION['storage_ssl'];
2033             $pass = $this->decrypt($_SESSION['password']);
2034
2035             if (!$storage->connect($host, $user, $pass, $port, $ssl)) {
2036                 if (is_object($this->output)) {
2037                     $error = $storage->get_error_code() == -1 ? 'storageerror' : 'sessionerror';
2038                     $this->output->show_message($error, 'error');
2039                 }
2040             }
2041             else {
2042                 $this->set_storage_prop();
2043                 return $storage->is_connected();
2044             }
2045         }
2046
2047         return false;
2048     }
197601 2049 }