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