Aleksander Machniak
2016-05-18 930a3ceac0aeb42474fb0a6129517aaa15b794b4
commit | author | age
0c2596 1 <?php
A 2
a95874 3 /**
0c2596 4  +-----------------------------------------------------------------------+
A 5  | This file is part of the Roundcube Webmail client                     |
ce2019 6  | Copyright (C) 2008-2014, The Roundcube Dev Team                       |
TB 7  | Copyright (C) 2011-2014, Kolab Systems AG                             |
0c2596 8  |                                                                       |
A 9  | Licensed under the GNU General Public License version 3 or            |
10  | any later version with exceptions for skins & plugins.                |
11  | See the README file for a full license statement.                     |
12  |                                                                       |
13  | PURPOSE:                                                              |
14  |   Framework base class providing core functions and holding           |
15  |   instances of all 'global' objects like db- and storage-connections  |
16  +-----------------------------------------------------------------------+
17  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
18  +-----------------------------------------------------------------------+
19 */
20
21 /**
22  * Base class of the Roundcube Framework
23  * implemented as singleton
24  *
315418 25  * @package    Framework
AM 26  * @subpackage Core
0c2596 27  */
A 28 class rcube
29 {
681ba6 30     // Init options
AM 31     const INIT_WITH_DB      = 1;
315418 32     const INIT_WITH_PLUGINS = 2;
681ba6 33
AM 34     // Request status
35     const REQUEST_VALID       = 0;
36     const REQUEST_ERROR_URL   = 1;
37     const REQUEST_ERROR_TOKEN = 2;
0c2596 38
11d5e7 39     const DEBUG_LINE_LENGTH = 4096;
AM 40
315418 41     /**
AM 42      * Singleton instace of rcube
43      *
996af3 44      * @var rcube
315418 45      */
AM 46     static protected $instance;
0c2596 47
315418 48     /**
AM 49      * Stores instance of rcube_config.
50      *
51      * @var rcube_config
52      */
53     public $config;
0c2596 54
315418 55     /**
AM 56      * Instace of database class.
57      *
58      * @var rcube_db
59      */
60     public $db;
0c2596 61
315418 62     /**
AM 63      * Instace of Memcache class.
64      *
65      * @var Memcache
66      */
67     public $memcache;
0c2596 68
315418 69    /**
AM 70      * Instace of rcube_session class.
71      *
72      * @var rcube_session
73      */
74     public $session;
0c2596 75
315418 76     /**
AM 77      * Instance of rcube_smtp class.
78      *
79      * @var rcube_smtp
80      */
81     public $smtp;
0c2596 82
315418 83     /**
AM 84      * Instance of rcube_storage class.
85      *
86      * @var rcube_storage
87      */
88     public $storage;
0c2596 89
315418 90     /**
AM 91      * Instance of rcube_output class.
92      *
93      * @var rcube_output
94      */
95     public $output;
0c2596 96
315418 97     /**
AM 98      * Instance of rcube_plugin_api.
99      *
100      * @var rcube_plugin_api
101      */
102     public $plugins;
ce2019 103
TB 104     /**
105      * Instance of rcube_user class.
106      *
107      * @var rcube_user
108      */
109     public $user;
0c2596 110
681ba6 111     /**
AM 112      * Request status
113      *
114      * @var int
115      */
116     public $request_status = 0;
0c2596 117
315418 118     /* private/protected vars */
AM 119     protected $texts;
120     protected $caches = array();
121     protected $shutdown_functions = array();
0c2596 122
A 123
315418 124     /**
AM 125      * This implements the 'singleton' design pattern
126      *
1b39d9 127      * @param integer $mode Options to initialize with this instance. See rcube::INIT_WITH_* constants
AM 128      * @param string  $env  Environment name to run (e.g. live, dev, test)
315418 129      *
AM 130      * @return rcube The one and only instance
131      */
deb2b8 132     static function get_instance($mode = 0, $env = '')
315418 133     {
AM 134         if (!self::$instance) {
deb2b8 135             self::$instance = new rcube($env);
315418 136             self::$instance->init($mode);
71ee56 137         }
AM 138
315418 139         return self::$instance;
0c2596 140     }
A 141
315418 142     /**
AM 143      * Private constructor
144      */
deb2b8 145     protected function __construct($env = '')
315418 146     {
AM 147         // load configuration
deb2b8 148         $this->config  = new rcube_config($env);
315418 149         $this->plugins = new rcube_dummy_plugin_api;
0c2596 150
315418 151         register_shutdown_function(array($this, 'shutdown'));
0c2596 152     }
A 153
315418 154     /**
AM 155      * Initial startup function
156      */
157     protected function init($mode = 0)
158     {
159         // initialize syslog
160         if ($this->config->get('log_driver') == 'syslog') {
161             $syslog_id       = $this->config->get('syslog_id', 'roundcube');
162             $syslog_facility = $this->config->get('syslog_facility', LOG_USER);
163             openlog($syslog_id, LOG_ODELAY, $syslog_facility);
164         }
0c2596 165
315418 166         // connect to database
AM 167         if ($mode & self::INIT_WITH_DB) {
168             $this->get_dbh();
169         }
0c2596 170
315418 171         // create plugin API and load plugins
AM 172         if ($mode & self::INIT_WITH_PLUGINS) {
173             $this->plugins = rcube_plugin_api::get_instance();
174         }
0c2596 175     }
A 176
315418 177     /**
AM 178      * Get the current database connection
179      *
180      * @return rcube_db Database object
181      */
182     public function get_dbh()
183     {
184         if (!$this->db) {
6d5a1b 185             $this->db = rcube_db::factory(
AM 186                 $this->config->get('db_dsnw'),
187                 $this->config->get('db_dsnr'),
188                 $this->config->get('db_persistent')
189             );
190
191             $this->db->set_debug((bool)$this->config->get('sql_debug'));
315418 192         }
0c2596 193
315418 194         return $this->db;
0c2596 195     }
A 196
315418 197     /**
AM 198      * Get global handle for memcache access
199      *
200      * @return object Memcache
201      */
202     public function get_memcache()
203     {
204         if (!isset($this->memcache)) {
205             // no memcache support in PHP
206             if (!class_exists('Memcache')) {
207                 $this->memcache = false;
208                 return false;
209             }
210
211             $this->memcache     = new Memcache;
212             $this->mc_available = 0;
213
214             // add all configured hosts to pool
c3e441 215             $pconnect       = $this->config->get('memcache_pconnect', true);
JVM(S 216             $timeout        = $this->config->get('memcache_timeout', 1);
217             $retry_interval = $this->config->get('memcache_retry_interval', 15);
218
315418 219             foreach ($this->config->get('memcache_hosts', array()) as $host) {
AM 220                 if (substr($host, 0, 7) != 'unix://') {
221                     list($host, $port) = explode(':', $host);
222                     if (!$port) $port = 11211;
223                 }
224                 else {
225                     $port = 0;
226                 }
227
228                 $this->mc_available += intval($this->memcache->addServer(
c3e441 229                     $host, $port, $pconnect, 1, $timeout, $retry_interval, false, array($this, 'memcache_failure')));
315418 230             }
AM 231
232             // test connection and failover (will result in $this->mc_available == 0 on complete failure)
233             $this->memcache->increment('__CONNECTIONTEST__', 1);  // NOP if key doesn't exist
234
235             if (!$this->mc_available) {
236                 $this->memcache = false;
237             }
238         }
239
240         return $this->memcache;
0c2596 241     }
A 242
315418 243     /**
AM 244      * Callback for memcache failure
245      */
246     public function memcache_failure($host, $port)
247     {
248         static $seen = array();
0c2596 249
315418 250         // only report once
AM 251         if (!$seen["$host:$port"]++) {
252             $this->mc_available--;
253             self::raise_error(array(
254                 'code' => 604, 'type' => 'db',
255                 'line' => __LINE__, 'file' => __FILE__,
256                 'message' => "Memcache failure on host $host:$port"),
257                 true, false);
258         }
0c2596 259     }
A 260
315418 261     /**
AM 262      * Initialize and get cache object
263      *
264      * @param string $name   Cache identifier
265      * @param string $type   Cache type ('db', 'apc' or 'memcache')
266      * @param string $ttl    Expiration time for cache items
267      * @param bool   $packed Enables/disables data serialization
268      *
269      * @return rcube_cache Cache object
270      */
271     public function get_cache($name, $type='db', $ttl=0, $packed=true)
272     {
273         if (!isset($this->caches[$name]) && ($userid = $this->get_user_id())) {
274             $this->caches[$name] = new rcube_cache($type, $userid, $name, $ttl, $packed);
275         }
0c2596 276
315418 277         return $this->caches[$name];
0c2596 278     }
A 279
315418 280     /**
50abd5 281      * Initialize and get shared cache object
AM 282      *
283      * @param string $name   Cache identifier
284      * @param bool   $packed Enables/disables data serialization
285      *
286      * @return rcube_cache_shared Cache object
287      */
288     public function get_cache_shared($name, $packed=true)
289     {
290         $shared_name = "shared_$name";
291
22a41b 292         if (!array_key_exists($shared_name, $this->caches)) {
50abd5 293             $opt  = strtolower($name) . '_cache';
AM 294             $type = $this->config->get($opt);
295             $ttl  = $this->config->get($opt . '_ttl');
296
297             if (!$type) {
22a41b 298                 // cache is disabled
AM 299                 return $this->caches[$shared_name] = null;
50abd5 300             }
22a41b 301
50abd5 302             if ($ttl === null) {
22a41b 303                 $ttl = $this->config->get('shared_cache_ttl', '10d');
50abd5 304             }
AM 305
306             $this->caches[$shared_name] = new rcube_cache_shared($type, $name, $ttl, $packed);
307         }
308
309         return $this->caches[$shared_name];
310     }
311
312     /**
315418 313      * Create SMTP object and connect to server
AM 314      *
a95874 315      * @param boolean $connect True if connection should be established
315418 316      */
AM 317     public function smtp_init($connect = false)
318     {
319         $this->smtp = new rcube_smtp();
0c2596 320
315418 321         if ($connect) {
AM 322             $this->smtp->connect();
323         }
0c2596 324     }
315418 325
AM 326     /**
327      * Initialize and get storage object
328      *
329      * @return rcube_storage Storage object
330      */
331     public function get_storage()
332     {
333         // already initialized
334         if (!is_object($this->storage)) {
335             $this->storage_init();
336         }
337
338         return $this->storage;
0c2596 339     }
315418 340
AM 341     /**
342      * Initialize storage object
343      */
344     public function storage_init()
345     {
346         // already initialized
347         if (is_object($this->storage)) {
348             return;
349         }
350
351         $driver       = $this->config->get('storage_driver', 'imap');
352         $driver_class = "rcube_{$driver}";
353
354         if (!class_exists($driver_class)) {
355             self::raise_error(array(
356                 'code' => 700, 'type' => 'php',
357                 'file' => __FILE__, 'line' => __LINE__,
358                 'message' => "Storage driver class ($driver) not found!"),
359                 true, true);
360         }
361
362         // Initialize storage object
363         $this->storage = new $driver_class;
364
365         // for backward compat. (deprecated, will be removed)
366         $this->imap = $this->storage;
367
368         // set class options
369         $options = array(
109bcc 370             'auth_type'      => $this->config->get("{$driver}_auth_type", 'check'),
AM 371             'auth_cid'       => $this->config->get("{$driver}_auth_cid"),
372             'auth_pw'        => $this->config->get("{$driver}_auth_pw"),
373             'debug'          => (bool) $this->config->get("{$driver}_debug"),
374             'force_caps'     => (bool) $this->config->get("{$driver}_force_caps"),
375             'disabled_caps'  => $this->config->get("{$driver}_disabled_caps"),
376             'socket_options' => $this->config->get("{$driver}_conn_options"),
377             'timeout'        => (int) $this->config->get("{$driver}_timeout"),
378             'skip_deleted'   => (bool) $this->config->get('skip_deleted'),
379             'driver'         => $driver,
315418 380         );
AM 381
382         if (!empty($_SESSION['storage_host'])) {
383             $options['host']     = $_SESSION['storage_host'];
384             $options['user']     = $_SESSION['username'];
385             $options['port']     = $_SESSION['storage_port'];
386             $options['ssl']      = $_SESSION['storage_ssl'];
387             $options['password'] = $this->decrypt($_SESSION['password']);
388             $_SESSION[$driver.'_host'] = $_SESSION['storage_host'];
389         }
390
391         $options = $this->plugins->exec_hook("storage_init", $options);
392
393         // for backward compat. (deprecated, to be removed)
394         $options = $this->plugins->exec_hook("imap_init", $options);
395
396         $this->storage->set_options($options);
397         $this->set_storage_prop();
398
f95492 399         // subscribe to 'storage_connected' hook for session logging
TB 400         if ($this->config->get('imap_log_session', false)) {
401             $this->plugins->register_hook('storage_connected', array($this, 'storage_log_session'));
402         }
403     }
315418 404
AM 405     /**
406      * Set storage parameters.
407      */
408     protected function set_storage_prop()
409     {
410         $storage = $this->get_storage();
411
8d34b9 412         // set pagesize from config
AM 413         $pagesize = $this->config->get('mail_pagesize');
414         if (!$pagesize) {
415             $pagesize = $this->config->get('pagesize', 50);
416         }
417
418         $storage->set_pagesize($pagesize);
a92beb 419         $storage->set_charset($this->config->get('default_charset', RCUBE_CHARSET));
315418 420
8d34b9 421         // enable caching of mail data
AM 422         $driver         = $this->config->get('storage_driver', 'imap');
423         $storage_cache  = $this->config->get("{$driver}_cache");
424         $messages_cache = $this->config->get('messages_cache');
425         // for backward compatybility
426         if ($storage_cache === null && $messages_cache === null && $this->config->get('enable_caching')) {
427             $storage_cache  = 'db';
428             $messages_cache = true;
315418 429         }
8d34b9 430
AM 431         if ($storage_cache) {
432             $storage->set_caching($storage_cache);
433         }
434         if ($messages_cache) {
435             $storage->set_messages_caching(true);
315418 436         }
AM 437     }
0c2596 438
963a10 439     /**
dc0b50 440      * Set special folders type association.
AM 441      * This must be done AFTER connecting to the server!
442      */
443     protected function set_special_folders()
444     {
445         $storage = $this->get_storage();
446         $folders = $storage->get_special_folders(true);
447         $prefs   = array();
448
449         // check SPECIAL-USE flags on IMAP folders
450         foreach ($folders as $type => $folder) {
451             $idx = $type . '_mbox';
452             if ($folder !== $this->config->get($idx)) {
453                 $prefs[$idx] = $folder;
454             }
455         }
456
457         // Some special folders differ, update user preferences
458         if (!empty($prefs) && $this->user) {
459             $this->user->save_prefs($prefs);
460         }
461
462         // create default folders (on login)
463         if ($this->config->get('create_default_folders')) {
464             $storage->create_default_folders();
465         }
466     }
f95492 467
TB 468     /**
469      * Callback for IMAP connection events to log session identifiers
470      */
471     public function storage_log_session($args)
472     {
473         if (!empty($args['session']) && session_id()) {
474             $this->write_log('imap_session', $args['session']);
475         }
476     }
dc0b50 477
AM 478     /**
963a10 479      * Create session object and start the session.
A 480      */
481     public function session_init()
482     {
483         // session started (Installer?)
484         if (session_id()) {
485             return;
486         }
487
488         $sess_name   = $this->config->get('session_name');
489         $sess_domain = $this->config->get('session_domain');
ae7027 490         $sess_path   = $this->config->get('session_path');
963a10 491         $lifetime    = $this->config->get('session_lifetime', 0) * 60;
8e4b49 492         $is_secure   = $this->config->get('use_https') || rcube_utils::https_check();
963a10 493
A 494         // set session domain
495         if ($sess_domain) {
496             ini_set('session.cookie_domain', $sess_domain);
497         }
ae7027 498         // set session path
AM 499         if ($sess_path) {
500             ini_set('session.cookie_path', $sess_path);
501         }
963a10 502         // set session garbage collecting time according to session_lifetime
A 503         if ($lifetime) {
504             ini_set('session.gc_maxlifetime', $lifetime * 2);
505         }
506
8e4b49 507         ini_set('session.cookie_secure', $is_secure);
7e3298 508         ini_set('session.name', $sess_name ?: 'roundcube_sessid');
963a10 509         ini_set('session.use_cookies', 1);
A 510         ini_set('session.use_only_cookies', 1);
0afe27 511         ini_set('session.cookie_httponly', 1);
963a10 512
b4be89 513         // get session driver instance
C 514         $this->session = rcube_session::factory($this->config);
87ff88 515         $this->session->register_gc_handler(array($this, 'gc'));
8d2963 516
963a10 517         // start PHP session (if not in CLI mode)
A 518         if ($_SERVER['REMOTE_ADDR']) {
42de33 519             $this->session->start();
963a10 520         }
A 521     }
522
523     /**
ee73a7 524      * Garbage collector - cache/temp cleaner
AM 525      */
526     public function gc()
527     {
60b6d7 528         rcube_cache::gc();
AM 529         rcube_cache_shared::gc();
530         $this->get_storage()->cache_gc();
ee73a7 531
AM 532         $this->gc_temp();
533     }
534
535     /**
963a10 536      * Garbage collector function for temp files.
A 537      * Remove temp files older than two days
538      */
ee73a7 539     public function gc_temp()
963a10 540     {
A 541         $tmp = unslashify($this->config->get('temp_dir'));
de8687 542
DC 543         // expire in 48 hours by default
544         $temp_dir_ttl = $this->config->get('temp_dir_ttl', '48h');
545         $temp_dir_ttl = get_offset_sec($temp_dir_ttl);
546         if ($temp_dir_ttl < 6*3600)
547             $temp_dir_ttl = 6*3600;   // 6 hours sensible lower bound.
548
549         $expire = time() - $temp_dir_ttl;
963a10 550
A 551         if ($tmp && ($dir = opendir($tmp))) {
552             while (($fname = readdir($dir)) !== false) {
311d87 553                 if ($fname[0] == '.') {
963a10 554                     continue;
A 555                 }
556
311d87 557                 if (@filemtime($tmp.'/'.$fname) < $expire) {
963a10 558                     @unlink($tmp.'/'.$fname);
A 559                 }
560             }
561
562             closedir($dir);
563         }
ee73a7 564     }
AM 565
566     /**
567      * Runs garbage collector with probability based on
568      * session settings. This is intended for environments
569      * without a session.
570      */
571     public function gc_run()
572     {
573         $probability = (int) ini_get('session.gc_probability');
574         $divisor     = (int) ini_get('session.gc_divisor');
575
576         if ($divisor > 0 && $probability > 0) {
577             $random = mt_rand(1, $divisor);
578             if ($random <= $probability) {
579                 $this->gc();
580             }
581         }
963a10 582     }
A 583
315418 584     /**
AM 585      * Get localized text in the desired language
586      *
587      * @param mixed   $attrib  Named parameters array or label name
588      * @param string  $domain  Label domain (plugin) name
589      *
590      * @return string Localized text
0c2596 591      */
315418 592     public function gettext($attrib, $domain=null)
AM 593     {
594         // load localization files if not done yet
595         if (empty($this->texts)) {
596             $this->load_language();
597         }
0c2596 598
315418 599         // extract attributes
AM 600         if (is_string($attrib)) {
601             $attrib = array('name' => $attrib);
602         }
0c2596 603
7e3298 604         $name = (string) $attrib['name'];
315418 605
AM 606         // attrib contain text values: use them from now
607         if (($setval = $attrib[strtolower($_SESSION['language'])]) || ($setval = $attrib['en_us'])) {
608             $this->texts[$name] = $setval;
609         }
610
611         // check for text with domain
612         if ($domain && ($text = $this->texts[$domain.'.'.$name])) {
613         }
614         // text does not exist
615         else if (!($text = $this->texts[$name])) {
616             return "[$name]";
617         }
618
619         // replace vars in text
620         if (is_array($attrib['vars'])) {
621             foreach ($attrib['vars'] as $var_key => $var_value) {
7e3298 622                 $text = str_replace($var_key[0] != '$' ? '$'.$var_key : $var_key, $var_value, $text);
315418 623             }
AM 624         }
625
626         // format output
627         if (($attrib['uppercase'] && strtolower($attrib['uppercase'] == 'first')) || $attrib['ucfirst']) {
628             return ucfirst($text);
629         }
630         else if ($attrib['uppercase']) {
631             return mb_strtoupper($text);
632         }
633         else if ($attrib['lowercase']) {
634             return mb_strtolower($text);
635         }
636
637         return strtr($text, array('\n' => "\n"));
0c2596 638     }
A 639
315418 640     /**
AM 641      * Check if the given text label exists
642      *
643      * @param string  $name       Label name
644      * @param string  $domain     Label domain (plugin) name or '*' for all domains
645      * @param string  $ref_domain Sets domain name if label is found
646      *
647      * @return boolean True if text exists (either in the current language or in en_US)
648      */
649     public function text_exists($name, $domain = null, &$ref_domain = null)
650     {
651         // load localization files if not done yet
652         if (empty($this->texts)) {
653             $this->load_language();
654         }
0c2596 655
315418 656         if (isset($this->texts[$name])) {
AM 657             $ref_domain = '';
658             return true;
659         }
0c2596 660
315418 661         // any of loaded domains (plugins)
AM 662         if ($domain == '*') {
663             foreach ($this->plugins->loaded_plugins() as $domain) {
664                 if (isset($this->texts[$domain.'.'.$name])) {
665                     $ref_domain = $domain;
666                     return true;
667                 }
668             }
669         }
670         // specified domain
671         else if ($domain) {
672             $ref_domain = $domain;
673             return isset($this->texts[$domain.'.'.$name]);
674         }
0c2596 675
315418 676         return false;
AM 677     }
678
679     /**
680      * Load a localization package
681      *
75a5c3 682      * @param string $lang  Language ID
AM 683      * @param array  $add   Additional text labels/messages
684      * @param array  $merge Additional text labels/messages to merge
315418 685      */
75a5c3 686     public function load_language($lang = null, $add = array(), $merge = array())
315418 687     {
7e3298 688         $lang = $this->language_prop($lang ?: $_SESSION['language']);
315418 689
AM 690         // load localized texts
691         if (empty($this->texts) || $lang != $_SESSION['language']) {
692             $this->texts = array();
693
694             // handle empty lines after closing PHP tag in localization files
695             ob_start();
696
697             // get english labels (these should be complete)
9be2f4 698             @include(RCUBE_LOCALIZATION_DIR . 'en_US/labels.inc');
TB 699             @include(RCUBE_LOCALIZATION_DIR . 'en_US/messages.inc');
315418 700
AM 701             if (is_array($labels))
702                 $this->texts = $labels;
703             if (is_array($messages))
704                 $this->texts = array_merge($this->texts, $messages);
705
706             // include user language files
9be2f4 707             if ($lang != 'en' && $lang != 'en_US' && is_dir(RCUBE_LOCALIZATION_DIR . $lang)) {
TB 708                 include_once(RCUBE_LOCALIZATION_DIR . $lang . '/labels.inc');
709                 include_once(RCUBE_LOCALIZATION_DIR . $lang . '/messages.inc');
315418 710
AM 711                 if (is_array($labels))
712                     $this->texts = array_merge($this->texts, $labels);
713                 if (is_array($messages))
714                     $this->texts = array_merge($this->texts, $messages);
715             }
716
717             ob_end_clean();
718
719             $_SESSION['language'] = $lang;
720         }
721
722         // append additional texts (from plugin)
723         if (is_array($add) && !empty($add)) {
724             $this->texts += $add;
725         }
75a5c3 726
AM 727         // merge additional texts (from plugin)
728         if (is_array($merge) && !empty($merge)) {
729             $this->texts = array_merge($this->texts, $merge);
730         }
315418 731     }
AM 732
733     /**
734      * Check the given string and return a valid language code
735      *
a95874 736      * @param string $lang Language code
315418 737      *
AM 738      * @return string Valid language code
739      */
740     protected function language_prop($lang)
741     {
742         static $rcube_languages, $rcube_language_aliases;
743
744         // user HTTP_ACCEPT_LANGUAGE if no language is specified
745         if (empty($lang) || $lang == 'auto') {
746             $accept_langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
2c6a23 747             $lang         = $accept_langs[0];
AM 748
749             if (preg_match('/^([a-z]+)[_-]([a-z]+)$/i', $lang, $m)) {
750                 $lang = $m[1] . '_' . strtoupper($m[2]);
751             }
315418 752         }
AM 753
754         if (empty($rcube_languages)) {
9be2f4 755             @include(RCUBE_LOCALIZATION_DIR . 'index.inc');
315418 756         }
AM 757
758         // check if we have an alias for that language
759         if (!isset($rcube_languages[$lang]) && isset($rcube_language_aliases[$lang])) {
760             $lang = $rcube_language_aliases[$lang];
761         }
762         // try the first two chars
763         else if (!isset($rcube_languages[$lang])) {
764             $short = substr($lang, 0, 2);
765
766             // check if we have an alias for the short language code
767             if (!isset($rcube_languages[$short]) && isset($rcube_language_aliases[$short])) {
768                 $lang = $rcube_language_aliases[$short];
769             }
770             // expand 'nn' to 'nn_NN'
771             else if (!isset($rcube_languages[$short])) {
772                 $lang = $short.'_'.strtoupper($short);
773             }
774         }
775
9be2f4 776         if (!isset($rcube_languages[$lang]) || !is_dir(RCUBE_LOCALIZATION_DIR . $lang)) {
315418 777             $lang = 'en_US';
AM 778         }
779
780         return $lang;
781     }
782
783     /**
784      * Read directory program/localization and return a list of available languages
785      *
786      * @return array List of available localizations
787      */
788     public function list_languages()
789     {
790         static $sa_languages = array();
791
792         if (!sizeof($sa_languages)) {
9be2f4 793             @include(RCUBE_LOCALIZATION_DIR . 'index.inc');
315418 794
9be2f4 795             if ($dh = @opendir(RCUBE_LOCALIZATION_DIR)) {
315418 796                 while (($name = readdir($dh)) !== false) {
9be2f4 797                     if ($name[0] == '.' || !is_dir(RCUBE_LOCALIZATION_DIR . $name)) {
315418 798                         continue;
AM 799                     }
800
801                     if ($label = $rcube_languages[$name]) {
802                         $sa_languages[$name] = $label;
803                     }
804                 }
805                 closedir($dh);
806             }
807         }
808
809         return $sa_languages;
810     }
811
812     /**
e4c660 813      * Encrypt a string
315418 814      *
a95874 815      * @param string  $clear  Clear text input
AM 816      * @param string  $key    Encryption key to retrieve from the configuration, defaults to 'des_key'
817      * @param boolean $base64 Whether or not to base64_encode() the result before returning
315418 818      *
e4c660 819      * @return string Encrypted text
315418 820      */
AM 821     public function encrypt($clear, $key = 'des_key', $base64 = true)
822     {
e4c660 823         if (!is_string($clear) || !strlen($clear)) {
315418 824             return '';
AM 825         }
826
8447ba 827         $ckey   = $this->config->get_crypto_key($key);
e4c660 828         $method = $this->config->get_crypto_method();
8447ba 829         $opts   = defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : true;
AM 830         $iv     = rcube_utils::random_bytes(openssl_cipher_iv_length($method), true);
831         $cipher = $iv . openssl_encrypt($clear, $method, $ckey, $opts, $iv);
315418 832
AM 833         return $base64 ? base64_encode($cipher) : $cipher;
834     }
835
836     /**
e4c660 837      * Decrypt a string
315418 838      *
a95874 839      * @param string  $cipher Encrypted text
AM 840      * @param string  $key    Encryption key to retrieve from the configuration, defaults to 'des_key'
841      * @param boolean $base64 Whether or not input is base64-encoded
315418 842      *
e4c660 843      * @return string Decrypted text
315418 844      */
AM 845     public function decrypt($cipher, $key = 'des_key', $base64 = true)
846     {
847         if (!$cipher) {
848             return '';
849         }
850
e4c660 851         $cipher  = $base64 ? base64_decode($cipher) : $cipher;
AM 852         $ckey    = $this->config->get_crypto_key($key);
853         $method  = $this->config->get_crypto_method();
8447ba 854         $opts    = defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : true;
AM 855         $iv_size = openssl_cipher_iv_length($method);
856         $iv      = substr($cipher, 0, $iv_size);
6c1c60 857
8447ba 858         // session corruption? (#1485970)
AM 859         if (strlen($iv) < $iv_size) {
860             return '';
6c1c60 861         }
315418 862
8447ba 863         $cipher = substr($cipher, $iv_size);
AM 864         $clear  = openssl_decrypt($cipher, $method, $ckey, $opts, $iv);
315418 865
AM 866         return $clear;
867     }
868
869     /**
681ba6 870      * Returns session token for secure URLs
AM 871      *
872      * @param bool $generate Generate token if not exists in session yet
873      *
874      * @return string|bool Token string, False when disabled
875      */
876     public function get_secure_url_token($generate = false)
877     {
878         if ($len = $this->config->get('use_secure_urls')) {
879             if (empty($_SESSION['secure_token']) && $generate) {
880                 // generate x characters long token
881                 $length = $len > 1 ? $len : 16;
3994b3 882                 $token  = rcube_utils::random_bytes($length);
681ba6 883
AM 884                 $plugin = $this->plugins->exec_hook('secure_token',
885                     array('value' => $token, 'length' => $length));
886
887                 $_SESSION['secure_token'] = $plugin['value'];
888             }
889
890             return $_SESSION['secure_token'];
891         }
892
893         return false;
894     }
895
896     /**
897      * Generate a unique token to be used in a form request
898      *
899      * @return string The request token
900      */
901     public function get_request_token()
902     {
f75bc5 903         if (empty($_SESSION['request_token'])) {
AM 904             $plugin = $this->plugins->exec_hook('request_token', array(
905                 'value' => rcube_utils::random_bytes(32)));
906
907             $_SESSION['request_token'] = $plugin['value'];
681ba6 908         }
AM 909
f75bc5 910         return $_SESSION['request_token'];
681ba6 911     }
AM 912
913     /**
914      * Check if the current request contains a valid token.
915      * Empty requests aren't checked until use_secure_urls is set.
916      *
a95874 917      * @param int $mode Request method
681ba6 918      *
AM 919      * @return boolean True if request token is valid false if not
920      */
921     public function check_request($mode = rcube_utils::INPUT_POST)
922     {
923         // check secure token in URL if enabled
924         if ($token = $this->get_secure_url_token()) {
925             foreach (explode('/', preg_replace('/[?#&].*$/', '', $_SERVER['REQUEST_URI'])) as $tok) {
926                 if ($tok == $token) {
927                     return true;
928                 }
929             }
930
931             $this->request_status = self::REQUEST_ERROR_URL;
932
933             return false;
934         }
935
936         $sess_tok = $this->get_request_token();
937
938         // ajax requests
939         if (rcube_utils::request_header('X-Roundcube-Request') == $sess_tok) {
940             return true;
941         }
942
943         // skip empty requests
944         if (($mode == rcube_utils::INPUT_POST && empty($_POST))
945             || ($mode == rcube_utils::INPUT_GET && empty($_GET))
946         ) {
947             return true;
948         }
949
950         // default method of securing requests
951         $token   = rcube_utils::get_input_value('_token', $mode);
952         $sess_id = $_COOKIE[ini_get('session.name')];
953
954         if (empty($sess_id) || $token != $sess_tok) {
955             $this->request_status = self::REQUEST_ERROR_TOKEN;
956             return false;
957         }
958
959         return true;
960     }
961
962     /**
315418 963      * Build a valid URL to this instance of Roundcube
AM 964      *
a95874 965      * @param mixed $p Either a string with the action or url parameters as key-value pairs
AM 966      *
315418 967      * @return string Valid application URL
AM 968      */
969     public function url($p)
970     {
971         // STUB: should be overloaded by the application
0c2596 972         return '';
A 973     }
315418 974
AM 975     /**
976      * Function to be executed in script shutdown
977      * Registered with register_shutdown_function()
0c2596 978      */
315418 979     public function shutdown()
AM 980     {
981         foreach ($this->shutdown_functions as $function) {
982             call_user_func($function);
0c2596 983         }
A 984
3dbe4f 985         // write session data as soon as possible and before
AM 986         // closing database connection, don't do this before
987         // registered shutdown functions, they may need the session
988         // Note: this will run registered gc handlers (ie. cache gc)
989         if ($_SERVER['REMOTE_ADDR'] && is_object($this->session)) {
990             $this->session->write_close();
315418 991         }
AM 992
3dbe4f 993         if (is_object($this->smtp)) {
AM 994             $this->smtp->disconnect();
ee73a7 995         }
AM 996
315418 997         foreach ($this->caches as $cache) {
AM 998             if (is_object($cache)) {
999                 $cache->close();
1000             }
1001         }
1002
1003         if (is_object($this->storage)) {
1004             $this->storage->close();
1005         }
0c2596 1006     }
A 1007
315418 1008     /**
AM 1009      * Registers shutdown function to be executed on shutdown.
1010      * The functions will be executed before destroying any
1011      * objects like smtp, imap, session, etc.
1012      *
1013      * @param callback Function callback
1014      */
1015     public function add_shutdown_function($function)
1016     {
1017         $this->shutdown_functions[] = $function;
1018     }
1019
1020     /**
10da75 1021      * Quote a given string.
TB 1022      * Shortcut function for rcube_utils::rep_specialchars_output()
1023      *
1024      * @return string HTML-quoted string
1025      */
1026     public static function Q($str, $mode = 'strict', $newlines = true)
1027     {
1028         return rcube_utils::rep_specialchars_output($str, 'html', $mode, $newlines);
1029     }
1030
1031     /**
1032      * Quote a given string for javascript output.
1033      * Shortcut function for rcube_utils::rep_specialchars_output()
1034      *
1035      * @return string JS-quoted string
1036      */
1037     public static function JQ($str)
1038     {
1039         return rcube_utils::rep_specialchars_output($str, 'js');
1040     }
1041
1042     /**
315418 1043      * Construct shell command, execute it and return output as string.
AM 1044      * Keywords {keyword} are replaced with arguments
1045      *
a95874 1046      * @param $cmd    Format string with {keywords} to be replaced
315418 1047      * @param $values (zero, one or more arrays can be passed)
AM 1048      *
1049      * @return output of command. shell errors not detectable
1050      */
1051     public static function exec(/* $cmd, $values1 = array(), ... */)
1052     {
1053         $args   = func_get_args();
1054         $cmd    = array_shift($args);
1055         $values = $replacements = array();
1056
1057         // merge values into one array
1058         foreach ($args as $arg) {
1059             $values += (array)$arg;
1060         }
1061
1062         preg_match_all('/({(-?)([a-z]\w*)})/', $cmd, $matches, PREG_SET_ORDER);
1063         foreach ($matches as $tags) {
1064             list(, $tag, $option, $key) = $tags;
1065             $parts = array();
1066
1067             if ($option) {
1068                 foreach ((array)$values["-$key"] as $key => $value) {
1069                     if ($value === true || $value === false || $value === null) {
1070                         $parts[] = $value ? $key : "";
1071                     }
1072                     else {
1073                         foreach ((array)$value as $val) {
1074                             $parts[] = "$key " . escapeshellarg($val);
1075                         }
1076                     }
1077                 }
1078             }
1079             else {
1080                 foreach ((array)$values[$key] as $value) {
1081                     $parts[] = escapeshellarg($value);
1082                 }
1083             }
1084
1085             $replacements[$tag] = join(" ", $parts);
1086         }
1087
1088         // use strtr behaviour of going through source string once
1089         $cmd = strtr($cmd, $replacements);
1090
1091         return (string)shell_exec($cmd);
1092     }
0c2596 1093
A 1094     /**
1095      * Print or write debug messages
1096      *
1097      * @param mixed Debug message or data
1098      */
1099     public static function console()
1100     {
1101         $args = func_get_args();
1102
be98df 1103         if (class_exists('rcube', false)) {
a95874 1104             $rcube  = self::get_instance();
be98df 1105             $plugin = $rcube->plugins->exec_hook('console', array('args' => $args));
A 1106             if ($plugin['abort']) {
1107                 return;
0c2596 1108             }
a95874 1109
AM 1110             $args = $plugin['args'];
0c2596 1111         }
A 1112
1113         $msg = array();
1114         foreach ($args as $arg) {
1115             $msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
1116         }
1117
1118         self::write_log('console', join(";\n", $msg));
1119     }
1120
1121     /**
1122      * Append a line to a logfile in the logs directory.
1123      * Date will be added automatically to the line.
1124      *
a95874 1125      * @param string $name Name of the log file
AM 1126      * @param mixed  $line Line to append
0c2596 1127      */
A 1128     public static function write_log($name, $line)
1129     {
1130         if (!is_string($line)) {
1131             $line = var_export($line, true);
1132         }
1133
f95492 1134         $date_format = $log_driver = $session_key = null;
TB 1135         if (self::$instance) {
1136             $date_format = self::$instance->config->get('log_date_format');
1137             $log_driver  = self::$instance->config->get('log_driver');
1138             $session_key = intval(self::$instance->config->get('log_session_id', 8));
1139         }
0c2596 1140
9aae1b 1141         $date = rcube_utils::date_format($date_format);
0c2596 1142
A 1143         // trigger logging hook
1144         if (is_object(self::$instance) && is_object(self::$instance->plugins)) {
930a3c 1145             $log = self::$instance->plugins->exec_hook('write_log',
AM 1146                 array('name' => $name, 'date' => $date, 'line' => $line));
1147
0c2596 1148             $name = $log['name'];
A 1149             $line = $log['line'];
1150             $date = $log['date'];
930a3c 1151
AM 1152             if ($log['abort']) {
0c2596 1153                 return true;
930a3c 1154             }
0c2596 1155         }
A 1156
596d43 1157         // add session ID to the log
f95492 1158         if ($session_key > 0 && ($sess = session_id())) {
TB 1159             $line = '<' . substr($sess, 0, $session_key) . '> ' . $line;
596d43 1160         }
AM 1161
0c2596 1162         if ($log_driver == 'syslog') {
A 1163             $prio = $name == 'errors' ? LOG_ERR : LOG_INFO;
1164             syslog($prio, $line);
1165             return true;
1166         }
1167
1168         // log_driver == 'file' is assumed here
1169
1170         $line = sprintf("[%s]: %s\n", $date, $line);
3786a4 1171         $log_dir = null;
TB 1172
1173         // per-user logging is activated
1174         if (self::$instance && self::$instance->config->get('per_user_logging', false) && self::$instance->get_user_id()) {
1175             $log_dir = self::$instance->get_user_log_dir();
930a3c 1176             if (empty($log_dir) && $name != 'errors') {
3786a4 1177                 return false;
930a3c 1178             }
3786a4 1179         }
930a3c 1180
AM 1181         if (empty($log_dir)) {
1182             if (!empty($log['dir'])) {
1183                 $log_dir = $log['dir'];
1184             }
1185             else if (self::$instance) {
1186                 $log_dir = self::$instance->config->get('log_dir');
1187             }
3786a4 1188         }
0c2596 1189
A 1190         if (empty($log_dir)) {
9be2f4 1191             $log_dir = RCUBE_INSTALL_PATH . 'logs';
0c2596 1192         }
A 1193
930a3c 1194         return file_put_contents("$log_dir/$name", $line, FILE_APPEND) !== false;
0c2596 1195     }
A 1196
1197     /**
1198      * Throw system error (and show error page).
1199      *
a95874 1200      * @param array $arg Named parameters
0c2596 1201      *      - code:    Error code (required)
A 1202      *      - type:    Error type [php|db|imap|javascript] (required)
1203      *      - message: Error message
b3e259 1204      *      - file:    File where error occurred
AM 1205      *      - line:    Line where error occurred
a95874 1206      * @param boolean $log       True to log the error
AM 1207      * @param boolean $terminate Terminate script execution
0c2596 1208      */
A 1209     public static function raise_error($arg = array(), $log = false, $terminate = false)
1210     {
76e499 1211         // handle PHP exceptions
TB 1212         if (is_object($arg) && is_a($arg, 'Exception')) {
a39fd4 1213             $arg = array(
76e499 1214                 'code' => $arg->getCode(),
TB 1215                 'line' => $arg->getLine(),
1216                 'file' => $arg->getFile(),
1217                 'message' => $arg->getMessage(),
1218             );
a39fd4 1219         }
f23ef1 1220         else if (is_string($arg)) {
0301d9 1221             $arg = array('message' => $arg);
f23ef1 1222         }
a39fd4 1223
AM 1224         if (empty($arg['code'])) {
1225             $arg['code'] = 500;
76e499 1226         }
TB 1227
0c2596 1228         // installer
eea11e 1229         if (class_exists('rcmail_install', false)) {
TB 1230             $rci = rcmail_install::get_instance();
0c2596 1231             $rci->raise_error($arg);
A 1232             return;
1233         }
1234
f23ef1 1235         $cli = php_sapi_name() == 'cli';
AM 1236
0301d9 1237         if (($log || $terminate) && !$cli && $arg['message']) {
819315 1238             $arg['fatal'] = $terminate;
0c2596 1239             self::log_bug($arg);
A 1240         }
1241
f23ef1 1242         // terminate script
AM 1243         if ($terminate) {
1244             // display error page
1245             if (is_object(self::$instance->output)) {
1246                 self::$instance->output->raise_error($arg['code'], $arg['message']);
1247             }
1248             else if ($cli) {
1249                 fwrite(STDERR, 'ERROR: ' . $arg['message']);
1250             }
1251
1252             exit(1);
0c2596 1253         }
8cc65d 1254         else if ($cli) {
AM 1255             fwrite(STDERR, 'ERROR: ' . $arg['message']);
1256         }
0c2596 1257     }
A 1258
1259     /**
1260      * Report error according to configured debug_level
1261      *
a95874 1262      * @param array $arg_arr Named parameters
0c2596 1263      * @see self::raise_error()
A 1264      */
1265     public static function log_bug($arg_arr)
1266     {
7e3298 1267         $program = strtoupper($arg_arr['type'] ?: 'php');
0c2596 1268         $level   = self::get_instance()->config->get('debug_level');
A 1269
1270         // disable errors for ajax requests, write to log instead (#1487831)
1271         if (($level & 4) && !empty($_REQUEST['_remote'])) {
1272             $level = ($level ^ 4) | 1;
1273         }
1274
1275         // write error to local log file
819315 1276         if (($level & 1) || !empty($arg_arr['fatal'])) {
87cb24 1277             $post_query = '';
0c2596 1278             if ($_SERVER['REQUEST_METHOD'] == 'POST') {
87cb24 1279                 foreach (array('_task', '_action') as $arg) {
AM 1280                     if ($_POST[$arg] && !$_GET[$arg]) {
1281                         $post_query[$arg] = $_POST[$arg];
1282                     }
1283                 }
1284
1285                 if (!empty($post_query)) {
1286                     $post_query = (strpos($_SERVER['REQUEST_URI'], '?') != false ? '&' : '?')
329696 1287                         . http_build_query($post_query, '', '&');
87cb24 1288                 }
0c2596 1289             }
A 1290
1291             $log_entry = sprintf("%s Error: %s%s (%s %s)",
1292                 $program,
1293                 $arg_arr['message'],
1294                 $arg_arr['file'] ? sprintf(' in %s on line %d', $arg_arr['file'], $arg_arr['line']) : '',
1295                 $_SERVER['REQUEST_METHOD'],
1296                 $_SERVER['REQUEST_URI'] . $post_query);
1297
1298             if (!self::write_log('errors', $log_entry)) {
1299                 // send error to PHPs error handler if write_log didn't succeed
f23ef1 1300                 trigger_error($arg_arr['message'], E_USER_WARNING);
0c2596 1301             }
A 1302         }
1303
1304         // report the bug to the global bug reporting system
1305         if ($level & 2) {
1306             // TODO: Send error via HTTP
1307         }
1308
1309         // show error if debug_mode is on
1310         if ($level & 4) {
1311             print "<b>$program Error";
1312
1313             if (!empty($arg_arr['file']) && !empty($arg_arr['line'])) {
1314                 print " in $arg_arr[file] ($arg_arr[line])";
1315             }
1316
1317             print ':</b>&nbsp;';
1318             print nl2br($arg_arr['message']);
1319             print '<br />';
1320             flush();
1321         }
1322     }
1323
1324     /**
11d5e7 1325      * Write debug info to the log
AM 1326      *
a95874 1327      * @param string $engine Engine type - file name (memcache, apc)
AM 1328      * @param string $data   Data string to log
1329      * @param bool   $result Operation result
11d5e7 1330      */
AM 1331     public static function debug($engine, $data, $result = null)
1332     {
1333         static $debug_counter;
1334
1335         $line = '[' . (++$debug_counter[$engine]) . '] ' . $data;
1336
1337         if (($len = strlen($line)) > self::DEBUG_LINE_LENGTH) {
1338             $diff = $len - self::DEBUG_LINE_LENGTH;
1339             $line = substr($line, 0, self::DEBUG_LINE_LENGTH) . "... [truncated $diff bytes]";
1340         }
1341
1342         if ($result !== null) {
1343             $line .= ' [' . ($result ? 'TRUE' : 'FALSE') . ']';
1344         }
1345
1346         self::write_log($engine, $line);
1347     }
1348
1349     /**
0c2596 1350      * Returns current time (with microseconds).
A 1351      *
1352      * @return float Current time in seconds since the Unix
1353      */
1354     public static function timer()
1355     {
1356         return microtime(true);
1357     }
1358
1359     /**
1360      * Logs time difference according to provided timer
1361      *
a95874 1362      * @param float  $timer Timer (self::timer() result)
AM 1363      * @param string $label Log line prefix
1364      * @param string $dest  Log file name
0c2596 1365      *
A 1366      * @see self::timer()
1367      */
1368     public static function print_timer($timer, $label = 'Timer', $dest = 'console')
1369     {
1370         static $print_count = 0;
1371
1372         $print_count++;
1373         $now  = self::timer();
1374         $diff = $now - $timer;
1375
1376         if (empty($label)) {
1377             $label = 'Timer '.$print_count;
1378         }
1379
1380         self::write_log($dest, sprintf("%s: %0.4f sec", $label, $diff));
1381     }
1382
ce2019 1383     /**
TB 1384      * Setter for system user object
1385      *
1386      * @param rcube_user Current user instance
1387      */
1388     public function set_user($user)
1389     {
1390         if (is_object($user)) {
1391             $this->user = $user;
1392
1393             // overwrite config with user preferences
1394             $this->config->set_user_prefs((array)$this->user->get_prefs());
1395         }
1396     }
0c2596 1397
A 1398     /**
1399      * Getter for logged user ID.
1400      *
1401      * @return mixed User identifier
1402      */
1403     public function get_user_id()
1404     {
1405         if (is_object($this->user)) {
1406             return $this->user->ID;
1407         }
a2f896 1408         else if (isset($_SESSION['user_id'])) {
A 1409             return $_SESSION['user_id'];
1410         }
0c2596 1411
A 1412         return null;
1413     }
1414
1415     /**
1416      * Getter for logged user name.
1417      *
1418      * @return string User name
1419      */
1420     public function get_user_name()
1421     {
1422         if (is_object($this->user)) {
1423             return $this->user->get_username();
1424         }
789e59 1425         else if (isset($_SESSION['username'])) {
AM 1426             return $_SESSION['username'];
1427         }
1428     }
0c2596 1429
789e59 1430     /**
AM 1431      * Getter for logged user email (derived from user name not identity).
1432      *
1433      * @return string User email address
1434      */
1435     public function get_user_email()
1436     {
1437         if (is_object($this->user)) {
1438             return $this->user->get_username('mail');
1439         }
0c2596 1440     }
5b06e2 1441
AM 1442     /**
1443      * Getter for logged user password.
1444      *
1445      * @return string User password
1446      */
1447     public function get_user_password()
1448     {
1449         if ($this->password) {
1450             return $this->password;
1451         }
1452         else if ($_SESSION['password']) {
1453             return $this->decrypt($_SESSION['password']);
1454         }
1455     }
a5b8ef 1456
3786a4 1457     /**
TB 1458      * Get the per-user log directory
1459      */
1460     protected function get_user_log_dir()
1461     {
1462         $log_dir = $this->config->get('log_dir', RCUBE_INSTALL_PATH . 'logs');
1463         $user_name = $this->get_user_name();
1464         $user_log_dir = $log_dir . '/' . $user_name;
1465
1466         return !empty($user_name) && is_writable($user_log_dir) ? $user_log_dir : false;
1467     }
a5b8ef 1468
AM 1469     /**
1470      * Getter for logged user language code.
1471      *
1472      * @return string User language code
1473      */
1474     public function get_user_language()
1475     {
1476         if (is_object($this->user)) {
1477             return $this->user->language;
1478         }
1479         else if (isset($_SESSION['language'])) {
1480             return $_SESSION['language'];
1481         }
1482     }
0b9a7b 1483
TB 1484     /**
1485      * Unique Message-ID generator.
1486      *
1487      * @return string Message-ID
1488      */
1489     public function gen_message_id()
1490     {
1491         $local_part  = md5(uniqid('rcube'.mt_rand(), true));
1492         $domain_part = $this->user->get_username('domain');
1493
1494         // Try to find FQDN, some spamfilters doesn't like 'localhost' (#1486924)
1495         if (!preg_match('/\.[a-z]+$/i', $domain_part)) {
1496             foreach (array($_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']) as $host) {
1497                 $host = preg_replace('/:[0-9]+$/', '', $host);
1498                 if ($host && preg_match('/\.[a-z]+$/i', $host)) {
1499                     $domain_part = $host;
e91c35 1500                     break;
0b9a7b 1501                 }
TB 1502             }
1503         }
1504
1505         return sprintf('<%s@%s>', $local_part, $domain_part);
1506     }
1507
1508     /**
1509      * Send the given message using the configured method.
1510      *
a95874 1511      * @param object $message   Reference to Mail_MIME object
AM 1512      * @param string $from      Sender address string
1513      * @param array  $mailto    Array of recipient address strings
1514      * @param array  $error     SMTP error array (reference)
1515      * @param string $body_file Location of file with saved message body (reference),
1516      *                          used when delay_file_io is enabled
1517      * @param array  $options   SMTP options (e.g. DSN request)
0b9a7b 1518      *
TB 1519      * @return boolean Send status.
1520      */
1521     public function deliver_message(&$message, $from, $mailto, &$error, &$body_file = null, $options = null)
1522     {
1523         $plugin = $this->plugins->exec_hook('message_before_send', array(
1524             'message' => $message,
1525             'from'    => $from,
1526             'mailto'  => $mailto,
1527             'options' => $options,
1528         ));
1529
6efadf 1530         if ($plugin['abort']) {
efdbf4 1531             if (!empty($plugin['error'])) {
AM 1532                 $error = $plugin['error'];
1533             }
1534             if (!empty($plugin['body_file'])) {
1535                 $body_file = $plugin['body_file'];
1536             }
1537
6efadf 1538             return isset($plugin['result']) ? $plugin['result'] : false;
AM 1539         }
1540
0b9a7b 1541         $from    = $plugin['from'];
TB 1542         $mailto  = $plugin['mailto'];
1543         $options = $plugin['options'];
1544         $message = $plugin['message'];
1545         $headers = $message->headers();
1546
1547         // send thru SMTP server using custom SMTP library
1548         if ($this->config->get('smtp_server')) {
1549             // generate list of recipients
409b64 1550             $a_recipients = (array) $mailto;
0b9a7b 1551
TB 1552             if (strlen($headers['Cc']))
1553                 $a_recipients[] = $headers['Cc'];
1554             if (strlen($headers['Bcc']))
1555                 $a_recipients[] = $headers['Bcc'];
1556
a7efdd 1557             // remove Bcc header and get the whole head of the message as string
6f249b 1558             $smtp_headers = $this->message_head($message, array('Bcc'));
0b9a7b 1559
TB 1560             if ($message->getParam('delay_file_io')) {
1561                 // use common temp dir
b59b72 1562                 $temp_dir    = $this->config->get('temp_dir');
AM 1563                 $body_file   = tempnam($temp_dir, 'rcmMsg');
1564                 $mime_result = $message->saveMessageBody($body_file);
1565
1566                 if (is_a($mime_result, 'PEAR_Error')) {
0b9a7b 1567                     self::raise_error(array('code' => 650, 'type' => 'php',
TB 1568                         'file' => __FILE__, 'line' => __LINE__,
1569                         'message' => "Could not create message: ".$mime_result->getMessage()),
b59b72 1570                         true, false);
0b9a7b 1571                     return false;
TB 1572                 }
b59b72 1573
0b9a7b 1574                 $msg_body = fopen($body_file, 'r');
TB 1575             }
1576             else {
1577                 $msg_body = $message->get();
1578             }
1579
1580             // send message
1581             if (!is_object($this->smtp)) {
1582                 $this->smtp_init(true);
1583             }
1584
1585             $sent     = $this->smtp->send_mail($from, $a_recipients, $smtp_headers, $msg_body, $options);
1586             $response = $this->smtp->get_response();
1587             $error    = $this->smtp->get_error();
1588
1589             // log error
1590             if (!$sent) {
1591                 self::raise_error(array('code' => 800, 'type' => 'smtp',
1592                     'line' => __LINE__, 'file' => __FILE__,
a3fa84 1593                     'message' => join("\n", $response)), true, false);
0b9a7b 1594             }
TB 1595         }
1596         // send mail using PHP's mail() function
1597         else {
6f249b 1598             // unset To,Subject headers because they will be added by the mail() function
AM 1599             $header_str = $this->message_head($message, array('To', 'Subject'));
0b9a7b 1600
TB 1601             // #1485779
1602             if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
6f249b 1603                 if (preg_match_all('/<([^@]+@[^>]+)>/', $headers['To'], $m)) {
AM 1604                     $headers['To'] = implode(', ', $m[1]);
0b9a7b 1605                 }
TB 1606             }
1607
1608             $msg_body = $message->get();
1609
b59b72 1610             if (is_a($msg_body, 'PEAR_Error')) {
0b9a7b 1611                 self::raise_error(array('code' => 650, 'type' => 'php',
TB 1612                     'file' => __FILE__, 'line' => __LINE__,
1613                     'message' => "Could not create message: ".$msg_body->getMessage()),
b59b72 1614                     true, false);
0b9a7b 1615             }
TB 1616             else {
1617                 $delim   = $this->config->header_delimiter();
6f249b 1618                 $to      = $headers['To'];
AM 1619                 $subject = $headers['Subject'];
0b9a7b 1620                 $header_str = rtrim($header_str);
TB 1621
1622                 if ($delim != "\r\n") {
1623                     $header_str = str_replace("\r\n", $delim, $header_str);
1624                     $msg_body   = str_replace("\r\n", $delim, $msg_body);
1625                     $to         = str_replace("\r\n", $delim, $to);
1626                     $subject    = str_replace("\r\n", $delim, $subject);
1627                 }
1628
39b905 1629                 if (filter_var(ini_get('safe_mode'), FILTER_VALIDATE_BOOLEAN))
0b9a7b 1630                     $sent = mail($to, $subject, $msg_body, $header_str);
TB 1631                 else
1632                     $sent = mail($to, $subject, $msg_body, $header_str, "-f$from");
1633             }
1634         }
1635
1636         if ($sent) {
1637             $this->plugins->exec_hook('message_sent', array('headers' => $headers, 'body' => $msg_body));
1638
1639             // remove MDN headers after sending
1640             unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']);
1641
1642             if ($this->config->get('smtp_log')) {
409b64 1643                 // get all recipient addresses
AM 1644                 if (is_array($mailto)) {
1645                     $mailto = implode(',', $mailto);
1646                 }
1647                 if ($headers['Cc']) {
1648                     $mailto .= ',' . $headers['Cc'];
1649                 }
1650                 if ($headers['Bcc']) {
1651                     $mailto .= ',' . $headers['Bcc'];
1652                 }
1653
1654                 $mailto = rcube_mime::decode_address_list($mailto, null, false, null, true);
1655
0b9a7b 1656                 self::write_log('sendmail', sprintf("User %s [%s]; Message for %s; %s",
TB 1657                     $this->user->get_username(),
409b64 1658                     rcube_utils::remote_addr(),
AM 1659                     implode(', ', $mailto),
0b9a7b 1660                     !empty($response) ? join('; ', $response) : ''));
TB 1661             }
1662         }
aef6ed 1663         else {
TB 1664             // allow plugins to catch sending errors with the same parameters as in 'message_before_send'
1665             $this->plugins->exec_hook('message_send_error', $plugin + array('error' => $error));
1666         }
0b9a7b 1667
TB 1668         if (is_resource($msg_body)) {
1669             fclose($msg_body);
1670         }
1671
a7efdd 1672         $message->headers($headers, true);
0b9a7b 1673
TB 1674         return $sent;
1675     }
6f249b 1676
AM 1677     /**
1678      * Return message headers as a string
1679      */
1680     protected function message_head($message, $unset = array())
1681     {
6a94f6 1682         // requires Mail_mime >= 1.9.0
AM 1683         $headers = array();
1684         foreach ((array) $unset as $header) {
1685             $headers[$header] = null;
6f249b 1686         }
AM 1687
1688         return $message->txtHeaders($headers, true);
1689     }
0c2596 1690 }
A 1691
1692
1693 /**
1694  * Lightweight plugin API class serving as a dummy if plugins are not enabled
1695  *
a95874 1696  * @package    Framework
a07224 1697  * @subpackage Core
0c2596 1698  */
A 1699 class rcube_dummy_plugin_api
1700 {
1701     /**
1702      * Triggers a plugin hook.
1703      * @see rcube_plugin_api::exec_hook()
1704      */
1705     public function exec_hook($hook, $args = array())
1706     {
1707         return $args;
1708     }
1709 }