From a3644638aaf0418598196a870204e0b632a4c8ad Mon Sep 17 00:00:00 2001 From: Thomas Bruederli <thomas@roundcube.net> Date: Fri, 17 Apr 2015 06:28:40 -0400 Subject: [PATCH] Allow preference sections to define CSS class names --- program/lib/Roundcube/rcube.php | 472 +++++++++++++++++++++++++++++++++++++++++++++------------- 1 files changed, 367 insertions(+), 105 deletions(-) diff --git a/program/lib/Roundcube/rcube.php b/program/lib/Roundcube/rcube.php index fbf9540..cf6ebf9 100644 --- a/program/lib/Roundcube/rcube.php +++ b/program/lib/Roundcube/rcube.php @@ -3,8 +3,8 @@ /* +-----------------------------------------------------------------------+ | This file is part of the Roundcube Webmail client | - | Copyright (C) 2008-2012, The Roundcube Dev Team | - | Copyright (C) 2011-2012, Kolab Systems AG | + | Copyright (C) 2008-2014, The Roundcube Dev Team | + | Copyright (C) 2011-2014, Kolab Systems AG | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | @@ -28,8 +28,14 @@ */ class rcube { - const INIT_WITH_DB = 1; + // Init options + const INIT_WITH_DB = 1; const INIT_WITH_PLUGINS = 2; + + // Request status + const REQUEST_VALID = 0; + const REQUEST_ERROR_URL = 1; + const REQUEST_ERROR_TOKEN = 2; /** * Singleton instace of rcube @@ -94,6 +100,19 @@ */ public $plugins; + /** + * Instance of rcube_user class. + * + * @var rcube_user + */ + public $user; + + /** + * Request status + * + * @var int + */ + public $request_status = 0; /* private/protected vars */ protected $texts; @@ -165,9 +184,13 @@ public function get_dbh() { if (!$this->db) { - $config_all = $this->config->all(); - $this->db = rcube_db::factory($config_all['db_dsnw'], $config_all['db_dsnr'], $config_all['db_persistent']); - $this->db->set_debug((bool)$config_all['sql_debug']); + $this->db = rcube_db::factory( + $this->config->get('db_dsnw'), + $this->config->get('db_dsnr'), + $this->config->get('db_persistent') + ); + + $this->db->set_debug((bool)$this->config->get('sql_debug')); } return $this->db; @@ -192,7 +215,10 @@ $this->mc_available = 0; // add all configured hosts to pool - $pconnect = $this->config->get('memcache_pconnect', true); + $pconnect = $this->config->get('memcache_pconnect', true); + $timeout = $this->config->get('memcache_timeout', 1); + $retry_interval = $this->config->get('memcache_retry_interval', 15); + foreach ($this->config->get('memcache_hosts', array()) as $host) { if (substr($host, 0, 7) != 'unix://') { list($host, $port) = explode(':', $host); @@ -203,7 +229,7 @@ } $this->mc_available += intval($this->memcache->addServer( - $host, $port, $pconnect, 1, 1, 15, false, array($this, 'memcache_failure'))); + $host, $port, $pconnect, 1, $timeout, $retry_interval, false, array($this, 'memcache_failure'))); } // test connection and failover (will result in $this->mc_available == 0 on complete failure) @@ -348,40 +374,18 @@ // for backward compat. (deprecated, will be removed) $this->imap = $this->storage; - // enable caching of mail data - $storage_cache = $this->config->get("{$driver}_cache"); - $messages_cache = $this->config->get('messages_cache'); - // for backward compatybility - if ($storage_cache === null && $messages_cache === null && $this->config->get('enable_caching')) { - $storage_cache = 'db'; - $messages_cache = true; - } - - if ($storage_cache) { - $this->storage->set_caching($storage_cache); - } - if ($messages_cache) { - $this->storage->set_messages_caching(true); - } - - // set pagesize from config - $pagesize = $this->config->get('mail_pagesize'); - if (!$pagesize) { - $pagesize = $this->config->get('pagesize', 50); - } - $this->storage->set_pagesize($pagesize); - // set class options $options = array( - 'auth_type' => $this->config->get("{$driver}_auth_type", 'check'), - 'auth_cid' => $this->config->get("{$driver}_auth_cid"), - 'auth_pw' => $this->config->get("{$driver}_auth_pw"), - 'debug' => (bool) $this->config->get("{$driver}_debug"), - 'force_caps' => (bool) $this->config->get("{$driver}_force_caps"), - 'disabled_caps' => $this->config->get("{$driver}_disabled_caps"), - 'timeout' => (int) $this->config->get("{$driver}_timeout"), - 'skip_deleted' => (bool) $this->config->get('skip_deleted'), - 'driver' => $driver, + 'auth_type' => $this->config->get("{$driver}_auth_type", 'check'), + 'auth_cid' => $this->config->get("{$driver}_auth_cid"), + 'auth_pw' => $this->config->get("{$driver}_auth_pw"), + 'debug' => (bool) $this->config->get("{$driver}_debug"), + 'force_caps' => (bool) $this->config->get("{$driver}_force_caps"), + 'disabled_caps' => $this->config->get("{$driver}_disabled_caps"), + 'socket_options' => $this->config->get("{$driver}_conn_options"), + 'timeout' => (int) $this->config->get("{$driver}_timeout"), + 'skip_deleted' => (bool) $this->config->get('skip_deleted'), + 'driver' => $driver, ); if (!empty($_SESSION['storage_host'])) { @@ -400,30 +404,87 @@ $this->storage->set_options($options); $this->set_storage_prop(); - } + // subscribe to 'storage_connected' hook for session logging + if ($this->config->get('imap_log_session', false)) { + $this->plugins->register_hook('storage_connected', array($this, 'storage_log_session')); + } + } /** * Set storage parameters. - * This must be done AFTER connecting to the server! */ protected function set_storage_prop() { $storage = $this->get_storage(); + // set pagesize from config + $pagesize = $this->config->get('mail_pagesize'); + if (!$pagesize) { + $pagesize = $this->config->get('pagesize', 50); + } + + $storage->set_pagesize($pagesize); $storage->set_charset($this->config->get('default_charset', RCUBE_CHARSET)); - if ($default_folders = $this->config->get('default_folders')) { - $storage->set_default_folders($default_folders); + // enable caching of mail data + $driver = $this->config->get('storage_driver', 'imap'); + $storage_cache = $this->config->get("{$driver}_cache"); + $messages_cache = $this->config->get('messages_cache'); + // for backward compatybility + if ($storage_cache === null && $messages_cache === null && $this->config->get('enable_caching')) { + $storage_cache = 'db'; + $messages_cache = true; } - if (isset($_SESSION['mbox'])) { - $storage->set_folder($_SESSION['mbox']); + + if ($storage_cache) { + $storage->set_caching($storage_cache); } - if (isset($_SESSION['page'])) { - $storage->set_page($_SESSION['page']); + if ($messages_cache) { + $storage->set_messages_caching(true); } } + + /** + * Set special folders type association. + * This must be done AFTER connecting to the server! + */ + protected function set_special_folders() + { + $storage = $this->get_storage(); + $folders = $storage->get_special_folders(true); + $prefs = array(); + + // check SPECIAL-USE flags on IMAP folders + foreach ($folders as $type => $folder) { + $idx = $type . '_mbox'; + if ($folder !== $this->config->get($idx)) { + $prefs[$idx] = $folder; + } + } + + // Some special folders differ, update user preferences + if (!empty($prefs) && $this->user) { + $this->user->save_prefs($prefs); + } + + // create default folders (on login) + if ($this->config->get('create_default_folders')) { + $storage->create_default_folders(); + } + } + + + /** + * Callback for IMAP connection events to log session identifiers + */ + public function storage_log_session($args) + { + if (!empty($args['session']) && session_id()) { + $this->write_log('imap_session', $args['session']); + } + } /** * Create session object and start the session. @@ -460,23 +521,15 @@ ini_set('session.use_only_cookies', 1); ini_set('session.cookie_httponly', 1); - // use database for storing session data - $this->session = new rcube_session($this->get_dbh(), $this->config); - + // get session driver instance + $this->session = rcube_session::factory($this->config); $this->session->register_gc_handler(array($this, 'gc')); - $this->session->set_secret($this->config->get('des_key') . dirname($_SERVER['SCRIPT_NAME'])); - $this->session->set_ip_check($this->config->get('ip_check')); - - if ($this->config->get('session_auth_name')) { - $this->session->set_cookiename($this->config->get('session_auth_name')); - } // start PHP session (if not in CLI mode) if ($_SERVER['REMOTE_ADDR']) { $this->session->start(); } } - /** * Garbage collector - cache/temp cleaner @@ -498,7 +551,14 @@ public function gc_temp() { $tmp = unslashify($this->config->get('temp_dir')); - $expire = time() - 172800; // expire in 48 hours + + // expire in 48 hours by default + $temp_dir_ttl = $this->config->get('temp_dir_ttl', '48h'); + $temp_dir_ttl = get_offset_sec($temp_dir_ttl); + if ($temp_dir_ttl < 6*3600) + $temp_dir_ttl = 6*3600; // 6 hours sensible lower bound. + + $expire = time() - $temp_dir_ttl; if ($tmp && ($dir = opendir($tmp))) { while (($fname = readdir($dir)) !== false) { @@ -635,10 +695,11 @@ /** * Load a localization package * - * @param string Language ID - * @param array Additional text labels/messages + * @param string $lang Language ID + * @param array $add Additional text labels/messages + * @param array $merge Additional text labels/messages to merge */ - public function load_language($lang = null, $add = array()) + public function load_language($lang = null, $add = array(), $merge = array()) { $lang = $this->language_prop(($lang ? $lang : $_SESSION['language'])); @@ -678,6 +739,11 @@ if (is_array($add) && !empty($add)) { $this->texts += $add; } + + // merge additional texts (from plugin) + if (is_array($merge) && !empty($merge)) { + $this->texts = array_merge($this->texts, $merge); + } } @@ -695,7 +761,11 @@ // user HTTP_ACCEPT_LANGUAGE if no language is specified if (empty($lang) || $lang == 'auto') { $accept_langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']); - $lang = str_replace('-', '_', $accept_langs[0]); + $lang = $accept_langs[0]; + + if (preg_match('/^([a-z]+)[_-]([a-z]+)$/i', $lang, $m)) { + $lang = $m[1] . '_' . strtoupper($m[2]); + } } if (empty($rcube_languages)) { @@ -779,12 +849,19 @@ * upon decryption; see http://php.net/mcrypt_generic#68082 */ $clear = pack("a*H2", $clear, "80"); + $ckey = $this->config->get_crypto_key($key); - if (function_exists('mcrypt_module_open') && + if (function_exists('openssl_encrypt')) { + $method = 'DES-EDE3-CBC'; + $opts = defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : true; + $iv = $this->create_iv(openssl_cipher_iv_length($method)); + $cipher = $iv . openssl_encrypt($clear, $method, $ckey, $opts, $iv); + } + else if (function_exists('mcrypt_module_open') && ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, "")) ) { $iv = $this->create_iv(mcrypt_enc_get_iv_size($td)); - mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv); + mcrypt_generic_init($td, $ckey, $iv); $cipher = $iv . mcrypt_generic($td, $clear); mcrypt_generic_deinit($td); mcrypt_module_close($td); @@ -795,13 +872,13 @@ if (function_exists('des')) { $des_iv_size = 8; $iv = $this->create_iv($des_iv_size); - $cipher = $iv . des($this->config->get_crypto_key($key), $clear, 1, 1, $iv); + $cipher = $iv . des($ckey, $clear, 1, 1, $iv); } else { self::raise_error(array( 'code' => 500, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Could not perform encryption; make sure Mcrypt is installed or lib/des.inc is available" + 'message' => "Could not perform encryption; make sure OpenSSL or Mcrypt or lib/des.inc is available" ), true, true); } } @@ -826,12 +903,13 @@ } $cipher = $base64 ? base64_decode($cipher) : $cipher; + $ckey = $this->config->get_crypto_key($key); - if (function_exists('mcrypt_module_open') && - ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, "")) - ) { - $iv_size = mcrypt_enc_get_iv_size($td); - $iv = substr($cipher, 0, $iv_size); + if (function_exists('openssl_decrypt')) { + $method = 'DES-EDE3-CBC'; + $opts = defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : true; + $iv_size = openssl_cipher_iv_length($method); + $iv = substr($cipher, 0, $iv_size); // session corruption? (#1485970) if (strlen($iv) < $iv_size) { @@ -839,7 +917,21 @@ } $cipher = substr($cipher, $iv_size); - mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv); + $clear = openssl_decrypt($cipher, $method, $ckey, $opts, $iv); + } + else if (function_exists('mcrypt_module_open') && + ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, "")) + ) { + $iv_size = mcrypt_enc_get_iv_size($td); + $iv = substr($cipher, 0, $iv_size); + + // session corruption? (#1485970) + if (strlen($iv) < $iv_size) { + return ''; + } + + $cipher = substr($cipher, $iv_size); + mcrypt_generic_init($td, $ckey, $iv); $clear = mdecrypt_generic($td, $cipher); mcrypt_generic_deinit($td); mcrypt_module_close($td); @@ -849,15 +941,15 @@ if (function_exists('des')) { $des_iv_size = 8; - $iv = substr($cipher, 0, $des_iv_size); + $iv = substr($cipher, 0, $des_iv_size); $cipher = substr($cipher, $des_iv_size); - $clear = des($this->config->get_crypto_key($key), $cipher, 0, 1, $iv); + $clear = des($ckey, $cipher, 0, 1, $iv); } else { self::raise_error(array( 'code' => 500, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Could not perform decryption; make sure Mcrypt is installed or lib/des.inc is available" + 'message' => "Could not perform decryption; make sure OpenSSL or Mcrypt or lib/des.inc is available" ), true, true); } } @@ -889,6 +981,104 @@ } return $iv; + } + + + /** + * Returns session token for secure URLs + * + * @param bool $generate Generate token if not exists in session yet + * + * @return string|bool Token string, False when disabled + */ + public function get_secure_url_token($generate = false) + { + if ($len = $this->config->get('use_secure_urls')) { + if (empty($_SESSION['secure_token']) && $generate) { + // generate x characters long token + $length = $len > 1 ? $len : 16; + $token = openssl_random_pseudo_bytes($length / 2); + $token = bin2hex($token); + + $plugin = $this->plugins->exec_hook('secure_token', + array('value' => $token, 'length' => $length)); + + $_SESSION['secure_token'] = $plugin['value']; + } + + return $_SESSION['secure_token']; + } + + return false; + } + + + /** + * Generate a unique token to be used in a form request + * + * @return string The request token + */ + public function get_request_token() + { + $sess_id = $_COOKIE[ini_get('session.name')]; + if (!$sess_id) { + $sess_id = session_id(); + } + + $plugin = $this->plugins->exec_hook('request_token', array( + 'value' => md5('RT' . $this->get_user_id() . $this->config->get('des_key') . $sess_id))); + + return $plugin['value']; + } + + + /** + * Check if the current request contains a valid token. + * Empty requests aren't checked until use_secure_urls is set. + * + * @param int Request method + * + * @return boolean True if request token is valid false if not + */ + public function check_request($mode = rcube_utils::INPUT_POST) + { + // check secure token in URL if enabled + if ($token = $this->get_secure_url_token()) { + foreach (explode('/', preg_replace('/[?#&].*$/', '', $_SERVER['REQUEST_URI'])) as $tok) { + if ($tok == $token) { + return true; + } + } + + $this->request_status = self::REQUEST_ERROR_URL; + + return false; + } + + $sess_tok = $this->get_request_token(); + + // ajax requests + if (rcube_utils::request_header('X-Roundcube-Request') == $sess_tok) { + return true; + } + + // skip empty requests + if (($mode == rcube_utils::INPUT_POST && empty($_POST)) + || ($mode == rcube_utils::INPUT_GET && empty($_GET)) + ) { + return true; + } + + // default method of securing requests + $token = rcube_utils::get_input_value('_token', $mode); + $sess_id = $_COOKIE[ini_get('session.name')]; + + if (empty($sess_id) || $token != $sess_tok) { + $this->request_status = self::REQUEST_ERROR_TOKEN; + return false; + } + + return true; } @@ -1069,8 +1259,12 @@ $line = var_export($line, true); } - $date_format = self::$instance ? self::$instance->config->get('log_date_format') : null; - $log_driver = self::$instance ? self::$instance->config->get('log_driver') : null; + $date_format = $log_driver = $session_key = null; + if (self::$instance) { + $date_format = self::$instance->config->get('log_date_format'); + $log_driver = self::$instance->config->get('log_driver'); + $session_key = intval(self::$instance->config->get('log_session_id', 8)); + } if (empty($date_format)) { $date_format = 'd-M-Y H:i:s O'; @@ -1088,6 +1282,11 @@ return true; } + // add session ID to the log + if ($session_key > 0 && ($sess = session_id())) { + $line = '<' . substr($sess, 0, $session_key) . '> ' . $line; + } + if ($log_driver == 'syslog') { $prio = $name == 'errors' ? LOG_ERR : LOG_INFO; syslog($prio, $line); @@ -1097,7 +1296,20 @@ // log_driver == 'file' is assumed here $line = sprintf("[%s]: %s\n", $date, $line); - $log_dir = self::$instance ? self::$instance->config->get('log_dir') : null; + $log_dir = null; + + // per-user logging is activated + if (self::$instance && self::$instance->config->get('per_user_logging', false) && self::$instance->get_user_id()) { + $log_dir = self::$instance->get_user_log_dir(); + if (empty($log_dir)) + return false; + } + else if (!empty($log['dir'])) { + $log_dir = $log['dir']; + } + else if (self::$instance) { + $log_dir = self::$instance->config->get('log_dir'); + } if (empty($log_dir)) { $log_dir = RCUBE_INSTALL_PATH . 'logs'; @@ -1125,8 +1337,8 @@ * - code: Error code (required) * - type: Error type [php|db|imap|javascript] (required) * - message: Error message - * - file: File where error occured - * - line: Line where error occured + * - file: File where error occurred + * - line: Line where error occurred * @param boolean True to log the error * @param boolean Terminate script execution */ @@ -1135,7 +1347,6 @@ // handle PHP exceptions if (is_object($arg) && is_a($arg, 'Exception')) { $arg = array( - 'type' => 'php', 'code' => $arg->getCode(), 'line' => $arg->getLine(), 'file' => $arg->getFile(), @@ -1143,7 +1354,7 @@ ); } else if (is_string($arg)) { - $arg = array('message' => $arg, 'type' => 'php'); + $arg = array('message' => $arg); } if (empty($arg['code'])) { @@ -1151,15 +1362,15 @@ } // installer - if (class_exists('rcube_install', false)) { - $rci = rcube_install::get_instance(); + if (class_exists('rcmail_install', false)) { + $rci = rcmail_install::get_instance(); $rci->raise_error($arg); return; } $cli = php_sapi_name() == 'cli'; - if (($log || $terminate) && !$cli && $arg['type'] && $arg['message']) { + if (($log || $terminate) && !$cli && $arg['message']) { $arg['fatal'] = $terminate; self::log_bug($arg); } @@ -1176,6 +1387,9 @@ exit(1); } + else if ($cli) { + fwrite(STDERR, 'ERROR: ' . $arg['message']); + } } @@ -1187,7 +1401,7 @@ */ public static function log_bug($arg_arr) { - $program = strtoupper($arg_arr['type']); + $program = strtoupper(!empty($arg_arr['type']) ? $arg_arr['type'] : 'php'); $level = self::get_instance()->config->get('debug_level'); // disable errors for ajax requests, write to log instead (#1487831) @@ -1273,6 +1487,20 @@ self::write_log($dest, sprintf("%s: %0.4f sec", $label, $diff)); } + /** + * Setter for system user object + * + * @param rcube_user Current user instance + */ + public function set_user($user) + { + if (is_object($user)) { + $this->user = $user; + + // overwrite config with user preferences + $this->config->set_user_prefs((array)$this->user->get_prefs()); + } + } /** * Getter for logged user ID. @@ -1336,6 +1564,17 @@ } } + /** + * Get the per-user log directory + */ + protected function get_user_log_dir() + { + $log_dir = $this->config->get('log_dir', RCUBE_INSTALL_PATH . 'logs'); + $user_name = $this->get_user_name(); + $user_log_dir = $log_dir . '/' . $user_name; + + return !empty($user_name) && is_writable($user_log_dir) ? $user_log_dir : false; + } /** * Getter for logged user language code. @@ -1397,6 +1636,17 @@ 'options' => $options, )); + if ($plugin['abort']) { + if (!empty($plugin['error'])) { + $error = $plugin['error']; + } + if (!empty($plugin['body_file'])) { + $body_file = $plugin['body_file']; + } + + return isset($plugin['result']) ? $plugin['result'] : false; + } + $from = $plugin['from']; $mailto = $plugin['mailto']; $options = $plugin['options']; @@ -1406,7 +1656,7 @@ // send thru SMTP server using custom SMTP library if ($this->config->get('smtp_server')) { // generate list of recipients - $a_recipients = array($mailto); + $a_recipients = (array) $mailto; if (strlen($headers['Cc'])) $a_recipients[] = $headers['Cc']; @@ -1423,15 +1673,18 @@ if ($message->getParam('delay_file_io')) { // use common temp dir - $temp_dir = $this->config->get('temp_dir'); - $body_file = tempnam($temp_dir, 'rcmMsg'); - if (PEAR::isError($mime_result = $message->saveMessageBody($body_file))) { + $temp_dir = $this->config->get('temp_dir'); + $body_file = tempnam($temp_dir, 'rcmMsg'); + $mime_result = $message->saveMessageBody($body_file); + + if (is_a($mime_result, 'PEAR_Error')) { self::raise_error(array('code' => 650, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Could not create message: ".$mime_result->getMessage()), - TRUE, FALSE); + true, false); return false; } + $msg_body = fopen($body_file, 'r'); } else { @@ -1451,7 +1704,7 @@ if (!$sent) { self::raise_error(array('code' => 800, 'type' => 'smtp', 'line' => __LINE__, 'file' => __FILE__, - 'message' => "SMTP error: ".join("\n", $response)), TRUE, FALSE); + 'message' => join("\n", $response)), true, false); } } // send mail using PHP's mail() function @@ -1474,11 +1727,11 @@ $msg_body = $message->get(); - if (PEAR::isError($msg_body)) { + if (is_a($msg_body, 'PEAR_Error')) { self::raise_error(array('code' => 650, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Could not create message: ".$msg_body->getMessage()), - TRUE, FALSE); + true, false); } else { $delim = $this->config->header_delimiter(); @@ -1506,22 +1759,31 @@ // remove MDN headers after sending unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']); - // get all recipients - if ($headers['Cc']) - $mailto .= $headers['Cc']; - if ($headers['Bcc']) - $mailto .= $headers['Bcc']; - if (preg_match_all('/<([^@]+@[^>]+)>/', $mailto, $m)) - $mailto = implode(', ', array_unique($m[1])); - if ($this->config->get('smtp_log')) { + // get all recipient addresses + if (is_array($mailto)) { + $mailto = implode(',', $mailto); + } + if ($headers['Cc']) { + $mailto .= ',' . $headers['Cc']; + } + if ($headers['Bcc']) { + $mailto .= ',' . $headers['Bcc']; + } + + $mailto = rcube_mime::decode_address_list($mailto, null, false, null, true); + self::write_log('sendmail', sprintf("User %s [%s]; Message for %s; %s", $this->user->get_username(), - $_SERVER['REMOTE_ADDR'], - $mailto, + rcube_utils::remote_addr(), + implode(', ', $mailto), !empty($response) ? join('; ', $response) : '')); } } + else { + // allow plugins to catch sending errors with the same parameters as in 'message_before_send' + $this->plugins->exec_hook('message_send_error', $plugin + array('error' => $error)); + } if (is_resource($msg_body)) { fclose($msg_body); -- Gitblit v1.9.1