]> git.donarmstrong.com Git - roundcube.git/blob - program/include/rcmail.php.orig
Imported Upstream version 0.7
[roundcube.git] / program / include / rcmail.php.orig
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 5527 2011-12-02 09:58:03Z alec $
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     // add to the 'books' array for shutdown function
456     $this->address_books[$id] = $contacts;
457
458     return $contacts;
459   }
460
461
462   /**
463    * Return address books list
464    *
465    * @param boolean True if the address book needs to be writeable
466    *
467    * @return array  Address books array
468    */
469   public function get_address_sources($writeable = false)
470   {
471     $abook_type = strtolower($this->config->get('address_book_type'));
472     $ldap_config = $this->config->get('ldap_public');
473     $autocomplete = (array) $this->config->get('autocomplete_addressbooks');
474     $list = array();
475
476     // We are using the DB address book
477     if ($abook_type != 'ldap') {
478       if (!isset($this->address_books['0']))
479         $this->address_books['0'] = new rcube_contacts($this->db, $this->user->ID);
480       $list['0'] = array(
481         'id'       => '0',
482         'name'     => rcube_label('personaladrbook'),
483         'groups'   => $this->address_books['0']->groups,
484         'readonly' => $this->address_books['0']->readonly,
485         'autocomplete' => in_array('sql', $autocomplete),
486         'undelete' => $this->address_books['0']->undelete && $this->config->get('undo_timeout'),
487       );
488     }
489
490     if ($ldap_config) {
491       $ldap_config = (array) $ldap_config;
492       foreach ($ldap_config as $id => $prop)
493         $list[$id] = array(
494           'id'       => $id,
495           'name'     => $prop['name'],
496           'groups'   => is_array($prop['groups']),
497           'readonly' => !$prop['writable'],
498           'hidden'   => $prop['hidden'],
499           'autocomplete' => in_array($id, $autocomplete)
500         );
501     }
502
503     $plugin = $this->plugins->exec_hook('addressbooks_list', array('sources' => $list));
504     $list = $plugin['sources'];
505
506     foreach ($list as $idx => $item) {
507       // register source for shutdown function
508       if (!is_object($this->address_books[$item['id']]))
509         $this->address_books[$item['id']] = $item;
510       // remove from list if not writeable as requested
511       if ($writeable && $item['readonly'])
512           unset($list[$idx]);
513     }
514
515     return $list;
516   }
517
518
519   /**
520    * Init output object for GUI and add common scripts.
521    * This will instantiate a rcmail_template object and set
522    * environment vars according to the current session and configuration
523    *
524    * @param boolean True if this request is loaded in a (i)frame
525    * @return rcube_template Reference to HTML output object
526    */
527   public function load_gui($framed = false)
528   {
529     // init output page
530     if (!($this->output instanceof rcube_template))
531       $this->output = new rcube_template($this->task, $framed);
532
533     // set keep-alive/check-recent interval
534     if ($this->session && ($keep_alive = $this->session->get_keep_alive())) {
535       $this->output->set_env('keep_alive', $keep_alive);
536     }
537
538     if ($framed) {
539       $this->comm_path .= '&_framed=1';
540       $this->output->set_env('framed', true);
541     }
542
543     $this->output->set_env('task', $this->task);
544     $this->output->set_env('action', $this->action);
545     $this->output->set_env('comm_path', $this->comm_path);
546     $this->output->set_charset(RCMAIL_CHARSET);
547
548     // add some basic labels to client
549     $this->output->add_label('loading', 'servererror');
550
551     return $this->output;
552   }
553
554
555   /**
556    * Create an output object for JSON responses
557    *
558    * @return rcube_json_output Reference to JSON output object
559    */
560   public function json_init()
561   {
562     if (!($this->output instanceof rcube_json_output))
563       $this->output = new rcube_json_output($this->task);
564
565     return $this->output;
566   }
567
568
569   /**
570    * Create SMTP object and connect to server
571    *
572    * @param boolean True if connection should be established
573    */
574   public function smtp_init($connect = false)
575   {
576     $this->smtp = new rcube_smtp();
577
578     if ($connect)
579       $this->smtp->connect();
580   }
581
582
583   /**
584    * Create global IMAP object and connect to server
585    *
586    * @param boolean True if connection should be established
587    * @todo Remove global $IMAP
588    */
589   public function imap_init($connect = false)
590   {
591     // already initialized
592     if (is_object($this->imap))
593       return;
594
595     $this->imap = new rcube_imap();
596     $this->imap->skip_deleted = $this->config->get('skip_deleted');
597
598     // enable caching of imap data
599     $imap_cache = $this->config->get('imap_cache');
600     $messages_cache = $this->config->get('messages_cache');
601     // for backward compatybility
602     if ($imap_cache === null && $messages_cache === null && $this->config->get('enable_caching')) {
603         $imap_cache     = 'db';
604         $messages_cache = true;
605     }
606     if ($imap_cache)
607         $this->imap->set_caching($imap_cache);
608     if ($messages_cache)
609         $this->imap->set_messages_caching(true);
610
611     // set pagesize from config
612     $this->imap->set_pagesize($this->config->get('pagesize', 50));
613
614     // Setting root and delimiter before establishing the connection
615     // can save time detecting them using NAMESPACE and LIST
616     $options = array(
617       'auth_type'   => $this->config->get('imap_auth_type', 'check'),
618       'auth_cid'    => $this->config->get('imap_auth_cid'),
619       'auth_pw'     => $this->config->get('imap_auth_pw'),
620       'debug'       => (bool) $this->config->get('imap_debug', 0),
621       'force_caps'  => (bool) $this->config->get('imap_force_caps'),
622       'timeout'     => (int) $this->config->get('imap_timeout', 0),
623     );
624
625     $this->imap->set_options($options);
626
627     // set global object for backward compatibility
628     $GLOBALS['IMAP'] = $this->imap;
629
630     $hook = $this->plugins->exec_hook('imap_init', array('fetch_headers' => $this->imap->fetch_add_headers));
631     if ($hook['fetch_headers'])
632       $this->imap->fetch_add_headers = $hook['fetch_headers'];
633
634     // support this parameter for backward compatibility but log warning
635     if ($connect) {
636       $this->imap_connect();
637       raise_error(array(
638         'code' => 800, 'type' => 'imap',
639         'file' => __FILE__, 'line' => __LINE__,
640         'message' => "rcube::imap_init(true) is deprecated, use rcube::imap_connect() instead"),
641         true, false);
642     }
643   }
644
645
646   /**
647    * Connect to IMAP server with stored session data
648    *
649    * @return bool True on success, false on error
650    */
651   public function imap_connect()
652   {
653     if (!$this->imap)
654       $this->imap_init();
655
656     if ($_SESSION['imap_host'] && !$this->imap->conn->connected()) {
657       if (!$this->imap->connect($_SESSION['imap_host'], $_SESSION['username'], $this->decrypt($_SESSION['password']), $_SESSION['imap_port'], $_SESSION['imap_ssl'])) {
658         if ($this->output)
659           $this->output->show_message($this->imap->get_error_code() == -1 ? 'imaperror' : 'sessionerror', 'error');
660       }
661       else {
662         $this->set_imap_prop();
663         return $this->imap->conn;
664       }
665     }
666
667     return false;
668   }
669
670
671   /**
672    * Create session object and start the session.
673    */
674   public function session_init()
675   {
676     // session started (Installer?)
677     if (session_id())
678       return;
679
680     $sess_name   = $this->config->get('session_name');
681     $sess_domain = $this->config->get('session_domain');
682     $lifetime    = $this->config->get('session_lifetime', 0) * 60;
683
684     // set session domain
685     if ($sess_domain) {
686       ini_set('session.cookie_domain', $sess_domain);
687     }
688     // set session garbage collecting time according to session_lifetime
689     if ($lifetime) {
690       ini_set('session.gc_maxlifetime', $lifetime * 2);
691     }
692
693     ini_set('session.cookie_secure', rcube_https_check());
694     ini_set('session.name', $sess_name ? $sess_name : 'roundcube_sessid');
695     ini_set('session.use_cookies', 1);
696     ini_set('session.use_only_cookies', 1);
697     ini_set('session.serialize_handler', 'php');
698
699     // use database for storing session data
700     $this->session = new rcube_session($this->get_dbh(), $this->config);
701
702     $this->session->register_gc_handler('rcmail_temp_gc');
703     if ($this->config->get('enable_caching'))
704       $this->session->register_gc_handler('rcmail_cache_gc');
705
706     // start PHP session (if not in CLI mode)
707     if ($_SERVER['REMOTE_ADDR'])
708       session_start();
709
710     // set initial session vars
711     if (!$_SESSION['user_id'])
712       $_SESSION['temp'] = true;
713   }
714
715
716   /**
717    * Configure session object internals
718    */
719   public function session_configure()
720   {
721     if (!$this->session)
722       return;
723
724     $lifetime = $this->config->get('session_lifetime', 0) * 60;
725
726     // set keep-alive/check-recent interval
727     if ($keep_alive = $this->config->get('keep_alive')) {
728       // be sure that it's less than session lifetime
729       if ($lifetime)
730         $keep_alive = min($keep_alive, $lifetime - 30);
731       $keep_alive = max(60, $keep_alive);
732       $this->session->set_keep_alive($keep_alive);
733     }
734
735     $this->session->set_secret($this->config->get('des_key') . $_SERVER['HTTP_USER_AGENT']);
736     $this->session->set_ip_check($this->config->get('ip_check'));
737   }
738
739
740   /**
741    * Perfom login to the IMAP server and to the webmail service.
742    * This will also create a new user entry if auto_create_user is configured.
743    *
744    * @param string IMAP user name
745    * @param string IMAP password
746    * @param string IMAP host
747    * @return boolean True on success, False on failure
748    */
749   function login($username, $pass, $host=NULL)
750   {
751     $user = NULL;
752     $config = $this->config->all();
753
754     if (!$host)
755       $host = $config['default_host'];
756
757     // Validate that selected host is in the list of configured hosts
758     if (is_array($config['default_host'])) {
759       $allowed = false;
760       foreach ($config['default_host'] as $key => $host_allowed) {
761         if (!is_numeric($key))
762           $host_allowed = $key;
763         if ($host == $host_allowed) {
764           $allowed = true;
765           break;
766         }
767       }
768       if (!$allowed)
769         return false;
770       }
771     else if (!empty($config['default_host']) && $host != rcube_parse_host($config['default_host']))
772       return false;
773
774     // parse $host URL
775     $a_host = parse_url($host);
776     if ($a_host['host']) {
777       $host = $a_host['host'];
778       $imap_ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null;
779       if (!empty($a_host['port']))
780         $imap_port = $a_host['port'];
781       else if ($imap_ssl && $imap_ssl != 'tls' && (!$config['default_port'] || $config['default_port'] == 143))
782         $imap_port = 993;
783     }
784
785     $imap_port = $imap_port ? $imap_port : $config['default_port'];
786
787     /* Modify username with domain if required
788        Inspired by Marco <P0L0_notspam_binware.org>
789     */
790     // Check if we need to add domain
791     if (!empty($config['username_domain']) && strpos($username, '@') === false) {
792       if (is_array($config['username_domain']) && isset($config['username_domain'][$host]))
793         $username .= '@'.rcube_parse_host($config['username_domain'][$host], $host);
794       else if (is_string($config['username_domain']))
795         $username .= '@'.rcube_parse_host($config['username_domain'], $host);
796     }
797
798     // Convert username to lowercase. If IMAP backend
799     // is case-insensitive we need to store always the same username (#1487113)
800     if ($config['login_lc']) {
801       $username = mb_strtolower($username);
802     }
803
804     // try to resolve email address from virtuser table
805     if (strpos($username, '@') && ($virtuser = rcube_user::email2user($username))) {
806       $username = $virtuser;
807     }
808
809     // Here we need IDNA ASCII
810     // Only rcube_contacts class is using domain names in Unicode
811     $host = rcube_idn_to_ascii($host);
812     if (strpos($username, '@')) {
813       // lowercase domain name
814       list($local, $domain) = explode('@', $username);
815       $username = $local . '@' . mb_strtolower($domain);
816       $username = rcube_idn_to_ascii($username);
817     }
818
819     // user already registered -> overwrite username
820     if ($user = rcube_user::query($username, $host))
821       $username = $user->data['username'];
822
823     if (!$this->imap)
824       $this->imap_init();
825
826     // try IMAP login
827     if (!($imap_login = $this->imap->connect($host, $username, $pass, $imap_port, $imap_ssl))) {
828       // try with lowercase
829       $username_lc = mb_strtolower($username);
830       if ($username_lc != $username) {
831         // try to find user record again -> overwrite username
832         if (!$user && ($user = rcube_user::query($username_lc, $host)))
833           $username_lc = $user->data['username'];
834
835         if ($imap_login = $this->imap->connect($host, $username_lc, $pass, $imap_port, $imap_ssl))
836           $username = $username_lc;
837       }
838     }
839
840     // exit if IMAP login failed
841     if (!$imap_login)
842       return false;
843
844     // user already registered -> update user's record
845     if (is_object($user)) {
846       // update last login timestamp
847       $user->touch();
848     }
849     // create new system user
850     else if ($config['auto_create_user']) {
851       if ($created = rcube_user::create($username, $host)) {
852         $user = $created;
853       }
854       else {
855         raise_error(array(
856           'code' => 620, 'type' => 'php',
857           'file' => __FILE__, 'line' => __LINE__,
858           'message' => "Failed to create a user record. Maybe aborted by a plugin?"
859           ), true, false);
860       }
861     }
862     else {
863       raise_error(array(
864         'code' => 621, 'type' => 'php',
865         'file' => __FILE__, 'line' => __LINE__,
866         'message' => "Access denied for new user $username. 'auto_create_user' is disabled"
867         ), true, false);
868     }
869
870     // login succeeded
871     if (is_object($user) && $user->ID) {
872       // Configure environment
873       $this->set_user($user);
874       $this->set_imap_prop();
875       $this->session_configure();
876
877       // fix some old settings according to namespace prefix
878       $this->fix_namespace_settings($user);
879
880       // create default folders on first login
881       if ($config['create_default_folders'] && (!empty($created) || empty($user->data['last_login']))) {
882         $this->imap->create_default_folders();
883       }
884
885       // set session vars
886       $_SESSION['user_id']   = $user->ID;
887       $_SESSION['username']  = $user->data['username'];
888       $_SESSION['imap_host'] = $host;
889       $_SESSION['imap_port'] = $imap_port;
890       $_SESSION['imap_ssl']  = $imap_ssl;
891       $_SESSION['password']  = $this->encrypt($pass);
892       $_SESSION['login_time'] = mktime();
893
894       if (isset($_REQUEST['_timezone']) && $_REQUEST['_timezone'] != '_default_')
895         $_SESSION['timezone'] = floatval($_REQUEST['_timezone']);
896       if (isset($_REQUEST['_dstactive']) && $_REQUEST['_dstactive'] != '_default_')
897         $_SESSION['dst_active'] = intval($_REQUEST['_dstactive']);
898
899       // force reloading complete list of subscribed mailboxes
900       $this->imap->clear_cache('mailboxes', true);
901
902       return true;
903     }
904
905     return false;
906   }
907
908
909   /**
910    * Set root dir and last stored mailbox
911    * This must be done AFTER connecting to the server!
912    */
913   public function set_imap_prop()
914   {
915     $this->imap->set_charset($this->config->get('default_charset', RCMAIL_CHARSET));
916
917     if ($default_folders = $this->config->get('default_imap_folders')) {
918       $this->imap->set_default_mailboxes($default_folders);
919     }
920     if (isset($_SESSION['mbox'])) {
921       $this->imap->set_mailbox($_SESSION['mbox']);
922     }
923     if (isset($_SESSION['page'])) {
924       $this->imap->set_page($_SESSION['page']);
925     }
926   }
927
928
929   /**
930    * Auto-select IMAP host based on the posted login information
931    *
932    * @return string Selected IMAP host
933    */
934   public function autoselect_host()
935   {
936     $default_host = $this->config->get('default_host');
937     $host = null;
938
939     if (is_array($default_host)) {
940       $post_host = get_input_value('_host', RCUBE_INPUT_POST);
941
942       // direct match in default_host array
943       if ($default_host[$post_host] || in_array($post_host, array_values($default_host))) {
944         $host = $post_host;
945       }
946
947       // try to select host by mail domain
948       list($user, $domain) = explode('@', get_input_value('_user', RCUBE_INPUT_POST));
949       if (!empty($domain)) {
950         foreach ($default_host as $imap_host => $mail_domains) {
951           if (is_array($mail_domains) && in_array($domain, $mail_domains)) {
952             $host = $imap_host;
953             break;
954           }
955         }
956       }
957
958       // take the first entry if $host is still an array
959       if (empty($host)) {
960         $host = array_shift($default_host);
961       }
962     }
963     else if (empty($default_host)) {
964       $host = get_input_value('_host', RCUBE_INPUT_POST);
965     }
966     else
967       $host = rcube_parse_host($default_host);
968
969     return $host;
970   }
971
972
973   /**
974    * Get localized text in the desired language
975    *
976    * @param mixed   $attrib  Named parameters array or label name
977    * @param string  $domain  Label domain (plugin) name
978    *
979    * @return string Localized text
980    */
981   public function gettext($attrib, $domain=null)
982   {
983     // load localization files if not done yet
984     if (empty($this->texts))
985       $this->load_language();
986
987     // extract attributes
988     if (is_string($attrib))
989       $attrib = array('name' => $attrib);
990
991     $nr = is_numeric($attrib['nr']) ? $attrib['nr'] : 1;
992     $name = $attrib['name'] ? $attrib['name'] : '';
993
994     // attrib contain text values: use them from now
995     if (($setval = $attrib[strtolower($_SESSION['language'])]) || ($setval = $attrib['en_us']))
996         $this->texts[$name] = $setval;
997
998     // check for text with domain
999     if ($domain && ($text_item = $this->texts[$domain.'.'.$name]))
1000       ;
1001     // text does not exist
1002     else if (!($text_item = $this->texts[$name])) {
1003       return "[$name]";
1004     }
1005
1006     // make text item array
1007     $a_text_item = is_array($text_item) ? $text_item : array('single' => $text_item);
1008
1009     // decide which text to use
1010     if ($nr == 1) {
1011       $text = $a_text_item['single'];
1012     }
1013     else if ($nr > 0) {
1014       $text = $a_text_item['multiple'];
1015     }
1016     else if ($nr == 0) {
1017       if ($a_text_item['none'])
1018         $text = $a_text_item['none'];
1019       else if ($a_text_item['single'])
1020         $text = $a_text_item['single'];
1021       else if ($a_text_item['multiple'])
1022         $text = $a_text_item['multiple'];
1023     }
1024
1025     // default text is single
1026     if ($text == '') {
1027       $text = $a_text_item['single'];
1028     }
1029
1030     // replace vars in text
1031     if (is_array($attrib['vars'])) {
1032       foreach ($attrib['vars'] as $var_key => $var_value)
1033         $text = str_replace($var_key[0]!='$' ? '$'.$var_key : $var_key, $var_value, $text);
1034     }
1035
1036     // format output
1037     if (($attrib['uppercase'] && strtolower($attrib['uppercase']=='first')) || $attrib['ucfirst'])
1038       return ucfirst($text);
1039     else if ($attrib['uppercase'])
1040       return mb_strtoupper($text);
1041     else if ($attrib['lowercase'])
1042       return mb_strtolower($text);
1043
1044     return $text;
1045   }
1046
1047
1048   /**
1049    * Check if the given text label exists
1050    *
1051    * @param string  $name       Label name
1052    * @param string  $domain     Label domain (plugin) name or '*' for all domains
1053    * @param string  $ref_domain Sets domain name if label is found
1054    *
1055    * @return boolean True if text exists (either in the current language or in en_US)
1056    */
1057   public function text_exists($name, $domain = null, &$ref_domain = null)
1058   {
1059     // load localization files if not done yet
1060     if (empty($this->texts))
1061       $this->load_language();
1062
1063     if (isset($this->texts[$name])) {
1064         $ref_domain = '';
1065         return true;
1066     }
1067
1068     // any of loaded domains (plugins)
1069     if ($domain == '*') {
1070       foreach ($this->plugins->loaded_plugins() as $domain)
1071         if (isset($this->texts[$domain.'.'.$name])) {
1072           $ref_domain = $domain;
1073           return true;
1074         }
1075     }
1076     // specified domain
1077     else if ($domain) {
1078       $ref_domain = $domain;
1079       return isset($this->texts[$domain.'.'.$name]);
1080     }
1081
1082     return false;
1083   }
1084
1085   /**
1086    * Load a localization package
1087    *
1088    * @param string Language ID
1089    */
1090   public function load_language($lang = null, $add = array())
1091   {
1092     $lang = $this->language_prop(($lang ? $lang : $_SESSION['language']));
1093
1094     // load localized texts
1095     if (empty($this->texts) || $lang != $_SESSION['language']) {
1096       $this->texts = array();
1097
1098       // handle empty lines after closing PHP tag in localization files
1099       ob_start();
1100
1101       // get english labels (these should be complete)
1102       @include(INSTALL_PATH . 'program/localization/en_US/labels.inc');
1103       @include(INSTALL_PATH . 'program/localization/en_US/messages.inc');
1104
1105       if (is_array($labels))
1106         $this->texts = $labels;
1107       if (is_array($messages))
1108         $this->texts = array_merge($this->texts, $messages);
1109
1110       // include user language files
1111       if ($lang != 'en' && is_dir(INSTALL_PATH . 'program/localization/' . $lang)) {
1112         include_once(INSTALL_PATH . 'program/localization/' . $lang . '/labels.inc');
1113         include_once(INSTALL_PATH . 'program/localization/' . $lang . '/messages.inc');
1114
1115         if (is_array($labels))
1116           $this->texts = array_merge($this->texts, $labels);
1117         if (is_array($messages))
1118           $this->texts = array_merge($this->texts, $messages);
1119       }
1120
1121       ob_end_clean();
1122
1123       $_SESSION['language'] = $lang;
1124     }
1125
1126     // append additional texts (from plugin)
1127     if (is_array($add) && !empty($add))
1128       $this->texts += $add;
1129   }
1130
1131
1132   /**
1133    * Read directory program/localization and return a list of available languages
1134    *
1135    * @return array List of available localizations
1136    */
1137   public function list_languages()
1138   {
1139     static $sa_languages = array();
1140
1141     if (!sizeof($sa_languages)) {
1142       @include(INSTALL_PATH . 'program/localization/index.inc');
1143
1144       if ($dh = @opendir(INSTALL_PATH . 'program/localization')) {
1145         while (($name = readdir($dh)) !== false) {
1146           if ($name[0] == '.' || !is_dir(INSTALL_PATH . 'program/localization/' . $name))
1147             continue;
1148
1149           if ($label = $rcube_languages[$name])
1150             $sa_languages[$name] = $label;
1151         }
1152         closedir($dh);
1153       }
1154     }
1155
1156     return $sa_languages;
1157   }
1158
1159
1160   /**
1161    * Destroy session data and remove cookie
1162    */
1163   public function kill_session()
1164   {
1165     $this->plugins->exec_hook('session_destroy');
1166
1167     $this->session->kill();
1168     $_SESSION = array('language' => $this->user->language, 'temp' => true);
1169     $this->user->reset();
1170   }
1171
1172
1173   /**
1174    * Do server side actions on logout
1175    */
1176   public function logout_actions()
1177   {
1178     $config = $this->config->all();
1179
1180     // on logout action we're not connected to imap server
1181     if (($config['logout_purge'] && !empty($config['trash_mbox'])) || $config['logout_expunge']) {
1182       if (!$this->session->check_auth())
1183         return;
1184
1185       $this->imap_connect();
1186     }
1187
1188     if ($config['logout_purge'] && !empty($config['trash_mbox'])) {
1189       $this->imap->clear_mailbox($config['trash_mbox']);
1190     }
1191
1192     if ($config['logout_expunge']) {
1193       $this->imap->expunge('INBOX');
1194     }
1195
1196     // Try to save unsaved user preferences
1197     if (!empty($_SESSION['preferences'])) {
1198       $this->user->save_prefs(unserialize($_SESSION['preferences']));
1199     }
1200   }
1201
1202
1203   /**
1204    * Function to be executed in script shutdown
1205    * Registered with register_shutdown_function()
1206    */
1207   public function shutdown()
1208   {
1209     foreach ($this->shutdown_functions as $function)
1210       call_user_func($function);
1211
1212     if (is_object($this->smtp))
1213       $this->smtp->disconnect();
1214
1215     foreach ($this->address_books as $book) {
1216       if (is_object($book) && is_a($book, 'rcube_addressbook'))
1217         $book->close();
1218     }
1219
1220     foreach ($this->caches as $cache) {
1221         if (is_object($cache))
1222             $cache->close();
1223     }
1224
1225     if (is_object($this->imap))
1226       $this->imap->close();
1227
1228     // before closing the database connection, write session data
1229     if ($_SERVER['REMOTE_ADDR'] && is_object($this->session)) {
1230       session_write_close();
1231     }
1232
1233     // write performance stats to logs/console
1234     if ($this->config->get('devel_mode')) {
1235       if (function_exists('memory_get_usage'))
1236         $mem = show_bytes(memory_get_usage());
1237       if (function_exists('memory_get_peak_usage'))
1238         $mem .= '/'.show_bytes(memory_get_peak_usage());
1239
1240       $log = $this->task . ($this->action ? '/'.$this->action : '') . ($mem ? " [$mem]" : '');
1241       if (defined('RCMAIL_START'))
1242         rcube_print_time(RCMAIL_START, $log);
1243       else
1244         console($log);
1245     }
1246   }
1247
1248
1249   /**
1250    * Registers shutdown function to be executed on shutdown.
1251    * The functions will be executed before destroying any
1252    * objects like smtp, imap, session, etc.
1253    *
1254    * @param callback Function callback
1255    */
1256   public function add_shutdown_function($function)
1257   {
1258     $this->shutdown_functions[] = $function;
1259   }
1260
1261
1262   /**
1263    * Generate a unique token to be used in a form request
1264    *
1265    * @return string The request token
1266    */
1267   public function get_request_token()
1268   {
1269     $sess_id = $_COOKIE[ini_get('session.name')];
1270     if (!$sess_id) $sess_id = session_id();
1271     $plugin = $this->plugins->exec_hook('request_token', array('value' => md5('RT' . $this->user->ID . $this->config->get('des_key') . $sess_id)));
1272     return $plugin['value'];
1273   }
1274
1275
1276   /**
1277    * Check if the current request contains a valid token
1278    *
1279    * @param int Request method
1280    * @return boolean True if request token is valid false if not
1281    */
1282   public function check_request($mode = RCUBE_INPUT_POST)
1283   {
1284     $token = get_input_value('_token', $mode);
1285     $sess_id = $_COOKIE[ini_get('session.name')];
1286     return !empty($sess_id) && $token == $this->get_request_token();
1287   }
1288
1289
1290   /**
1291    * Create unique authorization hash
1292    *
1293    * @param string Session ID
1294    * @param int Timestamp
1295    * @return string The generated auth hash
1296    */
1297   private function get_auth_hash($sess_id, $ts)
1298   {
1299     $auth_string = sprintf('rcmail*sess%sR%s*Chk:%s;%s',
1300       $sess_id,
1301       $ts,
1302       $this->config->get('ip_check') ? $_SERVER['REMOTE_ADDR'] : '***.***.***.***',
1303       $_SERVER['HTTP_USER_AGENT']);
1304
1305     if (function_exists('sha1'))
1306       return sha1($auth_string);
1307     else
1308       return md5($auth_string);
1309   }
1310
1311
1312   /**
1313    * Encrypt using 3DES
1314    *
1315    * @param string $clear clear text input
1316    * @param string $key encryption key to retrieve from the configuration, defaults to 'des_key'
1317    * @param boolean $base64 whether or not to base64_encode() the result before returning
1318    *
1319    * @return string encrypted text
1320    */
1321   public function encrypt($clear, $key = 'des_key', $base64 = true)
1322   {
1323     if (!$clear)
1324       return '';
1325     /*-
1326      * Add a single canary byte to the end of the clear text, which
1327      * will help find out how much of padding will need to be removed
1328      * upon decryption; see http://php.net/mcrypt_generic#68082
1329      */
1330     $clear = pack("a*H2", $clear, "80");
1331
1332     if (function_exists('mcrypt_module_open') &&
1333         ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, "")))
1334     {
1335       $iv = $this->create_iv(mcrypt_enc_get_iv_size($td));
1336       mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv);
1337       $cipher = $iv . mcrypt_generic($td, $clear);
1338       mcrypt_generic_deinit($td);
1339       mcrypt_module_close($td);
1340     }
1341     else {
1342       @include_once 'des.inc';
1343
1344       if (function_exists('des')) {
1345         $des_iv_size = 8;
1346         $iv = $this->create_iv($des_iv_size);
1347         $cipher = $iv . des($this->config->get_crypto_key($key), $clear, 1, 1, $iv);
1348       }
1349       else {
1350         raise_error(array(
1351           'code' => 500, 'type' => 'php',
1352           'file' => __FILE__, 'line' => __LINE__,
1353           'message' => "Could not perform encryption; make sure Mcrypt is installed or lib/des.inc is available"
1354         ), true, true);
1355       }
1356     }
1357
1358     return $base64 ? base64_encode($cipher) : $cipher;
1359   }
1360
1361   /**
1362    * Decrypt 3DES-encrypted string
1363    *
1364    * @param string $cipher encrypted text
1365    * @param string $key encryption key to retrieve from the configuration, defaults to 'des_key'
1366    * @param boolean $base64 whether or not input is base64-encoded
1367    *
1368    * @return string decrypted text
1369    */
1370   public function decrypt($cipher, $key = 'des_key', $base64 = true)
1371   {
1372     if (!$cipher)
1373       return '';
1374
1375     $cipher = $base64 ? base64_decode($cipher) : $cipher;
1376
1377     if (function_exists('mcrypt_module_open') &&
1378         ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, "")))
1379     {
1380       $iv_size = mcrypt_enc_get_iv_size($td);
1381       $iv = substr($cipher, 0, $iv_size);
1382
1383       // session corruption? (#1485970)
1384       if (strlen($iv) < $iv_size)
1385         return '';
1386
1387       $cipher = substr($cipher, $iv_size);
1388       mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv);
1389       $clear = mdecrypt_generic($td, $cipher);
1390       mcrypt_generic_deinit($td);
1391       mcrypt_module_close($td);
1392     }
1393     else {
1394       @include_once 'des.inc';
1395
1396       if (function_exists('des')) {
1397         $des_iv_size = 8;
1398         $iv = substr($cipher, 0, $des_iv_size);
1399         $cipher = substr($cipher, $des_iv_size);
1400         $clear = des($this->config->get_crypto_key($key), $cipher, 0, 1, $iv);
1401       }
1402       else {
1403         raise_error(array(
1404           'code' => 500, 'type' => 'php',
1405           'file' => __FILE__, 'line' => __LINE__,
1406           'message' => "Could not perform decryption; make sure Mcrypt is installed or lib/des.inc is available"
1407         ), true, true);
1408       }
1409     }
1410
1411     /*-
1412      * Trim PHP's padding and the canary byte; see note in
1413      * rcmail::encrypt() and http://php.net/mcrypt_generic#68082
1414      */
1415     $clear = substr(rtrim($clear, "\0"), 0, -1);
1416
1417     return $clear;
1418   }
1419
1420   /**
1421    * Generates encryption initialization vector (IV)
1422    *
1423    * @param int Vector size
1424    * @return string Vector string
1425    */
1426   private function create_iv($size)
1427   {
1428     // mcrypt_create_iv() can be slow when system lacks entrophy
1429     // we'll generate IV vector manually
1430     $iv = '';
1431     for ($i = 0; $i < $size; $i++)
1432         $iv .= chr(mt_rand(0, 255));
1433     return $iv;
1434   }
1435
1436   /**
1437    * Build a valid URL to this instance of Roundcube
1438    *
1439    * @param mixed Either a string with the action or url parameters as key-value pairs
1440    * @return string Valid application URL
1441    */
1442   public function url($p)
1443   {
1444     if (!is_array($p))
1445       $p = array('_action' => @func_get_arg(0));
1446
1447     $task = $p['_task'] ? $p['_task'] : ($p['task'] ? $p['task'] : $this->task);
1448     $p['_task'] = $task;
1449     unset($p['task']);
1450
1451     $url = './';
1452     $delm = '?';
1453     foreach (array_reverse($p) as $key => $val) {
1454       if ($val !== '') {
1455         $par = $key[0] == '_' ? $key : '_'.$key;
1456         $url .= $delm.urlencode($par).'='.urlencode($val);
1457         $delm = '&';
1458       }
1459     }
1460     return $url;
1461   }
1462
1463
1464   /**
1465    * Use imagemagick or GD lib to read image properties
1466    *
1467    * @param string Absolute file path
1468    * @return mixed Hash array with image props like type, width, height or False on error
1469    */
1470   public static function imageprops($filepath)
1471   {
1472     $rcmail = rcmail::get_instance();
1473     if ($cmd = $rcmail->config->get('im_identify_path', false)) {
1474       list(, $type, $size) = explode(' ', strtolower(rcmail::exec($cmd. ' 2>/dev/null {in}', array('in' => $filepath))));
1475       if ($size)
1476         list($width, $height) = explode('x', $size);
1477     }
1478     else if (function_exists('getimagesize')) {
1479       $imsize = @getimagesize($filepath);
1480       $width = $imsize[0];
1481       $height = $imsize[1];
1482       $type = preg_replace('!image/!', '', $imsize['mime']);
1483     }
1484
1485     return $type ? array('type' => $type, 'width' => $width, 'height' => $height) : false;
1486   }
1487
1488
1489   /**
1490    * Convert an image to a given size and type using imagemagick (ensures input is an image)
1491    *
1492    * @param $p['in']  Input filename (mandatory)
1493    * @param $p['out'] Output filename (mandatory)
1494    * @param $p['size']  Width x height of resulting image, e.g. "160x60"
1495    * @param $p['type']  Output file type, e.g. "jpg"
1496    * @param $p['-opts'] Custom command line options to ImageMagick convert
1497    * @return Success of convert as true/false
1498    */
1499   public static function imageconvert($p)
1500   {
1501     $result = false;
1502     $rcmail = rcmail::get_instance();
1503     $convert  = $rcmail->config->get('im_convert_path', false);
1504     $identify = $rcmail->config->get('im_identify_path', false);
1505
1506     // imagemagick is required for this
1507     if (!$convert)
1508         return false;
1509
1510     if (!(($imagetype = @exif_imagetype($p['in'])) && ($type = image_type_to_extension($imagetype, false))))
1511       list(, $type) = explode(' ', strtolower(rcmail::exec($identify . ' 2>/dev/null {in}', $p))); # for things like eps
1512
1513     $type = strtr($type, array("jpeg" => "jpg", "tiff" => "tif", "ps" => "eps", "ept" => "eps"));
1514     $p += array('type' => $type, 'types' => "bmp,eps,gif,jp2,jpg,png,svg,tif", 'quality' => 75);
1515     $p['-opts'] = array('-resize' => $p['size'].'>') + (array)$p['-opts'];
1516
1517     if (in_array($type, explode(',', $p['types']))) # Valid type?
1518       $result = rcmail::exec($convert . ' 2>&1 -flatten -auto-orient -colorspace RGB -quality {quality} {-opts} {in} {type}:{out}', $p) === "";
1519
1520     return $result;
1521   }
1522
1523
1524   /**
1525    * Construct shell command, execute it and return output as string.
1526    * Keywords {keyword} are replaced with arguments
1527    *
1528    * @param $cmd Format string with {keywords} to be replaced
1529    * @param $values (zero, one or more arrays can be passed)
1530    * @return output of command. shell errors not detectable
1531    */
1532   public static function exec(/* $cmd, $values1 = array(), ... */)
1533   {
1534     $args = func_get_args();
1535     $cmd = array_shift($args);
1536     $values = $replacements = array();
1537
1538     // merge values into one array
1539     foreach ($args as $arg)
1540       $values += (array)$arg;
1541
1542     preg_match_all('/({(-?)([a-z]\w*)})/', $cmd, $matches, PREG_SET_ORDER);
1543     foreach ($matches as $tags) {
1544       list(, $tag, $option, $key) = $tags;
1545       $parts = array();
1546
1547       if ($option) {
1548         foreach ((array)$values["-$key"] as $key => $value) {
1549           if ($value === true || $value === false || $value === null)
1550             $parts[] = $value ? $key : "";
1551           else foreach ((array)$value as $val)
1552             $parts[] = "$key " . escapeshellarg($val);
1553         }
1554       }
1555       else {
1556         foreach ((array)$values[$key] as $value)
1557           $parts[] = escapeshellarg($value);
1558       }
1559
1560       $replacements[$tag] = join(" ", $parts);
1561     }
1562
1563     // use strtr behaviour of going through source string once
1564     $cmd = strtr($cmd, $replacements);
1565
1566     return (string)shell_exec($cmd);
1567   }
1568
1569
1570   /**
1571    * Helper method to set a cookie with the current path and host settings
1572    *
1573    * @param string Cookie name
1574    * @param string Cookie value
1575    * @param string Expiration time
1576    */
1577   public static function setcookie($name, $value, $exp = 0)
1578   {
1579     if (headers_sent())
1580       return;
1581
1582     $cookie = session_get_cookie_params();
1583
1584     setcookie($name, $value, $exp, $cookie['path'], $cookie['domain'],
1585       rcube_https_check(), true);
1586   }
1587
1588   /**
1589    * Registers action aliases for current task
1590    *
1591    * @param array $map Alias-to-filename hash array
1592    */
1593   public function register_action_map($map)
1594   {
1595     if (is_array($map)) {
1596       foreach ($map as $idx => $val) {
1597         $this->action_map[$idx] = $val;
1598       }
1599     }
1600   }
1601
1602   /**
1603    * Returns current action filename
1604    *
1605    * @param array $map Alias-to-filename hash array
1606    */
1607   public function get_action_file()
1608   {
1609     if (!empty($this->action_map[$this->action])) {
1610       return $this->action_map[$this->action];
1611     }
1612
1613     return strtr($this->action, '-', '_') . '.inc';
1614   }
1615
1616   /**
1617    * Fixes some user preferences according to namespace handling change.
1618    * Old Roundcube versions were using folder names with removed namespace prefix.
1619    * Now we need to add the prefix on servers where personal namespace has prefix.
1620    *
1621    * @param rcube_user $user User object
1622    */
1623   private function fix_namespace_settings($user)
1624   {
1625     $prefix     = $this->imap->get_namespace('prefix');
1626     $prefix_len = strlen($prefix);
1627
1628     if (!$prefix_len)
1629       return;
1630
1631     $prefs = $this->config->all();
1632     if (!empty($prefs['namespace_fixed']))
1633       return;
1634
1635     // Build namespace prefix regexp
1636     $ns     = $this->imap->get_namespace();
1637     $regexp = array();
1638
1639     foreach ($ns as $entry) {
1640       if (!empty($entry)) {
1641         foreach ($entry as $item) {
1642           if (strlen($item[0])) {
1643             $regexp[] = preg_quote($item[0], '/');
1644           }
1645         }
1646       }
1647     }
1648     $regexp = '/^('. implode('|', $regexp).')/';
1649
1650     // Fix preferences
1651     $opts = array('drafts_mbox', 'junk_mbox', 'sent_mbox', 'trash_mbox', 'archive_mbox');
1652     foreach ($opts as $opt) {
1653       if ($value = $prefs[$opt]) {
1654         if ($value != 'INBOX' && !preg_match($regexp, $value)) {
1655           $prefs[$opt] = $prefix.$value;
1656         }
1657       }
1658     }
1659
1660     if (!empty($prefs['default_imap_folders'])) {
1661       foreach ($prefs['default_imap_folders'] as $idx => $name) {
1662         if ($name != 'INBOX' && !preg_match($regexp, $name)) {
1663           $prefs['default_imap_folders'][$idx] = $prefix.$name;
1664         }
1665       }
1666     }
1667
1668     if (!empty($prefs['search_mods'])) {
1669       $folders = array();
1670       foreach ($prefs['search_mods'] as $idx => $value) {
1671         if ($idx != 'INBOX' && $idx != '*' && !preg_match($regexp, $idx)) {
1672           $idx = $prefix.$idx;
1673         }
1674         $folders[$idx] = $value;
1675       }
1676       $prefs['search_mods'] = $folders;
1677     }
1678
1679     if (!empty($prefs['message_threading'])) {
1680       $folders = array();
1681       foreach ($prefs['message_threading'] as $idx => $value) {
1682         if ($idx != 'INBOX' && !preg_match($regexp, $idx)) {
1683           $idx = $prefix.$idx;
1684         }
1685         $folders[$prefix.$idx] = $value;
1686       }
1687       $prefs['message_threading'] = $folders;
1688     }
1689
1690     if (!empty($prefs['collapsed_folders'])) {
1691       $folders     = explode('&&', $prefs['collapsed_folders']);
1692       $count       = count($folders);
1693       $folders_str = '';
1694
1695       if ($count) {
1696           $folders[0]        = substr($folders[0], 1);
1697           $folders[$count-1] = substr($folders[$count-1], 0, -1);
1698       }
1699
1700       foreach ($folders as $value) {
1701         if ($value != 'INBOX' && !preg_match($regexp, $value)) {
1702           $value = $prefix.$value;
1703         }
1704         $folders_str .= '&'.$value.'&';
1705       }
1706       $prefs['collapsed_folders'] = $folders_str;
1707     }
1708
1709     $prefs['namespace_fixed'] = true;
1710
1711     // save updated preferences and reset imap settings (default folders)
1712     $user->save_prefs($prefs);
1713     $this->set_imap_prop();
1714   }
1715
1716 }