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