]> git.donarmstrong.com Git - roundcube.git/blob - program/include/rcmail.php
Fix symlink mess
[roundcube.git] / program / include / rcmail.php
1 <?php
2
3 /*
4  +-----------------------------------------------------------------------+
5  | program/include/rcmail.php                                            |
6  |                                                                       |
7  | This file is part of the Roundcube Webmail client                     |
8  | Copyright (C) 2008-2011, The Roundcube Dev Team                       |
9  | Copyright (C) 2011, Kolab Systems AG                                  |
10  | Licensed under the GNU GPL                                            |
11  |                                                                       |
12  | PURPOSE:                                                              |
13  |   Application class providing core functions and holding              |
14  |   instances of all 'global' objects like db- and imap-connections     |
15  +-----------------------------------------------------------------------+
16  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
17  +-----------------------------------------------------------------------+
18
19  $Id: rcmail.php 5897 2012-02-21 20:46:15Z thomasb $
20
21 */
22
23
24 /**
25  * Application class of Roundcube Webmail
26  * implemented as singleton
27  *
28  * @package Core
29  */
30 class rcmail
31 {
32   /**
33    * Main tasks.
34    *
35    * @var array
36    */
37   static public $main_tasks = array('mail','settings','addressbook','login','logout','utils','dummy');
38
39   /**
40    * Singleton instace of rcmail
41    *
42    * @var rcmail
43    */
44   static private $instance;
45
46   /**
47    * Stores instance of rcube_config.
48    *
49    * @var rcube_config
50    */
51   public $config;
52
53   /**
54    * Stores rcube_user instance.
55    *
56    * @var rcube_user
57    */
58   public $user;
59
60   /**
61    * Instace of database class.
62    *
63    * @var rcube_mdb2
64    */
65   public $db;
66
67   /**
68    * Instace of Memcache class.
69    *
70    * @var rcube_mdb2
71    */
72   public $memcache;
73
74   /**
75    * Instace of rcube_session class.
76    *
77    * @var rcube_session
78    */
79   public $session;
80
81   /**
82    * Instance of rcube_smtp class.
83    *
84    * @var rcube_smtp
85    */
86   public $smtp;
87
88   /**
89    * Instance of rcube_imap class.
90    *
91    * @var rcube_imap
92    */
93   public $imap;
94
95   /**
96    * Instance of rcube_template class.
97    *
98    * @var rcube_template
99    */
100   public $output;
101
102   /**
103    * Instance of rcube_plugin_api.
104    *
105    * @var rcube_plugin_api
106    */
107   public $plugins;
108
109   /**
110    * Current task.
111    *
112    * @var string
113    */
114   public $task;
115
116   /**
117    * Current action.
118    *
119    * @var string
120    */
121   public $action = '';
122   public $comm_path = './';
123
124   private $texts;
125   private $address_books = array();
126   private $caches = array();
127   private $action_map = array();
128   private $shutdown_functions = array();
129
130
131   /**
132    * This implements the 'singleton' design pattern
133    *
134    * @return rcmail The one and only instance
135    */
136   static function get_instance()
137   {
138     if (!self::$instance) {
139       self::$instance = new rcmail();
140       self::$instance->startup();  // init AFTER object was linked with self::$instance
141     }
142
143     return self::$instance;
144   }
145
146
147   /**
148    * Private constructor
149    */
150   private function __construct()
151   {
152     // load configuration
153     $this->config = new rcube_config();
154
155     register_shutdown_function(array($this, 'shutdown'));
156   }
157
158
159   /**
160    * Initial startup function
161    * to register session, create database and imap connections
162    *
163    * @todo Remove global vars $DB, $USER
164    */
165   private function startup()
166   {
167     // initialize syslog
168     if ($this->config->get('log_driver') == 'syslog') {
169       $syslog_id = $this->config->get('syslog_id', 'roundcube');
170       $syslog_facility = $this->config->get('syslog_facility', LOG_USER);
171       openlog($syslog_id, LOG_ODELAY, $syslog_facility);
172     }
173
174     // connect to database
175     $GLOBALS['DB'] = $this->get_dbh();
176
177     // start session
178     $this->session_init();
179
180     // create user object
181     $this->set_user(new rcube_user($_SESSION['user_id']));
182
183     // configure session (after user config merge!)
184     $this->session_configure();
185
186     // set task and action properties
187     $this->set_task(get_input_value('_task', RCUBE_INPUT_GPC));
188     $this->action = asciiwords(get_input_value('_action', RCUBE_INPUT_GPC));
189
190     // reset some session parameters when changing task
191     if ($this->task != 'utils') {
192       if ($this->session && $_SESSION['task'] != $this->task)
193         $this->session->remove('page');
194       // set current task to session
195       $_SESSION['task'] = $this->task;
196     }
197
198     // init output class
199     if (!empty($_REQUEST['_remote']))
200       $GLOBALS['OUTPUT'] = $this->json_init();
201     else
202       $GLOBALS['OUTPUT'] = $this->load_gui(!empty($_REQUEST['_framed']));
203
204     // create plugin API and load plugins
205     $this->plugins = rcube_plugin_api::get_instance();
206
207     // init plugins
208     $this->plugins->init();
209   }
210
211
212   /**
213    * Setter for application task
214    *
215    * @param string Task to set
216    */
217   public function set_task($task)
218   {
219     $task = asciiwords($task);
220
221     if ($this->user && $this->user->ID)
222       $task = !$task ? 'mail' : $task;
223     else
224       $task = 'login';
225
226     $this->task = $task;
227     $this->comm_path = $this->url(array('task' => $this->task));
228
229     if ($this->output)
230       $this->output->set_env('task', $this->task);
231   }
232
233
234   /**
235    * Setter for system user object
236    *
237    * @param rcube_user Current user instance
238    */
239   public function set_user($user)
240   {
241     if (is_object($user)) {
242       $this->user = $user;
243       $GLOBALS['USER'] = $this->user;
244
245       // overwrite config with user preferences
246       $this->config->set_user_prefs((array)$this->user->get_prefs());
247     }
248
249     $_SESSION['language'] = $this->user->language = $this->language_prop($this->config->get('language', $_SESSION['language']));
250
251     // set localization
252     setlocale(LC_ALL, $_SESSION['language'] . '.utf8', 'en_US.utf8');
253
254     // workaround for http://bugs.php.net/bug.php?id=18556
255     if (in_array($_SESSION['language'], array('tr_TR', 'ku', 'az_AZ')))
256       setlocale(LC_CTYPE, 'en_US' . '.utf8');
257   }
258
259
260   /**
261    * Check the given string and return a valid language code
262    *
263    * @param string Language code
264    * @return string Valid language code
265    */
266   private function language_prop($lang)
267   {
268     static $rcube_languages, $rcube_language_aliases;
269
270     // user HTTP_ACCEPT_LANGUAGE if no language is specified
271     if (empty($lang) || $lang == 'auto') {
272        $accept_langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
273        $lang = str_replace('-', '_', $accept_langs[0]);
274      }
275
276     if (empty($rcube_languages)) {
277       @include(INSTALL_PATH . 'program/localization/index.inc');
278     }
279
280     // check if we have an alias for that language
281     if (!isset($rcube_languages[$lang]) && isset($rcube_language_aliases[$lang])) {
282       $lang = $rcube_language_aliases[$lang];
283     }
284     // try the first two chars
285     else if (!isset($rcube_languages[$lang])) {
286       $short = substr($lang, 0, 2);
287
288       // check if we have an alias for the short language code
289       if (!isset($rcube_languages[$short]) && isset($rcube_language_aliases[$short])) {
290         $lang = $rcube_language_aliases[$short];
291       }
292       // expand 'nn' to 'nn_NN'
293       else if (!isset($rcube_languages[$short])) {
294         $lang = $short.'_'.strtoupper($short);
295       }
296     }
297
298     if (!isset($rcube_languages[$lang]) || !is_dir(INSTALL_PATH . 'program/localization/' . $lang)) {
299       $lang = 'en_US';
300     }
301
302     return $lang;
303   }
304
305
306   /**
307    * Get the current database connection
308    *
309    * @return rcube_mdb2  Database connection object
310    */
311   public function get_dbh()
312   {
313     if (!$this->db) {
314       $config_all = $this->config->all();
315
316       $this->db = new rcube_mdb2($config_all['db_dsnw'], $config_all['db_dsnr'], $config_all['db_persistent']);
317       $this->db->sqlite_initials = INSTALL_PATH . 'SQL/sqlite.initial.sql';
318       $this->db->set_debug((bool)$config_all['sql_debug']);
319     }
320
321     return $this->db;
322   }
323   
324   
325   /**
326    * Get global handle for memcache access
327    *
328    * @return object Memcache
329    */
330   public function get_memcache()
331   {
332     if (!isset($this->memcache)) {
333       // no memcache support in PHP
334       if (!class_exists('Memcache')) {
335         $this->memcache = false;
336         return false;
337       }
338
339       $this->memcache = new Memcache;
340       $this->mc_available = 0;
341       
342       // add alll configured hosts to pool
343       $pconnect = $this->config->get('memcache_pconnect', true);
344       foreach ($this->config->get('memcache_hosts', array()) as $host) {
345         list($host, $port) = explode(':', $host);
346         if (!$port) $port = 11211;
347         $this->mc_available += intval($this->memcache->addServer($host, $port, $pconnect, 1, 1, 15, false, array($this, 'memcache_failure')));
348       }
349       
350       // test connection and failover (will result in $this->mc_available == 0 on complete failure)
351       $this->memcache->increment('__CONNECTIONTEST__', 1);  // NOP if key doesn't exist
352
353       if (!$this->mc_available)
354         $this->memcache = false;
355     }
356
357     return $this->memcache;
358   }
359   
360   /**
361    * Callback for memcache failure
362    */
363   public function memcache_failure($host, $port)
364   {
365     static $seen = array();
366     
367     // only report once
368     if (!$seen["$host:$port"]++) {
369       $this->mc_available--;
370       raise_error(array('code' => 604, 'type' => 'db',
371         'line' => __LINE__, 'file' => __FILE__,
372         'message' => "Memcache failure on host $host:$port"),
373         true, false);
374     }
375   }
376
377
378   /**
379    * Initialize and get cache object
380    *
381    * @param string $name   Cache identifier
382    * @param string $type   Cache type ('db', 'apc' or 'memcache')
383    * @param int    $ttl    Expiration time for cache items in seconds
384    * @param bool   $packed Enables/disables data serialization
385    *
386    * @return rcube_cache Cache object
387    */
388   public function get_cache($name, $type='db', $ttl=0, $packed=true)
389   {
390     if (!isset($this->caches[$name])) {
391       $this->caches[$name] = new rcube_cache($type, $_SESSION['user_id'], $name, $ttl, $packed);
392     }
393
394     return $this->caches[$name];
395   }
396
397
398   /**
399    * Return instance of the internal address book class
400    *
401    * @param string  Address book identifier
402    * @param boolean True if the address book needs to be writeable
403    *
404    * @return rcube_contacts Address book object
405    */
406   public function get_address_book($id, $writeable = false)
407   {
408     $contacts    = null;
409     $ldap_config = (array)$this->config->get('ldap_public');
410     $abook_type  = strtolower($this->config->get('address_book_type'));
411
412     // 'sql' is the alias for '0' used by autocomplete
413     if ($id == 'sql')
414         $id = '0';
415
416     // use existing instance
417     if (isset($this->address_books[$id]) && is_object($this->address_books[$id])
418       && is_a($this->address_books[$id], 'rcube_addressbook')
419       && (!$writeable || !$this->address_books[$id]->readonly)
420     ) {
421       $contacts = $this->address_books[$id];
422     }
423     else if ($id && $ldap_config[$id]) {
424       $contacts = new rcube_ldap($ldap_config[$id], $this->config->get('ldap_debug'), $this->config->mail_domain($_SESSION['imap_host']));
425     }
426     else if ($id === '0') {
427       $contacts = new rcube_contacts($this->db, $this->user->ID);
428     }
429     else {
430       $plugin = $this->plugins->exec_hook('addressbook_get', array('id' => $id, 'writeable' => $writeable));
431
432       // plugin returned instance of a rcube_addressbook
433       if ($plugin['instance'] instanceof rcube_addressbook) {
434         $contacts = $plugin['instance'];
435       }
436       // get first source from the list
437       else if (!$id) {
438         $source = reset($this->get_address_sources($writeable));
439         if (!empty($source)) {
440           $contacts = $this->get_address_book($source['id']);
441           if ($contacts)
442             $id = $source['id'];
443         }
444       }
445     }
446
447     if (!$contacts) {
448       raise_error(array(
449         'code' => 700, 'type' => 'php',
450         'file' => __FILE__, 'line' => __LINE__,
451         'message' => "Addressbook source ($id) not found!"),
452         true, true);
453     }
454
455     // set configured sort order
456     if ($sort_col = $this->config->get('addressbook_sort_col'))
457         $contacts->set_sort_order($sort_col);
458
459     // add to the 'books' array for shutdown function
460     $this->address_books[$id] = $contacts;
461
462     return $contacts;
463   }
464
465
466   /**
467    * Return address books list
468    *
469    * @param boolean True if the address book needs to be writeable
470    *
471    * @return array  Address books array
472    */
473   public function get_address_sources($writeable = false)
474   {
475     $abook_type = strtolower($this->config->get('address_book_type'));
476     $ldap_config = $this->config->get('ldap_public');
477     $autocomplete = (array) $this->config->get('autocomplete_addressbooks');
478     $list = array();
479
480     // We are using the DB address book
481     if ($abook_type != 'ldap') {
482       if (!isset($this->address_books['0']))
483         $this->address_books['0'] = new rcube_contacts($this->db, $this->user->ID);
484       $list['0'] = array(
485         'id'       => '0',
486         'name'     => rcube_label('personaladrbook'),
487         'groups'   => $this->address_books['0']->groups,
488         'readonly' => $this->address_books['0']->readonly,
489         'autocomplete' => in_array('sql', $autocomplete),
490         'undelete' => $this->address_books['0']->undelete && $this->config->get('undo_timeout'),
491       );
492     }
493
494     if ($ldap_config) {
495       $ldap_config = (array) $ldap_config;
496       foreach ($ldap_config as $id => $prop)
497         $list[$id] = array(
498           'id'       => $id,
499           'name'     => $prop['name'],
500           'groups'   => is_array($prop['groups']),
501           'readonly' => !$prop['writable'],
502           'hidden'   => $prop['hidden'],
503           'autocomplete' => in_array($id, $autocomplete)
504         );
505     }
506
507     $plugin = $this->plugins->exec_hook('addressbooks_list', array('sources' => $list));
508     $list = $plugin['sources'];
509
510     foreach ($list as $idx => $item) {
511       // register source for shutdown function
512       if (!is_object($this->address_books[$item['id']]))
513         $this->address_books[$item['id']] = $item;
514       // remove from list if not writeable as requested
515       if ($writeable && $item['readonly'])
516           unset($list[$idx]);
517     }
518
519     return $list;
520   }
521
522
523   /**
524    * Init output object for GUI and add common scripts.
525    * This will instantiate a rcmail_template object and set
526    * environment vars according to the current session and configuration
527    *
528    * @param boolean True if this request is loaded in a (i)frame
529    * @return rcube_template Reference to HTML output object
530    */
531   public function load_gui($framed = false)
532   {
533     // init output page
534     if (!($this->output instanceof rcube_template))
535       $this->output = new rcube_template($this->task, $framed);
536
537     // set keep-alive/check-recent interval
538     if ($this->session && ($keep_alive = $this->session->get_keep_alive())) {
539       $this->output->set_env('keep_alive', $keep_alive);
540     }
541
542     if ($framed) {
543       $this->comm_path .= '&_framed=1';
544       $this->output->set_env('framed', true);
545     }
546
547     $this->output->set_env('task', $this->task);
548     $this->output->set_env('action', $this->action);
549     $this->output->set_env('comm_path', $this->comm_path);
550     $this->output->set_charset(RCMAIL_CHARSET);
551
552     // add some basic labels to client
553     $this->output->add_label('loading', 'servererror');
554
555     return $this->output;
556   }
557
558
559   /**
560    * Create an output object for JSON responses
561    *
562    * @return rcube_json_output Reference to JSON output object
563    */
564   public function json_init()
565   {
566     if (!($this->output instanceof rcube_json_output))
567       $this->output = new rcube_json_output($this->task);
568
569     return $this->output;
570   }
571
572
573   /**
574    * Create SMTP object and connect to server
575    *
576    * @param boolean True if connection should be established
577    */
578   public function smtp_init($connect = false)
579   {
580     $this->smtp = new rcube_smtp();
581
582     if ($connect)
583       $this->smtp->connect();
584   }
585
586
587   /**
588    * Create global IMAP object and connect to server
589    *
590    * @param boolean True if connection should be established
591    * @todo Remove global $IMAP
592    */
593   public function imap_init($connect = false)
594   {
595     // already initialized
596     if (is_object($this->imap))
597       return;
598
599     $this->imap = new rcube_imap();
600     $this->imap->skip_deleted = $this->config->get('skip_deleted');
601
602     // enable caching of imap data
603     $imap_cache = $this->config->get('imap_cache');
604     $messages_cache = $this->config->get('messages_cache');
605     // for backward compatybility
606     if ($imap_cache === null && $messages_cache === null && $this->config->get('enable_caching')) {
607         $imap_cache     = 'db';
608         $messages_cache = true;
609     }
610     if ($imap_cache)
611         $this->imap->set_caching($imap_cache);
612     if ($messages_cache)
613         $this->imap->set_messages_caching(true);
614
615     // set pagesize from config
616     $this->imap->set_pagesize($this->config->get('pagesize', 50));
617
618     // Setting root and delimiter before establishing the connection
619     // can save time detecting them using NAMESPACE and LIST
620     $options = array(
621       'auth_type'   => $this->config->get('imap_auth_type', 'check'),
622       'auth_cid'    => $this->config->get('imap_auth_cid'),
623       'auth_pw'     => $this->config->get('imap_auth_pw'),
624       'debug'       => (bool) $this->config->get('imap_debug', 0),
625       'force_caps'  => (bool) $this->config->get('imap_force_caps'),
626       'timeout'     => (int) $this->config->get('imap_timeout', 0),
627     );
628
629     $this->imap->set_options($options);
630
631     // set global object for backward compatibility
632     $GLOBALS['IMAP'] = $this->imap;
633
634     $hook = $this->plugins->exec_hook('imap_init', array('fetch_headers' => $this->imap->fetch_add_headers));
635     if ($hook['fetch_headers'])
636       $this->imap->fetch_add_headers = $hook['fetch_headers'];
637
638     // support this parameter for backward compatibility but log warning
639     if ($connect) {
640       $this->imap_connect();
641       raise_error(array(
642         'code' => 800, 'type' => 'imap',
643         'file' => __FILE__, 'line' => __LINE__,
644         'message' => "rcube::imap_init(true) is deprecated, use rcube::imap_connect() instead"),
645         true, false);
646     }
647   }
648
649
650   /**
651    * Connect to IMAP server with stored session data
652    *
653    * @return bool True on success, false on error
654    */
655   public function imap_connect()
656   {
657     if (!$this->imap)
658       $this->imap_init();
659
660     if ($_SESSION['imap_host'] && !$this->imap->conn->connected()) {
661       if (!$this->imap->connect($_SESSION['imap_host'], $_SESSION['username'], $this->decrypt($_SESSION['password']), $_SESSION['imap_port'], $_SESSION['imap_ssl'])) {
662         if ($this->output)
663           $this->output->show_message($this->imap->get_error_code() == -1 ? 'imaperror' : 'sessionerror', 'error');
664       }
665       else {
666         $this->set_imap_prop();
667         return $this->imap->conn;
668       }
669     }
670
671     return false;
672   }
673
674
675   /**
676    * Create session object and start the session.
677    */
678   public function session_init()
679   {
680     // session started (Installer?)
681     if (session_id())
682       return;
683
684     $sess_name   = $this->config->get('session_name');
685     $sess_domain = $this->config->get('session_domain');
686     $lifetime    = $this->config->get('session_lifetime', 0) * 60;
687
688     // set session domain
689     if ($sess_domain) {
690       ini_set('session.cookie_domain', $sess_domain);
691     }
692     // set session garbage collecting time according to session_lifetime
693     if ($lifetime) {
694       ini_set('session.gc_maxlifetime', $lifetime * 2);
695     }
696
697     ini_set('session.cookie_secure', rcube_https_check());
698     ini_set('session.name', $sess_name ? $sess_name : 'roundcube_sessid');
699     ini_set('session.use_cookies', 1);
700     ini_set('session.use_only_cookies', 1);
701     ini_set('session.serialize_handler', 'php');
702
703     // use database for storing session data
704     $this->session = new rcube_session($this->get_dbh(), $this->config);
705
706     $this->session->register_gc_handler('rcmail_temp_gc');
707     $this->session->register_gc_handler('rcmail_cache_gc');
708
709     // start PHP session (if not in CLI mode)
710     if ($_SERVER['REMOTE_ADDR'])
711       session_start();
712
713     // set initial session vars
714     if (!$_SESSION['user_id'])
715       $_SESSION['temp'] = true;
716   }
717
718
719   /**
720    * Configure session object internals
721    */
722   public function session_configure()
723   {
724     if (!$this->session)
725       return;
726
727     $lifetime = $this->config->get('session_lifetime', 0) * 60;
728
729     // set keep-alive/check-recent interval
730     if ($keep_alive = $this->config->get('keep_alive')) {
731       // be sure that it's less than session lifetime
732       if ($lifetime)
733         $keep_alive = min($keep_alive, $lifetime - 30);
734       $keep_alive = max(60, $keep_alive);
735       $this->session->set_keep_alive($keep_alive);
736     }
737
738     $this->session->set_secret($this->config->get('des_key') . $_SERVER['HTTP_USER_AGENT']);
739     $this->session->set_ip_check($this->config->get('ip_check'));
740   }
741
742
743   /**
744    * Perfom login to the IMAP server and to the webmail service.
745    * This will also create a new user entry if auto_create_user is configured.
746    *
747    * @param string IMAP user name
748    * @param string IMAP password
749    * @param string IMAP host
750    * @return boolean True on success, False on failure
751    */
752   function login($username, $pass, $host=NULL)
753   {
754     $user = NULL;
755     $config = $this->config->all();
756
757     if (!$host)
758       $host = $config['default_host'];
759
760     // Validate that selected host is in the list of configured hosts
761     if (is_array($config['default_host'])) {
762       $allowed = false;
763       foreach ($config['default_host'] as $key => $host_allowed) {
764         if (!is_numeric($key))
765           $host_allowed = $key;
766         if ($host == $host_allowed) {
767           $allowed = true;
768           break;
769         }
770       }
771       if (!$allowed)
772         return false;
773       }
774     else if (!empty($config['default_host']) && $host != rcube_parse_host($config['default_host']))
775       return false;
776
777     // parse $host URL
778     $a_host = parse_url($host);
779     if ($a_host['host']) {
780       $host = $a_host['host'];
781       $imap_ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null;
782       if (!empty($a_host['port']))
783         $imap_port = $a_host['port'];
784       else if ($imap_ssl && $imap_ssl != 'tls' && (!$config['default_port'] || $config['default_port'] == 143))
785         $imap_port = 993;
786     }
787
788     $imap_port = $imap_port ? $imap_port : $config['default_port'];
789
790     /* Modify username with domain if required
791        Inspired by Marco <P0L0_notspam_binware.org>
792     */
793     // Check if we need to add domain
794     if (!empty($config['username_domain']) && strpos($username, '@') === false) {
795       if (is_array($config['username_domain']) && isset($config['username_domain'][$host]))
796         $username .= '@'.rcube_parse_host($config['username_domain'][$host], $host);
797       else if (is_string($config['username_domain']))
798         $username .= '@'.rcube_parse_host($config['username_domain'], $host);
799     }
800
801     // Convert username to lowercase. If IMAP backend
802     // is case-insensitive we need to store always the same username (#1487113)
803     if ($config['login_lc']) {
804       $username = mb_strtolower($username);
805     }
806
807     // try to resolve email address from virtuser table
808     if (strpos($username, '@') && ($virtuser = rcube_user::email2user($username))) {
809       $username = $virtuser;
810     }
811
812     // Here we need IDNA ASCII
813     // Only rcube_contacts class is using domain names in Unicode
814     $host = rcube_idn_to_ascii($host);
815     if (strpos($username, '@')) {
816       // lowercase domain name
817       list($local, $domain) = explode('@', $username);
818       $username = $local . '@' . mb_strtolower($domain);
819       $username = rcube_idn_to_ascii($username);
820     }
821
822     // user already registered -> overwrite username
823     if ($user = rcube_user::query($username, $host))
824       $username = $user->data['username'];
825
826     if (!$this->imap)
827       $this->imap_init();
828
829     // try IMAP login
830     if (!($imap_login = $this->imap->connect($host, $username, $pass, $imap_port, $imap_ssl))) {
831       // try with lowercase
832       $username_lc = mb_strtolower($username);
833       if ($username_lc != $username) {
834         // try to find user record again -> overwrite username
835         if (!$user && ($user = rcube_user::query($username_lc, $host)))
836           $username_lc = $user->data['username'];
837
838         if ($imap_login = $this->imap->connect($host, $username_lc, $pass, $imap_port, $imap_ssl))
839           $username = $username_lc;
840       }
841     }
842
843     // exit if IMAP login failed
844     if (!$imap_login)
845       return false;
846
847     // user already registered -> update user's record
848     if (is_object($user)) {
849       // update last login timestamp
850       $user->touch();
851     }
852     // create new system user
853     else if ($config['auto_create_user']) {
854       if ($created = rcube_user::create($username, $host)) {
855         $user = $created;
856       }
857       else {
858         raise_error(array(
859           'code' => 620, 'type' => 'php',
860           'file' => __FILE__, 'line' => __LINE__,
861           'message' => "Failed to create a user record. Maybe aborted by a plugin?"
862           ), true, false);
863       }
864     }
865     else {
866       raise_error(array(
867         'code' => 621, 'type' => 'php',
868         'file' => __FILE__, 'line' => __LINE__,
869         'message' => "Access denied for new user $username. 'auto_create_user' is disabled"
870         ), true, false);
871     }
872
873     // login succeeded
874     if (is_object($user) && $user->ID) {
875       // Configure environment
876       $this->set_user($user);
877       $this->set_imap_prop();
878       $this->session_configure();
879
880       // fix some old settings according to namespace prefix
881       $this->fix_namespace_settings($user);
882
883       // create default folders on first login
884       if ($config['create_default_folders'] && (!empty($created) || empty($user->data['last_login']))) {
885         $this->imap->create_default_folders();
886       }
887
888       // set session vars
889       $_SESSION['user_id']   = $user->ID;
890       $_SESSION['username']  = $user->data['username'];
891       $_SESSION['imap_host'] = $host;
892       $_SESSION['imap_port'] = $imap_port;
893       $_SESSION['imap_ssl']  = $imap_ssl;
894       $_SESSION['password']  = $this->encrypt($pass);
895       $_SESSION['login_time'] = mktime();
896
897       if (isset($_REQUEST['_timezone']) && $_REQUEST['_timezone'] != '_default_')
898         $_SESSION['timezone'] = floatval($_REQUEST['_timezone']);
899       if (isset($_REQUEST['_dstactive']) && $_REQUEST['_dstactive'] != '_default_')
900         $_SESSION['dst_active'] = intval($_REQUEST['_dstactive']);
901
902       // force reloading complete list of subscribed mailboxes
903       $this->imap->clear_cache('mailboxes', true);
904
905       return true;
906     }
907
908     return false;
909   }
910
911
912   /**
913    * Set root dir and last stored mailbox
914    * This must be done AFTER connecting to the server!
915    */
916   public function set_imap_prop()
917   {
918     $this->imap->set_charset($this->config->get('default_charset', RCMAIL_CHARSET));
919
920     if ($default_folders = $this->config->get('default_imap_folders')) {
921       $this->imap->set_default_mailboxes($default_folders);
922     }
923     if (isset($_SESSION['mbox'])) {
924       $this->imap->set_mailbox($_SESSION['mbox']);
925     }
926     if (isset($_SESSION['page'])) {
927       $this->imap->set_page($_SESSION['page']);
928     }
929   }
930
931
932   /**
933    * Auto-select IMAP host based on the posted login information
934    *
935    * @return string Selected IMAP host
936    */
937   public function autoselect_host()
938   {
939     $default_host = $this->config->get('default_host');
940     $host = null;
941
942     if (is_array($default_host)) {
943       $post_host = get_input_value('_host', RCUBE_INPUT_POST);
944
945       // direct match in default_host array
946       if ($default_host[$post_host] || in_array($post_host, array_values($default_host))) {
947         $host = $post_host;
948       }
949
950       // try to select host by mail domain
951       list($user, $domain) = explode('@', get_input_value('_user', RCUBE_INPUT_POST));
952       if (!empty($domain)) {
953         foreach ($default_host as $imap_host => $mail_domains) {
954           if (is_array($mail_domains) && in_array($domain, $mail_domains)) {
955             $host = $imap_host;
956             break;
957           }
958         }
959       }
960
961       // take the first entry if $host is still an array
962       if (empty($host)) {
963         $host = array_shift($default_host);
964       }
965     }
966     else if (empty($default_host)) {
967       $host = get_input_value('_host', RCUBE_INPUT_POST);
968     }
969     else
970       $host = rcube_parse_host($default_host);
971
972     return $host;
973   }
974
975
976   /**
977    * Get localized text in the desired language
978    *
979    * @param mixed   $attrib  Named parameters array or label name
980    * @param string  $domain  Label domain (plugin) name
981    *
982    * @return string Localized text
983    */
984   public function gettext($attrib, $domain=null)
985   {
986     // load localization files if not done yet
987     if (empty($this->texts))
988       $this->load_language();
989
990     // extract attributes
991     if (is_string($attrib))
992       $attrib = array('name' => $attrib);
993
994     $nr = is_numeric($attrib['nr']) ? $attrib['nr'] : 1;
995     $name = $attrib['name'] ? $attrib['name'] : '';
996
997     // attrib contain text values: use them from now
998     if (($setval = $attrib[strtolower($_SESSION['language'])]) || ($setval = $attrib['en_us']))
999         $this->texts[$name] = $setval;
1000
1001     // check for text with domain
1002     if ($domain && ($text_item = $this->texts[$domain.'.'.$name]))
1003       ;
1004     // text does not exist
1005     else if (!($text_item = $this->texts[$name])) {
1006       return "[$name]";
1007     }
1008
1009     // make text item array
1010     $a_text_item = is_array($text_item) ? $text_item : array('single' => $text_item);
1011
1012     // decide which text to use
1013     if ($nr == 1) {
1014       $text = $a_text_item['single'];
1015     }
1016     else if ($nr > 0) {
1017       $text = $a_text_item['multiple'];
1018     }
1019     else if ($nr == 0) {
1020       if ($a_text_item['none'])
1021         $text = $a_text_item['none'];
1022       else if ($a_text_item['single'])
1023         $text = $a_text_item['single'];
1024       else if ($a_text_item['multiple'])
1025         $text = $a_text_item['multiple'];
1026     }
1027
1028     // default text is single
1029     if ($text == '') {
1030       $text = $a_text_item['single'];
1031     }
1032
1033     // replace vars in text
1034     if (is_array($attrib['vars'])) {
1035       foreach ($attrib['vars'] as $var_key => $var_value)
1036         $text = str_replace($var_key[0]!='$' ? '$'.$var_key : $var_key, $var_value, $text);
1037     }
1038
1039     // format output
1040     if (($attrib['uppercase'] && strtolower($attrib['uppercase']=='first')) || $attrib['ucfirst'])
1041       return ucfirst($text);
1042     else if ($attrib['uppercase'])
1043       return mb_strtoupper($text);
1044     else if ($attrib['lowercase'])
1045       return mb_strtolower($text);
1046
1047     return $text;
1048   }
1049
1050
1051   /**
1052    * Check if the given text label exists
1053    *
1054    * @param string  $name       Label name
1055    * @param string  $domain     Label domain (plugin) name or '*' for all domains
1056    * @param string  $ref_domain Sets domain name if label is found
1057    *
1058    * @return boolean True if text exists (either in the current language or in en_US)
1059    */
1060   public function text_exists($name, $domain = null, &$ref_domain = null)
1061   {
1062     // load localization files if not done yet
1063     if (empty($this->texts))
1064       $this->load_language();
1065
1066     if (isset($this->texts[$name])) {
1067         $ref_domain = '';
1068         return true;
1069     }
1070
1071     // any of loaded domains (plugins)
1072     if ($domain == '*') {
1073       foreach ($this->plugins->loaded_plugins() as $domain)
1074         if (isset($this->texts[$domain.'.'.$name])) {
1075           $ref_domain = $domain;
1076           return true;
1077         }
1078     }
1079     // specified domain
1080     else if ($domain) {
1081       $ref_domain = $domain;
1082       return isset($this->texts[$domain.'.'.$name]);
1083     }
1084
1085     return false;
1086   }
1087
1088   /**
1089    * Load a localization package
1090    *
1091    * @param string Language ID
1092    */
1093   public function load_language($lang = null, $add = array())
1094   {
1095     $lang = $this->language_prop(($lang ? $lang : $_SESSION['language']));
1096
1097     // load localized texts
1098     if (empty($this->texts) || $lang != $_SESSION['language']) {
1099       $this->texts = array();
1100
1101       // handle empty lines after closing PHP tag in localization files
1102       ob_start();
1103
1104       // get english labels (these should be complete)
1105       @include(INSTALL_PATH . 'program/localization/en_US/labels.inc');
1106       @include(INSTALL_PATH . 'program/localization/en_US/messages.inc');
1107
1108       if (is_array($labels))
1109         $this->texts = $labels;
1110       if (is_array($messages))
1111         $this->texts = array_merge($this->texts, $messages);
1112
1113       // include user language files
1114       if ($lang != 'en' && is_dir(INSTALL_PATH . 'program/localization/' . $lang)) {
1115         include_once(INSTALL_PATH . 'program/localization/' . $lang . '/labels.inc');
1116         include_once(INSTALL_PATH . 'program/localization/' . $lang . '/messages.inc');
1117
1118         if (is_array($labels))
1119           $this->texts = array_merge($this->texts, $labels);
1120         if (is_array($messages))
1121           $this->texts = array_merge($this->texts, $messages);
1122       }
1123
1124       ob_end_clean();
1125
1126       $_SESSION['language'] = $lang;
1127     }
1128
1129     // append additional texts (from plugin)
1130     if (is_array($add) && !empty($add))
1131       $this->texts += $add;
1132   }
1133
1134
1135   /**
1136    * Read directory program/localization and return a list of available languages
1137    *
1138    * @return array List of available localizations
1139    */
1140   public function list_languages()
1141   {
1142     static $sa_languages = array();
1143
1144     if (!sizeof($sa_languages)) {
1145       @include(INSTALL_PATH . 'program/localization/index.inc');
1146
1147       if ($dh = @opendir(INSTALL_PATH . 'program/localization')) {
1148         while (($name = readdir($dh)) !== false) {
1149           if ($name[0] == '.' || !is_dir(INSTALL_PATH . 'program/localization/' . $name))
1150             continue;
1151
1152           if ($label = $rcube_languages[$name])
1153             $sa_languages[$name] = $label;
1154         }
1155         closedir($dh);
1156       }
1157     }
1158
1159     return $sa_languages;
1160   }
1161
1162
1163   /**
1164    * Destroy session data and remove cookie
1165    */
1166   public function kill_session()
1167   {
1168     $this->plugins->exec_hook('session_destroy');
1169
1170     $this->session->kill();
1171     $_SESSION = array('language' => $this->user->language, 'temp' => true);
1172     $this->user->reset();
1173   }
1174
1175
1176   /**
1177    * Do server side actions on logout
1178    */
1179   public function logout_actions()
1180   {
1181     $config = $this->config->all();
1182
1183     // on logout action we're not connected to imap server
1184     if (($config['logout_purge'] && !empty($config['trash_mbox'])) || $config['logout_expunge']) {
1185       if (!$this->session->check_auth())
1186         return;
1187
1188       $this->imap_connect();
1189     }
1190
1191     if ($config['logout_purge'] && !empty($config['trash_mbox'])) {
1192       $this->imap->clear_mailbox($config['trash_mbox']);
1193     }
1194
1195     if ($config['logout_expunge']) {
1196       $this->imap->expunge('INBOX');
1197     }
1198
1199     // Try to save unsaved user preferences
1200     if (!empty($_SESSION['preferences'])) {
1201       $this->user->save_prefs(unserialize($_SESSION['preferences']));
1202     }
1203   }
1204
1205
1206   /**
1207    * Function to be executed in script shutdown
1208    * Registered with register_shutdown_function()
1209    */
1210   public function shutdown()
1211   {
1212     foreach ($this->shutdown_functions as $function)
1213       call_user_func($function);
1214
1215     if (is_object($this->smtp))
1216       $this->smtp->disconnect();
1217
1218     foreach ($this->address_books as $book) {
1219       if (is_object($book) && is_a($book, 'rcube_addressbook'))
1220         $book->close();
1221     }
1222
1223     foreach ($this->caches as $cache) {
1224         if (is_object($cache))
1225             $cache->close();
1226     }
1227
1228     if (is_object($this->imap))
1229       $this->imap->close();
1230
1231     // before closing the database connection, write session data
1232     if ($_SERVER['REMOTE_ADDR'] && is_object($this->session)) {
1233       session_write_close();
1234     }
1235
1236     // write performance stats to logs/console
1237     if ($this->config->get('devel_mode')) {
1238       if (function_exists('memory_get_usage'))
1239         $mem = show_bytes(memory_get_usage());
1240       if (function_exists('memory_get_peak_usage'))
1241         $mem .= '/'.show_bytes(memory_get_peak_usage());
1242
1243       $log = $this->task . ($this->action ? '/'.$this->action : '') . ($mem ? " [$mem]" : '');
1244       if (defined('RCMAIL_START'))
1245         rcube_print_time(RCMAIL_START, $log);
1246       else
1247         console($log);
1248     }
1249   }
1250
1251
1252   /**
1253    * Registers shutdown function to be executed on shutdown.
1254    * The functions will be executed before destroying any
1255    * objects like smtp, imap, session, etc.
1256    *
1257    * @param callback Function callback
1258    */
1259   public function add_shutdown_function($function)
1260   {
1261     $this->shutdown_functions[] = $function;
1262   }
1263
1264
1265   /**
1266    * Generate a unique token to be used in a form request
1267    *
1268    * @return string The request token
1269    */
1270   public function get_request_token()
1271   {
1272     $sess_id = $_COOKIE[ini_get('session.name')];
1273     if (!$sess_id) $sess_id = session_id();
1274     $plugin = $this->plugins->exec_hook('request_token', array('value' => md5('RT' . $this->user->ID . $this->config->get('des_key') . $sess_id)));
1275     return $plugin['value'];
1276   }
1277
1278
1279   /**
1280    * Check if the current request contains a valid token
1281    *
1282    * @param int Request method
1283    * @return boolean True if request token is valid false if not
1284    */
1285   public function check_request($mode = RCUBE_INPUT_POST)
1286   {
1287     $token = get_input_value('_token', $mode);
1288     $sess_id = $_COOKIE[ini_get('session.name')];
1289     return !empty($sess_id) && $token == $this->get_request_token();
1290   }
1291
1292
1293   /**
1294    * Create unique authorization hash
1295    *
1296    * @param string Session ID
1297    * @param int Timestamp
1298    * @return string The generated auth hash
1299    */
1300   private function get_auth_hash($sess_id, $ts)
1301   {
1302     $auth_string = sprintf('rcmail*sess%sR%s*Chk:%s;%s',
1303       $sess_id,
1304       $ts,
1305       $this->config->get('ip_check') ? $_SERVER['REMOTE_ADDR'] : '***.***.***.***',
1306       $_SERVER['HTTP_USER_AGENT']);
1307
1308     if (function_exists('sha1'))
1309       return sha1($auth_string);
1310     else
1311       return md5($auth_string);
1312   }
1313
1314
1315   /**
1316    * Encrypt using 3DES
1317    *
1318    * @param string $clear clear text input
1319    * @param string $key encryption key to retrieve from the configuration, defaults to 'des_key'
1320    * @param boolean $base64 whether or not to base64_encode() the result before returning
1321    *
1322    * @return string encrypted text
1323    */
1324   public function encrypt($clear, $key = 'des_key', $base64 = true)
1325   {
1326     if (!$clear)
1327       return '';
1328     /*-
1329      * Add a single canary byte to the end of the clear text, which
1330      * will help find out how much of padding will need to be removed
1331      * upon decryption; see http://php.net/mcrypt_generic#68082
1332      */
1333     $clear = pack("a*H2", $clear, "80");
1334
1335     if (function_exists('mcrypt_module_open') &&
1336         ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, "")))
1337     {
1338       $iv = $this->create_iv(mcrypt_enc_get_iv_size($td));
1339       mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv);
1340       $cipher = $iv . mcrypt_generic($td, $clear);
1341       mcrypt_generic_deinit($td);
1342       mcrypt_module_close($td);
1343     }
1344     else {
1345       // @include_once 'des.inc'; (not shipped with this distribution)
1346
1347       if (function_exists('des')) {
1348         $des_iv_size = 8;
1349         $iv = $this->create_iv($des_iv_size);
1350         $cipher = $iv . des($this->config->get_crypto_key($key), $clear, 1, 1, $iv);
1351       }
1352       else {
1353         raise_error(array(
1354           'code' => 500, 'type' => 'php',
1355           'file' => __FILE__, 'line' => __LINE__,
1356           'message' => "Could not perform encryption; make sure Mcrypt is installed or lib/des.inc is available"
1357         ), true, true);
1358       }
1359     }
1360
1361     return $base64 ? base64_encode($cipher) : $cipher;
1362   }
1363
1364   /**
1365    * Decrypt 3DES-encrypted string
1366    *
1367    * @param string $cipher encrypted text
1368    * @param string $key encryption key to retrieve from the configuration, defaults to 'des_key'
1369    * @param boolean $base64 whether or not input is base64-encoded
1370    *
1371    * @return string decrypted text
1372    */
1373   public function decrypt($cipher, $key = 'des_key', $base64 = true)
1374   {
1375     if (!$cipher)
1376       return '';
1377
1378     $cipher = $base64 ? base64_decode($cipher) : $cipher;
1379
1380     if (function_exists('mcrypt_module_open') &&
1381         ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, "")))
1382     {
1383       $iv_size = mcrypt_enc_get_iv_size($td);
1384       $iv = substr($cipher, 0, $iv_size);
1385
1386       // session corruption? (#1485970)
1387       if (strlen($iv) < $iv_size)
1388         return '';
1389
1390       $cipher = substr($cipher, $iv_size);
1391       mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv);
1392       $clear = mdecrypt_generic($td, $cipher);
1393       mcrypt_generic_deinit($td);
1394       mcrypt_module_close($td);
1395     }
1396     else {
1397       // @include_once 'des.inc'; (not shipped with this distribution)
1398
1399       if (function_exists('des')) {
1400         $des_iv_size = 8;
1401         $iv = substr($cipher, 0, $des_iv_size);
1402         $cipher = substr($cipher, $des_iv_size);
1403         $clear = des($this->config->get_crypto_key($key), $cipher, 0, 1, $iv);
1404       }
1405       else {
1406         raise_error(array(
1407           'code' => 500, 'type' => 'php',
1408           'file' => __FILE__, 'line' => __LINE__,
1409           'message' => "Could not perform decryption; make sure Mcrypt is installed or lib/des.inc is available"
1410         ), true, true);
1411       }
1412     }
1413
1414     /*-
1415      * Trim PHP's padding and the canary byte; see note in
1416      * rcmail::encrypt() and http://php.net/mcrypt_generic#68082
1417      */
1418     $clear = substr(rtrim($clear, "\0"), 0, -1);
1419
1420     return $clear;
1421   }
1422
1423   /**
1424    * Generates encryption initialization vector (IV)
1425    *
1426    * @param int Vector size
1427    * @return string Vector string
1428    */
1429   private function create_iv($size)
1430   {
1431     // mcrypt_create_iv() can be slow when system lacks entrophy
1432     // we'll generate IV vector manually
1433     $iv = '';
1434     for ($i = 0; $i < $size; $i++)
1435         $iv .= chr(mt_rand(0, 255));
1436     return $iv;
1437   }
1438
1439   /**
1440    * Build a valid URL to this instance of Roundcube
1441    *
1442    * @param mixed Either a string with the action or url parameters as key-value pairs
1443    * @return string Valid application URL
1444    */
1445   public function url($p)
1446   {
1447     if (!is_array($p))
1448       $p = array('_action' => @func_get_arg(0));
1449
1450     $task = $p['_task'] ? $p['_task'] : ($p['task'] ? $p['task'] : $this->task);
1451     $p['_task'] = $task;
1452     unset($p['task']);
1453
1454     $url = './';
1455     $delm = '?';
1456     foreach (array_reverse($p) as $key => $val) {
1457       if ($val !== '') {
1458         $par = $key[0] == '_' ? $key : '_'.$key;
1459         $url .= $delm.urlencode($par).'='.urlencode($val);
1460         $delm = '&';
1461       }
1462     }
1463     return $url;
1464   }
1465
1466
1467   /**
1468    * Use imagemagick or GD lib to read image properties
1469    *
1470    * @param string Absolute file path
1471    * @return mixed Hash array with image props like type, width, height or False on error
1472    */
1473   public static function imageprops($filepath)
1474   {
1475     $rcmail = rcmail::get_instance();
1476     if ($cmd = $rcmail->config->get('im_identify_path', false)) {
1477       list(, $type, $size) = explode(' ', strtolower(rcmail::exec($cmd. ' 2>/dev/null {in}', array('in' => $filepath))));
1478       if ($size)
1479         list($width, $height) = explode('x', $size);
1480     }
1481     else if (function_exists('getimagesize')) {
1482       $imsize = @getimagesize($filepath);
1483       $width = $imsize[0];
1484       $height = $imsize[1];
1485       $type = preg_replace('!image/!', '', $imsize['mime']);
1486     }
1487
1488     return $type ? array('type' => $type, 'width' => $width, 'height' => $height) : false;
1489   }
1490
1491
1492   /**
1493    * Convert an image to a given size and type using imagemagick (ensures input is an image)
1494    *
1495    * @param $p['in']  Input filename (mandatory)
1496    * @param $p['out'] Output filename (mandatory)
1497    * @param $p['size']  Width x height of resulting image, e.g. "160x60"
1498    * @param $p['type']  Output file type, e.g. "jpg"
1499    * @param $p['-opts'] Custom command line options to ImageMagick convert
1500    * @return Success of convert as true/false
1501    */
1502   public static function imageconvert($p)
1503   {
1504     $result = false;
1505     $rcmail = rcmail::get_instance();
1506     $convert  = $rcmail->config->get('im_convert_path', false);
1507     $identify = $rcmail->config->get('im_identify_path', false);
1508
1509     // imagemagick is required for this
1510     if (!$convert)
1511         return false;
1512
1513     if (!(($imagetype = @exif_imagetype($p['in'])) && ($type = image_type_to_extension($imagetype, false))))
1514       list(, $type) = explode(' ', strtolower(rcmail::exec($identify . ' 2>/dev/null {in}', $p))); # for things like eps
1515
1516     $type = strtr($type, array("jpeg" => "jpg", "tiff" => "tif", "ps" => "eps", "ept" => "eps"));
1517     $p += array('type' => $type, 'types' => "bmp,eps,gif,jp2,jpg,png,svg,tif", 'quality' => 75);
1518     $p['-opts'] = array('-resize' => $p['size'].'>') + (array)$p['-opts'];
1519
1520     if (in_array($type, explode(',', $p['types']))) # Valid type?
1521       $result = rcmail::exec($convert . ' 2>&1 -flatten -auto-orient -colorspace RGB -quality {quality} {-opts} {in} {type}:{out}', $p) === "";
1522
1523     return $result;
1524   }
1525
1526
1527   /**
1528    * Construct shell command, execute it and return output as string.
1529    * Keywords {keyword} are replaced with arguments
1530    *
1531    * @param $cmd Format string with {keywords} to be replaced
1532    * @param $values (zero, one or more arrays can be passed)
1533    * @return output of command. shell errors not detectable
1534    */
1535   public static function exec(/* $cmd, $values1 = array(), ... */)
1536   {
1537     $args = func_get_args();
1538     $cmd = array_shift($args);
1539     $values = $replacements = array();
1540
1541     // merge values into one array
1542     foreach ($args as $arg)
1543       $values += (array)$arg;
1544
1545     preg_match_all('/({(-?)([a-z]\w*)})/', $cmd, $matches, PREG_SET_ORDER);
1546     foreach ($matches as $tags) {
1547       list(, $tag, $option, $key) = $tags;
1548       $parts = array();
1549
1550       if ($option) {
1551         foreach ((array)$values["-$key"] as $key => $value) {
1552           if ($value === true || $value === false || $value === null)
1553             $parts[] = $value ? $key : "";
1554           else foreach ((array)$value as $val)
1555             $parts[] = "$key " . escapeshellarg($val);
1556         }
1557       }
1558       else {
1559         foreach ((array)$values[$key] as $value)
1560           $parts[] = escapeshellarg($value);
1561       }
1562
1563       $replacements[$tag] = join(" ", $parts);
1564     }
1565
1566     // use strtr behaviour of going through source string once
1567     $cmd = strtr($cmd, $replacements);
1568
1569     return (string)shell_exec($cmd);
1570   }
1571
1572
1573   /**
1574    * Helper method to set a cookie with the current path and host settings
1575    *
1576    * @param string Cookie name
1577    * @param string Cookie value
1578    * @param string Expiration time
1579    */
1580   public static function setcookie($name, $value, $exp = 0)
1581   {
1582     if (headers_sent())
1583       return;
1584
1585     $cookie = session_get_cookie_params();
1586
1587     setcookie($name, $value, $exp, $cookie['path'], $cookie['domain'],
1588       rcube_https_check(), true);
1589   }
1590
1591   /**
1592    * Registers action aliases for current task
1593    *
1594    * @param array $map Alias-to-filename hash array
1595    */
1596   public function register_action_map($map)
1597   {
1598     if (is_array($map)) {
1599       foreach ($map as $idx => $val) {
1600         $this->action_map[$idx] = $val;
1601       }
1602     }
1603   }
1604
1605   /**
1606    * Returns current action filename
1607    *
1608    * @param array $map Alias-to-filename hash array
1609    */
1610   public function get_action_file()
1611   {
1612     if (!empty($this->action_map[$this->action])) {
1613       return $this->action_map[$this->action];
1614     }
1615
1616     return strtr($this->action, '-', '_') . '.inc';
1617   }
1618
1619   /**
1620    * Fixes some user preferences according to namespace handling change.
1621    * Old Roundcube versions were using folder names with removed namespace prefix.
1622    * Now we need to add the prefix on servers where personal namespace has prefix.
1623    *
1624    * @param rcube_user $user User object
1625    */
1626   private function fix_namespace_settings($user)
1627   {
1628     $prefix     = $this->imap->get_namespace('prefix');
1629     $prefix_len = strlen($prefix);
1630
1631     if (!$prefix_len)
1632       return;
1633
1634     $prefs = $this->config->all();
1635     if (!empty($prefs['namespace_fixed']))
1636       return;
1637
1638     // Build namespace prefix regexp
1639     $ns     = $this->imap->get_namespace();
1640     $regexp = array();
1641
1642     foreach ($ns as $entry) {
1643       if (!empty($entry)) {
1644         foreach ($entry as $item) {
1645           if (strlen($item[0])) {
1646             $regexp[] = preg_quote($item[0], '/');
1647           }
1648         }
1649       }
1650     }
1651     $regexp = '/^('. implode('|', $regexp).')/';
1652
1653     // Fix preferences
1654     $opts = array('drafts_mbox', 'junk_mbox', 'sent_mbox', 'trash_mbox', 'archive_mbox');
1655     foreach ($opts as $opt) {
1656       if ($value = $prefs[$opt]) {
1657         if ($value != 'INBOX' && !preg_match($regexp, $value)) {
1658           $prefs[$opt] = $prefix.$value;
1659         }
1660       }
1661     }
1662
1663     if (!empty($prefs['default_imap_folders'])) {
1664       foreach ($prefs['default_imap_folders'] as $idx => $name) {
1665         if ($name != 'INBOX' && !preg_match($regexp, $name)) {
1666           $prefs['default_imap_folders'][$idx] = $prefix.$name;
1667         }
1668       }
1669     }
1670
1671     if (!empty($prefs['search_mods'])) {
1672       $folders = array();
1673       foreach ($prefs['search_mods'] as $idx => $value) {
1674         if ($idx != 'INBOX' && $idx != '*' && !preg_match($regexp, $idx)) {
1675           $idx = $prefix.$idx;
1676         }
1677         $folders[$idx] = $value;
1678       }
1679       $prefs['search_mods'] = $folders;
1680     }
1681
1682     if (!empty($prefs['message_threading'])) {
1683       $folders = array();
1684       foreach ($prefs['message_threading'] as $idx => $value) {
1685         if ($idx != 'INBOX' && !preg_match($regexp, $idx)) {
1686           $idx = $prefix.$idx;
1687         }
1688         $folders[$prefix.$idx] = $value;
1689       }
1690       $prefs['message_threading'] = $folders;
1691     }
1692
1693     if (!empty($prefs['collapsed_folders'])) {
1694       $folders     = explode('&&', $prefs['collapsed_folders']);
1695       $count       = count($folders);
1696       $folders_str = '';
1697
1698       if ($count) {
1699           $folders[0]        = substr($folders[0], 1);
1700           $folders[$count-1] = substr($folders[$count-1], 0, -1);
1701       }
1702
1703       foreach ($folders as $value) {
1704         if ($value != 'INBOX' && !preg_match($regexp, $value)) {
1705           $value = $prefix.$value;
1706         }
1707         $folders_str .= '&'.$value.'&';
1708       }
1709       $prefs['collapsed_folders'] = $folders_str;
1710     }
1711
1712     $prefs['namespace_fixed'] = true;
1713
1714     // save updated preferences and reset imap settings (default folders)
1715     $user->save_prefs($prefs);
1716     $this->set_imap_prop();
1717   }
1718
1719 }