Page MenuHomePhorge

No OneTemporary

Size
274 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/index.php b/index.php
index a7dd0fe53..e3c542ba2 100644
--- a/index.php
+++ b/index.php
@@ -1,273 +1,273 @@
<?php
/*
+-------------------------------------------------------------------------+
| RoundCube Webmail IMAP Client |
| Version 0.3-20090814 |
| |
| Copyright (C) 2005-2009, RoundCube Dev. - Switzerland |
| |
| This program is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License version 2 |
| as published by the Free Software Foundation. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License along |
| with this program; if not, write to the Free Software Foundation, Inc., |
| 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| |
+-------------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-------------------------------------------------------------------------+
$Id$
*/
// include environment
require_once 'program/include/iniset.php';
// init application and start session with requested task
$RCMAIL = rcmail::get_instance();
// init output class
$OUTPUT = !empty($_REQUEST['_remote']) ? $RCMAIL->init_json() : $RCMAIL->load_gui(!empty($_REQUEST['_framed']));
// init plugin API
$RCMAIL->plugins->init();
// turn on output buffering
ob_start();
// check if config files had errors
if ($err_str = $RCMAIL->config->get_error()) {
raise_error(array(
'code' => 601,
'type' => 'php',
'message' => $err_str), false, true);
}
// check DB connections and exit on failure
if ($err_str = $DB->is_error()) {
raise_error(array(
'code' => 603,
'type' => 'db',
'message' => $err_str), FALSE, TRUE);
}
// error steps
if ($RCMAIL->action=='error' && !empty($_GET['_code'])) {
raise_error(array('code' => hexdec($_GET['_code'])), FALSE, TRUE);
}
// check if https is required (for login) and redirect if necessary
if (empty($_SESSION['user_id']) && ($force_https = $RCMAIL->config->get('force_https', false))) {
$https_port = is_bool($force_https) ? 443 : $force_https;
if (!rcube_https_check($https_port)) {
$host = preg_replace('/:[0-9]+$/', '', $_SERVER['HTTP_HOST']);
$host .= ($https_port != 443 ? ':' . $https_port : '');
header('Location: https://' . $host . $_SERVER['REQUEST_URI']);
exit;
}
}
// trigger startup plugin hook
$startup = $RCMAIL->plugins->exec_hook('startup', array('task' => $RCMAIL->task, 'action' => $RCMAIL->action));
$RCMAIL->set_task($startup['task']);
$RCMAIL->action = $startup['action'];
// try to log in
if ($RCMAIL->action=='login' && $RCMAIL->task=='mail') {
// purge the session in case of new login when a session already exists
$RCMAIL->kill_session();
$auth = $RCMAIL->plugins->exec_hook('authenticate', array(
'host' => $RCMAIL->autoselect_host(),
'user' => trim(get_input_value('_user', RCUBE_INPUT_POST)),
'cookiecheck' => true,
)) + array('pass' => get_input_value('_pass', RCUBE_INPUT_POST, true, 'ISO-8859-1'));
// check if client supports cookies
if ($auth['cookiecheck'] && empty($_COOKIE)) {
$OUTPUT->show_message("cookiesdisabled", 'warning');
}
else if ($_SESSION['temp'] && !$auth['abort'] && !empty($auth['host']) &&
!empty($auth['user']) && isset($auth['pass']) &&
$RCMAIL->login($auth['user'], $auth['pass'], $auth['host'])) {
// create new session ID
rcube_sess_unset('temp');
rcube_sess_regenerate_id();
// send auth cookie if necessary
$RCMAIL->authenticate_session();
// log successful login
if ($RCMAIL->config->get('log_logins')) {
write_log('userlogins', sprintf('Successful login for %s (id %d) from %s',
$RCMAIL->user->get_username(),
$RCMAIL->user->ID,
$_SERVER['REMOTE_ADDR']));
}
-
+
// restore original request parameters
$query = array();
if ($url = get_input_value('_url', RCUBE_INPUT_POST))
parse_str($url, $query);
// allow plugins to control the redirect url after login success
$redir = $RCMAIL->plugins->exec_hook('login_after', $query + array('task' => $RCMAIL->task));
unset($redir['abort']);
// send redirect
$OUTPUT->redirect($redir);
}
else {
$OUTPUT->show_message($IMAP->error_code < -1 ? 'imaperror' : 'loginfailed', 'warning');
$RCMAIL->plugins->exec_hook('login_failed', array('code' => $IMAP->error_code, 'host' => $auth['host'], 'user' => $auth['user']));
$RCMAIL->kill_session();
}
}
// end session
else if ($RCMAIL->task=='logout' && isset($_SESSION['user_id'])) {
$userdata = array('user' => $_SESSION['username'], 'host' => $_SESSION['imap_host'], 'lang' => $RCMAIL->user->language);
$OUTPUT->show_message('loggedout');
$RCMAIL->logout_actions();
$RCMAIL->kill_session();
$RCMAIL->plugins->exec_hook('logout_after', $userdata);
}
// check session and auth cookie
else if ($RCMAIL->action != 'login' && $_SESSION['user_id'] && $RCMAIL->action != 'send') {
if (!$RCMAIL->authenticate_session()) {
$OUTPUT->show_message('sessionerror', 'error');
$RCMAIL->kill_session();
}
}
// don't check for valid request tokens in these actions
$request_check_whitelist = array('login'=>1, 'spell'=>1);
// check client X-header to verify request origin
if ($OUTPUT->ajax_call) {
if (!$RCMAIL->config->get('devel_mode') && rc_request_header('X-RoundCube-Request') != $RCMAIL->get_request_token() && !empty($RCMAIL->user->ID)) {
header('HTTP/1.1 404 Not Found');
die("Invalid Request");
}
}
// check request token in POST form submissions
else if (!empty($_POST) && !$request_check_whitelist[$RCMAIL->action] && !$RCMAIL->check_request()) {
$OUTPUT->show_message('invalidrequest', 'error');
$OUTPUT->send($RCMAIL->task);
}
// not logged in -> show login page
if (empty($RCMAIL->user->ID)) {
if ($OUTPUT->ajax_call)
$OUTPUT->redirect(array(), 2000);
if (!empty($_REQUEST['_framed']))
$OUTPUT->command('redirect', '?');
// check if installer is still active
if ($RCMAIL->config->get('enable_installer') && is_readable('./installer/index.php')) {
$OUTPUT->add_footer(html::div(array('style' => "background:#ef9398; border:2px solid #dc5757; padding:0.5em; margin:2em auto; width:50em"),
html::tag('h2', array('style' => "margin-top:0.2em"), "Installer script is still accessible") .
html::p(null, "The install script of your RoundCube installation is still stored in its default location!") .
html::p(null, "Please <b>remove</b> the whole <tt>installer</tt> folder from the RoundCube directory because .
these files may expose sensitive configuration data like server passwords and encryption keys
to the public. Make sure you cannot access the <a href=\"./installer/\">installer script</a> from your browser.")
)
);
}
$OUTPUT->set_env('task', 'login');
$OUTPUT->send('login');
}
// handle keep-alive signal
if ($RCMAIL->action == 'keep-alive') {
$OUTPUT->reset();
$OUTPUT->send();
}
// save preference value
else if ($RCMAIL->action == 'save-pref') {
$RCMAIL->user->save_prefs(array(get_input_value('_name', RCUBE_INPUT_POST) => get_input_value('_value', RCUBE_INPUT_POST)));
$OUTPUT->reset();
$OUTPUT->send();
}
// map task/action to a certain include file
$action_map = array(
'mail' => array(
'preview' => 'show.inc',
'print' => 'show.inc',
'moveto' => 'move_del.inc',
'delete' => 'move_del.inc',
'send' => 'sendmail.inc',
'expunge' => 'folders.inc',
'purge' => 'folders.inc',
'remove-attachment' => 'attachments.inc',
'display-attachment' => 'attachments.inc',
'upload' => 'attachments.inc',
),
'addressbook' => array(
'add' => 'edit.inc',
),
'settings' => array(
'folders' => 'manage_folders.inc',
'create-folder' => 'manage_folders.inc',
'rename-folder' => 'manage_folders.inc',
'delete-folder' => 'manage_folders.inc',
'subscribe' => 'manage_folders.inc',
'unsubscribe' => 'manage_folders.inc',
'add-identity' => 'edit_identity.inc',
)
);
// include task specific functions
if (is_file($incfile = 'program/steps/'.$RCMAIL->task.'/func.inc'))
include_once($incfile);
// allow 5 "redirects" to another action
$redirects = 0; $incstep = null;
while ($redirects < 5) {
$stepfile = !empty($action_map[$RCMAIL->task][$RCMAIL->action]) ?
$action_map[$RCMAIL->task][$RCMAIL->action] : strtr($RCMAIL->action, '-', '_') . '.inc';
// execute a plugin action
if (preg_match('/^plugin\./', $RCMAIL->action)) {
$RCMAIL->plugins->exec_action($RCMAIL->action);
break;
}
// try to include the step file
else if (is_file($incfile = 'program/steps/'.$RCMAIL->task.'/'.$stepfile)) {
include($incfile);
$redirects++;
}
else {
break;
}
}
// parse main template (default)
$OUTPUT->send($RCMAIL->task);
// if we arrive here, something went wrong
raise_error(array(
'code' => 404,
'type' => 'php',
'line' => __LINE__,
'file' => __FILE__,
'message' => "Invalid request"), true, true);
?>
diff --git a/program/include/rcmail.php b/program/include/rcmail.php
index 28ccb0cb0..1afb7d72a 100644
--- a/program/include/rcmail.php
+++ b/program/include/rcmail.php
@@ -1,1060 +1,1057 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcmail.php |
| |
| This file is part of the RoundCube Webmail client |
| Copyright (C) 2008-2009, RoundCube Dev. - Switzerland |
| Licensed under the GNU GPL |
| |
| PURPOSE: |
| Application class providing core functions and holding |
| instances of all 'global' objects like db- and imap-connections |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
$Id$
*/
/**
* Application class of RoundCube Webmail
* implemented as singleton
*
* @package Core
*/
class rcmail
{
static public $main_tasks = array('mail','settings','addressbook','login','logout','dummy');
static private $instance;
public $config;
public $user;
public $db;
public $smtp;
public $imap;
public $output;
public $plugins;
public $task = 'mail';
public $action = '';
public $comm_path = './';
private $texts;
/**
* This implements the 'singleton' design pattern
*
* @return object rcmail The one and only instance
*/
static function get_instance()
{
if (!self::$instance) {
self::$instance = new rcmail();
self::$instance->startup(); // init AFTER object was linked with self::$instance
}
return self::$instance;
}
/**
* Private constructor
*/
private function __construct()
{
// load configuration
$this->config = new rcube_config();
register_shutdown_function(array($this, 'shutdown'));
}
/**
* Initial startup function
* to register session, create database and imap connections
*
* @todo Remove global vars $DB, $USER
*/
private function startup()
{
$config_all = $this->config->all();
// initialize syslog
if ($this->config->get('log_driver') == 'syslog') {
$syslog_id = $this->config->get('syslog_id', 'roundcube');
$syslog_facility = $this->config->get('syslog_facility', LOG_USER);
openlog($syslog_id, LOG_ODELAY, $syslog_facility);
}
// set task and action properties
$this->set_task(get_input_value('_task', RCUBE_INPUT_GPC));
$this->action = asciiwords(get_input_value('_action', RCUBE_INPUT_GPC));
// connect to database
$GLOBALS['DB'] = $this->get_dbh();
// use database for storing session data
include_once('include/session.inc');
// set session domain
if (!empty($config_all['session_domain'])) {
ini_set('session.cookie_domain', $config_all['session_domain']);
}
// set session garbage collecting time according to session_lifetime
if (!empty($config_all['session_lifetime'])) {
ini_set('session.gc_maxlifetime', ($config_all['session_lifetime']) * 120);
}
// start PHP session (if not in CLI mode)
if ($_SERVER['REMOTE_ADDR'])
session_start();
// set initial session vars
if (!isset($_SESSION['auth_time'])) {
$_SESSION['auth_time'] = time();
$_SESSION['temp'] = true;
}
// create user object
$this->set_user(new rcube_user($_SESSION['user_id']));
// reset some session parameters when changing task
if ($_SESSION['task'] != $this->task)
rcube_sess_unset('page');
// set current task to session
$_SESSION['task'] = $this->task;
// create IMAP object
if ($this->task == 'mail')
$this->imap_init();
// create plugin API and load plugins
$this->plugins = rcube_plugin_api::get_instance();
}
/**
* Setter for application task
*
* @param string Task to set
*/
public function set_task($task)
{
$task = asciiwords($task);
$this->task = $task ? $task : 'mail';
$this->comm_path = $this->url(array('task' => $this->task));
if ($this->output)
$this->output->set_env('task', $this->task);
}
/**
* Setter for system user object
*
* @param object rcube_user Current user instance
*/
public function set_user($user)
{
if (is_object($user)) {
$this->user = $user;
$GLOBALS['USER'] = $this->user;
// overwrite config with user preferences
$this->config->merge((array)$this->user->get_prefs());
}
$_SESSION['language'] = $this->user->language = $this->language_prop($this->config->get('language', $_SESSION['language']));
// set localization
setlocale(LC_ALL, $_SESSION['language'] . '.utf8', 'en_US.utf8');
// workaround for http://bugs.php.net/bug.php?id=18556
if (in_array($_SESSION['language'], array('tr_TR', 'ku', 'az_AZ')))
setlocale(LC_CTYPE, 'en_US' . '.utf8');
}
/**
* Check the given string and return a valid language code
*
* @param string Language code
* @return string Valid language code
*/
private function language_prop($lang)
{
static $rcube_languages, $rcube_language_aliases;
// 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]);
}
if (empty($rcube_languages)) {
@include(INSTALL_PATH . 'program/localization/index.inc');
}
// check if we have an alias for that language
if (!isset($rcube_languages[$lang]) && isset($rcube_language_aliases[$lang])) {
$lang = $rcube_language_aliases[$lang];
}
// try the first two chars
else if (!isset($rcube_languages[$lang])) {
$short = substr($lang, 0, 2);
// check if we have an alias for the short language code
if (!isset($rcube_languages[$short]) && isset($rcube_language_aliases[$short])) {
$lang = $rcube_language_aliases[$short];
}
// expand 'nn' to 'nn_NN'
else if (!isset($rcube_languages[$short])) {
$lang = $short.'_'.strtoupper($short);
}
}
if (!isset($rcube_languages[$lang]) || !is_dir(INSTALL_PATH . 'program/localization/' . $lang)) {
$lang = 'en_US';
}
return $lang;
}
/**
* Get the current database connection
*
* @return object rcube_mdb2 Database connection object
*/
public function get_dbh()
{
if (!$this->db) {
$config_all = $this->config->all();
$this->db = new rcube_mdb2($config_all['db_dsnw'], $config_all['db_dsnr'], $config_all['db_persistent']);
$this->db->sqlite_initials = INSTALL_PATH . 'SQL/sqlite.initial.sql';
$this->db->set_debug((bool)$config_all['sql_debug']);
$this->db->db_connect('w');
}
return $this->db;
}
/**
* Return instance of the internal address book class
*
* @param boolean True if the address book needs to be writeable
* @return object rcube_contacts Address book object
*/
public function get_address_book($id, $writeable = false)
{
$contacts = null;
$ldap_config = (array)$this->config->get('ldap_public');
$abook_type = strtolower($this->config->get('address_book_type'));
$plugin = $this->plugins->exec_hook('get_address_book', array('id' => $id, 'writeable' => $writeable));
// plugin returned instance of a rcube_addressbook
if ($plugin['instance'] instanceof rcube_addressbook) {
$contacts = $plugin['instance'];
}
else if ($id && $ldap_config[$id]) {
$contacts = new rcube_ldap($ldap_config[$id], $this->config->get('ldap_debug'), $this->config->mail_domain($_SESSION['imap_host']));
}
else if ($id === '0') {
$contacts = new rcube_contacts($this->db, $this->user->ID);
}
else if ($abook_type == 'ldap') {
// Use the first writable LDAP address book.
foreach ($ldap_config as $id => $prop) {
if (!$writeable || $prop['writable']) {
$contacts = new rcube_ldap($prop, $this->config->get('ldap_debug'), $this->config->mail_domain($_SESSION['imap_host']));
break;
}
}
}
else {
$contacts = new rcube_contacts($this->db, $this->user->ID);
}
return $contacts;
}
/**
* Init output object for GUI and add common scripts.
* This will instantiate a rcmail_template object and set
* environment vars according to the current session and configuration
*
* @param boolean True if this request is loaded in a (i)frame
* @return object rcube_template Reference to HTML output object
*/
public function load_gui($framed = false)
{
// init output page
if (!($this->output instanceof rcube_template))
$this->output = new rcube_template($this->task, $framed);
// set keep-alive/check-recent interval
if ($keep_alive = $this->config->get('keep_alive')) {
// be sure that it's less than session lifetime
if ($session_lifetime = $this->config->get('session_lifetime'))
$keep_alive = min($keep_alive, $session_lifetime * 60 - 30);
$this->output->set_env('keep_alive', max(60, $keep_alive));
}
if ($framed) {
$this->comm_path .= '&_framed=1';
$this->output->set_env('framed', true);
}
$this->output->set_env('task', $this->task);
$this->output->set_env('action', $this->action);
$this->output->set_env('comm_path', $this->comm_path);
$this->output->set_charset(RCMAIL_CHARSET);
// add some basic label to client
$this->output->add_label('loading', 'servererror');
return $this->output;
}
/**
* Create an output object for JSON responses
*
* @return object rcube_json_output Reference to JSON output object
*/
public function init_json()
{
if (!($this->output instanceof rcube_json_output))
$this->output = new rcube_json_output($this->task);
return $this->output;
}
/**
* Create SMTP object and connect to server
*
* @param boolean True if connection should be established
*/
public function smtp_init($connect = false)
{
$this->smtp = new rcube_smtp();
if ($connect)
$this->smtp->connect();
}
/**
* Create global IMAP object and connect to server
*
* @param boolean True if connection should be established
* @todo Remove global $IMAP
*/
public function imap_init($connect = false)
{
$this->imap = new rcube_imap($this->db);
$this->imap->debug_level = $this->config->get('debug_level');
$this->imap->skip_deleted = $this->config->get('skip_deleted');
$this->imap->index_sort = $this->config->get('index_sort', true);
// enable caching of imap data
if ($this->config->get('enable_caching')) {
$this->imap->set_caching(true);
}
// set pagesize from config
$this->imap->set_pagesize($this->config->get('pagesize', 50));
// Setting root and delimiter before iil_Connect can save time detecting them
// using NAMESPACE and LIST
$options = array(
'auth_method' => $this->config->get('imap_auth_type', 'check'),
'delimiter' => isset($_SESSION['imap_delimiter']) ? $_SESSION['imap_delimiter'] : $this->config->get('imap_delimiter'),
'rootdir' => isset($_SESSION['imap_root']) ? $_SESSION['imap_root'] : $this->config->get('imap_root'),
'debug_mode' => (bool) $this->config->get('imap_debug', 0),
);
$this->imap->set_options($options);
// set global object for backward compatibility
$GLOBALS['IMAP'] = $this->imap;
if ($connect)
$this->imap_connect();
}
/**
* Connect to IMAP server with stored session data
*
* @return bool True on success, false on error
*/
public function imap_connect()
{
$conn = false;
if ($_SESSION['imap_host'] && !$this->imap->conn) {
if (!($conn = $this->imap->connect($_SESSION['imap_host'], $_SESSION['username'], $this->decrypt($_SESSION['password']), $_SESSION['imap_port'], $_SESSION['imap_ssl']))) {
if ($this->output)
$this->output->show_message($this->imap->error_code == -1 ? 'imaperror' : 'sessionerror', 'error');
}
$this->set_imap_prop();
}
return $conn;
}
/**
* Perfom login to the IMAP server and to the webmail service.
* This will also create a new user entry if auto_create_user is configured.
*
* @param string IMAP user name
* @param string IMAP password
* @param string IMAP host
* @return boolean True on success, False on failure
*/
function login($username, $pass, $host=NULL)
{
$user = NULL;
$config = $this->config->all();
if (!$host)
$host = $config['default_host'];
// Validate that selected host is in the list of configured hosts
if (is_array($config['default_host'])) {
$allowed = false;
foreach ($config['default_host'] as $key => $host_allowed) {
if (!is_numeric($key))
$host_allowed = $key;
if ($host == $host_allowed) {
$allowed = true;
break;
}
}
if (!$allowed)
return false;
}
else if (!empty($config['default_host']) && $host != $config['default_host'])
return false;
// parse $host URL
$a_host = parse_url($host);
if ($a_host['host']) {
$host = $a_host['host'];
$imap_ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null;
if(!empty($a_host['port']))
$imap_port = $a_host['port'];
else if ($imap_ssl && $imap_ssl != 'tls')
$imap_port = 993;
}
$imap_port = $imap_port ? $imap_port : $config['default_port'];
/* Modify username with domain if required
Inspired by Marco <P0L0_notspam_binware.org>
*/
// Check if we need to add domain
if (!empty($config['username_domain']) && !strpos($username, '@')) {
if (is_array($config['username_domain']) && isset($config['username_domain'][$host]))
$username .= '@'.$config['username_domain'][$host];
else if (is_string($config['username_domain']))
$username .= '@'.$config['username_domain'];
}
// try to resolve email address from virtuser table
if (strpos($username, '@'))
if ($virtuser = rcube_user::email2user($username))
$username = $virtuser;
// lowercase username if it's an e-mail address (#1484473)
if (strpos($username, '@'))
$username = mb_strtolower($username);
// user already registered -> overwrite username
if ($user = rcube_user::query($username, $host))
$username = $user->data['username'];
// exit if IMAP login failed
if (!($imap_login = $this->imap->connect($host, $username, $pass, $imap_port, $imap_ssl)))
return false;
// user already registered -> update user's record
if (is_object($user)) {
$user->touch();
}
// create new system user
else if ($config['auto_create_user']) {
if ($created = rcube_user::create($username, $host)) {
$user = $created;
// get existing mailboxes (but why?)
// $a_mailboxes = $this->imap->list_mailboxes();
}
else {
raise_error(array(
- 'code' => 600,
- 'type' => 'php',
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to create a user record. Maybe aborted by a plugin?"
- ), true, false);
+ ), true, false);
}
}
else {
raise_error(array(
- 'code' => 600,
- 'type' => 'php',
- 'file' => RCMAIL_CONFIG_DIR."/main.inc.php",
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
'message' => "Acces denied for new user $username. 'auto_create_user' is disabled"
), true, false);
}
// login succeeded
if (is_object($user) && $user->ID) {
$this->set_user($user);
// set session vars
$_SESSION['user_id'] = $user->ID;
$_SESSION['username'] = $user->data['username'];
$_SESSION['imap_host'] = $host;
$_SESSION['imap_port'] = $imap_port;
$_SESSION['imap_ssl'] = $imap_ssl;
$_SESSION['password'] = $this->encrypt($pass);
$_SESSION['login_time'] = mktime();
if ($_REQUEST['_timezone'] != '_default_')
$_SESSION['timezone'] = floatval($_REQUEST['_timezone']);
// force reloading complete list of subscribed mailboxes
$this->set_imap_prop();
$this->imap->clear_cache('mailboxes');
if ($config['create_default_folders'])
$this->imap->create_default_folders();
return true;
}
return false;
}
/**
* Set root dir and last stored mailbox
* This must be done AFTER connecting to the server!
*/
public function set_imap_prop()
{
$this->imap->set_charset($this->config->get('default_charset', RCMAIL_CHARSET));
if ($default_folders = $this->config->get('default_imap_folders')) {
$this->imap->set_default_mailboxes($default_folders);
}
if (!empty($_SESSION['mbox'])) {
$this->imap->set_mailbox($_SESSION['mbox']);
}
if (isset($_SESSION['page'])) {
$this->imap->set_page($_SESSION['page']);
}
// cache IMAP root and delimiter in session for performance reasons
$_SESSION['imap_root'] = $this->imap->root_dir;
$_SESSION['imap_delimiter'] = $this->imap->delimiter;
}
/**
* Auto-select IMAP host based on the posted login information
*
* @return string Selected IMAP host
*/
public function autoselect_host()
{
$default_host = $this->config->get('default_host');
$host = null;
if (is_array($default_host)) {
$post_host = get_input_value('_host', RCUBE_INPUT_POST);
// direct match in default_host array
if ($default_host[$post_host] || in_array($post_host, array_values($default_host))) {
$host = $post_host;
}
// try to select host by mail domain
list($user, $domain) = explode('@', get_input_value('_user', RCUBE_INPUT_POST));
if (!empty($domain)) {
foreach ($default_host as $imap_host => $mail_domains) {
if (is_array($mail_domains) && in_array($domain, $mail_domains)) {
$host = $imap_host;
break;
}
}
}
// take the first entry if $host is still an array
if (empty($host)) {
$host = array_shift($default_host);
}
}
else if (empty($default_host)) {
$host = get_input_value('_host', RCUBE_INPUT_POST);
}
else
$host = $default_host;
return $host;
}
/**
* Get localized text in the desired language
*
* @param mixed Named parameters array or label name
* @return string Localized text
*/
public function gettext($attrib, $domain=null)
{
// load localization files if not done yet
if (empty($this->texts))
$this->load_language();
// extract attributes
if (is_string($attrib))
$attrib = array('name' => $attrib);
$nr = is_numeric($attrib['nr']) ? $attrib['nr'] : 1;
$name = $attrib['name'] ? $attrib['name'] : '';
// check for text with domain
if ($domain && ($text_item = $this->texts[$domain.'.'.$name]))
;
// text does not exist
else if (!($text_item = $this->texts[$name])) {
return "[$name]";
}
// make text item array
$a_text_item = is_array($text_item) ? $text_item : array('single' => $text_item);
// decide which text to use
if ($nr == 1) {
$text = $a_text_item['single'];
}
else if ($nr > 0) {
$text = $a_text_item['multiple'];
}
else if ($nr == 0) {
if ($a_text_item['none'])
$text = $a_text_item['none'];
else if ($a_text_item['single'])
$text = $a_text_item['single'];
else if ($a_text_item['multiple'])
$text = $a_text_item['multiple'];
}
// default text is single
if ($text == '') {
$text = $a_text_item['single'];
}
// replace vars in text
if (is_array($attrib['vars'])) {
foreach ($attrib['vars'] as $var_key => $var_value)
$text = str_replace($var_key[0]!='$' ? '$'.$var_key : $var_key, $var_value, $text);
}
// format output
if (($attrib['uppercase'] && strtolower($attrib['uppercase']=='first')) || $attrib['ucfirst'])
return ucfirst($text);
else if ($attrib['uppercase'])
return strtoupper($text);
else if ($attrib['lowercase'])
return strtolower($text);
return $text;
}
/**
* Load a localization package
*
* @param string Language ID
*/
public function load_language($lang = null, $add = array())
{
$lang = $this->language_prop(($lang ? $lang : $_SESSION['language']));
// load localized texts
if (empty($this->texts) || $lang != $_SESSION['language']) {
$this->texts = array();
// get english labels (these should be complete)
@include(INSTALL_PATH . 'program/localization/en_US/labels.inc');
@include(INSTALL_PATH . 'program/localization/en_US/messages.inc');
if (is_array($labels))
$this->texts = $labels;
if (is_array($messages))
$this->texts = array_merge($this->texts, $messages);
// include user language files
if ($lang != 'en' && is_dir(INSTALL_PATH . 'program/localization/' . $lang)) {
include_once(INSTALL_PATH . 'program/localization/' . $lang . '/labels.inc');
include_once(INSTALL_PATH . 'program/localization/' . $lang . '/messages.inc');
if (is_array($labels))
$this->texts = array_merge($this->texts, $labels);
if (is_array($messages))
$this->texts = array_merge($this->texts, $messages);
}
$_SESSION['language'] = $lang;
}
// append additional texts (from plugin)
if (is_array($add) && !empty($add))
$this->texts += $add;
}
/**
* Read directory program/localization and return a list of available languages
*
* @return array List of available localizations
*/
public function list_languages()
{
static $sa_languages = array();
if (!sizeof($sa_languages)) {
@include(INSTALL_PATH . 'program/localization/index.inc');
if ($dh = @opendir(INSTALL_PATH . 'program/localization')) {
while (($name = readdir($dh)) !== false) {
if ($name{0}=='.' || !is_dir(INSTALL_PATH . 'program/localization/' . $name))
continue;
if ($label = $rcube_languages[$name])
$sa_languages[$name] = $label;
}
closedir($dh);
}
}
return $sa_languages;
}
/**
* Check the auth hash sent by the client against the local session credentials
*
* @return boolean True if valid, False if not
*/
function authenticate_session()
{
global $SESS_CLIENT_IP, $SESS_CHANGED;
// advanced session authentication
if ($this->config->get('double_auth')) {
$now = time();
$valid = ($_COOKIE['sessauth'] == $this->get_auth_hash(session_id(), $_SESSION['auth_time']) ||
$_COOKIE['sessauth'] == $this->get_auth_hash(session_id(), $_SESSION['last_auth']));
// renew auth cookie every 5 minutes (only for GET requests)
if (!$valid || ($_SERVER['REQUEST_METHOD']!='POST' && $now - $_SESSION['auth_time'] > 300)) {
$_SESSION['last_auth'] = $_SESSION['auth_time'];
$_SESSION['auth_time'] = $now;
rcmail::setcookie('sessauth', $this->get_auth_hash(session_id(), $now), 0);
}
}
else {
$valid = $this->config->get('ip_check') ? $_SERVER['REMOTE_ADDR'] == $SESS_CLIENT_IP : true;
}
// check session filetime
$lifetime = $this->config->get('session_lifetime');
if (!empty($lifetime) && isset($SESS_CHANGED) && $SESS_CHANGED + $lifetime*60 < time()) {
$valid = false;
}
return $valid;
}
/**
* Destroy session data and remove cookie
*/
public function kill_session()
{
$this->plugins->exec_hook('kill_session');
rcube_sess_unset();
$_SESSION = array('language' => $this->user->language, 'auth_time' => time(), 'temp' => true);
rcmail::setcookie('sessauth', '-del-', time() - 60);
$this->user->reset();
}
/**
* Do server side actions on logout
*/
public function logout_actions()
{
$config = $this->config->all();
// on logout action we're not connected to imap server
if (($config['logout_purge'] && !empty($config['trash_mbox'])) || $config['logout_expunge']) {
if (!$this->authenticate_session())
return;
$this->imap_init(true);
}
if ($config['logout_purge'] && !empty($config['trash_mbox'])) {
$this->imap->clear_mailbox($config['trash_mbox']);
}
if ($config['logout_expunge']) {
$this->imap->expunge('INBOX');
}
}
/**
* Function to be executed in script shutdown
* Registered with register_shutdown_function()
*/
public function shutdown()
{
if (is_object($this->imap)) {
$this->imap->close();
$this->imap->write_cache();
}
if (is_object($this->smtp))
$this->smtp->disconnect();
if (is_object($this->contacts))
$this->contacts->close();
// before closing the database connection, write session data
if ($_SERVER['REMOTE_ADDR'])
session_write_close();
}
/**
* Generate a unique token to be used in a form request
*
* @return string The request token
*/
public function get_request_token()
{
$key = $this->task;
if (!$_SESSION['request_tokens'][$key])
$_SESSION['request_tokens'][$key] = md5(uniqid($key . mt_rand(), true));
return $_SESSION['request_tokens'][$key];
}
/**
* Check if the current request contains a valid token
*
* @param int Request method
* @return boolean True if request token is valid false if not
*/
public function check_request($mode = RCUBE_INPUT_POST)
{
$token = get_input_value('_token', $mode);
return !empty($token) && $_SESSION['request_tokens'][$this->task] == $token;
}
/**
* Create unique authorization hash
*
* @param string Session ID
* @param int Timestamp
* @return string The generated auth hash
*/
private function get_auth_hash($sess_id, $ts)
{
$auth_string = sprintf('rcmail*sess%sR%s*Chk:%s;%s',
$sess_id,
$ts,
$this->config->get('ip_check') ? $_SERVER['REMOTE_ADDR'] : '***.***.***.***',
$_SERVER['HTTP_USER_AGENT']);
if (function_exists('sha1'))
return sha1($auth_string);
else
return md5($auth_string);
}
/**
* Encrypt using 3DES
*
* @param string $clear clear text input
* @param string $key encryption key to retrieve from the configuration, defaults to 'des_key'
* @param boolean $base64 whether or not to base64_encode() the result before returning
*
* @return string encrypted text
*/
public function encrypt($clear, $key = 'des_key', $base64 = true)
{
if (!$clear)
return '';
/*-
* Add a single canary byte to the end of the clear text, which
* will help find out how much of padding will need to be removed
* upon decryption; see http://php.net/mcrypt_generic#68082
*/
$clear = pack("a*H2", $clear, "80");
if (function_exists('mcrypt_module_open') &&
($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, "")))
{
$iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($td), MCRYPT_RAND);
mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv);
$cipher = $iv . mcrypt_generic($td, $clear);
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
}
else if (function_exists('des'))
{
define('DES_IV_SIZE', 8);
$iv = '';
for ($i = 0; $i < constant('DES_IV_SIZE'); $i++)
$iv .= sprintf("%c", mt_rand(0, 255));
$cipher = $iv . des($this->config->get_crypto_key($key), $clear, 1, 1, $iv);
}
else
{
raise_error(array(
- 'code' => 500,
- 'type' => 'php',
- 'file' => __FILE__,
+ 'code' => 500, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not perform encryption; make sure Mcrypt is installed or lib/des.inc is available"
), true, true);
}
return $base64 ? base64_encode($cipher) : $cipher;
}
/**
* Decrypt 3DES-encrypted string
*
* @param string $cipher encrypted text
* @param string $key encryption key to retrieve from the configuration, defaults to 'des_key'
* @param boolean $base64 whether or not input is base64-encoded
*
* @return string decrypted text
*/
public function decrypt($cipher, $key = 'des_key', $base64 = true)
{
if (!$cipher)
return '';
$cipher = $base64 ? base64_decode($cipher) : $cipher;
if (function_exists('mcrypt_module_open') &&
($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, "")))
{
$iv = substr($cipher, 0, mcrypt_enc_get_iv_size($td));
$cipher = substr($cipher, mcrypt_enc_get_iv_size($td));
mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv);
$clear = mdecrypt_generic($td, $cipher);
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
}
else if (function_exists('des'))
{
define('DES_IV_SIZE', 8);
$iv = substr($cipher, 0, constant('DES_IV_SIZE'));
$cipher = substr($cipher, constant('DES_IV_SIZE'));
$clear = des($this->config->get_crypto_key($key), $cipher, 0, 1, $iv);
}
else
{
raise_error(array(
- 'code' => 500,
- 'type' => 'php',
- 'file' => __FILE__,
+ 'code' => 500, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not perform decryption; make sure Mcrypt is installed or lib/des.inc is available"
), true, true);
}
/*-
* Trim PHP's padding and the canary byte; see note in
* rcmail::encrypt() and http://php.net/mcrypt_generic#68082
*/
$clear = substr(rtrim($clear, "\0"), 0, -1);
return $clear;
}
/**
* Build a valid URL to this instance of RoundCube
*
* @param mixed Either a string with the action or url parameters as key-value pairs
* @return string Valid application URL
*/
public function url($p)
{
if (!is_array($p))
$p = array('_action' => @func_get_arg(0));
$task = $p['_task'] ? $p['_task'] : ($p['task'] ? $p['task'] : $this->task);
$p['_task'] = $task;
unset($p['task']);
$url = './';
$delm = '?';
foreach (array_reverse($p) as $key => $val)
{
if (!empty($val)) {
$par = $key[0] == '_' ? $key : '_'.$key;
$url .= $delm.urlencode($par).'='.urlencode($val);
$delm = '&';
}
}
return $url;
}
/**
* Helper method to set a cookie with the current path and host settings
*
* @param string Cookie name
* @param string Cookie value
* @param string Expiration time
*/
public static function setcookie($name, $value, $exp = 0)
{
if (headers_sent())
return;
$cookie = session_get_cookie_params();
setcookie($name, $value, $exp, $cookie['path'], $cookie['domain'],
rcube_https_check(), true);
}
}
diff --git a/program/include/rcube_config.php b/program/include/rcube_config.php
index a4f3699d8..85ff714bf 100644
--- a/program/include/rcube_config.php
+++ b/program/include/rcube_config.php
@@ -1,283 +1,282 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcube_config.php |
| |
| This file is part of the RoundCube Webmail client |
| Copyright (C) 2008-2009, RoundCube Dev. - Switzerland |
| Licensed under the GNU GPL |
| |
| PURPOSE: |
| Class to read configuration settings |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
$Id$
*/
/**
* Configuration class for RoundCube
*
* @package Core
*/
class rcube_config
{
private $prop = array();
private $errors = array();
/**
* Object constructor
*/
public function __construct()
{
$this->load();
}
/**
* Load config from local config file
*
* @todo Remove global $CONFIG
*/
private function load()
{
// start output buffering, we don't need any output yet,
// it'll be cleared after reading of config files, etc.
ob_start();
// load main config file
if (!$this->load_from_file(RCMAIL_CONFIG_DIR . '/main.inc.php'))
$this->errors[] = 'main.inc.php was not found.';
// load database config
if (!$this->load_from_file(RCMAIL_CONFIG_DIR . '/db.inc.php'))
$this->errors[] = 'db.inc.php was not found.';
// load host-specific configuration
$this->load_host_config();
// set skin (with fallback to old 'skin_path' property)
if (empty($this->prop['skin']) && !empty($this->prop['skin_path']))
$this->prop['skin'] = str_replace('skins/', '', unslashify($this->prop['skin_path']));
else if (empty($this->prop['skin']))
$this->prop['skin'] = 'default';
// fix paths
$this->prop['log_dir'] = $this->prop['log_dir'] ? realpath(unslashify($this->prop['log_dir'])) : INSTALL_PATH . 'logs';
$this->prop['temp_dir'] = $this->prop['temp_dir'] ? realpath(unslashify($this->prop['temp_dir'])) : INSTALL_PATH . 'temp';
// fix default imap folders encoding
foreach (array('drafts_mbox', 'junk_mbox', 'sent_mbox', 'trash_mbox') as $folder)
$this->prop[$folder] = rcube_charset_convert($this->prop[$folder], RCMAIL_CHARSET, 'UTF7-IMAP');
if (!empty($this->prop['default_imap_folders']))
foreach ($this->prop['default_imap_folders'] as $n => $folder)
$this->prop['default_imap_folders'][$n] = rcube_charset_convert($folder, RCMAIL_CHARSET, 'UTF7-IMAP');
// set PHP error logging according to config
if ($this->prop['debug_level'] & 1) {
ini_set('log_errors', 1);
if ($this->prop['log_driver'] == 'syslog') {
ini_set('error_log', 'syslog');
} else {
ini_set('error_log', $this->prop['log_dir'].'/errors');
}
}
if ($this->prop['debug_level'] & 4) {
ini_set('display_errors', 1);
}
else {
ini_set('display_errors', 0);
}
// clear output buffer
ob_end_clean();
// export config data
$GLOBALS['CONFIG'] = &$this->prop;
}
/**
* Load a host-specific config file if configured
* This will merge the host specific configuration with the given one
*/
private function load_host_config()
{
$fname = null;
if (is_array($this->prop['include_host_config'])) {
$fname = $this->prop['include_host_config'][$_SERVER['HTTP_HOST']];
}
else if (!empty($this->prop['include_host_config'])) {
$fname = preg_replace('/[^a-z0-9\.\-_]/i', '', $_SERVER['HTTP_HOST']) . '.inc.php';
}
if ($fname) {
$this->load_from_file(RCMAIL_CONFIG_DIR . '/' . $fname);
}
}
/**
* Read configuration from a file
* and merge with the already stored config values
*
* @param string Full path to the config file to be loaded
* @return booelan True on success, false on failure
*/
public function load_from_file($fpath, $merge = true)
{
if (is_file($fpath) && is_readable($fpath)) {
include($fpath);
if (is_array($rcmail_config)) {
$this->prop = $merge ? array_merge($this->prop, $rcmail_config) : $this->prop + $rcmail_config;
return true;
}
}
return false;
}
/**
* Getter for a specific config parameter
*
* @param string Parameter name
* @param mixed Default value if not set
* @return mixed The requested config value
*/
public function get($name, $def = null)
{
return isset($this->prop[$name]) ? $this->prop[$name] : $def;
}
/**
* Setter for a config parameter
*
* @param string Parameter name
* @param mixed Parameter value
*/
public function set($name, $value)
{
$this->prop[$name] = $value;
}
/**
* Override config options with the given values (eg. user prefs)
*
* @param array Hash array with config props to merge over
*/
public function merge($prefs)
{
$this->prop = array_merge($this->prop, $prefs);
}
-
+
/**
* Getter for all config options
*
* @return array Hash array containg all config properties
*/
public function all()
{
return $this->prop;
}
+
/**
* Return requested DES crypto key.
*
* @param string Crypto key name
* @return string Crypto key
*/
public function get_crypto_key($key)
{
// Bomb out if the requested key does not exist
if (!array_key_exists($key, $this->prop))
{
raise_error(array(
- 'code' => 500,
- 'type' => 'php',
- 'file' => __FILE__,
+ 'code' => 500, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
'message' => "Request for unconfigured crypto key \"$key\""
), true, true);
}
$key = $this->prop[$key];
// Bomb out if the configured key is not exactly 24 bytes long
if (strlen($key) != 24)
{
raise_error(array(
- 'code' => 500,
- 'type' => 'php',
- 'file' => __FILE__,
+ 'code' => 500, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
'message' => "Configured crypto key \"$key\" is not exactly 24 bytes long"
), true, true);
}
return $key;
}
+
/**
* Try to autodetect operating system and find the correct line endings
*
* @return string The appropriate mail header delimiter
*/
public function header_delimiter()
{
// use the configured delimiter for headers
if (!empty($this->prop['mail_header_delimiter']))
return $this->prop['mail_header_delimiter'];
else if (strtolower(substr(PHP_OS, 0, 3)) == 'win')
return "\r\n";
else if (strtolower(substr(PHP_OS, 0, 3)) == 'mac')
return "\r\n";
else
return "\n";
}
-
-
+
/**
* Return the mail domain configured for the given host
*
* @param string IMAP host
* @return string Resolved SMTP host
*/
public function mail_domain($host)
{
$domain = $host;
if (is_array($this->prop['mail_domain'])) {
if (isset($this->prop['mail_domain'][$host]))
$domain = $this->prop['mail_domain'][$host];
}
else if (!empty($this->prop['mail_domain']))
$domain = $this->prop['mail_domain'];
return $domain;
}
/**
* Getter for error state
*
* @return mixed Error message on error, False if no errors
*/
public function get_error()
{
return empty($this->errors) ? false : join("\n", $this->errors);
}
}
diff --git a/program/include/rcube_imap.php b/program/include/rcube_imap.php
index 8d200363b..9137ada99 100644
--- a/program/include/rcube_imap.php
+++ b/program/include/rcube_imap.php
@@ -1,3250 +1,3251 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcube_imap.php |
| |
| This file is part of the RoundCube Webmail client |
| Copyright (C) 2005-2009, RoundCube Dev. - Switzerland |
| Licensed under the GNU GPL |
| |
| PURPOSE: |
| IMAP wrapper that implements the Iloha IMAP Library (IIL) |
| See http://ilohamail.org/ for details |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
$Id$
*/
/*
* Obtain classes from the Iloha IMAP library
*/
require_once('lib/imap.inc');
require_once('lib/mime.inc');
require_once('lib/tnef_decoder.inc');
/**
* Interface class for accessing an IMAP server
*
* This is a wrapper that implements the Iloha IMAP Library (IIL)
*
* @package Mail
* @author Thomas Bruederli <roundcube@gmail.com>
* @version 1.5
* @link http://ilohamail.org
*/
class rcube_imap
{
var $db;
var $conn;
var $root_ns = '';
var $root_dir = '';
var $mailbox = 'INBOX';
var $list_page = 1;
var $page_size = 10;
var $sort_field = 'date';
var $sort_order = 'DESC';
var $index_sort = true;
var $delimiter = NULL;
var $caching_enabled = FALSE;
var $default_charset = 'ISO-8859-1';
var $struct_charset = NULL;
var $default_folders = array('INBOX');
var $default_folders_lc = array('inbox');
var $fetch_add_headers = '';
var $cache = array();
var $cache_keys = array();
var $cache_changes = array();
var $uid_id_map = array();
var $msg_headers = array();
var $skip_deleted = FALSE;
var $search_set = NULL;
var $search_string = '';
var $search_charset = '';
var $search_sort_field = '';
var $debug_level = 1;
var $error_code = 0;
var $options = array('auth_method' => 'check');
private $host, $user, $pass, $port, $ssl;
/**
* Object constructor
*
* @param object DB Database connection
*/
function __construct($db_conn)
{
$this->db = $db_conn;
}
/**
* Connect to an IMAP server
*
* @param string Host to connect
* @param string Username for IMAP account
* @param string Password for IMAP account
* @param number Port to connect to
* @param string SSL schema (either ssl or tls) or null if plain connection
* @return boolean TRUE on success, FALSE on failure
* @access public
*/
function connect($host, $user, $pass, $port=143, $use_ssl=null)
{
global $ICL_SSL, $ICL_PORT, $IMAP_USE_INTERNAL_DATE;
// check for Open-SSL support in PHP build
if ($use_ssl && extension_loaded('openssl'))
$ICL_SSL = $use_ssl == 'imaps' ? 'ssl' : $use_ssl;
else if ($use_ssl) {
- raise_error(array('code' => 403, 'type' => 'imap', 'file' => __FILE__,
- 'message' => 'Open SSL not available;'), TRUE, FALSE);
+ raise_error(array('code' => 403, 'type' => 'imap',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Open SSL not available"), TRUE, FALSE);
$port = 143;
}
$ICL_PORT = $port;
$IMAP_USE_INTERNAL_DATE = false;
$attempt = 0;
do {
$data = rcmail::get_instance()->plugins->exec_hook('imap_connect', array('host' => $host, 'user' => $user, 'attempt' => ++$attempt));
if (!empty($data['pass']))
$pass = $data['pass'];
$this->conn = iil_Connect($data['host'], $data['user'], $pass, $this->options);
} while(!$this->conn && $data['retry']);
$this->host = $data['host'];
$this->user = $data['user'];
$this->pass = $pass;
$this->port = $port;
$this->ssl = $use_ssl;
// print trace messages
if ($this->conn && ($this->debug_level & 8))
console($this->conn->message);
// write error log
else if (!$this->conn && $GLOBALS['iil_error'])
{
$this->error_code = $GLOBALS['iil_errornum'];
- raise_error(array('code' => 403,
- 'type' => 'imap',
- 'message' => $GLOBALS['iil_error']), TRUE, FALSE);
+ raise_error(array('code' => 403, 'type' => 'imap',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => $GLOBALS['iil_error']), TRUE, FALSE);
}
// get server properties
if ($this->conn)
{
if (!empty($this->conn->rootdir))
{
$this->set_rootdir($this->conn->rootdir);
$this->root_ns = preg_replace('/[.\/]$/', '', $this->conn->rootdir);
}
if (empty($this->delimiter))
$this->get_hierarchy_delimiter();
}
return $this->conn ? TRUE : FALSE;
}
/**
* Close IMAP connection
* Usually done on script shutdown
*
* @access public
*/
function close()
{
if ($this->conn)
iil_Close($this->conn);
}
/**
* Close IMAP connection and re-connect
* This is used to avoid some strange socket errors when talking to Courier IMAP
*
* @access public
*/
function reconnect()
{
$this->close();
$this->connect($this->host, $this->user, $this->pass, $this->port, $this->ssl);
// issue SELECT command to restore connection status
if ($this->mailbox)
iil_C_Select($this->conn, $this->mailbox);
}
/**
* Set options to be used in iil_Connect()
*/
function set_options($opt)
{
$this->options = array_merge($this->options, (array)$opt);
}
/**
* Set a root folder for the IMAP connection.
*
* Only folders within this root folder will be displayed
* and all folder paths will be translated using this folder name
*
* @param string Root folder
* @access public
*/
function set_rootdir($root)
{
if (preg_match('/[.\/]$/', $root)) //(substr($root, -1, 1)==='/')
$root = substr($root, 0, -1);
$this->root_dir = $root;
$this->options['rootdir'] = $root;
if (empty($this->delimiter))
$this->get_hierarchy_delimiter();
}
/**
* Set default message charset
*
* This will be used for message decoding if a charset specification is not available
*
* @param string Charset string
* @access public
*/
function set_charset($cs)
{
$this->default_charset = $cs;
}
/**
* This list of folders will be listed above all other folders
*
* @param array Indexed list of folder names
* @access public
*/
function set_default_mailboxes($arr)
{
if (is_array($arr))
{
$this->default_folders = $arr;
$this->default_folders_lc = array();
// add inbox if not included
if (!in_array_nocase('INBOX', $this->default_folders))
array_unshift($this->default_folders, 'INBOX');
// create a second list with lower cased names
foreach ($this->default_folders as $mbox)
$this->default_folders_lc[] = strtolower($mbox);
}
}
/**
* Set internal mailbox reference.
*
* All operations will be perfomed on this mailbox/folder
*
* @param string Mailbox/Folder name
* @access public
*/
function set_mailbox($new_mbox)
{
$mailbox = $this->mod_mailbox($new_mbox);
if ($this->mailbox == $mailbox)
return;
$this->mailbox = $mailbox;
// clear messagecount cache for this mailbox
$this->_clear_messagecount($mailbox);
}
/**
* Set internal list page
*
* @param number Page number to list
* @access public
*/
function set_page($page)
{
$this->list_page = (int)$page;
}
/**
* Set internal page size
*
* @param number Number of messages to display on one page
* @access public
*/
function set_pagesize($size)
{
$this->page_size = (int)$size;
}
/**
* Save a set of message ids for future message listing methods
*
* @param string IMAP Search query
* @param array List of message ids or NULL if empty
* @param string Charset of search string
* @param string Sorting field
*/
function set_search_set($str=null, $msgs=null, $charset=null, $sort_field=null)
{
if (is_array($str) && $msgs == null)
list($str, $msgs, $charset, $sort_field) = $str;
if ($msgs != null && !is_array($msgs))
$msgs = explode(',', $msgs);
$this->search_string = $str;
$this->search_set = $msgs;
$this->search_charset = $charset;
$this->search_sort_field = $sort_field;
}
/**
* Return the saved search set as hash array
* @return array Search set
*/
function get_search_set()
{
return array($this->search_string, $this->search_set, $this->search_charset, $this->search_sort_field);
}
/**
* Returns the currently used mailbox name
*
* @return string Name of the mailbox/folder
* @access public
*/
function get_mailbox_name()
{
return $this->conn ? $this->mod_mailbox($this->mailbox, 'out') : '';
}
/**
* Returns the IMAP server's capability
*
* @param string Capability name
* @return mixed Capability value or TRUE if supported, FALSE if not
* @access public
*/
function get_capability($cap)
{
return iil_C_GetCapability($this->conn, strtoupper($cap));
}
/**
* Checks the PERMANENTFLAGS capability of the current mailbox
* and returns true if the given flag is supported by the IMAP server
*
* @param string Permanentflag name
* @return mixed True if this flag is supported
* @access public
*/
function check_permflag($flag)
{
$flag = strtoupper($flag);
$imap_flag = $GLOBALS['IMAP_FLAGS'][$flag];
return (in_array_nocase($imap_flag, $this->conn->permanentflags));
}
/**
* Returns the delimiter that is used by the IMAP server for folder separation
*
* @return string Delimiter string
* @access public
*/
function get_hierarchy_delimiter()
{
if ($this->conn && empty($this->delimiter))
$this->delimiter = iil_C_GetHierarchyDelimiter($this->conn);
if (empty($this->delimiter))
$this->delimiter = '/';
return $this->delimiter;
}
/**
* Public method for mailbox listing.
*
* Converts mailbox name with root dir first
*
* @param string Optional root folder
* @param string Optional filter for mailbox listing
* @return array List of mailboxes/folders
* @access public
*/
function list_mailboxes($root='', $filter='*')
{
$a_out = array();
$a_mboxes = $this->_list_mailboxes($root, $filter);
foreach ($a_mboxes as $mbox_row)
{
$name = $this->mod_mailbox($mbox_row, 'out');
if (strlen($name))
$a_out[] = $name;
}
// INBOX should always be available
if (!in_array_nocase('INBOX', $a_out))
array_unshift($a_out, 'INBOX');
// sort mailboxes
$a_out = $this->_sort_mailbox_list($a_out);
return $a_out;
}
/**
* Private method for mailbox listing
*
* @return array List of mailboxes/folders
* @see rcube_imap::list_mailboxes()
* @access private
*/
private function _list_mailboxes($root='', $filter='*')
{
$a_defaults = $a_out = array();
// get cached folder list
$a_mboxes = $this->get_cache('mailboxes');
if (is_array($a_mboxes))
return $a_mboxes;
// Give plugins a chance to provide a list of mailboxes
$data = rcmail::get_instance()->plugins->exec_hook('list_mailboxes',array('root'=>$root,'filter'=>$filter));
if (isset($data['folders'])) {
$a_folders = $data['folders'];
}
else{
// retrieve list of folders from IMAP server
$a_folders = iil_C_ListSubscribed($this->conn, $this->mod_mailbox($root), $filter);
}
if (!is_array($a_folders) || !sizeof($a_folders))
$a_folders = array();
// write mailboxlist to cache
$this->update_cache('mailboxes', $a_folders);
return $a_folders;
}
/**
* Get message count for a specific mailbox
*
* @param string Mailbox/folder name
* @param string Mode for count [ALL|UNSEEN|RECENT]
* @param boolean Force reading from server and update cache
* @return int Number of messages
* @access public
*/
function messagecount($mbox_name='', $mode='ALL', $force=FALSE)
{
$mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
return $this->_messagecount($mailbox, $mode, $force);
}
/**
* Private method for getting nr of messages
*
* @access private
* @see rcube_imap::messagecount()
*/
private function _messagecount($mailbox='', $mode='ALL', $force=FALSE)
{
$mode = strtoupper($mode);
if (empty($mailbox))
$mailbox = $this->mailbox;
// count search set
if ($this->search_string && $mailbox == $this->mailbox && $mode == 'ALL' && !$force)
return count((array)$this->search_set);
$a_mailbox_cache = $this->get_cache('messagecount');
// return cached value
if (!$force && is_array($a_mailbox_cache[$mailbox]) && isset($a_mailbox_cache[$mailbox][$mode]))
return $a_mailbox_cache[$mailbox][$mode];
// RECENT count is fetched a bit different
if ($mode == 'RECENT')
$count = iil_C_CheckForRecent($this->conn, $mailbox);
// use SEARCH for message counting
else if ($this->skip_deleted)
{
$search_str = "ALL UNDELETED";
// get message count and store in cache
if ($mode == 'UNSEEN')
$search_str .= " UNSEEN";
// get message count using SEARCH
// not very performant but more precise (using UNDELETED)
$index = $this->_search_index($mailbox, $search_str);
$count = is_array($index) ? count($index) : 0;
}
else
{
if ($mode == 'UNSEEN')
$count = iil_C_CountUnseen($this->conn, $mailbox);
else
$count = iil_C_CountMessages($this->conn, $mailbox);
}
if (!is_array($a_mailbox_cache[$mailbox]))
$a_mailbox_cache[$mailbox] = array();
$a_mailbox_cache[$mailbox][$mode] = (int)$count;
// write back to cache
$this->update_cache('messagecount', $a_mailbox_cache);
return (int)$count;
}
/**
* Public method for listing headers
* convert mailbox name with root dir first
*
* @param string Mailbox/folder name
* @param int Current page to list
* @param string Header field to sort by
* @param string Sort order [ASC|DESC]
* @param boolean Number of slice items to extract from result array
* @return array Indexed array with message header objects
* @access public
*/
function list_headers($mbox_name='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
{
$mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
return $this->_list_headers($mailbox, $page, $sort_field, $sort_order, false, $slice);
}
/**
* Private method for listing message headers
*
* @access private
* @see rcube_imap::list_headers
*/
private function _list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $recursive=FALSE, $slice=0)
{
if (!strlen($mailbox))
return array();
// use saved message set
if ($this->search_string && $mailbox == $this->mailbox)
return $this->_list_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
$this->_set_sort_order($sort_field, $sort_order);
$page = $page ? $page : $this->list_page;
$cache_key = $mailbox.'.msg';
$cache_status = $this->check_cache_status($mailbox, $cache_key);
// cache is OK, we can get all messages from local cache
if ($cache_status>0)
{
$start_msg = ($page-1) * $this->page_size;
$a_msg_headers = $this->get_message_cache($cache_key, $start_msg, $start_msg+$this->page_size, $this->sort_field, $this->sort_order);
$result = array_values($a_msg_headers);
if ($slice)
$result = array_slice($result, -$slice, $slice);
return $result;
}
// cache is dirty, sync it
else if ($this->caching_enabled && $cache_status==-1 && !$recursive)
{
$this->sync_header_index($mailbox);
return $this->_list_headers($mailbox, $page, $this->sort_field, $this->sort_order, TRUE, $slice);
}
// retrieve headers from IMAP
$a_msg_headers = array();
// use message index sort for sorting by Date (for better performance)
if ($this->index_sort && $this->sort_field == 'date')
{
if ($this->skip_deleted) {
// @TODO: this could be cached
if ($msg_index = $this->_search_index($mailbox, 'ALL UNDELETED')) {
$max = max($msg_index);
list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
$msg_index = array_slice($msg_index, $begin, $end-$begin);
}
} else if ($max = iil_C_CountMessages($this->conn, $mailbox)) {
list($begin, $end) = $this->_get_message_range($max, $page);
$msg_index = range($begin+1, $end);
} else
$msg_index = array();
if ($slice)
$msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
// fetch reqested headers from server
if ($msg_index)
$this->_fetch_headers($mailbox, join(",", $msg_index), $a_msg_headers, $cache_key);
}
// use SORT command
else if ($this->get_capability('sort') && ($msg_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')))
{
list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
$max = max($msg_index);
$msg_index = array_slice($msg_index, $begin, $end-$begin);
if ($slice)
$msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
// fetch reqested headers from server
$this->_fetch_headers($mailbox, join(',', $msg_index), $a_msg_headers, $cache_key);
}
// fetch specified header for all messages and sort
else if ($a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, "1:*", $this->sort_field, $this->skip_deleted))
{
asort($a_index); // ASC
$msg_index = array_keys($a_index);
$max = max($msg_index);
list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
$msg_index = array_slice($msg_index, $begin, $end-$begin);
if ($slice)
$msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
// fetch reqested headers from server
$this->_fetch_headers($mailbox, join(",", $msg_index), $a_msg_headers, $cache_key);
}
// delete cached messages with a higher index than $max+1
// Changed $max to $max+1 to fix this bug : #1484295
$this->clear_message_cache($cache_key, $max + 1);
// kick child process to sync cache
// ...
// return empty array if no messages found
if (!is_array($a_msg_headers) || empty($a_msg_headers))
return array();
// use this class for message sorting
$sorter = new rcube_header_sorter();
$sorter->set_sequence_numbers($msg_index);
$sorter->sort_headers($a_msg_headers);
if ($this->sort_order == 'DESC')
$a_msg_headers = array_reverse($a_msg_headers);
return array_values($a_msg_headers);
}
/**
* Private method for listing a set of message headers (search results)
*
* @param string Mailbox/folder name
* @param int Current page to list
* @param string Header field to sort by
* @param string Sort order [ASC|DESC]
* @param boolean Number of slice items to extract from result array
* @return array Indexed array with message header objects
* @access private
* @see rcube_imap::list_header_set()
*/
private function _list_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
{
if (!strlen($mailbox) || empty($this->search_set))
return array();
$msgs = $this->search_set;
$a_msg_headers = array();
$page = $page ? $page : $this->list_page;
$start_msg = ($page-1) * $this->page_size;
$this->_set_sort_order($sort_field, $sort_order);
// quickest method
if ($this->index_sort && $this->search_sort_field == 'date' && $this->sort_field == 'date')
{
if ($sort_order == 'DESC')
$msgs = array_reverse($msgs);
// get messages uids for one page
$msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
if ($slice)
$msgs = array_slice($msgs, -$slice, $slice);
// fetch headers
$this->_fetch_headers($mailbox, join(',',$msgs), $a_msg_headers, NULL);
// I didn't found in RFC that FETCH always returns messages sorted by index
$sorter = new rcube_header_sorter();
$sorter->set_sequence_numbers($msgs);
$sorter->sort_headers($a_msg_headers);
return array_values($a_msg_headers);
}
// sorted messages, so we can first slice array and then fetch only wanted headers
if ($this->get_capability('sort') && (!$this->index_sort || $this->sort_field != 'date')) // SORT searching result
{
// reset search set if sorting field has been changed
if ($this->sort_field && $this->search_sort_field != $this->sort_field)
$msgs = $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
// return empty array if no messages found
if (empty($msgs))
return array();
if ($sort_order == 'DESC')
$msgs = array_reverse($msgs);
// get messages uids for one page
$msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
if ($slice)
$msgs = array_slice($msgs, -$slice, $slice);
// fetch headers
$this->_fetch_headers($mailbox, join(',',$msgs), $a_msg_headers, NULL);
$sorter = new rcube_header_sorter();
$sorter->set_sequence_numbers($msgs);
$sorter->sort_headers($a_msg_headers);
return array_values($a_msg_headers);
}
else { // SEARCH searching result, need sorting
$cnt = count($msgs);
// 300: experimantal value for best result
if (($cnt > 300 && $cnt > $this->page_size) || ($this->index_sort && $this->sort_field == 'date')) {
// use memory less expensive (and quick) method for big result set
$a_index = $this->message_index('', $this->sort_field, $this->sort_order);
// get messages uids for one page...
$msgs = array_slice($a_index, $start_msg, min($cnt-$start_msg, $this->page_size));
if ($slice)
$msgs = array_slice($msgs, -$slice, $slice);
// ...and fetch headers
$this->_fetch_headers($mailbox, join(',', $msgs), $a_msg_headers, NULL);
// return empty array if no messages found
if (!is_array($a_msg_headers) || empty($a_msg_headers))
return array();
$sorter = new rcube_header_sorter();
$sorter->set_sequence_numbers($msgs);
$sorter->sort_headers($a_msg_headers);
return array_values($a_msg_headers);
}
else {
// for small result set we can fetch all messages headers
$this->_fetch_headers($mailbox, join(',', $msgs), $a_msg_headers, NULL);
// return empty array if no messages found
if (!is_array($a_msg_headers) || empty($a_msg_headers))
return array();
// if not already sorted
$a_msg_headers = iil_SortHeaders($a_msg_headers, $this->sort_field, $this->sort_order);
// only return the requested part of the set
$a_msg_headers = array_slice(array_values($a_msg_headers), $start_msg, min($cnt-$start_msg, $this->page_size));
if ($slice)
$a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
return $a_msg_headers;
}
}
}
/**
* Helper function to get first and last index of the requested set
*
* @param int message count
* @param mixed page number to show, or string 'all'
* @return array array with two values: first index, last index
* @access private
*/
private function _get_message_range($max, $page)
{
$start_msg = ($page-1) * $this->page_size;
if ($page=='all')
{
$begin = 0;
$end = $max;
}
else if ($this->sort_order=='DESC')
{
$begin = $max - $this->page_size - $start_msg;
$end = $max - $start_msg;
}
else
{
$begin = $start_msg;
$end = $start_msg + $this->page_size;
}
if ($begin < 0) $begin = 0;
if ($end < 0) $end = $max;
if ($end > $max) $end = $max;
return array($begin, $end);
}
/**
* Fetches message headers
* Used for loop
*
* @param string Mailbox name
* @param string Message index to fetch
* @param array Reference to message headers array
* @param array Array with cache index
* @return int Messages count
* @access private
*/
private function _fetch_headers($mailbox, $msgs, &$a_msg_headers, $cache_key)
{
// fetch reqested headers from server
$a_header_index = iil_C_FetchHeaders($this->conn, $mailbox, $msgs, false, false, $this->fetch_add_headers);
if (!empty($a_header_index))
{
// cache is incomplete
$cache_index = $this->get_message_cache_index($cache_key);
foreach ($a_header_index as $i => $headers) {
if ($this->caching_enabled && $cache_index[$headers->id] != $headers->uid) {
// prevent index duplicates
if ($cache_index[$headers->id]) {
$this->remove_message_cache($cache_key, $headers->id, true);
unset($cache_index[$headers->id]);
}
// add message to cache
$this->add_message_cache($cache_key, $headers->id, $headers, NULL,
!in_array($headers->uid, $cache_index));
}
$a_msg_headers[$headers->uid] = $headers;
}
}
return count($a_msg_headers);
}
/**
* Return sorted array of message IDs (not UIDs)
*
* @param string Mailbox to get index from
* @param string Sort column
* @param string Sort order [ASC, DESC]
* @return array Indexed array with message ids
*/
function message_index($mbox_name='', $sort_field=NULL, $sort_order=NULL)
{
$this->_set_sort_order($sort_field, $sort_order);
$mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
$key = "{$mailbox}:{$this->sort_field}:{$this->sort_order}:{$this->search_string}.msgi";
// we have a saved search result, get index from there
if (!isset($this->cache[$key]) && $this->search_string && $mailbox == $this->mailbox)
{
$this->cache[$key] = array();
// use message index sort for sorting by Date
if ($this->index_sort && $this->sort_field == 'date')
{
$msgs = $this->search_set;
if ($this->search_sort_field != 'date')
sort($msgs);
if ($this->sort_order == 'DESC')
$this->cache[$key] = array_reverse($msgs);
else
$this->cache[$key] = $msgs;
}
// sort with SORT command
else if ($this->get_capability('sort'))
{
if ($this->sort_field && $this->search_sort_field != $this->sort_field)
$this->search('', $this->search_string, $this->search_charset, $this->sort_field);
if ($this->sort_order == 'DESC')
$this->cache[$key] = array_reverse($this->search_set);
else
$this->cache[$key] = $this->search_set;
}
else
{
$a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, join(',', $this->search_set), $this->sort_field, $this->skip_deleted);
if ($this->sort_order=="ASC")
asort($a_index);
else if ($this->sort_order=="DESC")
arsort($a_index);
$this->cache[$key] = array_keys($a_index);
}
}
// have stored it in RAM
if (isset($this->cache[$key]))
return $this->cache[$key];
// check local cache
$cache_key = $mailbox.'.msg';
$cache_status = $this->check_cache_status($mailbox, $cache_key);
// cache is OK
if ($cache_status>0)
{
$a_index = $this->get_message_cache_index($cache_key, TRUE, $this->sort_field, $this->sort_order);
return array_keys($a_index);
}
// use message index sort for sorting by Date
if ($this->index_sort && $this->sort_field == 'date')
{
if ($this->skip_deleted) {
$a_index = $this->_search_index($mailbox, 'ALL');
} else if ($max = $this->_messagecount($mailbox)) {
$a_index = range(1, $max);
}
if ($this->sort_order == 'DESC')
$a_index = array_reverse($a_index);
$this->cache[$key] = $a_index;
}
// fetch complete message index
else if ($this->get_capability('sort') && ($a_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')))
{
if ($this->sort_order == 'DESC')
$a_index = array_reverse($a_index);
$this->cache[$key] = $a_index;
}
else
{
$a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, "1:*", $this->sort_field, $this->skip_deleted);
if ($this->sort_order=="ASC")
asort($a_index);
else if ($this->sort_order=="DESC")
arsort($a_index);
$this->cache[$key] = array_keys($a_index);
}
return $this->cache[$key];
}
/**
* @access private
*/
function sync_header_index($mailbox)
{
$cache_key = $mailbox.'.msg';
$cache_index = $this->get_message_cache_index($cache_key);
// fetch complete message index
$a_message_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, "1:*", 'UID', $this->skip_deleted);
if ($a_message_index === false)
return false;
foreach ($a_message_index as $id => $uid)
{
// message in cache at correct position
if ($cache_index[$id] == $uid)
{
unset($cache_index[$id]);
continue;
}
// message in cache but in wrong position
if (in_array((string)$uid, $cache_index, TRUE))
{
unset($cache_index[$id]);
}
// other message at this position
if (isset($cache_index[$id]))
{
$for_remove[] = $cache_index[$id];
unset($cache_index[$id]);
}
$for_update[] = $id;
}
// clear messages at wrong positions and those deleted that are still in cache_index
if (!empty($for_remove))
$cache_index = array_merge($cache_index, $for_remove);
if (!empty($cache_index))
$this->remove_message_cache($cache_key, $cache_index);
// fetch complete headers and add to cache
if (!empty($for_update)) {
if ($headers = iil_C_FetchHeader($this->conn, $mailbox, join(',', $for_update), false, $this->fetch_add_headers))
foreach ($headers as $header)
$this->add_message_cache($cache_key, $header->id, $header, NULL,
in_array($header->uid, (array)$for_remove));
}
}
/**
* Invoke search request to IMAP server
*
* @param string mailbox name to search in
* @param string search string
* @param string search string charset
* @param string header field to sort by
* @return array search results as list of message ids
* @access public
*/
function search($mbox_name='', $str=NULL, $charset=NULL, $sort_field=NULL)
{
if (!$str)
return false;
$mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
$results = $this->_search_index($mailbox, $str, $charset, $sort_field);
// try search with US-ASCII charset (should be supported by server)
// only if UTF-8 search is not supported
if (empty($results) && !is_array($results) && !empty($charset) && $charset != 'US-ASCII')
{
// convert strings to US_ASCII
if(preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE))
{
$last = 0; $res = '';
foreach($matches[1] as $m)
{
$string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n
$string = substr($str, $string_offset - 1, $m[0]);
$string = rcube_charset_convert($string, $charset, 'US-ASCII');
if (!$string) continue;
$res .= sprintf("%s{%d}\r\n%s", substr($str, $last, $m[1] - $last - 1), strlen($string), $string);
$last = $m[0] + $string_offset - 1;
}
if ($last < strlen($str))
$res .= substr($str, $last, strlen($str)-$last);
}
else // strings for conversion not found
$res = $str;
$results = $this->search($mbox_name, $res, NULL, $sort_field);
}
$this->set_search_set($str, $results, $charset, $sort_field);
return $results;
}
/**
* Private search method
*
* @return array search results as list of message ids
* @access private
* @see rcube_imap::search()
*/
private function _search_index($mailbox, $criteria='ALL', $charset=NULL, $sort_field=NULL)
{
$orig_criteria = $criteria;
if ($this->skip_deleted && !preg_match('/UNDELETED/', $criteria))
$criteria = 'UNDELETED '.$criteria;
if ($sort_field && $this->get_capability('sort') && (!$this->index_sort || $sort_field != 'date')) {
$charset = $charset ? $charset : $this->default_charset;
$a_messages = iil_C_Sort($this->conn, $mailbox, $sort_field, $criteria, FALSE, $charset);
}
else {
if ($orig_criteria == 'ALL') {
$max = $this->_messagecount($mailbox);
$a_messages = $max ? range(1, $max) : array();
}
else {
$a_messages = iil_C_Search($this->conn, $mailbox, ($charset ? "CHARSET $charset " : '') . $criteria);
// I didn't found that SEARCH always returns sorted IDs
if ($this->index_sort && $this->sort_field == 'date')
sort($a_messages);
}
}
// update messagecount cache ?
// $a_mailbox_cache = get_cache('messagecount');
// $a_mailbox_cache[$mailbox][$criteria] = sizeof($a_messages);
// $this->update_cache('messagecount', $a_mailbox_cache);
return $a_messages;
}
/**
* Refresh saved search set
*
* @return array Current search set
*/
function refresh_search()
{
if (!empty($this->search_string))
$this->search_set = $this->search('', $this->search_string, $this->search_charset, $this->search_sort_field);
return $this->get_search_set();
}
/**
* Check if the given message ID is part of the current search set
*
* @return boolean True on match or if no search request is stored
*/
function in_searchset($msgid)
{
if (!empty($this->search_string))
return in_array("$msgid", (array)$this->search_set, true);
else
return true;
}
/**
* Return message headers object of a specific message
*
* @param int Message ID
* @param string Mailbox to read from
* @param boolean True if $id is the message UID
* @param boolean True if we need also BODYSTRUCTURE in headers
* @return object Message headers representation
*/
function get_headers($id, $mbox_name=NULL, $is_uid=TRUE, $bodystr=FALSE)
{
$mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
$uid = $is_uid ? $id : $this->_id2uid($id);
// get cached headers
if ($uid && ($headers = &$this->get_cached_message($mailbox.'.msg', $uid)))
return $headers;
$headers = iil_C_FetchHeader($this->conn, $mailbox, $id, $is_uid, $bodystr, $this->fetch_add_headers);
// write headers cache
if ($headers)
{
if ($headers->uid && $headers->id)
$this->uid_id_map[$mailbox][$headers->uid] = $headers->id;
$this->add_message_cache($mailbox.'.msg', $headers->id, $headers, NULL, true);
}
return $headers;
}
/**
* Fetch body structure from the IMAP server and build
* an object structure similar to the one generated by PEAR::Mail_mimeDecode
*
* @param int Message UID to fetch
* @param string Message BODYSTRUCTURE string (optional)
* @return object rcube_message_part Message part tree or False on failure
*/
function &get_structure($uid, $structure_str='')
{
$cache_key = $this->mailbox.'.msg';
$headers = &$this->get_cached_message($cache_key, $uid);
// return cached message structure
if (is_object($headers) && is_object($headers->structure)) {
return $headers->structure;
}
if (!$structure_str)
$structure_str = iil_C_FetchStructureString($this->conn, $this->mailbox, $uid, true);
$structure = iml_GetRawStructureArray($structure_str);
$struct = false;
// parse structure and add headers
if (!empty($structure))
{
$headers = $this->get_headers($uid);
$this->_msg_id = $headers->id;
// set message charset from message headers
if ($headers->charset)
$this->struct_charset = $headers->charset;
else
$this->struct_charset = $this->_structure_charset($structure);
// Here we can recognize malformed BODYSTRUCTURE and
// 1. [@TODO] parse the message in other way to create our own message structure
// 2. or just show the raw message body.
// Example of structure for malformed MIME message:
// ("text" "plain" ("charset" "us-ascii") NIL NIL "7bit" 2154 70 NIL NIL NIL)
if ($headers->ctype && $headers->ctype != 'text/plain'
&& $structure[0] == 'text' && $structure[1] == 'plain') {
return false;
}
$struct = &$this->_structure_part($structure);
$struct->headers = get_object_vars($headers);
// don't trust given content-type
if (empty($struct->parts) && !empty($struct->headers['ctype']))
{
$struct->mime_id = '1';
$struct->mimetype = strtolower($struct->headers['ctype']);
list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype);
}
// write structure to cache
if ($this->caching_enabled)
$this->add_message_cache($cache_key, $this->_msg_id, $headers, $struct);
}
return $struct;
}
/**
* Build message part object
*
* @access private
*/
function &_structure_part($part, $count=0, $parent='', $mime_headers=null, $raw_headers=null)
{
$struct = new rcube_message_part;
$struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
// multipart
if (is_array($part[0]))
{
$struct->ctype_primary = 'multipart';
// find first non-array entry
for ($i=1; $i<count($part); $i++)
if (!is_array($part[$i]))
{
$struct->ctype_secondary = strtolower($part[$i]);
break;
}
$struct->mimetype = 'multipart/'.$struct->ctype_secondary;
// build parts list for headers pre-fetching
for ($i=0, $count=0; $i<count($part); $i++)
if (is_array($part[$i]) && count($part[$i]) > 3) {
// fetch message headers if message/rfc822 or named part (could contain Content-Location header)
if (!is_array($part[$i][0])) {
$tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
if (strtolower($part[$i][0]) == 'message' && strtolower($part[$i][1]) == 'rfc822') {
$raw_part_headers[] = $tmp_part_id;
$mime_part_headers[] = $tmp_part_id;
}
else if (in_array('name', (array)$part[$i][2]) && (empty($part[$i][3]) || $part[$i][3]=='NIL')) {
$mime_part_headers[] = $tmp_part_id;
}
}
}
// pre-fetch headers of all parts (in one command for better performance)
// @TODO: we could do this before _structure_part() call, to fetch
// headers for parts on all levels
if ($mime_part_headers)
$mime_part_headers = iil_C_FetchMIMEHeaders($this->conn, $this->mailbox,
$this->_msg_id, $mime_part_headers);
// we'll need a real content-type of message/rfc822 part
if ($raw_part_headers)
$raw_part_headers = iil_C_FetchMIMEHeaders($this->conn, $this->mailbox,
$this->_msg_id, $raw_part_headers, false);
$struct->parts = array();
for ($i=0, $count=0; $i<count($part); $i++)
if (is_array($part[$i]) && count($part[$i]) > 3) {
$tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
$struct->parts[] = $this->_structure_part($part[$i], ++$count, $struct->mime_id,
$mime_part_headers[$tmp_part_id], $raw_part_headers[$tmp_part_id]);
}
return $struct;
}
// regular part
$struct->ctype_primary = strtolower($part[0]);
$struct->ctype_secondary = strtolower($part[1]);
$struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary;
// read content type parameters
if (is_array($part[2]))
{
$struct->ctype_parameters = array();
for ($i=0; $i<count($part[2]); $i+=2)
$struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i+1];
if (isset($struct->ctype_parameters['charset']))
$struct->charset = $struct->ctype_parameters['charset'];
}
// read content encoding
if (!empty($part[5]) && $part[5]!='NIL')
{
$struct->encoding = strtolower($part[5]);
$struct->headers['content-transfer-encoding'] = $struct->encoding;
}
// get part size
if (!empty($part[6]) && $part[6]!='NIL')
$struct->size = intval($part[6]);
// read part disposition
$di = count($part) - 2;
if ((is_array($part[$di]) && count($part[$di]) == 2 && is_array($part[$di][1])) ||
(is_array($part[--$di]) && count($part[$di]) == 2))
{
$struct->disposition = strtolower($part[$di][0]);
if (is_array($part[$di][1]))
for ($n=0; $n<count($part[$di][1]); $n+=2)
$struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1];
}
// get child parts
if (is_array($part[8]) && $di != 8)
{
$struct->parts = array();
for ($i=0, $count=0; $i<count($part[8]); $i++)
if (is_array($part[8][$i]) && count($part[8][$i]) > 5)
$struct->parts[] = $this->_structure_part($part[8][$i], ++$count, $struct->mime_id);
}
// get part ID
if (!empty($part[3]) && $part[3]!='NIL')
{
$struct->content_id = $part[3];
$struct->headers['content-id'] = $part[3];
if (empty($struct->disposition))
$struct->disposition = 'inline';
}
// fetch message headers if message/rfc822 or named part (could contain Content-Location header)
if ($struct->ctype_primary == 'message' || ($struct->ctype_parameters['name'] && !$struct->content_id)) {
if (empty($mime_headers))
$mime_headers = iil_C_FetchPartHeader($this->conn, $this->mailbox, $this->_msg_id, false, $struct->mime_id);
$struct->headers = $this->_parse_headers($mime_headers) + $struct->headers;
// get real headers for message of type 'message/rfc822'
if ($struct->mimetype == 'message/rfc822') {
if (empty($raw_headers))
$raw_headers = iil_C_FetchMIMEHeaders($this->conn, $this->mailbox, $this->_msg_id, (array)$struct->mime_id, false);
$struct->real_headers = $this->_parse_headers($raw_headers);
// get real content-type of message/rfc822
if (preg_match('/^([a-z0-9_\/-]+)/i', $struct->real_headers['content-type'], $matches)) {
$struct->real_mimetype = strtolower($matches[1]);
}
}
}
if ($struct->ctype_primary=='message') {
if (is_array($part[8]) && $di != 8 && empty($struct->parts))
$struct->parts[] = $this->_structure_part($part[8], ++$count, $struct->mime_id);
}
// normalize filename property
$this->_set_part_filename($struct, $mime_headers);
return $struct;
}
/**
* Set attachment filename from message part structure
*
* @access private
* @param object rcube_message_part Part object
* @param string Part's raw headers
*/
private function _set_part_filename(&$part, $headers=null)
{
if (!empty($part->d_parameters['filename']))
$filename_mime = $part->d_parameters['filename'];
else if (!empty($part->d_parameters['filename*']))
$filename_encoded = $part->d_parameters['filename*'];
else if (!empty($part->ctype_parameters['name*']))
$filename_encoded = $part->ctype_parameters['name*'];
// RFC2231 value continuations
// TODO: this should be rewrited to support RFC2231 4.1 combinations
else if (!empty($part->d_parameters['filename*0'])) {
$i = 0;
while (isset($part->d_parameters['filename*'.$i])) {
$filename_mime .= $part->d_parameters['filename*'.$i];
$i++;
}
// some servers (eg. dovecot-1.x) have no support for parameter value continuations
// we must fetch and parse headers "manually"
if ($i<2) {
if (!$headers)
$headers = iil_C_FetchPartHeader($this->conn, $this->mailbox, $this->_msg_id, false, $part->mime_id);
$filename_mime = '';
$i = 0;
while (preg_match('/filename\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
$filename_mime .= $matches[1];
$i++;
}
}
}
else if (!empty($part->d_parameters['filename*0*'])) {
$i = 0;
while (isset($part->d_parameters['filename*'.$i.'*'])) {
$filename_encoded .= $part->d_parameters['filename*'.$i.'*'];
$i++;
}
if ($i<2) {
if (!$headers)
$headers = iil_C_FetchPartHeader($this->conn, $this->mailbox, $this->_msg_id, false, $part->mime_id);
$filename_encoded = '';
$i = 0; $matches = array();
while (preg_match('/filename\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
$filename_encoded .= $matches[1];
$i++;
}
}
}
else if (!empty($part->ctype_parameters['name*0'])) {
$i = 0;
while (isset($part->ctype_parameters['name*'.$i])) {
$filename_mime .= $part->ctype_parameters['name*'.$i];
$i++;
}
if ($i<2) {
if (!$headers)
$headers = iil_C_FetchPartHeader($this->conn, $this->mailbox, $this->_msg_id, false, $part->mime_id);
$filename_mime = '';
$i = 0; $matches = array();
while (preg_match('/\s+name\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
$filename_mime .= $matches[1];
$i++;
}
}
}
else if (!empty($part->ctype_parameters['name*0*'])) {
$i = 0;
while (isset($part->ctype_parameters['name*'.$i.'*'])) {
$filename_encoded .= $part->ctype_parameters['name*'.$i.'*'];
$i++;
}
if ($i<2) {
if (!$headers)
$headers = iil_C_FetchPartHeader($this->conn, $this->mailbox, $this->_msg_id, false, $part->mime_id);
$filename_encoded = '';
$i = 0; $matches = array();
while (preg_match('/\s+name\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
$filename_encoded .= $matches[1];
$i++;
}
}
}
// read 'name' after rfc2231 parameters as it may contains truncated filename (from Thunderbird)
else if (!empty($part->ctype_parameters['name']))
$filename_mime = $part->ctype_parameters['name'];
// Content-Disposition
else if (!empty($part->headers['content-description']))
$filename_mime = $part->headers['content-description'];
else
return;
// decode filename
if (!empty($filename_mime)) {
$part->filename = rcube_imap::decode_mime_string($filename_mime,
$part->charset ? $part->charset : $this->struct_charset ? $this->struct_charset :
rc_detect_encoding($filename_mime, $this->default_charset));
}
else if (!empty($filename_encoded)) {
// decode filename according to RFC 2231, Section 4
if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) {
$filename_charset = $fmatches[1];
$filename_encoded = $fmatches[2];
}
$part->filename = rcube_charset_convert(urldecode($filename_encoded), $filename_charset);
}
}
/**
* Get charset name from message structure (first part)
*
* @access private
* @param array Message structure
* @return string Charset name
*/
function _structure_charset($structure)
{
while (is_array($structure)) {
if (is_array($structure[2]) && $structure[2][0] == 'charset')
return $structure[2][1];
$structure = $structure[0];
}
}
/**
* Fetch message body of a specific message from the server
*
* @param int Message UID
* @param string Part number
* @param object rcube_message_part Part object created by get_structure()
* @param mixed True to print part, ressource to write part contents in
* @param resource File pointer to save the message part
* @return string Message/part body if not printed
*/
function &get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL)
{
// get part encoding if not provided
if (!is_object($o_part))
{
$structure_str = iil_C_FetchStructureString($this->conn, $this->mailbox, $uid, true);
$structure = iml_GetRawStructureArray($structure_str);
// error or message not found
if (empty($structure))
return false;
$part_type = iml_GetPartTypeCode($structure, $part);
$o_part = new rcube_message_part;
$o_part->ctype_primary = $part_type==0 ? 'text' : ($part_type==2 ? 'message' : 'other');
$o_part->encoding = strtolower(iml_GetPartEncodingString($structure, $part));
$o_part->charset = iml_GetPartCharset($structure, $part);
}
// TODO: Add caching for message parts
if (!$part) $part = 'TEXT';
$body = iil_C_HandlePartBody($this->conn, $this->mailbox, $uid, true, $part,
$o_part->encoding, $print, $fp);
if ($fp || $print)
return true;
// convert charset (if text or message part)
if ($o_part->ctype_primary=='text' || $o_part->ctype_primary=='message') {
// assume default if no charset specified
if (empty($o_part->charset) || strtolower($o_part->charset) == 'us-ascii')
$o_part->charset = $this->default_charset;
$body = rcube_charset_convert($body, $o_part->charset);
}
return $body;
}
/**
* Fetch message body of a specific message from the server
*
* @param int Message UID
* @return string Message/part body
* @see rcube_imap::get_message_part()
*/
function &get_body($uid, $part=1)
{
$headers = $this->get_headers($uid);
return rcube_charset_convert($this->get_message_part($uid, $part, NULL),
$headers->charset ? $headers->charset : $this->default_charset);
}
/**
* Returns the whole message source as string
*
* @param int Message UID
* @return string Message source string
*/
function &get_raw_body($uid)
{
return iil_C_HandlePartBody($this->conn, $this->mailbox, $uid, true);
}
/**
* Returns the message headers as string
*
* @param int Message UID
* @return string Message headers string
*/
function &get_raw_headers($uid)
{
return iil_C_FetchPartHeader($this->conn, $this->mailbox, $uid, true);
}
/**
* Sends the whole message source to stdout
*
* @param int Message UID
*/
function print_raw_body($uid)
{
iil_C_HandlePartBody($this->conn, $this->mailbox, $uid, true, NULL, NULL, true);
}
/**
* Set message flag to one or several messages
*
* @param mixed Message UIDs as array or as comma-separated string
* @param string Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
* @param string Folder name
* @param boolean True to skip message cache clean up
* @return boolean True on success, False on failure
*/
function set_flag($uids, $flag, $mbox_name=NULL, $skip_cache=false)
{
$mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
$flag = strtoupper($flag);
if (!is_array($uids))
$uids = explode(',',$uids);
if (strpos($flag, 'UN') === 0)
$result = iil_C_UnFlag($this->conn, $mailbox, join(',', $uids), substr($flag, 2));
else
$result = iil_C_Flag($this->conn, $mailbox, join(',', $uids), $flag);
// reload message headers if cached
if ($this->caching_enabled && !$skip_cache) {
$cache_key = $mailbox.'.msg';
$this->remove_message_cache($cache_key, $uids);
}
// set nr of messages that were flaged
$count = count($uids);
// clear message count cache
if ($result && $flag=='SEEN')
$this->_set_messagecount($mailbox, 'UNSEEN', $count*(-1));
else if ($result && $flag=='UNSEEN')
$this->_set_messagecount($mailbox, 'UNSEEN', $count);
else if ($result && $flag=='DELETED')
$this->_set_messagecount($mailbox, 'ALL', $count*(-1));
return $result;
}
/**
* Remove message flag for one or several messages
*
* @param mixed Message UIDs as array or as comma-separated string
* @param string Flag to unset: SEEN, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
* @param string Folder name
* @return boolean True on success, False on failure
* @see set_flag
*/
function unset_flag($uids, $flag, $mbox_name=NULL)
{
return $this->set_flag($uids, 'UN'.$flag, $mbox_name);
}
/**
* Append a mail message (source) to a specific mailbox
*
* @param string Target mailbox
* @param string Message source
* @return boolean True on success, False on error
*/
function save_message($mbox_name, &$message)
{
$mailbox = $this->mod_mailbox($mbox_name);
// make sure mailbox exists
if (($mailbox == 'INBOX') || in_array($mailbox, $this->_list_mailboxes()))
$saved = iil_C_Append($this->conn, $mailbox, $message);
if ($saved)
{
// increase messagecount of the target mailbox
$this->_set_messagecount($mailbox, 'ALL', 1);
}
return $saved;
}
/**
* Move a message from one mailbox to another
*
* @param string List of UIDs to move, separated by comma
* @param string Target mailbox
* @param string Source mailbox
* @return boolean True on success, False on error
*/
function move_message($uids, $to_mbox, $from_mbox='')
{
$fbox = $from_mbox;
$tbox = $to_mbox;
$to_mbox = $this->mod_mailbox($to_mbox);
$from_mbox = $from_mbox ? $this->mod_mailbox($from_mbox) : $this->mailbox;
// make sure mailbox exists
if ($to_mbox != 'INBOX' && !in_array($to_mbox, $this->_list_mailboxes()))
{
if (in_array($tbox, $this->default_folders))
$this->create_mailbox($tbox, TRUE);
else
return FALSE;
}
// convert the list of uids to array
$a_uids = is_string($uids) ? explode(',', $uids) : (is_array($uids) ? $uids : NULL);
// exit if no message uids are specified
if (!is_array($a_uids) || empty($a_uids))
return false;
// flag messages as read before moving them
$config = rcmail::get_instance()->config;
if ($config->get('read_when_deleted') && $tbox == $config->get('trash_mbox')) {
// don't flush cache (4th argument)
$this->set_flag($uids, 'SEEN', $fbox, true);
}
// move messages
$iil_move = iil_C_Move($this->conn, join(',', $a_uids), $from_mbox, $to_mbox);
$moved = !($iil_move === false || $iil_move < 0);
// send expunge command in order to have the moved message
// really deleted from the source mailbox
if ($moved) {
$this->_expunge($from_mbox, FALSE, $a_uids);
$this->_clear_messagecount($from_mbox);
$this->_clear_messagecount($to_mbox);
}
// moving failed
else if (rcmail::get_instance()->config->get('delete_always', false)) {
return iil_C_Delete($this->conn, $from_mbox, join(',', $a_uids));
}
// remove message ids from search set
if ($moved && $this->search_set && $from_mbox == $this->mailbox) {
foreach ($a_uids as $uid)
$a_mids[] = $this->_uid2id($uid, $from_mbox);
$this->search_set = array_diff($this->search_set, $a_mids);
}
// update cached message headers
$cache_key = $from_mbox.'.msg';
if ($moved && $start_index = $this->get_message_cache_index_min($cache_key, $a_uids)) {
// clear cache from the lowest index on
$this->clear_message_cache($cache_key, $start_index);
}
return $moved;
}
/**
* Mark messages as deleted and expunge mailbox
*
* @param string List of UIDs to move, separated by comma
* @param string Source mailbox
* @return boolean True on success, False on error
*/
function delete_message($uids, $mbox_name='')
{
$mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
// convert the list of uids to array
$a_uids = is_string($uids) ? explode(',', $uids) : (is_array($uids) ? $uids : NULL);
// exit if no message uids are specified
if (!is_array($a_uids) || empty($a_uids))
return false;
$deleted = iil_C_Delete($this->conn, $mailbox, join(',', $a_uids));
// send expunge command in order to have the deleted message
// really deleted from the mailbox
if ($deleted)
{
$this->_expunge($mailbox, FALSE, $a_uids);
$this->_clear_messagecount($mailbox);
unset($this->uid_id_map[$mailbox]);
}
// remove message ids from search set
if ($deleted && $this->search_set && $mailbox == $this->mailbox) {
foreach ($a_uids as $uid)
$a_mids[] = $this->_uid2id($uid, $mailbox);
$this->search_set = array_diff($this->search_set, $a_mids);
}
// remove deleted messages from cache
$cache_key = $mailbox.'.msg';
if ($deleted && $start_index = $this->get_message_cache_index_min($cache_key, $a_uids)) {
// clear cache from the lowest index on
$this->clear_message_cache($cache_key, $start_index);
}
return $deleted;
}
/**
* Clear all messages in a specific mailbox
*
* @param string Mailbox name
* @return int Above 0 on success
*/
function clear_mailbox($mbox_name=NULL)
{
$mailbox = !empty($mbox_name) ? $this->mod_mailbox($mbox_name) : $this->mailbox;
$msg_count = $this->_messagecount($mailbox, 'ALL');
if ($msg_count>0)
{
$cleared = iil_C_ClearFolder($this->conn, $mailbox);
// make sure the message count cache is cleared as well
if ($cleared)
{
$this->clear_message_cache($mailbox.'.msg');
$a_mailbox_cache = $this->get_cache('messagecount');
unset($a_mailbox_cache[$mailbox]);
$this->update_cache('messagecount', $a_mailbox_cache);
}
return $cleared;
}
else
return 0;
}
/**
* Send IMAP expunge command and clear cache
*
* @param string Mailbox name
* @param boolean False if cache should not be cleared
* @return boolean True on success
*/
function expunge($mbox_name='', $clear_cache=TRUE)
{
$mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
return $this->_expunge($mailbox, $clear_cache);
}
/**
* Send IMAP expunge command and clear cache
*
* @see rcube_imap::expunge()
* @param string Mailbox name
* @param boolean False if cache should not be cleared
* @param string List of UIDs to remove, separated by comma
* @return boolean True on success
* @access private
*/
private function _expunge($mailbox, $clear_cache=TRUE, $uids=NULL)
{
if ($uids && $this->get_capability('UIDPLUS'))
$a_uids = is_array($uids) ? join(',', $uids) : $uids;
else
$a_uids = NULL;
$result = iil_C_Expunge($this->conn, $mailbox, $a_uids);
if ($result>=0 && $clear_cache)
{
$this->clear_message_cache($mailbox.'.msg');
$this->_clear_messagecount($mailbox);
}
return $result;
}
/* --------------------------------
* folder managment
* --------------------------------*/
/**
* Get a list of all folders available on the IMAP server
*
* @param string IMAP root dir
* @return array Indexed array with folder names
*/
function list_unsubscribed($root='')
{
static $sa_unsubscribed;
if (is_array($sa_unsubscribed))
return $sa_unsubscribed;
// retrieve list of folders from IMAP server
$a_mboxes = iil_C_ListMailboxes($this->conn, $this->mod_mailbox($root), '*');
// modify names with root dir
foreach ($a_mboxes as $mbox_name)
{
$name = $this->mod_mailbox($mbox_name, 'out');
if (strlen($name))
$a_folders[] = $name;
}
// filter folders and sort them
$sa_unsubscribed = $this->_sort_mailbox_list($a_folders);
return $sa_unsubscribed;
}
/**
* Get mailbox quota information
* added by Nuny
*
* @return mixed Quota info or False if not supported
*/
function get_quota()
{
if ($this->get_capability('QUOTA'))
return iil_C_GetQuota($this->conn);
return FALSE;
}
/**
* Subscribe to a specific mailbox(es)
*
* @param array Mailbox name(s)
* @return boolean True on success
*/
function subscribe($a_mboxes)
{
if (!is_array($a_mboxes))
$a_mboxes = array($a_mboxes);
// let this common function do the main work
return $this->_change_subscription($a_mboxes, 'subscribe');
}
/**
* Unsubscribe mailboxes
*
* @param array Mailbox name(s)
* @return boolean True on success
*/
function unsubscribe($a_mboxes)
{
if (!is_array($a_mboxes))
$a_mboxes = array($a_mboxes);
// let this common function do the main work
return $this->_change_subscription($a_mboxes, 'unsubscribe');
}
/**
* Create a new mailbox on the server and register it in local cache
*
* @param string New mailbox name (as utf-7 string)
* @param boolean True if the new mailbox should be subscribed
* @param string Name of the created mailbox, false on error
*/
function create_mailbox($name, $subscribe=FALSE)
{
$result = FALSE;
// reduce mailbox name to 100 chars
$name = substr($name, 0, 100);
$abs_name = $this->mod_mailbox($name);
$a_mailbox_cache = $this->get_cache('mailboxes');
if (strlen($abs_name) && (!is_array($a_mailbox_cache) || !in_array($abs_name, $a_mailbox_cache)))
$result = iil_C_CreateFolder($this->conn, $abs_name);
// try to subscribe it
if ($result && $subscribe)
$this->subscribe($name);
return $result ? $name : FALSE;
}
/**
* Set a new name to an existing mailbox
*
* @param string Mailbox to rename (as utf-7 string)
* @param string New mailbox name (as utf-7 string)
* @return string Name of the renames mailbox, False on error
*/
function rename_mailbox($mbox_name, $new_name)
{
$result = FALSE;
// encode mailbox name and reduce it to 100 chars
$name = substr($new_name, 0, 100);
// make absolute path
$mailbox = $this->mod_mailbox($mbox_name);
$abs_name = $this->mod_mailbox($name);
// check if mailbox is subscribed
$a_subscribed = $this->_list_mailboxes();
$subscribed = in_array($mailbox, $a_subscribed);
// unsubscribe folder
if ($subscribed)
iil_C_UnSubscribe($this->conn, $mailbox);
if (strlen($abs_name))
$result = iil_C_RenameFolder($this->conn, $mailbox, $abs_name);
if ($result)
{
$delm = $this->get_hierarchy_delimiter();
// check if mailbox children are subscribed
foreach ($a_subscribed as $c_subscribed)
if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_subscribed))
{
iil_C_UnSubscribe($this->conn, $c_subscribed);
iil_C_Subscribe($this->conn, preg_replace('/^'.preg_quote($mailbox, '/').'/', $abs_name, $c_subscribed));
}
// clear cache
$this->clear_message_cache($mailbox.'.msg');
$this->clear_cache('mailboxes');
}
// try to subscribe it
if ($result && $subscribed)
iil_C_Subscribe($this->conn, $abs_name);
return $result ? $name : FALSE;
}
/**
* Remove mailboxes from server
*
* @param string Mailbox name(s) string/array
* @return boolean True on success
*/
function delete_mailbox($mbox_name)
{
$deleted = FALSE;
if (is_array($mbox_name))
$a_mboxes = $mbox_name;
else if (is_string($mbox_name) && strlen($mbox_name))
$a_mboxes = explode(',', $mbox_name);
$all_mboxes = iil_C_ListMailboxes($this->conn, $this->mod_mailbox($root), '*');
if (is_array($a_mboxes))
foreach ($a_mboxes as $mbox_name)
{
$mailbox = $this->mod_mailbox($mbox_name);
// unsubscribe mailbox before deleting
iil_C_UnSubscribe($this->conn, $mailbox);
// send delete command to server
$result = iil_C_DeleteFolder($this->conn, $mailbox);
if ($result >= 0) {
$deleted = TRUE;
$this->clear_message_cache($mailbox.'.msg');
}
foreach ($all_mboxes as $c_mbox)
{
$regex = preg_quote($mailbox . $this->delimiter, '/');
$regex = '/^' . $regex . '/';
if (preg_match($regex, $c_mbox))
{
iil_C_UnSubscribe($this->conn, $c_mbox);
$result = iil_C_DeleteFolder($this->conn, $c_mbox);
if ($result >= 0) {
$deleted = TRUE;
$this->clear_message_cache($c_mbox.'.msg');
}
}
}
}
// clear mailboxlist cache
if ($deleted)
$this->clear_cache('mailboxes');
return $deleted;
}
/**
* Create all folders specified as default
*/
function create_default_folders()
{
$a_folders = iil_C_ListMailboxes($this->conn, $this->mod_mailbox(''), '*');
$a_subscribed = iil_C_ListSubscribed($this->conn, $this->mod_mailbox(''), '*');
// create default folders if they do not exist
foreach ($this->default_folders as $folder)
{
$abs_name = $this->mod_mailbox($folder);
if (!in_array_nocase($abs_name, $a_folders))
$this->create_mailbox($folder, TRUE);
else if (!in_array_nocase($abs_name, $a_subscribed))
$this->subscribe($folder);
}
}
/* --------------------------------
* internal caching methods
* --------------------------------*/
/**
* @access private
*/
function set_caching($set)
{
if ($set && is_object($this->db))
$this->caching_enabled = TRUE;
else
$this->caching_enabled = FALSE;
}
/**
* @access private
*/
function get_cache($key)
{
// read cache (if it was not read before)
if (!count($this->cache) && $this->caching_enabled)
{
return $this->_read_cache_record($key);
}
return $this->cache[$key];
}
/**
* @access private
*/
function update_cache($key, $data)
{
$this->cache[$key] = $data;
$this->cache_changed = TRUE;
$this->cache_changes[$key] = TRUE;
}
/**
* @access private
*/
function write_cache()
{
if ($this->caching_enabled && $this->cache_changed)
{
foreach ($this->cache as $key => $data)
{
if ($this->cache_changes[$key])
$this->_write_cache_record($key, serialize($data));
}
}
}
/**
* @access private
*/
function clear_cache($key=NULL)
{
if (!$this->caching_enabled)
return;
if ($key===NULL)
{
foreach ($this->cache as $key => $data)
$this->_clear_cache_record($key);
$this->cache = array();
$this->cache_changed = FALSE;
$this->cache_changes = array();
}
else
{
$this->_clear_cache_record($key);
$this->cache_changes[$key] = FALSE;
unset($this->cache[$key]);
}
}
/**
* @access private
*/
private function _read_cache_record($key)
{
if ($this->db)
{
// get cached data from DB
$sql_result = $this->db->query(
"SELECT cache_id, data, cache_key
FROM ".get_table_name('cache')."
WHERE user_id=?
AND cache_key LIKE 'IMAP.%'",
$_SESSION['user_id']);
while ($sql_arr = $this->db->fetch_assoc($sql_result))
{
$sql_key = preg_replace('/^IMAP\./', '', $sql_arr['cache_key']);
$this->cache_keys[$sql_key] = $sql_arr['cache_id'];
if (!isset($this->cache[$sql_key]))
$this->cache[$sql_key] = $sql_arr['data'] ? unserialize($sql_arr['data']) : FALSE;
}
}
return $this->cache[$key];
}
/**
* @access private
*/
private function _write_cache_record($key, $data)
{
if (!$this->db)
return FALSE;
// update existing cache record
if ($this->cache_keys[$key])
{
$this->db->query(
"UPDATE ".get_table_name('cache')."
SET created=". $this->db->now().", data=?
WHERE user_id=?
AND cache_key=?",
$data,
$_SESSION['user_id'],
'IMAP.'.$key);
}
// add new cache record
else
{
$this->db->query(
"INSERT INTO ".get_table_name('cache')."
(created, user_id, cache_key, data)
VALUES (".$this->db->now().", ?, ?, ?)",
$_SESSION['user_id'],
'IMAP.'.$key,
$data);
// get cache entry ID for this key
$sql_result = $this->db->query(
"SELECT cache_id
FROM ".get_table_name('cache')."
WHERE user_id=?
AND cache_key=?",
$_SESSION['user_id'],
'IMAP.'.$key);
if ($sql_arr = $this->db->fetch_assoc($sql_result))
$this->cache_keys[$key] = $sql_arr['cache_id'];
}
}
/**
* @access private
*/
private function _clear_cache_record($key)
{
$this->db->query(
"DELETE FROM ".get_table_name('cache')."
WHERE user_id=?
AND cache_key=?",
$_SESSION['user_id'],
'IMAP.'.$key);
unset($this->cache_keys[$key]);
}
/* --------------------------------
* message caching methods
* --------------------------------*/
/**
* Checks if the cache is up-to-date
*
* @param string Mailbox name
* @param string Internal cache key
* @return int Cache status: -3 = off, -2 = incomplete, -1 = dirty
*/
private function check_cache_status($mailbox, $cache_key)
{
if (!$this->caching_enabled)
return -3;
$cache_index = $this->get_message_cache_index($cache_key);
$msg_count = $this->_messagecount($mailbox);
$cache_count = count($cache_index);
// empty mailbox
if (!$msg_count)
return $cache_count ? -2 : 1;
// @TODO: We've got one big performance problem in cache status checking method
// E.g. mailbox contains 1000 messages, in cache table we've got first 100
// of them. Now if we want to display only that 100 (which we've got)
// check_cache_status returns 'incomplete' and messages are fetched
// from IMAP instead of DB.
if ($cache_count==$msg_count) {
if ($this->skip_deleted) {
$h_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, "1:*", 'UID', $this->skip_deleted);
if (sizeof($h_index) == $cache_count) {
$cache_index = array_flip($cache_index);
foreach ($h_index as $idx => $uid)
unset($cache_index[$uid]);
if (empty($cache_index))
return 1;
}
return -2;
} else {
// get UID of message with highest index
$uid = iil_C_ID2UID($this->conn, $mailbox, $msg_count);
$cache_uid = array_pop($cache_index);
// uids of highest message matches -> cache seems OK
if ($cache_uid == $uid)
return 1;
}
// cache is dirty
return -1;
}
// if cache count differs less than 10% report as dirty
else if (abs($msg_count - $cache_count) < $msg_count/10)
return -1;
else
return -2;
}
/**
* @access private
*/
private function get_message_cache($key, $from, $to, $sort_field, $sort_order)
{
$cache_key = "$key:$from:$to:$sort_field:$sort_order";
$db_header_fields = array('idx', 'uid', 'subject', 'from', 'to', 'cc', 'date', 'size');
$config = rcmail::get_instance()->config;
// use idx sort for sorting by Date with index_sort=true or for unknown field
if (($sort_field == 'date' && $this->index_sort)
|| !in_array($sort_field, $db_header_fields)) {
$sort_field = 'idx';
}
if ($this->caching_enabled && !isset($this->cache[$cache_key]))
{
$this->cache[$cache_key] = array();
$sql_result = $this->db->limitquery(
"SELECT idx, uid, headers
FROM ".get_table_name('messages')."
WHERE user_id=?
AND cache_key=?
ORDER BY ".$this->db->quoteIdentifier($sort_field)." ".strtoupper($sort_order),
$from,
$to-$from,
$_SESSION['user_id'],
$key);
while ($sql_arr = $this->db->fetch_assoc($sql_result))
{
$uid = $sql_arr['uid'];
$this->cache[$cache_key][$uid] = $this->db->decode(unserialize($sql_arr['headers']));
// featch headers if unserialize failed
if (empty($this->cache[$cache_key][$uid]))
$this->cache[$cache_key][$uid] = iil_C_FetchHeader($this->conn, preg_replace('/.msg$/', '', $key), $uid, true, $this->fetch_add_headers);
}
}
return $this->cache[$cache_key];
}
/**
* @access private
*/
private function &get_cached_message($key, $uid)
{
$internal_key = '__single_msg';
if ($this->caching_enabled && !isset($this->cache[$internal_key][$uid]))
{
$sql_result = $this->db->query(
"SELECT idx, headers, structure
FROM ".get_table_name('messages')."
WHERE user_id=?
AND cache_key=?
AND uid=?",
$_SESSION['user_id'],
$key,
$uid);
if ($sql_arr = $this->db->fetch_assoc($sql_result))
{
$this->uid_id_map[preg_replace('/\.msg$/', '', $key)][$uid] = $sql_arr['idx'];
$this->cache[$internal_key][$uid] = $this->db->decode(unserialize($sql_arr['headers']));
if (is_object($this->cache[$internal_key][$uid]) && !empty($sql_arr['structure']))
$this->cache[$internal_key][$uid]->structure = $this->db->decode(unserialize($sql_arr['structure']));
}
}
return $this->cache[$internal_key][$uid];
}
/**
* @access private
*/
private function get_message_cache_index($key, $force=FALSE, $sort_field='idx', $sort_order='ASC')
{
static $sa_message_index = array();
// empty key -> empty array
if (!$this->caching_enabled || empty($key))
return array();
if (!empty($sa_message_index[$key]) && !$force)
return $sa_message_index[$key];
// use idx sort for sorting by Date with index_sort=true
if ($sort_field == 'date' && $this->index_sort)
$sort_field = 'idx';
$sa_message_index[$key] = array();
$sql_result = $this->db->query(
"SELECT idx, uid
FROM ".get_table_name('messages')."
WHERE user_id=?
AND cache_key=?
ORDER BY ".$this->db->quote_identifier($sort_field)." ".$sort_order,
$_SESSION['user_id'],
$key);
while ($sql_arr = $this->db->fetch_assoc($sql_result))
$sa_message_index[$key][$sql_arr['idx']] = $sql_arr['uid'];
return $sa_message_index[$key];
}
/**
* @access private
*/
private function add_message_cache($key, $index, $headers, $struct=null, $force=false)
{
if (empty($key) || !is_object($headers) || empty($headers->uid))
return;
// add to internal (fast) cache
$this->cache['__single_msg'][$headers->uid] = clone $headers;
$this->cache['__single_msg'][$headers->uid]->structure = $struct;
// no further caching
if (!$this->caching_enabled)
return;
// check for an existing record (probly headers are cached but structure not)
if (!$force) {
$sql_result = $this->db->query(
"SELECT message_id
FROM ".get_table_name('messages')."
WHERE user_id=?
AND cache_key=?
AND uid=?",
$_SESSION['user_id'],
$key,
$headers->uid);
if ($sql_arr = $this->db->fetch_assoc($sql_result))
$message_id = $sql_arr['message_id'];
}
// update cache record
if ($message_id)
{
$this->db->query(
"UPDATE ".get_table_name('messages')."
SET idx=?, headers=?, structure=?
WHERE message_id=?",
$index,
serialize($this->db->encode(clone $headers)),
is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL,
$message_id
);
}
else // insert new record
{
$this->db->query(
"INSERT INTO ".get_table_name('messages')."
(user_id, del, cache_key, created, idx, uid, subject, ".$this->db->quoteIdentifier('from').", ".$this->db->quoteIdentifier('to').", cc, date, size, headers, structure)
VALUES (?, 0, ?, ".$this->db->now().", ?, ?, ?, ?, ?, ?, ".$this->db->fromunixtime($headers->timestamp).", ?, ?, ?)",
$_SESSION['user_id'],
$key,
$index,
$headers->uid,
(string)mb_substr($this->db->encode($this->decode_header($headers->subject, TRUE)), 0, 128),
(string)mb_substr($this->db->encode($this->decode_header($headers->from, TRUE)), 0, 128),
(string)mb_substr($this->db->encode($this->decode_header($headers->to, TRUE)), 0, 128),
(string)mb_substr($this->db->encode($this->decode_header($headers->cc, TRUE)), 0, 128),
(int)$headers->size,
serialize($this->db->encode(clone $headers)),
is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL
);
}
}
/**
* @access private
*/
private function remove_message_cache($key, $ids, $idx=false)
{
if (!$this->caching_enabled)
return;
$this->db->query(
"DELETE FROM ".get_table_name('messages')."
WHERE user_id=?
AND cache_key=?
AND ".($idx ? "idx" : "uid")." IN (".$this->db->array2list($ids, 'integer').")",
$_SESSION['user_id'],
$key);
}
/**
* @access private
*/
private function clear_message_cache($key, $start_index=1)
{
if (!$this->caching_enabled)
return;
$this->db->query(
"DELETE FROM ".get_table_name('messages')."
WHERE user_id=?
AND cache_key=?
AND idx>=?",
$_SESSION['user_id'], $key, $start_index);
}
/**
* @access private
*/
private function get_message_cache_index_min($key, $uids=NULL)
{
if (!$this->caching_enabled)
return;
$sql_result = $this->db->query(
"SELECT MIN(idx) AS minidx
FROM ".get_table_name('messages')."
WHERE user_id=?
AND cache_key=?"
.(!empty($uids) ? " AND uid IN (".$this->db->array2list($uids, 'integer').")" : ''),
$_SESSION['user_id'],
$key);
if ($sql_arr = $this->db->fetch_assoc($sql_result))
return $sql_arr['minidx'];
else
return 0;
}
/* --------------------------------
* encoding/decoding methods
* --------------------------------*/
/**
* Split an address list into a structured array list
*
* @param string Input string
* @param int List only this number of addresses
* @param boolean Decode address strings
* @return array Indexed list of addresses
*/
function decode_address_list($input, $max=null, $decode=true)
{
$a = $this->_parse_address_list($input, $decode);
$out = array();
// Special chars as defined by RFC 822 need to in quoted string (or escaped).
$special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
if (!is_array($a))
return $out;
$c = count($a);
$j = 0;
foreach ($a as $val)
{
$j++;
$address = $val['address'];
$name = preg_replace(array('/^[\'"]/', '/[\'"]$/'), '', trim($val['name']));
if ($name && $address && $name != $address)
$string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
else if ($address)
$string = $address;
else if ($name)
$string = $name;
$out[$j] = array('name' => $name,
'mailto' => $address,
'string' => $string);
if ($max && $j==$max)
break;
}
return $out;
}
/**
* Decode a Microsoft Outlook TNEF part (winmail.dat)
*
* @param object rcube_message_part Message part to decode
* @param string UID of the message
* @return array List of rcube_message_parts extracted from windmail.dat
*/
function tnef_decode(&$part, $uid)
{
if (!isset($part->body))
$part->body = $this->get_message_part($uid, $part->mime_id, $part);
$pid = 0;
$tnef_parts = array();
$tnef_arr = tnef_decode($part->body);
foreach ($tnef_arr as $winatt) {
$tpart = new rcube_message_part;
$tpart->filename = $winatt["name"];
$tpart->encoding = 'stream';
$tpart->ctype_primary = $winatt["type0"];
$tpart->ctype_secondary = $winatt["type1"];
$tpart->mimetype = strtolower($winatt["type0"] . "/" . $winatt["type1"]);
$tpart->mime_id = "winmail." . $part->mime_id . ".$pid";
$tpart->size = $winatt["size"];
$tpart->body = $winatt['stream'];
$tnef_parts[] = $tpart;
$pid++;
}
return $tnef_parts;
}
/**
* Decode a message header value
*
* @param string Header value
* @param boolean Remove quotes if necessary
* @return string Decoded string
*/
function decode_header($input, $remove_quotes=FALSE)
{
$str = rcube_imap::decode_mime_string((string)$input, $this->default_charset);
if ($str{0}=='"' && $remove_quotes)
$str = str_replace('"', '', $str);
return $str;
}
/**
* Decode a mime-encoded string to internal charset
*
* @param string $input Header value
* @param string $fallback Fallback charset if none specified
*
* @return string Decoded string
* @static
*/
public static function decode_mime_string($input, $fallback=null)
{
// Initialize variable
$out = '';
// Iterate instead of recursing, this way if there are too many values we don't have stack overflows
// rfc: all line breaks or other characters not found
// in the Base64 Alphabet must be ignored by decoding software
// delete all blanks between MIME-lines, differently we can
// receive unnecessary blanks and broken utf-8 symbols
$input = preg_replace("/\?=\s+=\?/", '?==?', $input);
// Check if there is stuff to decode
if (strpos($input, '=?') !== false) {
// Loop through the string to decode all occurences of =? ?= into the variable $out
while(($pos = strpos($input, '=?')) !== false) {
// Append everything that is before the text to be decoded
$out .= substr($input, 0, $pos);
// Get the location of the text to decode
$end_cs_pos = strpos($input, "?", $pos+2);
$end_en_pos = strpos($input, "?", $end_cs_pos+1);
$end_pos = strpos($input, "?=", $end_en_pos+1);
// Extract the encoded string
$encstr = substr($input, $pos+2, ($end_pos-$pos-2));
// Extract the remaining string
$input = substr($input, $end_pos+2);
// Decode the string fragement
$out .= rcube_imap::_decode_mime_string_part($encstr);
}
// Deocde the rest (if any)
if (strlen($input) != 0)
$out .= rcube_imap::decode_mime_string($input, $fallback);
// return the results
return $out;
}
// no encoding information, use fallback
return rcube_charset_convert($input,
!empty($fallback) ? $fallback : rcmail::get_instance()->config->get('default_charset', 'ISO-8859-1'));
}
/**
* Decode a part of a mime-encoded string
*
* @access private
*/
private function _decode_mime_string_part($str)
{
$a = explode('?', $str);
$count = count($a);
// should be in format "charset?encoding?base64_string"
if ($count >= 3)
{
for ($i=2; $i<$count; $i++)
$rest.=$a[$i];
if (($a[1]=="B")||($a[1]=="b"))
$rest = base64_decode($rest);
else if (($a[1]=="Q")||($a[1]=="q"))
{
$rest = str_replace("_", " ", $rest);
$rest = quoted_printable_decode($rest);
}
return rcube_charset_convert($rest, $a[0]);
}
else
return $str; // we dont' know what to do with this
}
/**
* Decode a mime part
*
* @param string Input string
* @param string Part encoding
* @return string Decoded string
*/
function mime_decode($input, $encoding='7bit')
{
switch (strtolower($encoding))
{
case 'quoted-printable':
return quoted_printable_decode($input);
break;
case 'base64':
return base64_decode($input);
break;
case 'x-uuencode':
case 'x-uue':
case 'uue':
case 'uuencode':
return convert_uudecode($input);
break;
case '7bit':
default:
return $input;
}
}
/**
* Convert body charset to RCMAIL_CHARSET according to the ctype_parameters
*
* @param string Part body to decode
* @param string Charset to convert from
* @return string Content converted to internal charset
*/
function charset_decode($body, $ctype_param)
{
if (is_array($ctype_param) && !empty($ctype_param['charset']))
return rcube_charset_convert($body, $ctype_param['charset']);
// defaults to what is specified in the class header
return rcube_charset_convert($body, $this->default_charset);
}
/**
* Translate UID to message ID
*
* @param int Message UID
* @param string Mailbox name
* @return int Message ID
*/
function get_id($uid, $mbox_name=NULL)
{
$mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
return $this->_uid2id($uid, $mailbox);
}
/**
* Translate message number to UID
*
* @param int Message ID
* @param string Mailbox name
* @return int Message UID
*/
function get_uid($id,$mbox_name=NULL)
{
$mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
return $this->_id2uid($id, $mailbox);
}
/**
* Modify folder name for input/output according to root dir and namespace
*
* @param string Folder name
* @param string Mode
* @return string Folder name
*/
function mod_mailbox($mbox_name, $mode='in')
{
if ((!empty($this->root_ns) && $this->root_ns == $mbox_name) || $mbox_name == 'INBOX')
return $mbox_name;
if (!empty($this->root_dir)) {
if ($mode=='in')
$mbox_name = $this->root_dir.$this->delimiter.$mbox_name;
else if (!empty($mbox_name)) // $mode=='out'
$mbox_name = substr($mbox_name, strlen($this->root_dir)+1);
}
return $mbox_name;
}
/* --------------------------------
* private methods
* --------------------------------*/
/**
* Validate the given input and save to local properties
* @access private
*/
private function _set_sort_order($sort_field, $sort_order)
{
if ($sort_field != null)
$this->sort_field = asciiwords($sort_field);
if ($sort_order != null)
$this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
}
/**
* Sort mailboxes first by default folders and then in alphabethical order
* @access private
*/
private function _sort_mailbox_list($a_folders)
{
$a_out = $a_defaults = $folders = array();
$delimiter = $this->get_hierarchy_delimiter();
// find default folders and skip folders starting with '.'
foreach ($a_folders as $i => $folder)
{
if ($folder{0}=='.')
continue;
if (($p = array_search(strtolower($folder), $this->default_folders_lc)) !== false && !$a_defaults[$p])
$a_defaults[$p] = $folder;
else
$folders[$folder] = mb_strtolower(rcube_charset_convert($folder, 'UTF7-IMAP'));
}
// sort folders and place defaults on the top
asort($folders, SORT_LOCALE_STRING);
ksort($a_defaults);
$folders = array_merge($a_defaults, array_keys($folders));
// finally we must rebuild the list to move
// subfolders of default folders to their place...
// ...also do this for the rest of folders because
// asort() is not properly sorting case sensitive names
while (list($key, $folder) = each($folders)) {
// set the type of folder name variable (#1485527)
$a_out[] = (string) $folder;
unset($folders[$key]);
$this->_rsort($folder, $delimiter, $folders, $a_out);
}
return $a_out;
}
/**
* @access private
*/
private function _rsort($folder, $delimiter, &$list, &$out)
{
while (list($key, $name) = each($list)) {
if (strpos($name, $folder.$delimiter) === 0) {
// set the type of folder name variable (#1485527)
$out[] = (string) $name;
unset($list[$key]);
$this->_rsort($name, $delimiter, $list, $out);
}
}
reset($list);
}
/**
* @access private
*/
private function _uid2id($uid, $mbox_name=NULL)
{
if (!$mbox_name)
$mbox_name = $this->mailbox;
if (!isset($this->uid_id_map[$mbox_name][$uid]))
$this->uid_id_map[$mbox_name][$uid] = iil_C_UID2ID($this->conn, $mbox_name, $uid);
return $this->uid_id_map[$mbox_name][$uid];
}
/**
* @access private
*/
private function _id2uid($id, $mbox_name=NULL)
{
if (!$mbox_name)
$mbox_name = $this->mailbox;
$index = array_flip((array)$this->uid_id_map[$mbox_name]);
if (isset($index[$id]))
$uid = $index[$id];
else
{
$uid = iil_C_ID2UID($this->conn, $mbox_name, $id);
$this->uid_id_map[$mbox_name][$uid] = $id;
}
return $uid;
}
/**
* Subscribe/unsubscribe a list of mailboxes and update local cache
* @access private
*/
private function _change_subscription($a_mboxes, $mode)
{
$updated = FALSE;
if (is_array($a_mboxes))
foreach ($a_mboxes as $i => $mbox_name)
{
$mailbox = $this->mod_mailbox($mbox_name);
$a_mboxes[$i] = $mailbox;
if ($mode=='subscribe')
$updated = iil_C_Subscribe($this->conn, $mailbox);
else if ($mode=='unsubscribe')
$updated = iil_C_UnSubscribe($this->conn, $mailbox);
}
// get cached mailbox list
if ($updated)
{
$a_mailbox_cache = $this->get_cache('mailboxes');
if (!is_array($a_mailbox_cache))
return $updated;
// modify cached list
if ($mode=='subscribe')
$a_mailbox_cache = array_merge($a_mailbox_cache, $a_mboxes);
else if ($mode=='unsubscribe')
$a_mailbox_cache = array_diff($a_mailbox_cache, $a_mboxes);
// write mailboxlist to cache
$this->update_cache('mailboxes', $this->_sort_mailbox_list($a_mailbox_cache));
}
return $updated;
}
/**
* Increde/decrese messagecount for a specific mailbox
* @access private
*/
private function _set_messagecount($mbox_name, $mode, $increment)
{
$a_mailbox_cache = FALSE;
$mailbox = $mbox_name ? $mbox_name : $this->mailbox;
$mode = strtoupper($mode);
$a_mailbox_cache = $this->get_cache('messagecount');
if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
return FALSE;
// add incremental value to messagecount
$a_mailbox_cache[$mailbox][$mode] += $increment;
// there's something wrong, delete from cache
if ($a_mailbox_cache[$mailbox][$mode] < 0)
unset($a_mailbox_cache[$mailbox][$mode]);
// write back to cache
$this->update_cache('messagecount', $a_mailbox_cache);
return TRUE;
}
/**
* Remove messagecount of a specific mailbox from cache
* @access private
*/
private function _clear_messagecount($mbox_name='')
{
$a_mailbox_cache = FALSE;
$mailbox = $mbox_name ? $mbox_name : $this->mailbox;
$a_mailbox_cache = $this->get_cache('messagecount');
if (is_array($a_mailbox_cache[$mailbox]))
{
unset($a_mailbox_cache[$mailbox]);
$this->update_cache('messagecount', $a_mailbox_cache);
}
}
/**
* Split RFC822 header string into an associative array
* @access private
*/
private function _parse_headers($headers)
{
$a_headers = array();
$headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
$lines = explode("\n", $headers);
$c = count($lines);
for ($i=0; $i<$c; $i++)
{
if ($p = strpos($lines[$i], ': '))
{
$field = strtolower(substr($lines[$i], 0, $p));
$value = trim(substr($lines[$i], $p+1));
if (!empty($value))
$a_headers[$field] = $value;
}
}
return $a_headers;
}
/**
* @access private
*/
private function _parse_address_list($str, $decode=true)
{
// remove any newlines and carriage returns before
$a = rcube_explode_quoted_string('[,;]', preg_replace( "/[\r\n]/", " ", $str));
$result = array();
foreach ($a as $key => $val)
{
$val = preg_replace("/([\"\w])</", "$1 <", $val);
$sub_a = rcube_explode_quoted_string(' ', $decode ? $this->decode_header($val) : $val);
$result[$key]['name'] = '';
foreach ($sub_a as $k => $v)
{
// use angle brackets in regexp to not handle names with @ sign
if (preg_match('/^<\S+@\S+>$/', $v))
$result[$key]['address'] = trim($v, '<>');
else
$result[$key]['name'] .= (empty($result[$key]['name'])?'':' ').str_replace("\"",'',stripslashes($v));
}
if (empty($result[$key]['name']))
$result[$key]['name'] = $result[$key]['address'];
elseif (empty($result[$key]['address']))
$result[$key]['address'] = $result[$key]['name'];
}
return $result;
}
} // end class rcube_imap
/**
* Class representing a message part
*
* @package Mail
*/
class rcube_message_part
{
var $mime_id = '';
var $ctype_primary = 'text';
var $ctype_secondary = 'plain';
var $mimetype = 'text/plain';
var $disposition = '';
var $filename = '';
var $encoding = '8bit';
var $charset = '';
var $size = 0;
var $headers = array();
var $d_parameters = array();
var $ctype_parameters = array();
function __clone()
{
if (isset($this->parts))
foreach ($this->parts as $idx => $part)
if (is_object($part))
$this->parts[$idx] = clone $part;
}
}
/**
* Class for sorting an array of iilBasicHeader objects in a predetermined order.
*
* @package Mail
* @author Eric Stadtherr
*/
class rcube_header_sorter
{
var $sequence_numbers = array();
/**
* Set the predetermined sort order.
*
* @param array Numerically indexed array of IMAP message sequence numbers
*/
function set_sequence_numbers($seqnums)
{
$this->sequence_numbers = array_flip($seqnums);
}
/**
* Sort the array of header objects
*
* @param array Array of iilBasicHeader objects indexed by UID
*/
function sort_headers(&$headers)
{
/*
* uksort would work if the keys were the sequence number, but unfortunately
* the keys are the UIDs. We'll use uasort instead and dereference the value
* to get the sequence number (in the "id" field).
*
* uksort($headers, array($this, "compare_seqnums"));
*/
uasort($headers, array($this, "compare_seqnums"));
}
/**
* Sort method called by uasort()
*/
function compare_seqnums($a, $b)
{
// First get the sequence number from the header object (the 'id' field).
$seqa = $a->id;
$seqb = $b->id;
// then find each sequence number in my ordered list
$posa = isset($this->sequence_numbers[$seqa]) ? intval($this->sequence_numbers[$seqa]) : -1;
$posb = isset($this->sequence_numbers[$seqb]) ? intval($this->sequence_numbers[$seqb]) : -1;
// return the relative position as the comparison value
return $posa - $posb;
}
}
diff --git a/program/include/rcube_ldap.php b/program/include/rcube_ldap.php
index 286bbff53..2b2bec280 100644
--- a/program/include/rcube_ldap.php
+++ b/program/include/rcube_ldap.php
@@ -1,715 +1,719 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcube_ldap.php |
| |
| This file is part of the RoundCube Webmail client |
| Copyright (C) 2006-2009, RoundCube Dev. - Switzerland |
| Licensed under the GNU GPL |
| |
| PURPOSE: |
| Interface to an LDAP address directory |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
$Id$
*/
/**
* Model class to access an LDAP address directory
*
* @package Addressbook
*/
class rcube_ldap extends rcube_addressbook
{
var $conn;
var $prop = array();
var $fieldmap = array();
var $filter = '';
var $result = null;
var $ldap_result = null;
var $sort_col = '';
var $mail_domain = '';
var $debug = false;
/** public properties */
var $primary_key = 'ID';
var $readonly = true;
var $list_page = 1;
var $page_size = 10;
var $ready = false;
/**
* Object constructor
*
* @param array LDAP connection properties
* @param boolean Enables debug mode
* @param string Current user mail domain name
* @param integer User-ID
*/
function __construct($p, $debug=false, $mail_domain=NULL)
{
$this->prop = $p;
foreach ($p as $prop => $value)
if (preg_match('/^(.+)_field$/', $prop, $matches))
$this->fieldmap[$matches[1]] = $this->_attr_name(strtolower($value));
// make sure 'required_fields' is an array
if (!is_array($this->prop['required_fields']))
$this->prop['required_fields'] = (array) $this->prop['required_fields'];
foreach ($this->prop['required_fields'] as $key => $val)
$this->prop['required_fields'][$key] = $this->_attr_name(strtolower($val));
$this->sort_col = $p['sort'];
$this->debug = $debug;
$this->mail_domain = $mail_domain;
$this->connect();
}
/**
* Establish a connection to the LDAP server
*/
function connect()
{
global $RCMAIL;
if (!function_exists('ldap_connect'))
- raise_error(array('code' => 100, 'type' => 'ldap', 'message' => "No ldap support in this installation of PHP"), true);
+ raise_error(array('code' => 100, 'type' => 'ldap',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "No ldap support in this installation of PHP"), true);
if (is_resource($this->conn))
return true;
if (!is_array($this->prop['hosts']))
$this->prop['hosts'] = array($this->prop['hosts']);
if (empty($this->prop['ldap_version']))
$this->prop['ldap_version'] = 3;
foreach ($this->prop['hosts'] as $host)
{
$this->_debug("C: Connect [$host".($this->prop['port'] ? ':'.$this->prop['port'] : '')."]");
if ($lc = @ldap_connect($host, $this->prop['port']))
{
if ($this->prop['use_tls']===true)
if (!ldap_start_tls($lc))
continue;
$this->_debug("S: OK");
ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->prop['ldap_version']);
$this->prop['host'] = $host;
$this->conn = $lc;
break;
}
$this->_debug("S: NOT OK");
}
if (is_resource($this->conn))
{
$this->ready = true;
// User specific access, generate the proper values to use.
if ($this->prop['user_specific']) {
// No password set, use the session password
if (empty($this->prop['bind_pass'])) {
$this->prop['bind_pass'] = $RCMAIL->decrypt($_SESSION['password']);
}
// Get the pieces needed for variable replacement.
$fu = $RCMAIL->user->get_username();
list($u, $d) = explode('@', $fu);
// Replace the bind_dn and base_dn variables.
$replaces = array('%fu' => $fu, '%u' => $u, '%d' => $d);
$this->prop['bind_dn'] = strtr($this->prop['bind_dn'], $replaces);
$this->prop['base_dn'] = strtr($this->prop['base_dn'], $replaces);
}
if (!empty($this->prop['bind_dn']) && !empty($this->prop['bind_pass']))
$this->ready = $this->bind($this->prop['bind_dn'], $this->prop['bind_pass']);
}
else
- raise_error(array('code' => 100, 'type' => 'ldap', 'message' => "Could not connect to any LDAP server, tried $host:{$this->prop[port]} last"), true);
+ raise_error(array('code' => 100, 'type' => 'ldap',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Could not connect to any LDAP server, last tried $host:{$this->prop[port]}"), true);
// See if the directory is writeable.
if ($this->prop['writable']) {
$this->readonly = false;
} // end if
}
/**
* Bind connection with DN and password
*
* @param string Bind DN
* @param string Bind password
* @return boolean True on success, False on error
*/
function bind($dn, $pass)
{
if (!$this->conn) {
return false;
}
$this->_debug("C: Bind [dn: $dn] [pass: $pass]");
if (@ldap_bind($this->conn, $dn, $pass)) {
$this->_debug("S: OK");
return true;
}
$this->_debug("S: ".ldap_error($this->conn));
raise_error(array(
- 'code' => ldap_errno($this->conn),
- 'type' => 'ldap',
+ 'code' => ldap_errno($this->conn), 'type' => 'ldap',
+ 'file' => __FILE__, 'line' => __LINE__,
'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)),
true);
return false;
}
/**
* Close connection to LDAP server
*/
function close()
{
if ($this->conn)
{
$this->_debug("C: Close");
ldap_unbind($this->conn);
$this->conn = null;
}
}
/**
* Set internal list page
*
* @param number Page number to list
* @access public
*/
function set_page($page)
{
$this->list_page = (int)$page;
}
/**
* Set internal page size
*
* @param number Number of messages to display on one page
* @access public
*/
function set_pagesize($size)
{
$this->page_size = (int)$size;
}
/**
* Save a search string for future listings
*
* @param string Filter string
*/
function set_search_set($filter)
{
$this->filter = $filter;
}
/**
* Getter for saved search properties
*
* @return mixed Search properties used by this class
*/
function get_search_set()
{
return $this->filter;
}
/**
* Reset all saved results and search parameters
*/
function reset()
{
$this->result = null;
$this->ldap_result = null;
$this->filter = '';
}
/**
* List the current set of contact records
*
* @param array List of cols to show
* @param int Only return this number of records
* @return array Indexed list of contact records, each a hash array
*/
function list_records($cols=null, $subset=0)
{
// add general filter to query
if (!empty($this->prop['filter']) && empty($this->filter))
{
$filter = $this->prop['filter'];
$this->set_search_set($filter);
}
// exec LDAP search if no result resource is stored
if ($this->conn && !$this->ldap_result)
$this->_exec_search();
// count contacts for this user
$this->result = $this->count();
// we have a search result resource
if ($this->ldap_result && $this->result->count > 0)
{
if ($this->sort_col && $this->prop['scope'] !== 'base')
ldap_sort($this->conn, $this->ldap_result, $this->sort_col);
$start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
$last_row = $this->result->first + $this->page_size;
$last_row = $subset != 0 ? $start_row + abs($subset) : $last_row;
$entries = ldap_get_entries($this->conn, $this->ldap_result);
for ($i = $start_row; $i < min($entries['count'], $last_row); $i++)
$this->result->add($this->_ldap2result($entries[$i]));
}
return $this->result;
}
/**
* Search contacts
*
* @param array List of fields to search in
* @param string Search value
* @param boolean True if results are requested, False if count only
* @return array Indexed list of contact records and 'count' value
*/
function search($fields, $value, $strict=false, $select=true)
{
// special treatment for ID-based search
if ($fields == 'ID' || $fields == $this->primary_key)
{
$ids = explode(',', $value);
$result = new rcube_result_set();
foreach ($ids as $id)
if ($rec = $this->get_record($id, true))
{
$result->add($rec);
$result->count++;
}
return $result;
}
$filter = '(|';
$wc = !$strict && $this->prop['fuzzy_search'] ? '*' : '';
if (is_array($this->prop['search_fields']))
{
foreach ($this->prop['search_fields'] as $k => $field)
$filter .= "($field=$wc" . rcube_ldap::quote_string($value) . "$wc)";
}
else
{
foreach ((array)$fields as $field)
if ($f = $this->_map_field($field))
$filter .= "($f=$wc" . rcube_ldap::quote_string($value) . "$wc)";
}
$filter .= ')';
// avoid double-wildcard if $value is empty
$filter = preg_replace('/\*+/', '*', $filter);
// add general filter to query
if (!empty($this->prop['filter']))
$filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['filter']) . ')' . $filter . ')';
// set filter string and execute search
$this->set_search_set($filter);
$this->_exec_search();
if ($select)
$this->list_records();
else
$this->result = $this->count();
return $this->result;
}
/**
* Count number of available contacts in database
*
* @return object rcube_result_set Resultset with values for 'count' and 'first'
*/
function count()
{
$count = 0;
if ($this->conn && $this->ldap_result) {
$count = ldap_count_entries($this->conn, $this->ldap_result);
} // end if
elseif ($this->conn) {
// We have a connection but no result set, attempt to get one.
if (empty($this->filter)) {
// The filter is not set, set it.
$this->filter = $this->prop['filter'];
} // end if
$this->_exec_search();
if ($this->ldap_result) {
$count = ldap_count_entries($this->conn, $this->ldap_result);
} // end if
} // end else
return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
}
/**
* Return the last result set
*
* @return object rcube_result_set Current resultset or NULL if nothing selected yet
*/
function get_result()
{
return $this->result;
}
/**
* Get a specific contact record
*
* @param mixed Record identifier
* @param boolean Return as associative array
* @return mixed Hash array or rcube_result_set with all record fields
*/
function get_record($dn, $assoc=false)
{
$res = null;
if ($this->conn && $dn)
{
$dn = base64_decode($dn);
$this->_debug("C: Read [dn: $dn] [(objectclass=*)]");
if ($this->ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', array_values($this->fieldmap)))
$entry = ldap_first_entry($this->conn, $this->ldap_result);
else
$this->_debug("S: ".ldap_error($this->conn));
if ($entry && ($rec = ldap_get_attributes($this->conn, $entry)))
{
$this->_debug("S: OK");
$rec = array_change_key_case($rec, CASE_LOWER);
// Add in the dn for the entry.
$rec['dn'] = $dn;
$res = $this->_ldap2result($rec);
$this->result = new rcube_result_set(1);
$this->result->add($res);
}
}
return $assoc ? $res : $this->result;
}
/**
* Create a new contact record
*
* @param array Hash array with save data
* @return encoded record ID on success, False on error
*/
function insert($save_cols)
{
// Map out the column names to their LDAP ones to build the new entry.
$newentry = array();
$newentry['objectClass'] = $this->prop['LDAP_Object_Classes'];
foreach ($save_cols as $col => $val) {
$fld = $this->_map_field($col);
if ($fld && $val) {
// The field does exist, add it to the entry.
$newentry[$fld] = $val;
} // end if
} // end foreach
// Verify that the required fields are set.
// We know that the email address is required as a default of rcube, so
// we will default its value into any unfilled required fields.
foreach ($this->prop['required_fields'] as $fld) {
if (!isset($newentry[$fld])) {
$newentry[$fld] = $newentry[$this->_map_field('email')];
} // end if
} // end foreach
// Build the new entries DN.
$dn = $this->prop['LDAP_rdn'].'='.rcube_ldap::quote_string($newentry[$this->prop['LDAP_rdn']], true)
.','.$this->prop['base_dn'];
$this->_debug("C: Add [dn: $dn]: ".print_r($newentry, true));
$res = ldap_add($this->conn, $dn, $newentry);
if ($res === FALSE) {
$this->_debug("S: ".ldap_error($this->conn));
return false;
} // end if
$this->_debug("S: OK");
return base64_encode($dn);
}
/**
* Update a specific contact record
*
* @param mixed Record identifier
* @param array Hash array with save data
* @return boolean True on success, False on error
*/
function update($id, $save_cols)
{
$record = $this->get_record($id, true);
$result = $this->get_result();
$record = $result->first();
$newdata = array();
$replacedata = array();
$deletedata = array();
foreach ($save_cols as $col => $val) {
$fld = $this->_map_field($col);
if ($fld) {
// The field does exist compare it to the ldap record.
if ($record[$col] != $val) {
// Changed, but find out how.
if (!isset($record[$col])) {
// Field was not set prior, need to add it.
$newdata[$fld] = $val;
} // end if
elseif ($val == '') {
// Field supplied is empty, verify that it is not required.
if (!in_array($fld, $this->prop['required_fields'])) {
// It is not, safe to clear.
$deletedata[$fld] = $record[$col];
} // end if
} // end elseif
else {
// The data was modified, save it out.
$replacedata[$fld] = $val;
} // end else
} // end if
} // end if
} // end foreach
$dn = base64_decode($id);
// Update the entry as required.
if (!empty($deletedata)) {
// Delete the fields.
$this->_debug("C: Delete [dn: $dn]: ".print_r($deletedata, true));
if (!ldap_mod_del($this->conn, $dn, $deletedata)) {
$this->_debug("S: ".ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
} // end if
if (!empty($replacedata)) {
// Handle RDN change
if ($replacedata[$this->prop['LDAP_rdn']]) {
$newdn = $this->prop['LDAP_rdn'].'='
.rcube_ldap::quote_string($replacedata[$this->prop['LDAP_rdn']], true)
.','.$this->prop['base_dn'];
if ($dn != $newdn) {
$newrdn = $this->prop['LDAP_rdn'].'='
.rcube_ldap::quote_string($replacedata[$this->prop['LDAP_rdn']], true);
unset($replacedata[$this->prop['LDAP_rdn']]);
}
}
// Replace the fields.
if (!empty($replacedata)) {
$this->_debug("C: Replace [dn: $dn]: ".print_r($replacedata, true));
if (!ldap_mod_replace($this->conn, $dn, $replacedata)) {
$this->_debug("S: ".ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
} // end if
} // end if
if (!empty($newdata)) {
// Add the fields.
$this->_debug("C: Add [dn: $dn]: ".print_r($newdata, true));
if (!ldap_mod_add($this->conn, $dn, $newdata)) {
$this->_debug("S: ".ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
} // end if
// Handle RDN change
if (!empty($newrdn)) {
$this->_debug("C: Rename [dn: $dn] [dn: $newrdn]");
if (@ldap_rename($this->conn, $dn, $newrdn, NULL, TRUE)) {
$this->_debug("S: ".ldap_error($this->conn));
return base64_encode($newdn);
}
$this->_debug("S: OK");
}
return true;
}
/**
* Mark one or more contact records as deleted
*
* @param array Record identifiers
* @return boolean True on success, False on error
*/
function delete($ids)
{
if (!is_array($ids)) {
// Not an array, break apart the encoded DNs.
$dns = explode(',', $ids);
} // end if
foreach ($dns as $id) {
$dn = base64_decode($id);
$this->_debug("C: Delete [dn: $dn]");
// Delete the record.
$res = ldap_delete($this->conn, $dn);
if ($res === FALSE) {
$this->_debug("S: ".ldap_error($this->conn));
return false;
} // end if
$this->_debug("S: OK");
} // end foreach
return count($dns);
}
/**
* Execute the LDAP search based on the stored credentials
*
* @access private
*/
private function _exec_search()
{
if ($this->ready)
{
$filter = $this->filter ? $this->filter : '(objectclass=*)';
$function = $this->prop['scope'] == 'sub' ? 'ldap_search' : ($this->prop['scope'] == 'base' ? 'ldap_read' : 'ldap_list');
$this->_debug("C: Search [".$filter."]");
if ($this->ldap_result = @$function($this->conn, $this->prop['base_dn'], $filter, array_values($this->fieldmap), 0, 0)) {
$this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)");
return true;
} else
$this->_debug("S: ".ldap_error($this->conn));
}
return false;
}
/**
* @access private
*/
private function _ldap2result($rec)
{
global $RCMAIL;
$out = array();
if ($rec['dn'])
$out[$this->primary_key] = base64_encode($rec['dn']);
foreach ($this->fieldmap as $rf => $lf)
{
if ($rec[$lf]['count']) {
if ($rf == 'email' && $this->mail_domain && !strpos($rec[$lf][0], '@'))
$out[$rf] = sprintf('%s@%s', $rec[$lf][0], $this->mail_domain);
else
$out[$rf] = $rec[$lf][0];
}
}
return $out;
}
/**
* @access private
*/
private function _map_field($field)
{
return $this->fieldmap[$field];
}
/**
* @access private
*/
private function _attr_name($name)
{
// list of known attribute aliases
$aliases = array(
'gn' => 'givenname',
'rfc822mailbox' => 'mail',
'userid' => 'uid',
'emailaddress' => 'email',
'pkcs9email' => 'email',
);
return isset($aliases[$name]) ? $aliases[$name] : $name;
}
/**
* @access private
*/
private function _debug($str)
{
if ($this->debug)
write_log('ldap', $str);
}
/**
* @static
*/
function quote_string($str, $dn=false)
{
if ($dn)
$replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c',
'>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23');
else
$replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c',
'/'=>'\2f');
return strtr($str, $replace);
}
}
?>
diff --git a/program/include/rcube_mdb2.php b/program/include/rcube_mdb2.php
index 137ffb273..e8dd7e0a7 100644
--- a/program/include/rcube_mdb2.php
+++ b/program/include/rcube_mdb2.php
@@ -1,714 +1,716 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcube_mdb2.php |
| |
| This file is part of the RoundCube Webmail client |
| Copyright (C) 2005-2009, RoundCube Dev. - Switzerland |
| Licensed under the GNU GPL |
| |
| PURPOSE: |
| PEAR:DB wrapper class that implements PEAR MDB2 functions |
| See http://pear.php.net/package/MDB2 |
| |
+-----------------------------------------------------------------------+
| Author: Lukas Kahwe Smith <smith@pooteeweet.org> |
+-----------------------------------------------------------------------+
$Id$
*/
/**
* Database independent query interface
*
* This is a wrapper for the PEAR::MDB2 class
*
* @package Database
* @author David Saez Padros <david@ols.es>
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Lukas Kahwe Smith <smith@pooteeweet.org>
* @version 1.16
* @link http://pear.php.net/package/MDB2
*/
class rcube_mdb2
{
var $db_dsnw; // DSN for write operations
var $db_dsnr; // DSN for read operations
var $db_connected = false; // Already connected ?
var $db_mode = ''; // Connection mode
var $db_handle = 0; // Connection handle
var $db_error = false;
var $db_error_msg = '';
var $debug_mode = false;
var $a_query_results = array('dummy');
var $last_res_id = 0;
/**
* Object constructor
*
* @param string DSN for read/write operations
* @param string Optional DSN for read only operations
*/
function __construct($db_dsnw, $db_dsnr='', $pconn=false)
{
if ($db_dsnr=='')
$db_dsnr=$db_dsnw;
$this->db_dsnw = $db_dsnw;
$this->db_dsnr = $db_dsnr;
$this->db_pconn = $pconn;
$dsn_array = MDB2::parseDSN($db_dsnw);
$this->db_provider = $dsn_array['phptype'];
}
/**
* Connect to specific database
*
* @param string DSN for DB connections
* @return object PEAR database handle
* @access private
*/
function dsn_connect($dsn)
{
// Use persistent connections if available
$db_options = array(
'persistent' => $this->db_pconn,
'emulate_prepared' => $this->debug_mode,
'debug' => $this->debug_mode,
'debug_handler' => 'mdb2_debug_handler',
'portability' => MDB2_PORTABILITY_ALL ^ MDB2_PORTABILITY_EMPTY_TO_NULL);
if ($this->db_provider == 'pgsql') {
$db_options['disable_smart_seqname'] = true;
$db_options['seqname_format'] = '%s';
}
$dbh = MDB2::connect($dsn, $db_options);
if (MDB2::isError($dbh))
{
$this->db_error = TRUE;
$this->db_error_msg = $dbh->getMessage();
raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__,
'file' => __FILE__, 'message' => $dbh->getUserInfo()), TRUE, FALSE);
}
else if ($this->db_provider=='sqlite')
{
$dsn_array = MDB2::parseDSN($dsn);
if (!filesize($dsn_array['database']) && !empty($this->sqlite_initials))
$this->_sqlite_create_database($dbh, $this->sqlite_initials);
}
else if ($this->db_provider!='mssql' && $this->db_provider!='sqlsrv')
$dbh->setCharset('utf8');
return $dbh;
}
/**
* Connect to appropiate databse
* depending on the operation
*
* @param string Connection mode (r|w)
* @access public
*/
function db_connect($mode)
{
$this->db_mode = $mode;
// Already connected
if ($this->db_connected)
{
// no replication, current connection is ok
if ($this->db_dsnw==$this->db_dsnr)
return;
// connected to master, current connection is ok
if ($this->db_mode=='w')
return;
// Same mode, current connection is ok
if ($this->db_mode==$mode)
return;
}
if ($mode=='r')
$dsn = $this->db_dsnr;
else
$dsn = $this->db_dsnw;
$this->db_handle = $this->dsn_connect($dsn);
$this->db_connected = true;
}
/**
* Activate/deactivate debug mode
*
* @param boolean True if SQL queries should be logged
*/
function set_debug($dbg = true)
{
$this->debug_mode = $dbg;
if ($this->db_connected)
{
$this->db_handle->setOption('debug', $dbg);
$this->db_handle->setOption('emulate_prepared', $dbg);
}
}
/**
* Getter for error state
*
* @param boolean True on error
*/
function is_error()
{
return $this->db_error ? $this->db_error_msg : FALSE;
}
/**
* Connection state checker
*
* @param boolean True if in connected state
*/
function is_connected()
{
return PEAR::isError($this->db_handle) ? false : true;
}
/**
* Execute a SQL query
*
* @param string SQL query to execute
* @param mixed Values to be inserted in query
* @return number Query handle identifier
* @access public
*/
function query()
{
if (!$this->is_connected())
return NULL;
$params = func_get_args();
$query = array_shift($params);
return $this->_query($query, 0, 0, $params);
}
/**
* Execute a SQL query with limits
*
* @param string SQL query to execute
* @param number Offset for LIMIT statement
* @param number Number of rows for LIMIT statement
* @param mixed Values to be inserted in query
* @return number Query handle identifier
* @access public
*/
function limitquery()
{
$params = func_get_args();
$query = array_shift($params);
$offset = array_shift($params);
$numrows = array_shift($params);
return $this->_query($query, $offset, $numrows, $params);
}
/**
* Execute a SQL query with limits
*
* @param string SQL query to execute
* @param number Offset for LIMIT statement
* @param number Number of rows for LIMIT statement
* @param array Values to be inserted in query
* @return number Query handle identifier
* @access private
*/
function _query($query, $offset, $numrows, $params)
{
// Read or write ?
if (strtolower(substr(trim($query),0,6))=='select')
$mode='r';
else
$mode='w';
$this->db_connect($mode);
if ($this->db_provider == 'sqlite')
$this->_sqlite_prepare();
if ($numrows || $offset)
$result = $this->db_handle->setLimit($numrows,$offset);
if (empty($params))
$result = $mode=='r' ? $this->db_handle->query($query) : $this->db_handle->exec($query);
else
{
$params = (array)$params;
$q = $this->db_handle->prepare($query, null, $mode=='w' ? MDB2_PREPARE_MANIP : null);
if ($this->db_handle->isError($q))
{
$this->db_error = TRUE;
$this->db_error_msg = $q->userinfo;
- raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__, 'file' => __FILE__,
- 'message' => $this->db_error_msg), TRUE, TRUE);
+ raise_error(array('code' => 500, 'type' => 'db',
+ 'line' => __LINE__, 'file' => __FILE__,
+ 'message' => $this->db_error_msg), TRUE, TRUE);
}
else
{
$result = $q->execute($params);
$q->free();
}
}
// add result, even if it's an error
return $this->_add_result($result);
}
/**
* Get number of rows for a SQL query
* If no query handle is specified, the last query will be taken as reference
*
* @param number Optional query handle identifier
* @return mixed Number of rows or FALSE on failure
* @access public
*/
function num_rows($res_id=NULL)
{
if (!$this->db_handle)
return FALSE;
if ($result = $this->_get_result($res_id))
return $result->numRows();
else
return FALSE;
}
/**
* Get number of affected rows for the last query
*
* @param number Optional query handle identifier
* @return mixed Number of rows or FALSE on failure
* @access public
*/
function affected_rows($res_id = null)
{
if (!$this->db_handle)
return FALSE;
return (int) $this->_get_result($res_id);
}
/**
* Get last inserted record ID
* For Postgres databases, a sequence name is required
*
* @param string Table name (to find the incremented sequence)
* @return mixed ID or FALSE on failure
* @access public
*/
function insert_id($table = '')
{
if (!$this->db_handle || $this->db_mode=='r')
return FALSE;
// find sequence name
if ($table && $this->db_provider == 'pgsql')
$table = get_sequence_name($table);
$id = $this->db_handle->lastInsertID($table);
return $this->db_handle->isError($id) ? null : $id;
}
/**
* Get an associative array for one row
* If no query handle is specified, the last query will be taken as reference
*
* @param number Optional query handle identifier
* @return mixed Array with col values or FALSE on failure
* @access public
*/
function fetch_assoc($res_id=NULL)
{
$result = $this->_get_result($res_id);
return $this->_fetch_row($result, MDB2_FETCHMODE_ASSOC);
}
/**
* Get an index array for one row
* If no query handle is specified, the last query will be taken as reference
*
* @param number Optional query handle identifier
* @return mixed Array with col values or FALSE on failure
* @access public
*/
function fetch_array($res_id=NULL)
{
$result = $this->_get_result($res_id);
return $this->_fetch_row($result, MDB2_FETCHMODE_ORDERED);
}
/**
* Get col values for a result row
*
* @param object Query result handle
* @param number Fetch mode identifier
* @return mixed Array with col values or FALSE on failure
* @access private
*/
function _fetch_row($result, $mode)
{
if ($result === FALSE || PEAR::isError($result) || !$this->is_connected())
return FALSE;
return $result->fetchRow($mode);
}
/**
* Formats input so it can be safely used in a query
*
* @param mixed Value to quote
* @return string Quoted/converted string for use in query
* @access public
*/
function quote($input, $type = null)
{
// create DB handle if not available
if (!$this->db_handle)
$this->db_connect('r');
// escape pear identifier chars
$rep_chars = array('?' => '\?',
'!' => '\!',
'&' => '\&');
return $this->db_handle->quote($input, $type);
}
/**
* Quotes a string so it can be safely used as a table or column name
*
* @param string Value to quote
* @return string Quoted string for use in query
* @deprecated Replaced by rcube_MDB2::quote_identifier
* @see rcube_mdb2::quote_identifier
* @access public
*/
function quoteIdentifier($str)
{
return $this->quote_identifier($str);
}
/**
* Quotes a string so it can be safely used as a table or column name
*
* @param string Value to quote
* @return string Quoted string for use in query
* @access public
*/
function quote_identifier($str)
{
if (!$this->db_handle)
$this->db_connect('r');
return $this->db_handle->quoteIdentifier($str);
}
/**
* Escapes a string
*
* @param string The string to be escaped
* @return string The escaped string
* @access public
* @since 0.1.1
*/
function escapeSimple($str)
{
if (!$this->db_handle)
$this->db_connect('r');
return $this->db_handle->escape($str);
}
/**
* Return SQL function for current time and date
*
* @return string SQL function to use in query
* @access public
*/
function now()
{
switch($this->db_provider)
{
case 'mssql':
case 'sqlsrv':
return "getdate()";
default:
return "now()";
}
}
/**
* Return list of elements for use with SQL's IN clause
*
* @param string Input array
* @return string Elements list string
* @access public
*/
function array2list($arr, $type=null)
{
if (!is_array($arr))
return $this->quote($arr, $type);
$res = array();
foreach ($arr as $item)
$res[] = $this->quote($item, $type);
return implode(',', $res);
}
/**
* Return SQL statement to convert a field value into a unix timestamp
*
* @param string Field name
* @return string SQL statement to use in query
* @access public
*/
function unixtimestamp($field)
{
switch($this->db_provider)
{
case 'pgsql':
return "EXTRACT (EPOCH FROM $field)";
break;
case 'mssql':
case 'sqlsrv':
return "DATEDIFF(second, '19700101', $field) + DATEDIFF(second, GETDATE(), GETUTCDATE())";
default:
return "UNIX_TIMESTAMP($field)";
}
}
/**
* Return SQL statement to convert from a unix timestamp
*
* @param string Field name
* @return string SQL statement to use in query
* @access public
*/
function fromunixtime($timestamp)
{
switch($this->db_provider)
{
case 'mysqli':
case 'mysql':
case 'sqlite':
return sprintf("FROM_UNIXTIME(%d)", $timestamp);
default:
return date("'Y-m-d H:i:s'", $timestamp);
}
}
/**
* Return SQL statement for case insensitive LIKE
*
* @param string Field name
* @param string Search value
* @return string SQL statement to use in query
* @access public
*/
function ilike($column, $value)
{
// TODO: use MDB2's matchPattern() function
switch($this->db_provider)
{
case 'pgsql':
return $this->quote_identifier($column).' ILIKE '.$this->quote($value);
default:
return $this->quote_identifier($column).' LIKE '.$this->quote($value);
}
}
/**
* Encodes non-UTF-8 characters in string/array/object (recursive)
*
* @param mixed Data to fix
* @return mixed Properly UTF-8 encoded data
* @access public
*/
function encode($input)
{
if (is_object($input)) {
foreach (get_object_vars($input) as $idx => $value)
$input->$idx = $this->encode($value);
return $input;
}
else if (is_array($input)) {
foreach ($input as $idx => $value)
$input[$idx] = $this->encode($value);
return $input;
}
return utf8_encode($input);
}
/**
* Decodes encoded UTF-8 string/object/array (recursive)
*
* @param mixed Input data
* @return mixed Decoded data
* @access public
*/
function decode($input)
{
if (is_object($input)) {
foreach (get_object_vars($input) as $idx => $value)
$input->$idx = $this->decode($value);
return $input;
}
else if (is_array($input)) {
foreach ($input as $idx => $value)
$input[$idx] = $this->decode($value);
return $input;
}
return utf8_decode($input);
}
/**
* Adds a query result and returns a handle ID
*
* @param object Query handle
* @return mixed Handle ID
* @access private
*/
function _add_result($res)
{
// sql error occured
if (PEAR::isError($res))
{
$this->db_error = TRUE;
$this->db_error_msg = $res->getMessage();
- raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__, 'file' => __FILE__,
+ raise_error(array('code' => 500, 'type' => 'db',
+ 'line' => __LINE__, 'file' => __FILE__,
'message' => $res->getMessage() . " Query: "
. substr(preg_replace('/[\r\n]+\s*/', ' ', $res->userinfo), 0, 512)),
TRUE, FALSE);
}
$res_id = sizeof($this->a_query_results);
$this->last_res_id = $res_id;
$this->a_query_results[$res_id] = $res;
return $res_id;
}
/**
* Resolves a given handle ID and returns the according query handle
* If no ID is specified, the last resource handle will be returned
*
* @param number Handle ID
* @return mixed Resource handle or FALSE on failure
* @access private
*/
function _get_result($res_id=NULL)
{
if ($res_id==NULL)
$res_id = $this->last_res_id;
if (isset($this->a_query_results[$res_id]))
if (!PEAR::isError($this->a_query_results[$res_id]))
return $this->a_query_results[$res_id];
return FALSE;
}
/**
* Create a sqlite database from a file
*
* @param object SQLite database handle
* @param string File path to use for DB creation
* @access private
*/
function _sqlite_create_database($dbh, $file_name)
{
if (empty($file_name) || !is_string($file_name))
return;
$data = file_get_contents($file_name);
if (strlen($data))
if (!sqlite_exec($dbh->connection, $data, $error) || MDB2::isError($dbh))
raise_error(array('code' => 500, 'type' => 'db',
'line' => __LINE__, 'file' => __FILE__, 'message' => $error), TRUE, FALSE);
}
/**
* Add some proprietary database functions to the current SQLite handle
* in order to make it MySQL compatible
*
* @access private
*/
function _sqlite_prepare()
{
include_once('include/rcube_sqlite.inc');
// we emulate via callback some missing MySQL function
sqlite_create_function($this->db_handle->connection, "from_unixtime", "rcube_sqlite_from_unixtime");
sqlite_create_function($this->db_handle->connection, "unix_timestamp", "rcube_sqlite_unix_timestamp");
sqlite_create_function($this->db_handle->connection, "now", "rcube_sqlite_now");
sqlite_create_function($this->db_handle->connection, "md5", "rcube_sqlite_md5");
}
} // end class rcube_db
/* this is our own debug handler for the MDB2 connection */
function mdb2_debug_handler(&$db, $scope, $message, $context = array())
{
if ($scope != 'prepare')
{
$debug_output = $scope . '('.$db->db_index.'): ';
$debug_output .= $message . $db->getOption('log_line_break');
write_log('sql', $debug_output);
}
}
diff --git a/program/include/rcube_plugin.php b/program/include/rcube_plugin.php
index fac7db5a2..c5eb88439 100644
--- a/program/include/rcube_plugin.php
+++ b/program/include/rcube_plugin.php
@@ -1,259 +1,265 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcube_plugin.php |
| |
| This file is part of the RoundCube Webmail client |
| Copyright (C) 2008-2009, RoundCube Dev. - Switzerland |
| Licensed under the GNU GPL |
| |
| PURPOSE: |
| Abstract plugins interface/class |
| All plugins need to extend this class |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
$Id$
*/
/**
* Plugin interface class
*
* @package Core
*/
abstract class rcube_plugin
{
public $ID;
public $api;
public $task;
protected $home;
protected $urlbase;
/**
* Default constructor.
*/
public function __construct($api)
{
$this->ID = get_class($this);
$this->api = $api;
$this->home = $api->dir . DIRECTORY_SEPARATOR . $this->ID;
$this->urlbase = $api->url . $this->ID . '/';
}
/**
* Initialization method, needs to be implemented by the plugin itself
*/
abstract function init();
/**
* Load local config file from plugins directory.
* The loaded values are patched over the global configuration.
*
* @param string Config file name relative to the plugin's folder
* @return boolean True on success, false on failure
*/
public function load_config($fname = 'config.inc.php')
{
$fpath = $this->home.'/'.$fname;
$rcmail = rcmail::get_instance();
if (is_file($fpath) && !$rcmail->config->load_from_file($fpath, false)) {
- raise_error(array('code' => 527, 'type' => 'php', 'message' => "Failed to load config from $fpath"), true, false);
+ raise_error(array('code' => 527, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Failed to load config from $fpath"), true, false);
return false;
}
return true;
}
/**
* Register a callback function for a specific (server-side) hook
*
* @param string Hook name
* @param mixed Callback function as string or array with object reference and method name
*/
public function add_hook($hook, $callback)
{
$this->api->register_hook($hook, $callback);
}
/**
* Load localized texts from the plugins dir
*
* @param string Directory to search in
* @param mixed Make texts also available on the client (array with list or true for all)
*/
public function add_texts($dir, $add2client = false)
{
$domain = $this->ID;
$lang = $_SESSION['language'];
$locdir = slashify(realpath(slashify($this->home) . $dir));
$texts = array();
foreach (array('en_US', $lang) as $lng) {
@include($locdir . $lng . '.inc');
$texts = (array)$labels + (array)$messages + (array)$texts;
}
// prepend domain to text keys and add to the application texts repository
if (!empty($texts)) {
$add = array();
foreach ($texts as $key => $value)
$add[$domain.'.'.$key] = $value;
$rcmail = rcmail::get_instance();
$rcmail->load_language($lang, $add);
// add labels to client
if ($add2client) {
$js_labels = is_array($add2client) ? array_map(array($this, 'label_map_callback'), $add2client) : array_keys($add);
$rcmail->output->add_label($js_labels);
}
}
}
/**
* Wrapper for rcmail::gettext() adding the plugin ID as domain
*
* @return string Localized text
* @see rcmail::gettext()
*/
public function gettext($p)
{
return rcmail::get_instance()->gettext($p, $this->ID);
}
/**
* Register this plugin to be responsible for a specific task
*
* @param string Task name (only characters [a-z0-9_.-] are allowed)
*/
public function register_task($task)
{
if ($task != asciiwords($task)) {
- raise_error(array('code' => 526, 'type' => 'php', 'message' => "Invalid task name: $task. Only characters [a-z0-9_.-] are allowed"), true, false);
+ raise_error(array('code' => 526, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Invalid task name: $task. Only characters [a-z0-9_.-] are allowed"), true, false);
}
else if (in_array(rcmail::$main_tasks, $task)) {
- raise_error(array('code' => 526, 'type' => 'php', 'message' => "Cannot register taks $task; already taken by another plugin or the application itself"), true, false);
+ raise_error(array('code' => 526, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Cannot register taks $task; already taken by another plugin or the application itself"), true, false);
}
else {
rcmail::$main_tasks[] = $task;
}
}
/**
* Register a handler for a specific client-request action
*
* The callback will be executed upon a request like /?_task=mail&_action=plugin.myaction
*
* @param string Action name (should be unique)
* @param mixed Callback function as string or array with object reference and method name
*/
public function register_action($action, $callback)
{
$this->api->register_action($action, $this->ID, $callback);
}
/**
* Register a handler function for a template object
*
* When parsing a template for display, tags like <roundcube:object name="plugin.myobject" />
* will be replaced by the return value if the registered callback function.
*
* @param string Object name (should be unique and start with 'plugin.')
* @param mixed Callback function as string or array with object reference and method name
*/
public function register_handler($name, $callback)
{
$this->api->register_handler($name, $this->ID, $callback);
}
/**
* Make this javascipt file available on the client
*
* @param string File path; absolute or relative to the plugin directory
*/
public function include_script($fn)
{
$this->api->include_script($this->resource_url($fn));
}
/**
* Make this stylesheet available on the client
*
* @param string File path; absolute or relative to the plugin directory
*/
public function include_stylesheet($fn)
{
$this->api->include_stylesheet($this->resource_url($fn));
}
/**
* Append a button to a certain container
*
* @param array Hash array with named parameters (as used in skin templates)
* @param string Container name where the buttons should be added to
* @see rcube_remplate::button()
*/
public function add_button($p, $container)
{
if ($this->api->output->type == 'html') {
// fix relative paths
foreach (array('imagepas', 'imageact', 'imagesel') as $key)
if ($p[$key])
$p[$key] = $this->api->url . $this->resource_url($p[$key]);
$this->api->add_content($this->api->output->button($p), $container);
}
}
/**
* Generate an absolute URL to the given resource within the current
* plugin directory
*
* @param string The file name
* @return string Absolute URL to the given resource
*/
public function url($fn)
{
return $this->api->url . $this->resource_url($fn);
}
/**
* Make the given file name link into the plugin directory
*/
private function resource_url($fn)
{
if ($fn[0] != '/' && !preg_match('|^https?://|i', $fn))
return $this->ID . '/' . $fn;
else
return $fn;
}
/**
* Provide path to the currently selected skin folder within the plugin directory
* with a fallback to the default skin folder.
*
* @return string Skin path relative to plugins directory
*/
protected function local_skin_path()
{
$skin_path = 'skins/'.$this->api->output->config['skin'];
if (!is_dir(realpath(slashify($this->home) . $skin_path)))
$skin_path = 'skins/default';
return $skin_path;
}
/**
* Callback function for array_map
*/
private function label_map_callback($key)
{
return $this->ID.'.'.$key;
}
}
diff --git a/program/include/rcube_plugin_api.php b/program/include/rcube_plugin_api.php
index 3a8ae524c..1eeadce3a 100644
--- a/program/include/rcube_plugin_api.php
+++ b/program/include/rcube_plugin_api.php
@@ -1,332 +1,346 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcube_plugin_api.php |
| |
| This file is part of the RoundCube Webmail client |
| Copyright (C) 2008-2009, RoundCube Dev. - Switzerland |
| Licensed under the GNU GPL |
| |
| PURPOSE: |
| Plugins repository |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
$Id$
*/
/**
* The plugin loader and global API
*
* @package Core
*/
class rcube_plugin_api
{
static private $instance;
public $dir;
public $url = 'plugins/';
public $output;
public $handlers = array();
private $plugins = array();
private $actions = array();
private $actionmap = array();
private $objectsmap = array();
private $template_contents = array();
private $required_plugins = array('filesystem_attachments');
private $active_hook = false;
/**
* This implements the 'singleton' design pattern
*
* @return object rcube_plugin_api The one and only instance if this class
*/
static function get_instance()
{
if (!self::$instance) {
self::$instance = new rcube_plugin_api();
}
return self::$instance;
}
/**
* Private constructor
*/
private function __construct()
{
$this->dir = INSTALL_PATH . $this->url;
}
/**
* Load and init all enabled plugins
*
* This has to be done after rcmail::load_gui() or rcmail::init_json()
* was called because plugins need to have access to rcmail->output
*/
public function init()
{
$rcmail = rcmail::get_instance();
$this->output = $rcmail->output;
$plugins_dir = dir($this->dir);
$plugins_enabled = (array)$rcmail->config->get('plugins', array());
foreach ($plugins_enabled as $plugin_name) {
$fn = $plugins_dir->path . DIRECTORY_SEPARATOR . $plugin_name . DIRECTORY_SEPARATOR . $plugin_name . '.php';
if (file_exists($fn)) {
include($fn);
// instantiate class if exists
if (class_exists($plugin_name, false)) {
$plugin = new $plugin_name($this);
// check inheritance and task specification
if (is_subclass_of($plugin, 'rcube_plugin') && (!$plugin->task || preg_match('/('.$plugin->task.')/i', $rcmail->task))) {
$plugin->init();
$this->plugins[] = $plugin;
}
}
else {
- raise_error(array('code' => 520, 'type' => 'php', 'message' => "No plugin class $plugin_name found in $fn"), true, false);
+ raise_error(array('code' => 520, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "No plugin class $plugin_name found in $fn"), true, false);
}
}
else {
- raise_error(array('code' => 520, 'type' => 'php', 'message' => "Failed to load plugin file $fn"), true, false);
+ raise_error(array('code' => 520, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Failed to load plugin file $fn"), true, false);
}
}
// check existance of all required core plugins
foreach ($this->required_plugins as $plugin_name) {
$loaded = false;
foreach ($this->plugins as $plugin) {
if ($plugin instanceof $plugin_name) {
$loaded = true;
break;
}
}
// load required core plugin if no derivate was found
if (!$loaded) {
$fn = $plugins_dir->path . DIRECTORY_SEPARATOR . $plugin_name . DIRECTORY_SEPARATOR . $plugin_name . '.php';
if (file_exists($fn)) {
include_once($fn);
if (class_exists($plugin_name, false)) {
$plugin = new $plugin_name($this);
// check inheritance
if (is_subclass_of($plugin, 'rcube_plugin')) {
if (!$plugin->task || preg_match('/('.$plugin->task.')/i', $rcmail->task)) {
$plugin->init();
$this->plugins[] = $plugin;
}
$loaded = true;
}
}
}
}
// trigger fatal error if still not loaded
if (!$loaded) {
- raise_error(array('code' => 520, 'type' => 'php', 'message' => "Requried plugin $plugin_name was not loaded"), true, true);
+ raise_error(array('code' => 520, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Requried plugin $plugin_name was not loaded"), true, true);
}
}
// register an internal hook
$this->register_hook('template_container', array($this, 'template_container_hook'));
// maybe also register a shudown function which triggers shutdown functions of all plugin objects
// call imap_init right now
// (should actually be done in rcmail::imap_init() but plugins are not initialized then)
if ($rcmail->imap) {
$hook = $this->exec_hook('imap_init', array('fetch_headers' => $rcmail->imap->fetch_add_headers));
if ($hook['fetch_headers'])
$rcmail->imap->fetch_add_headers = $hook['fetch_headers'];
}
}
/**
* Allows a plugin object to register a callback for a certain hook
*
* @param string Hook name
* @param mixed String with global function name or array($obj, 'methodname')
*/
public function register_hook($hook, $callback)
{
if (is_callable($callback))
$this->handlers[$hook][] = $callback;
else
- raise_error(array('code' => 521, 'type' => 'php', 'message' => "Invalid callback function for $hook"), true, false);
+ raise_error(array('code' => 521, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Invalid callback function for $hook"), true, false);
}
/**
* Triggers a plugin hook.
* This is called from the application and executes all registered handlers
*
* @param string Hook name
* @param array Named arguments (key->value pairs)
* @return array The (probably) altered hook arguments
*/
public function exec_hook($hook, $args = array())
{
if (!is_array($args))
$args = array('arg' => $args);
$args += array('abort' => false);
$this->active_hook = $hook;
foreach ((array)$this->handlers[$hook] as $callback) {
$ret = call_user_func($callback, $args);
if ($ret && is_array($ret))
$args = $ret + $args;
if ($args['abort'])
break;
}
$this->active_hook = false;
return $args;
}
/**
* Let a plugin register a handler for a specific request
*
* @param string Action name (_task=mail&_action=plugin.foo)
* @param string Plugin name that registers this action
* @param mixed Callback: string with global function name or array($obj, 'methodname')
*/
public function register_action($action, $owner, $callback)
{
// check action name
if (strpos($action, 'plugin.') !== 0)
$action = 'plugin.'.$action;
// can register action only if it's not taken or registered by myself
if (!isset($this->actionmap[$action]) || $this->actionmap[$action] == $owner) {
$this->actions[$action] = $callback;
$this->actionmap[$action] = $owner;
}
else {
- raise_error(array('code' => 523, 'type' => 'php', 'message' => "Cannot register action $action; already taken by another plugin"), true, false);
+ raise_error(array('code' => 523, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Cannot register action $action; already taken by another plugin"), true, false);
}
}
/**
* This method handles requests like _task=mail&_action=plugin.foo
* It executes the callback function that was registered with the given action.
*
* @param string Action name
*/
public function exec_action($action)
{
if (isset($this->actions[$action])) {
call_user_func($this->actions[$action]);
}
else {
- raise_error(array('code' => 524, 'type' => 'php', 'message' => "No handler found for action $action"), true, true);
+ raise_error(array('code' => 524, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "No handler found for action $action"), true, true);
}
}
/**
* Register a handler function for template objects
*
* @param string Object name
* @param string Plugin name that registers this action
* @param mixed Callback: string with global function name or array($obj, 'methodname')
*/
public function register_handler($name, $owner, $callback)
{
// check name
if (strpos($name, 'plugin.') !== 0)
$name = 'plugin.'.$name;
// can register handler only if it's not taken or registered by myself
if (!isset($this->objectsmap[$name]) || $this->objectsmap[$name] == $owner) {
$this->output->add_handler($name, $callback);
$this->objectsmap[$name] = $owner;
}
else {
- raise_error(array('code' => 525, 'type' => 'php', 'message' => "Cannot register template handler $name; already taken by another plugin"), true, false);
+ raise_error(array('code' => 525, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Cannot register template handler $name; already taken by another plugin"), true, false);
}
}
/**
* Check if a plugin hook is currently processing.
* Mainly used to prevent loops and recursion.
*
* @param string Hook to check (optional)
* @return boolean True if any/the given hook is currently processed, otherwise false
*/
public function is_processing($hook = null)
{
return $this->active_hook && (!$hook || $this->active_hook == $hook);
}
/**
* Include a plugin script file in the current HTML page
*/
public function include_script($fn)
{
if ($this->output->type == 'html') {
$src = $this->resource_url($fn);
$this->output->add_header(html::tag('script', array('type' => "text/javascript", 'src' => $src)));
}
}
/**
* Include a plugin stylesheet in the current HTML page
*/
public function include_stylesheet($fn)
{
if ($this->output->type == 'html') {
$src = $this->resource_url($fn);
$this->output->add_header(html::tag('link', array('rel' => "stylesheet", 'type' => "text/css", 'href' => $src)));
}
}
/**
* Save the given HTML content to be added to a template container
*/
public function add_content($html, $container)
{
$this->template_contents[$container] .= $html . "\n";
}
/**
* Callback for template_container hooks
*/
private function template_container_hook($attrib)
{
$container = $attrib['name'];
return array('content' => $attrib['content'] . $this->template_contents[$container]);
}
/**
* Make the given file name link into the plugins directory
*/
private function resource_url($fn)
{
if ($fn[0] != '/' && !preg_match('|^https?://|i', $fn))
return $this->url . $fn;
else
return $fn;
}
}
diff --git a/program/include/rcube_template.php b/program/include/rcube_template.php
index c583f740e..fef0eac2a 100755
--- a/program/include/rcube_template.php
+++ b/program/include/rcube_template.php
@@ -1,1172 +1,1174 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcube_template.php |
| |
| This file is part of the RoundCube Webmail client |
| Copyright (C) 2006-2009, RoundCube Dev. - Switzerland |
| Licensed under the GNU GPL |
| |
| PURPOSE: |
| Class to handle HTML page output using a skin template. |
| Extends rcube_html_page class from rcube_shared.inc |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
$Id$
*/
/**
* Class to create HTML page output using a skin template
*
* @package View
* @todo Documentation
* @uses rcube_html_page
*/
class rcube_template extends rcube_html_page
{
var $app;
var $config;
var $framed = false;
var $pagetitle = '';
var $message = null;
var $env = array();
var $js_env = array();
var $js_commands = array();
var $object_handlers = array();
public $type = 'html';
public $ajax_call = false;
/**
* Constructor
*
* @todo Use jQuery's $(document).ready() here.
* @todo Replace $this->config with the real rcube_config object
*/
public function __construct($task, $framed = false)
{
parent::__construct();
$this->app = rcmail::get_instance();
$this->config = $this->app->config->all();
$this->browser = new rcube_browser();
//$this->framed = $framed;
$this->set_env('task', $task);
$this->set_env('request_token', $this->app->get_request_token());
// load the correct skin (in case user-defined)
$this->set_skin($this->config['skin']);
// add common javascripts
$javascript = 'var '.JS_OBJECT_NAME.' = new rcube_webmail();';
// don't wait for page onload. Call init at the bottom of the page (delayed)
$javascript_foot = '$(document).ready(function(){ '.JS_OBJECT_NAME.'.init(); });';
$this->add_script($javascript, 'head_top');
$this->add_script($javascript_foot, 'foot');
$this->scripts_path = 'program/js/';
$this->include_script('jquery-1.4.min.js');
$this->include_script('common.js');
$this->include_script('app.js');
// register common UI objects
$this->add_handlers(array(
'loginform' => array($this, 'login_form'),
'username' => array($this, 'current_username'),
'message' => array($this, 'message_container'),
'charsetselector' => array($this, 'charset_selector'),
));
}
/**
* Set environment variable
*
* @param string Property name
* @param mixed Property value
* @param boolean True if this property should be added to client environment
*/
public function set_env($name, $value, $addtojs = true)
{
$this->env[$name] = $value;
if ($addtojs || isset($this->js_env[$name])) {
$this->js_env[$name] = $value;
}
}
/**
* Set page title variable
*/
public function set_pagetitle($title)
{
$this->pagetitle = $title;
}
/**
* Getter for the current page title
*
* @return string The page title
*/
public function get_pagetitle()
{
if (!empty($this->pagetitle)) {
$title = $this->pagetitle;
}
else if ($this->env['task'] == 'login') {
$title = rcube_label(array('name' => 'welcome', 'vars' => array('product' => $this->config['product_name'])));
}
else {
$title = ucfirst($this->env['task']);
}
return $title;
}
/**
* Set skin
*/
public function set_skin($skin)
{
if (!empty($skin) && is_dir('skins/'.$skin) && is_readable('skins/'.$skin))
$skin_path = 'skins/'.$skin;
else
$skin_path = $this->config['skin_path'] ? $this->config['skin_path'] : 'skins/default';
$this->app->config->set('skin_path', $skin_path);
$this->config['skin_path'] = $skin_path;
}
/**
* Check if a specific template exists
*
* @param string Template name
* @return boolean True if template exists
*/
public function template_exists($name)
{
$filename = $this->config['skin_path'] . '/templates/' . $name . '.html';
return (is_file($filename) && is_readable($filename));
}
/**
* Register a template object handler
*
* @param string Object name
* @param string Function name to call
* @return void
*/
public function add_handler($obj, $func)
{
$this->object_handlers[$obj] = $func;
}
/**
* Register a list of template object handlers
*
* @param array Hash array with object=>handler pairs
* @return void
*/
public function add_handlers($arr)
{
$this->object_handlers = array_merge($this->object_handlers, $arr);
}
/**
* Register a GUI object to the client script
*
* @param string Object name
* @param string Object ID
* @return void
*/
public function add_gui_object($obj, $id)
{
$this->add_script(JS_OBJECT_NAME.".gui_object('$obj', '$id');");
}
/**
* Call a client method
*
* @param string Method to call
* @param ... Additional arguments
*/
public function command()
{
$cmd = func_get_args();
if (strpos($cmd[0], 'plugin.') === false)
$this->js_commands[] = $cmd;
}
/**
* Add a localized label to the client environment
*/
public function add_label()
{
$args = func_get_args();
if (count($args) == 1 && is_array($args[0]))
$args = $args[0];
foreach ($args as $name) {
$this->command('add_label', $name, rcube_label($name));
}
}
/**
* Invoke display_message command
*
* @param string Message to display
* @param string Message type [notice|confirm|error]
* @param array Key-value pairs to be replaced in localized text
* @param boolean Override last set message
* @uses self::command()
*/
public function show_message($message, $type='notice', $vars=null, $override=true)
{
if ($override || !$this->message) {
$this->message = $message;
$this->command(
'display_message',
rcube_label(array('name' => $message, 'vars' => $vars)),
$type);
}
}
/**
* Delete all stored env variables and commands
*
* @return void
* @uses rcube_html::reset()
* @uses self::$env
* @uses self::$js_env
* @uses self::$js_commands
* @uses self::$object_handlers
*/
public function reset()
{
$this->env = array();
$this->js_env = array();
$this->js_commands = array();
$this->object_handlers = array();
parent::reset();
}
/**
* Redirect to a certain url
*
* @param mixed Either a string with the action or url parameters as key-value pairs
* @see rcmail::url()
*/
public function redirect($p = array())
{
$location = $this->app->url($p);
header('Location: ' . $location);
exit;
}
/**
* Send the request output to the client.
* This will either parse a skin tempalte or send an AJAX response
*
* @param string Template name
* @param boolean True if script should terminate (default)
*/
public function send($templ = null, $exit = true)
{
if ($templ != 'iframe') {
// prevent from endless loops
if ($exit != 'recur' && $this->app->plugins->is_processing('render_page')) {
- raise_error(array('code' => 505, 'type' => 'php', 'message' => 'Recursion alert: ignoring output->send()'), true, false);
+ raise_error(array('code' => 505, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => 'Recursion alert: ignoring output->send()'), true, false);
return;
}
$this->parse($templ, false);
}
else {
$this->framed = $templ == 'iframe' ? true : $this->framed;
$this->write();
}
// set output asap
ob_flush();
flush();
if ($exit) {
exit;
}
}
/**
* Process template and write to stdOut
*
* @param string HTML template
* @see rcube_html_page::write()
* @override
*/
public function write($template = '')
{
// unlock interface after iframe load
if ($this->framed) {
array_unshift($this->js_commands, array('set_busy', false));
}
// write all env variables to client
$js = $this->framed ? "if(window.parent) {\n" : '';
$js .= $this->get_js_commands() . ($this->framed ? ' }' : '');
$this->add_script($js, 'head_top');
// make sure all <form> tags have a valid request token
$template = preg_replace_callback('/<form\s+([^>]+)>/Ui', array($this, 'alter_form_tag'), $template);
$this->footer = preg_replace_callback('/<form\s+([^>]+)>/Ui', array($this, 'alter_form_tag'), $this->footer);
// call super method
parent::write($template, $this->config['skin_path']);
}
/**
* Parse a specific skin template and deliver to stdout
*
* Either returns nothing, or exists hard (exit();)
*
* @param string Template name
* @param boolean Exit script
* @return void
* @link http://php.net/manual/en/function.exit.php
*/
private function parse($name = 'main', $exit = true)
{
$skin_path = $this->config['skin_path'];
$plugin = false;
$temp = explode(".", $name, 2);
if (count($temp) > 1) {
$plugin = $temp[0];
$name = $temp[1];
$skin_dir = $plugin . '/skins/' . $this->config['skin'];
$skin_path = $this->app->plugins->dir . $skin_dir;
if (!is_dir($skin_path)) { // fallback to default skin
$skin_dir = $plugin . '/skins/default';
$skin_path = $this->app->plugins->dir . $skin_dir;
}
}
$path = "$skin_path/templates/$name.html";
// read template file
if (($templ = @file_get_contents($path)) === false) {
raise_error(array(
'code' => 501,
'type' => 'php',
'line' => __LINE__,
'file' => __FILE__,
'message' => 'Error loading template for '.$name
), true, true);
return false;
}
// replace all path references to plugins/... with the configured plugins dir
// and /this/ to the current plugin skin directory
if ($plugin) {
$templ = preg_replace(array('/\bplugins\//', '/(["\']?)\/this\//'), array($this->app->plugins->url, '\\1'.$this->app->plugins->url.$skin_dir.'/'), $templ);
}
// parse for specialtags
$output = $this->parse_conditions($templ);
$output = $this->parse_xml($output);
// trigger generic hook where plugins can put additional content to the page
$hook = $this->app->plugins->exec_hook("render_page", array('template' => $name, 'content' => $output));
// add debug console
if ($this->config['debug_level'] & 8) {
$this->add_footer('<div id="console" style="position:absolute;top:5px;left:5px;width:405px;padding:2px;background:white;z-index:9000;">
<a href="#toggle" onclick="con=document.getElementById(\'dbgconsole\');con.style.display=(con.style.display==\'none\'?\'block\':\'none\');return false">console</a>
<form action="/" name="debugform" style="display:inline"><textarea name="console" id="dbgconsole" rows="20" cols="40" wrap="off" style="display:none;width:400px;border:none;font-size:x-small" spellcheck="false"></textarea></form></div>'
);
}
$output = $this->parse_with_globals($hook['content']);
$this->write(trim($output));
if ($exit) {
exit;
}
}
/**
* Return executable javascript code for all registered commands
*
* @return string $out
*/
private function get_js_commands()
{
$out = '';
if (!$this->framed && !empty($this->js_env)) {
$out .= JS_OBJECT_NAME . '.set_env('.json_serialize($this->js_env).");\n";
}
foreach ($this->js_commands as $i => $args) {
$method = array_shift($args);
foreach ($args as $i => $arg) {
$args[$i] = json_serialize($arg);
}
$parent = $this->framed || preg_match('/^parent\./', $method);
$out .= sprintf(
"%s.%s(%s);\n",
($parent ? 'if(window.parent && parent.'.JS_OBJECT_NAME.') parent.' : '') . JS_OBJECT_NAME,
preg_replace('/^parent\./', '', $method),
implode(',', $args)
);
}
return $out;
}
/**
* Make URLs starting with a slash point to skin directory
*
* @param string Input string
* @return string
*/
public function abs_url($str)
{
return preg_replace('/^\//', $this->config['skin_path'].'/', $str);
}
/***** Template parsing methods *****/
/**
* Replace all strings ($varname)
* with the content of the according global variable.
*/
private function parse_with_globals($input)
{
$GLOBALS['__version'] = Q(RCMAIL_VERSION);
$GLOBALS['__comm_path'] = Q($this->app->comm_path);
return preg_replace_callback('/\$(__[a-z0-9_\-]+)/',
array($this, 'globals_callback'), $input);
}
/**
* Callback funtion for preg_replace_callback() in parse_with_globals()
*/
private function globals_callback($matches)
{
return $GLOBALS[$matches[1]];
}
/**
* Public wrapper to dipp into template parsing.
*
* @param string $input
* @return string
* @uses rcube_template::parse_xml()
* @since 0.1-rc1
*/
public function just_parse($input)
{
return $this->parse_xml($input);
}
/**
* Parse for conditional tags
*
* @param string $input
* @return string
*/
private function parse_conditions($input)
{
$matches = preg_split('/<roundcube:(if|elseif|else|endif)\s+([^>]+)>/is', $input, 2, PREG_SPLIT_DELIM_CAPTURE);
if ($matches && count($matches) == 4) {
if (preg_match('/^(else|endif)$/i', $matches[1])) {
return $matches[0] . $this->parse_conditions($matches[3]);
}
$attrib = parse_attrib_string($matches[2]);
if (isset($attrib['condition'])) {
$condmet = $this->check_condition($attrib['condition']);
$submatches = preg_split('/<roundcube:(elseif|else|endif)\s+([^>]+)>/is', $matches[3], 2, PREG_SPLIT_DELIM_CAPTURE);
if ($condmet) {
$result = $submatches[0];
$result.= ($submatches[1] != 'endif' ? preg_replace('/.*<roundcube:endif\s+[^>]+>/Uis', '', $submatches[3], 1) : $submatches[3]);
}
else {
$result = "<roundcube:$submatches[1] $submatches[2]>" . $submatches[3];
}
return $matches[0] . $this->parse_conditions($result);
}
raise_error(array(
'code' => 500,
'type' => 'php',
'line' => __LINE__,
'file' => __FILE__,
'message' => "Unable to parse conditional tag " . $matches[2]
), true, false);
}
return $input;
}
/**
* Determines if a given condition is met
*
* @todo Get rid off eval() once I understand what this does.
* @todo Extend this to allow real conditions, not just "set"
* @param string Condition statement
* @return boolean True if condition is met, False if not
*/
private function check_condition($condition)
{
return eval("return (".$this->parse_expression($condition).");");
}
/**
*
*/
private function alter_form_tag($matches)
{
$out = $matches[0];
$attrib = parse_attrib_string($matches[1]);
if (strtolower($attrib['method']) == 'post') {
$hidden = new html_hiddenfield(array('name' => '_token', 'value' => $this->app->get_request_token()));
$out .= "\n" . $hidden->show();
}
return $out;
}
/**
* Parses expression and replaces variables
*
* @param string Expression statement
* @return string Expression statement
*/
private function parse_expression($expression)
{
return preg_replace(
array(
'/session:([a-z0-9_]+)/i',
'/config:([a-z0-9_]+)(:([a-z0-9_]+))?/i',
'/env:([a-z0-9_]+)/i',
'/request:([a-z0-9_]+)/i',
'/cookie:([a-z0-9_]+)/i',
'/browser:([a-z0-9_]+)/i'
),
array(
"\$_SESSION['\\1']",
"\$this->app->config->get('\\1',get_boolean('\\3'))",
"\$this->env['\\1']",
"get_input_value('\\1', RCUBE_INPUT_GPC)",
"\$_COOKIE['\\1']",
"\$this->browser->{'\\1'}"
),
$expression);
}
/**
* Search for special tags in input and replace them
* with the appropriate content
*
* @param string Input string to parse
* @return string Altered input string
* @todo Use DOM-parser to traverse template HTML
* @todo Maybe a cache.
*/
private function parse_xml($input)
{
return preg_replace_callback('/<roundcube:([-_a-z]+)\s+([^>]+)>/Ui', array($this, 'xml_command'), $input);
}
/**
* Callback function for parsing an xml command tag
* and turn it into real html content
*
* @param array Matches array of preg_replace_callback
* @return string Tag/Object content
*/
private function xml_command($matches)
{
$command = strtolower($matches[1]);
$attrib = parse_attrib_string($matches[2]);
// empty output if required condition is not met
if (!empty($attrib['condition']) && !$this->check_condition($attrib['condition'])) {
return '';
}
// execute command
switch ($command) {
// return a button
case 'button':
if ($attrib['name'] || $attrib['command']) {
return $this->button($attrib);
}
break;
// show a label
case 'label':
if ($attrib['name'] || $attrib['command']) {
return Q(rcube_label($attrib + array('vars' => array('product' => $this->config['product_name']))));
}
break;
// include a file
case 'include':
$path = realpath($this->config['skin_path'].$attrib['file']);
if (is_readable($path)) {
if ($this->config['skin_include_php']) {
$incl = $this->include_php($path);
}
else {
$incl = file_get_contents($path);
}
$incl = $this->parse_conditions($incl);
return $this->parse_xml($incl);
}
break;
case 'plugin.include':
$hook = $this->app->plugins->exec_hook("template_plugin_include", $attrib);
return $hook['content'];
break;
// define a container block
case 'container':
if ($attrib['name'] && $attrib['id']) {
$this->command('gui_container', $attrib['name'], $attrib['id']);
// let plugins insert some content here
$hook = $this->app->plugins->exec_hook("template_container", $attrib);
return $hook['content'];
}
break;
// return code for a specific application object
case 'object':
$object = strtolower($attrib['name']);
$content = '';
// we are calling a class/method
if (($handler = $this->object_handlers[$object]) && is_array($handler)) {
if ((is_object($handler[0]) && method_exists($handler[0], $handler[1])) ||
(is_string($handler[0]) && class_exists($handler[0])))
$content = call_user_func($handler, $attrib);
}
// execute object handler function
else if (function_exists($handler)) {
$content = call_user_func($handler, $attrib);
}
else if ($object == 'productname') {
$name = !empty($this->config['product_name']) ? $this->config['product_name'] : 'RoundCube Webmail';
$content = Q($name);
}
else if ($object == 'version') {
$ver = (string)RCMAIL_VERSION;
if (is_file(INSTALL_PATH . '.svn/entries')) {
if (preg_match('/Revision:\s(\d+)/', @shell_exec('svn info'), $regs))
$ver .= ' [SVN r'.$regs[1].']';
}
$content = Q($ver);
}
else if ($object == 'steptitle') {
$content = Q($this->get_pagetitle());
}
else if ($object == 'pagetitle') {
$title = !empty($this->config['product_name']) ? $this->config['product_name'].' :: ' : '';
$title .= $this->get_pagetitle();
$content = Q($title);
}
// exec plugin hooks for this template object
$hook = $this->app->plugins->exec_hook("template_object_$object", $attrib + array('content' => $content));
return $hook['content'];
// return code for a specified eval expression
case 'exp':
$value = $this->parse_expression($attrib['expression']);
return eval("return Q($value);");
// return variable
case 'var':
$var = explode(':', $attrib['name']);
$name = $var[1];
$value = '';
switch ($var[0]) {
case 'env':
$value = $this->env[$name];
break;
case 'config':
$value = $this->config[$name];
if (is_array($value) && $value[$_SESSION['imap_host']]) {
$value = $value[$_SESSION['imap_host']];
}
break;
case 'request':
$value = get_input_value($name, RCUBE_INPUT_GPC);
break;
case 'session':
$value = $_SESSION[$name];
break;
case 'cookie':
$value = htmlspecialchars($_COOKIE[$name]);
break;
case 'browser':
$value = $this->browser->{$name};
break;
}
if (is_array($value)) {
$value = implode(', ', $value);
}
return Q($value);
break;
}
return '';
}
/**
* Include a specific file and return it's contents
*
* @param string File path
* @return string Contents of the processed file
*/
private function include_php($file)
{
ob_start();
include $file;
$out = ob_get_contents();
ob_end_clean();
return $out;
}
/**
* Create and register a button
*
* @param array Named button attributes
* @return string HTML button
* @todo Remove all inline JS calls and use jQuery instead.
* @todo Remove all sprintf()'s - they are pretty, but also slow.
*/
public function button($attrib)
{
static $sa_buttons = array();
static $s_button_count = 100;
// these commands can be called directly via url
$a_static_commands = array('compose', 'list', 'preferences', 'folders', 'identities');
if (!($attrib['command'] || $attrib['name'])) {
return '';
}
// try to find out the button type
if ($attrib['type']) {
$attrib['type'] = strtolower($attrib['type']);
}
else {
$attrib['type'] = ($attrib['image'] || $attrib['imagepas'] || $attrib['imageact']) ? 'image' : 'link';
}
$command = $attrib['command'];
// take the button from the stack
if ($attrib['name'] && $sa_buttons[$attrib['name']]) {
$attrib = $sa_buttons[$attrib['name']];
}
else if($attrib['image'] || $attrib['imageact'] || $attrib['imagepas'] || $attrib['class']) {
// add button to button stack
if (!$attrib['name']) {
$attrib['name'] = $command;
}
if (!$attrib['image']) {
$attrib['image'] = $attrib['imagepas'] ? $attrib['imagepas'] : $attrib['imageact'];
}
$sa_buttons[$attrib['name']] = $attrib;
}
else if ($command && $sa_buttons[$command]) {
// get saved button for this command/name
$attrib = $sa_buttons[$command];
}
if (!$attrib['id']) {
$attrib['id'] = sprintf('rcmbtn%d', $s_button_count++);
}
// get localized text for labels and titles
if ($attrib['title']) {
$attrib['title'] = Q(rcube_label($attrib['title'], $attrib['domain']));
}
if ($attrib['label']) {
$attrib['label'] = Q(rcube_label($attrib['label'], $attrib['domain']));
}
if ($attrib['alt']) {
$attrib['alt'] = Q(rcube_label($attrib['alt'], $attrib['domain']));
}
// set title to alt attribute for IE browsers
if ($this->browser->ie && !$attrib['title'] && $attrib['alt']) {
$attrib['title'] = $attrib['alt'];
}
// add empty alt attribute for XHTML compatibility
if (!isset($attrib['alt'])) {
$attrib['alt'] = '';
}
// register button in the system
if ($attrib['command']) {
$this->add_script(sprintf(
"%s.register_button('%s', '%s', '%s', '%s', '%s', '%s');",
JS_OBJECT_NAME,
$command,
$attrib['id'],
$attrib['type'],
$attrib['imageact'] ? $this->abs_url($attrib['imageact']) : $attrib['classact'],
$attrib['imagesel'] ? $this->abs_url($attrib['imagesel']) : $attrib['classsel'],
$attrib['imageover'] ? $this->abs_url($attrib['imageover']) : ''
));
// make valid href to specific buttons
if (in_array($attrib['command'], rcmail::$main_tasks)) {
$attrib['href'] = rcmail_url(null, null, $attrib['command']);
}
else if (in_array($attrib['command'], $a_static_commands)) {
$attrib['href'] = rcmail_url($attrib['command']);
}
else if ($attrib['command'] == 'permaurl' && !empty($this->env['permaurl'])) {
$attrib['href'] = $this->env['permaurl'];
}
}
// overwrite attributes
if (!$attrib['href']) {
$attrib['href'] = '#';
}
if ($command) {
$attrib['onclick'] = sprintf(
"return %s.command('%s','%s',this)",
JS_OBJECT_NAME,
$command,
$attrib['prop']
);
}
$out = '';
// generate image tag
if ($attrib['type']=='image') {
$attrib_str = html::attrib_string(
$attrib,
array(
'style', 'class', 'id', 'width', 'height', 'border', 'hspace',
'vspace', 'align', 'alt', 'tabindex', 'title'
)
);
$btn_content = sprintf('<img src="%s"%s />', $this->abs_url($attrib['image']), $attrib_str);
if ($attrib['label']) {
$btn_content .= ' '.$attrib['label'];
}
$link_attrib = array('href', 'onclick', 'onmouseover', 'onmouseout', 'onmousedown', 'onmouseup', 'target');
}
else if ($attrib['type']=='link') {
$btn_content = isset($attrib['content']) ? $attrib['content'] : ($attrib['label'] ? $attrib['label'] : $attrib['command']);
$link_attrib = array('href', 'onclick', 'title', 'id', 'class', 'style', 'tabindex', 'target');
}
else if ($attrib['type']=='input') {
$attrib['type'] = 'button';
if ($attrib['label']) {
$attrib['value'] = $attrib['label'];
}
$attrib_str = html::attrib_string(
$attrib,
array(
'type', 'value', 'onclick', 'id', 'class', 'style', 'tabindex'
)
);
$out = sprintf('<input%s disabled="disabled" />', $attrib_str);
}
// generate html code for button
if ($btn_content) {
$attrib_str = html::attrib_string($attrib, $link_attrib);
$out = sprintf('<a%s>%s</a>', $attrib_str, $btn_content);
}
return $out;
}
/* ************* common functions delivering gui objects ************** */
/**
* Create a form tag with the necessary hidden fields
*
* @param array Named tag parameters
* @return string HTML code for the form
*/
public function form_tag($attrib, $content = null)
{
if ($this->framed || !empty($_REQUEST['_framed'])) {
$hiddenfield = new html_hiddenfield(array('name' => '_framed', 'value' => '1'));
$hidden = $hiddenfield->show();
}
if (!$content)
$attrib['noclose'] = true;
return html::tag('form',
$attrib + array('action' => "./", 'method' => "get"),
$hidden . $content,
array('id','class','style','name','method','action','enctype','onsubmit'));
}
/**
* Build a form tag with a unique request token
*
* @param array Named tag parameters including 'action' and 'task' values which will be put into hidden fields
* @param string Form content
* @return string HTML code for the form
*/
public function request_form($attrib, $content = '')
{
$hidden = new html_hiddenfield();
if ($attrib['task']) {
$hidden->add(array('name' => '_task', 'value' => $attrib['task']));
}
if ($attrib['action']) {
$hidden->add(array('name' => '_action', 'value' => $attrib['action']));
}
unset($attrib['task'], $attrib['request']);
$attrib['action'] = './';
// we already have a <form> tag
if ($attrib['form'])
return $hidden->show() . $content;
else
return $this->form_tag($attrib, $hidden->show() . $content);
}
/**
* GUI object 'username'
* Showing IMAP username of the current session
*
* @param array Named tag parameters (currently not used)
* @return string HTML code for the gui object
*/
public function current_username($attrib)
{
static $username;
// alread fetched
if (!empty($username)) {
return $username;
}
// get e-mail address form default identity
if ($sql_arr = $this->app->user->get_identity()) {
$username = $sql_arr['email'];
}
else {
$username = $this->app->user->get_username();
}
return $username;
}
/**
* GUI object 'loginform'
* Returns code for the webmail login form
*
* @param array Named parameters
* @return string HTML code for the gui object
*/
private function login_form($attrib)
{
$default_host = $this->config['default_host'];
$_SESSION['temp'] = true;
// save original url
$url = get_input_value('_url', RCUBE_INPUT_POST);
if (empty($url) && !preg_match('/_(task|action)=logout/', $_SERVER['QUERY_STRING']))
$url = $_SERVER['QUERY_STRING'];
$input_user = new html_inputfield(array('name' => '_user', 'id' => 'rcmloginuser') + $attrib);
$input_pass = new html_passwordfield(array('name' => '_pass', 'id' => 'rcmloginpwd') + $attrib);
$input_action = new html_hiddenfield(array('name' => '_action', 'value' => 'login'));
$input_tzone = new html_hiddenfield(array('name' => '_timezone', 'id' => 'rcmlogintz', 'value' => '_default_'));
$input_url = new html_hiddenfield(array('name' => '_url', 'id' => 'rcmloginurl', 'value' => $url));
$input_host = null;
if (is_array($default_host)) {
$input_host = new html_select(array('name' => '_host', 'id' => 'rcmloginhost'));
foreach ($default_host as $key => $value) {
if (!is_array($value)) {
$input_host->add($value, (is_numeric($key) ? $value : $key));
}
else {
$input_host = null;
break;
}
}
}
else if (empty($default_host)) {
$input_host = new html_inputfield(array('name' => '_host', 'id' => 'rcmloginhost') + $attrib);
}
$form_name = !empty($attrib['form']) ? $attrib['form'] : 'form';
$this->add_gui_object('loginform', $form_name);
// create HTML table with two cols
$table = new html_table(array('cols' => 2));
$table->add('title', html::label('rcmloginuser', Q(rcube_label('username'))));
$table->add(null, $input_user->show(get_input_value('_user', RCUBE_INPUT_POST)));
$table->add('title', html::label('rcmloginpwd', Q(rcube_label('password'))));
$table->add(null, $input_pass->show());
// add host selection row
if (is_object($input_host)) {
$table->add('title', html::label('rcmloginhost', Q(rcube_label('server'))));
$table->add(null, $input_host->show(get_input_value('_host', RCUBE_INPUT_POST)));
}
$out = $input_action->show();
$out .= $input_tzone->show();
$out .= $input_url->show();
$out .= $table->show();
// surround html output with a form tag
if (empty($attrib['form'])) {
$out = $this->form_tag(array('name' => $form_name, 'method' => "post"), $out);
}
return $out;
}
/**
* GUI object 'searchform'
* Returns code for search function
*
* @param array Named parameters
* @return string HTML code for the gui object
*/
private function search_form($attrib)
{
// add some labels to client
$this->add_label('searching');
$attrib['name'] = '_q';
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmqsearchbox';
}
if ($attrib['type'] == 'search' && !$this->browser->khtml) {
unset($attrib['type'], $attrib['results']);
}
$input_q = new html_inputfield($attrib);
$out = $input_q->show();
$this->add_gui_object('qsearchbox', $attrib['id']);
// add form tag around text field
if (empty($attrib['form'])) {
$out = $this->form_tag(array(
'name' => "rcmqsearchform",
'onsubmit' => JS_OBJECT_NAME . ".command('search');return false;",
'style' => "display:inline"),
$out);
}
return $out;
}
/**
* Builder for GUI object 'message'
*
* @param array Named tag parameters
* @return string HTML code for the gui object
*/
private function message_container($attrib)
{
if (isset($attrib['id']) === false) {
$attrib['id'] = 'rcmMessageContainer';
}
$this->add_gui_object('message', $attrib['id']);
return html::div($attrib, "");
}
/**
* GUI object 'charsetselector'
*
* @param array Named parameters for the select tag
* @return string HTML code for the gui object
*/
static function charset_selector($attrib)
{
// pass the following attributes to the form class
$field_attrib = array('name' => '_charset');
foreach ($attrib as $attr => $value) {
if (in_array($attr, array('id', 'class', 'style', 'size', 'tabindex'))) {
$field_attrib[$attr] = $value;
}
}
$charsets = array(
'US-ASCII' => 'ASCII (English)',
'EUC-JP' => 'EUC-JP (Japanese)',
'EUC-KR' => 'EUC-KR (Korean)',
'BIG5' => 'BIG5 (Chinese)',
'GB2312' => 'GB2312 (Chinese)',
'ISO-2022-JP' => 'ISO-2022-JP (Japanese)',
'ISO-8859-1' => 'ISO-8859-1 (Latin-1)',
'ISO-8859-2' => 'ISO-8895-2 (Central European)',
'ISO-8859-7' => 'ISO-8859-7 (Greek)',
'ISO-8859-9' => 'ISO-8859-9 (Turkish)',
'Windows-1251' => 'Windows-1251 (Cyrillic)',
'Windows-1252' => 'Windows-1252 (Western)',
'Windows-1255' => 'Windows-1255 (Hebrew)',
'Windows-1256' => 'Windows-1256 (Arabic)',
'Windows-1257' => 'Windows-1257 (Baltic)',
'UTF-8' => 'UTF-8'
);
$select = new html_select($field_attrib);
$select->add(array_values($charsets), array_keys($charsets));
$set = $_POST['_charset'] ? $_POST['_charset'] : $this->get_charset();
return $select->show($set);
}
} // end class rcube_template
diff --git a/program/steps/mail/sendmail.inc b/program/steps/mail/sendmail.inc
index 230572f66..589c9af7f 100644
--- a/program/steps/mail/sendmail.inc
+++ b/program/steps/mail/sendmail.inc
@@ -1,594 +1,596 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/steps/mail/sendmail.inc |
| |
| This file is part of the RoundCube Webmail client |
| Copyright (C) 2005-2009, RoundCube Dev. - Switzerland |
| Licensed under the GNU GPL |
| |
| PURPOSE: |
| Compose a new mail message with all headers and attachments |
| and send it using the PEAR::Net_SMTP class or with PHP mail() |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
$Id$
*/
// remove all scripts and act as called in frame
$OUTPUT->reset();
$OUTPUT->framed = TRUE;
$savedraft = !empty($_POST['_draft']) ? TRUE : FALSE;
/****** checks ********/
if (!isset($_SESSION['compose']['id'])) {
- raise_error(array('code' => 500, 'type' => 'smtp', 'file' => __FILE__, 'message' => "Invalid compose ID"), true, false);
- console("Sendmail error", $_SESSION['compose']);
+ raise_error(array('code' => 500, 'type' => 'smtp',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Invalid compose ID"), true, false);
+
$OUTPUT->show_message("An internal error occured. Please try again.", 'error');
$OUTPUT->send('iframe');
}
if (!$savedraft) {
if (empty($_POST['_to']) && empty($_POST['_cc']) && empty($_POST['_bcc'])
&& empty($_POST['_subject']) && $_POST['_message']) {
$OUTPUT->show_message('sendingfailed', 'error');
$OUTPUT->send('iframe');
}
if(!empty($CONFIG['sendmail_delay'])) {
$wait_sec = time() - intval($CONFIG['sendmail_delay']) - intval($CONFIG['last_message_time']);
if($wait_sec < 0) {
$OUTPUT->show_message('senttooquickly', 'error', array('sec' => $wait_sec * -1));
$OUTPUT->send('iframe');
}
}
}
/****** message sending functions ********/
// encrypt parts of the header
function rcmail_encrypt_header($what)
{
global $CONFIG, $RCMAIL;
if (!$CONFIG['http_received_header_encrypt'])
{
return $what;
}
return $RCMAIL->encrypt($what);
}
// get identity record
function rcmail_get_identity($id)
{
global $USER, $OUTPUT;
if ($sql_arr = $USER->get_identity($id))
{
$out = $sql_arr;
$out['mailto'] = $sql_arr['email'];
// Special chars as defined by RFC 822 need to in quoted string (or escaped).
if (preg_match('/[\(\)\<\>\\\.\[\]@,;:"]/', $sql_arr['name']))
$name = '"' . addcslashes($sql_arr['name'], '"') . '"';
else
$name = $sql_arr['name'];
$out['string'] = rcube_charset_convert($name, RCMAIL_CHARSET, $OUTPUT->get_charset());
if ($sql_arr['email'])
$out['string'] .= ' <' . $sql_arr['email'] . '>';
return $out;
}
return FALSE;
}
/**
* go from this:
* <img src=".../tiny_mce/plugins/emotions/images/smiley-cool.gif" border="0" alt="Cool" title="Cool" />
*
* to this:
*
* <IMG src="cid:smiley-cool.gif"/>
* ...
* ------part...
* Content-Type: image/gif
* Content-Transfer-Encoding: base64
* Content-ID: <smiley-cool.gif>
*/
function rcmail_attach_emoticons(&$mime_message)
{
global $CONFIG;
$body = $mime_message->getHtmlBody();
// remove any null-byte characters before parsing
$body = preg_replace('/\x00/', '', $body);
$searchstr = 'program/js/tiny_mce/plugins/emotions/img/';
$offset = 0;
// keep track of added images, so they're only added once
$included_images = array();
if (preg_match_all('# src=[\'"]([^\'"]+)#', $body, $matches, PREG_OFFSET_CAPTURE)) {
foreach ($matches[1] as $m) {
// find emoticon image tags
if (preg_match('#'.$searchstr.'(.*)$#', $m[0], $imatches)) {
$image_name = $imatches[1];
// sanitize image name so resulting attachment doesn't leave images dir
$image_name = preg_replace('/[^a-zA-Z0-9_\.\-]/i', '', $image_name);
$img_file = INSTALL_PATH . '/' . $searchstr . $image_name;
if (! in_array($image_name, $included_images)) {
// add the image to the MIME message
if(! $mime_message->addHTMLImage($img_file, 'image/gif', '', true, $image_name))
$OUTPUT->show_message("emoticonerror", 'error');
array_push($included_images, $image_name);
}
$body = substr_replace($body, $img_file, $m[1] + $offset, strlen($m[0]));
$offset += strlen($img_file) - strlen($m[0]);
}
}
}
$mime_message->setHTMLBody($body);
return $body;
}
// parse email address input
function rcmail_email_input_format($mailto)
{
global $EMAIL_FORMAT_ERROR;
$regexp = array('/[,;]\s*[\r\n]+/', '/[\r\n]+/', '/[,;]\s*$/m', '/;/', '/(\S{1})(<\S+@\S+>)/U');
$replace = array(', ', ', ', '', ',', '\\1 \\2');
// replace new lines and strip ending ', ', make address input more valid
$mailto = trim(preg_replace($regexp, $replace, $mailto));
$result = array();
$items = rcube_explode_quoted_string(',', $mailto);
foreach($items as $item) {
$item = trim($item);
// address in brackets without name (do nothing)
if (preg_match('/^<\S+@\S+>$/', $item)) {
$result[] = $item;
// address without brackets and without name (add brackets)
} else if (preg_match('/^\S+@\S+$/', $item)) {
$result[] = '<'.$item.'>';
// address with name (handle name)
} else if (preg_match('/\S+@\S+>*$/', $item, $matches)) {
$address = $matches[0];
$name = str_replace($address, '', $item);
$name = trim($name);
if ($name && ($name[0] != '"' || $name[strlen($name)-1] != '"')
&& preg_match('/[\(\)\<\>\\\.\[\]@,;:"]/', $name)) {
$name = '"'.addcslashes($name, '"').'"';
}
if (!preg_match('/^<\S+@\S+>$/', $address))
$address = '<'.$address.'>';
$result[] = $name.' '.$address;
$item = $address;
} else if (trim($item)) {
continue;
}
// check address format
$item = trim($item, '<>');
if ($item && !check_email($item)) {
$EMAIL_FORMAT_ERROR = $item;
return;
}
}
return implode(', ', $result);
}
/****** compose message ********/
if (strlen($_POST['_draft_saveid']) > 3)
$olddraftmessageid = get_input_value('_draft_saveid', RCUBE_INPUT_POST);
$message_id = sprintf('<%s@%s>', md5(uniqid('rcmail'.mt_rand(),true)), $RCMAIL->config->mail_domain($_SESSION['imap_host']));
// set default charset
$input_charset = $OUTPUT->get_charset();
$message_charset = isset($_POST['_charset']) ? $_POST['_charset'] : $input_charset;
$EMAIL_FORMAT_ERROR = NULL;
$mailto = rcmail_email_input_format(get_input_value('_to', RCUBE_INPUT_POST, TRUE, $message_charset));
$mailcc = rcmail_email_input_format(get_input_value('_cc', RCUBE_INPUT_POST, TRUE, $message_charset));
$mailbcc = rcmail_email_input_format(get_input_value('_bcc', RCUBE_INPUT_POST, TRUE, $message_charset));
if ($EMAIL_FORMAT_ERROR) {
$OUTPUT->show_message('emailformaterror', 'error', array('email' => $EMAIL_FORMAT_ERROR));
$OUTPUT->send('iframe');
}
if (empty($mailto) && !empty($mailcc)) {
$mailto = $mailcc;
$mailcc = null;
}
else if (empty($mailto))
$mailto = 'undisclosed-recipients:;';
// get sender name and address
$from = get_input_value('_from', RCUBE_INPUT_POST, true, $message_charset);
$identity_arr = rcmail_get_identity($from);
if (!$identity_arr && ($from = rcmail_email_input_format($from))) {
if (preg_match('/(\S+@\S+)/', $from, $m))
$identity_arr['mailto'] = $m[1];
} else
$from = $identity_arr['mailto'];
if (empty($identity_arr['string']))
$identity_arr['string'] = $from;
// compose headers array
$headers = array();
// if configured, the Received headers goes to top, for good measure
if ($CONFIG['http_received_header'])
{
$nldlm = $RCMAIL->config->header_delimiter() . "\t";
// FROM/VIA
$http_header = 'from ';
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$host = $_SERVER['HTTP_X_FORWARDED_FOR'];
$hostname = gethostbyaddr($host);
if ($CONFIG['http_received_header_encrypt']) {
$http_header .= rcmail_encrypt_header($hostname);
if ($host != $hostname)
$http_header .= ' ('. rcmail_encrypt_header($host) . ')';
} else {
$http_header .= (($host != $hostname) ? $hostname : '[' . $host . ']');
$http_header .= ' ('. ($host == $hostname ? '' : $hostname . ' ') .
'[' . $host .'])';
}
$http_header .= $nldlm . ' via ';
}
$host = $_SERVER['REMOTE_ADDR'];
$hostname = gethostbyaddr($host);
if ($CONFIG['http_received_header_encrypt']) {
$http_header .= rcmail_encrypt_header($hostname);
if ($host != $hostname)
$http_header .= ' ('. rcmail_encrypt_header($host) . ')';
} else {
$http_header .= (($host != $hostname) ? $hostname : '[' . $host . ']');
$http_header .= ' ('. ($host == $hostname ? '' : $hostname . ' ') .
'[' . $host .'])';
}
// BY
$http_header .= $nldlm . 'by ' . $_SERVER['HTTP_HOST'];
// WITH
$http_header .= $nldlm . 'with ' . $_SERVER['SERVER_PROTOCOL'] .
' ('.$_SERVER['REQUEST_METHOD'] . '); ' . date('r');
$http_header = wordwrap($http_header, 69, $nldlm);
$headers['Received'] = $http_header;
}
$headers['Date'] = date('r');
$headers['From'] = rcube_charset_convert($identity_arr['string'], RCMAIL_CHARSET, $message_charset);
$headers['To'] = $mailto;
// additional recipients
if (!empty($mailcc))
$headers['Cc'] = $mailcc;
if (!empty($mailbcc))
$headers['Bcc'] = $mailbcc;
if (!empty($identity_arr['bcc']))
$headers['Bcc'] = ($headers['Bcc'] ? $headers['Bcc'].', ' : '') . $identity_arr['bcc'];
// add subject
$headers['Subject'] = trim(get_input_value('_subject', RCUBE_INPUT_POST, TRUE, $message_charset));
if (!empty($identity_arr['organization']))
$headers['Organization'] = $identity_arr['organization'];
if (!empty($_POST['_replyto']))
$headers['Reply-To'] = rcmail_email_input_format(get_input_value('_replyto', RCUBE_INPUT_POST, TRUE, $message_charset));
else if (!empty($identity_arr['reply-to']))
$headers['Reply-To'] = $identity_arr['reply-to'];
if (!empty($_SESSION['compose']['reply_msgid']))
$headers['In-Reply-To'] = $_SESSION['compose']['reply_msgid'];
if (!empty($_SESSION['compose']['references']))
$headers['References'] = $_SESSION['compose']['references'];
if (!empty($_POST['_priority']))
{
$priority = intval($_POST['_priority']);
$a_priorities = array(1=>'highest', 2=>'high', 4=>'low', 5=>'lowest');
if ($str_priority = $a_priorities[$priority])
$headers['X-Priority'] = sprintf("%d (%s)", $priority, ucfirst($str_priority));
}
if (!empty($_POST['_receipt']))
{
$headers['Return-Receipt-To'] = $identity_arr['string'];
$headers['Disposition-Notification-To'] = $identity_arr['string'];
}
// additional headers
$headers['Message-ID'] = $message_id;
$headers['X-Sender'] = $from;
if (!empty($CONFIG['useragent']))
$headers['User-Agent'] = $CONFIG['useragent'];
$isHtmlVal = strtolower(get_input_value('_is_html', RCUBE_INPUT_POST));
$isHtml = ($isHtmlVal == "1");
// fetch message body
$message_body = get_input_value('_message', RCUBE_INPUT_POST, TRUE, $message_charset);
if (!$savedraft) {
// remove signature's div ID
if ($isHtml)
$message_body = preg_replace('/\s*id="_rc_sig"/', '', $message_body);
// generic footer for all messages
if (!empty($CONFIG['generic_message_footer'])) {
$footer = file_get_contents(realpath($CONFIG['generic_message_footer']));
$footer = rcube_charset_convert($footer, RCMAIL_CHARSET, $message_charset);
}
}
// create extended PEAR::Mail_mime instance
$MAIL_MIME = new rcube_mail_mime($RCMAIL->config->header_delimiter());
// For HTML-formatted messages, construct the MIME message with both
// the HTML part and the plain-text part
if ($isHtml) {
$plugin = $RCMAIL->plugins->exec_hook('outgoing_message_body', array('body' => $message_body, 'type' => 'html', 'message' => $MAIL_MIME));
$MAIL_MIME->setHTMLBody($plugin['body'] . ($footer ? "\r\n<pre>".$footer.'</pre>' : ''));
// add a plain text version of the e-mail as an alternative part.
$h2t = new html2text($plugin['body'], false, true, 0);
$plainTextPart = rc_wordwrap($h2t->get_text(), 75, "\r\n") . ($footer ? "\r\n".$footer : '');
$plainTextPart = wordwrap($plainTextPart, 998, "\r\n", true);
if (!strlen($plainTextPart)) {
// empty message body breaks attachment handling in drafts
$plainTextPart = "\r\n";
}
$plugin = $RCMAIL->plugins->exec_hook('outgoing_message_body', array('body' => $plainTextPart, 'type' => 'alternative', 'message' => $MAIL_MIME));
$MAIL_MIME->setTXTBody($plugin['body']);
// look for "emoticon" images from TinyMCE and copy into message as attachments
$message_body = rcmail_attach_emoticons($MAIL_MIME);
}
else
{
$message_body = rc_wordwrap($message_body, 75, "\r\n");
if ($footer)
$message_body .= "\r\n" . $footer;
$message_body = wordwrap($message_body, 998, "\r\n", true);
if (!strlen($message_body)) {
// empty message body breaks attachment handling in drafts
$message_body = "\r\n";
}
$plugin = $RCMAIL->plugins->exec_hook('outgoing_message_body', array('body' => $message_body, 'type' => 'plain', 'message' => $MAIL_MIME));
$MAIL_MIME->setTXTBody($plugin['body'], false, true);
}
// chose transfer encoding
$charset_7bit = array('ASCII', 'ISO-2022-JP', 'ISO-8859-1', 'ISO-8859-2', 'ISO-8859-15');
$transfer_encoding = in_array(strtoupper($message_charset), $charset_7bit) ? '7bit' : '8bit';
// add stored attachments, if any
if (is_array($_SESSION['compose']['attachments'])) {
foreach ($_SESSION['compose']['attachments'] as $id => $attachment) {
// This hook retrieves the attachment contents from the file storage backend
$attachment = $RCMAIL->plugins->exec_hook('get_attachment', $attachment);
$dispurl = '/\ssrc\s*=\s*[\'"]*\S+display-attachment\S+file=rcmfile' . preg_quote($attachment['id']) . '[\s\'"]\s*/';
$message_body = $MAIL_MIME->getHTMLBody();
if ($isHtml && (preg_match($dispurl, $message_body) > 0)) {
$message_body = preg_replace($dispurl, ' src="'.$attachment['name'].'" ', $message_body);
$MAIL_MIME->setHTMLBody($message_body);
if ($attachment['data'])
$MAIL_MIME->addHTMLImage($attachment['data'], $attachment['mimetype'], $attachment['name'], false);
else
$MAIL_MIME->addHTMLImage($attachment['path'], $attachment['mimetype'], $attachment['name'], true);
}
else {
$ctype = str_replace('image/pjpeg', 'image/jpeg', $attachment['mimetype']); // #1484914
$file = $attachment['data'] ? $attachment['data'] : $attachment['path'];
// .eml attachments send inline
$MAIL_MIME->addAttachment($file,
$ctype,
$attachment['name'],
($attachment['data'] ? false : true),
($ctype == 'message/rfc822' ? $transfer_encoding : 'base64'),
($ctype == 'message/rfc822' ? 'inline' : 'attachment'),
$message_charset, '', '',
$CONFIG['mime_param_folding'] ? 'quoted-printable' : NULL,
$CONFIG['mime_param_folding'] == 2 ? 'quoted-printable' : NULL
);
}
}
}
// add submitted attachments
if (is_array($_FILES['_attachments']['tmp_name'])) {
foreach ($_FILES['_attachments']['tmp_name'] as $i => $filepath) {
$ctype = $files['type'][$i];
$ctype = str_replace('image/pjpeg', 'image/jpeg', $ctype); // #1484914
$MAIL_MIME->addAttachment($filepath, $ctype, $files['name'][$i], true,
$ctype == 'message/rfc822' ? $transfer_encoding : 'base64',
'attachment', $message_charset, '', '',
$CONFIG['mime_param_folding'] ? 'quoted-printable' : NULL,
$CONFIG['mime_param_folding'] == 2 ? 'quoted-printable' : NULL
);
}
}
// encoding settings for mail composing
$MAIL_MIME->setParam(array(
'text_encoding' => $transfer_encoding,
'html_encoding' => 'quoted-printable',
'head_encoding' => 'quoted-printable',
'head_charset' => $message_charset,
'html_charset' => $message_charset,
'text_charset' => $message_charset,
));
$data = $RCMAIL->plugins->exec_hook('outgoing_message_headers', array('headers' => $headers));
$headers = $data['headers'];
// encoding subject header with mb_encode provides better results with asian characters
if (function_exists('mb_encode_mimeheader'))
{
mb_internal_encoding($message_charset);
$headers['Subject'] = mb_encode_mimeheader($headers['Subject'],
$message_charset, 'Q', $RCMAIL->config->header_delimiter(), 8);
mb_internal_encoding(RCMAIL_CHARSET);
}
// pass headers to message object
$MAIL_MIME->headers($headers);
// Begin SMTP Delivery Block
if (!$savedraft)
{
// check for 'From' address (identity may be incomplete)
if ($identity_arr && !$identity_arr['mailto']) {
$OUTPUT->show_message('nofromaddress', 'error');
$OUTPUT->send('iframe');
}
$sent = rcmail_deliver_message($MAIL_MIME, $from, $mailto, $smtp_error);
// return to compose page if sending failed
if (!$sent)
{
if ($smtp_error)
$OUTPUT->show_message($smtp_error['label'], 'error', $smtp_error['vars']);
else
$OUTPUT->show_message('sendingfailed', 'error');
$OUTPUT->send('iframe');
}
// save message sent time
if (!empty($CONFIG['sendmail_delay']))
$RCMAIL->user->save_prefs(array('last_message_time' => time()));
// set replied/forwarded flag
if ($_SESSION['compose']['reply_uid'])
$IMAP->set_flag($_SESSION['compose']['reply_uid'], 'ANSWERED', $_SESSION['compose']['mailbox']);
else if ($_SESSION['compose']['forward_uid'])
$IMAP->set_flag($_SESSION['compose']['forward_uid'], 'FORWARDED', $_SESSION['compose']['mailbox']);
} // End of SMTP Delivery Block
// Determine which folder to save message
if ($savedraft)
$store_target = $CONFIG['drafts_mbox'];
else
$store_target = isset($_POST['_store_target']) ? get_input_value('_store_target', RCUBE_INPUT_POST) : $CONFIG['sent_mbox'];
if ($store_target)
{
// check if mailbox exists
if (!in_array_nocase($store_target, $IMAP->list_mailboxes()))
{
// folder may be existing but not subscribed (#1485241)
if (!in_array_nocase($store_target, $IMAP->list_unsubscribed()))
$store_folder = $IMAP->create_mailbox($store_target, TRUE);
else if ($IMAP->subscribe($store_target))
$store_folder = TRUE;
}
else
$store_folder = TRUE;
// append message to sent box
if ($store_folder)
$saved = $IMAP->save_message($store_target, $MAIL_MIME->getMessage());
// raise error if saving failed
if (!$saved)
{
raise_error(array('code' => 800, 'type' => 'imap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not save message in $store_target"), TRUE, FALSE);
if ($savedraft) {
$OUTPUT->show_message('errorsaving', 'error');
$OUTPUT->send('iframe');
}
}
if ($olddraftmessageid)
{
// delete previous saved draft
$a_deleteid = $IMAP->search($CONFIG['drafts_mbox'], 'HEADER Message-ID '.$olddraftmessageid);
$deleted = $IMAP->delete_message($IMAP->get_uid($a_deleteid[0], $CONFIG['drafts_mbox']), $CONFIG['drafts_mbox']);
// raise error if deletion of old draft failed
if (!$deleted)
raise_error(array('code' => 800, 'type' => 'imap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not delete message from ".$CONFIG['drafts_mbox']), TRUE, FALSE);
}
}
if ($savedraft)
{
$msgid = strtr($message_id, array('>' => '', '<' => ''));
// remember new draft-uid
$draftids = $IMAP->search($CONFIG['drafts_mbox'], 'HEADER Message-ID '.$msgid);
$_SESSION['compose']['param']['_draft_uid'] = $IMAP->get_uid($draftids[0], $CONFIG['drafts_mbox']);
// display success
$OUTPUT->show_message('messagesaved', 'confirmation');
// update "_draft_saveid" and the "cmp_hash" to prevent "Unsaved changes" warning
$OUTPUT->command('set_draft_id', $msgid);
$OUTPUT->command('compose_field_hash', true);
// start the auto-save timer again
$OUTPUT->command('auto_save_start');
$OUTPUT->send('iframe');
}
else
{
rcmail_compose_cleanup();
if ($store_folder && !$saved)
$OUTPUT->command('sent_successfully', 'error', rcube_label('errorsavingsent'));
else
$OUTPUT->command('sent_successfully', 'confirmation', rcube_label('messagesent'));
$OUTPUT->send('iframe');
}
?>
diff --git a/program/steps/mail/spell_pspell.inc b/program/steps/mail/spell_pspell.inc
index 578bfe523..b443ed525 100644
--- a/program/steps/mail/spell_pspell.inc
+++ b/program/steps/mail/spell_pspell.inc
@@ -1,78 +1,78 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/steps/mail/spell_pspell.inc |
| |
| This file is part of the RoundCube Webmail client |
| Licensed under the GNU GPL |
| |
| PURPOSE: |
| Use the Pspell extension to check spelling, returns results |
| compatible with spell_googie.inc. |
| |
+-----------------------------------------------------------------------+
| Author: Kris Steinhoff <steinhof@umich.edu> |
+-----------------------------------------------------------------------+
$Id$
*/
if (!extension_loaded('pspell')) {
raise_error(array(
'code' => 500,
'type' => 'php',
- 'file' => __FILE__,
+ 'file' => __FILE__, 'line' => __LINE__,
'message' => "Pspell extension not available"), true, false);
header('HTTP/1.1 404 Not Found');
exit;
}
// max. number of suggestions for one word
define('MAX_SUGGESTIONS', 10);
// read input
$data = file_get_contents('php://input');
// parse data (simplexml_load_string breaks CRLFs)
$left = strpos($data, '<text>');
$right = strrpos($data, '</text>');
$text = substr($data, $left+6, $right-($left+6));
$text = html_entity_decode($text, ENT_QUOTES, RCMAIL_CHARSET);
// tokenize
$words = preg_split('/[ !"#$%&()*+\\,\/\n:;<=>?@\[\]^_{|}-]+|\.[^\w]/', $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE );
// init spellchecker
$plink = pspell_new(get_input_value('lang', RCUBE_INPUT_GET), null, null, RCMAIL_CHARSET, PSPELL_FAST);
// send output
$out = '<?xml version="1.0" encoding="'.RCMAIL_CHARSET.'"?><spellresult charschecked="'.mb_strlen($text).'">';
$diff = 0;
foreach ($words as $w) {
$word = trim($w[0]);
$pos = $w[1] - $diff;
$len = mb_strlen($word);
if ($word && $plink && preg_match('/[^0-9\.]/', $word)
&& !pspell_check($plink, $word)) {
$suggestions = pspell_suggest($plink, $word);
if (sizeof($suggestions)>10)
$suggestions = array_slice($suggestions, 0, MAX_SUGGESTIONS);
$out .= '<c o="'.$pos.'" l="'.$len.'">';
$out .= implode("\t", $suggestions);
$out .= '</c>';
}
$diff += (strlen($word) - $len);
}
$out .= '</spellresult>';
header("Content-Type: text/xml; charset=".RCMAIL_CHARSET);
echo $out;
exit;
?>
diff --git a/program/steps/mail/viewsource.inc b/program/steps/mail/viewsource.inc
index a61385df8..17e382456 100644
--- a/program/steps/mail/viewsource.inc
+++ b/program/steps/mail/viewsource.inc
@@ -1,59 +1,59 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/steps/mail/viewsource.inc |
| |
| This file is part of the RoundCube Webmail client |
| Copyright (C) 2005-2009, RoundCube Dev. - Switzerland |
| Licensed under the GNU GPL |
| |
| PURPOSE: |
| Display a mail message similar as a usual mail application does |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
$Id$
*/
ob_end_clean();
// similar code as in program/steps/mail/get.inc
if ($uid = get_input_value('_uid', RCUBE_INPUT_GET))
{
$headers = $IMAP->get_headers($uid);
$charset = $headers->charset ? $headers->charset : $IMAP->default_charset;
header("Content-Type: text/plain; charset={$charset}");
if (!empty($_GET['_save'])) {
$filename = ($headers->subject ? $IMAP->decode_header($headers->subject) : 'roundcube') . '.eml';
$browser = new rcube_browser;
if ($browser->ie && $browser->ver < 7)
$filename = rawurlencode(abbreviate_string($filename, 55));
else if ($browser->ie)
$filename = rawurlencode($filename);
else
$filename = addcslashes($filename, '"');
header("Content-Length: {$headers->size}");
header("Content-Disposition: attachment; filename=\"$filename\"");
}
$IMAP->print_raw_body($uid);
}
else
{
raise_error(array(
'code' => 500,
'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
'message' => 'Message UID '.$uid.' not found'),
- true,
- true);
+ true, true);
}
exit;
?>

File Metadata

Mime Type
text/x-diff
Expires
Mon, Sep 7, 12:29 AM (21 h, 7 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1357566
Default Alt Text
(274 KB)

Event Timeline