Page MenuHomePhorge

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/bin/cleandb.sh b/bin/cleandb.sh
index 0bf71ea62..f54ad98f9 100755
--- a/bin/cleandb.sh
+++ b/bin/cleandb.sh
@@ -1,33 +1,31 @@
#!/usr/bin/env php
<?php
/*
+-----------------------------------------------------------------------+
- | bin/cleandb.sh |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2010-2015, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Finally remove all db records marked as deleted some time ago |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/' );
require INSTALL_PATH.'program/include/clisetup.php';
if (!empty($_SERVER['argv'][1]))
$days = intval($_SERVER['argv'][1]);
else
$days = 7;
rcmail_utils::db_clean($days);
?>
diff --git a/bin/decrypt.sh b/bin/decrypt.sh
index dd4525972..b3a9d4673 100755
--- a/bin/decrypt.sh
+++ b/bin/decrypt.sh
@@ -1,67 +1,65 @@
#!/usr/bin/env php
<?php
/*
+-----------------------------------------------------------------------+
- | bin/decrypt.sh |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2009, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Decrypt the encrypted parts of the HTTP Received: headers |
- | |
+-----------------------------------------------------------------------+
| Author: Tomas Tevesz <ice@extreme.hu> |
+-----------------------------------------------------------------------+
*/
/**
* If http_received_header_encrypt is configured, the IP address and the
* host name of the added Received: header is encrypted with 3DES, to
* protect information that some could consider sensitve, yet their
* availability is a must in some circumstances.
*
* Such an encrypted Received: header might look like:
*
* Received: from DzgkvJBO5+bw+oje5JACeNIa/uSI4mRw2cy5YoPBba73eyBmjtyHnQ==
* [my0nUbjZXKtl7KVBZcsvWOxxtyVFxza4]
* with HTTP/1.1 (POST); Thu, 14 May 2009 19:17:28 +0200
*
* In this example, the two encrypted components are the sender host name
* (DzgkvJBO5+bw+oje5JACeNIa/uSI4mRw2cy5YoPBba73eyBmjtyHnQ==) and the IP
* address (my0nUbjZXKtl7KVBZcsvWOxxtyVFxza4).
*
* Using this tool, they can be decrypted into plain text:
*
* $ bin/decrypt.sh 'my0nUbjZXKtl7KVBZcsvWOxxtyVFxza4' \
* > 'DzgkvJBO5+bw+oje5JACeNIa/uSI4mRw2cy5YoPBba73eyBmjtyHnQ=='
* 84.3.187.208
* 5403BBD0.catv.pool.telekom.hu
* $
*
* Thus it is known that this particular message was sent by 84.3.187.208,
* having, at the time of sending, the name of 5403BBD0.catv.pool.telekom.hu.
*
* If (most likely binary) junk is shown, then
* - either the encryption password has, between the time the mail was sent
* and 'now', changed, or
* - you are dealing with counterfeit header data.
*/
define('INSTALL_PATH', realpath(__DIR__ .'/..') . '/');
require INSTALL_PATH . 'program/include/clisetup.php';
if ($argc < 2) {
die("Usage: " . basename($argv[0]) . " encrypted-hdr-part [encrypted-hdr-part ...]\n");
}
$RCMAIL = rcube::get_instance();
for ($i = 1; $i < $argc; $i++) {
printf("%s\n", $RCMAIL->decrypt($argv[$i]));
};
diff --git a/bin/deluser.sh b/bin/deluser.sh
index 63473012d..984320fd1 100755
--- a/bin/deluser.sh
+++ b/bin/deluser.sh
@@ -1,145 +1,144 @@
#!/usr/bin/env php
<?php
/*
+-----------------------------------------------------------------------+
- | bin/deluser.sh |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2014, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Utility script to remove all data related to a certain user |
| from the local database. |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <thomas@roundcube.net> |
+-----------------------------------------------------------------------+
*/
define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/' );
require_once INSTALL_PATH . 'program/include/clisetup.php';
function print_usage()
{
print "Usage: deluser.sh [--host=HOST][--age=DAYS][--dry-run] [username]\n";
print "--host=HOST The IMAP hostname or IP the given user is related to\n";
print "--age=DAYS Delete all users who have not logged in for more than X days\n";
print "--dry-run List users but do not delete them (for use with --age)\n";
}
function _die($msg, $usage=false)
{
fwrite(STDERR, $msg . "\n");
if ($usage) print_usage();
exit(1);
}
$rcmail = rcube::get_instance();
// get arguments
$args = rcube_utils::get_opt(array('h' => 'host', 'a' => 'age', 'd' => 'dry-run:bool'));
if (!empty($args['age']) && ($age = intval($args['age']))) {
$db = $rcmail->get_dbh();
$db->db_connect('r');
$query = $db->query("SELECT `username`, `mail_host` FROM " . $db->table_name('users', true)
. " WHERE `last_login` < " . $db->now($age * -1 * 86400)
. ($args['host'] ? " AND `mail_host` = " . $db->quote($args['host']) : '')
);
while ($user = $db->fetch_assoc($query)) {
if (!empty($args['dry-run'])) {
printf("%s (%s)\n", $user['username'], $user['mail_host']);
continue;
}
system(sprintf("php %s/deluser.sh --host=%s %s", INSTALL_PATH . 'bin', $user['mail_host'], $user['username']));
}
exit(1);
}
$username = trim($args[0]);
if (empty($username)) {
_die("Missing required parameters", true);
}
if (empty($args['host'])) {
$hosts = $rcmail->config->get('default_host', '');
if (is_string($hosts)) {
$args['host'] = $hosts;
}
else if (is_array($hosts) && count($hosts) == 1) {
$args['host'] = reset($hosts);
}
else {
_die("Specify a host name", true);
}
// host can be a URL like tls://192.168.12.44
$host_url = parse_url($args['host']);
if ($host_url['host']) {
$args['host'] = $host_url['host'];
}
}
// connect to DB
$db = $rcmail->get_dbh();
$db->db_connect('w');
$transaction = false;
if (!$db->is_connected() || $db->is_error()) {
_die("No DB connection\n" . $db->is_error());
}
// find user in local database
$user = rcube_user::query($username, $args['host']);
if (!$user) {
die("User not found.\n");
}
// inform plugins about approaching user deletion
$plugin = $rcmail->plugins->exec_hook('user_delete_prepare', array('user' => $user, 'username' => $username, 'host' => $args['host']));
// let plugins cleanup their own user-related data
if (!$plugin['abort']) {
$transaction = $db->startTransaction();
$plugin = $rcmail->plugins->exec_hook('user_delete', $plugin);
}
if ($plugin['abort']) {
if ($transaction) {
$db->rollbackTransaction();
}
_die("User deletion aborted by plugin");
}
// deleting the user record should be sufficient due to ON DELETE CASCADE foreign key references
// but not all database backends actually support this so let's do it by hand
foreach (array('identities','contacts','contactgroups','dictionary','cache','cache_index','cache_messages','cache_thread','searches','users') as $table) {
$db->query('DELETE FROM ' . $db->table_name($table, true) . ' WHERE `user_id` = ?', $user->ID);
}
if ($db->is_error()) {
$rcmail->plugins->exec_hook('user_delete_rollback', $plugin);
_die("DB error occurred: " . $db->is_error());
}
else {
// inform plugins about executed user deletion
$plugin = $rcmail->plugins->exec_hook('user_delete_commit', $plugin);
if ($plugin['abort']) {
unset($plugin['abort']);
$db->rollbackTransaction();
$rcmail->plugins->exec_hook('user_delete_rollback', $plugin);
}
else {
$db->endTransaction();
echo "Successfully deleted user $user->ID\n";
}
}
diff --git a/bin/dumpschema.sh b/bin/dumpschema.sh
index d1d2999d6..20f0c5e91 100755
--- a/bin/dumpschema.sh
+++ b/bin/dumpschema.sh
@@ -1,98 +1,96 @@
#!/usr/bin/env php
<?php
/*
+-----------------------------------------------------------------------+
- | bin/dumpschema.sh |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2009, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Dumps database schema in XML format using MDB2_Schema |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/' );
require INSTALL_PATH.'program/include/clisetup.php';
/** callback function for schema dump **/
function print_schema($dump)
{
foreach ((array)$dump as $part)
echo $dump . "\n";
}
$config = new rcube_config();
// don't allow public access if not in devel_mode
if (!$config->get('devel_mode') && $_SERVER['REMOTE_ADDR']) {
header("HTTP/1.0 401 Access denied");
die("Access denied!");
}
$options = array(
'use_transactions' => false,
'log_line_break' => "\n",
'idxname_format' => '%s',
'debug' => false,
'quote_identifier' => true,
'force_defaults' => false,
'portability' => false,
);
$dsnw = $config->get('db_dsnw');
$dsn_array = MDB2::parseDSN($dsnw);
// set options for postgres databases
if ($dsn_array['phptype'] == 'pgsql') {
$options['disable_smart_seqname'] = true;
$options['seqname_format'] = '%s';
}
$schema =& MDB2_Schema::factory($dsnw, $options);
$schema->db->supported['transactions'] = false;
// send as text/xml when opened in browser
if ($_SERVER['REMOTE_ADDR'])
header('Content-Type: text/xml');
if (PEAR::isError($schema)) {
$error = $schema->getMessage() . ' ' . $schema->getUserInfo();
}
else {
$dump_config = array(
// 'output_mode' => 'file',
'output' => 'print_schema',
);
$definition = $schema->getDefinitionFromDatabase();
$definition['charset'] = 'utf8';
if (PEAR::isError($definition)) {
$error = $definition->getMessage() . ' ' . $definition->getUserInfo();
}
else {
$operation = $schema->dumpDatabase($definition, $dump_config, MDB2_SCHEMA_DUMP_STRUCTURE);
if (PEAR::isError($operation)) {
$error = $operation->getMessage() . ' ' . $operation->getUserInfo();
}
}
}
$schema->disconnect();
if ($error && !$_SERVER['REMOTE_ADDR']) {
fwrite(STDERR, $error);
}
?>
diff --git a/bin/exportgettext.sh b/bin/exportgettext.sh
index 314cae7d2..e2f866854 100755
--- a/bin/exportgettext.sh
+++ b/bin/exportgettext.sh
@@ -1,233 +1,235 @@
#!/usr/bin/env php
<?php
/*
+-----------------------------------------------------------------------+
- | bin/exportgettext.sh |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2011, The Roundcube Dev Team |
- | Licensed under the GNU General Public License |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
| |
| PURPOSE: |
| Export PHP-based localization files to PO files for gettext |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/' );
require INSTALL_PATH.'program/include/clisetup.php';
if ($argc < 2) {
die("Usage: " . basename($argv[0]) . " SRCDIR DESTDIR\n");
}
$srcdir = unslashify(realpath($argv[1]));
$destdir = unslashify($argv[2]);
$layout = 'launchpad'; # or 'narro';
$langcode_map = array(
'hy_AM' => 'hy',
'ar_SA' => 'ar',
'az_AZ' => 'az',
'bg_BG' => 'bg',
'bs_BA' => 'bs',
'ca_ES' => 'ca',
'cs_CZ' => 'cs',
'cy_GB' => 'cy',
'da_DK' => 'da',
'et_EE' => 'et',
'el_GR' => 'el',
'eu_ES' => 'eu',
'fa_IR' => 'fa',
'ga_IE' => 'ga',
'ka_GE' => 'ka',
'gl_ES' => 'gl',
'he_IL' => 'he',
'hi_IN' => 'hi',
'hr_HR' => 'hr',
'ja_JP' => 'ja',
'ko_KR' => 'ko',
'km_KH' => 'km',
'ms_MY' => 'ms',
'mr_IN' => 'mr',
'ml_IN' => 'ml',
'pl_PL' => 'pl',
'si_LK' => 'si',
'sl_SI' => 'sl',
'sq_AL' => 'sq',
'sr_CS' => 'sr',
'sv_SE' => 'sv',
'uk_UA' => 'uk',
'vi_VN' => 'vi',
);
// converting roundcube localization dir
if (is_dir($srcdir.'/en_US')) {
load_en_US($srcdir.'/en_US');
foreach (glob($srcdir.'/*') as $locdir) {
if (is_dir($locdir)) {
$lang = basename($locdir);
//echo "$locdir => $destdir$lang\n";
convert_dir($locdir, $destdir . ($layout != 'launchpad' ? $lang : ''));
}
}
}
// converting single localization directory
else if (is_dir($srcdir)) {
if (is_file($srcdir.'/en_US.inc')) // plugin localization
load_en_US($srcdir.'/en_US.inc');
else
load_en_US(realpath($srcdir.'/../en_US')); // single language
convert_dir($srcdir, $destdir);
}
// converting a single file
else if (is_file($srcdir)) {
//load_en_US();
convert_file($srcdir, $destdir);
}
/**
* Load en_US localization which is used to build msgids
*/
function load_en_US($fn)
{
$texts = array();
if (is_dir($fn)) {
foreach (glob($fn.'/*.inc') as $ifn) {
include($ifn);
$texts = array_merge($texts, (array)$labels, (array)$messages);
}
}
else if (is_file($fn)) {
include($fn);
$texts = array_merge($texts, (array)$labels, (array)$messages);
}
$GLOBALS['en_US'] = $texts;
}
/**
* Convert all .inc files in the given src directory
*/
function convert_dir($indir, $outdir)
{
global $layout;
if (!is_dir($outdir)) // attempt to create destination dir
mkdir($outdir, 0777, true);
foreach (glob($indir.'/*.inc') as $fn) {
$filename = basename($fn);
// create subdir for each template (launchpad rules)
if ($layout == 'launchpad' && preg_match('/^(labels|messages)/', $filename, $m)) {
$lang = end(explode('/', $indir));
$destdir = $outdir . '/' . $m[1];
if (!is_dir($destdir))
mkdir($destdir, 0777, true);
$outfn = $destdir . '/' . $lang . '.po';
}
else {
$outfn = $outdir . '/' . preg_replace('/\.[a-z0-9]+$/i', '', basename($fn)) . '.po';
}
convert_file($fn, $outfn);
}
}
/**
* Convert the given Roundcube localization file into a gettext .po file
*/
function convert_file($fn, $outfn)
{
global $layout, $langcode_map;
$basename = basename($fn);
$srcname = str_replace(INSTALL_PATH, '', $fn);
$product = preg_match('!plugins/(\w+)!', $srcname, $m) ? 'roundcube-plugin-' . $m[1] : 'roundcubemail';
$lang = preg_match('!/([a-z]{2}(_[A-Z]{2})?)[./]!', $outfn, $m) ? $m[1] : '';
$labels = $messages = $seen = array();
if (is_dir($outfn))
$outfn .= '/' . $basename . '.po';
// launchpad requires the template file to have the same name as the directory
if (strstr($outfn, '/en_US') && $layout == 'launchpad') {
$a = explode('/', $outfn);
array_pop($a);
$templ = end($a);
$a[] = $templ . '.pot';
$outfn = join('/', $a);
$is_pot = true;
}
// launchpad is very picky about file names
else if ($layout == 'launchpad' && preg_match($regex = '!/([a-z]{2})_([A-Z]{2})!', $outfn, $m)) {
if ($shortlang = $langcode_map[$lang])
$outfn = preg_replace($regex, '/'.$shortlang, $outfn);
else if ($m[1] == strtolower($m[2]))
$outfn = preg_replace($regex, '/\1', $outfn);
}
include($fn);
$texts = array_merge($labels, $messages);
// write header
$header = <<<EOF
# Converted from Roundcube PHP localization files
# Copyright (C) 2011 The Roundcube Dev Team
# This file is distributed under the same license as the Roundcube package.
#
#: %s
msgid ""
msgstr ""
"Project-Id-Version: %s\\n"
"Report-Msgid-Bugs-To: \\n"
"%s: %s\\n"
"Last-Translator: \\n"
"Language-Team: Translations <hello@roundcube.net>\\n"
"Language: %s\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
EOF;
$out = sprintf($header, $srcname, $product, $is_pot ? "POT-Creation-Date" : "PO-Revision-Date", date('c'), $shortlang ? $shortlang : $lang);
$out .= "\n";
$messages = array();
foreach ((array)$texts as $label => $msgstr) {
$msgid = $is_pot ? $msgstr : ($GLOBALS['en_US'][$label] ?: $label);
$messages[$msgid][] = $label;
}
foreach ($messages as $msgid => $labels) {
$out .= "\n";
foreach ($labels as $label)
$out .= "#: $srcname:$label\n";
$msgstr = $texts[$label];
$out .= 'msgid ' . gettext_quote($msgid) . "\n";
$out .= 'msgstr ' . gettext_quote(!$is_pot ? $msgstr : '') . "\n";
}
if ($outfn == '-')
echo $out;
else {
echo "$fn\t=>\t$outfn\n";
file_put_contents($outfn, $out);
}
}
function gettext_quote($str)
{
$out = "";
$lines = explode("\n", wordwrap(stripslashes($str)));
$last = count($lines) - 1;
foreach ($lines as $i => $line)
$out .= '"' . addcslashes($line, '"') . ($i < $last ? ' ' : '') . "\"\n";
return rtrim($out);
}
?>
diff --git a/bin/gc.sh b/bin/gc.sh
index cde6debfb..e83d18e72 100755
--- a/bin/gc.sh
+++ b/bin/gc.sh
@@ -1,39 +1,37 @@
#!/usr/bin/env php
<?php
/*
+-----------------------------------------------------------------------+
- | bin/gc.sh |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Trigger garbage collecting routines manually (e.g. via cronjob) |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/' );
require INSTALL_PATH.'program/include/clisetup.php';
$rcmail = rcube::get_instance();
$session_driver = $rcmail->config->get('session_storage', 'db');
$session_lifetime = $rcmail->config->get('session_lifetime', 0) * 60 * 2;
// Clean expired SQL sessions
if ($session_driver == 'db' && $session_lifetime) {
$db = $rcmail->get_dbh();
$db->query("DELETE FROM " . $db->table_name('session')
. " WHERE changed < " . $db->now(-$session_lifetime));
}
// Clean caches and temp directory
$rcmail->gc();
diff --git a/bin/importgettext.sh b/bin/importgettext.sh
index 5a91e6bc1..fde141c3d 100755
--- a/bin/importgettext.sh
+++ b/bin/importgettext.sh
@@ -1,196 +1,198 @@
#!/usr/bin/env php
<?php
/*
+-----------------------------------------------------------------------+
- | bin/importgettext.sh |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2011, The Roundcube Dev Team |
- | Licensed under the GNU General Public License |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
| |
| PURPOSE: |
| Import localizations from gettext PO format |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/' );
require INSTALL_PATH.'program/include/clisetup.php';
if ($argc < 2) {
die("Usage: " . basename($argv[0]) . " SRCDIR\n");
}
$srcdir = unslashify(realpath($argv[1]));
if (is_dir($srcdir)) {
$out = import_dir($srcdir);
}
else if (is_file($srcdir)) {
$out = import_file($srcdir);
}
// write output files
foreach ($out as $outfn => $texts) {
$lang = preg_match('!/([a-z]{2}(_[A-Z]{2})?)[./]!', $outfn, $m) ? $m[1] : '';
$varname = strpos($outfn, 'messages.inc') !== false ? 'messages' : 'labels';
$header = <<<EOF
<?php
/*
+-----------------------------------------------------------------------+
| localization/%s/%-51s|
| |
| Language file of the Roundcube Webmail client |
| Copyright (C) %s, The Roundcube Dev Team |
| Licensed under the GNU General Public License |
| |
+-----------------------------------------------------------------------+
| Author: %-62s|
+-----------------------------------------------------------------------+
*/
$%s = array();
EOF;
$author = preg_replace('/\s*<Unknown>/i', '', $texts['_translator']);
$output = sprintf($header, $lang, $varname.'.inc', date('Y'), $author, $varname);
foreach ($texts as $label => $value) {
if (is_array($value)) { var_dump($outfn, $label, $value); exit; }
if ($label[0] != '_' && strlen($value))
$output .= sprintf("\$%s['%s'] = '%s';\n", $varname, $label, strtr(addcslashes($value, "'"), array("\r" => '', "\n" => '\n')));
}
$output .= "\n";
$dir = dirname($outfn);
@mkdir($dir, 0755, true);
if (file_put_contents($outfn, $output))
echo "-> $outfn\n";
}
/**
* Convert all .po files in the given src directory
*/
function import_dir($indir)
{
$out = array();
foreach (glob($indir.'/*.po') as $fn) {
$out = array_merge_recursive($out, import_file($fn));
}
return $out;
}
/**
* Convert the given .po file into a Roundcube localization array
*/
function import_file($fn)
{
$out = array();
$lines = file($fn);
$language = '';
$translator = '';
// get language code from file name
if (preg_match('/-([a-z_]+).po$/i', $fn, $m))
$language = expand_langcode($m[1]);
$is_header = true;
$msgid = null;
$msgstr = '';
$dests = array();
foreach ($lines as $i => $line) {
$line = trim($line);
// parse header
if ($is_header && $line[0] == '"') {
list($key, $val) = explode(": ", preg_replace('/\\\n$/', '', trim($line, '"')), 2);
switch (strtolower($key)) {
case 'language':
$language = expand_langcode($val);
break;
case 'last-translator':
$translator = $val;
break;
}
}
// empty line
if ($line == '') {
if ($msgid && $dests) {
foreach ($dests as $dest) {
list($file, $label) = explode(':', $dest);
$out[$file][$label] = $msgstr;
$out[$file]['_translator'] = $translator;
}
}
$msgid = null;
$msgstr = '';
$dests = array();
}
// meta line
if ($line[0] == '#') {
$value = trim(substr($line, 2));
if ($line[1] == ':')
$dests[] = str_replace('en_US', $language, $value);
}
else if (strpos($line, 'msgid') === 0) {
$msgid = gettext_decode(substr($line, 6));
if (!empty($msgid))
$is_header = false;
}
else if (strpos($line, 'msgstr') === 0) {
$msgstr = gettext_decode(substr($line, 7));
}
else if ($msgid && $line[0] == '"') {
$msgstr .= gettext_decode($line);
}
else if ($msgid !== null && $line[0] == '"') {
$msgid .= gettext_decode($line);
}
}
if ($msgid && $dests) {
foreach ($dests as $dest) {
list($file, $label) = explode(':', $dest);
$out[$file][$label] = $msgstr;
$out[$file]['_translator'] = $translator;
}
}
return $language ? $out : array();
}
function gettext_decode($str)
{
return stripslashes(trim($str, '"'));
}
/**
* Translate two-chars language codes to our internally used language identifiers
*/
function expand_langcode($lang)
{
static $rcube_language_aliases, $rcube_languages;
if (!$rcube_language_aliases)
include(INSTALL_PATH . 'program/localization/index.inc');
if ($rcube_language_aliases[$lang])
return $rcube_language_aliases[$lang];
else if (strlen($lang) == 2 && !isset($rcube_languages[$lang]))
return strtolower($lang) . '_' . strtoupper($lang);
else
return $lang;
}
?>
diff --git a/bin/indexcontacts.sh b/bin/indexcontacts.sh
index 760e53792..fefe18e6e 100755
--- a/bin/indexcontacts.sh
+++ b/bin/indexcontacts.sh
@@ -1,29 +1,28 @@
#!/usr/bin/env php
<?php
/*
+-----------------------------------------------------------------------+
- | bin/indexcontacts.sh |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2011, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Update the fulltext index for all contacts of the internal |
| address book. |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/' );
require_once INSTALL_PATH.'program/include/clisetup.php';
ini_set('memory_limit', -1);
rcmail_utils::indexcontacts();
?>
diff --git a/bin/initdb.sh b/bin/initdb.sh
index fd22007d2..cbfb9def9 100755
--- a/bin/initdb.sh
+++ b/bin/initdb.sh
@@ -1,42 +1,41 @@
#!/usr/bin/env php
<?php
/*
+-----------------------------------------------------------------------+
- | bin/initdb.sh |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2010-2015, The Roundcube Dev Team |
- | Copyright (C) 2010-2015, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Create database schema |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/' );
require_once INSTALL_PATH . 'program/include/clisetup.php';
// get arguments
$opts = rcube_utils::get_opt(array(
'd' => 'dir',
));
if (empty($opts['dir'])) {
rcube::raise_error("Database schema directory not specified (--dir).", false, true);
}
// Check if directory exists
if (!file_exists($opts['dir'])) {
rcube::raise_error("Specified database schema directory doesn't exist.", false, true);
}
rcmail_utils::db_init($opts['dir']);
?>
diff --git a/bin/install-jsdeps.sh b/bin/install-jsdeps.sh
index b0df1f763..347ae851e 100755
--- a/bin/install-jsdeps.sh
+++ b/bin/install-jsdeps.sh
@@ -1,376 +1,375 @@
#!/usr/bin/env php
<?php
/*
+-----------------------------------------------------------------------+
- | bin/install-jsdeps.sh |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2016, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Utility script to fetch and install all 3rd party javascript |
- | libraries used in Roundcube from source. |
+ | libraries used in Roundcube from source. |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <thomas@roundcube.net> |
+-----------------------------------------------------------------------+
*/
define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/' );
require_once INSTALL_PATH . 'program/include/clisetup.php';
if (!function_exists('exec')) {
rcube::raise_error("PHP exec() function is required. Check disable_functions in php.ini.", false, true);
}
$cfgfile = INSTALL_PATH . 'jsdeps.json';
$SOURCES = json_decode(file_get_contents($cfgfile), true);
if (empty($SOURCES['dependencies'])) {
rcube::raise_error("Failed to read dependencies list from $cfgfile", false, true);
}
$CURL = trim(`which curl`);
$WGET = trim(`which wget`);
$UNZIP = trim(`which unzip`);
$FILEINFO = trim(`which file`);
if (($CACHEDIR = getenv("CACHEDIR")) && is_writeable($CACHEDIR)) {
// use $CACHEDIR
}
else if (is_writeable(INSTALL_PATH . 'temp/js_cache') || @mkdir(INSTALL_PATH . 'temp/js_cache', 0774, true)) {
$CACHEDIR = INSTALL_PATH . 'temp/js_cache';
}
else {
$CACHEDIR = sys_get_temp_dir();
}
//////////////// License definitions
$LICENSES = array();
$LICENSES['MIT'] = <<<EOM
* Licensed under the MIT licenses
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
EOM;
$LICENSES['GPLv3'] = <<<EOG
* The JavaScript code in this page is free software: you can
* redistribute it and/or modify it under the terms of the GNU
* General Public License (GNU GPL) as published by the Free Software
* Foundation, either version 3 of the License, or (at your option)
* any later version. The code is distributed WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
*
* As additional permission under GNU GPL version 3 section 7, you
* may distribute non-source (e.g., minimized or compacted) forms of
* that code without the copy of the GNU GPL normally required by
* section 4, provided you include this license notice and a URL
* through which recipients can access the Corresponding Source.
EOG;
$LICENSES['LGPL'] = <<<EOL
* The JavaScript code in this page is free software: you can
* redistribute it and/or modify it under the terms of the GNU
* Lesser General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option)
* any later version.
EOL;
//////////////// Functions
/**
* Fetch package file from source
*/
function fetch_from_source($package, $useCache = true, &$filetype = null)
{
global $CURL, $WGET;
$cache_file = extract_filetype($package, $filetype);
if (!is_readable($cache_file) || !$useCache) {
if (empty($CURL) && empty($WGET)) {
rcube::raise_error("Required 'wget' or 'curl' program not found.", false, true);
}
$url = str_replace('$v', $package['version'], $package['url']);
echo "Fetching $url\n";
if ($CURL)
exec(sprintf('%s -L -s %s -o %s', $CURL, escapeshellarg($url), $cache_file), $out, $retval);
else
exec(sprintf('%s -q %s -O %s', $WGET, escapeshellarg($url), $cache_file), $out, $retval);
// Try Github API as a fallback (#6248)
if ($retval !== 0 && $package['api_url']) {
$url = str_replace('$v', $package['version'], $package['api_url']);
$header = 'Accept:application/vnd.github.v3.raw';
rcube::raise_error("Fetching failed. Using Github API on $url");
if ($CURL)
exec(sprintf('%s -L -H %s -s %s -o %s', $CURL, escapeshellarg($header), escapeshellarg($url), $cache_file), $out, $retval);
else
exec(sprintf('%s --header %s -q %s -O %s', $WGET, escapeshellarg($header), escapeshellarg($url), $cache_file), $out, $retval);
}
if ($retval !== 0) {
rcube::raise_error("Failed to download source file from $url", false, true);
}
}
return $cache_file;
}
/**
* Returns package source file location and type
*/
function extract_filetype($package, &$filetype = null)
{
global $FILEINFO, $CACHEDIR;
$filetype = pathinfo($package['url'], PATHINFO_EXTENSION) ?: 'tmp';
$cache_file = $CACHEDIR . '/' . $package['lib'] . '-' . $package['version'] . '.' . $filetype;
if (empty($FILEINFO)) {
rcube::raise_error("Required program 'file' not found.", false, true);
}
// detect downloaded/cached file type
exec(sprintf('%s -b %s', $FILEINFO, $cache_file), $out);
if (stripos($out[0], 'zip') === 0) {
$filetype = 'zip';
}
return $cache_file;
}
/**
* Create a destination javascript file with copyright and license header
*/
function compose_destfile($package, $srcfile)
{
global $LICENSES;
$header = sprintf("/**\n * %s - v%s\n *\n", $package['name'], $package['version']);
if (!empty($package['source'])) {
$header .= " * @source " . str_replace('$v', $package['version'], $package['source']) . "\n";
$header .= " *\n";
}
if (!empty($package['license']) && isset($LICENSES[$package['license']])) {
$header .= " * @licstart The following is the entire license notice for the\n";
$header .= " * JavaScript code in this file.\n";
$header .= " *\n";
if (!empty($package['copyright'])) {
$header .= " * " . $package['copyright'] . "\n";
$header .= " *\n";
}
$header .= $LICENSES[$package['license']];
$header .= " *\n";
$header .= " * @licend The above is the entire license notice\n";
$header .= " * for the JavaScript code in this file.\n";
}
$header .= " */\n";
if (file_put_contents(INSTALL_PATH . $package['dest'], $header . file_get_contents($srcfile))) {
echo "Wrote file " . INSTALL_PATH . $package['dest'] . "\n";
}
else {
rcube::raise_error("Failed to write destination file " . INSTALL_PATH . $package['dest'], false, true);
}
}
/**
* Extract a Zip archive into the destination specified by the package config
*/
function extract_zipfile($package, $srcfile)
{
global $UNZIP, $CACHEDIR;
if (empty($UNZIP)) {
rcube::raise_error("Required 'unzip' program not found.", false, true);
}
$destdir = INSTALL_PATH . $package['dest'];
if (!is_dir($destdir)) {
mkdir($destdir, 0775, true);
}
if (!is_writeable($destdir)) {
rcube::raise_error("Cannot write to destination directory: $destdir", false, true);
}
// pick files from zip archive
if (!empty($package['pick'])) {
foreach ($package['pick'] as $pattern) {
echo "Extracting files $pattern into $destdir\n";
exec(sprintf('%s -o %s %s -d %s', $UNZIP, escapeshellarg($srcfile), escapeshellarg($pattern), $destdir), $out, $retval);
if ($retval !== 0) {
rcube::raise_error("Failed to unpack $pattern; " . join('; ' . $out));
}
}
}
// unzip the archive and map source to dest files/directories
else if (!empty($package['map'])) {
$extract = $CACHEDIR . '/' . $package['lib'] . '-extract';
if (!is_dir($extract)) {
mkdir($extract, 0774, true);
}
$zip_command = '%s -' . ($package['flat'] ? 'j' : 'o') . ' %s -d %s';
exec(sprintf($zip_command, $UNZIP, escapeshellarg($srcfile), $extract), $out, $retval);
// get the root folder of the extracted package
$extract_tree = glob("$extract/*", GLOB_ONLYDIR);
$sourcedir = count($extract_tree) ? $extract_tree[0] : $extract;
foreach ($package['map'] as $src => $dest) {
echo "Installing $sourcedir/$src into $destdir/$dest\n";
// make sure the destination's parent directory exists
if (strpos($dest, '/') !== false) {
$parentdir = dirname($destdir . '/' . $dest);
if (!is_dir($parentdir)) {
mkdir($parentdir, 0775, true);
}
}
// avoid copying source directory as a child into destination
if (is_dir($sourcedir . '/' . $src) && is_dir($destdir . '/' . $dest)) {
exec(sprintf('rm -rf %s/%s', $destdir, $dest));
}
exec(sprintf('mv -f %s/%s %s/%s', $sourcedir, $src, $destdir, $dest), $out, $retval);
if ($retval !== 0) {
rcube::raise_error("Failed to move $src into $destdir/$dest; " . join('; ' . $out));
}
}
// remove temp extraction dir
exec('rm -rf ' . $extract);
}
// extract the archive into the destination directory
else {
echo "Extracting zip archive into $destdir\n";
exec(sprintf('%s -o %s -d %s', $UNZIP, escapeshellarg($srcfile), $destdir), $out, $retval);
if ($retval !== 0) {
rcube::raise_error("Failed to unzip $srcfile; " . join('; ' . $out));
}
}
// remove some files from the destination
if (!empty($package['omit'])) {
foreach ((array)$package['omit'] as $glob) {
exec(sprintf('rm -rf %s/%s', $destdir, escapeshellarg($glob)));
}
}
// prepend license header to extracted files
if (!empty($package['addlicense'])) {
foreach ((array)$package['addlicense'] as $filename) {
$pkg = $package;
$pkg['dest'] = $package['dest'] . '/' . $filename;
compose_destfile($pkg, $destdir . '/' . $filename);
}
}
}
/**
* Delete the package destination file/dir
*/
function delete_destfile($package)
{
$destdir = INSTALL_PATH . ($package['rm'] ?: $package['dest']);
if (file_exists($destdir)) {
if (PHP_OS === 'Windows') {
exec(sprintf("rd /s /q %s", escapeshellarg($destdir)));
}
else {
exec(sprintf("rm -rf %s", escapeshellarg($destdir)));
}
}
}
//////////////// Execution
$args = rcube_utils::get_opt(array('f' => 'force:bool', 'd' => 'delete:bool', 'g' => 'get:bool', 'e' => 'extract:bool'))
+ array('force' => false, 'delete' => false, 'get' => false, 'extract' => false);
$WHAT = $args[0];
$useCache = !$args['force'] && !$args['get'];
if (!$args['get'] && !$args['extract'] && !$args['delete']) {
$args['get'] = $args['extract'] = 1;
}
foreach ($SOURCES['dependencies'] as $package) {
if (!isset($package['name'])) {
$package['name'] = $package['lib'];
}
if ($WHAT && $package['lib'] !== $WHAT) {
continue;
}
if ($args['delete']) {
delete_destfile($package);
continue;
}
if ($args['get']) {
$srcfile = fetch_from_source($package, $useCache, $filetype);
}
else {
$srcfile = extract_filetype($package, $filetype);
}
if (!empty($package['sha1']) && ($sum = sha1_file($srcfile)) !== $package['sha1']) {
rcube::raise_error("Incorrect sha1 sum of $srcfile. Expected: {$package['sha1']}, got: $sum", false, true);
}
if ($args['extract']) {
echo "Installing {$package['name']}...\n";
if ($filetype === 'zip') {
extract_zipfile($package, $srcfile);
}
else {
compose_destfile($package, $srcfile);
}
echo "Done.\n";
}
}
diff --git a/bin/installto.sh b/bin/installto.sh
index f16f01622..16e9527ab 100755
--- a/bin/installto.sh
+++ b/bin/installto.sh
@@ -1,135 +1,134 @@
#!/usr/bin/env php
<?php
/*
+-----------------------------------------------------------------------+
- | bin/installto.sh |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2014-2016, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Update an existing Roundcube installation with files from |
| this version |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/' );
require_once INSTALL_PATH . 'program/include/clisetup.php';
if (!function_exists('system')) {
rcube::raise_error("PHP system() function is required. Check disable_functions in php.ini.", false, true);
}
$target_dir = unslashify($_SERVER['argv'][1]);
if (empty($target_dir) || !is_dir(realpath($target_dir)))
rcube::raise_error("Invalid target: not a directory\nUsage: installto.sh <TARGET>", false, true);
// read version from iniset.php
$iniset = @file_get_contents($target_dir . '/program/include/iniset.php');
if (!preg_match('/define\(.RCMAIL_VERSION.,\s*.([0-9.]+[a-z-]*)/', $iniset, $m))
rcube::raise_error("No valid Roundcube installation found at $target_dir", false, true);
$oldversion = $m[1];
if (version_compare(version_parse($oldversion), version_parse(RCMAIL_VERSION), '>='))
rcube::raise_error("Installation at target location is up-to-date!", false, true);
echo "Upgrading from $oldversion. Do you want to continue? (y/N)\n";
$input = trim(fgets(STDIN));
if (strtolower($input) == 'y') {
echo "Copying files to target location...";
$adds = array();
$dirs = array('program','bin','SQL','plugins','skins');
if (is_dir(INSTALL_PATH . 'vendor') && !is_file("$target_dir/composer.json")) {
$dirs[] = 'vendor';
}
if (file_exists("$target_dir/installer")) {
$dirs[] = 'installer';
}
foreach ($dirs as $dir) {
// @FIXME: should we use --delete for all directories?
$delete = in_array($dir, array('program', 'vendor', 'installer')) ? '--delete ' : '';
$command = "rsync -aC --out-format=%n " . $delete . INSTALL_PATH . "$dir/ $target_dir/$dir/";
if (system($command, $ret) === false || $ret > 0) {
rcube::raise_error("Failed to execute command: $command", false, true);
}
}
foreach (array('index.php','config/defaults.inc.php','composer.json-dist','jsdeps.json','CHANGELOG','README.md','UPGRADING','LICENSE','INSTALL') as $file) {
$command = "rsync -a --out-format=%n " . INSTALL_PATH . "$file $target_dir/$file";
if (file_exists(INSTALL_PATH . $file) && (system($command, $ret) === false || $ret > 0)) {
rcube::raise_error("Failed to execute command: $command", false, true);
}
}
// Copy .htaccess or .user.ini if needed
foreach (array('.htaccess','.user.ini') as $file) {
if (file_exists(INSTALL_PATH . $file)) {
if (!file_exists("$target_dir/$file") || file_get_contents(INSTALL_PATH . $file) != file_get_contents("$target_dir/$file")) {
if (copy(INSTALL_PATH . $file, "$target_dir/$file.new")) {
echo "$file.new\n";
$adds[] = "NOTICE: New $file file saved as $file.new.";
}
}
}
}
// remove old (<1.0) .htaccess file
@unlink("$target_dir/program/.htaccess");
echo "done.\n\n";
if (is_dir("$target_dir/skins/default")) {
echo "Removing old default skin...";
system("rm -rf $target_dir/skins/default $target_dir/plugins/jqueryui/themes/default");
foreach (glob(INSTALL_PATH . "plugins/*/skins") as $plugin_skin_dir) {
$plugin_skin_dir = preg_replace('!^.*' . INSTALL_PATH . '!', '', $plugin_skin_dir);
if (is_dir("$target_dir/$plugin_skin_dir/classic"))
system("rm -rf $target_dir/$plugin_skin_dir/default");
}
echo "done.\n\n";
}
// check if js-deps are up-to-date
if (file_exists("$target_dir/jsdeps.json") && file_exists("$target_dir/bin/install-jsdeps.sh")) {
$jsdeps = json_decode(file_get_contents("$target_dir/jsdeps.json"));
$package = $jsdeps->dependencies[0];
$dest_file = $target_dir . '/' . $package->dest;
if (!file_exists($dest_file) || sha1_file($dest_file) !== $package->sha1) {
echo "Installing JavaScript dependencies...";
system("cd $target_dir && bin/install-jsdeps.sh");
echo "done.\n\n";
}
}
else {
$adds[] = "NOTICE: JavaScript dependencies installation skipped...";
}
if (file_exists("$target_dir/installer")) {
$adds[] = "NOTICE: The 'installer' directory still exists. You should remove it after the upgrade.";
}
if (!empty($adds)) {
echo implode($adds, "\n") . "\n\n";
}
echo "Running update script at target...\n";
system("cd $target_dir && php bin/update.sh --version=$oldversion");
echo "All done.\n";
}
else {
echo "Update cancelled. See ya!\n";
}
?>
diff --git a/bin/moduserprefs.sh b/bin/moduserprefs.sh
index 94f9ec297..c7dada01f 100755
--- a/bin/moduserprefs.sh
+++ b/bin/moduserprefs.sh
@@ -1,68 +1,67 @@
#!/usr/bin/env php
<?php
/*
+-----------------------------------------------------------------------+
- | bin/moduserprefs.sh |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2012-2015, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Bulk-change settings stored in user preferences |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/' );
require_once INSTALL_PATH.'program/include/clisetup.php';
function print_usage()
{
print "Usage: moduserprefs.sh [options] pref-name [pref-value]\n";
print "Options:\n";
print " --user=user-id User ID in local database\n";
print " --config=path Location of additional configuration file\n";
print " --delete Unset the given preference\n";
print " --type=type Pref-value type: int, bool, string\n";
}
// get arguments
$args = rcube_utils::get_opt(array(
'u' => 'user',
'd' => 'delete:bool',
't' => 'type',
'c' => 'config',
));
if ($_SERVER['argv'][1] == 'help') {
print_usage();
exit;
}
else if (empty($args[0]) || (empty($args[1]) && empty($args['delete']))) {
print "Missing required parameters.\n";
print_usage();
exit;
}
$pref_name = trim($args[0]);
$pref_value = $args['delete'] ? null : trim($args[1]);
if ($pref_value === null) {
$args['type'] = null;
}
if ($args['config']) {
$rcube = rcube::get_instance();
$rcube->config->load_from_file($args['config']);
}
rcmail_utils::mod_pref($pref_name, $pref_value, $args['user'], $args['type']);
?>
diff --git a/bin/msgexport.sh b/bin/msgexport.sh
index f76aefe86..86ba935f5 100755
--- a/bin/msgexport.sh
+++ b/bin/msgexport.sh
@@ -1,143 +1,156 @@
#!/usr/bin/env php
<?php
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <thomas@roundcube.net> |
+ +-----------------------------------------------------------------------+
+*/
define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/' );
ini_set('memory_limit', -1);
require_once INSTALL_PATH.'program/include/clisetup.php';
function print_usage()
{
print "Usage: msgexport -h imap-host -u user-name -m mailbox name\n";
print "--host IMAP host\n";
print "--user IMAP user name\n";
print "--mbox Folder name, set to '*' for all\n";
print "--file Output file\n";
}
function vputs($str)
{
$out = $GLOBALS['args']['file'] ? STDOUT : STDERR;
fwrite($out, $str);
}
function progress_update($pos, $max)
{
$percent = round(100 * $pos / $max);
vputs(sprintf("%3d%% [%-51s] %d/%d\033[K\r", $percent, @str_repeat('=', $percent / 2) . '>', $pos, $max));
}
function export_mailbox($mbox, $filename)
{
global $IMAP;
$IMAP->set_folder($mbox);
$index = $IMAP->index($mbox, null, 'ASC');
$count = $index->count();
$index = $index->get();
vputs("Getting message list of {$mbox}...");
vputs("$count messages\n");
if ($filename)
{
if (!($out = fopen($filename, 'w')))
{
vputs("Cannot write to output file\n");
return;
}
vputs("Writing to $filename\n");
}
else
$out = STDOUT;
for ($i = 0; $i < $count; $i++)
{
$headers = $IMAP->get_message_headers($index[$i]);
$from = current(rcube_mime::decode_address_list($headers->from, 1, false));
fwrite($out, sprintf("From %s %s UID %d\n", $from['mailto'], $headers->date, $headers->uid));
$IMAP->get_raw_body($headers->uid, $out);
fwrite($out, "\n\n\n");
progress_update($i+1, $count);
}
vputs("\ncomplete.\n");
if ($filename)
fclose($out);
}
// get arguments
$opts = array('h' => 'host', 'u' => 'user', 'p' => 'pass', 'm' => 'mbox', 'f' => 'file');
$args = rcube_utils::get_opt($opts) + array('host' => 'localhost', 'mbox' => 'INBOX');
if ($_SERVER['argv'][1] == 'help')
{
print_usage();
exit;
}
else if (!$args['host'])
{
vputs("Missing required parameters.\n");
print_usage();
exit;
}
// prompt for username if not set
if (empty($args['user']))
{
vputs("IMAP user: ");
$args['user'] = trim(fgets(STDIN));
}
// prompt for password
$args['pass'] = rcube_utils::prompt_silent("Password: ");
// parse $host URL
$a_host = parse_url($args['host']);
if ($a_host['host'])
{
$host = $a_host['host'];
$imap_ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? TRUE : FALSE;
$imap_port = isset($a_host['port']) ? $a_host['port'] : ($imap_ssl ? 993 : 143);
}
else
{
$host = $args['host'];
$imap_port = 143;
}
// instantiate IMAP class
$IMAP = new rcube_imap(null);
// try to connect to IMAP server
if ($IMAP->connect($host, $args['user'], $args['pass'], $imap_port, $imap_ssl))
{
vputs("IMAP login successful.\n");
$filename = null;
$mailboxes = $args['mbox'] == '*' ? $IMAP->list_folders(null) : array($args['mbox']);
foreach ($mailboxes as $mbox)
{
if ($args['file'])
$filename = preg_replace('/\.[a-z0-9]{3,4}$/i', '', $args['file']) . asciiwords($mbox) . '.mbox';
else if ($args['mbox'] == '*')
$filename = asciiwords($mbox) . '.mbox';
if ($args['mbox'] == '*' && in_array(strtolower($mbox), array('junk','spam','trash')))
continue;
export_mailbox($mbox, $filename);
}
}
else
{
vputs("IMAP login failed.\n");
}
?>
diff --git a/bin/msgimport.sh b/bin/msgimport.sh
index 0c72622c4..42077c23a 100755
--- a/bin/msgimport.sh
+++ b/bin/msgimport.sh
@@ -1,113 +1,126 @@
#!/usr/bin/env php
<?php
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <thomas@roundcube.net> |
+ +-----------------------------------------------------------------------+
+*/
define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/' );
ini_set('memory_limit', -1);
require_once INSTALL_PATH.'program/include/clisetup.php';
function print_usage()
{
print "Usage: msgimport -h imap-host -u user-name -m mailbox -f message-file\n";
print "--host IMAP host\n";
print "--user IMAP user name\n";
print "--mbox Target mailbox\n";
print "--file Message file to upload\n";
}
// get arguments
$opts = array('h' => 'host', 'u' => 'user', 'p' => 'pass', 'm' => 'mbox', 'f' => 'file');
$args = rcube_utils::get_opt($opts) + array('host' => 'localhost', 'mbox' => 'INBOX');
if ($_SERVER['argv'][1] == 'help')
{
print_usage();
exit;
}
else if (!($args['host'] && $args['file']))
{
print "Missing required parameters.\n";
print_usage();
exit;
}
else if (!is_file($args['file']))
{
rcube::raise_error("Cannot read message file.", false, true);
}
// prompt for username if not set
if (empty($args['user']))
{
//fwrite(STDOUT, "Please enter your name\n");
echo "IMAP user: ";
$args['user'] = trim(fgets(STDIN));
}
// prompt for password
if (empty($args['pass']))
{
$args['pass'] = rcube_utils::prompt_silent("Password: ");
}
// parse $host URL
$a_host = parse_url($args['host']);
if ($a_host['host'])
{
$host = $a_host['host'];
$imap_ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? TRUE : FALSE;
$imap_port = isset($a_host['port']) ? $a_host['port'] : ($imap_ssl ? 993 : 143);
}
else
{
$host = $args['host'];
$imap_port = 143;
}
// instantiate IMAP class
$IMAP = new rcube_imap(null);
// try to connect to IMAP server
if ($IMAP->connect($host, $args['user'], $args['pass'], $imap_port, $imap_ssl))
{
print "IMAP login successful.\n";
print "Uploading messages...\n";
$count = 0;
$message = $lastline = '';
$fp = fopen($args['file'], 'r');
while (($line = fgets($fp)) !== false)
{
if (preg_match('/^From\s+-/', $line) && $lastline == '')
{
if (!empty($message))
{
if ($IMAP->save_message($args['mbox'], rtrim($message)))
$count++;
else
rcube::raise_error("Failed to save message to {$args['mbox']}", false, true);
$message = '';
}
continue;
}
$message .= $line;
$lastline = rtrim($line);
}
if (!empty($message) && $IMAP->save_message($args['mbox'], rtrim($message)))
$count++;
// upload message from file
if ($count)
print "$count messages successfully added to {$args['mbox']}.\n";
else
print "Adding messages failed!\n";
}
else
{
rcube::raise_error("IMAP login failed.", false, true);
}
?>
diff --git a/bin/package2composer.sh b/bin/package2composer.sh
index 5f3438cd0..3f9b15025 100755
--- a/bin/package2composer.sh
+++ b/bin/package2composer.sh
@@ -1,109 +1,107 @@
#!/usr/bin/env php
<?php
/*
+-----------------------------------------------------------------------+
- | bin/package2composer.sh |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Convert a plugin's package.xml file into a composer.json description |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <thomas@roundcube.net> |
+-----------------------------------------------------------------------+
*/
ini_set('error_reporting', E_ALL & ~E_NOTICE);
list(, $filename, $vendor) = $_SERVER['argv'];
if (!$filename || !is_readable($filename)) {
die("Invalid input file name!\nUsage: " . $_SERVER['argv'][0] . " XMLFILE VENDOR\n");
}
if (!$vendor) {
$vendor = 'anonymous';
}
$package = new SimpleXMLElement(file_get_contents($filename));
$data = array(
'name' => $vendor . '/' . strval($package->name),
'type' => 'roundcube-plugin',
'description' => trim(strval($package->description), '- ') ? trim(strval($package->description)) : trim(strval($package->summary)),
'homepage' => strval($package->uri),
'license' => 'GPLv3+',
'version' => strval($package->version->release),
'authors' => array(),
'repositories' => array(
array('type' => 'composer', 'url' => 'https://plugins.roundcube.net'),
),
'require' => array(
'php' => '>=5.3.0',
'roundcube/plugin-installer' => '>=0.1.3',
),
);
if ($package->license) {
$data['license'] = strval($package->license);
}
if ($package->lead) {
foreach ($package->lead as $lead) {
if (strval($lead->active) == 'no') {
continue;
}
$data['authors'][] = array(
'name' => strval($lead->name),
'email' => strval($lead->email),
'role' => 'Lead',
);
}
}
if ($devs = $package->developer) {
foreach ($package->developer as $dev) {
$data['authors'][] = array(
'name' => strval($dev->name),
'email' => strval($dev->email),
'role' => 'Developer',
);
}
}
if ($package->dependencies->required->extension) {
foreach ($package->dependencies->required->extension as $ext) {
$data['require']['ext-' . strval($ext->name)] = '*';
}
}
// remove empty values
$data = array_filter($data);
// use the JSON encoder from the Composer package
if (is_file('composer.phar')) {
include 'phar://composer.phar/src/Composer/Json/JsonFile.php';
echo \Composer\Json\JsonFile::encode($data);
}
// PHP 5.4's json_encode() does the job, too
else if (defined('JSON_PRETTY_PRINT')) {
$flags = defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT & JSON_UNESCAPED_SLASHES : 0;
echo json_encode($data, $flags);
}
else {
fwrite(STDERR,
"FAILED! composer.phar not found in current directory.
Please download it from http://getcomposer.org/download/ or with
curl -s http://getcomposer.org/installer | php
");
}
echo "\n";
diff --git a/bin/update.sh b/bin/update.sh
index 08e3bb531..5134dceb9 100755
--- a/bin/update.sh
+++ b/bin/update.sh
@@ -1,276 +1,275 @@
#!/usr/bin/env php
<?php
/*
+-----------------------------------------------------------------------+
- | bin/update.sh |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2010-2015, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Check local configuration and database schema after upgrading |
| to a new version |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/' );
require_once INSTALL_PATH . 'program/include/clisetup.php';
// get arguments
$opts = rcube_utils::get_opt(array('v' => 'version', 'y' => 'accept:bool'));
// ask user if no version is specified
if (!$opts['version']) {
echo "What version are you upgrading from? Type '?' if you don't know.\n";
if (($input = trim(fgets(STDIN))) && preg_match('/^[0-9.]+[a-z-]*$/', $input))
$opts['version'] = $input;
else
$opts['version'] = RCMAIL_VERSION;
}
$RCI = rcmail_install::get_instance();
$RCI->load_config();
if ($RCI->configured) {
$success = true;
if (($messages = $RCI->check_config()) || $RCI->legacy_config) {
$success = false;
$err = 0;
// list old/replaced config options
if (is_array($messages['replaced'])) {
echo "WARNING: Replaced config options:\n";
echo "(These config options have been replaced or renamed)\n";
foreach ($messages['replaced'] as $msg) {
echo "- '" . $msg['prop'] . "' was replaced by '" . $msg['replacement'] . "'\n";
$err++;
}
echo "\n";
}
// list obsolete config options (just a notice)
if (is_array($messages['obsolete'])) {
echo "NOTICE: Obsolete config options:\n";
echo "(You still have some obsolete or inexistent properties set. This isn't a problem but should be noticed)\n";
foreach ($messages['obsolete'] as $msg) {
echo "- '" . $msg['prop'] . ($msg['name'] ? "': " . $msg['name'] : "'") . "\n";
$err++;
}
echo "\n";
}
if (!$err && $RCI->legacy_config) {
echo "WARNING: Your configuration needs to be migrated!\n";
echo "We changed the configuration files structure and your two config files main.inc.php and db.inc.php have to be merged into one single file.\n";
$err++;
}
// ask user to update config files
if ($err) {
if (!$opts['accept']) {
echo "Do you want me to fix your local configuration? (y/N)\n";
$input = trim(fgets(STDIN));
}
// positive: let's merge the local config with the defaults
if ($opts['accept'] || strtolower($input) == 'y') {
$error = $written = false;
// backup current config
echo ". backing up the current config file(s)...\n";
foreach (array('config', 'main', 'db') as $file) {
if (file_exists(RCMAIL_CONFIG_DIR . '/' . $file . '.inc.php')) {
if (!copy(RCMAIL_CONFIG_DIR . '/' . $file . '.inc.php', RCMAIL_CONFIG_DIR . '/' . $file . '.old.php')) {
$error = true;
}
}
}
if (!$error) {
$RCI->merge_config();
echo ". writing " . RCMAIL_CONFIG_DIR . "/config.inc.php...\n";
$written = $RCI->save_configfile($RCI->create_config());
}
// Success!
if ($written) {
echo "Done.\n";
echo "Your configuration files are now up-to-date!\n";
if ($messages['missing']) {
echo "But you still need to add the following missing options:\n";
foreach ($messages['missing'] as $msg)
echo "- '" . $msg['prop'] . ($msg['name'] ? "': " . $msg['name'] : "'") . "\n";
}
if ($RCI->legacy_config) {
foreach (array('main', 'db') as $file) {
@unlink(RCMAIL_CONFIG_DIR . '/' . $file . '.inc.php');
}
}
}
else {
echo "Failed to write config file(s)!\n";
echo "Grant write privileges to the current user or update the files manually according to the above messages.\n";
}
}
else {
echo "Please update your config files manually according to the above messages.\n";
}
}
// check dependencies based on the current configuration
if (is_array($messages['dependencies'])) {
echo "WARNING: Dependency check failed!\n";
echo "(Some of your configuration settings require other options to be configured or additional PHP modules to be installed)\n";
foreach ($messages['dependencies'] as $msg) {
echo "- " . $msg['prop'] . ': ' . $msg['explain'] . "\n";
}
echo "Please fix your config files and run this script again!\n";
echo "See ya.\n";
}
}
// check file type detection
if ($RCI->check_mime_detection()) {
echo "WARNING: File type detection doesn't work properly!\n";
echo "Please check the 'mime_magic' config option or the finfo functions of PHP and run this script again.\n";
}
if ($RCI->check_mime_extensions()) {
echo "WARNING: Mimetype to file extension mapping doesn't work properly!\n";
echo "Please check the 'mime_types' config option and run this script again.\n";
}
// check database schema
if ($RCI->config['db_dsnw']) {
echo "Executing database schema update.\n";
$success = rcmail_utils::db_update(INSTALL_PATH . 'SQL', 'roundcube', $opts['version'],
array('errors' => true));
}
// update composer dependencies
if (is_file(INSTALL_PATH . 'composer.json') && is_readable(INSTALL_PATH . 'composer.json-dist')) {
$composer_data = json_decode(file_get_contents(INSTALL_PATH . 'composer.json'), true);
$composer_template = json_decode(file_get_contents(INSTALL_PATH . 'composer.json-dist'), true);
$comsposer_json = null;
// update the require section with the new dependencies
if (is_array($composer_data['require']) && is_array($composer_template['require'])) {
$composer_data['require'] = array_merge($composer_data['require'], $composer_template['require']);
// remove obsolete packages
$old_packages = array(
'pear-pear.php.net/net_socket',
'pear-pear.php.net/auth_sasl',
'pear-pear.php.net/net_idna2',
'pear-pear.php.net/mail_mime',
'pear-pear.php.net/net_smtp',
'pear-pear.php.net/crypt_gpg',
'pear-pear.php.net/net_sieve',
'pear/mail_mime-decode',
'roundcube/net_sieve',
'endroid/qrcode',
);
foreach ($old_packages as $pkg) {
if (array_key_exists($pkg, $composer_data['require'])) {
unset($composer_data['require'][$pkg]);
}
}
}
// update the repositories section with the new dependencies
if (is_array($composer_template['repositories'])) {
if (!is_array($composer_data['repositories'])) {
$composer_data['repositories'] = array();
}
foreach ($composer_template['repositories'] as $repo) {
$rkey = $repo['type'] . preg_replace('/^https?:/', '', $repo['url']) . $repo['package']['name'];
$existing = false;
foreach ($composer_data['repositories'] as $k => $_repo) {
if ($rkey == $_repo['type'] . preg_replace('/^https?:/', '', $_repo['url']) . $_repo['package']['name']) {
// switch to https://
if (isset($_repo['url']) && strpos($_repo['url'], 'http://') === 0)
$composer_data['repositories'][$k]['url'] = 'https:' . substr($_repo['url'], 5);
$existing = true;
break;
}
// remove old repos
else if (strpos($_repo['url'], 'git://git.kolab.org') === 0) {
unset($composer_data['repositories'][$k]);
}
else if ($_repo['type'] == 'package' && $_repo['package']['name'] == 'Net_SMTP') {
unset($composer_data['repositories'][$k]);
}
}
if (!$existing) {
$composer_data['repositories'][] = $repo;
}
}
$composer_data['repositories'] = array_values($composer_data['repositories']);
}
// use the JSON encoder from the Composer package
if (is_file('composer.phar')) {
include 'phar://composer.phar/src/Composer/Json/JsonFile.php';
$comsposer_json = \Composer\Json\JsonFile::encode($composer_data);
}
// PHP 5.4's json_encode() does the job, too
else if (defined('JSON_PRETTY_PRINT')) {
$comsposer_json = json_encode($composer_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
else {
$success = false;
$comsposer_json = null;
}
// write updated composer.json back to disk
if ($comsposer_json && is_writeable(INSTALL_PATH . 'composer.json')) {
$success &= (bool)file_put_contents(INSTALL_PATH . 'composer.json', $comsposer_json);
}
else {
echo "WARNING: unable to update composer.json!\n";
echo "Please replace the 'require' section in your composer.json with the following:\n";
$require_json = '';
foreach ($composer_data['require'] as $pkg => $ver) {
$require_json .= sprintf(' "%s": "%s",'."\n", $pkg, $ver);
}
echo ' "require": {'."\n";
echo rtrim($require_json, ",\n");
echo "\n }\n\n";
}
echo "NOTE: Update dependencies by running `php composer.phar update --no-dev`\n";
}
// index contacts for fulltext searching
if ($opts['version'] && version_compare(version_parse($opts['version']), '0.6.0', '<')) {
rcmail_utils::indexcontacts();
}
if ($success) {
echo "This instance of Roundcube is up-to-date.\n";
echo "Have fun!\n";
}
}
else {
echo "This instance of Roundcube is not yet configured!\n";
echo "Open http://url-to-roundcube/installer/ in your browser and follow the instuctions.\n";
}
?>
diff --git a/bin/updatecss.sh b/bin/updatecss.sh
index 6ecfeac98..6ff6f4efa 100755
--- a/bin/updatecss.sh
+++ b/bin/updatecss.sh
@@ -1,122 +1,121 @@
#!/usr/bin/env php
<?php
/*
+-----------------------------------------------------------------------+
- | bin/updatecss.sh |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2010-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Update cache-baster marks for css background images |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/' );
require_once INSTALL_PATH . 'program/include/clisetup.php';
// get arguments
$opts = rcube_utils::get_opt(array(
'd' => 'dir',
));
if (empty($opts['dir'])) {
print "Skin directory not specified (--dir). Using skins/ and plugins/*/skins/.\n";
$dir = INSTALL_PATH . 'skins';
$dir_p = INSTALL_PATH . 'plugins';
$skins = glob("$dir/*", GLOB_ONLYDIR);
$skins_p = glob("$dir_p/*/skins/*", GLOB_ONLYDIR);
$dirs = array_merge($skins, $skins_p);
}
// Check if directory exists
else if (!file_exists($opts['dir'])) {
rcube::raise_error("Specified directory doesn't exist.", false, true);
}
else {
$dirs = array($opts['dir']);
}
foreach ($dirs as $dir) {
$img_dir = $dir . '/images';
if (!file_exists($img_dir)) {
continue;
}
$files = get_files($dir);
$images = get_images($img_dir);
$find = array();
$replace = array();
// build regexps array
foreach ($images as $path => $sum) {
$path_ex = str_replace('.', '\\.', $path);
$find[] = "#url\(['\"]?images/$path_ex(\?v=[a-f0-9-\.]+)?['\"]?\)#";
$replace[] = "url(images/$path?v=$sum)";
}
foreach ($files as $file) {
$file = $dir . '/' . $file;
print "File: $file\n";
$content = file_get_contents($file);
$content = preg_replace($find, $replace, $content, -1, $count);
if ($count) {
file_put_contents($file, $content);
}
}
}
function get_images($dir)
{
$images = array();
$dh = opendir($dir);
while ($file = readdir($dh)) {
if (preg_match('/^(.+)\.(gif|ico|png|jpg|jpeg)$/', $file, $m)) {
$filepath = "$dir/$file";
$images[$file] = substr(md5_file($filepath), 0, 4) . '.' . filesize($filepath);
print "Image: $filepath ({$images[$file]})\n";
}
else if ($file != '.' && $file != '..' && is_dir($dir . '/' . $file)) {
foreach (get_images($dir . '/' . $file) as $img => $sum) {
$images[$file . '/' . $img] = $sum;
}
}
}
closedir($dh);
return $images;
}
function get_files($dir)
{
$files = array();
$dh = opendir($dir);
while ($file = readdir($dh)) {
if (preg_match('/^(.+)\.(css|html)$/', $file, $m)) {
$files[] = $file;
}
else if ($file != '.' && $file != '..' && is_dir($dir . '/' . $file)) {
foreach (get_files($dir . '/' . $file) as $f) {
$files[] = $file . '/' . $f;
}
}
}
closedir($dh);
return $files;
}
?>
diff --git a/bin/updatedb.sh b/bin/updatedb.sh
index 7e4f0f526..211f2415e 100755
--- a/bin/updatedb.sh
+++ b/bin/updatedb.sh
@@ -1,42 +1,41 @@
#!/usr/bin/env php
<?php
/*
+-----------------------------------------------------------------------+
- | bin/updatedb.sh |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2010-2012, The Roundcube Dev Team |
- | Copyright (C) 2010-2012, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Update database schema |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/' );
require_once INSTALL_PATH . 'program/include/clisetup.php';
// get arguments
$opts = rcube_utils::get_opt(array(
'v' => 'version',
'd' => 'dir',
'p' => 'package',
));
if (empty($opts['dir'])) {
rcube::raise_error("Database schema directory not specified (--dir).", false, true);
}
if (empty($opts['package'])) {
rcube::raise_error("Database schema package name not specified (--package).", false, true);
}
rcmail_utils::db_update($opts['dir'], $opts['package'], $opts['version'], array('errors' => true));
?>
diff --git a/config/config.inc.php.sample b/config/config.inc.php.sample
index fae3adfc9..248570467 100644
--- a/config/config.inc.php.sample
+++ b/config/config.inc.php.sample
@@ -1,86 +1,86 @@
<?php
/*
+-----------------------------------------------------------------------+
| Local configuration for the Roundcube Webmail installation. |
| |
| This is a sample configuration file only containing the minimum |
| setup required for a functional installation. Copy more options |
| from defaults.inc.php to this file to override the defaults. |
| |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
+-----------------------------------------------------------------------+
*/
$config = array();
// Database connection string (DSN) for read+write operations
// Format (compatible with PEAR MDB2): db_provider://user:password@host/database
// Currently supported db_providers: mysql, pgsql, sqlite, mssql, sqlsrv, oracle
// For examples see http://pear.php.net/manual/en/package.database.mdb2.intro-dsn.php
// NOTE: for SQLite use absolute path (Linux): 'sqlite:////full/path/to/sqlite.db?mode=0646'
// or (Windows): 'sqlite:///C:/full/path/to/sqlite.db'
$config['db_dsnw'] = 'mysql://roundcube:pass@localhost/roundcubemail';
// The IMAP host chosen to perform the log-in.
// Leave blank to show a textbox at login, give a list of hosts
// to display a pulldown menu or set one host as string.
// To use SSL/TLS connection, enter hostname with prefix ssl:// or tls://
// Supported replacement variables:
// %n - hostname ($_SERVER['SERVER_NAME'])
// %t - hostname without the first part
// %d - domain (http hostname $_SERVER['HTTP_HOST'] without the first part)
// %s - domain name after the '@' from e-mail address provided at login screen
// For example %n = mail.domain.tld, %t = domain.tld
$config['default_host'] = 'localhost';
// SMTP server host (for sending mails).
// Enter hostname with prefix tls:// to use STARTTLS, or use
// prefix ssl:// to use the deprecated SSL over SMTP (aka SMTPS)
// Supported replacement variables:
// %h - user's IMAP hostname
// %n - hostname ($_SERVER['SERVER_NAME'])
// %t - hostname without the first part
// %d - domain (http hostname $_SERVER['HTTP_HOST'] without the first part)
// %z - IMAP domain (IMAP hostname without the first part)
// For example %n = mail.domain.tld, %t = domain.tld
$config['smtp_server'] = 'localhost';
// SMTP port (default is 25; use 587 for STARTTLS or 465 for the
// deprecated SSL over SMTP (aka SMTPS))
$config['smtp_port'] = 587;
// SMTP username (if required) if you use %u as the username Roundcube
// will use the current username for login
$config['smtp_user'] = '%u';
// SMTP password (if required) if you use %p as the password Roundcube
// will use the current user's password for login
$config['smtp_pass'] = '%p';
// provide an URL where a user can get support for this Roundcube installation
// PLEASE DO NOT LINK TO THE ROUNDCUBE.NET WEBSITE HERE!
$config['support_url'] = '';
// Name your service. This is displayed on the login screen and in the window title
$config['product_name'] = 'Roundcube Webmail';
// this key is used to encrypt the users imap password which is stored
// in the session record (and the client cookie if remember password is enabled).
// please provide a string of exactly 24 chars.
// YOUR KEY MUST BE DIFFERENT THAN THE SAMPLE VALUE FOR SECURITY REASONS
$config['des_key'] = 'rcmail-!24ByteDESkey*Str';
// List of active plugins (in plugins/ directory)
$config['plugins'] = array(
'archive',
'zipdownload',
);
// skin name: folder from skins/
$config['skin'] = 'larry';
diff --git a/config/defaults.inc.php b/config/defaults.inc.php
index b60a0d04d..aa2138d56 100644
--- a/config/defaults.inc.php
+++ b/config/defaults.inc.php
@@ -1,1260 +1,1260 @@
<?php
// ---------------------------------------------------------------------
// WARNING: Do not edit this file! Copy configuration to config.inc.php.
// ---------------------------------------------------------------------
/*
+-----------------------------------------------------------------------+
| Default settings for all configuration options |
| |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2018, The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
+-----------------------------------------------------------------------+
*/
$config = array();
// ----------------------------------
// SQL DATABASE
// ----------------------------------
// Database connection string (DSN) for read+write operations
// Format (compatible with PEAR MDB2): db_provider://user:password@host/database
// Currently supported db_providers: mysql, pgsql, sqlite, mssql, sqlsrv, oracle
// For examples see http://pear.php.net/manual/en/package.database.mdb2.intro-dsn.php
// NOTE: for SQLite use absolute path (Linux): 'sqlite:////full/path/to/sqlite.db?mode=0646'
// or (Windows): 'sqlite:///C:/full/path/to/sqlite.db'
$config['db_dsnw'] = 'mysql://roundcube:@localhost/roundcubemail';
// Database DSN for read-only operations (if empty write database will be used)
// useful for database replication
$config['db_dsnr'] = '';
// Disable the use of already established dsnw connections for subsequent reads
$config['db_dsnw_noread'] = false;
// use persistent db-connections
// beware this will not "always" work as expected
// see: http://www.php.net/manual/en/features.persistent-connections.php
$config['db_persistent'] = false;
// you can define specific table (and sequence) names prefix
$config['db_prefix'] = '';
// Mapping of table names and connections to use for ALL operations.
// This can be used in a setup with replicated databases and a DB master
// where read/write access to cache tables should not go to master.
$config['db_table_dsn'] = array(
// 'cache' => 'r',
// 'cache_index' => 'r',
// 'cache_thread' => 'r',
// 'cache_messages' => 'r',
);
// It is possible to specify database variable values e.g. some limits here.
// Use them if your server is not MySQL or for better performance.
// For example Roundcube uses max_allowed_packet value (in bytes)
// which limits query size for database cache operations.
$config['db_max_allowed_packet'] = null;
// ----------------------------------
// LOGGING/DEBUGGING
// ----------------------------------
// log driver: 'syslog', 'stdout' or 'file'.
$config['log_driver'] = 'file';
// date format for log entries
// (read http://php.net/manual/en/function.date.php for all format characters)
$config['log_date_format'] = 'd-M-Y H:i:s O';
// length of the session ID to prepend each log line with
// set to 0 to avoid session IDs being logged.
$config['log_session_id'] = 8;
// Default extension used for log file name
$config['log_file_ext'] = '.log';
// Syslog ident string to use, if using the 'syslog' log driver.
$config['syslog_id'] = 'roundcube';
// Syslog facility to use, if using the 'syslog' log driver.
// For possible values see installer or http://php.net/manual/en/function.openlog.php
$config['syslog_facility'] = LOG_USER;
// Activate this option if logs should be written to per-user directories.
// Data will only be logged if a directory <log_dir>/<username>/ exists and is writable.
$config['per_user_logging'] = false;
// Log sent messages to <log_dir>/sendmail or to syslog
$config['smtp_log'] = true;
// Log successful/failed logins to <log_dir>/userlogins or to syslog
$config['log_logins'] = false;
// Log session authentication errors to <log_dir>/session or to syslog
$config['log_session'] = false;
// Log SQL queries to <log_dir>/sql or to syslog
$config['sql_debug'] = false;
// Log IMAP conversation to <log_dir>/imap or to syslog
$config['imap_debug'] = false;
// Log LDAP conversation to <log_dir>/ldap or to syslog
$config['ldap_debug'] = false;
// Log SMTP conversation to <log_dir>/smtp or to syslog
$config['smtp_debug'] = false;
// Log Memcache conversation to <log_dir>/memcache or to syslog
$config['memcache_debug'] = false;
// Log APC conversation to <log_dir>/apc or to syslog
$config['apc_debug'] = false;
// Log Redis conversation to <log_dir>/redis or to syslog
$config['redis_debug'] = false;
// ----------------------------------
// IMAP
// ----------------------------------
// The IMAP host chosen to perform the log-in.
// Leave blank to show a textbox at login, give a list of hosts
// to display a pulldown menu or set one host as string.
// To use SSL/TLS connection, enter hostname with prefix ssl:// or tls://
// Supported replacement variables:
// %n - hostname ($_SERVER['SERVER_NAME'])
// %t - hostname without the first part
// %d - domain (http hostname $_SERVER['HTTP_HOST'] without the first part)
// %s - domain name after the '@' from e-mail address provided at login screen
// For example %n = mail.domain.tld, %t = domain.tld
// WARNING: After hostname change update of mail_host column in users table is
// required to match old user data records with the new host.
$config['default_host'] = 'localhost';
// TCP port used for IMAP connections
$config['default_port'] = 143;
// IMAP authentication method (DIGEST-MD5, CRAM-MD5, LOGIN, PLAIN or null).
// Use 'IMAP' to authenticate with IMAP LOGIN command.
// By default the most secure method (from supported) will be selected.
$config['imap_auth_type'] = null;
// IMAP socket context options
// See http://php.net/manual/en/context.ssl.php
// The example below enables server certificate validation
//$config['imap_conn_options'] = array(
// 'ssl' => array(
// 'verify_peer' => true,
// 'verify_depth' => 3,
// 'cafile' => '/etc/openssl/certs/ca.crt',
// ),
// );
// Note: These can be also specified as an array of options indexed by hostname
$config['imap_conn_options'] = null;
// IMAP connection timeout, in seconds. Default: 0 (use default_socket_timeout)
$config['imap_timeout'] = 0;
// Optional IMAP authentication identifier to be used as authorization proxy
$config['imap_auth_cid'] = null;
// Optional IMAP authentication password to be used for imap_auth_cid
$config['imap_auth_pw'] = null;
// If you know your imap's folder delimiter, you can specify it here.
// Otherwise it will be determined automatically
$config['imap_delimiter'] = null;
// If you know your imap's folder vendor, you can specify it here.
// Otherwise it will be determined automatically. Use lower-case
// identifiers, e.g. 'dovecot', 'cyrus', 'gmail', 'hmail', 'uw-imap'.
$config['imap_vendor'] = null;
// If IMAP server doesn't support NAMESPACE extension, but you're
// using shared folders or personal root folder is non-empty, you'll need to
// set these options. All can be strings or arrays of strings.
// Note: Folders need to be ended with directory separator, e.g. "INBOX."
// (special directory "~" is an exception to this rule)
// Note: These can be used also to overwrite server's namespaces
// Note: Set these to FALSE to disable access to specified namespace
$config['imap_ns_personal'] = null;
$config['imap_ns_other'] = null;
$config['imap_ns_shared'] = null;
// By default IMAP capabilities are readed after connection to IMAP server
// In some cases, e.g. when using IMAP proxy, there's a need to refresh the list
// after login. Set to True if you've got this case.
$config['imap_force_caps'] = false;
// By default list of subscribed folders is determined using LIST-EXTENDED
// extension if available. Some servers (dovecot 1.x) returns wrong results
// for shared namespaces in this case. https://github.com/roundcube/roundcubemail/issues/2474
// Enable this option to force LSUB command usage instead.
// Deprecated: Use imap_disabled_caps = array('LIST-EXTENDED')
$config['imap_force_lsub'] = false;
// Some server configurations (e.g. Courier) doesn't list folders in all namespaces
// Enable this option to force listing of folders in all namespaces
$config['imap_force_ns'] = false;
// Some servers return hidden folders (name starting witha dot)
// from user home directory. IMAP RFC does not forbid that.
// Enable this option to hide them and disable possibility to create such.
$config['imap_skip_hidden_folders'] = false;
// Some servers do not support folders with both folders and messages inside
// If your server supports that use true, if it does not, use false.
// By default it will be determined automatically (once per user session).
$config['imap_dual_use_folders'] = null;
// List of disabled imap extensions.
// Use if your IMAP server has broken implementation of some feature
// and you can't remove it from CAPABILITY string on server-side.
// For example UW-IMAP server has broken ESEARCH.
// Note: Because the list is cached, re-login is required after change.
$config['imap_disabled_caps'] = array();
// Log IMAP session identifiers after each IMAP login.
// This is used to relate IMAP session with Roundcube user sessions
$config['imap_log_session'] = false;
// Type of IMAP indexes cache. Supported values: 'db', 'apc' and 'memcache'.
$config['imap_cache'] = null;
// Enables messages cache. Only 'db' cache is supported.
// This requires an IMAP server that supports QRESYNC and CONDSTORE
// extensions (RFC7162). See synchronize() in program/lib/Roundcube/rcube_imap_cache.php
// for further info, or if you experience syncing problems.
$config['messages_cache'] = false;
// Lifetime of IMAP indexes cache. Possible units: s, m, h, d, w
$config['imap_cache_ttl'] = '10d';
// Lifetime of messages cache. Possible units: s, m, h, d, w
$config['messages_cache_ttl'] = '10d';
// Maximum cached message size in kilobytes.
// Note: On MySQL this should be less than (max_allowed_packet - 30%)
$config['messages_cache_threshold'] = 50;
// ----------------------------------
// SMTP
// ----------------------------------
// SMTP server host (for sending mails).
// Enter hostname with prefix tls:// to use STARTTLS, or use
// prefix ssl:// to use the deprecated SSL over SMTP (aka SMTPS)
// Supported replacement variables:
// %h - user's IMAP hostname
// %n - hostname ($_SERVER['SERVER_NAME'])
// %t - hostname without the first part
// %d - domain (http hostname $_SERVER['HTTP_HOST'] without the first part)
// %z - IMAP domain (IMAP hostname without the first part)
// For example %n = mail.domain.tld, %t = domain.tld
$config['smtp_server'] = 'localhost';
// SMTP port (default is 587)
$config['smtp_port'] = 587;
// SMTP username (if required) if you use %u as the username Roundcube
// will use the current username for login
$config['smtp_user'] = '%u';
// SMTP password (if required) if you use %p as the password Roundcube
// will use the current user's password for login
$config['smtp_pass'] = '%p';
// SMTP AUTH type (DIGEST-MD5, CRAM-MD5, LOGIN, PLAIN or empty to use
// best server supported one)
$config['smtp_auth_type'] = null;
// Optional SMTP authentication identifier to be used as authorization proxy
$config['smtp_auth_cid'] = null;
// Optional SMTP authentication password to be used for smtp_auth_cid
$config['smtp_auth_pw'] = null;
// SMTP HELO host
// Hostname to give to the remote server for SMTP 'HELO' or 'EHLO' messages
// Leave this blank and you will get the server variable 'server_name' or
// localhost if that isn't defined.
$config['smtp_helo_host'] = '';
// SMTP connection timeout, in seconds. Default: 0 (use default_socket_timeout)
// Note: There's a known issue where using ssl connection with
// timeout > 0 causes connection errors (https://bugs.php.net/bug.php?id=54511)
$config['smtp_timeout'] = 0;
// SMTP socket context options
// See http://php.net/manual/en/context.ssl.php
// The example below enables server certificate validation, and
// requires 'smtp_timeout' to be non zero.
// $config['smtp_conn_options'] = array(
// 'ssl' => array(
// 'verify_peer' => true,
// 'verify_depth' => 3,
// 'cafile' => '/etc/openssl/certs/ca.crt',
// ),
// );
// Note: These can be also specified as an array of options indexed by hostname
$config['smtp_conn_options'] = null;
// ----------------------------------
// LDAP
// ----------------------------------
// Type of LDAP cache. Supported values: 'db', 'apc' and 'memcache'.
$config['ldap_cache'] = 'db';
// Lifetime of LDAP cache. Possible units: s, m, h, d, w
$config['ldap_cache_ttl'] = '10m';
// ----------------------------------
// CACHE(S)
// ----------------------------------
// Use these hosts for accessing memcached
// Define any number of hosts in the form of hostname:port or unix:///path/to/socket.file
$config['memcache_hosts'] = null; // e.g. array( 'localhost:11211', '192.168.1.12:11211', 'unix:///var/tmp/memcached.sock' );
// Controls the use of a persistent connections to memcache servers
// See http://php.net/manual/en/memcache.addserver.php
$config['memcache_pconnect'] = true;
// Value in seconds which will be used for connecting to the daemon
// See http://php.net/manual/en/memcache.addserver.php
$config['memcache_timeout'] = 1;
// Controls how often a failed server will be retried (value in seconds).
// Setting this parameter to -1 disables automatic retry.
// See http://php.net/manual/en/memcache.addserver.php
$config['memcache_retry_interval'] = 15;
// use these hosts for accessing Redis.
// Currently only one host is supported. cluster support may come in a future release.
// You can pass 4 fields, host, port, database and password.
// Unset fields will be set to the default values host=127.0.0.1, port=6379, database=0, password= (empty)
$config['redis_hosts'] = null; // e.g. array( 'localhost:6379' ); array( '192.168.1.1:6379:1:secret' );
// Maximum size of an object in memcache (in bytes). Default: 2MB
$config['memcache_max_allowed_packet'] = '2M';
// Maximum size of an object in APC cache (in bytes). Default: 2MB
$config['apc_max_allowed_packet'] = '2M';
// Maximum size of an object in Redis cache (in bytes). Default: 2MB
$config['redis_max_allowed_packet'] = '2M';
// ----------------------------------
// SYSTEM
// ----------------------------------
// THIS OPTION WILL ALLOW THE INSTALLER TO RUN AND CAN EXPOSE SENSITIVE CONFIG DATA.
// ONLY ENABLE IT IF YOU'RE REALLY SURE WHAT YOU'RE DOING!
$config['enable_installer'] = false;
// don't allow these settings to be overridden by the user
$config['dont_override'] = array();
// List of disabled UI elements/actions
$config['disabled_actions'] = array();
// define which settings should be listed under the 'advanced' block
// which is hidden by default
$config['advanced_prefs'] = array();
// provide an URL where a user can get support for this Roundcube installation
// PLEASE DO NOT LINK TO THE ROUNDCUBE.NET WEBSITE HERE!
$config['support_url'] = '';
// replace Roundcube logo with this image
// specify an URL relative to the document root of this Roundcube installation
// an array can be used to specify different logos for specific template files
// '*' for default logo
// ':favicon' for favicon
// ':print' for logo on all print templates (e.g. messageprint, contactprint)
// ':small' for small screen logo in Elastic
// different logos can be specified for different skins by prefixing the skin name to the array key
// config applied in order: <skin>:<template>, <skin>:*, <template>, *
// for example array("*" => "/images/roundcube_logo.png", "messageprint" => "/images/roundcube_logo_print.png", "elastic:*" => "/images/logo.png")
$config['skin_logo'] = null;
// automatically create a new Roundcube user when log-in the first time.
// a new user will be created once the IMAP login succeeds.
// set to false if only registered users can use this service
$config['auto_create_user'] = true;
// Enables possibility to log in using email address from user identities
$config['user_aliases'] = false;
// use this folder to store log files
// must be writeable for the user who runs PHP process (Apache user if mod_php is being used)
// This is used by the 'file' log driver.
$config['log_dir'] = RCUBE_INSTALL_PATH . 'logs/';
// use this folder to store temp files
// must be writeable for the user who runs PHP process (Apache user if mod_php is being used)
$config['temp_dir'] = RCUBE_INSTALL_PATH . 'temp/';
// expire files in temp_dir after 48 hours
// possible units: s, m, h, d, w
$config['temp_dir_ttl'] = '48h';
// Enforce connections over https
// With this option enabled, all non-secure connections will be redirected.
// It can be also a port number, hostname or hostname:port if they are
// different than default HTTP_HOST:443
$config['force_https'] = false;
// tell PHP that it should work as under secure connection
// even if it doesn't recognize it as secure ($_SERVER['HTTPS'] is not set)
// e.g. when you're running Roundcube behind a https proxy
// this option is mutually exclusive to 'force_https' and only either one of them should be set to true.
$config['use_https'] = false;
// Allow browser-autocompletion on login form.
// 0 - disabled, 1 - username and host only, 2 - username, host, password
$config['login_autocomplete'] = 0;
// Forces conversion of logins to lower case.
// 0 - disabled, 1 - only domain part, 2 - domain and local part.
// If users authentication is case-insensitive this must be enabled.
// Note: After enabling it all user records need to be updated, e.g. with query:
// UPDATE users SET username = LOWER(username);
$config['login_lc'] = 2;
// Maximum length (in bytes) of logon username and password.
$config['login_username_maxlen'] = 1024;
$config['login_password_maxlen'] = 1024;
// Logon username filter. Regular expression for use with preg_match().
// Example: '/^[a-z0-9_@.-]+$/'
$config['login_username_filter'] = null;
// Brute-force attacks prevention.
// The value specifies maximum number of failed logon attempts per minute.
$config['login_rate_limit'] = 3;
// Includes should be interpreted as PHP files
$config['skin_include_php'] = false;
// display product name and software version on login screen
// 0 - hide product name and version number, 1 - show product name only, 2 - show product name and version number
$config['display_product_info'] = 1;
// Session lifetime in minutes
$config['session_lifetime'] = 10;
// Session domain: .example.org
$config['session_domain'] = '';
// Session name. Default: 'roundcube_sessid'
$config['session_name'] = null;
// Session authentication cookie name. Default: 'roundcube_sessauth'
$config['session_auth_name'] = null;
// Session path. Defaults to PHP session.cookie_path setting.
$config['session_path'] = null;
// Backend to use for session storage. Can either be 'db' (default), 'redis', 'memcache', or 'php'
//
// If set to 'memcache', a list of servers need to be specified in 'memcache_hosts'
// Make sure the Memcache extension (http://pecl.php.net/package/memcache) version >= 2.0.0 is installed
//
// If set to 'redis', a server needs to be specified in 'redis_hosts'
// Make sure the Redis extension (http://pecl.php.net/package/redis) version >= 2.0.0 is installed
//
// Setting this value to 'php' will use the default session save handler configured in PHP
$config['session_storage'] = 'db';
// List of trusted proxies
// X_FORWARDED_* and X_REAL_IP headers are only accepted from these IPs
$config['proxy_whitelist'] = array();
// List of trusted host names
// Attackers can modify Host header of the HTTP request causing $_SERVER['SERVER_NAME']
// or $_SERVER['HTTP_HOST'] variables pointing to a different host, that could be used
// to collect user names and passwords. Some server configurations prevent that, but not all.
// An empty list accepts any host name. The list can contain host names
// or PCRE patterns (without // delimiters, that will be added automatically).
$config['trusted_host_patterns'] = array();
// check client IP in session authorization
$config['ip_check'] = false;
// X-Frame-Options HTTP header value sent to prevent from Clickjacking.
// Possible values: sameorigin|deny|allow-from <uri>.
// Set to false in order to disable sending the header.
$config['x_frame_options'] = 'sameorigin';
// This key is used for encrypting purposes, like storing of imap password
// in the session. For historical reasons it's called DES_key, but it's used
// with any configured cipher_method (see below).
$config['des_key'] = 'rcmail-!24ByteDESkey*Str';
// Encryption algorithm. You can use any method supported by openssl.
// Default is set for backward compatibility to DES-EDE3-CBC,
// but you can choose e.g. AES-256-CBC which we consider a better choice.
$config['cipher_method'] = 'DES-EDE3-CBC';
// Automatically add this domain to user names for login
// Only for IMAP servers that require full e-mail addresses for login
// Specify an array with 'host' => 'domain' values to support multiple hosts
// Supported replacement variables:
// %h - user's IMAP hostname
// %n - hostname ($_SERVER['SERVER_NAME'])
// %t - hostname without the first part
// %d - domain (http hostname $_SERVER['HTTP_HOST'] without the first part)
// %z - IMAP domain (IMAP hostname without the first part)
// For example %n = mail.domain.tld, %t = domain.tld
$config['username_domain'] = '';
// Force domain configured in username_domain to be used for login.
// Any domain in username will be replaced by username_domain.
$config['username_domain_forced'] = false;
// This domain will be used to form e-mail addresses of new users
// Specify an array with 'host' => 'domain' values to support multiple hosts
// Supported replacement variables:
// %h - user's IMAP hostname
// %n - http hostname ($_SERVER['SERVER_NAME'])
// %d - domain (http hostname without the first part)
// %z - IMAP domain (IMAP hostname without the first part)
// For example %n = mail.domain.tld, %t = domain.tld
$config['mail_domain'] = '';
// Password character set, to change the password for user
// authentication or for password change operations
$config['password_charset'] = 'UTF-8';
// How many seconds must pass between emails sent by a user
$config['sendmail_delay'] = 0;
// Message size limit. Note that SMTP server(s) may use a different value.
// This limit is verified when user attaches files to a composed message.
// Size in bytes (possible unit suffix: K, M, G)
$config['max_message_size'] = '100M';
// Maximum number of recipients per message (including To, Cc, Bcc).
// Default: 0 (no limit)
$config['max_recipients'] = 0;
// Maximum number of recipients per message exluding Bcc header.
// This is a soft limit, which means we only display a warning to the user.
// Default: 5
$config['max_disclosed_recipients'] = 5;
// Maximum allowed number of members of an address group. Default: 0 (no limit)
// If 'max_recipients' is set this value should be less or equal
$config['max_group_members'] = 0;
// Name your service. This is displayed on the login screen and in the window title
$config['product_name'] = 'Roundcube Webmail';
// Add this user-agent to message headers when sending
$config['useragent'] = 'Roundcube Webmail/'.RCUBE_VERSION;
// try to load host-specific configuration
// see https://github.com/roundcube/roundcubemail/wiki/Configuration:-Multi-Domain-Setup
// for more details
$config['include_host_config'] = false;
// path to a text file which will be added to each sent message
// paths are relative to the Roundcube root folder
$config['generic_message_footer'] = '';
// path to a text file which will be added to each sent HTML message
// paths are relative to the Roundcube root folder
$config['generic_message_footer_html'] = '';
// add a received header to outgoing mails containing the creators IP and hostname
$config['http_received_header'] = false;
// Whether or not to encrypt the IP address and the host name
// these could, in some circles, be considered as sensitive information;
// however, for the administrator, these could be invaluable help
// when tracking down issues.
$config['http_received_header_encrypt'] = false;
// number of chars allowed for line when wrapping text.
// text wrapping is done when composing/sending messages
$config['line_length'] = 72;
// send plaintext messages as format=flowed
$config['send_format_flowed'] = true;
// According to RFC2298, return receipt envelope sender address must be empty.
// If this option is true, Roundcube will use user's identity as envelope sender for MDN responses.
$config['mdn_use_from'] = false;
// Set identities access level:
// 0 - many identities with possibility to edit all params
// 1 - many identities with possibility to edit all params but not email address
// 2 - one identity with possibility to edit all params
// 3 - one identity with possibility to edit all params but not email address
// 4 - one identity with possibility to edit only signature
$config['identities_level'] = 0;
// Maximum size of uploaded image in kilobytes
// Images (in html signatures) are stored in database as data URIs
$config['identity_image_size'] = 64;
// Mimetypes supported by the browser.
// attachments of these types will open in a preview window
// either a comma-separated list or an array: 'text/plain,text/html,text/xml,image/jpeg,image/gif,image/png,application/pdf'
$config['client_mimetypes'] = null; # null == default
// Path to a local mime magic database file for PHPs finfo extension.
// Set to null if the default path should be used.
$config['mime_magic'] = null;
// Absolute path to a local mime.types mapping table file.
// This is used to derive mime-types from the filename extension or vice versa.
// Such a file is usually part of the apache webserver. If you don't find a file named mime.types on your system,
// download it from http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types
$config['mime_types'] = null;
// path to imagemagick identify binary (if not set we'll use Imagick or GD extensions)
$config['im_identify_path'] = null;
// path to imagemagick convert binary (if not set we'll use Imagick or GD extensions)
$config['im_convert_path'] = null;
// Size of thumbnails from image attachments displayed below the message content.
// Note: whether images are displayed at all depends on the 'inline_images' option.
// Set to 0 to display images in full size.
$config['image_thumbnail_size'] = 240;
// maximum size of uploaded contact photos in pixel
$config['contact_photo_size'] = 160;
// Enable DNS checking for e-mail address validation
$config['email_dns_check'] = false;
// Disables saving sent messages in Sent folder (like gmail) (Default: false)
// Note: useful when SMTP server stores sent mail in user mailbox
$config['no_save_sent_messages'] = false;
// Improve system security by using special URL with security token.
// This can be set to a number defining token length. Default: 16.
// Warning: This requires http server configuration. Sample:
// RewriteRule ^/roundcubemail/[a-zA-Z0-9]{16}/(.*) /roundcubemail/$1 [PT]
// Alias /roundcubemail /var/www/roundcubemail/
// Note: Use assets_path to not prevent the browser from caching assets
$config['use_secure_urls'] = false;
// Allows to define separate server/path for image/js/css files
// Warning: If the domain is different cross-domain access to some
// resources need to be allowed
// Sample:
// <FilesMatch ".(eot|ttf|woff)">
// Header set Access-Control-Allow-Origin "*"
// </FilesMatch>
$config['assets_path'] = '';
// While assets_path is for the browser, assets_dir informs
// PHP code about the location of asset files in filesystem
$config['assets_dir'] = '';
// ----------------------------------
// PLUGINS
// ----------------------------------
// List of active plugins (in plugins/ directory)
$config['plugins'] = array();
// ----------------------------------
// USER INTERFACE
// ----------------------------------
// default messages sort column. Use empty value for default server's sorting,
// or 'arrival', 'date', 'subject', 'from', 'to', 'fromto', 'size', 'cc'
$config['message_sort_col'] = '';
// default messages sort order
$config['message_sort_order'] = 'DESC';
// These cols are shown in the message list. Available cols are:
// subject, from, to, fromto, cc, replyto, date, size, status, flag, attachment, priority
$config['list_cols'] = array('subject', 'status', 'fromto', 'date', 'size', 'flag', 'attachment');
// the default locale setting (leave empty for auto-detection)
// RFC1766 formatted language name like en_US, de_DE, de_CH, fr_FR, pt_BR
$config['language'] = null;
// use this format for date display (date or strftime format)
$config['date_format'] = 'Y-m-d';
// give this choice of date formats to the user to select from
// Note: do not use ambiguous formats like m/d/Y
$config['date_formats'] = array('Y-m-d', 'Y/m/d', 'Y.m.d', 'd-m-Y', 'd/m/Y', 'd.m.Y', 'j.n.Y');
// use this format for time display (date or strftime format)
$config['time_format'] = 'H:i';
// give this choice of time formats to the user to select from
$config['time_formats'] = array('G:i', 'H:i', 'g:i a', 'h:i A');
// use this format for short date display (derived from date_format and time_format)
$config['date_short'] = 'D H:i';
// use this format for detailed date/time formatting (derived from date_format and time_format)
$config['date_long'] = 'Y-m-d H:i';
// store draft message is this mailbox
// leave blank if draft messages should not be stored
// NOTE: Use folder names with namespace prefix (INBOX. on Courier-IMAP)
$config['drafts_mbox'] = 'Drafts';
// store spam messages in this mailbox
// NOTE: Use folder names with namespace prefix (INBOX. on Courier-IMAP)
$config['junk_mbox'] = 'Junk';
// store sent message is this mailbox
// leave blank if sent messages should not be stored
// NOTE: Use folder names with namespace prefix (INBOX. on Courier-IMAP)
$config['sent_mbox'] = 'Sent';
// move messages to this folder when deleting them
// leave blank if they should be deleted directly
// NOTE: Use folder names with namespace prefix (INBOX. on Courier-IMAP)
$config['trash_mbox'] = 'Trash';
// automatically create the above listed default folders on user login
$config['create_default_folders'] = false;
// protect the default folders from renames, deletes, and subscription changes
$config['protect_default_folders'] = true;
// Disable localization of the default folder names listed above
$config['show_real_foldernames'] = false;
// if in your system 0 quota means no limit set this option to true
$config['quota_zero_as_unlimited'] = false;
// Make use of the built-in spell checker. It is based on GoogieSpell.
$config['enable_spellcheck'] = true;
// Enables spellchecker exceptions dictionary.
// Setting it to 'shared' will make the dictionary shared by all users.
$config['spellcheck_dictionary'] = false;
// Set the spell checking engine. Possible values:
// - 'googie' - the default (also used for connecting to Nox Spell Server, see 'spellcheck_uri' setting)
// - 'pspell' - requires the PHP Pspell module and aspell installed
// - 'enchant' - requires the PHP Enchant module
// - 'atd' - install your own After the Deadline server or check with the people at http://www.afterthedeadline.com before using their API
// Since Google shut down their public spell checking service, the default settings
// connect to http://spell.roundcube.net which is a hosted service provided by Roundcube.
// You can connect to any other googie-compliant service by setting 'spellcheck_uri' accordingly.
$config['spellcheck_engine'] = 'googie';
// For locally installed Nox Spell Server or After the Deadline services,
// please specify the URI to call it.
// Get Nox Spell Server from http://orangoo.com/labs/?page_id=72 or
// the After the Deadline package from http://www.afterthedeadline.com.
// Leave empty to use the public API of service.afterthedeadline.com
$config['spellcheck_uri'] = '';
// These languages can be selected for spell checking.
// Configure as a PHP style hash array: array('en'=>'English', 'de'=>'Deutsch');
// Leave empty for default set of available language.
$config['spellcheck_languages'] = NULL;
// Makes that words with all letters capitalized will be ignored (e.g. GOOGLE)
$config['spellcheck_ignore_caps'] = false;
// Makes that words with numbers will be ignored (e.g. g00gle)
$config['spellcheck_ignore_nums'] = false;
// Makes that words with symbols will be ignored (e.g. g@@gle)
$config['spellcheck_ignore_syms'] = false;
// Number of lines at the end of a message considered to contain the signature.
// Increase this value if signatures are not properly detected and colored
$config['sig_max_lines'] = 15;
// don't let users set pagesize to more than this value if set
$config['max_pagesize'] = 200;
// Minimal value of user's 'refresh_interval' setting (in seconds)
$config['min_refresh_interval'] = 60;
// Specifies for how many seconds the Undo button will be available
// after object delete action. Currently used with supporting address book sources.
// Setting it to 0, disables the feature.
$config['undo_timeout'] = 0;
// A static list of canned responses which are immutable for the user
$config['compose_responses_static'] = array(
// array('name' => 'Canned Response 1', 'text' => 'Static Response One'),
// array('name' => 'Canned Response 2', 'text' => 'Static Response Two'),
);
// ----------------------------------
// ADDRESSBOOK SETTINGS
// ----------------------------------
// This indicates which type of address book to use. Possible choises:
// 'sql' (default), 'ldap' and ''.
// If set to 'ldap' then it will look at using the first writable LDAP
// address book as the primary address book and it will not display the
// SQL address book in the 'Address Book' view.
// If set to '' then no address book will be displayed or only the
// addressbook which is created by a plugin (like CardDAV).
$config['address_book_type'] = 'sql';
// In order to enable public ldap search, configure an array like the Verisign
// example further below. if you would like to test, simply uncomment the example.
// Array key must contain only safe characters, ie. a-zA-Z0-9_
$config['ldap_public'] = array();
// If you are going to use LDAP for individual address books, you will need to
// set 'user_specific' to true and use the variables to generate the appropriate DNs to access it.
//
// The recommended directory structure for LDAP is to store all the address book entries
// under the users main entry, e.g.:
//
// o=root
// ou=people
// uid=user@domain
// mail=contact@contactdomain
//
// So the base_dn would be uid=%fu,ou=people,o=root
// The bind_dn would be the same as based_dn or some super user login.
/*
* example config for Verisign directory
*
$config['ldap_public']['Verisign'] = array(
'name' => 'Verisign.com',
// Replacement variables supported in host names:
// %h - user's IMAP hostname
// %n - hostname ($_SERVER['SERVER_NAME'])
// %t - hostname without the first part
// %d - domain (http hostname $_SERVER['HTTP_HOST'] without the first part)
// %z - IMAP domain (IMAP hostname without the first part)
// For example %n = mail.domain.tld, %t = domain.tld
'hosts' => array('directory.verisign.com'),
'port' => 389,
'use_tls' => false,
'ldap_version' => 3, // using LDAPv3
'network_timeout' => 10, // The timeout (in seconds) for connect + bind arrempts. This is only supported in PHP >= 5.3.0 with OpenLDAP 2.x
'user_specific' => false, // If true the base_dn, bind_dn and bind_pass default to the user's IMAP login.
// When 'user_specific' is enabled following variables can be used in base_dn/bind_dn config:
// %fu - The full username provided, assumes the username is an email
// address, uses the username_domain value if not an email address.
// %u - The username prior to the '@'.
// %d - The domain name after the '@'.
// %dc - The domain name hierarchal string e.g. "dc=test,dc=domain,dc=com"
// %dn - DN found by ldap search when search_filter/search_base_dn are used
'base_dn' => '',
'bind_dn' => '',
'bind_pass' => '',
// It's possible to bind for an individual address book
// The login name is used to search for the DN to bind with
'search_base_dn' => '',
'search_filter' => '', // e.g. '(&(objectClass=posixAccount)(uid=%u))'
// DN and password to bind as before searching for bind DN, if anonymous search is not allowed
'search_bind_dn' => '',
'search_bind_pw' => '',
// Base DN and filter used for resolving the user's domain root DN which feeds the %dc variables
// Leave empty to skip this lookup and derive the root DN from the username domain
'domain_base_dn' => '',
'domain_filter' => '',
// Optional map of replacement strings => attributes used when binding for an individual address book
'search_bind_attrib' => array(), // e.g. array('%udc' => 'ou')
// Default for %dn variable if search doesn't return DN value
'search_dn_default' => '',
// Optional authentication identifier to be used as SASL authorization proxy
// bind_dn need to be empty
'auth_cid' => '',
// SASL authentication method (for proxy auth), e.g. DIGEST-MD5
'auth_method' => '',
// Indicates if the addressbook shall be hidden from the list.
// With this option enabled you can still search/view contacts.
'hidden' => false,
// Indicates if the addressbook shall not list contacts but only allows searching.
'searchonly' => false,
// Indicates if we can write to the LDAP directory or not.
// If writable is true then these fields need to be populated:
// LDAP_Object_Classes, required_fields, LDAP_rdn
'writable' => false,
// To create a new contact these are the object classes to specify
// (or any other classes you wish to use).
'LDAP_Object_Classes' => array('top', 'inetOrgPerson'),
// The RDN field that is used for new entries, this field needs
// to be one of the search_fields, the base of base_dn is appended
// to the RDN to insert into the LDAP directory.
'LDAP_rdn' => 'cn',
// The required fields needed to build a new contact as required by
// the object classes (can include additional fields not required by the object classes).
'required_fields' => array('cn', 'sn', 'mail'),
'search_fields' => array('mail', 'cn'), // fields to search in
// mapping of contact fields to directory attributes
// 1. for every attribute one can specify the number of values (limit) allowed.
// default is 1, a wildcard * means unlimited
// 2. another possible parameter is separator character for composite fields
// 3. it's possible to define field format for write operations, e.g. for date fields
// example: 'birthday:date[YmdHis\\Z]'
'fieldmap' => array(
// Roundcube => LDAP:limit
'name' => 'cn',
'surname' => 'sn',
'firstname' => 'givenName',
'jobtitle' => 'title',
'email' => 'mail:*',
'phone:home' => 'homePhone',
'phone:work' => 'telephoneNumber',
'phone:mobile' => 'mobile',
'phone:pager' => 'pager',
'phone:workfax' => 'facsimileTelephoneNumber',
'street' => 'street',
'zipcode' => 'postalCode',
'region' => 'st',
'locality' => 'l',
// if you country is a complex object, you need to configure 'sub_fields' below
'country' => 'c',
'organization' => 'o',
'department' => 'ou',
'jobtitle' => 'title',
'notes' => 'description',
'photo' => 'jpegPhoto',
// these currently don't work:
// 'manager' => 'manager',
// 'assistant' => 'secretary',
),
// Map of contact sub-objects (attribute name => objectClass(es)), e.g. 'c' => 'country'
'sub_fields' => array(),
// Generate values for the following LDAP attributes automatically when creating a new record
'autovalues' => array(
// 'uid' => 'md5(microtime())', // You may specify PHP code snippets which are then eval'ed
// 'mail' => '{givenname}.{sn}@mydomain.com', // or composite strings with placeholders for existing attributes
),
'sort' => 'cn', // The field to sort the listing by.
'scope' => 'sub', // search mode: sub|base|list
'filter' => '(objectClass=inetOrgPerson)', // used for basic listing (if not empty) and will be &'d with search queries. example: status=act
'fuzzy_search' => true, // server allows wildcard search
'vlv' => false, // Enable Virtual List View to more efficiently fetch paginated data (if server supports it)
'vlv_search' => false, // Use Virtual List View functions for autocompletion searches (if server supports it)
'numsub_filter' => '(objectClass=organizationalUnit)', // with VLV, we also use numSubOrdinates to query the total number of records. Set this filter to get all numSubOrdinates attributes for counting
'config_root_dn' => 'cn=config', // Root DN to search config entries (e.g. vlv indexes)
'sizelimit' => '0', // Enables you to limit the count of entries fetched. Setting this to 0 means no limit.
'timelimit' => '0', // Sets the number of seconds how long is spend on the search. Setting this to 0 means no limit.
'referrals' => false, // Sets the LDAP_OPT_REFERRALS option. Mostly used in multi-domain Active Directory setups
'dereference' => 0, // Sets the LDAP_OPT_DEREF option. One of: LDAP_DEREF_NEVER, LDAP_DEREF_SEARCHING, LDAP_DEREF_FINDING, LDAP_DEREF_ALWAYS
// Used where addressbook contains aliases to objects elsewhere in the LDAP tree.
// definition for contact groups (uncomment if no groups are supported)
// for the groups base_dn, the user replacements %fu, %u, %d and %dc work as for base_dn (see above)
// if the groups base_dn is empty, the contact base_dn is used for the groups as well
// -> in this case, assure that groups and contacts are separated due to the concernig filters!
'groups' => array(
'base_dn' => '',
'scope' => 'sub', // Search mode: sub|base|list
'filter' => '(objectClass=groupOfNames)',
'object_classes' => array('top', 'groupOfNames'), // Object classes to be assigned to new groups
'member_attr' => 'member', // Name of the default member attribute, e.g. uniqueMember
'name_attr' => 'cn', // Attribute to be used as group name
'email_attr' => 'mail', // Group email address attribute (e.g. for mailing lists)
'member_filter' => '(objectclass=*)', // Optional filter to use when querying for group members
'vlv' => false, // Use VLV controls to list groups
'class_member_attr' => array( // Mapping of group object class to member attribute used in these objects
'groupofnames' => 'member',
'groupofuniquenames' => 'uniquemember'
),
),
// this configuration replaces the regular groups listing in the directory tree with
// a hard-coded list of groups, each listing entries with the configured base DN and filter.
// if the 'groups' option from above is set, it'll be shown as the first entry with the name 'Groups'
'group_filters' => array(
'departments' => array(
'name' => 'Company Departments',
'scope' => 'list',
'base_dn' => 'ou=Groups,dc=mydomain,dc=com',
'filter' => '(|(objectclass=groupofuniquenames)(objectclass=groupofurls))',
'name_attr' => 'cn',
),
'customers' => array(
'name' => 'Customers',
'scope' => 'sub',
'base_dn' => 'ou=Customers,dc=mydomain,dc=com',
'filter' => '(objectClass=inetOrgPerson)',
'name_attr' => 'sn',
),
),
);
*/
// An ordered array of the ids of the addressbooks that should be searched
// when populating address autocomplete fields server-side. ex: array('sql','Verisign');
$config['autocomplete_addressbooks'] = array('sql');
// The minimum number of characters required to be typed in an autocomplete field
// before address books will be searched. Most useful for LDAP directories that
// may need to do lengthy results building given overly-broad searches
$config['autocomplete_min_length'] = 1;
// Number of parallel autocomplete requests.
// If there's more than one address book, n parallel (async) requests will be created,
// where each request will search in one address book. By default (0), all address
// books are searched in one request.
$config['autocomplete_threads'] = 0;
// Max. numer of entries in autocomplete popup. Default: 15.
$config['autocomplete_max'] = 15;
// show address fields in this order
// available placeholders: {street}, {locality}, {zipcode}, {country}, {region}
$config['address_template'] = '{street}<br/>{locality} {zipcode}<br/>{country} {region}';
// Matching mode for addressbook search (including autocompletion)
// 0 - partial (*abc*), default
// 1 - strict (abc)
// 2 - prefix (abc*)
// Note: For LDAP sources fuzzy_search must be enabled to use 'partial' or 'prefix' mode
$config['addressbook_search_mode'] = 0;
// List of fields used on contacts list and for autocompletion searches
// Warning: These are field names not LDAP attributes (see 'fieldmap' setting)!
$config['contactlist_fields'] = array('name', 'firstname', 'surname', 'email');
// Template of contact entry on the autocompletion list.
// You can use contact fields as: name, email, organization, department, etc.
// See program/steps/addressbook/func.inc for a list
$config['contact_search_name'] = '{name} <{email}>';
// ----------------------------------
// USER PREFERENCES
// ----------------------------------
// Use this charset as fallback for message decoding
$config['default_charset'] = 'ISO-8859-1';
// skin name: folder from skins/
$config['skin'] = 'larry';
// limit skins available/shown in the settings section
$config['skins_allowed'] = array();
// Enables using standard browser windows (that can be handled as tabs)
// instead of popup windows
$config['standard_windows'] = false;
// show up to X items in messages list view
$config['mail_pagesize'] = 50;
// show up to X items in contacts list view
$config['addressbook_pagesize'] = 50;
// sort contacts by this col (preferably either one of name, firstname, surname)
$config['addressbook_sort_col'] = 'surname';
// The way how contact names are displayed in the list.
// 0: prefix firstname middlename surname suffix (only if display name is not set)
// 1: firstname middlename surname
// 2: surname firstname middlename
// 3: surname, firstname middlename
$config['addressbook_name_listing'] = 0;
// use this timezone to display date/time
// valid timezone identifiers are listed here: php.net/manual/en/timezones.php
// 'auto' will use the browser's timezone settings
$config['timezone'] = 'auto';
// prefer displaying HTML messages
$config['prefer_html'] = true;
// display remote resources (inline images, styles)
// 0 - Never, always ask
// 1 - Ask if sender is not in address book
// 2 - Always allow
$config['show_images'] = 0;
// open messages in new window
$config['message_extwin'] = false;
// open message compose form in new window
$config['compose_extwin'] = false;
// compose html formatted messages by default
// 0 - never,
// 1 - always,
// 2 - on reply to HTML message,
// 3 - on forward or reply to HTML message
// 4 - always, except when replying to plain text message
$config['htmleditor'] = 0;
// save copies of compose messages in the browser's local storage
// for recovery in case of browser crashes and session timeout.
$config['compose_save_localstorage'] = true;
// show pretty dates as standard
$config['prettydate'] = true;
// save compose message every 300 seconds (5min)
$config['draft_autosave'] = 300;
// Interface layout. Default: 'widescreen'.
// 'widescreen' - three columns
// 'desktop' - two columns, preview on bottom
// 'list' - two columns, no preview
$config['layout'] = 'widescreen';
// Mark as read when viewing a message (delay in seconds)
// Set to -1 if messages should not be marked as read
$config['mail_read_time'] = 0;
// Clear Trash on logout
$config['logout_purge'] = false;
// Compact INBOX on logout
$config['logout_expunge'] = false;
// Display attached images below the message body
$config['inline_images'] = true;
// Encoding of long/non-ascii attachment names:
// 0 - Full RFC 2231 compatible
// 1 - RFC 2047 for 'name' and RFC 2231 for 'filename' parameter (Thunderbird's default)
// 2 - Full 2047 compatible
$config['mime_param_folding'] = 1;
// Set true if deleted messages should not be displayed
// This will make the application run slower
$config['skip_deleted'] = false;
// Set true to Mark deleted messages as read as well as deleted
// False means that a message's read status is not affected by marking it as deleted
$config['read_when_deleted'] = true;
// Set to true to never delete messages immediately
// Use 'Purge' to remove messages marked as deleted
$config['flag_for_deletion'] = false;
// Default interval for auto-refresh requests (in seconds)
// These are requests for system state updates e.g. checking for new messages, etc.
// Setting it to 0 disables the feature.
$config['refresh_interval'] = 60;
// If true all folders will be checked for recent messages
$config['check_all_folders'] = false;
// If true, after message delete/move, the next message will be displayed
$config['display_next'] = true;
// Default messages listing mode. One of 'threads' or 'list'.
$config['default_list_mode'] = 'list';
// 0 - Do not expand threads
// 1 - Expand all threads automatically
// 2 - Expand only threads with unread messages
$config['autoexpand_threads'] = 0;
// When replying:
// -1 - don't cite the original message
// 0 - place cursor below the original message
// 1 - place cursor above original message (top posting)
// 2 - place cursor above original message (top posting), but do not indent the quote
$config['reply_mode'] = 0;
// When replying strip original signature from message
$config['strip_existing_sig'] = true;
// Show signature:
// 0 - Never
// 1 - Always
// 2 - New messages only
// 3 - Forwards and Replies only
$config['show_sig'] = 1;
// By default the signature is placed depending on cursor position (reply_mode).
// Sometimes it might be convenient to start the reply on top but keep
// the signature below the quoted text (sig_below = true).
$config['sig_below'] = false;
// Enables adding of standard separator to the signature
$config['sig_separator'] = true;
// Use MIME encoding (quoted-printable) for 8bit characters in message body
$config['force_7bit'] = false;
// Defaults of the search field configuration.
// The array can contain a per-folder list of header fields which should be considered when searching
// The entry with key '*' stands for all folders which do not have a specific list set.
// Please note that folder names should to be in sync with $config['*_mbox'] options
$config['search_mods'] = null; // Example: array('*' => array('subject'=>1, 'from'=>1), 'Sent' => array('subject'=>1, 'to'=>1));
// Defaults of the addressbook search field configuration.
$config['addressbook_search_mods'] = null; // Example: array('name'=>1, 'firstname'=>1, 'surname'=>1, 'email'=>1, '*'=>1);
// 'Delete always'
// This setting reflects if mail should be always deleted
// when moving to Trash fails. This is necessary in some setups
// when user is over quota and Trash is included in the quota.
$config['delete_always'] = false;
// Directly delete messages in Junk instead of moving to Trash
$config['delete_junk'] = false;
// Behavior if a received message requests a message delivery notification (read receipt)
// 0 = ask the user, 1 = send automatically, 2 = ignore (never send or ask)
// 3 = send automatically if sender is in addressbook, otherwise ask the user
// 4 = send automatically if sender is in addressbook, otherwise ignore
$config['mdn_requests'] = 0;
// Return receipt checkbox default state
$config['mdn_default'] = 0;
// Delivery Status Notification checkbox default state
// Note: This can be used only if smtp_server is non-empty
$config['dsn_default'] = 0;
// Place replies in the folder of the message being replied to
$config['reply_same_folder'] = false;
// Sets default mode of Forward feature to "forward as attachment"
$config['forward_attachment'] = false;
// Defines address book (internal index) to which new contacts will be added
// By default it is the first writeable addressbook.
// Note: Use '0' for built-in address book.
$config['default_addressbook'] = null;
// Enables spell checking before sending a message.
$config['spellcheck_before_send'] = false;
// Skip alternative email addresses in autocompletion (show one address per contact)
$config['autocomplete_single'] = false;
// Default font for composed HTML message.
// Supported values: Andale Mono, Arial, Arial Black, Book Antiqua, Courier New,
// Georgia, Helvetica, Impact, Tahoma, Terminal, Times New Roman, Trebuchet MS, Verdana
$config['default_font'] = 'Verdana';
// Default font size for composed HTML message.
// Supported sizes: 8pt, 10pt, 12pt, 14pt, 18pt, 24pt, 36pt
$config['default_font_size'] = '10pt';
// Enables display of email address with name instead of a name (and address in title)
$config['message_show_email'] = false;
// Default behavior of Reply-All button:
// 0 - Reply-All always
// 1 - Reply-List if mailing list is detected
$config['reply_all_mode'] = 0;
diff --git a/index-test.php b/index-test.php
index b880f7c1f..db991b82a 100644
--- a/index-test.php
+++ b/index-test.php
@@ -1,27 +1,27 @@
<?php
/*
+-----------------------------------------------------------------------+
| Roundcube Webmail Selenium Tests Entry Point |
| |
- | Copyright (C) 2005-2014, The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| This is the public entry point for all HTTP requests to the |
| Roundcube webmail application loading the 'tests' environment. |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <thomas@roundcube.net> |
+-----------------------------------------------------------------------+
*/
define('INSTALL_PATH', realpath(__DIR__) . '/');
$GLOBALS['env'] = 'test';
// include index.php from application root directory
include INSTALL_PATH . 'index.php';
diff --git a/index.php b/index.php
index f0dc37ac1..5a54d5cd7 100644
--- a/index.php
+++ b/index.php
@@ -1,332 +1,332 @@
<?php
/**
+-------------------------------------------------------------------------+
| Roundcube Webmail IMAP Client |
| Version 1.4-git |
| |
- | Copyright (C) 2005-2018, The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU General Public License (with exceptions |
| for skins & plugins) as published by the Free Software Foundation, |
| either version 3 of the License, or (at your option) any later version. |
| |
| This file forms part of the Roundcube Webmail Software for which the |
| following exception is added: Plugins and Skins which merely make |
| function calls to the Roundcube Webmail Software, and for that purpose |
| include it by reference shall not be considered modifications of |
| the software. |
| |
| If you wish to use this file in another project or create a modified |
| version that will not be part of the Roundcube Webmail Software, you |
| may remove the exception above and use this source code under the |
| original version of the license. |
| |
| 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, see http://www.gnu.org/licenses/. |
| |
+-------------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-------------------------------------------------------------------------+
*/
// include environment
require_once 'program/include/iniset.php';
// init application, start session, init output class, etc.
$RCMAIL = rcmail::get_instance(0, $GLOBALS['env']);
// Make the whole PHP output non-cacheable (#1487797)
$RCMAIL->output->nocacheing_headers();
$RCMAIL->output->common_headers(!empty($_SESSION['user_id']));
// turn on output buffering
ob_start();
// check if config files had errors
if ($err_str = $RCMAIL->config->get_error()) {
rcmail::raise_error(array(
'code' => 601,
'type' => 'php',
'message' => $err_str), false, true);
}
// check DB connections and exit on failure
if ($err_str = $RCMAIL->db->is_error()) {
rcmail::raise_error(array(
'code' => 603,
'type' => 'db',
'message' => $err_str), false, true);
}
// error steps
if ($RCMAIL->action == 'error' && !empty($_GET['_code'])) {
rcmail::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))) {
// force_https can be true, <hostname>, <hostname>:<port>, <port>
if (!is_bool($force_https)) {
list($host, $port) = explode(':', $force_https);
if (is_numeric($host) && empty($port)) {
$port = $host;
$host = '';
}
}
if (!rcube_utils::https_check($port ?: 443)) {
if (empty($host)) {
$host = preg_replace('/:[0-9]+$/', '', $_SERVER['HTTP_HOST']);
}
if ($port && $port != 443) {
$host .= ':' . $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->task == 'login' && $RCMAIL->action == 'login') {
$request_valid = $_SESSION['temp'] && $RCMAIL->check_request();
$pass_charset = $RCMAIL->config->get('password_charset', 'UTF-8');
// 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(rcube_utils::get_input_value('_user', rcube_utils::INPUT_POST)),
'pass' => rcube_utils::get_input_value('_pass', rcube_utils::INPUT_POST, true, $pass_charset),
'valid' => $request_valid,
'cookiecheck' => true,
));
// Login
if ($auth['valid'] && !$auth['abort']
&& $RCMAIL->login($auth['user'], $auth['pass'], $auth['host'], $auth['cookiecheck'])
) {
// create new session ID, don't destroy the current session
// it was destroyed already by $RCMAIL->kill_session() above
$RCMAIL->session->remove('temp');
$RCMAIL->session->regenerate_id(false);
// send auth cookie if necessary
$RCMAIL->session->set_auth_cookie();
// log successful login
$RCMAIL->log_login();
// restore original request parameters
$query = array();
if ($url = rcube_utils::get_input_value('_url', rcube_utils::INPUT_POST)) {
parse_str($url, $query);
// prevent endless looping on login page
if ($query['_task'] == 'login') {
unset($query['_task']);
}
// prevent redirect to compose with specified ID (#1488226)
if ($query['_action'] == 'compose' && !empty($query['_id'])) {
$query = array('_action' => 'compose');
}
}
// allow plugins to control the redirect url after login success
$redir = $RCMAIL->plugins->exec_hook('login_after', $query + array('_task' => 'mail'));
unset($redir['abort'], $redir['_err']);
// send redirect
$OUTPUT->redirect($redir, 0, true);
}
else {
if (!$auth['valid']) {
$error_code = rcmail::ERROR_INVALID_REQUEST;
}
else {
$error_code = is_numeric($auth['error']) ? $auth['error'] : $RCMAIL->login_error();
}
$error_labels = array(
rcmail::ERROR_STORAGE => 'storageerror',
rcmail::ERROR_COOKIES_DISABLED => 'cookiesdisabled',
rcmail::ERROR_INVALID_REQUEST => 'invalidrequest',
rcmail::ERROR_INVALID_HOST => 'invalidhost',
rcmail::ERROR_RATE_LIMIT => 'accountlocked',
);
$error_message = !empty($auth['error']) && !is_numeric($auth['error']) ? $auth['error'] : ($error_labels[$error_code] ?: 'loginfailed');
$OUTPUT->show_message($error_message, 'warning');
// log failed login
$RCMAIL->log_login($auth['user'], true, $error_code);
$RCMAIL->plugins->exec_hook('login_failed', array(
'code' => $error_code, 'host' => $auth['host'], 'user' => $auth['user']));
$RCMAIL->kill_session();
}
}
// end session
else if ($RCMAIL->task == 'logout' && isset($_SESSION['user_id'])) {
$RCMAIL->request_security_check($mode = rcube_utils::INPUT_GET);
$userdata = array(
'user' => $_SESSION['username'],
'host' => $_SESSION['storage_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->task != 'login' && $_SESSION['user_id']) {
if (!$RCMAIL->session->check_auth()) {
$RCMAIL->kill_session();
$session_error = true;
}
}
// not logged in -> show login page
if (empty($RCMAIL->user->ID)) {
// log session failures
$task = rcube_utils::get_input_value('_task', rcube_utils::INPUT_GPC);
if ($task && !in_array($task, array('login','logout'))
&& !$session_error && ($sess_id = $_COOKIE[ini_get('session.name')])
) {
$RCMAIL->session->log("Aborted session $sess_id; no valid session data found");
$session_error = true;
}
if ($session_error || $_REQUEST['_err'] == 'session') {
$OUTPUT->show_message('sessionerror', 'error', null, true, -1);
}
if ($OUTPUT->ajax_call || $OUTPUT->get_env('framed')) {
$OUTPUT->command('session_error', $RCMAIL->url(array('_err' => 'session')));
$OUTPUT->send('iframe');
}
// check if installer is still active
if ($RCMAIL->config->get('enable_installer') && is_readable('./installer/index.php')) {
$OUTPUT->add_footer(html::div(array('id' => 'login-addon', '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.")
));
}
$plugin = $RCMAIL->plugins->exec_hook('unauthenticated', array(
'task' => 'login',
'error' => $session_error,
'http_code' => !$session_error ? 401 : 200
));
$RCMAIL->set_task($plugin['task']);
if ($plugin['http_code'] == 401) {
header('HTTP/1.0 401 Unauthorized');
}
$OUTPUT->send($plugin['task']);
}
else {
// CSRF prevention
$RCMAIL->request_security_check();
// check access to disabled actions
$disabled_actions = (array) $RCMAIL->config->get('disabled_actions');
if (in_array($RCMAIL->task . '.' . ($RCMAIL->action ?: 'index'), $disabled_actions)) {
rcube::raise_error(array(
'code' => 404, 'type' => 'php',
'message' => "Action disabled"), true, true);
}
}
// we're ready, user is authenticated and the request is safe
$plugin = $RCMAIL->plugins->exec_hook('ready', array('task' => $RCMAIL->task, 'action' => $RCMAIL->action));
$RCMAIL->set_task($plugin['task']);
$RCMAIL->action = $plugin['action'];
// handle special actions
if ($RCMAIL->action == 'keep-alive') {
$OUTPUT->reset();
$RCMAIL->plugins->exec_hook('keep_alive', array());
$OUTPUT->send();
}
else if ($RCMAIL->action == 'save-pref') {
include INSTALL_PATH . 'program/steps/utils/save_pref.inc';
}
// include task specific functions
if (is_file($incfile = INSTALL_PATH . 'program/steps/'.$RCMAIL->task.'/func.inc')) {
include_once $incfile;
}
// allow 5 "redirects" to another action
$redirects = 0;
while ($redirects < 5) {
// execute a plugin action
if (preg_match('/^plugin\./', $RCMAIL->action)) {
$RCMAIL->plugins->exec_action($RCMAIL->action);
break;
}
// execute action registered to a plugin task
else if ($RCMAIL->plugins->is_plugin_task($RCMAIL->task)) {
if (!$RCMAIL->action) $RCMAIL->action = 'index';
$RCMAIL->plugins->exec_action($RCMAIL->task.'.'.$RCMAIL->action);
break;
}
// try to include the step file
else if (($stepfile = $RCMAIL->get_action_file())
&& is_file($incfile = INSTALL_PATH . 'program/steps/'.$RCMAIL->task.'/'.$stepfile)
) {
// include action file only once (in case it don't exit)
include_once $incfile;
$redirects++;
}
else {
break;
}
}
if ($RCMAIL->action == 'refresh') {
$RCMAIL->plugins->exec_hook('refresh', array('last' => intval(rcube_utils::get_input_value('_last', rcube_utils::INPUT_GPC))));
}
// parse main template (default)
$OUTPUT->send($RCMAIL->task);
// if we arrive here, something went wrong
rcmail::raise_error(array(
'code' => 404,
'type' => 'php',
'line' => __LINE__,
'file' => __FILE__,
'message' => "Invalid request"), true, true);
diff --git a/installer/check.php b/installer/check.php
index 3d2538191..0c5175cbb 100644
--- a/installer/check.php
+++ b/installer/check.php
@@ -1,269 +1,283 @@
<?php
+/**
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+ +-----------------------------------------------------------------------+
+*/
+
if (!class_exists('rcmail_install', false) || !is_object($RCI)) {
die("Not allowed! Please open installer/index.php instead.");
}
?>
<form action="index.php" method="get">
<?php
$required_php_exts = array(
'PCRE' => 'pcre',
'DOM' => 'dom',
'Session' => 'session',
'XML' => 'xml',
'JSON' => 'json',
'PDO' => 'PDO',
'Multibyte' => 'mbstring',
'OpenSSL' => 'openssl',
);
$optional_php_exts = array(
'FileInfo' => 'fileinfo',
'Libiconv' => 'iconv',
'Intl' => 'intl',
'Exif' => 'exif',
'LDAP' => 'ldap',
'GD' => 'gd',
'Imagick' => 'imagick',
'Zip' => 'zip',
);
$required_libs = array(
'PEAR' => 'pear.php.net',
'Auth_SASL' => 'pear.php.net',
'Net_SMTP' => 'pear.php.net',
'Net_IDNA2' => 'pear.php.net',
'Mail_mime' => 'pear.php.net',
);
$optional_libs = array(
'Net_LDAP3' => 'git.kolab.org',
);
$ini_checks = array(
'file_uploads' => 1,
'session.auto_start' => 0,
'mbstring.func_overload' => 0,
'suhosin.session.encrypt' => 0,
);
$optional_checks = array(
// required for utils/modcss.inc, should we require this?
'allow_url_fopen' => 1,
'date.timezone' => '-VALID-',
);
$source_urls = array(
'Sockets' => 'http://www.php.net/manual/en/book.sockets.php',
'Session' => 'http://www.php.net/manual/en/book.session.php',
'PCRE' => 'http://www.php.net/manual/en/book.pcre.php',
'FileInfo' => 'http://www.php.net/manual/en/book.fileinfo.php',
'Libiconv' => 'http://www.php.net/manual/en/book.iconv.php',
'Multibyte' => 'http://www.php.net/manual/en/book.mbstring.php',
'OpenSSL' => 'http://www.php.net/manual/en/book.openssl.php',
'JSON' => 'http://www.php.net/manual/en/book.json.php',
'DOM' => 'http://www.php.net/manual/en/book.dom.php',
'Intl' => 'http://www.php.net/manual/en/book.intl.php',
'Exif' => 'http://www.php.net/manual/en/book.exif.php',
'oci8' => 'http://www.php.net/manual/en/book.oci8.php',
'PDO' => 'http://www.php.net/manual/en/book.pdo.php',
'LDAP' => 'http://www.php.net/manual/en/book.ldap.php',
'GD' => 'http://www.php.net/manual/en/book.image.php',
'Imagick' => 'http://www.php.net/manual/en/book.imagick.php',
'Zip' => 'http://www.php.net/manual/en/book.zip.php',
'pdo_mysql' => 'http://www.php.net/manual/en/ref.pdo-mysql.php',
'pdo_pgsql' => 'http://www.php.net/manual/en/ref.pdo-pgsql.php',
'pdo_sqlite' => 'http://www.php.net/manual/en/ref.pdo-sqlite.php',
'pdo_sqlite2' => 'http://www.php.net/manual/en/ref.pdo-sqlite.php',
'pdo_sqlsrv' => 'http://www.php.net/manual/en/ref.pdo-sqlsrv.php',
'pdo_dblib' => 'http://www.php.net/manual/en/ref.pdo-dblib.php',
'PEAR' => 'http://pear.php.net',
'Net_SMTP' => 'http://pear.php.net/package/Net_SMTP',
'Mail_mime' => 'http://pear.php.net/package/Mail_mime',
'Net_IDNA2' => 'http://pear.php.net/package/Net_IDNA2',
'Net_LDAP3' => 'https://git.kolab.org/diffusion/PNL',
);
echo '<input type="hidden" name="_step" value="' . ($RCI->configured ? 3 : 2) . '" />';
?>
<h3>Checking PHP version</h3>
<?php
define('MIN_PHP_VERSION', '5.4.0');
if (version_compare(PHP_VERSION, MIN_PHP_VERSION, '>=')) {
$RCI->pass('Version', 'PHP ' . PHP_VERSION . ' detected');
} else {
$RCI->fail('Version', 'PHP Version ' . MIN_PHP_VERSION . ' or greater is required ' . PHP_VERSION . ' detected');
}
?>
<h3>Checking PHP extensions</h3>
<p class="hint">The following modules/extensions are <em>required</em> to run Roundcube:</p>
<?php
// get extensions location
$ext_dir = ini_get('extension_dir');
$prefix = (PHP_SHLIB_SUFFIX === 'dll') ? 'php_' : '';
foreach ($required_php_exts as $name => $ext) {
if (extension_loaded($ext)) {
$RCI->pass($name);
} else {
$_ext = $ext_dir . '/' . $prefix . $ext . '.' . PHP_SHLIB_SUFFIX;
$msg = @is_readable($_ext) ? 'Could be loaded. Please add in php.ini' : '';
$RCI->fail($name, $msg, $source_urls[$name]);
}
echo '<br />';
}
?>
<p class="hint">The next couple of extensions are <em>optional</em> and recommended to get the best performance:</p>
<?php
foreach ($optional_php_exts as $name => $ext) {
if (extension_loaded($ext)) {
$RCI->pass($name);
}
else {
$_ext = $ext_dir . '/' . $prefix . $ext . '.' . PHP_SHLIB_SUFFIX;
$msg = @is_readable($_ext) ? 'Could be loaded. Please add in php.ini' : '';
$RCI->na($name, $msg, $source_urls[$name]);
}
echo '<br />';
}
?>
<h3>Checking available databases</h3>
<p class="hint">Check which of the supported extensions are installed. At least one of them is required.</p>
<?php
$prefix = (PHP_SHLIB_SUFFIX === 'dll') ? 'php_' : '';
foreach ($RCI->supported_dbs as $database => $ext) {
if (extension_loaded($ext)) {
$RCI->pass($database);
$found_db_driver = true;
}
else {
$_ext = $ext_dir . '/' . $prefix . $ext . '.' . PHP_SHLIB_SUFFIX;
$msg = @is_readable($_ext) ? 'Could be loaded. Please add in php.ini' : '';
$RCI->na($database, $msg, $source_urls[$ext]);
}
echo '<br />';
}
if (empty($found_db_driver)) {
$RCI->failures++;
}
?>
<h3>Check for required 3rd party libs</h3>
<p class="hint">This also checks if the include path is set correctly.</p>
<?php
foreach ($required_libs as $classname => $vendor) {
if (class_exists($classname)) {
$RCI->pass($classname);
}
else {
$RCI->fail($classname, "Failed to load class $classname from $vendor", $source_urls[$classname]);
}
echo "<br />";
}
foreach ($optional_libs as $classname => $vendor) {
if (class_exists($classname)) {
$RCI->pass($classname);
}
else {
$RCI->na($classname, "Recommended to install $classname from $vendor", $source_urls[$classname]);
}
echo "<br />";
}
?>
<h3>Checking php.ini/.htaccess settings</h3>
<p class="hint">The following settings are <em>required</em> to run Roundcube:</p>
<?php
foreach ($ini_checks as $var => $val) {
$status = ini_get($var);
if ($val === '-NOTEMPTY-') {
if (empty($status)) {
$RCI->fail($var, "empty value detected");
}
else {
$RCI->pass($var);
}
}
else if (filter_var($status, FILTER_VALIDATE_BOOLEAN) == $val) {
$RCI->pass($var);
}
else {
$RCI->fail($var, "is '$status', should be '$val'");
}
echo '<br />';
}
?>
<p class="hint">The following settings are <em>optional</em> and recommended:</p>
<?php
foreach ($optional_checks as $var => $val) {
$status = ini_get($var);
if ($val === '-NOTEMPTY-') {
if (empty($status)) {
$RCI->optfail($var, "Could be set");
} else {
$RCI->pass($var);
}
echo '<br />';
continue;
}
if ($val === '-VALID-') {
if ($var == 'date.timezone') {
try {
$tz = new DateTimeZone($status);
$RCI->pass($var);
}
catch (Exception $e) {
$RCI->optfail($var, empty($status) ? "not set" : "invalid value detected: $status");
}
}
else {
$RCI->pass($var);
}
}
else if (filter_var($status, FILTER_VALIDATE_BOOLEAN) == $val) {
$RCI->pass($var);
}
else {
$RCI->optfail($var, "is '$status', could be '$val'");
}
echo '<br />';
}
?>
<?php
if ($RCI->failures) {
echo '<p class="warning">Sorry but your webserver does not meet the requirements for Roundcube!<br />
Please install the missing modules or fix the php.ini settings according to the above check results.<br />
Hint: only checks showing <span class="fail">NOT OK</span> need to be fixed.</p>';
}
echo '<p><br /><input type="submit" value="NEXT" ' . ($RCI->failures ? 'disabled' : '') . ' /></p>';
?>
</form>
diff --git a/installer/client.js b/installer/client.js
index 2880ce34f..a2bdc89e2 100644
--- a/installer/client.js
+++ b/installer/client.js
@@ -1,51 +1,50 @@
/*
+-----------------------------------------------------------------------+
| Roundcube installer client function |
| |
- | This file is part of the Roundcube web development suite |
- | Copyright (C) 2009-2012, The Roundcube Dev Team |
+ | This file is part of the Roundcube Webmail client |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
function toggleblock(id, link)
{
var block = document.getElementById(id);
-
+
return false;
}
function addhostfield()
{
var container = document.getElementById('defaulthostlist');
var row = document.createElement('div');
var input = document.createElement('input');
var link = document.createElement('a');
-
+
input.name = '_default_host[]';
input.size = '30';
link.href = '#';
link.onclick = function() { removehostfield(this.parentNode); return false };
link.className = 'removelink';
link.innerHTML = 'remove';
-
+
row.appendChild(input);
row.appendChild(link);
container.appendChild(row);
}
function removehostfield(row)
{
var container = document.getElementById('defaulthostlist');
container.removeChild(row);
}
diff --git a/installer/config.php b/installer/config.php
index 90fa9929c..a246dc749 100644
--- a/installer/config.php
+++ b/installer/config.php
@@ -1,679 +1,693 @@
<?php
+/**
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+ +-----------------------------------------------------------------------+
+*/
+
if (!class_exists('rcmail_install', false) || !is_object($RCI)) {
die("Not allowed! Please open installer/index.php instead.");
}
// register these boolean fields
$RCI->bool_config_props = array(
'ip_check' => 1,
'enable_spellcheck' => 1,
'auto_create_user' => 1,
'smtp_log' => 1,
'prefer_html' => 1,
);
// allow the current user to get to the next step
$_SESSION['allowinstaller'] = true;
if (!empty($_POST['submit'])) {
$_SESSION['config'] = $RCI->create_config();
if ($RCI->save_configfile($_SESSION['config'])) {
echo '<p class="notice">The config file was saved successfully into <tt>'.RCMAIL_CONFIG_DIR.'</tt> directory of your Roundcube installation.';
if ($RCI->legacy_config) {
echo '<br/><br/>Afterwards, please <b>remove</b> the old configuration files <tt>main.inc.php</tt> and <tt>db.inc.php</tt> from the config directory.';
}
echo '</p>';
}
else {
if (($dir = sys_get_temp_dir()) && @is_writable($dir)) {
echo '<iframe name="getconfig" style="display:none"></iframe>';
echo '<form id="getconfig_form" action="index.php" method="get" target="getconfig" style="display:none">';
echo '<input name="_getconfig" value="2" /></form>';
$button_txt = html::quote('Save in ' . $dir);
$save_button = '&nbsp;<input type="button" onclick="document.getElementById(\'getconfig_form\').submit()" value="' . $button_txt . '" />';
}
echo '<p class="notice">Copy or download the following configuration and save it';
echo ' as <tt><b>config.inc.php</b></tt> within the <tt>'.RCUBE_CONFIG_DIR.'</tt> directory of your Roundcube installation.<br/>';
echo ' Make sure that there are no characters outside the <tt>&lt;?php ?&gt;</tt> brackets when saving the file.';
echo '&nbsp;<input type="button" onclick="location.href=\'index.php?_getconfig=1\'" value="Download" />';
echo $save_button;
if ($RCI->legacy_config) {
echo '<br/><br/>Afterwards, please <b>remove</b> the old configuration files <tt>main.inc.php</tt> and <tt>db.inc.php</tt> from the config directory.';
}
echo '</p>';
$textbox = new html_textarea(array('rows' => 16, 'cols' => 60, 'class' => "configfile"));
echo $textbox->show(($_SESSION['config']));
}
echo '<p class="hint">Of course there are more options to configure.
Have a look at the defaults.inc.php file or visit <a href="https://github.com/roundcube/roundcubemail/wiki/Configuration" target="_blank">Howto_Config</a> to find out.</p>';
echo '<p><input type="button" onclick="location.href=\'./index.php?_step=3\'" value="CONTINUE" /></p>';
// echo '<style type="text/css"> .configblock { display:none } </style>';
echo "\n<hr style='margin-bottom:1.6em' />\n";
}
?>
<form action="index.php" method="post">
<input type="hidden" name="_step" value="2" />
<fieldset>
<legend>General configuration</legend>
<dl class="configblock">
<dt class="propname">product_name</dt>
<dd>
<?php
$input_prodname = new html_inputfield(array('name' => '_product_name', 'size' => 30, 'id' => "cfgprodname"));
echo $input_prodname->show($RCI->getprop('product_name'));
?>
<div>The name of your service (used to compose page titles)</div>
</dd>
<dt class="propname">support_url</dt>
<dd>
<?php
$input_support = new html_inputfield(array('name' => '_support_url', 'size' => 50, 'id' => "cfgsupporturl"));
echo $input_support->show($RCI->getprop('support_url'));
?>
<div>Provide an URL where a user can get support for this Roundcube installation.<br/>PLEASE DO NOT LINK TO THE ROUNDCUBE.NET WEBSITE HERE!</div>
<p class="hint">Enter an absolute URL (including http://) to a support page/form or a mailto: link.</p>
</dd>
<dt class="propname">skin_logo</dt>
<dd>
<?php
$input_skin = new html_inputfield(array('name' => '_skin_logo', 'size' => 50, 'id' => "cfgskinlogo"));
echo $input_skin->show($RCI->getprop('skin_logo'));
?>
<div>Custom image to display instead of the Roundcube logo.</div>
<p class="hint">Enter a URL relative to the document root of this Roundcube installation.</p>
</dd>
<dt class="propname">temp_dir</dt>
<dd>
<?php
$input_tempdir = new html_inputfield(array('name' => '_temp_dir', 'size' => 30, 'id' => "cfgtempdir"));
echo $input_tempdir->show($RCI->getprop('temp_dir'));
?>
<div>Use this folder to store temp files (must be writeable for webserver)</div>
</dd>
<dt class="propname">des_key</dt>
<dd>
<?php
$input_deskey = new html_inputfield(array('name' => '_des_key', 'size' => 30, 'id' => "cfgdeskey"));
echo $input_deskey->show($RCI->getprop('des_key'));
?>
<div>This key is used to encrypt the users imap password before storing in the session record</div>
<p class="hint">It's a random generated string to ensure that every installation has its own key.</p>
</dd>
<dt class="propname">ip_check</dt>
<dd>
<?php
$check_ipcheck = new html_checkbox(array('name' => '_ip_check', 'id' => "cfgipcheck"));
echo $check_ipcheck->show(intval($RCI->getprop('ip_check')), array('value' => 1));
?>
<label for="cfgipcheck">Check client IP in session authorization</label><br />
<p class="hint">This increases security but can cause sudden logouts when someone uses a proxy with changing IPs.</p>
</dd>
<dt class="propname">enable_spellcheck</dt>
<dd>
<?php
$check_spell = new html_checkbox(array('name' => '_enable_spellcheck', 'id' => "cfgspellcheck"));
echo $check_spell->show(intval($RCI->getprop('enable_spellcheck')), array('value' => 1));
?>
<label for="cfgspellcheck">Make use of the spell checker</label><br />
</dd>
<dt class="propname">spellcheck_engine</dt>
<dd>
<?php
$select_spell = new html_select(array('name' => '_spellcheck_engine', 'id' => "cfgspellcheckengine"));
if (extension_loaded('pspell'))
$select_spell->add('Pspell', 'pspell');
if (extension_loaded('enchant'))
$select_spell->add('Enchant', 'enchant');
$select_spell->add('Googie', 'googie');
$select_spell->add('ATD', 'atd');
echo $select_spell->show($RCI->is_post ? $_POST['_spellcheck_engine'] : 'pspell');
?>
<label for="cfgspellcheckengine">Which spell checker to use</label><br />
<p class="hint">Googie implies that the message content will be sent to external server to check the spelling.</p>
</dd>
<dt class="propname">identities_level</dt>
<dd>
<?php
$input_ilevel = new html_select(array('name' => '_identities_level', 'id' => "cfgidentitieslevel"));
$input_ilevel->add('many identities with possibility to edit all params', 0);
$input_ilevel->add('many identities with possibility to edit all params but not email address', 1);
$input_ilevel->add('one identity with possibility to edit all params', 2);
$input_ilevel->add('one identity with possibility to edit all params but not email address', 3);
$input_ilevel->add('one identity with possibility to edit only signature', 4);
echo $input_ilevel->show($RCI->getprop('identities_level'), 0);
?>
<div>Level of identities access</div>
<p class="hint">Defines what users can do with their identities.</p>
</dd>
</dl>
</fieldset>
<fieldset>
<legend>Logging & Debugging</legend>
<dl class="loggingblock">
<dt class="propname">log_driver</dt>
<dd>
<?php
$select_log_driver = new html_select(array('name' => '_log_driver', 'id' => "cfglogdriver"));
$select_log_driver->add(array('file', 'syslog', 'stdout'), array('file', 'syslog', 'stdout'));
echo $select_log_driver->show($RCI->getprop('log_driver', 'file'));
?>
<div>How to do logging? 'file' - write to files in the log directory, 'syslog' - use the syslog facility, 'stdout' writes to the process' STDOUT file descriptor.</div>
</dd>
<dt class="propname">log_dir</dt>
<dd>
<?php
$input_logdir = new html_inputfield(array('name' => '_log_dir', 'size' => 30, 'id' => "cfglogdir"));
echo $input_logdir->show($RCI->getprop('log_dir'));
?>
<div>Use this folder to store log files (must be writeable for webserver). Note that this only applies if you are using the 'file' log_driver.</div>
</dd>
<dt class="propname">syslog_id</dt>
<dd>
<?php
$input_syslogid = new html_inputfield(array('name' => '_syslog_id', 'size' => 30, 'id' => "cfgsyslogid"));
echo $input_syslogid->show($RCI->getprop('syslog_id', 'roundcube'));
?>
<div>What ID to use when logging with syslog. Note that this only applies if you are using the 'syslog' log_driver.</div>
</dd>
<dt class="propname">syslog_facility</dt>
<dd>
<?php
$input_syslogfacility = new html_select(array('name' => '_syslog_facility', 'id' => "cfgsyslogfacility"));
$input_syslogfacility->add('user-level messages', LOG_USER);
$input_syslogfacility->add('mail subsystem', LOG_MAIL);
$input_syslogfacility->add('local level 0', LOG_LOCAL0);
$input_syslogfacility->add('local level 1', LOG_LOCAL1);
$input_syslogfacility->add('local level 2', LOG_LOCAL2);
$input_syslogfacility->add('local level 3', LOG_LOCAL3);
$input_syslogfacility->add('local level 4', LOG_LOCAL4);
$input_syslogfacility->add('local level 5', LOG_LOCAL5);
$input_syslogfacility->add('local level 6', LOG_LOCAL6);
$input_syslogfacility->add('local level 7', LOG_LOCAL7);
echo $input_syslogfacility->show($RCI->getprop('syslog_facility'), LOG_USER);
?>
<div>What ID to use when logging with syslog. Note that this only applies if you are using the 'syslog' log_driver.</div>
</dd>
</dl>
</fieldset>
<fieldset>
<legend>Database setup</legend>
<dl class="configblock" id="cgfblockdb">
<dt class="propname">db_dsnw</dt>
<dd>
<p>Database settings for read/write operations:</p>
<?php
$select_dbtype = new html_select(array('name' => '_dbtype', 'id' => "cfgdbtype"));
foreach ($RCI->supported_dbs as $database => $ext) {
if (extension_loaded($ext)) {
$select_dbtype->add($database, substr($ext, 4));
}
}
$input_dbhost = new html_inputfield(array('name' => '_dbhost', 'size' => 20, 'id' => "cfgdbhost"));
$input_dbname = new html_inputfield(array('name' => '_dbname', 'size' => 20, 'id' => "cfgdbname"));
$input_dbuser = new html_inputfield(array('name' => '_dbuser', 'size' => 20, 'id' => "cfgdbuser"));
$input_dbpass = new html_inputfield(array('name' => '_dbpass', 'size' => 20, 'id' => "cfgdbpass"));
$dsnw = rcube_db::parse_dsn($RCI->getprop('db_dsnw'));
echo $select_dbtype->show($RCI->is_post ? $_POST['_dbtype'] : $dsnw['phptype']);
echo '<label for="cfgdbtype">Database type</label><br />';
echo $input_dbhost->show($RCI->is_post ? $_POST['_dbhost'] : $dsnw['hostspec']);
echo '<label for="cfgdbhost">Database server (omit for sqlite)</label><br />';
echo $input_dbname->show($RCI->is_post ? $_POST['_dbname'] : $dsnw['database']);
echo '<label for="cfgdbname">Database name (use absolute path and filename for sqlite)</label><br />';
echo $input_dbuser->show($RCI->is_post ? $_POST['_dbuser'] : $dsnw['username']);
echo '<label for="cfgdbuser">Database user name (needs write permissions)(omit for sqlite)</label><br />';
echo $input_dbpass->show($RCI->is_post ? $_POST['_dbpass'] : $dsnw['password']);
echo '<label for="cfgdbpass">Database password (omit for sqlite)</label><br />';
?>
</dd>
<dt class="propname">db_prefix</dt>
<dd>
<?php
$input_prefix = new html_inputfield(array('name' => '_db_prefix', 'size' => 20, 'id' => "cfgdbprefix"));
echo $input_prefix->show($RCI->getprop('db_prefix'));
?>
<div>Optional prefix that will be added to database object names (tables and sequences).</div>
</dd>
</dl>
</fieldset>
<fieldset>
<legend>IMAP Settings</legend>
<dl class="configblock" id="cgfblockimap">
<dt class="propname">default_host</dt>
<dd>
<div>The IMAP host(s) chosen to perform the log-in</div>
<div id="defaulthostlist">
<?php
$text_imaphost = new html_inputfield(array('name' => '_default_host[]', 'size' => 30));
$default_hosts = $RCI->get_hostlist();
if (empty($default_hosts))
$default_hosts = array('');
$i = 0;
foreach ($default_hosts as $host) {
echo '<div id="defaulthostentry'.$i.'">' . $text_imaphost->show($host);
if ($i++ > 0)
echo '<a href="#" onclick="removehostfield(this.parentNode);return false" class="removelink" title="Remove this entry">remove</a>';
echo '</div>';
}
?>
</div>
<div><a href="javascript:addhostfield()" class="addlink" title="Add another field">add</a></div>
<p class="hint">Leave blank to show a textbox at login. To use SSL/IMAPS connection, type ssl://hostname</p>
</dd>
<dt class="propname">default_port</dt>
<dd>
<?php
$text_imapport = new html_inputfield(array('name' => '_default_port', 'size' => 6, 'id' => "cfgimapport"));
echo $text_imapport->show($RCI->getprop('default_port'));
?>
<div>TCP port used for IMAP connections</div>
</dd>
<dt class="propname">username_domain</dt>
<dd>
<?php
$text_userdomain = new html_inputfield(array('name' => '_username_domain', 'size' => 30, 'id' => "cfguserdomain"));
echo $text_userdomain->show($RCI->getprop('username_domain'));
?>
<div>Automatically add this domain to user names for login</div>
<p class="hint">Only for IMAP servers that require full e-mail addresses for login</p>
</dd>
<dt class="propname">auto_create_user</dt>
<dd>
<?php
$check_autocreate = new html_checkbox(array('name' => '_auto_create_user', 'id' => "cfgautocreate"));
echo $check_autocreate->show(intval($RCI->getprop('auto_create_user')), array('value' => 1));
?>
<label for="cfgautocreate">Automatically create a new Roundcube user when log-in the first time</label><br />
<p class="hint">A user is authenticated by the IMAP server but it requires a local record to store settings
and contacts. With this option enabled a new user record will automatically be created once the IMAP login succeeds.</p>
<p class="hint">If this option is disabled, the login only succeeds if there's a matching user-record in the local Roundcube database
what means that you have to create those records manually or disable this option after the first login.</p>
</dd>
<dt class="propname">sent_mbox</dt>
<dd>
<?php
$text_sentmbox = new html_inputfield(array('name' => '_sent_mbox', 'size' => 20, 'id' => "cfgsentmbox"));
echo $text_sentmbox->show($RCI->getprop('sent_mbox'));
?>
<div>Store sent messages in this folder</div>
<p class="hint">Leave blank if sent messages should not be stored. Note: folder must include namespace prefix if any.</p>
</dd>
<dt class="propname">trash_mbox</dt>
<dd>
<?php
$text_trashmbox = new html_inputfield(array('name' => '_trash_mbox', 'size' => 20, 'id' => "cfgtrashmbox"));
echo $text_trashmbox->show($RCI->getprop('trash_mbox'));
?>
<div>Move messages to this folder when deleting them</div>
<p class="hint">Leave blank if they should be deleted directly. Note: folder must include namespace prefix if any.</p>
</dd>
<dt class="propname">drafts_mbox</dt>
<dd>
<?php
$text_draftsmbox = new html_inputfield(array('name' => '_drafts_mbox', 'size' => 20, 'id' => "cfgdraftsmbox"));
echo $text_draftsmbox->show($RCI->getprop('drafts_mbox'));
?>
<div>Store draft messages in this folder</div>
<p class="hint">Leave blank if they should not be stored. Note: folder must include namespace prefix if any.</p>
</dd>
<dt class="propname">junk_mbox</dt>
<dd>
<?php
$text_junkmbox = new html_inputfield(array('name' => '_junk_mbox', 'size' => 20, 'id' => "cfgjunkmbox"));
echo $text_junkmbox->show($RCI->getprop('junk_mbox'));
?>
<div>Store spam messages in this folder</div>
<p class="hint">Note: folder must include namespace prefix if any.</p>
</dd>
</dd>
</dl>
</fieldset>
<fieldset>
<legend>SMTP Settings</legend>
<dl class="configblock" id="cgfblocksmtp">
<dt class="propname">smtp_server</dt>
<dd>
<?php
$text_smtphost = new html_inputfield(array('name' => '_smtp_server', 'size' => 30, 'id' => "cfgsmtphost"));
echo $text_smtphost->show($RCI->getprop('smtp_server', 'localhost'));
?>
<div>Use this host for sending mails</div>
<p class="hint">To use SSL connection, set ssl://smtp.host.com.</p>
</dd>
<dt class="propname">smtp_port</dt>
<dd>
<?php
$text_smtpport = new html_inputfield(array('name' => '_smtp_port', 'size' => 6, 'id' => "cfgsmtpport"));
echo $text_smtpport->show($RCI->getprop('smtp_port'));
?>
<div>SMTP port (default is 587)</div>
</dd>
<dt class="propname">smtp_user/smtp_pass</dt>
<dd>
<?php
$text_smtpuser = new html_inputfield(array('name' => '_smtp_user', 'size' => 20, 'id' => "cfgsmtpuser"));
$text_smtppass = new html_inputfield(array('name' => '_smtp_pass', 'size' => 20, 'id' => "cfgsmtppass"));
echo $text_smtpuser->show($RCI->getprop('smtp_user'));
echo $text_smtppass->show($RCI->getprop('smtp_pass'));
?>
<div>SMTP username and password (if required)</div>
<p>
<?php
$check_smtpuser = new html_checkbox(array('name' => '_smtp_user_u', 'id' => "cfgsmtpuseru"));
echo $check_smtpuser->show($RCI->getprop('smtp_user') == '%u' || $_POST['_smtp_user_u'] ? 1 : 0, array('value' => 1));
?>
<label for="cfgsmtpuseru">Use the current IMAP username and password for SMTP authentication</label>
</p>
</dd>
<!--
<dt class="propname">smtp_auth_type</dt>
<dd>
<?php
/*
$select_smtpauth = new html_select(array('name' => '_smtp_auth_type', 'id' => "cfgsmtpauth"));
$select_smtpauth->add(array('(auto)', 'PLAIN', 'DIGEST-MD5', 'CRAM-MD5', 'LOGIN'), array('0', 'PLAIN', 'DIGEST-MD5', 'CRAM-MD5', 'LOGIN'));
echo $select_smtpauth->show(intval($RCI->getprop('smtp_auth_type')));
*/
?>
<div>Method to authenticate at the SMTP server. Choose (auto) if you don't know what this is</div>
</dd>
-->
<dt class="propname">smtp_log</dt>
<dd>
<?php
$check_smtplog = new html_checkbox(array('name' => '_smtp_log', 'id' => "cfgsmtplog"));
echo $check_smtplog->show(intval($RCI->getprop('smtp_log')), array('value' => 1));
?>
<label for="cfgsmtplog">Log sent messages in <tt>{log_dir}/sendmail</tt> or to syslog.</label><br />
</dd>
</dl>
</fieldset>
<fieldset>
<legend>Display settings &amp; user prefs</legend>
<dl class="configblock" id="cgfblockdisplay">
<dt class="propname">language <span class="userconf">*</span></dt>
<dd>
<?php
$input_locale = new html_inputfield(array('name' => '_language', 'size' => 6, 'id' => "cfglocale"));
echo $input_locale->show($RCI->getprop('language'));
?>
<div>The default locale setting. This also defines the language of the login screen.<br/>Leave it empty to auto-detect the user agent language.</div>
<p class="hint">Enter a <a href="http://www.faqs.org/rfcs/rfc1766">RFC1766</a> formatted language name. Examples: en_US, de_DE, de_CH, fr_FR, pt_BR</p>
</dd>
<dt class="propname">skin <span class="userconf">*</span></dt>
<dd>
<?php
$input_skin = new html_select(array('name' => '_skin', 'id' => "cfgskin"));
$input_skin->add($RCI->list_skins());
echo $input_skin->show($RCI->getprop('skin'));
?>
<div>Name of interface skin (folder in /skins)</div>
</dd>
<dt class="propname">mail_pagesize <span class="userconf">*</span></dt>
<dd>
<?php
$pagesize = $RCI->getprop('mail_pagesize');
if (!$pagesize) {
$pagesize = $RCI->getprop('pagesize');
}
$input_pagesize = new html_inputfield(array('name' => '_mail_pagesize', 'size' => 6, 'id' => "cfgmailpagesize"));
echo $input_pagesize->show($pagesize);
?>
<div>Show up to X items in the mail messages list view.</div>
</dd>
<dt class="propname">addressbook_pagesize <span class="userconf">*</span></dt>
<dd>
<?php
$pagesize = $RCI->getprop('addressbook_pagesize');
if (!$pagesize) {
$pagesize = $RCI->getprop('pagesize');
}
$input_pagesize = new html_inputfield(array('name' => '_addressbook_pagesize', 'size' => 6, 'id' => "cfgabookpagesize"));
echo $input_pagesize->show($pagesize);
?>
<div>Show up to X items in the contacts list view.</div>
</dd>
<dt class="propname">prefer_html <span class="userconf">*</span></dt>
<dd>
<?php
$check_htmlview = new html_checkbox(array('name' => '_prefer_html', 'id' => "cfghtmlview", 'value' => 1));
echo $check_htmlview->show(intval($RCI->getprop('prefer_html')));
?>
<label for="cfghtmlview">Prefer displaying HTML messages</label><br />
</dd>
<dt class="propname">htmleditor <span class="userconf">*</span></dt>
<dd>
<label for="cfghtmlcompose">Compose HTML formatted messages</label>
<?php
$select_htmlcomp = new html_select(array('name' => '_htmleditor', 'id' => "cfghtmlcompose"));
$select_htmlcomp->add('never', 0);
$select_htmlcomp->add('always', 1);
$select_htmlcomp->add('on reply to HTML message only', 2);
echo $select_htmlcomp->show(intval($RCI->getprop('htmleditor')));
?>
</dd>
<dt class="propname">draft_autosave <span class="userconf">*</span></dt>
<dd>
<label for="cfgautosave">Save compose message every</label>
<?php
$select_autosave = new html_select(array('name' => '_draft_autosave', 'id' => 'cfgautosave'));
$select_autosave->add('never', 0);
foreach (array(1, 3, 5, 10) as $i => $min)
$select_autosave->add("$min min", $min*60);
echo $select_autosave->show(intval($RCI->getprop('draft_autosave')));
?>
</dd>
<dt class="propname">mdn_requests <span class="userconf">*</span></dt>
<dd>
<?php
$mdn_opts = array(
0 => 'ask the user',
1 => 'send automatically',
3 => 'send receipt to user contacts, otherwise ask the user',
4 => 'send receipt to user contacts, otherwise ignore',
2 => 'ignore',
);
$select_mdnreq = new html_select(array('name' => '_mdn_requests', 'id' => "cfgmdnreq"));
$select_mdnreq->add(array_values($mdn_opts), array_keys($mdn_opts));
echo $select_mdnreq->show(intval($RCI->getprop('mdn_requests')));
?>
<div>Behavior if a received message requests a message delivery notification (read receipt)</div>
</dd>
<dt class="propname">mime_param_folding <span class="userconf">*</span></dt>
<dd>
<?php
$select_param_folding = new html_select(array('name' => '_mime_param_folding', 'id' => "cfgmimeparamfolding"));
$select_param_folding->add('Full RFC 2231 (Roundcube, Thunderbird)', '0');
$select_param_folding->add('RFC 2047/2231 (MS Outlook, OE)', '1');
$select_param_folding->add('Full RFC 2047 (deprecated)', '2');
echo $select_param_folding->show(strval($RCI->getprop('mime_param_folding')));
?>
<div>How to encode attachment long/non-ascii names</div>
</dd>
</dl>
<p class="hint"><span class="userconf">*</span>&nbsp; These settings are defaults for the user preferences</p>
</fieldset>
<fieldset>
<legend>Plugins</legend>
<dl class="configblock" id="cgfblockdisplay">
<?php
$plugins = $RCI->list_plugins();
foreach ($plugins as $p) {
$p_check = new html_checkbox(array('name' => '_plugins_'.$p['name'], 'id' => 'cfgplugin_'.$p['name'], 'value' => $p['name']));
echo '<dt class="propname"><label>';
echo $p_check->show($p['enabled'] ? $p['name'] : 0);
echo '&nbsp;' . $p['name'] . '</label></dt><dd>';
echo '<label for="cfgplugin_'.$p['name'].'" class="hint">' . $p['desc'] . '</label><br/></dd>';
}
?>
</dl>
<p class="hint">Please consider checking dependencies of enabled plugins</p>
</fieldset>
<?php
echo '<p><input type="submit" name="submit" value="' . ($RCI->configured ? 'UPDATE' : 'CREATE') . ' CONFIG" ' . ($RCI->failures ? 'disabled' : '') . ' /></p>';
?>
</form>
diff --git a/installer/index.php b/installer/index.php
index 56b4122fe..51963c462 100644
--- a/installer/index.php
+++ b/installer/index.php
@@ -1,169 +1,169 @@
<?php
/**
+-------------------------------------------------------------------------+
| Roundcube Webmail setup tool |
| Version 1.4-git |
| |
- | Copyright (C) 2009-2017, The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU General Public License (with exceptions |
| for skins & plugins) as published by the Free Software Foundation, |
| either version 3 of the License, or (at your option) any later version. |
| |
| This file forms part of the Roundcube Webmail Software for which the |
| following exception is added: Plugins and Skins which merely make |
| function calls to the Roundcube Webmail Software, and for that purpose |
| include it by reference shall not be considered modifications of |
| the software. |
| |
| If you wish to use this file in another project or create a modified |
| version that will not be part of the Roundcube Webmail Software, you |
| may remove the exception above and use this source code under the |
| original version of the license. |
| |
| 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, see http://www.gnu.org/licenses/. |
| |
+-------------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-------------------------------------------------------------------------+
*/
ini_set('display_errors', 1);
define('INSTALL_PATH', realpath(__DIR__ . '/../').'/');
require INSTALL_PATH . 'program/include/iniset.php';
if (function_exists('session_start'))
session_start();
$RCI = rcmail_install::get_instance();
$RCI->load_config();
if (isset($_GET['_getconfig'])) {
$filename = 'config.inc.php';
if (!empty($_SESSION['config']) && $_GET['_getconfig'] == 2) {
$path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $filename;
@unlink($path);
file_put_contents($path, $_SESSION['config']);
exit;
}
else if (!empty($_SESSION['config'])) {
header('Content-type: text/plain');
header('Content-Disposition: attachment; filename="'.$filename.'"');
echo $_SESSION['config'];
exit;
}
else {
header('HTTP/1.0 404 Not found');
die("The requested configuration was not found. Please run the installer from the beginning.");
}
}
if ($RCI->configured && ($RCI->getprop('enable_installer') || $_SESSION['allowinstaller']) &&
!empty($_GET['_mergeconfig'])) {
$filename = 'config.inc.php';
header('Content-type: text/plain');
header('Content-Disposition: attachment; filename="'.$filename.'"');
$RCI->merge_config();
echo $RCI->create_config();
exit;
}
// go to 'check env' step if we have a local configuration
if ($RCI->configured && empty($_REQUEST['_step'])) {
header("Location: ./?_step=1");
exit;
}
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Roundcube Webmail Installer</title>
<meta name="Robots" content="noindex,nofollow" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="stylesheet" type="text/css" href="styles.css" />
<script type="text/javascript" src="client.js"></script>
</head>
<body>
<div id="banner">
<div class="banner-bg"></div>
<div class="banner-logo"><a href="http://roundcube.net"><img src="images/roundcube_logo.png" width="210" height="55" border="0" alt="Roundcube - open source webmail software" /></a></div>
</div>
<div id="topnav">
<a href="https://github.com/roundcube/roundcubemail/wiki/Installation">How-to Wiki</a>
</div>
<div id="content">
<?php
// exit if installation is complete
if ($RCI->configured && !$RCI->getprop('enable_installer') && !$_SESSION['allowinstaller']) {
// header("HTTP/1.0 404 Not Found");
if ($RCI->configured && $RCI->legacy_config) {
echo '<h2 class="error">Your configuration needs to be migrated!</h2>';
echo '<p>We changed the configuration files structure and your installation needs to be updated accordingly.</p>';
echo '<p>Please run the <tt>bin/update.sh</tt> script from the command line or set <p>&nbsp; <tt>$rcube_config[\'enable_installer\'] = true;</tt></p>';
echo ' in your RCUBE_CONFIG_DIR/main.inc.php to let the installer help you migrating it.</p>';
}
else {
echo '<h2 class="error">The installer is disabled!</h2>';
echo '<p>To enable it again, set <tt>$config[\'enable_installer\'] = true;</tt> in RCUBE_CONFIG_DIR/config.inc.php</p>';
}
echo '</div></body></html>';
exit;
}
?>
<h1>Roundcube Webmail Installer</h1>
<ol id="progress">
<?php
$include_steps = array(
1 => './check.php',
2 => './config.php',
3 => './test.php',
);
if (!in_array($RCI->step, array_keys($include_steps))) {
$RCI->step = 1;
}
foreach (array('Check environment', 'Create config', 'Test config') as $i => $item) {
$j = $i + 1;
$link = ($RCI->step >= $j || $RCI->configured) ? '<a href="./index.php?_step='.$j.'">' . rcube::Q($item) . '</a>' : rcube::Q($item);
printf('<li class="step%d%s">%s</li>', $j+1, $RCI->step > $j ? ' passed' : ($RCI->step == $j ? ' current' : ''), $link);
}
?>
</ol>
<?php
include $include_steps[$RCI->step];
?>
</div>
<div id="footer">
Installer by the Roundcube Dev Team. Copyright &copy; 2008-2012 – Published under the GNU Public License;&nbsp;
Icons by <a href="http://famfamfam.com">famfamfam</a>
</div>
</body>
</html>
diff --git a/installer/test.php b/installer/test.php
index 5c503ee6e..40d7a39d8 100644
--- a/installer/test.php
+++ b/installer/test.php
@@ -1,453 +1,466 @@
<?php
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+ +-----------------------------------------------------------------------+
+*/
if (!class_exists('rcmail_install', false) || !is_object($RCI)) {
die("Not allowed! Please open installer/index.php instead.");
}
?>
<form action="index.php?_step=3" method="post">
<h3>Check config file</h3>
<?php
if ($read_config = is_readable(RCUBE_CONFIG_DIR . 'defaults.inc.php')) {
$config = $RCI->load_config_file(RCUBE_CONFIG_DIR . 'defaults.inc.php');
if (!empty($config)) {
$RCI->pass('defaults.inc.php');
}
else {
$RCI->fail('defaults.inc.php', 'Syntax error');
}
}
else {
$RCI->fail('defaults.inc.php', 'Unable to read default config file?');
}
echo '<br />';
if ($read_config = is_readable(RCUBE_CONFIG_DIR . 'config.inc.php')) {
$config = $RCI->load_config_file(RCUBE_CONFIG_DIR . 'config.inc.php');
if (!empty($config)) {
$RCI->pass('config.inc.php');
}
else {
$RCI->fail('config.inc.php', 'Syntax error');
}
}
else {
$RCI->fail('config.inc.php', 'Unable to read file. Did you create the config file?');
}
echo '<br />';
if ($RCI->configured && ($messages = $RCI->check_config())) {
if (is_array($messages['replaced'])) {
echo '<h3 class="warning">Replaced config options</h3>';
echo '<p class="hint">The following config options have been replaced or renamed. ';
echo 'Please update them accordingly in your config files.</p>';
echo '<ul class="configwarings">';
foreach ($messages['replaced'] as $msg) {
echo html::tag('li', null, html::span('propname', $msg['prop']) .
' was replaced by ' . html::span('propname', $msg['replacement']));
}
echo '</ul>';
}
if (is_array($messages['obsolete'])) {
echo '<h3>Obsolete config options</h3>';
echo '<p class="hint">You still have some obsolete or inexistent properties set. This isn\'t a problem but should be noticed.</p>';
echo '<ul class="configwarings">';
foreach ($messages['obsolete'] as $msg) {
echo html::tag('li', null, html::span('propname', $msg['prop']) . ($msg['name'] ? ':&nbsp;' . $msg['name'] : ''));
}
echo '</ul>';
}
echo '<p class="suggestion">OK, lazy people can download the updated config file here: ';
echo html::a(array('href' => './?_mergeconfig=1'), 'config.inc.php') . ' &nbsp;';
echo "</p>";
if (is_array($messages['dependencies'])) {
echo '<h3 class="warning">Dependency check failed</h3>';
echo '<p class="hint">Some of your configuration settings require other options to be configured or additional PHP modules to be installed</p>';
echo '<ul class="configwarings">';
foreach ($messages['dependencies'] as $msg) {
echo html::tag('li', null, html::span('propname', $msg['prop']) . ': ' . $msg['explain']);
}
echo '</ul>';
}
}
?>
<h3>Check if directories are writable</h3>
<p>Roundcube may need to write/save files into these directories</p>
<?php
$dirs[] = $RCI->config['temp_dir'] ? $RCI->config['temp_dir'] : 'temp';
if ($RCI->config['log_driver'] != 'syslog')
$dirs[] = $RCI->config['log_dir'] ? $RCI->config['log_dir'] : 'logs';
foreach ($dirs as $dir) {
$dirpath = rcube_utils::is_absolute_path($dir) ? $dir : INSTALL_PATH . $dir;
if (is_writable(realpath($dirpath))) {
$RCI->pass($dir);
$pass = true;
}
else {
$RCI->fail($dir, 'not writeable for the webserver');
}
echo '<br />';
}
if (!$pass) {
echo '<p class="hint">Use <tt>chmod</tt> or <tt>chown</tt> to grant write privileges to the webserver</p>';
}
?>
<h3>Check DB config</h3>
<?php
$db_working = false;
if ($RCI->configured) {
if (!empty($RCI->config['db_dsnw'])) {
$DB = rcube_db::factory($RCI->config['db_dsnw'], '', false);
$DB->set_debug((bool)$RCI->config['sql_debug']);
$DB->db_connect('w');
if (!($db_error_msg = $DB->is_error())) {
$RCI->pass('DSN (write)');
echo '<br />';
$db_working = true;
}
else {
$RCI->fail('DSN (write)', $db_error_msg);
echo '<p class="hint">Make sure that the configured database exists and that the user has write privileges<br />';
echo 'DSN: ' . $RCI->config['db_dsnw'] . '</p>';
}
}
else {
$RCI->fail('DSN (write)', 'not set');
}
}
else {
$RCI->fail('DSN (write)', 'Could not read config file');
}
// initialize db with schema found in /SQL/*
if ($db_working && $_POST['initdb']) {
if (!($success = $RCI->init_db($DB))) {
$db_working = false;
echo '<p class="warning">Please try to inizialize the database manually as described in the INSTALL guide.
Make sure that the configured database extists and that the user as write privileges</p>';
}
}
else if ($db_working && $_POST['updatedb']) {
if (!($success = $RCI->update_db($_POST['version']))) {
echo '<p class="warning">Database schema update failed.</p>';
}
}
// test database
if ($db_working) {
$db_read = $DB->query("SELECT count(*) FROM " . $DB->quote_identifier($RCI->config['db_prefix'] . 'users'));
if ($DB->is_error()) {
$RCI->fail('DB Schema', "Database not initialized");
echo '<p><input type="submit" name="initdb" value="Initialize database" /></p>';
$db_working = false;
}
else if ($err = $RCI->db_schema_check($DB, $update = !empty($_POST['updatedb']))) {
$RCI->fail('DB Schema', "Database schema differs");
echo '<ul style="margin:0"><li>' . join("</li>\n<li>", $err) . "</li></ul>";
$select = $RCI->versions_select(array('name' => 'version'));
$select->add('0.9 or newer', '');
echo '<p class="suggestion">You should run the update queries to get the schema fixed.<br/><br/>Version to update from: ' . $select->show('') . '&nbsp;<input type="submit" name="updatedb" value="Update" /></p>';
$db_working = false;
}
else {
$RCI->pass('DB Schema');
echo '<br />';
}
}
// more database tests
if ($db_working) {
// write test
$insert_id = md5(uniqid());
$db_write = $DB->query("INSERT INTO " . $DB->quote_identifier($RCI->config['db_prefix'] . 'session')
. " (`sess_id`, `changed`, `ip`, `vars`) VALUES (?, ".$DB->now().", '127.0.0.1', 'foo')", $insert_id);
if ($db_write) {
$RCI->pass('DB Write');
$DB->query("DELETE FROM " . $DB->quote_identifier($RCI->config['db_prefix'] . 'session')
. " WHERE `sess_id` = ?", $insert_id);
}
else {
$RCI->fail('DB Write', $RCI->get_error());
}
echo '<br />';
// check timezone settings
$tz_db = 'SELECT ' . $DB->unixtimestamp($DB->now()) . ' AS tz_db';
$tz_db = $DB->query($tz_db);
$tz_db = $DB->fetch_assoc($tz_db);
$tz_db = (int) $tz_db['tz_db'];
$tz_local = (int) time();
$tz_diff = $tz_local - $tz_db;
// sometimes db and web servers are on separate hosts, so allow a 30 minutes delta
if (abs($tz_diff) > 1800) {
$RCI->fail('DB Time', "Database time differs {$td_ziff}s from PHP time");
}
else {
$RCI->pass('DB Time');
}
}
?>
<h3>Test filetype detection</h3>
<?php
if ($errors = $RCI->check_mime_detection()) {
$RCI->fail('Fileinfo/mime_content_type configuration');
if (!empty($RCI->config['mime_magic'])) {
echo '<p class="hint">Try setting the <tt>mime_magic</tt> config option to <tt>null</tt>.</p>';
}
else {
echo '<p class="hint">Check the <a href="http://www.php.net/manual/en/function.finfo-open.php">Fileinfo functions</a> of your PHP installation.<br/>';
echo 'The path to the magic.mime file can be set using the <tt>mime_magic</tt> config option in Roundcube.</p>';
}
}
else {
$RCI->pass('Fileinfo/mime_content_type configuration');
echo "<br/>";
}
if ($errors = $RCI->check_mime_extensions()) {
$RCI->fail('Mimetype to file extension mapping');
echo '<p class="hint">Please set a valid path to your webserver\'s mime.types file to the <tt>mime_types</tt> config option.<br/>';
echo 'If you can\'t find such a file, download it from <a href="http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types">svn.apache.org</a>.</p>';
}
else {
$RCI->pass('Mimetype to file extension mapping');
echo "<br/>";
}
$user = $RCI->getprop('smtp_user', '(none)');
$pass = $RCI->getprop('smtp_pass', '(none)');
if ($user == '%u') {
$user_field = new html_inputfield(array('name' => '_smtp_user', 'id' => 'smtp_user'));
$user = $user_field->show($_POST['_smtp_user']);
}
if ($pass == '%p') {
$pass_field = new html_passwordfield(array('name' => '_smtp_pass', 'id' => 'smtp_pass'));
$pass = $pass_field->show();
}
?>
<h3>Test SMTP config</h3>
<p>
<table>
<tbody>
<tr>
<td><label for="smtp_server">Server</label></td>
<td><?php echo rcube_utils::parse_host($RCI->getprop('smtp_server', 'localhost')); ?></td>
</tr>
<tr>
<td><label for="smtp_port">Port</label></td>
<td><?php echo $RCI->getprop('smtp_port'); ?></td>
</tr>
<tr>
<td><label for="smtp_user">Username</label></td>
<td><?php echo $user; ?></td>
</tr>
<tr>
<td><label for="smtp_pass">Password</label></td>
<td><?php echo $pass; ?></td>
</tr>
</tbody>
</table>
</p>
<?php
$from_field = new html_inputfield(array('name' => '_from', 'id' => 'sendmailfrom'));
$to_field = new html_inputfield(array('name' => '_to', 'id' => 'sendmailto'));
if (isset($_POST['sendmail'])) {
echo '<p>Trying to send email...<br />';
$from = idn_to_ascii(trim($_POST['_from']));
$to = idn_to_ascii(trim($_POST['_to']));
if (preg_match('/^' . $RCI->email_pattern . '$/i', $from) &&
preg_match('/^' . $RCI->email_pattern . '$/i', $to)
) {
$headers = array(
'From' => $from,
'To' => $to,
'Subject' => 'Test message from Roundcube',
);
$body = 'This is a test to confirm that Roundcube can send email.';
// send mail using configured SMTP server
$CONFIG = $RCI->config;
if (!empty($_POST['_smtp_user'])) {
$CONFIG['smtp_user'] = $_POST['_smtp_user'];
}
if (!empty($_POST['_smtp_pass'])) {
$CONFIG['smtp_pass'] = $_POST['_smtp_pass'];
}
$mail_object = new Mail_mime();
$send_headers = $mail_object->headers($headers);
$head = $mail_object->txtHeaders($send_headers);
$SMTP = new rcube_smtp();
$SMTP->connect(rcube_utils::parse_host($RCI->getprop('smtp_server')),
$RCI->getprop('smtp_port'), $CONFIG['smtp_user'], $CONFIG['smtp_pass']);
$status = $SMTP->send_mail($headers['From'], $headers['To'], $head, $body);
$smtp_response = $SMTP->get_response();
if ($status) {
$RCI->pass('SMTP send');
}
else {
$RCI->fail('SMTP send', join('; ', $smtp_response));
}
}
else {
$RCI->fail('SMTP send', 'Invalid sender or recipient');
}
echo '</p>';
}
?>
<table>
<tbody>
<tr>
<td><label for="sendmailfrom">Sender</label></td>
<td><?php echo $from_field->show($_POST['_from']); ?></td>
</tr>
<tr>
<td><label for="sendmailto">Recipient</label></td>
<td><?php echo $to_field->show($_POST['_to']); ?></td>
</tr>
</tbody>
</table>
<p><input type="submit" name="sendmail" value="Send test mail" /></p>
<h3>Test IMAP config</h3>
<?php
$default_hosts = $RCI->get_hostlist();
if (!empty($default_hosts)) {
$host_field = new html_select(array('name' => '_host', 'id' => 'imaphost'));
$host_field->add($default_hosts);
}
else {
$host_field = new html_inputfield(array('name' => '_host', 'id' => 'imaphost'));
}
$user_field = new html_inputfield(array('name' => '_user', 'id' => 'imapuser'));
$pass_field = new html_passwordfield(array('name' => '_pass', 'id' => 'imappass'));
?>
<table>
<tbody>
<tr>
<td><label for="imaphost">Server</label></td>
<td><?php echo $host_field->show($_POST['_host']); ?></td>
</tr>
<tr>
<td>Port</td>
<td><?php echo $RCI->getprop('default_port'); ?></td>
</tr>
<tr>
<td><label for="imapuser">Username</label></td>
<td><?php echo $user_field->show($_POST['_user']); ?></td>
</tr>
<tr>
<td><label for="imappass">Password</label></td>
<td><?php echo $pass_field->show(); ?></td>
</tr>
</tbody>
</table>
<?php
if (isset($_POST['imaptest']) && !empty($_POST['_host']) && !empty($_POST['_user'])) {
echo '<p>Connecting to ' . rcube::Q($_POST['_host']) . '...<br />';
$imap_host = trim($_POST['_host']);
$imap_port = $RCI->getprop('default_port');
$a_host = parse_url($imap_host);
if ($a_host['host']) {
$imap_host = $a_host['host'];
$imap_ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null;
if (isset($a_host['port']))
$imap_port = $a_host['port'];
else if ($imap_ssl && $imap_ssl != 'tls' && (!$imap_port || $imap_port == 143))
$imap_port = 993;
}
$imap_host = idn_to_ascii($imap_host);
$imap_user = idn_to_ascii($_POST['_user']);
$imap = new rcube_imap(null);
$imap->set_options(array(
'auth_type' => $RCI->getprop('imap_auth_type'),
'debug' => $RCI->getprop('imap_debug'),
'socket_options' => $RCI->getprop('imap_conn_options'),
));
if ($imap->connect($imap_host, $imap_user, $_POST['_pass'], $imap_port, $imap_ssl)) {
$RCI->pass('IMAP connect', 'SORT capability: ' . ($imap->get_capability('SORT') ? 'yes' : 'no'));
$imap->close();
}
else {
$RCI->fail('IMAP connect', $RCI->get_error());
}
}
?>
<p><input type="submit" name="imaptest" value="Check login" /></p>
</form>
<hr />
<p class="warning">
After completing the installation and the final tests please <b>remove</b> the whole
installer folder from the document root of the webserver or make sure that
<tt>enable_installer</tt> option in <tt>config.inc.php</tt> is disabled.<br />
<br />
These files may expose sensitive configuration data like server passwords and encryption keys
to the public. Make sure you cannot access this installer from your browser.
</p>
diff --git a/plugins/acl/acl.js b/plugins/acl/acl.js
index 7cfed90fd..97975a299 100644
--- a/plugins/acl/acl.js
+++ b/plugins/acl/acl.js
@@ -1,390 +1,403 @@
/**
* ACL plugin script
+ *
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this file.
+ *
+ * Copyright (c) The Roundcube Dev Team
+ *
+ * The JavaScript code in this page is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * @licend The above is the entire license notice
+ * for the JavaScript code in this file.
*/
if (window.rcmail) {
rcmail.addEventListener('init', function() {
if (rcmail.gui_objects.acltable) {
rcmail.acl_list_init();
// enable autocomplete on user input
if (rcmail.env.acl_users_source) {
var inst = rcmail.is_framed() ? parent.rcmail : rcmail;
inst.init_address_input_events($('#acluser'), {action:'settings/plugin.acl-autocomplete'});
// pass config settings and localized texts to autocomplete context
inst.set_env({ autocomplete_max:rcmail.env.autocomplete_max, autocomplete_min_length:rcmail.env.autocomplete_min_length });
inst.add_label('autocompletechars', rcmail.labels.autocompletechars);
inst.add_label('autocompletemore', rcmail.labels.autocompletemore);
// fix inserted value
inst.addEventListener('autocomplete_insert', function(e) {
if (e.field.id != 'acluser')
return;
e.field.value = e.insert.replace(/[ ,;]+$/, '');
});
}
}
rcmail.enable_command('acl-create', 'acl-save', 'acl-cancel', 'acl-mode-switch', true);
rcmail.enable_command('acl-delete', 'acl-edit', false);
if (rcmail.env.acl_advanced)
$('#acl-switch').addClass('selected').find('input').prop('checked', true);
});
}
// Display new-entry form
rcube_webmail.prototype.acl_create = function()
{
this.acl_init_form();
}
// Display ACL edit form
rcube_webmail.prototype.acl_edit = function()
{
// @TODO: multi-row edition
var id = this.acl_list.get_single_selection();
if (id)
this.acl_init_form(id);
}
// ACL entry delete
rcube_webmail.prototype.acl_delete = function()
{
var users = this.acl_get_usernames();
if (users && users.length) {
this.confirm_dialog(this.get_label('acl.deleteconfirm'), 'delete', function(e, ref) {
ref.http_post('settings/plugin.acl', {
_act: 'delete',
_user: users.join(','),
_mbox: rcmail.env.mailbox
}, ref.set_busy(true, 'acl.deleting'));
});
}
}
// Save ACL data
rcube_webmail.prototype.acl_save = function()
{
var data, type, rights = '', user = $('#acluser', this.acl_form).val();
$((this.env.acl_advanced ? '#advancedrights :checkbox' : '#simplerights :checkbox'), this.acl_form).map(function() {
if (this.checked)
rights += this.value;
});
if (type = $('input:checked[name=usertype]', this.acl_form).val()) {
if (type != 'user')
user = type;
}
if (!user) {
this.alert_dialog(this.get_label('acl.nouser'));
return;
}
if (!rights) {
this.alert_dialog(this.get_label('acl.norights'));
return;
}
data = {
_act: 'save',
_user: user,
_acl: rights,
_mbox: this.env.mailbox
}
if (this.acl_id) {
data._old = this.acl_id;
}
this.http_post('settings/plugin.acl', data, this.set_busy(true, 'acl.saving'));
}
// Cancel/Hide form
rcube_webmail.prototype.acl_cancel = function()
{
this.ksearch_blur();
this.acl_popup.dialog('close');
}
// Update data after save (and hide form)
rcube_webmail.prototype.acl_update = function(o)
{
// delete old row
if (o.old)
this.acl_remove_row(o.old);
// make sure the same ID doesn't exist
else if (this.env.acl[o.id])
this.acl_remove_row(o.id);
// add new row
this.acl_add_row(o, true);
// hide autocomplete popup
this.ksearch_blur();
// hide form
this.acl_popup.dialog('close');
}
// Switch table display mode
rcube_webmail.prototype.acl_mode_switch = function(elem)
{
this.env.acl_advanced = !this.env.acl_advanced;
this.enable_command('acl-delete', 'acl-edit', false);
this.http_request('settings/plugin.acl', '_act=list'
+ '&_mode='+(this.env.acl_advanced ? 'advanced' : 'simple')
+ '&_mbox='+urlencode(this.env.mailbox),
this.set_busy(true, 'loading'));
}
// ACL table initialization
rcube_webmail.prototype.acl_list_init = function()
{
var method = this.env.acl_advanced ? 'addClass' : 'removeClass';
$('#acl-switch')[method]('selected');
$(this.gui_objects.acltable)[method]('advanced');
this.acl_list = new rcube_list_widget(this.gui_objects.acltable,
{multiselect: true, draggable: false, keyboard: true});
this.acl_list.addEventListener('select', function(o) { rcmail.acl_list_select(o); })
.addEventListener('dblclick', function(o) { rcmail.acl_list_dblclick(o); })
.addEventListener('keypress', function(o) { rcmail.acl_list_keypress(o); })
.init();
}
// ACL table row selection handler
rcube_webmail.prototype.acl_list_select = function(list)
{
rcmail.enable_command('acl-delete', list.get_selection().length > 0);
rcmail.enable_command('acl-edit', list.get_selection().length == 1);
list.focus();
}
// ACL table double-click handler
rcube_webmail.prototype.acl_list_dblclick = function(list)
{
this.acl_edit();
}
// ACL table keypress handler
rcube_webmail.prototype.acl_list_keypress = function(list)
{
if (list.key_pressed == list.ENTER_KEY)
this.command('acl-edit');
else if (list.key_pressed == list.DELETE_KEY || list.key_pressed == list.BACKSPACE_KEY)
if (!this.acl_form || !this.acl_form.is(':visible'))
this.command('acl-delete');
}
// Reloads ACL table
rcube_webmail.prototype.acl_list_update = function(html)
{
$(this.gui_objects.acltable).html(html);
this.acl_list_init();
}
// Returns names of users in selected rows
rcube_webmail.prototype.acl_get_usernames = function()
{
var users = [], n, len, cell, row,
list = this.acl_list,
selection = list.get_selection();
for (n=0, len=selection.length; n<len; n++) {
if (this.env.acl_specials.length && $.inArray(selection[n], this.env.acl_specials) >= 0) {
users.push(selection[n]);
}
else if (row = list.rows[selection[n]]) {
cell = $('td.user', row.obj);
if (cell.length == 1)
users.push(cell.text());
}
}
return users;
}
// Removes ACL table row
rcube_webmail.prototype.acl_remove_row = function(id)
{
var list = this.acl_list;
list.remove_row(id);
list.clear_selection();
// we don't need it anymore (remove id conflict)
$('#rcmrow'+id).remove();
this.env.acl[id] = null;
this.enable_command('acl-delete', list.get_selection().length > 0);
this.enable_command('acl-edit', list.get_selection().length == 1);
}
// Adds ACL table row
rcube_webmail.prototype.acl_add_row = function(o, sel)
{
var n, len, ids = [], spec = [], id = o.id, list = this.acl_list,
items = this.env.acl_advanced ? [] : this.env.acl_items,
table = this.gui_objects.acltable,
row = $('thead > tr', table).clone();
// Update new row
$('th', row).map(function() {
var td = $('<td>'),
title = $(this).attr('title'),
cl = this.className.replace(/^acl/, '');
if (title)
td.attr('title', title);
if (items && items[cl])
cl = items[cl];
if (cl == 'user')
td.addClass(cl).append($('<a>').text(o.username));
else
td.addClass(this.className + ' ' + rcmail.acl_class(o.acl, cl)).html('<span/>');
$(this).replaceWith(td);
});
row.attr('id', 'rcmrow'+id);
row = row.get(0);
this.env.acl[id] = o.acl;
// sorting... (create an array of user identifiers, then sort it)
for (n in this.env.acl) {
if (this.env.acl[n]) {
if (this.env.acl_specials.length && $.inArray(n, this.env.acl_specials) >= 0)
spec.push(n);
else
ids.push(n);
}
}
ids.sort();
// specials on the top
ids = spec.concat(ids);
// find current id
for (n=0, len=ids.length; n<len; n++)
if (ids[n] == id)
break;
// add row
if (n && n < len) {
$('#rcmrow'+ids[n-1]).after(row);
list.init_row(row);
list.rowcount++;
}
else
list.insert_row(row);
if (sel)
list.select_row(o.id);
}
// Initializes and shows ACL create/edit form
rcube_webmail.prototype.acl_init_form = function(id)
{
var ul, row, td, val = '', type = 'user', li_elements, body = $('body'),
adv_ul = $('#advancedrights'), sim_ul = $('#simplerights'),
name_input = $('#acluser'), type_list = $('#usertype');
if (!this.acl_form) {
var fn = function () { $('input[value="user"]').prop('checked', true); };
name_input.click(fn).keypress(fn);
}
this.acl_form = $('#aclform');
// Hide unused items
if (this.env.acl_advanced) {
adv_ul.show();
sim_ul.hide();
ul = adv_ul;
}
else {
sim_ul.show();
adv_ul.hide();
ul = sim_ul;
}
// initialize form fields
li_elements = $(':checkbox', ul);
li_elements.attr('checked', false);
if (id && (row = this.acl_list.rows[id])) {
row = row.obj;
li_elements.map(function() {
td = $('td.'+this.id, row);
if (td.length && td.hasClass('enabled'))
this.checked = true;
});
if (!this.env.acl_specials.length || $.inArray(id, this.env.acl_specials) < 0)
val = $('td.user', row).text();
else
type = id;
}
// mark read (lrs) rights by default
else {
li_elements.filter(function() { return this.id.match(/^acl([lrs]|read)$/); }).prop('checked', true);
}
name_input.val(val);
$('input[value='+type+']').prop('checked', true);
this.acl_id = id;
var buttons = {}, me = this, body = document.body;
buttons[this.get_label('save')] = function(e) { me.command('acl-save'); };
buttons[this.get_label('cancel')] = function(e) { me.command('acl-cancel'); };
// display it as popup
this.acl_popup = this.show_popup_dialog(
this.acl_form.show(),
id ? this.get_label('acl.editperms') : this.get_label('acl.newuser'),
buttons,
{
button_classes: ['mainaction submit', 'cancel'],
modal: true,
closeOnEscape: true,
close: function(e, ui) {
(me.is_framed() ? parent.rcmail : me).ksearch_hide();
me.acl_form.appendTo(body).hide();
$(this).remove();
window.focus(); // focus iframe
}
}
);
if (type == 'user')
name_input.focus();
else
$('input:checked', type_list).focus();
}
// Returns class name according to ACL comparison result
rcube_webmail.prototype.acl_class = function(acl1, acl2)
{
var i, len, found = 0;
acl1 = String(acl1);
acl2 = String(acl2);
for (i=0, len=acl2.length; i<len; i++)
if (acl1.indexOf(acl2[i]) > -1)
found++;
if (found == len)
return 'enabled';
else if (found)
return 'partial';
return 'disabled';
}
diff --git a/plugins/acl/acl.php b/plugins/acl/acl.php
index 74044ee74..73a0fdacb 100644
--- a/plugins/acl/acl.php
+++ b/plugins/acl/acl.php
@@ -1,790 +1,790 @@
<?php
/**
* Folders Access Control Lists Management (RFC4314, RFC2086)
*
* @author Aleksander Machniak <alec@alec.pl>
*
- * Copyright (C) 2011-2012, Kolab Systems AG
+ * Copyright (C) Kolab Systems AG
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class acl extends rcube_plugin
{
public $task = 'settings';
private $rc;
private $supported = null;
private $mbox;
private $ldap;
private $specials = array('anyone', 'anonymous');
/**
* Plugin initialization
*/
function init()
{
$this->rc = rcmail::get_instance();
// Register hooks
$this->add_hook('folder_form', array($this, 'folder_form'));
// Plugin actions
$this->register_action('plugin.acl', array($this, 'acl_actions'));
$this->register_action('plugin.acl-autocomplete', array($this, 'acl_autocomplete'));
}
/**
* Handler for plugin actions (AJAX)
*/
function acl_actions()
{
$action = trim(rcube_utils::get_input_value('_act', rcube_utils::INPUT_GPC));
// Connect to IMAP
$this->rc->storage_init();
// Load localization and configuration
$this->add_texts('localization/');
$this->load_config();
if ($action == 'save') {
$this->action_save();
}
else if ($action == 'delete') {
$this->action_delete();
}
else if ($action == 'list') {
$this->action_list();
}
// Only AJAX actions
$this->rc->output->send();
}
/**
* Handler for user login autocomplete request
*/
function acl_autocomplete()
{
$this->load_config();
$search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC, true);
$reqid = rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC);
$users = array();
$keys = array();
if ($this->init_ldap()) {
$max = (int) $this->rc->config->get('autocomplete_max', 15);
$mode = (int) $this->rc->config->get('addressbook_search_mode');
$this->ldap->set_pagesize($max);
$result = $this->ldap->search('*', $search, $mode);
foreach ($result->records as $record) {
$user = $record['uid'];
if (is_array($user)) {
$user = array_filter($user);
$user = $user[0];
}
if ($user) {
$display = rcube_addressbook::compose_search_name($record);
$user = array('name' => $user, 'display' => $display);
$users[] = $user;
$keys[] = $display ?: $user['name'];
}
}
if ($this->rc->config->get('acl_groups')) {
$prefix = $this->rc->config->get('acl_group_prefix');
$group_field = $this->rc->config->get('acl_group_field', 'name');
$result = $this->ldap->list_groups($search, $mode);
foreach ($result as $record) {
$group = $record['name'];
$group_id = is_array($record[$group_field]) ? $record[$group_field][0] : $record[$group_field];
if ($group) {
$users[] = array('name' => ($prefix ?: '') . $group_id, 'display' => $group, 'type' => 'group');
$keys[] = $group;
}
}
}
}
if (count($users)) {
// sort users index
asort($keys, SORT_LOCALE_STRING);
// re-sort users according to index
foreach ($keys as $idx => $val) {
$keys[$idx] = $users[$idx];
}
$users = array_values($keys);
}
$this->rc->output->command('ksearch_query_results', $users, $search, $reqid);
$this->rc->output->send();
}
/**
* Handler for 'folder_form' hook
*
* @param array $args Hook arguments array (form data)
*
* @return array Hook arguments array
*/
function folder_form($args)
{
$mbox_imap = $args['options']['name'];
$myrights = $args['options']['rights'];
// Edited folder name (empty in create-folder mode)
if (!strlen($mbox_imap)) {
return $args;
}
/*
// Do nothing on protected folders (?)
if ($args['options']['protected']) {
return $args;
}
*/
// Get MYRIGHTS
if (empty($myrights)) {
return $args;
}
// Load localization and include scripts
$this->load_config();
$this->specials = $this->rc->config->get('acl_specials', $this->specials);
$this->add_texts('localization/', array('deleteconfirm', 'norights',
'nouser', 'deleting', 'saving', 'newuser', 'editperms'));
$this->rc->output->add_label('save', 'cancel');
$this->include_script('acl.js');
$this->rc->output->include_script('list.js');
$this->include_stylesheet($this->local_skin_path() . '/acl.css');
// add Info fieldset if it doesn't exist
if (!isset($args['form']['props']['fieldsets']['info']))
$args['form']['props']['fieldsets']['info'] = array(
'name' => $this->rc->gettext('info'),
'content' => array());
// Display folder rights to 'Info' fieldset
$args['form']['props']['fieldsets']['info']['content']['myrights'] = array(
'label' => rcube::Q($this->gettext('myrights')),
'value' => $this->acl2text($myrights)
);
// Return if not folder admin
if (!in_array('a', $myrights)) {
return $args;
}
// The 'Sharing' tab
$this->mbox = $mbox_imap;
$this->rc->output->set_env('acl_users_source', (bool) $this->rc->config->get('acl_users_source'));
$this->rc->output->set_env('mailbox', $mbox_imap);
$this->rc->output->add_handlers(array(
'acltable' => array($this, 'templ_table'),
'acluser' => array($this, 'templ_user'),
'aclrights' => array($this, 'templ_rights'),
));
$this->rc->output->set_env('autocomplete_max', (int)$this->rc->config->get('autocomplete_max', 15));
$this->rc->output->set_env('autocomplete_min_length', $this->rc->config->get('autocomplete_min_length'));
$this->rc->output->add_label('autocompletechars', 'autocompletemore');
$args['form']['sharing'] = array(
'name' => rcube::Q($this->gettext('sharing')),
'content' => $this->rc->output->parse('acl.table', false, false),
);
return $args;
}
/**
* Creates ACL rights table
*
* @param array $attrib Template object attributes
*
* @return string HTML Content
*/
function templ_table($attrib)
{
if (empty($attrib['id']))
$attrib['id'] = 'acl-table';
$out = $this->list_rights($attrib);
$this->rc->output->add_gui_object('acltable', $attrib['id']);
return $out;
}
/**
* Creates ACL rights form (rights list part)
*
* @param array $attrib Template object attributes
*
* @return string HTML Content
*/
function templ_rights($attrib)
{
// Get supported rights
$supported = $this->rights_supported();
// give plugins the opportunity to adjust this list
$data = $this->rc->plugins->exec_hook('acl_rights_supported',
array('rights' => $supported, 'folder' => $this->mbox, 'labels' => array()));
$supported = $data['rights'];
// depending on server capability either use 'te' or 'd' for deleting msgs
$deleteright = implode(array_intersect(str_split('ted'), $supported));
$out = '';
$ul = '';
$input = new html_checkbox();
// Advanced rights
$attrib['id'] = 'advancedrights';
foreach ($supported as $key => $val) {
$id = "acl$val";
$ul .= html::tag('li', null,
$input->show('', array(
'name' => "acl[$val]", 'value' => $val, 'id' => $id))
. html::label(array('for' => $id, 'title' => $this->gettext('longacl'.$val)),
$this->gettext('acl'.$val)));
}
$out = html::tag('ul', $attrib, $ul, html::$common_attrib);
// Simple rights
$ul = '';
$attrib['id'] = 'simplerights';
$items = array(
'read' => 'lrs',
'write' => 'wi',
'delete' => $deleteright,
'other' => preg_replace('/[lrswi'.$deleteright.']/', '', implode($supported)),
);
// give plugins the opportunity to adjust this list
$data = $this->rc->plugins->exec_hook('acl_rights_simple',
array('rights' => $items, 'folder' => $this->mbox, 'labels' => array(), 'titles' => array()));
foreach ($data['rights'] as $key => $val) {
$id = "acl$key";
$ul .= html::tag('li', null,
$input->show('', array(
'name' => "acl[$val]", 'value' => $val, 'id' => $id))
. html::label(array('for' => $id, 'title' => $data['titles'][$key] ?: $this->gettext('longacl'.$key)),
$data['labels'][$key] ?: $this->gettext('acl'.$key)));
}
$out .= "\n" . html::tag('ul', $attrib, $ul, html::$common_attrib);
$this->rc->output->set_env('acl_items', $data['rights']);
return $out;
}
/**
* Creates ACL rights form (user part)
*
* @param array $attrib Template object attributes
*
* @return string HTML Content
*/
function templ_user($attrib)
{
// Create username input
$attrib['name'] = 'acluser';
$class = $attrib['class'];
unset($attrib['class']);
$textfield = new html_inputfield($attrib);
$fields['user'] = html::div('input-group',
html::span('input-group-prepend',
html::label(array('for' => $attrib['id'], 'class' => 'input-group-text'), $this->gettext('username')))
. ' ' . $textfield->show());
// Add special entries
if (!empty($this->specials)) {
foreach ($this->specials as $key) {
$fields[$key] = html::label(array('for' => 'id'.$key), $this->gettext($key));
}
}
$this->rc->output->set_env('acl_specials', $this->specials);
// Create list with radio buttons
if (count($fields) > 1) {
$ul = '';
$radio = new html_radiobutton(array('name' => 'usertype'));
foreach ($fields as $key => $val) {
$ul .= html::tag('li', null, $radio->show($key == 'user' ? 'user' : '',
array('value' => $key, 'id' => 'id'.$key))
. $val);
}
$out = html::tag('ul', array('id' => 'usertype', 'class' => $class), $ul, html::$common_attrib);
}
// Display text input alone
else {
$out = html::div($class, $fields['user']);
}
return $out;
}
/**
* Creates ACL rights table
*
* @param array $attrib Template object attributes
*
* @return string HTML Content
*/
private function list_rights($attrib=array())
{
// Get ACL for the folder
$acl = $this->rc->storage->get_acl($this->mbox);
if (!is_array($acl)) {
$acl = array();
}
// Keep special entries (anyone/anonymous) on top of the list
if (!empty($this->specials) && !empty($acl)) {
foreach ($this->specials as $key) {
if (isset($acl[$key])) {
$acl_special[$key] = $acl[$key];
unset($acl[$key]);
}
}
}
// Sort the list by username
uksort($acl, 'strnatcasecmp');
if (!empty($acl_special)) {
$acl = array_merge($acl_special, $acl);
}
// Get supported rights and build column names
$supported = $this->rights_supported();
// give plugins the opportunity to adjust this list
$data = $this->rc->plugins->exec_hook('acl_rights_supported',
array('rights' => $supported, 'folder' => $this->mbox, 'labels' => array()));
$supported = $data['rights'];
// depending on server capability either use 'te' or 'd' for deleting msgs
$deleteright = implode(array_intersect(str_split('ted'), $supported));
// Use advanced or simple (grouped) rights
$advanced = $this->rc->config->get('acl_advanced_mode');
if ($advanced) {
$items = array();
foreach ($supported as $sup) {
$items[$sup] = $sup;
}
}
else {
$items = array(
'read' => 'lrs',
'write' => 'wi',
'delete' => $deleteright,
'other' => preg_replace('/[lrswi'.$deleteright.']/', '', implode($supported)),
);
// give plugins the opportunity to adjust this list
$data = $this->rc->plugins->exec_hook('acl_rights_simple',
array('rights' => $items, 'folder' => $this->mbox, 'labels' => array()));
$items = $data['rights'];
}
// Create the table
$attrib['noheader'] = true;
$table = new html_table($attrib);
// Create table header
$table->add_header('user', $this->gettext('identifier'));
foreach (array_keys($items) as $key) {
$label = $data['labels'][$key] ?: $this->gettext('shortacl'.$key);
$table->add_header(array('class' => 'acl'.$key, 'title' => $label), $label);
}
$js_table = array();
foreach ($acl as $user => $rights) {
if ($this->rc->storage->conn->user == $user) {
continue;
}
// filter out virtual rights (c or d) the server may return
$userrights = array_intersect($rights, $supported);
$userid = rcube_utils::html_identifier($user);
if (!empty($this->specials) && in_array($user, $this->specials)) {
$user = $this->gettext($user);
}
$table->add_row(array('id' => 'rcmrow'.$userid));
$table->add('user', html::a(array('id' => 'rcmlinkrow'.$userid), rcube::Q($user)));
foreach ($items as $key => $right) {
$in = $this->acl_compare($userrights, $right);
switch ($in) {
case 2: $class = 'enabled'; break;
case 1: $class = 'partial'; break;
default: $class = 'disabled'; break;
}
$table->add('acl' . $key . ' ' . $class, '<span></span>');
}
$js_table[$userid] = implode($userrights);
}
$this->rc->output->set_env('acl', $js_table);
$this->rc->output->set_env('acl_advanced', $advanced);
$out = $table->show();
return $out;
}
/**
* Handler for ACL update/create action
*/
private function action_save()
{
$mbox = trim(rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true)); // UTF7-IMAP
$user = trim(rcube_utils::get_input_value('_user', rcube_utils::INPUT_POST));
$acl = trim(rcube_utils::get_input_value('_acl', rcube_utils::INPUT_POST));
$oldid = trim(rcube_utils::get_input_value('_old', rcube_utils::INPUT_POST));
$acl = array_intersect(str_split($acl), $this->rights_supported());
$users = $oldid ? array($user) : explode(',', $user);
$result = 0;
foreach ($users as $user) {
$user = trim($user);
$prefix = $this->rc->config->get('acl_groups') ? $this->rc->config->get('acl_group_prefix') : '';
if ($prefix && strpos($user, $prefix) === 0) {
$username = $user;
}
else if (!empty($this->specials) && in_array($user, $this->specials)) {
$username = $this->gettext($user);
}
else if (!empty($user)) {
if (!strpos($user, '@') && ($realm = $this->get_realm())) {
$user .= '@' . rcube_utils::idn_to_ascii(preg_replace('/^@/', '', $realm));
}
// Make sure it's valid email address to prevent from "disappearing folder"
// issue in Cyrus IMAP e.g. when the acl user identifier contains spaces inside.
if (strpos($user, '@') && !rcube_utils::check_email($user, false)) {
$user = null;
}
$username = $user;
}
if (!$acl || !$user || !strlen($mbox)) {
continue;
}
$user = $this->mod_login($user);
$username = $this->mod_login($username);
if ($user != $_SESSION['username'] && $username != $_SESSION['username']) {
if ($this->rc->storage->set_acl($mbox, $user, $acl)) {
$ret = array('id' => rcube_utils::html_identifier($user),
'username' => $username, 'acl' => implode($acl), 'old' => $oldid);
$this->rc->output->command('acl_update', $ret);
$result++;
}
}
}
if ($result) {
$this->rc->output->show_message($oldid ? 'acl.updatesuccess' : 'acl.createsuccess', 'confirmation');
}
else {
$this->rc->output->show_message($oldid ? 'acl.updateerror' : 'acl.createerror', 'error');
}
}
/**
* Handler for ACL delete action
*/
private function action_delete()
{
$mbox = trim(rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true)); //UTF7-IMAP
$user = trim(rcube_utils::get_input_value('_user', rcube_utils::INPUT_POST));
$user = explode(',', $user);
foreach ($user as $u) {
$u = trim($u);
if ($this->rc->storage->delete_acl($mbox, $u)) {
$this->rc->output->command('acl_remove_row', rcube_utils::html_identifier($u));
}
else {
$error = true;
}
}
if (!$error) {
$this->rc->output->show_message('acl.deletesuccess', 'confirmation');
}
else {
$this->rc->output->show_message('acl.deleteerror', 'error');
}
}
/**
* Handler for ACL list update action (with display mode change)
*/
private function action_list()
{
if (in_array('acl_advanced_mode', (array)$this->rc->config->get('dont_override'))) {
return;
}
$this->mbox = trim(rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GPC, true)); // UTF7-IMAP
$advanced = trim(rcube_utils::get_input_value('_mode', rcube_utils::INPUT_GPC));
$advanced = $advanced == 'advanced';
// Save state in user preferences
$this->rc->user->save_prefs(array('acl_advanced_mode' => $advanced));
$out = $this->list_rights();
$out = preg_replace(array('/^<table[^>]+>/', '/<\/table>$/'), '', $out);
$this->rc->output->command('acl_list_update', $out);
}
/**
* Creates <UL> list with descriptive access rights
*
* @param array $rights MYRIGHTS result
*
* @return string HTML content
*/
function acl2text($rights)
{
if (empty($rights)) {
return '';
}
$supported = $this->rights_supported();
$list = array();
$attrib = array(
'name' => 'rcmyrights',
'style' => 'margin:0; padding:0 15px;',
);
foreach ($supported as $right) {
if (in_array($right, $rights)) {
$list[] = html::tag('li', null, rcube::Q($this->gettext('acl' . $right)));
}
}
if (count($list) == count($supported))
return rcube::Q($this->gettext('aclfull'));
return html::tag('ul', $attrib, implode("\n", $list));
}
/**
* Compares two ACLs (according to supported rights)
*
* @param array $acl1 ACL rights array (or string)
* @param array $acl2 ACL rights array (or string)
*
* @param int Comparison result, 2 - full match, 1 - partial match, 0 - no match
*/
function acl_compare($acl1, $acl2)
{
if (!is_array($acl1)) $acl1 = str_split($acl1);
if (!is_array($acl2)) $acl2 = str_split($acl2);
$rights = $this->rights_supported();
$acl1 = array_intersect($acl1, $rights);
$acl2 = array_intersect($acl2, $rights);
$res = array_intersect($acl1, $acl2);
$cnt1 = count($res);
$cnt2 = count($acl2);
if ($cnt1 == $cnt2)
return 2;
else if ($cnt1)
return 1;
else
return 0;
}
/**
* Get list of supported access rights (according to RIGHTS capability)
*
* @return array List of supported access rights abbreviations
*/
function rights_supported()
{
if ($this->supported !== null) {
return $this->supported;
}
$capa = $this->rc->storage->get_capability('RIGHTS');
if (is_array($capa)) {
$rights = strtolower($capa[0]);
}
else {
$rights = 'cd';
}
return $this->supported = str_split('lrswi' . $rights . 'pa');
}
/**
* Username realm detection.
*
* @return string Username realm (domain)
*/
private function get_realm()
{
// When user enters a username without domain part, realm
// allows to add it to the username (and display correct username in the table)
if (isset($_SESSION['acl_username_realm'])) {
return $_SESSION['acl_username_realm'];
}
// find realm in username of logged user (?)
list($name, $domain) = explode('@', $_SESSION['username']);
// Use (always existent) ACL entry on the INBOX for the user to determine
// whether or not the user ID in ACL entries need to be qualified and how
// they would need to be qualified.
if (empty($domain)) {
$acl = $this->rc->storage->get_acl('INBOX');
if (is_array($acl)) {
$regexp = '/^' . preg_quote($_SESSION['username'], '/') . '@(.*)$/';
foreach (array_keys($acl) as $name) {
if (preg_match($regexp, $name, $matches)) {
$domain = $matches[1];
break;
}
}
}
}
return $_SESSION['acl_username_realm'] = $domain;
}
/**
* Initializes autocomplete LDAP backend
*/
private function init_ldap()
{
if ($this->ldap) {
return $this->ldap->ready;
}
// get LDAP config
$config = $this->rc->config->get('acl_users_source');
if (empty($config)) {
return false;
}
// not an array, use configured ldap_public source
if (!is_array($config)) {
$ldap_config = (array) $this->rc->config->get('ldap_public');
$config = $ldap_config[$config];
}
$uid_field = $this->rc->config->get('acl_users_field', 'mail');
$filter = $this->rc->config->get('acl_users_filter');
if (empty($uid_field) || empty($config)) {
return false;
}
// get name attribute
if (!empty($config['fieldmap'])) {
$name_field = $config['fieldmap']['name'];
}
// ... no fieldmap, use the old method
if (empty($name_field)) {
$name_field = $config['name_field'];
}
// add UID field to fieldmap, so it will be returned in a record with name
$config['fieldmap']['name'] = $name_field;
$config['fieldmap']['uid'] = $uid_field;
// search in UID and name fields
// $name_field can be in a form of <field>:<modifier> (#1490591)
$name_field = preg_replace('/:.*$/', '', $name_field);
$search = array_unique(array($name_field, $uid_field));
$config['search_fields'] = $search;
$config['required_fields'] = array($uid_field);
// set search filter
if ($filter) {
$config['filter'] = $filter;
}
// disable vlv
$config['vlv'] = false;
// Initialize LDAP connection
$this->ldap = new rcube_ldap($config,
$this->rc->config->get('ldap_debug'),
$this->rc->config->mail_domain($_SESSION['imap_host']));
return $this->ldap->ready;
}
/**
* Modify user login according to 'login_lc' setting
*/
protected function mod_login($user)
{
$login_lc = $this->rc->config->get('login_lc');
if ($login_lc === true || $login_lc == 2) {
$user = mb_strtolower($user);
}
// lowercase domain name
else if ($login_lc && strpos($user, '@')) {
list($local, $domain) = explode('@', $user);
$user = $local . '@' . mb_strtolower($domain);
}
return $user;
}
}
diff --git a/plugins/acl/localization/en_US.inc b/plugins/acl/localization/en_US.inc
index e35d22a3f..22c58b975 100644
--- a/plugins/acl/localization/en_US.inc
+++ b/plugins/acl/localization/en_US.inc
@@ -1,109 +1,107 @@
<?php
/*
+-----------------------------------------------------------------------+
- | plugins/acl/localization/<lang>.inc |
- | |
| Localization file of the Roundcube Webmail ACL plugin |
- | Copyright (C) 2012-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-----------------------------------------------------------------------+
For translation see https://www.transifex.com/projects/p/roundcube-webmail/resource/plugin-acl/
*/
$labels['sharing'] = 'Sharing';
$labels['myrights'] = 'Access Rights';
$labels['username'] = 'User:';
$labels['advanced'] = 'Advanced mode';
$labels['add'] = 'Add';
$labels['newuser'] = 'Add entry';
$labels['editperms'] = 'Edit permissions';
$labels['actions'] = 'Access right actions...';
$labels['anyone'] = 'All users (anyone)';
$labels['anonymous'] = 'Guests (anonymous)';
$labels['identifier'] = 'Identifier';
$labels['acll'] = 'Lookup';
$labels['aclr'] = 'Read messages';
$labels['acls'] = 'Keep Seen state';
$labels['aclw'] = 'Write flags';
$labels['acli'] = 'Insert (Copy into)';
$labels['aclp'] = 'Post';
$labels['aclc'] = 'Create subfolders';
$labels['aclk'] = 'Create subfolders';
$labels['acld'] = 'Delete messages';
$labels['aclt'] = 'Delete messages';
$labels['acle'] = 'Expunge';
$labels['aclx'] = 'Delete folder';
$labels['acla'] = 'Administer';
$labels['acln'] = 'Annotate messages';
$labels['aclfull'] = 'Full control';
$labels['aclother'] = 'Other';
$labels['aclread'] = 'Read';
$labels['aclwrite'] = 'Write';
$labels['acldelete'] = 'Delete';
$labels['shortacll'] = 'Lookup';
$labels['shortaclr'] = 'Read';
$labels['shortacls'] = 'Keep';
$labels['shortaclw'] = 'Write';
$labels['shortacli'] = 'Insert';
$labels['shortaclp'] = 'Post';
$labels['shortaclc'] = 'Create';
$labels['shortaclk'] = 'Create';
$labels['shortacld'] = 'Delete';
$labels['shortaclt'] = 'Delete';
$labels['shortacle'] = 'Expunge';
$labels['shortaclx'] = 'Folder delete';
$labels['shortacla'] = 'Administer';
$labels['shortacln'] = 'Annotate';
$labels['shortaclother'] = 'Other';
$labels['shortaclread'] = 'Read';
$labels['shortaclwrite'] = 'Write';
$labels['shortacldelete'] = 'Delete';
$labels['longacll'] = 'The folder is visible on lists and can be subscribed to';
$labels['longaclr'] = 'The folder can be opened for reading';
$labels['longacls'] = 'Messages Seen flag can be changed';
$labels['longaclw'] = 'Messages flags and keywords can be changed, except Seen and Deleted';
$labels['longacli'] = 'Messages can be written or copied to the folder';
$labels['longaclp'] = 'Messages can be posted to this folder';
$labels['longaclc'] = 'Folders can be created (or renamed) directly under this folder';
$labels['longaclk'] = 'Folders can be created (or renamed) directly under this folder';
$labels['longacld'] = 'Messages Delete flag can be changed';
$labels['longaclt'] = 'Messages Delete flag can be changed';
$labels['longacle'] = 'Messages can be expunged';
$labels['longaclx'] = 'The folder can be deleted or renamed';
$labels['longacla'] = 'The folder access rights can be changed';
$labels['longacln'] = 'Messages shared metadata (annotations) can be changed';
$labels['longaclfull'] = 'Full control including folder administration';
$labels['longaclread'] = 'The folder can be opened for reading';
$labels['longaclwrite'] = 'Messages can be marked, written or copied to the folder';
$labels['longacldelete'] = 'Messages can be deleted';
$labels['longaclother'] = 'Other access rights';
$labels['ariasummaryacltable'] = 'List of access rights';
$labels['arialabelaclactions'] = 'List actions';
$labels['arialabelaclform'] = 'Access rights form';
$messages['deleting'] = 'Deleting access rights...';
$messages['saving'] = 'Saving access rights...';
$messages['updatesuccess'] = 'Successfully changed access rights';
$messages['deletesuccess'] = 'Successfully deleted access rights';
$messages['createsuccess'] = 'Successfully added access rights';
$messages['updateerror'] = 'Unable to update access rights';
$messages['deleteerror'] = 'Unable to delete access rights';
$messages['createerror'] = 'Unable to add access rights';
$messages['deleteconfirm'] = 'Are you sure, you want to remove access rights of selected user(s)?';
$messages['norights'] = 'No rights has been specified!';
$messages['nouser'] = 'No username has been specified!';
?>
diff --git a/plugins/archive/archive.js b/plugins/archive/archive.js
index 6ae93fac0..f8b280412 100644
--- a/plugins/archive/archive.js
+++ b/plugins/archive/archive.js
@@ -1,80 +1,80 @@
/**
* Archive plugin script
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
- * Copyright (c) 2012-2016, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*/
function rcmail_archive(prop)
{
if (rcmail_is_archive())
return;
var post_data = rcmail.selection_post_data();
// exit if selection is empty
if (!post_data._uid)
return;
rcmail.show_contentframe(false);
// Disable message command buttons until a message is selected
rcmail.enable_command(rcmail.env.message_commands, false);
rcmail.enable_command('plugin.archive', false);
// let the server sort the messages to the according subfolders
rcmail.with_selected_messages('move', post_data, null, 'plugin.move2archive');
}
function rcmail_is_archive()
{
// check if current folder is an archive folder or one of its children
return rcmail.env.mailbox == rcmail.env.archive_folder
|| rcmail.env.mailbox.startsWith(rcmail.env.archive_folder + rcmail.env.delimiter);
}
// callback for app-onload event
if (window.rcmail) {
rcmail.addEventListener('init', function(evt) {
// register command (directly enable in message view mode)
rcmail.register_command('plugin.archive', rcmail_archive, rcmail.env.uid && !rcmail_is_archive());
// add event-listener to message list
if (rcmail.message_list)
rcmail.message_list.addEventListener('select', function(list) {
rcmail.enable_command('plugin.archive', list.get_selection().length > 0 && !rcmail_is_archive());
});
// set css style for archive folder
var li;
if (rcmail.env.archive_folder) {
// in Settings > Folders
if (rcmail.subscription_list)
li = rcmail.subscription_list.get_item(rcmail.env.archive_folder);
// in folders list
else
li = rcmail.get_folder_li(rcmail.env.archive_folder, '', true);
if (li) {
$(li).addClass('archive');
// in folder selector popup
rcmail.addEventListener('menu-open', function(p) {
if (p.name == 'folder-selector') {
$('a[rel="' + $('a', li).attr('rel') + '"]', p.obj).parent().addClass('archive');
}
});
}
}
});
}
diff --git a/plugins/archive/localization/en_US.inc b/plugins/archive/localization/en_US.inc
index 600d1ddc6..1ef8692f9 100644
--- a/plugins/archive/localization/en_US.inc
+++ b/plugins/archive/localization/en_US.inc
@@ -1,36 +1,34 @@
<?php
/*
+-----------------------------------------------------------------------+
- | plugins/archive/localization/<lang>.inc |
- | |
| Localization file of the Roundcube Webmail Archive plugin |
- | Copyright (C) 2016, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-----------------------------------------------------------------------+
For translation see https://www.transifex.com/projects/p/roundcube-webmail/resource/plugin-archive/
*/
$labels = array();
$labels['buttontext'] = 'Archive';
$labels['buttontitle'] = 'Archive this message';
$labels['archived'] = 'Successfully archived';
$labels['archivedreload'] = 'Successfully archived. Reload the page to see the new archive folders.';
$labels['archiveerror'] = 'Some messages could not be archived';
$labels['archivefolder'] = 'Archive';
$labels['settingstitle'] = 'Archive';
$labels['archivetype'] = 'Divide archive by';
$labels['archivetypeyear'] = 'Year (e.g. Archive/2012)';
$labels['archivetypemonth'] = 'Month (e.g. Archive/2012/06)';
$labels['archivetypetbmonth'] = 'Month - Thunderbird compatible (e.g. Archive/2012/2012-06)';
$labels['archivetypefolder'] = 'Original folder';
$labels['archivetypesender'] = 'Sender email';
$labels['unkownsender'] = 'unknown';
$labels['readonarchive'] = 'Mark the message as read on archive';
?>
diff --git a/plugins/attachment_reminder/attachment_reminder.js b/plugins/attachment_reminder/attachment_reminder.js
index 32d10e65c..973ff5561 100644
--- a/plugins/attachment_reminder/attachment_reminder.js
+++ b/plugins/attachment_reminder/attachment_reminder.js
@@ -1,88 +1,88 @@
/**
* Attachment Reminder plugin script
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
- * Copyright (c) 2013, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*/
function rcmail_get_compose_message()
{
var msg;
if (window.tinyMCE && (ed = tinyMCE.get(rcmail.env.composebody))) {
msg = ed.getContent();
msg = msg.replace(/<blockquote[^>]*>(.|[\r\n])*<\/blockquote>/gmi, '');
}
else {
msg = $('#' + rcmail.env.composebody).val();
msg = msg.replace(/^>.*$/gmi, '');
}
return msg;
};
function rcmail_check_message(msg)
{
var i, rx, keywords = rcmail.get_label('keywords', 'attachment_reminder').split(",").concat([".doc", ".pdf"]);
keywords = $.map(keywords, function(n) { return RegExp.escape(n); });
rx = new RegExp('(' + keywords.join('|') + ')', 'i');
return msg.search(rx) != -1;
};
function rcmail_have_attachments()
{
return rcmail.env.attachments && $('li', rcmail.gui_objects.attachmentlist).length;
};
function rcmail_attachment_reminder_dialog()
{
var buttons = {};
buttons[rcmail.get_label('addattachment')] = function() {
$(this).remove();
if (window.UI && UI.show_uploadform) // Larry skin
UI.show_uploadform();
else if (window.rcmail_ui && rcmail_ui.show_popup) // classic skin
rcmail_ui.show_popup('uploadmenu', true);
};
buttons[rcmail.get_label('send')] = function(e) {
$(this).remove();
rcmail.env.attachment_reminder = true;
rcmail.command('send', '', e);
};
rcmail.env.attachment_reminder = false;
rcmail.show_popup_dialog(
rcmail.get_label('attachment_reminder.forgotattachment'),
rcmail.get_label('attachment_reminder.missingattachment'),
buttons,
{button_classes: ['mainaction attach', 'send']}
);
};
if (window.rcmail) {
rcmail.addEventListener('beforesend', function(evt) {
var msg = rcmail_get_compose_message(),
subject = $('#compose-subject').val();
if (!rcmail.env.attachment_reminder && !rcmail_have_attachments()
&& (rcmail_check_message(msg) || rcmail_check_message(subject))
) {
rcmail_attachment_reminder_dialog();
return false;
}
});
}
diff --git a/plugins/attachment_reminder/attachment_reminder.php b/plugins/attachment_reminder/attachment_reminder.php
index dfc25abd1..77d774a48 100644
--- a/plugins/attachment_reminder/attachment_reminder.php
+++ b/plugins/attachment_reminder/attachment_reminder.php
@@ -1,83 +1,83 @@
<?php
/**
* Attachment Reminder
*
* A plugin that reminds a user to attach the files
*
* @author Thomas Yu - Sian, Liu
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2013 Thomas Yu - Sian, Liu
- * Copyright (C) 2013, Kolab Systems AG
+ * Copyright (C) Kolab Systems AG
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see <http://www.gnu.org/licenses/>
*/
class attachment_reminder extends rcube_plugin
{
public $task = 'mail|settings';
public $noajax = true;
function init()
{
$rcmail = rcube::get_instance();
if ($rcmail->task == 'mail' && $rcmail->action == 'compose') {
if ($rcmail->config->get('attachment_reminder')) {
$this->include_script('attachment_reminder.js');
$this->add_texts('localization/', array('keywords', 'forgotattachment', 'missingattachment'));
$rcmail->output->add_label('addattachment', 'send');
}
}
if ($rcmail->task == 'settings') {
$dont_override = $rcmail->config->get('dont_override', array());
if (!in_array('attachment_reminder', $dont_override)) {
$this->add_hook('preferences_list', array($this, 'prefs_list'));
$this->add_hook('preferences_save', array($this, 'prefs_save'));
}
}
}
function prefs_list($args)
{
if ($args['section'] == 'compose') {
$this->add_texts('localization/');
$reminder = rcube::get_instance()->config->get('attachment_reminder');
$field_id = 'rcmfd_attachment_reminder';
$checkbox = new html_checkbox(array('name' => '_attachment_reminder', 'id' => $field_id, 'value' => 1));
$args['blocks']['main']['options']['attachment_reminder'] = array(
'title' => html::label($field_id, rcube::Q($this->gettext('reminderoption'))),
'content' => $checkbox->show($reminder ? 1 : 0),
);
}
return $args;
}
function prefs_save($args)
{
if ($args['section'] == 'compose') {
$dont_override = rcube::get_instance()->config->get('dont_override', array());
if (!in_array('attachment_reminder', $dont_override)) {
$args['prefs']['attachment_reminder'] = !empty($_POST['_attachment_reminder']);
}
}
+
return $args;
}
-
}
diff --git a/plugins/attachment_reminder/localization/en_US.inc b/plugins/attachment_reminder/localization/en_US.inc
index bca2bf169..b1fdc9e37 100644
--- a/plugins/attachment_reminder/localization/en_US.inc
+++ b/plugins/attachment_reminder/localization/en_US.inc
@@ -1,23 +1,20 @@
<?php
/*
+-----------------------------------------------------------------------+
- | plugins/attachment_reminder/localization/<lang>.inc |
- | |
| Localization file of the Roundcube Webmail Archive plugin |
- | Copyright (C) 2013, The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-----------------------------------------------------------------------+
For translation see https://www.transifex.com/projects/p/roundcube-webmail/resource/plugin-attachment_reminder/
*/
$messages = array();
$messages['missingattachment'] = "Missing attachment?";
$messages['forgotattachment'] = "Did you forget to attach a file?";
$messages['reminderoption'] = "Remind about forgotten attachments";
$messages['keywords'] = "attachment,file,attach,attached,attaching,enclosed,CV,cover letter";
diff --git a/plugins/database_attachments/database_attachments.php b/plugins/database_attachments/database_attachments.php
index b06717458..bd9d681bd 100644
--- a/plugins/database_attachments/database_attachments.php
+++ b/plugins/database_attachments/database_attachments.php
@@ -1,194 +1,194 @@
<?php
/**
* Database Attachments
*
* This plugin which provides database backed storage for temporary
* attachment file handling. The primary advantage of this plugin
* is its compatibility with round-robin dns multi-server roundcube
* installations.
*
* This plugin relies on the core filesystem_attachments plugin
*
* @author Ziba Scott <ziba@umich.edu>
* @author Aleksander Machniak <alec@alec.pl>
*
- * Copyright (C) 2011-2018, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* 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.
*/
if (class_exists('filesystem_attachments', false) && !defined('TESTS_DIR')) {
die("Configuration issue. There can be only one enabled plugin for attachments handling");
}
require_once INSTALL_PATH . 'plugins/filesystem_attachments/filesystem_attachments.php';
class database_attachments extends filesystem_attachments
{
// Cache object
protected $cache;
// A prefix for the cache key used in the session and in the key field of the cache table
const PREFIX = "ATTACH";
/**
* Save a newly uploaded attachment
*/
function upload($args)
{
$args['status'] = false;
$cache = $this->get_cache();
$key = $this->_key($args);
$data = file_get_contents($args['path']);
if ($data === false) {
return $args;
}
$data = base64_encode($data);
$status = $cache->write($key, $data);
if ($status) {
$args['id'] = $key;
$args['status'] = true;
$args['path'] = null;
}
return $args;
}
/**
* Save an attachment from a non-upload source (draft or forward)
*/
function save($args)
{
$args['status'] = false;
$cache = $this->get_cache();
$key = $this->_key($args);
if ($args['path']) {
$args['data'] = file_get_contents($args['path']);
if ($args['data'] === false) {
return $args;
}
$args['path'] = null;
}
$data = base64_encode($args['data']);
$status = $cache->write($key, $data);
if ($status) {
$args['id'] = $key;
$args['status'] = true;
}
return $args;
}
/**
* Remove an attachment from storage
* This is triggered by the remove attachment button on the compose screen
*/
function remove($args)
{
$cache = $this->get_cache();
$status = $cache->remove($args['id']);
$args['status'] = true;
return $args;
}
/**
* When composing an html message, image attachments may be shown
* For this plugin, $this->get() will check the file and
* return it's contents
*/
function display($args)
{
return $this->get($args);
}
/**
* When displaying or sending the attachment the file contents are fetched
* using this method. This is also called by the attachment_display hook.
*/
function get($args)
{
$cache = $this->get_cache();
$data = $cache->read($args['id']);
if ($data !== null && $data !== false) {
$args['data'] = base64_decode($data);
$args['status'] = true;
}
else {
$args['status'] = false;
}
return $args;
}
/**
* Delete all temp files associated with this user
*/
function cleanup($args)
{
// check if cache object exist, it may be empty on session_destroy (#1489726)
if ($cache = $this->get_cache()) {
$cache->remove($args['group'], true);
}
}
/**
* Helper method to generate a unique key for the given attachment file
*/
protected function _key($args)
{
$uname = $args['path'] ?: $args['name'];
return $args['group'] . md5(time() . $uname . $_SESSION['user_id']);
}
/**
* Initialize and return cache object
*/
protected function get_cache()
{
if (!$this->cache) {
$this->load_config();
$rcmail = rcube::get_instance();
$ttl = 12 * 60 * 60; // default: 12 hours
$ttl = $rcmail->config->get('database_attachments_cache_ttl', $ttl);
$type = $rcmail->config->get('database_attachments_cache', 'db');
$prefix = self::PREFIX;
// Add session identifier to the prefix to prevent from removing attachments
// in other sessions of the same user (#1490542)
if ($id = session_id()) {
$prefix .= $id;
}
// Init SQL cache (disable cache data serialization)
$this->cache = $rcmail->get_cache($prefix, $type, $ttl, false);
}
return $this->cache;
}
}
diff --git a/plugins/emoticons/localization/en_US.inc b/plugins/emoticons/localization/en_US.inc
index 04b10964d..c1ab1dab8 100644
--- a/plugins/emoticons/localization/en_US.inc
+++ b/plugins/emoticons/localization/en_US.inc
@@ -1,23 +1,21 @@
<?php
/*
+-----------------------------------------------------------------------+
- | plugins/emoticons/localization/<lang>.inc |
- | |
| Localization file of the Roundcube Webmail Emoticons plugin |
- | Copyright (C) 2012-2015, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-----------------------------------------------------------------------+
For translation see https://www.transifex.com/projects/p/roundcube-webmail/resource/plugin-emoticons/
*/
$labels = array();
$labels['emoticonsdisplay'] = 'Display emoticons in plain text messages';
$labels['emoticonscompose'] = 'Enable emoticons';
?>
diff --git a/plugins/enigma/enigma.js b/plugins/enigma/enigma.js
index 94bcb2a0d..1bc65e749 100644
--- a/plugins/enigma/enigma.js
+++ b/plugins/enigma/enigma.js
@@ -1,703 +1,718 @@
-/* Enigma Plugin */
+/**
+ * Enigma plugin script
+ *
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this file.
+ *
+ * Copyright (c) The Roundcube Dev Team
+ *
+ * The JavaScript code in this page is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * @licend The above is the entire license notice
+ * for the JavaScript code in this file.
+ */
window.rcmail && rcmail.addEventListener('init', function(evt) {
if (rcmail.env.task == 'settings') {
if (rcmail.gui_objects.keyslist) {
rcmail.keys_list = new rcube_list_widget(rcmail.gui_objects.keyslist,
{multiselect:true, draggable:false, keyboard:true});
rcmail.keys_list
.addEventListener('select', function(o) { rcmail.enigma_keylist_select(o); })
.addEventListener('keypress', function(o) { rcmail.enigma_keylist_keypress(o); })
.init()
.focus();
rcmail.enigma_list();
rcmail.register_command('firstpage', function(props) { return rcmail.enigma_list_page('first'); });
rcmail.register_command('previouspage', function(props) { return rcmail.enigma_list_page('previous'); });
rcmail.register_command('nextpage', function(props) { return rcmail.enigma_list_page('next'); });
rcmail.register_command('lastpage', function(props) { return rcmail.enigma_list_page('last'); });
}
if (rcmail.env.action == 'plugin.enigmakeys') {
rcmail.register_command('search', function(props) {return rcmail.enigma_search(props); }, true);
rcmail.register_command('reset-search', function(props) {return rcmail.enigma_search_reset(props); }, true);
rcmail.register_command('plugin.enigma-import', function() { rcmail.enigma_import(); }, true);
rcmail.register_command('plugin.enigma-import-search', function() { rcmail.enigma_import_search(); }, true);
rcmail.register_command('plugin.enigma-key-export', function() { rcmail.enigma_export(); });
rcmail.register_command('plugin.enigma-key-export-selected', function() { rcmail.enigma_export(true); });
rcmail.register_command('plugin.enigma-key-import', function() { rcmail.enigma_key_import(); }, true);
rcmail.register_command('plugin.enigma-key-import-search', function() { rcmail.enigma_key_import_search(); }, true);
rcmail.register_command('plugin.enigma-key-delete', function(props) { return rcmail.enigma_delete(); });
rcmail.register_command('plugin.enigma-key-create', function(props) { return rcmail.enigma_key_create(); }, true);
rcmail.register_command('plugin.enigma-key-save', function(props) { return rcmail.enigma_key_create_save(); }, true);
rcmail.addEventListener('responseafterplugin.enigmakeys', function() {
rcmail.enable_command('plugin.enigma-key-export', rcmail.env.rowcount > 0);
rcmail.triggerEvent('listupdate', {list: rcmail.keys_list, rowcount: rcmail.env.rowcount});
});
if (rcmail.gui_objects.importform) {
// make sure Enter key in search input starts searching
// instead of submitting the form
$('#rcmimportsearch').keydown(function(e) {
if (e.which == 13) {
rcmail.enigma_import_search();
return false;
}
});
}
}
}
else if (rcmail.env.task == 'mail') {
if (rcmail.env.action == 'compose') {
rcmail.addEventListener('beforesend', function(props) { rcmail.enigma_beforesend_handler(props); })
.addEventListener('beforesavedraft', function(props) { rcmail.enigma_beforesavedraft_handler(props); });
$('input,label', $('#enigmamenu')).mouseup(function(e) {
// don't close the menu on mouse click inside
e.stopPropagation();
});
$('a.button.enigma').prop('tabindex', $('#messagetoolbar > a').first().prop('tabindex'));
$.each(['encrypt', 'sign'], function() {
var opt = this, input = $('#enigma' + opt + 'opt');
if (rcmail.env['enigma_force_' + opt]) {
input.prop('checked', true);
}
// Compose status bar in Elastic
if (window.UI && UI.compose_status) {
input.on('change', function() { UI.compose_status(opt, this.checked); }).trigger('change');
}
});
}
if (rcmail.env.enigma_password_request) {
rcmail.enigma_password_request(rcmail.env.enigma_password_request);
}
}
});
/*********************************************************/
/********* Enigma Settings/Keys/Certs UI *********/
/*********************************************************/
// Display key(s) import form
rcube_webmail.prototype.enigma_key_import = function()
{
var dialog = $('<iframe>').attr('src', this.url('plugin.enigmakeys', {_a: 'import', _framed: 1})),
import_func = function(e) {
var win = dialog[0].contentWindow;
win.rcmail.enigma_import();
};
this.enigma_import_dialog = this.simple_dialog(dialog, this.gettext('enigma.importkeys'), import_func, {
button: 'import',
width: 500,
height: 150
});
};
// Display key(s) search/import form
rcube_webmail.prototype.enigma_key_import_search = function()
{
var dialog = $('<iframe>').attr('src', this.url('plugin.enigmakeys', {_a: 'import-search', _framed: 1})),
search_func = function() {
var win = dialog[0].contentWindow;
win.rcmail.enigma_import_search();
};
this.enigma_import_dialog = this.simple_dialog(dialog, this.gettext('enigma.keyimportsearchlabel'), search_func, {
button: 'search',
width: 500,
height: 150
});
};
rcube_webmail.prototype.enigma_import_success = function()
{
var dialog = this.enigma_import_dialog || parent.rcmail.enigma_import_dialog;
dialog.dialog('destroy');
};
// Display key(s) generation form
rcube_webmail.prototype.enigma_key_create = function()
{
this.enigma_loadframe('&_action=plugin.enigmakeys&_a=create&_nav=hide');
};
// Generate key(s) and submit them
rcube_webmail.prototype.enigma_key_create_save = function()
{
var options, lock, users = [],
password = $('#key-pass').val(),
confirm = $('#key-pass-confirm').val(),
size = $('#key-size').val();
$('[name="identity[]"]:checked').each(function() {
users.push({name: $(this).data('name') || '', email: $(this).data('email')});
});
// validate the form
if (!password || !confirm) {
this.alert_dialog(this.get_label('enigma.formerror'));
return;
}
if (password != confirm) {
this.alert_dialog(this.get_label('enigma.passwordsdiffer'));
return;
}
if (!users.length) {
this.alert_dialog(this.get_label('enigma.noidentselected'));
return;
}
// generate keys
// use OpenPGP.js if browser supports required features
if (window.openpgp && (window.msCrypto || (window.crypto && (window.crypto.getRandomValues || window.crypto.subtle)))) {
lock = this.set_busy(true, 'enigma.keygenerating');
options = {
numBits: size,
userIds: users,
passphrase: password
};
openpgp.generateKey(options).then(function(keypair) {
// success
var post = {_a: 'import', _keys: keypair.privateKeyArmored, _generated: 1,
_passwd: password, _keyid: keypair.key.primaryKey.getFingerprint()};
// send request to server
rcmail.http_post('plugin.enigmakeys', post, lock);
}, function(error) {
// failure
console.error(error);
rcmail.set_busy(false, null, lock);
rcmail.display_message(rcmail.get_label('enigma.keygenerateerror'), 'error');
});
}
else {
rcmail.display_message(rcmail.get_label('enigma.keygennosupport'), 'error');
}
};
// Action executed after successful key generation and import
rcube_webmail.prototype.enigma_key_create_success = function()
{
parent.rcmail.enigma_list(1);
};
// Delete key(s)
rcube_webmail.prototype.enigma_delete = function()
{
var keys = this.keys_list.get_selection();
if (!keys.length)
return;
this.confirm_dialog(this.get_label('enigma.keyremoveconfirm'), 'delete', function(e, ref) {
var lock = ref.display_message(ref.get_label('enigma.keyremoving'), 'loading'),
post = {_a: 'delete', _keys: keys};
// send request to server
ref.http_post('plugin.enigmakeys', post, lock);
});
};
// Export key(s)
rcube_webmail.prototype.enigma_export = function(selected)
{
var priv = false,
list = this.keys_list,
keys = selected ? list.get_selection().join(',') : '*',
args = {_keys: keys};
if (!keys.length)
return;
// find out whether selected keys are private
if (keys == '*')
priv = true;
else
$.each(list.get_selection(), function() {
flags = $(list.rows[this].obj).data('flags');
if (flags && flags.indexOf('p') >= 0) {
priv = true;
return false;
}
});
// ask the user about including private key in the export
if (priv)
return this.show_popup_dialog(
this.get_label('enigma.keyexportprompt'),
this.get_label('enigma.exportkeys'),
[{
'class': 'export mainaction',
text: this.get_label('enigma.onlypubkeys'),
click: function(e) {
rcmail.enigma_export_submit(args);
$(this).remove();
}
},
{
'class': 'export',
text: this.get_label('enigma.withprivkeys'),
click: function(e) {
args._priv = 1;
rcmail.enigma_export_submit(args);
$(this).remove();
}
},
{
'class': 'cancel',
text: this.get_label('close'),
click: function(e) {
$(this).remove();
}
}],
{width: 500}
);
this.enigma_export_submit(args);
};
// Sumbitting request for key(s) export
// Done this way to handle password input
rcube_webmail.prototype.enigma_export_submit = function(data)
{
var id = 'keyexport-' + new Date().getTime(),
form = $('<form>').attr({target: id, method: 'post', style: 'display:none',
action: '?_action=plugin.enigmakeys&_task=settings&_a=export'}),
iframe = $('<iframe>').attr({name: id, style: 'display:none'})
form.append($('<input>').attr({name: '_token', value: this.env.request_token}));
$.each(data, function(i, v) {
form.append($('<input>').attr({name: i, value: v}));
});
iframe.appendTo(document.body);
form.appendTo(document.body).submit();
};
// Submit key(s) import form
rcube_webmail.prototype.enigma_import = function()
{
var form, file, lock,
id = 'keyimport-' + new Date().getTime();
if (form = this.gui_objects.importform) {
file = document.getElementById('rcmimportfile');
if (file && !file.value) {
this.alert_dialog(this.get_label('selectimportfile'));
return;
}
lock = this.set_busy(true, 'importwait');
$('<iframe>').attr({name: id, style: 'display:none'}).appendTo(document.body);
$(form).attr({target: id, action: this.add_url(form.action, '_unlock', lock)}).submit();
return true;
}
};
// Ssearch for key(s) for import
rcube_webmail.prototype.enigma_import_search = function()
{
var form, search;
if (form = this.gui_objects.importform) {
search = $('#rcmimportsearch').val();
if (!search) {
return;
}
this.enigma_find_publickey(search);
}
};
// list row selection handler
rcube_webmail.prototype.enigma_keylist_select = function(list)
{
var id = list.get_single_selection(), url;
if (id)
url = '&_action=plugin.enigmakeys&_a=info&_id=' + id;
this.enigma_loadframe(url);
this.enable_command('plugin.enigma-key-delete', 'plugin.enigma-key-export-selected', list.get_selection().length > 0);
};
rcube_webmail.prototype.enigma_keylist_keypress = function(list)
{
if (list.modkey == CONTROL_KEY)
return;
if (list.key_pressed == list.DELETE_KEY || list.key_pressed == list.BACKSPACE_KEY)
this.command('plugin.enigma-key-delete');
else if (list.key_pressed == 33)
this.command('previouspage');
else if (list.key_pressed == 34)
this.command('nextpage');
};
// load key frame
rcube_webmail.prototype.enigma_loadframe = function(url)
{
var win;
if (win = this.get_frame_window(this.env.contentframe)) {
if (!url) {
if (win.location && win.location.href.indexOf(this.env.blankpage) < 0)
win.location.href = this.env.blankpage;
if (this.env.frame_lock)
this.set_busy(false, null, this.env.frame_lock);
return;
}
this.env.frame_lock = this.set_busy(true, 'loading');
win.location.href = this.env.comm_path + '&_framed=1' + url;
}
};
// Search keys/certs
rcube_webmail.prototype.enigma_search = function(props)
{
if (!props && this.gui_objects.qsearchbox)
props = this.gui_objects.qsearchbox.value;
if (props || this.env.search_request) {
var params = {'_a': 'search', '_q': props},
lock = this.set_busy(true, 'searching');
// if (this.gui_objects.search_filter)
// addurl += '&_filter=' + this.gui_objects.search_filter.value;
this.env.current_page = 1;
this.enigma_loadframe();
this.enigma_clear_list();
this.http_post('plugin.enigmakeys', params, lock);
}
return false;
};
// Reset search filter and the list
rcube_webmail.prototype.enigma_search_reset = function(props)
{
var s = this.env.search_request;
this.reset_qsearch();
if (s) {
this.enigma_loadframe();
this.enigma_clear_list();
// refresh the list
this.enigma_list();
}
return false;
};
// Keys/certs listing
rcube_webmail.prototype.enigma_list = function(page, reset_frame)
{
if (this.is_framed())
return parent.rcmail.enigma_list(page, reset_frame);
var params = {'_a': 'list'},
lock = this.set_busy(true, 'loading');
this.env.current_page = page ? page : 1;
if (this.env.search_request)
params._q = this.env.search_request;
if (page)
params._p = page;
this.enigma_clear_list(reset_frame);
this.http_post('plugin.enigmakeys', params, lock);
};
// Change list page
rcube_webmail.prototype.enigma_list_page = function(page)
{
if (page == 'next')
page = this.env.current_page + 1;
else if (page == 'last')
page = this.env.pagecount;
else if (page == 'prev' && this.env.current_page > 1)
page = this.env.current_page - 1;
else if (page == 'first' && this.env.current_page > 1)
page = 1;
this.enigma_list(page);
};
// Remove list rows
rcube_webmail.prototype.enigma_clear_list = function(reset_frame)
{
if (reset_frame !== false)
this.enigma_loadframe();
if (this.keys_list)
this.keys_list.clear(true);
this.enable_command('plugin.enigma-key-delete', 'plugin.enigma-key-delete-selected', false);
this.triggerEvent('listupdate', {list: this.keys_list, rowcount: this.keys_list.rowcount});
};
// Adds a row to the list
rcube_webmail.prototype.enigma_add_list_row = function(r)
{
if (!this.gui_objects.keyslist || !this.keys_list)
return false;
var list = this.keys_list,
tbody = this.gui_objects.keyslist.tBodies[0],
rowcount = tbody.rows.length,
even = rowcount%2,
// for performance use DOM instead of jQuery here
row = document.createElement('tr'),
col = document.createElement('td');
row.id = 'rcmrow' + r.id;
row.className = 'message';
if (r.flags) $(row).data('flags', r.flags);
col.className = 'name';
col.innerHTML = r.name;
row.appendChild(col);
list.insert_row(row);
};
/*********************************************************/
/********* Enigma Message methods *********/
/*********************************************************/
// handle message send/save action
rcube_webmail.prototype.enigma_beforesend_handler = function(props)
{
this.env.last_action = 'send';
this.enigma_compose_handler(props);
};
rcube_webmail.prototype.enigma_beforesavedraft_handler = function(props)
{
this.env.last_action = 'savedraft';
this.enigma_compose_handler(props);
};
rcube_webmail.prototype.enigma_compose_handler = function(props)
{
var form = this.gui_objects.messageform;
// copy inputs from enigma menu to the form
$('#enigmamenu input').each(function() {
var id = this.id + '_cpy', input = $('#' + id);
if (!input.length) {
input = $(this).clone();
input.prop({id: id, type: 'hidden'}).appendTo(form);
}
input.val(this.checked ? '1' : '');
});
// disable signing when saving drafts
if (this.env.last_action == 'savedraft') {
$('input[name="_enigma_sign"]', form).val(0);
}
};
// Import attached keys/certs file
rcube_webmail.prototype.enigma_import_attachment = function(mime_id)
{
var lock = this.set_busy(true, 'loading'),
post = {_uid: this.env.uid, _mbox: this.env.mailbox, _part: mime_id};
this.http_post('plugin.enigmaimport', post, lock);
return false;
};
// password request popup
rcube_webmail.prototype.enigma_password_request = function(data)
{
if (!data || !data.keyid) {
return;
}
var ref = this,
msg = this.get_label('enigma.enterkeypass'),
myprompt = $('<div class="prompt">'),
myprompt_content = $('<p class="message">')
.appendTo(myprompt),
myprompt_input = $('<input>').attr({type: 'password', size: 30})
.keypress(function(e) {
if (e.which == 13)
(ref.is_framed() ? window.parent.$ : $)('.ui-dialog-buttonpane button.mainaction:visible').click();
})
.appendTo(myprompt);
data.key = data.keyid;
if (data.keyid.length > 8)
data.keyid = data.keyid.substr(data.keyid.length - 8);
$.each(['keyid', 'user'], function() {
msg = msg.replace('$' + this, data[this]);
});
myprompt_content.text(msg);
this.show_popup_dialog(myprompt, this.get_label('enigma.enterkeypasstitle'),
[{
text: this.get_label('ok'),
'class': 'mainaction save unlock',
click: function(e) {
e.stopPropagation();
var jq = ref.is_framed() ? window.parent.$ : $;
data.password = myprompt_input.val();
if (!data.password) {
myprompt_input.focus();
return;
}
ref.enigma_password_submit(data);
jq(this).remove();
}
},
{
text: this.get_label('cancel'),
'class': 'cancel',
click: function(e) {
var jq = ref.is_framed() ? window.parent.$ : $;
e.stopPropagation();
jq(this).remove();
}
}], {width: 400});
if (this.is_framed() && parent.rcmail.message_list) {
// this fixes bug when pressing Enter on "Save" button in the dialog
parent.rcmail.message_list.blur();
}
};
// submit entered password
rcube_webmail.prototype.enigma_password_submit = function(data)
{
var lock, form;
if (this.env.action == 'compose' && !data['compose-init']) {
return this.enigma_password_compose_submit(data);
}
else if (this.env.action == 'plugin.enigmakeys' && (form = this.gui_objects.importform)) {
if (!$('input[name="_keyid"]', form).length) {
$(form).append($('<input>').attr({type: 'hidden', name: '_keyid', value: data.key}))
.append($('<input>').attr({type: 'hidden', name: '_passwd', value: data.password}))
}
return this.enigma_import();
}
lock = data.nolock ? null : this.set_busy(true, 'loading');
form = $('<form>')
.attr({method: 'post', action: data.action || location.href, style: 'display:none'})
.append($('<input>').attr({type: 'hidden', name: '_keyid', value: data.key}))
.append($('<input>').attr({type: 'hidden', name: '_passwd', value: data.password}))
.append($('<input>').attr({type: 'hidden', name: '_token', value: this.env.request_token}))
.append($('<input>').attr({type: 'hidden', name: '_unlock', value: lock}));
// Additional form fields for request parameters
$.each(data, function(i, v) {
if (i.indexOf('input') == 0)
form.append($('<input>').attr({type: 'hidden', name: i.substring(5), value: v}))
});
if (data.iframe) {
var name = 'enigma_frame_' + (new Date()).getTime(),
frame = $('<iframe>').attr({style: 'display:none', name: name}).appendTo(document.body);
form.attr('target', name);
}
form.appendTo(document.body).submit();
};
// submit entered password - in mail compose page
rcube_webmail.prototype.enigma_password_compose_submit = function(data)
{
var form = this.gui_objects.messageform;
if (!$('input[name="_keyid"]', form).length) {
$(form).append($('<input>').attr({type: 'hidden', name: '_keyid', value: data.key}))
.append($('<input>').attr({type: 'hidden', name: '_passwd', value: data.password}));
}
else {
$('input[name="_keyid"]', form).val(data.key);
$('input[name="_passwd"]', form).val(data.password);
}
this.submit_messageform(this.env.last_action == 'savedraft');
};
// Display no-key error with key search button
rcube_webmail.prototype.enigma_key_not_found = function(data)
{
var params = {width: 500, dialogClass: 'error'},
buttons = [{
'class': 'mainaction search',
text: data.button,
click: function() {
$(this).remove();
rcmail.enigma_find_publickey(data.email);
}
}];
if (data.mode == 'encrypt') {
buttons.push({
'class': 'send',
text: rcmail.get_label('enigma.sendunencrypted'),
click: function(e) {
$(this).remove();
$('#enigmaencryptopt').prop('checked', false);
rcmail.command('send', {nocheck: true}, e.target, e.originalEvent);
}
});
}
buttons.push({
'class': 'cancel',
text: this.get_label('cancel'),
click: function() {
$(this).remove();
}
});
return this.show_popup_dialog(data.text, data.title, buttons, params);
};
// Search for a public key on the key server
rcube_webmail.prototype.enigma_find_publickey = function(email)
{
this.mailvelope_search_pubkeys([email],
function(status) {},
function(key) {
var lock = rcmail.set_busy(true, 'enigma.importwait'),
post = {_a: 'import', _keys: key};
if (rcmail.env.action == 'plugin.enigmakeys')
post._refresh = 1;
// send request to server
rcmail.http_post('plugin.enigmakeys', post, lock);
}
);
};
diff --git a/plugins/enigma/enigma.php b/plugins/enigma/enigma.php
index 5a5bbc03d..fda9338ba 100644
--- a/plugins/enigma/enigma.php
+++ b/plugins/enigma/enigma.php
@@ -1,585 +1,584 @@
<?php
/**
+-------------------------------------------------------------------------+
| Enigma Plugin for Roundcube |
| |
- | Copyright (C) 2010-2015 The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-------------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-------------------------------------------------------------------------+
*/
/**
* This class contains only hooks and action handlers.
* Most plugin logic is placed in enigma_engine and enigma_ui classes.
*/
class enigma extends rcube_plugin
{
public $task = 'mail|settings|cli';
public $rc;
public $engine;
public $ui;
private $env_loaded = false;
/**
* Plugin initialization.
*/
function init()
{
$this->rc = rcube::get_instance();
if ($this->rc->task == 'mail') {
// message parse/display hooks
$this->add_hook('message_part_structure', array($this, 'part_structure'));
$this->add_hook('message_part_body', array($this, 'part_body'));
$this->add_hook('message_body_prefix', array($this, 'status_message'));
$this->register_action('plugin.enigmaimport', array($this, 'import_file'));
$this->register_action('plugin.enigmakeys', array($this, 'preferences_ui'));
// load the Enigma plugin configuration
$this->load_config();
$enabled = $this->rc->config->get('enigma_encryption', true);
// message displaying
if ($this->rc->action == 'show' || $this->rc->action == 'preview' || $this->rc->action == 'print') {
$this->add_hook('message_load', array($this, 'message_load'));
$this->add_hook('template_object_messagebody', array($this, 'message_output'));
}
// message composing
else if ($enabled && $this->rc->action == 'compose') {
$this->add_hook('message_compose_body', array($this, 'message_compose'));
$this->load_ui();
$this->ui->init();
}
// message sending (and draft storing)
else if ($enabled && $this->rc->action == 'send') {
$this->add_hook('message_ready', array($this, 'message_ready'));
}
$this->password_handler();
}
else if ($this->rc->task == 'settings') {
// add hooks for Enigma settings
$this->add_hook('settings_actions', array($this, 'settings_actions'));
$this->add_hook('preferences_sections_list', array($this, 'preferences_sections_list'));
$this->add_hook('preferences_list', array($this, 'preferences_list'));
$this->add_hook('preferences_save', array($this, 'preferences_save'));
$this->add_hook('identity_form', array($this, 'identity_form'));
// register handler for keys/certs management
$this->register_action('plugin.enigmakeys', array($this, 'preferences_ui'));
// $this->register_action('plugin.enigmacerts', array($this, 'preferences_ui'));
$this->load_ui();
if (empty($_REQUEST['_framed']) || strpos($this->rc->action, 'plugin.enigma') === 0) {
$this->ui->add_css();
}
$this->password_handler();
}
else if ($this->rc->task == 'cli') {
$this->add_hook('user_delete_commit', array($this, 'user_delete'));
}
$this->add_hook('refresh', array($this, 'refresh'));
}
/**
* Plugin environment initialization.
*/
function load_env()
{
if ($this->env_loaded) {
return;
}
$this->env_loaded = true;
// Add include path for Enigma classes and drivers
$include_path = $this->home . '/lib' . PATH_SEPARATOR;
$include_path .= ini_get('include_path');
set_include_path($include_path);
// load the Enigma plugin configuration
$this->load_config();
// include localization (if wasn't included before)
$this->add_texts('localization/');
}
/**
* Plugin UI initialization.
*/
function load_ui($all = false)
{
if (!$this->ui) {
// load config/localization
$this->load_env();
// Load UI
$this->ui = new enigma_ui($this, $this->home);
}
if ($all) {
$this->ui->add_css();
$this->ui->add_js();
}
}
/**
* Plugin engine initialization.
*/
function load_engine()
{
if ($this->engine) {
return $this->engine;
}
// load config/localization
$this->load_env();
return $this->engine = new enigma_engine($this);
}
/**
* Handler for message_part_structure hook.
* Called for every part of the message.
*
* @param array Original parameters
*
* @return array Modified parameters
*/
function part_structure($p)
{
$this->load_engine();
return $this->engine->part_structure($p);
}
/**
* Handler for message_part_body hook.
* Called to get body of a message part.
*
* @param array Original parameters
*
* @return array Modified parameters
*/
function part_body($p)
{
$this->load_engine();
return $this->engine->part_body($p);
}
/**
* Handler for settings_actions hook.
* Adds Enigma settings section into preferences.
*
* @param array Original parameters
*
* @return array Modified parameters
*/
function settings_actions($args)
{
// add labels
$this->add_texts('localization/');
// register as settings action
$args['actions'][] = array(
'action' => 'plugin.enigmakeys',
'class' => 'enigma keys',
'label' => 'enigmakeys',
'title' => 'enigmakeys',
'domain' => 'enigma',
);
/*
$args['actions'][] = array(
'action' => 'plugin.enigmacerts',
'class' => 'enigma certs',
'label' => 'enigmacerts',
'title' => 'enigmacerts',
'domain' => 'enigma',
);
*/
return $args;
}
/**
* Handler for preferences_sections_list hook.
* Adds Encryption settings section into preferences sections list.
*
* @param array Original parameters
*
* @return array Modified parameters
*/
function preferences_sections_list($p)
{
$p['list']['enigma'] = array(
'id' => 'enigma', 'section' => $this->gettext('encryption'),
);
return $p;
}
/**
* Handler for preferences_list hook.
* Adds options blocks into Enigma settings sections in Preferences.
*
* @param array Original parameters
*
* @return array Modified parameters
*/
function preferences_list($p)
{
if ($p['section'] != 'enigma') {
return $p;
}
$no_override = array_flip((array)$this->rc->config->get('dont_override'));
$p['blocks']['main']['name'] = $this->gettext('mainoptions');
if (!isset($no_override['enigma_encryption'])) {
if (!$p['current']) {
$p['blocks']['main']['content'] = true;
return $p;
}
$field_id = 'rcmfd_enigma_encryption';
$input = new html_checkbox(array(
'name' => '_enigma_encryption',
'id' => $field_id,
'value' => 1,
));
$p['blocks']['main']['options']['enigma_encryption'] = array(
'title' => html::label($field_id, $this->gettext('supportencryption')),
'content' => $input->show(intval($this->rc->config->get('enigma_encryption'))),
);
}
if (!isset($no_override['enigma_signatures'])) {
if (!$p['current']) {
$p['blocks']['main']['content'] = true;
return $p;
}
$field_id = 'rcmfd_enigma_signatures';
$input = new html_checkbox(array(
'name' => '_enigma_signatures',
'id' => $field_id,
'value' => 1,
));
$p['blocks']['main']['options']['enigma_signatures'] = array(
'title' => html::label($field_id, $this->gettext('supportsignatures')),
'content' => $input->show(intval($this->rc->config->get('enigma_signatures'))),
);
}
if (!isset($no_override['enigma_decryption'])) {
if (!$p['current']) {
$p['blocks']['main']['content'] = true;
return $p;
}
$field_id = 'rcmfd_enigma_decryption';
$input = new html_checkbox(array(
'name' => '_enigma_decryption',
'id' => $field_id,
'value' => 1,
));
$p['blocks']['main']['options']['enigma_decryption'] = array(
'title' => html::label($field_id, $this->gettext('supportdecryption')),
'content' => $input->show(intval($this->rc->config->get('enigma_decryption'))),
);
}
if (!isset($no_override['enigma_sign_all'])) {
if (!$p['current']) {
$p['blocks']['main']['content'] = true;
return $p;
}
$field_id = 'rcmfd_enigma_sign_all';
$input = new html_checkbox(array(
'name' => '_enigma_sign_all',
'id' => $field_id,
'value' => 1,
));
$p['blocks']['main']['options']['enigma_sign_all'] = array(
'title' => html::label($field_id, $this->gettext('signdefault')),
'content' => $input->show($this->rc->config->get('enigma_sign_all') ? 1 : 0),
);
}
if (!isset($no_override['enigma_encrypt_all'])) {
if (!$p['current']) {
$p['blocks']['main']['content'] = true;
return $p;
}
$field_id = 'rcmfd_enigma_encrypt_all';
$input = new html_checkbox(array(
'name' => '_enigma_encrypt_all',
'id' => $field_id,
'value' => 1,
));
$p['blocks']['main']['options']['enigma_encrypt_all'] = array(
'title' => html::label($field_id, $this->gettext('encryptdefault')),
'content' => $input->show($this->rc->config->get('enigma_encrypt_all') ? 1 : 0),
);
}
if (!isset($no_override['enigma_attach_pubkey'])) {
if (!$p['current']) {
$p['blocks']['main']['content'] = true;
return $p;
}
$field_id = 'rcmfd_enigma_attach_pubkey';
$input = new html_checkbox(array(
'name' => '_enigma_attach_pubkey',
'id' => $field_id,
'value' => 1,
));
$p['blocks']['main']['options']['enigma_attach_pubkey'] = array(
'title' => html::label($field_id, $this->gettext('attachpubkeydefault')),
'content' => $input->show($this->rc->config->get('enigma_attach_pubkey') ? 1 : 0),
);
}
if (!isset($no_override['enigma_password_time'])) {
if (!$p['current']) {
$p['blocks']['main']['content'] = true;
return $p;
}
$field_id = 'rcmfd_enigma_password_time';
$select = new html_select(array('name' => '_enigma_password_time', 'id' => $field_id));
foreach (array(1, 5, 10, 15, 30) as $m) {
$label = $this->gettext(array('name' => 'nminutes', 'vars' => array('m' => $m)));
$select->add($label, $m);
}
$select->add($this->gettext('wholesession'), 0);
$p['blocks']['main']['options']['enigma_password_time'] = array(
'title' => html::label($field_id, $this->gettext('passwordtime')),
'content' => $select->show(intval($this->rc->config->get('enigma_password_time'))),
);
}
return $p;
}
/**
* Handler for preferences_save hook.
* Executed on Enigma settings form submit.
*
* @param array Original parameters
*
* @return array Modified parameters
*/
function preferences_save($p)
{
if ($p['section'] == 'enigma') {
$p['prefs'] = array(
'enigma_signatures' => (bool) rcube_utils::get_input_value('_enigma_signatures', rcube_utils::INPUT_POST),
'enigma_decryption' => (bool) rcube_utils::get_input_value('_enigma_decryption', rcube_utils::INPUT_POST),
'enigma_encryption' => (bool) rcube_utils::get_input_value('_enigma_encryption', rcube_utils::INPUT_POST),
'enigma_sign_all' => (bool) rcube_utils::get_input_value('_enigma_sign_all', rcube_utils::INPUT_POST),
'enigma_encrypt_all' => (bool) rcube_utils::get_input_value('_enigma_encrypt_all', rcube_utils::INPUT_POST),
'enigma_attach_pubkey' => (bool) rcube_utils::get_input_value('_enigma_attach_pubkey', rcube_utils::INPUT_POST),
'enigma_password_time' => intval(rcube_utils::get_input_value('_enigma_password_time', rcube_utils::INPUT_POST)),
);
}
return $p;
}
/**
* Handler for keys/certs management UI template.
*/
function preferences_ui()
{
$this->load_ui();
$this->ui->init();
}
/**
* Handler for 'identity_form' plugin hook.
*
* This will list private keys matching this identity
* and add a link to enigma key management action.
*
* @param array Original parameters
*
* @return array Modified parameters
*/
function identity_form($p)
{
if (isset($p['form']['encryption']) && !empty($p['record']['identity_id'])) {
$content = '';
// find private keys for this identity
if ($p['record']['email']) {
$listing = array();
$engine = $this->load_engine();
$keys = (array)$engine->list_keys($p['record']['email']);
foreach ($keys as $key) {
if ($key->get_type() === enigma_key::TYPE_KEYPAIR) {
$listing[] = html::tag('li', null,
html::tag('strong', 'uid', html::quote($key->id))
. ' ' . html::tag('span', 'identity', html::quote($key->name))
);
}
}
if (count($listing)) {
$content .= html::p(null, $this->gettext(array('name' => 'identitymatchingprivkeys', 'vars' => array('nr' => count($listing)))));
$content .= html::tag('ul', 'keylist', join('\n', $listing));
} else {
$content .= html::p(null, $this->gettext('identitynoprivkeys'));
}
}
// add button linking to enigma key management
$button_attr = array(
'class' => 'button',
'href' => $this->rc->url(array('action' => 'plugin.enigmakeys')),
'target' => '_parent',
);
$content .= html::p(null, html::a($button_attr, $this->gettext('managekeys')));
// rename class to avoid Mailvelope key management to kick in
$p['form']['encryption']['attrs'] = array('class' => 'enigma-identity-encryption');
// fill fieldset content with our stuff
$p['form']['encryption']['content'] = html::div('identity-encryption-block', $content);
}
return $p;
}
/**
* Handler for message_body_prefix hook.
* Called for every displayed (content) part of the message.
* Adds infobox about signature verification and/or decryption
* status above the body.
*
* @param array Original parameters
*
* @return array Modified parameters
*/
function status_message($p)
{
$this->load_ui();
return $this->ui->status_message($p);
}
/**
* Handler for message_load hook.
* Check message bodies and attachments for keys/certs.
*/
function message_load($p)
{
$this->load_ui();
return $this->ui->message_load($p);
}
/**
* Handler for template_object_messagebody hook.
* This callback function adds a box below the message content
* if there is a key/cert attachment available
*/
function message_output($p)
{
$this->load_ui();
return $this->ui->message_output($p);
}
/**
* Handler for attached keys/certs import
*/
function import_file()
{
$this->load_ui();
$this->ui->import_file();
}
/**
* Handle password submissions
*/
function password_handler()
{
$this->load_engine();
$this->engine->password_handler();
}
/**
* Handle message_ready hook (encryption/signing)
*/
function message_ready($p)
{
$this->load_ui();
return $this->ui->message_ready($p);
}
/**
* Handle message_compose_body hook
*/
function message_compose($p)
{
$this->load_ui();
return $this->ui->message_compose($p);
}
/**
* Handler for refresh hook.
*/
function refresh($p)
{
// calling enigma_engine constructor to remove passwords
// stored in session after expiration time
$this->load_engine();
return $p;
}
/**
* Handle delete_user_commit hook
*/
function user_delete($p)
{
$this->load_engine();
$p['abort'] = $p['abort'] || !$this->engine->delete_user_data($p['username']);
return $p;
}
}
diff --git a/plugins/enigma/lib/enigma_driver.php b/plugins/enigma/lib/enigma_driver.php
index bab3e9fc1..cf4a5bfe2 100644
--- a/plugins/enigma/lib/enigma_driver.php
+++ b/plugins/enigma/lib/enigma_driver.php
@@ -1,142 +1,141 @@
<?php
/**
+-------------------------------------------------------------------------+
| Abstract driver for the Enigma Plugin |
| |
- | Copyright (C) 2010-2015 The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-------------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-------------------------------------------------------------------------+
*/
abstract class enigma_driver
{
/**
* Class constructor.
*
* @param string User name (email address)
*/
abstract function __construct($user);
/**
* Driver initialization.
*
* @return mixed NULL on success, enigma_error on failure
*/
abstract function init();
/**
* Encryption (and optional signing).
*
* @param string Message body
* @param array List of keys (enigma_key objects)
* @param enigma_key Optional signing Key ID
*
* @return mixed Encrypted message or enigma_error on failure
*/
abstract function encrypt($text, $keys, $sign_key = null);
/**
* Decryption (and sig verification if sig exists).
*
* @param string Encrypted message
* @param array List of key-password
* @param enigma_signature Signature information (if available)
*
* @return mixed Decrypted message or enigma_error on failure
*/
abstract function decrypt($text, $keys = array(), &$signature = null);
/**
* Signing.
*
* @param string Message body
* @param enigma_key The signing key
* @param int Signing mode (enigma_engine::SIGN_*)
*
* @return mixed True on success or enigma_error on failure
*/
abstract function sign($text, $key, $mode = null);
/**
* Signature verification.
*
* @param string Message body
* @param string Signature, if message is of type PGP/MIME and body doesn't contain it
*
* @return mixed Signature information (enigma_signature) or enigma_error
*/
abstract function verify($text, $signature);
/**
* Key/Cert file import.
*
* @param string File name or file content
* @param bolean True if first argument is a filename
* @param array Optional key => password map
*
* @return mixed Import status array or enigma_error
*/
abstract function import($content, $isfile = false, $passwords = array());
/**
* Key/Cert export.
*
* @param string Key ID
* @param bool Include private key
* @param array Optional key => password map
*
* @return mixed Key content or enigma_error
*/
abstract function export($key, $with_private = false, $passwords = array());
/**
* Keys listing.
*
* @param string Optional pattern for key ID, user ID or fingerprint
*
* @return mixed Array of enigma_key objects or enigma_error
*/
abstract function list_keys($pattern = '');
/**
* Single key information.
*
* @param string Key ID, user ID or fingerprint
*
* @return mixed Key (enigma_key) object or enigma_error
*/
abstract function get_key($keyid);
/**
* Key pair generation.
*
* @param array Key/User data (name, email, password, size)
*
* @return mixed Key (enigma_key) object or enigma_error
*/
abstract function gen_key($data);
/**
* Key deletion.
*
* @param string Key ID
*
* @return mixed True on success or enigma_error
*/
abstract function delete_key($keyid);
/**
* Returns a name of the hash algorithm used for the last
* signing operation.
*
* @return string Hash algorithm name e.g. sha1
*/
abstract function signature_algorithm();
}
diff --git a/plugins/enigma/lib/enigma_driver_gnupg.php b/plugins/enigma/lib/enigma_driver_gnupg.php
index 7d20becf7..80753f2ec 100644
--- a/plugins/enigma/lib/enigma_driver_gnupg.php
+++ b/plugins/enigma/lib/enigma_driver_gnupg.php
@@ -1,755 +1,754 @@
<?php
/**
+-------------------------------------------------------------------------+
| GnuPG (PGP) driver for the Enigma Plugin |
| |
- | Copyright (C) 2010-2015 The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-------------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-------------------------------------------------------------------------+
*/
require_once 'Crypt/GPG.php';
class enigma_driver_gnupg extends enigma_driver
{
protected $rc;
protected $gpg;
protected $homedir;
protected $user;
protected $last_sig_algorithm;
protected $debug = false;
protected $db_files = array('pubring.gpg', 'secring.gpg', 'pubring.kbx');
function __construct($user)
{
$this->rc = rcmail::get_instance();
$this->user = $user;
}
/**
* Driver initialization and environment checking.
* Should only return critical errors.
*
* @return mixed NULL on success, enigma_error on failure
*/
function init()
{
$homedir = $this->rc->config->get('enigma_pgp_homedir');
$debug = $this->rc->config->get('enigma_debug');
$binary = $this->rc->config->get('enigma_pgp_binary');
$agent = $this->rc->config->get('enigma_pgp_agent');
$gpgconf = $this->rc->config->get('enigma_pgp_gpgconf');
if (!$homedir) {
return new enigma_error(enigma_error::INTERNAL,
"Option 'enigma_pgp_homedir' not specified");
}
// check if homedir exists (create it if not) and is readable
if (!file_exists($homedir)) {
return new enigma_error(enigma_error::INTERNAL,
"Keys directory doesn't exists: $homedir");
}
if (!is_writable($homedir)) {
return new enigma_error(enigma_error::INTERNAL,
"Keys directory isn't writeable: $homedir");
}
$homedir = $homedir . '/' . $this->user;
// check if user's homedir exists (create it if not) and is readable
if (!file_exists($homedir)) {
mkdir($homedir, 0700);
}
if (!file_exists($homedir)) {
return new enigma_error(enigma_error::INTERNAL,
"Unable to create keys directory: $homedir");
}
if (!is_writable($homedir)) {
return new enigma_error(enigma_error::INTERNAL,
"Unable to write to keys directory: $homedir");
}
$this->debug = $debug;
$this->homedir = $homedir;
$options = array('homedir' => $this->homedir);
if ($debug) {
$options['debug'] = array($this, 'debug');
}
if ($binary) {
$options['binary'] = $binary;
}
if ($agent) {
$options['agent'] = $agent;
}
if ($gpgconf) {
$options['gpgconf'] = $gpgconf;
}
$options['cipher-algo'] = $this->rc->config->get('enigma_pgp_cipher_algo');
$options['digest-algo'] = $this->rc->config->get('enigma_pgp_digest_algo');
// Create Crypt_GPG object
try {
$this->gpg = new Crypt_GPG($options);
}
catch (Exception $e) {
return $this->get_error_from_exception($e);
}
$this->db_sync();
}
/**
* Encryption (and optional signing).
*
* @param string Message body
* @param array List of keys (enigma_key objects)
* @param enigma_key Optional signing Key ID
*
* @return mixed Encrypted message or enigma_error on failure
*/
function encrypt($text, $keys, $sign_key = null)
{
try {
foreach ($keys as $key) {
$this->gpg->addEncryptKey($key->reference);
}
if ($sign_key) {
$this->gpg->addSignKey($sign_key->reference, $sign_key->password);
$res = $this->gpg->encryptAndSign($text, true);
$sigInfo = $this->gpg->getLastSignatureInfo();
$this->last_sig_algorithm = $sigInfo->getHashAlgorithmName();
return $res;
}
return $this->gpg->encrypt($text, true);
}
catch (Exception $e) {
return $this->get_error_from_exception($e);
}
}
/**
* Decrypt a message (and verify if signature found)
*
* @param string Encrypted message
* @param array List of key-password mapping
* @param enigma_signature Signature information (if available)
*
* @return mixed Decrypted message or enigma_error on failure
*/
function decrypt($text, $keys = array(), &$signature = null)
{
try {
foreach ($keys as $key => $password) {
$this->gpg->addDecryptKey($key, $password);
}
$result = $this->gpg->decryptAndVerify($text, true);
if (!empty($result['signatures'])) {
$signature = $this->parse_signature($result['signatures'][0]);
}
// EFAIL vulnerability mitigation (#6289)
// Handle MDC warning as an exception, this is the default for gpg 2.3.
if (method_exists($this->gpg, 'getWarnings')) {
foreach ($this->gpg->getWarnings() as $warning_msg) {
if (strpos($warning_msg, 'not integrity protected') !== false) {
return new enigma_error(enigma_error::NOMDC, ucfirst($warning_msg));
}
}
}
return $result['data'];
}
catch (Exception $e) {
return $this->get_error_from_exception($e);
}
}
/**
* Signing.
*
* @param string Message body
* @param enigma_key The key
* @param int Signing mode (enigma_engine::SIGN_*)
*
* @return mixed True on success or enigma_error on failure
*/
function sign($text, $key, $mode = null)
{
try {
$this->gpg->addSignKey($key->reference, $key->password);
$res = $this->gpg->sign($text, $mode, CRYPT_GPG::ARMOR_ASCII, true);
$sigInfo = $this->gpg->getLastSignatureInfo();
$this->last_sig_algorithm = $sigInfo->getHashAlgorithmName();
return $res;
}
catch (Exception $e) {
return $this->get_error_from_exception($e);
}
}
/**
* Signature verification.
*
* @param string Message body
* @param string Signature, if message is of type PGP/MIME and body doesn't contain it
*
* @return mixed Signature information (enigma_signature) or enigma_error
*/
function verify($text, $signature)
{
try {
$verified = $this->gpg->verify($text, $signature);
return $this->parse_signature($verified[0]);
}
catch (Exception $e) {
return $this->get_error_from_exception($e);
}
}
/**
* Key file import.
*
* @param string File name or file content
* @param bolean True if first argument is a filename
* @param array Optional key => password map
*
* @return mixed Import status array or enigma_error
*/
public function import($content, $isfile = false, $passwords = array())
{
try {
// GnuPG 2.1 requires secret key passphrases on import
foreach ($passwords as $keyid => $pass) {
$this->gpg->addPassphrase($keyid, $pass);
}
if ($isfile) {
$result = $this->gpg->importKeyFile($content);
}
else {
$result = $this->gpg->importKey($content);
}
$this->db_save();
return $result;
}
catch (Exception $e) {
return $this->get_error_from_exception($e);
}
}
/**
* Key export.
*
* @param string Key ID
* @param bool Include private key
* @param array Optional key => password map
*
* @return mixed Key content or enigma_error
*/
public function export($keyid, $with_private = false, $passwords = array())
{
try {
$key = $this->gpg->exportPublicKey($keyid, true);
if ($with_private) {
// GnuPG 2.1 requires secret key passphrases on export
foreach ($passwords as $_keyid => $pass) {
$this->gpg->addPassphrase($_keyid, $pass);
}
$priv = $this->gpg->exportPrivateKey($keyid, true);
$key .= $priv;
}
return $key;
}
catch (Exception $e) {
return $this->get_error_from_exception($e);
}
}
/**
* Keys listing.
*
* @param string Optional pattern for key ID, user ID or fingerprint
*
* @return mixed Array of enigma_key objects or enigma_error
*/
public function list_keys($pattern = '')
{
try {
$keys = $this->gpg->getKeys($pattern);
$result = array();
foreach ($keys as $idx => $key) {
$result[] = $this->parse_key($key);
unset($keys[$idx]);
}
return $result;
}
catch (Exception $e) {
return $this->get_error_from_exception($e);
}
}
/**
* Single key information.
*
* @param string Key ID, user ID or fingerprint
*
* @return mixed Key (enigma_key) object or enigma_error
*/
public function get_key($keyid)
{
$list = $this->list_keys($keyid);
if (is_array($list)) {
return $list[key($list)];
}
// error
return $list;
}
/**
* Key pair generation.
*
* @param array Key/User data (user, email, password, size)
*
* @return mixed Key (enigma_key) object or enigma_error
*/
public function gen_key($data)
{
try {
$debug = $this->rc->config->get('enigma_debug');
$keygen = new Crypt_GPG_KeyGenerator(array(
'homedir' => $this->homedir,
// 'binary' => '/usr/bin/gpg2',
'debug' => $debug ? array($this, 'debug') : false,
));
$key = $keygen
->setExpirationDate(0)
->setPassphrase($data['password'])
->generateKey($data['user'], $data['email']);
return $this->parse_key($key);
}
catch (Exception $e) {
return $this->get_error_from_exception($e);
}
}
/**
* Key deletion.
*
* @param string Key ID
*
* @return mixed True on success or enigma_error
*/
public function delete_key($keyid)
{
// delete public key
$result = $this->delete_pubkey($keyid);
// error handling
if ($result !== true) {
$code = $result->getCode();
// if not found, delete private key
if ($code == enigma_error::KEYNOTFOUND) {
$result = $this->delete_privkey($keyid);
}
// need to delete private key first
else if ($code == enigma_error::DELKEY) {
$result = $this->delete_privkey($keyid);
if ($result === true) {
$result = $this->delete_pubkey($keyid);
}
}
}
$this->db_save();
return $result;
}
/**
* Returns a name of the hash algorithm used for the last
* signing operation.
*
* @return string Hash algorithm name e.g. sha1
*/
public function signature_algorithm()
{
return $this->last_sig_algorithm;
}
/**
* Private key deletion.
*/
protected function delete_privkey($keyid)
{
try {
$this->gpg->deletePrivateKey($keyid);
return true;
}
catch (Exception $e) {
return $this->get_error_from_exception($e);
}
}
/**
* Public key deletion.
*/
protected function delete_pubkey($keyid)
{
try {
$this->gpg->deletePublicKey($keyid);
return true;
}
catch (Exception $e) {
return $this->get_error_from_exception($e);
}
}
/**
* Converts Crypt_GPG exception into Enigma's error object
*
* @param mixed Exception object
*
* @return enigma_error Error object
*/
protected function get_error_from_exception($e)
{
$data = array();
if ($e instanceof Crypt_GPG_KeyNotFoundException) {
$error = enigma_error::KEYNOTFOUND;
$data['id'] = $e->getKeyId();
}
else if ($e instanceof Crypt_GPG_BadPassphraseException) {
$error = enigma_error::BADPASS;
$data['bad'] = $e->getBadPassphrases();
$data['missing'] = $e->getMissingPassphrases();
}
else if ($e instanceof Crypt_GPG_NoDataException) {
$error = enigma_error::NODATA;
}
else if ($e instanceof Crypt_GPG_DeletePrivateKeyException) {
$error = enigma_error::DELKEY;
}
else {
$error = enigma_error::INTERNAL;
}
$msg = $e->getMessage();
return new enigma_error($error, $msg, $data);
}
/**
* Converts Crypt_GPG_Signature object into Enigma's signature object
*
* @param Crypt_GPG_Signature Signature object
*
* @return enigma_signature Signature object
*/
protected function parse_signature($sig)
{
$data = new enigma_signature();
$data->id = $sig->getId() ?: $sig->getKeyId();
$data->valid = $sig->isValid();
$data->fingerprint = $sig->getKeyFingerprint();
$data->created = $sig->getCreationDate();
$data->expires = $sig->getExpirationDate();
// In case of ERRSIG user may not be set
if ($user = $sig->getUserId()) {
$data->name = $user->getName();
$data->comment = $user->getComment();
$data->email = $user->getEmail();
}
return $data;
}
/**
* Converts Crypt_GPG_Key object into Enigma's key object
*
* @param Crypt_GPG_Key Key object
*
* @return enigma_key Key object
*/
protected function parse_key($key)
{
$ekey = new enigma_key();
foreach ($key->getUserIds() as $idx => $user) {
$id = new enigma_userid();
$id->name = $user->getName();
$id->comment = $user->getComment();
$id->email = $user->getEmail();
$id->valid = $user->isValid();
$id->revoked = $user->isRevoked();
$ekey->users[$idx] = $id;
}
$ekey->name = trim($ekey->users[0]->name . ' <' . $ekey->users[0]->email . '>');
// keep reference to Crypt_GPG's key for performance reasons
$ekey->reference = $key;
foreach ($key->getSubKeys() as $idx => $subkey) {
$skey = new enigma_subkey();
$skey->id = $subkey->getId();
$skey->revoked = $subkey->isRevoked();
$skey->created = $subkey->getCreationDate();
$skey->expires = $subkey->getExpirationDate();
$skey->fingerprint = $subkey->getFingerprint();
$skey->has_private = $subkey->hasPrivate();
$skey->algorithm = $subkey->getAlgorithm();
$skey->length = $subkey->getLength();
$skey->usage = $subkey->usage();
$ekey->subkeys[$idx] = $skey;
};
$ekey->id = $ekey->subkeys[0]->id;
return $ekey;
}
/**
* Syncronize keys database on multi-host setups
*/
protected function db_sync()
{
if (!$this->rc->config->get('enigma_multihost')) {
return;
}
$db = $this->rc->get_dbh();
$table = $db->table_name('filestore', true);
$files = array();
$result = $db->query(
"SELECT `file_id`, `filename`, `mtime` FROM $table WHERE `user_id` = ? AND `context` = ?",
$this->rc->user->ID, 'enigma');
while ($record = $db->fetch_assoc($result)) {
$file = $this->homedir . '/' . $record['filename'];
$mtime = @filemtime($file);
$files[] = $record['filename'];
if ($mtime < $record['mtime']) {
$data_result = $db->query("SELECT `data`, `mtime` FROM $table"
. " WHERE `file_id` = ?", $record['file_id']);
$data = $db->fetch_assoc($data_result);
$data = $data ? base64_decode($data['data']) : null;
if ($data === null || $data === false) {
rcube::raise_error(array(
'code' => 605, 'line' => __LINE__, 'file' => __FILE__,
'message' => "Enigma: Failed to sync $file ({$record['file_id']}). Decode error."
), true, false);
continue;
}
$tmpfile = $file . '.tmp';
if (file_put_contents($tmpfile, $data, LOCK_EX) === strlen($data)) {
rename($tmpfile, $file);
touch($file, $data_record['mtime']);
if ($this->debug) {
$this->debug("SYNC: Fetched file: $file");
}
}
else {
// error
@unlink($tmpfile);
rcube::raise_error(array(
'code' => 605, 'line' => __LINE__, 'file' => __FILE__,
'message' => "Enigma: Failed to sync $file."
), true, false);
}
}
}
// Remove files not in database
if (!$db->is_error($result)) {
foreach (array_diff($this->db_files_list(), $files) as $file) {
$file = $this->homedir . '/' . $file;
if (unlink($file)) {
if ($this->debug) {
$this->debug("SYNC: Removed file: $file");
}
}
}
}
// No records found, do initial sync if already have the keyring
if (!$db->is_error($result) && empty($file)) {
$this->db_save(true);
}
}
/**
* Save keys database for multi-host setups
*/
protected function db_save($is_empty = false)
{
if (!$this->rc->config->get('enigma_multihost')) {
return true;
}
$db = $this->rc->get_dbh();
$table = $db->table_name('filestore', true);
$records = array();
if (!$is_empty) {
$result = $db->query(
"SELECT `file_id`, `filename`, `mtime` FROM $table WHERE `user_id` = ? AND `context` = ?",
$this->rc->user->ID, 'enigma'
);
while ($record = $db->fetch_assoc($result)) {
$records[$record['filename']] = $record;
}
}
foreach ($this->db_files_list() as $filename) {
$file = $this->homedir . '/' . $filename;
$mtime = @filemtime($file);
$existing = $records[$filename];
unset($records[$filename]);
if ($mtime && (empty($existing) || $mtime > $existing['mtime'])) {
$data = file_get_contents($file);
$data = base64_encode($data);
$datasize = strlen($data);
if (empty($maxsize)) {
$maxsize = min($db->get_variable('max_allowed_packet', 1048500), 4*1024*1024) - 2000;
}
if ($datasize > $maxsize) {
rcube::raise_error(array(
'code' => 605, 'line' => __LINE__, 'file' => __FILE__,
'message' => "Enigma: Failed to save $file. Size exceeds max_allowed_packet."
), true, false);
continue;
}
if (empty($existing)) {
$result = $db->query(
"INSERT INTO $table (`user_id`, `context`, `filename`, `mtime`, `data`)"
. " VALUES(?, 'enigma', ?, ?, ?)",
$this->rc->user->ID, $filename, $mtime, $data);
}
else {
$result = $db->query(
"UPDATE $table SET `mtime` = ?, `data` = ? WHERE `file_id` = ?",
$mtime, $data, $existing['file_id']);
}
if ($db->is_error($result)) {
rcube::raise_error(array(
'code' => 605, 'line' => __LINE__, 'file' => __FILE__,
'message' => "Enigma: Failed to save $file into database."
), true, false);
break;
}
if ($this->debug) {
$this->debug("SYNC: Pushed file: $file");
}
}
}
// Delete removed files from database
foreach (array_keys($records) as $filename) {
$file = $this->homedir . '/' . $filename;
$result = $db->query("DELETE FROM $table WHERE `user_id` = ? AND `context` = ? AND `filename` = ?",
$this->rc->user->ID, 'enigma', $filename);
if ($db->is_error($result)) {
rcube::raise_error(array(
'code' => 605, 'line' => __LINE__, 'file' => __FILE__,
'message' => "Enigma: Failed to delete $file from database."
), true, false);
break;
}
if ($this->debug) {
$this->debug("SYNC: Removed file: $file");
}
}
}
/**
* Returns list of homedir files to backup
*/
protected function db_files_list()
{
$files = array();
foreach ($this->db_files as $file) {
if (file_exists($this->homedir . '/' . $file)) {
$files[] = $file;
}
}
foreach (glob($this->homedir . '/private-keys-v1.d/*.key') as $file) {
$files[] = ltrim(substr($file, strlen($this->homedir)), '/');
}
return $files;
}
/**
* Write debug info from Crypt_GPG to logs/enigma
*/
public function debug($line)
{
rcube::write_log('enigma', 'GPG: ' . $line);
}
}
diff --git a/plugins/enigma/lib/enigma_driver_phpssl.php b/plugins/enigma/lib/enigma_driver_phpssl.php
index e0a078461..be1277985 100644
--- a/plugins/enigma/lib/enigma_driver_phpssl.php
+++ b/plugins/enigma/lib/enigma_driver_phpssl.php
@@ -1,233 +1,232 @@
<?php
/**
+-------------------------------------------------------------------------+
| S/MIME driver for the Enigma Plugin |
| |
- | Copyright (C) 2010-2015 The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-------------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-------------------------------------------------------------------------+
*/
class enigma_driver_phpssl extends enigma_driver
{
private $rc;
private $homedir;
private $user;
function __construct($user)
{
$rcmail = rcmail::get_instance();
$this->rc = $rcmail;
$this->user = $user;
}
/**
* Driver initialization and environment checking.
* Should only return critical errors.
*
* @return mixed NULL on success, enigma_error on failure
*/
function init()
{
$homedir = $this->rc->config->get('enigma_smime_homedir', INSTALL_PATH . '/plugins/enigma/home');
if (!$homedir)
return new enigma_error(enigma_error::INTERNAL,
"Option 'enigma_smime_homedir' not specified");
// check if homedir exists (create it if not) and is readable
if (!file_exists($homedir))
return new enigma_error(enigma_error::INTERNAL,
"Keys directory doesn't exists: $homedir");
if (!is_writable($homedir))
return new enigma_error(enigma_error::INTERNAL,
"Keys directory isn't writeable: $homedir");
$homedir = $homedir . '/' . $this->user;
// check if user's homedir exists (create it if not) and is readable
if (!file_exists($homedir))
mkdir($homedir, 0700);
if (!file_exists($homedir))
return new enigma_error(enigma_error::INTERNAL,
"Unable to create keys directory: $homedir");
if (!is_writable($homedir))
return new enigma_error(enigma_error::INTERNAL,
"Unable to write to keys directory: $homedir");
$this->homedir = $homedir;
}
function encrypt($text, $keys, $sign_key = null)
{
}
function decrypt($text, $keys = array(), &$signature = null)
{
}
function sign($text, $key, $mode = null)
{
}
function verify($struct, $message)
{
// use common temp dir
$msg_file = rcube_utils::temp_filename('enigmsg');
$cert_file = rcube_utils::temp_filename('enigcrt');
$fh = fopen($msg_file, "w");
if ($struct->mime_id) {
$message->get_part_body($struct->mime_id, false, 0, $fh);
}
else {
$this->rc->storage->get_raw_body($message->uid, $fh);
}
fclose($fh);
// @TODO: use stored certificates
// try with certificate verification
$sig = openssl_pkcs7_verify($msg_file, 0, $cert_file);
$validity = true;
if ($sig !== true) {
// try without certificate verification
$sig = openssl_pkcs7_verify($msg_file, PKCS7_NOVERIFY, $cert_file);
$validity = enigma_error::UNVERIFIED;
}
if ($sig === true) {
$sig = $this->parse_sig_cert($cert_file, $validity);
}
else {
$errorstr = $this->get_openssl_error();
$sig = new enigma_error(enigma_error::INTERNAL, $errorstr);
}
// remove temp files
@unlink($msg_file);
@unlink($cert_file);
return $sig;
}
public function import($content, $isfile = false, $passwords = array())
{
}
public function export($key, $with_private = false, $passwords = array())
{
}
public function list_keys($pattern='')
{
}
public function get_key($keyid)
{
}
public function gen_key($data)
{
}
public function delete_key($keyid)
{
}
/**
* Returns a name of the hash algorithm used for the last
* signing operation.
*
* @return string Hash algorithm name e.g. sha1
*/
public function signature_algorithm()
{
}
/**
* Converts Crypt_GPG_Key object into Enigma's key object
*
* @param Crypt_GPG_Key Key object
*
* @return enigma_key Key object
*/
private function parse_key($key)
{
/*
$ekey = new enigma_key();
foreach ($key->getUserIds() as $idx => $user) {
$id = new enigma_userid();
$id->name = $user->getName();
$id->comment = $user->getComment();
$id->email = $user->getEmail();
$id->valid = $user->isValid();
$id->revoked = $user->isRevoked();
$ekey->users[$idx] = $id;
}
$ekey->name = trim($ekey->users[0]->name . ' <' . $ekey->users[0]->email . '>');
foreach ($key->getSubKeys() as $idx => $subkey) {
$skey = new enigma_subkey();
$skey->id = $subkey->getId();
$skey->revoked = $subkey->isRevoked();
$skey->created = $subkey->getCreationDate();
$skey->expires = $subkey->getExpirationDate();
$skey->fingerprint = $subkey->getFingerprint();
$skey->has_private = $subkey->hasPrivate();
$ekey->subkeys[$idx] = $skey;
};
$ekey->id = $ekey->subkeys[0]->id;
return $ekey;
*/
}
private function get_openssl_error()
{
$tmp = array();
while ($errorstr = openssl_error_string()) {
$tmp[] = $errorstr;
}
return join("\n", array_values($tmp));
}
private function parse_sig_cert($file, $validity)
{
$cert = openssl_x509_parse(file_get_contents($file));
if (empty($cert) || empty($cert['subject'])) {
$errorstr = $this->get_openssl_error();
return new enigma_error(enigma_error::INTERNAL, $errorstr);
}
$data = new enigma_signature();
$data->id = $cert['hash']; //?
$data->valid = $validity;
$data->fingerprint = $cert['serialNumber'];
$data->created = $cert['validFrom_time_t'];
$data->expires = $cert['validTo_time_t'];
$data->name = $cert['subject']['CN'];
// $data->comment = '';
$data->email = $cert['subject']['emailAddress'];
return $data;
}
}
diff --git a/plugins/enigma/lib/enigma_engine.php b/plugins/enigma/lib/enigma_engine.php
index efe163a70..b63fd7937 100644
--- a/plugins/enigma/lib/enigma_engine.php
+++ b/plugins/enigma/lib/enigma_engine.php
@@ -1,1411 +1,1410 @@
<?php
/**
+-------------------------------------------------------------------------+
| Engine of the Enigma Plugin |
| |
- | Copyright (C) 2010-2016 The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-------------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-------------------------------------------------------------------------+
*/
/**
* Enigma plugin engine.
*
* RFC2440: OpenPGP Message Format
* RFC3156: MIME Security with OpenPGP
* RFC3851: S/MIME
*/
class enigma_engine
{
private $rc;
private $enigma;
private $pgp_driver;
private $smime_driver;
private $password_time;
private $cache = array();
public $decryptions = array();
public $signatures = array();
public $encrypted_parts = array();
const ENCRYPTED_PARTIALLY = 100;
const SIGN_MODE_BODY = 1;
const SIGN_MODE_SEPARATE = 2;
const SIGN_MODE_MIME = 4;
const ENCRYPT_MODE_BODY = 1;
const ENCRYPT_MODE_MIME = 2;
const ENCRYPT_MODE_SIGN = 4;
/**
* Plugin initialization.
*/
function __construct($enigma)
{
$this->rc = rcmail::get_instance();
$this->enigma = $enigma;
$this->password_time = $this->rc->config->get('enigma_password_time') * 60;
// this will remove passwords from session after some time
if ($this->password_time) {
$this->get_passwords();
}
}
/**
* PGP driver initialization.
*/
function load_pgp_driver()
{
if ($this->pgp_driver) {
return;
}
$driver = 'enigma_driver_' . $this->rc->config->get('enigma_pgp_driver', 'gnupg');
$username = $this->rc->user->get_username();
// Load driver
$this->pgp_driver = new $driver($username);
if (!$this->pgp_driver) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Enigma plugin: Unable to load PGP driver: $driver"
), true, true);
}
// Initialise driver
$result = $this->pgp_driver->init();
if ($result instanceof enigma_error) {
self::raise_error($result, __LINE__, true);
}
}
/**
* S/MIME driver initialization.
*/
function load_smime_driver()
{
if ($this->smime_driver) {
return;
}
$driver = 'enigma_driver_' . $this->rc->config->get('enigma_smime_driver', 'phpssl');
$username = $this->rc->user->get_username();
// Load driver
$this->smime_driver = new $driver($username);
if (!$this->smime_driver) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Enigma plugin: Unable to load S/MIME driver: $driver"
), true, true);
}
// Initialise driver
$result = $this->smime_driver->init();
if ($result instanceof enigma_error) {
self::raise_error($result, __LINE__, true);
}
}
/**
* Handler for message signing
*
* @param Mail_mime Original message
* @param int Encryption mode
*
* @return enigma_error On error returns error object
*/
function sign_message(&$message, $mode = null)
{
$mime = new enigma_mime_message($message, enigma_mime_message::PGP_SIGNED);
$from = $mime->getFromAddress();
// find private key
$key = $this->find_key($from, true);
if (empty($key)) {
return new enigma_error(enigma_error::KEYNOTFOUND);
}
// check if we have password for this key
$passwords = $this->get_passwords();
$pass = $passwords[$key->id];
if ($pass === null) {
// ask for password
$error = array('missing' => array($key->id => $key->name));
return new enigma_error(enigma_error::BADPASS, '', $error);
}
$key->password = $pass;
// select mode
switch ($mode) {
case self::SIGN_MODE_BODY:
$pgp_mode = Crypt_GPG::SIGN_MODE_CLEAR;
break;
case self::SIGN_MODE_MIME:
$pgp_mode = Crypt_GPG::SIGN_MODE_DETACHED;
break;
default:
if ($mime->isMultipart()) {
$pgp_mode = Crypt_GPG::SIGN_MODE_DETACHED;
}
else {
$pgp_mode = Crypt_GPG::SIGN_MODE_CLEAR;
}
}
// get message body
if ($pgp_mode == Crypt_GPG::SIGN_MODE_CLEAR) {
// in this mode we'll replace text part
// with the one containing signature
$body = $message->getTXTBody();
$text_charset = $message->getParam('text_charset');
$line_length = $this->rc->config->get('line_length', 72);
// We can't use format=flowed for signed messages
if (strpos($text_charset, 'format=flowed')) {
list($charset, $params) = explode(';', $text_charset);
$body = rcube_mime::unfold_flowed($body);
$body = rcube_mime::wordwrap($body, $line_length, "\r\n", false, $charset);
$text_charset = str_replace(";\r\n format=flowed", '', $text_charset);
}
}
else {
// here we'll build PGP/MIME message
$body = $mime->getOrigBody();
}
// sign the body
$result = $this->pgp_sign($body, $key, $pgp_mode);
if ($result !== true) {
if ($result->getCode() == enigma_error::BADPASS) {
// ask for password
$error = array('bad' => array($key->id => $key->name));
return new enigma_error(enigma_error::BADPASS, '', $error);
}
return $result;
}
// replace message body
if ($pgp_mode == Crypt_GPG::SIGN_MODE_CLEAR) {
$message->setTXTBody($body);
$message->setParam('text_charset', $text_charset);
}
else {
$mime->addPGPSignature($body, $this->pgp_driver->signature_algorithm());
$message = $mime;
}
}
/**
* Handler for message encryption
*
* @param Mail_mime Original message
* @param int Encryption mode
* @param bool Is draft-save action - use only sender's key for encryption
*
* @return enigma_error On error returns error object
*/
function encrypt_message(&$message, $mode = null, $is_draft = false)
{
$mime = new enigma_mime_message($message, enigma_mime_message::PGP_ENCRYPTED);
// always use sender's key
$from = $mime->getFromAddress();
// check senders key for signing
if ($mode & self::ENCRYPT_MODE_SIGN) {
$sign_key = $this->find_key($from, true);
if (empty($sign_key)) {
return new enigma_error(enigma_error::KEYNOTFOUND);
}
// check if we have password for this key
$passwords = $this->get_passwords();
$sign_pass = $passwords[$sign_key->id];
if ($sign_pass === null) {
// ask for password
$error = array('missing' => array($sign_key->id => $sign_key->name));
return new enigma_error(enigma_error::BADPASS, '', $error);
}
$sign_key->password = $sign_pass;
}
$recipients = array($from);
// if it's not a draft we add all recipients' keys
if (!$is_draft) {
$recipients = array_merge($recipients, $mime->getRecipients());
}
if (empty($recipients)) {
return new enigma_error(enigma_error::KEYNOTFOUND);
}
$recipients = array_unique($recipients);
// find recipient public keys
foreach ((array) $recipients as $email) {
if ($email == $from && $sign_key) {
$key = $sign_key;
}
else {
$key = $this->find_key($email);
}
if (empty($key)) {
return new enigma_error(enigma_error::KEYNOTFOUND, '', array(
'missing' => $email
));
}
$keys[] = $key;
}
// select mode
if ($mode & self::ENCRYPT_MODE_BODY) {
$encrypt_mode = $mode;
}
else if ($mode & self::ENCRYPT_MODE_MIME) {
$encrypt_mode = $mode;
}
else {
$encrypt_mode = $mime->isMultipart() ? self::ENCRYPT_MODE_MIME : self::ENCRYPT_MODE_BODY;
}
// get message body
if ($encrypt_mode == self::ENCRYPT_MODE_BODY) {
// in this mode we'll replace text part
// with the one containing encrypted message
$body = $message->getTXTBody();
}
else {
// here we'll build PGP/MIME message
$body = $mime->getOrigBody();
}
// sign the body
$result = $this->pgp_encrypt($body, $keys, $sign_key);
if ($result !== true) {
if ($result->getCode() == enigma_error::BADPASS) {
// ask for password
$error = array('bad' => array($sign_key->id => $sign_key->name));
return new enigma_error(enigma_error::BADPASS, '', $error);
}
return $result;
}
// replace message body
if ($encrypt_mode == self::ENCRYPT_MODE_BODY) {
$message->setTXTBody($body);
}
else {
$mime->setPGPEncryptedBody($body);
$message = $mime;
}
}
/**
* Handler for attaching public key to a message
*
* @param Mail_mime Original message
*
* @return bool True on success, False on failure
*/
function attach_public_key(&$message)
{
$headers = $message->headers();
$from = rcube_mime::decode_address_list($headers['From'], 1, false, null, true);
$from = $from[1];
// find my key
if ($from && ($key = $this->find_key($from, true))) {
$pubkey_armor = $this->export_key($key->id);
if (!$pubkey_armor instanceof enigma_error) {
$pubkey_name = '0x' . enigma_key::format_id($key->id) . '.asc';
$message->addAttachment($pubkey_armor, 'application/pgp-keys', $pubkey_name, false, '7bit');
return true;
}
}
return false;
}
/**
* Handler for message_part_structure hook.
* Called for every part of the message.
*
* @param array Original parameters
* @param string Part body (will be set if used internally)
*
* @return array Modified parameters
*/
function part_structure($p, $body = null)
{
// Don't be tempted to support encryption in text/html parts
// Because of EFAIL vulnerability we should never support this (#6289)
if ($p['mimetype'] == 'text/plain' || $p['mimetype'] == 'application/pgp') {
$this->parse_plain($p, $body);
}
else if ($p['mimetype'] == 'multipart/signed') {
$this->parse_signed($p, $body);
}
else if ($p['mimetype'] == 'multipart/encrypted') {
$this->parse_encrypted($p);
}
else if ($p['mimetype'] == 'application/pkcs7-mime') {
$this->parse_encrypted($p);
}
return $p;
}
/**
* Handler for message_part_body hook.
*
* @param array Original parameters
*
* @return array Modified parameters
*/
function part_body($p)
{
// encrypted attachment, see parse_plain_encrypted()
if ($p['part']->need_decryption && $p['part']->body === null) {
$this->load_pgp_driver();
$storage = $this->rc->get_storage();
$body = $storage->get_message_part($p['object']->uid, $p['part']->mime_id, $p['part'], null, null, true, 0, false);
$result = $this->pgp_decrypt($body);
// @TODO: what to do on error?
if ($result === true) {
$p['part']->body = $body;
$p['part']->size = strlen($body);
$p['part']->body_modified = true;
}
}
return $p;
}
/**
* Handler for plain/text message.
*
* @param array Reference to hook's parameters
* @param string Part body (will be set if used internally)
*/
function parse_plain(&$p, $body = null)
{
$part = $p['structure'];
// Get message body from IMAP server
if ($body === null) {
$body = $this->get_part_body($p['object'], $part);
}
// In this way we can use fgets on string as on file handle
// Don't use php://temp for security (body may come from an encrypted part)
$fd = fopen('php://memory', 'r+');
if (!$fd) {
return;
}
fwrite($fd, $body);
rewind($fd);
$body = '';
$prefix = '';
$mode = '';
$tokens = array(
'BEGIN PGP SIGNED MESSAGE' => 'signed-start',
'END PGP SIGNATURE' => 'signed-end',
'BEGIN PGP MESSAGE' => 'encrypted-start',
'END PGP MESSAGE' => 'encrypted-end',
);
$regexp = '/^-----(' . implode('|', array_keys($tokens)) . ')-----[\r\n]*/';
while (($line = fgets($fd)) !== false) {
if ($line[0] === '-' && $line[4] === '-' && preg_match($regexp, $line, $m)) {
switch ($tokens[$m[1]]) {
case 'signed-start':
$body = $line;
$mode = 'signed';
break;
case 'signed-end':
if ($mode === 'signed') {
$body .= $line;
}
break 2; // ignore anything after this line
case 'encrypted-start':
$body = $line;
$mode = 'encrypted';
break;
case 'encrypted-end':
if ($mode === 'encrypted') {
$body .= $line;
}
break 2; // ignore anything after this line
}
continue;
}
if ($mode === 'signed') {
$body .= $line;
}
else if ($mode === 'encrypted') {
$body .= $line;
}
else {
$prefix .= $line;
}
}
fclose($fd);
if ($mode === 'signed') {
$this->parse_plain_signed($p, $body, $prefix);
}
else if ($mode === 'encrypted') {
$this->parse_plain_encrypted($p, $body, $prefix);
}
}
/**
* Handler for multipart/signed message.
*
* @param array Reference to hook's parameters
* @param string Part body (will be set if used internally)
*/
function parse_signed(&$p, $body = null)
{
$struct = $p['structure'];
// S/MIME
if ($struct->parts[1] && $struct->parts[1]->mimetype == 'application/pkcs7-signature') {
$this->parse_smime_signed($p, $body);
}
// PGP/MIME: RFC3156
// The multipart/signed body MUST consist of exactly two parts.
// The first part contains the signed data in MIME canonical format,
// including a set of appropriate content headers describing the data.
// The second body MUST contain the PGP digital signature. It MUST be
// labeled with a content type of "application/pgp-signature".
else if (count($struct->parts) == 2
&& $struct->parts[1] && $struct->parts[1]->mimetype == 'application/pgp-signature'
) {
$this->parse_pgp_signed($p, $body);
}
}
/**
* Handler for multipart/encrypted message.
*
* @param array Reference to hook's parameters
*/
function parse_encrypted(&$p)
{
$struct = $p['structure'];
// S/MIME
if ($p['mimetype'] == 'application/pkcs7-mime') {
$this->parse_smime_encrypted($p);
}
// PGP/MIME: RFC3156
// The multipart/encrypted MUST consist of exactly two parts. The first
// MIME body part must have a content type of "application/pgp-encrypted".
// This body contains the control information.
// The second MIME body part MUST contain the actual encrypted data. It
// must be labeled with a content type of "application/octet-stream".
else if (count($struct->parts) == 2
&& $struct->parts[0] && $struct->parts[0]->mimetype == 'application/pgp-encrypted'
&& $struct->parts[1] && $struct->parts[1]->mimetype == 'application/octet-stream'
) {
$this->parse_pgp_encrypted($p);
}
}
/**
* Handler for plain signed message.
* Excludes message and signature bodies and verifies signature.
*
* @param array Reference to hook's parameters
* @param string Message (part) body
* @param string Body prefix (additional text before the encrypted block)
*/
private function parse_plain_signed(&$p, $body, $prefix = '')
{
if (!$this->rc->config->get('enigma_signatures', true)) {
return;
}
$this->load_pgp_driver();
$part = $p['structure'];
// Verify signature
if ($this->rc->action == 'show' || $this->rc->action == 'preview' || $this->rc->action == 'print') {
$sig = $this->pgp_verify($body);
}
// In this way we can use fgets on string as on file handle
// Don't use php://temp for security (body may come from an encrypted part)
$fd = fopen('php://memory', 'r+');
if (!$fd) {
return;
}
fwrite($fd, $body);
rewind($fd);
$body = $part->body = null;
$part->body_modified = true;
// Extract body (and signature?)
while (($line = fgets($fd, 1024)) !== false) {
if ($part->body === null)
$part->body = '';
else if (preg_match('/^-----BEGIN PGP SIGNATURE-----/', $line))
break;
else
$part->body .= $line;
}
fclose($fd);
// Remove "Hash" Armor Headers
$part->body = preg_replace('/^.*\r*\n\r*\n/', '', $part->body);
// de-Dash-Escape (RFC2440)
$part->body = preg_replace('/(^|\n)- -/', '\\1-', $part->body);
if ($prefix) {
$part->body = $prefix . $part->body;
}
// Store signature data for display
if (!empty($sig)) {
$sig->partial = !empty($prefix);
$this->signatures[$part->mime_id] = $sig;
}
}
/**
* Handler for PGP/MIME signed message.
* Verifies signature.
*
* @param array Reference to hook's parameters
* @param string Part body (will be set if used internally)
*/
private function parse_pgp_signed(&$p, $body = null)
{
if (!$this->rc->config->get('enigma_signatures', true)) {
return;
}
if ($this->rc->action != 'show' && $this->rc->action != 'preview' && $this->rc->action != 'print') {
return;
}
$this->load_pgp_driver();
$struct = $p['structure'];
$msg_part = $struct->parts[0];
$sig_part = $struct->parts[1];
// Get bodies
if ($body === null) {
if (!$struct->body_modified) {
$body = $this->get_part_body($p['object'], $struct);
}
}
$boundary = $struct->ctype_parameters['boundary'];
// when it is a signed message forwarded as attachment
// ctype_parameters property will not be set
if (!$boundary && $struct->headers['content-type']
&& preg_match('/boundary="?([a-zA-Z0-9\'()+_,-.\/:=?]+)"?/', $struct->headers['content-type'], $m)
) {
$boundary = $m[1];
}
// set signed part body
list($msg_body, $sig_body) = $this->explode_signed_body($body, $boundary);
// Verify
if ($sig_body && $msg_body) {
$sig = $this->pgp_verify($msg_body, $sig_body);
// Store signature data for display
$this->signatures[$struct->mime_id] = $sig;
$this->signatures[$msg_part->mime_id] = $sig;
}
}
/**
* Handler for S/MIME signed message.
* Verifies signature.
*
* @param array Reference to hook's parameters
* @param string Part body (will be set if used internally)
*/
private function parse_smime_signed(&$p, $body = null)
{
if (!$this->rc->config->get('enigma_signatures', true)) {
return;
}
// @TODO
}
/**
* Handler for plain encrypted message.
*
* @param array Reference to hook's parameters
* @param string Message (part) body
* @param string Body prefix (additional text before the encrypted block)
*/
private function parse_plain_encrypted(&$p, $body, $prefix = '')
{
if (!$this->rc->config->get('enigma_decryption', true)) {
return;
}
$this->load_pgp_driver();
$part = $p['structure'];
// Decrypt
$result = $this->pgp_decrypt($body, $signature);
// Store decryption status
$this->decryptions[$part->mime_id] = $result;
// Store signature data for display
if ($signature) {
$this->signatures[$part->mime_id] = $signature;
}
// find parent part ID
if (strpos($part->mime_id, '.')) {
$items = explode('.', $part->mime_id);
array_pop($items);
$parent = implode('.', $items);
}
else {
$parent = 0;
}
// Parse decrypted message
if ($result === true) {
$part->body = $prefix . $body;
$part->body_modified = true;
// it maybe PGP signed inside, verify signature
$this->parse_plain($p, $body);
// Remember it was decrypted
$this->encrypted_parts[] = $part->mime_id;
// Inform the user that only a part of the body was encrypted
if ($prefix) {
$this->decryptions[$part->mime_id] = self::ENCRYPTED_PARTIALLY;
}
// Encrypted plain message may contain encrypted attachments
// in such case attachments have .pgp extension and type application/octet-stream.
// This is what happens when you select "Encrypt each attachment separately
// and send the message using inline PGP" in Thunderbird's Enigmail.
if ($p['object']->mime_parts[$parent]) {
foreach ((array)$p['object']->mime_parts[$parent]->parts as $p) {
if ($p->disposition == 'attachment' && $p->mimetype == 'application/octet-stream'
&& preg_match('/^(.*)\.pgp$/i', $p->filename, $m)
) {
// modify filename
$p->filename = $m[1];
// flag the part, it will be decrypted when needed
$p->need_decryption = true;
// disable caching
$p->body_modified = true;
}
}
}
}
// decryption failed, but the message may have already
// been cached with the modified parts (see above),
// let's bring the original state back
else if ($p['object']->mime_parts[$parent]) {
foreach ((array)$p['object']->mime_parts[$parent]->parts as $p) {
if ($p->need_decryption && !preg_match('/^(.*)\.pgp$/i', $p->filename, $m)) {
// modify filename
$p->filename .= '.pgp';
// flag the part, it will be decrypted when needed
unset($p->need_decryption);
}
}
}
}
/**
* Handler for PGP/MIME encrypted message.
*
* @param array Reference to hook's parameters
*/
private function parse_pgp_encrypted(&$p)
{
if (!$this->rc->config->get('enigma_decryption', true)) {
return;
}
$this->load_pgp_driver();
$struct = $p['structure'];
$part = $struct->parts[1];
// Get body
$body = $this->get_part_body($p['object'], $part);
// Decrypt
$result = $this->pgp_decrypt($body, $signature);
if ($result === true) {
// Parse decrypted message
$struct = $this->parse_body($body);
// Modify original message structure
$this->modify_structure($p, $struct, strlen($body));
// Parse the structure (there may be encrypted/signed parts inside
$this->part_structure(array(
'object' => $p['object'],
'structure' => $struct,
'mimetype' => $struct->mimetype
), $body);
// Attach the decryption message to all parts
$this->decryptions[$struct->mime_id] = $result;
foreach ((array) $struct->parts as $sp) {
$this->decryptions[$sp->mime_id] = $result;
if ($signature) {
$this->signatures[$sp->mime_id] = $signature;
}
}
}
else {
$this->decryptions[$part->mime_id] = $result;
// Make sure decryption status message will be displayed
$part->type = 'content';
$p['object']->parts[] = $part;
// don't show encrypted part on attachments list
// don't show "cannot display encrypted message" text
$p['abort'] = true;
}
}
/**
* Handler for S/MIME encrypted message.
*
* @param array Reference to hook's parameters
*/
private function parse_smime_encrypted(&$p)
{
if (!$this->rc->config->get('enigma_decryption', true)) {
return;
}
// @TODO
}
/**
* PGP signature verification.
*
* @param mixed Message body
* @param mixed Signature body (for MIME messages)
*
* @return mixed enigma_signature or enigma_error
*/
private function pgp_verify(&$msg_body, $sig_body = null)
{
// @TODO: Handle big bodies using (temp) files
$sig = $this->pgp_driver->verify($msg_body, $sig_body);
if (($sig instanceof enigma_error) && $sig->getCode() != enigma_error::KEYNOTFOUND) {
self::raise_error($sig, __LINE__);
}
return $sig;
}
/**
* PGP message decryption.
*
* @param mixed &$msg_body Message body
* @param enigma_signature &$signature Signature verification result
*
* @return mixed True or enigma_error
*/
private function pgp_decrypt(&$msg_body, &$signature = null)
{
// @TODO: Handle big bodies using (temp) files
$keys = $this->get_passwords();
$result = $this->pgp_driver->decrypt($msg_body, $keys, $signature);
if ($result instanceof enigma_error) {
if ($result->getCode() != enigma_error::KEYNOTFOUND) {
self::raise_error($result, __LINE__);
}
return $result;
}
$msg_body = $result;
return true;
}
/**
* PGP message signing
*
* @param mixed Message body
* @param enigma_key The key (with passphrase)
* @param int Signing mode
*
* @return mixed True or enigma_error
*/
private function pgp_sign(&$msg_body, $key, $mode = null)
{
// @TODO: Handle big bodies using (temp) files
$result = $this->pgp_driver->sign($msg_body, $key, $mode);
if ($result instanceof enigma_error) {
if ($result->getCode() != enigma_error::KEYNOTFOUND) {
self::raise_error($result, __LINE__);
}
return $result;
}
$msg_body = $result;
return true;
}
/**
* PGP message encrypting
*
* @param mixed Message body
* @param array Keys (array of enigma_key objects)
* @param string Optional signing Key ID
* @param string Optional signing Key password
*
* @return mixed True or enigma_error
*/
private function pgp_encrypt(&$msg_body, $keys, $sign_key = null, $sign_pass = null)
{
// @TODO: Handle big bodies using (temp) files
$result = $this->pgp_driver->encrypt($msg_body, $keys, $sign_key, $sign_pass);
if ($result instanceof enigma_error) {
if ($result->getCode() != enigma_error::KEYNOTFOUND) {
self::raise_error($result, __LINE__);
}
return $result;
}
$msg_body = $result;
return true;
}
/**
* PGP keys listing.
*
* @param mixed Key ID/Name pattern
*
* @return mixed Array of keys or enigma_error
*/
function list_keys($pattern = '')
{
$this->load_pgp_driver();
$result = $this->pgp_driver->list_keys($pattern);
if ($result instanceof enigma_error) {
self::raise_error($result, __LINE__);
}
return $result;
}
/**
* Find PGP private/public key
*
* @param string E-mail address
* @param bool Need a key for signing?
*
* @return enigma_key The key
*/
function find_key($email, $can_sign = false)
{
if ($can_sign && array_key_exists($email, $this->cache)) {
return $this->cache[$email];
}
$this->load_pgp_driver();
$result = $this->pgp_driver->list_keys($email);
if ($result instanceof enigma_error) {
self::raise_error($result, __LINE__);
return;
}
$mode = $can_sign ? enigma_key::CAN_SIGN : enigma_key::CAN_ENCRYPT;
$ret = null;
// check key validity and type
foreach ($result as $key) {
if (($subkey = $key->find_subkey($email, $mode))
&& (!$can_sign || $key->get_type() == enigma_key::TYPE_KEYPAIR)
) {
$ret = $key;
break;
}
}
// cache private key info for better performance
// we can skip one list_keys() call when signing and attaching a key
if ($can_sign) {
$this->cache[$email] = $ret;
}
return $ret;
}
/**
* PGP key details.
*
* @param mixed Key ID
*
* @return mixed enigma_key or enigma_error
*/
function get_key($keyid)
{
$this->load_pgp_driver();
$result = $this->pgp_driver->get_key($keyid);
if ($result instanceof enigma_error) {
self::raise_error($result, __LINE__);
}
return $result;
}
/**
* PGP key delete.
*
* @param string Key ID
*
* @return enigma_error|bool True on success
*/
function delete_key($keyid)
{
$this->load_pgp_driver();
$result = $this->pgp_driver->delete_key($keyid);
if ($result instanceof enigma_error) {
self::raise_error($result, __LINE__);
}
return $result;
}
/**
* PGP keys pair generation.
*
* @param array Key pair parameters
*
* @return mixed enigma_key or enigma_error
*/
function generate_key($data)
{
$this->load_pgp_driver();
$result = $this->pgp_driver->gen_key($data);
if ($result instanceof enigma_error) {
self::raise_error($result, __LINE__);
}
return $result;
}
/**
* PGP keys/certs import.
*
* @param mixed Import file name or content
* @param boolean True if first argument is a filename
*
* @return mixed Import status data array or enigma_error
*/
function import_key($content, $isfile = false)
{
$this->load_pgp_driver();
$result = $this->pgp_driver->import($content, $isfile, $this->get_passwords());
if ($result instanceof enigma_error) {
self::raise_error($result, __LINE__);
}
else {
$result['imported'] = $result['public_imported'] + $result['private_imported'];
$result['unchanged'] = $result['public_unchanged'] + $result['private_unchanged'];
}
return $result;
}
/**
* PGP keys/certs export.
*
* @param string Key ID
* @param resource Optional output stream
* @param bool Include private key
*
* @return mixed Key content or enigma_error
*/
function export_key($key, $fp = null, $include_private = false)
{
$this->load_pgp_driver();
$result = $this->pgp_driver->export($key, $include_private, $this->get_passwords());
if ($result instanceof enigma_error) {
self::raise_error($result, __LINE__);
return $result;
}
if ($fp) {
fwrite($fp, $result);
}
else {
return $result;
}
}
/**
* Registers password for specified key/cert sent by the password prompt.
*/
function password_handler()
{
$keyid = rcube_utils::get_input_value('_keyid', rcube_utils::INPUT_POST);
$passwd = rcube_utils::get_input_value('_passwd', rcube_utils::INPUT_POST, true);
if ($keyid && $passwd !== null && strlen($passwd)) {
$this->save_password(strtoupper($keyid), $passwd);
}
}
/**
* Saves key/cert password in user session
*/
function save_password($keyid, $password)
{
// we store passwords in session for specified time
if ($config = $_SESSION['enigma_pass']) {
$config = $this->rc->decrypt($config);
$config = @unserialize($config);
}
$config[$keyid] = array($password, time());
$_SESSION['enigma_pass'] = $this->rc->encrypt(serialize($config));
}
/**
* Returns currently stored passwords
*/
function get_passwords()
{
if ($config = $_SESSION['enigma_pass']) {
$config = $this->rc->decrypt($config);
$config = @unserialize($config);
}
$threshold = $this->password_time ? time() - $this->password_time : 0;
$keys = array();
// delete expired passwords
foreach ((array) $config as $key => $value) {
if ($threshold && $value[1] < $threshold) {
unset($config[$key]);
$modified = true;
}
else {
$keys[$key] = $value[0];
}
}
if ($modified) {
$_SESSION['enigma_pass'] = $this->rc->encrypt(serialize($config));
}
return $keys;
}
/**
* Get message part body.
*
* @param rcube_message Message object
* @param rcube_message_part Message part
*/
private function get_part_body($msg, $part)
{
// @TODO: Handle big bodies using file handles
// This is a special case when we want to get the whole body
// using direct IMAP access, in other cases we prefer
// rcube_message::get_part_body() as the body may be already in memory
if (!$part->mime_id) {
// fake the size which may be empty for multipart/* parts
// otherwise get_message_part() below will fail
if (!$part->size) {
$reset = true;
$part->size = 1;
}
$storage = $this->rc->get_storage();
$body = $storage->get_message_part($msg->uid, $part->mime_id, $part,
null, null, true, 0, false);
if ($reset) {
$part->size = 0;
}
}
else {
$body = $msg->get_part_body($part->mime_id, false);
// Convert charset to get rid of possible non-ascii characters (#5962)
if ($part->charset && stripos($part->charset, 'ASCII') === false) {
$body = rcube_charset::convert($body, $part->charset, 'US-ASCII');
}
}
return $body;
}
/**
* Parse decrypted message body into structure
*
* @param string Message body
*
* @return array Message structure
*/
private function parse_body(&$body)
{
// Mail_mimeDecode need \r\n end-line, but gpg may return \n
$body = preg_replace('/\r?\n/', "\r\n", $body);
// parse the body into structure
$struct = rcube_mime::parse_message($body);
return $struct;
}
/**
* Replace message encrypted structure with decrypted message structure
*
* @param array Hook arguments
* @param rcube_message_part Part structure
* @param int Part size
*/
private function modify_structure(&$p, $struct, $size = 0)
{
// modify mime_parts property of the message object
$old_id = $p['structure']->mime_id;
foreach (array_keys($p['object']->mime_parts) as $idx) {
if (!$old_id || $idx == $old_id || strpos($idx, $old_id . '.') === 0) {
unset($p['object']->mime_parts[$idx]);
}
}
// set some part params used by Roundcube core
$struct->headers = array_merge($p['structure']->headers, $struct->headers);
$struct->size = $size;
$struct->filename = $p['structure']->filename;
// modify the new structure to be correctly handled by Roundcube
$this->modify_structure_part($struct, $p['object'], $old_id);
// replace old structure with the new one
$p['structure'] = $struct;
$p['mimetype'] = $struct->mimetype;
}
/**
* Modify decrypted message part
*
* @param rcube_message_part
* @param rcube_message
*/
private function modify_structure_part($part, $msg, $old_id)
{
// never cache the body
$part->body_modified = true;
$part->encoding = 'stream';
// modify part identifier
if ($old_id) {
$part->mime_id = !$part->mime_id ? $old_id : ($old_id . '.' . $part->mime_id);
}
// Cache the fact it was decrypted
$this->encrypted_parts[] = $part->mime_id;
$msg->mime_parts[$part->mime_id] = $part;
// modify sub-parts
foreach ((array) $part->parts as $p) {
$this->modify_structure_part($p, $msg, $old_id);
}
}
/**
* Extracts body and signature of multipart/signed message body
*/
private function explode_signed_body($body, $boundary)
{
if (!$body) {
return array();
}
$boundary = '--' . $boundary;
$boundary_len = strlen($boundary) + 2;
// Find boundaries
$start = strpos($body, $boundary) + $boundary_len;
$end = strpos($body, $boundary, $start);
// Get signed body and signature
$sig = substr($body, $end + $boundary_len);
$body = substr($body, $start, $end - $start - 2);
// Cleanup signature
$sig = substr($sig, strpos($sig, "\r\n\r\n") + 4);
$sig = substr($sig, 0, strpos($sig, $boundary));
return array($body, $sig);
}
/**
* Checks if specified message part is a PGP-key or S/MIME cert data
*
* @param rcube_message_part Part object
*
* @return boolean True if part is a key/cert
*/
public function is_keys_part($part)
{
// @TODO: S/MIME
return (
// Content-Type: application/pgp-keys
$part->mimetype == 'application/pgp-keys'
);
}
/**
* Removes all user keys and assigned data
*
* @param string Username
*
* @return bool True on success, False on failure
*/
public function delete_user_data($username)
{
$homedir = $this->rc->config->get('enigma_pgp_homedir', INSTALL_PATH . 'plugins/enigma/home');
$homedir .= DIRECTORY_SEPARATOR . $username;
return file_exists($homedir) ? self::delete_dir($homedir) : true;
}
/**
* Recursive method to remove directory with its content
*
* @param string Directory
*/
public static function delete_dir($dir)
{
// This code can be executed from command line, make sure
// we have permissions to delete keys directory
if (!is_writable($dir)) {
rcube::raise_error("Unable to delete $dir", false, true);
return false;
}
if ($content = scandir($dir)) {
foreach ($content as $filename) {
if ($filename != '.' && $filename != '..') {
$filename = $dir . DIRECTORY_SEPARATOR . $filename;
if (is_dir($filename)) {
self::delete_dir($filename);
}
else {
unlink($filename);
}
}
}
rmdir($dir);
}
return true;
}
/**
* Raise/log (relevant) errors
*/
protected static function raise_error($result, $line, $abort = false)
{
if ($result->getCode() != enigma_error::BADPASS) {
rcube::raise_error(array(
'code' => 600,
'file' => __FILE__,
'line' => $line,
'message' => "Enigma plugin: " . $result->getMessage()
), true, $abort);
}
}
}
diff --git a/plugins/enigma/lib/enigma_error.php b/plugins/enigma/lib/enigma_error.php
index 4c6931570..971f4c99c 100644
--- a/plugins/enigma/lib/enigma_error.php
+++ b/plugins/enigma/lib/enigma_error.php
@@ -1,57 +1,56 @@
<?php
/**
+-------------------------------------------------------------------------+
| Error class for the Enigma Plugin |
| |
- | Copyright (C) 2010-2015 The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-------------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-------------------------------------------------------------------------+
*/
class enigma_error
{
private $code;
private $message;
private $data = array();
// error codes
const OK = 0;
const INTERNAL = 1;
const NODATA = 2;
const KEYNOTFOUND = 3;
const DELKEY = 4;
const BADPASS = 5;
const EXPIRED = 6;
const UNVERIFIED = 7;
const NOMDC = 8;
function __construct($code = null, $message = '', $data = array())
{
$this->code = $code;
$this->message = $message;
$this->data = $data;
}
function getCode()
{
return $this->code;
}
function getMessage()
{
return $this->message;
}
function getData($name)
{
return $name ? $this->data[$name] : $this->data;
}
}
diff --git a/plugins/enigma/lib/enigma_key.php b/plugins/enigma/lib/enigma_key.php
index b6c36b0ac..a4470da24 100644
--- a/plugins/enigma/lib/enigma_key.php
+++ b/plugins/enigma/lib/enigma_key.php
@@ -1,167 +1,166 @@
<?php
/**
+-------------------------------------------------------------------------+
| Key class for the Enigma Plugin |
| |
- | Copyright (C) 2010-2015 The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-------------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-------------------------------------------------------------------------+
*/
class enigma_key
{
public $id;
public $name;
public $users = array();
public $subkeys = array();
public $reference;
public $password;
const TYPE_UNKNOWN = 0;
const TYPE_KEYPAIR = 1;
const TYPE_PUBLIC = 2;
const CAN_ENCRYPT = 1;
const CAN_SIGN = 2;
const CAN_CERTIFY = 4;
const CAN_AUTHENTICATE = 8;
/**
* Keys list sorting callback for usort()
*/
static function cmp($a, $b)
{
return strcmp($a->name, $b->name);
}
/**
* Returns key type
*/
function get_type()
{
if ($this->subkeys[0]->has_private)
return enigma_key::TYPE_KEYPAIR;
else if (!empty($this->subkeys[0]))
return enigma_key::TYPE_PUBLIC;
return enigma_key::TYPE_UNKNOWN;
}
/**
* Returns true if all user IDs are revoked
*/
function is_revoked()
{
foreach ($this->subkeys as $subkey)
if (!$subkey->revoked)
return false;
return true;
}
/**
* Returns true if any user ID is valid
*/
function is_valid()
{
foreach ($this->users as $user)
if ($user->valid)
return true;
return false;
}
/**
* Returns true if any of subkeys is not expired
*/
function is_expired()
{
$now = time();
foreach ($this->subkeys as $subkey)
if (!$subkey->expires || $subkey->expires > $now)
return true;
return false;
}
/**
* Returns true if any of subkeys is a private key
*/
function is_private()
{
$now = time();
foreach ($this->subkeys as $subkey)
if ($subkey->has_private)
return true;
return false;
}
/**
* Get key ID by user email
*/
function find_subkey($email, $mode)
{
$now = time();
foreach ($this->users as $user) {
if (strcasecmp($user->email, $email) === 0 && $user->valid && !$user->revoked) {
foreach ($this->subkeys as $subkey) {
if (!$subkey->revoked && (!$subkey->expires || $subkey->expires > $now)) {
if ($subkey->usage & $mode) {
return $subkey;
}
}
}
}
}
}
/**
* Converts long ID or Fingerprint to short ID
* Crypt_GPG uses internal, but e.g. Thunderbird's Enigmail displays short ID
*
* @param string Key ID or fingerprint
* @return string Key short ID
*/
static function format_id($id)
{
// E.g. 04622F2089E037A5 => 89E037A5
return substr($id, -8);
}
/**
* Formats fingerprint string
*
* @param string Key fingerprint
*
* @return string Formatted fingerprint (with spaces)
*/
static function format_fingerprint($fingerprint)
{
if (!$fingerprint) {
return '';
}
$result = '';
for ($i=0; $i<40; $i++) {
if ($i % 4 == 0) {
$result .= ' ';
}
$result .= $fingerprint[$i];
}
return $result;
}
}
diff --git a/plugins/enigma/lib/enigma_mime_message.php b/plugins/enigma/lib/enigma_mime_message.php
index a81f3cfc6..e21c63ba8 100644
--- a/plugins/enigma/lib/enigma_mime_message.php
+++ b/plugins/enigma/lib/enigma_mime_message.php
@@ -1,305 +1,304 @@
<?php
/**
+-------------------------------------------------------------------------+
| Mail_mime wrapper for the Enigma Plugin |
| |
- | Copyright (C) 2010-2015 The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-------------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-------------------------------------------------------------------------+
*/
class enigma_mime_message extends Mail_mime
{
const PGP_SIGNED = 1;
const PGP_ENCRYPTED = 2;
protected $type;
protected $message;
protected $body;
protected $signature;
protected $encrypted;
protected $micalg;
/**
* Object constructor
*
* @param Mail_mime Original message
* @param int Output message type
*/
function __construct($message, $type)
{
$this->message = $message;
$this->type = $type;
// clone parameters
foreach (array_keys($this->build_params) as $param) {
$this->build_params[$param] = $message->getParam($param);
}
// clone headers
$this->headers = $message->headers();
// \r\n is must-have here
$this->body = $message->get() . "\r\n";
}
/**
* Check if the message is multipart (requires PGP/MIME)
*
* @return bool True if it is multipart, otherwise False
*/
public function isMultipart()
{
return $this->message instanceof enigma_mime_message
|| $this->message->isMultipart() || $this->message->getHTMLBody();
}
/**
* Get e-mail address of message sender
*
* @return string Sender address
*/
public function getFromAddress()
{
// get sender address
$headers = $this->message->headers();
$from = rcube_mime::decode_address_list($headers['From'], 1, false, null, true);
$from = $from[1];
return $from;
}
/**
* Get recipients' e-mail addresses
*
* @return array Recipients' addresses
*/
public function getRecipients()
{
// get sender address
$headers = $this->message->headers();
$to = rcube_mime::decode_address_list($headers['To'], null, false, null, true);
$cc = rcube_mime::decode_address_list($headers['Cc'], null, false, null, true);
$bcc = rcube_mime::decode_address_list($headers['Bcc'], null, false, null, true);
$recipients = array_unique(array_merge($to, $cc, $bcc));
$recipients = array_diff($recipients, array('undisclosed-recipients:'));
return $recipients;
}
/**
* Get original message body, to be encrypted/signed
*
* @return string Message body
*/
public function getOrigBody()
{
$_headers = $this->message->headers();
$headers = array();
if ($_headers['Content-Transfer-Encoding']
&& stripos($_headers['Content-Type'], 'multipart') === false
) {
$headers[] = 'Content-Transfer-Encoding: ' . $_headers['Content-Transfer-Encoding'];
}
$headers[] = 'Content-Type: ' . $_headers['Content-Type'];
return implode("\r\n", $headers) . "\r\n\r\n" . $this->body;
}
/**
* Register signature attachment
*
* @param string Signature body
* @param string Hash algorithm name
*/
public function addPGPSignature($body, $algorithm = null)
{
$this->signature = $body;
$this->micalg = $algorithm;
// Reset Content-Type to be overwritten with valid boundary
unset($this->headers['Content-Type']);
unset($this->headers['Content-Transfer-Encoding']);
}
/**
* Register encrypted body
*
* @param string Encrypted body
*/
public function setPGPEncryptedBody($body)
{
$this->encrypted = $body;
// Reset Content-Type to be overwritten with valid boundary
unset($this->headers['Content-Type']);
unset($this->headers['Content-Transfer-Encoding']);
}
/**
* Builds the multipart message.
*
* @param array $params Build parameters that change the way the email
* is built. Should be associative. See $_build_params.
* @param resource $filename Output file where to save the message instead of
* returning it
* @param boolean $skip_head True if you want to return/save only the message
* without headers
*
* @return mixed The MIME message content string, null or PEAR error object
*/
public function get($params = null, $filename = null, $skip_head = false)
{
if (!empty($params)) {
foreach ($params as $key => $value) {
$this->build_params[$key] = $value;
}
}
$this->checkParams();
if ($this->type == self::PGP_SIGNED) {
$params = array(
'preamble' => "This is an OpenPGP/MIME signed message (RFC 4880 and 3156)",
'content_type' => "multipart/signed; protocol=\"application/pgp-signature\"",
'eol' => $this->build_params['eol'],
);
if ($this->micalg) {
$params['content_type'] .= "; micalg=pgp-" . $this->micalg;
}
$message = new Mail_mimePart('', $params);
if (!empty($this->body)) {
$headers = $this->message->headers();
$params = array('content_type' => $headers['Content-Type']);
if ($headers['Content-Transfer-Encoding']
&& stripos($headers['Content-Type'], 'multipart') === false
) {
$params['encoding'] = $headers['Content-Transfer-Encoding'];
}
$message->addSubpart($this->body, $params);
}
if (!empty($this->signature)) {
$message->addSubpart($this->signature, array(
'filename' => 'signature.asc',
'content_type' => 'application/pgp-signature',
'disposition' => 'attachment',
'description' => 'OpenPGP digital signature',
));
}
}
else if ($this->type == self::PGP_ENCRYPTED) {
$params = array(
'preamble' => "This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)",
'content_type' => "multipart/encrypted; protocol=\"application/pgp-encrypted\"",
'eol' => $this->build_params['eol'],
);
$message = new Mail_mimePart('', $params);
$message->addSubpart('Version: 1', array(
'content_type' => 'application/pgp-encrypted',
'description' => 'PGP/MIME version identification',
));
$message->addSubpart($this->encrypted, array(
'content_type' => 'application/octet-stream',
'description' => 'PGP/MIME encrypted message',
'disposition' => 'inline',
'filename' => 'encrypted.asc',
));
}
// Use saved boundary
if (!empty($this->build_params['boundary'])) {
$boundary = $this->build_params['boundary'];
}
else {
$boundary = null;
}
// Write output to file
if ($filename) {
// Append mimePart message headers and body into file
$headers = $message->encodeToFile($filename, $boundary, $skip_head);
if ($this->isError($headers)) {
return $headers;
}
$this->headers = array_merge($this->headers, $headers);
return;
}
else {
$output = $message->encode($boundary, $skip_head);
if ($this->isError($output)) {
return $output;
}
$this->headers = array_merge($this->headers, $output['headers']);
return $output['body'];
}
}
/**
* Get Content-Type and Content-Transfer-Encoding headers of the message
*
* @return array Headers array
*/
protected function contentHeaders()
{
$this->checkParams();
$eol = $this->build_params['eol'] ?: "\r\n";
// multipart message: and boundary
if (!empty($this->build_params['boundary'])) {
$boundary = $this->build_params['boundary'];
}
else if (!empty($this->headers['Content-Type'])
&& preg_match('/boundary="([^"]+)"/', $this->headers['Content-Type'], $m)
) {
$boundary = $m[1];
}
else {
$boundary = '=_' . md5(rand() . microtime());
}
$this->build_params['boundary'] = $boundary;
if ($this->type == self::PGP_SIGNED) {
$headers['Content-Type'] = "multipart/signed;$eol"
." protocol=\"application/pgp-signature\";$eol"
." boundary=\"$boundary\"";
if ($this->micalg) {
$headers['Content-Type'] .= ";{$eol} micalg=pgp-" . $this->micalg;
}
}
else if ($this->type == self::PGP_ENCRYPTED) {
$headers['Content-Type'] = "multipart/encrypted;$eol"
." protocol=\"application/pgp-encrypted\";$eol"
." boundary=\"$boundary\"";
}
return $headers;
}
}
diff --git a/plugins/enigma/lib/enigma_signature.php b/plugins/enigma/lib/enigma_signature.php
index 8e9f1c9b5..a93edc60d 100644
--- a/plugins/enigma/lib/enigma_signature.php
+++ b/plugins/enigma/lib/enigma_signature.php
@@ -1,32 +1,31 @@
<?php
/**
+-------------------------------------------------------------------------+
| Signature class for the Enigma Plugin |
| |
- | Copyright (C) 2010-2015 The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-------------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-------------------------------------------------------------------------+
*/
class enigma_signature
{
public $id;
public $valid;
public $fingerprint;
public $created;
public $expires;
public $name;
public $comment;
public $email;
// Set it to true if signature is valid, but part of the message
// was out of the signed block
public $partial;
}
diff --git a/plugins/enigma/lib/enigma_subkey.php b/plugins/enigma/lib/enigma_subkey.php
index 3fb5cc676..5905f4fec 100644
--- a/plugins/enigma/lib/enigma_subkey.php
+++ b/plugins/enigma/lib/enigma_subkey.php
@@ -1,79 +1,78 @@
<?php
/**
+-------------------------------------------------------------------------+
| SubKey class for the Enigma Plugin |
| |
- | Copyright (C) 2010-2015 The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-------------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-------------------------------------------------------------------------+
*/
class enigma_subkey
{
public $id;
public $fingerprint;
public $expires;
public $created;
public $revoked;
public $has_private;
public $algorithm;
public $length;
public $usage;
/**
* Converts internal ID to short ID
* Crypt_GPG uses internal, but e.g. Thunderbird's Enigmail displays short ID
*
* @return string Key ID
*/
function get_short_id()
{
// E.g. 04622F2089E037A5 => 89E037A5
return enigma_key::format_id($this->id);
}
/**
* Getter for formatted fingerprint
*
* @return string Formatted fingerprint
*/
function get_fingerprint()
{
return enigma_key::format_fingerprint($this->fingerprint);
}
/**
* Returns human-readable name of the key's algorithm
*
* @return string Algorithm name
*/
function get_algorithm()
{
// http://tools.ietf.org/html/rfc4880#section-9.1
switch ($this->algorithm) {
case 1:
case 2:
case 3:
return 'RSA';
case 16:
case 20:
return 'Elgamal';
case 17:
return 'DSA';
case 18:
return 'Elliptic Curve';
case 19:
return 'ECDSA';
case 21:
return 'Diffie-Hellman';
}
}
}
diff --git a/plugins/enigma/lib/enigma_ui.php b/plugins/enigma/lib/enigma_ui.php
index 447804a9f..7ce169f25 100644
--- a/plugins/enigma/lib/enigma_ui.php
+++ b/plugins/enigma/lib/enigma_ui.php
@@ -1,1313 +1,1312 @@
<?php
/**
+-------------------------------------------------------------------------+
| User Interface for the Enigma Plugin |
| |
- | Copyright (C) 2010-2015 The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-------------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-------------------------------------------------------------------------+
*/
class enigma_ui
{
private $rc;
private $enigma;
private $home;
private $css_loaded;
private $js_loaded;
private $data;
private $keys_parts = array();
private $keys_bodies = array();
function __construct($enigma_plugin, $home='')
{
$this->enigma = $enigma_plugin;
$this->rc = $enigma_plugin->rc;
$this->home = $home; // we cannot use $enigma_plugin->home here
}
/**
* UI initialization and requests handlers.
*
* @param string Preferences section
*/
function init()
{
$this->add_js();
$action = rcube_utils::get_input_value('_a', rcube_utils::INPUT_GPC);
if ($this->rc->action == 'plugin.enigmakeys') {
switch ($action) {
case 'delete':
$this->key_delete();
break;
/*
case 'edit':
$this->key_edit();
break;
*/
case 'import':
$this->key_import();
break;
case 'import-search':
$this->key_import_search();
break;
case 'export':
$this->key_export();
break;
case 'generate':
$this->key_generate();
break;
case 'create':
$this->key_create();
break;
case 'search':
case 'list':
$this->key_list();
break;
case 'info':
$this->key_info();
break;
}
$this->rc->output->add_handlers(array(
'keyslist' => array($this, 'tpl_keys_list'),
'countdisplay' => array($this, 'tpl_keys_rowcount'),
'searchform' => array($this->rc->output, 'search_form'),
));
$this->rc->output->set_pagetitle($this->enigma->gettext('enigmakeys'));
$this->rc->output->send('enigma.keys');
}
/*
// Preferences UI
else if ($this->rc->action == 'plugin.enigmacerts') {
$this->rc->output->add_handlers(array(
'keyslist' => array($this, 'tpl_certs_list'),
'keyframe' => array($this, 'tpl_cert_frame'),
'countdisplay' => array($this, 'tpl_certs_rowcount'),
'searchform' => array($this->rc->output, 'search_form'),
));
$this->rc->output->set_pagetitle($this->enigma->gettext('enigmacerts'));
$this->rc->output->send('enigma.certs');
}
*/
// Message composing UI
else if ($this->rc->action == 'compose') {
$this->compose_ui();
}
}
/**
* Adds CSS style file to the page header.
*/
function add_css()
{
if ($this->css_loaded) {
return;
}
$skin_path = $this->enigma->local_skin_path();
$this->enigma->include_stylesheet("$skin_path/enigma.css");
$this->css_loaded = true;
}
/**
* Adds javascript file to the page header.
*/
function add_js()
{
if ($this->js_loaded) {
return;
}
$this->enigma->include_script('enigma.js');
$this->js_loaded = true;
}
/**
* Initializes key password prompt
*
* @param enigma_error $status Error object with key info
* @param array $params Optional prompt parameters
*/
function password_prompt($status, $params = array())
{
$data = $status->getData('missing');
if (empty($data)) {
$data = $status->getData('bad');
}
$keyid = key($data);
$data = array(
'keyid' => $params['keyid'] ?: $keyid,
'user' => $data[$keyid]
);
// With GnuPG 2.1 user name may not be specified (e.g. on private
// key export), we'll get the key information and set the name appropriately
if ($keyid && $params['keyid'] && strpos($data['user'], $keyid) !== false) {
$key = $this->enigma->engine->get_key($params['keyid']);
if ($key && $key->name) {
$data['user'] = $key->name;
}
}
if (!empty($params)) {
$data = array_merge($params, $data);
}
if (preg_match('/^(send|plugin.enigmaimport|plugin.enigmakeys)$/', $this->rc->action)) {
$this->rc->output->command('enigma_password_request', $data);
}
else {
$this->rc->output->set_env('enigma_password_request', $data);
}
// add some labels to client
$this->rc->output->add_label('enigma.enterkeypasstitle', 'enigma.enterkeypass',
'save', 'cancel');
$this->add_css();
$this->add_js();
}
/**
* Template object for list of keys.
*
* @param array Object attributes
*
* @return string HTML content
*/
function tpl_keys_list($attrib)
{
// add id to message list table if not specified
if (!strlen($attrib['id'])) {
$attrib['id'] = 'rcmenigmakeyslist';
}
// define list of cols to be displayed
$a_show_cols = array('name');
// create XHTML table
$out = $this->rc->table_output($attrib, array(), $a_show_cols, 'id');
// set client env
$this->rc->output->add_gui_object('keyslist', $attrib['id']);
$this->rc->output->include_script('list.js');
// add some labels to client
$this->rc->output->add_label('enigma.keyremoveconfirm', 'enigma.keyremoving',
'enigma.keyexportprompt', 'enigma.withprivkeys', 'enigma.onlypubkeys',
'enigma.exportkeys', 'enigma.importkeys', 'enigma.keyimportsearchlabel',
'import', 'search'
);
return $out;
}
/**
* Key listing (and searching) request handler
*/
private function key_list()
{
$this->enigma->load_engine();
$pagesize = $this->rc->config->get('pagesize', 100);
$page = max(intval(rcube_utils::get_input_value('_p', rcube_utils::INPUT_GPC)), 1);
$search = rcube_utils::get_input_value('_q', rcube_utils::INPUT_GPC);
// Get the list
$list = $this->enigma->engine->list_keys($search);
if ($list && ($list instanceof enigma_error))
$this->rc->output->show_message('enigma.keylisterror', 'error');
else if (empty($list))
$this->rc->output->show_message('enigma.nokeysfound', 'notice');
else if (is_array($list)) {
// Save the size
$listsize = count($list);
// Sort the list by key (user) name
usort($list, array('enigma_key', 'cmp'));
// Slice current page
$list = array_slice($list, ($page - 1) * $pagesize, $pagesize);
$size = count($list);
// Add rows
foreach ($list as $key) {
$this->rc->output->command('enigma_add_list_row', array(
'name' => rcube::Q($key->name),
'id' => $key->id,
'flags' => $key->is_private() ? 'p' : ''
));
}
}
$this->rc->output->set_env('rowcount', $size);
$this->rc->output->set_env('search_request', $search);
$this->rc->output->set_env('pagecount', ceil($listsize/$pagesize));
$this->rc->output->set_env('current_page', $page);
$this->rc->output->command('set_rowcount',
$this->get_rowcount_text($listsize, $size, $page));
$this->rc->output->send();
}
/**
* Template object for list records counter.
*
* @param array Object attributes
*
* @return string HTML output
*/
function tpl_keys_rowcount($attrib)
{
if (!$attrib['id'])
$attrib['id'] = 'rcmcountdisplay';
$this->rc->output->add_gui_object('countdisplay', $attrib['id']);
return html::span($attrib, $this->get_rowcount_text());
}
/**
* Returns text representation of list records counter
*/
private function get_rowcount_text($all=0, $curr_count=0, $page=1)
{
if (!$curr_count) {
$out = $this->enigma->gettext('nokeysfound');
}
else {
$pagesize = $this->rc->config->get('pagesize', 100);
$first = ($page - 1) * $pagesize;
$out = $this->enigma->gettext(array(
'name' => 'keysfromto',
'vars' => array(
'from' => $first + 1,
'to' => $first + $curr_count,
'count' => $all)
));
}
return $out;
}
/**
* Key information page handler
*/
private function key_info()
{
$this->enigma->load_engine();
$id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GET);
$res = $this->enigma->engine->get_key($id);
if ($res instanceof enigma_key) {
$this->data = $res;
}
else { // error
$this->rc->output->show_message('enigma.keyopenerror', 'error');
$this->rc->output->command('parent.enigma_loadframe');
$this->rc->output->send('iframe');
}
$this->rc->output->add_handlers(array(
'keyname' => array($this, 'tpl_key_name'),
'keydata' => array($this, 'tpl_key_data'),
));
$this->rc->output->set_pagetitle($this->enigma->gettext('keyinfo'));
$this->rc->output->send('enigma.keyinfo');
}
/**
* Template object for key name
*/
function tpl_key_name($attrib)
{
return rcube::Q($this->data->name);
}
/**
* Template object for key information page content
*/
function tpl_key_data($attrib)
{
$out = '';
$table = new html_table(array('cols' => 2));
// Key user ID
$table->add('title', html::label(null, $this->enigma->gettext('keyuserid')));
$table->add(null, rcube::Q($this->data->name));
// Key ID
$table->add('title', html::label(null, $this->enigma->gettext('keyid')));
$table->add(null, $this->data->subkeys[0]->get_short_id());
// Key type
$keytype = $this->data->get_type();
if ($keytype == enigma_key::TYPE_KEYPAIR) {
$type = $this->enigma->gettext('typekeypair');
}
else if ($keytype == enigma_key::TYPE_PUBLIC) {
$type = $this->enigma->gettext('typepublickey');
}
$table->add('title', html::label(null, $this->enigma->gettext('keytype')));
$table->add(null, $type);
// Key fingerprint
$table->add('title', html::label(null, $this->enigma->gettext('fingerprint')));
$table->add(null, $this->data->subkeys[0]->get_fingerprint());
$out .= html::tag('fieldset', null,
html::tag('legend', null,
$this->enigma->gettext('basicinfo')) . $table->show($attrib));
// Subkeys
$table = new html_table(array('cols' => 5, 'id' => 'enigmasubkeytable', 'class' => 'records-table'));
$table->add_header('id', $this->enigma->gettext('subkeyid'));
$table->add_header('algo', $this->enigma->gettext('subkeyalgo'));
$table->add_header('created', $this->enigma->gettext('subkeycreated'));
$table->add_header('expires', $this->enigma->gettext('subkeyexpires'));
$table->add_header('usage', $this->enigma->gettext('subkeyusage'));
$now = time();
$date_format = $this->rc->config->get('date_format', 'Y-m-d');
$usage_map = array(
enigma_key::CAN_ENCRYPT => $this->enigma->gettext('typeencrypt'),
enigma_key::CAN_SIGN => $this->enigma->gettext('typesign'),
enigma_key::CAN_CERTIFY => $this->enigma->gettext('typecert'),
enigma_key::CAN_AUTHENTICATE => $this->enigma->gettext('typeauth'),
);
foreach ($this->data->subkeys as $subkey) {
$algo = $subkey->get_algorithm();
if ($algo && $subkey->length) {
$algo .= ' (' . $subkey->length . ')';
}
$usage = array();
foreach ($usage_map as $key => $text) {
if ($subkey->usage & $key) {
$usage[] = $text;
}
}
$table->add('id', $subkey->get_short_id());
$table->add('algo', $algo);
$table->add('created', $subkey->created ? $this->rc->format_date($subkey->created, $date_format, false) : '');
$table->add('expires', $subkey->expires ? $this->rc->format_date($subkey->expires, $date_format, false) : $this->enigma->gettext('expiresnever'));
$table->add('usage', implode(',', $usage));
$table->set_row_attribs($subkey->revoked || ($subkey->expires && $subkey->expires < $now) ? 'deleted' : '');
}
$out .= html::tag('fieldset', null,
html::tag('legend', null,
$this->enigma->gettext('subkeys')) . $table->show());
// Additional user IDs
$table = new html_table(array('cols' => 2, 'id' => 'enigmausertable', 'class' => 'records-table'));
$table->add_header('id', $this->enigma->gettext('userid'));
$table->add_header('valid', $this->enigma->gettext('uservalid'));
foreach ($this->data->users as $user) {
$username = $user->name;
if ($user->comment) {
$username .= ' (' . $user->comment . ')';
}
$username .= ' <' . $user->email . '>';
$table->add('id', rcube::Q(trim($username)));
$table->add('valid', $this->enigma->gettext($user->valid ? 'valid' : 'unknown'));
$table->set_row_attribs($user->revoked || !$user->valid ? 'deleted' : '');
}
$out .= html::tag('fieldset', null,
html::tag('legend', null,
$this->enigma->gettext('userids')) . $table->show());
return $out;
}
/**
* Key(s) export handler
*/
private function key_export()
{
$keys = rcube_utils::get_input_value('_keys', rcube_utils::INPUT_POST);
$priv = rcube_utils::get_input_value('_priv', rcube_utils::INPUT_POST);
$engine = $this->enigma->load_engine();
$list = $keys == '*' ? $engine->list_keys() : explode(',', $keys);
if (is_array($list) && ($fp = fopen('php://memory', 'rw'))) {
$filename = 'export.pgp';
if (count($list) == 1) {
$filename = (is_object($list[0]) ? $list[0]->id : $list[0]) . '.pgp';
}
$status = null;
foreach ($list as $key) {
$keyid = is_object($key) ? $key->id : $key;
$status = $engine->export_key($keyid, $fp, (bool) $priv);
if ($status instanceof enigma_error) {
$code = $status->getCode();
if ($code == enigma_error::BADPASS) {
$this->password_prompt($status, array(
'input_keys' => $keys,
'input_priv' => 1,
'input_task' => 'settings',
'input_action' => 'plugin.enigmakeys',
'input_a' => 'export',
'action' => '?',
'iframe' => true,
'nolock' => true,
'keyid' => $keyid,
));
fclose($fp);
$this->rc->output->send('iframe');
}
}
}
// send downlaod headers
header('Content-Type: application/pgp-keys');
header('Content-Disposition: attachment; filename="' . $filename . '"');
rewind($fp);
while (!feof($fp)) {
echo fread($fp, 1024 * 1024);
}
fclose($fp);
}
exit;
}
/**
* Key import (page) handler
*/
private function key_import()
{
// Import process
if ($data = rcube_utils::get_input_value('_keys', rcube_utils::INPUT_POST)) {
$this->enigma->load_engine();
$this->enigma->engine->password_handler();
$result = $this->enigma->engine->import_key($data);
if (is_array($result)) {
if (rcube_utils::get_input_value('_generated', rcube_utils::INPUT_POST)) {
$this->rc->output->command('enigma_key_create_success');
$this->rc->output->show_message('enigma.keygeneratesuccess', 'confirmation');
}
else {
$this->rc->output->show_message('enigma.keysimportsuccess', 'confirmation',
array('new' => $result['imported'], 'old' => $result['unchanged']));
if ($result['imported'] && !empty($_POST['_refresh'])) {
$this->rc->output->command('enigma_list', 1, false);
}
}
}
else {
$this->rc->output->show_message('enigma.keysimportfailed', 'error');
}
$this->rc->output->send();
}
else if ($_FILES['_file']['tmp_name'] && is_uploaded_file($_FILES['_file']['tmp_name'])) {
$this->enigma->load_engine();
$result = $this->enigma->engine->import_key($_FILES['_file']['tmp_name'], true);
if (is_array($result)) {
// reload list if any keys has been added
if ($result['imported']) {
$this->rc->output->command('parent.enigma_list', 1);
}
$this->rc->output->show_message('enigma.keysimportsuccess', 'confirmation',
array('new' => $result['imported'], 'old' => $result['unchanged']));
$this->rc->output->command('parent.enigma_import_success');
}
else if ($result instanceof enigma_error && $result->getCode() == enigma_error::BADPASS) {
$this->password_prompt($result);
}
else {
$this->rc->output->show_message('enigma.keysimportfailed', 'error');
}
$this->rc->output->send('iframe');
}
else if ($err = $_FILES['_file']['error']) {
if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
$this->rc->output->show_message('filesizeerror', 'error',
array('size' => $this->rc->show_bytes(rcube_utils::max_upload_size())));
} else {
$this->rc->output->show_message('fileuploaderror', 'error');
}
$this->rc->output->send('iframe');
}
$this->rc->output->add_handlers(array(
'importform' => array($this, 'tpl_key_import_form'),
));
$this->rc->output->send('enigma.keyimport');
}
/**
* Key import-search (page) handler
*/
private function key_import_search()
{
$this->rc->output->add_handlers(array(
'importform' => array($this, 'tpl_key_import_form'),
));
$this->rc->output->send('enigma.keysearch');
}
/**
* Template object for key import (upload) form
*/
function tpl_key_import_form($attrib)
{
$attrib += array('id' => 'rcmKeyImportForm');
if (empty($attrib['part']) || $attrib['part'] == 'import') {
$title = $this->enigma->gettext('keyimportlabel');
$upload = new html_inputfield(array('type' => 'file', 'name' => '_file',
'id' => 'rcmimportfile', 'size' => 30));
$max_filesize = $this->rc->upload_init();
$upload_button = new html_button(array(
'class' => 'button import',
'onclick' => "return rcmail.command('plugin.enigma-import','',this,event)",
));
$form = html::div(null, html::p(null, rcube::Q($this->enigma->gettext('keyimporttext'), 'show'))
. $upload->show()
. html::div('hint', $this->rc->gettext(array('id' => 'importfile', 'name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize))))
. (empty($attrib['part']) ? html::br() . html::br() . $upload_button->show($this->rc->gettext('import')) : '')
);
if (empty($attrib['part'])) {
$form = html::tag('fieldset', '', html::tag('legend', null, $title) . $form);
}
else {
$this->rc->output->set_pagetitle($title);
}
}
if (empty($attrib['part']) || $attrib['part'] == 'search') {
$title = $this->enigma->gettext('keyimportsearchlabel');
$search = new html_inputfield(array('type' => 'text', 'name' => '_search',
'id' => 'rcmimportsearch', 'size' => 30));
$search_button = new html_button(array(
'class' => 'button search',
'onclick' => "return rcmail.command('plugin.enigma-import-search','',this,event)",
));
$form = html::div(null,
rcube::Q($this->enigma->gettext('keyimportsearchtext'), 'show')
. html::br() . html::br() . $search->show()
. (empty($attrib['part']) ? html::br() . html::br() . $search_button->show($this->rc->gettext('search')) : '')
);
if (empty($attrib['part'])) {
$form = html::tag('fieldset', '', html::tag('legend', null, $title) . $form);
}
else {
$this->rc->output->set_pagetitle($title);
}
$this->rc->output->include_script('publickey.js');
}
$this->rc->output->add_label('selectimportfile', 'importwait', 'nopubkeyfor', 'nopubkeyforsender',
'encryptnoattachments','encryptedsendialog','searchpubkeyservers', 'importpubkeys',
'encryptpubkeysfound', 'search', 'close', 'import', 'keyid', 'keylength', 'keyexpired',
'keyrevoked', 'keyimportsuccess', 'keyservererror');
$this->rc->output->add_gui_object('importform', $attrib['id']);
$out = $this->rc->output->form_tag(array(
'action' => $this->rc->url(array('action' => $this->rc->action, 'a' => 'import')),
'method' => 'post',
'enctype' => 'multipart/form-data') + $attrib,
$form
);
return $out;
}
/**
* Server-side key pair generation handler
*/
private function key_generate()
{
// Crypt_GPG does not support key generation for multiple identities
// It is also very slow (which is problematic because it may exceed
// request time limit) and requires entropy generator
// That's why we use only OpenPGP.js method of key generation
return;
$user = rcube_utils::get_input_value('_user', rcube_utils::INPUT_POST, true);
$pass = rcube_utils::get_input_value('_password', rcube_utils::INPUT_POST, true);
$size = (int) rcube_utils::get_input_value('_size', rcube_utils::INPUT_POST);
if ($size > 4096) {
$size = 4096;
}
$ident = rcube_mime::decode_address_list($user, 1, false);
if (empty($ident)) {
$this->rc->output->show_message('enigma.keygenerateerror', 'error');
$this->rc->output->send();
}
$this->enigma->load_engine();
$result = $this->enigma->engine->generate_key(array(
'user' => $ident[1]['name'],
'email' => $ident[1]['mailto'],
'password' => $pass,
'size' => $size,
));
if ($result instanceof enigma_key) {
$this->rc->output->command('enigma_key_create_success');
$this->rc->output->show_message('enigma.keygeneratesuccess', 'confirmation');
}
else {
$this->rc->output->show_message('enigma.keygenerateerror', 'error');
}
$this->rc->output->send();
}
/**
* Key generation page handler
*/
private function key_create()
{
$this->enigma->include_script('openpgp.min.js');
$this->rc->output->add_handlers(array(
'keyform' => array($this, 'tpl_key_create_form'),
));
$this->rc->output->set_pagetitle($this->enigma->gettext('keygenerate'));
$this->rc->output->send('enigma.keycreate');
}
/**
* Template object for key generation form
*/
function tpl_key_create_form($attrib)
{
$attrib += array('id' => 'rcmKeyCreateForm');
$table = new html_table(array('cols' => 2));
// get user's identities
$identities = $this->rc->user->list_identities(null, true);
$checkbox = new html_checkbox(array('name' => 'identity[]'));
$plugin = $this->rc->plugins->exec_hook('enigma_user_identities', array('identities' => $identities));
$identities = $plugin['identities'];
foreach ($identities as $idx => $ident) {
$name = empty($ident['name']) ? ($ident['email']) : $ident['ident'];
$attr = array('value' => $idx, 'data-name' => $ident['name'], 'data-email' => $ident['email']);
$identities[$idx] = html::tag('li', null, html::label(null, $checkbox->show($idx, $attr) . rcube::Q($name)));
}
$table->add('title', html::label('key-name', rcube::Q($this->enigma->gettext('newkeyident'))));
$table->add(null, html::tag('ul', 'proplist', implode($identities, "\n")));
// Key size
$select = new html_select(array('name' => 'size', 'id' => 'key-size'));
$select->add($this->enigma->gettext('key2048'), '2048');
$select->add($this->enigma->gettext('key4096'), '4096');
$table->add('title', html::label('key-size', rcube::Q($this->enigma->gettext('newkeysize'))));
$table->add(null, $select->show());
// Password and confirm password
$table->add('title', html::label('key-pass', rcube::Q($this->enigma->gettext('newkeypass'))));
$table->add(null, rcube_output::get_edit_field('password', '', array(
'id' => 'key-pass',
'size' => $attrib['size'],
'required' => true,
'autocomplete' => 'new-password',
'oninput' => "this.type = this.value.length ? 'password' : 'text'",
), 'text'));
$table->add('title', html::label('key-pass-confirm', rcube::Q($this->enigma->gettext('newkeypassconfirm'))));
$table->add(null, rcube_output::get_edit_field('password-confirm', '', array(
'id' => 'key-pass-confirm',
'size' => $attrib['size'],
'required' => true,
'autocomplete' => 'new-password',
'oninput' => "this.type = this.value.length ? 'password' : 'text'",
), 'text'));
$this->rc->output->add_gui_object('keyform', $attrib['id']);
$this->rc->output->add_label('enigma.keygenerating', 'enigma.formerror',
'enigma.passwordsdiffer', 'enigma.keygenerateerror', 'enigma.noidentselected',
'enigma.keygennosupport');
return $this->rc->output->form_tag(array(), $table->show($attrib));
}
/**
* Key deleting
*/
private function key_delete()
{
$keys = rcube_utils::get_input_value('_keys', rcube_utils::INPUT_POST);
$engine = $this->enigma->load_engine();
foreach ((array)$keys as $key) {
$res = $engine->delete_key($key);
if ($res !== true) {
$this->rc->output->show_message('enigma.keyremoveerror', 'error');
$this->rc->output->command('enigma_list');
$this->rc->output->send();
}
}
$this->rc->output->command('enigma_list');
$this->rc->output->show_message('enigma.keyremovesuccess', 'confirmation');
$this->rc->output->send();
}
/**
* Init compose UI (add task button and the menu)
*/
private function compose_ui()
{
$this->add_css();
$this->rc->output->add_label('enigma.sendunencrypted');
// Elastic skin (or a skin based on it)
if (array_key_exists('elastic', (array) $this->rc->output->skins)) {
$this->enigma->api->add_content($this->compose_ui_options(), 'composeoptions');
}
// other skins
else {
// Options menu button
$this->enigma->add_button(array(
'type' => 'link',
'command' => 'plugin.enigma',
'onclick' => "rcmail.command('menu-open', 'enigmamenu', event.target, event)",
'class' => 'button enigma',
'title' => 'encryptionoptions',
'label' => 'encryption',
'domain' => $this->enigma->ID,
'width' => 32,
'height' => 32,
'aria-owns' => 'enigmamenu',
'aria-haspopup' => 'true',
'aria-expanded' => 'false',
), 'toolbar');
// Options menu contents
$this->rc->output->add_footer($this->compose_ui_options(true));
}
}
/**
* Init compose UI (add task button and the menu)
*/
private function compose_ui_options($wrap = false)
{
$locks = (array) $this->rc->config->get('enigma_options_lock');
$chbox = new html_checkbox(array('value' => 1));
$out = html::div('form-group form-check row',
html::label(array('for' => 'enigmasignopt', 'class' => 'col-form-label col-6'),
rcube::Q($this->enigma->gettext('signmsg')))
. html::div('form-check col-6',
$chbox->show($this->rc->config->get('enigma_sign_all') ? 1 : 0, array(
'name' => '_enigma_sign',
'id' => 'enigmasignopt',
'class' => 'form-check-input',
'disabled' => in_array('sign', $locks),
))));
$out .= html::div('form-group form-check row',
html::label(array('for' => 'enigmaencryptopt', 'class' => 'col-form-label col-6'),
rcube::Q($this->enigma->gettext('encryptmsg')))
. html::div('form-check col-6',
$chbox->show($this->rc->config->get('enigma_encrypt_all') ? 1 : 0, array(
'name' => '_enigma_encrypt',
'id' => 'enigmaencryptopt',
'class' => 'form-check-input',
'disabled' => in_array('encrypt', $locks),
))));
$out .= html::div('form-group form-check row',
html::label(array('for' => 'enigmaattachpubkeyopt', 'class' => 'col-form-label col-6'),
rcube::Q($this->enigma->gettext('attachpubkeymsg')))
. html::div('form-check col-6',
$chbox->show($this->rc->config->get('enigma_attach_pubkey') ? 1 : 0, array(
'name' => '_enigma_attachpubkey',
'id' => 'enigmaattachpubkeyopt',
'class' => 'form-check-input',
'disabled' => in_array('pubkey', $locks),
))));
if (!$wrap) {
return $out;
}
return html::div(array('id' => 'enigmamenu', 'class' => 'popupmenu'), $out);
}
/**
* Handler for message_body_prefix hook.
* Called for every displayed (content) part of the message.
* Adds infobox about signature verification and/or decryption
* status above the body.
*
* @param array Original parameters
*
* @return array Modified parameters
*/
function status_message($p)
{
// skip: not a message part
if ($p['part'] instanceof rcube_message) {
return $p;
}
// skip: message has no signed/encoded content
if (!$this->enigma->engine) {
return $p;
}
$engine = $this->enigma->engine;
$part_id = $p['part']->mime_id;
$messages = array();
// Decryption status
if (($found = $this->find_part_id($part_id, $engine->decryptions)) !== null
&& ($status = $engine->decryptions[$found])
) {
$attach_scripts = true;
// show the message only once
unset($engine->decryptions[$found]);
// display status info
$attrib['id'] = 'enigma-message';
if ($status instanceof enigma_error) {
$attrib['class'] = 'boxerror enigmaerror encrypted';
$code = $status->getCode();
if ($code == enigma_error::KEYNOTFOUND) {
$msg = rcube::Q(str_replace('$keyid', enigma_key::format_id($status->getData('id')),
$this->enigma->gettext('decryptnokey')));
}
else if ($code == enigma_error::BADPASS) {
$missing = $status->getData('missing');
$label = 'decrypt' . (!empty($missing) ? 'no' : 'bad') . 'pass';
$msg = rcube::Q($this->enigma->gettext($label));
$this->password_prompt($status);
}
else if ($code == enigma_error::NOMDC) {
$msg = rcube::Q($this->enigma->gettext('decryptnomdc'));
}
else {
$msg = rcube::Q($this->enigma->gettext('decrypterror'));
}
}
else if ($status === enigma_engine::ENCRYPTED_PARTIALLY) {
$attrib['class'] = 'boxwarning enigmawarning encrypted';
$msg = rcube::Q($this->enigma->gettext('decryptpartial'));
}
else {
$attrib['class'] = 'boxconfirmation enigmanotice encrypted';
$msg = rcube::Q($this->enigma->gettext('decryptok'));
}
$attrib['msg'] = $msg;
$messages[] = $attrib;
}
// Signature verification status
if (($found = $this->find_part_id($part_id, $engine->signatures)) !== null
&& ($sig = $engine->signatures[$found])
) {
$attach_scripts = true;
// show the message only once
unset($engine->signatures[$found]);
// display status info
$attrib['id'] = 'enigma-message';
if ($sig instanceof enigma_signature) {
$sender = $sig->name ?: '';
if ($sig->email) {
$sender .= ' <' . $sig->email . '>';
}
if ($sig->valid === enigma_error::UNVERIFIED) {
$attrib['class'] = 'boxwarning enigmawarning signed';
$msg = str_replace('$sender', $sender, $this->enigma->gettext('sigunverified'));
$msg = str_replace('$keyid', $sig->id, $msg);
$msg = rcube::Q($msg);
}
else if ($sig->valid) {
$attrib['class'] = ($sig->partial ? 'boxwarning enigmawarning' : 'boxconfirmation enigmanotice') . ' signed';
$label = 'sigvalid' . ($sig->partial ? 'partial' : '');
$msg = rcube::Q(str_replace('$sender', $sender, $this->enigma->gettext($label)));
}
else {
$attrib['class'] = 'boxwarning enigmawarning signed';
if ($sender) {
$msg = rcube::Q(str_replace('$sender', $sender, $this->enigma->gettext('siginvalid')));
}
else {
$msg = rcube::Q(str_replace('$keyid', enigma_key::format_id($sig->id),
$this->enigma->gettext('signokey')));
}
}
}
else if ($sig && $sig->getCode() == enigma_error::KEYNOTFOUND) {
$attrib['class'] = 'boxwarning enigmawarning signed';
$msg = rcube::Q(str_replace('$keyid', enigma_key::format_id($sig->getData('id')),
$this->enigma->gettext('signokey')));
}
else {
$attrib['class'] = 'boxwarning enigmaerror signed';
$msg = rcube::Q($this->enigma->gettext('sigerror'));
}
/*
$msg .= '&nbsp;' . html::a(array('href' => "#sigdetails",
'onclick' => rcmail_output::JS_OBJECT_NAME.".command('enigma-sig-details')"),
rcube::Q($this->enigma->gettext('showdetails')));
*/
// test
// $msg .= '<br /><pre>'.$sig->body.'</pre>';
$attrib['msg'] = $msg;
$messages[] = $attrib;
}
if ($count = count($messages)) {
if ($count == 2 && $messages[0]['class'] == $messages[1]['class']) {
$p['prefix'] .= html::div($messages[0], $messages[0]['msg'] . ' ' . $messages[1]['msg']);
}
else {
foreach ($messages as $msg) {
$p['prefix'] .= html::div($msg, $msg['msg']);
}
}
}
if ($attach_scripts) {
// add css and js script
$this->add_css();
$this->add_js();
}
return $p;
}
/**
* Handler for message_load hook.
* Check message bodies and attachments for keys/certs.
*/
function message_load($p)
{
$engine = $this->enigma->load_engine();
// handle keys/certs in attachments
foreach ((array) $p['object']->attachments as $attachment) {
if ($engine->is_keys_part($attachment)) {
$this->keys_parts[] = $attachment->mime_id;
}
}
// the same with message bodies
foreach ((array) $p['object']->parts as $part) {
if ($engine->is_keys_part($part)) {
$this->keys_parts[] = $part->mime_id;
$this->keys_bodies[] = $part->mime_id;
}
}
// @TODO: inline PGP keys
if ($this->keys_parts) {
$this->enigma->add_texts('localization');
}
return $p;
}
/**
* Handler for template_object_messagebody hook.
* This callback function adds a box below the message content
* if there is a key/cert attachment available
*/
function message_output($p)
{
foreach ($this->keys_parts as $part) {
// remove part's body
if (in_array($part, $this->keys_bodies)) {
$p['content'] = '';
}
// add box above the message body
$p['content'] = html::p(array('class' => 'enigmaattachment boxinformation aligned-buttons'),
html::span(null, rcube::Q($this->enigma->gettext('keyattfound'))) .
html::tag('button', array(
'onclick' => "return ".rcmail_output::JS_OBJECT_NAME.".enigma_import_attachment('".rcube::JQ($part)."')",
'title' => $this->enigma->gettext('keyattimport'),
'class' => 'import',
), rcube::Q($this->rc->gettext('import')))
) . $p['content'];
$attach_scripts = true;
}
if ($attach_scripts) {
// add css and js script
$this->add_css();
$this->add_js();
}
return $p;
}
/**
* Handle message_ready hook (encryption/signing/attach public key)
*/
function message_ready($p)
{
// The message might have been already encrypted by Mailvelope
if (strpos($p['message']->getParam('ctype'), 'multipart/encrypted') === 0) {
return $p;
}
$savedraft = !empty($_POST['_draft']) && empty($_GET['_saveonly']);
$sign_enable = (bool) rcube_utils::get_input_value('_enigma_sign', rcube_utils::INPUT_POST);
$encrypt_enable = (bool) rcube_utils::get_input_value('_enigma_encrypt', rcube_utils::INPUT_POST);
$pubkey_enable = (bool) rcube_utils::get_input_value('_enigma_attachpubkey', rcube_utils::INPUT_POST);
$locks = (array) $this->rc->config->get('enigma_options_lock');
if (in_array('sign', $locks)) {
$sign_enable = (bool) $this->rc->config->get('enigma_sign_all');
}
if (in_array('encrypt', $locks)) {
$encrypt_enable = (bool) $this->rc->config->get('enigma_encrypt_all');
}
if (in_array('pubkey', $locks)) {
$pubkey_enable = (bool) $this->rc->config->get('enigma_attach_pubkey');
}
if (!$savedraft && $pubkey_enable) {
$engine = $this->enigma->load_engine();
$engine->attach_public_key($p['message']);
}
if ($encrypt_enable) {
$engine = $this->enigma->load_engine();
$mode = !$savedraft && $sign_enable ? enigma_engine::ENCRYPT_MODE_SIGN : null;
$status = $engine->encrypt_message($p['message'], $mode, $savedraft);
$mode = 'encrypt';
}
else if (!$savedraft && $sign_enable) {
$engine = $this->enigma->load_engine();
$status = $engine->sign_message($p['message'], enigma_engine::SIGN_MODE_MIME);
$mode = 'sign';
}
if ($mode && ($status instanceof enigma_error)) {
$code = $status->getCode();
if ($code == enigma_error::KEYNOTFOUND) {
$vars = array('email' => $status->getData('missing'));
$msg = 'enigma.' . $mode . 'nokey';
}
else if ($code == enigma_error::BADPASS) {
$this->password_prompt($status);
}
else {
$msg = 'enigma.' . $mode . 'error';
}
if ($msg) {
if ($vars && $vars['email']) {
$this->rc->output->command('enigma_key_not_found', array(
'email' => $vars['email'],
'text' => $this->rc->gettext(array('name' => $msg, 'vars' => $vars)),
'title' => $this->enigma->gettext('keynotfound'),
'button' => $this->enigma->gettext('findkey'),
'mode' => $mode,
));
}
else {
$this->rc->output->show_message($msg, 'error', $vars);
}
}
$this->rc->output->send('iframe');
}
return $p;
}
/**
* Handler for message_compose_body hook
* Display error when the message cannot be encrypted
* and provide a way to try again with a password.
*/
function message_compose($p)
{
$engine = $this->enigma->load_engine();
// skip: message has no signed/encoded content
if (!$this->enigma->engine) {
return $p;
}
$engine = $this->enigma->engine;
$locks = (array) $this->rc->config->get('enigma_options_lock');
// Decryption status
foreach ($engine->decryptions as $status) {
if ($status instanceof enigma_error) {
$code = $status->getCode();
if ($code == enigma_error::KEYNOTFOUND) {
$msg = rcube::Q(str_replace('$keyid', enigma_key::format_id($status->getData('id')),
$this->enigma->gettext('decryptnokey')));
}
else if ($code == enigma_error::BADPASS) {
$this->password_prompt($status, array('compose-init' => true));
return $p;
}
else {
$msg = rcube::Q($this->enigma->gettext('decrypterror'));
}
}
}
if ($msg) {
$this->rc->output->show_message($msg, 'error');
}
// Check sign/ecrypt options for signed/encrypted drafts
if (!in_array('encrypt', $locks)) {
$this->rc->output->set_env('enigma_force_encrypt', !empty($engine->decryptions));
}
if (!in_array('sign', $locks)) {
$this->rc->output->set_env('enigma_force_sign', !empty($engine->signatures));
}
return $p;
}
/**
* Handler for keys/certs import request action
*/
function import_file()
{
$uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
$mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
$storage = $this->rc->get_storage();
$engine = $this->enigma->load_engine();
if ($uid && $mime_id) {
// Note: we get the attachment body via rcube_message class
// to support keys inside encrypted messages (#5285)
$message = new rcube_message($uid, $mbox);
// Check if we don't need to ask for password again
foreach ($engine->decryptions as $status) {
if ($status instanceof enigma_error) {
if ($status->getCode() == enigma_error::BADPASS) {
$this->password_prompt($status, array(
'input_uid' => $uid,
'input_mbox' => $mbox,
'input_part' => $mime_id,
'input_task' => 'mail',
'input_action' => 'plugin.enigmaimport',
'action' => '?',
'iframe' => true,
));
$this->rc->output->send($this->rc->output->type == 'html' ? 'iframe' : null);
return;
}
}
}
if ($engine->is_keys_part($message->mime_parts[$mime_id])) {
$part = $message->get_part_body($mime_id);
}
}
if ($part && is_array($result = $engine->import_key($part))) {
$this->rc->output->show_message('enigma.keysimportsuccess', 'confirmation',
array('new' => $result['imported'], 'old' => $result['unchanged']));
}
else {
$this->rc->output->show_message('enigma.keysimportfailed', 'error');
}
$this->rc->output->send($this->rc->output->type == 'html' ? 'iframe' : null);
}
/**
* Check if the part or its parent exists in the array
* of decryptions/signatures. Returns found ID.
*/
private function find_part_id($part_id, $data)
{
$ids = explode('.', $part_id);
$i = 0;
$count = count($ids);
while ($i < $count && strlen($part = implode('.', array_slice($ids, 0, ++$i)))) {
if (array_key_exists($part, $data)) {
return $part;
}
}
}
}
diff --git a/plugins/enigma/lib/enigma_userid.php b/plugins/enigma/lib/enigma_userid.php
index 11baef4db..d89e5a74b 100644
--- a/plugins/enigma/lib/enigma_userid.php
+++ b/plugins/enigma/lib/enigma_userid.php
@@ -1,25 +1,24 @@
<?php
/**
+-------------------------------------------------------------------------+
| User ID class for the Enigma Plugin |
| |
- | Copyright (C) 2010-2015 The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-------------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-------------------------------------------------------------------------+
*/
class enigma_userid
{
public $revoked;
public $valid;
public $name;
public $comment;
public $email;
}
diff --git a/plugins/enigma/localization/en_US.inc b/plugins/enigma/localization/en_US.inc
index 2378328d1..5772be7ec 100644
--- a/plugins/enigma/localization/en_US.inc
+++ b/plugins/enigma/localization/en_US.inc
@@ -1,144 +1,142 @@
<?php
/**
+-----------------------------------------------------------------------+
- | plugins/enigma/localization/<lang>.inc |
+ | Localization file of the Roundcube Webmail Enigma plugin |
| |
- | Localization file of the Roundcube Webmail ACL plugin |
- | Copyright (C) 2012-2016, The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-----------------------------------------------------------------------+
For translation see https://www.transifex.com/roundcube/roundcube-webmail/plugin-enigma/
*/
$labels = array();
$labels['encryption'] = 'Encryption';
$labels['enigmacerts'] = 'S/MIME Certificates';
$labels['enigmakeys'] = 'PGP Keys';
$labels['keysfromto'] = 'Keys $from to $to of $count';
$labels['keyname'] = 'Name';
$labels['keyid'] = 'Key ID';
$labels['keyuserid'] = 'User ID';
$labels['keytype'] = 'Key type';
$labels['fingerprint'] = 'Fingerprint';
$labels['subkeys'] = 'Subkeys';
$labels['keyprops'] = 'Key properties';
$labels['basicinfo'] = 'Basic Information';
$labels['userids'] = 'Additional Users';
$labels['typepublickey'] = 'public key';
$labels['typekeypair'] = 'key pair';
$labels['keyattfound'] = 'This message contains attached PGP key(s).';
$labels['keyattimport'] = 'Import key(s)';
$labels['typesign'] = 'Sign';
$labels['typeencrypt'] = 'Encrypt';
$labels['typecert'] = 'Certify';
$labels['typeauth'] = 'Authentication';
$labels['subkeyid'] = 'ID';
$labels['subkeyalgo'] = 'Algorithm';
$labels['subkeycreated'] = 'Created';
$labels['subkeyexpires'] = 'Expires';
$labels['subkeyusage'] = 'Usage';
$labels['expiresnever'] = 'never';
$labels['unknown'] = 'unknown';
$labels['uservalid'] = 'Valid';
$labels['userid'] = 'ID';
$labels['valid'] = 'valid';
$labels['supportencryption'] = 'Enable message encryption and signing';
$labels['supportsignatures'] = 'Enable message signatures verification';
$labels['supportdecryption'] = 'Enable message decryption';
$labels['signdefault'] = 'Sign all messages by default';
$labels['encryptdefault'] = 'Encrypt all messages by default';
$labels['attachpubkeydefault'] = 'Attach my public PGP key by default';
$labels['passwordtime'] = 'Keep private key passwords for';
$labels['nminutes'] = '$m minute(s)';
$labels['wholesession'] = 'the whole session';
$labels['createkeys'] = 'Create a new key pair';
$labels['importkeys'] = 'Import key(s)';
$labels['exportkeys'] = 'Export key(s)';
$labels['keyactions'] = 'Key actions...';
$labels['keyremove'] = 'Remove';
$labels['keydisable'] = 'Disable';
$labels['keyrevoke'] = 'Revoke';
$labels['keysend'] = 'Send public key in a message';
$labels['keychpass'] = 'Change password';
$labels['newkeyident'] = 'Identity';
$labels['newkeypass'] = 'Password';
$labels['newkeypassconfirm'] = 'Confirm password';
$labels['newkeysize'] = 'Key size';
$labels['key2048'] = '2048 bits - default';
$labels['key4096'] = '4096 bits - more secure';
$labels['keygenerating'] = 'Generating keys...';
$labels['encryptionoptions'] = 'Encryption options...';
$labels['encryptmsg'] = 'Encrypt this message';
$labels['signmsg'] = 'Digitally sign this message';
$labels['sendunencrypted'] = 'Send unencrypted';
$labels['enterkeypasstitle'] = 'Enter key passphrase';
$labels['enterkeypass'] = 'A passphrase is needed to unlock the secret key ($keyid) for user: $user.';
$labels['arialabelkeyexportoptions'] = 'Keys export options';
$labels['attachpubkeymsg'] = 'Attach my public key';
$labels['keyexportprompt'] = 'Do you want to include secret keys in the saved OpenPGP keys file?';
$labels['onlypubkeys'] = 'Export Public Keys Only';
$labels['withprivkeys'] = 'Export Secret Keys';
$labels['findkey'] = 'Search on key server(s)';
$labels['keyimportlabel'] = 'Import from file';
$labels['keyimportsearchlabel'] = 'Search on key server(s)';
$labels['managekeys'] = 'Manage PGP keys';
$labels['identitymatchingprivkeys'] = 'You have $nr matching PGP private keys stored in your keyring:';
$labels['identitynoprivkeys'] = 'This sender identity doesn\'t yet have a PGP private key stored in your keyring.';
$messages = array();
$messages['sigvalid'] = 'Verified signature from $sender.';
$messages['sigvalidpartial'] = 'Verified signature from $sender, but part of the body was not signed.';
$messages['siginvalid'] = 'Invalid signature from $sender.';
$messages['sigunverified'] = 'Unverified signature. Certificate not verified. Certificate ID: $keyid.';
$messages['signokey'] = 'Unverified signature. Public key not found. Key ID: $keyid.';
$messages['sigerror'] = 'Unverified signature. Internal error.';
$messages['decryptok'] = 'Message decrypted.';
$messages['decrypterror'] = 'Decryption failed.';
$messages['decryptnokey'] = 'Decryption failed. Private key not found. Key ID: $keyid.';
$messages['decryptnomdc'] = 'Decryption skipped. Message is not integrity protected.';
$messages['decryptbadpass'] = 'Decryption failed. Invalid password.';
$messages['decryptnopass'] = 'Decryption failed. Key password required.';
$messages['decryptpartial'] = 'Message decrypted, but part of the body was not encrypted.';
$messages['signerror'] = 'Signing failed.';
$messages['signnokey'] = 'Signing failed. Private key not found.';
$messages['signbadpass'] = 'Signing failed. Invalid password.';
$messages['signnopass'] = 'Signing failed. Key password required.';
$messages['encrypterror'] = 'Encryption failed.';
$messages['encryptnokey'] = 'Encryption failed. Public key not found for $email.';
$messages['nokeysfound'] = 'No keys found';
$messages['keynotfound'] = 'Key not found!';
$messages['keyopenerror'] = 'Unable to get key information! Internal error.';
$messages['keylisterror'] = 'Unable to list keys! Internal error.';
$messages['keysimportfailed'] = 'Unable to import key(s)! Internal error.';
$messages['keysimportsuccess'] = 'Key(s) imported successfully. Imported: $new, unchanged: $old.';
$messages['keyremoving'] = 'Removing key(s)...';
$messages['keyremoveconfirm'] = 'Are you sure, you want to delete selected key(s)?';
$messages['keyremovesuccess'] = 'Key(s) deleted successfully';
$messages['keyremoveerror'] = 'Unable to delete selected key(s).';
$messages['keyimporttext'] = 'You can import private and public key(s) or revocation signatures in ASCII-Armor format.';
$messages['keyimportsearchtext'] = 'You can search for public keys by key identifier, user name or email address and then import them directly.';
$messages['formerror'] = 'Please, fill the form. All fields are required!';
$messages['passwordsdiffer'] = 'Passwords do not match!';
$messages['keygenerateerror'] = 'Failed to generate a key pair';
$messages['keygeneratesuccess'] = 'A key pair generated and imported successfully.';
$messages['keygennosupport'] = 'Your web browser does not support cryptography. Unable to generate a key pair!';
$messages['noidentselected'] = 'You have to select at least one identity for the key!';
// removed in 1.3
$messages['nonameident'] = 'Identity must have a user name defined!';
?>
diff --git a/plugins/help/help.js b/plugins/help/help.js
index 16e1a2a9a..7928b2017 100644
--- a/plugins/help/help.js
+++ b/plugins/help/help.js
@@ -1,87 +1,86 @@
/**
* Help plugin client script
- * @version 1.4
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
- * Copyright (c) 2012-2014, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*/
// hook into switch-task event to open the help window
if (window.rcmail) {
rcmail.addEventListener('beforeswitch-task', function(prop) {
// catch clicks to help task button
if (prop == 'help') {
if (rcmail.task == 'help') // we're already there
return false;
var url = rcmail.url('help/index', { _rel: rcmail.task + (rcmail.env.action ? '/'+rcmail.env.action : '') });
if (rcmail.env.help_open_extwin) {
rcmail.open_window(url, 1020, false);
}
else {
rcmail.redirect(url, false);
}
return false;
}
});
rcmail.addEventListener('init', function(prop) {
if (rcmail.env.contentframe && rcmail.task == 'help') {
$('#' + rcmail.env.contentframe).on('load error', function(e) {
// Unlock UI
rcmail.set_busy(false, null, rcmail.env.frame_lock);
rcmail.env.frame_lock = null;
// Select menu item
if (e.type == 'load') {
$(rcmail.env.help_action_item).parents('ul').children().removeClass('selected');
$(rcmail.env.help_action_item).parent().addClass('selected');
}
});
try {
var win = rcmail.get_frame_window(rcmail.env.contentframe);
if (win && win.location.href.indexOf(rcmail.env.blankpage) >= 0) {
show_help_content(rcmail.env.action);
}
}
catch (e) { /* ignore */}
}
});
}
function show_help_content(action, event)
{
var win, target = window,
url = rcmail.env.help_links[action];
if (win = rcmail.get_frame_window(rcmail.env.contentframe)) {
target = win;
url += (url.indexOf('?') > -1 ? '&' : '?') + '_framed=1';
}
if (rcmail.env.extwin) {
url += (url.indexOf('?') > -1 ? '&' : '?') + '_extwin=1';
}
if (/^self/.test(url)) {
url = url.substr(4) + '&_content=1&_task=help&_action=' + action;
}
rcmail.env.help_action_item = event ? event.target : $('[rel="' + action + '"]');
rcmail.show_contentframe(true);
rcmail.location_href(url, target, true);
return false;
}
diff --git a/plugins/help/help.php b/plugins/help/help.php
index 352c764df..bd18ec8d4 100644
--- a/plugins/help/help.php
+++ b/plugins/help/help.php
@@ -1,180 +1,179 @@
<?php
/**
* Roundcube Help Plugin
*
* @author Aleksander 'A.L.E.C' Machniak
* @author Thomas Bruederli <thomas@roundcube.net>
* @license GNU GPLv3+
*
* Configuration (see config.inc.php.dist)
- *
- **/
+ */
class help extends rcube_plugin
{
// all task excluding 'login' and 'logout'
public $task = '?(?!login|logout).*';
// we've got no ajax handlers
public $noajax = true;
function init()
{
$this->load_config();
$this->add_texts('localization/', false);
// register task
$this->register_task('help');
// register actions
$this->register_action('index', array($this, 'action'));
$this->register_action('about', array($this, 'action'));
$this->register_action('license', array($this, 'action'));
$this->add_hook('startup', array($this, 'startup'));
$this->add_hook('error_page', array($this, 'error_page'));
}
function startup($args)
{
$rcmail = rcmail::get_instance();
if (!$rcmail->output->framed) {
// add taskbar button
$this->add_button(array(
'command' => 'help',
'class' => 'button-help',
'classsel' => 'button-help button-selected',
'innerclass' => 'button-inner',
'label' => 'help.help',
'type' => 'link',
), 'taskbar');
$this->include_script('help.js');
$rcmail->output->set_env('help_open_extwin', $rcmail->config->get('help_open_extwin', false), true);
}
// add style for taskbar button (must be here) and Help UI
$this->include_stylesheet($this->local_skin_path() . '/help.css');
}
function action()
{
$rcmail = rcmail::get_instance();
if ($rcmail->action == 'about') {
$rcmail->output->set_pagetitle($this->gettext('about'));
}
else if ($rcmail->action == 'license') {
$rcmail->output->set_pagetitle($this->gettext('license'));
}
else {
$rcmail->output->set_pagetitle($this->gettext('help'));
}
// register UI objects
$rcmail->output->add_handlers(array(
'helpcontent' => array($this, 'help_content'),
'tablink' => array($this, 'tablink'),
));
$rcmail->output->set_env('help_links', $this->help_metadata());
$rcmail->output->send(!empty($_GET['_content']) ? 'help.content' : 'help.help');
}
function help_content($attrib)
{
$rcmail = rcmail::get_instance();
// $rcmail->output->set_env('content', $content);
if (!empty($_GET['_content'])) {
if ($rcmail->action == 'about') {
return file_get_contents($this->home . '/content/about.html');
}
else if ($rcmail->action == 'license') {
return file_get_contents($this->home . '/content/license.html');
}
}
}
function tablink($attrib)
{
$rcmail = rcmail::get_instance();
$attrib['name'] = 'helplink' . $attrib['action'];
$attrib['href'] = $rcmail->url(array('_action' => $attrib['action'], '_extwin' => !empty($_REQUEST['_extwin']) ? 1 : null));
$attrib['rel'] = $attrib['action'];
// title might be already translated here, so revert to it's initial value
// so button() will translate it correctly
$attrib['title'] = $attrib['label'];
$attrib['onclick'] = sprintf("return show_help_content('%s', event)", $attrib['action']);
return $rcmail->output->button($attrib);
}
function help_metadata()
{
$rcmail = rcmail::get_instance();
$content = array();
// About
if (is_readable($this->home . '/content/about.html')) {
$content['about'] = 'self';
}
else {
$default = $rcmail->url(array('_task' => 'settings', '_action' => 'about', '_framed' => 1));
$content['about'] = $rcmail->config->get('help_about_url', $default);
$content['about'] = $this->resolve_language($content['about']);
}
// License
if (is_readable($this->home . '/content/license.html')) {
$content['license'] = 'self';
}
else {
$content['license'] = $rcmail->config->get('help_license_url', 'http://www.gnu.org/licenses/gpl-3.0-standalone.html');
$content['license'] = $this->resolve_language($content['license']);
}
// Help Index
$src = $rcmail->config->get('help_source', 'http://docs.roundcube.net/doc/help/1.1/%l/');
$index_map = $rcmail->config->get('help_index_map', array());
// resolve task/action for deep linking
$rel = $_REQUEST['_rel'];
list($task, $action) = explode('/', $rel);
if ($add = $index_map[$rel]) {
$src .= $add;
}
else if ($add = $index_map[$task]) {
$src .= $add;
}
$content['index'] = $this->resolve_language($src);
return $content;
}
function error_page($args)
{
$rcmail = rcmail::get_instance();
if ($args['code'] == 403 && $rcmail->request_status == rcube::REQUEST_ERROR_URL && ($url = $rcmail->config->get('help_csrf_info'))) {
$args['text'] .= '<p>' . html::a(array('href' => $url, 'target' => '_blank'), $this->gettext('csrfinfo')) . '</p>';
}
return $args;
}
private function resolve_language($path)
{
// resolve language placeholder
$rcmail = rcmail::get_instance();
$langmap = $rcmail->config->get('help_language_map', array('*' => 'en_US'));
$lang = $langmap[$_SESSION['language']] ?: $langmap['*'];
return str_replace('%l', $lang, $path);
}
}
diff --git a/plugins/help/localization/en_US.inc b/plugins/help/localization/en_US.inc
index d44b9a886..6aafd24e6 100644
--- a/plugins/help/localization/en_US.inc
+++ b/plugins/help/localization/en_US.inc
@@ -1,25 +1,23 @@
<?php
/*
+-----------------------------------------------------------------------+
- | plugins/help/localization/<lang>.inc |
- | |
| Localization file of the Roundcube Webmail Help plugin |
- | Copyright (C) 2012-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-----------------------------------------------------------------------+
For translation see https://www.transifex.com/projects/p/roundcube-webmail/resource/plugin-help/
*/
$labels = array();
$labels['help'] = 'Help';
$labels['about'] = 'About';
$labels['license'] = 'License';
$labels['csrfinfo'] = 'Read more about CSRF and how we protect you';
?>
diff --git a/plugins/hide_blockquote/hide_blockquote.js b/plugins/hide_blockquote/hide_blockquote.js
index e938b33e9..635333a67 100644
--- a/plugins/hide_blockquote/hide_blockquote.js
+++ b/plugins/hide_blockquote/hide_blockquote.js
@@ -1,69 +1,69 @@
/**
* Hide Blockquotes plugin script
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
- * Copyright (c) 2012-2014, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*/
if (window.rcmail)
rcmail.addEventListener('init', function() { hide_blockquote(); });
function hide_blockquote()
{
var limit = rcmail.env.blockquote_limit;
if (limit <= 0)
return;
$('div.message-part div.pre > blockquote', $('#messagebody')).each(function() {
var res, text, div, link, q = $(this);
// Add new-line character before each blockquote
// This fixes counting lines of text, it also prevents
// from merging lines from different quoting level
$('blockquote').before(document.createTextNode("\n"));
text = $.trim(q.text());
res = text.split(/\n/);
if (res.length <= limit) {
// there can be also a block with very long wrapped line
// assume line height = 15px
if (q.height() <= limit * 15)
return;
}
div = $('<blockquote class="blockquote-header">')
.css({'white-space': 'nowrap', overflow: 'hidden', position: 'relative'})
.text(res[0]);
link = $('<span class="blockquote-link"></span>')
.css({position: 'absolute', 'z-Index': 2})
.text(rcmail.get_label('hide_blockquote.show'))
.data('parent', div)
.click(function() {
var t = $(this), parent = t.data('parent'), visible = parent.is(':visible');
t.text(rcmail.get_label(visible ? 'hide' : 'show', 'hide_blockquote'))
.detach().appendTo(visible ? q : parent).toggleClass('collapsed');
parent[visible ? 'hide' : 'show']();
q[visible ? 'show' : 'hide']();
});
link.appendTo(div);
// Modify blockquote
q.hide().css({position: 'relative'}).before(div);
});
}
diff --git a/plugins/hide_blockquote/localization/en_US.inc b/plugins/hide_blockquote/localization/en_US.inc
index 90dd28955..782bb59ff 100644
--- a/plugins/hide_blockquote/localization/en_US.inc
+++ b/plugins/hide_blockquote/localization/en_US.inc
@@ -1,24 +1,20 @@
<?php
/*
+-----------------------------------------------------------------------+
- | plugins/hide_blockquote/localization/<lang>.inc |
- | |
| Localization file of the Roundcube Webmail Hide-Blockquote plugin |
- | Copyright (C) 2012-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-----------------------------------------------------------------------+
For translation see https://www.transifex.com/projects/p/roundcube-webmail/resource/plugin-hide_blockquote/
*/
$labels = array();
$labels['hide'] = 'Hide';
$labels['show'] = 'Show';
$labels['quotelimit'] = 'Hide citation when lines count is greater than';
-
-?>
diff --git a/plugins/managesieve/lib/Roundcube/rcube_sieve.php b/plugins/managesieve/lib/Roundcube/rcube_sieve.php
index 38f06afd0..2ee76615a 100644
--- a/plugins/managesieve/lib/Roundcube/rcube_sieve.php
+++ b/plugins/managesieve/lib/Roundcube/rcube_sieve.php
@@ -1,483 +1,483 @@
<?php
/**
* Classes for managesieve operations (using PEAR::Net_Sieve)
*
- * Copyright (C) 2008-2011, The Roundcube Dev Team
- * Copyright (C) 2011, Kolab Systems AG
+ * Copyright (C) The Roundcube Dev Team
+ * Copyright (C) Kolab Systems AG
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
// Managesieve Protocol: RFC5804
class rcube_sieve
{
private $sieve; // Net_Sieve object
private $error = false; // error flag
private $errorLines = array(); // array of line numbers within sieve script which raised an error
private $list = array(); // scripts list
private $exts; // array of supported extensions
private $active; // active script name
public $script; // rcube_sieve_script object
public $current; // name of currently loaded script
const ERROR_CONNECTION = 1;
const ERROR_LOGIN = 2;
const ERROR_NOT_EXISTS = 3; // script not exists
const ERROR_INSTALL = 4; // script installation
const ERROR_ACTIVATE = 5; // script activation
const ERROR_DELETE = 6; // script deletion
const ERROR_INTERNAL = 7; // internal error
const ERROR_DEACTIVATE = 8; // script activation
const ERROR_OTHER = 255; // other/unknown error
/**
* Object constructor
*
* @param string Username (for managesieve login)
* @param string Password (for managesieve login)
* @param string Managesieve server hostname/address
* @param string Managesieve server port number
* @param string Managesieve authentication method
* @param boolean Enable/disable TLS use
* @param array Disabled extensions
* @param boolean Enable/disable debugging
* @param string Proxy authentication identifier
* @param string Proxy authentication password
* @param array List of options to pass to stream_context_create().
*/
public function __construct($username, $password='', $host='localhost', $port=2000,
$auth_type=null, $usetls=true, $disabled=array(), $debug=false,
$auth_cid=null, $auth_pw=null, $options=array(), $gssapi_principal=null,
$gssapi_cname=null)
{
$this->sieve = new Net_Sieve();
if ($debug) {
$this->sieve->setDebug(true, array($this, 'debug_handler'));
}
if (isset($gssapi_principal)) {
$this->sieve->setServicePrincipal($gssapi_principal);
}
if (isset($gssapi_cname)) {
$this->sieve->setServiceCN($gssapi_cname);
}
$result = $this->sieve->connect($host, $port, $options, $usetls);
if (is_a($result, 'PEAR_Error')) {
return $this->_set_error(self::ERROR_CONNECTION);
}
if (!empty($auth_cid)) {
$authz = $username;
$username = $auth_cid;
}
if (!empty($auth_pw)) {
$password = $auth_pw;
}
$result = $this->sieve->login($username, $password, $auth_type ? strtoupper($auth_type) : null, $authz);
if (is_a($result, 'PEAR_Error')) {
return $this->_set_error(self::ERROR_LOGIN);
}
$this->exts = $this->get_extensions();
// disable features by config
if (!empty($disabled)) {
// we're working on lower-cased names
$disabled = array_map('strtolower', (array) $disabled);
foreach ($disabled as $ext) {
if (($idx = array_search($ext, $this->exts)) !== false) {
unset($this->exts[$idx]);
}
}
}
}
public function __destruct()
{
$this->sieve->disconnect();
}
/**
* Getter for error code
*/
public function error()
{
return $this->error ?: false;
}
/**
* Saves current script into server
*/
public function save($name = null)
{
if (!$this->sieve) {
return $this->_set_error(self::ERROR_INTERNAL);
}
if (!$this->script) {
return $this->_set_error(self::ERROR_INTERNAL);
}
if (!$name) {
$name = $this->current;
}
$script = $this->script->as_text();
if (!$script) {
$script = '/* empty script */';
}
$result = $this->sieve->installScript($name, $script);
if (is_a($result, 'PEAR_Error')) {
return $this->_set_error(self::ERROR_INSTALL);
}
return true;
}
/**
* Saves text script into server
*/
public function save_script($name, $content = null)
{
if (!$this->sieve) {
return $this->_set_error(self::ERROR_INTERNAL);
}
if (!$content) {
$content = '/* empty script */';
}
$result = $this->sieve->installScript($name, $content);
if (is_a($result, 'PEAR_Error')) {
$rawErrorMessage = $result->getMessage();
$errMessages = preg_split("/$name:/", $rawErrorMessage);
if (count($errMessages) > 0) {
foreach ($errMessages as $singleError) {
$matches = array();
$res = preg_match('/line (\d+):(.*)/i', $singleError, $matches);
if ($res === 1 ) {
if (count($matches) > 2) {
$this->errorLines[] = array("line" => $matches[1], "msg" => $matches[2]);
}
else {
$this->errorLines[] = array("line" => $matches[1], "msg" => null);
}
}
}
}
return $this->_set_error(self::ERROR_INSTALL);
}
return true;
}
/**
* Returns the current error line within the saved sieve script
*/
public function get_error_lines()
{
return $this->errorLines;
}
/**
* Activates specified script
*/
public function activate($name = null)
{
if (!$this->sieve) {
return $this->_set_error(self::ERROR_INTERNAL);
}
if (!$name) {
$name = $this->current;
}
$result = $this->sieve->setActive($name);
if (is_a($result, 'PEAR_Error')) {
return $this->_set_error(self::ERROR_ACTIVATE);
}
$this->active = $name;
return true;
}
/**
* De-activates specified script
*/
public function deactivate()
{
if (!$this->sieve) {
return $this->_set_error(self::ERROR_INTERNAL);
}
$result = $this->sieve->setActive('');
if (is_a($result, 'PEAR_Error')) {
return $this->_set_error(self::ERROR_DEACTIVATE);
}
$this->active = null;
return true;
}
/**
* Removes specified script
*/
public function remove($name = null)
{
if (!$this->sieve) {
return $this->_set_error(self::ERROR_INTERNAL);
}
if (!$name) {
$name = $this->current;
}
// script must be deactivated first
if ($name == $this->sieve->getActive()) {
$result = $this->sieve->setActive('');
if (is_a($result, 'PEAR_Error')) {
return $this->_set_error(self::ERROR_DELETE);
}
$this->active = null;
}
$result = $this->sieve->removeScript($name);
if (is_a($result, 'PEAR_Error')) {
return $this->_set_error(self::ERROR_DELETE);
}
if ($name == $this->current) {
$this->current = null;
}
$this->list = null;
return true;
}
/**
* Gets list of supported by server Sieve extensions
*/
public function get_extensions()
{
if ($this->exts) {
return $this->exts;
}
if (!$this->sieve) {
return $this->_set_error(self::ERROR_INTERNAL);
}
$ext = $this->sieve->getExtensions();
if (is_a($ext, 'PEAR_Error')) {
return array();
}
// we're working on lower-cased names
$ext = array_map('strtolower', (array) $ext);
if ($this->script) {
$supported = $this->script->get_extensions();
$ext = array_values(array_intersect($ext, $supported));
}
return $ext;
}
/**
* Gets list of scripts from server
*/
public function get_scripts()
{
if (!$this->list) {
if (!$this->sieve) {
return $this->_set_error(self::ERROR_INTERNAL);
}
$list = $this->sieve->listScripts($active);
if (is_a($list, 'PEAR_Error')) {
return $this->_set_error(self::ERROR_OTHER);
}
$this->list = $list;
$this->active = $active;
}
return $this->list;
}
/**
* Returns active script name
*/
public function get_active()
{
if ($this->active !== null) {
return $this->active;
}
if (!$this->sieve) {
return $this->_set_error(self::ERROR_INTERNAL);
}
return $this->active = $this->sieve->getActive();
}
/**
* Loads script by name
*/
public function load($name)
{
if ($this->current === $name) {
return true;
}
$script = $this->get_script($name);
if ($script === false) {
return false;
}
// try to parse to Roundcube format
$this->script = $this->_parse($script);
$this->current = $name;
return true;
}
/**
* Loads script from text content
*/
public function load_script($script)
{
if (!$this->sieve) {
return $this->_set_error(self::ERROR_INTERNAL);
}
// try to parse to Roundcube format
$this->script = $this->_parse($script);
}
/**
* Creates rcube_sieve_script object from text script
*/
private function _parse($txt)
{
// parse
$script = new rcube_sieve_script($txt, $this->exts);
// fix/convert to Roundcube format
if (!empty($script->content)) {
// replace all elsif with if+stop, we support only ifs
foreach ($script->content as $idx => $rule) {
if (empty($rule['type']) || !preg_match('/^(if|elsif|else)$/', $rule['type'])) {
continue;
}
$script->content[$idx]['type'] = 'if';
// 'stop' not found?
foreach ($rule['actions'] as $action) {
if (preg_match('/^(stop|vacation)$/', $action['type'])) {
continue 2;
}
}
if (!empty($script->content[$idx+1]) && $script->content[$idx+1]['type'] != 'if') {
$script->content[$idx]['actions'][] = array('type' => 'stop');
}
}
}
return $script;
}
/**
* Gets specified script as text
*/
public function get_script($name)
{
if (!$this->sieve) {
return $this->_set_error(self::ERROR_INTERNAL);
}
$content = $this->sieve->getScript($name);
if (is_a($content, 'PEAR_Error')) {
return $this->_set_error(self::ERROR_OTHER);
}
return $content;
}
/**
* Creates empty script or copy of other script
*/
public function copy($name, $copy)
{
if (!$this->sieve) {
return $this->_set_error(self::ERROR_INTERNAL);
}
if ($copy) {
$content = $this->sieve->getScript($copy);
if (is_a($content, 'PEAR_Error')) {
return $this->_set_error(self::ERROR_OTHER);
}
}
return $this->save_script($name, $content);
}
private function _set_error($error)
{
$this->error = $error;
return false;
}
/**
* This is our own debug handler for connection
*/
public function debug_handler(&$sieve, $message)
{
rcube::write_log('sieve', preg_replace('/\r\n$/', '', $message));
}
}
diff --git a/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php b/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
index db0697d43..074d3be85 100644
--- a/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
+++ b/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
@@ -1,3085 +1,3085 @@
<?php
/**
* Managesieve (Sieve Filters) Engine
*
* Engine part of Managesieve plugin implementing UI and backend access.
*
- * Copyright (C) 2008-2014, The Roundcube Dev Team
- * Copyright (C) 2011-2014, Kolab Systems AG
+ * Copyright (C) The Roundcube Dev Team
+ * Copyright (C) Kolab Systems AG
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_sieve_engine
{
protected $rc;
protected $sieve;
protected $errors;
protected $form;
protected $list;
protected $tips = array();
protected $script = array();
protected $exts = array();
protected $active = array();
protected $headers = array();
protected $disabled_actions = array();
protected $addr_headers = array(
// Required
"from", "to", "cc", "bcc", "sender", "resent-from", "resent-to",
// Additional (RFC 822 / RFC 2822)
"reply-to", "resent-reply-to", "resent-sender", "resent-cc", "resent-bcc",
// Non-standard (RFC 2076, draft-palme-mailext-headers-08.txt)
"for-approval", "for-handling", "for-comment", "apparently-to", "errors-to",
"delivered-to", "return-receipt-to", "x-admin", "read-receipt-to",
"x-confirm-reading-to", "return-receipt-requested",
"registered-mail-reply-requested-by", "mail-followup-to", "mail-reply-to",
"abuse-reports-to", "x-complaints-to", "x-report-abuse-to",
// Undocumented
"x-beenthere",
);
protected $notify_methods = array(
'mailto',
// 'sms',
// 'tel',
);
protected $notify_importance_options = array(
3 => 'notifyimportancelow',
2 => 'notifyimportancenormal',
1 => 'notifyimportancehigh'
);
const VERSION = '9.2';
const PROGNAME = 'Roundcube (Managesieve)';
const PORT = 4190;
/**
* Class constructor
*/
function __construct($plugin)
{
$this->rc = rcube::get_instance();
$this->plugin = $plugin;
$this->headers = $this->get_default_headers();
}
/**
* Loads configuration, initializes plugin (including sieve connection)
*/
function start($mode = null)
{
// register UI objects
$this->rc->output->add_handlers(array(
'filterslist' => array($this, 'filters_list'),
'filtersetslist' => array($this, 'filtersets_list'),
'filterform' => array($this, 'filter_form'),
'filtersetform' => array($this, 'filterset_form'),
'filterseteditraw' => array($this, 'filterset_editraw'),
));
$this->disabled_actions = (array) $this->rc->config->get('managesieve_disabled_actions');
// connect to managesieve server
$error = $this->connect($_SESSION['username'], $this->rc->decrypt($_SESSION['password']));
// load current/active script
if (!$error) {
// Get list of scripts
$list = $this->list_scripts();
// reset current script when entering filters UI (#1489412)
if ($this->rc->action == 'plugin.managesieve') {
$this->rc->session->remove('managesieve_current');
}
if ($mode != 'vacation' && $mode != 'forward') {
if (!empty($_GET['_set']) || !empty($_POST['_set'])) {
$script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_GPC, true);
}
else if (!empty($_SESSION['managesieve_current'])) {
$script_name = $_SESSION['managesieve_current'];
}
}
$error = $this->load_script($script_name);
}
// finally set script objects
if ($error) {
switch ($error) {
case rcube_sieve::ERROR_CONNECTION:
case rcube_sieve::ERROR_LOGIN:
$this->rc->output->show_message('managesieve.filterconnerror', 'error');
break;
default:
$this->rc->output->show_message('managesieve.filterunknownerror', 'error');
break;
}
// reload interface in case of possible error when specified script wasn't found (#1489412)
if ($script_name !== null && !empty($list) && !in_array($script_name, $list)) {
$this->rc->output->command('reload', 500);
}
// to disable 'Add filter' button set env variable
$this->rc->output->set_env('filterconnerror', true);
$this->script = array();
}
else {
$this->exts = $this->sieve->get_extensions();
$this->init_script();
$this->rc->output->set_env('currentset', $this->sieve->current);
$_SESSION['managesieve_current'] = $this->sieve->current;
}
$this->rc->output->set_env('raw_sieve_editor', $this->rc->config->get('managesieve_raw_editor', true));
$this->rc->output->set_env('managesieve_disabled_actions', $this->disabled_actions);
if (in_array('list_sets', $this->disabled_actions))
$this->rc->output->set_env('managesieve_no_set_list', true);
return $error;
}
/**
* Connect to configured managesieve server
*
* @param string $username User login
* @param string $password User password
*
* @return int Connection status: 0 on success, >0 on failure
*/
public function connect($username, $password)
{
// Get connection parameters
$host = $this->rc->config->get('managesieve_host', 'localhost');
$port = $this->rc->config->get('managesieve_port');
$tls = $this->rc->config->get('managesieve_usetls', false);
$host = rcube_utils::parse_host($host);
$host = rcube_utils::idn_to_ascii($host);
// remove tls:// prefix, set TLS flag
if (($host = preg_replace('|^tls://|i', '', $host, 1, $cnt)) && $cnt) {
$tls = true;
}
if (empty($port)) {
$port = getservbyname('sieve', 'tcp') ?: self::PORT;
}
$plugin = $this->rc->plugins->exec_hook('managesieve_connect', array(
'user' => $username,
'password' => $password,
'host' => $host,
'port' => $port,
'usetls' => $tls,
'auth_type' => $this->rc->config->get('managesieve_auth_type'),
'disabled' => $this->rc->config->get('managesieve_disabled_extensions'),
'debug' => $this->rc->config->get('managesieve_debug', false),
'auth_cid' => $this->rc->config->get('managesieve_auth_cid'),
'auth_pw' => $this->rc->config->get('managesieve_auth_pw'),
'socket_options' => $this->rc->config->get('managesieve_conn_options')
));
// Handle per-host socket options
rcube_utils::parse_socket_options($plugin['socket_options'], $plugin['host']);
// try to connect to managesieve server and to fetch the script
$this->sieve = new rcube_sieve(
$plugin['user'],
$plugin['password'],
$plugin['host'],
$plugin['port'],
$plugin['auth_type'],
$plugin['usetls'],
$plugin['disabled'],
$plugin['debug'],
$plugin['auth_cid'],
$plugin['auth_pw'],
$plugin['socket_options'],
$plugin['gssapi_context'],
$plugin['gssapi_cn']
);
$error = $this->sieve->error();
if ($error) {
rcube::raise_error(array(
'code' => 403,
'file' => __FILE__,
'line' => __LINE__,
'message' => "Unable to connect to managesieve on $host:$port"
), true, false);
}
return $error;
}
/**
* Load specified (or active) script
*
* @param string $script_name Optional script name
*
* @return int Connection status: 0 on success, >0 on failure
*/
protected function load_script($script_name = null)
{
// Get list of scripts
$list = $this->list_scripts();
if ($script_name === null || $script_name === '') {
// get (first) active script
if (!empty($this->active)) {
$script_name = $this->active[0];
}
else if ($list) {
$script_name = $list[0];
}
// create a new (initial) script
else {
// if script not exists build default script contents
$script_file = $this->rc->config->get('managesieve_default');
$script_name = $this->rc->config->get('managesieve_script_name');
if (empty($script_name)) {
$script_name = 'roundcube';
}
if ($script_file && is_readable($script_file)) {
$content = file_get_contents($script_file);
}
// add script and set it active
if ($this->sieve->save_script($script_name, $content)) {
$this->activate_script($script_name);
$this->list[] = $script_name;
}
}
}
if ($script_name) {
$this->sieve->load($script_name);
}
return $this->sieve->error();
}
/**
* User interface actions handler
*/
function actions()
{
$error = $this->start();
// Handle user requests
if ($action = rcube_utils::get_input_value('_act', rcube_utils::INPUT_GPC)) {
$fid = (int) rcube_utils::get_input_value('_fid', rcube_utils::INPUT_POST);
if ($action == 'delete' && !$error) {
if (!in_array('delete_rule', $this->disabled_actions)) {
if (isset($this->script[$fid])) {
if ($this->sieve->script->delete_rule($fid))
$result = $this->save_script();
if ($result === true) {
$this->rc->output->show_message('managesieve.filterdeleted', 'confirmation');
$this->rc->output->command('managesieve_updatelist', 'del', array('id' => $fid));
}
else {
$this->rc->output->show_message('managesieve.filterdeleteerror', 'error');
}
}
}
else {
$this->rc->output->show_message('managesieve.disabledaction', 'error');
}
}
else if ($action == 'move' && !$error) {
if (isset($this->script[$fid])) {
$to = (int) rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST);
$rule = $this->script[$fid];
// remove rule
unset($this->script[$fid]);
$this->script = array_values($this->script);
// add at target position
if ($to >= count($this->script)) {
$this->script[] = $rule;
}
else {
$script = array();
foreach ($this->script as $idx => $r) {
if ($idx == $to)
$script[] = $rule;
$script[] = $r;
}
$this->script = $script;
}
$this->sieve->script->content = $this->script;
$result = $this->save_script();
if ($result === true) {
$result = $this->list_rules();
$this->rc->output->show_message('managesieve.moved', 'confirmation');
$this->rc->output->command('managesieve_updatelist', 'list',
array('list' => $result, 'clear' => true, 'set' => $to));
}
else {
$this->rc->output->show_message('managesieve.moveerror', 'error');
}
}
}
else if ($action == 'act' && !$error) {
if (isset($this->script[$fid])) {
$rule = $this->script[$fid];
$disabled = !empty($rule['disabled']);
$rule['disabled'] = !$disabled;
$result = $this->sieve->script->update_rule($fid, $rule);
if ($result !== false)
$result = $this->save_script();
if ($result === true) {
if ($rule['disabled'])
$this->rc->output->show_message('managesieve.deactivated', 'confirmation');
else
$this->rc->output->show_message('managesieve.activated', 'confirmation');
$this->rc->output->command('managesieve_updatelist', 'update',
array('id' => $fid, 'disabled' => $rule['disabled']));
}
else {
if ($rule['disabled'])
$this->rc->output->show_message('managesieve.deactivateerror', 'error');
else
$this->rc->output->show_message('managesieve.activateerror', 'error');
}
}
}
else if ($action == 'setact' && !$error) {
if (!in_array('enable_disable_set', $this->disabled_actions)) {
$script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_POST, true);
$result = $this->activate_script($script_name);
$kep14 = $this->rc->config->get('managesieve_kolab_master');
if ($result === true) {
$this->rc->output->set_env('active_sets', $this->active);
$this->rc->output->show_message('managesieve.setactivated', 'confirmation');
$this->rc->output->command('managesieve_updatelist', 'setact',
array('name' => $script_name, 'active' => true, 'all' => !$kep14));
}
else {
$this->rc->output->show_message('managesieve.setactivateerror', 'error');
}
}
else {
$this->rc->output->show_message('managesieve.disabledaction', 'error');
}
}
else if ($action == 'deact' && !$error) {
if (!in_array('enable_disable_set', $this->disabled_actions)) {
$script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_POST, true);
$result = $this->deactivate_script($script_name);
if ($result === true) {
$this->rc->output->set_env('active_sets', $this->active);
$this->rc->output->show_message('managesieve.setdeactivated', 'confirmation');
$this->rc->output->command('managesieve_updatelist', 'setact',
array('name' => $script_name, 'active' => false));
}
else {
$this->rc->output->show_message('managesieve.setdeactivateerror', 'error');
}
}
else {
$this->rc->output->show_message('managesieve.disabledaction', 'error');
}
}
else if ($action == 'setdel' && !$error) {
if (!in_array('delete_set', $this->disabled_actions)) {
$script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_POST, true);
$result = $this->remove_script($script_name);
if ($result === true) {
$this->rc->output->show_message('managesieve.setdeleted', 'confirmation');
$this->rc->output->command('managesieve_updatelist', 'setdel',
array('name' => $script_name));
$this->rc->session->remove('managesieve_current');
}
else {
$this->rc->output->show_message('managesieve.setdeleteerror', 'error');
}
}
else {
$this->rc->output->show_message('managesieve.disabledaction', 'error');
}
}
else if ($action == 'setget') {
if (!in_array('download_set', $this->disabled_actions)) {
$this->rc->request_security_check(rcube_utils::INPUT_GET);
$script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_GPC, true);
$script = $this->sieve->get_script($script_name);
if ($script !== false) {
$this->rc->output->download_headers($script_name . '.txt', array('length' => strlen($script)));
echo $script;
}
exit;
}
}
else if ($action == 'list') {
$result = $this->list_rules();
$this->rc->output->command('managesieve_updatelist', 'list', array('list' => $result));
}
else if ($action == 'ruleadd') {
$rid = rcube_utils::get_input_value('_rid', rcube_utils::INPUT_POST);
$id = $this->genid();
$content = $this->rule_div($fid, $id, false, $_SESSION['managesieve-compact-form']);
$this->rc->output->command('managesieve_rulefill', $content, $id, $rid);
}
else if ($action == 'actionadd') {
$aid = rcube_utils::get_input_value('_aid', rcube_utils::INPUT_POST);
$id = $this->genid();
$content = $this->action_div($fid, $id, false);
$this->rc->output->command('managesieve_actionfill', $content, $id, $aid);
}
else if ($action == 'addresses') {
$aid = rcube_utils::get_input_value('_aid', rcube_utils::INPUT_POST);
$this->rc->output->command('managesieve_vacation_addresses_update', $aid, $this->user_emails());
}
$this->rc->output->send();
}
else if ($this->rc->task == 'mail') {
// Initialize the form
$rules = rcube_utils::get_input_value('r', rcube_utils::INPUT_GET);
if (!empty($rules)) {
$tests = array();
foreach ($rules as $rule) {
list($header, $value) = explode(':', $rule, 2);
$tests[] = array(
'type' => 'contains',
'test' => 'header',
'arg1' => $header,
'arg2' => $value,
);
}
$this->form = array(
'join' => count($tests) > 1 ? 'allof' : 'anyof',
'name' => '',
'tests' => $tests,
'actions' => array(
0 => array('type' => 'fileinto'),
1 => array('type' => 'stop'),
),
);
}
}
$this->send();
}
function saveraw()
{
// Init plugin and handle managesieve connection
$error = $this->start();
$script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_POST);
$result = $this->sieve->save_script($script_name, $_POST['rawsetcontent']);
if ($result === false) {
$this->rc->output->show_message('managesieve.filtersaveerror', 'error');
$errorLines = $this->sieve->get_error_lines();
if (count($errorLines) > 0) {
$this->rc->output->set_env("sieve_errors", $errorLines);
}
}
else {
$this->rc->output->show_message('managesieve.setupdated', 'confirmation');
$this->rc->output->command('parent.managesieve_updatelist', 'refresh');
}
$this->send();
}
function save()
{
// Init plugin and handle managesieve connection
$error = $this->start();
// get request size limits (#1488648)
$max_post = max(array(
ini_get('max_input_vars'),
ini_get('suhosin.request.max_vars'),
ini_get('suhosin.post.max_vars'),
));
$max_depth = max(array(
ini_get('suhosin.request.max_array_depth'),
ini_get('suhosin.post.max_array_depth'),
));
// check request size limit
if ($max_post && count($_POST, COUNT_RECURSIVE) >= $max_post) {
rcube::raise_error(array(
'code' => 500, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Request size limit exceeded (one of max_input_vars/suhosin.request.max_vars/suhosin.post.max_vars)"
), true, false);
$this->rc->output->show_message('managesieve.filtersaveerror', 'error');
}
// check request depth limits
else if ($max_depth && count($_POST['_header']) > $max_depth) {
rcube::raise_error(array(
'code' => 500, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Request size limit exceeded (one of suhosin.request.max_array_depth/suhosin.post.max_array_depth)"
), true, false);
$this->rc->output->show_message('managesieve.filtersaveerror', 'error');
}
// filters set add action
else if (!empty($_POST['_newset'])) {
$name = rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST, true);
$copy = rcube_utils::get_input_value('_copy', rcube_utils::INPUT_POST, true);
$from = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST);
$exceptions = $this->rc->config->get('managesieve_filename_exceptions');
$kolab = $this->rc->config->get('managesieve_kolab_master');
$name_uc = mb_strtolower($name);
$list = $this->list_scripts();
if (in_array('new_set', $this->disabled_actions)) {
$error = 'managesieve.disabledaction';
}
else if (!$name) {
$this->errors['name'] = $this->plugin->gettext('cannotbeempty');
}
else if (mb_strlen($name) > 128) {
$this->errors['name'] = $this->plugin->gettext('nametoolong');
}
else if (!empty($exceptions) && in_array($name, (array)$exceptions)) {
$this->errors['name'] = $this->plugin->gettext('namereserved');
}
else if (!empty($kolab) && in_array($name_uc, array('MASTER', 'USER', 'MANAGEMENT'))) {
$this->errors['name'] = $this->plugin->gettext('namereserved');
}
else if (in_array($name, $list)) {
$this->errors['name'] = $this->plugin->gettext('setexist');
}
else if ($from == 'file') {
// from file
if (is_uploaded_file($_FILES['_file']['tmp_name'])) {
$file = file_get_contents($_FILES['_file']['tmp_name']);
$file = preg_replace('/\r/', '', $file);
// for security don't save script directly
// check syntax before, like this...
$this->sieve->load_script($file);
if (!$this->save_script($name)) {
$this->errors['file'] = $this->plugin->gettext('setcreateerror');
}
}
else { // upload failed
$err = $_FILES['_file']['error'];
if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
$msg = $this->rc->gettext(array('name' => 'filesizeerror',
'vars' => array('size' =>
$this->rc->show_bytes(rcube_utils::max_upload_size()))));
}
else {
$this->errors['file'] = $this->plugin->gettext('fileuploaderror');
}
}
}
else if (!$this->sieve->copy($name, $from == 'set' ? $copy : '')) {
$error = 'managesieve.setcreateerror';
}
if (!$error && empty($this->errors)) {
// Find position of the new script on the list
$list[] = $name;
asort($list, SORT_LOCALE_STRING);
$list = array_values($list);
$index = array_search($name, $list);
$this->rc->output->show_message('managesieve.setcreated', 'confirmation');
$this->rc->output->command('parent.managesieve_updatelist', 'setadd',
array('name' => $name, 'index' => $index));
}
else if ($msg) {
$this->rc->output->command('display_message', $msg, 'error');
}
else if ($error) {
$this->rc->output->show_message($error, 'error');
}
}
// filter add/edit action
else if (isset($_POST['_name'])) {
$name = trim(rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST, true));
$fid = trim(rcube_utils::get_input_value('_fid', rcube_utils::INPUT_POST));
$join = trim(rcube_utils::get_input_value('_join', rcube_utils::INPUT_POST));
// and arrays
$headers = rcube_utils::get_input_value('_header', rcube_utils::INPUT_POST);
$cust_headers = rcube_utils::get_input_value('_custom_header', rcube_utils::INPUT_POST);
$cust_vars = rcube_utils::get_input_value('_custom_var', rcube_utils::INPUT_POST);
$ops = rcube_utils::get_input_value('_rule_op', rcube_utils::INPUT_POST);
$sizeops = rcube_utils::get_input_value('_rule_size_op', rcube_utils::INPUT_POST);
$sizeitems = rcube_utils::get_input_value('_rule_size_item', rcube_utils::INPUT_POST);
$sizetargets = rcube_utils::get_input_value('_rule_size_target', rcube_utils::INPUT_POST);
$targets = rcube_utils::get_input_value('_rule_target', rcube_utils::INPUT_POST, true);
$mods = rcube_utils::get_input_value('_rule_mod', rcube_utils::INPUT_POST);
$mod_types = rcube_utils::get_input_value('_rule_mod_type', rcube_utils::INPUT_POST);
$body_trans = rcube_utils::get_input_value('_rule_trans', rcube_utils::INPUT_POST);
$body_types = rcube_utils::get_input_value('_rule_trans_type', rcube_utils::INPUT_POST, true);
$comparators = rcube_utils::get_input_value('_rule_comp', rcube_utils::INPUT_POST);
$indexes = rcube_utils::get_input_value('_rule_index', rcube_utils::INPUT_POST);
$lastindexes = rcube_utils::get_input_value('_rule_index_last', rcube_utils::INPUT_POST);
$dateheaders = rcube_utils::get_input_value('_rule_date_header', rcube_utils::INPUT_POST);
$dateparts = rcube_utils::get_input_value('_rule_date_part', rcube_utils::INPUT_POST);
$mime_parts = rcube_utils::get_input_value('_rule_mime_part', rcube_utils::INPUT_POST);
$mime_types = rcube_utils::get_input_value('_rule_mime_type', rcube_utils::INPUT_POST);
$mime_params = rcube_utils::get_input_value('_rule_mime_param', rcube_utils::INPUT_POST, true);
$message = rcube_utils::get_input_value('_rule_message', rcube_utils::INPUT_POST);
$dup_handles = rcube_utils::get_input_value('_rule_duplicate_handle', rcube_utils::INPUT_POST, true);
$dup_headers = rcube_utils::get_input_value('_rule_duplicate_header', rcube_utils::INPUT_POST, true);
$dup_uniqueids = rcube_utils::get_input_value('_rule_duplicate_uniqueid', rcube_utils::INPUT_POST, true);
$dup_seconds = rcube_utils::get_input_value('_rule_duplicate_seconds', rcube_utils::INPUT_POST);
$dup_lasts = rcube_utils::get_input_value('_rule_duplicate_last', rcube_utils::INPUT_POST);
$act_types = rcube_utils::get_input_value('_action_type', rcube_utils::INPUT_POST, true);
$mailboxes = rcube_utils::get_input_value('_action_mailbox', rcube_utils::INPUT_POST, true);
$act_targets = rcube_utils::get_input_value('_action_target', rcube_utils::INPUT_POST, true);
$domain_targets = rcube_utils::get_input_value('_action_target_domain', rcube_utils::INPUT_POST);
$area_targets = rcube_utils::get_input_value('_action_target_area', rcube_utils::INPUT_POST, true);
$reasons = rcube_utils::get_input_value('_action_reason', rcube_utils::INPUT_POST, true);
$addresses = rcube_utils::get_input_value('_action_addresses', rcube_utils::INPUT_POST, true);
$intervals = rcube_utils::get_input_value('_action_interval', rcube_utils::INPUT_POST);
$interval_types = rcube_utils::get_input_value('_action_interval_type', rcube_utils::INPUT_POST);
$from = rcube_utils::get_input_value('_action_from', rcube_utils::INPUT_POST);
$subject = rcube_utils::get_input_value('_action_subject', rcube_utils::INPUT_POST, true);
$flags = rcube_utils::get_input_value('_action_flags', rcube_utils::INPUT_POST);
$varnames = rcube_utils::get_input_value('_action_varname', rcube_utils::INPUT_POST);
$varvalues = rcube_utils::get_input_value('_action_varvalue', rcube_utils::INPUT_POST);
$varmods = rcube_utils::get_input_value('_action_varmods', rcube_utils::INPUT_POST);
$notifymethods = rcube_utils::get_input_value('_action_notifymethod', rcube_utils::INPUT_POST);
$notifytargets = rcube_utils::get_input_value('_action_notifytarget', rcube_utils::INPUT_POST, true);
$notifyoptions = rcube_utils::get_input_value('_action_notifyoption', rcube_utils::INPUT_POST, true);
$notifymessages = rcube_utils::get_input_value('_action_notifymessage', rcube_utils::INPUT_POST, true);
$notifyfrom = rcube_utils::get_input_value('_action_notifyfrom', rcube_utils::INPUT_POST);
$notifyimp = rcube_utils::get_input_value('_action_notifyimportance', rcube_utils::INPUT_POST);
$addheader_name = rcube_utils::get_input_value('_action_addheader_name', rcube_utils::INPUT_POST);
$addheader_value = rcube_utils::get_input_value('_action_addheader_value', rcube_utils::INPUT_POST, true);
$addheader_pos = rcube_utils::get_input_value('_action_addheader_pos', rcube_utils::INPUT_POST);
$delheader_name = rcube_utils::get_input_value('_action_delheader_name', rcube_utils::INPUT_POST);
$delheader_value = rcube_utils::get_input_value('_action_delheader_value', rcube_utils::INPUT_POST, true);
$delheader_pos = rcube_utils::get_input_value('_action_delheader_pos', rcube_utils::INPUT_POST);
$delheader_index = rcube_utils::get_input_value('_action_delheader_index', rcube_utils::INPUT_POST);
$delheader_op = rcube_utils::get_input_value('_action_delheader_op', rcube_utils::INPUT_POST);
$delheader_comp = rcube_utils::get_input_value('_action_delheader_comp', rcube_utils::INPUT_POST);
$this->form['disabled'] = !empty($_POST['_disabled']);
$this->form['join'] = $join == 'allof';
$this->form['name'] = $name;
$this->form['tests'] = array();
$this->form['actions'] = array();
if ($name == '')
$this->errors['name'] = $this->plugin->gettext('cannotbeempty');
else {
foreach ($this->script as $idx => $rule)
if($rule['name'] == $name && $idx != $fid) {
$this->errors['name'] = $this->plugin->gettext('ruleexist');
break;
}
}
$i = 0;
// rules
if ($join == 'any') {
$this->form['tests'][0]['test'] = 'true';
}
else {
foreach ($headers as $idx => $header) {
// targets are indexed differently (assume form order)
$target = $this->strip_value($targets[$idx], true);
$header = $this->strip_value($header);
$operator = $this->strip_value($ops[$idx]);
$comparator = $this->strip_value($comparators[$idx]);
if ($header == 'size') {
$sizeop = $this->strip_value($sizeops[$idx]);
$sizeitem = $this->strip_value($sizeitems[$idx]);
$sizetarget = $this->strip_value($sizetargets[$idx]);
$this->form['tests'][$i]['test'] = 'size';
$this->form['tests'][$i]['type'] = $sizeop;
$this->form['tests'][$i]['arg'] = $sizetarget;
if ($sizetarget == '')
$this->errors['tests'][$i]['sizetarget'] = $this->plugin->gettext('cannotbeempty');
else if (!preg_match('/^[0-9]+(K|M|G)?$/i', $sizetarget.$sizeitem, $m)) {
$this->errors['tests'][$i]['sizetarget'] = $this->plugin->gettext('forbiddenchars');
$this->form['tests'][$i]['item'] = $sizeitem;
}
else
$this->form['tests'][$i]['arg'] .= $m[1];
}
else if ($header == 'currentdate') {
$datepart = $this->strip_value($dateparts[$idx]);
if (preg_match('/^not/', $operator))
$this->form['tests'][$i]['not'] = true;
$type = preg_replace('/^not/', '', $operator);
if ($type == 'exists') {
$this->errors['tests'][$i]['op'] = true;
}
$this->form['tests'][$i]['test'] = 'currentdate';
$this->form['tests'][$i]['type'] = $type;
$this->form['tests'][$i]['part'] = $datepart;
$this->form['tests'][$i]['arg'] = $target;
if ($type != 'exists') {
if (empty($target)) {
$this->errors['tests'][$i]['target'] = $this->plugin->gettext('cannotbeempty');
}
else if (strpos($type, 'count-') === 0) {
foreach ($target as $arg) {
if (preg_match('/[^0-9]/', $arg)) {
$this->errors['tests'][$i]['target'] = $this->plugin->gettext('forbiddenchars');
}
}
}
else if (strpos($type, 'value-') === 0) {
// Some date/time formats do not support i;ascii-numeric comparator
if ($comparator == 'i;ascii-numeric' && in_array($datepart, array('date', 'time', 'iso8601', 'std11'))) {
$comparator = '';
}
}
if (!preg_match('/^(regex|matches|count-)/', $type) && !empty($target)) {
foreach ($target as $arg) {
if (!$this->validate_date_part($datepart, $arg)) {
$this->errors['tests'][$i]['target'] = $this->plugin->gettext('invaliddateformat');
break;
}
}
}
}
}
else if ($header == 'date') {
$datepart = $this->strip_value($dateparts[$idx]);
$dateheader = $this->strip_value($dateheaders[$idx]);
$index = $this->strip_value($indexes[$idx]);
$indexlast = $this->strip_value($lastindexes[$idx]);
if (preg_match('/^not/', $operator)) {
$this->form['tests'][$i]['not'] = true;
}
$type = preg_replace('/^not/', '', $operator);
if ($type == 'exists') {
$this->errors['tests'][$i]['op'] = true;
}
if (!empty($index) && $mod != 'envelope') {
$this->form['tests'][$i]['index'] = intval($index);
$this->form['tests'][$i]['last'] = !empty($indexlast);
}
if (empty($dateheader)) {
$dateheader = 'Date';
}
else if (!preg_match('/^[\x21-\x39\x41-\x7E]+$/i', $dateheader)) {
$this->errors['tests'][$i]['dateheader'] = $this->plugin->gettext('forbiddenchars');
}
$this->form['tests'][$i]['test'] = 'date';
$this->form['tests'][$i]['type'] = $type;
$this->form['tests'][$i]['part'] = $datepart;
$this->form['tests'][$i]['arg'] = $target;
$this->form['tests'][$i]['header'] = $dateheader;
if ($type != 'exists') {
if (empty($target)) {
$this->errors['tests'][$i]['target'] = $this->plugin->gettext('cannotbeempty');
}
else if (strpos($type, 'count-') === 0) {
foreach ($target as $arg) {
if (preg_match('/[^0-9]/', $arg)) {
$this->errors['tests'][$i]['target'] = $this->plugin->gettext('forbiddenchars');
}
}
}
else if (strpos($type, 'value-') === 0) {
// Some date/time formats do not support i;ascii-numeric comparator
if ($comparator == 'i;ascii-numeric' && in_array($datepart, array('date', 'time', 'iso8601', 'std11'))) {
$comparator = '';
}
}
if (!empty($target) && !preg_match('/^(regex|matches|count-)/', $type)) {
foreach ($target as $arg) {
if (!$this->validate_date_part($datepart, $arg)) {
$this->errors['tests'][$i]['target'] = $this->plugin->gettext('invaliddateformat');
break;
}
}
}
}
}
else if ($header == 'body') {
$trans = $this->strip_value($body_trans[$idx]);
$trans_type = $this->strip_value($body_types[$idx], true);
if (preg_match('/^not/', $operator))
$this->form['tests'][$i]['not'] = true;
$type = preg_replace('/^not/', '', $operator);
if ($type == 'exists') {
$this->errors['tests'][$i]['op'] = true;
}
$this->form['tests'][$i]['test'] = 'body';
$this->form['tests'][$i]['type'] = $type;
$this->form['tests'][$i]['arg'] = $target;
if (empty($target) && $type != 'exists') {
$this->errors['tests'][$i]['target'] = $this->plugin->gettext('cannotbeempty');
}
else if (preg_match('/^(value|count)-/', $type)) {
foreach ($target as $target_value) {
if (preg_match('/[^0-9]/', $target_value)) {
$this->errors['tests'][$i]['target'] = $this->plugin->gettext('forbiddenchars');
}
}
}
$this->form['tests'][$i]['part'] = $trans;
if ($trans == 'content') {
$this->form['tests'][$i]['content'] = $trans_type;
}
}
else if ($header == 'message') {
$test = $this->strip_value($message[$idx]);
if (preg_match('/^not/', $test)) {
$this->form['tests'][$i]['not'] = true;
$test = substr($test, 3);
}
$this->form['tests'][$i]['test'] = $test;
if ($test == 'duplicate') {
$this->form['tests'][$i]['last'] = !empty($dup_lasts[$idx]);
$this->form['tests'][$i]['handle'] = trim($dup_handles[$idx]);
$this->form['tests'][$i]['header'] = trim($dup_headers[$idx]);
$this->form['tests'][$i]['uniqueid'] = trim($dup_uniqueids[$idx]);
$this->form['tests'][$i]['seconds'] = trim($dup_seconds[$idx]);
if ($this->form['tests'][$i]['seconds']
&& preg_match('/[^0-9]/', $this->form['tests'][$i]['seconds'])
) {
$this->errors['tests'][$i]['duplicate_seconds'] = $this->plugin->gettext('forbiddenchars');
}
if ($this->form['tests'][$i]['header'] && $this->form['tests'][$i]['uniqueid']) {
$this->errors['tests'][$i]['duplicate_uniqueid'] = $this->plugin->gettext('duplicate.conflict.err');
}
}
}
else {
$cust_header = $headers = $this->strip_value($cust_headers[$idx]);
$mod = $this->strip_value($mods[$idx]);
$mod_type = $this->strip_value($mod_types[$idx]);
$index = $this->strip_value($indexes[$idx]);
$indexlast = $this->strip_value($lastindexes[$idx]);
$mime_param = $this->strip_value($mime_params[$idx]);
$mime_type = $mime_types[$idx];
$mime_part = $mime_parts[$idx];
if ($header == 'string') {
$cust_var = $headers = $this->strip_value($cust_vars[$idx]);
}
if (preg_match('/^not/', $operator))
$this->form['tests'][$i]['not'] = true;
$type = preg_replace('/^not/', '', $operator);
if (!empty($index) && $mod != 'envelope') {
$this->form['tests'][$i]['index'] = intval($index);
$this->form['tests'][$i]['last'] = !empty($indexlast);
}
if ($header == '...' || $header == 'string') {
if (!count($headers))
$this->errors['tests'][$i]['header'] = $this->plugin->gettext('cannotbeempty');
else if ($header == '...') {
foreach ($headers as $hr) {
// RFC2822: printable ASCII except colon
if (!preg_match('/^[\x21-\x39\x41-\x7E]+$/i', $hr)) {
$this->errors['tests'][$i]['header'] = $this->plugin->gettext('forbiddenchars');
}
}
}
if (empty($this->errors['tests'][$i]['header']))
$cust_header = $cust_var = (is_array($headers) && count($headers) == 1) ? $headers[0] : $headers;
}
$test = $header == 'string' ? 'string' : 'header';
$header = $header == 'string' ? $cust_var : $header;
$header = $header == '...' ? $cust_header : $header;
if (is_array($header)) {
foreach ($header as $h_index => $val) {
if (isset($this->headers[$val])) {
$header[$h_index] = $this->headers[$val];
}
}
}
if ($type == 'exists') {
$this->form['tests'][$i]['test'] = 'exists';
$this->form['tests'][$i]['arg'] = $header;
}
else {
if ($mod == 'address' || $mod == 'envelope') {
$found = false;
if (empty($this->errors['tests'][$i]['header'])) {
foreach ((array)$header as $hdr) {
if (!in_array(strtolower(trim($hdr)), $this->addr_headers))
$found = true;
}
}
if (!$found)
$test = $mod;
}
$this->form['tests'][$i]['type'] = $type;
$this->form['tests'][$i]['test'] = $test;
$this->form['tests'][$i]['arg1'] = $header;
$this->form['tests'][$i]['arg2'] = $target;
if (empty($target)) {
$this->errors['tests'][$i]['target'] = $this->plugin->gettext('cannotbeempty');
}
else if (preg_match('/^(value|count)-/', $type)) {
foreach ($target as $target_value) {
if (preg_match('/[^0-9]/', $target_value)) {
$this->errors['tests'][$i]['target'] = $this->plugin->gettext('forbiddenchars');
}
}
}
if ($mod) {
$this->form['tests'][$i]['part'] = $mod_type;
}
}
if ($test == 'header') {
if (in_array($mime_type, array('type', 'subtype', 'contenttype', 'param'))) {
$this->form['tests'][$i]['mime-' . $mime_type] = true;
if ($mime_type == 'param') {
if (empty($mime_param)) {
$this->errors['tests'][$i]['mime-param'] = $this->plugin->gettext('cannotbeempty');
}
$this->form['tests'][$i]['mime-param'] = $mime_param;
}
}
if ($mime_part == 'anychild') {
$this->form['tests'][$i]['mime-anychild'] = true;
}
}
}
if ($header != 'size' && $comparator) {
$this->form['tests'][$i]['comparator'] = $comparator;
}
$i++;
}
}
$i = 0;
// actions
foreach ($act_types as $idx => $type) {
$type = $this->strip_value($type);
switch ($type) {
case 'fileinto':
case 'fileinto_copy':
$mailbox = $this->strip_value($mailboxes[$idx], false, false);
$this->form['actions'][$i]['target'] = $this->mod_mailbox($mailbox, 'in');
if ($type == 'fileinto_copy') {
$type = 'fileinto';
$this->form['actions'][$i]['copy'] = true;
}
break;
case 'reject':
case 'ereject':
$target = $this->strip_value($area_targets[$idx]);
$this->form['actions'][$i]['target'] = str_replace("\r\n", "\n", $target);
// if ($target == '')
// $this->errors['actions'][$i]['targetarea'] = $this->plugin->gettext('cannotbeempty');
break;
case 'redirect':
case 'redirect_copy':
$target = $this->strip_value($act_targets[$idx]);
$domain = $this->strip_value($domain_targets[$idx]);
// force one of the configured domains
$domains = (array) $this->rc->config->get('managesieve_domains');
if (!empty($domains) && !empty($target)) {
if (!$domain || !in_array($domain, $domains)) {
$domain = $domains[0];
}
$target .= '@' . $domain;
}
$this->form['actions'][$i]['target'] = $target;
if ($target == '')
$this->errors['actions'][$i]['target'] = $this->plugin->gettext('cannotbeempty');
else if (!rcube_utils::check_email($target))
$this->errors['actions'][$i]['target'] = $this->plugin->gettext(!empty($domains) ? 'forbiddenchars' : 'noemailwarning');
if ($type == 'redirect_copy') {
$type = 'redirect';
$this->form['actions'][$i]['copy'] = true;
}
break;
case 'addflag':
case 'setflag':
case 'removeflag':
$_target = array();
if (empty($flags[$idx])) {
$this->errors['actions'][$i]['target'] = $this->plugin->gettext('noflagset');
}
else {
foreach ($flags[$idx] as $flag) {
$_target[] = $this->strip_value($flag);
}
}
$this->form['actions'][$i]['target'] = $_target;
break;
case 'addheader':
case 'deleteheader':
$this->form['actions'][$i]['name'] = trim($type == 'addheader' ? $addheader_name[$idx] : $delheader_name[$idx]);
$this->form['actions'][$i]['value'] = $type == 'addheader' ? $addheader_value[$idx] : $delheader_value[$idx];
$this->form['actions'][$i]['last'] = ($type == 'addheader' ? $addheader_pos[$idx] : $delheader_pos[$idx]) == 'last';
if (empty($this->form['actions'][$i]['name'])) {
$this->errors['actions'][$i]['name'] = $this->plugin->gettext('cannotbeempty');
}
else if (!preg_match('/^[0-9a-z_-]+$/i', $this->form['actions'][$i]['name'])) {
$this->errors['actions'][$i]['name'] = $this->plugin->gettext('forbiddenchars');
}
if ($type == 'deleteheader') {
foreach ((array) $this->form['actions'][$i]['value'] as $pidx => $pattern) {
if (empty($pattern)) {
unset($this->form['actions'][$i]['value'][$pidx]);
}
}
$this->form['actions'][$i]['match-type'] = $delheader_op[$idx];
$this->form['actions'][$i]['comparator'] = $delheader_comp[$idx];
$this->form['actions'][$i]['index'] = $delheader_index[$idx];
if (empty($this->form['actions'][$i]['index'])) {
if (!empty($this->form['actions'][$i]['last'])) {
$this->errors['actions'][$i]['index'] = $this->plugin->gettext('lastindexempty');
}
}
else if (!preg_match('/^[0-9]+$/i', $this->form['actions'][$i]['index'])) {
$this->errors['actions'][$i]['index'] = $this->plugin->gettext('forbiddenchars');
}
}
else {
if (empty($this->form['actions'][$i]['value'])) {
$this->errors['actions'][$i]['value'] = $this->plugin->gettext('cannotbeempty');
}
}
break;
case 'vacation':
$reason = $this->strip_value($reasons[$idx]);
$interval_type = $interval_types[$idx] == 'seconds' ? 'seconds' : 'days';
$this->form['actions'][$i]['reason'] = str_replace("\r\n", "\n", $reason);
$this->form['actions'][$i]['from'] = $from[$idx];
$this->form['actions'][$i]['subject'] = $subject[$idx];
$this->form['actions'][$i]['addresses'] = $addresses[$idx];
$this->form['actions'][$i][$interval_type] = $intervals[$idx];
// @TODO: vacation :mime, :handle
foreach ((array)$this->form['actions'][$i]['addresses'] as $aidx => $address) {
$this->form['actions'][$i]['addresses'][$aidx] = $address = trim($address);
if (empty($address)) {
unset($this->form['actions'][$i]['addresses'][$aidx]);
}
else if (!rcube_utils::check_email($address)) {
$this->errors['actions'][$i]['addresses'] = $this->plugin->gettext('noemailwarning');
break;
}
}
if (!empty($this->form['actions'][$i]['from']) && !rcube_utils::check_email($this->form['actions'][$i]['from'])) {
$this->errors['actions'][$i]['from'] = $this->plugin->gettext('noemailwarning');
}
if ($this->form['actions'][$i]['reason'] == '')
$this->errors['actions'][$i]['reason'] = $this->plugin->gettext('cannotbeempty');
if ($this->form['actions'][$i][$interval_type] && !preg_match('/^[0-9]+$/', $this->form['actions'][$i][$interval_type]))
$this->errors['actions'][$i]['interval'] = $this->plugin->gettext('forbiddenchars');
break;
case 'set':
$this->form['actions'][$i]['name'] = $varnames[$idx];
$this->form['actions'][$i]['value'] = $varvalues[$idx];
foreach ((array)$varmods[$idx] as $v_m) {
$this->form['actions'][$i][$v_m] = true;
}
if (empty($varnames[$idx])) {
$this->errors['actions'][$i]['name'] = $this->plugin->gettext('cannotbeempty');
}
else if (!preg_match('/^[0-9a-z_]+$/i', $varnames[$idx])) {
$this->errors['actions'][$i]['name'] = $this->plugin->gettext('forbiddenchars');
}
if (!isset($varvalues[$idx]) || $varvalues[$idx] === '') {
$this->errors['actions'][$i]['value'] = $this->plugin->gettext('cannotbeempty');
}
break;
case 'notify':
if (empty($notifymethods[$idx])) {
$this->errors['actions'][$i]['method'] = $this->plugin->gettext('cannotbeempty');
}
if (empty($notifytargets[$idx])) {
$this->errors['actions'][$i]['target'] = $this->plugin->gettext('cannotbeempty');
}
if (!empty($notifyfrom[$idx]) && !rcube_utils::check_email($notifyfrom[$idx])) {
$this->errors['actions'][$i]['from'] = $this->plugin->gettext('noemailwarning');
}
// skip empty options
foreach ((array)$notifyoptions[$idx] as $opt_idx => $opt) {
if (!strlen(trim($opt))) {
unset($notifyoptions[$idx][$opt_idx]);
}
}
$this->form['actions'][$i]['method'] = $notifymethods[$idx] . ':' . $notifytargets[$idx];
$this->form['actions'][$i]['options'] = $notifyoptions[$idx];
$this->form['actions'][$i]['message'] = $notifymessages[$idx];
$this->form['actions'][$i]['from'] = $notifyfrom[$idx];
$this->form['actions'][$i]['importance'] = $notifyimp[$idx];
break;
}
$this->form['actions'][$i]['type'] = $type;
$i++;
}
if (!$this->errors && !$error) {
// save the script
if (!isset($this->script[$fid])) {
$fid = $this->sieve->script->add_rule($this->form);
$new = true;
}
else {
$fid = $this->sieve->script->update_rule($fid, $this->form);
}
if ($fid !== false) {
$save = $this->save_script();
}
if ($save && $fid !== false) {
$this->rc->output->show_message('managesieve.filtersaved', 'confirmation');
if ($this->rc->task != 'mail') {
$this->rc->output->command('parent.managesieve_updatelist',
isset($new) ? 'add' : 'update',
array(
'name' => $this->form['name'],
'id' => $fid,
'disabled' => $this->form['disabled']
));
}
else {
$this->rc->output->command('managesieve_dialog_close');
$this->rc->output->send('iframe');
}
}
else {
$this->rc->output->show_message('managesieve.filtersaveerror', 'error');
}
}
else {
$this->rc->output->show_message('managesieve.filterformerror', 'warning');
}
}
$this->send();
}
protected function send()
{
// Handle form action
if (isset($_GET['_framed']) || isset($_POST['_framed'])) {
if (isset($_GET['_newset']) || isset($_POST['_newset'])) {
$this->rc->output->send('managesieve.setedit');
}
else if (isset($_GET['_seteditraw']) || isset($_POST['_seteditraw'])) {
$this->rc->output->send('managesieve.seteditraw');
}
else {
$this->rc->output->send('managesieve.filteredit');
}
}
else {
$this->rc->output->set_pagetitle($this->plugin->gettext('filters'));
$this->rc->output->send('managesieve.managesieve');
}
}
// return the filters list as HTML table
function filters_list($attrib)
{
// add id to message list table if not specified
if (!strlen($attrib['id']))
$attrib['id'] = 'rcmfilterslist';
// define list of cols to be displayed
$a_show_cols = array('name');
$result = $this->list_rules();
// create XHTML table
$out = $this->rc->table_output($attrib, $result, $a_show_cols, 'id');
// set client env
$this->rc->output->add_gui_object('filterslist', $attrib['id']);
$this->rc->output->include_script('list.js');
// add some labels to client
$this->rc->output->add_label('managesieve.filterdeleteconfirm');
return $out;
}
// return the filters list as <SELECT>
function filtersets_list($attrib, $no_env = false)
{
// add id to message list table if not specified
if (!strlen($attrib['id'])) {
$attrib['id'] = 'rcmfiltersetslist';
}
$list = $this->list_scripts();
if ($list) {
asort($list, SORT_LOCALE_STRING);
}
if (!empty($attrib['type']) && $attrib['type'] == 'list') {
// define list of cols to be displayed
$a_show_cols = array('name');
if ($list) {
foreach ($list as $idx => $set) {
$scripts['S'.$idx] = $set;
$result[] = array(
'name' => $set,
'id' => 'S'.$idx,
'class' => !in_array($set, $this->active) ? 'disabled' : '',
);
}
}
// create XHTML table
$out = $this->rc->table_output($attrib, $result, $a_show_cols, 'id');
$this->rc->output->set_env('filtersets', $scripts);
$this->rc->output->include_script('list.js');
}
else {
$select = new html_select(array('name' => '_set', 'id' => $attrib['id'],
'onchange' => $this->rc->task != 'mail' ? 'rcmail.managesieve_set()' : ''));
if ($list) {
foreach ($list as $set)
$select->add($set, $set);
}
$out = $select->show($this->sieve->current);
}
// set client env
if (!$no_env) {
$this->rc->output->add_gui_object('filtersetslist', $attrib['id']);
$this->rc->output->add_label('managesieve.setdeleteconfirm');
}
return $out;
}
function filterset_editraw($attrib)
{
$script_name = isset($_GET['_set']) ? $_GET['_set'] : $_POST['_set'];
$script = $this->sieve->get_script($script_name);
$script_post = $_POST['rawsetcontent'];
$hiddenfields = new html_hiddenfield();
$hiddenfields->add(array('name' => '_task', 'value' => $this->rc->task));
$hiddenfields->add(array('name' => '_action', 'value' => 'plugin.managesieve-saveraw'));
$hiddenfields->add(array('name' => '_set', 'value' => $script_name));
$hiddenfields->add(array('name' => '_seteditraw', 'value' => 1));
$hiddenfields->add(array('name' => '_framed', 'value' => ($_POST['_framed'] || $_GET['_framed'] ? 1 : 0)));
$out = $hiddenfields->show();
$txtarea = new html_textarea(array(
'id' => 'rawfiltersettxt',
'name' => 'rawsetcontent',
'class' => 'form-control',
'rows' => '15'
));
$out .= $txtarea->show($script_post !== null ? $script_post : ($script !== false ? rtrim($script) : ''));
$this->rc->output->add_gui_object('sievesetrawform', 'filtersetrawform');
$this->plugin->include_stylesheet('codemirror/lib/codemirror.css');
$this->plugin->include_script('codemirror/lib/codemirror.js');
$this->plugin->include_script('codemirror/addon/selection/active-line.js');
$this->plugin->include_script('codemirror/mode/sieve/sieve.js');
if ($script === false) {
$this->rc->output->show_message('managesieve.filterunknownerror', 'error');
}
$out = html::tag('form', $attrib + array(
'id' => 'filtersetrawform',
'name' => 'filtersetrawform',
'action' => './',
'method' => 'post',
'enctype' => 'multipart/form-data',
), $out);
return str_replace('</form>', '', $out);
}
function filterset_form($attrib)
{
if (!$attrib['id']) {
$attrib['id'] = 'rcmfiltersetform';
}
$table = new html_table(array('cols' => 2, 'class' => 'propform'));
$hiddenfields = new html_hiddenfield(array('name' => '_task', 'value' => $this->rc->task));
$hiddenfields->add(array('name' => '_action', 'value' => 'plugin.managesieve-save'));
$hiddenfields->add(array('name' => '_framed', 'value' => ($_POST['_framed'] || $_GET['_framed'] ? 1 : 0)));
$hiddenfields->add(array('name' => '_newset', 'value' => 1));
$name = rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST);
$copy = rcube_utils::get_input_value('_copy', rcube_utils::INPUT_POST);
$selected = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST);
// filter set name input
$input_name = new html_inputfield(array('name' => '_name', 'id' => '_name', 'size' => 30,
'class' => ($this->errors['name'] ? 'error' : '')));
$table->add('title', html::label('_name', rcube::Q($this->plugin->gettext('filtersetname'))));
$table->add(null, $input_name->show($name));
$filters = '<ul class="proplist">';
$filters .= '<li>' . html::label('from_none', html::tag('input', array(
'type' => 'radio',
'id' => 'from_none',
'name' => '_from',
'value' => 'none',
'checked' => !$selected || $selected == 'none'
)) . rcube::Q($this->plugin->gettext('none'))) . '</li>';
// filters set list
$list = $this->list_scripts();
$select = new html_select(array('name' => '_copy', 'id' => '_copy'));
if (is_array($list)) {
asort($list, SORT_LOCALE_STRING);
if (!$copy)
$copy = $_SESSION['managesieve_current'];
foreach ($list as $set) {
$select->add($set, $set);
}
$filters .= '<li>' . html::label('from_set', html::tag('input', array(
'type' => 'radio',
'id' => 'from_set',
'name' => '_from',
'value' => 'set',
'checked' => $selected == 'set',
)) . rcube::Q($this->plugin->gettext('fromset')) . ' ' . $select->show($copy)) . '</li>';
}
// script upload box
$upload = new html_inputfield(array('name' => '_file', 'id' => '_file', 'size' => 30,
'type' => 'file', 'class' => ($this->errors['file'] ? 'error' : '')));
$filters .= '<li>' . html::label('from_file', html::tag('input', array(
'type' => 'radio',
'id' => 'from_file',
'name' => '_from',
'value' => 'file',
'checked' => $selected == 'file',
)) . rcube::Q($this->plugin->gettext('fromfile')) . ' ' . $upload->show()) . '</li>';
$filters .= '</ul>';
$table->add('title', html::label('from_none', rcube::Q($this->plugin->gettext('filters'))));
$table->add('', $filters);
$out = '<form name="filtersetform" action="./" method="post" enctype="multipart/form-data">'
. "\n" . $hiddenfields->show() . "\n" . $table->show();
$this->rc->output->add_gui_object('sieveform', 'filtersetform');
if ($this->errors['name'])
$this->add_tip('_name', $this->errors['name'], true);
if ($this->errors['file'])
$this->add_tip('_file', $this->errors['file'], true);
$this->print_tips();
return $out;
}
function filter_form($attrib)
{
if (!$attrib['id']) {
$attrib['id'] = 'rcmfilterform';
}
$fid = rcube_utils::get_input_value('_fid', rcube_utils::INPUT_GPC);
$scr = isset($this->form) ? $this->form : $this->script[$fid];
$compact = !empty($attrib['compact-form']);
$_SESSION['managesieve-compact-form'] = $compact;
// do not allow creation of new rules
if ($fid == null && in_array('new_rule', $this->disabled_actions)) {
$this->rc->output->show_message('managesieve.disabledaction', 'error');
return;
}
$hiddenfields = new html_hiddenfield(array('name' => '_task', 'value' => $this->rc->task));
$hiddenfields->add(array('name' => '_action', 'value' => 'plugin.managesieve-save'));
$hiddenfields->add(array('name' => '_framed', 'value' => ($_POST['_framed'] || $_GET['_framed'] ? 1 : 0)));
$hiddenfields->add(array('name' => '_fid', 'value' => $fid));
$out = $hiddenfields->show();
// 'any' flag
if ((!isset($this->form) && empty($scr['tests']) && !empty($scr))
|| (is_array($scr['tests']) && count($scr['tests']) == 1 && $scr['tests'][0]['test'] == 'true' && !$scr['tests'][0]['not'])
) {
$any = true;
}
// filter name input
$input_name = new html_inputfield(array(
'name' => '_name',
'id' => '_name',
'size' => 30,
'class' => ($this->errors['name'] ? ' error' : '')
));
if ($this->errors['name']) {
$this->add_tip('_name', $this->errors['name'], true);
}
$input_name = $input_name->show(isset($scr) ? $scr['name'] : '');
$out .= sprintf("\n" . '<div class="form-group row">'
. '<label for="_name" class="col-sm-4 col-form-label">%s</label>'
. '<div class="col-sm-8">%s</div></div>',
rcube::Q($this->plugin->gettext('filtername')), $input_name);
// filter set selector
if ($this->rc->task == 'mail') {
$out .= sprintf("\n" . '<div class="form-group row">'
. '<label for="%s" class="col-sm-4 col-form-label">%s</label>'
. '<div class="col-sm-8">%s</div></div>',
'sievescriptname',
rcube::Q($this->plugin->gettext('filterset')),
$this->filtersets_list(array('id' => 'sievescriptname'), true)
);
}
else if ($compact) {
$out .= sprintf("\n" . '<div class="form-group row form-check">'
. '<label for="disabled" class="col-sm-4 col-form-label">%s</label>'
. '<div class="col-sm-8 form-check"><input type="checkbox" id="disabled" name="_disabled" value="1" /></div></div>',
rcube::Q($this->plugin->gettext('filterdisabled')));
}
if ($compact) {
$select = new html_select(array('name' => '_join', 'id' => '_join' . $id,
'onchange' => 'rule_join_radio(this.value)'));
foreach (array('allof', 'anyof', 'any') as $val) {
$select->add($this->plugin->gettext('filter' . $val), $val);
}
$join = $any ? 'any' : 'allof';
if (isset($scr) && !$any) {
$join = $scr['join'] ? 'allof' : 'anyof';
}
$out .= sprintf("\n" . '<div class="form-group row">'
. '<label for="_join" class="col-sm-4 col-form-label">%s</label>'
. '<div class="col-sm-8">%s</div></div>',
rcube::Q($this->plugin->gettext('scope')), $select->show($join));
$out .= '<div id="rules"'.($any ? ' style="display: none"' : '').'>';
$out .= "\n<fieldset><legend>" . rcube::Q($this->plugin->gettext('rules')) . "</legend>\n";
}
else {
$out .= '<br><fieldset><legend>' . rcube::Q($this->plugin->gettext('messagesrules')) . "</legend>\n";
// any, allof, anyof radio buttons
$field_id = '_allof';
$input_join = new html_radiobutton(array('name' => '_join', 'id' => $field_id, 'value' => 'allof',
'onclick' => 'rule_join_radio(\'allof\')', 'class' => 'radio'));
if (isset($scr) && !$any)
$input_join = $input_join->show($scr['join'] ? 'allof' : '');
else
$input_join = $input_join->show();
$out .= $input_join . html::label($field_id, rcube::Q($this->plugin->gettext('filterallof')));
$field_id = '_anyof';
$input_join = new html_radiobutton(array('name' => '_join', 'id' => $field_id, 'value' => 'anyof',
'onclick' => 'rule_join_radio(\'anyof\')', 'class' => 'radio'));
if (isset($scr) && !$any)
$input_join = $input_join->show($scr['join'] ? '' : 'anyof');
else
$input_join = $input_join->show('anyof'); // default
$out .= $input_join . html::label($field_id, rcube::Q($this->plugin->gettext('filteranyof')));
$field_id = '_any';
$input_join = new html_radiobutton(array('name' => '_join', 'id' => $field_id, 'value' => 'any',
'onclick' => 'rule_join_radio(\'any\')', 'class' => 'radio'));
$input_join = $input_join->show($any ? 'any' : '');
$out .= $input_join . html::label($field_id, rcube::Q($this->plugin->gettext('filterany')));
$out .= '<div id="rules"'.($any ? ' style="display: none"' : '').'>';
}
$rows_num = !empty($scr['tests']) ? count($scr['tests']) : 1;
for ($x=0; $x<$rows_num; $x++) {
$out .= $this->rule_div($fid, $x, true, $compact);
}
$out .= $compact ? "</fieldset>\n</div>\n" : "</div>\n</fieldset>\n";
// actions
$label = $this->plugin->gettext($compact ? 'actions' : 'messagesactions');
$out .= '<fieldset><legend>' . rcube::Q($label) . "</legend>\n";
$rows_num = isset($scr) ? count($scr['actions']) : 1;
$out .= '<div id="actions">';
for ($x=0; $x<$rows_num; $x++)
$out .= $this->action_div($fid, $x);
$out .= "</div>\n";
$out .= "</fieldset>\n";
$this->print_tips();
if ($scr['disabled']) {
$this->rc->output->set_env('rule_disabled', true);
}
$this->rc->output->add_label(
'managesieve.ruledeleteconfirm',
'managesieve.actiondeleteconfirm'
);
$this->rc->output->add_gui_object('sieveform', 'filterform');
$attrib['name'] = 'filterform';
$attrib['action'] = './';
$attrib['method'] = 'post';
$out = html::tag('form', $attrib, $out, array('name', 'action', 'method', 'class'));
if (!$compact) {
$out = str_replace('</form>', '', $out);
}
return $out;
}
function rule_div($fid, $id, $div = true, $compact = false)
{
$rule = isset($this->form) ? $this->form['tests'][$id] : $this->script[$fid]['tests'][$id];
if (isset($this->form['tests'])) {
$rows_num = count($this->form['tests']);
}
else if (isset($this->script[$fid]['tests'])) {
$rows_num = count($this->script[$fid]['tests']);
}
else {
$rows_num = 0;
}
// headers select
$select_header = new html_select(array('name' => "_header[$id]", 'id' => 'header'.$id,
'onchange' => 'rule_header_select(' .$id .')'));
foreach ($this->headers as $index => $header) {
$header = $this->rc->text_exists($index) ? $this->plugin->gettext($index) : $header;
$select_header->add($header, $index);
}
$select_header->add($this->plugin->gettext('...'), '...');
if (in_array('body', $this->exts)) {
$select_header->add($this->plugin->gettext('body'), 'body');
}
$select_header->add($this->plugin->gettext('size'), 'size');
if (in_array('date', $this->exts)) {
$select_header->add($this->plugin->gettext('datetest'), 'date');
$select_header->add($this->plugin->gettext('currdate'), 'currentdate');
}
if (in_array('variables', $this->exts)) {
$select_header->add($this->plugin->gettext('string'), 'string');
}
if (in_array('duplicate', $this->exts)) {
$select_header->add($this->plugin->gettext('message'), 'message');
}
if (isset($rule['test'])) {
if (in_array($rule['test'], array('header', 'address', 'envelope'))) {
if (is_array($rule['arg1']) && count($rule['arg1']) == 1) {
$rule['arg1'] = $rule['arg1'][0];
}
$matches = !is_array($rule['arg1']) && ($header = strtolower($rule['arg1'])) && isset($this->headers[$header]);
$test = $matches ? $header : '...';
}
else if ($rule['test'] == 'exists') {
if (is_array($rule['arg']) && count($rule['arg']) == 1) {
$rule['arg'] = $rule['arg'][0];
}
$matches = !is_array($rule['arg']) && ($header = strtolower($rule['arg'])) && isset($this->headers[$header]);
$test = $matches ? $header : '...';
}
else if (in_array($rule['test'], array('size', 'body', 'date', 'currentdate', 'string'))) {
$test = $rule['test'];
}
else if (in_array($rule['test'], array('duplicate'))) {
$test = 'message';
}
else if ($rule['test'] != 'true') {
$test = '...';
}
}
$tout = '<div class="flexbox">';
$aout = $select_header->show($test);
// custom headers input
if (isset($rule['test']) && in_array($rule['test'], array('header', 'address', 'envelope'))) {
$custom = (array) $rule['arg1'];
if (count($custom) == 1 && isset($this->headers[strtolower($custom[0])])) {
unset($custom);
}
}
else if (isset($rule['test']) && $rule['test'] == 'string') {
$customv = (array) $rule['arg1'];
if (count($customv) == 1 && isset($this->headers[strtolower($customv[0])])) {
unset($customv);
}
}
else if (isset($rule['test']) && $rule['test'] == 'exists') {
$custom = (array) $rule['arg'];
if (count($custom) == 1 && isset($this->headers[strtolower($custom[0])])) {
unset($custom);
}
}
// custom header and variable inputs
$aout .= $this->list_input($id, 'custom_header', $custom, isset($custom),
$this->error_class($id, 'test', 'header', 'custom_header'), 15) . "\n";
$aout .= $this->list_input($id, 'custom_var', $customv, isset($customv),
$this->error_class($id, 'test', 'header', 'custom_var'), 15) . "\n";
$test = self::rule_test($rule);
$target = '';
// target(s) input
if (in_array($rule['test'], array('header', 'address', 'envelope','string'))) {
$target = $rule['arg2'];
}
else if (in_array($rule['test'], array('body', 'date', 'currentdate'))) {
$target = $rule['arg'];
}
else if ($rule['test'] == 'size') {
if (preg_match('/^([0-9]+)(K|M|G)?$/', $rule['arg'], $matches)) {
$sizetarget = $matches[1];
$sizeitem = $matches[2];
}
else {
$sizetarget = $rule['arg'];
$sizeitem = $rule['item'];
}
}
// (current)date part select
if (in_array('date', $this->exts) || in_array('currentdate', $this->exts)) {
$date_parts = array('date', 'iso8601', 'std11', 'julian', 'time',
'year', 'month', 'day', 'hour', 'minute', 'second', 'weekday', 'zone');
$select_dp = new html_select(array('name' => "_rule_date_part[$id]", 'id' => 'rule_date_part'.$id,
'style' => in_array($rule['test'], array('currentdate', 'date')) && !preg_match('/^(notcount|count)-/', $test) ? '' : 'display:none',
'class' => 'datepart_selector',
));
foreach ($date_parts as $part) {
$select_dp->add(rcube::Q($this->plugin->gettext($part)), $part);
}
$aout .= $select_dp->show($rule['test'] == 'currentdate' || $rule['test'] == 'date' ? $rule['part'] : '');
}
// message test select (e.g. duplicate)
if (in_array('duplicate', $this->exts)) {
$select_msg = new html_select(array('name' => "_rule_message[$id]", 'id' => 'rule_message'.$id,
'style' => in_array($rule['test'], array('duplicate')) ? '' : 'display:none',
'class' => 'message_selector',
));
$select_msg->add(rcube::Q($this->plugin->gettext('duplicate')), 'duplicate');
$select_msg->add(rcube::Q($this->plugin->gettext('notduplicate')), 'notduplicate');
$tout .= $select_msg->show($test);
}
$tout .= $this->match_type_selector('rule_op', $id, $test, $rule['test']);
$tout .= $this->list_input($id, 'rule_target', $target,
$rule['test'] != 'size' && $rule['test'] != 'exists' && $rule['test'] != 'duplicate',
$this->error_class($id, 'test', 'target', 'rule_target')) . "\n";
$select_size_op = new html_select(array('name' => "_rule_size_op[$id]", 'id' => 'rule_size_op'.$id, 'class' => 'input-group-prepend'));
$select_size_op->add(rcube::Q($this->plugin->gettext('filterover')), 'over');
$select_size_op->add(rcube::Q($this->plugin->gettext('filterunder')), 'under');
$select_size_item = new html_select(array('name' => "_rule_size_item[$id]", 'id' => 'rule_size_item'.$id, 'class' => 'input-group-append'));
foreach (array('', 'K', 'M', 'G') as $unit) {
$select_size_item->add($this->plugin->gettext($unit . 'B'), $unit);
}
$tout .= '<div id="rule_size' .$id. '" class="input-group" style="display:' . ($rule['test']=='size' ? 'inline' : 'none') .'">';
$tout .= $select_size_op->show($rule['test']=='size' ? $rule['type'] : '');
$tout .= html::tag('input', array(
'type' => 'text',
'name' => "_rule_size_target[$id]",
'id' => 'rule_size_i'.$id,
'value' => $sizetarget,
'size' => 10,
'class' => $this->error_class($id, 'test', 'sizetarget', 'rule_size_i'),
));
$tout .= "\n" . $select_size_item->show($sizeitem);
$tout .= '</div>';
$tout .= '</div>';
// Advanced modifiers (address, envelope)
$select_mod = new html_select(array('name' => "_rule_mod[$id]", 'id' => 'rule_mod_op'.$id,
'onchange' => 'rule_mod_select(' .$id .')'));
$select_mod->add(rcube::Q($this->plugin->gettext('none')), '');
$select_mod->add(rcube::Q($this->plugin->gettext('address')), 'address');
if (in_array('envelope', $this->exts)) {
$select_mod->add(rcube::Q($this->plugin->gettext('envelope')), 'envelope');
}
$select_type = new html_select(array('name' => "_rule_mod_type[$id]", 'id' => 'rule_mod_type'.$id));
$select_type->add(rcube::Q($this->plugin->gettext('allparts')), 'all');
$select_type->add(rcube::Q($this->plugin->gettext('domain')), 'domain');
$select_type->add(rcube::Q($this->plugin->gettext('localpart')), 'localpart');
if (in_array('subaddress', $this->exts)) {
$select_type->add(rcube::Q($this->plugin->gettext('user')), 'user');
$select_type->add(rcube::Q($this->plugin->gettext('detail')), 'detail');
}
$need_mod = !in_array($rule['test'], array('size', 'body', 'date', 'currentdate', 'duplicate', 'string'));
$mout = '<div id="rule_mod' .$id. '" class="adv input-group"' . (!$need_mod ? ' style="display:none"' : '') . '>';
$mout .= html::span('label input-group-prepend', html::span('input-group-text', rcube::Q($this->plugin->gettext('modifier'))));
$mout .= $select_mod->show($rule['test']);
$mout .= '</div>';
$mout .= '<div id="rule_mod_type' . $id . '" class="adv input-group"';
$mout .= (!in_array($rule['test'], array('address', 'envelope')) ? ' style="display:none"' : '') . '>';
$mout .= html::span('label input-group-prepend', html::span('input-group-text', rcube::Q($this->plugin->gettext('modtype'))));
$mout .= $select_type->show($rule['part']);
$mout .= '</div>';
// Advanced modifiers (comparators)
$need_comp = $rule['test'] != 'size' && $rule['test'] != 'duplicate';
$mout .= '<div id="rule_comp' .$id. '" class="adv input-group"' . (!$need_comp ? ' style="display:none"' : '') . '>';
$mout .= html::span('label input-group-prepend', html::span('input-group-text', rcube::Q($this->plugin->gettext('comparator'))));
$mout .= $this->comparator_selector($rule['comparator'], 'rule_comp', $id);
$mout .= '</div>';
// Advanced modifiers (mime)
if (in_array('mime', $this->exts)) {
$need_mime = !$rule || in_array($rule['test'], array('header', 'address', 'exists'));
$mime_type = '';
$select_mime = new html_select(array('name' => "_rule_mime_type[$id]", 'id' => 'rule_mime_type' . $id,
'style' => 'min-width:8em', 'onchange' => 'rule_mime_select(' . $id . ')'));
$select_mime->add('-', '');
foreach (array('contenttype', 'type', 'subtype', 'param') as $val) {
if (isset($rule['mime-' . $val])) {
$mime_type = $val;
}
$select_mime->add(rcube::Q($this->plugin->gettext('mime-' . $val)), $val);
}
$select_mime_part = new html_select(array('name' => "_rule_mime_part[$id]", 'id' => 'rule_mime_part' . $id));
$select_mime_part->add(rcube::Q($this->plugin->gettext('mime-message')), '');
$select_mime_part->add(rcube::Q($this->plugin->gettext('mime-anychild')), 'anychild');
$mout .= '<div id="rule_mime_part' .$id. '" class="adv input-group"' . (!$need_mime ? ' style="display:none"' : '') . '>';
$mout .= html::span('label input-group-prepend', html::span('input-group-text', rcube::Q($this->plugin->gettext('mimepart'))));
$mout .= $select_mime_part->show(!empty($rule['mime-anychild']) ? 'anychild' : '');
$mout .= '</div>';
$mout .= '<div id="rule_mime' .$id. '" class="adv input-group"' . (!$need_mime ? ' style="display:none"' : '') . '>';
$mout .= html::span('label input-group-prepend', html::span('input-group-text', rcube::Q($this->plugin->gettext('mime'))));
$mout .= $select_mime->show($mime_type);
$mout .= $this->list_input($id, 'rule_mime_param', $rule['mime-param'], true,
$this->error_class($id, 'test', 'mime_param', 'rule_mime_param'), 30, $mime_type != 'param');
$mout .= '</div>';
}
// Advanced modifiers (body transformations)
$select_mod = new html_select(array('name' => "_rule_trans[$id]", 'id' => 'rule_trans_op'.$id,
'onchange' => 'rule_trans_select(' .$id .')'));
$select_mod->add(rcube::Q($this->plugin->gettext('text')), 'text');
$select_mod->add(rcube::Q($this->plugin->gettext('undecoded')), 'raw');
$select_mod->add(rcube::Q($this->plugin->gettext('contenttype')), 'content');
$mout .= '<div id="rule_trans' .$id. '" class="adv input-group"' . ($rule['test'] != 'body' ? ' style="display:none"' : '') . '>';
$mout .= html::span('label input-group-prepend', html::span('input-group-text', rcube::Q($this->plugin->gettext('modifier'))));
$mout .= $select_mod->show($rule['part']);
$mout .= html::tag('input', array(
'type' => 'text',
'name' => "_rule_trans_type[$id]",
'id' => 'rule_trans_type'.$id,
'value' => is_array($rule['content']) ? implode(',', $rule['content']) : $rule['content'],
'size' => 20,
'style' => $rule['part'] != 'content' ? 'display:none' : '',
'class' => $this->error_class($id, 'test', 'part', 'rule_trans_type'),
));
$mout .= '</div>';
// Date header
if (in_array('date', $this->exts)) {
$mout .= '<div id="rule_date_header_div' .$id. '" class="adv input-group"'. ($rule['test'] != 'date' ? ' style="display:none"' : '') .'>';
$mout .= html::span('label input-group-prepend', html::span('input-group-text', rcube::Q($this->plugin->gettext('dateheader'))));
$mout .= html::tag('input', array(
'type' => 'text',
'name' => "_rule_date_header[$id]",
'id' => 'rule_date_header' . $id,
'value' => $rule['test'] == 'date' ? $rule['header'] : '',
'size' => 15,
'class' => $this->error_class($id, 'test', 'dateheader', 'rule_date_header'),
));
$mout .= '</div>';
}
// Index
if (in_array('index', $this->exts)) {
$need_index = in_array($rule['test'], array('header', ', address', 'date'));
$mout .= '<div id="rule_index_div' .$id. '" class="adv input-group"'. (!$need_index ? ' style="display:none"' : '') .'>';
$mout .= html::span('label input-group-prepend', html::span('input-group-text', rcube::Q($this->plugin->gettext('index'))));
$mout .= html::tag('input', array(
'type' => 'text',
'name' => "_rule_index[$id]",
'id' => 'rule_index' . $id,
'value' => $rule['index'] ? intval($rule['index']) : '',
'size' => 3,
'class' => $this->error_class($id, 'test', 'index', 'rule_index'),
));
$mout .= html::label('input-group-append',
html::tag('input', array(
'type' => 'checkbox',
'name' => "_rule_index_last[$id]",
'id' => 'rule_index_last' . $id,
'value' => 1,
'checked' => !empty($rule['last']),
)) . rcube::Q($this->plugin->gettext('indexlast')));
$mout .= '</div>';
}
// Duplicate
if (in_array('duplicate', $this->exts)) {
$need_duplicate = $rule['test'] == 'duplicate';
$mout .= '<div id="rule_duplicate_div' .$id. '" class="adv"'. (!$need_duplicate ? ' style="display:none"' : '') .'>';
foreach (array('handle', 'header', 'uniqueid') as $unit) {
$mout .= '<div class="input-group">';
$mout .= html::span('label input-group-prepend', html::span('input-group-text', rcube::Q($this->plugin->gettext('duplicate.' . $unit))));
$mout .= html::tag('input', array(
'type' => 'text',
'name' => '_rule_duplicate_' . $unit . "[$id]",
'id' => 'rule_duplicate_' . $unit . $id,
'value' => $rule[$unit],
'size' => 30,
'class' => $this->error_class($id, 'test', 'duplicate_' . $unit, 'rule_duplicate_' . $unit),
));
$mout .= '</div>';
}
$mout .= '<div class="input-group">';
$mout .= html::span('label input-group-prepend', html::span('input-group-text', rcube::Q($this->plugin->gettext('duplicate.seconds'))));
$mout .= html::tag('input', array(
'type' => 'text',
'name' => "_rule_duplicate_seconds[$id]",
'id' => 'rule_duplicate_seconds' . $id,
'value' => $rule['seconds'],
'size' => 6,
'class' => $this->error_class($id, 'test', 'duplicate_seconds', 'rule_duplicate_seconds'),
));
$mout .= html::label('input-group-append',
html::tag('input', array(
'type' => 'checkbox',
'name' => '_rule_duplicate_last[' . $id . ']',
'id' => 'rule_duplicate_last' . $id,
'value' => 1,
'checked' => !empty($rule['last']),
)) . rcube::Q($this->plugin->gettext('duplicate.last')));
$mout .= '</div>';
$mout .= '</div>';
}
$add_title = rcube::Q($this->plugin->gettext('add'));
$del_title = rcube::Q($this->plugin->gettext('del'));
$adv_title = rcube::Q($this->plugin->gettext('advancedopts'));
// Build output table
$out = $div ? '<div class="rulerow" id="rulerow' .$id .'">'."\n" : '';
$out .= '<table class="compact-table"><tr>';
if (!$compact) {
$out .= '<td class="advbutton">';
$out .= sprintf('<a href="#" id="ruleadv%s" title="%s" onclick="rule_adv_switch(%s, this); return false" class="show">'
. '<span class="inner">%s</span></a>', $id, $adv_title, $id, $adv_title);
$out .= '</td>';
}
$out .= '<td class="rowactions"><div class="flexbox">' . $aout . '</div></td>';
$out .= '<td class="rowtargets">' . $tout . "\n";
$out .= '<div id="rule_advanced' .$id. '" style="display:none" class="advanced">' . $mout . '</div>';
$out .= '</td>';
$out .= '<td class="rowbuttons">';
if ($compact) {
$out .= sprintf('<a href="#" id="ruleadv%s" title="%s" onclick="rule_adv_switch(%s, this); return false" class="advanced show">'
. '<span class="inner">%s</span></a>', $id, $adv_title, $id, $adv_title);
}
$out .= sprintf('<a href="#" id="ruleadd%s" title="%s" onclick="rcmail.managesieve_ruleadd(\'%s\'); return false" class="button create add">'
. '<span class="inner">%s</span></a>', $id, $add_title, $id, $add_title);
$out .= sprintf('<a href="#" id="ruledel%s" title="%s" onclick="rcmail.managesieve_ruledel(\'%s\'); return false" class="button delete del%s">'
. '<span class="inner">%s</span></a>', $id, $del_title, $id, ($rows_num < 2 ? ' disabled' : ''), $del_title);
$out .= '</td>';
$out .= '</tr></table>';
$out .= $div ? "</div>\n" : '';
return $out;
}
private static function rule_test(&$rule)
{
// first modify value/count tests with 'not' keyword
// we'll revert the meaning of operators
if ($rule['not'] && preg_match('/^(count|value)-([gteqnl]{2})/', $rule['type'], $m)) {
$rule['not'] = false;
switch ($m[2]) {
case 'gt': $rule['type'] = $m[1] . '-le'; break;
case 'ge': $rule['type'] = $m[1] . '-lt'; break;
case 'lt': $rule['type'] = $m[1] . '-ge'; break;
case 'le': $rule['type'] = $m[1] . '-gt'; break;
case 'eq': $rule['type'] = $m[1] . '-ne'; break;
case 'ne': $rule['type'] = $m[1] . '-eq'; break;
}
}
else if ($rule['not'] && $rule['test'] == 'size') {
$rule['not'] = false;
$rule['type'] = $rule['type'] == 'over' ? 'under' : 'over';
}
$set = array('header', 'address', 'envelope', 'body', 'date', 'currentdate', 'string');
// build test string supported by select element
if ($rule['size']) {
$test = $rule['type'];
}
else if (in_array($rule['test'], $set)) {
$test = ($rule['not'] ? 'not' : '') . ($rule['type'] ?: 'is');
}
else {
$test = ($rule['not'] ? 'not' : '') . $rule['test'];
}
return $test;
}
function action_div($fid, $id, $div=true)
{
$action = isset($this->form) ? $this->form['actions'][$id] : $this->script[$fid]['actions'][$id];
if (isset($this->form['actions'])) {
$rows_num = count($this->form['actions']);
}
else if (isset($this->script[$fid]['actions'])) {
$rows_num = count($this->script[$fid]['actions']);
}
else {
$rows_num = 0;
}
$out = $div ? '<div class="actionrow" id="actionrow' .$id .'">'."\n" : '';
$out .= '<table class="compact-table"><tr><td class="rowactions">';
// action select
$select_action = new html_select(array('name' => "_action_type[$id]", 'id' => 'action_type'.$id,
'onchange' => 'action_type_select(' .$id .')'));
if (in_array('fileinto', $this->exts))
$select_action->add($this->plugin->gettext('messagemoveto'), 'fileinto');
if (in_array('fileinto', $this->exts) && in_array('copy', $this->exts))
$select_action->add($this->plugin->gettext('messagecopyto'), 'fileinto_copy');
$select_action->add($this->plugin->gettext('messageredirect'), 'redirect');
if (in_array('copy', $this->exts))
$select_action->add($this->plugin->gettext('messagesendcopy'), 'redirect_copy');
if (in_array('reject', $this->exts))
$select_action->add($this->plugin->gettext('messagediscard'), 'reject');
else if (in_array('ereject', $this->exts))
$select_action->add($this->plugin->gettext('messagediscard'), 'ereject');
if (in_array('vacation', $this->exts))
$select_action->add($this->plugin->gettext('messagereply'), 'vacation');
$select_action->add($this->plugin->gettext('messagedelete'), 'discard');
if (in_array('imapflags', $this->exts) || in_array('imap4flags', $this->exts)) {
$select_action->add($this->plugin->gettext('setflags'), 'setflag');
$select_action->add($this->plugin->gettext('addflags'), 'addflag');
$select_action->add($this->plugin->gettext('removeflags'), 'removeflag');
}
if (in_array('editheader', $this->exts)) {
$select_action->add($this->plugin->gettext('addheader'), 'addheader');
$select_action->add($this->plugin->gettext('deleteheader'), 'deleteheader');
}
if (in_array('variables', $this->exts)) {
$select_action->add($this->plugin->gettext('setvariable'), 'set');
}
if (in_array('enotify', $this->exts) || in_array('notify', $this->exts)) {
$select_action->add($this->plugin->gettext('notify'), 'notify');
}
$select_action->add($this->plugin->gettext('messagekeep'), 'keep');
$select_action->add($this->plugin->gettext('rulestop'), 'stop');
$select_type = $action['type'];
if (in_array($action['type'], array('fileinto', 'redirect')) && $action['copy']) {
$select_type .= '_copy';
}
$out .= $select_action->show($select_type);
$out .= '</td>';
// actions target inputs
$out .= '<td class="rowtargets">';
// force domain selection in redirect email input
$domains = (array) $this->rc->config->get('managesieve_domains');
if (!empty($domains)) {
sort($domains);
$domain_select = new html_select(array('name' => "_action_target_domain[$id]", 'id' => 'action_target_domain'.$id));
$domain_select->add(array_combine($domains, $domains));
if ($action['type'] == 'redirect') {
$parts = explode('@', $action['target']);
if (!empty($parts)) {
$action['domain'] = array_pop($parts);
$action['target'] = implode('@', $parts);
}
}
}
// redirect target
$out .= '<span id="redirect_target' . $id . '" style="white-space:nowrap;'
. ' display:' . ($action['type'] == 'redirect' ? 'inline' : 'none') . '">'
. html::tag('input', array(
'type' => 'text',
'name' => '_action_target[' . $id . ']',
'id' => 'action_target' . $id,
'value' => $action['type'] == 'redirect' ? $action['target'] : '',
'size' => !empty($domains) ? 20 : 35,
'class' => $this->error_class($id, 'action', 'target', 'action_target'),
));
$out .= !empty($domains) ? ' @ ' . $domain_select->show($action['domain']) : '';
$out .= '</span>';
// (e)reject target
$out .= html::tag('textarea', array(
'name' => '_action_target_area[' . $id . ']',
'id' => 'action_target_area' . $id,
'rows' => 3,
'cols' => 35,
'class' => $this->error_class($id, 'action', 'targetarea', 'action_target_area'),
'style' => 'display:' . (in_array($action['type'], array('reject', 'ereject')) ? 'inline' : 'none'),
), (in_array($action['type'], array('reject', 'ereject')) ? rcube::Q($action['target'], 'strict', false) : ''));
// vacation
$vsec = in_array('vacation-seconds', $this->exts);
$auto_addr = $this->rc->config->get('managesieve_vacation_addresses_init');
$from_addr = $this->rc->config->get('managesieve_vacation_from_init');
if (empty($action)) {
if ($auto_addr) {
$action['addresses'] = $this->user_emails();
}
if ($from_addr) {
$default_identity = $this->rc->user->list_emails(true);
$action['from'] = $default_identity['email'];
}
}
$out .= '<div id="action_vacation' .$id.'" style="display:' .($action['type']=='vacation' ? 'inline' : 'none') .'" class="composite">';
$out .= '<span class="label">'. rcube::Q($this->plugin->gettext('vacationreason')) .'</span><br>';
$out .= html::tag('textarea', array(
'name' => '_action_reason[' . $id . ']',
'id' => 'action_reason' . $id,
'rows' => 3,
'cols' => 35,
'class' => $this->error_class($id, 'action', 'reason', 'action_reason'),
), rcube::Q($action['reason'], 'strict', false));
$out .= '<br><span class="label">' .rcube::Q($this->plugin->gettext('vacationsubject')) . '</span><br>';
$out .= html::tag('input', array(
'type' => 'text',
'name' => '_action_subject[' . $id . ']',
'id' => 'action_subject' . $id,
'value' => is_array($action['subject']) ? implode(', ', $action['subject']) : $action['subject'],
'size' => 35,
'class' => $this->error_class($id, 'action', 'subject', 'action_subject'),
));
$out .= '<br><span class="label">' .rcube::Q($this->plugin->gettext('vacationfrom')) . '</span><br>';
$out .= html::tag('input', array(
'type' => 'text',
'name' => '_action_from[' . $id . ']',
'id' => 'action_from' . $id,
'value' => $action['from'],
'size' => 35,
'class' => $this->error_class($id, 'action', 'from', 'action_from'),
));
$out .= '<br><span class="label">' .rcube::Q($this->plugin->gettext('vacationaddr')) . '</span><br>';
$out .= $this->list_input($id, 'action_addresses', $action['addresses'], true,
$this->error_class($id, 'action', 'addresses', 'action_addresses'), 30)
. html::a(array('href' => '#', 'onclick' => rcmail_output::JS_OBJECT_NAME . ".managesieve_vacation_addresses($id)"),
rcube::Q($this->plugin->gettext('filladdresses')));
$out .= '<br><span class="label">' . rcube::Q($this->plugin->gettext('vacationinterval')) . '</span><br>';
$out .= '<div class="input-group">' . html::tag('input', array(
'type' => 'text',
'name' => '_action_interval[' . $id . ']',
'id' => 'action_interval' . $id,
'value' => rcube_sieve_vacation::vacation_interval($action),
'size' => 2,
'class' => $this->error_class($id, 'action', 'interval', 'action_interval'),
));
if ($vsec) {
$interval_select = new html_select(array('name' => '_action_interval_type[' . $id . ']', 'class' => 'input-group-append'));
$interval_select->add($this->plugin->gettext('days'), 'days');
$interval_select->add($this->plugin->gettext('seconds'), 'seconds');
$out .= $interval_select->show(isset($action['seconds']) ? 'seconds' : 'days');
}
else {
$out .= "\n" . html::span('input-group-append', html::span('input-group-text', $this->plugin->gettext('days')));
}
$out .= '</div></div>';
// flags
$flags = array(
'read' => '\\Seen',
'answered' => '\\Answered',
'flagged' => '\\Flagged',
'deleted' => '\\Deleted',
'draft' => '\\Draft',
);
$flags_target = (array) $action['target'];
$custom_flags = array();
$is_flag_action = preg_match('/^(set|add|remove)flag$/', $action['type']);
if ($is_flag_action) {
$custom_flags = array_filter($flags_target, function($v) use($flags) {
return !in_array_nocase($v, $flags);
});
}
$flout = '';
foreach ($flags as $fidx => $flag) {
$flout .= html::label(null, html::tag('input', array(
'type' => 'checkbox',
'name' => "_action_flags[$id][]",
'value' => $flag,
'checked' => $is_flag_action && in_array_nocase($flag, $flags_target),
))
. rcube::Q($this->plugin->gettext('flag'.$fidx))) . '<br>';
}
$flout .= $this->list_input($id, 'action_flags', $custom_flags, true,
$this->error_class($id, 'action', 'flags', 'action_flags'));
$out .= html::div(array(
'id' => 'action_flags' . $id,
'style' => 'display:' . ($is_flag_action ? 'inline' : 'none'),
'class' => trim('checklist ' . $this->error_class($id, 'action', 'flags', 'action_flags')),
), $flout);
// set variable
$set_modifiers = array(
'lower',
'upper',
'lowerfirst',
'upperfirst',
'quotewildcard',
'length'
);
$out .= '<div id="action_set' .$id.'" style="display:' .($action['type']=='set' ? 'inline' : 'none') .'">';
foreach (array('name', 'value') as $unit) {
$out .= '<span class="label">' .rcube::Q($this->plugin->gettext('setvar' . $unit)) . '</span><br>';
$out .= html::tag('input', array(
'type' => 'text',
'name' => '_action_var' . $unit . '[' . $id . ']',
'id' => 'action_var' . $unit . $id,
'value' => $action[$unit],
'size' => 35,
'class' => $this->error_class($id, 'action', $unit, 'action_var' . $unit),
));
$out .= '<br>';
}
$out .= '<span class="label">' .rcube::Q($this->plugin->gettext('setvarmodifiers')) . '</span>';
foreach ($set_modifiers as $s_m) {
$s_m_id = 'action_varmods' . $id . $s_m;
$out .= '<br>' . html::tag('input', array(
'type' => 'checkbox',
'name' => "_action_varmods[$id][]",
'value' => $s_m,
'id' => $s_m_id,
'checked' => array_key_exists($s_m, (array)$action) && $action[$s_m],
))
.rcube::Q($this->plugin->gettext('var' . $s_m));
}
$out .= '</div>';
// notify
$notify_methods = (array) $this->rc->config->get('managesieve_notify_methods');
$importance_options = $this->notify_importance_options;
if (empty($notify_methods)) {
$notify_methods = $this->notify_methods;
}
list($method, $target) = explode(':', $action['method'], 2);
$method = strtolower($method);
if ($method && !in_array($method, $notify_methods)) {
$notify_methods[] = $method;
}
$select_method = new html_select(array(
'name' => "_action_notifymethod[$id]",
'id' => "_action_notifymethod$id",
'class' => 'input-group-prepend ' . $this->error_class($id, 'action', 'method', 'action_notifymethod'),
));
foreach ($notify_methods as $m_n) {
$select_method->add(rcube::Q($this->rc->text_exists('managesieve.notifymethod'.$m_n) ? $this->plugin->gettext('managesieve.notifymethod'.$m_n) : $m_n), $m_n);
}
$select_importance = new html_select(array(
'name' => "_action_notifyimportance[$id]",
'id' => "_action_notifyimportance$id",
'class' => $this->error_class($id, 'action', 'importance', 'action_notifyimportance')
));
foreach ($importance_options as $io_v => $io_n) {
$select_importance->add(rcube::Q($this->plugin->gettext($io_n)), $io_v);
}
// @TODO: nice UI for mailto: (other methods too) URI parameters
$out .= '<div id="action_notify' .$id.'" style="display:' .($action['type'] == 'notify' ? 'inline' : 'none') .'" class="composite">';
$out .= '<span class="label">' .rcube::Q($this->plugin->gettext('notifytarget')) . '</span><br>';
$out .= '<div class="input-group">';
$out .= $select_method->show($method);
$out .= html::tag('input', array(
'type' => 'text',
'name' => '_action_notifytarget[' . $id . ']',
'id' => 'action_notifytarget' . $id,
'value' => $target,
'size' => 25,
'class' => $this->error_class($id, 'action', 'target', 'action_notifytarget'),
));
$out .= '</div>';
$out .= '<br><span class="label">'. rcube::Q($this->plugin->gettext('notifymessage')) .'</span><br>';
$out .= html::tag('textarea', array(
'name' => '_action_notifymessage[' . $id . ']',
'id' => 'action_notifymessage' . $id,
'rows' => 3,
'cols' => 35,
'class' => $this->error_class($id, 'action', 'message', 'action_notifymessage'),
), rcube::Q($action['message'], 'strict', false));
if (in_array('enotify', $this->exts)) {
$out .= '<br><span class="label">' .rcube::Q($this->plugin->gettext('notifyfrom')) . '</span><br>';
$out .= html::tag('input', array(
'type' => 'text',
'name' => '_action_notifyfrom[' . $id . ']',
'id' => 'action_notifyfrom' . $id,
'value' => $action['from'],
'size' => 35,
'class' => $this->error_class($id, 'action', 'from', 'action_notifyfrom'),
));
}
$out .= '<br><span class="label">' . rcube::Q($this->plugin->gettext('notifyimportance')) . '</span><br>';
$out .= $select_importance->show($action['importance'] ? (int) $action['importance'] : 2);
$out .= '<div id="action_notifyoption_div' . $id . '">'
.'<span class="label">' . rcube::Q($this->plugin->gettext('notifyoptions')) . '</span><br>'
.$this->list_input($id, 'action_notifyoption', (array)$action['options'], true,
$this->error_class($id, 'action', 'options', 'action_notifyoption'), 30) . '</div>';
$out .= '</div>';
if (in_array('editheader', $this->exts)) {
$action['pos'] = $action['last'] ? 'last' : '';
$pos1_selector = new html_select(array(
'name' => "_action_addheader_pos[$id]",
'id' => "action_addheader_pos$id",
'class' => $this->error_class($id, 'action', 'pos', 'action_addheader_pos')
));
$pos1_selector->add($this->plugin->gettext('headeratstart'), '');
$pos1_selector->add($this->plugin->gettext('headeratend'), 'last');
$pos2_selector = new html_select(array(
'name' => "_action_delheader_pos[$id]",
'id' => "action_delheader_pos$id",
'class' => $this->error_class($id, 'action', 'pos', 'action_delheader_pos')
));
$pos2_selector->add($this->plugin->gettext('headerfromstart'), '');
$pos2_selector->add($this->plugin->gettext('headerfromend'), 'last');
// addheader
$out .= '<div id="action_addheader' .$id.'" style="display:' .($action['type'] == 'addheader' ? 'inline' : 'none') .'" class="composite">';
$out .= '<label class="label" for="action_addheader_name' . $id .'">' .rcube::Q($this->plugin->gettext('headername')) . '</label><br>';
$out .= html::tag('input', array(
'type' => 'text',
'name' => '_action_addheader_name[' . $id . ']',
'id' => 'action_addheader_name' . $id,
'value' => $action['name'],
'size' => 35,
'class' => $this->error_class($id, 'action', 'name', 'action_addheader_name'),
));
$out .= '<br><label class="label" for="action_addheader_value' . $id .'">'. rcube::Q($this->plugin->gettext('headervalue')) .'</label><br>';
$out .= html::tag('input', array(
'type' => 'text',
'name' => '_action_addheader_value[' . $id . ']',
'id' => 'action_addheader_value' . $id,
'value' => $action['value'],
'size' => 35,
'class' => $this->error_class($id, 'action', 'value', 'action_addheader_value'),
));
$out .= '<br><label class="label" for="action_addheader_pos' . $id .'">'. rcube::Q($this->plugin->gettext('headerpos')) .'</label><br>';
$out .= $pos1_selector->show($action['pos']);
$out .= '</div>';
// deleteheader
$out .= '<div id="action_deleteheader' .$id.'" style="display:' .($action['type'] == 'deleteheader' ? 'inline' : 'none') .'" class="composite">';
$out .= '<label class="label" for="action_delheader_name' . $id .'">' .rcube::Q($this->plugin->gettext('headername')) . '</label><br>';
$out .= html::tag('input', array(
'type' => 'text',
'name' => '_action_delheader_name[' . $id . ']',
'id' => 'action_delheader_name' . $id,
'value' => $action['name'],
'size' => 35,
'class' => $this->error_class($id, 'action', 'name', 'action_delheader_name'),
));
$out .= '<br><label class="label" for="action_delheader_value' . $id .'">'. rcube::Q($this->plugin->gettext('headerpatterns')) .'</label><br>';
$out .= $this->list_input($id, 'action_delheader_value', $action['value'], true,
$this->error_class($id, 'action', 'value', 'action_delheader_value')) . "\n";
$out .= '<br><div class="adv input-group">';
$out .= html::span('label input-group-prepend', html::label(array(
'class' => 'input-group-text', 'for' => 'action_delheader_op'.$id), rcube::Q($this->plugin->gettext('headermatchtype'))));
$out .= $this->match_type_selector('action_delheader_op', $id, $action['match-type'], null, 'basic');
$out .= '</div>';
$out .= '<div class="adv input-group">';
$out .= html::span('label input-group-prepend', html::label(array(
'class' => 'input-group-text', 'for' => 'action_delheader_comp_op'.$id), rcube::Q($this->plugin->gettext('comparator'))));
$out .= $this->comparator_selector($action['comparator'], 'action_delheader_comp', $id);
$out .= '</div>';
$out .= '<br><label class="label" for="action_delheader_index' . $id .'">'. rcube::Q($this->plugin->gettext('headeroccurrence')) .'</label><br>';
$out .= '<div class="input-group">';
$out .= html::tag('input', array(
'type' => 'text',
'name' => '_action_delheader_index[' . $id . ']',
'id' => 'action_delheader_index' . $id,
'value' => $action['index'] ? intval($action['index']) : '',
'size' => 5,
'class' => $this->error_class($id, 'action', 'index', 'action_delheader_index'),
));
$out .= ' ' . $pos2_selector->show($action['pos']);
$out .= '</div></div>';
}
// mailbox select
if ($action['type'] == 'fileinto') {
// make sure non-existing (or unsubscribed) mailbox is listed (#1489956)
if ($mailbox = $this->mod_mailbox($action['target'], 'out')) {
$additional = array($mailbox);
}
}
else {
$mailbox = '';
}
$select = $this->rc->folder_selector(array(
'maxlength' => 100,
'id' => 'action_mailbox' . $id,
'name' => "_action_mailbox[$id]",
'style' => 'display:'.(empty($action['type']) || $action['type'] == 'fileinto' ? 'inline' : 'none'),
'additional' => $additional,
));
$out .= $select->show($mailbox);
$out .= '</td>';
// add/del buttons
$add_label = rcube::Q($this->plugin->gettext('add'));
$del_label = rcube::Q($this->plugin->gettext('del'));
$out .= '<td class="rowbuttons">';
$out .= sprintf('<a href="#" id="actionadd%s" title="%s" onclick="rcmail.managesieve_actionadd(%s)" class="button create add">'
. '<span class="inner">%s</span></a>', $id, $add_label, $id, $add_label);
$out .= sprintf('<a href="#" id="actiondel%s" title="%s" onclick="rcmail.managesieve_actiondel(%s)" class="button delete del%s">'
. '<span class="inner">%s</span></a>', $id, $del_label, $id, ($rows_num < 2 ? ' disabled' : ''), $del_label);
$out .= '</td>';
$out .= '</tr></table>';
$out .= $div ? "</div>\n" : '';
return $out;
}
protected function genid()
{
return preg_replace('/[^0-9]/', '', microtime(true));
}
protected function strip_value($str, $allow_html = false, $trim = true)
{
if (is_array($str)) {
foreach ($str as $idx => $val) {
$val = $this->strip_value($val, $allow_html, $trim);
if ($val === '') {
unset($str[$idx]);
}
}
return $str;
}
if (!$allow_html) {
$str = strip_tags($str);
}
return $trim ? trim($str) : $str;
}
protected function error_class($id, $type, $target, $elem_prefix='')
{
// TODO: tooltips
if (($type == 'test' && ($str = $this->errors['tests'][$id][$target])) ||
($type == 'action' && ($str = $this->errors['actions'][$id][$target]))
) {
$this->add_tip($elem_prefix.$id, $str, true);
return 'error';
}
return '';
}
protected function add_tip($id, $str, $error=false)
{
if ($error) {
$class = 'sieve error';
}
$this->tips[] = array($id, $class ?: '', $str);
}
protected function print_tips()
{
if (empty($this->tips)) {
return;
}
$script = rcmail_output::JS_OBJECT_NAME.'.managesieve_tip_register('.json_encode($this->tips).');';
$this->rc->output->add_script($script, 'docready');
}
protected function list_input($id, $name, $value, $enabled, $class, $size = null, $hidden = false)
{
$value = (array) $value;
$value = array_map(array('rcube', 'Q'), $value);
$value = implode("\n", $value);
return html::tag('textarea', array(
'data-type' => 'list',
'data-size' => $size,
'data-hidden' => $hidden ?: null,
'name' => '_' . $name . '[' . $id . ']',
'id' => $name.$id,
'disabled' => !$enabled,
'class' => $class,
'style' => 'display:none',
), $value);
}
/**
* Validate input for date part elements
*/
protected function validate_date_part($type, $value)
{
// we do simple validation of date/part format
switch ($type) {
case 'date': // yyyy-mm-dd
return preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/', $value);
case 'iso8601':
return preg_match('/^[0-9: .,ZWT+-]+$/', $value);
case 'std11':
return preg_match('/^((Sun|Mon|Tue|Wed|Thu|Fri|Sat),\s+)?[0-9]{1,2}\s+'
. '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+[0-9]{2,4}\s+'
. '[0-9]{2}:[0-9]{2}(:[0-9]{2})?\s+([+-]*[0-9]{4}|[A-Z]{1,3})$', $value);
case 'julian':
return preg_match('/^[0-9]+$/', $value);
case 'time': // hh:mm:ss
return preg_match('/^[0-9]{2}:[0-9]{2}:[0-9]{2}$/', $value);
case 'year':
return preg_match('/^[0-9]{4}$/', $value);
case 'month':
return preg_match('/^[0-9]{2}$/', $value) && $value > 0 && $value < 13;
case 'day':
return preg_match('/^[0-9]{2}$/', $value) && $value > 0 && $value < 32;
case 'hour':
return preg_match('/^[0-9]{2}$/', $value) && $value < 24;
case 'minute':
return preg_match('/^[0-9]{2}$/', $value) && $value < 60;
case 'second':
// According to RFC5260, seconds can be from 00 to 60
return preg_match('/^[0-9]{2}$/', $value) && $value < 61;
case 'weekday':
return preg_match('/^[0-9]$/', $value) && $value < 7;
case 'zone':
return preg_match('/^[+-][0-9]{4}$/', $value);
}
}
/**
* Converts mailbox name from/to UTF7-IMAP from/to internal Sieve encoding
* with delimiter replacement.
*
* @param string $mailbox Mailbox name
* @param string $mode Conversion direction ('in'|'out')
*
* @return string Mailbox name
*/
protected function mod_mailbox($mailbox, $mode = 'out')
{
$delimiter = $_SESSION['imap_delimiter'];
$replace_delimiter = $this->rc->config->get('managesieve_replace_delimiter');
$mbox_encoding = $this->rc->config->get('managesieve_mbox_encoding', 'UTF7-IMAP');
if ($mode == 'out') {
$mailbox = rcube_charset::convert($mailbox, $mbox_encoding, 'UTF7-IMAP');
if ($replace_delimiter && $replace_delimiter != $delimiter)
$mailbox = str_replace($replace_delimiter, $delimiter, $mailbox);
}
else {
$mailbox = rcube_charset::convert($mailbox, 'UTF7-IMAP', $mbox_encoding);
if ($replace_delimiter && $replace_delimiter != $delimiter)
$mailbox = str_replace($delimiter, $replace_delimiter, $mailbox);
}
return $mailbox;
}
/**
* List sieve scripts
*
* @return array Scripts list
*/
public function list_scripts()
{
if ($this->list !== null) {
return $this->list;
}
$this->list = $this->sieve->get_scripts();
// Handle active script(s) and list of scripts according to Kolab's KEP:14
if ($this->rc->config->get('managesieve_kolab_master')) {
// Skip protected names
foreach ((array)$this->list as $idx => $name) {
$_name = strtoupper($name);
if ($_name == 'MASTER')
$master_script = $name;
else if ($_name == 'MANAGEMENT')
$management_script = $name;
else if($_name == 'USER')
$user_script = $name;
else
continue;
unset($this->list[$idx]);
}
// get active script(s), read USER script
if ($user_script) {
$extension = $this->rc->config->get('managesieve_filename_extension', '.sieve');
$filename_regex = '/'.preg_quote($extension, '/').'$/';
$_SESSION['managesieve_user_script'] = $user_script;
$this->sieve->load($user_script);
foreach ($this->sieve->script->as_array() as $rules) {
foreach ($rules['actions'] as $action) {
if ($action['type'] == 'include' && empty($action['global'])) {
$name = preg_replace($filename_regex, '', $action['target']);
// make sure the script exist
if (in_array($name, $this->list)) {
$this->active[] = $name;
}
}
}
}
}
// create USER script if it doesn't exist
else {
$content = "# USER Management Script\n"
."#\n"
."# This script includes the various active sieve scripts\n"
."# it is AUTOMATICALLY GENERATED. DO NOT EDIT MANUALLY!\n"
."#\n"
."# For more information, see http://wiki.kolab.org/KEP:14#USER\n"
."#\n";
if ($this->sieve->save_script('USER', $content)) {
$_SESSION['managesieve_user_script'] = 'USER';
if (empty($this->master_file))
$this->sieve->activate('USER');
}
}
}
else if (!empty($this->list)) {
// Get active script name
if ($active = $this->sieve->get_active()) {
$this->active = array($active);
}
// Hide scripts from config
$exceptions = $this->rc->config->get('managesieve_filename_exceptions');
if (!empty($exceptions)) {
$this->list = array_diff($this->list, (array)$exceptions);
}
}
// When no script listing allowed limit the list to the defined script
if (in_array('list_sets', $this->disabled_actions)) {
$script_name = $this->rc->config->get('managesieve_script_name', 'roundcube');
$this->list = array_intersect($this->list, array($script_name));
$this->active = null;
if (in_array($script_name, $this->list)) {
// Because its the only allowed script make sure its active
$this->activate_script($script_name);
}
}
// reindex
if (!empty($this->list)) {
$this->list = array_values($this->list);
}
return $this->list;
}
/**
* Removes sieve script
*
* @param string $name Script name
*
* @return bool True on success, False on failure
*/
public function remove_script($name)
{
$result = $this->sieve->remove($name);
// Kolab's KEP:14
if ($result && $this->rc->config->get('managesieve_kolab_master')) {
$this->deactivate_script($name);
}
return $result;
}
/**
* Activates sieve script
*
* @param string $name Script name
*
* @return bool True on success, False on failure
*/
public function activate_script($name)
{
// Kolab's KEP:14
if ($this->rc->config->get('managesieve_kolab_master')) {
$extension = $this->rc->config->get('managesieve_filename_extension', '.sieve');
$user_script = $_SESSION['managesieve_user_script'];
// if the script is not active...
if ($user_script && array_search($name, (array) $this->active) === false) {
// ...rewrite USER file adding appropriate include command
if ($this->sieve->load($user_script)) {
$script = $this->sieve->script->as_array();
$list = array();
$regexp = '/' . preg_quote($extension, '/') . '$/';
// Create new include entry
$rule = array(
'actions' => array(
0 => array(
'target' => $name.$extension,
'type' => 'include',
'personal' => true,
)));
// get all active scripts for sorting
foreach ($script as $rid => $rules) {
foreach ($rules['actions'] as $action) {
if ($action['type'] == 'include' && empty($action['global'])) {
$target = $extension ? preg_replace($regexp, '', $action['target']) : $action['target'];
$list[] = $target;
}
}
}
$list[] = $name;
// Sort and find current script position
asort($list, SORT_LOCALE_STRING);
$list = array_values($list);
$index = array_search($name, $list);
// add rule at the end of the script
if ($index === false || $index == count($list)-1) {
$this->sieve->script->add_rule($rule);
}
// add rule at index position
else {
$script2 = array();
foreach ($script as $rid => $rules) {
if ($rid == $index) {
$script2[] = $rule;
}
$script2[] = $rules;
}
$this->sieve->script->content = $script2;
}
$result = $this->sieve->save();
if ($result) {
$this->active[] = $name;
}
}
}
}
else {
$result = $this->sieve->activate($name);
if ($result)
$this->active = array($name);
}
return $result;
}
/**
* Deactivates sieve script
*
* @param string $name Script name
*
* @return bool True on success, False on failure
*/
public function deactivate_script($name)
{
// Kolab's KEP:14
if ($this->rc->config->get('managesieve_kolab_master')) {
$extension = $this->rc->config->get('managesieve_filename_extension', '.sieve');
$user_script = $_SESSION['managesieve_user_script'];
// if the script is active...
if ($user_script && ($key = array_search($name, $this->active)) !== false) {
// ...rewrite USER file removing appropriate include command
if ($this->sieve->load($user_script)) {
$script = $this->sieve->script->as_array();
$name = $name.$extension;
foreach ($script as $rid => $rules) {
foreach ($rules['actions'] as $action) {
if ($action['type'] == 'include' && empty($action['global'])
&& $action['target'] == $name
) {
break 2;
}
}
}
// Entry found
if ($rid < count($script)) {
$this->sieve->script->delete_rule($rid);
$result = $this->sieve->save();
if ($result) {
unset($this->active[$key]);
}
}
}
}
}
else {
$result = $this->sieve->deactivate();
if ($result)
$this->active = array();
}
return $result;
}
/**
* Saves current script (adding some variables)
*/
public function save_script($name = null)
{
// Kolab's KEP:14
if ($this->rc->config->get('managesieve_kolab_master')) {
$this->sieve->script->set_var('EDITOR', self::PROGNAME);
$this->sieve->script->set_var('EDITOR_VERSION', self::VERSION);
}
return $this->sieve->save($name);
}
/**
* Returns list of rules from the current script
*
* @return array List of rules
*/
public function list_rules()
{
$result = array();
$i = 1;
foreach ($this->script as $idx => $filter) {
if (empty($filter['actions'])) {
continue;
}
$fname = $filter['name'] ?: "#$i";
$result[] = array(
'id' => $idx,
'name' => $fname,
'class' => $filter['disabled'] ? 'disabled' : '',
);
$i++;
}
return $result;
}
/**
* Initializes internal script data
*/
protected function init_script()
{
if (!$this->sieve->script) {
return;
}
$this->script = $this->sieve->script->as_array();
$headers = array();
$exceptions = array('date', 'currentdate', 'size', 'body');
// find common headers used in script, will be added to the list
// of available (predefined) headers (#1489271)
foreach ($this->script as $rule) {
foreach ((array) $rule['tests'] as $test) {
if ($test['test'] == 'header') {
foreach ((array) $test['arg1'] as $header) {
$lc_header = strtolower($header);
// skip special names to not confuse UI
if (in_array($lc_header, $exceptions)) {
continue;
}
if (!isset($this->headers[$lc_header]) && !isset($headers[$lc_header])) {
$headers[$lc_header] = $header;
}
}
}
}
}
ksort($headers);
$this->headers += $headers;
}
/**
* Get all e-mail addresses of the user
*/
protected function user_emails()
{
$addresses = $this->rc->user->list_emails();
foreach ($addresses as $idx => $email) {
$addresses[$idx] = $email['email'];
}
$addresses = array_unique($addresses);
sort($addresses);
return $addresses;
}
/**
* Convert configured default headers into internal format
*/
protected function get_default_headers()
{
$default = array('Subject', 'From', 'To');
$headers = (array) $this->rc->config->get('managesieve_default_headers', $default);
$keys = array_map('strtolower', $headers);
$headers = array_combine($keys, $headers);
// make sure there's no Date header
unset($headers['date']);
return $headers;
}
/**
* Match type selector
*/
protected function match_type_selector($name, $id, $test, $rule = null, $mode = 'all')
{
// matching type select (operator)
$select_op = new html_select(array(
'name' => "_{$name}[$id]",
'id' => "{$name}{$id}",
'style' => 'display:' .(!in_array($rule, array('size', 'duplicate')) ? 'inline' : 'none'),
'class' => 'operator_selector',
'onchange' => "{$name}_select(this, '{$id}')",
));
$select_op->add(rcube::Q($this->plugin->gettext('filtercontains')), 'contains');
$select_op->add(rcube::Q($this->plugin->gettext('filternotcontains')), 'notcontains');
$select_op->add(rcube::Q($this->plugin->gettext('filteris')), 'is');
$select_op->add(rcube::Q($this->plugin->gettext('filterisnot')), 'notis');
if ($mode == 'all') {
$select_op->add(rcube::Q($this->plugin->gettext('filterexists')), 'exists');
$select_op->add(rcube::Q($this->plugin->gettext('filternotexists')), 'notexists');
}
$select_op->add(rcube::Q($this->plugin->gettext('filtermatches')), 'matches');
$select_op->add(rcube::Q($this->plugin->gettext('filternotmatches')), 'notmatches');
if (in_array('regex', $this->exts)) {
$select_op->add(rcube::Q($this->plugin->gettext('filterregex')), 'regex');
$select_op->add(rcube::Q($this->plugin->gettext('filternotregex')), 'notregex');
}
if ($mode == 'all' && in_array('relational', $this->exts)) {
$select_op->add(rcube::Q($this->plugin->gettext('countisgreaterthan')), 'count-gt');
$select_op->add(rcube::Q($this->plugin->gettext('countisgreaterthanequal')), 'count-ge');
$select_op->add(rcube::Q($this->plugin->gettext('countislessthan')), 'count-lt');
$select_op->add(rcube::Q($this->plugin->gettext('countislessthanequal')), 'count-le');
$select_op->add(rcube::Q($this->plugin->gettext('countequals')), 'count-eq');
$select_op->add(rcube::Q($this->plugin->gettext('countnotequals')), 'count-ne');
$select_op->add(rcube::Q($this->plugin->gettext('valueisgreaterthan')), 'value-gt');
$select_op->add(rcube::Q($this->plugin->gettext('valueisgreaterthanequal')), 'value-ge');
$select_op->add(rcube::Q($this->plugin->gettext('valueislessthan')), 'value-lt');
$select_op->add(rcube::Q($this->plugin->gettext('valueislessthanequal')), 'value-le');
$select_op->add(rcube::Q($this->plugin->gettext('valueequals')), 'value-eq');
$select_op->add(rcube::Q($this->plugin->gettext('valuenotequals')), 'value-ne');
}
return $select_op->show($test);
}
protected function comparator_selector($comparator, $name, $id)
{
$select_comp = new html_select(array('name' => "_{$name}[$id]", 'id' => "{$name}_op{$id}"));
$select_comp->add(rcube::Q($this->plugin->gettext('default')), '');
$select_comp->add(rcube::Q($this->plugin->gettext('octet')), 'i;octet');
$select_comp->add(rcube::Q($this->plugin->gettext('asciicasemap')), 'i;ascii-casemap');
if (in_array('comparator-i;ascii-numeric', $this->exts)) {
$select_comp->add(rcube::Q($this->plugin->gettext('asciinumeric')), 'i;ascii-numeric');
}
return $select_comp->show($comparator);
}
}
diff --git a/plugins/managesieve/lib/Roundcube/rcube_sieve_forward.php b/plugins/managesieve/lib/Roundcube/rcube_sieve_forward.php
index 967cb1622..db1c1b653 100644
--- a/plugins/managesieve/lib/Roundcube/rcube_sieve_forward.php
+++ b/plugins/managesieve/lib/Roundcube/rcube_sieve_forward.php
@@ -1,517 +1,517 @@
<?php
/**
* Managesieve Forward Engine
*
* Engine part of Managesieve plugin implementing UI and backend access.
*
- * Copyright (C) 2011-2017, Kolab Systems AG
+ * Copyright (C) Kolab Systems AG
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_sieve_forward extends rcube_sieve_engine
{
protected $error;
protected $script_name;
protected $forward = array();
function actions()
{
$error = $this->start('forward');
// find current forward rule
if (!$error) {
$this->forward_rule();
$this->forward_post();
}
$this->plugin->add_label('forward.saving');
$this->rc->output->add_handlers(array(
'forwardform' => array($this, 'forward_form'),
));
$this->rc->output->set_pagetitle($this->plugin->gettext('forward'));
$this->rc->output->send('managesieve.forward');
}
/**
* Find and load sieve script with/for forward rule
*
* @param string $script_name Optional script name
*
* @return int Connection status: 0 on success, >0 on failure
*/
protected function load_script($script_name = null)
{
if ($this->script_name !== null) {
return 0;
}
$list = $this->list_scripts();
$master = $this->rc->config->get('managesieve_kolab_master');
$included = array();
$this->script_name = false;
// first try the active script(s)...
if (!empty($this->active)) {
// Note: there can be more than one active script on KEP:14-enabled server
foreach ($this->active as $script) {
if ($this->sieve->load($script)) {
foreach ($this->sieve->script->as_array() as $rule) {
if (!empty($rule['actions'])) {
if ($rule['actions'][0]['type'] == 'redirect') {
$this->script_name = $script;
return 0;
}
else if (empty($master) && $rule['actions'][0]['type'] == 'include') {
$included[] = $rule['actions'][0]['target'];
}
}
}
}
}
// ...else try scripts included in active script (not for KEP:14)
foreach ($included as $script) {
if ($this->sieve->load($script)) {
foreach ($this->sieve->script->as_array() as $rule) {
if (!empty($rule['actions']) && $rule['actions'][0]['type'] == 'redirect') {
$this->script_name = $script;
return 0;
}
}
}
}
}
// try all other scripts
if (!empty($list)) {
// else try included scripts
foreach (array_diff($list, $included, $this->active) as $script) {
if ($this->sieve->load($script)) {
foreach ($this->sieve->script->as_array() as $rule) {
if (!empty($rule['actions']) && $rule['actions'][0]['type'] == 'redirect') {
$this->script_name = $script;
return 0;
}
}
}
}
// none of the scripts contains existing forward rule
// use any (first) active or just existing script (in that order)
if (!empty($this->active)) {
$this->sieve->load($this->script_name = $this->active[0]);
}
else {
$this->sieve->load($this->script_name = $list[0]);
}
}
return $this->sieve->error();
}
private function forward_rule()
{
if ($this->script_name === false || $this->script_name === null || !$this->sieve->load($this->script_name)) {
return;
}
$list = array();
$active = in_array($this->script_name, $this->active);
// find (first) simple forward rule that can be expressed with the minimal settings
foreach ($this->script as $idx => $rule) {
if (empty($this->forward) && !empty($rule['actions']) && $rule['actions'][0]['type'] == 'redirect') {
$ignore_rule = false;
$target = null;
$stop_found = false;
foreach ($rule['actions'] as $act) {
if ($stop_found) {
// we might loose information if there rules after the stop
$ignore_rule = true;
}
if ($act['type'] == 'keep') {
$action = 'copy';
}
else if ($act['type'] == 'stop') {
// we might loose information if there rules after the stop
$stop_found = true;
}
else if ($act['type'] == 'discard') {
$action = 'redirect';
}
else if ($act['type'] == 'redirect') {
if (!empty($target)) {
// we cannot use this rule, because there are multiple targets
$ignore_rule = true;
}
else {
$action = $act['copy'] ? 'copy' : 'redirect';
$target = $act['target'];
}
}
else {
// we cannot use this rule, because there are unknown commands, and we don't want to overwrite them.
$ignore_rule = true;
}
}
if (count($rule['tests']) != 1 || $rule['tests'][0]['test'] != "true" || $rule['tests'][0]['not'] != null) {
// ignore rules that have special conditions
$ignore_rule = true;
}
if (!$ignore_rule) {
$this->forward = array_merge($rule['actions'][0], array(
'idx' => $idx,
'disabled' => $rule['disabled'] || !$active,
'name' => $rule['name'],
'tests' => $rule['tests'],
'action' => $action ?: 'keep',
'target' => $target,
));
}
}
else if ($active) {
$list[$idx] = $rule['name'];
}
}
$this->forward['list'] = $list;
}
private function forward_post()
{
if (empty($_POST)) {
return;
}
$date_extension = in_array('date', $this->exts);
$regex_extension = in_array('regex', $this->exts);
$status = rcube_utils::get_input_value('forward_status', rcube_utils::INPUT_POST);
$action = rcube_utils::get_input_value('forward_action', rcube_utils::INPUT_POST);
$target = rcube_utils::get_input_value('action_target', rcube_utils::INPUT_POST, true);
$forward_action['type'] = 'forward';
$forward_action['reason'] = $this->strip_value(str_replace("\r\n", "\n", $reason));
$forward_action['subject'] = trim($subject);
$forward_action['from'] = trim($from);
$forward_tests = (array) $this->forward['tests'];
if ($action == 'redirect' || $action == 'copy') {
if (empty($target) || !rcube_utils::check_email($target)) {
$error = 'noemailwarning';
}
}
if (empty($forward_tests)) {
$forward_tests = (array) $this->rc->config->get('managesieve_forward_test', array(array('test' => 'true')));
}
if (!$error) {
$rule = $this->forward;
$rule['type'] = 'if';
$rule['name'] = $rule['name'] ?: $this->plugin->gettext('forward');
$rule['disabled'] = $status == 'off';
$rule['tests'] = $forward_tests;
$rule['join'] = $date_extension ? count($forward_tests) > 1 : false;
$rule['actions'] = array($forward_action);
$rule['after'] = $after;
if ($action && $action != 'keep') {
$rule['actions'][] = array(
'type' => $action == 'discard' ? 'discard' : 'redirect',
'copy' => $action == 'copy',
'target' => $action != 'discard' ? $target : '',
);
}
if ($this->save_forward_script($rule)) {
$this->rc->output->show_message('managesieve.forwardsaved', 'confirmation');
$this->rc->output->send();
}
}
$this->rc->output->show_message($error ?: 'managesieve.saveerror', 'error');
$this->rc->output->send();
}
/**
* Independent forward form
*/
public function forward_form($attrib)
{
// build FORM tag
$form_id = $attrib['id'] ?: 'form';
$out = $this->rc->output->request_form(array(
'id' => $form_id,
'name' => $form_id,
'method' => 'post',
'task' => 'settings',
'action' => 'plugin.managesieve-forward',
'noclose' => true
) + $attrib);
// form elements
$status = new html_select(array('name' => 'forward_status', 'id' => 'forward_status'));
$action = new html_select(array('name' => 'forward_action', 'id' => 'forward_action'));
$status->add($this->plugin->gettext('forward.on'), 'on');
$status->add($this->plugin->gettext('forward.off'), 'off');
if (in_array('copy', $this->exts)) {
$action->add($this->plugin->gettext('forward.copy'), 'copy');
}
$action->add($this->plugin->gettext('forward.redirect'), 'redirect');
// force domain selection in redirect email input
$domains = (array) $this->rc->config->get('managesieve_domains');
$redirect = $this->forward['action'] == 'redirect' || $this->forward['action'] == 'copy';
if (!empty($domains)) {
sort($domains);
$domain_select = new html_select(array('name' => 'action_domain', 'id' => 'action_domain'));
$domain_select->add(array_combine($domains, $domains));
if ($redirect && $this->forward['target']) {
$parts = explode('@', $this->forward['target']);
if (!empty($parts)) {
$this->forward['domain'] = array_pop($parts);
$this->forward['target'] = implode('@', $parts);
}
}
}
// redirect target
$action_target = '<span id="action_target_span" class="input-group">'
. '<input type="text" name="action_target" id="action_target"'
. ' value="' .($redirect ? rcube::Q($this->forward['target'], 'strict', false) : '') . '"'
. (!empty($domains) ? ' size="20"' : ' size="35"') . '/>'
. (!empty($domains) ? ' <span class="input-group-prepend input-group-append"><span class="input-group-text">@</span></span> '
. $domain_select->show($this->forward['domain']) : '')
. '</span>';
// Message tab
$table = new html_table(array('cols' => 2));
$table->add('title', html::label('forward_action', $this->plugin->gettext('forward.action')));
$table->add('forward input-group input-group-combo', $action->show($this->forward['action']) . ' ' . $action_target);
$table->add('title', html::label('forward_status', $this->plugin->gettext('forward.status')));
$table->add(null, $status->show(!isset($this->forward['disabled']) || $this->forward['disabled'] ? 'off' : 'on'));
$out .= $table->show($attrib);
$out .= '</form>';
$this->rc->output->add_gui_object('sieveform', $form_id);
return $out;
}
/**
* Saves forward script (adding some variables)
*/
protected function save_forward_script($rule)
{
// if script does not exist create a new one
if ($this->script_name === null || $this->script_name === false) {
$this->script_name = $this->rc->config->get('managesieve_script_name');
if (empty($this->script_name)) {
$this->script_name = 'roundcube';
}
// use default script contents
if (!$this->rc->config->get('managesieve_kolab_master')) {
$script_file = $this->rc->config->get('managesieve_default');
if ($script_file && is_readable($script_file)) {
$content = file_get_contents($script_file);
}
}
// create and load script
if ($this->sieve->save_script($this->script_name, $content)) {
$this->sieve->load($this->script_name);
}
}
$script_active = in_array($this->script_name, $this->active);
// re-order rules if needed
if (isset($rule['after']) && $rule['after'] !== '') {
// reset original forward rule
if (isset($this->forward['idx'])) {
$this->script[$this->forward['idx']] = null;
}
// add at target position
if ($rule['after'] >= count($this->script) - 1) {
$this->script[] = $rule;
}
else {
$script = array();
foreach ($this->script as $idx => $r) {
if ($r) {
$script[] = $r;
}
if ($idx == $rule['after']) {
$script[] = $rule;
}
}
$this->script = $script;
}
$this->script = array_values(array_filter($this->script));
}
// update original forward rule if it exists
else if (isset($this->forward['idx'])) {
$this->script[$this->forward['idx']] = $rule;
}
// otherwise put forward rule on top
else {
array_unshift($this->script, $rule);
}
// if the script was not active, we need to de-activate
// all rules except the forward rule, but only if it is not disabled
if (!$script_active && !$rule['disabled']) {
foreach ($this->script as $idx => $r) {
if (empty($r['actions']) || $r['actions'][0]['type'] != 'forward') {
$this->script[$idx]['disabled'] = true;
}
}
}
if (!$this->sieve->script) {
return false;
}
$this->sieve->script->content = $this->script;
// save the script
$saved = $this->save_script($this->script_name);
// activate the script
if ($saved && !$script_active && !$rule['disabled']) {
$this->activate_script($this->script_name);
}
return $saved;
}
/**
* API: get forward rule
*
* @return array forward rule information
*/
public function get_forward()
{
$this->exts = $this->sieve->get_extensions();
$this->init_script();
$this->forward_rule();
$forward = array(
'supported' => $this->exts,
'enabled' => empty($this->forward['disabled']),
'action' => $this->forward['action'],
'target' => $this->forward['target'],
);
return $forward;
}
/**
* API: set forward rule
*
* @param array $forward forward rule information (see self::get_forward())
*
* @return bool True on success, False on failure
*/
public function set_forward($data)
{
$this->exts = $this->sieve->get_extensions();
$this->error = false;
$this->init_script();
$this->forward_rule();
$forward['type'] = 'forward';
if ($data['action'] == 'redirect' || $data['action'] == 'copy') {
if (empty($data['target']) || !rcube_utils::check_email($data['target'])) {
$this->error = "Invalid address in action taget: " . $data['target'];
return false;
}
}
else if ($data['action']) {
$this->error = "Unsupported forward action: " . $data['action'];
return false;
}
if (empty($forward_tests)) {
$forward_tests = (array) $this->rc->config->get('managesieve_forward_test', array(array('test' => 'true')));
}
$rule = $this->forward;
$rule['type'] = 'if';
$rule['name'] = $rule['name'] ?: 'Out-of-Office';
$rule['disabled'] = isset($data['enabled']) && !$data['enabled'];
$rule['tests'] = $forward_tests;
$rule['join'] = $date_extension ? count($forward_tests) > 1 : false;
$rule['actions'] = array($forward);
if ($data['action'] && $data['action'] != 'keep') {
$rule['actions'][] = array(
'type' => $data['action'] == 'discard' ? 'discard' : 'redirect',
'copy' => $data['action'] == 'copy',
'target' => $data['action'] != 'discard' ? $data['target'] : '',
);
}
return $this->save_forward_script($rule);
}
/**
* API: connect to managesieve server
*/
public function connect($username, $password)
{
$error = parent::connect($username, $password);
if ($error) {
return $error;
}
return $this->load_script();
}
/**
* API: Returns last error
*
* @return string Error message
*/
public function get_error()
{
return $this->error;
}
}
diff --git a/plugins/managesieve/lib/Roundcube/rcube_sieve_script.php b/plugins/managesieve/lib/Roundcube/rcube_sieve_script.php
index fdfbf6e25..4d8a72c08 100644
--- a/plugins/managesieve/lib/Roundcube/rcube_sieve_script.php
+++ b/plugins/managesieve/lib/Roundcube/rcube_sieve_script.php
@@ -1,1433 +1,1433 @@
<?php
/**
* Class for operations on Sieve scripts
*
- * Copyright (C) 2008-2011, The Roundcube Dev Team
- * Copyright (C) 2011, Kolab Systems AG
+ * Copyright (C) The Roundcube Dev Team
+ * Copyright (C) Kolab Systems AG
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_sieve_script
{
public $content = array(); // script rules array
private $vars = array(); // "global" variables
private $prefix = ''; // script header (comments)
private $supported = array( // supported Sieve extensions:
'body', // RFC5173
'copy', // RFC3894
'date', // RFC5260
'duplicate', // RFC7352
'editheader', // RFC5293
'enotify', // RFC5435
'envelope', // RFC5228
'ereject', // RFC5429
'fileinto', // RFC5228
'imapflags', // draft-melnikov-sieve-imapflags-06
'imap4flags', // RFC5232
'include', // RFC6609
'index', // RFC5260
'mime', // RFC5703 (except: foreverypart/break, enclose, extracttext)
'notify', // RFC5435
'regex', // draft-ietf-sieve-regex-01
'reject', // RFC5429
'relational', // RFC3431
'subaddress', // RFC5233
'vacation', // RFC5230
'vacation-seconds', // RFC6131
'variables', // RFC5229
// @TODO: spamtest+virustest, mailbox
);
/**
* Object constructor
*
* @param string Script's text content
* @param array List of capabilities supported by server
*/
public function __construct($script, $capabilities=array())
{
$capabilities = array_map('strtolower', (array) $capabilities);
// disable features by server capabilities
if (!empty($capabilities)) {
foreach ($this->supported as $idx => $ext) {
if (!in_array($ext, $capabilities)) {
unset($this->supported[$idx]);
}
}
}
// Parse text content of the script
$this->_parse_text($script);
}
/**
* Adds rule to the script (at the end)
*
* @param string Rule name
* @param array Rule content (as array)
*
* @return int The index of the new rule
*/
public function add_rule($content)
{
// TODO: check this->supported
array_push($this->content, $content);
return count($this->content) - 1;
}
public function delete_rule($index)
{
if (isset($this->content[$index])) {
unset($this->content[$index]);
return true;
}
return false;
}
public function size()
{
return count($this->content);
}
public function update_rule($index, $content)
{
// TODO: check this->supported
if ($this->content[$index]) {
$this->content[$index] = $content;
return $index;
}
return false;
}
/**
* Sets "global" variable
*
* @param string $name Variable name
* @param string $value Variable value
* @param array $mods Variable modifiers
*/
public function set_var($name, $value, $mods = array())
{
// Check if variable exists
for ($i=0, $len=count($this->vars); $i<$len; $i++) {
if ($this->vars[$i]['name'] == $name) {
break;
}
}
$var = array_merge($mods, array('name' => $name, 'value' => $value));
$this->vars[$i] = $var;
}
/**
* Unsets "global" variable
*
* @param string $name Variable name
*/
public function unset_var($name)
{
// Check if variable exists
foreach ($this->vars as $idx => $var) {
if ($var['name'] == $name) {
unset($this->vars[$idx]);
break;
}
}
}
/**
* Gets the value of "global" variable
*
* @param string $name Variable name
*
* @return string Variable value
*/
public function get_var($name)
{
// Check if variable exists
for ($i=0, $len=count($this->vars); $i<$len; $i++) {
if ($this->vars[$i]['name'] == $name) {
return $this->vars[$i]['name'];
}
}
}
/**
* Sets script header content
*
* @param string $text Header content
*/
public function set_prefix($text)
{
$this->prefix = $text;
}
/**
* Returns script as text
*/
public function as_text()
{
$output = '';
$exts = array();
$idx = 0;
if (!empty($this->vars)) {
if (in_array('variables', (array)$this->supported)) {
$has_vars = true;
array_push($exts, 'variables');
}
foreach ($this->vars as $var) {
if (empty($has_vars)) {
// 'variables' extension not supported, put vars in comments
$output .= sprintf("# %s %s\n", $var['name'], $var['value']);
}
else {
$output .= 'set ';
foreach (array_diff(array_keys($var), array('name', 'value')) as $opt) {
$output .= ":$opt ";
}
$output .= self::escape_string($var['name']) . ' ' . self::escape_string($var['value']) . ";\n";
}
}
}
$imapflags = in_array('imap4flags', $this->supported) ? 'imap4flags' : 'imapflags';
$notify = in_array('enotify', $this->supported) ? 'enotify' : 'notify';
// rules
foreach ($this->content as $rule) {
$script = '';
$tests = array();
$i = 0;
// header
if (!empty($rule['name']) && strlen($rule['name'])) {
$script .= '# rule:[' . $rule['name'] . "]\n";
}
// constraints expressions
if (!empty($rule['tests'])) {
foreach ($rule['tests'] as $test) {
$tests[$i] = '';
switch ($test['test']) {
case 'size':
$tests[$i] .= ($test['not'] ? 'not ' : '');
$tests[$i] .= 'size :' . ($test['type']=='under' ? 'under ' : 'over ') . $test['arg'];
break;
case 'true':
$tests[$i] .= ($test['not'] ? 'false' : 'true');
break;
case 'exists':
$tests[$i] .= ($test['not'] ? 'not ' : '') . 'exists';
$this->add_mime($test, $tests[$i], $exts);
$tests[$i] .= ' ' . self::escape_string($test['arg']);
break;
case 'header':
case 'string':
if ($test['test'] == 'string') {
array_push($exts, 'variables');
}
$tests[$i] .= ($test['not'] ? 'not ' : '');
$tests[$i] .= $test['test'];
if ($test['test'] == 'header') {
$this->add_mime($test, $tests[$i], $exts);
}
$this->add_index($test, $tests[$i], $exts);
$this->add_operator($test, $tests[$i], $exts);
$tests[$i] .= ' ' . self::escape_string($test['arg1']);
$tests[$i] .= ' ' . self::escape_string($test['arg2']);
break;
case 'address':
case 'envelope':
if ($test['test'] == 'envelope') {
array_push($exts, 'envelope');
}
$tests[$i] .= ($test['not'] ? 'not ' : '');
$tests[$i] .= $test['test'];
if ($test['test'] == 'address') {
$this->add_mime($test, $tests[$i], $exts);
$this->add_index($test, $tests[$i], $exts);
}
// :all address-part is optional, skip it
if (!empty($test['part']) && $test['part'] != 'all') {
$tests[$i] .= ' :' . $test['part'];
if ($test['part'] == 'user' || $test['part'] == 'detail') {
array_push($exts, 'subaddress');
}
}
$this->add_operator($test, $tests[$i], $exts);
$tests[$i] .= ' ' . self::escape_string($test['arg1']);
$tests[$i] .= ' ' . self::escape_string($test['arg2']);
break;
case 'body':
array_push($exts, 'body');
$tests[$i] .= ($test['not'] ? 'not ' : '') . 'body';
if (!empty($test['part'])) {
$tests[$i] .= ' :' . $test['part'];
if (!empty($test['content']) && $test['part'] == 'content') {
$tests[$i] .= ' ' . self::escape_string($test['content']);
}
}
$this->add_operator($test, $tests[$i], $exts);
$tests[$i] .= ' ' . self::escape_string($test['arg']);
break;
case 'date':
case 'currentdate':
array_push($exts, 'date');
$tests[$i] .= ($test['not'] ? 'not ' : '') . $test['test'];
$this->add_index($test, $tests[$i], $exts);
if (!empty($test['originalzone']) && $test['test'] == 'date') {
$tests[$i] .= ' :originalzone';
}
else if (!empty($test['zone'])) {
$tests[$i] .= ' :zone ' . self::escape_string($test['zone']);
}
$this->add_operator($test, $tests[$i], $exts);
if ($test['test'] == 'date') {
$tests[$i] .= ' ' . self::escape_string($test['header']);
}
$tests[$i] .= ' ' . self::escape_string($test['part']);
$tests[$i] .= ' ' . self::escape_string($test['arg']);
break;
case 'duplicate':
array_push($exts, 'duplicate');
$tests[$i] .= ($test['not'] ? 'not ' : '') . $test['test'];
$tokens = array('handle', 'uniqueid', 'header');
foreach ($tokens as $token)
if ($test[$token] !== null && $test[$token] !== '') {
$tests[$i] .= " :$token " . self::escape_string($test[$token]);
}
if (!empty($test['seconds'])) {
$tests[$i] .= ' :seconds ' . intval($test['seconds']);
}
if (!empty($test['last'])) {
$tests[$i] .= ' :last';
}
break;
}
$i++;
}
}
// disabled rule: if false #....
if (!empty($tests)) {
$script .= 'if ' . ($rule['disabled'] ? 'false # ' : '');
if (count($tests) > 1) {
$tests_str = implode(', ', $tests);
}
else {
$tests_str = $tests[0];
}
if ($rule['join'] || count($tests) > 1) {
$script .= sprintf('%s (%s)', $rule['join'] ? 'allof' : 'anyof', $tests_str);
}
else {
$script .= $tests_str;
}
$script .= "\n{\n";
}
// action(s)
if (!empty($rule['actions'])) {
foreach ($rule['actions'] as $action) {
$action_script = '';
switch ($action['type']) {
case 'fileinto':
array_push($exts, 'fileinto');
$action_script .= 'fileinto ';
if ($action['copy']) {
$action_script .= ':copy ';
array_push($exts, 'copy');
}
$action_script .= self::escape_string($action['target']);
break;
case 'redirect':
$action_script .= 'redirect ';
if ($action['copy']) {
$action_script .= ':copy ';
array_push($exts, 'copy');
}
$action_script .= self::escape_string($action['target']);
break;
case 'reject':
case 'ereject':
array_push($exts, $action['type']);
$action_script .= $action['type'].' '
. self::escape_string($action['target']);
break;
case 'addflag':
case 'setflag':
case 'removeflag':
array_push($exts, $imapflags);
$action_script .= $action['type'].' '
. self::escape_string($action['target']);
break;
case 'addheader':
case 'deleteheader':
array_push($exts, 'editheader');
$action_script .= $action['type'];
if (!empty($action['index'])) {
$action_script .= " :index " . intval($action['index']);
}
if (!empty($action['last']) && (!empty($action['index']) || $action['type'] == 'addheader')) {
$action_script .= " :last";
}
if ($action['type'] == 'deleteheader') {
$action['type'] = $action['match-type'];
$this->add_operator($action, $action_script, $exts);
}
$action_script .= " " . self::escape_string($action['name']);
if ((is_string($action['value']) && strlen($action['value']) > 0) || (is_array($action['value']) && !empty($action['value']))) {
$action_script .= " " . self::escape_string($action['value']);
}
break;
case 'keep':
case 'discard':
case 'stop':
$action_script .= $action['type'];
break;
case 'include':
array_push($exts, 'include');
$action_script .= 'include ';
foreach (array_diff(array_keys($action), array('target', 'type')) as $opt) {
$action_script .= ":$opt ";
}
$action_script .= self::escape_string($action['target']);
break;
case 'set':
array_push($exts, 'variables');
$action_script .= 'set ';
foreach (array_diff(array_keys($action), array('name', 'value', 'type')) as $opt) {
$action_script .= ":$opt ";
}
$action_script .= self::escape_string($action['name']) . ' ' . self::escape_string($action['value']);
break;
case 'replace':
array_push($exts, 'mime');
$action_script .= 'replace';
if (!empty($action['mime'])) {
$action_script .= " :mime";
}
if (!empty($action['subject'])) {
$action_script .= " :subject " . self::escape_string($action['subject']);
}
if (!empty($action['from'])) {
$action_script .= " :from " . self::escape_string($action['from']);
}
$action_script .= ' ' . self::escape_string($action['replace']);
break;
case 'notify':
array_push($exts, $notify);
$action_script .= 'notify';
$method = $action['method'];
unset($action['method']);
$action['options'] = (array) $action['options'];
// Here we support draft-martin-sieve-notify-01 used by Cyrus
if ($notify == 'notify') {
switch ($action['importance']) {
case 1: $action_script .= " :high"; break;
//case 2: $action_script .= " :normal"; break;
case 3: $action_script .= " :low"; break;
}
// Old-draft way: :method "mailto" :options "email@address"
if (!empty($method)) {
$parts = explode(':', $method, 2);
$action['method'] = $parts[0];
array_unshift($action['options'], $parts[1]);
}
unset($action['importance']);
unset($action['from']);
unset($method);
}
foreach (array('id', 'importance', 'method', 'options', 'from', 'message') as $n_tag) {
if (!empty($action[$n_tag])) {
$action_script .= " :$n_tag " . self::escape_string($action[$n_tag]);
}
}
if (!empty($method)) {
$action_script .= ' ' . self::escape_string($method);
}
break;
case 'vacation':
array_push($exts, 'vacation');
$action_script .= 'vacation';
if (isset($action['seconds'])) {
array_push($exts, 'vacation-seconds');
$action_script .= " :seconds " . intval($action['seconds']);
}
else if (!empty($action['days'])) {
$action_script .= " :days " . intval($action['days']);
}
if (!empty($action['addresses']))
$action_script .= " :addresses " . self::escape_string($action['addresses']);
if (!empty($action['subject']))
$action_script .= " :subject " . self::escape_string($action['subject']);
if (!empty($action['handle']))
$action_script .= " :handle " . self::escape_string($action['handle']);
if (!empty($action['from']))
$action_script .= " :from " . self::escape_string($action['from']);
if (!empty($action['mime']))
$action_script .= " :mime";
$action_script .= " " . self::escape_string($action['reason']);
break;
}
if ($action_script) {
$script .= !empty($tests) ? "\t" : '';
$script .= $action_script . ";\n";
}
}
}
if ($script) {
$output .= $script . (!empty($tests) ? "}\n" : '');
$idx++;
}
}
// requires
if (!empty($exts)) {
$exts = array_unique($exts);
if (in_array('vacation-seconds', $exts) && ($key = array_search('vacation', $exts)) !== false) {
unset($exts[$key]);
}
sort($exts); // for convenience use always the same order
$output = 'require ["' . implode('","', $exts) . "\"];\n" . $output;
}
if (!empty($this->prefix)) {
$output = $this->prefix . "\n\n" . $output;
}
return $output;
}
/**
* Returns script object
*
*/
public function as_array()
{
return $this->content;
}
/**
* Returns array of supported extensions
*
*/
public function get_extensions()
{
return array_values($this->supported);
}
/**
* Converts text script to rules array
*
* @param string Text script
*/
private function _parse_text($script)
{
$prefix = '';
$options = array();
$position = 0;
$length = strlen($script);
while ($position < $length) {
// skip whitespace chars
$position = self::ltrim_position($script, $position);
$rulename = '';
// Comments
while ($script[$position] === '#') {
$endl = strpos($script, "\n", $position) ?: $length;
$line = substr($script, $position, $endl - $position);
// Roundcube format
if (preg_match('/^# rule:\[(.*)\]/', $line, $matches)) {
$rulename = $matches[1];
}
// KEP:14 variables
else if (preg_match('/^# (EDITOR|EDITOR_VERSION) (.+)$/', $line, $matches)) {
$this->set_var($matches[1], $matches[2]);
}
// Horde-Ingo format
else if (!empty($options['format']) && $options['format'] == 'INGO'
&& preg_match('/^# (.*)/', $line, $matches)
) {
$rulename = $matches[1];
}
else if (empty($options['prefix'])) {
$prefix .= $line . "\n";
}
// skip empty lines after the comment (#5657)
$position = self::ltrim_position($script, $endl + 1);
}
// handle script header
if (empty($options['prefix'])) {
$options['prefix'] = true;
if ($prefix && strpos($prefix, 'horde.org/ingo')) {
$options['format'] = 'INGO';
}
}
// Control structures/blocks
if (preg_match('/^(if|else|elsif)/i', substr($script, $position, 5))) {
$rule = $this->_tokenize_rule($script, $position);
if (strlen($rulename) && !empty($rule)) {
$rule['name'] = $rulename;
}
}
// Simple commands
else {
$rule = $this->_parse_actions($script, $position, ';');
if (!empty($rule[0]) && is_array($rule)) {
// set "global" variables
if ($rule[0]['type'] == 'set') {
unset($rule[0]['type']);
$this->vars[] = $rule[0];
unset($rule);
}
else {
$rule = array('actions' => $rule);
}
}
}
if (!empty($rule)) {
$this->content[] = $rule;
}
}
if (!empty($prefix)) {
$this->prefix = trim($prefix);
}
}
/**
* Convert text script fragment to rule object
*
* @param string $content The whole script content
* @param int &$position Start position in the script
*
* @return array Rule data
*/
private function _tokenize_rule($content, &$position)
{
$cond = strtolower(self::tokenize($content, 1, $position));
if ($cond != 'if' && $cond != 'elsif' && $cond != 'else') {
return null;
}
$disabled = false;
$join = false;
$join_not = false;
$length = strlen($content);
// disabled rule (false + comment): if false # .....
if (preg_match('/^\s*false\s+#\s*/i', substr($content, $position, 20), $m)) {
$position += strlen($m[0]);
$disabled = true;
}
while ($position < $length) {
$tokens = self::tokenize($content, true, $position);
$separator = array_pop($tokens);
if (!empty($tokens)) {
$token = array_shift($tokens);
}
else {
$token = $separator;
}
$token = strtolower($token);
if ($token == 'not') {
$not = true;
$token = strtolower(array_shift($tokens));
}
else {
$not = false;
}
// we support "not allof" as a negation of allof sub-tests
if ($join_not) {
$not = !$not;
}
switch ($token) {
case 'allof':
$join = true;
$join_not = $not;
break;
case 'anyof':
break;
case 'size':
$test = array('test' => 'size', 'not' => $not);
$test['arg'] = array_pop($tokens);
for ($i=0, $len=count($tokens); $i<$len; $i++) {
if (!is_array($tokens[$i])
&& preg_match('/^:(under|over)$/i', $tokens[$i])
) {
$test['type'] = strtolower(substr($tokens[$i], 1));
}
}
$tests[] = $test;
break;
case 'header':
case 'string':
case 'address':
case 'envelope':
$test = array('test' => $token, 'not' => $not);
$test['arg2'] = array_pop($tokens);
$test['arg1'] = array_pop($tokens);
$test += $this->test_tokens($tokens);
if ($token != 'header' && $token != 'string' && !empty($tokens)) {
for ($i=0, $len=count($tokens); $i<$len; $i++) {
if (!is_array($tokens[$i]) && preg_match('/^:(localpart|domain|all|user|detail)$/i', $tokens[$i])) {
$test['part'] = strtolower(substr($tokens[$i], 1));
}
}
}
$tests[] = $test;
break;
case 'body':
$test = array('test' => 'body', 'not' => $not);
$test['arg'] = array_pop($tokens);
$test += $this->test_tokens($tokens);
for ($i=0, $len=count($tokens); $i<$len; $i++) {
if (!is_array($tokens[$i]) && preg_match('/^:(raw|content|text)$/i', $tokens[$i])) {
$test['part'] = strtolower(substr($tokens[$i], 1));
if ($test['part'] == 'content') {
$test['content'] = $tokens[++$i];
}
}
}
$tests[] = $test;
break;
case 'date':
case 'currentdate':
$test = array('test' => $token, 'not' => $not);
$test['arg'] = array_pop($tokens);
$test['part'] = array_pop($tokens);
if ($token == 'date') {
$test['header'] = array_pop($tokens);
}
$test += $this->test_tokens($tokens);
for ($i=0, $len=count($tokens); $i<$len; $i++) {
if (!is_array($tokens[$i]) && preg_match('/^:zone$/i', $tokens[$i])) {
$test['zone'] = $tokens[++$i];
}
else if (!is_array($tokens[$i]) && preg_match('/^:originalzone$/i', $tokens[$i])) {
$test['originalzone'] = true;
}
}
$tests[] = $test;
break;
case 'duplicate':
$test = array('test' => $token, 'not' => $not);
for ($i=0, $len=count($tokens); $i<$len; $i++) {
if (!is_array($tokens[$i])) {
if (preg_match('/^:(handle|header|uniqueid|seconds)$/i', $tokens[$i], $m)) {
$test[strtolower($m[1])] = $tokens[++$i];
}
else if (preg_match('/^:last$/i', $tokens[$i])) {
$test['last'] = true;
}
}
}
$tests[] = $test;
break;
case 'exists':
$test = array('test' => 'exists', 'not' => $not, 'arg' => array_pop($tokens));
$test += $this->test_tokens($tokens);
$tests[] = $test;
break;
case 'true':
$tests[] = array('test' => 'true', 'not' => $not);
break;
case 'false':
$tests[] = array('test' => 'true', 'not' => !$not);
break;
}
// goto actions...
if ($separator == '{') {
break;
}
}
// ...and actions block
$actions = $this->_parse_actions($content, $position);
if ($tests && $actions) {
$result = array(
'type' => $cond,
'tests' => $tests,
'actions' => $actions,
'join' => $join,
'disabled' => $disabled,
);
}
return $result;
}
/**
* Parse body of actions section
*
* @param string $content The whole script content
* @param int &$position Start position in the script
* @param string $end End of text separator
*
* @return array Array of parsed action type/target pairs
*/
private function _parse_actions($content, &$position, $end = '}')
{
$result = null;
$length = strlen($content);
while ($position < $length) {
$tokens = self::tokenize($content, true, $position);
$separator = array_pop($tokens);
$token = !empty($tokens) ? array_shift($tokens) : $separator;
switch ($token) {
case 'if':
// nested 'if' conditions, ignore the whole rule (#5540)
$this->_parse_actions($content, $position);
continue 2;
case 'discard':
case 'keep':
case 'stop':
$result[] = array('type' => $token);
break;
case 'fileinto':
case 'redirect':
$action = array('type' => $token, 'target' => array_pop($tokens));
$args = array('copy');
$action += $this->action_arguments($tokens, $args);
$result[] = $action;
break;
case 'vacation':
$action = array('type' => 'vacation', 'reason' => array_pop($tokens));
$args = array('mime');
$vargs = array('seconds', 'days', 'addresses', 'subject', 'handle', 'from');
$action += $this->action_arguments($tokens, $args, $vargs);
$result[] = $action;
break;
case 'addheader':
case 'deleteheader':
$args = $this->test_tokens($tokens);
if ($token == 'deleteheader') {
$args['match-type'] = $args['type'];
}
if (($index = array_search(':last', $tokens)) !== false) {
$args['last'] = true;
unset($tokens[$index]);
}
$action = array('type' => $token, 'name' => array_shift($tokens), 'value' => array_shift($tokens));
$result[] = $action + $args;
break;
case 'reject':
case 'ereject':
case 'setflag':
case 'addflag':
case 'removeflag':
$result[] = array('type' => $token, 'target' => array_pop($tokens));
break;
case 'include':
$action = array('type' => 'include', 'target' => array_pop($tokens));
$args = array('once', 'optional', 'global', 'personal');
$action += $this->action_arguments($tokens, $args);
$result[] = $action;
break;
case 'set':
$action = array('type' => 'set', 'value' => array_pop($tokens), 'name' => array_pop($tokens));
$args = array('lower', 'upper', 'lowerfirst', 'upperfirst', 'quotewildcard', 'length');
$action += $this->action_arguments($tokens, $args);
$result[] = $action;
break;
case 'replace':
$action = array('type' => 'replace', 'replace' => array_pop($tokens));
$args = array('mime');
$vargs = array('subject', 'from');
$action += $this->action_arguments($tokens, $args, $vargs);
$result[] = $action;
break;
case 'require':
// skip, will be build according to used commands
// $result[] = array('type' => 'require', 'target' => array_pop($tokens));
break;
case 'notify':
$action = array('type' => 'notify');
$priorities = array('high' => 1, 'normal' => 2, 'low' => 3);
$vargs = array('from', 'id', 'importance', 'options', 'message', 'method');
$args = array_keys($priorities);
$action += $this->action_arguments($tokens, $args, $vargs);
// Here we'll convert draft-martin-sieve-notify-01 into RFC 5435
if (!isset($action['importance'])) {
foreach ($priorities as $key => $val) {
if (isset($action[$key])) {
$action['importance'] = $val;
unset($action[$key]);
}
}
}
$action['options'] = (array) $action['options'];
// Old-draft way: :method "mailto" :options "email@address"
if (!empty($action['method']) && !empty($action['options'])) {
$action['method'] .= ':' . array_shift($action['options']);
}
// unnamed parameter is a :method in enotify extension
else if (!isset($action['method'])) {
$action['method'] = array_pop($tokens);
}
$result[] = $action;
break;
}
if ($separator == $end) {
break;
}
}
return $result;
}
/**
* Add comparator to the test
*/
private function add_comparator($test, &$out, &$exts)
{
if (empty($test['comparator'])) {
return;
}
if ($test['comparator'] == 'i;ascii-numeric') {
array_push($exts, 'relational');
array_push($exts, 'comparator-i;ascii-numeric');
}
else if (!in_array($test['comparator'], array('i;octet', 'i;ascii-casemap'))) {
array_push($exts, 'comparator-' . $test['comparator']);
}
// skip default comparator
if ($test['comparator'] != 'i;ascii-casemap') {
$out .= ' :comparator ' . self::escape_string($test['comparator']);
}
}
/**
* Add index argument to the test
*/
private function add_index($test, &$out, &$exts)
{
if (!empty($test['index'])) {
array_push($exts, 'index');
$out .= ' :index ' . intval($test['index']) . ($test['last'] ? ' :last' : '');
}
}
/**
* Add mime argument(s) to the test
*/
private function add_mime($test, &$out, &$exts)
{
foreach (array('mime', 'mime-anychild', 'mime-type', 'mime-subtype', 'mime-contenttype', 'mime-param') as $opt) {
if (!empty($test[$opt])) {
$opt_name = str_replace('mime-', '', $opt);
if (!$got_mime) {
$out .= ' :mime';
$got_mime = true;
array_push($exts, 'mime');
}
if ($opt_name != 'mime') {
$out .= " :$opt_name";
}
if ($opt_name == 'param') {
$out .= ' ' . self::escape_string($test[$opt]);
}
}
}
}
/**
* Add operators to the test
*/
private function add_operator($test, &$out, &$exts)
{
if (empty($test['type'])) {
return;
}
// relational operator
if (preg_match('/^(value|count)-([gteqnl]{2})/', $test['type'], $m)) {
array_push($exts, 'relational');
$out .= ' :' . $m[1] . ' "' . $m[2] . '"';
}
else {
if ($test['type'] == 'regex') {
array_push($exts, 'regex');
}
$out .= ' :' . $test['type'];
}
$this->add_comparator($test, $out, $exts);
}
/**
* Extract test tokens
*/
private function test_tokens(&$tokens)
{
$test = array();
$result = array();
for ($i=0, $len=count($tokens); $i<$len; $i++) {
$token = is_array($tokens[$i]) ? null : $tokens[$i];
if ($token && preg_match('/^:comparator$/i', $token)) {
$test['comparator'] = $tokens[++$i];
}
else if ($token && preg_match('/^:(count|value)$/i', $token)) {
$test['type'] = strtolower(substr($token, 1)) . '-' . $tokens[++$i];
}
else if ($token && preg_match('/^:(is|contains|matches|regex)$/i', $token)) {
$test['type'] = strtolower(substr($token, 1));
}
else if ($token && preg_match('/^:(mime|anychild|type|subtype|contenttype|param)$/i', $token)) {
$token = strtolower(substr($token, 1));
$key = $token == 'mime' ? $token : "mime-$token";
$test[$key] = $token == 'param' ? $tokens[++$i] : true;
}
else if ($token && preg_match('/^:index$/i', $token)) {
$test['index'] = intval($tokens[++$i]);
if ($tokens[$i+1] && preg_match('/^:last$/i', $tokens[$i+1])) {
$test['last'] = true;
$i++;
}
}
else {
$result[] = $tokens[$i];
}
}
$tokens = $result;
return $test;
}
/**
* Extract action arguments
*/
private function action_arguments(&$tokens, $bool_args, $val_args = array())
{
$action = array();
$result = array();
for ($i=0, $len=count($tokens); $i<$len; $i++) {
$tok = $tokens[$i];
if (!is_array($tok) && $tok[0] == ':') {
$tok = strtolower(substr($tok, 1));
if (in_array($tok, $bool_args)) {
$action[$tok] = true;
}
else if (in_array($tok, $val_args)) {
$action[$tok] = $tokens[++$i];
}
else {
$result[] = $tok;
}
}
else {
$result[] = $tok;
}
}
$tokens = $result;
return $action;
}
/**
* Escape special chars into quoted string value or multi-line string
* or list of strings
*
* @param string $str Text or array (list) of strings
*
* @return string Result text
*/
static function escape_string($str)
{
if (is_array($str) && count($str) > 1) {
foreach ($str as $idx => $val) {
$str[$idx] = self::escape_string($val);
}
return '[' . implode(',', $str) . ']';
}
else if (is_array($str)) {
$str = array_pop($str);
}
// multi-line string
if (preg_match('/[\r\n\0]/', $str)) {
return sprintf("text:\n%s\n.\n", self::escape_multiline_string($str));
}
// quoted-string
else {
return '"' . addcslashes($str, '\\"') . '"';
}
}
/**
* Escape special chars in multi-line string value
*
* @param string $str Text
*
* @return string Text
*/
static function escape_multiline_string($str)
{
$str = preg_split('/(\r?\n)/', $str, -1, PREG_SPLIT_DELIM_CAPTURE);
foreach ($str as $idx => $line) {
// dot-stuffing
if (isset($line[0]) && $line[0] == '.') {
$str[$idx] = '.' . $line;
}
}
return implode($str);
}
/**
* Splits script into string tokens
*
* @param string $str The script
* @param mixed $num Number of tokens to return, 0 for all
* or True for all tokens until separator is found.
* Separator will be returned as last token.
* @param int &$position Parsing start position
*
* @return mixed Tokens array or string if $num=1
*/
static function tokenize($str, $num = 0, &$position = 0)
{
$result = array();
$length = strlen($str);
// remove spaces from the beginning of the string
while ($position < $length && (!$num || $num === true || count($result) < $num)) {
// skip whitespace chars
$position = self::ltrim_position($str, $position);
switch ($str[$position]) {
// Quoted string
case '"':
for ($pos = $position + 1; $pos < $length; $pos++) {
if ($str[$pos] == '"') {
break;
}
if ($str[$pos] == "\\") {
if ($str[$pos + 1] == '"' || $str[$pos + 1] == "\\") {
$pos++;
}
}
}
if ($str[$pos] != '"') {
// error
}
// we need to strip slashes for a quoted string
$result[] = stripslashes(substr($str, $position + 1, $pos - $position - 1));
$position = $pos + 1;
break;
// Parenthesized list
case '[':
$position++;
$result[] = self::tokenize($str, 0, $position);
break;
case ']':
$position++;
return $result;
break;
// list/test separator
case ',':
// command separator
case ';':
// block/tests-list
case '(':
case ')':
case '{':
case '}':
$sep = $str[$position];
$position++;
if ($num === true) {
$result[] = $sep;
break 2;
}
break;
// bracket-comment
case '/':
if ($str[$position + 1] == '*') {
if ($end_pos = strpos($str, '*/', $position + 2)) {
$position = $end_pos + 2;
}
else {
// error
$position = $length;
}
}
break;
// hash-comment
case '#':
if ($lf_pos = strpos($str, "\n", $position)) {
$position = $lf_pos + 1;
break;
}
else {
$position = $length;
}
// String atom
default:
// empty or one character
if ($position == $length) {
break 2;
}
if ($length - $position < 2) {
$result[] = substr($str, $position);
$position = $length;
break;
}
// tag/identifier/number
if (preg_match('/[a-zA-Z0-9:_]+/', $str, $m, PREG_OFFSET_CAPTURE, $position)
&& $m[0][1] == $position
) {
$atom = $m[0][0];
$position += strlen($atom);
if ($atom != 'text:') {
$result[] = $atom;
}
// multiline string
else {
// skip whitespace chars (except \r\n)
$position = self::ltrim_position($str, $position, false);
// possible hash-comment after "text:"
if ($str[$position] === '#') {
$endl = strpos($str, "\n", $position);
$position = $endl ?: $length;
}
// skip \n or \r\n
if ($str[$position] == "\n") {
$position++;
}
else if ($str[$position] == "\r" && $str[$position + 1] == "\n") {
$position += 2;
}
$text = '';
// get text until alone dot in a line
while ($position < $length) {
$pos = strpos($str, "\n.", $position);
if ($pos === false) {
break;
}
$text .= substr($str, $position, $pos - $position);
$position = $pos + 2;
if ($str[$position] == "\n") {
break;
}
if ($str[$position] == "\r" && $str[$position + 1] == "\n") {
$position++;
break;
}
$text .= "\n.";
}
// remove dot-stuffing
$text = str_replace("\n..", "\n.", $text);
$result[] = $text;
$position++;
}
}
// fallback, skip one character as infinite loop prevention
else {
$position++;
}
break;
}
}
return $num === 1 ? (isset($result[0]) ? $result[0] : null) : $result;
}
/**
* Skip whitespace characters in a string from specified position.
*/
static function ltrim_position($content, $position, $br = true)
{
$blanks = array("\t", "\0", "\x0B", " ");
if ($br) {
$blanks[] = "\r";
$blanks[] = "\n";
}
while (isset($content[$position]) && isset($content[$position + 1])
&& in_array($content[$position], $blanks, true)
) {
$position++;
}
return $position;
}
}
diff --git a/plugins/managesieve/lib/Roundcube/rcube_sieve_vacation.php b/plugins/managesieve/lib/Roundcube/rcube_sieve_vacation.php
index 70eaaded3..43ce9fc97 100644
--- a/plugins/managesieve/lib/Roundcube/rcube_sieve_vacation.php
+++ b/plugins/managesieve/lib/Roundcube/rcube_sieve_vacation.php
@@ -1,938 +1,938 @@
<?php
/**
* Managesieve Vacation Engine
*
* Engine part of Managesieve plugin implementing UI and backend access.
*
- * Copyright (C) 2011-2014, Kolab Systems AG
+ * Copyright (C) Kolab Systems AG
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_sieve_vacation extends rcube_sieve_engine
{
protected $error;
protected $script_name;
protected $vacation = array();
function actions()
{
$error = $this->start('vacation');
// find current vacation rule
if (!$error) {
$this->vacation_rule();
$this->vacation_post();
}
$this->plugin->add_label('vacation.saving');
$this->rc->output->add_handlers(array(
'vacationform' => array($this, 'vacation_form'),
));
$this->rc->output->set_pagetitle($this->plugin->gettext('vacation'));
$this->rc->output->send('managesieve.vacation');
}
/**
* Find and load sieve script with/for vacation rule
*
* @param string $script_name Optional script name
*
* @return int Connection status: 0 on success, >0 on failure
*/
protected function load_script($script_name = null)
{
if ($this->script_name !== null) {
return 0;
}
$list = $this->list_scripts();
$master = $this->rc->config->get('managesieve_kolab_master');
$included = array();
$this->script_name = false;
// first try the active script(s)...
if (!empty($this->active)) {
// Note: there can be more than one active script on KEP:14-enabled server
foreach ($this->active as $script) {
if ($this->sieve->load($script)) {
foreach ($this->sieve->script->as_array() as $rule) {
if (!empty($rule['actions'])) {
$action = $rule['actions'][0];
if ($action['type'] == 'vacation') {
$this->script_name = $script;
return 0;
}
else if (empty($master) && empty($action['global']) && $action['type'] == 'include') {
$included[] = $action['target'];
}
}
}
}
}
// ...else try scripts included in active script (not for KEP:14)
foreach ($included as $script) {
if ($this->sieve->load($script)) {
foreach ($this->sieve->script->as_array() as $rule) {
if (!empty($rule['actions']) && $rule['actions'][0]['type'] == 'vacation') {
$this->script_name = $script;
return 0;
}
}
}
}
}
// try all other scripts
if (!empty($list)) {
// else try included scripts
foreach (array_diff($list, $included, $this->active) as $script) {
if ($this->sieve->load($script)) {
foreach ($this->sieve->script->as_array() as $rule) {
if (!empty($rule['actions']) && $rule['actions'][0]['type'] == 'vacation') {
$this->script_name = $script;
return 0;
}
}
}
}
// none of the scripts contains existing vacation rule
// use any (first) active or just existing script (in that order)
if (!empty($this->active)) {
$this->sieve->load($this->script_name = $this->active[0]);
}
else {
$this->sieve->load($this->script_name = $list[0]);
}
}
return $this->sieve->error();
}
private function vacation_rule()
{
if ($this->script_name === false || $this->script_name === null || !$this->sieve->load($this->script_name)) {
return;
}
$list = array();
$active = in_array($this->script_name, $this->active);
// find (first) vacation rule
foreach ($this->script as $idx => $rule) {
if (empty($this->vacation) && !empty($rule['actions']) && $rule['actions'][0]['type'] == 'vacation') {
foreach ($rule['actions'] as $act) {
if ($act['type'] == 'discard' || $act['type'] == 'keep') {
$action = $act['type'];
}
else if ($act['type'] == 'redirect') {
$action = $act['copy'] ? 'copy' : 'redirect';
$target = $act['target'];
}
}
$this->vacation = array_merge($rule['actions'][0], array(
'idx' => $idx,
'disabled' => $rule['disabled'] || !$active,
'name' => $rule['name'],
'tests' => $rule['tests'],
'action' => $action ?: 'keep',
'target' => $target,
));
}
else if ($active) {
$list[$idx] = $rule['name'];
}
}
$this->vacation['list'] = $list;
}
private function vacation_post()
{
if (empty($_POST)) {
return;
}
$date_extension = in_array('date', $this->exts);
$regex_extension = in_array('regex', $this->exts);
// set user's timezone
try {
$timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT'));
}
catch (Exception $e) {
$timezone = new DateTimeZone('GMT');
}
$status = rcube_utils::get_input_value('vacation_status', rcube_utils::INPUT_POST);
$from = rcube_utils::get_input_value('vacation_from', rcube_utils::INPUT_POST);
$subject = rcube_utils::get_input_value('vacation_subject', rcube_utils::INPUT_POST, true);
$reason = rcube_utils::get_input_value('vacation_reason', rcube_utils::INPUT_POST, true);
$addresses = rcube_utils::get_input_value('vacation_addresses', rcube_utils::INPUT_POST, true);
$interval = rcube_utils::get_input_value('vacation_interval', rcube_utils::INPUT_POST);
$interval_type = rcube_utils::get_input_value('vacation_interval_type', rcube_utils::INPUT_POST);
$date_from = rcube_utils::get_input_value('vacation_datefrom', rcube_utils::INPUT_POST);
$date_to = rcube_utils::get_input_value('vacation_dateto', rcube_utils::INPUT_POST);
$time_from = rcube_utils::get_input_value('vacation_timefrom', rcube_utils::INPUT_POST);
$time_to = rcube_utils::get_input_value('vacation_timeto', rcube_utils::INPUT_POST);
$after = rcube_utils::get_input_value('vacation_after', rcube_utils::INPUT_POST);
$action = rcube_utils::get_input_value('vacation_action', rcube_utils::INPUT_POST);
$target = rcube_utils::get_input_value('action_target', rcube_utils::INPUT_POST, true);
$target_domain = rcube_utils::get_input_value('action_domain', rcube_utils::INPUT_POST);
$interval_type = $interval_type == 'seconds' ? 'seconds' : 'days';
$vacation_action['type'] = 'vacation';
$vacation_action['reason'] = $this->strip_value(str_replace("\r\n", "\n", $reason));
$vacation_action['subject'] = trim($subject);
$vacation_action['from'] = trim($from);
$vacation_action['addresses'] = $addresses;
$vacation_action[$interval_type] = $interval;
$vacation_tests = (array) $this->vacation['tests'];
foreach ((array) $vacation_action['addresses'] as $aidx => $address) {
$vacation_action['addresses'][$aidx] = $address = trim($address);
if (empty($address)) {
unset($vacation_action['addresses'][$aidx]);
}
else if (!rcube_utils::check_email($address)) {
$error = 'noemailwarning';
break;
}
}
if (!empty($vacation_action['from']) && !rcube_utils::check_email($vacation_action['from'])) {
$error = 'noemailwarning';
}
if ($vacation_action['reason'] == '') {
$error = 'managesieve.emptyvacationbody';
}
if ($vacation_action[$interval_type] && !preg_match('/^[0-9]+$/', $vacation_action[$interval_type])) {
$error = 'managesieve.forbiddenchars';
}
// find and remove existing date/regex/true rules
foreach ((array) $vacation_tests as $idx => $t) {
if ($t['test'] == 'currentdate' || $t['test'] == 'true'
|| ($t['test'] == 'header' && $t['type'] == 'regex' && $t['arg1'] == 'received')
) {
unset($vacation_tests[$idx]);
}
}
if ($date_extension) {
$date_format = $this->rc->config->get('date_format', 'Y-m-d');
foreach (array('date_from', 'date_to') as $var) {
$time = ${str_replace('date', 'time', $var)};
$date = rcube_utils::format_datestr($$var, $date_format);
$date = trim($date . ' ' . $time);
if ($date && ($dt = rcube_utils::anytodatetime($date, $timezone))) {
if ($time) {
$vacation_tests[] = array(
'test' => 'currentdate',
'part' => 'iso8601',
'type' => 'value-' . ($var == 'date_from' ? 'ge' : 'le'),
'zone' => $dt->format('O'),
'arg' => str_replace('+00:00', 'Z', strtoupper($dt->format('c'))),
);
}
else {
$vacation_tests[] = array(
'test' => 'currentdate',
'part' => 'date',
'type' => 'value-' . ($var == 'date_from' ? 'ge' : 'le'),
'zone' => $dt->format('O'),
'arg' => $dt->format('Y-m-d'),
);
}
}
}
}
else if ($regex_extension) {
// Add date range rules if range specified
if ($date_from && $date_to) {
if ($tests = self::build_regexp_tests($date_from, $date_to, $error)) {
$vacation_tests = array_merge($vacation_tests, $tests);
}
}
}
if ($action == 'redirect' || $action == 'copy') {
if ($target_domain) {
$target .= '@' . $target_domain;
}
if (empty($target) || !rcube_utils::check_email($target)) {
$error = 'noemailwarning';
}
}
if (empty($vacation_tests)) {
$vacation_tests = (array) $this->rc->config->get('managesieve_vacation_test', array(array('test' => 'true')));
}
if (!$error) {
$rule = $this->vacation;
$rule['type'] = 'if';
$rule['name'] = $rule['name'] ?: $this->plugin->gettext('vacation');
$rule['disabled'] = $status == 'off';
$rule['tests'] = $vacation_tests;
$rule['join'] = $date_extension ? count($vacation_tests) > 1 : false;
$rule['actions'] = array($vacation_action);
$rule['after'] = $after;
if ($action && $action != 'keep') {
$rule['actions'][] = array(
'type' => $action == 'discard' ? 'discard' : 'redirect',
'copy' => $action == 'copy',
'target' => $action != 'discard' ? $target : '',
);
}
if ($this->save_vacation_script($rule)) {
$this->rc->output->show_message('managesieve.vacationsaved', 'confirmation');
$this->rc->output->send();
}
}
$this->rc->output->show_message($error ?: 'managesieve.saveerror', 'error');
$this->rc->output->send();
}
/**
* Independent vacation form
*/
public function vacation_form($attrib)
{
// check supported extensions
$date_extension = in_array('date', $this->exts);
$regex_extension = in_array('regex', $this->exts);
$seconds_extension = in_array('vacation-seconds', $this->exts);
// build FORM tag
$form_id = $attrib['id'] ?: 'form';
$out = $this->rc->output->request_form(array(
'id' => $form_id,
'name' => $form_id,
'method' => 'post',
'task' => 'settings',
'action' => 'plugin.managesieve-vacation',
'noclose' => true
) + $attrib);
$from_addr = $this->rc->config->get('managesieve_vacation_from_init');
$auto_addr = $this->rc->config->get('managesieve_vacation_addresses_init');
if (count($this->vacation) < 2) {
if ($auto_addr) {
$this->vacation['addresses'] = $this->user_emails();
}
if ($from_addr) {
$default_identity = $this->rc->user->list_emails(true);
$this->vacation['from'] = $default_identity['email'];
}
}
// form elements
$from = new html_inputfield(array('name' => 'vacation_from', 'id' => 'vacation_from', 'size' => 50));
$subject = new html_inputfield(array('name' => 'vacation_subject', 'id' => 'vacation_subject', 'size' => 50));
$reason = new html_textarea(array('name' => 'vacation_reason', 'id' => 'vacation_reason', 'cols' => 60, 'rows' => 8));
$interval = new html_inputfield(array('name' => 'vacation_interval', 'id' => 'vacation_interval', 'size' => 5));
$addresses = '<textarea name="vacation_addresses" id="vacation_addresses" data-type="list" data-size="30" style="display: none">'
. rcube::Q(implode("\n", (array) $this->vacation['addresses']), 'strict', false) . '</textarea>';
$status = new html_select(array('name' => 'vacation_status', 'id' => 'vacation_status'));
$action = new html_select(array('name' => 'vacation_action', 'id' => 'vacation_action', 'onchange' => 'vacation_action_select()'));
$addresses_link = new html_inputfield(array(
'type' => 'button',
'href' => '#',
'class' => 'button',
'onclick' => rcmail_output::JS_OBJECT_NAME . '.managesieve_vacation_addresses()'
));
$status->add($this->plugin->gettext('vacation.on'), 'on');
$status->add($this->plugin->gettext('vacation.off'), 'off');
$action->add($this->plugin->gettext('vacation.keep'), 'keep');
$action->add($this->plugin->gettext('vacation.discard'), 'discard');
$action->add($this->plugin->gettext('vacation.redirect'), 'redirect');
if (in_array('copy', $this->exts)) {
$action->add($this->plugin->gettext('vacation.copy'), 'copy');
}
if ($this->rc->config->get('managesieve_vacation') != 2 && !empty($this->vacation['list'])) {
$after = new html_select(array('name' => 'vacation_after', 'id' => 'vacation_after'));
$after->add('---', '');
foreach ($this->vacation['list'] as $idx => $rule) {
$after->add($rule, $idx);
}
}
$interval_txt = $interval->show(self::vacation_interval($this->vacation));
if ($seconds_extension) {
$interval_select = new html_select(array('name' => 'vacation_interval_type'));
$interval_select->add($this->plugin->gettext('days'), 'days');
$interval_select->add($this->plugin->gettext('seconds'), 'seconds');
$interval_txt .= $interval_select->show(isset($this->vacation['seconds']) ? 'seconds' : 'days');
}
else {
$interval_txt .= "\n" . html::span('input-group-append',
html::span('input-group-text', $this->plugin->gettext('days')));
}
if ($date_extension || $regex_extension) {
$date_from = new html_inputfield(array('name' => 'vacation_datefrom', 'id' => 'vacation_datefrom', 'class' => 'datepicker', 'size' => 12));
$date_to = new html_inputfield(array('name' => 'vacation_dateto', 'id' => 'vacation_dateto', 'class' => 'datepicker', 'size' => 12));
$date_format = $this->rc->config->get('date_format', 'Y-m-d');
}
if ($date_extension) {
$time_from = new html_inputfield(array('name' => 'vacation_timefrom', 'id' => 'vacation_timefrom', 'size' => 7));
$time_to = new html_inputfield(array('name' => 'vacation_timeto', 'id' => 'vacation_timeto', 'size' => 7));
$time_format = $this->rc->config->get('time_format', 'H:i');
$date_value = array();
foreach ((array) $this->vacation['tests'] as $test) {
if ($test['test'] == 'currentdate') {
$idx = $test['type'] == 'value-ge' ? 'from' : 'to';
if ($test['part'] == 'date') {
$date_value[$idx]['date'] = $test['arg'];
}
else if ($test['part'] == 'iso8601') {
$date_value[$idx]['datetime'] = $test['arg'];
}
}
}
foreach ($date_value as $idx => $value) {
$date = $value['datetime'] ?: $value['date'];
$date_value[$idx] = $this->rc->format_date($date, $date_format, false);
if (!empty($value['datetime'])) {
$date_value['time_' . $idx] = $this->rc->format_date($date, $time_format, true);
}
}
}
else if ($regex_extension) {
// Sieve 'date' extension not available, read start/end from RegEx based rules instead
if ($date_tests = self::parse_regexp_tests($this->vacation['tests'])) {
$date_value['from'] = $this->rc->format_date($date_tests['from'], $date_format, false);
$date_value['to'] = $this->rc->format_date($date_tests['to'], $date_format, false);
}
}
// force domain selection in redirect email input
$domains = (array) $this->rc->config->get('managesieve_domains');
$redirect = $this->vacation['action'] == 'redirect' || $this->vacation['action'] == 'copy';
if (!empty($domains)) {
sort($domains);
$domain_select = new html_select(array('name' => 'action_domain', 'id' => 'action_domain'));
$domain_select->add(array_combine($domains, $domains));
if ($redirect && $this->vacation['target']) {
$parts = explode('@', $this->vacation['target']);
if (!empty($parts)) {
$this->vacation['domain'] = array_pop($parts);
$this->vacation['target'] = implode('@', $parts);
}
}
}
// redirect target
$action_target = ' <span id="action_target_span" class="input-group"' . (!$redirect ? ' style="display:none"' : '') . '>'
. '<input type="text" name="action_target" id="action_target"'
. ' value="' .($redirect ? rcube::Q($this->vacation['target'], 'strict', false) : '') . '"'
. (!empty($domains) ? ' size="20"' : ' size="35"') . '/>'
. (!empty($domains) ? ' <span class="input-group-append input-group-prepend"><span class="input-group-text">@</span></span>'
. $domain_select->show($this->vacation['domain']) : '')
. '</span>';
// Message tab
$table = new html_table(array('cols' => 2));
$table->add('title', html::label('vacation_subject', $this->plugin->gettext('vacation.subject')));
$table->add(null, $subject->show($this->vacation['subject']));
$table->add('title', html::label('vacation_reason', $this->plugin->gettext('vacation.body')));
$table->add(null, $reason->show($this->vacation['reason']));
if ($date_extension || $regex_extension) {
$table->add('title', html::label('vacation_datefrom', $this->plugin->gettext('vacation.start')));
$table->add(null, $date_from->show($date_value['from']) . ($time_from ? ' ' . $time_from->show($date_value['time_from']) : ''));
$table->add('title', html::label('vacation_dateto', $this->plugin->gettext('vacation.end')));
$table->add(null, $date_to->show($date_value['to']) . ($time_to ? ' ' . $time_to->show($date_value['time_to']) : ''));
}
$table->add('title', html::label('vacation_status', $this->plugin->gettext('vacation.status')));
$table->add(null, $status->show(!isset($this->vacation['disabled']) || $this->vacation['disabled'] ? 'off' : 'on'));
$out .= html::tag('fieldset', $class, html::tag('legend', null, $this->plugin->gettext('vacation.reply')) . $table->show($attrib));
// Advanced tab
$table = new html_table(array('cols' => 2));
$table->add('title', html::label('vacation_from', $this->plugin->gettext('vacation.from')));
$table->add(null, $from->show($this->vacation['from']));
$table->add('title', html::label('vacation_addresses', $this->plugin->gettext('vacation.addresses')));
$table->add(null, $addresses . $addresses_link->show($this->plugin->gettext('filladdresses')));
$table->add('title', html::label('vacation_interval', $this->plugin->gettext('vacation.interval')));
$table->add('input-group', $interval_txt);
if ($after) {
$table->add('title', html::label('vacation_after', $this->plugin->gettext('vacation.after')));
$table->add(null, $after->show($this->vacation['idx'] - 1));
}
$table->add('title', html::label('vacation_action', $this->plugin->gettext('vacation.action')));
$table->add('vacation input-group input-group-combo', $action->show($this->vacation['action']) . $action_target);
$out .= html::tag('fieldset', $class, html::tag('legend', null, $this->plugin->gettext('vacation.advanced')) . $table->show($attrib));
$out .= '</form>';
$this->rc->output->add_gui_object('sieveform', $form_id);
if ($time_format) {
$this->rc->output->set_env('time_format', $time_format);
}
return $out;
}
public static function build_regexp_tests($date_from, $date_to, &$error)
{
$tests = array();
$dt_from = rcube_utils::anytodatetime($date_from);
$dt_to = rcube_utils::anytodatetime($date_to);
$interval = $dt_from->diff($dt_to);
if ($interval->invert || $interval->days > 365) {
$error = 'managesieve.invaliddateformat';
return;
}
$dt_i = $dt_from;
$interval = new DateInterval('P1D');
$matchexp = '';
while (!$dt_i->diff($dt_to)->invert) {
$days = (int) $dt_i->format('d');
$matchexp .= $days < 10 ? "[ 0]$days" : $days;
if ($days == $dt_i->format('t') || $dt_i->diff($dt_to)->days == 0) {
$test = array(
'test' => 'header',
'type' => 'regex',
'arg1' => 'received',
'arg2' => '('.$matchexp.') '.$dt_i->format('M Y')
);
$tests[] = $test;
$matchexp = '';
}
else {
$matchexp .= '|';
}
$dt_i->add($interval);
}
return $tests;
}
public static function parse_regexp_tests($tests)
{
$rx_from = '/^\(([0-9]{2}).*\)\s([A-Za-z]+)\s([0-9]{4})/';
$rx_to = '/^\(.*([0-9]{2})\)\s([A-Za-z]+)\s([0-9]{4})/';
$result = array();
foreach ((array) $tests as $test) {
if ($test['test'] == 'header' && $test['type'] == 'regex' && $test['arg1'] == 'received') {
$textexp = preg_replace('/\[ ([^\]]*)\]/', '0', $test['arg2']);
if (!$result['from'] && preg_match($rx_from, $textexp, $matches)) {
$result['from'] = $matches[1]." ".$matches[2]." ".$matches[3];
}
if (preg_match($rx_to, $textexp, $matches)) {
$result['to'] = $matches[1]." ".$matches[2]." ".$matches[3];
}
}
}
return $result;
}
/**
* Get current vacation interval
*/
public static function vacation_interval(&$vacation)
{
$rcube = rcube::get_instance();
if (isset($vacation['seconds'])) {
$interval = $vacation['seconds'];
}
else if (isset($vacation['days'])) {
$interval = $vacation['days'];
}
else if ($interval_cfg = $rcube->config->get('managesieve_vacation_interval')) {
if (preg_match('/^([0-9]+)s$/', $interval_cfg, $m)) {
if ($seconds_extension) {
$vacation['seconds'] = ($interval = intval($m[1])) ? $interval : null;
}
else {
$vacation['days'] = $interval = ceil(intval($m[1])/86400);
}
}
else {
$vacation['days'] = $interval = intval($interval_cfg);
}
}
return $interval ?: '';
}
/**
* Saves vacation script (adding some variables)
*/
protected function save_vacation_script($rule)
{
// if script does not exist create a new one
if ($this->script_name === null || $this->script_name === false) {
$this->script_name = $this->rc->config->get('managesieve_script_name');
if (empty($this->script_name)) {
$this->script_name = 'roundcube';
}
// use default script contents
if (!$this->rc->config->get('managesieve_kolab_master')) {
$script_file = $this->rc->config->get('managesieve_default');
if ($script_file && is_readable($script_file)) {
$content = file_get_contents($script_file);
}
}
// create and load script
if ($this->sieve->save_script($this->script_name, $content)) {
$this->sieve->load($this->script_name);
}
}
$script_active = in_array($this->script_name, $this->active);
// re-order rules if needed
if (isset($rule['after']) && $rule['after'] !== '') {
// reset original vacation rule
if (isset($this->vacation['idx'])) {
$this->script[$this->vacation['idx']] = null;
}
// add at target position
if ($rule['after'] >= count($this->script) - 1) {
$this->script[] = $rule;
}
else {
$script = array();
foreach ($this->script as $idx => $r) {
if ($r) {
$script[] = $r;
}
if ($idx == $rule['after']) {
$script[] = $rule;
}
}
$this->script = $script;
}
$this->script = array_values(array_filter($this->script));
}
// update original vacation rule if it exists
else if (isset($this->vacation['idx'])) {
$this->script[$this->vacation['idx']] = $rule;
}
// otherwise put vacation rule on top
else {
array_unshift($this->script, $rule);
}
// if the script was not active, we need to de-activate
// all rules except the vacation rule, but only if it is not disabled
if (!$script_active && !$rule['disabled']) {
foreach ($this->script as $idx => $r) {
if (empty($r['actions']) || $r['actions'][0]['type'] != 'vacation') {
$this->script[$idx]['disabled'] = true;
}
}
}
if (!$this->sieve->script) {
return false;
}
$this->sieve->script->content = $this->script;
// save the script
$saved = $this->save_script($this->script_name);
// activate the script
if ($saved && !$script_active && !$rule['disabled']) {
$this->activate_script($this->script_name);
}
return $saved;
}
/**
* API: get vacation rule
*
* @return array Vacation rule information
*/
public function get_vacation()
{
$this->exts = $this->sieve->get_extensions();
$this->init_script();
$this->vacation_rule();
// check supported extensions
$date_extension = in_array('date', $this->exts);
$regex_extension = in_array('regex', $this->exts);
$seconds_extension = in_array('vacation-seconds', $this->exts);
// set user's timezone
try {
$timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT'));
}
catch (Exception $e) {
$timezone = new DateTimeZone('GMT');
}
if ($date_extension) {
$date_value = array();
foreach ((array) $this->vacation['tests'] as $test) {
if ($test['test'] == 'currentdate') {
$idx = $test['type'] == 'value-ge' ? 'start' : 'end';
if ($test['part'] == 'date') {
$date_value[$idx]['date'] = $test['arg'];
}
else if ($test['part'] == 'iso8601') {
$date_value[$idx]['datetime'] = $test['arg'];
}
}
}
foreach ($date_value as $idx => $value) {
$$idx = new DateTime($value['datetime'] ?: $value['date'], $timezone);
}
}
else if ($regex_extension) {
// Sieve 'date' extension not available, read start/end from RegEx based rules instead
if ($date_tests = self::parse_regexp_tests($this->vacation['tests'])) {
$from = new DateTime($date_tests['from'] . ' ' . '00:00:00', $timezone);
$to = new DateTime($date_tests['to'] . ' ' . '23:59:59', $timezone);
}
}
if (isset($this->vacation['seconds'])) {
$interval = $this->vacation['seconds'] . 's';
}
else if (isset($this->vacation['days'])) {
$interval = $this->vacation['days'] . 'd';
}
$vacation = array(
'supported' => $this->exts,
'interval' => $interval,
'start' => $start,
'end' => $end,
'enabled' => $this->vacation['reason'] && empty($this->vacation['disabled']),
'message' => $this->vacation['reason'],
'subject' => $this->vacation['subject'],
'action' => $this->vacation['action'],
'target' => $this->vacation['target'],
'addresses' => $this->vacation['addresses'],
'from' => $this->vacation['from'],
);
return $vacation;
}
/**
* API: set vacation rule
*
* @param array $vacation Vacation rule information (see self::get_vacation())
*
* @return bool True on success, False on failure
*/
public function set_vacation($data)
{
$this->exts = $this->sieve->get_extensions();
$this->error = false;
$this->init_script();
$this->vacation_rule();
// check supported extensions
$date_extension = in_array('date', $this->exts);
$regex_extension = in_array('regex', $this->exts);
$seconds_extension = in_array('vacation-seconds', $this->exts);
$vacation['type'] = 'vacation';
$vacation['reason'] = $this->strip_value(str_replace("\r\n", "\n", $data['message']));
$vacation['addresses'] = $data['addresses'];
$vacation['subject'] = trim($data['subject']);
$vacation['from'] = trim($data['from']);
$vacation_tests = (array) $this->vacation['tests'];
foreach ((array) $vacation['addresses'] as $aidx => $address) {
$vacation['addresses'][$aidx] = $address = trim($address);
if (empty($address)) {
unset($vacation['addresses'][$aidx]);
}
else if (!rcube_utils::check_email($address)) {
$this->error = "Invalid address in vacation addresses: $address";
return false;
}
}
if (!empty($vacation['from']) && !rcube_utils::check_email($vacation['from'])) {
$this->error = "Invalid address in 'from': " . $vacation['from'];
return false;
}
if ($vacation['reason'] == '') {
$this->error = "No vacation message specified";
return false;
}
if ($data['interval']) {
if (!preg_match('/^([0-9]+)\s*([sd])$/', $data['interval'], $m)) {
$this->error = "Invalid vacation interval value: " . $data['interval'];
return false;
}
else if ($m[1]) {
$vacation[strtolower($m[2]) == 's' ? 'seconds' : 'days'] = $m[1];
}
}
// find and remove existing date/regex/true rules
foreach ((array) $vacation_tests as $idx => $t) {
if ($t['test'] == 'currentdate' || $t['test'] == 'true'
|| ($t['test'] == 'header' && $t['type'] == 'regex' && $t['arg1'] == 'received')
) {
unset($vacation_tests[$idx]);
}
}
if ($date_extension) {
foreach (array('start', 'end') as $var) {
if ($dt = $data[$var]) {
$vacation_tests[] = array(
'test' => 'currentdate',
'part' => 'iso8601',
'type' => 'value-' . ($var == 'start' ? 'ge' : 'le'),
'zone' => $dt->format('O'),
'arg' => str_replace('+00:00', 'Z', strtoupper($dt->format('c'))),
);
}
}
}
else if ($regex_extension) {
// Add date range rules if range specified
if ($data['start'] && $data['end']) {
if ($tests = self::build_regexp_tests($data['start'], $data['end'], $error)) {
$vacation_tests = array_merge($vacation_tests, $tests);
}
if ($error) {
$this->error = "Invalid dates specified or unsupported period length";
return false;
}
}
}
if ($data['action'] == 'redirect' || $data['action'] == 'copy') {
if (empty($data['target']) || !rcube_utils::check_email($data['target'])) {
$this->error = "Invalid address in action taget: " . $data['target'];
return false;
}
}
else if ($data['action'] && $data['action'] != 'keep' && $data['action'] != 'discard') {
$this->error = "Unsupported vacation action: " . $data['action'];
return false;
}
if (empty($vacation_tests)) {
$vacation_tests = (array) $this->rc->config->get('managesieve_vacation_test', array(array('test' => 'true')));
}
$rule = $this->vacation;
$rule['type'] = 'if';
$rule['name'] = $rule['name'] ?: 'Out-of-Office';
$rule['disabled'] = isset($data['enabled']) && !$data['enabled'];
$rule['tests'] = $vacation_tests;
$rule['join'] = $date_extension ? count($vacation_tests) > 1 : false;
$rule['actions'] = array($vacation);
if ($data['action'] && $data['action'] != 'keep') {
$rule['actions'][] = array(
'type' => $data['action'] == 'discard' ? 'discard' : 'redirect',
'copy' => $data['action'] == 'copy',
'target' => $data['action'] != 'discard' ? $data['target'] : '',
);
}
return $this->save_vacation_script($rule);
}
/**
* API: connect to managesieve server
*/
public function connect($username, $password)
{
$error = parent::connect($username, $password);
if ($error) {
return $error;
}
return $this->load_script();
}
/**
* API: Returns last error
*
* @return string Error message
*/
public function get_error()
{
return $this->error;
}
}
diff --git a/plugins/managesieve/localization/en_US.inc b/plugins/managesieve/localization/en_US.inc
index ebc78aa00..0ab602ce5 100644
--- a/plugins/managesieve/localization/en_US.inc
+++ b/plugins/managesieve/localization/en_US.inc
@@ -1,281 +1,277 @@
<?php
/*
+-----------------------------------------------------------------------+
- | plugins/managesieve/localization/<lang>.inc |
- | |
| Localization file of the Roundcube Webmail Managesieve plugin |
- | Copyright (C) 2012-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-----------------------------------------------------------------------+
For translation see https://www.transifex.com/projects/p/roundcube-webmail/resource/plugin-managesieve/
*/
$labels['filters'] = 'Filters';
$labels['managefilters'] = 'Manage incoming mail filters';
$labels['filtername'] = 'Filter name';
$labels['newfilter'] = 'New filter';
$labels['filteradd'] = 'Add filter';
$labels['filterdel'] = 'Delete filter';
$labels['rules'] = 'Rules';
$labels['actions'] = 'Actions';
$labels['scope'] = 'Scope';
$labels['moveup'] = 'Move up';
$labels['movedown'] = 'Move down';
$labels['filterallof'] = 'matching all of the following rules';
$labels['filteranyof'] = 'matching any of the following rules';
$labels['filterany'] = 'all messages';
$labels['filtercontains'] = 'contains';
$labels['filternotcontains'] = 'not contains';
$labels['filteris'] = 'is equal to';
$labels['filterisnot'] = 'is not equal to';
$labels['filterexists'] = 'exists';
$labels['filternotexists'] = 'not exists';
$labels['filtermatches'] = 'matches expression';
$labels['filternotmatches'] = 'not matches expression';
$labels['filterregex'] = 'matches regular expression';
$labels['filternotregex'] = 'not matches regular expression';
$labels['filterunder'] = 'under';
$labels['filterover'] = 'over';
$labels['mime'] = 'MIME:';
$labels['mimepart'] = 'MIME part:';
$labels['mime-message'] = 'message';
$labels['mime-anychild'] = 'any';
$labels['mime-type'] = 'type';
$labels['mime-subtype'] = 'subtype';
$labels['mime-contenttype'] = 'content-type';
$labels['mime-param'] = 'parameter';
$labels['addrule'] = 'Add rule';
$labels['delrule'] = 'Delete rule';
$labels['messagemoveto'] = 'Move message to';
$labels['messageredirect'] = 'Redirect message to';
$labels['messagecopyto'] = 'Copy message to';
$labels['messagesendcopy'] = 'Send message copy to';
$labels['messagereply'] = 'Reply with message';
$labels['messagedelete'] = 'Delete message';
$labels['messagediscard'] = 'Discard with message';
$labels['messagekeep'] = 'Keep message in Inbox';
$labels['messagesrules'] = 'For incoming mail:';
$labels['messagesactions'] = '...execute the following actions:';
$labels['add'] = 'Add';
$labels['del'] = 'Delete';
$labels['sender'] = 'Sender';
$labels['recipient'] = 'Recipient';
$labels['vacationaddr'] = 'My e-mail addresses:';
$labels['vacationdays'] = 'How often send messages (in days):';
$labels['vacationinterval'] = 'How often send messages:';
$labels['vacationreason'] = 'Message body (vacation reason):';
$labels['vacationfrom'] = 'Reply sender address:';
$labels['vacationsubject'] = 'Message subject:';
$labels['days'] = 'days';
$labels['seconds'] = 'seconds';
$labels['rulestop'] = 'Stop evaluating rules';
$labels['enable'] = 'Enable/Disable';
$labels['filterset'] = 'Filter set';
$labels['filtersets'] = 'Filter sets';
$labels['filtersetadd'] = 'Add filter set';
$labels['filtersetdel'] = 'Delete filter set';
$labels['filtersetact'] = 'Activate current filter set';
$labels['filtersetdeact'] = 'Deactivate current filter set';
$labels['filterseteditraw'] = 'Edit filter set';
$labels['filtersetswitch'] = 'Enable/disable filter set';
$labels['filterdef'] = 'Filter definition';
$labels['filtersetname'] = 'Filter set name';
$labels['newfilterset'] = 'New filter set';
$labels['active'] = 'active';
$labels['none'] = 'none';
$labels['fromset'] = 'from set';
$labels['fromfile'] = 'from file';
$labels['filterdisabled'] = 'Filter disabled';
$labels['countisgreaterthan'] = 'count is greater than';
$labels['countisgreaterthanequal'] = 'count is greater than or equal to';
$labels['countislessthan'] = 'count is less than';
$labels['countislessthanequal'] = 'count is less than or equal to';
$labels['countequals'] = 'count is equal to';
$labels['countnotequals'] = 'count is not equal to';
$labels['valueisgreaterthan'] = 'value is greater than';
$labels['valueisgreaterthanequal'] = 'value is greater than or equal to';
$labels['valueislessthan'] = 'value is less than';
$labels['valueislessthanequal'] = 'value is less than or equal to';
$labels['valueequals'] = 'value is equal to';
$labels['valuenotequals'] = 'value is not equal to';
$labels['setflags'] = 'Set flags to the message';
$labels['addflags'] = 'Add flags to the message';
$labels['removeflags'] = 'Remove flags from the message';
$labels['flagread'] = 'Read';
$labels['flagdeleted'] = 'Deleted';
$labels['flaganswered'] = 'Answered';
$labels['flagflagged'] = 'Flagged';
$labels['flagdraft'] = 'Draft';
$labels['addheader'] = 'Add header to the message';
$labels['deleteheader'] = 'Remove header from the message';
$labels['headername'] = 'Header name';
$labels['headervalue'] = 'Header value';
$labels['headerpos'] = 'Header position';
$labels['headeratstart'] = 'at the beginning';
$labels['headeratend'] = 'at the end';
$labels['headeroccurrence'] = 'Header occurrence/position';
$labels['headerfromstart'] = 'from start';
$labels['headerfromend'] = 'from end';
$labels['headerpatterns'] = 'Header value patterns';
$labels['headermatchtype'] = 'match type:';
$labels['setvariable'] = 'Set variable';
$labels['setvarname'] = 'Variable name:';
$labels['setvarvalue'] = 'Variable value:';
$labels['setvarmodifiers'] = 'Modifiers:';
$labels['varlower'] = 'lower-case';
$labels['varupper'] = 'upper-case';
$labels['varlowerfirst'] = 'first character lower-case';
$labels['varupperfirst'] = 'first character upper-case';
$labels['varquotewildcard'] = 'quote special characters';
$labels['varlength'] = 'length';
$labels['notify'] = 'Send notification';
$labels['notifytarget'] = 'Notification target:';
$labels['notifymessage'] = 'Notification message (optional):';
$labels['notifyoptions'] = 'Notification options (optional):';
$labels['notifyfrom'] = 'Notification sender (optional):';
$labels['notifyimportance'] = 'Importance:';
$labels['notifyimportancelow'] = 'low';
$labels['notifyimportancenormal'] = 'normal';
$labels['notifyimportancehigh'] = 'high';
$labels['notifymethodmailto'] = 'Email';
$labels['notifymethodtel'] = 'Phone';
$labels['notifymethodsms'] = 'SMS';
$labels['filtercreate'] = 'Create filter';
$labels['usedata'] = 'Use following data in the filter:';
$labels['nextstep'] = 'Next Step';
$labels['...'] = '...';
$labels['string'] = 'String';
$labels['currdate'] = 'Current date';
$labels['datetest'] = 'Date';
$labels['dateheader'] = 'header:';
$labels['year'] = 'year';
$labels['month'] = 'month';
$labels['day'] = 'day';
$labels['date'] = 'date (yyyy-mm-dd)';
$labels['julian'] = 'date (julian)';
$labels['hour'] = 'hour';
$labels['minute'] = 'minute';
$labels['second'] = 'second';
$labels['time'] = 'time (hh:mm:ss)';
$labels['iso8601'] = 'date (ISO8601)';
$labels['std11'] = 'date (RFC2822)';
$labels['zone'] = 'time-zone';
$labels['weekday'] = 'weekday (0-6)';
$labels['advancedopts'] = 'Advanced options';
$labels['body'] = 'Body';
$labels['address'] = 'address';
$labels['envelope'] = 'envelope';
$labels['modifier'] = 'modifier:';
$labels['text'] = 'text';
$labels['undecoded'] = 'undecoded (raw)';
$labels['contenttype'] = 'content type';
$labels['modtype'] = 'type:';
$labels['allparts'] = 'all';
$labels['domain'] = 'domain';
$labels['localpart'] = 'local part';
$labels['user'] = 'user';
$labels['detail'] = 'detail';
$labels['comparator'] = 'comparator:';
$labels['default'] = 'default';
$labels['octet'] = 'strict (octet)';
$labels['asciicasemap'] = 'case insensitive (ascii-casemap)';
$labels['asciinumeric'] = 'numeric (ascii-numeric)';
$labels['index'] = 'index:';
$labels['indexlast'] = 'backwards';
$labels['vacation'] = 'Out of Office';
$labels['vacation.reply'] = 'Reply message';
$labels['vacation.advanced'] = 'Advanced settings';
$labels['vacation.from'] = 'Reply sender address';
$labels['vacation.subject'] = 'Subject';
$labels['vacation.body'] = 'Body';
$labels['vacation.start'] = 'Start time';
$labels['vacation.end'] = 'End time';
$labels['vacation.status'] = 'Status';
$labels['vacation.on'] = 'On';
$labels['vacation.off'] = 'Off';
$labels['vacation.addresses'] = 'My e-mail addresses';
$labels['vacation.interval'] = 'Reply interval';
$labels['vacation.after'] = 'Put the out-of-office rule after';
$labels['vacation.saving'] = 'Saving data...';
$labels['vacation.action'] = 'Incoming message action';
$labels['vacation.keep'] = 'Keep';
$labels['vacation.discard'] = 'Discard';
$labels['vacation.redirect'] = 'Redirect to';
$labels['vacation.copy'] = 'Send copy to';
$labels['forward'] = 'Forwarding';
$labels['forward.redirect'] = 'Redirect to';
$labels['forward.copy'] = 'Send copy to';
$labels['forward.on'] = 'On';
$labels['forward.off'] = 'Off';
$labels['forward.status'] = 'Status:';
$labels['forward.action'] = 'For incoming mails execute the following action:';
$labels['forward.saving'] = 'Saving data...';
$labels['filladdresses'] = 'Fill with all my addresses';
$labels['arialabelfiltersetactions'] = 'Filter set actions';
$labels['arialabelfilteractions'] = 'Filter actions';
$labels['arialabelfilterform'] = 'Filter properties';
$labels['ariasummaryfilterslist'] = 'List of filters';
$labels['ariasummaryfiltersetslist'] = 'List of filter sets';
$labels['filterstitle'] = 'Edit incoming mail filters';
$labels['vacationtitle'] = 'Edit out-of-office rule';
$labels['forwardtitle'] = 'Edit mail forwarding rule';
$labels['message'] = 'Message';
$labels['duplicate'] = 'is duplicate';
$labels['notduplicate'] = 'is not duplicate';
$labels['duplicate.handle'] = 'handle:';
$labels['duplicate.header'] = 'header:';
$labels['duplicate.uniqueid'] = 'identifier:';
$labels['duplicate.seconds'] = 'timeout (seconds):';
$labels['duplicate.last'] = 'relative to the last execution';
$messages = array();
$messages['filterunknownerror'] = 'Unknown server error.';
$messages['filterconnerror'] = 'Unable to connect to server.';
$messages['filterdeleteerror'] = 'Unable to delete filter. Server error occurred.';
$messages['filterdeleted'] = 'Filter deleted successfully.';
$messages['filtersaved'] = 'Filter saved successfully.';
$messages['filtersaveerror'] = 'Unable to save filter. Server error occurred.';
$messages['filterformerror'] = 'Filter form contains errors.';
$messages['filterdeleteconfirm'] = 'Do you really want to delete selected filter?';
$messages['ruledeleteconfirm'] = 'Are you sure, you want to delete selected rule?';
$messages['actiondeleteconfirm'] = 'Are you sure, you want to delete selected action?';
$messages['forbiddenchars'] = 'Forbidden characters in field.';
$messages['cannotbeempty'] = 'Field cannot be empty.';
$messages['ruleexist'] = 'Filter with specified name already exists.';
$messages['setactivateerror'] = 'Unable to activate selected filter set. Server error occurred.';
$messages['setdeactivateerror'] = 'Unable to deactivate selected filter set. Server error occurred.';
$messages['setdeleteerror'] = 'Unable to delete selected filter set. Server error occurred.';
$messages['setactivated'] = 'Filter set activated successfully.';
$messages['setdeactivated'] = 'Filter set deactivated successfully.';
$messages['setdeleted'] = 'Filter set deleted successfully.';
$messages['setupdated'] = 'Filter set updated successfully.';
$messages['setdeleteconfirm'] = 'Are you sure, you want to delete selected filter set?';
$messages['setcreateerror'] = 'Unable to create filter set. Server error occurred.';
$messages['setcreated'] = 'Filter set created successfully.';
$messages['activateerror'] = 'Unable to enable selected filter(s). Server error occurred.';
$messages['deactivateerror'] = 'Unable to disable selected filter(s). Server error occurred.';
$messages['deactivated'] = 'Filter(s) disabled successfully.';
$messages['activated'] = 'Filter(s) enabled successfully.';
$messages['moved'] = 'Filter moved successfully.';
$messages['moveerror'] = 'Unable to move selected filter. Server error occurred.';
$messages['nametoolong'] = 'Name too long.';
$messages['namereserved'] = 'Reserved name.';
$messages['setexist'] = 'Set already exists.';
$messages['nodata'] = 'At least one position must be selected!';
$messages['invaliddateformat'] = 'Invalid date or date part format';
$messages['saveerror'] = 'Unable to save data. Server error occurred.';
$messages['vacationsaved'] = 'Out-of-office data saved successfully.';
$messages['forwardsaved'] = 'Forward data saved successfully.';
$messages['emptyvacationbody'] = 'Body of vacation message is required!';
$messages['duplicate.conflict.err'] = 'Both header and unique identifier are not allowed.';
$messages['disabledaction'] = 'Action not permitted.';
$messages['lastindexempty'] = 'Index is required when counting from end';
-
-?>
diff --git a/plugins/managesieve/managesieve.js b/plugins/managesieve/managesieve.js
index f81f78a41..874856d31 100644
--- a/plugins/managesieve/managesieve.js
+++ b/plugins/managesieve/managesieve.js
@@ -1,1200 +1,1200 @@
/**
* (Manage)Sieve Filters plugin
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
- * Copyright (c) 2012-2014, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*/
if (window.rcmail) {
rcmail.addEventListener('init', function(evt) {
// add managesieve-create command to message_commands array,
// so it's state will be updated on message selection/unselection
if (rcmail.env.task == 'mail') {
if (rcmail.env.action != 'show')
rcmail.env.message_commands.push('managesieve-create');
else
rcmail.enable_command('managesieve-create', true);
}
if (rcmail.env.task == 'mail' || rcmail.env.action.startsWith('plugin.managesieve')) {
// Create layer for form tips
if (!rcmail.env.framed) {
rcmail.env.ms_tip_layer = $('<div id="managesieve-tip" class="popupmenu"></div>');
rcmail.env.ms_tip_layer.appendTo(document.body);
}
}
// register commands
rcmail.register_command('plugin.managesieve-save', function() { rcmail.managesieve_save() });
rcmail.register_command('plugin.managesieve-act', function() { rcmail.managesieve_act() });
rcmail.register_command('plugin.managesieve-add', function() { rcmail.managesieve_add() });
rcmail.register_command('plugin.managesieve-del', function() { rcmail.managesieve_del() });
rcmail.register_command('plugin.managesieve-move', function() { rcmail.managesieve_move() });
rcmail.register_command('plugin.managesieve-setadd', function() { rcmail.managesieve_setadd() });
rcmail.register_command('plugin.managesieve-setdel', function() { rcmail.managesieve_setdel() });
rcmail.register_command('plugin.managesieve-setact', function() { rcmail.managesieve_setact() });
rcmail.register_command('plugin.managesieve-setget', function() { rcmail.managesieve_setget() });
rcmail.register_command('plugin.managesieve-seteditraw', function() { rcmail.managesieve_seteditraw() });
if (rcmail.env.action.startsWith('plugin.managesieve')) {
if (rcmail.gui_objects.sieveform) {
rcmail.enable_command('plugin.managesieve-save', true);
sieve_form_init();
}
else if (rcmail.gui_objects.sievesetrawform) {
rcmail.enable_command('plugin.managesieve-save', true);
sieve_raw_editor_init();
}
else {
rcmail.enable_command('plugin.managesieve-add', !rcmail.env.sieveconnerror && $.inArray('new_rule', rcmail.env.managesieve_disabled_actions) == -1);
rcmail.enable_command('plugin.managesieve-setadd', !rcmail.env.sieveconnerror && $.inArray('new_set', rcmail.env.managesieve_disabled_actions) == -1);
}
var setcnt, set = rcmail.env.currentset;
if (rcmail.gui_objects.filterslist) {
rcmail.filters_list = new rcube_list_widget(rcmail.gui_objects.filterslist,
{multiselect:false, draggable:true, keyboard:true});
rcmail.filters_list
.addEventListener('select', function(e) { rcmail.managesieve_select(e); })
.addEventListener('dragstart', function(e) { rcmail.managesieve_dragstart(e); })
.addEventListener('dragend', function(e) { rcmail.managesieve_dragend(e); })
.addEventListener('initrow', function(row) {
row.obj.onmouseover = function() { rcmail.managesieve_focus_filter(row); };
row.obj.onmouseout = function() { rcmail.managesieve_unfocus_filter(row); };
})
.init();
}
if (rcmail.gui_objects.filtersetslist) {
rcmail.filtersets_list = new rcube_list_widget(rcmail.gui_objects.filtersetslist,
{multiselect:false, draggable:false, keyboard:true});
rcmail.filtersets_list.init().focus();
if (set != null) {
$('#filterset-name').text(set);
set = rcmail.managesieve_setid(set);
rcmail.filtersets_list.select(set);
}
// attach select event after initial record was selected
rcmail.filtersets_list.addEventListener('select', function(e) { rcmail.managesieve_setselect(e); });
setcnt = rcmail.filtersets_list.rowcount;
rcmail.enable_command('plugin.managesieve-set', true);
rcmail.enable_command('plugin.managesieve-setact', setcnt > 0 && $.inArray('enable_disable_set', rcmail.env.managesieve_disabled_actions) == -1);
rcmail.enable_command('plugin.managesieve-setget', setcnt > 0 && $.inArray('download_set', rcmail.env.managesieve_disabled_actions) == -1);
rcmail.enable_command('plugin.managesieve-setdel', setcnt > 1 && $.inArray('delete_set', rcmail.env.managesieve_disabled_actions) == -1);
rcmail.enable_command('plugin.managesieve-seteditraw', setcnt > 0 && rcmail.env.raw_sieve_editor);
// Fix dragging filters over sets list
$('tr', rcmail.gui_objects.filtersetslist).each(function (i, e) { rcmail.managesieve_fixdragend(e); });
}
}
if (rcmail.gui_objects.sieveform && rcmail.env.rule_disabled)
$('#disabled').attr('checked', true);
});
};
/*********************************************************/
/********* Managesieve UI methods *********/
/*********************************************************/
rcube_webmail.prototype.managesieve_add = function()
{
this.load_managesieveframe('_nav=hide', true);
};
rcube_webmail.prototype.managesieve_del = function()
{
var id = this.filters_list.get_single_selection();
this.confirm_dialog(this.get_label('managesieve.filterdeleteconfirm'), 'delete', function(e, ref) {
var post = '_act=delete&_fid=' + ref.filters_list.rows[id].uid,
lock = ref.set_busy(true, 'loading');
ref.http_post('plugin.managesieve-action', post, lock);
});
};
rcube_webmail.prototype.managesieve_act = function()
{
var id = this.filters_list.get_single_selection(),
lock = this.set_busy(true, 'loading');
this.http_post('plugin.managesieve-action',
'_act=act&_fid='+this.filters_list.rows[id].uid, lock);
};
// Filter selection
rcube_webmail.prototype.managesieve_select = function(list)
{
var id = list.get_single_selection();
if (id != null) {
id = list.rows[id].uid;
this.load_managesieveframe('_fid=' + id);
}
var has_id = typeof(id) != 'undefined' && id != null;
this.enable_command('plugin.managesieve-act', has_id);
this.enable_command('plugin.managesieve-del', has_id && $.inArray('delete_rule', rcmail.env.managesieve_disabled_actions) == -1);
};
// Set selection
rcube_webmail.prototype.managesieve_setselect = function(list)
{
this.enable_command('plugin.managesieve-setdel', list.rowcount > 1 && $.inArray('delete_set', rcmail.env.managesieve_disabled_actions) == -1);
this.enable_command('plugin.managesieve-setact', list.rowcount > 0 && $.inArray('enable_disable_set', rcmail.env.managesieve_disabled_actions) == -1);
this.enable_command('plugin.managesieve-setget', list.rowcount > 0 && $.inArray('delete_set', rcmail.env.managesieve_disabled_actions) == -1);
this.enable_command('plugin.managesieve-seteditraw', list.rowcount > 0 && this.env.raw_sieve_editor);
if (rcmail.env.contextmenu_opening)
return;
this.show_contentframe(false);
this.filters_list.clear(true);
var id = list.get_single_selection();
if (id != null) {
this.managesieve_list(this.env.filtersets[id]);
$('#filterset-name').text(this.env.filtersets[id]);
}
};
rcube_webmail.prototype.managesieve_rowid = function(id)
{
var i, rows = this.filters_list.rows;
for (i in rows)
if (rows[i] != null && rows[i].uid == id)
return i;
};
// Returns set's identifier
rcube_webmail.prototype.managesieve_setid = function(name)
{
for (var i in this.env.filtersets)
if (this.env.filtersets[i] == name)
return i;
};
// Filters listing request
rcube_webmail.prototype.managesieve_list = function(script)
{
var lock = this.set_busy(true, 'loading');
this.http_post('plugin.managesieve-action', '_act=list&_set='+urlencode(script), lock);
};
// Script download request
rcube_webmail.prototype.managesieve_setget = function()
{
var id = this.filtersets_list.get_single_selection(),
script = this.env.filtersets[id];
this.goto_url('plugin.managesieve-action', {_act: 'setget', _set: script}, false, true);
};
// Set activate/deactivate request
rcube_webmail.prototype.managesieve_setact = function()
{
var id = this.filtersets_list.get_single_selection(),
lock = this.set_busy(true, 'loading'),
script = this.env.filtersets[id],
action = $('#rcmrow'+id).hasClass('disabled') ? 'setact' : 'deact';
this.http_post('plugin.managesieve-action', '_act='+action+'&_set='+urlencode(script), lock);
};
// Set delete request
rcube_webmail.prototype.managesieve_setdel = function()
{
var id = this.filtersets_list.get_single_selection();
this.confirm_dialog(this.get_label('managesieve.setdeleteconfirm'), 'delete', function(e, ref) {
var script = ref.env.filtersets[id],
lock = ref.set_busy(true, 'loading');
ref.http_post('plugin.managesieve-action', '_act=setdel&_set=' + urlencode(script), lock);
});
};
// Set edit raw request
rcube_webmail.prototype.managesieve_seteditraw = function()
{
var id = this.filtersets_list.get_single_selection(),
script = this.env.filtersets[id];
this.load_managesieveframe('_nav=hide&_seteditraw=1&_set=' + urlencode(script), true);
};
// Set add request
rcube_webmail.prototype.managesieve_setadd = function()
{
this.load_managesieveframe('_nav=hide&_newset=1', true);
};
rcube_webmail.prototype.managesieve_updatelist = function(action, o)
{
this.set_busy(true);
switch (action) {
// Delete filter row
case 'del':
var id = o.id, list = this.filters_list;
list.remove_row(this.managesieve_rowid(o.id));
this.show_contentframe(false);
this.reset_filters_list();
// filter identifiers changed, fix the list
$('tr', this.filters_list.list).each(function() {
// remove hidden (deleted) rows
if (this.style.display == 'none') {
$(this).detach();
return;
}
var rowid = this.id.substr(6);
// remove all attached events
$(this).off();
// update row id
if (rowid > id) {
this.uid = rowid - 1;
$(this).attr('id', 'rcmrow' + this.uid);
}
});
list.init();
break;
// Update filter row
case 'update':
var i, row = $('#rcmrow'+this.managesieve_rowid(o.id));
if (o.name)
$('td', row).text(o.name);
if (o.disabled)
row.addClass('disabled');
else
row.removeClass('disabled');
$('#disabled', $('iframe').contents()).prop('checked', o.disabled);
break;
// Add filter row to the list
case 'add':
var list = this.filters_list,
row = $('<tr><td class="name"></td></tr>');
$('td', row).text(o.name);
row.attr('id', 'rcmrow'+o.id);
if (o.disabled)
row.addClass('disabled');
list.insert_row(row.get(0));
list.highlight_row(o.id);
this.enable_command('plugin.managesieve-del', $.inArray('delete_rule', rcmail.env.managesieve_disabled_actions) == -1);
this.enable_command('plugin.managesieve-act', true);
break;
// Filling rules list
case 'list':
var i, tr, td, el, list = this.filters_list;
if (o.clear)
list.clear();
for (i in o.list) {
el = o.list[i];
tr = document.createElement('TR');
td = document.createElement('TD');
$(td).text(el.name);
td.className = 'name';
tr.id = 'rcmrow' + el.id;
if (el['class'])
tr.className = el['class'];
tr.appendChild(td);
list.insert_row(tr);
}
if (o.set)
list.highlight_row(o.set);
else
this.enable_command('plugin.managesieve-del', 'plugin.managesieve-act', false);
break;
// Sactivate/deactivate set
case 'setact':
var id = this.managesieve_setid(o.name), row = $('#rcmrow' + id);
if (o.active) {
if (o.all)
$('tr', this.gui_objects.filtersetslist).addClass('disabled');
row.removeClass('disabled');
}
else
row.addClass('disabled');
break;
// Delete set row
case 'setdel':
var id = this.managesieve_setid(o.name);
this.filters_list.clear();
this.show_contentframe(false);
this.enable_command('plugin.managesieve-setdel', 'plugin.managesieve-setact',
'plugin.managesieve-setget', 'plugin.managesieve-seteditraw', false);
this.filtersets_list.remove_row(id, true);
delete this.env.filtersets[id];
break;
// Create set row
case 'setadd':
var id = 'S' + new Date().getTime(),
list = this.filtersets_list,
row = $('<tr class="disabled"><td class="name"></td></tr>');
$('td', row).text(o.name);
row.attr('id', 'rcmrow'+id);
this.env.filtersets[id] = o.name;
list.insert_row(row.get(0));
// move row into its position on the list
if (o.index != list.rowcount-1) {
row.detach();
var elem = $('tr:visible', list.list).get(o.index);
row.insertBefore(elem);
}
list.select(id);
// Fix dragging filters over sets list
this.managesieve_fixdragend(row);
break;
case 'refresh':
this.reset_filters_list(true);
break;
}
this.set_busy(false);
};
// Resets filters list state
rcube_webmail.prototype.reset_filters_list = function(reload)
{
this.filters_list.clear_selection();
this.enable_command('plugin.managesieve-act', 'plugin.managesieve-del', false);
if (reload) {
var id = this.filtersets_list.get_single_selection();
this.filters_list.clear(true);
this.managesieve_list(this.env.filtersets[id]);
}
};
// load filter frame
rcube_webmail.prototype.load_managesieveframe = function(add_url, reset)
{
if (reset)
this.reset_filters_list();
if (this.env.contentframe && window.frames && window.frames[this.env.contentframe]) {
var lock = this.set_busy(true, 'loading'),
target = window.frames[this.env.contentframe];
target.location.href = this.env.comm_path
+ '&_action=plugin.managesieve-action&_framed=1&_unlock=' + lock
+ (add_url ? ('&' + add_url) : '');
}
};
// load filter frame
rcube_webmail.prototype.managesieve_dragstart = function(list)
{
var id = this.filters_list.get_single_selection();
this.drag_active = true;
this.drag_filter = id;
};
rcube_webmail.prototype.managesieve_dragend = function(e)
{
if (this.drag_active) {
if (this.drag_filter_target) {
var lock = this.set_busy(true, 'loading');
this.show_contentframe(false);
this.http_post('plugin.managesieve-action', '_act=move&_fid='+this.drag_filter
+'&_to='+this.drag_filter_target, lock);
}
this.drag_active = false;
}
};
// Fixes filters dragging over sets list
// @TODO: to be removed after implementing copying filters
rcube_webmail.prototype.managesieve_fixdragend = function(elem)
{
var p = this;
$(elem).on('mouseup' + ((bw.iphone || bw.ipad) ? ' touchend' : ''), function(e) {
if (p.drag_active)
p.filters_list.drag_mouse_up(e);
});
};
rcube_webmail.prototype.managesieve_focus_filter = function(row)
{
var id = row.id.replace(/^rcmrow/, '');
if (this.drag_active && id != this.drag_filter) {
this.drag_filter_target = id;
$(row.obj).addClass(id < this.drag_filter ? 'filtermoveup' : 'filtermovedown');
}
};
rcube_webmail.prototype.managesieve_unfocus_filter = function(row)
{
if (this.drag_active) {
$(row.obj).removeClass('filtermoveup filtermovedown');
this.drag_filter_target = null;
}
};
/*********************************************************/
/********* Filter Form methods *********/
/*********************************************************/
// Form submition
rcube_webmail.prototype.managesieve_save = function()
{
if (this.env.action == 'plugin.managesieve-vacation') {
var data = $(this.gui_objects.sieveform).serialize();
this.http_post('plugin.managesieve-vacation', data, this.display_message(this.get_label('managesieve.vacation.saving'), 'loading'));
return;
}
if (this.env.action == 'plugin.managesieve-forward') {
var data = $(this.gui_objects.sieveform).serialize();
this.http_post('plugin.managesieve-forward', data, this.display_message(this.get_label('managesieve.forward.saving'), 'loading'));
return;
}
if (this.gui_objects.sieveform) {
if (parent.rcmail && parent.rcmail.filters_list && this.gui_objects.sieveform.name != 'filtersetform') {
var id = parent.rcmail.filters_list.get_single_selection();
if (id != null)
this.gui_objects.sieveform.elements['_fid'].value = parent.rcmail.filters_list.rows[id].uid;
}
this.gui_objects.sieveform.submit();
}
else if (this.gui_objects.sievesetrawform) {
this.gui_objects.sievesetrawform.submit();
}
};
// Operations on filters form
rcube_webmail.prototype.managesieve_ruleadd = function(id)
{
this.http_post('plugin.managesieve-action', '_act=ruleadd&_rid='+id);
};
rcube_webmail.prototype.managesieve_rulefill = function(content, id, after)
{
if (content != '') {
// create new element
var div = $('#rules')[0],
row = $('<div>').attr({'class': 'rulerow', id: 'rulerow'+id})
.html(content);
this.managesieve_insertrow(div, row, after);
// initialize smart list inputs
$('textarea[data-type="list"]', row).each(function() {
smart_field_init(this);
});
this.managesieve_formbuttons(div);
}
};
rcube_webmail.prototype.managesieve_ruledel = function(id)
{
if ($('#ruledel'+id).hasClass('disabled'))
return;
this.confirm_dialog(this.get_label('managesieve.ruledeleteconfirm'), 'delete', function(e, ref) {
var row = document.getElementById('rulerow'+id);
row.parentNode.removeChild(row);
ref.managesieve_formbuttons(document.getElementById('rules'));
});
};
rcube_webmail.prototype.managesieve_actionadd = function(id)
{
this.http_post('plugin.managesieve-action', '_act=actionadd&_aid='+id);
};
rcube_webmail.prototype.managesieve_actionfill = function(content, id, after)
{
if (content != '') {
var div = $('#actions')[0],
row = $('<div>').attr({'class': 'actionrow', id: 'actionrow'+id})
.html(content);
this.managesieve_insertrow(div, row, after);
// initialize smart list inputs
$('textarea[data-type="list"]', row).each(function() {
smart_field_init(this);
});
this.managesieve_formbuttons(div);
}
};
rcube_webmail.prototype.managesieve_actiondel = function(id)
{
if ($('#actiondel'+id).hasClass('disabled'))
return;
this.confirm_dialog(this.get_label('managesieve.actiondeleteconfirm'), 'delete', function(e, ref) {
var row = document.getElementById('actionrow'+id);
row.parentNode.removeChild(row);
ref.managesieve_formbuttons(document.getElementById('actions'));
});
};
// insert rule/action row in specified place on the list
rcube_webmail.prototype.managesieve_insertrow = function(div, row, after)
{
var node = $('#' + ($(div).attr('id') == 'rules' ? 'rulerow' : 'actionrow') + after)[0];
if (node)
$(row).insertAfter(node);
else
$(div).append(row);
this.triggerEvent('managesieve.insertrow', {obj: row});
};
// update Delete buttons status
rcube_webmail.prototype.managesieve_formbuttons = function(div)
{
var buttons = $('a.delete', div);
buttons.removeClass('disabled');
if (buttons.length == 1)
buttons.addClass('disabled');
};
// update vacation addresses field with user identities
rcube_webmail.prototype.managesieve_vacation_addresses = function(id)
{
var lock = this.set_busy(true, 'loading');
this.http_post('plugin.managesieve-action', {_act: 'addresses', _aid: id}, lock);
};
// update vacation addresses field with user identities
rcube_webmail.prototype.managesieve_vacation_addresses_update = function(id, addresses)
{
var field = $('#vacation_addresses,#action_addresses' + (id || ''));
smart_field_reset(field.get(0), addresses);
};
function rule_header_select(id)
{
var is_header,
obj = document.getElementById('header' + id),
size = document.getElementById('rule_size' + id),
msg = document.getElementById('rule_message' + id),
op = document.getElementById('rule_op' + id),
header = document.getElementById('custom_header' + id + '_list'),
custstr = document.getElementById('custom_var' + id + '_list'),
mod = document.getElementById('rule_mod' + id),
trans = document.getElementById('rule_trans' + id),
comp = document.getElementById('rule_comp' + id),
mime = document.getElementById('rule_mime' + id),
mime_part = document.getElementById('rule_mime_part' + id),
datepart = document.getElementById('rule_date_part' + id),
dateheader = document.getElementById('rule_date_header_div' + id),
rule = $('#rule_op' + id),
h = obj.value,
set = [op, header, custstr, mod, trans, comp, size, mime, mime_part];
if (h == 'size') {
if (msg) set.push(msg);
$.each(set, function() { if (this != window) this.style.display = 'none'; });
size.style.display = '';
}
else if (h == 'message' && msg) {
$.each(set, function() { if (this != window) this.style.display = 'none'; });
msg.style.display = '';
}
else {
is_header = h != 'body' && h != 'currentdate' && h != 'date' && h != 'string';
header.style.display = h != '...' ? 'none' : '';
custstr.style.display = h != 'string' ? 'none' : '';
size.style.display = 'none';
op.style.display = '';
comp.style.display = '';
mod.style.display = is_header ? '' : 'none';
trans.style.display = h == 'body' ? '' : 'none';
if (mime)
mime.style.display = is_header ? '' : 'none';
if (mime_part)
mime_part.style.display = is_header ? '' : 'none';
if (msg)
msg.style.display = h == 'message' ? '' : 'none';
}
if (datepart)
datepart.style.display = h == 'currentdate' || h == 'date' ? 'inline' : 'none';
if (dateheader)
dateheader.style.display = h == 'date' ? '' : 'none';
$('[value="exists"],[value="notexists"]', rule).prop('disabled', h == 'string');
if (!rule.val())
rule.val('contains');
rule_op_select(op, id, h);
rule_mod_select(id, h);
rule_mime_select(id);
obj.style.width = h == '...' ? '40px' : '';
};
function rule_op_select(obj, id, header)
{
var target = document.getElementById('rule_target' + id + '_list');
if (!header)
header = document.getElementById('header' + id).value;
target.style.display = obj.value.match(/^(exists|notexists)$/) || header.match(/^(size|message)$/) ? 'none' : '';
};
function rule_trans_select(id)
{
var obj = document.getElementById('rule_trans_op' + id),
target = document.getElementById('rule_trans_type' + id);
target.style.display = obj.value != 'content' ? 'none' : 'inline';
};
function rule_mod_select(id, header)
{
var obj = document.getElementById('rule_mod_op' + id),
target = document.getElementById('rule_mod_type' + id),
duplicate = document.getElementById('rule_duplicate_div' + id),
index = document.getElementById('rule_index_div' + id);
if (!header)
header = document.getElementById('header' + id).value;
target.style.display = obj.value != 'address' && obj.value != 'envelope' ? 'none' : '';
if (index)
index.style.display = !header.match(/^(body|currentdate|size|message|string)$/) && obj.value != 'envelope' ? '' : 'none';
if (duplicate)
duplicate.style.display = header == 'message' ? '' : 'none';
};
function rule_join_radio(value)
{
$('#rules').css('display', value == 'any' ? 'none' : 'block');
};
function rule_adv_switch(id, elem)
{
var elem = $(elem), enabled = elem.hasClass('hide'), adv = $('#rule_advanced'+id);
if (enabled) {
adv.get(0).style.display = 'none';
elem.removeClass('hide').addClass('show');
}
else {
adv.get(0).style.display = '';
elem.removeClass('show').addClass('hide');
}
};
function rule_mime_select(id)
{
var elem = $('#rule_mime_type' + id),
param_elem = $('#rule_mime_param' + id + '_list');
if (param_elem.length)
param_elem[0].style.display = elem.val() == 'param' ? '' : 'none';
};
function action_type_select(id)
{
var obj = document.getElementById('action_type' + id),
v = obj.value, enabled = {},
elems = {
mailbox: document.getElementById('action_mailbox' + id),
target: document.getElementById('redirect_target' + id),
target_area: document.getElementById('action_target_area' + id),
flags: document.getElementById('action_flags' + id),
vacation: document.getElementById('action_vacation' + id),
forward: document.getElementById('action_forward' + id),
set: document.getElementById('action_set' + id),
notify: document.getElementById('action_notify' + id),
addheader: document.getElementById('action_addheader' + id),
deleteheader: document.getElementById('action_deleteheader' + id)
};
if (v == 'fileinto' || v == 'fileinto_copy') {
enabled.mailbox = 1;
}
else if (v == 'redirect' || v == 'redirect_copy') {
enabled.target = 1;
}
else if (v.match(/^reject|ereject$/)) {
enabled.target_area = 1;
}
else if (v.match(/^(add|set|remove)flag$/)) {
enabled.flags = 1;
}
else if (v.match(/^(vacation|forward|set|notify|addheader|deleteheader)$/)) {
enabled[v] = 1;
}
for (var x in elems) {
if (elems[x])
elems[x].style.display = !enabled[x] ? 'none' : 'inline';
}
};
function vacation_action_select()
{
var selected = $('#vacation_action').val();
$('#action_target_span')[selected == 'discard' || selected == 'keep' ? 'hide' : 'show']();
};
// Inititalizes smart list input
function smart_field_init(field)
{
if (window.UI && UI.smart_field_init)
return UI.smart_field_init(field);
var id = field.id + '_list',
area = $('<span class="listarea"></span>'),
list = field.value ? field.value.split("\n") : [''];
if ($('#'+id).length)
return;
// add input rows
$.each(list, function(i, v) {
area.append(smart_field_row(v, field.name, i, $(field).data('size')));
});
area.attr('id', id);
field = $(field);
if (field.attr('disabled'))
area.hide();
// disable the original field anyway, we don't want it in POST
else
field.prop('disabled', true);
if (field.data('hidden'))
area.hide();
field.after(area);
if (field.hasClass('error')) {
area.addClass('error');
rcmail.managesieve_tip_register([[id, field.data('tip-class'), field.data('tip-msg')]]);
}
};
function smart_field_row(value, name, idx, size)
{
// build row element content
var input, content = '<span class="listelement">'
+ '<span class="reset"></span><input type="text"></span>',
elem = $(content),
attrs = {value: value, name: name + '[]'};
if (size)
attrs.size = size;
input = $('input', elem).attr(attrs).keydown(function(e) {
var input = $(this);
// element creation event (on Enter)
if (e.which == 13) {
var name = input.attr('name').replace(/\[\]$/, ''),
dt = (new Date()).getTime(),
elem = smart_field_row('', name, dt, size);
input.parent().after(elem);
$('input', elem).focus();
}
// backspace or delete: remove input, focus previous one
else if ((e.which == 8 || e.which == 46) && input.val() == '') {
var parent = input.parent(), siblings = parent.parent().children();
if (siblings.length > 1) {
if (parent.prev().length)
parent.prev().children('input').focus();
else
parent.next().children('input').focus();
parent.remove();
return false;
}
}
});
// element deletion event
$('span[class="reset"]', elem).click(function() {
var span = $(this.parentNode);
if (span.parent().children().length > 1)
span.remove();
else
$('input', span).val('').focus();
});
return elem;
}
// Reset and fill the smart list input with new data
function smart_field_reset(field, data)
{
if (window.UI && UI.smart_field_reset)
return UI.smart_field_reset(field, data);
var id = field.id + '_list',
list = data.length ? data : [''];
area = $('#' + id);
area.empty();
// add input rows
$.each(list, function(i, v) {
area.append(smart_field_row(v, field.name, i, $(field).data('size')));
});
}
// Register onmouse(leave/enter) events for tips on specified form element
rcube_webmail.prototype.managesieve_tip_register = function(tips)
{
if (window.UI && UI.form_errors)
return UI.form_errors(tips);
var n, framed = parent.rcmail,
tip = framed ? parent.rcmail.env.ms_tip_layer : rcmail.env.ms_tip_layer;
for (n in tips) {
$('#'+tips[n][0])
.data('tip-class', tips[n][1])
.data('tip-msg', tips[n][2])
.mouseleave(function(e) { tip.hide(); })
.mouseenter(function(e) {
var elem = $(this),
offset = elem.offset(),
left = offset.left,
top = offset.top - 12,
minwidth = elem.width(),
span = $('<span>').addClass(elem.data('tip-class')).text(elem.data('tip-msg'));
if (framed) {
offset = $((rcmail.env.task == 'mail' ? '#sievefilterform > iframe' : '#filter-box'), parent.document).offset();
top += offset.top;
left += offset.left;
}
tip.html('').append(span);
top -= tip.height();
tip.css({left: left, top: top, minWidth: (minwidth-2) + 'px'}).show();
});
}
};
// format time string
function sieve_formattime(hour, minutes)
{
var i, c, h, time = '', format = rcmail.env.time_format || 'H:i';
for (i=0; i<format.length; i++) {
c = format.charAt(i);
switch (c) {
case 'a': time += hour >= 12 ? 'pm' : 'am'; break;
case 'A': time += hour >= 12 ? 'PM' : 'AM'; break;
case 'g':
case 'h':
h = hour == 0 ? 12 : hour > 12 ? hour - 12 : hour;
time += (c == 'h' && hour < 10 ? '0' : '') + hour;
break;
case 'G': time += hour; break;
case 'H': time += (hour < 10 ? '0' : '') + hour; break;
case 'i': time += (minutes < 10 ? '0' : '') + minutes; break;
case 's': time += '00';
default: time += c;
}
}
return time;
}
function sieve_form_init()
{
var form = rcmail.gui_objects.sieveform;
// resize dialog window
if (rcmail.env.action == 'plugin.managesieve' && rcmail.env.task == 'mail') {
parent.rcmail.managesieve_dialog_resize(form);
}
$('input[type="text"]', form).first().focus();
// initialize smart list inputs
$('textarea[data-type="list"]', form).each(function() {
smart_field_init(this);
});
// initialize rules form(s)
$('[name^="_header"]', form).each(function() {
if (/([0-9]+)$/.test(this.id)) {
rule_header_select(RegExp.$1);
}
});
// enable date pickers on date fields
if ($.datepicker && rcmail.env.date_format) {
$.datepicker.setDefaults({
dateFormat: rcmail.env.date_format,
changeMonth: true,
showOtherMonths: true,
selectOtherMonths: true,
onSelect: function(dateText) { $(this).focus().val(dateText); }
});
$('input.datepicker').datepicker();
}
// configure drop-down menu on time input fields based on jquery UI autocomplete
$('#vacation_timefrom, #vacation_timeto')
.attr('autocomplete', "off")
.autocomplete({
delay: 100,
minLength: 1,
source: function(p, callback) {
var h, result = [];
for (h = 0; h < 24; h++)
result.push(sieve_formattime(h, 0));
result.push(sieve_formattime(23, 59));
return callback(result);
},
open: function(event, ui) {
// scroll to current time
var $this = $(this), val = $this.val(),
widget = $this.autocomplete('widget').css('width', '10em'),
menu = $this.data('ui-autocomplete').menu;
if (val && val.length)
widget.children().each(function() {
var li = $(this);
if (li.text().indexOf(val) == 0)
menu._scrollIntoView(li);
});
},
select: function(event, ui) {
$(this).val(ui.item.value);
return false;
}
})
.click(function() { // show drop-down upon clicks
$(this).autocomplete('search', $(this).val() || ' ');
})
// display advanced controls when contain errors
$('input.error').each(function() {
if (String(this.id).match(/([0-9]+)$/)) {
$('#ruleadv' + RegExp.$1 + '.show').click();
}
});
}
/*********************************************************/
/********* RAW editor methods *********/
/*********************************************************/
var cmeditor;
function cmCreateErrorElem(msg)
{
var marker = document.createElement("div");
marker.style.color = "#822";
marker.innerHTML = "●";
marker.title = msg;
return marker;
}
function cmScrollToError()
{
var line = $('.CodeMirror-lines .line-error'),
scroll = $('.CodeMirror-scroll'),
h = line.parent();
scroll.scrollTop(line.offset().top - scroll.offset().top - Math.round(scroll.height()/2));
}
function sieve_raw_editor_init()
{
var textArea = document.getElementById('rawfiltersettxt');
if (textArea && !cmeditor) {
cmeditor = CodeMirror.fromTextArea(textArea, {
mode: 'sieve',
lineNumbers: true,
gutters: ["CodeMirror-linenumbers", "errorGutter"],
styleActiveLine: true
});
// fetching errors from environment and setting the line background
// and a gutter element with the error message accordingly
$.each(rcmail.env.sieve_errors || [], function(i, err) {
var lineNo = Number(err.line) - 1;
cmeditor.addLineClass(lineNo, 'background', 'line-error');
cmeditor.setGutterMarker(lineNo, 'errorGutter', cmCreateErrorElem(err.msg));
if (!i) cmScrollToError();
});
}
}
/*********************************************************/
/********* Mail UI methods *********/
/*********************************************************/
rcube_webmail.prototype.managesieve_create = function(force)
{
if (!force && this.env.action != 'show') {
var uid = this.message_list.get_single_selection(),
lock = this.set_busy(true, 'loading');
this.http_post('plugin.managesieve-action', {_uid: uid}, lock);
return;
}
if (!this.env.sieve_headers || !this.env.sieve_headers.length)
return;
var i, buttons = {},
title = this.get_label('managesieve.newfilter'),
dialog = $('<div id="sievefilterform" class="propform"></div>'),
props = {minWidth: 600, minHeight: 250, height: 300};
// build dialog window content
dialog.append($('<fieldset>')
.append($('<legend>').text(this.get_label('managesieve.usedata')))
.append($('<ul class="proplist">'))
);
$.each(this.env.sieve_headers, function(i, v) {
var attr = {type: 'checkbox', name: 'headers[]', id: 'sievehdr' + i, value: i, checked: true},
label = rcmail.env.sieve_headers[i][0] + ': ' + rcmail.env.sieve_headers[i][1];
$('ul', dialog).append($('<li>')
.append($('<input>').attr(attr))
.append($('<label>').attr('for', 'sievehdr' + i).text(label))
);
});
// [Next Step] button action
buttons[this.get_label('managesieve.nextstep')] = function () {
// check if there's at least one checkbox checked
var hdrs = $('input[name="headers[]"]:checked', dialog);
if (!hdrs.length) {
rcmail.alert_dialog(rcmail.get_label('managesieve.nodata'));
return;
}
// build frame URL
var url = rcmail.get_task_url('mail');
url = rcmail.add_url(url, '_action', 'plugin.managesieve');
url = rcmail.add_url(url, '_framed', 1);
hdrs.map(function() {
var val = rcmail.env.sieve_headers[this.value];
url = rcmail.add_url(url, 'r['+this.value+']', val[0]+':'+val[1]);
});
// load form in the iframe
var buttons = {}, iframe = $('<iframe>').attr({src: url, frameborder: 0});
// Change [Next Step] button with [Save] button
buttons[rcmail.get_label('save')] = function() {
var win = $('iframe', dialog).get(0).contentWindow;
win.rcmail.managesieve_save();
};
buttons[rcmail.get_label('cancel')] = function() { $(this).dialog('destroy'); };
dialog.dialog('destroy');
rcmail.env.managesieve_dialog = dialog = rcmail.show_popup_dialog(
iframe, title, buttons, $.extend(props, {button_classes: ['mainaction save', 'cancel']})
);
};
buttons[this.get_label('cancel')] = function() { $(this).dialog('destroy'); };
this.env.managesieve_dialog = dialog = this.show_popup_dialog(
dialog, title, buttons, $.extend(props, {button_classes: ['mainaction next', 'cancel']})
);
}
rcube_webmail.prototype.managesieve_dialog_close = function()
{
this.env.managesieve_dialog.dialog('destroy');
}
rcube_webmail.prototype.managesieve_dialog_resize = function(o)
{
var dialog = this.env.managesieve_dialog,
win = $(window), form = $(o);
width = $('fieldset', o).first().width(), // fieldset width is more appropriate here
height = form.height(),
w = win.width(), h = win.height();
if (height < 100)
return;
dialog.dialog('option', { height: Math.min(h-20, height+120), width: Math.min(w-20, width+65) });
}
diff --git a/plugins/managesieve/managesieve.php b/plugins/managesieve/managesieve.php
index 3c71080eb..e69f7c3da 100644
--- a/plugins/managesieve/managesieve.php
+++ b/plugins/managesieve/managesieve.php
@@ -1,306 +1,306 @@
<?php
/**
* Managesieve (Sieve Filters)
*
* Plugin that adds a possibility to manage Sieve filters in Thunderbird's style.
* It's clickable interface which operates on text scripts and communicates
* with server using managesieve protocol. Adds Filters tab in Settings.
*
* @author Aleksander Machniak <alec@alec.pl>
*
* Configuration (see config.inc.php.dist)
*
- * Copyright (C) 2008-2013, The Roundcube Dev Team
- * Copyright (C) 2011-2013, Kolab Systems AG
+ * Copyright (C) The Roundcube Dev Team
+ * Copyright (C) Kolab Systems AG
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class managesieve extends rcube_plugin
{
public $task = 'mail|settings';
private $rc;
private $engine;
function init()
{
$this->rc = rcube::get_instance();
$this->load_config();
$allowed_hosts = $this->rc->config->get('managesieve_allowed_hosts');
if (!empty($allowed_hosts) && !in_array($_SESSION['storage_host'], (array) $allowed_hosts)) {
return;
}
// register actions
$this->register_action('plugin.managesieve', array($this, 'managesieve_actions'));
$this->register_action('plugin.managesieve-action', array($this, 'managesieve_actions'));
$this->register_action('plugin.managesieve-vacation', array($this, 'managesieve_actions'));
$this->register_action('plugin.managesieve-forward', array($this, 'managesieve_actions'));
$this->register_action('plugin.managesieve-save', array($this, 'managesieve_save'));
$this->register_action('plugin.managesieve-saveraw', array($this, 'managesieve_saveraw'));
if ($this->rc->task == 'settings') {
$this->add_hook('settings_actions', array($this, 'settings_actions'));
$this->init_ui();
}
else if ($this->rc->task == 'mail') {
// register message hook
if ($this->rc->action == 'show') {
$this->add_hook('message_headers_output', array($this, 'mail_headers'));
}
// inject Create Filter popup stuff
if (empty($this->rc->action) || $this->rc->action == 'show'
|| strpos($this->rc->action, 'plugin.managesieve') === 0
) {
$this->mail_task_handler();
}
}
}
/**
* Initializes plugin's UI (localization, js script)
*/
function init_ui()
{
if ($this->ui_initialized) {
return;
}
// load localization
$this->add_texts('localization/');
$sieve_action = strpos($this->rc->action, 'plugin.managesieve') === 0;
if ($this->rc->task == 'mail' || $sieve_action) {
$this->include_script('managesieve.js');
}
// include styles
$skin_path = $this->local_skin_path();
if ($sieve_action || ($this->rc->task == 'settings' && empty($_REQUEST['_framed']))) {
$this->include_stylesheet("$skin_path/managesieve.css");
}
else if ($this->rc->task == 'mail') {
$this->include_stylesheet("$skin_path/managesieve_mail.css");
}
$this->ui_initialized = true;
}
/**
* Adds Filters section in Settings
*/
function settings_actions($args)
{
$vacation_mode = (int) $this->rc->config->get('managesieve_vacation');
$forward_mode = (int) $this->rc->config->get('managesieve_forward');
// register Filters action
if ($vacation_mode != 2 && $forward_mode != 2) {
$args['actions'][] = array(
'action' => 'plugin.managesieve',
'class' => 'filter',
'label' => 'filters',
'domain' => 'managesieve',
'title' => 'filterstitle',
);
}
// register Vacation action
if ($vacation_mode > 0) {
$args['actions'][] = array(
'action' => 'plugin.managesieve-vacation',
'class' => 'vacation',
'label' => 'vacation',
'domain' => 'managesieve',
'title' => 'vacationtitle',
);
}
// register Forward action
if ($forward_mode > 0) {
$args['actions'][] = array(
'action' => 'plugin.managesieve-forward',
'class' => 'forward',
'label' => 'forward',
'domain' => 'managesieve',
'title' => 'forwardtitle',
);
}
return $args;
}
/**
* Add UI elements to the 'mailbox view' and 'show message' UI.
*/
function mail_task_handler()
{
// make sure we're not in ajax request
if ($this->rc->output->type != 'html') {
return;
}
// include js script and localization
$this->init_ui();
// add 'Create filter' item to message menu
$this->api->add_content(html::tag('li', null,
$this->api->output->button(array(
'command' => 'managesieve-create',
'label' => 'managesieve.filtercreate',
'type' => 'link',
'classact' => 'icon filterlink active',
'class' => 'icon filterlink',
'innerclass' => 'icon filterlink',
))), 'messagemenu');
// register some labels/messages
$this->rc->output->add_label('managesieve.newfilter', 'managesieve.usedata',
'managesieve.nodata', 'managesieve.nextstep', 'save');
$this->rc->session->remove('managesieve_current');
}
/**
* Get message headers for popup window
*/
function mail_headers($args)
{
// this hook can be executed many times
if ($this->mail_headers_done) {
return $args;
}
$this->mail_headers_done = true;
$headers = $this->parse_headers($args['headers']);
if ($this->rc->action == 'preview')
$this->rc->output->command('parent.set_env', array('sieve_headers' => $headers));
else
$this->rc->output->set_env('sieve_headers', $headers);
return $args;
}
/**
* Plugin action handler
*/
function managesieve_actions()
{
$uids = rcmail::get_uids(null, null, $multifolder, rcube_utils::INPUT_POST);
// handle fetching email headers for the new filter form
if (!empty($uids)) {
$mailbox = key($uids);
$message = new rcube_message($uids[$mailbox][0], $mailbox);
$headers = $this->parse_headers($message->headers);
$this->rc->output->set_env('sieve_headers', $headers);
$this->rc->output->command('managesieve_create', true);
$this->rc->output->send();
}
// handle other actions
$engine_type = $this->rc->action == 'plugin.managesieve-vacation' ? 'vacation' : '';
$engine_type = $this->rc->action == 'plugin.managesieve-forward' ? 'forward' : $engine_type;
$engine = $this->get_engine($engine_type);
$this->init_ui();
$engine->actions();
}
/**
* Forms save action handler
*/
function managesieve_save()
{
// load localization
$this->add_texts('localization/', array('filters','managefilters'));
// include main js script
if ($this->api->output->type == 'html') {
$this->include_script('managesieve.js');
}
$engine = $this->get_engine();
$engine->save();
}
/**
* Raw form save action handler
*/
function managesieve_saveraw()
{
$engine = $this->get_engine();
if (!$this->rc->config->get('managesieve_raw_editor', true)) {
return;
}
// load localization
$this->add_texts('localization/', array('filters','managefilters'));
$engine->saveraw();
}
/**
* Initializes engine object
*/
public function get_engine($type = null)
{
if (!$this->engine) {
// Add include path for internal classes
$include_path = $this->home . '/lib' . PATH_SEPARATOR;
$include_path .= ini_get('include_path');
set_include_path($include_path);
$class_name = 'rcube_sieve_' . ($type ?: 'engine');
$this->engine = new $class_name($this);
}
return $this->engine;
}
/**
* Extract mail headers for new filter form
*/
private function parse_headers($headers)
{
$result = array();
if ($headers->subject)
$result[] = array('Subject', rcube_mime::decode_header($headers->subject));
// @TODO: List-Id, others?
foreach (array('From', 'To') as $h) {
$hl = strtolower($h);
if ($headers->$hl) {
$list = rcube_mime::decode_address_list($headers->$hl);
foreach ($list as $item) {
if ($item['mailto']) {
$result[] = array($h, $item['mailto']);
}
}
}
}
return $result;
}
}
diff --git a/plugins/markasjunk/localization/en_US.inc b/plugins/markasjunk/localization/en_US.inc
index 9f9e5fd3f..b6ad8bc46 100644
--- a/plugins/markasjunk/localization/en_US.inc
+++ b/plugins/markasjunk/localization/en_US.inc
@@ -1,30 +1,28 @@
<?php
/*
+-----------------------------------------------------------------------+
- | plugins/markasjunk/localization/<lang>.inc |
- | |
| Localization file of the Roundcube Webmail Mark-As-Junk plugin |
- | Copyright (C) 2012-2018, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-----------------------------------------------------------------------+
For translation see https://www.transifex.com/projects/p/roundcube-webmail/resource/plugin-markasjunk/
*/
$labels = array();
$labels['buttonjunk'] = 'This message is junk';
$labels['asjunk'] = 'As junk';
$labels['markasjunk'] = 'Mark as junk';
$labels['buttonnotjunk'] = 'This message is not junk';
$labels['asnotjunk'] = 'As not junk';
$labels['markasnotjunk'] = 'Mark as not junk';
$labels['notjunk'] = 'Not junk';
$messages = array();
$messages['reportedasjunk'] = 'Successfully reported as junk';
$messages['reportedasnotjunk'] = 'Successfully reported as not junk';
diff --git a/plugins/markasjunk/markasjunk.js b/plugins/markasjunk/markasjunk.js
index 964cae8bb..378bb305f 100644
--- a/plugins/markasjunk/markasjunk.js
+++ b/plugins/markasjunk/markasjunk.js
@@ -1,132 +1,132 @@
/**
* Mark-as-Junk plugin script
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
- * Copyright (c) 2013-2018 The Roundcube Dev Team
- * Copyright (C) 2009-2018 Philip Weir
+ * Copyright (c) The Roundcube Dev Team
+ * Copyright (C) Philip Weir
*
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*/
rcube_webmail.prototype.markasjunk_mark = function(is_spam) {
var uids = this.env.uid ? [this.env.uid] : this.message_list.get_selection();
if (!uids)
return;
var lock = this.set_busy(true, 'loading');
this.http_post('plugin.markasjunk.' + (is_spam ? 'junk' : 'not_junk'), this.selection_post_data({_uid: uids}), lock);
}
rcube_webmail.prototype.rcmail_markasjunk_move = function(mbox, uids) {
var prev_uid = this.env.uid, a_uids = $.isArray(uids) ? uids : uids.split(",");
if (this.message_list && a_uids.length == 1 && !this.message_list.in_selection([a_uids[0]]))
this.env.uid = a_uids[0];
if (mbox)
this.move_messages(mbox);
else if (this.env.markasjunk_permanently_remove == true)
this.permanently_remove_messages();
else
this.delete_messages();
this.env.uid = prev_uid;
}
rcube_webmail.prototype.markasjunk_toggle_button = function() {
var spamobj = $('a.junk'),
hamobj = $('a.notjunk'),
disp = {spam: true, ham: true};
if (this.env.markasjunk_spam_only) {
disp.ham = false;
}
else if (!this.is_multifolder_listing() && this.env.markasjunk_spam_mailbox) {
if (this.env.mailbox != this.env.markasjunk_spam_mailbox)
disp.ham = false;
else
disp.spam = false;
}
// if only 1 button is visible make sure its the last one (for styling)
// allow for multiple instances of the buttons, eg toolbar and contextmenu
$.each(spamobj, function(i) {
var cur_spamobj = spamobj.eq(i),
cur_hamobj = hamobj.eq(i),
cur_index = spamobj.eq(i).index();
if (cur_spamobj.parent('li').length > 0) {
cur_spamobj = cur_spamobj.parent();
cur_hamobj = cur_hamobj.parent();
}
var evt_rtn = rcmail.triggerEvent('markasjunk-update', {objs: {spamobj: cur_spamobj, hamobj: cur_hamobj}, disp: disp});
if (evt_rtn && evt_rtn.abort)
return;
disp = evt_rtn ? evt_rtn.disp : disp;
disp.spam ? cur_spamobj.show() : cur_spamobj.hide();
disp.ham ? cur_hamobj.show() : cur_hamobj.hide();
if (disp.spam && !disp.ham) {
if (cur_index < cur_hamobj.index()) {
cur_spamobj.insertAfter(cur_hamobj);
}
}
else if (cur_index > cur_hamobj.index()) {
cur_hamobj.insertAfter(cur_spamobj);
}
});
}
rcube_webmail.prototype.markasjunk_is_spam_mbox = function() {
return !this.is_multifolder_listing() && this.env.mailbox == this.env.markasjunk_spam_mailbox;
}
if (window.rcmail) {
rcmail.addEventListener('init', function() {
// register command (directly enable in message view mode)
rcmail.register_command('plugin.markasjunk.junk', function() { rcmail.markasjunk_mark(true); }, !rcmail.markasjunk_is_spam_mbox() && rcmail.env.uid);
rcmail.register_command('plugin.markasjunk.not_junk', function() { rcmail.markasjunk_mark(false); }, rcmail.env.uid);
if (rcmail.message_list) {
rcmail.message_list.addEventListener('select', function(list) {
rcmail.enable_command('plugin.markasjunk.junk', !rcmail.markasjunk_is_spam_mbox() && list.get_selection(false).length > 0);
rcmail.enable_command('plugin.markasjunk.not_junk', list.get_selection(false).length > 0);
});
}
// make sure the correct icon is displayed even when there is no listupdate event
rcmail.markasjunk_toggle_button();
});
rcmail.addEventListener('listupdate', function() { rcmail.markasjunk_toggle_button(); });
rcmail.addEventListener('beforemoveto', function(mbox) {
if (mbox && typeof mbox === 'object')
mbox = mbox.id;
var is_spam = null;
// check if destination mbox equals junk box (and we're not already in the junk box)
if (rcmail.env.markasjunk_move_spam && mbox && mbox == rcmail.env.markasjunk_spam_mailbox && mbox != rcmail.env.mailbox)
is_spam = true;
// or if destination mbox equals ham box and we are in the junk box
else if (rcmail.env.markasjunk_move_ham && mbox && mbox == rcmail.env.markasjunk_ham_mailbox && rcmail.env.mailbox == rcmail.env.markasjunk_spam_mailbox)
is_spam = false;
if (is_spam !== null) {
rcmail.markasjunk_mark(is_spam);
return false;
}
});
}
diff --git a/plugins/markasjunk/markasjunk.php b/plugins/markasjunk/markasjunk.php
index bad4933e2..0d8e90c64 100644
--- a/plugins/markasjunk/markasjunk.php
+++ b/plugins/markasjunk/markasjunk.php
@@ -1,321 +1,321 @@
<?php
/**
* MarkAsJunk
*
* A plugin that adds a new button to the mailbox toolbar
* to mark the selected messages as Junk and move them to the Junk folder
* or to move messages in the Junk folder to the inbox - moving only the
* attachment if it is a Spamassassin spam report email
*
* @author Philip Weir
* @author Thomas Bruederli
*
- * Copyright (C) 2009-2018 The Roundcube Dev Team
- * Copyright (C) 2009-2018 Philip Weir
+ * Copyright (C) The Roundcube Dev Team
+ * Copyright (C) Philip Weir
*
* This program is a Roundcube (https://roundcube.net) plugin.
* For more information see README.md.
* For configuration see config.inc.php.dist.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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 Roundcube. If not, see https://www.gnu.org/licenses/.
*/
class markasjunk extends rcube_plugin
{
public $task = 'mail';
private $rcube;
private $spam_mbox;
private $ham_mbox;
private $flags = array(
'JUNK' => 'Junk',
'NONJUNK' => 'NonJunk'
);
public function init()
{
$this->register_action('plugin.markasjunk.junk', array($this, 'mark_message'));
$this->register_action('plugin.markasjunk.not_junk', array($this, 'mark_message'));
$this->rcube = rcube::get_instance();
$this->load_config();
$this->_load_host_config();
// Host exceptions
$hosts = $this->rcube->config->get('markasjunk_allowed_hosts');
if (!empty($hosts) && !in_array($_SESSION['storage_host'], (array) $hosts)) {
return;
}
$this->ham_mbox = $this->rcube->config->get('markasjunk_ham_mbox', 'INBOX');
$this->spam_mbox = $this->rcube->config->get('markasjunk_spam_mbox', $this->rcube->config->get('junk_mbox'));
$toolbar = $this->rcube->config->get('markasjunk_toolbar', true);
$this->_init_flags();
if ($this->rcube->action == '' || $this->rcube->action == 'show') {
$this->include_script('markasjunk.js');
$this->add_texts('localization', true);
$this->include_stylesheet($this->local_skin_path() . '/markasjunk.css');
if ($toolbar) {
// add the buttons to the main toolbar
$this->add_button(array(
'command' => 'plugin.markasjunk.junk',
'type' => 'link',
'class' => 'button buttonPas junk disabled',
'classact' => 'button junk',
'classsel' => 'button junk pressed',
'title' => 'markasjunk.buttonjunk',
'innerclass' => 'inner',
'label' => 'junk'
), 'toolbar');
$this->add_button(array(
'command' => 'plugin.markasjunk.not_junk',
'type' => 'link',
'class' => 'button buttonPas notjunk disabled',
'classact' => 'button notjunk',
'classsel' => 'button notjunk pressed',
'title' => 'markasjunk.buttonnotjunk',
'innerclass' => 'inner',
'label' => 'markasjunk.notjunk'
), 'toolbar');
}
else {
// add the buttons to the mark message menu
$this->add_button(array(
'command' => 'plugin.markasjunk.junk',
'type' => 'link-menuitem',
'label' => 'markasjunk.asjunk',
'id' => 'markasjunk',
'class' => 'icon junk',
'classact' => 'icon junk active',
'innerclass' => 'icon junk'
), 'markmenu');
$this->add_button(array(
'command' => 'plugin.markasjunk.not_junk',
'type' => 'link-menuitem',
'label' => 'markasjunk.asnotjunk',
'id' => 'markasnotjunk',
'class' => 'icon notjunk',
'classact' => 'icon notjunk active',
'innerclass' => 'icon notjunk'
), 'markmenu');
}
// add markasjunk folder settings to the env for JS
$this->rcube->output->set_env('markasjunk_ham_mailbox', $this->ham_mbox);
$this->rcube->output->set_env('markasjunk_spam_mailbox', $this->spam_mbox);
$this->rcube->output->set_env('markasjunk_move_spam', $this->rcube->config->get('markasjunk_move_spam', false));
$this->rcube->output->set_env('markasjunk_move_ham', $this->rcube->config->get('markasjunk_move_ham', false));
$this->rcube->output->set_env('markasjunk_permanently_remove', $this->rcube->config->get('markasjunk_permanently_remove', false));
$this->rcube->output->set_env('markasjunk_spam_only', $this->rcube->config->get('markasjunk_spam_only', false));
// check for init method from driver
$this->_call_driver('init');
}
}
public function mark_message()
{
$this->add_texts('localization');
$is_spam = $this->rcube->action == 'plugin.markasjunk.junk' ? true : false;
$messageset = rcmail::get_uids(null, null, $multifolder, rcube_utils::INPUT_POST);
$mbox_name = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
$dest_mbox = $is_spam ? $this->spam_mbox : $this->ham_mbox;
$result = $is_spam ? $this->_spam($messageset, $dest_mbox) : $this->_ham($messageset, $dest_mbox);
if ($result) {
if ($dest_mbox && ($mbox_name !== $dest_mbox || $multifolder)) {
$this->rcube->output->command('rcmail_markasjunk_move', $dest_mbox, $this->_messageset_to_uids($messageset, $multifolder));
}
else {
$this->rcube->output->command('command', 'list', $mbox_name);
}
$this->rcube->output->command('display_message', $is_spam ? $this->gettext('reportedasjunk') : $this->gettext('reportedasnotjunk'), 'confirmation');
}
$this->rcube->output->send();
}
public function set_flags($p)
{
$p['message_flags'] = array_merge((array) $p['message_flags'], $this->flags);
return $p;
}
private function _spam(&$messageset, $dest_mbox = null)
{
$storage = $this->rcube->get_storage();
$result = true;
foreach ($messageset as $source_mbox => &$uids) {
$storage->set_folder($source_mbox);
$result = $this->_call_driver('spam', $uids, $source_mbox, $dest_mbox);
// abort function of the driver says so
if (!$result) {
break;
}
if ($this->rcube->config->get('markasjunk_read_spam', false)) {
$storage->set_flag($uids, 'SEEN', $source_mbox);
}
if (array_key_exists('JUNK', $this->flags)) {
$storage->set_flag($uids, 'JUNK', $source_mbox);
}
if (array_key_exists('NONJUNK', $this->flags)) {
$storage->unset_flag($uids, 'NONJUNK', $source_mbox);
}
}
return $result;
}
private function _ham(&$messageset, $dest_mbox = null)
{
$storage = $this->rcube->get_storage();
$result = true;
foreach ($messageset as $source_mbox => &$uids) {
$storage->set_folder($source_mbox);
$result = $this->_call_driver('ham', $uids, $source_mbox, $dest_mbox);
// abort function of the driver says so
if (!$result) {
break;
}
if ($this->rcube->config->get('markasjunk_unread_ham', false)) {
$storage->unset_flag($uids, 'SEEN', $source_mbox);
}
if (array_key_exists('JUNK', $this->flags)) {
$storage->unset_flag($uids, 'JUNK', $source_mbox);
}
if (array_key_exists('NONJUNK', $this->flags)) {
$storage->set_flag($uids, 'NONJUNK', $source_mbox);
}
}
return $result;
}
private function _call_driver($action, &$uids = null, $source_mbox = null, $dest_mbox = null)
{
$driver_name = $this->rcube->config->get('markasjunk_learning_driver');
if (empty($driver_name)) {
return true;
}
$driver = $this->home . "/drivers/$driver_name.php";
$class = "markasjunk_$driver_name";
if (!is_readable($driver)) {
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "markasjunk plugin: Unable to open driver file $driver"
), true, false);
}
include_once $driver;
if (!class_exists($class, false) || !method_exists($class, 'spam') || !method_exists($class, 'ham')) {
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "markasjunk plugin: Broken driver: $driver"
), true, false);
}
// call the relevant function from the driver
$object = new $class();
if ($action == 'spam') {
$object->spam($uids, $source_mbox, $dest_mbox);
}
elseif ($action == 'ham') {
$object->ham($uids, $source_mbox, $dest_mbox);
}
elseif ($action == 'init' && method_exists($object, 'init')) { // method_exists check here for backwards compatibility
$object->init();
}
return $object->is_error ? false : true;
}
private function _messageset_to_uids($messageset, $multifolder)
{
$a_uids = array();
foreach ($messageset as $mbox => $uids) {
foreach ($uids as $uid) {
$a_uids[] = $multifolder ? $uid . '-' . $mbox : $uid;
}
}
return $a_uids;
}
private function _load_host_config()
{
$configs = $this->rcube->config->get('markasjunk_host_config');
if (empty($configs) || !array_key_exists($_SESSION['storage_host'], (array) $configs)) {
return;
}
$file = $configs[$_SESSION['storage_host']];
$this->load_config($file);
}
private function _init_flags()
{
$spam_flag = $this->rcube->config->get('markasjunk_spam_flag');
$ham_flag = $this->rcube->config->get('markasjunk_ham_flag');
if ($spam_flag === false) {
unset($this->flags['JUNK']);
}
elseif (!empty($spam_flag)) {
$this->flags['JUNK'] = $spam_flag;
}
if ($ham_flag === false) {
unset($this->flags['NONJUNK']);
}
elseif (!empty($ham_flag)) {
$this->flags['NONJUNK'] = $ham_flag;
}
if (count($this->flags) > 0) {
// register the ham/spam flags with the core
$this->add_hook('storage_init', array($this, 'set_flags'));
}
}
}
diff --git a/plugins/new_user_dialog/localization/en_US.inc b/plugins/new_user_dialog/localization/en_US.inc
index d508cfc9c..f0445108b 100644
--- a/plugins/new_user_dialog/localization/en_US.inc
+++ b/plugins/new_user_dialog/localization/en_US.inc
@@ -1,23 +1,19 @@
<?php
/*
+-----------------------------------------------------------------------+
- | plugins/new_user_dialog/localization/<lang>.inc |
- | |
| Localization file of the Roundcube Webmail New User Dialog plugin |
- | Copyright (C) 2012-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-----------------------------------------------------------------------+
For translation see https://www.transifex.com/projects/p/roundcube-webmail/resource/plugin-new_user_dialog/
*/
$labels = array();
$labels['identitydialogtitle'] = 'Please complete your sender identity';
$labels['identitydialoghint'] = 'This box only appears once at the first login.';
-
-?>
\ No newline at end of file
diff --git a/plugins/newmail_notifier/localization/en_US.inc b/plugins/newmail_notifier/localization/en_US.inc
index 1c4054615..c244b3ae2 100644
--- a/plugins/newmail_notifier/localization/en_US.inc
+++ b/plugins/newmail_notifier/localization/en_US.inc
@@ -1,30 +1,26 @@
<?php
/*
+-----------------------------------------------------------------------+
- | plugins/newmail_notifier/localization/<lang>.inc |
- | |
| Localization file of the Roundcube Webmail New Mail Notifier plugin |
- | Copyright (C) 2012-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-----------------------------------------------------------------------+
For translation see https://www.transifex.com/projects/p/roundcube-webmail/resource/plugin-newmail_notifier/
*/
$labels['basic'] = 'Display browser notifications on new message';
$labels['desktop'] = 'Display desktop notifications on new message';
$labels['sound'] = 'Play the sound on new message';
$labels['test'] = 'Test';
$labels['title'] = 'New Email!';
$labels['body'] = 'You\'ve received a new message.';
$labels['testbody'] = 'This is a test notification.';
$labels['desktopdisabled'] = 'Desktop notifications are disabled in your browser.';
$labels['desktopunsupported'] = 'Your browser does not support desktop notifications.';
$labels['desktoptimeout'] = 'Close desktop notification';
-
-?>
diff --git a/plugins/newmail_notifier/newmail_notifier.js b/plugins/newmail_notifier/newmail_notifier.js
index 5e95284cc..f06d6ea49 100644
--- a/plugins/newmail_notifier/newmail_notifier.js
+++ b/plugins/newmail_notifier/newmail_notifier.js
@@ -1,160 +1,160 @@
/**
* New Mail Notifier plugin script
*
* @author Aleksander Machniak <alec@alec.pl>
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
- * Copyright (c) 2013-2016, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*/
if (window.rcmail && rcmail.env.task == 'mail') {
rcmail.addEventListener('plugin.newmail_notifier', newmail_notifier_run)
.addEventListener('actionbefore', newmail_notifier_stop)
.addEventListener('init', function() {
// bind to messages list select event, so favicon will be reverted on message preview too
if (rcmail.message_list)
rcmail.message_list.addEventListener('select', newmail_notifier_stop);
});
}
// Executes notification methods
function newmail_notifier_run(prop)
{
if (prop.basic)
newmail_notifier_basic();
if (prop.sound)
newmail_notifier_sound();
if (prop.desktop)
newmail_notifier_desktop(rcmail.get_label('body', 'newmail_notifier'));
}
// Stops notification
function newmail_notifier_stop(prop)
{
// revert original favicon
if (rcmail.env.favicon_href && rcmail.env.favicon_changed && (!prop || prop.action != 'check-recent')) {
$('<link rel="shortcut icon" href="'+rcmail.env.favicon_href+'"/>').replaceAll('link[rel="shortcut icon"]');
rcmail.env.favicon_changed = 0;
}
// Remove IE icon overlay if we're pinned to Taskbar
try {
if(window.external.msIsSiteMode()) {
window.external.msSiteModeClearIconOverlay();
}
} catch(e) {}
}
// Basic notification: window.focus and favicon change
function newmail_notifier_basic()
{
var w = rcmail.is_framed() ? window.parent : window,
path = rcmail.assets_path('plugins/newmail_notifier');
w.focus();
// we cannot simply change a href attribute, we must to replace the link element (at least in FF)
var link = $('<link rel="shortcut icon">').attr('href', path + '/favicon.ico'),
oldlink = $('link[rel="shortcut icon"]', w.document);
if (!rcmail.env.favicon_href)
rcmail.env.favicon_href = oldlink.attr('href');
rcmail.env.favicon_changed = 1;
link.replaceAll(oldlink);
// Add IE icon overlay if we're pinned to Taskbar
try {
if (window.external.msIsSiteMode()) {
window.external.msSiteModeSetIconOverlay(path + '/overlay.ico', rcmail.get_label('title', 'newmail_notifier'));
}
} catch(e) {}
}
// Sound notification
function newmail_notifier_sound()
{
var elem, src = rcmail.assets_path('plugins/newmail_notifier/sound'),
plugin = navigator.mimeTypes ? navigator.mimeTypes['audio/mp3'] : {};
// Internet Explorer does not support wav files,
// support in other browsers depends on enabled plugins,
// so we use wav as a fallback
src += bw.ie || (plugin && plugin.enabledPlugin) ? '.mp3' : '.wav';
// HTML5
try {
elem = $('<audio>').attr('src', src);
elem.get(0).play();
}
// old method
catch (e) {
elem = $('<embed id="sound" src="' + src + '" hidden=true autostart=true loop=false />');
elem.appendTo($('body'));
window.setTimeout("$('#sound').remove()", 5000);
}
}
// Desktop notification
// - Require window.Notification API support (Chrome 22+ or Firefox 22+)
function newmail_notifier_desktop(body, disabled_callback)
{
var timeout = rcmail.env.newmail_notifier_timeout || 10,
icon = rcmail.assets_path('plugins/newmail_notifier/mail.png'),
success_callback = function() {
var popup = new window.Notification(rcmail.get_label('title', 'newmail_notifier'), {
dir: "auto",
lang: "",
body: body,
tag: "newmail_notifier",
icon: icon
});
popup.onclick = function() { this.close(); };
setTimeout(function() { popup.close(); }, timeout * 1000);
};
try {
window.Notification.requestPermission(function(perm) {
if (perm == 'granted')
success_callback();
else if (perm == 'denied' && disabled_callback)
disabled_callback();
});
return true;
}
catch (e) {
return false;
}
}
function newmail_notifier_test_desktop()
{
var status = newmail_notifier_desktop(rcmail.get_label('testbody', 'newmail_notifier'), function() {
rcmail.display_message(rcmail.get_label('desktopdisabled', 'newmail_notifier'), 'error');
});
if (!status) {
rcmail.display_message(rcmail.get_label('desktopunsupported', 'newmail_notifier'), 'error');
}
}
function newmail_notifier_test_basic()
{
newmail_notifier_basic();
}
function newmail_notifier_test_sound()
{
newmail_notifier_sound();
}
diff --git a/plugins/newmail_notifier/newmail_notifier.php b/plugins/newmail_notifier/newmail_notifier.php
index 939d118d3..0e848e68c 100644
--- a/plugins/newmail_notifier/newmail_notifier.php
+++ b/plugins/newmail_notifier/newmail_notifier.php
@@ -1,215 +1,215 @@
<?php
/**
* New Mail Notifier plugin
*
* Supports three methods of notification:
* 1. Basic - focus browser window and change favicon
* 2. Sound - play wav file
* 3. Desktop - display desktop notification (using window.Notification API)
*
* @author Aleksander Machniak <alec@alec.pl>
*
- * Copyright (C) 2011-2016, Kolab Systems AG
+ * Copyright (C) Kolab Systems AG
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class newmail_notifier extends rcube_plugin
{
public $task = 'mail|settings';
private $rc;
private $notified;
private $opt = array();
private $exceptions = array();
/**
* Plugin initialization
*/
function init()
{
$this->rc = rcmail::get_instance();
// Preferences hooks
if ($this->rc->task == 'settings') {
$this->add_hook('preferences_list', array($this, 'prefs_list'));
$this->add_hook('preferences_save', array($this, 'prefs_save'));
}
else { // if ($this->rc->task == 'mail') {
// add script when not in ajax and not in frame and only in main window
if ($this->rc->output->type == 'html' && empty($_REQUEST['_framed']) && $this->rc->action == '') {
$this->add_texts('localization/');
$this->rc->output->add_label('newmail_notifier.title', 'newmail_notifier.body');
$this->include_script('newmail_notifier.js');
}
if ($this->rc->action == 'refresh') {
// Load configuration
$this->load_config();
$this->opt['basic'] = $this->rc->config->get('newmail_notifier_basic');
$this->opt['sound'] = $this->rc->config->get('newmail_notifier_sound');
$this->opt['desktop'] = $this->rc->config->get('newmail_notifier_desktop');
if (!empty($this->opt)) {
// Get folders to skip checking for
$exceptions = array('drafts_mbox', 'sent_mbox', 'trash_mbox', 'junk_mbox');
foreach ($exceptions as $folder) {
$folder = $this->rc->config->get($folder);
if (strlen($folder) && $folder != 'INBOX') {
$this->exceptions[] = $folder;
}
}
$this->add_hook('new_messages', array($this, 'notify'));
}
}
}
}
/**
* Handler for user preferences form (preferences_list hook)
*/
function prefs_list($args)
{
if ($args['section'] != 'mailbox') {
return $args;
}
// Load configuration
$this->load_config();
// Load localization and configuration
$this->add_texts('localization/');
if (!empty($_REQUEST['_framed'])) {
$this->rc->output->add_label('newmail_notifier.title', 'newmail_notifier.testbody',
'newmail_notifier.desktopunsupported', 'newmail_notifier.desktopenabled', 'newmail_notifier.desktopdisabled');
$this->include_script('newmail_notifier.js');
}
// Check that configuration is not disabled
$dont_override = (array) $this->rc->config->get('dont_override', array());
foreach (array('basic', 'desktop', 'sound') as $type) {
$key = 'newmail_notifier_' . $type;
if (!in_array($key, $dont_override)) {
$field_id = '_' . $key;
$input = new html_checkbox(array('name' => $field_id, 'id' => $field_id, 'value' => 1));
$content = $input->show($this->rc->config->get($key))
. ' ' . html::a(array('href' => '#', 'onclick' => 'newmail_notifier_test_'.$type.'()'),
$this->gettext('test'));
$args['blocks']['new_message']['options'][$key] = array(
'title' => html::label($field_id, rcube::Q($this->gettext($type))),
'content' => $content
);
}
}
$type = 'desktop_timeout';
$key = 'newmail_notifier_' . $type;
if (!in_array($key, $dont_override)) {
$field_id = '_' . $key;
$select = new html_select(array('name' => $field_id, 'id' => $field_id));
foreach (array(5, 10, 15, 30, 45, 60) as $sec) {
$label = $this->rc->gettext(array('name' => 'afternseconds', 'vars' => array('n' => $sec)));
$select->add($label, $sec);
}
$args['blocks']['new_message']['options'][$key] = array(
'title' => html::label($field_id, rcube::Q($this->gettext('desktoptimeout'))),
'content' => $select->show((int) $this->rc->config->get($key))
);
}
return $args;
}
/**
* Handler for user preferences save (preferences_save hook)
*/
function prefs_save($args)
{
if ($args['section'] != 'mailbox') {
return $args;
}
// Load configuration
$this->load_config();
// Check that configuration is not disabled
$dont_override = (array) $this->rc->config->get('dont_override', array());
foreach (array('basic', 'desktop', 'sound') as $type) {
$key = 'newmail_notifier_' . $type;
if (!in_array($key, $dont_override)) {
$args['prefs'][$key] = rcube_utils::get_input_value('_' . $key, rcube_utils::INPUT_POST) ? true : false;
}
}
$option = 'newmail_notifier_desktop_timeout';
if (!in_array($option, $dont_override)) {
if ($value = (int) rcube_utils::get_input_value('_' . $option, rcube_utils::INPUT_POST)) {
$args['prefs'][$option] = $value;
}
}
return $args;
}
/**
* Handler for new message action (new_messages hook)
*/
function notify($args)
{
// Already notified or unexpected input
if ($this->notified || empty($args['diff']['new'])) {
return $args;
}
$mbox = $args['mailbox'];
$storage = $this->rc->get_storage();
$delimiter = $storage->get_hierarchy_delimiter();
// Skip exception (sent/drafts) folders (and their subfolders)
foreach ($this->exceptions as $folder) {
if (strpos($mbox.$delimiter, $folder.$delimiter) === 0) {
return $args;
}
}
// Check if any of new messages is UNSEEN
$deleted = $this->rc->config->get('skip_deleted') ? 'UNDELETED ' : '';
$search = $deleted . 'UNSEEN UID ' . $args['diff']['new'];
$unseen = $storage->search_once($mbox, $search);
if ($unseen->count()) {
$this->notified = true;
$this->rc->output->set_env('newmail_notifier_timeout', $this->rc->config->get('newmail_notifier_desktop_timeout'));
$this->rc->output->command('plugin.newmail_notifier',
array(
'basic' => $this->opt['basic'],
'sound' => $this->opt['sound'],
'desktop' => $this->opt['desktop'],
));
}
return $args;
}
}
diff --git a/plugins/password/drivers/chpasswd.php b/plugins/password/drivers/chpasswd.php
index 31e50241c..fe6c6d16b 100644
--- a/plugins/password/drivers/chpasswd.php
+++ b/plugins/password/drivers/chpasswd.php
@@ -1,52 +1,52 @@
<?php
/**
* chpasswd driver
*
* Driver that adds functionality to change the systems user password via
* the 'chpasswd' command.
*
* For installation instructions please read the README file.
*
* @version 2.0
* @author Alex Cartwright <acartwright@mutinydesign.co.uk>
*
- * Copyright (C) 2005-2013, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_chpasswd_password
{
public function save($currpass, $newpass, $username)
{
$cmd = rcmail::get_instance()->config->get('password_chpasswd_cmd');
$handle = popen($cmd, "w");
fwrite($handle, "$username:$newpass\n");
if (pclose($handle) == 0) {
return PASSWORD_SUCCESS;
}
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: Unable to execute $cmd"
), true, false);
return PASSWORD_ERROR;
}
}
diff --git a/plugins/password/drivers/cpanel.php b/plugins/password/drivers/cpanel.php
index 663ebd173..e094232d4 100644
--- a/plugins/password/drivers/cpanel.php
+++ b/plugins/password/drivers/cpanel.php
@@ -1,116 +1,116 @@
<?php
/**
* cPanel Password Driver
*
* Driver that adds functionality to change the users cPanel password.
* Originally written by Fulvio Venturelli <fulvio@venturelli.org>
*
* Completely rewritten using the cPanel API2 call Email::passwdpop
* as opposed to the original coding against the UI, which is a fragile method that
* makes the driver to always return a failure message for any language other than English
* see https://github.com/roundcube/roundcubemail/issues/3063
*
* This driver has been tested with o2switch hosting and seems to work fine.
*
* @version 3.1
* @author Christian Chech <christian@chech.fr>
*
- * Copyright (C) 2005-2016, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_cpanel_password
{
public function save($curpas, $newpass)
{
require_once 'xmlapi.php';
$rcmail = rcmail::get_instance();
$this->cuser = $rcmail->config->get('password_cpanel_username');
$cpanel_host = $rcmail->config->get('password_cpanel_host');
$cpanel_port = $rcmail->config->get('password_cpanel_port');
$cpanel_hash = $rcmail->config->get('password_cpanel_hash');
$cpanel_pass = $rcmail->config->get('password_cpanel_password');
// Setup the xmlapi connection
$this->xmlapi = new xmlapi($cpanel_host);
$this->xmlapi->set_port($cpanel_port);
// Hash auth
if (!empty($cpanel_hash)) {
$this->xmlapi->hash_auth($this->cuser, $cpanel_hash);
}
// Pass auth
else if (!empty($cpanel_pass)) {
$this->xmlapi->password_auth($this->cuser, $cpanel_pass);
}
else {
return PASSWORD_ERROR;
}
$this->xmlapi->set_output('json');
$this->xmlapi->set_debug(0);
return $this->setPassword($_SESSION['username'], $newpass);
}
/**
* Change email account password
*
* @param string $address Email address/username
* @param string $password Email account password
*
* @return int|array Operation status
*/
function setPassword($address, $password)
{
if (strpos($address, '@')) {
list($data['email'], $data['domain']) = explode('@', $address);
}
else {
list($data['email'], $data['domain']) = array($address, '');
}
$data['password'] = $password;
// Get the cPanel user
$query = $this->xmlapi->listaccts('domain', $data['domain']);
$query = json_decode($query, true);
if ($query['status'] != 1) {
return false;
}
$cpanel_user = $query['acct'][0]['user'];
$query = $this->xmlapi->api2_query($cpanel_user, 'Email', 'passwdpop', $data);
$query = json_decode($query, true);
$result = $query['cpanelresult']['data'][0];
if ($result['result'] == 1) {
return PASSWORD_SUCCESS;
}
if ($result['reason']) {
return array(
'code' => PASSWORD_ERROR,
'message' => $result['reason'],
);
}
return PASSWORD_ERROR;
}
}
diff --git a/plugins/password/drivers/cpanel_webmail.php b/plugins/password/drivers/cpanel_webmail.php
index 49bfab992..3d5770442 100644
--- a/plugins/password/drivers/cpanel_webmail.php
+++ b/plugins/password/drivers/cpanel_webmail.php
@@ -1,146 +1,146 @@
<?php
/**
* cPanel Webmail Password Driver
*
* It uses Cpanel's Webmail UAPI to change the users password.
*
* This driver has been tested successfully with Digital Pacific hosting.
*
* @author Maikel Linke <maikel@email.org.au>
*
- * Copyright (C) 2005-2016, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_cpanel_webmail_password
{
/**
* Changes the user's password. It is called by password.php.
* See "Driver API" README and password.php for the interface details.
*
* @param string $curpas current (old) password
* @param string $newpass new requested password
*
* @return mixed int code or assoc array with 'code' and 'message', see
* "Driver API" README and password.php
*/
public function save($curpas, $newpass)
{
$url = self::url();
$user = password::username();
$userpwd = "$user:$curpas";
$data = array(
'email' => password::username('%l'),
'password' => $newpass
);
$response = $this->curl_auth_post($userpwd, $url, $data);
return self::decode_response($response);
}
/**
* Provides the UAPI URL of the Email::passwd_pop function.
*
* @return string HTTPS URL
*/
public static function url()
{
$config = rcmail::get_instance()->config;
$storage_host = $_SESSION['storage_host'];
$host = $config->get('password_cpanel_webmail_host', $storage_host);
$port = $config->get('password_cpanel_webmail_port', 2096);
return "https://$host:$port/execute/Email/passwd_pop";
}
/**
* Converts a UAPI response to a password driver response.
*
* @param string $response JSON response by the Cpanel UAPI
*
* @return mixed response code or array, see <code>save</code>
*/
public static function decode_response($response)
{
if (!$response) {
return PASSWORD_CONNECT_ERROR;
}
// $result should be `null` or `stdClass` object
$result = json_decode($response);
// The UAPI may return HTML instead of JSON on missing authentication
if ($result && $result->status === 1) {
return PASSWORD_SUCCESS;
}
if ($result && is_array($result->errors) && count($result->errors) > 0) {
return array(
'code' => PASSWORD_ERROR,
'message' => $result->errors[0],
);
}
return PASSWORD_ERROR;
}
/**
* Post data to the given URL using basic authentication.
*
* Example:
*
* <code>
* curl_auth_post('john:Secr3t', 'https://example.org', array(
* 'param' => 'value',
* 'param' => 'value'
* ));
* </code>
*
* @param string $userpwd user name and password separated by a colon
* <code>:</code>
* @param string $url the URL to post data to
* @param array $postdata the data to post
*
* @return string|false The body of the reply, False on error
*/
private function curl_auth_post($userpwd, $url, $postdata)
{
$ch = curl_init();
$postfields = http_build_query($postdata, '', '&');
// see http://php.net/manual/en/function.curl-setopt.php
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_BUFFERSIZE, 131072);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postfields);
curl_setopt($ch, CURLOPT_USERPWD, $userpwd);
$result = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if ($result === false) {
rcube::raise_error("curl error: $error", true, false);
}
return $result;
}
}
diff --git a/plugins/password/drivers/dbmail.php b/plugins/password/drivers/dbmail.php
index 1a872aa56..7c6103610 100644
--- a/plugins/password/drivers/dbmail.php
+++ b/plugins/password/drivers/dbmail.php
@@ -1,58 +1,58 @@
<?php
/**
* DBMail Password Driver
*
* Driver that adds functionality to change the users DBMail password.
* The code is derrived from the Squirrelmail "Change SASL Password" Plugin
* by Galen Johnson.
*
* It only works with dbmail-users on the same host where Roundcube runs
* and requires shell access and gcc in order to compile the binary.
*
* For installation instructions please read the README file.
*
* @version 1.0
*
- * Copyright (C) 2005-2013, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_dbmail_password
{
function save($currpass, $newpass, $username)
{
$curdir = RCUBE_PLUGINS_DIR . 'password/helpers';
$username = escapeshellarg($username);
$password = escapeshellarg($newpass);
$args = rcmail::get_instance()->config->get('password_dbmail_args', '');
$command = "$curdir/chgdbmailusers -c $username -w $password $args";
exec($command, $output, $return_value);
if ($return_value == 0) {
return PASSWORD_SUCCESS;
}
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: Unable to execute $curdir/chgdbmailusers"
), true, false);
return PASSWORD_ERROR;
}
}
diff --git a/plugins/password/drivers/directadmin.php b/plugins/password/drivers/directadmin.php
index fb1bf75a9..7444e1407 100644
--- a/plugins/password/drivers/directadmin.php
+++ b/plugins/password/drivers/directadmin.php
@@ -1,504 +1,502 @@
<?php
/**
* DirectAdmin Password Driver
*
* Driver to change passwords via DirectAdmin Control Panel
*
* @version 2.2
* @author Victor Benincasa <vbenincasa @ gmail.com>
*
- * Copyright (C) 2005-2018, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_directadmin_password
{
public function save($curpass, $passwd)
{
$rcmail = rcmail::get_instance();
$Socket = new HTTPSocket;
$da_user = $_SESSION['username'];
$da_curpass = $curpass;
$da_newpass = $passwd;
$da_host = $rcmail->config->get('password_directadmin_host');
$da_port = $rcmail->config->get('password_directadmin_port');
if (strpos($da_user, '@') === false) {
return array('code' => PASSWORD_ERROR, 'message' => 'Change the SYSTEM user password through control panel!');
}
$da_host = str_replace('%h', $_SESSION['imap_host'], $da_host);
$da_host = str_replace('%d', $rcmail->user->get_username('domain'), $da_host);
$Socket->connect($da_host,$da_port);
$Socket->set_method('POST');
$Socket->query('/CMD_CHANGE_EMAIL_PASSWORD',
array(
'email' => $da_user,
'oldpassword' => $da_curpass,
'password1' => $da_newpass,
'password2' => $da_newpass,
'api' => '1'
));
$response = $Socket->fetch_parsed_body();
//DEBUG
//rcube::console("Password Plugin: [USER: $da_user] [HOST: $da_host] - Response: [SOCKET: ".$Socket->result_status_code."] [DA ERROR: ".strip_tags($response['error'])."] [TEXT: ".$response[text]."]");
if ($Socket->result_status_code != 200) {
return array('code' => PASSWORD_CONNECT_ERROR, 'message' => $Socket->error[0]);
}
if ($response['error'] == 1) {
return array('code' => PASSWORD_ERROR, 'message' => strip_tags($response['text']));
}
return PASSWORD_SUCCESS;
}
}
/**
* Socket communication class.
*
* Originally designed for use with DirectAdmin's API, this class will fill any HTTP socket need.
*
* Very, very basic usage:
* $Socket = new HTTPSocket;
* echo $Socket->get('http://user:pass@somesite.com/somedir/some.file?query=string&this=that');
*
* @author Phi1 'l0rdphi1' Stier <l0rdphi1@liquenox.net>
* @package HTTPSocket
* @version 3.0.2
* 3.0.2
* added longer curl timeouts
* 3.0.1
* support for tcp:// conversion to http://
* 3.0.0
* swapped to use curl to address ssl certificate issues with php 5.6
* 2.7.2
* added x-use-https header check
* added max number of location redirects
* added custom settable message if x-use-https is found, so users can be told where to set their scripts
* if a redirect host is https, add ssl:// to remote_host
* 2.7.1
* added isset to headers['location'], line 306
*/
class HTTPSocket {
var $version = '3.0.2';
/* all vars are private except $error, $query_cache, and $doFollowLocationHeader */
var $method = 'GET';
var $remote_host;
var $remote_port;
var $remote_uname;
var $remote_passwd;
var $result;
var $result_header;
var $result_body;
var $result_status_code;
var $lastTransferSpeed;
var $bind_host;
var $error = array();
var $warn = array();
var $query_cache = array();
var $doFollowLocationHeader = TRUE;
var $redirectURL;
var $max_redirects = 5;
var $ssl_setting_message = 'DirectAdmin appears to be using SSL. Change your script to connect to ssl://';
var $extra_headers = array();
/**
* Create server "connection".
*
*/
function connect($host, $port = '' )
{
if (!is_numeric($port))
{
$port = 2222;
}
$this->remote_host = $host;
$this->remote_port = $port;
}
function bind( $ip = '' )
{
if ( $ip == '' )
{
$ip = $_SERVER['SERVER_ADDR'];
}
$this->bind_host = $ip;
}
/**
* Change the method being used to communicate.
*
* @param string|null request method. supports GET, POST, and HEAD. default is GET
*/
function set_method( $method = 'GET' )
{
$this->method = strtoupper($method);
}
/**
* Specify a username and password.
*
* @param string|null username. default is null
* @param string|null password. default is null
*/
function set_login( $uname = '', $passwd = '' )
{
if ( strlen($uname) > 0 )
{
$this->remote_uname = $uname;
}
if ( strlen($passwd) > 0 )
{
$this->remote_passwd = $passwd;
}
}
/**
* Query the server
*
* @param string containing properly formatted server API. See DA API docs and examples. Http:// URLs O.K. too.
* @param string|array query to pass to url
* @param int if connection KB/s drops below value here, will drop connection
*/
function query( $request, $content = '', $doSpeedCheck = 0 )
{
$this->error = $this->warn = array();
$this->result_status_code = NULL;
$is_ssl = FALSE;
// is our request a http:// ... ?
if (preg_match('!^http://!i',$request) || preg_match('!^https://!i',$request))
{
$location = parse_url($request);
if (preg_match('!^https://!i',$request))
{
$this->connect('https://'.$location['host'],$location['port']);
}
else
$this->connect('http://'.$location['host'],$location['port']);
$this->set_login($location['user'],$location['pass']);
$request = $location['path'];
$content = $location['query'];
if ( strlen($request) < 1 )
{
$request = '/';
}
}
if (preg_match('!^ssl://!i', $this->remote_host))
$this->remote_host = 'https://'.substr($this->remote_host, 6);
if (preg_match('!^tcp://!i', $this->remote_host))
$this->remote_host = 'http://'.substr($this->remote_host, 6);
if (preg_match('!^https://!i', $this->remote_host))
$is_ssl = TRUE;
$array_headers = array(
'Host' => ( $this->remote_port == 80 ? $this->remote_host : "$this->remote_host:$this->remote_port" ),
'Accept' => '*/*',
'Connection' => 'Close' );
foreach ( $this->extra_headers as $key => $value )
{
$array_headers[$key] = $value;
}
$this->result = $this->result_header = $this->result_body = '';
// was content sent as an array? if so, turn it into a string
if (is_array($content))
{
$pairs = array();
foreach ( $content as $key => $value )
{
$pairs[] = "$key=".urlencode($value);
}
$content = join('&',$pairs);
unset($pairs);
}
$OK = TRUE;
if ($this->method == 'GET')
$request .= '?'.$content;
$ch = curl_init($this->remote_host.':'.$this->remote_port.$request);
if ($is_ssl)
{
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); //1
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); //2
//curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2);
}
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
curl_setopt($ch, CURLOPT_USERAGENT, "HTTPSocket/$this->version");
curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 100);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_LOW_SPEED_LIMIT, 512);
curl_setopt($ch, CURLOPT_LOW_SPEED_TIME, 120);
//if ($this->doFollowLocationHeader)
//{
// curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
// curl_setopt($ch, CURLOPT_MAXREDIRS, $this->max_redirects);
//}
// instance connection
if ($this->bind_host)
{
curl_setopt($ch, CURLOPT_INTERFACE, $this->bind_host);
}
// if we have a username and password, add the header
if ( isset($this->remote_uname) && isset($this->remote_passwd) )
{
curl_setopt($ch, CURLOPT_USERPWD, $this->remote_uname.':'.$this->remote_passwd);
}
// for DA skins: if $this->remote_passwd is NULL, try to use the login key system
if ( isset($this->remote_uname) && $this->remote_passwd == NULL )
{
$array_headers['Cookie'] = "session={$_SERVER['SESSION_ID']}; key={$_SERVER['SESSION_KEY']}";
}
// if method is POST, add content length & type headers
if ( $this->method == 'POST' )
{
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $content);
//$array_headers['Content-type'] = 'application/x-www-form-urlencoded';
$array_headers['Content-length'] = strlen($content);
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $array_headers);
if( !($this->result = curl_exec($ch)) )
{
$this->error[] .= curl_error($ch);
$OK = FALSE;
}
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$this->result_header = substr($this->result, 0, $header_size);
$this->result_body = substr($this->result, $header_size);
$this->result_status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$this->lastTransferSpeed = curl_getinfo($ch, CURLINFO_SPEED_DOWNLOAD) / 1024;
curl_close($ch);
$this->query_cache[] = $this->remote_host.':'.$this->remote_port.$request;
$headers = $this->fetch_header();
// did we get the full file?
if ( !empty($headers['content-length']) && $headers['content-length'] != strlen($this->result_body) )
{
$this->result_status_code = 206;
}
// now, if we're being passed a location header, should we follow it?
if ($this->doFollowLocationHeader)
{
//dont bother if we didn't even setup the script correctly
if (isset($headers['x-use-https']) && $headers['x-use-https']=='yes')
die($this->ssl_setting_message);
if (isset($headers['location']))
{
if ($this->max_redirects <= 0)
die("Too many redirects on: ".$headers['location']);
$this->max_redirects--;
$this->redirectURL = $headers['location'];
$this->query($headers['location']);
}
}
}
function getTransferSpeed()
{
return $this->lastTransferSpeed;
}
/**
* The quick way to get a URL's content :)
*
* @param string URL
* @param boolean return as array? (like PHP's file() command)
* @return string result body
*/
function get($location, $asArray = FALSE )
{
$this->query($location);
if ( $this->get_status_code() == 200 )
{
if ($asArray)
{
return preg_split("/\n/",$this->fetch_body());
}
return $this->fetch_body();
}
return FALSE;
}
/**
* Returns the last status code.
* 200 = OK;
* 403 = FORBIDDEN;
* etc.
*
* @return int status code
*/
function get_status_code()
{
return $this->result_status_code;
}
/**
* Adds a header, sent with the next query.
*
* @param string header name
* @param string header value
*/
function add_header($key,$value)
{
$this->extra_headers[$key] = $value;
}
/**
* Clears any extra headers.
*
*/
function clear_headers()
{
$this->extra_headers = array();
}
/**
* Return the result of a query.
*
* @return string result
*/
function fetch_result()
{
return $this->result;
}
/**
* Return the header of result (stuff before body).
*
* @param string (optional) header to return
* @return array result header
*/
function fetch_header( $header = '' )
{
$array_headers = preg_split("/\r\n/",$this->result_header);
$array_return = array( 0 => $array_headers[0] );
unset($array_headers[0]);
foreach ( $array_headers as $pair )
{
if ($pair == '' || $pair == "\r\n") continue;
list($key,$value) = preg_split("/: /",$pair,2);
$array_return[strtolower($key)] = $value;
}
if ( $header != '' )
{
return $array_return[strtolower($header)];
}
return $array_return;
}
/**
* Return the body of result (stuff after header).
*
* @return string result body
*/
function fetch_body()
{
return $this->result_body;
}
/**
* Return parsed body in array format.
*
* @return array result parsed
*/
function fetch_parsed_body()
{
parse_str($this->result_body,$x);
return $x;
}
-
/**
* Set a specifc message on how to change the SSL setting, in the event that it's not set correctly.
*/
function set_ssl_setting_message($str)
{
$this->ssl_setting_message = $str;
}
-
}
diff --git a/plugins/password/drivers/domainfactory.php b/plugins/password/drivers/domainfactory.php
index 7b3b65caa..513e31e36 100644
--- a/plugins/password/drivers/domainfactory.php
+++ b/plugins/password/drivers/domainfactory.php
@@ -1,96 +1,96 @@
<?php
/**
* domainFACTORY Password Driver
*
* Driver to change passwords with the hosting provider domainFACTORY.
* http://www.df.eu/
*
* @version 2.1
* @author Till Krüss <me@tillkruess.com>
* @link http://tillkruess.com/projects/roundcube/
*
- * Copyright (C) 2005-2014, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_domainfactory_password
{
function save($curpass, $passwd, $username)
{
$rcmail = rcmail::get_instance();
if ($ch = curl_init()) {
// initial login
curl_setopt_array($ch, array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_URL => 'https://ssl.df.eu/chmail.php',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query(array(
'login' => $username,
'pwd' => $curpass,
'action' => 'change'
))
));
if ($result = curl_exec($ch)) {
// login successful, get token!
$postfields = array(
'pwd1' => $passwd,
'pwd2' => $passwd,
'action[update]' => 'Speichern'
);
preg_match_all('~<input name="(.+?)" type="hidden" value="(.+?)">~i', $result, $fields);
foreach ($fields[1] as $field_key => $field_name) {
$postfields[$field_name] = $fields[2][$field_key];
}
// change password
$ch = curl_copy_handle($ch);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postfields));
if ($result = curl_exec($ch)) {
// has the password been changed?
if (strpos($result, 'Einstellungen erfolgreich') !== false) {
return PASSWORD_SUCCESS;
}
// show error message(s) if possible
if (strpos($result, '<div class="d-msg-text">') !== false) {
preg_match_all('#<div class="d-msg-text">(.*?)</div>#s', $result, $errors);
if (isset($errors[1])) {
$error_message = '';
foreach ($errors[1] as $error) {
$error_message .= trim(mb_convert_encoding( $error, 'UTF-8', 'ISO-8859-15' )).' ';
}
return array('code' => PASSWORD_ERROR, 'message' => $error_message);
}
}
}
else {
return PASSWORD_CONNECT_ERROR;
}
}
else {
return PASSWORD_CONNECT_ERROR;
}
}
else {
return PASSWORD_CONNECT_ERROR;
}
return PASSWORD_ERROR;
}
}
diff --git a/plugins/password/drivers/expect.php b/plugins/password/drivers/expect.php
index 6fa266b04..0af39f4da 100644
--- a/plugins/password/drivers/expect.php
+++ b/plugins/password/drivers/expect.php
@@ -1,71 +1,71 @@
<?php
/**
* expect driver
*
* Driver that adds functionality to change the systems user password via
* the 'expect' command.
*
* For installation instructions please read the README file.
*
* @version 2.0
* @author Andy Theuninck <gohanman@gmail.com)
*
* Based on chpasswd roundcubemail password driver by
* @author Alex Cartwright <acartwright@mutinydesign.co.uk)
* and expect horde passwd driver by
* @author Gaudenz Steinlin <gaudenz@soziologie.ch>
*
* Configuration settings:
* password_expect_bin => location of expect (e.g. /usr/bin/expect)
* password_expect_script => path to "password-expect" file
* password_expect_params => arguments for the expect script
* see the password-expect file for details. This is probably
* a good starting default:
* -telent -host localhost -output /tmp/passwd.log -log /tmp/passwd.log
*
- * Copyright (C) 2005-2014, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_expect_password
{
public function save($currpass, $newpass, $username)
{
$rcmail = rcmail::get_instance();
$bin = $rcmail->config->get('password_expect_bin');
$script = $rcmail->config->get('password_expect_script');
$params = $rcmail->config->get('password_expect_params');
$cmd = $bin . ' -f ' . $script . ' -- ' . $params;
$handle = popen($cmd, "w");
fwrite($handle, "$username\n");
fwrite($handle, "$currpass\n");
fwrite($handle, "$newpass\n");
if (pclose($handle) == 0) {
return PASSWORD_SUCCESS;
}
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: Unable to execute $cmd"
), true, false);
return PASSWORD_ERROR;
}
}
diff --git a/plugins/password/drivers/gearman.php b/plugins/password/drivers/gearman.php
index 4527af6d0..a05e09a7f 100644
--- a/plugins/password/drivers/gearman.php
+++ b/plugins/password/drivers/gearman.php
@@ -1,69 +1,69 @@
<?php
/**
* Gearman Password Driver
*
* Payload is json string containing username, oldPassword and newPassword
* Return value is a json string saying result: true if success.
*
* @version 1.0
* @author Mohammad Anwari <mdamt@mdamt.net>
*
- * Copyright (C) 2005-2014, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_gearman_password
{
function save($currpass, $newpass, $username)
{
if (extension_loaded('gearman')) {
$rcmail = rcmail::get_instance();
$payload = array(
'username' => $username,
'oldPassword' => $currpass,
'newPassword' => $newpass,
);
$gmc = new GearmanClient();
$gmc->addServer($rcmail->config->get('password_gearman_host', 'localhost'));
$result = $gmc->doNormal('setPassword', json_encode($payload));
$success = json_decode($result);
if ($success && $success->result == 1) {
return PASSWORD_SUCCESS;
}
else {
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: Gearman authentication failed for user $username: $error"
), true, false);
}
}
else {
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: PECL Gearman module not loaded"
), true, false);
}
return PASSWORD_ERROR;
}
}
diff --git a/plugins/password/drivers/hmail.php b/plugins/password/drivers/hmail.php
index 66641ed9a..39339ef41 100644
--- a/plugins/password/drivers/hmail.php
+++ b/plugins/password/drivers/hmail.php
@@ -1,74 +1,74 @@
<?php
/**
* hMailserver password driver
*
* @version 2.0
* @author Roland 'rosali' Liebl <myroundcube@mail4us.net>
*
- * Copyright (C) 2005-2014, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_hmail_password
{
public function save($curpass, $passwd, $username)
{
$rcmail = rcmail::get_instance();
try {
$remote = $rcmail->config->get('hmailserver_remote_dcom', false);
if ($remote)
$obApp = new COM("hMailServer.Application", $rcmail->config->get('hmailserver_server'));
else
$obApp = new COM("hMailServer.Application");
}
catch (Exception $e) {
rcube::write_log('errors', "Plugin password (hmail driver): " . trim(strip_tags($e->getMessage())));
rcube::write_log('errors', "Plugin password (hmail driver): This problem is often caused by DCOM permissions not being set.");
return PASSWORD_ERROR;
}
if (strstr($username,'@')) {
$temparr = explode('@', $username);
$domain = $temparr[1];
}
else {
$domain = $rcmail->config->get('username_domain',false);
if (!$domain) {
rcube::write_log('errors','Plugin password (hmail driver): $config[\'username_domain\'] is not defined.');
return PASSWORD_ERROR;
}
$username = $username . "@" . $domain;
}
$obApp->Authenticate($username, $curpass);
try {
$obDomain = $obApp->Domains->ItemByName($domain);
$obAccount = $obDomain->Accounts->ItemByAddress($username);
$obAccount->Password = $passwd;
$obAccount->Save();
return PASSWORD_SUCCESS;
}
catch (Exception $e) {
rcube::write_log('errors', "Plugin password (hmail driver): " . trim(strip_tags($e->getMessage())));
rcube::write_log('errors', "Plugin password (hmail driver): This problem is often caused by DCOM permissions not being set.");
return PASSWORD_ERROR;
}
}
}
diff --git a/plugins/password/drivers/ldap.php b/plugins/password/drivers/ldap.php
index 65fe965a3..a6b979b8f 100644
--- a/plugins/password/drivers/ldap.php
+++ b/plugins/password/drivers/ldap.php
@@ -1,200 +1,200 @@
<?php
/**
* LDAP Password Driver
*
* Driver for passwords stored in LDAP
* This driver use the PEAR Net_LDAP2 class (http://pear.php.net/package/Net_LDAP2).
*
* @version 2.0
* @author Edouard MOREAU <edouard.moreau@ensma.fr>
*
* method hashPassword based on code from the phpLDAPadmin development team (http://phpldapadmin.sourceforge.net/).
* method randomSalt based on code from the phpLDAPadmin development team (http://phpldapadmin.sourceforge.net/).
*
- * Copyright (C) 2005-2014, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_ldap_password
{
public function save($curpass, $passwd)
{
$rcmail = rcmail::get_instance();
require_once 'Net/LDAP2.php';
require_once __DIR__ . '/ldap_simple.php';
// Building user DN
if ($userDN = $rcmail->config->get('password_ldap_userDN_mask')) {
$userDN = rcube_ldap_simple_password::substitute_vars($userDN);
}
else {
$userDN = $this->search_userdn($rcmail);
}
if (empty($userDN)) {
return PASSWORD_CONNECT_ERROR;
}
// Connection Method
switch($rcmail->config->get('password_ldap_method')) {
case 'admin':
$binddn = $rcmail->config->get('password_ldap_adminDN');
$bindpw = $rcmail->config->get('password_ldap_adminPW');
break;
case 'user':
default:
$binddn = $userDN;
$bindpw = $curpass;
break;
}
// Configuration array
$ldapConfig = array (
'binddn' => $binddn,
'bindpw' => $bindpw,
'basedn' => $rcmail->config->get('password_ldap_basedn'),
'host' => $rcmail->config->get('password_ldap_host', 'localhost'),
'port' => $rcmail->config->get('password_ldap_port', '389'),
'starttls' => $rcmail->config->get('password_ldap_starttls'),
'version' => $rcmail->config->get('password_ldap_version', '3'),
);
// Connecting using the configuration array
$ldap = Net_LDAP2::connect($ldapConfig);
// Checking for connection error
if (is_a($ldap, 'PEAR_Error')) {
return PASSWORD_CONNECT_ERROR;
}
$force = $rcmail->config->get('password_ldap_force_replace', true);
$pwattr = $rcmail->config->get('password_ldap_pwattr', 'userPassword');
$lchattr = $rcmail->config->get('password_ldap_lchattr');
$smbpwattr = $rcmail->config->get('password_ldap_samba_pwattr');
$smblchattr = $rcmail->config->get('password_ldap_samba_lchattr');
$samba = $rcmail->config->get('password_ldap_samba');
$encodage = $rcmail->config->get('password_ldap_encodage', 'crypt');
// Support multiple userPassword values where desired.
// multiple encodings can be specified separated by '+' (e.g. "cram-md5+ssha")
$encodages = explode('+', $encodage);
$crypted_pass = array();
foreach ($encodages as $enc) {
if ($cpw = password::hash_password($passwd, $enc)) {
$crypted_pass[] = $cpw;
}
}
// Support password_ldap_samba option for backward compat.
if ($samba && !$smbpwattr) {
$smbpwattr = 'sambaNTPassword';
$smblchattr = 'sambaPwdLastSet';
}
// Crypt new password
if (empty($crypted_pass)) {
return PASSWORD_CRYPT_ERROR;
}
// Crypt new samba password
if ($smbpwattr && !($samba_pass = password::hash_password($passwd, 'samba'))) {
return PASSWORD_CRYPT_ERROR;
}
// Writing new crypted password to LDAP
$userEntry = $ldap->getEntry($userDN);
if (Net_LDAP2::isError($userEntry)) {
return PASSWORD_CONNECT_ERROR;
}
if (!$userEntry->replace(array($pwattr => $crypted_pass), $force)) {
return PASSWORD_CONNECT_ERROR;
}
// Updating PasswordLastChange Attribute if desired
if ($lchattr) {
$current_day = (int)(time() / 86400);
if (!$userEntry->replace(array($lchattr => $current_day), $force)) {
return PASSWORD_CONNECT_ERROR;
}
}
// Update Samba password and last change fields
if ($smbpwattr) {
$userEntry->replace(array($smbpwattr => $samba_pass), $force);
}
// Update Samba password last change field
if ($smblchattr) {
$userEntry->replace(array($smblchattr => time()), $force);
}
if (Net_LDAP2::isError($userEntry->update())) {
return PASSWORD_CONNECT_ERROR;
}
// All done, no error
return PASSWORD_SUCCESS;
}
/**
* Bind with searchDN and searchPW and search for the user's DN.
* Use search_base and search_filter defined in config file.
* Return the found DN.
*/
function search_userdn($rcmail)
{
$binddn = $rcmail->config->get('password_ldap_searchDN');
$bindpw = $rcmail->config->get('password_ldap_searchPW');
$ldapConfig = array (
'basedn' => $rcmail->config->get('password_ldap_basedn'),
'host' => $rcmail->config->get('password_ldap_host', 'localhost'),
'port' => $rcmail->config->get('password_ldap_port', '389'),
'starttls' => $rcmail->config->get('password_ldap_starttls'),
'version' => $rcmail->config->get('password_ldap_version', '3'),
);
// allow anonymous searches
if (!empty($binddn)) {
$ldapConfig['binddn'] = $binddn;
$ldapConfig['bindpw'] = $bindpw;
}
$ldap = Net_LDAP2::connect($ldapConfig);
if (is_a($ldap, 'PEAR_Error')) {
return '';
}
$base = rcube_ldap_simple_password::substitute_vars($rcmail->config->get('password_ldap_search_base'));
$filter = rcube_ldap_simple_password::substitute_vars($rcmail->config->get('password_ldap_search_filter'));
$options = array (
'scope' => 'sub',
'attributes' => array(),
);
$result = $ldap->search($base, $filter, $options);
if (is_a($result, 'PEAR_Error') || ($result->count() != 1)) {
$ldap->done();
return '';
}
$userDN = $result->current()->dn();
$ldap->done();
return $userDN;
}
}
diff --git a/plugins/password/drivers/ldap_exop.php b/plugins/password/drivers/ldap_exop.php
index a034c95c1..796bef85b 100644
--- a/plugins/password/drivers/ldap_exop.php
+++ b/plugins/password/drivers/ldap_exop.php
@@ -1,76 +1,76 @@
<?php
/**
* LDAP - Password Modify Extended Operation Driver
*
* Driver for passwords stored in LDAP
* This driver is based on Simple LDAP Password Driver, but uses
* Password Modify Extended Operation
* PHP >= 7.2 required
*
* @version 1.0
* @author Peter Kubica <peter@kubica.ch>
*
- * Copyright (C) 2005-2019, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
require_once __DIR__ . '/ldap_simple.php';
class rcube_ldap_exop_password extends rcube_ldap_simple_password
{
private $debug = false;
function save($curpass, $passwd)
{
if (!function_exists('ldap_exop_passwd')) {
rcube::raise_error(array(
'code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "ldap_exop_passwd not supported"
),
true);
return PASSWORD_ERROR;
}
// Connect and bind
$ret = $this->connect($curpass);
if ($ret !== true) {
return $ret;
}
if (!ldap_exop_passwd($this->conn, $this->user, $curpass, $passwd)) {
$this->_debug("S: ".ldap_error($this->conn));
$errno = ldap_errno($this->conn);
ldap_unbind($this->conn);
if ($errno == 0x13) {
return PASSWORD_CONSTRAINT_VIOLATION;
}
return PASSWORD_CONNECT_ERROR;
}
$this->_debug("S: OK");
// All done, no error
ldap_unbind($this->conn);
return PASSWORD_SUCCESS;
}
}
diff --git a/plugins/password/drivers/ldap_ppolicy.php b/plugins/password/drivers/ldap_ppolicy.php
index 6b7fd1a6b..84c2c2874 100644
--- a/plugins/password/drivers/ldap_ppolicy.php
+++ b/plugins/password/drivers/ldap_ppolicy.php
@@ -1,96 +1,95 @@
<?php
/**
* ldap_ppolicy driver
*
* Driver that adds functionality to change the user password via
* the 'change_ldap_pass.pl' command respecting password policy (history) in LDAP.
*
* @version 1.0
* @author Zbigniew Szmyd <zbigniew.szmyd@linseco.pl>
- *
*/
class rcube_ldap_ppolicy_password
{
public function save($currpass, $newpass, $username)
{
$rcmail = rcmail::get_instance();
$this->debug = $rcmail->config->get('ldap_debug');
$cmd = $rcmail->config->get('password_ldap_ppolicy_cmd');
$uri = $rcmail->config->get('password_ldap_ppolicy_uri');
$baseDN = $rcmail->config->get('password_ldap_ppolicy_basedn');
$filter = $rcmail->config->get('password_ldap_ppolicy_search_filter');
$bindDN = $rcmail->config->get('password_ldap_ppolicy_searchDN');
$bindPW = $rcmail->config->get('password_ldap_ppolicy_searchPW');
$cafile = $rcmail->config->get('password_ldap_ppolicy_cafile');
$log_dir = $rcmail->config->get('log_dir');
if (empty($log_dir)) {
$log_dir = RCUBE_INSTALL_PATH . 'logs';
}
// try to open specific log file for writing
$logfile = $log_dir.'/password_ldap_ppolicy.err';
$descriptorspec = array(
0 => array("pipe", "r"), // stdin is a pipe that the child will read from
1 => array("pipe", "w"), // stdout is a pipe that the child will write to
2 => array("file", $logfile, "a") // stderr is a file to write to
);
$cmd = 'plugins/password/helpers/'. $cmd;
$this->_debug("parameters:\ncmd:$cmd\nuri:$uri\nbaseDN:$baseDN\nfilter:$filter");
$process = proc_open($cmd, $descriptorspec, $pipes);
if (is_resource($process)) {
// $pipes now looks like this:
// 0 => writeable handle connected to child stdin
// 1 => readable handle connected to child stdout
// Any error output will be appended to /tmp/error-output.txt
fwrite($pipes[0], $uri."\n");
fwrite($pipes[0], $baseDN."\n");
fwrite($pipes[0], $filter."\n");
fwrite($pipes[0], $bindDN."\n");
fwrite($pipes[0], $bindPW."\n");
fwrite($pipes[0], $username."\n");
fwrite($pipes[0], $currpass."\n");
fwrite($pipes[0], $newpass."\n");
fwrite($pipes[0], $cafile);
fclose($pipes[0]);
$result = stream_get_contents($pipes[1]);
fclose($pipes[1]);
$this->_debug('Result:'.$result);
switch ($result) {
case "OK":
return PASSWORD_SUCCESS;
case "Password is in history of old passwords":
return PASSWORD_IN_HISTORY;
case "Cannot connect to any server":
return PASSWORD_CONNECT_ERROR;
default:
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => $result
), true, false);
}
return PASSWORD_ERROR;
}
}
private function _debug($str)
{
if ($this->debug) {
rcube::write_log('password_ldap_ppolicy', $str);
}
}
}
diff --git a/plugins/password/drivers/ldap_simple.php b/plugins/password/drivers/ldap_simple.php
index 1a6e411ed..33e939dad 100644
--- a/plugins/password/drivers/ldap_simple.php
+++ b/plugins/password/drivers/ldap_simple.php
@@ -1,291 +1,291 @@
<?php
/**
* Simple LDAP Password Driver
*
* Driver for passwords stored in LDAP
* This driver is based on Edouard's LDAP Password Driver, but does not
* require PEAR's Net_LDAP2 to be installed
*
* @version 2.1
* @author Wout Decre <wout@canodus.be>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
- * Copyright (C) 2005-2014, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_ldap_simple_password
{
protected $debug = false;
protected $user;
protected $conn;
public function save($curpass, $passwd)
{
$rcmail = rcmail::get_instance();
$lchattr = $rcmail->config->get('password_ldap_lchattr');
$pwattr = $rcmail->config->get('password_ldap_pwattr', 'userPassword');
$smbpwattr = $rcmail->config->get('password_ldap_samba_pwattr');
$smblchattr = $rcmail->config->get('password_ldap_samba_lchattr');
$samba = $rcmail->config->get('password_ldap_samba');
$pass_mode = $rcmail->config->get('password_ldap_encodage', 'crypt');
$crypted_pass = password::hash_password($passwd, $pass_mode);
// Support password_ldap_samba option for backward compat.
if ($samba && !$smbpwattr) {
$smbpwattr = 'sambaNTPassword';
$smblchattr = 'sambaPwdLastSet';
}
// Crypt new password
if (!$crypted_pass) {
return PASSWORD_CRYPT_ERROR;
}
// Crypt new Samba password
if ($smbpwattr && !($samba_pass = password::hash_password($passwd, 'samba'))) {
return PASSWORD_CRYPT_ERROR;
}
// Connect and bind
$ret = $this->connect($curpass);
if ($ret !== true) {
return $ret;
}
$entry[$pwattr] = $crypted_pass;
// Update PasswordLastChange Attribute if desired
if ($lchattr) {
$entry[$lchattr] = (int)(time() / 86400);
}
// Update Samba password
if ($smbpwattr) {
$entry[$smbpwattr] = $samba_pass;
}
// Update Samba password last change
if ($smblchattr) {
$entry[$smblchattr] = time();
}
$this->_debug("C: Modify $user_dn: " . print_r($entry, true));
if (!ldap_modify($this->conn, $this->user, $entry)) {
$this->_debug("S: ".ldap_error($this->conn));
$errno = ldap_errno($this->conn);
ldap_unbind($this->conn);
if ($errno == 0x13) {
return PASSWORD_CONSTRAINT_VIOLATION;
}
return PASSWORD_CONNECT_ERROR;
}
$this->_debug("S: OK");
// All done, no error
ldap_unbind($this->conn);
return PASSWORD_SUCCESS;
}
/**
* Connect and bind to LDAP server
*/
function connect($curpass)
{
$rcmail = rcmail::get_instance();
$this->debug = $rcmail->config->get('ldap_debug');
$ldap_host = $rcmail->config->get('password_ldap_host', 'localhost');
$ldap_port = $rcmail->config->get('password_ldap_port', '389');
$this->_debug("C: Connect to $ldap_host:$ldap_port");
// Connect
if (!$ds = ldap_connect($ldap_host, $ldap_port)) {
$this->_debug("S: NOT OK");
rcube::raise_error(array(
'code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not connect to LDAP server"
),
true);
return PASSWORD_CONNECT_ERROR;
}
$this->_debug("S: OK");
// Set protocol version
ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION,
$rcmail->config->get('password_ldap_version', '3'));
// Start TLS
if ($rcmail->config->get('password_ldap_starttls')) {
if (!ldap_start_tls($ds)) {
ldap_unbind($ds);
return PASSWORD_CONNECT_ERROR;
}
}
// other plugins might want to modify user DN
$plugin = $rcmail->plugins->exec_hook('password_ldap_bind', array(
'user_dn' => '', 'conn' => $ds));
// Build user DN
if (!empty($plugin['user_dn'])) {
$user_dn = $plugin['user_dn'];
}
else if ($user_dn = $rcmail->config->get('password_ldap_userDN_mask')) {
$user_dn = self::substitute_vars($user_dn);
}
else {
$user_dn = $this->search_userdn($rcmail, $ds);
}
if (empty($user_dn)) {
ldap_unbind($ds);
return PASSWORD_CONNECT_ERROR;
}
// Connection method
switch ($rcmail->config->get('password_ldap_method')) {
case 'admin':
$binddn = $rcmail->config->get('password_ldap_adminDN');
$bindpw = $rcmail->config->get('password_ldap_adminPW');
break;
case 'user':
default:
$binddn = $user_dn;
$bindpw = $curpass;
break;
}
$this->_debug("C: Bind $binddn, pass: **** [" . strlen($bindpw) . "]");
// Bind
if (!ldap_bind($ds, $binddn, $bindpw)) {
$this->_debug("S: ".ldap_error($ds));
ldap_unbind($ds);
return PASSWORD_CONNECT_ERROR;
}
$this->_debug("S: OK");
$this->conn = $ds;
$this->user = $user_dn;
return true;
}
/**
* Bind with searchDN and searchPW and search for the user's DN
* Use search_base and search_filter defined in config file
* Return the found DN
*/
function search_userdn($rcmail, $ds)
{
$search_user = $rcmail->config->get('password_ldap_searchDN');
$search_pass = $rcmail->config->get('password_ldap_searchPW');
$search_base = $rcmail->config->get('password_ldap_search_base');
$search_filter = $rcmail->config->get('password_ldap_search_filter');
if (empty($search_filter)) {
return false;
}
$this->_debug("C: Bind " . ($search_user ? $search_user : '[anonymous]'));
// Bind
if (!ldap_bind($ds, $search_user, $search_pass)) {
$this->_debug("S: ".ldap_error($ds));
return false;
}
$this->_debug("S: OK");
$search_base = self::substitute_vars($search_base);
$search_filter = self::substitute_vars($search_filter);
$this->_debug("C: Search $search_base for $search_filter");
// Search for the DN
if (!$sr = ldap_search($ds, $search_base, $search_filter)) {
$this->_debug("S: ".ldap_error($ds));
return false;
}
$found = ldap_count_entries($ds, $sr);
$this->_debug("S: OK [found $found records]");
// If no or more entries were found, return false
if ($found != 1) {
return false;
}
return ldap_get_dn($ds, ldap_first_entry($ds, $sr));
}
/**
* Substitute %login, %name, %domain, %dc in $str
* See plugin config for details
*/
public static function substitute_vars($str)
{
$str = str_replace('%login', $_SESSION['username'], $str);
$str = str_replace('%l', $_SESSION['username'], $str);
$parts = explode('@', $_SESSION['username']);
if (count($parts) == 2) {
$dc = 'dc='.strtr($parts[1], array('.' => ',dc=')); // hierarchal domain string
$str = str_replace('%name', $parts[0], $str);
$str = str_replace('%n', $parts[0], $str);
$str = str_replace('%dc', $dc, $str);
$str = str_replace('%domain', $parts[1], $str);
$str = str_replace('%d', $parts[1], $str);
}
else if ( count($parts) == 1) {
$str = str_replace('%name', $parts[0], $str);
$str = str_replace('%n', $parts[0], $str);
}
return $str;
}
/**
* Prints debug info to the log
*/
protected function _debug($str)
{
if ($this->debug) {
rcube::write_log('ldap', $str);
}
}
}
diff --git a/plugins/password/drivers/modoboa.php b/plugins/password/drivers/modoboa.php
index 18ed0576a..4f41c2d35 100644
--- a/plugins/password/drivers/modoboa.php
+++ b/plugins/password/drivers/modoboa.php
@@ -1,119 +1,119 @@
<?php
/**
* Modoboa Password Driver
*
* Payload is json string containing username, oldPassword and newPassword
* Return value is a json string saying result: true if success.
*
* @version 1.0.1
* @author stephane @actionweb.fr
*
- * Copyright (C) 2018, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*
* The driver need modoboa core 1.10.6 or later
*
* You need to define theses variables in plugin/password/config.inc.php
*
* $config['password_driver'] = 'modoboa'; // use modoboa as driver
* $config['password_modoboa_api_token'] = ''; // put token number from Modoboa server
* $config['password_minimum_length'] = 8; // select same number as in Modoboa server
*/
class rcube_modoboa_password
{
function save($curpass, $passwd)
{
// Init config access
$rcmail = rcmail::get_instance();
$ModoboaToken = $rcmail->config->get('password_modoboa_api_token');
$RoudCubeUsername = $_SESSION['username'];
$IMAPhost = $_SESSION['imap_host'];
// Call GET to fetch values from modoboa server
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => "https://" . $IMAPhost . "/api/v1/accounts/?search=" . urlencode($RoudCubeUsername),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => array(
"Authorization: Token " . $ModoboaToken,
"Cache-Control: no-cache",
"Content-Type: application/json"
),
));
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
return PASSWORD_CONNECT_ERROR;
}
// Decode json string
$decoded = json_decode($response);
if (!is_array($decoded)) {
return PASSWORD_CONNECT_ERROR;
}
// Get user ID (pk)
$userid = $decoded[0]->pk;
// Encode json with new password
$ret['username'] = $decoded[0]->username;
$ret['role'] = $decoded[0]->role;
$ret['password'] = $passwd; // new password
$encoded = json_encode($ret);
// Call HTTP API Modoboa
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => "https://" . $IMAPhost . "/api/v1/accounts/" . $userid . "/",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => "" . $encoded . "",
CURLOPT_HTTPHEADER => array(
"Authorization: Token " . $ModoboaToken,
"Cache-Control: no-cache",
"Content-Type: application/json"
),
));
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
return PASSWORD_CONNECT_ERROR;
}
return PASSWORD_SUCCESS;
}
}
diff --git a/plugins/password/drivers/pam.php b/plugins/password/drivers/pam.php
index 5c293394a..c70dafddf 100644
--- a/plugins/password/drivers/pam.php
+++ b/plugins/password/drivers/pam.php
@@ -1,57 +1,57 @@
<?php
/**
* PAM Password Driver
*
* @version 2.0
* @author Aleksander Machniak
*
- * Copyright (C) 2005-2013, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_pam_password
{
function save($currpass, $newpass, $username)
{
$error = '';
if (extension_loaded('pam') || extension_loaded('pam_auth')) {
if (pam_auth($username, $currpass, $error, false)) {
if (pam_chpass($username, $currpass, $newpass)) {
return PASSWORD_SUCCESS;
}
}
else {
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: PAM authentication failed for user $username: $error"
), true, false);
}
}
else {
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: PECL-PAM module not loaded"
), true, false);
}
return PASSWORD_ERROR;
}
}
diff --git a/plugins/password/drivers/poppassd.php b/plugins/password/drivers/poppassd.php
index 0e31b4dd6..e01fa58b5 100644
--- a/plugins/password/drivers/poppassd.php
+++ b/plugins/password/drivers/poppassd.php
@@ -1,86 +1,86 @@
<?php
/**
* Poppassd Password Driver
*
* Driver to change passwords via Poppassd/Courierpassd
*
* @version 2.0
* @author Philip Weir
*
- * Copyright (C) 2005-2013, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_poppassd_password
{
function format_error_result($code, $line)
{
if (preg_match('/^\d\d\d\s+(\S.*)\s*$/', $line, $matches)) {
return array('code' => $code, 'message' => $matches[1]);
}
return $code;
}
function save($curpass, $passwd, $username)
{
$rcmail = rcmail::get_instance();
$poppassd = new Net_Socket();
$port = $rcmail->config->get('password_pop_port', 106);
$host = $rcmail->config->get('password_pop_host', 'localhost');
$host = rcube_utils::parse_host($host);
$result = $poppassd->connect($host, $port, null);
if (is_a($result, 'PEAR_Error')) {
return $this->format_error_result(PASSWORD_CONNECT_ERROR, $result->getMessage());
}
$result = $poppassd->readLine();
if (!preg_match('/^2\d\d/', $result)) {
$poppassd->disconnect();
return $this->format_error_result(PASSWORD_ERROR, $result);
}
$poppassd->writeLine("user ". $username);
$result = $poppassd->readLine();
if (!preg_match('/^[23]\d\d/', $result)) {
$poppassd->disconnect();
return $this->format_error_result(PASSWORD_CONNECT_ERROR, $result);
}
$poppassd->writeLine("pass ". $curpass);
$result = $poppassd->readLine();
if (!preg_match('/^[23]\d\d/', $result)) {
$poppassd->disconnect();
return $this->format_error_result(PASSWORD_ERROR, $result);
}
$poppassd->writeLine("newpass ". $passwd);
$result = $poppassd->readLine();
$poppassd->disconnect();
if (!preg_match('/^2\d\d/', $result)) {
return $this->format_error_result(PASSWORD_ERROR, $result);
}
return PASSWORD_SUCCESS;
}
}
diff --git a/plugins/password/drivers/pw_usermod.php b/plugins/password/drivers/pw_usermod.php
index 8772190b6..d439d6fb2 100644
--- a/plugins/password/drivers/pw_usermod.php
+++ b/plugins/password/drivers/pw_usermod.php
@@ -1,54 +1,54 @@
<?php
/**
* pw_usermod Driver
*
* Driver that adds functionality to change the systems user password via
* the 'pw usermod' command.
*
* For installation instructions please read the README file.
*
* @version 2.0
* @author Alex Cartwright <acartwright@mutinydesign.co.uk>
* @author Adamson Huang <adomputer@gmail.com>
*
- * Copyright (C) 2005-2013, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_pw_usermod_password
{
public function save($currpass, $newpass, $username)
{
$cmd = rcmail::get_instance()->config->get('password_pw_usermod_cmd');
$cmd .= ' ' . escapeshellarg($username) . ' > /dev/null';
$handle = popen($cmd, 'w');
fwrite($handle, "$newpass\n");
if (pclose($handle) == 0) {
return PASSWORD_SUCCESS;
}
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: Unable to execute $cmd"
), true, false);
return PASSWORD_ERROR;
}
}
diff --git a/plugins/password/drivers/sasl.php b/plugins/password/drivers/sasl.php
index a64107fc1..e68ba6e5e 100644
--- a/plugins/password/drivers/sasl.php
+++ b/plugins/password/drivers/sasl.php
@@ -1,60 +1,60 @@
<?php
/**
* SASL Password Driver
*
* Driver that adds functionality to change the users Cyrus/SASL password.
* The code is derrived from the Squirrelmail "Change SASL Password" Plugin
* by Galen Johnson.
*
* It only works with saslpasswd2 on the same host where Roundcube runs
* and requires shell access and gcc in order to compile the binary.
*
* For installation instructions please read the README file.
*
* @version 2.0
* @author Thomas Bruederli
*
- * Copyright (C) 2005-2013, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_sasl_password
{
function save($currpass, $newpass, $username)
{
$curdir = RCUBE_PLUGINS_DIR . 'password/helpers';
$username = escapeshellarg($username);
$args = rcmail::get_instance()->config->get('password_saslpasswd_args', '');
if ($fh = popen("$curdir/chgsaslpasswd -p $args $username", 'w')) {
fwrite($fh, $newpass."\n");
$code = pclose($fh);
if ($code == 0) {
return PASSWORD_SUCCESS;
}
}
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: Unable to execute $curdir/chgsaslpasswd"
), true, false);
return PASSWORD_ERROR;
}
}
diff --git a/plugins/password/drivers/smb.php b/plugins/password/drivers/smb.php
index 0ffcbda57..5d916d24a 100644
--- a/plugins/password/drivers/smb.php
+++ b/plugins/password/drivers/smb.php
@@ -1,71 +1,71 @@
<?php
/**
* smb Driver
*
* Driver that adds functionality to change the systems user password via
* the 'smbpasswd' command.
*
* For installation instructions please read the README file.
*
* @version 2.0
* @author Andy Theuninck <gohanman@gmail.com)
*
* Based on chpasswd roundcubemail password driver by
* @author Alex Cartwright <acartwright@mutinydesign.co.uk)
* and smbpasswd horde passwd driver by
* @author Rene Lund Jensen <Rene@lundjensen.net>
*
* Configuration settings:
* password_smb_host => samba host (default: localhost)
* password_smb_cmd => smbpasswd binary (default: /usr/bin/smbpasswd)
*
- * Copyright (C) 2005-2013, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_smb_password
{
public function save($currpass, $newpass, $username)
{
$host = rcmail::get_instance()->config->get('password_smb_host','localhost');
$bin = rcmail::get_instance()->config->get('password_smb_cmd','/usr/bin/smbpasswd');
$host = rcube_utils::parse_host($host);
$tmpfile = tempnam(sys_get_temp_dir(),'smb');
$cmd = $bin . ' -r ' . escapeshellarg($host) . ' -s -U "' . escapeshellarg($username) . '" > ' . $tmpfile . ' 2>&1';
$handle = @popen($cmd, 'w');
fwrite($handle, $currpass."\n");
fwrite($handle, $newpass."\n");
fwrite($handle, $newpass."\n");
@pclose($handle);
$res = file($tmpfile);
unlink($tmpfile);
if (strstr($res[count($res) - 1], 'Password changed for user') !== false) {
return PASSWORD_SUCCESS;
}
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: Unable to execute $cmd"
), true, false);
return PASSWORD_ERROR;
}
}
diff --git a/plugins/password/drivers/sql.php b/plugins/password/drivers/sql.php
index eeea0c59d..ca7e0d983 100644
--- a/plugins/password/drivers/sql.php
+++ b/plugins/password/drivers/sql.php
@@ -1,191 +1,191 @@
<?php
/**
* SQL Password Driver
*
* Driver for passwords stored in SQL database
*
* @version 2.0
- * @author Aleksander 'A.L.E.C' Machniak <alec@alec.pl>
+ * @author Aleksander Machniak <alec@alec.pl>
*
- * Copyright (C) 2005-2013, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_sql_password
{
function save($curpass, $passwd)
{
$rcmail = rcmail::get_instance();
if (!($sql = $rcmail->config->get('password_query'))) {
$sql = 'SELECT update_passwd(%c, %u)';
}
if ($dsn = $rcmail->config->get('password_db_dsn')) {
$db = rcube_db::factory(self::parse_dsn($dsn), '', false);
$db->set_debug((bool)$rcmail->config->get('sql_debug'));
}
else {
$db = $rcmail->get_dbh();
}
if ($db->is_error()) {
return PASSWORD_ERROR;
}
// new password - default hash method
if (strpos($sql, '%P') !== false) {
$password = password::hash_password($passwd);
if ($password === false) {
return PASSWORD_CRYPT_ERROR;
}
$sql = str_replace('%P', $db->quote($password), $sql);
}
// old password - default hash method
if (strpos($sql, '%O') !== false) {
$password = password::hash_password($curpass);
if ($password === false) {
return PASSWORD_CRYPT_ERROR;
}
$sql = str_replace('%O', $db->quote($password), $sql);
}
// crypted password (deprecated, use %P)
if (strpos($sql, '%c') !== false) {
$password = password::hash_password($passwd, 'crypt', false);
if ($password === false) {
return PASSWORD_CRYPT_ERROR;
}
$sql = str_replace('%c', $db->quote($password), $sql);
}
// dovecotpw (deprecated, use %P)
if (strpos($sql, '%D') !== false) {
$password = password::hash_password($passwd, 'dovecot', false);
if ($password === false) {
return PASSWORD_CRYPT_ERROR;
}
$sql = str_replace('%D', $db->quote($password), $sql);
}
// hashed passwords (deprecated, use %P)
if (strpos($sql, '%n') !== false) {
$password = password::hash_password($passwd, 'hash', false);
if ($password === false) {
return PASSWORD_CRYPT_ERROR;
}
$sql = str_replace('%n', $db->quote($password, 'text'), $sql);
}
// hashed passwords (deprecated, use %P)
if (strpos($sql, '%q') !== false) {
$password = password::hash_password($curpass, 'hash', false);
if ($password === false) {
return PASSWORD_CRYPT_ERROR;
}
$sql = str_replace('%q', $db->quote($password, 'text'), $sql);
}
// Handle clear text passwords securely (#1487034)
$sql_vars = array();
if (preg_match_all('/%[p|o]/', $sql, $m)) {
foreach ($m[0] as $var) {
if ($var == '%p') {
$sql = preg_replace('/%p/', '?', $sql, 1);
$sql_vars[] = (string) $passwd;
}
else { // %o
$sql = preg_replace('/%o/', '?', $sql, 1);
$sql_vars[] = (string) $curpass;
}
}
}
$local_part = $rcmail->user->get_username('local');
$domain_part = $rcmail->user->get_username('domain');
$username = $_SESSION['username'];
$host = $_SESSION['imap_host'];
// convert domains to/from punnycode
if ($rcmail->config->get('password_idn_ascii')) {
$domain_part = rcube_utils::idn_to_ascii($domain_part);
$username = rcube_utils::idn_to_ascii($username);
$host = rcube_utils::idn_to_ascii($host);
}
else {
$domain_part = rcube_utils::idn_to_utf8($domain_part);
$username = rcube_utils::idn_to_utf8($username);
$host = rcube_utils::idn_to_utf8($host);
}
// at least we should always have the local part
$sql = str_replace('%l', $db->quote($local_part, 'text'), $sql);
$sql = str_replace('%d', $db->quote($domain_part, 'text'), $sql);
$sql = str_replace('%u', $db->quote($username, 'text'), $sql);
$sql = str_replace('%h', $db->quote($host, 'text'), $sql);
$res = $db->query($sql, $sql_vars);
if (!$db->is_error()) {
if (strtolower(substr(trim($sql),0,6)) == 'select') {
if ($db->fetch_array($res)) {
return PASSWORD_SUCCESS;
}
}
else {
// This is the good case: 1 row updated
if ($db->affected_rows($res) == 1)
return PASSWORD_SUCCESS;
// @TODO: Some queries don't affect any rows
// Should we assume a success if there was no error?
}
}
return PASSWORD_ERROR;
}
/**
* Parse DSN string and replace host variables
*/
protected static function parse_dsn($dsn)
{
if (strpos($dsn, '%')) {
// parse DSN and replace variables in hostname
$parsed = rcube_db::parse_dsn($dsn);
$host = rcube_utils::parse_host($parsed['hostspec']);
// build back the DSN string
if ($host != $parsed['hostspec']) {
$dsn = str_replace('@' . $parsed['hostspec'], '@' . $host, $dsn);
}
}
return $dsn;
}
}
diff --git a/plugins/password/drivers/virtualmin.php b/plugins/password/drivers/virtualmin.php
index cfecf39b2..da8a59840 100644
--- a/plugins/password/drivers/virtualmin.php
+++ b/plugins/password/drivers/virtualmin.php
@@ -1,73 +1,73 @@
<?php
/**
* Virtualmin Password Driver
*
* Driver that adds functionality to change the users Virtualmin password.
* The code is derrived from the Squirrelmail "Change Cyrus/SASL Password" Plugin
* by Thomas Bruederli.
*
* It only works with virtualmin on the same host where Roundcube runs
* and requires shell access and gcc in order to compile the binary.
*
* @version 3.0
* @author Martijn de Munnik
*
- * Copyright (C) 2005-2013, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_virtualmin_password
{
function save($currpass, $newpass, $username)
{
$rcmail = rcmail::get_instance();
$curdir = RCUBE_PLUGINS_DIR . 'password/helpers';
$username = escapeshellarg($username);
// Get the domain using virtualmin CLI:
exec("$curdir/chgvirtualminpasswd list-domains --mail-user $username --name-only", $output_domain, $returnvalue);
if ($returnvalue == 0 && count($output_domain) == 1) {
$domain = trim($output_domain[0]);
}
else {
rcube::raise_error(array(
'code' => 600,
'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: Unable to execute $curdir/chgvirtualminpasswd or domain for mail-user '$username' not known to Virtualmin"
), true, false);
return PASSWORD_ERROR;
}
$domain = escapeshellarg($domain);
$newpass = escapeshellarg($newpass);
exec("$curdir/chgvirtualminpasswd modify-user --domain $domain --user $username --pass $newpass", $output, $returnvalue);
if ($returnvalue == 0) {
return PASSWORD_SUCCESS;
}
rcube::raise_error(array(
'code' => 600,
'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: Unable to execute $curdir/chgvirtualminpasswd"
), true, false);
return PASSWORD_ERROR;
}
}
diff --git a/plugins/password/drivers/vpopmaild.php b/plugins/password/drivers/vpopmaild.php
index 85da98698..5de5c438c 100644
--- a/plugins/password/drivers/vpopmaild.php
+++ b/plugins/password/drivers/vpopmaild.php
@@ -1,71 +1,71 @@
<?php
/**
* vpopmail Password Driver
*
* Driver to change passwords via vpopmaild
*
* @version 2.0
* @author Johannes Hessellund
*
- * Copyright (C) 2005-2013, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_vpopmaild_password
{
function save($curpass, $passwd, $username)
{
$rcmail = rcmail::get_instance();
$vpopmaild = new Net_Socket();
$host = $rcmail->config->get('password_vpopmaild_host');
$port = $rcmail->config->get('password_vpopmaild_port');
$result = $vpopmaild->connect($host, $port, null);
if (is_a($result, 'PEAR_Error')) {
return PASSWORD_CONNECT_ERROR;
}
$vpopmaild->setTimeout($rcmail->config->get('password_vpopmaild_timeout'),0);
$result = $vpopmaild->readLine();
if(!preg_match('/^\+OK/', $result)) {
$vpopmaild->disconnect();
return PASSWORD_CONNECT_ERROR;
}
$vpopmaild->writeLine("slogin ". $username . " " . $curpass);
$result = $vpopmaild->readLine();
if(!preg_match('/^\+OK/', $result) ) {
$vpopmaild->writeLine("quit");
$vpopmaild->disconnect();
return PASSWORD_ERROR;
}
$vpopmaild->writeLine("mod_user ". $username);
$vpopmaild->writeLine("clear_text_password ". $passwd);
$vpopmaild->writeLine(".");
$result = $vpopmaild->readLine();
$vpopmaild->writeLine("quit");
$vpopmaild->disconnect();
if (!preg_match('/^\+OK/', $result)) {
return PASSWORD_ERROR;
}
return PASSWORD_SUCCESS;
}
}
diff --git a/plugins/password/drivers/ximss.php b/plugins/password/drivers/ximss.php
index 711554aba..0dd53ffb5 100644
--- a/plugins/password/drivers/ximss.php
+++ b/plugins/password/drivers/ximss.php
@@ -1,89 +1,89 @@
<?php
/**
* Communigate driver for the Password Plugin for Roundcube
*
* Tested with Communigate Pro 5.1.2
*
* Configuration options:
* password_ximss_host - Host name of Communigate server
* password_ximss_port - XIMSS port on Communigate server
*
* References:
* http://www.communigate.com/WebGuide/XMLAPI.html
*
* @version 2.0
* @author Erik Meitner <erik wanderings.us>
*
- * Copyright (C) 2005-2013, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_ximss_password
{
function save($pass, $newpass, $username)
{
$rcmail = rcmail::get_instance();
$host = $rcmail->config->get('password_ximss_host');
$port = $rcmail->config->get('password_ximss_port');
$sock = stream_socket_client("tcp://$host:$port", $errno, $errstr, 30);
if ($sock === FALSE) {
return PASSWORD_CONNECT_ERROR;
}
// send all requests at once(pipelined)
fwrite($sock, '<login id="A001" authData="'.$username.'" password="'.$pass.'" />'."\0");
fwrite($sock, '<passwordModify id="A002" oldPassword="'.$pass.'" newPassword="'.$newpass.'" />'."\0");
fwrite($sock, '<bye id="A003" />'."\0");
//example responses
// <session id="A001" urlID="4815-vN2Txjkggy7gjHRD10jw" userName="user@example.com"/>\0
// <response id="A001"/>\0
// <response id="A002"/>\0
// <response id="A003"/>\0
// or an error:
// <response id="A001" errorText="incorrect password or account name" errorNum="515"/>\0
$responseblob = '';
while (!feof($sock)) {
$responseblob .= fgets($sock, 1024);
}
fclose($sock);
foreach (explode( "\0",$responseblob) as $response) {
$resp = simplexml_load_string("<xml>".$response."</xml>");
if ($resp->response[0]['id'] == 'A001') {
if (isset($resp->response[0]['errorNum'])) {
return PASSWORD_CONNECT_ERROR;
}
}
else if ($resp->response[0]['id'] == 'A002') {
if (isset($resp->response[0]['errorNum'])) {
return PASSWORD_ERROR;
}
}
else if ($resp->response[0]['id'] == 'A003') {
if (isset($resp->response[0]['errorNum'])) {
// There was a problem during logout (This is probably harmless)
}
}
}
return PASSWORD_SUCCESS;
}
}
diff --git a/plugins/password/drivers/xmail.php b/plugins/password/drivers/xmail.php
index 3dcad4da3..1f6f6a0e4 100644
--- a/plugins/password/drivers/xmail.php
+++ b/plugins/password/drivers/xmail.php
@@ -1,121 +1,121 @@
<?php
/**
* XMail Password Driver
*
* Driver for XMail password
*
* @version 2.0
* @author Helio Cavichiolo Jr <helio@hcsistemas.com.br>
*
* Setup xmail_host, xmail_user, xmail_pass and xmail_port into
* config.inc.php of password plugin as follows:
*
* $config['xmail_host'] = 'localhost';
* $config['xmail_user'] = 'YourXmailControlUser';
* $config['xmail_pass'] = 'YourXmailControlPass';
* $config['xmail_port'] = 6017;
*
- * Copyright (C) 2005-2013, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_xmail_password
{
function save($currpass, $newpass)
{
$rcmail = rcmail::get_instance();
list($user, $domain) = explode('@', $_SESSION['username']);
$xmail = new XMail;
$xmail->hostname = $rcmail->config->get('xmail_host');
$xmail->username = $rcmail->config->get('xmail_user');
$xmail->password = $rcmail->config->get('xmail_pass');
$xmail->port = $rcmail->config->get('xmail_port');
if (!$xmail->connect()) {
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: Unable to connect to mail server"
), true, false);
return PASSWORD_CONNECT_ERROR;
}
if (!$xmail->send("userpasswd\t".$domain."\t".$user."\t".$newpass."\n")) {
$xmail->close();
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: Unable to change password"
), true, false);
return PASSWORD_ERROR;
}
$xmail->close();
return PASSWORD_SUCCESS;
}
}
class XMail {
var $socket;
var $hostname = 'localhost';
var $username = 'xmail';
var $password = '';
var $port = 6017;
function send($msg)
{
socket_write($this->socket,$msg);
if (substr(socket_read($this->socket, 512, PHP_BINARY_READ),0,1) != "+") {
return false;
}
return true;
}
function connect()
{
$this->socket = socket_create(AF_INET, SOCK_STREAM, 0);
if ($this->socket < 0)
return false;
$result = socket_connect($this->socket, $this->hostname, $this->port);
if ($result < 0) {
socket_close($this->socket);
return false;
}
if (substr(socket_read($this->socket, 512, PHP_BINARY_READ),0,1) != "+") {
socket_close($this->socket);
return false;
}
if (!$this->send("$this->username\t$this->password\n")) {
socket_close($this->socket);
return false;
}
return true;
}
function close()
{
$this->send("quit\n");
socket_close($this->socket);
}
}
diff --git a/plugins/password/drivers/zxcvbn.php b/plugins/password/drivers/zxcvbn.php
index ff79639dd..c2084b0e6 100644
--- a/plugins/password/drivers/zxcvbn.php
+++ b/plugins/password/drivers/zxcvbn.php
@@ -1,65 +1,65 @@
<?php
/**
* Zxcvb Password Strength Driver
*
* Driver to check password strength using Zxcvbn-PHP
*
* @version 0.1
* @author Philip Weir
*
- * Copyright (C) 2018 Philip Weir
+ * Copyright (C) Philip Weir
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
class rcube_zxcvbn_password
{
function strength_rules()
{
$rcmail = rcmail::get_instance();
$rules = array();
$rules[] = $rcmail->gettext('password.passwordweak');
$rules[] = $rcmail->gettext('password.passwordnoseq');
$rules[] = $rcmail->gettext('password.passwordnocommon');
return $rules;
}
/**
* Password strength check
*
* @param string $passwd Password
*
* @return array Score (1 to 5) and Reason
*/
function check_strength($passwd)
{
if (!class_exists('ZxcvbnPhp\Zxcvbn')) {
rcube::raise_error(array(
'code' => 600,
'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: Zxcvbn library not found."
), true, false);
return;
}
$rcmail = rcmail::get_instance();
$zxcvbn = new ZxcvbnPhp\Zxcvbn();
$strength = $zxcvbn->passwordStrength($passwd);
return array($strength['score'] + 1, $strength['feedback']['warning']);
}
}
diff --git a/plugins/password/localization/en_US.inc b/plugins/password/localization/en_US.inc
index 641627948..c39ae5ce7 100644
--- a/plugins/password/localization/en_US.inc
+++ b/plugins/password/localization/en_US.inc
@@ -1,45 +1,43 @@
<?php
/*
+-----------------------------------------------------------------------+
- | plugins/password/localization/<lang>.inc |
- | |
| Localization file of the Roundcube Webmail Password plugin |
- | Copyright (C) 2012-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-----------------------------------------------------------------------+
For translation see https://www.transifex.com/projects/p/roundcube-webmail/resource/plugin-password/
*/
$labels = array();
$labels['changepasswd'] = 'Change password';
$labels['curpasswd'] = 'Current Password:';
$labels['newpasswd'] = 'New Password:';
$labels['confpasswd'] = 'Confirm New Password:';
$messages = array();
$messages['nopassword'] = 'Please input new password.';
$messages['nocurpassword'] = 'Please input current password.';
$messages['passwordincorrect'] = 'Current password incorrect.';
$messages['passwordinconsistency'] = 'Passwords do not match, please try again.';
$messages['crypterror'] = 'Could not save new password. Encryption function missing.';
$messages['connecterror'] = 'Could not save new password. Connection error.';
$messages['internalerror'] = 'Could not save new password.';
$messages['passwordshort'] = 'Password must be at least $length characters long.';
$messages['passwordweak'] = 'Password must include at least one number and one punctuation character.';
$messages['passwordtooweak'] = 'Password too weak.';
$messages['passwordnoseq'] = 'Password should not be a sequence like 123456 or QWERTY.';
$messages['passwordnocommon'] = 'Password should not be a common word or name.';
$messages['passwordforbidden'] = 'Password contains forbidden characters.';
$messages['firstloginchange'] = 'This is your first login. Please change your password.';
$messages['disablednotice'] = 'The system is currently under maintenance and password change is not possible at the moment. Everything should be back to normal soon. We apologize for any inconvenience.';
$messages['passwinhistory'] = 'This password has already been used previously.';
$messages['samepasswd'] = 'New password have to be different from the old one.';
$messages['passwdexpirewarning'] = 'Warning! Your password will expire soon, change it before $expirationdatetime.';
$messages['passwdexpired'] = 'Your password has expired, you have to change it now!';
$messages['passwdconstraintviolation'] = 'Password constraint violation. Password probably too weak.';
diff --git a/plugins/password/password.js b/plugins/password/password.js
index 78389f088..0a7c9c200 100644
--- a/plugins/password/password.js
+++ b/plugins/password/password.js
@@ -1,62 +1,62 @@
/**
* Password plugin script
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
- * Copyright (c) 2012-2014, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*/
window.rcmail && rcmail.addEventListener('init', function(evt) {
if (rcmail.env.password_disabled) {
$('#password-form input').prop('disabled', true);
// reload page after ca. 3 minutes
rcmail.reload(3 * 60 * 1000 - 2000);
return;
}
// register command handler
rcmail.register_command('plugin.password-save', function() {
var input_curpasswd = rcube_find_object('_curpasswd'),
input_newpasswd = rcube_find_object('_newpasswd'),
input_confpasswd = rcube_find_object('_confpasswd');
if (input_curpasswd && input_curpasswd.value == '') {
rcmail.alert_dialog(rcmail.get_label('nocurpassword', 'password'), function() {
input_curpasswd.focus();
return true;
});
}
else if (input_newpasswd && input_newpasswd.value == '') {
rcmail.alert_dialog(rcmail.get_label('nopassword', 'password'), function() {
input_newpasswd.focus();
return true;
});
}
else if (input_confpasswd && input_confpasswd.value == '') {
rcmail.alert_dialog(rcmail.get_label('nopassword', 'password'), function() {
input_confpasswd.focus();
return true;
});
}
else if (input_newpasswd && input_confpasswd && input_newpasswd.value != input_confpasswd.value) {
rcmail.alert_dialog(rcmail.get_label('passwordinconsistency', 'password'), function() {
input_newpasswd.focus();
return true;
});
}
else {
rcmail.gui_objects.passform.submit();
}
}, true);
$('input:not(:hidden)').first().focus();
});
diff --git a/plugins/password/password.php b/plugins/password/password.php
index 7eba439bb..728d81774 100644
--- a/plugins/password/password.php
+++ b/plugins/password/password.php
@@ -1,792 +1,792 @@
<?php
/**
* Password Plugin for Roundcube
*
* @author Aleksander Machniak <alec@alec.pl>
*
- * Copyright (C) 2005-2018, The Roundcube Dev Team
+ * Copyright (C) The Roundcube Dev Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see http://www.gnu.org/licenses/.
*/
define('PASSWORD_CRYPT_ERROR', 1);
define('PASSWORD_ERROR', 2);
define('PASSWORD_CONNECT_ERROR', 3);
define('PASSWORD_IN_HISTORY', 4);
define('PASSWORD_CONSTRAINT_VIOLATION', 5);
define('PASSWORD_COMPARE_CURRENT', 6);
define('PASSWORD_COMPARE_NEW', 7);
define('PASSWORD_SUCCESS', 0);
/**
* Change password plugin
*
* Plugin that adds functionality to change a users password.
* It provides common functionality and user interface and supports
* several backends to finally update the password.
*
* For installation and configuration instructions please read the README file.
*
* @author Aleksander Machniak
*/
class password extends rcube_plugin
{
public $task = '?(?!logout).*';
public $noframe = true;
public $noajax = true;
private $newuser = false;
private $drivers = array();
private $rc;
function init()
{
$this->rc = rcmail::get_instance();
$this->load_config();
// update deprecated password_require_nonalpha option removed 20181007
if ($this->rc->config->get('password_minimum_score') === null && $this->rc->config->get('password_require_nonalpha')) {
$this->rc->config->set('password_minimum_score', 2);
}
if ($this->rc->task == 'settings') {
if (!$this->check_host_login_exceptions()) {
return;
}
$this->add_texts('localization/');
$this->add_hook('settings_actions', array($this, 'settings_actions'));
$this->register_action('plugin.password', array($this, 'password_init'));
$this->register_action('plugin.password-save', array($this, 'password_save'));
}
if ($this->rc->config->get('password_force_new_user')) {
if ($this->rc->config->get('newuserpassword') && $this->check_host_login_exceptions()) {
if (!($this->rc->task == 'settings' && strpos($this->rc->action, 'plugin.password') === 0)) {
$this->rc->output->command('redirect', '?_task=settings&_action=plugin.password&_first=1', false);
}
}
$this->add_hook('user_create', array($this, 'user_create'));
$this->add_hook('login_after', array($this, 'login_after'));
}
}
function settings_actions($args)
{
// register as settings action
$args['actions'][] = array(
'action' => 'plugin.password',
'class' => 'password',
'label' => 'password',
'title' => 'changepasswd',
'domain' => 'password',
);
return $args;
}
function password_init()
{
$this->register_handler('plugin.body', array($this, 'password_form'));
$this->rc->output->set_pagetitle($this->gettext('changepasswd'));
if (rcube_utils::get_input_value('_first', rcube_utils::INPUT_GET)) {
$this->rc->output->command('display_message', $this->gettext('firstloginchange'), 'notice');
}
else if (!empty($_SESSION['password_expires'])) {
if ($_SESSION['password_expires'] == 1) {
$this->rc->output->command('display_message', $this->gettext('passwdexpired'), 'error');
}
else {
$this->rc->output->command('display_message', $this->gettext(array(
'name' => 'passwdexpirewarning',
'vars' => array('expirationdatetime' => $_SESSION['password_expires'])
)), 'warning');
}
}
$this->rc->output->send('plugin');
}
function password_save()
{
$this->register_handler('plugin.body', array($this, 'password_form'));
$this->rc->output->set_pagetitle($this->gettext('changepasswd'));
$form_disabled = $this->rc->config->get('password_disabled');
$confirm = $this->rc->config->get('password_confirm_current');
$required_length = intval($this->rc->config->get('password_minimum_length'));
$force_save = $this->rc->config->get('password_force_save');
if (($confirm && !isset($_POST['_curpasswd'])) || !isset($_POST['_newpasswd']) || !strlen($_POST['_newpasswd'])) {
$this->rc->output->command('display_message', $this->gettext('nopassword'), 'error');
}
else {
$charset = strtoupper($this->rc->config->get('password_charset', 'UTF-8'));
$rc_charset = strtoupper($this->rc->output->get_charset());
$sespwd = $this->rc->decrypt($_SESSION['password']);
$curpwd = $confirm ? rcube_utils::get_input_value('_curpasswd', rcube_utils::INPUT_POST, true, $charset) : $sespwd;
$newpwd = rcube_utils::get_input_value('_newpasswd', rcube_utils::INPUT_POST, true);
$conpwd = rcube_utils::get_input_value('_confpasswd', rcube_utils::INPUT_POST, true);
// check allowed characters according to the configured 'password_charset' option
// by converting the password entered by the user to this charset and back to UTF-8
$orig_pwd = $newpwd;
$chk_pwd = rcube_charset::convert($orig_pwd, $rc_charset, $charset);
$chk_pwd = rcube_charset::convert($chk_pwd, $charset, $rc_charset);
// We're doing this for consistence with Roundcube core
$newpwd = rcube_charset::convert($newpwd, $rc_charset, $charset);
$conpwd = rcube_charset::convert($conpwd, $rc_charset, $charset);
if ($chk_pwd != $orig_pwd || preg_match('/[\x00-\x1F\x7F]/', $newpwd)) {
$this->rc->output->command('display_message', $this->gettext('passwordforbidden'), 'error');
}
// other passwords validity checks
else if ($conpwd != $newpwd) {
$this->rc->output->command('display_message', $this->gettext('passwordinconsistency'), 'error');
}
else if ($confirm && ($res = $this->_compare($sespwd, $curpwd, PASSWORD_COMPARE_CURRENT))) {
$this->rc->output->command('display_message', $res, 'error');
}
else if ($required_length && strlen($newpwd) < $required_length) {
$this->rc->output->command('display_message', $this->gettext(
array('name' => 'passwordshort', 'vars' => array('length' => $required_length))), 'error');
}
else if ($res = $this->_check_strength($newpwd)) {
$this->rc->output->command('display_message', $res, 'error');
}
// password is the same as the old one, warn user, return error
else if (!$force_save && ($res = $this->_compare($sespwd, $newpwd, PASSWORD_COMPARE_NEW))) {
$this->rc->output->command('display_message', $res, 'error');
}
// try to save the password
else if (!($res = $this->_save($curpwd, $newpwd))) {
$this->rc->output->command('display_message', $this->gettext('successfullysaved'), 'confirmation');
// allow additional actions after password change (e.g. reset some backends)
$plugin = $this->rc->plugins->exec_hook('password_change', array(
'old_pass' => $curpwd, 'new_pass' => $newpwd));
// Reset session password
$_SESSION['password'] = $this->rc->encrypt($plugin['new_pass']);
if ($this->rc->config->get('newuserpassword')) {
$this->rc->user->save_prefs(array('newuserpassword' => false));
}
// Log password change
if ($this->rc->config->get('password_log')) {
rcube::write_log('password', sprintf('Password changed for user %s (ID: %d) from %s',
$this->rc->get_user_name(), $this->rc->user->ID, rcube_utils::remote_ip()));
}
// Remove expiration date/time
$this->rc->session->remove('password_expires');
}
else {
$this->rc->output->command('display_message', $res, 'error');
}
}
$this->rc->overwrite_action('plugin.password');
$this->rc->output->send('plugin');
}
function password_form()
{
// add some labels to client
$this->rc->output->add_label(
'password.nopassword',
'password.nocurpassword',
'password.passwordinconsistency'
);
$form_disabled = $this->rc->config->get('password_disabled');
$this->rc->output->set_env('product_name', $this->rc->config->get('product_name'));
$this->rc->output->set_env('password_disabled', !empty($form_disabled));
$table = new html_table(array('cols' => 2, 'class' => 'propform'));
if ($this->rc->config->get('password_confirm_current')) {
// show current password selection
$field_id = 'curpasswd';
$input_curpasswd = new html_passwordfield(array(
'name' => '_curpasswd',
'id' => $field_id,
'size' => 20,
'autocomplete' => 'off',
));
$table->add('title', html::label($field_id, rcube::Q($this->gettext('curpasswd'))));
$table->add(null, $input_curpasswd->show());
}
// show new password selection
$field_id = 'newpasswd';
$input_newpasswd = new html_passwordfield(array(
'name' => '_newpasswd',
'id' => $field_id,
'size' => 20,
'autocomplete' => 'off',
));
$table->add('title', html::label($field_id, rcube::Q($this->gettext('newpasswd'))));
$table->add(null, $input_newpasswd->show());
// show confirm password selection
$field_id = 'confpasswd';
$input_confpasswd = new html_passwordfield(array(
'name' => '_confpasswd',
'id' => $field_id,
'size' => 20,
'autocomplete' => 'off',
));
$table->add('title', html::label($field_id, rcube::Q($this->gettext('confpasswd'))));
$table->add(null, $input_confpasswd->show());
$rules = '';
$required_length = intval($this->rc->config->get('password_minimum_length'));
if ($required_length > 0) {
$rules .= html::tag('li', array('class' => 'required-length'), $this->gettext(array(
'name' => 'passwordshort',
'vars' => array('length' => $required_length)
)));
}
if ($msgs = $this->_strength_rules()) {
foreach ($msgs as $msg) {
$rules .= html::tag('li', array('class' => 'strength-rule'), $msg);
}
}
if (!empty($rules)) {
$rules = html::tag('ul', array('id' => 'ruleslist', 'class' => 'hint proplist'), $rules);
}
$disabled_msg = '';
if ($form_disabled) {
$disabled_msg = is_string($form_disabled) ? $form_disabled : $this->gettext('disablednotice');
$disabled_msg = html::div(array('class' => 'boxwarning', 'id' => 'password-notice'), $disabled_msg);
}
$submit_button = $this->rc->output->button(array(
'command' => 'plugin.password-save',
'class' => 'button mainaction submit',
'label' => 'save',
));
$form_buttons = html::p(array('class' => 'formbuttons footerleft'), $submit_button);
$this->rc->output->add_gui_object('passform', 'password-form');
$this->include_script('password.js');
$form = $this->rc->output->form_tag(array(
'id' => 'password-form',
'name' => 'password-form',
'method' => 'post',
'action' => './?_task=settings&_action=plugin.password-save',
), $disabled_msg . $table->show() . $rules);
return html::div(array('id' => 'prefs-title', 'class' => 'boxtitle'), $this->gettext('changepasswd'))
. html::div(array('class' => 'box formcontainer scroller'),
html::div(array('class' => 'boxcontent formcontent'), $form)
. $form_buttons);
}
private function _compare($curpwd, $newpwd, $type)
{
$driver = $this->_load_driver();
if (!$driver) {
$result = $this->gettext('internalerror');
}
else if (method_exists($driver, 'compare')) {
$result = $driver->compare($curpwd, $newpwd, $type);
}
else {
switch ($type) {
case PASSWORD_COMPARE_CURRENT:
$result = $curpwd != $newpwd ? $this->gettext('passwordincorrect') : null;
break;
case PASSWORD_COMPARE_NEW:
$result = $curpwd == $newpwd ? $this->gettext('samepasswd') : null;
break;
default:
$result = $this->gettext('internalerror');
}
}
return $result;
}
private function _strength_rules()
{
if (($driver = $this->_load_driver('strength')) && method_exists($driver, 'strength_rules')) {
$result = $driver->strength_rules();
}
else if ($this->rc->config->get('password_minimum_score') > 1) {
$result = $this->gettext('passwordweak');
}
if (!is_array($result)) {
$result = array($result);
}
return $result;
}
private function _check_strength($passwd)
{
$min_score = $this->rc->config->get('password_minimum_score');
if (!$min_score) {
return;
}
if (($driver = $this->_load_driver('strength')) && method_exists($driver, 'check_strength')) {
list($score, $reason) = $driver->check_strength($passwd);
}
else {
$score = (!preg_match("/[0-9]/", $passwd) || !preg_match("/[^A-Za-z0-9]/", $passwd)) ? 1 : 5;
}
if ($score < $min_score) {
return $this->gettext('passwordtooweak') . (!empty($reason) ? " $reason" : '');
}
}
private function _save($curpass, $passwd)
{
if (!($driver = $this->_load_driver())) {
return $this->gettext('internalerror');
}
$result = $driver->save($curpass, $passwd, self::username());
$message = '';
if (is_array($result)) {
$message = $result['message'];
$result = $result['code'];
}
switch ($result) {
case PASSWORD_SUCCESS:
return;
case PASSWORD_CRYPT_ERROR:
$reason = $this->gettext('crypterror');
break;
case PASSWORD_CONNECT_ERROR:
$reason = $this->gettext('connecterror');
break;
case PASSWORD_IN_HISTORY:
$reason = $this->gettext('passwdinhistory');
break;
case PASSWORD_CONSTRAINT_VIOLATION:
$reason = $this->gettext('passwdconstraintviolation');
break;
case PASSWORD_ERROR:
default:
$reason = $this->gettext('internalerror');
}
if ($message) {
$reason .= ' ' . $message;
}
return $reason;
}
private function _load_driver($type = 'password')
{
if (!($type && $driver = $this->rc->config->get('password_' . $type . '_driver'))) {
$driver = $this->rc->config->get('password_driver', 'sql');
}
if (!$this->drivers[$type]) {
$class = "rcube_{$driver}_password";
$file = $this->home . "/drivers/$driver.php";
if (!file_exists($file)) {
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: Driver file does not exist ($file)"
), true, false);
return false;
}
include_once $file;
if (!class_exists($class, false) || (!method_exists($class, 'save') && !method_exists($class, 'check_strength'))) {
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: Broken driver $driver"
), true, false);
return false;
}
$this->drivers[$type] = new $class;
}
return $this->drivers[$type];
}
function user_create($args)
{
$this->newuser = true;
return $args;
}
function login_after($args)
{
if ($this->newuser && $this->check_host_login_exceptions()) {
$this->rc->user->save_prefs(array('newuserpassword' => true));
$args['_task'] = 'settings';
$args['_action'] = 'plugin.password';
$args['_first'] = 'true';
}
return $args;
}
// Check if host and login is allowed to change the password, false = not allowed, true = not allowed
private function check_host_login_exceptions()
{
// Host exceptions
$hosts = $this->rc->config->get('password_hosts');
if (!empty($hosts) && !in_array($_SESSION['storage_host'], (array) $hosts)) {
return false;
}
// Login exceptions
if ($exceptions = $this->rc->config->get('password_login_exceptions')) {
$exceptions = array_map('trim', (array) $exceptions);
$exceptions = array_filter($exceptions);
$username = $_SESSION['username'];
foreach ($exceptions as $ec) {
if ($username === $ec) {
return false;
}
}
}
return true;
}
/**
* Hashes a password and returns the hash based on the specified method
*
* Parts of the code originally from the phpLDAPadmin development team
* http://phpldapadmin.sourceforge.net/
*
* @param string Clear password
* @param string Hashing method
* @param bool|string Prefix string or TRUE to add a default prefix
*
* @return string Hashed password
*/
static function hash_password($password, $method = '', $prefixed = true)
{
$method = strtolower($method);
$rcmail = rcmail::get_instance();
$prefix = '';
$crypted = '';
if (empty($method) || $method == 'default') {
$method = $rcmail->config->get('password_algorithm');
$prefixed = $rcmail->config->get('password_algorithm_prefix');
}
else if ($method == 'crypt') { // deprecated
if (!($method = $rcmail->config->get('password_crypt_hash'))) {
$method = 'md5';
}
if (!strpos($method, '-crypt')) {
$method .= '-crypt';
}
}
switch ($method) {
case 'des':
case 'des-crypt':
$crypted = crypt($password, rcube_utils::random_bytes(2));
$prefix = '{CRYPT}';
break;
case 'ext_des': // for BC
case 'ext-des-crypt':
$crypted = crypt($password, '_' . rcube_utils::random_bytes(8));
$prefix = '{CRYPT}';
break;
case 'md5crypt': // for BC
case 'md5-crypt':
$crypted = crypt($password, '$1$' . rcube_utils::random_bytes(9));
$prefix = '{CRYPT}';
break;
case 'sha256-crypt':
$rounds = (int) $rcmail->config->get('password_crypt_rounds');
$prefix = '$5$';
if ($rounds > 1000) {
$prefix .= 'rounds=' . $rounds . '$';
}
$crypted = crypt($password, $prefix . rcube_utils::random_bytes(16));
$prefix = '{CRYPT}';
break;
case 'sha512-crypt':
$rounds = (int) $rcmail->config->get('password_crypt_rounds');
$prefix = '$6$';
if ($rounds > 1000) {
$prefix .= 'rounds=' . $rounds . '$';
}
$crypted = crypt($password, $prefix . rcube_utils::random_bytes(16));
$prefix = '{CRYPT}';
break;
case 'blowfish': // for BC
case 'blowfish-crypt':
$cost = (int) $rcmail->config->get('password_blowfish_cost');
$cost = $cost < 4 || $cost > 31 ? 12 : $cost;
$prefix = sprintf('$2a$%02d$', $cost);
$crypted = crypt($password, $prefix . rcube_utils::random_bytes(22));
$prefix = '{CRYPT}';
break;
case 'md5':
$crypted = base64_encode(pack('H*', md5($password)));
$prefix = '{MD5}';
break;
case 'sha':
if (function_exists('sha1')) {
$crypted = pack('H*', sha1($password));
}
else if (function_exists('hash')) {
$crypted = hash('sha1', $password, true);
}
else if (function_exists('mhash')) {
$crypted = mhash(MHASH_SHA1, $password);
}
else {
rcube::raise_error(array(
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: Your PHP install does not have the mhash()/hash() nor sha1() function"
), true, true);
}
$crypted = base64_encode($crypted);
$prefix = '{SHA}';
break;
case 'ssha':
$salt = rcube_utils::random_bytes(8);
if (function_exists('mhash') && function_exists('mhash_keygen_s2k')) {
$salt = mhash_keygen_s2k(MHASH_SHA1, $password, $salt, 4);
$crypted = mhash(MHASH_SHA1, $password . $salt);
}
else if (function_exists('sha1')) {
$salt = substr(pack("H*", sha1($salt . $password)), 0, 4);
$crypted = sha1($password . $salt, true);
}
else if (function_exists('hash')) {
$salt = substr(pack("H*", hash('sha1', $salt . $password)), 0, 4);
$crypted = hash('sha1', $password . $salt, true);
}
else {
rcube::raise_error(array(
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: Your PHP install does not have the mhash()/hash() nor sha1() function"
), true, true);
}
$crypted = base64_encode($crypted . $salt);
$prefix = '{SSHA}';
break;
case 'smd5':
$salt = rcube_utils::random_bytes(8);
if (function_exists('mhash') && function_exists('mhash_keygen_s2k')) {
$salt = mhash_keygen_s2k(MHASH_MD5, $password, $salt, 4);
$crypted = mhash(MHASH_MD5, $password . $salt);
}
else if (function_exists('hash')) {
$salt = substr(pack("H*", hash('md5', $salt . $password)), 0, 4);
$crypted = hash('md5', $password . $salt, true);
}
else {
$salt = substr(pack("H*", md5($salt . $password)), 0, 4);
$crypted = md5($password . $salt, true);
}
$crypted = base64_encode($crypted . $salt);
$prefix = '{SMD5}';
break;
case 'samba':
if (function_exists('hash')) {
$crypted = hash('md4', rcube_charset::convert($password, RCUBE_CHARSET, 'UTF-16LE'));
$crypted = strtoupper($crypted);
}
else {
rcube::raise_error(array(
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: Your PHP install does not have hash() function"
), true, true);
}
break;
case 'ad':
$crypted = rcube_charset::convert('"' . $password . '"', RCUBE_CHARSET, 'UTF-16LE');
break;
case 'cram-md5': // deprecated
require_once __DIR__ . '/../helpers/dovecot_hmacmd5.php';
$crypted = dovecot_hmacmd5($password);
$prefix = '{CRAM-MD5}';
break;
case 'dovecot':
if (!($dovecotpw = $rcmail->config->get('password_dovecotpw'))) {
$dovecotpw = 'dovecotpw';
}
if (!($method = $rcmail->config->get('password_dovecotpw_method'))) {
$method = 'CRAM-MD5';
}
$spec = array(0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('file', '/dev/null', 'a'));
$pipe = proc_open("$dovecotpw -s '$method'", $spec, $pipes);
if (!is_resource($pipe)) {
return false;
}
fwrite($pipes[0], $password . "\n", 1+strlen($password));
usleep(1000);
fwrite($pipes[0], $password . "\n", 1+strlen($password));
$crypted = trim(stream_get_contents($pipes[1]), "\n");
fclose($pipes[0]);
fclose($pipes[1]);
proc_close($pipe);
if (!preg_match('/^\{' . $method . '\}/', $crypted)) {
return false;
}
if (!$prefixed) {
$prefixed = (bool) $rcmail->config->get('password_dovecotpw_with_method');
}
if (!$prefixed) {
$crypted = trim(str_replace('{' . $method . '}', '', $crypted));
}
$prefixed = false;
break;
case 'hash': // deprecated
if (!extension_loaded('hash')) {
rcube::raise_error(array(
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Password plugin: 'hash' extension not loaded!"
), true, true);
}
if (!($hash_algo = strtolower($rcmail->config->get('password_hash_algorithm')))) {
$hash_algo = 'sha1';
}
$crypted = hash($hash_algo, $password);
if ($rcmail->config->get('password_hash_base64')) {
$crypted = base64_encode(pack('H*', $crypted));
}
break;
case 'clear':
$crypted = $password;
}
if ($crypted === null || $crypted === false) {
return false;
}
if ($prefixed && $prefixed !== true) {
$prefix = $prefixed;
$prefixed = true;
}
if ($prefixed === true && $prefix) {
$crypted = $prefix . $crypted;
}
return $crypted;
}
/**
* Returns username in a configured form appropriate for the driver
*
* @param string $format Username format
*
* @return string Username
*/
static function username($format = null)
{
$rcmail = rcmail::get_instance();
if (!$format) {
$format = $rcmail->config->get('password_username_format');
}
if (!$format) {
return $_SESSION['username'];
}
return strtr($format, array(
'%l' => $rcmail->user->get_username('local'),
'%d' => $rcmail->user->get_username('domain'),
'%u' => $_SESSION['username'],
));
}
}
diff --git a/plugins/redundant_attachments/redundant_attachments.php b/plugins/redundant_attachments/redundant_attachments.php
index 149ac9998..243b43aea 100644
--- a/plugins/redundant_attachments/redundant_attachments.php
+++ b/plugins/redundant_attachments/redundant_attachments.php
@@ -1,242 +1,242 @@
<?php
/**
* Redundant attachments
*
* This plugin provides a redundant storage for temporary uploaded
* attachment files. They are stored in both the database backend
* as well as on the local file system.
*
* It provides also memcache/redis store as a fallback (see config file).
*
* This plugin relies on the core filesystem_attachments plugin
* and combines it with the functionality of the database_attachments plugin.
*
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
- * Copyright (C) 2011-2018, The Roundcube Dev Team
- * Copyright (C) 2011-2018, Kolab Systems AG
+ * Copyright (C) The Roundcube Dev Team
+ * Copyright (C) Kolab Systems AG
*
* 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.
*/
if (class_exists('filesystem_attachments', false) && !defined('TESTS_DIR')) {
die("Configuration issue. There can be only one enabled plugin for attachments handling");
}
require_once(RCUBE_PLUGINS_DIR . 'filesystem_attachments/filesystem_attachments.php');
class redundant_attachments extends filesystem_attachments
{
// A prefix for the cache key used in the session and in the key field of the cache table
const PREFIX = "ATTACH";
// rcube_cache instance for SQL DB
private $cache;
// rcube_cache instance for memcache/redis
private $mem_cache;
private $loaded;
/**
* Loads plugin configuration and initializes cache object(s)
*/
private function _load_drivers()
{
if ($this->loaded) {
return;
}
$rcmail = rcube::get_instance();
// load configuration
$this->load_config();
$ttl = 12 * 60 * 60; // 12 hours
$ttl = $rcmail->config->get('redundant_attachments_cache_ttl', $ttl);
$fallback = $rcmail->config->get('redundant_attachments_fallback');
$prefix = self::PREFIX;
if ($id = session_id()) {
$prefix .= $id;
}
if ($fallback === null) {
$fallback = $rcmail->config->get('redundant_attachments_memcache') ? 'memcache' : null; // BC
}
// Init SQL cache (disable cache data serialization)
$this->cache = $rcmail->get_cache($prefix, 'db', $ttl, false);
// Init memcache/redis (fallback) cache
if ($fallback) {
$this->mem_cache = $rcmail->get_cache($prefix, $fallback, $ttl, false);
}
$this->loaded = true;
}
/**
* Helper method to generate a unique key for the given attachment file
*/
private function _key($args)
{
$uname = $args['path'] ?: $args['name'];
return $args['group'] . md5(time() . $uname . $_SESSION['user_id']);
}
/**
* Save a newly uploaded attachment
*/
function upload($args)
{
$args = parent::upload($args);
$this->_load_drivers();
$key = $this->_key($args);
$data = base64_encode(file_get_contents($args['path']));
$status = $this->cache->write($key, $data);
if (!$status && $this->mem_cache) {
$status = $this->mem_cache->write($key, $data);
}
if ($status) {
$args['id'] = $key;
$args['status'] = true;
}
return $args;
}
/**
* Save an attachment from a non-upload source (draft or forward)
*/
function save($args)
{
$args = parent::save($args);
$this->_load_drivers();
$data = $args['path'] ? file_get_contents($args['path']) : $args['data'];
$args['data'] = null;
$key = $this->_key($args);
$data = base64_encode($data);
$status = $this->cache->write($key, $data);
if (!$status && $this->mem_cache) {
$status = $this->mem_cache->write($key, $data);
}
if ($status) {
$args['id'] = $key;
$args['status'] = true;
}
return $args;
}
/**
* Remove an attachment from storage
* This is triggered by the remove attachment button on the compose screen
*/
function remove($args)
{
parent::remove($args);
$this->_load_drivers();
$status = $this->cache->remove($args['id']);
if (!$status && $this->mem_cache) {
$status = $this->cache->remove($args['id']);
}
// we cannot trust the result of any of the methods above
// assume true, attachments will be removed on cleanup
$args['status'] = true;
return $args;
}
/**
* When composing an html message, image attachments may be shown
* For this plugin, $this->get() will check the file and
* return it's contents
*/
function display($args)
{
return $this->get($args);
}
/**
* When displaying or sending the attachment the file contents are fetched
* using this method. This is also called by the attachment_display hook.
*/
function get($args)
{
// attempt to get file from local file system
$args = parent::get($args);
if ($args['path'] && ($args['status'] = file_exists($args['path'])))
return $args;
$this->_load_drivers();
// fetch from database if not found on FS
$data = $this->cache->read($args['id']);
// fetch from memcache if not found on FS and DB
if (($data === false || $data === null) && $this->mem_cache) {
$data = $this->mem_cache->read($args['id']);
}
if ($data) {
$args['data'] = base64_decode($data);
$args['status'] = true;
}
return $args;
}
/**
* Delete all temp files associated with this user
*/
function cleanup($args)
{
$this->_load_drivers();
if ($this->cache) {
$this->cache->remove($args['group'], true);
}
if ($this->mem_cache) {
$this->mem_cache->remove($args['group'], true);
}
parent::cleanup($args);
$args['status'] = true;
return $args;
}
}
diff --git a/plugins/subscriptions_option/localization/en_US.inc b/plugins/subscriptions_option/localization/en_US.inc
index 3eb18fc1d..f4008f8d6 100644
--- a/plugins/subscriptions_option/localization/en_US.inc
+++ b/plugins/subscriptions_option/localization/en_US.inc
@@ -1,22 +1,18 @@
<?php
/*
+-----------------------------------------------------------------------+
- | plugins/subscriptions_option/localization/<lang>.inc |
- | |
| Localization file of the Roundcube Webmail Subscriptions plugin |
- | Copyright (C) 2012-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-----------------------------------------------------------------------+
For translation see https://www.transifex.com/projects/p/roundcube-webmail/resource/plugin-subscriptions_option/
*/
$labels = array();
$labels['useimapsubscriptions'] = 'Use IMAP Subscriptions';
-
-?>
diff --git a/plugins/userinfo/localization/en_US.inc b/plugins/userinfo/localization/en_US.inc
index 01230de85..86dd44017 100644
--- a/plugins/userinfo/localization/en_US.inc
+++ b/plugins/userinfo/localization/en_US.inc
@@ -1,25 +1,21 @@
<?php
/*
+-----------------------------------------------------------------------+
- | plugins/userinfo/localization/<lang>.inc |
- | |
| Localization file of the Roundcube Webmail Userinfo plugin |
- | Copyright (C) 2012-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-----------------------------------------------------------------------+
For translation see https://www.transifex.com/projects/p/roundcube-webmail/resource/plugin-userinfo/
*/
$labels = array();
$labels['userinfo'] = 'User info';
$labels['created'] = 'Created';
$labels['lastlogin'] = 'Last Login';
$labels['defaultidentity'] = 'Default Identity';
-
-?>
\ No newline at end of file
diff --git a/plugins/vcard_attachments/localization/en_US.inc b/plugins/vcard_attachments/localization/en_US.inc
index 99af44509..e891776a0 100644
--- a/plugins/vcard_attachments/localization/en_US.inc
+++ b/plugins/vcard_attachments/localization/en_US.inc
@@ -1,27 +1,23 @@
<?php
/*
+-----------------------------------------------------------------------+
- | plugins/vcard_attachments/localization/<lang>.inc |
- | |
| Localization file of the Roundcube Webmail Vcard Attachments plugin |
- | Copyright (C) 2012-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-----------------------------------------------------------------------+
For translation see https://www.transifex.com/projects/p/roundcube-webmail/resource/plugin-vcard_attachments/
*/
$labels = array();
$labels['addvcardmsg'] = 'Add vCard to addressbook';
$labels['vcardsavefailed'] = 'Unable to save vCard';
$labels['attachvcard'] = 'Attach vCard';
$labels['vcardattached'] = 'vCard file attached.';
$labels['vcard'] = 'vCard';
$labels['forwardvcard'] = 'Forward vCard';
-
-?>
\ No newline at end of file
diff --git a/plugins/vcard_attachments/vcardattach.js b/plugins/vcard_attachments/vcardattach.js
index bb2c18c59..248e80070 100644
--- a/plugins/vcard_attachments/vcardattach.js
+++ b/plugins/vcard_attachments/vcardattach.js
@@ -1,105 +1,105 @@
/**
* vcard_attachments plugin script
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
- * Copyright (c) 2012-2017, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*/
function plugin_vcard_save_contact(mime_id)
{
var lock = rcmail.set_busy(true, 'loading');
rcmail.http_post('plugin.savevcard', { _uid: rcmail.env.uid, _mbox: rcmail.env.mailbox, _part: mime_id }, lock);
return false;
}
function plugin_vcard_insertrow(data)
{
if (data.row.ctype.match(/^(text\/vcard|text\/x-vcard|text\/directory)$/i)) {
$(data.row.obj).find('.attachment > .attachment').addClass('vcard');
}
}
function plugin_vcard_attach()
{
var id, n, contacts = [],
ts = new Date().getTime(),
args = {_uploadid: ts, _id: rcmail.env.compose_id || null},
selection = rcmail.contact_list.get_selection();
for (n=0; n < selection.length; n++) {
if (rcmail.env.task == 'addressbook') {
id = selection[n];
contacts.push(rcmail.env.source + '-' + id + '-0');
}
else {
id = selection[n];
if (id && id.charAt(0) != 'E' && rcmail.env.contactdata[id])
contacts.push(id);
}
}
if (!contacts.length)
return false;
args._uri = 'vcard://' + contacts.join(',');
if (rcmail.env.task == 'addressbook') {
args._attach_vcard = 1;
rcmail.open_compose_step(args);
}
else {
// add to attachments list
if (!rcmail.add2attachment_list(ts, {name: '', html: rcmail.get_label('attaching'), classname: 'uploading', complete: false}))
rcmail.file_upload_id = rcmail.set_busy(true, 'attaching');
rcmail.http_post('upload', args);
}
}
window.rcmail && rcmail.addEventListener('init', function(evt) {
if (rcmail.gui_objects.messagelist)
rcmail.addEventListener('insertrow', function(data, evt) { plugin_vcard_insertrow(data); });
if ((rcmail.env.action == 'compose' || (rcmail.env.task == 'addressbook' && rcmail.env.action == '')) && rcmail.gui_objects.contactslist) {
if (rcmail.env.action == 'compose') {
rcmail.env.compose_commands.push('attach-vcard');
// Elastic: add "Attach vCard" button to the attachments widget
if (window.UI && UI.recipient_selector) {
var button, form = $('#compose-attachments > div');
button = $('<button class="btn btn-secondary attach vcard">')
.attr({type: 'button', tabindex: $('button,input', form).first().attr('tabindex') || 0})
.text(rcmail.gettext('vcard_attachments.attachvcard'))
.appendTo(form)
.click(function() {
UI.recipient_selector('', {
title: 'vcard_attachments.attachvcard',
button: 'vcard_attachments.attachvcard',
button_class: 'attach',
focus: button,
multiselect: false,
action: function() { rcmail.command('attach-vcard'); }
});
});
}
}
rcmail.register_command('attach-vcard', function() { plugin_vcard_attach(); });
rcmail.contact_list.addEventListener('select', function(list) {
// TODO: support attaching more than one at once
var selection = list.get_selection();
rcmail.enable_command('attach-vcard', selection.length == 1 && selection[0].charAt(0) != 'E');
});
}
});
diff --git a/plugins/zipdownload/localization/en_US.inc b/plugins/zipdownload/localization/en_US.inc
index 15041d57b..a5847fba9 100644
--- a/plugins/zipdownload/localization/en_US.inc
+++ b/plugins/zipdownload/localization/en_US.inc
@@ -1,25 +1,23 @@
<?php
/*
+-----------------------------------------------------------------------+
- | plugins/zipdownload/localization/<lang>.inc |
- | |
| Localization file of the Roundcube Webmail Zipdownload plugin |
- | Copyright (C) 2012-2014, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-----------------------------------------------------------------------+
For translation see https://www.transifex.com/projects/p/roundcube-webmail/resource/plugin-zipdownload/
*/
$labels = array();
$labels['downloadall'] = 'Download all attachments';
$labels['download'] = 'Download...';
$labels['downloadmbox'] = 'Mbox format (.zip)';
$labels['downloadmaildir'] = 'Maildir format (.zip)';
$labels['downloademl'] = 'Source (.eml)';
$labels['sizelimiterror'] = 'Total size of selected messages exceeds the limit ($size)';
diff --git a/plugins/zipdownload/zipdownload.js b/plugins/zipdownload/zipdownload.js
index ae3f0aad3..c2e7b4ead 100644
--- a/plugins/zipdownload/zipdownload.js
+++ b/plugins/zipdownload/zipdownload.js
@@ -1,102 +1,102 @@
/**
* ZipDownload plugin script
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
- * Copyright (c) 2013-2018, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*/
window.rcmail && rcmail.addEventListener('init', function(evt) {
// register additional actions
rcmail.register_command('download-eml', function() { rcmail_zipdownload('eml'); });
rcmail.register_command('download-mbox', function() { rcmail_zipdownload('mbox'); });
rcmail.register_command('download-maildir', function() { rcmail_zipdownload('maildir'); });
// commands status
rcmail.message_list && rcmail.message_list.addEventListener('select', function(list) {
var selected = list.get_selection().length;
rcmail.enable_command('download', selected > 0);
rcmail.enable_command('download-eml', selected == 1);
rcmail.enable_command('download-mbox', 'download-maildir', selected > 1);
});
// hook before default download action
rcmail.addEventListener('beforedownload', rcmail_zipdownload_menu);
// find and modify default download link/button
$.each(rcmail.buttons['download'] || [], function() {
var link = $('#' + this.id),
span = $('span', link);
if (!span.length) {
span = $('<span>');
link.html('').append(span);
}
link.attr('aria-haspopup', 'true');
span.text(rcmail.get_label('zipdownload.download'));
rcmail.env.download_link = link;
});
});
function rcmail_zipdownload(mode)
{
// default .eml download of single message
if (mode == 'eml') {
var uid = rcmail.get_single_uid();
rcmail.goto_url('viewsource', rcmail.params_from_uid(uid, {_save: 1}), false, true);
return;
}
// multi-message download, use hidden form to POST selection
if (rcmail.message_list && rcmail.message_list.get_selection().length > 1) {
var inputs = [],
post = rcmail.selection_post_data(),
id = 'zipdownload-' + new Date().getTime(),
iframe = $('<iframe>').attr({name: id, style: 'display:none'}),
form = $('<form>').attr({
target: id,
style: 'display: none',
method: 'post',
action: '?_task=mail&_action=plugin.zipdownload.messages'
});
post._mode = mode;
post._token = rcmail.env.request_token;
$.each(post, function(k, v) {
if (typeof v == 'object' && v.length > 1) {
for (var j=0; j < v.length; j++)
inputs.push($('<input>').attr({type: 'hidden', name: k+'[]', value: v[j]}));
}
else {
inputs.push($('<input>').attr({type: 'hidden', name: k, value: v}));
}
});
iframe.appendTo(document.body);
form.append(inputs).appendTo(document.body).submit();
}
}
// display download options menu
function rcmail_zipdownload_menu(e)
{
// show (sub)menu for download selection
rcmail.command('menu-open', 'zipdownload-menu', e && e.target ? e.target : rcmail.env.download_link, e);
// abort default download action
return false;
}
diff --git a/program/include/clisetup.php b/program/include/clisetup.php
index 324bac364..5c2e30c5d 100644
--- a/program/include/clisetup.php
+++ b/program/include/clisetup.php
@@ -1,32 +1,31 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/include/clisetup.php |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2010-2014, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Setup the command line environment and provide some utitlity |
| functions. |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
if (php_sapi_name() != 'cli') {
die('Not on the "shell" (php-cli).');
}
require_once INSTALL_PATH . 'program/include/iniset.php';
// Unset max. execution time limit, set to 120 seconds in iniset.php
@set_time_limit(0);
$rcmail = rcmail::get_instance();
$rcmail->output = new rcmail_output_cli();
diff --git a/program/include/iniset.php b/program/include/iniset.php
index 0c019724d..255925cab 100644
--- a/program/include/iniset.php
+++ b/program/include/iniset.php
@@ -1,82 +1,81 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/include/iniset.php |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2017, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Setup the application environment required to process |
| any request. |
+-----------------------------------------------------------------------+
| Author: Till Klampaeckel <till@php.net> |
| Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
// application constants
define('RCMAIL_VERSION', '1.4-git');
define('RCMAIL_START', microtime(true));
if (!defined('INSTALL_PATH')) {
define('INSTALL_PATH', dirname($_SERVER['SCRIPT_FILENAME']).'/');
}
if (!defined('RCMAIL_CONFIG_DIR')) {
define('RCMAIL_CONFIG_DIR', getenv('ROUNDCUBE_CONFIG_DIR') ?: (INSTALL_PATH . 'config'));
}
if (!defined('RCUBE_LOCALIZATION_DIR')) {
define('RCUBE_LOCALIZATION_DIR', INSTALL_PATH . 'program/localization/');
}
define('RCUBE_INSTALL_PATH', INSTALL_PATH);
define('RCUBE_CONFIG_DIR', RCMAIL_CONFIG_DIR.'/');
// RC include folders MUST be included FIRST to avoid other
// possible not compatible libraries (i.e PEAR) to be included
// instead the ones provided by RC
$include_path = INSTALL_PATH . 'program/lib' . PATH_SEPARATOR;
$include_path.= ini_get('include_path');
if (set_include_path($include_path) === false) {
die("Fatal error: ini_set/set_include_path does not work.");
}
// increase maximum execution time for php scripts
// (does not work in safe mode)
@set_time_limit(120);
// include composer autoloader (if available)
if (@file_exists(INSTALL_PATH . 'vendor/autoload.php')) {
require INSTALL_PATH . 'vendor/autoload.php';
}
// include Roundcube Framework
require_once 'Roundcube/bootstrap.php';
// register autoloader for rcmail app classes
spl_autoload_register('rcmail_autoload');
/**
* PHP5 autoloader routine for dynamic class loading
*/
function rcmail_autoload($classname)
{
if (strpos($classname, 'rcmail') === 0) {
$filepath = INSTALL_PATH . "program/include/$classname.php";
if (is_readable($filepath)) {
include_once $filepath;
return true;
}
}
return false;
}
diff --git a/program/include/rcmail.php b/program/include/rcmail.php
index 3c4c5bd04..2dde84c66 100644
--- a/program/include/rcmail.php
+++ b/program/include/rcmail.php
@@ -1,2494 +1,2493 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/include/rcmail.php |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2014, The Roundcube Dev Team |
- | Copyright (C) 2011-2014, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Application class providing core functions and holding |
| instances of all 'global' objects like db- and imap-connections |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Application class of Roundcube Webmail
* implemented as singleton
*
* @package Webmail
*/
class rcmail extends rcube
{
/**
* Main tasks.
*
* @var array
*/
static public $main_tasks = array('mail','settings','addressbook','login','logout','utils','dummy');
/**
* Current task.
*
* @var string
*/
public $task;
/**
* Current action.
*
* @var string
*/
public $action = '';
public $comm_path = './';
public $filename = '';
private $address_books = array();
private $action_map = array();
const ERROR_STORAGE = -2;
const ERROR_INVALID_REQUEST = 1;
const ERROR_INVALID_HOST = 2;
const ERROR_COOKIES_DISABLED = 3;
const ERROR_RATE_LIMIT = 4;
/**
* This implements the 'singleton' design pattern
*
* @param integer $mode Ignored rcube::get_instance() argument
* @param string $env Environment name to run (e.g. live, dev, test)
*
* @return rcmail The one and only instance
*/
static function get_instance($mode = 0, $env = '')
{
if (!self::$instance || !is_a(self::$instance, 'rcmail')) {
self::$instance = new rcmail($env);
// init AFTER object was linked with self::$instance
self::$instance->startup();
}
return self::$instance;
}
/**
* Initial startup function
* to register session, create database and imap connections
*/
protected function startup()
{
$this->init(self::INIT_WITH_DB | self::INIT_WITH_PLUGINS);
// set filename if not index.php
if (($basename = basename($_SERVER['SCRIPT_FILENAME'])) && $basename != 'index.php') {
$this->filename = $basename;
}
// load all configured plugins
$plugins = (array) $this->config->get('plugins', array());
$required_plugins = array('filesystem_attachments', 'jqueryui');
$this->plugins->load_plugins($plugins, $required_plugins);
// Remember default skin, before it's replaced by user prefs
$this->default_skin = $this->config->get('skin');
// start session
$this->session_init();
// create user object
$this->set_user(new rcube_user($_SESSION['user_id']));
// set task and action properties
$this->set_task(rcube_utils::get_input_value('_task', rcube_utils::INPUT_GPC));
$this->action = asciiwords(rcube_utils::get_input_value('_action', rcube_utils::INPUT_GPC));
// reset some session parameters when changing task
if ($this->task != 'utils') {
// we reset list page when switching to another task
// but only to the main task interface - empty action (#1489076, #1490116)
// this will prevent from unintentional page reset on cross-task requests
if ($this->session && $_SESSION['task'] != $this->task && empty($this->action)) {
$this->session->remove('page');
// set current task to session
$_SESSION['task'] = $this->task;
}
}
// init output class (not in CLI mode)
if (!empty($_REQUEST['_remote'])) {
$GLOBALS['OUTPUT'] = $this->json_init();
}
else if ($_SERVER['REMOTE_ADDR']) {
$GLOBALS['OUTPUT'] = $this->load_gui(!empty($_REQUEST['_framed']));
}
// run init method on all the plugins
$this->plugins->init($this, $this->task);
}
/**
* Setter for application task
*
* @param string $task Task to set
*/
public function set_task($task)
{
if (php_sapi_name() == 'cli') {
$task = 'cli';
}
else if (!$this->user || !$this->user->ID) {
$task = 'login';
}
else {
$task = asciiwords($task, true) ?: 'mail';
}
$this->task = $task;
$this->comm_path = $this->url(array('task' => $this->task));
if (!empty($_REQUEST['_framed'])) {
$this->comm_path .= '&_framed=1';
}
if ($this->output) {
$this->output->set_env('task', $this->task);
$this->output->set_env('comm_path', $this->comm_path);
}
}
/**
* Setter for system user object
*
* @param rcube_user $user Current user instance
*/
public function set_user($user)
{
parent::set_user($user);
$lang = $this->language_prop($this->config->get('language', $_SESSION['language']));
$_SESSION['language'] = $this->user->language = $lang;
// set localization
setlocale(LC_ALL, $lang . '.utf8', $lang . '.UTF-8', 'en_US.utf8', 'en_US.UTF-8');
// Workaround for http://bugs.php.net/bug.php?id=18556
// Also strtoupper/strtolower and other methods are locale-aware
// for these locales it is problematic (#1490519)
if (in_array($lang, array('tr_TR', 'ku', 'az_AZ'))) {
setlocale(LC_CTYPE, 'en_US.utf8', 'en_US.UTF-8', 'C');
}
}
/**
* Return instance of the internal address book class
*
* @param string $id Address book identifier (-1 for default addressbook)
* @param boolean $writeable True if the address book needs to be writeable
*
* @return rcube_contacts Address book object
*/
public function get_address_book($id, $writeable = false)
{
$contacts = null;
$ldap_config = (array)$this->config->get('ldap_public');
// 'sql' is the alias for '0' used by autocomplete
if ($id == 'sql')
$id = '0';
else if ($id == -1) {
$id = $this->config->get('default_addressbook');
$default = true;
}
// use existing instance
if (isset($this->address_books[$id]) && ($this->address_books[$id] instanceof rcube_addressbook)) {
$contacts = $this->address_books[$id];
}
else if ($id && $ldap_config[$id]) {
$domain = $this->config->mail_domain($_SESSION['storage_host']);
$contacts = new rcube_ldap($ldap_config[$id], $this->config->get('ldap_debug'), $domain);
}
else if ($id === '0') {
$contacts = new rcube_contacts($this->db, $this->get_user_id());
}
else {
$plugin = $this->plugins->exec_hook('addressbook_get', array('id' => $id, 'writeable' => $writeable));
// plugin returned instance of a rcube_addressbook
if ($plugin['instance'] instanceof rcube_addressbook) {
$contacts = $plugin['instance'];
}
}
// when user requested default writeable addressbook
// we need to check if default is writeable, if not we
// will return first writeable book (if any exist)
if ($contacts && $default && $contacts->readonly && $writeable) {
$contacts = null;
}
// Get first addressbook from the list if configured default doesn't exist
// This can happen when user deleted the addressbook (e.g. Kolab folder)
if (!$contacts && (!$id || $default)) {
$source = reset($this->get_address_sources($writeable, !$default));
if (!empty($source)) {
$contacts = $this->get_address_book($source['id']);
if ($contacts) {
$id = $source['id'];
}
}
}
if (!$contacts) {
// there's no default, just return
if ($default) {
return null;
}
self::raise_error(array(
'code' => 700,
'file' => __FILE__,
'line' => __LINE__,
'message' => "Addressbook source ($id) not found!"
),
true, true);
}
// add to the 'books' array for shutdown function
$this->address_books[$id] = $contacts;
if ($writeable && $contacts->readonly) {
return null;
}
// set configured sort order
if ($sort_col = $this->config->get('addressbook_sort_col')) {
$contacts->set_sort_order($sort_col);
}
return $contacts;
}
/**
* Return identifier of the address book object
*
* @param rcube_addressbook $object Addressbook source object
*
* @return string Source identifier
*/
public function get_address_book_id($object)
{
foreach ($this->address_books as $index => $book) {
if ($book === $object) {
return $index;
}
}
}
/**
* Return address books list
*
* @param boolean $writeable True if the address book needs to be writeable
* @param boolean $skip_hidden True if the address book needs to be not hidden
*
* @return array Address books array
*/
public function get_address_sources($writeable = false, $skip_hidden = false)
{
$abook_type = (string) $this->config->get('address_book_type');
$ldap_config = (array) $this->config->get('ldap_public');
$autocomplete = (array) $this->config->get('autocomplete_addressbooks');
$list = array();
// We are using the DB address book or a plugin address book
if (!empty($abook_type) && strtolower($abook_type) != 'ldap') {
if (!isset($this->address_books['0'])) {
$this->address_books['0'] = new rcube_contacts($this->db, $this->get_user_id());
}
$list['0'] = array(
'id' => '0',
'name' => $this->gettext('personaladrbook'),
'groups' => $this->address_books['0']->groups,
'readonly' => $this->address_books['0']->readonly,
'undelete' => $this->address_books['0']->undelete && $this->config->get('undo_timeout'),
'autocomplete' => in_array('sql', $autocomplete),
);
}
if (!empty($ldap_config)) {
foreach ($ldap_config as $id => $prop) {
// handle misconfiguration
if (empty($prop) || !is_array($prop)) {
continue;
}
$list[$id] = array(
'id' => $id,
'name' => html::quote($prop['name']),
'groups' => !empty($prop['groups']) || !empty($prop['group_filters']),
'readonly' => !$prop['writable'],
'hidden' => $prop['hidden'],
'autocomplete' => in_array($id, $autocomplete)
);
}
}
$plugin = $this->plugins->exec_hook('addressbooks_list', array('sources' => $list));
$list = $plugin['sources'];
foreach ($list as $idx => $item) {
// register source for shutdown function
if (!is_object($this->address_books[$item['id']])) {
$this->address_books[$item['id']] = $item;
}
// remove from list if not writeable as requested
if ($writeable && $item['readonly']) {
unset($list[$idx]);
}
// remove from list if hidden as requested
else if ($skip_hidden && $item['hidden']) {
unset($list[$idx]);
}
}
return $list;
}
/**
* Getter for compose responses.
* These are stored in local config and user preferences.
*
* @param boolean $sorted True to sort the list alphabetically
* @param boolean $user_only True if only this user's responses shall be listed
*
* @return array List of the current user's stored responses
*/
public function get_compose_responses($sorted = false, $user_only = false)
{
$responses = array();
if (!$user_only) {
foreach ($this->config->get('compose_responses_static', array()) as $response) {
if (empty($response['key'])) {
$response['key'] = substr(md5($response['name']), 0, 16);
}
$response['static'] = true;
$response['class'] = 'readonly';
$k = $sorted ? '0000-' . mb_strtolower($response['name']) : $response['key'];
$responses[$k] = $response;
}
}
foreach ($this->config->get('compose_responses', array()) as $response) {
if (empty($response['key'])) {
$response['key'] = substr(md5($response['name']), 0, 16);
}
$k = $sorted ? mb_strtolower($response['name']) : $response['key'];
$responses[$k] = $response;
}
// sort list by name
if ($sorted) {
ksort($responses, SORT_LOCALE_STRING);
}
$responses = array_values($responses);
$hook = $this->plugins->exec_hook('get_compose_responses', array(
'list' => $responses,
'sorted' => $sorted,
'user_only' => $user_only,
));
return $hook['list'];
}
/**
* Init output object for GUI and add common scripts.
* This will instantiate a rcmail_output_html object and set
* environment vars according to the current session and configuration
*
* @param boolean $framed True if this request is loaded in a (i)frame
*
* @return rcube_output Reference to HTML output object
*/
public function load_gui($framed = false)
{
// init output page
if (!($this->output instanceof rcmail_output_html)) {
$this->output = new rcmail_output_html($this->task, $framed);
}
// set refresh interval
$this->output->set_env('refresh_interval', $this->config->get('refresh_interval', 0));
$this->output->set_env('session_lifetime', $this->config->get('session_lifetime', 0) * 60);
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(RCUBE_CHARSET);
if ($this->user && $this->user->ID) {
$this->output->set_env('user_id', $this->user->get_hash());
}
// set compose mode for all tasks (message compose step can be triggered from everywhere)
$this->output->set_env('compose_extwin', $this->config->get('compose_extwin',false));
// add some basic labels to client
$this->output->add_label('loading', 'servererror', 'connerror', 'requesttimedout',
'refreshing', 'windowopenerror', 'uploadingmany', 'uploading', 'close', 'save', 'cancel',
'alerttitle', 'confirmationtitle', 'delete', 'continue', 'ok');
return $this->output;
}
/**
* Create an output object for JSON responses
*
* @return rcube_output Reference to JSON output object
*/
public function json_init()
{
if (!($this->output instanceof rcmail_output_json)) {
$this->output = new rcmail_output_json($this->task);
}
return $this->output;
}
/**
* Create session object and start the session.
*/
public function session_init()
{
parent::session_init();
// set initial session vars
if (!$_SESSION['user_id']) {
$_SESSION['temp'] = true;
}
}
/**
* Perform login to the mail server and to the webmail service.
* This will also create a new user entry if auto_create_user is configured.
*
* @param string $username Mail storage (IMAP) user name
* @param string $password Mail storage (IMAP) password
* @param string $host Mail storage (IMAP) host
* @param bool $cookiecheck Enables cookie check
*
* @return boolean True on success, False on failure
*/
function login($username, $password, $host = null, $cookiecheck = false)
{
$this->login_error = null;
if (empty($username)) {
return false;
}
if ($cookiecheck && empty($_COOKIE)) {
$this->login_error = self::ERROR_COOKIES_DISABLED;
return false;
}
$username_filter = $this->config->get('login_username_filter');
$username_maxlen = $this->config->get('login_username_maxlen', 1024);
$password_maxlen = $this->config->get('login_password_maxlen', 1024);
$default_host = $this->config->get('default_host');
$default_port = $this->config->get('default_port');
$username_domain = $this->config->get('username_domain');
$login_lc = $this->config->get('login_lc', 2);
// check input for security (#1490500)
if (($username_maxlen && strlen($username) > $username_maxlen)
|| ($username_filter && !preg_match($username_filter, $username))
|| ($password_maxlen && strlen($password) > $password_maxlen)
) {
$this->login_error = self::ERROR_INVALID_REQUEST;
return false;
}
// host is validated in rcmail::autoselect_host(), so here
// we'll only handle unset host (if possible)
if (!$host && !empty($default_host)) {
if (is_array($default_host)) {
$key = key($default_host);
$host = is_numeric($key) ? $default_host[$key] : $key;
}
else {
$host = $default_host;
}
$host = rcube_utils::parse_host($host);
}
if (!$host) {
$this->login_error = self::ERROR_INVALID_HOST;
return false;
}
// parse $host URL
$a_host = parse_url($host);
if ($a_host['host']) {
$host = $a_host['host'];
$ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null;
if (!empty($a_host['port']))
$port = $a_host['port'];
else if ($ssl && $ssl != 'tls' && (!$default_port || $default_port == 143))
$port = 993;
}
if (!$port) {
$port = $default_port;
}
// Check if we need to add/force domain to username
if (!empty($username_domain)) {
$domain = is_array($username_domain) ? $username_domain[$host] : $username_domain;
if ($domain = rcube_utils::parse_host((string)$domain, $host)) {
$pos = strpos($username, '@');
// force configured domains
if ($pos !== false && $this->config->get('username_domain_forced')) {
$username = substr($username, 0, $pos) . '@' . $domain;
}
// just add domain if not specified
else if ($pos === false) {
$username .= '@' . $domain;
}
}
}
// Convert username to lowercase. If storage backend
// is case-insensitive we need to store always the same username (#1487113)
if ($login_lc) {
if ($login_lc == 2 || $login_lc === true) {
$username = mb_strtolower($username);
}
else if (strpos($username, '@')) {
// lowercase domain name
list($local, $domain) = explode('@', $username);
$username = $local . '@' . mb_strtolower($domain);
}
}
// try to resolve email address from virtuser table
if (strpos($username, '@') && ($virtuser = rcube_user::email2user($username))) {
$username = $virtuser;
}
// Here we need IDNA ASCII
// Only rcube_contacts class is using domain names in Unicode
$host = rcube_utils::idn_to_ascii($host);
if (strpos($username, '@')) {
$username = rcube_utils::idn_to_ascii($username);
}
// user already registered -> overwrite username
if ($user = rcube_user::query($username, $host)) {
$username = $user->data['username'];
// Brute-force prevention
if ($user->is_locked()) {
$this->login_error = self::ERROR_RATE_LIMIT;
return false;
}
}
$storage = $this->get_storage();
// try to log in
if (!$storage->connect($host, $username, $password, $port, $ssl)) {
if ($user) {
$user->failed_login();
}
// Wait a second to slow down brute-force attacks (#1490549)
sleep(1);
return false;
}
// user already registered -> update user's record
if (is_object($user)) {
// update last login timestamp
$user->touch();
}
// create new system user
else if ($this->config->get('auto_create_user')) {
if ($created = rcube_user::create($username, $host)) {
$user = $created;
}
else {
self::raise_error(array(
'code' => 620,
'file' => __FILE__,
'line' => __LINE__,
'message' => "Failed to create a user record. Maybe aborted by a plugin?"
),
true, false);
}
}
else {
self::raise_error(array(
'code' => 621,
'file' => __FILE__,
'line' => __LINE__,
'message' => "Access denied for new user $username. 'auto_create_user' is disabled"
),
true, false);
}
// login succeeded
if (is_object($user) && $user->ID) {
// Configure environment
$this->set_user($user);
$this->set_storage_prop();
// set session vars
$_SESSION['user_id'] = $user->ID;
$_SESSION['username'] = $user->data['username'];
$_SESSION['storage_host'] = $host;
$_SESSION['storage_port'] = $port;
$_SESSION['storage_ssl'] = $ssl;
$_SESSION['password'] = $this->encrypt($password);
$_SESSION['login_time'] = time();
$timezone = rcube_utils::get_input_value('_timezone', rcube_utils::INPUT_GPC);
if ($timezone && is_string($timezone) && $timezone != '_default_') {
$_SESSION['timezone'] = $timezone;
}
// fix some old settings according to namespace prefix
$this->fix_namespace_settings($user);
// set/create special folders
$this->set_special_folders();
// clear all mailboxes related cache(s)
$storage->clear_cache('mailboxes', true);
return true;
}
return false;
}
/**
* Returns error code of last login operation
*
* @return int Error code
*/
public function login_error()
{
if ($this->login_error) {
return $this->login_error;
}
if ($this->storage && $this->storage->get_error_code() < -1) {
return self::ERROR_STORAGE;
}
}
/**
* 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 = rcube_utils::get_input_value('_host', rcube_utils::INPUT_POST);
$post_user = rcube_utils::get_input_value('_user', rcube_utils::INPUT_POST);
list(, $domain) = explode('@', $post_user);
// 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
else if (!empty($domain)) {
foreach ($default_host as $storage_host => $mail_domains) {
if (is_array($mail_domains) && in_array_nocase($domain, $mail_domains)) {
$host = $storage_host;
break;
}
else if (stripos($storage_host, $domain) !== false || stripos(strval($mail_domains), $domain) !== false) {
$host = is_numeric($storage_host) ? $mail_domains : $storage_host;
break;
}
}
}
// take the first entry if $host is still not set
if (empty($host)) {
$key = key($default_host);
$host = is_numeric($key) ? $default_host[$key] : $key;
}
}
else if (empty($default_host)) {
$host = rcube_utils::get_input_value('_host', rcube_utils::INPUT_POST);
}
else {
$host = rcube_utils::parse_host($default_host);
}
return $host;
}
/**
* Destroy session data and remove cookie
*/
public function kill_session()
{
$this->plugins->exec_hook('session_destroy');
$this->session->kill();
$_SESSION = array('language' => $this->user->language, 'temp' => true);
$this->user->reset();
if ($this->config->get('skin') != $this->default_skin && method_exists($this->output, 'set_skin')) {
$this->output->set_skin($this->default_skin);
}
}
/**
* Do server side actions on logout
*/
public function logout_actions()
{
$storage = $this->get_storage();
$logout_expunge = $this->config->get('logout_expunge');
$logout_purge = $this->config->get('logout_purge');
$trash_mbox = $this->config->get('trash_mbox');
if ($logout_purge && !empty($trash_mbox)) {
$storage->clear_folder($trash_mbox);
}
if ($logout_expunge) {
$storage->expunge_folder('INBOX');
}
// Try to save unsaved user preferences
if (!empty($_SESSION['preferences'])) {
$this->user->save_prefs(unserialize($_SESSION['preferences']));
}
}
/**
* Build a valid URL to this instance of Roundcube
*
* @param mixed $p Either a string with the action or
* url parameters as key-value pairs
* @param boolean $absolute Build an URL absolute to document root
* @param boolean $full Create fully qualified URL including http(s):// and hostname
* @param bool $secure Return absolute URL in secure location
*
* @return string Valid application URL
*/
public function url($p, $absolute = false, $full = false, $secure = false)
{
if (!is_array($p)) {
if (strpos($p, 'http') === 0) {
return $p;
}
$p = array('_action' => @func_get_arg(0));
}
$pre = array();
$task = $p['_task'] ?: ($p['task'] ?: $this->task);
$pre['_task'] = $task;
unset($p['task'], $p['_task']);
$url = $this->filename;
$delm = '?';
foreach (array_merge($pre, $p) as $key => $val) {
if ($val !== '' && $val !== null) {
$par = $key[0] == '_' ? $key : '_'.$key;
$url .= $delm.urlencode($par).'='.urlencode($val);
$delm = '&';
}
}
$base_path = strval($_SERVER['REDIRECT_SCRIPT_URL'] ?: $_SERVER['SCRIPT_NAME']);
$base_path = preg_replace('![^/]+$!', '', $base_path);
if ($secure && ($token = $this->get_secure_url_token(true))) {
// add token to the url
$url = $token . '/' . $url;
// remove old token from the path
$base_path = rtrim($base_path, '/');
$base_path = preg_replace('/\/[a-zA-Z0-9]{' . strlen($token) . '}$/', '', $base_path);
// this need to be full url to make redirects work
$absolute = true;
}
else if ($secure && ($token = $this->get_request_token()))
$url .= $delm . '_token=' . urlencode($token);
if ($absolute || $full) {
// add base path to this Roundcube installation
if ($base_path == '') $base_path = '/';
$prefix = $base_path;
// prepend protocol://hostname:port
if ($full) {
$prefix = rcube_utils::resolve_url($prefix);
}
$prefix = rtrim($prefix, '/') . '/';
}
else {
$prefix = './';
}
return $prefix . $url;
}
/**
* Function to be executed in script shutdown
*/
public function shutdown()
{
parent::shutdown();
foreach ($this->address_books as $book) {
if (is_object($book) && is_a($book, 'rcube_addressbook')) {
$book->close();
}
}
// write performance stats to logs/console
if ($this->config->get('devel_mode') || $this->config->get('performance_stats')) {
// we have to disable per_user_logging to make sure stats end up in the main console log
$this->config->set('per_user_logging', false);
// make sure logged numbers use unified format
setlocale(LC_NUMERIC, 'en_US.utf8', 'en_US.UTF-8', 'en_US', 'C');
if (function_exists('memory_get_usage')) {
$mem = round(memory_get_usage() / 1024 /1024, 1);
}
if (function_exists('memory_get_peak_usage')) {
$mem .= '/'. round(memory_get_peak_usage() / 1024 / 1024, 1);
}
$log = $this->task . ($this->action ? '/'.$this->action : '') . ($mem ? " [$mem]" : '');
if (defined('RCMAIL_START')) {
self::print_timer(RCMAIL_START, $log);
}
else {
self::console($log);
}
}
}
/**
* CSRF attack prevention code. Raises error when check fails.
*
* @param int $mode Request mode
*/
public function request_security_check($mode = rcube_utils::INPUT_POST)
{
// check request token
if (!$this->check_request($mode)) {
$error = array('code' => 403, 'message' => "Request security check failed");
self::raise_error($error, false, true);
}
}
/**
* Registers action aliases for current task
*
* @param array $map Alias-to-filename hash array
*/
public function register_action_map($map)
{
if (is_array($map)) {
foreach ($map as $idx => $val) {
$this->action_map[$idx] = $val;
}
}
}
/**
* Returns current action filename
*
* @param array $map Alias-to-filename hash array
*/
public function get_action_file()
{
if (!empty($this->action_map[$this->action])) {
return $this->action_map[$this->action];
}
return strtr($this->action, '-', '_') . '.inc';
}
/**
* Fixes some user preferences according to namespace handling change.
* Old Roundcube versions were using folder names with removed namespace prefix.
* Now we need to add the prefix on servers where personal namespace has prefix.
*
* @param rcube_user $user User object
*/
private function fix_namespace_settings($user)
{
$prefix = $this->storage->get_namespace('prefix');
$prefix_len = strlen($prefix);
if (!$prefix_len) {
return;
}
if ($this->config->get('namespace_fixed')) {
return;
}
$prefs = array();
// Build namespace prefix regexp
$ns = $this->storage->get_namespace();
$regexp = array();
foreach ($ns as $entry) {
if (!empty($entry)) {
foreach ($entry as $item) {
if (strlen($item[0])) {
$regexp[] = preg_quote($item[0], '/');
}
}
}
}
$regexp = '/^('. implode('|', $regexp).')/';
// Fix preferences
$opts = array('drafts_mbox', 'junk_mbox', 'sent_mbox', 'trash_mbox', 'archive_mbox');
foreach ($opts as $opt) {
if ($value = $this->config->get($opt)) {
if ($value != 'INBOX' && !preg_match($regexp, $value)) {
$prefs[$opt] = $prefix.$value;
}
}
}
if (($search_mods = $this->config->get('search_mods')) && !empty($search_mods)) {
$folders = array();
foreach ($search_mods as $idx => $value) {
if ($idx != 'INBOX' && $idx != '*' && !preg_match($regexp, $idx)) {
$idx = $prefix.$idx;
}
$folders[$idx] = $value;
}
$prefs['search_mods'] = $folders;
}
if (($threading = $this->config->get('message_threading')) && !empty($threading)) {
$folders = array();
foreach ($threading as $idx => $value) {
if ($idx != 'INBOX' && !preg_match($regexp, $idx)) {
$idx = $prefix.$idx;
}
$folders[$prefix.$idx] = $value;
}
$prefs['message_threading'] = $folders;
}
if ($collapsed = $this->config->get('collapsed_folders')) {
$folders = explode('&&', $collapsed);
$count = count($folders);
$folders_str = '';
if ($count) {
$folders[0] = substr($folders[0], 1);
$folders[$count-1] = substr($folders[$count-1], 0, -1);
}
foreach ($folders as $value) {
if ($value != 'INBOX' && !preg_match($regexp, $value)) {
$value = $prefix.$value;
}
$folders_str .= '&'.$value.'&';
}
$prefs['collapsed_folders'] = $folders_str;
}
$prefs['namespace_fixed'] = true;
// save updated preferences and reset imap settings (default folders)
$user->save_prefs($prefs);
$this->set_storage_prop();
}
/**
* Overwrite action variable
*
* @param string $action New action value
*/
public function overwrite_action($action)
{
$this->action = $action;
$this->output->set_env('action', $action);
}
/**
* Set environment variables for specified config options
*
* @param array $options List of configuration option names
*/
public function set_env_config($options)
{
foreach ((array) $options as $option) {
if ($this->config->get($option)) {
$this->output->set_env($option, true);
}
}
}
/**
* Returns RFC2822 formatted current date in user's timezone
*
* @return string Date
*/
public function user_date()
{
// get user's timezone
try {
$tz = new DateTimeZone($this->config->get('timezone'));
$date = new DateTime('now', $tz);
}
catch (Exception $e) {
$date = new DateTime();
}
return $date->format('r');
}
/**
* Write login data (name, ID, IP address) to the 'userlogins' log file.
*/
public function log_login($user = null, $failed_login = false, $error_code = 0)
{
if (!$this->config->get('log_logins')) {
return;
}
// failed login
if ($failed_login) {
// don't fill the log with complete input, which could
// have been prepared by a hacker
if (strlen($user) > 256) {
$user = substr($user, 0, 256) . '...';
}
$message = sprintf('Failed login for %s from %s in session %s (error: %d)',
$user, rcube_utils::remote_ip(), session_id(), $error_code);
}
// successful login
else {
$user_name = $this->get_user_name();
$user_id = $this->get_user_id();
if (!$user_id) {
return;
}
$message = sprintf('Successful login for %s (ID: %d) from %s in session %s',
$user_name, $user_id, rcube_utils::remote_ip(), session_id());
}
// log login
self::write_log('userlogins', $message);
}
/**
* Create a HTML table based on the given data
*
* @param array $attrib Named table attributes
* @param mixed $table_data Table row data. Either a two-dimensional array
* or a valid SQL result set
* @param array $show_cols List of cols to show
* @param string $id_col Name of the identifier col
*
* @return string HTML table code
*/
public function table_output($attrib, $table_data, $show_cols, $id_col)
{
$table = new html_table($attrib);
// add table header
if (!$attrib['noheader']) {
foreach ($show_cols as $col) {
$table->add_header($col, $this->Q($this->gettext($col)));
}
}
if (!is_array($table_data)) {
$db = $this->get_dbh();
while ($table_data && ($sql_arr = $db->fetch_assoc($table_data))) {
$table->add_row(array('id' => 'rcmrow' . rcube_utils::html_identifier($sql_arr[$id_col])));
// format each col
foreach ($show_cols as $col) {
$table->add($col, $this->Q($sql_arr[$col]));
}
}
}
else {
foreach ($table_data as $row_data) {
$class = !empty($row_data['class']) ? $row_data['class'] : null;
if (!empty($attrib['rowclass']))
$class = trim($class . ' ' . $attrib['rowclass']);
$rowid = 'rcmrow' . rcube_utils::html_identifier($row_data[$id_col]);
$table->add_row(array('id' => $rowid, 'class' => $class));
// format each col
foreach ($show_cols as $col) {
$val = is_array($row_data[$col]) ? $row_data[$col][0] : $row_data[$col];
$table->add($col, empty($attrib['ishtml']) ? $this->Q($val) : $val);
}
}
}
return $table->show($attrib);
}
/**
* Convert the given date to a human readable form
* This uses the date formatting properties from config
*
* @param mixed $date Date representation (string, timestamp or DateTime object)
* @param string $format Date format to use
* @param bool $convert Enables date conversion according to user timezone
*
* @return string Formatted date string
*/
public function format_date($date, $format = null, $convert = true)
{
if (is_object($date) && is_a($date, 'DateTime')) {
$timestamp = $date->format('U');
}
else {
if (!empty($date)) {
$timestamp = rcube_utils::strtotime($date);
}
if (empty($timestamp)) {
return '';
}
try {
$date = new DateTime("@".$timestamp);
}
catch (Exception $e) {
return '';
}
}
if ($convert) {
try {
// convert to the right timezone
$stz = date_default_timezone_get();
$tz = new DateTimeZone($this->config->get('timezone'));
$date->setTimezone($tz);
date_default_timezone_set($tz->getName());
$timestamp = $date->format('U');
}
catch (Exception $e) {
}
}
// define date format depending on current time
if (!$format) {
$now = time();
$now_date = getdate($now);
$today_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday'], $now_date['year']);
$week_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday']-6, $now_date['year']);
$pretty_date = $this->config->get('prettydate');
if ($pretty_date && $timestamp > $today_limit && $timestamp <= $now) {
$format = $this->config->get('date_today', $this->config->get('time_format', 'H:i'));
$today = true;
}
else if ($pretty_date && $timestamp > $week_limit && $timestamp <= $now) {
$format = $this->config->get('date_short', 'D H:i');
}
else {
$format = $this->config->get('date_long', 'Y-m-d H:i');
}
}
// strftime() format
if (preg_match('/%[a-z]+/i', $format)) {
$format = strftime($format, $timestamp);
if ($stz) {
date_default_timezone_set($stz);
}
return $today ? ($this->gettext('today') . ' ' . $format) : $format;
}
// parse format string manually in order to provide localized weekday and month names
// an alternative would be to convert the date() format string to fit with strftime()
$out = '';
for ($i=0; $i<strlen($format); $i++) {
if ($format[$i] == "\\") { // skip escape chars
continue;
}
// write char "as-is"
if ($format[$i] == ' ' || $format[$i-1] == "\\") {
$out .= $format[$i];
}
// weekday (short)
else if ($format[$i] == 'D') {
$out .= $this->gettext(strtolower(date('D', $timestamp)));
}
// weekday long
else if ($format[$i] == 'l') {
$out .= $this->gettext(strtolower(date('l', $timestamp)));
}
// month name (short)
else if ($format[$i] == 'M') {
$out .= $this->gettext(strtolower(date('M', $timestamp)));
}
// month name (long)
else if ($format[$i] == 'F') {
$out .= $this->gettext('long'.strtolower(date('M', $timestamp)));
}
else if ($format[$i] == 'x') {
$out .= strftime('%x %X', $timestamp);
}
else {
$out .= date($format[$i], $timestamp);
}
}
if ($today) {
$label = $this->gettext('today');
// replcae $ character with "Today" label (#1486120)
if (strpos($out, '$') !== false) {
$out = preg_replace('/\$/', $label, $out, 1);
}
else {
$out = $label . ' ' . $out;
}
}
if ($stz) {
date_default_timezone_set($stz);
}
return $out;
}
/**
* Return folders list in HTML
*
* @param array $attrib Named parameters
*
* @return string HTML code for the gui object
*/
public function folder_list($attrib)
{
static $a_mailboxes;
$attrib += array('maxlength' => 100, 'realnames' => false, 'unreadwrap' => ' (%s)');
$type = $attrib['type'] ? $attrib['type'] : 'ul';
unset($attrib['type']);
if ($type == 'ul' && !$attrib['id']) {
$attrib['id'] = 'rcmboxlist';
}
if (empty($attrib['folder_name'])) {
$attrib['folder_name'] = '*';
}
// get current folder
$storage = $this->get_storage();
$mbox_name = $storage->get_folder();
// build the folders tree
if (empty($a_mailboxes)) {
// get mailbox list
$a_folders = $storage->list_folders_subscribed(
'', $attrib['folder_name'], $attrib['folder_filter']);
$delimiter = $storage->get_hierarchy_delimiter();
$a_mailboxes = array();
foreach ($a_folders as $folder) {
$this->build_folder_tree($a_mailboxes, $folder, $delimiter);
}
}
// allow plugins to alter the folder tree or to localize folder names
$hook = $this->plugins->exec_hook('render_mailboxlist', array(
'list' => $a_mailboxes,
'delimiter' => $delimiter,
'type' => $type,
'attribs' => $attrib,
));
$a_mailboxes = $hook['list'];
$attrib = $hook['attribs'];
if ($type == 'select') {
$attrib['is_escaped'] = true;
$select = new html_select($attrib);
// add no-selection option
if ($attrib['noselection']) {
$select->add(html::quote($this->gettext($attrib['noselection'])), '');
}
$this->render_folder_tree_select($a_mailboxes, $mbox_name, $attrib['maxlength'], $select, $attrib['realnames']);
$out = $select->show($attrib['default']);
}
else {
$js_mailboxlist = array();
$tree = $this->render_folder_tree_html($a_mailboxes, $mbox_name, $js_mailboxlist, $attrib);
if ($type != 'js') {
$out = html::tag('ul', $attrib, $tree, html::$common_attrib);
$this->output->include_script('treelist.js');
$this->output->add_gui_object('mailboxlist', $attrib['id']);
$this->output->set_env('unreadwrap', $attrib['unreadwrap']);
$this->output->set_env('collapsed_folders', (string) $this->config->get('collapsed_folders'));
}
$this->output->set_env('mailboxes', $js_mailboxlist);
// we can't use object keys in javascript because they are unordered
// we need sorted folders list for folder-selector widget
$this->output->set_env('mailboxes_list', array_keys($js_mailboxlist));
}
// add some labels to client
$this->output->add_label('purgefolderconfirm', 'deletemessagesconfirm');
return $out;
}
/**
* Return folders list as html_select object
*
* @param array $p Named parameters
*
* @return html_select HTML drop-down object
*/
public function folder_selector($p = array())
{
$realnames = $this->config->get('show_real_foldernames');
$p += array('maxlength' => 100, 'realnames' => $realnames, 'is_escaped' => true);
$a_mailboxes = array();
$storage = $this->get_storage();
if (empty($p['folder_name'])) {
$p['folder_name'] = '*';
}
if ($p['unsubscribed']) {
$list = $storage->list_folders('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
}
else {
$list = $storage->list_folders_subscribed('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
}
$delimiter = $storage->get_hierarchy_delimiter();
if (!empty($p['exceptions'])) {
$list = array_diff($list, (array) $p['exceptions']);
}
if (!empty($p['additional'])) {
foreach ($p['additional'] as $add_folder) {
$add_items = explode($delimiter, $add_folder);
$folder = '';
while (count($add_items)) {
$folder .= array_shift($add_items);
// @TODO: sorting
if (!in_array($folder, $list)) {
$list[] = $folder;
}
$folder .= $delimiter;
}
}
}
foreach ($list as $folder) {
$this->build_folder_tree($a_mailboxes, $folder, $delimiter);
}
$select = new html_select($p);
if ($p['noselection']) {
$select->add(html::quote($p['noselection']), '');
}
$this->render_folder_tree_select($a_mailboxes, $mbox, $p['maxlength'], $select, $p['realnames'], 0, $p);
return $select;
}
/**
* Create a hierarchical array of the mailbox list
*/
public function build_folder_tree(&$arrFolders, $folder, $delm = '/', $path = '')
{
// Handle namespace prefix
$prefix = '';
if (!$path) {
$n_folder = $folder;
$folder = $this->storage->mod_folder($folder);
if ($n_folder != $folder) {
$prefix = substr($n_folder, 0, -strlen($folder));
}
}
$pos = strpos($folder, $delm);
if ($pos !== false) {
$subFolders = substr($folder, $pos+1);
$currentFolder = substr($folder, 0, $pos);
// sometimes folder has a delimiter as the last character
if (!strlen($subFolders)) {
$virtual = false;
}
else if (!isset($arrFolders[$currentFolder])) {
$virtual = true;
}
else {
$virtual = $arrFolders[$currentFolder]['virtual'];
}
}
else {
$subFolders = false;
$currentFolder = $folder;
$virtual = false;
}
$path .= $prefix . $currentFolder;
if (!isset($arrFolders[$currentFolder])) {
$arrFolders[$currentFolder] = array(
'id' => $path,
'name' => rcube_charset::convert($currentFolder, 'UTF7-IMAP'),
'virtual' => $virtual,
'folders' => array()
);
}
else {
$arrFolders[$currentFolder]['virtual'] = $virtual;
}
if (strlen($subFolders)) {
$this->build_folder_tree($arrFolders[$currentFolder]['folders'], $subFolders, $delm, $path.$delm);
}
}
/**
* Return html for a structured list &lt;ul&gt; for the mailbox tree
*/
public function render_folder_tree_html(&$arrFolders, &$mbox_name, &$jslist, $attrib, $nestLevel = 0)
{
$maxlength = intval($attrib['maxlength']);
$realnames = (bool)$attrib['realnames'];
$msgcounts = $this->storage->get_cache('messagecount');
$collapsed = $this->config->get('collapsed_folders');
$realnames = $this->config->get('show_real_foldernames');
$out = '';
foreach ($arrFolders as $folder) {
$title = null;
$folder_class = $this->folder_classname($folder['id']);
$is_collapsed = strpos($collapsed, '&'.rawurlencode($folder['id']).'&') !== false;
$unread = $msgcounts ? intval($msgcounts[$folder['id']]['UNSEEN']) : 0;
if ($folder_class && !$realnames) {
$foldername = $this->gettext($folder_class);
}
else {
$foldername = $folder['name'];
// shorten the folder name to a given length
if ($maxlength && $maxlength > 1) {
$fname = abbreviate_string($foldername, $maxlength);
if ($fname != $foldername) {
$title = $foldername;
}
$foldername = $fname;
}
}
// make folder name safe for ids and class names
$folder_id = rcube_utils::html_identifier($folder['id'], true);
$classes = array('mailbox');
// set special class for Sent, Drafts, Trash and Junk
if ($folder_class) {
$classes[] = $folder_class;
}
if ($folder['id'] == $mbox_name) {
$classes[] = 'selected';
}
if ($folder['virtual']) {
$classes[] = 'virtual';
}
else if ($unread) {
$classes[] = 'unread';
}
$js_name = $this->JQ($folder['id']);
$html_name = $this->Q($foldername) . ($unread ? html::span('unreadcount', sprintf($attrib['unreadwrap'], $unread)) : '');
$link_attrib = $folder['virtual'] ? array() : array(
'href' => $this->url(array('_mbox' => $folder['id'])),
'onclick' => sprintf("return %s.command('list','%s',this,event)", rcmail_output::JS_OBJECT_NAME, $js_name),
'rel' => $folder['id'],
'title' => $title,
);
$out .= html::tag('li', array(
'id' => "rcmli" . $folder_id,
'class' => join(' ', $classes),
'noclose' => true
),
html::a($link_attrib, $html_name));
if (!empty($folder['folders'])) {
$out .= html::div('treetoggle ' . ($is_collapsed ? 'collapsed' : 'expanded'), '&nbsp;');
}
$jslist[$folder['id']] = array(
'id' => $folder['id'],
'name' => $foldername,
'virtual' => $folder['virtual'],
);
if (!empty($folder_class)) {
$jslist[$folder['id']]['class'] = $folder_class;
}
if (!empty($folder['folders'])) {
$out .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
$this->render_folder_tree_html($folder['folders'], $mbox_name, $jslist, $attrib, $nestLevel+1));
}
$out .= "</li>\n";
}
return $out;
}
/**
* Return html for a flat list <select> for the mailbox tree
*/
public function render_folder_tree_select(&$arrFolders, &$mbox_name, $maxlength, &$select, $realnames = false, $nestLevel = 0, $opts = array())
{
$out = '';
foreach ($arrFolders as $folder) {
// skip exceptions (and its subfolders)
if (!empty($opts['exceptions']) && in_array($folder['id'], $opts['exceptions'])) {
continue;
}
// skip folders in which it isn't possible to create subfolders
if (!empty($opts['skip_noinferiors'])) {
$attrs = $this->storage->folder_attributes($folder['id']);
if ($attrs && in_array_nocase('\\Noinferiors', $attrs)) {
continue;
}
}
if (!$realnames && ($folder_class = $this->folder_classname($folder['id']))) {
$foldername = $this->gettext($folder_class);
}
else {
$foldername = $folder['name'];
// shorten the folder name to a given length
if ($maxlength && $maxlength > 1) {
$foldername = abbreviate_string($foldername, $maxlength);
}
}
$select->add(str_repeat('&nbsp;', $nestLevel*4) . html::quote($foldername), $folder['id']);
if (!empty($folder['folders'])) {
$out .= $this->render_folder_tree_select($folder['folders'], $mbox_name, $maxlength,
$select, $realnames, $nestLevel+1, $opts);
}
}
return $out;
}
/**
* Return internal name for the given folder if it matches the configured special folders
*/
public function folder_classname($folder_id)
{
if ($folder_id == 'INBOX') {
return 'inbox';
}
// for these mailboxes we have localized labels and css classes
foreach (array('sent', 'drafts', 'trash', 'junk') as $smbx)
{
if ($folder_id === $this->config->get($smbx.'_mbox')) {
return $smbx;
}
}
}
/**
* Try to localize the given IMAP folder name.
* UTF-7 decode it in case no localized text was found
*
* @param string $name Folder name
* @param bool $with_path Enable path localization
* @param bool $path_remove Remove the path
*
* @return string Localized folder name in UTF-8 encoding
*/
public function localize_foldername($name, $with_path = false, $path_remove = false)
{
$realnames = $this->config->get('show_real_foldernames');
if (!$realnames && ($folder_class = $this->folder_classname($name))) {
return $this->gettext($folder_class);
}
$storage = $this->get_storage();
$delimiter = $storage->get_hierarchy_delimiter();
// Remove the path
if ($path_remove) {
if (strpos($name, $delimiter)) {
$path = explode($delimiter, $name);
$name = array_pop($path);
}
}
// try to localize path of the folder
else if ($with_path && !$realnames) {
$path = explode($delimiter, $name);
$count = count($path);
if ($count > 1) {
for ($i = 1; $i < $count; $i++) {
$folder = implode($delimiter, array_slice($path, 0, -$i));
if ($folder_class = $this->folder_classname($folder)) {
$name = implode($delimiter, array_slice($path, $count - $i));
$name = rcube_charset::convert($name, 'UTF7-IMAP');
return $this->gettext($folder_class) . $delimiter . $name;
}
}
}
}
return rcube_charset::convert($name, 'UTF7-IMAP');
}
/**
* Localize folder path
*/
public function localize_folderpath($path)
{
$protect_folders = $this->config->get('protect_default_folders');
$delimiter = $this->storage->get_hierarchy_delimiter();
$path = explode($delimiter, $path);
$result = array();
foreach ($path as $idx => $dir) {
$directory = implode($delimiter, array_slice($path, 0, $idx+1));
if ($protect_folders && $this->storage->is_special_folder($directory)) {
unset($result);
$result[] = $this->localize_foldername($directory);
}
else {
$result[] = rcube_charset::convert($dir, 'UTF7-IMAP');
}
}
return implode($delimiter, $result);
}
/**
* Return HTML for quota indicator object
*
* @param array $attrib Named parameters
*
* @return string HTML code for the quota indicator object
*/
public static function quota_display($attrib)
{
$rcmail = rcmail::get_instance();
if (!$attrib['id']) {
$attrib['id'] = 'rcmquotadisplay';
}
$_SESSION['quota_display'] = !empty($attrib['display']) ? $attrib['display'] : 'text';
$rcmail->output->add_gui_object('quotadisplay', $attrib['id']);
$quota = $rcmail->quota_content($attrib);
$rcmail->output->add_script('rcmail.set_quota('.rcube_output::json_serialize($quota).');', 'docready');
return html::span($attrib, '&nbsp;');
}
/**
* Return (parsed) quota information
*
* @param array $attrib Named parameters
* @param array $folder Current folder
*
* @return array Quota information
*/
public function quota_content($attrib = null, $folder = null)
{
$quota = $this->storage->get_quota($folder);
$quota = $this->plugins->exec_hook('quota', $quota);
$quota_result = (array) $quota;
$quota_result['type'] = isset($_SESSION['quota_display']) ? $_SESSION['quota_display'] : '';
$quota_result['folder'] = $folder !== null && $folder !== '' ? $folder : 'INBOX';
if ($quota['total'] > 0) {
if (!isset($quota['percent'])) {
$quota_result['percent'] = min(100, round(($quota['used']/max(1,$quota['total']))*100));
}
$title = $this->gettext('quota') . ': ' . sprintf('%s / %s (%.0f%%)',
$this->show_bytes($quota['used'] * 1024),
$this->show_bytes($quota['total'] * 1024),
$quota_result['percent']
);
$quota_result['title'] = $title;
if ($attrib['width']) {
$quota_result['width'] = $attrib['width'];
}
if ($attrib['height']) {
$quota_result['height'] = $attrib['height'];
}
// build a table of quota types/roots info
if (($root_cnt = count($quota_result['all'])) > 1 || count($quota_result['all'][key($quota_result['all'])]) > 1) {
$table = new html_table(array('cols' => 3, 'class' => 'quota-info'));
$table->add_header(null, self::Q($this->gettext('quotatype')));
$table->add_header(null, self::Q($this->gettext('quotatotal')));
$table->add_header(null, self::Q($this->gettext('quotaused')));
foreach ($quota_result['all'] as $root => $data) {
if ($root_cnt > 1 && $root) {
$table->add(array('colspan' => 3, 'class' => 'root'), self::Q($root));
}
if ($storage = $data['storage']) {
$percent = min(100, round(($storage['used']/max(1,$storage['total']))*100));
$table->add('name', self::Q($this->gettext('quotastorage')));
$table->add(null, $this->show_bytes($storage['total'] * 1024));
$table->add(null, sprintf('%s (%.0f%%)', $this->show_bytes($storage['used'] * 1024), $percent));
}
if ($message = $data['message']) {
$percent = min(100, round(($message['used']/max(1,$message['total']))*100));
$table->add('name', self::Q($this->gettext('quotamessage')));
$table->add(null, intval($message['total']));
$table->add(null, sprintf('%d (%.0f%%)', $message['used'], $percent));
}
}
$quota_result['table'] = $table->show();
}
}
else {
$unlimited = $this->config->get('quota_zero_as_unlimited');
$quota_result['title'] = $this->gettext($unlimited ? 'unlimited' : 'unknown');
$quota_result['percent'] = 0;
}
// cleanup
unset($quota_result['abort']);
if (empty($quota_result['table'])) {
unset($quota_result['all']);
}
return $quota_result;
}
/**
* Outputs error message according to server error/response codes
*
* @param string $fallback Fallback message label
* @param array $fallback_args Fallback message label arguments
* @param string $suffix Message label suffix
* @param array $params Additional parameters (type, prefix)
*/
public function display_server_error($fallback = null, $fallback_args = null, $suffix = '', $params = array())
{
$err_code = $this->storage->get_error_code();
$res_code = $this->storage->get_response_code();
$args = array();
if ($res_code == rcube_storage::NOPERM) {
$error = 'errornoperm';
}
else if ($res_code == rcube_storage::READONLY) {
$error = 'errorreadonly';
}
else if ($res_code == rcube_storage::OVERQUOTA) {
$error = 'erroroverquota';
}
else if ($err_code && ($err_str = $this->storage->get_error_str())) {
// try to detect access rights problem and display appropriate message
if (stripos($err_str, 'Permission denied') !== false) {
$error = 'errornoperm';
}
// try to detect full mailbox problem and display appropriate message
// there can be e.g. "Quota exceeded" / "quotum would exceed" / "Over quota"
else if (stripos($err_str, 'quot') !== false && preg_match('/exceed|over/i', $err_str)) {
$error = 'erroroverquota';
}
else {
$error = 'servererrormsg';
$args = array('msg' => rcube::Q($err_str));
}
}
else if ($err_code < 0) {
$error = 'storageerror';
}
else if ($fallback) {
$error = $fallback;
$args = $fallback_args;
$params['prefix'] = false;
}
if ($error) {
if ($suffix && $this->text_exists($error . $suffix)) {
$error .= $suffix;
}
$msg = $this->gettext(array('name' => $error, 'vars' => $args));
if ($params['prefix'] && $fallback) {
$msg = $this->gettext(array('name' => $fallback, 'vars' => $fallback_args)) . ' ' . $msg;
}
$this->output->show_message($msg, $params['type'] ?: 'error');
}
}
/**
* Displays an error message on storage fatal errors
*/
public function storage_fatal_error()
{
$err_code = $this->storage->get_error_code();
switch ($err_code) {
// Not all are really fatal, but these should catch
// connection/authentication errors the best we can
case rcube_imap_generic::ERROR_NO:
case rcube_imap_generic::ERROR_BAD:
case rcube_imap_generic::ERROR_BYE:
$this->display_server_error();
}
}
/**
* Output HTML editor scripts
*
* @param string $mode Editor mode
*/
public function html_editor($mode = '')
{
$spellcheck = intval($this->config->get('enable_spellcheck'));
$spelldict = intval($this->config->get('spellcheck_dictionary'));
$disabled_plugins = array();
$disabled_buttons = array();
$extra_plugins = array();
$extra_buttons = array();
if (!$spellcheck) {
$disabled_plugins[] = 'spellchecker';
}
$hook = $this->plugins->exec_hook('html_editor', array(
'mode' => $mode,
'disabled_plugins' => $disabled_plugins,
'disabled_buttons' => $disabled_buttons,
'extra_plugins' => $extra_plugins,
'extra_buttons' => $extra_buttons,
));
if ($hook['abort']) {
return;
}
$lang_codes = array($_SESSION['language']);
$assets_dir = $this->config->get('assets_dir') ?: INSTALL_PATH;
$skin_path = $this->output->get_skin_path();
if ($pos = strpos($_SESSION['language'], '_')) {
$lang_codes[] = substr($_SESSION['language'], 0, $pos);
}
foreach ($lang_codes as $code) {
if (file_exists("$assets_dir/program/js/tinymce/langs/$code.js")) {
$lang = $code;
break;
}
}
if (empty($lang)) {
$lang = 'en';
}
$config = array(
'mode' => $mode,
'lang' => $lang,
'skin_path' => $skin_path,
'spellcheck' => $spellcheck, // deprecated
'spelldict' => $spelldict,
'content_css' => 'program/resources/tinymce/content.css',
'disabled_plugins' => $hook['disabled_plugins'],
'disabled_buttons' => $hook['disabled_buttons'],
'extra_plugins' => $hook['extra_plugins'],
'extra_buttons' => $hook['extra_buttons'],
);
if ($path = $this->config->get('editor_css_location')) {
$config['content_css'] = $skin_path . $path;
}
$this->output->add_label('selectimage', 'addimage', 'selectmedia', 'addmedia');
$this->output->set_env('editor_config', $config);
if ($path = $this->config->get('media_browser_css_location', 'program/resources/tinymce/browser.css')) {
if ($path != 'none') {
$this->output->include_css($path);
}
}
$this->output->include_script('tinymce/tinymce.min.js');
$this->output->include_script('editor.js');
}
/**
* File upload progress handler.
*
* @deprecated We're using HTML5 upload progress
*/
public function upload_progress()
{
// NOOP
$this->output->send();
}
/**
* Initializes file uploading interface.
*
* @param int $max_size Optional maximum file size in bytes
*
* @return string Human-readable file size limit
*/
public function upload_init($max_size = null)
{
// find max filesize value
$max_filesize = rcube_utils::max_upload_size();
if ($max_size && $max_size < $max_filesize) {
$max_filesize = $max_size;
}
$max_filesize_txt = $this->show_bytes($max_filesize);
$this->output->set_env('max_filesize', $max_filesize);
$this->output->set_env('filesizeerror', $this->gettext(array(
'name' => 'filesizeerror', 'vars' => array('size' => $max_filesize_txt))));
if ($max_filecount = ini_get('max_file_uploads')) {
$this->output->set_env('max_filecount', $max_filecount);
$this->output->set_env('filecounterror', $this->gettext(array(
'name' => 'filecounterror', 'vars' => array('count' => $max_filecount))));
}
$this->output->add_label('uploadprogress', 'GB', 'MB', 'KB', 'B');
return $max_filesize_txt;
}
/**
* Upload form object
*
* @param array $attrib Object attributes
* @param string $name Form object name
* @param string $action Form action name
* @param array $input_attr File input attributes
*
* @return string HTML output
*/
public function upload_form($attrib, $name, $action, $input_attr = array())
{
// Get filesize, enable upload progress bar
$max_filesize = $this->upload_init();
$hint = html::div('hint', $this->gettext(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize))));
if ($attrib['mode'] == 'hint') {
return $hint;
}
// set defaults
$attrib += array('id' => 'rcmUploadbox', 'buttons' => 'yes');
$event = rcmail_output::JS_OBJECT_NAME . ".command('$action', this.form)";
$form_id = $attrib['id'] . 'Frm';
// Default attributes of file input and form
$input_attr += array(
'id' => $attrib['id'] . 'Input',
'type' => 'file',
'name' => '_attachments[]',
);
$form_attr = array(
'id' => $form_id,
'name' => $name,
'method' => 'post',
'enctype' => 'multipart/form-data'
);
if ($attrib['mode'] == 'smart') {
unset($attrib['buttons']);
$form_attr['class'] = 'smart-upload';
$input_attr = array_merge($input_attr, array(
// #5854: Chrome does not execute onchange when selecting the same file.
// To fix this we reset the input using null value.
'onchange' => "$event; this.value=null",
'class' => 'smart-upload',
'tabindex' => '-1',
));
}
$input = new html_inputfield($input_attr);
$content = $attrib['prefix'] . $input->show();
if ($attrib['mode'] != 'smart') {
$content = html::div(null, $content . $hint);
}
if (rcube_utils::get_boolean($attrib['buttons'])) {
$button = new html_inputfield(array('type' => 'button'));
$content .= html::div('buttons',
$button->show($this->gettext('close'), array('class' => 'button', 'onclick' => "$('#{$attrib['id']}').hide()")) . ' ' .
$button->show($this->gettext('upload'), array('class' => 'button mainaction', 'onclick' => $event))
);
}
$this->output->add_gui_object($name, $form_id);
return html::div($attrib, $this->output->form_tag($form_attr, $content));
}
/**
* Outputs uploaded file content (with image thumbnails support
*
* @param array $file Upload file data
*/
public function display_uploaded_file($file)
{
if (empty($file)) {
return;
}
$file = $this->plugins->exec_hook('attachment_display', $file);
if ($file['status']) {
if (empty($file['size'])) {
$file['size'] = $file['data'] ? strlen($file['data']) : @filesize($file['path']);
}
// generate image thumbnail for file browser in HTML editor
if (!empty($_GET['_thumbnail'])) {
$thumbnail_size = 80;
$mimetype = $file['mimetype'];
$file_ident = $file['id'] . ':' . $file['mimetype'] . ':' . $file['size'];
$thumb_name = 'thumb' . md5($file_ident . ':' . $this->user->ID . ':' . $thumbnail_size);
$cache_file = rcube_utils::temp_filename($thumb_name, false, false);
// render thumbnail image if not done yet
if (!is_file($cache_file)) {
if (!$file['path']) {
$orig_name = $filename = $cache_file . '.tmp';
file_put_contents($orig_name, $file['data']);
}
else {
$filename = $file['path'];
}
$image = new rcube_image($filename);
if ($imgtype = $image->resize($thumbnail_size, $cache_file, true)) {
$mimetype = 'image/' . $imgtype;
if ($orig_name) {
unlink($orig_name);
}
}
}
if (is_file($cache_file)) {
// cache for 1h
$this->output->future_expire_header(3600);
header('Content-Type: ' . $mimetype);
header('Content-Length: ' . filesize($cache_file));
readfile($cache_file);
exit;
}
}
header('Content-Type: ' . $file['mimetype']);
header('Content-Length: ' . $file['size']);
if ($file['data']) {
echo $file['data'];
}
else if ($file['path']) {
readfile($file['path']);
}
}
}
/**
* Initializes client-side autocompletion.
*/
public function autocomplete_init()
{
static $init;
if ($init) {
return;
}
$init = 1;
if (($threads = (int)$this->config->get('autocomplete_threads')) > 0) {
$book_types = (array) $this->config->get('autocomplete_addressbooks', 'sql');
if (count($book_types) > 1) {
$this->output->set_env('autocomplete_threads', $threads);
$this->output->set_env('autocomplete_sources', $book_types);
}
}
$this->output->set_env('autocomplete_max', (int)$this->config->get('autocomplete_max', 15));
$this->output->set_env('autocomplete_min_length', $this->config->get('autocomplete_min_length'));
$this->output->add_label('autocompletechars', 'autocompletemore');
}
/**
* Returns supported font-family specifications
*
* @param string $font Font name
*
* @param string|array Font-family specification array or string (if $font is used)
*/
public static function font_defs($font = null)
{
$fonts = array(
'Andale Mono' => '"Andale Mono",Times,monospace',
'Arial' => 'Arial,Helvetica,sans-serif',
'Arial Black' => '"Arial Black","Avant Garde",sans-serif',
'Book Antiqua' => '"Book Antiqua",Palatino,serif',
'Courier New' => '"Courier New",Courier,monospace',
'Georgia' => 'Georgia,Palatino,serif',
'Helvetica' => 'Helvetica,Arial,sans-serif',
'Impact' => 'Impact,Chicago,sans-serif',
'Tahoma' => 'Tahoma,Arial,Helvetica,sans-serif',
'Terminal' => 'Terminal,Monaco,monospace',
'Times New Roman' => '"Times New Roman",Times,serif',
'Trebuchet MS' => '"Trebuchet MS",Geneva,sans-serif',
'Verdana' => 'Verdana,Geneva,sans-serif',
);
if ($font) {
return $fonts[$font];
}
return $fonts;
}
/**
* Create a human readable string for a number of bytes
*
* @param int $bytes Number of bytes
* @param string &$unit Size unit
*
* @return string Byte string
*/
public function show_bytes($bytes, &$unit = null)
{
// Plugins may want to display different units
$plugin = $this->plugins->exec_hook('show_bytes', array('bytes' => $bytes));
$unit = $plugin['unit'];
if ($plugin['result'] !== null) {
return $plugin['result'];
}
if ($bytes >= 1073741824) {
$unit = 'GB';
$gb = $bytes/1073741824;
$str = sprintf($gb >= 10 ? "%d " : "%.1f ", $gb) . $this->gettext($unit);
}
else if ($bytes >= 1048576) {
$unit = 'MB';
$mb = $bytes/1048576;
$str = sprintf($mb >= 10 ? "%d " : "%.1f ", $mb) . $this->gettext($unit);
}
else if ($bytes >= 1024) {
$unit = 'KB';
$str = sprintf("%d ", round($bytes/1024)) . $this->gettext($unit);
}
else {
$unit = 'B';
$str = sprintf('%d ', $bytes) . $this->gettext($unit);
}
return $str;
}
/**
* Returns real size (calculated) of the message part
*
* @param rcube_message_part $part Message part
*
* @return string Part size (and unit)
*/
public function message_part_size($part)
{
if (isset($part->d_parameters['size'])) {
$size = $this->show_bytes((int)$part->d_parameters['size']);
}
else {
$size = $part->size;
if ($size === 0) {
$part->exact_size = true;
}
if ($part->encoding == 'base64') {
$size = $size / 1.33;
}
$size = $this->show_bytes($size);
}
if (!$part->exact_size) {
$size = '~' . $size;
}
return $size;
}
/**
* Returns message UID(s) and IMAP folder(s) from GET/POST data
*
* @param string $uids UID value to decode
* @param string $mbox Default mailbox value (if not encoded in UIDs)
* @param bool $is_multifolder Will be set to True if multi-folder request
* @param int $mode Request mode. Default: rcube_utils::INPUT_GPC.
*
* @return array List of message UIDs per folder
*/
public static function get_uids($uids = null, $mbox = null, &$is_multifolder = false, $mode = null)
{
// message UID (or comma-separated list of IDs) is provided in
// the form of <ID>-<MBOX>[,<ID>-<MBOX>]*
$_uid = $uids ?: rcube_utils::get_input_value('_uid', $mode ?: rcube_utils::INPUT_GPC);
$_mbox = $mbox ?: (string) rcube_utils::get_input_value('_mbox', $mode ?: rcube_utils::INPUT_GPC);
// already a hash array
if (is_array($_uid) && !isset($_uid[0])) {
return $_uid;
}
$result = array();
// special case: *
if ($_uid == '*' && is_object($_SESSION['search'][1]) && $_SESSION['search'][1]->multi) {
$is_multifolder = true;
// extract the full list of UIDs per folder from the search set
foreach ($_SESSION['search'][1]->sets as $subset) {
$mbox = $subset->get_parameters('MAILBOX');
$result[$mbox] = $subset->get();
}
}
else {
if (is_string($_uid)) {
$_uid = explode(',', $_uid);
}
// create a per-folder UIDs array
foreach ((array)$_uid as $uid) {
list($uid, $mbox) = explode('-', $uid, 2);
if (!strlen($mbox)) {
$mbox = $_mbox;
}
else {
$is_multifolder = true;
}
if ($uid == '*') {
$result[$mbox] = $uid;
}
else if (preg_match('/^[0-9:.]+$/', $uid)) {
$result[$mbox][] = $uid;
}
}
}
return $result;
}
/**
* Get resource file content (with assets_dir support)
*
* @param string $name File name
*
* @return string File content
*/
public function get_resource_content($name)
{
if (!strpos($name, '/')) {
$name = "program/resources/$name";
}
$assets_dir = $this->config->get('assets_dir');
if ($assets_dir) {
$path = slashify($assets_dir) . $name;
if (@file_exists($path)) {
$name = $path;
}
}
return file_get_contents($name, false);
}
/**
* Converts HTML content into plain text
*
* @param string $html HTML content
* @param array $options Conversion parameters (width, links, charset)
*
* @return string Plain text
*/
public function html2text($html, $options = array())
{
$default_options = array(
'links' => true,
'width' => 75,
'body' => $html,
'charset' => RCUBE_CHARSET,
);
$options = array_merge($default_options, (array) $options);
// Plugins may want to modify HTML in another/additional way
$options = $this->plugins->exec_hook('html2text', $options);
// Convert to text
if (!$options['abort']) {
$converter = new rcube_html2text($options['body'],
false, $options['links'], $options['width'], $options['charset']);
$options['body'] = rtrim($converter->get_text());
}
return $options['body'];
}
/**
* Connect to the mail storage server with stored session data
*
* @return bool True on success, False on error
*/
public function storage_connect()
{
$storage = $this->get_storage();
if ($_SESSION['storage_host'] && !$storage->is_connected()) {
$host = $_SESSION['storage_host'];
$user = $_SESSION['username'];
$port = $_SESSION['storage_port'];
$ssl = $_SESSION['storage_ssl'];
$pass = $this->decrypt($_SESSION['password']);
if (!$storage->connect($host, $user, $pass, $port, $ssl)) {
if (is_object($this->output)) {
$this->output->show_message('storageerror', 'error');
}
}
else {
$this->set_storage_prop();
}
}
return $storage->is_connected();
}
}
diff --git a/program/include/rcmail_html_page.php b/program/include/rcmail_html_page.php
index df50552e7..73f791914 100644
--- a/program/include/rcmail_html_page.php
+++ b/program/include/rcmail_html_page.php
@@ -1,83 +1,81 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/include/rcmail_html_page.php |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2006-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Render a simple HTML page with the given contents |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Class to create an empty HTML page with some default styles
*
* @package Webmail
* @subpackage View
*/
class rcmail_html_page extends rcmail_output_html
{
protected $inline_warning;
public function write($contents = '')
{
self::reset(true);
// load embed.css from skin folder (if exists)
if ($embed_css = $this->get_skin_file($this->config->get('embed_css_location', '/embed.css'))) {
$this->include_css($embed_css);
}
else { // set default styles for warning blocks inside the attachment part frame
$this->add_header(html::tag('style', array('type' => 'text/css'),
".rcmail-inline-message { font-family: sans-serif; border:2px solid #ffdf0e;"
. "background:#fef893; padding:0.6em 1em; margin-bottom:0.6em }\n" .
".rcmail-inline-buttons { margin-bottom:0 }"
));
}
if (empty($contents)) {
$contents = '<html><body></body></html>';
}
if ($this->inline_warning) {
$body_start = 0;
if ($body_pos = strpos($contents, '<body')) {
$body_start = strpos($contents, '>', $body_pos) + 1;
}
$contents = substr_replace($contents, $this->inline_warning, $body_start, 0);
}
parent::write($contents);
}
/**
* Add inline warning with optional button
*
* @param string $text Warning content
* @param string $button_label Button label
* @param string $button_url Button URL
*/
public function register_inline_warning($text, $button_label = null, $button_url = null)
{
$text = html::span(null, $text);
if ($button_label) {
$onclick = "location.href = '$button_url'";
$button = html::tag('button', array('onclick' => $onclick), rcube::Q($button_label));
$text .= html::p(array('class' => 'rcmail-inline-buttons'), $button);
}
$this->inline_warning = html::div(array('class' => 'rcmail-inline-message rcmail-inline-warning'), $text);
}
}
diff --git a/program/include/rcmail_install.php b/program/include/rcmail_install.php
index 8a9ae0fc2..2846330c0 100644
--- a/program/include/rcmail_install.php
+++ b/program/include/rcmail_install.php
@@ -1,839 +1,844 @@
<?php
/**
+-----------------------------------------------------------------------+
- | rcmail_install.php |
+ | This file is part of the Roundcube Webmail client |
| |
- | This file is part of the Roundcube Webmail package |
- | Copyright (C) 2008-2018, The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Roundcube Installer |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+ | Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class to control the installation process of the Roundcube Webmail package
*
* @category Install
* @package Webmail
* @author Thomas Bruederli
*/
class rcmail_install
{
public $step;
public $last_error;
public $is_post = false;
public $failures = 0;
public $config = array();
public $configured = false;
public $legacy_config = false;
public $email_pattern = '([a-z0-9][a-z0-9\-\.\+\_]*@[a-z0-9]([a-z0-9\-][.]?)*[a-z0-9])';
public $bool_config_props = array();
public $local_config = array('db_dsnw', 'default_host', 'support_url', 'des_key', 'plugins');
public $obsolete_config = array('db_backend', 'db_max_length', 'double_auth', 'preview_pane', 'debug_level', 'referer_check');
public $replaced_config = array(
'skin_path' => 'skin',
'locale_string' => 'language',
'multiple_identities' => 'identities_level',
'addrbook_show_images' => 'show_images',
'imap_root' => 'imap_ns_personal',
'pagesize' => 'mail_pagesize',
'top_posting' => 'reply_mode',
'keep_alive' => 'refresh_interval',
'min_keep_alive' => 'min_refresh_interval',
);
// list of supported database drivers
public $supported_dbs = array(
'MySQL' => 'pdo_mysql',
'PostgreSQL' => 'pdo_pgsql',
'SQLite' => 'pdo_sqlite',
'SQLite (v2)' => 'pdo_sqlite2',
'SQL Server (SQLSRV)' => 'pdo_sqlsrv',
'SQL Server (DBLIB)' => 'pdo_dblib',
'Oracle' => 'oci8',
);
/**
* Constructor
*/
public function __construct()
{
$this->step = intval($_REQUEST['_step']);
$this->is_post = $_SERVER['REQUEST_METHOD'] == 'POST';
}
/**
* Singleton getter
*/
public static function get_instance()
{
static $inst;
if (!$inst) {
$inst = new rcmail_install();
}
return $inst;
}
/**
* Read the local config files and store properties
*/
public function load_config()
{
// defaults
if ($config = $this->load_config_file(RCUBE_CONFIG_DIR . 'defaults.inc.php')) {
$this->config = (array) $config;
$this->defaults = $this->config;
}
$config = null;
// config
if ($config = $this->load_config_file(RCUBE_CONFIG_DIR . 'config.inc.php')) {
$this->config = array_merge($this->config, $config);
}
else {
if ($config = $this->load_config_file(RCUBE_CONFIG_DIR . 'main.inc.php')) {
$this->config = array_merge($this->config, $config);
$this->legacy_config = true;
}
if ($config = $this->load_config_file(RCUBE_CONFIG_DIR . 'db.inc.php')) {
$this->config = array_merge($this->config, $config);
$this->legacy_config = true;
}
}
$this->configured = !empty($config);
}
/**
* Read the default config file and store properties
*
* @param string $file File name with path
*/
public function load_config_file($file)
{
if (!is_readable($file)) {
return;
}
include $file;
// read comments from config file
if (function_exists('token_get_all')) {
$tokens = token_get_all(file_get_contents($file));
$in_config = false;
$buffer = '';
for ($i = 0; $i < count($tokens); $i++) {
$token = $tokens[$i];
if ($token[0] == T_VARIABLE && ($token[1] == '$config' || $token[1] == '$rcmail_config')) {
$in_config = true;
if ($buffer && $tokens[$i+1] == '[' && $tokens[$i+2][0] == T_CONSTANT_ENCAPSED_STRING) {
$propname = trim($tokens[$i+2][1], "'\"");
$this->comments[$propname] = $buffer;
$buffer = '';
$i += 3;
}
}
else if ($in_config && $token[0] == T_COMMENT) {
$buffer .= strtr($token[1], array('\n' => "\n"));
}
}
}
// deprecated name of config variable
if (is_array($rcmail_config)) {
return $rcmail_config;
}
return $config;
}
/**
* Getter for a certain config property
*
* @param string $name Property name
* @param string $default Default value
*
* @return string The property value
*/
public function getprop($name, $default = '')
{
$value = $this->config[$name];
if ($name == 'des_key' && !$this->configured && !isset($_REQUEST["_$name"])) {
$value = rcube_utils::random_bytes(24);
}
return $value !== null && $value !== '' ? $value : $default;
}
/**
* Create configuration file that contains parameters
* that differ from default values.
*
* @return string The complete config file content
*/
public function create_config()
{
$config = array();
foreach ($this->config as $prop => $default) {
$is_default = !isset($_POST["_$prop"]);
$value = !$is_default || $this->bool_config_props[$prop] ? $_POST["_$prop"] : $default;
// always disable installer
if ($prop == 'enable_installer') {
$value = false;
}
// reset useragent to default (keeps version up-to-date)
if ($prop == 'useragent' && stripos($value, 'Roundcube Webmail/') !== false) {
$value = $this->defaults[$prop];
}
// generate new encryption key, never use the default value
if ($prop == 'des_key' && $value == $this->defaults[$prop]) {
$value = rcube_utils::random_bytes(24);
}
// convert some form data
if ($prop == 'db_dsnw' && !empty($_POST['_dbtype'])) {
if ($_POST['_dbtype'] == 'sqlite') {
$value = sprintf('%s://%s?mode=0646', $_POST['_dbtype'],
$_POST['_dbname']{0} == '/' ? '/' . $_POST['_dbname'] : $_POST['_dbname']);
}
else if ($_POST['_dbtype']) {
$value = sprintf('%s://%s:%s@%s/%s', $_POST['_dbtype'],
rawurlencode($_POST['_dbuser']), rawurlencode($_POST['_dbpass']), $_POST['_dbhost'], $_POST['_dbname']);
}
}
else if ($prop == 'smtp_auth_type' && $value == '0') {
$value = '';
}
else if ($prop == 'default_host' && is_array($value)) {
$value = self::_clean_array($value);
if (count($value) <= 1) {
$value = $value[0];
}
}
else if ($prop == 'mail_pagesize' || $prop == 'addressbook_pagesize') {
$value = max(2, intval($value));
}
else if ($prop == 'smtp_user' && !empty($_POST['_smtp_user_u'])) {
$value = '%u';
}
else if ($prop == 'smtp_pass' && !empty($_POST['_smtp_user_u'])) {
$value = '%p';
}
else if (is_bool($default)) {
$value = (bool) $value;
}
else if (is_numeric($value)) {
$value = intval($value);
}
else if ($prop == 'plugins' && !empty($_POST['submit'])) {
$value = array();
foreach (array_keys($_POST) as $key) {
if (preg_match('/^_plugins_*/', $key)) {
array_push($value, $_POST[$key]);
}
}
}
// skip this property
if ($value == $this->defaults[$prop]
&& (!in_array($prop, $this->local_config)
|| in_array($prop, array_merge($this->obsolete_config, array_keys($this->replaced_config)))
|| preg_match('/^db_(table|sequence)_/', $prop)
)
) {
continue;
}
// save change
$this->config[$prop] = $value;
$config[$prop] = $value;
}
$out = "<?php\n\n";
$out .= "/* Local configuration for Roundcube Webmail */\n\n";
foreach ($config as $prop => $value) {
// copy option descriptions from existing config or defaults.inc.php
$out .= $this->comments[$prop];
$out .= "\$config['$prop'] = " . self::_dump_var($value, $prop) . ";\n\n";
}
return $out;
}
/**
* save generated config file in RCUBE_CONFIG_DIR
*
* @return boolean True if the file was saved successfully, false if not
*/
public function save_configfile($config)
{
if (is_writable(RCUBE_CONFIG_DIR)) {
return file_put_contents(RCUBE_CONFIG_DIR . 'config.inc.php', $config);
}
return false;
}
/**
* Check the current configuration for missing properties
* and deprecated or obsolete settings
*
* @return array List with problems detected
*/
public function check_config()
{
$this->load_config();
if (!$this->configured) {
return;
}
$out = $seen = array();
// iterate over the current configuration
foreach (array_keys($this->config) as $prop) {
if ($replacement = $this->replaced_config[$prop]) {
$out['replaced'][] = array('prop' => $prop, 'replacement' => $replacement);
$seen[$replacement] = true;
}
else if (!$seen[$prop] && in_array($prop, $this->obsolete_config)) {
$out['obsolete'][] = array('prop' => $prop);
$seen[$prop] = true;
}
}
// the old default mime_magic reference is obsolete
if ($this->config['mime_magic'] == '/usr/share/misc/magic') {
$out['obsolete'][] = array(
'prop' => 'mime_magic',
'explain' => "Set value to null in order to use system default"
);
}
// check config dependencies and contradictions
if ($this->config['enable_spellcheck'] && $this->config['spellcheck_engine'] == 'pspell') {
if (!extension_loaded('pspell')) {
$out['dependencies'][] = array(
'prop' => 'spellcheck_engine',
'explain' => "This requires the <tt>pspell</tt> extension which could not be loaded."
);
}
else if (!empty($this->config['spellcheck_languages'])) {
foreach ($this->config['spellcheck_languages'] as $lang => $descr) {
if (!@pspell_new($lang)) {
$out['dependencies'][] = array(
'prop' => 'spellcheck_languages',
'explain' => "You are missing pspell support for language $lang ($descr)"
);
}
}
}
}
if ($this->config['log_driver'] == 'syslog') {
if (!function_exists('openlog')) {
$out['dependencies'][] = array(
'prop' => 'log_driver',
'explain' => "This requires the <tt>syslog</tt> extension which could not be loaded."
);
}
if (empty($this->config['syslog_id'])) {
$out['dependencies'][] = array(
'prop' => 'syslog_id',
'explain' => "Using <tt>syslog</tt> for logging requires a syslog ID to be configured"
);
}
}
// check ldap_public sources having global_search enabled
if (is_array($this->config['ldap_public']) && !is_array($this->config['autocomplete_addressbooks'])) {
foreach ($this->config['ldap_public'] as $ldap_public) {
if ($ldap_public['global_search']) {
$out['replaced'][] = array(
'prop' => 'ldap_public::global_search',
'replacement' => 'autocomplete_addressbooks'
);
break;
}
}
}
return $out;
}
/**
* Merge the current configuration with the defaults
* and copy replaced values to the new options.
*/
public function merge_config()
{
$current = $this->config;
$this->config = array();
foreach ($this->replaced_config as $prop => $replacement) {
if (isset($current[$prop])) {
if ($prop == 'skin_path') {
$this->config[$replacement] = preg_replace('#skins/(\w+)/?$#', '\\1', $current[$prop]);
}
else if ($prop == 'multiple_identities') {
$this->config[$replacement] = $current[$prop] ? 2 : 0;
}
else {
$this->config[$replacement] = $current[$prop];
}
}
unset($current[$prop]);
}
foreach ($this->obsolete_config as $prop) {
unset($current[$prop]);
}
// add all ldap_public sources having global_search enabled to autocomplete_addressbooks
if (is_array($current['ldap_public'])) {
foreach ($current['ldap_public'] as $key => $ldap_public) {
if ($ldap_public['global_search']) {
$this->config['autocomplete_addressbooks'][] = $key;
unset($current['ldap_public'][$key]['global_search']);
}
}
}
$this->config = array_merge($this->config, $current);
foreach (array_keys((array) $current['ldap_public']) as $key) {
$this->config['ldap_public'][$key] = $current['ldap_public'][$key];
}
}
/**
* Compare the local database schema with the reference schema
* required for this version of Roundcube
*
* @param rcube_db $db Database object
*
* @return boolean True if the schema is up-to-date, false if not or an error occurred
*/
public function db_schema_check($db)
{
if (!$this->configured) {
return false;
}
// read reference schema from mysql.initial.sql
$engine = $db->db_provider;
$db_schema = $this->db_read_schema(INSTALL_PATH . "SQL/$engine.initial.sql");
$errors = array();
// check list of tables
$existing_tables = $db->list_tables();
foreach ($db_schema as $table => $cols) {
$table = $this->config['db_prefix'] . $table;
if (!in_array($table, $existing_tables)) {
$errors[] = "Missing table '".$table."'";
}
else { // compare cols
$db_cols = $db->list_cols($table);
$diff = array_diff(array_keys($cols), $db_cols);
if (!empty($diff)) {
$errors[] = "Missing columns in table '$table': " . join(',', $diff);
}
}
}
return !empty($errors) ? $errors : false;
}
/**
* Utility function to read database schema from an .sql file
*/
private function db_read_schema($schemafile)
{
$lines = file($schemafile);
$table_block = false;
$schema = array();
$keywords = array('PRIMARY','KEY','INDEX','UNIQUE','CONSTRAINT','REFERENCES','FOREIGN');
foreach ($lines as $line) {
if (preg_match('/^\s*create table ([\S]+)/i', $line, $m)) {
$table_name = explode('.', $m[1]);
$table_name = end($table_name);
$table_name = preg_replace('/[`"\[\]]/', '', $table_name);
}
else if ($table_name && ($line = trim($line))) {
if ($line == 'GO' || $line[0] == ')' || $line[strlen($line)-1] == ';') {
$table_name = null;
}
else {
$items = explode(' ', $line);
$col = $items[0];
$col = preg_replace('/[`"\[\]]/', '', $col);
if (!in_array(strtoupper($col), $keywords)) {
$type = strtolower($items[1]);
$type = preg_replace('/[^a-zA-Z0-9()]/', '', $type);
$schema[$table_name][$col] = $type;
}
}
}
}
return $schema;
}
/**
* Try to detect some file's mimetypes to test the correct behavior of fileinfo
*/
public function check_mime_detection()
{
$errors = array();
$files = array(
'program/resources/tinymce/video.png' => 'image/png',
'program/resources/blank.tiff' => 'image/tiff',
'program/resources/blocked.gif' => 'image/gif',
);
foreach ($files as $path => $expected) {
$mimetype = rcube_mime::file_content_type(INSTALL_PATH . $path, basename($path));
if ($mimetype != $expected) {
$errors[] = array($path, $mimetype, $expected);
}
}
return $errors;
}
/**
* Check the correct configuration of the 'mime_types' mapping option
*/
public function check_mime_extensions()
{
$errors = array();
$types = array(
'application/zip' => 'zip',
'text/css' => 'css',
'application/pdf' => 'pdf',
'image/gif' => 'gif',
'image/svg+xml' => 'svg',
);
foreach ($types as $mimetype => $expected) {
$ext = rcube_mime::get_mime_extensions($mimetype);
if (!in_array($expected, (array) $ext)) {
$errors[] = array($mimetype, $ext, $expected);
}
}
return $errors;
}
/**
* Getter for the last error message
*
* @return string Error message or null if none exists
*/
public function get_error()
{
return $this->last_error['message'];
}
/**
* Return a list with all imap hosts configured
*
* @return array Clean list with imap hosts
*/
public function get_hostlist()
{
$default_hosts = (array) $this->getprop('default_host');
$out = array();
foreach ($default_hosts as $key => $name) {
if (!empty($name)) {
$out[] = rcube_utils::parse_host(is_numeric($key) ? $name : $key);
}
}
return $out;
}
/**
* Create a HTML dropdown to select a previous version of Roundcube
*/
public function versions_select($attrib = array())
{
$select = new html_select($attrib);
$select->add(array(
'0.1-stable', '0.1.1',
'0.2-alpha', '0.2-beta', '0.2-stable',
'0.3-stable', '0.3.1',
'0.4-beta', '0.4.2',
'0.5-beta', '0.5', '0.5.1', '0.5.2', '0.5.3', '0.5.4',
'0.6-beta', '0.6',
'0.7-beta', '0.7', '0.7.1', '0.7.2', '0.7.3', '0.7.4',
'0.8-beta', '0.8-rc', '0.8.0', '0.8.1', '0.8.2', '0.8.3', '0.8.4', '0.8.5', '0.8.6',
'0.9-beta', '0.9-rc', '0.9-rc2',
// Note: Do not add newer versions here
));
return $select;
}
/**
* Return a list with available subfolders of the skin directory
*/
public function list_skins()
{
$skins = array();
$skindir = INSTALL_PATH . 'skins/';
foreach (glob($skindir . '*') as $path) {
if (is_dir($path) && is_readable($path)) {
$skins[] = substr($path, strlen($skindir));
}
}
return $skins;
}
/**
* Return a list with available subfolders of the plugins directory
* (with their associated description in composer.json)
*/
public function list_plugins()
{
$plugins = array();
$plugin_dir = INSTALL_PATH . 'plugins/';
foreach (glob($plugin_dir . '*') as $path) {
if (!is_dir($path)) {
continue;
}
if (is_readable($path . '/composer.json')) {
$file_json = json_decode(file_get_contents($path . '/composer.json'));
$plugin_desc = $file_json->description ?: 'N/A';
}
else {
$plugin_desc = 'N/A';
}
$name = substr($path, strlen($plugin_dir));
$plugins[] = array(
'name' => $name,
'desc' => $plugin_desc,
'enabled' => in_array($name, (array) $this->config['plugins'])
);
}
return $plugins;
}
/**
* Display OK status
*
* @param string $name Test name
* @param string $message Confirm message
*/
public function pass($name, $message = '')
{
echo rcube::Q($name) . ':&nbsp; <span class="success">OK</span>';
$this->_showhint($message);
}
/**
* Display an error status and increase failure count
*
* @param string $name Test name
* @param string $message Error message
* @param string $url URL for details
* @param bool $optional Do not count this failure
*/
public function fail($name, $message = '', $url = '', $optional=false)
{
if (!$optional) {
$this->failures++;
}
echo rcube::Q($name) . ':&nbsp; <span class="fail">NOT OK</span>';
$this->_showhint($message, $url);
}
/**
* Display an error status for optional settings/features
*
* @param string $name Test name
* @param string $message Error message
* @param string $url URL for details
*/
public function optfail($name, $message = '', $url = '')
{
echo rcube::Q($name) . ':&nbsp; <span class="na">NOT OK</span>';
$this->_showhint($message, $url);
}
/**
* Display warning status
*
* @param string $name Test name
* @param string $message Warning message
* @param string $url URL for details
*/
public function na($name, $message = '', $url = '')
{
echo rcube::Q($name) . ':&nbsp; <span class="na">NOT AVAILABLE</span>';
$this->_showhint($message, $url);
}
private function _showhint($message, $url = '')
{
$hint = rcube::Q($message);
if ($url) {
$hint .= ($hint ? '; ' : '') . 'See <a href="' . rcube::Q($url) . '" target="_blank">' . rcube::Q($url) . '</a>';
}
if ($hint) {
echo '<span class="indent">(' . $hint . ')</span>';
}
}
private static function _clean_array($arr)
{
$out = array();
foreach (array_unique($arr) as $k => $val) {
if (!empty($val)) {
if (is_numeric($k)) {
$out[] = $val;
}
else {
$out[$k] = $val;
}
}
}
return $out;
}
private static function _dump_var($var, $name=null)
{
// special values
switch ($name) {
case 'syslog_facility':
$list = array(32 => 'LOG_AUTH', 80 => 'LOG_AUTHPRIV', 72 => ' LOG_CRON',
24 => 'LOG_DAEMON', 0 => 'LOG_KERN', 128 => 'LOG_LOCAL0',
136 => 'LOG_LOCAL1', 144 => 'LOG_LOCAL2', 152 => 'LOG_LOCAL3',
160 => 'LOG_LOCAL4', 168 => 'LOG_LOCAL5', 176 => 'LOG_LOCAL6',
184 => 'LOG_LOCAL7', 48 => 'LOG_LPR', 16 => 'LOG_MAIL',
56 => 'LOG_NEWS', 40 => 'LOG_SYSLOG', 8 => 'LOG_USER', 64 => 'LOG_UUCP'
);
if ($val = $list[$var]) {
return $val;
}
break;
/*
// RCMAIL_VERSION is undefined here
case 'useragent':
if (preg_match('|^(.*)/('.preg_quote(RCMAIL_VERSION, '|').')$|i', $var, $m)) {
return '"' . addcslashes($var, '"') . '/" . RCMAIL_VERSION';
}
break;
*/
}
if (is_array($var)) {
if (empty($var)) {
return 'array()';
}
else { // check if all keys are numeric
$isnum = true;
foreach (array_keys($var) as $key) {
if (!is_numeric($key)) {
$isnum = false;
break;
}
}
if ($isnum) {
return 'array(' . join(', ', array_map(array('rcmail_install', '_dump_var'), $var)) . ')';
}
}
}
return var_export($var, true);
}
/**
* Initialize the database with the according schema
*
* @param rcube_db $db Database connection
*
* @return boolen True on success, False on error
*/
public function init_db($db)
{
$engine = $db->db_provider;
// read schema file from /SQL/*
$fname = INSTALL_PATH . "SQL/$engine.initial.sql";
if ($sql = @file_get_contents($fname)) {
$db->set_option('table_prefix', $this->config['db_prefix']);
$db->exec_script($sql);
}
else {
$this->fail('DB Schema', "Cannot read the schema file: $fname");
return false;
}
if ($err = $this->get_error()) {
$this->fail('DB Schema', "Error creating database schema: $err");
return false;
}
return true;
}
/**
* Update database schema
*
* @param string $version Version to update from
*
* @return boolen True on success, False on error
*/
public function update_db($version)
{
return rcmail_utils::db_update(INSTALL_PATH . 'SQL',
'roundcube', $version, array('quiet' => true));
}
/**
* Handler for Roundcube errors
*/
public function raise_error($p)
{
$this->last_error = $p;
}
}
diff --git a/program/include/rcmail_output.php b/program/include/rcmail_output.php
index f34c147e7..0038f2cac 100644
--- a/program/include/rcmail_output.php
+++ b/program/include/rcmail_output.php
@@ -1,118 +1,117 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/include/rcmail_output.php |
+ | This file is part of the Roundcube Webmail client |
| |
- | This file is part of the Roundcube PHP suite |
- | Copyright (C) 2005-2012 The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
+ | |
| CONTENTS: |
| Abstract class for output generation |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class for output generation
*
* @package Webmail
* @subpackage View
*/
abstract class rcmail_output extends rcube_output
{
const JS_OBJECT_NAME = 'rcmail';
const BLANK_GIF = 'R0lGODlhDwAPAIAAAMDAwAAAACH5BAEAAAAALAAAAAAPAA8AQAINhI+py+0Po5y02otnAQA7';
public $type = 'html';
public $ajax_call = false;
public $framed = false;
protected $pagetitle = '';
protected $object_handlers = array();
protected $devel_mode = false;
/**
* Object constructor
*/
public function __construct($task = null, $framed = false)
{
parent::__construct();
$this->devel_mode = (bool) $this->config->get('devel_mode');
}
/**
* Setter for page title
*
* @param string $title Page title
*/
public function set_pagetitle($title)
{
$this->pagetitle = $title;
}
/**
* Getter for the current skin path property
*/
public function get_skin_path()
{
return $this->config->get('skin_path');
}
/**
* Delete all stored env variables and commands
*/
public function reset()
{
parent::reset();
$this->object_handlers = array();
$this->pagetitle = '';
}
/**
* Call a client method
*
* @param string Method to call
* @param ... Additional arguments
*/
abstract function command();
/**
* Add a localized label to the client environment
*/
abstract function add_label();
/**
* Register a template object handler
*
* @param string $name Object name
* @param string $func Function name to call
*
* @return void
*/
public function add_handler($name, $func)
{
$this->object_handlers[$name] = $func;
}
/**
* Register a list of template object handlers
*
* @param array $handlers Hash array with object=>handler pairs
*
* @return void
*/
public function add_handlers($handlers)
{
$this->object_handlers = array_merge($this->object_handlers, $handlers);
}
}
diff --git a/program/include/rcmail_output_cli.php b/program/include/rcmail_output_cli.php
index 7d877cc88..68bc7f8d3 100644
--- a/program/include/rcmail_output_cli.php
+++ b/program/include/rcmail_output_cli.php
@@ -1,88 +1,87 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/include/rcmail_output_cli.php |
+ | This file is part of the Roundcube Webmail client |
| |
- | This file is part of the Roundcube PHP suite |
- | Copyright (C) 2005-2014 The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
+ | |
| CONTENTS: |
| Abstract class for output generation |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Class for output generation
*
* @package Webmail
* @subpackage View
*/
class rcmail_output_cli extends rcmail_output
{
public $type = 'cli';
/**
* Object constructor
*/
public function __construct($task = null, $framed = false)
{
parent::__construct();
}
/**
* Call a client method
*
* @see rcube_output::command()
*/
function command()
{
// NOP
}
/**
* Add a localized label to the client environment
*/
function add_label()
{
// NOP
}
/**
* Invoke display_message command
*
* @see rcube_output::show_message()
*/
function show_message($message, $type = 'notice', $vars = null, $override = true, $timeout = 0)
{
if ($this->app->text_exists($message)) {
$message = $this->app->gettext(array('name' => $message, 'vars' => $vars));
}
printf("[%s] %s\n", strtoupper($type), $message);
}
/**
* Redirect to a certain url.
*
* @see rcube_output::redirect()
*/
function redirect($p = array(), $delay = 1)
{
// NOP
}
/**
* Send output to the client.
*/
function send()
{
// NOP
}
}
diff --git a/program/include/rcmail_output_html.php b/program/include/rcmail_output_html.php
index e1d67d6c2..89fb3988a 100644
--- a/program/include/rcmail_output_html.php
+++ b/program/include/rcmail_output_html.php
@@ -1,2465 +1,2463 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/include/rcmail_output_html.php |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2006-2014, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Class to handle HTML page output using a skin template. |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Class to create HTML page output using a skin template
*
* @package Webmail
* @subpackage View
*/
class rcmail_output_html extends rcmail_output
{
public $type = 'html';
protected $message;
protected $template_name;
protected $objects = array();
protected $js_env = array();
protected $js_labels = array();
protected $js_commands = array();
protected $skin_paths = array();
protected $skin_name = '';
protected $scripts_path = '';
protected $script_files = array();
protected $css_files = array();
protected $scripts = array();
protected $meta_tags = array();
protected $link_tags = array('shortcut icon' => '');
protected $header = '';
protected $footer = '';
protected $body = '';
protected $base_path = '';
protected $assets_path;
protected $assets_dir = RCUBE_INSTALL_PATH;
protected $devel_mode = false;
protected $default_template = "<html>\n<head><title></title></head>\n<body></body>\n</html>";
// deprecated names of templates used before 0.5
protected $deprecated_templates = array(
'contact' => 'showcontact',
'contactadd' => 'addcontact',
'contactedit' => 'editcontact',
'identityedit' => 'editidentity',
'messageprint' => 'printmessage',
);
// deprecated names of template objects used before 1.4
protected $deprecated_template_objects = array(
'addressframe' => 'contentframe',
'messagecontentframe' => 'contentframe',
'prefsframe' => 'contentframe',
'folderframe' => 'contentframe',
'identityframe' => 'contentframe',
'responseframe' => 'contentframe',
'keyframe' => 'contentframe',
'filterframe' => 'contentframe',
);
/**
* Constructor
*/
public function __construct($task = null, $framed = false)
{
parent::__construct();
$this->devel_mode = $this->config->get('devel_mode');
$this->set_env('task', $task);
$this->set_env('standard_windows', (bool) $this->config->get('standard_windows'));
$this->set_env('locale', $_SESSION['language']);
$this->set_env('devel_mode', $this->devel_mode);
// Version number e.g. 1.4.2 will be 10402
$version = explode('.', preg_replace('/[^0-9.].*/', '', RCMAIL_VERSION));
$this->set_env('rcversion', $version[0] * 10000 + $version[1] * 100 + $version[2]);
// add cookie info
$this->set_env('cookie_domain', ini_get('session.cookie_domain'));
$this->set_env('cookie_path', ini_get('session.cookie_path'));
$this->set_env('cookie_secure', filter_var(ini_get('session.cookie_secure'), FILTER_VALIDATE_BOOLEAN));
// Easy way to change skin via GET argument, for developers
if ($this->devel_mode && !empty($_GET['skin']) && preg_match('/^[a-z0-9-_]+$/i', $_GET['skin'])) {
if ($this->check_skin($_GET['skin'])) {
$this->set_skin($_GET['skin']);
$this->app->user->save_prefs(array('skin' => $_GET['skin']));
}
}
// load and setup the skin
$this->set_skin($this->config->get('skin'));
$this->set_assets_path($this->config->get('assets_path'), $this->config->get('assets_dir'));
if (!empty($_REQUEST['_extwin']))
$this->set_env('extwin', 1);
if ($this->framed || $framed)
$this->set_env('framed', 1);
$lic = <<<EOF
/*
@licstart The following is the entire license notice for the
JavaScript code in this page.
- Copyright (C) 2005-2014 The Roundcube Dev Team
+ Copyright (C) The Roundcube Dev Team
The JavaScript code in this page is free software: you can redistribute
it and/or modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
The code is distributed WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU GPL for more details.
@licend The above is the entire license notice
for the JavaScript code in this page.
*/
EOF;
// add common javascripts
$this->add_script($lic, 'head_top');
$this->add_script('var '.self::JS_OBJECT_NAME.' = new rcube_webmail();', 'head_top');
// don't wait for page onload. Call init at the bottom of the page (delayed)
$this->add_script(self::JS_OBJECT_NAME.'.init();', 'docready');
$this->scripts_path = 'program/js/';
$this->include_script('jquery.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'),
'preloader' => array($this, 'preloader'),
'username' => array($this, 'current_username'),
'message' => array($this, 'message_container'),
'charsetselector' => array($this, 'charset_selector'),
'aboutcontent' => array($this, 'about_content'),
));
// set blankpage (watermark) url
$blankpage = $this->config->get('blankpage_url', '/watermark.html');
$this->set_env('blankpage', $blankpage);
}
/**
* Set environment variable
*
* @param string $name Property name
* @param mixed $value Property value
* @param boolean $addtojs 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;
}
}
/**
* Parse and set assets path
*
* @param string $path Assets path URL (relative or absolute)
* @param string $fs_dif Assets path in filesystem
*/
public function set_assets_path($path, $fs_dir = null)
{
if (empty($path)) {
return;
}
$path = rtrim($path, '/') . '/';
// handle relative assets path
if (!preg_match('|^https?://|', $path) && $path[0] != '/') {
// save the path to search for asset files later
$this->assets_dir = $path;
$base = preg_replace('/[?#&].*$/', '', $_SERVER['REQUEST_URI']);
$base = rtrim($base, '/');
// remove url token if exists
if ($len = intval($this->config->get('use_secure_urls'))) {
$_base = explode('/', $base);
$last = count($_base) - 1;
$length = $len > 1 ? $len : 16; // as in rcube::get_secure_url_token()
// we can't use real token here because it
// does not exists in unauthenticated state,
// hope this will not produce false-positive matches
if ($last > -1 && preg_match('/^[a-f0-9]{' . $length . '}$/', $_base[$last])) {
$path = '../' . $path;
}
}
}
// set filesystem path for assets
if ($fs_dir) {
if ($fs_dir[0] != '/') {
$fs_dir = realpath(RCUBE_INSTALL_PATH . $fs_dir);
}
// ensure the path ends with a slash
$this->assets_dir = rtrim($fs_dir, '/') . '/';
}
$this->assets_path = $path;
$this->set_env('assets_path', $path);
}
/**
* Getter for the current page title
*
* @return string The page title
*/
protected function get_pagetitle()
{
if (!empty($this->pagetitle)) {
$title = $this->pagetitle;
}
else if ($this->env['task'] == 'login') {
$title = $this->app->gettext(array(
'name' => 'welcome',
'vars' => array('product' => $this->config->get('product_name')
)));
}
else {
$title = ucfirst($this->env['task']);
}
return $title;
}
/**
* Getter for the current skin path property
*/
public function get_skin_path()
{
return $this->skin_paths[0];
}
/**
* Set skin
*
* @param string $skin Skin name
*/
public function set_skin($skin)
{
if (!$this->check_skin($skin)) {
$skin = rcube_config::DEFAULT_SKIN;
}
$skin_path = 'skins/' . $skin;
$this->config->set('skin_path', $skin_path);
$this->base_path = $skin_path;
// register skin path(s)
$this->skin_paths = array();
$this->load_skin($skin_path);
$this->skin_name = $skin;
$this->set_env('skin', $skin);
}
/**
* Check skin validity/existence
*
* @param string $skin Skin name
*
* @return bool True if the skin exist and is readable, False otherwise
*/
public function check_skin($skin)
{
// Sanity check to prevent from path traversal vulnerability (#1490620)
if (strpos($skin, '/') !== false || strpos($skin, "\\") !== false) {
rcube::raise_error(array(
'file' => __FILE__,
'line' => __LINE__,
'message' => 'Invalid skin name'
), true, false);
return false;
}
$path = RCUBE_INSTALL_PATH . 'skins/';
return !empty($skin) && is_dir($path . $skin) && is_readable($path . $skin);
}
/**
* Helper method to recursively read skin meta files and register search paths
*/
private function load_skin($skin_path)
{
$this->skin_paths[] = $skin_path;
// read meta file and check for dependencies
$meta = @file_get_contents(RCUBE_INSTALL_PATH . $skin_path . '/meta.json');
$meta = @json_decode($meta, true);
$meta['path'] = $skin_path;
$path_elements = explode('/', $skin_path);
$skin_id = end($path_elements);
if (!$meta['name']) {
$meta['name'] = $skin_id;
}
$this->skins[$skin_id] = $meta;
// Keep skin config for ajax requests (#6613)
$_SESSION['skin_config'] = array();
if ($meta['extends']) {
$path = RCUBE_INSTALL_PATH . 'skins/';
if (is_dir($path . $meta['extends']) && is_readable($path . $meta['extends'])) {
$_SESSION['skin_config'] = $this->load_skin('skins/' . $meta['extends']);
}
}
if (!empty($meta['config'])) {
foreach ($meta['config'] as $key => $value) {
$this->config->set($key, $value, true);
$_SESSION['skin_config'][$key] = $value;
}
$value = array_merge((array) $this->config->get('dont_override'), array_keys($meta['config']));
$this->config->set('dont_override', $value, true);
}
if (!empty($meta['localization'])) {
$locdir = $meta['localization'] === true ? 'localization' : $meta['localization'];
if ($texts = $this->app->read_localization(RCUBE_INSTALL_PATH . $skin_path . '/' . $locdir)) {
$this->app->load_language($_SESSION['language'], $texts);
}
}
// Use array_merge() here to allow for global default and extended skins
$this->meta_tags = array_merge($this->meta_tags, (array) $meta['meta']);
$this->link_tags = array_merge($this->link_tags, (array) $meta['links']);
return $_SESSION['skin_config'];
}
/**
* Check if a specific template exists
*
* @param string $name Template name
*
* @return boolean True if template exists
*/
public function template_exists($name)
{
foreach ($this->skin_paths as $skin_path) {
$filename = RCUBE_INSTALL_PATH . $skin_path . '/templates/' . $name . '.html';
if ((is_file($filename) && is_readable($filename))
|| ($this->deprecated_templates[$name] && $this->template_exists($this->deprecated_templates[$name]))
) {
return true;
}
}
return false;
}
/**
* Find the given file in the current skin path stack
*
* @param string $file File name/path to resolve (starting with /)
* @param string &$skin_path Reference to the base path of the matching skin
* @param string $add_path Additional path to search in
*
* @return mixed Relative path to the requested file or False if not found
*/
public function get_skin_file($file, &$skin_path = null, $add_path = null)
{
$skin_paths = $this->skin_paths;
if ($add_path) {
array_unshift($skin_paths, $add_path);
$skin_paths = array_unique($skin_paths);
}
if ($skin_path = $this->find_file_path($file, $skin_paths)) {
return $skin_path . $file;
}
return false;
}
/**
* Find path of the asset file
*/
protected function find_file_path($file, $skin_paths)
{
foreach ($skin_paths as $skin_path) {
if ($this->assets_dir != RCUBE_INSTALL_PATH) {
if (realpath($this->assets_dir . $skin_path . $file)) {
return $skin_path;
}
}
if (realpath(RCUBE_INSTALL_PATH . $skin_path . $file)) {
return $skin_path;
}
}
}
/**
* Register a GUI object to the client script
*
* @param string $obj Object name
* @param string $id Object ID
*/
public function add_gui_object($obj, $id)
{
$this->add_script(self::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[] = array('triggerEvent', $cmd[0], $cmd[1]);
else
$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->js_labels[$name] = $this->app->gettext($name);
}
}
/**
* Invoke display_message command
*
* @param string $message Message to display
* @param string $type Message type [notice|confirm|error]
* @param array $vars Key-value pairs to be replaced in localized text
* @param boolean $override Override last set message
* @param int $timeout Message display time in seconds
*
* @uses self::command()
*/
public function show_message($message, $type='notice', $vars=null, $override=true, $timeout=0)
{
if ($override || !$this->message) {
if ($this->app->text_exists($message)) {
if (!empty($vars))
$vars = array_map(array('rcube','Q'), $vars);
$msgtext = $this->app->gettext(array('name' => $message, 'vars' => $vars));
}
else
$msgtext = $message;
$this->message = $message;
$this->command('display_message', $msgtext, $type, $timeout * 1000);
}
}
/**
* Delete all stored env variables and commands
*
* @param bool $all Reset all env variables (including internal)
*/
public function reset($all = false)
{
$framed = $this->framed;
$task = $this->env['task'];
$env = $all ? null : array_intersect_key($this->env, array('extwin'=>1, 'framed'=>1));
// keep jQuery-UI files
$css_files = $script_files = array();
foreach ($this->css_files as $file) {
if (strpos($file, 'plugins/jqueryui') === 0) {
$css_files[] = $file;
}
}
foreach ($this->script_files as $position => $files) {
foreach ($files as $file) {
if (strpos($file, 'plugins/jqueryui') === 0) {
$script_files[$position][] = $file;
}
}
}
parent::reset();
// let some env variables survive
$this->env = $this->js_env = $env;
$this->framed = $framed || $this->env['framed'];
$this->js_labels = array();
$this->js_commands = array();
$this->scripts = array();
$this->header = '';
$this->footer = '';
$this->body = '';
$this->css_files = array();
$this->script_files = array();
// load defaults
if (!$all) {
$this->__construct();
}
// Note: we merge jQuery-UI scripts after jQuery...
$this->css_files = array_merge($this->css_files, $css_files);
$this->script_files = array_merge_recursive($this->script_files, $script_files);
$this->set_env('orig_task', $task);
}
/**
* Redirect to a certain url
*
* @param mixed $p Either a string with the action or url parameters as key-value pairs
* @param int $delay Delay in seconds
* @param bool $secure Redirect to secure location (see rcmail::url())
*/
public function redirect($p = array(), $delay = 1, $secure = false)
{
if ($this->env['extwin'])
$p['extwin'] = 1;
$location = $this->app->url($p, false, false, $secure);
header('Location: ' . $location);
exit;
}
/**
* Send the request output to the client.
* This will either parse a skin template.
*
* @param string $templ Template name
* @param boolean $exit 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')) {
rcube::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 = true;
$this->write();
}
// set output asap
ob_flush();
flush();
if ($exit) {
exit;
}
}
/**
* Process template and write to stdOut
*
* @param string $template HTML template content
*/
public function write($template = '')
{
if (!empty($this->script_files)) {
$this->set_env('request_token', $this->app->get_request_token());
}
// Fix assets path on blankpage
if ($this->js_env['blankpage']) {
$this->js_env['blankpage'] = $this->asset_url($this->abs_url($this->js_env['blankpage'], true));
}
$commands = $this->get_js_commands($framed);
// if all js commands go to parent window we can ignore all
// script files and skip rcube_webmail initialization (#1489792)
// but not on error pages where skins may need jQuery, etc.
if ($framed && empty($this->js_env['server_error'])) {
$this->scripts = array();
$this->script_files = array();
$this->header = '';
$this->footer = '';
}
// write all javascript commands
if (!empty($commands)) {
$this->add_script($commands, 'head_top');
}
$this->page_headers();
// call super method
$this->_write($template, $this->config->get('skin_path'));
}
/**
* Send common page headers
* For now it only (re)sets X-Frame-Options when needed
*/
public function page_headers()
{
if (headers_sent()) {
return;
}
// allow (legal) iframe content to be loaded
$framed = $this->framed || $this->env['framed'];
if ($framed && ($xopt = $this->app->config->get('x_frame_options', 'sameorigin'))) {
if (strtolower($xopt) === 'deny') {
header('X-Frame-Options: sameorigin', true);
}
}
}
/**
* Parse a specific skin template and deliver to stdout (or return)
*
* @param string $name Template name
* @param boolean $exit Exit script
* @param boolean $write Don't write to stdout, return parsed content instead
*
* @link http://php.net/manual/en/function.exit.php
*/
function parse($name = 'main', $exit = true, $write = true)
{
$plugin = false;
$realname = $name;
$plugin_skin_paths = array();
$this->template_name = $realname;
$temp = explode('.', $name, 2);
if (count($temp) > 1) {
$plugin = $temp[0];
$name = $temp[1];
$skin_dir = $plugin . '/skins/' . $this->config->get('skin');
// apply skin search escalation list to plugin directory
foreach ($this->skin_paths as $skin_path) {
$plugin_skin_paths[] = $this->app->plugins->url . $plugin . '/' . $skin_path;
}
// prepend plugin skin paths to search list
$this->skin_paths = array_merge($plugin_skin_paths, $this->skin_paths);
}
// find skin template
$path = false;
foreach ($this->skin_paths as $skin_path) {
// when requesting a plugin template ignore global skin path(s)
if ($plugin && strpos($skin_path, $this->app->plugins->url) !== 0) {
continue;
}
$path = RCUBE_INSTALL_PATH . "$skin_path/templates/$name.html";
// fallback to deprecated template names
if (!is_readable($path) && ($dname = $this->deprecated_templates[$realname])) {
$path = RCUBE_INSTALL_PATH . "$skin_path/templates/$dname.html";
if (is_readable($path)) {
rcube::raise_error(array(
'code' => 502, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Using deprecated template '$dname' in $skin_path/templates. Please rename to '$realname'"
), true, false);
}
}
if (is_readable($path)) {
$this->config->set('skin_path', $skin_path);
// set base_path to core skin directory (not plugin's skin)
$this->base_path = preg_replace('!plugins/\w+/!', '', $skin_path);
$skin_dir = preg_replace('!^plugins/!', '', $skin_path);
break;
}
else {
$path = false;
}
}
// read template file
if (!$path || ($templ = @file_get_contents($path)) === false) {
rcube::raise_error(array(
'code' => 404,
'type' => 'php',
'line' => __LINE__,
'file' => __FILE__,
'message' => 'Error loading template for '.$realname
), true, $write);
$this->skin_paths = array_slice($this->skin_paths, count($plugin_skin_paths));
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' => $realname, 'content' => $output, 'write' => $write));
// save some memory
$output = $hook['content'];
unset($hook['content']);
// remove plugin skin paths from current context
$this->skin_paths = array_slice($this->skin_paths, count($plugin_skin_paths));
if (!$write) {
return $this->postrender($output);
}
$this->write(trim($output));
if ($exit) {
exit;
}
}
/**
* Return executable javascript code for all registered commands
*/
protected function get_js_commands(&$framed = null)
{
$out = '';
$parent_commands = 0;
$top_commands = array();
// these should be always on top,
// e.g. hide_message() below depends on env.framed
if (!$this->framed && !empty($this->js_env)) {
$top_commands[] = array('set_env', $this->js_env);
}
if (!empty($this->js_labels)) {
$top_commands[] = array('add_label', $this->js_labels);
}
// unlock interface after iframe load
$unlock = preg_replace('/[^a-z0-9]/i', '', $_REQUEST['_unlock']);
if ($this->framed) {
$top_commands[] = array('iframe_loaded', $unlock);
}
else if ($unlock) {
$top_commands[] = array('hide_message', $unlock);
}
$commands = array_merge($top_commands, $this->js_commands);
foreach ($commands as $i => $args) {
$method = array_shift($args);
$parent = $this->framed || preg_match('/^parent\./', $method);
foreach ($args as $i => $arg) {
$args[$i] = self::json_serialize($arg, $this->devel_mode);
}
if ($parent) {
$parent_commands++;
$method = preg_replace('/^parent\./', '', $method);
$parent_prefix = 'if (window.parent && parent.' . self::JS_OBJECT_NAME . ') parent.';
$method = $parent_prefix . self::JS_OBJECT_NAME . '.' . $method;
}
else {
$method = self::JS_OBJECT_NAME . '.' . $method;
}
$out .= sprintf("%s(%s);\n", $method, implode(',', $args));
}
$framed = $parent_prefix && $parent_commands == count($commands);
// make the output more compact if all commands go to parent window
if ($framed) {
$out = "if (window.parent && parent." . self::JS_OBJECT_NAME . ") {\n"
. str_replace($parent_prefix, "\tparent.", $out)
. "}\n";
}
return $out;
}
/**
* Make URLs starting with a slash point to skin directory
*
* @param string $str Input string
* @param bool $search_path True if URL should be resolved using the current skin path stack
*
* @return string URL
*/
public function abs_url($str, $search_path = false)
{
if ($str[0] == '/') {
if ($search_path && ($file_url = $this->get_skin_file($str, $skin_path))) {
return $file_url;
}
return $this->base_path . $str;
}
return $str;
}
/**
* Show error page and terminate script execution
*
* @param int $code Error code
* @param string $message Error message
*/
public function raise_error($code, $message)
{
global $__page_content, $ERROR_CODE, $ERROR_MESSAGE;
$ERROR_CODE = $code;
$ERROR_MESSAGE = $message;
include RCUBE_INSTALL_PATH . 'program/steps/utils/error.inc';
exit;
}
/**
* Modify path by adding URL prefix if configured
*/
public function asset_url($path)
{
// iframe content can't be in a different domain
// @TODO: check if assests are on a different domain
if (!$this->assets_path || in_array($path[0], array('?', '/', '.')) || strpos($path, '://')) {
return $path;
}
return $this->assets_path . $path;
}
/***** Template parsing methods *****/
/**
* Replace all strings ($varname)
* with the content of the according global variable.
*/
protected function parse_with_globals($input)
{
$GLOBALS['__version'] = html::quote(RCMAIL_VERSION);
$GLOBALS['__comm_path'] = html::quote($this->app->comm_path);
$GLOBALS['__skin_path'] = html::quote($this->base_path);
return preg_replace_callback('/\$(__[a-z0-9_\-]+)/',
array($this, 'globals_callback'), $input);
}
/**
* Callback function for preg_replace_callback() in parse_with_globals()
*/
protected function globals_callback($matches)
{
return $GLOBALS[$matches[1]];
}
/**
* Correct absolute paths in images and other tags (add cache busters)
*/
protected function fix_paths($output)
{
return preg_replace_callback(
'!(src|href|background|data-src-[a-z]+)=(["\']?)([a-z0-9/_.-]+)(["\'\s>])!i',
array($this, 'file_callback'), $output);
}
/**
* Callback function for preg_replace_callback in fix_paths()
*
* @return string Parsed string
*/
protected function file_callback($matches)
{
$file = $matches[3];
$file = preg_replace('!^/this/!', '/', $file);
// correct absolute paths
if ($file[0] == '/') {
$this->get_skin_file($file, $skin_path, $this->base_path);
$file = ($skin_path ?: $this->base_path) . $file;
}
// add file modification timestamp
if (preg_match('/\.(js|css|less|ico|png|svg|jpeg)$/', $file, $m)) {
$file = $this->file_mod($file);
}
return $matches[1] . '=' . $matches[2] . $file . $matches[4];
}
/**
* Correct paths of asset files according to assets_path
*/
protected function fix_assets_paths($output)
{
return preg_replace_callback(
'!(src|href|background)=(["\']?)([a-z0-9/_.?=-]+)(["\'\s>])!i',
array($this, 'assets_callback'), $output);
}
/**
* Callback function for preg_replace_callback in fix_assets_paths()
*
* @return string Parsed string
*/
protected function assets_callback($matches)
{
$file = $this->asset_url($matches[3]);
return $matches[1] . '=' . $matches[2] . $file . $matches[4];
}
/**
* Modify file by adding mtime indicator
*/
protected function file_mod($file)
{
$fs = false;
$ext = substr($file, strrpos($file, '.') + 1);
// use minified file if exists (not in development mode)
if (!$this->devel_mode && !preg_match('/\.min\.' . $ext . '$/', $file)) {
$minified_file = substr($file, 0, strlen($ext) * -1) . 'min.' . $ext;
if ($fs = @filemtime($this->assets_dir . $minified_file)) {
return $minified_file . '?s=' . $fs;
}
}
if ($fs = @filemtime($this->assets_dir . $file)) {
$file .= '?s=' . $fs;
}
return $file;
}
/**
* Public wrapper to dipp into template parsing.
*
* @param string $input Template content
*
* @return string
* @uses rcmail_output_html::parse_xml()
* @since 0.1-rc1
*/
public function just_parse($input)
{
$input = $this->parse_conditions($input);
$input = $this->parse_xml($input);
$input = $this->postrender($input);
return $input;
}
/**
* Parse for conditional tags
*/
protected function parse_conditions($input)
{
$matches = preg_split('/<roundcube:(if|elseif|else|endif)\s+([^>]+)>\n?/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 = html::parse_attrib_string($matches[2]);
if (isset($attrib['condition'])) {
$condmet = $this->check_condition($attrib['condition']);
$submatches = preg_split('/<roundcube:(elseif|else|endif)\s+([^>]+)>\n?/is', $matches[3], 2, PREG_SPLIT_DELIM_CAPTURE);
if ($condmet) {
$result = $submatches[0];
if ($submatches[1] != 'endif') {
$result .= preg_replace('/.*<roundcube:endif\s+[^>]+>\n?/Uis', '', $submatches[3], 1);
}
else {
$result .= $submatches[3];
}
}
else {
$result = "<roundcube:$submatches[1] $submatches[2]>" . $submatches[3];
}
return $matches[0] . $this->parse_conditions($result);
}
rcube::raise_error(array(
'code' => 500, 'line' => __LINE__, 'file' => __FILE__,
'message' => "Unable to parse conditional tag " . $matches[2]
), true, false);
}
return $input;
}
/**
* Determines if a given condition is met
*
* @param string $condition Condition statement
*
* @return boolean True if condition is met, False if not
* @todo Extend this to allow real conditions, not just "set"
*/
protected function check_condition($condition)
{
return $this->eval_expression($condition);
}
/**
* Inserts hidden field with CSRF-prevention-token into POST forms
*/
protected function alter_form_tag($matches)
{
$out = $matches[0];
$attrib = html::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;
}
/**
* Parse & evaluate a given expression and return its result.
*
* @param string $expression Expression statement
*
* @return mixed Expression result
*/
protected function eval_expression($expression)
{
$expression = 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',
'/template:name/i',
),
array(
"\$_SESSION['\\1']",
"\$this->app->config->get('\\1',rcube_utils::get_boolean('\\3'))",
"\$this->env['\\1']",
"rcube_utils::get_input_value('\\1', rcube_utils::INPUT_GPC)",
"\$_COOKIE['\\1']",
"\$this->browser->{'\\1'}",
"'{$this->template_name}'",
),
$expression
);
// Note: We used create_function() before but it's deprecated in PHP 7.2
// and really it was just a wrapper on eval().
return eval("return ($expression);");
}
/**
* Parse variable strings
*
* @param string $type Variable type (env, config etc)
* @param string $name Variable name
*
* @return mixed Variable value
*/
protected function parse_variable($type, $name)
{
$value = '';
switch ($type) {
case 'env':
$value = $this->env[$name];
break;
case 'config':
$value = $this->config->get($name);
if (is_array($value) && $value[$_SESSION['storage_host']]) {
$value = $value[$_SESSION['storage_host']];
}
break;
case 'request':
$value = rcube_utils::get_input_value($name, rcube_utils::INPUT_GPC);
break;
case 'session':
$value = $_SESSION[$name];
break;
case 'cookie':
$value = htmlspecialchars($_COOKIE[$name], ENT_COMPAT | ENT_HTML401, RCUBE_CHARSET);
break;
case 'browser':
$value = $this->browser->{$name};
break;
}
return $value;
}
/**
* Search for special tags in input and replace them
* with the appropriate content
*
* @param string $input Input string to parse
*
* @return string Altered input string
* @todo Use DOM-parser to traverse template HTML
* @todo Maybe a cache.
*/
protected 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 Matches array of preg_replace_callback
*
* @return string Tag/Object content
*/
protected function xml_command($matches)
{
$command = strtolower($matches[1]);
$attrib = html::parse_attrib_string($matches[2]);
// empty output if required condition is not met
if (!empty($attrib['condition']) && !$this->check_condition($attrib['condition'])) {
return '';
}
// localize title and summary attributes
if ($command != 'button' && !empty($attrib['title']) && $this->app->text_exists($attrib['title'])) {
$attrib['title'] = $this->app->gettext($attrib['title']);
}
if ($command != 'button' && !empty($attrib['summary']) && $this->app->text_exists($attrib['summary'])) {
$attrib['summary'] = $this->app->gettext($attrib['summary']);
}
// execute command
switch ($command) {
// return a button
case 'button':
if ($attrib['name'] || $attrib['command']) {
return $this->button($attrib);
}
break;
// frame
case 'frame':
return $this->frame($attrib);
break;
// show a label
case 'label':
if ($attrib['expression'])
$attrib['name'] = $this->eval_expression($attrib['expression']);
if ($attrib['name'] || $attrib['command']) {
$vars = $attrib + array('product' => $this->config->get('product_name'));
unset($vars['name'], $vars['command']);
$label = $this->app->gettext($attrib + array('vars' => $vars));
$quoting = !empty($attrib['quoting']) ? strtolower($attrib['quoting']) : (rcube_utils::get_boolean((string)$attrib['html']) ? 'no' : '');
// 'noshow' can be used in skins to define new labels
if ($attrib['noshow']) {
return '';
}
switch ($quoting) {
case 'no':
case 'raw':
break;
case 'javascript':
case 'js':
$label = rcube::JQ($label);
break;
default:
$label = html::quote($label);
break;
}
return $label;
}
break;
case 'add_label':
$this->add_label($attrib['name']);
break;
// include a file
case 'include':
if ($attrib['condition'] && !$this->check_condition($attrib['condition'])) {
break;
}
if ($attrib['file'][0] != '/') {
$attrib['file'] = '/templates/' . $attrib['file'];
}
$old_base_path = $this->base_path;
$include = '';
if (!empty($attrib['skin_path'])) {
$attrib['skinpath'] = $attrib['skin_path'];
}
if ($path = $this->get_skin_file($attrib['file'], $skin_path, $attrib['skinpath'])) {
// set base_path to core skin directory (not plugin's skin)
$this->base_path = preg_replace('!plugins/\w+/!', '', $skin_path);
$path = realpath(RCUBE_INSTALL_PATH . $path);
}
if (is_readable($path)) {
$allow_php = $this->config->get('skin_include_php');
$include = $allow_php ? $this->include_php($path) : file_get_contents($path);
$include = $this->parse_conditions($include);
$include = $this->parse_xml($include);
$include = $this->fix_paths($include);
}
$this->base_path = $old_base_path;
return $include;
case 'plugin.include':
$hook = $this->app->plugins->exec_hook("template_plugin_include", $attrib);
return $hook['content'];
// 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 = '';
// correct deprecated object names
if ($this->deprecated_template_objects[$object]) {
$object = $this->deprecated_template_objects[$object];
}
$handler = $this->object_handlers[$object];
// execute object handler function
if (is_callable($handler)) {
$this->prepare_object_attribs($attrib);
// We assume that objects with src attribute are internal (in most
// cases this is a watermark frame). We need this to make sure assets_path
// is added to the internal assets paths
$external = empty($attrib['src']);
$content = call_user_func($handler, $attrib);
}
else if ($object == 'doctype') {
$content = html::doctype($attrib['value']);
}
else if ($object == 'logo') {
$attrib += array('alt' => $this->xml_command(array('', 'object', 'name="productname"')));
if (!empty($attrib['type']) && ($template_logo = $this->get_template_logo(':' . $attrib['type'], true)) !== null) {
$attrib['src'] = $template_logo;
}
else if (($template_logo = $this->get_template_logo()) !== null) {
$attrib['src'] = $template_logo;
}
// process alternative logos (eg for Elastic small screen)
foreach ($attrib as $key => $value) {
if (preg_match('/data-src-(.*)/', $key, $matches)) {
if (($template_logo = $this->get_template_logo(':' . $matches[1], true)) !== null) {
$attrib[$key] = $template_logo;
}
$attrib[$key] = !empty($attrib[$key]) ? $this->abs_url($attrib[$key]) : null;
}
}
if ($attrib['src']) {
$content = html::img($attrib);
}
}
else if ($object == 'productname') {
$name = $this->config->get('product_name', 'Roundcube Webmail');
$content = html::quote($name);
}
else if ($object == 'version') {
$ver = (string)RCMAIL_VERSION;
if (is_file(RCUBE_INSTALL_PATH . '.svn/entries')) {
if (preg_match('/Revision:\s(\d+)/', @shell_exec('svn info'), $regs))
$ver .= ' [SVN r'.$regs[1].']';
}
else if (is_file(RCUBE_INSTALL_PATH . '.git/index')) {
if (preg_match('/Date:\s+([^\n]+)/', @shell_exec('git log -1'), $regs)) {
if ($date = date('Ymd.Hi', strtotime($regs[1]))) {
$ver .= ' [GIT '.$date.']';
}
}
}
$content = html::quote($ver);
}
else if ($object == 'steptitle') {
$content = html::quote($this->get_pagetitle());
}
else if ($object == 'pagetitle') {
if ($this->devel_mode && !empty($_SESSION['username']))
$title = $_SESSION['username'].' :: ';
else if ($prod_name = $this->config->get('product_name'))
$title = $prod_name . ' :: ';
else
$title = '';
$title .= $this->get_pagetitle();
$content = html::quote($title);
}
else if ($object == 'contentframe') {
if (empty($attrib['id'])) {
$attrib['id'] = 'rcm' . $this->env['task'] . 'frame';
}
// parse variables
if (preg_match('/^(config|env):([a-z0-9_]+)$/i', $attrib['src'], $matches)) {
$attrib['src'] = $this->parse_variable($matches[1], $matches[2]);
}
$content = $this->frame($attrib, true);
}
else if ($object == 'meta' || $object == 'links') {
if ($object == 'meta') {
$source = 'meta_tags';
$tag = 'meta';
$key = 'name';
$param = 'content';
}
else if ($object == 'links') {
$source = 'link_tags';
$tag = 'link';
$key = 'rel';
$param = 'href';
}
foreach ($this->$source as $name => $vars) {
// $vars can be in many forms:
// - string
// - array('key' => 'val')
// - array(string, string)
// - array(array(), string)
// - array(array('key' => 'val'), array('key' => 'val'))
// normalise this for processing by checking for string array keys
$vars = is_array($vars) ? (count(array_filter(array_keys($vars), 'is_string')) > 0 ? array($vars) : $vars) : array($vars);
foreach ($vars as $args) {
// skip unset headers e.g. when extending a skin and removing a header defined in the parent
if ($args === false) {
continue;
}
$args = is_array($args) ? $args : array($param => $args);
// special handling for favicon
if ($object == 'links' && $name == 'shortcut icon' && empty($args[$param])) {
if ($href = $this->get_template_logo(':favicon', true)) {
$args[$param] = $href;
}
else if ($href = $this->config->get('favicon', '/images/favicon.ico')) {
$args[$param] = $href;
}
}
$content .= html::tag($tag, array($key => $name, 'nl' => true) + $args);
}
}
}
// exec plugin hooks for this template object
$hook = $this->app->plugins->exec_hook("template_object_$object", $attrib + array('content' => $content));
if (strlen($hook['content']) && !empty($external)) {
$object_id = uniqid('TEMPLOBJECT:', true);
$this->objects[$object_id] = $hook['content'];
$hook['content'] = $object_id;
}
return $hook['content'];
// return <link> element
case 'link':
if ($attrib['condition'] && !$this->check_condition($attrib['condition'])) {
break;
}
unset($attrib['condition']);
return html::tag('link', $attrib);
// return code for a specified eval expression
case 'exp':
return html::quote($this->eval_expression($attrib['expression']));
// return variable
case 'var':
$var = explode(':', $attrib['name']);
$value = $this->parse_variable($var[0], $var[1]);
if (is_array($value)) {
$value = implode(', ', $value);
}
return html::quote($value);
case 'form':
return $this->form_tag($attrib);
}
return '';
}
/**
* Prepares template object attributes
*
* @param array &$attribs Attributes
*/
protected function prepare_object_attribs(&$attribs)
{
// Localize data-label-* attributes
array_walk($attribs, function(&$value, $key, $rcube) {
if (strpos($key, 'data-label-') === 0) {
$value = $rcube->gettext($value);
}
}, $this->app);
}
/**
* Include a specific file and return it's contents
*
* @param string $file File path
*
* @return string Contents of the processed file
*/
protected function include_php($file)
{
ob_start();
include $file;
$out = ob_get_contents();
ob_end_clean();
return $out;
}
/**
* Put objects' content back into template output
*/
protected function postrender($output)
{
// insert objects' contents
foreach ($this->objects as $key => $val) {
$output = str_replace($key, $val, $output, $count);
if ($count) {
$this->objects[$key] = null;
}
}
// make sure all <form> tags have a valid request token
$output = preg_replace_callback('/<form\s+([^>]+)>/Ui', array($this, 'alter_form_tag'), $output);
return $output;
}
/**
* Create and register a button
*
* @param array $attrib 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 $s_button_count = 100;
static $disabled_actions = null;
// these commands can be called directly via url
$a_static_commands = array('compose', 'list', 'preferences', 'folders', 'identities');
if (!($attrib['command'] || $attrib['name'] || $attrib['href'])) {
return '';
}
// try to find out the button type
if ($attrib['type']) {
$attrib['type'] = strtolower($attrib['type']);
if ($pos = strpos($attrib['type'], '-menuitem')) {
$attrib['type'] = substr($attrib['type'], 0, -9);
$menuitem = true;
}
}
else {
$attrib['type'] = ($attrib['image'] || $attrib['imagepas'] || $attrib['imageact']) ? 'image' : 'button';
}
$command = $attrib['command'];
$action = $command ?: $attrib['name'];
if ($attrib['task']) {
$command = $attrib['task'] . '.' . $command;
$element = $attrib['task'] . '.' . $action;
}
else {
$element = ($this->env['task'] ? $this->env['task'] . '.' : '') . $action;
}
if ($disabled_actions === null) {
$disabled_actions = (array) $this->config->get('disabled_actions');
}
// remove buttons for disabled actions
if (in_array($element, $disabled_actions) || in_array($action, $disabled_actions)) {
return '';
}
if (!$attrib['image']) {
$attrib['image'] = $attrib['imagepas'] ? $attrib['imagepas'] : $attrib['imageact'];
}
if (!$attrib['id']) {
$attrib['id'] = sprintf('rcmbtn%d', $s_button_count++);
}
// get localized text for labels and titles
if ($attrib['title']) {
$attrib['title'] = html::quote($this->app->gettext($attrib['title'], $attrib['domain']));
}
if ($attrib['label']) {
$attrib['label'] = html::quote($this->app->gettext($attrib['label'], $attrib['domain']));
}
if ($attrib['alt']) {
$attrib['alt'] = html::quote($this->app->gettext($attrib['alt'], $attrib['domain']));
}
// set accessibility attributes
if (!$attrib['role']) {
$attrib['role'] = 'button';
}
if (!empty($attrib['class']) && !empty($attrib['classact']) || !empty($attrib['imagepas']) && !empty($attrib['imageact'])) {
if (array_key_exists('tabindex', $attrib))
$attrib['data-tabindex'] = $attrib['tabindex'];
$attrib['tabindex'] = '-1'; // disable button by default
$attrib['aria-disabled'] = 'true';
}
// 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');",
self::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'] = $this->app->url(array('task' => $attrib['command']));
$attrib['onclick'] = sprintf("return %s.command('switch-task','%s',this,event)", self::JS_OBJECT_NAME, $attrib['command']);
}
else if ($attrib['task'] && in_array($attrib['task'], rcmail::$main_tasks)) {
$attrib['href'] = $this->app->url(array('action' => $attrib['command'], 'task' => $attrib['task']));
}
else if (in_array($attrib['command'], $a_static_commands)) {
$attrib['href'] = $this->app->url(array('action' => $attrib['command']));
}
else if (($attrib['command'] == 'permaurl' || $attrib['command'] == 'extwin') && !empty($this->env['permaurl'])) {
$attrib['href'] = $this->env['permaurl'];
}
}
// overwrite attributes
if (!$attrib['href'] && !$menuitem) {
$attrib['href'] = '#';
}
if ($attrib['task']) {
if ($attrib['classact']) {
$attrib['class'] = $attrib['classact'];
}
}
else if ($command && !$attrib['onclick']) {
$attrib['onclick'] = sprintf(
"return %s.command('%s','%s',this,event)",
self::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_merge(html::$common_attrib, array('href', 'onclick', 'tabindex', 'target', 'rel'));
if ($attrib['innerclass']) {
$btn_content = html::span($attrib['innerclass'], $btn_content);
}
}
else if ($attrib['type'] == 'input') {
$attrib['type'] = 'button';
if ($attrib['label']) {
$attrib['value'] = $attrib['label'];
}
if ($attrib['command']) {
$attrib['disabled'] = 'disabled';
}
$out = html::tag('input', $attrib, null, array('type', 'value', 'onclick', 'id', 'class', 'style', 'tabindex', 'disabled'));
}
else {
if ($attrib['label']) {
$attrib['value'] = $attrib['label'];
}
if ($attrib['command']) {
$attrib['disabled'] = 'disabled';
}
$content = isset($attrib['content']) ? $attrib['content'] : $attrib['label'];
$out = html::tag('button', $attrib, $content, array('type', 'value', 'onclick', 'id', 'class', 'style', 'tabindex', 'disabled'));
}
// 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);
}
if ($attrib['wrapper']) {
$out = html::tag($attrib['wrapper'], null, $out);
}
if ($menuitem) {
$class = $attrib['menuitem-class'] ? ' class="' . $attrib['menuitem-class'] . '"' : '';
$out = '<li role="menuitem"' . $class . '>' . $out . '</li>';
}
return $out;
}
/**
* Link an external script file
*
* @param string $file File URL
* @param string $position Target position [head|head_bottom|foot]
*/
public function include_script($file, $position = 'head', $add_path = true)
{
if ($add_path && !preg_match('|^https?://|i', $file) && $file[0] != '/') {
$file = $this->file_mod($this->scripts_path . $file);
}
if (!is_array($this->script_files[$position])) {
$this->script_files[$position] = array();
}
if (!in_array($file, $this->script_files[$position])) {
$this->script_files[$position][] = $file;
}
}
/**
* Add inline javascript code
*
* @param string $script JS code snippet
* @param string $position Target position [head|head_top|foot|docready]
*/
public function add_script($script, $position = 'head')
{
if (!isset($this->scripts[$position])) {
$this->scripts[$position] = "\n" . rtrim($script);
}
else {
$this->scripts[$position] .= "\n" . rtrim($script);
}
}
/**
* Link an external css file
*
* @param string $file File URL
*/
public function include_css($file)
{
$this->css_files[] = $file;
}
/**
* Add HTML code to the page header
*
* @param string $str HTML code
*/
public function add_header($str)
{
$this->header .= "\n" . $str;
}
/**
* Add HTML code to the page footer
* To be added right befor </body>
*
* @param string $str HTML code
*/
public function add_footer($str)
{
$this->footer .= "\n" . $str;
}
/**
* Process template and write to stdOut
*
* @param string $output HTML output
* @param string $base_path Base for absolute paths
*/
protected function _write($output = '', $base_path = '')
{
$output = trim($output);
if (empty($output)) {
$output = html::doctype('html5') . "\n" . $this->default_template;
$is_empty = true;
}
// set default page title
if (empty($this->pagetitle)) {
$this->pagetitle = 'Roundcube Mail';
}
// declare page language
if (!empty($_SESSION['language'])) {
$lang = substr($_SESSION['language'], 0, 2);
$output = preg_replace('/<html/', '<html lang="' . html::quote($lang) . '"', $output, 1);
if (!headers_sent()) {
header('Content-Language: ' . $lang);
}
}
$merge_script_files = function($output, $script) {
return $output . html::script($script);
};
$merge_scripts = function($output, $script) {
return $output . html::script(array(), $script);
};
// put docready commands into page footer
if (!empty($this->scripts['docready'])) {
$this->add_script('$(document).ready(function(){ ' . $this->scripts['docready'] . "\n});", 'foot');
}
// replace specialchars in content
$page_title = html::quote($this->pagetitle);
$page_header = '';
$page_footer = '';
// include meta tag with charset
if (!empty($this->charset)) {
if (!headers_sent()) {
header('Content-Type: text/html; charset=' . $this->charset);
}
$page_header = '<meta http-equiv="content-type"';
$page_header.= ' content="text/html; charset=';
$page_header.= $this->charset . '" />'."\n";
}
// include scripts into header/footer
$page_header .= array_reduce((array) $this->script_files['head'], $merge_script_files);
$page_header .= array_reduce(array($this->scripts['head_top'] . $this->scripts['head']), $merge_scripts);
$page_header .= $this->header . "\n";
$page_header .= array_reduce((array) $this->script_files['head_bottom'], $merge_script_files);
$page_footer .= array_reduce((array) $this->script_files['foot'], $merge_script_files);
$page_footer .= $this->footer . "\n";
$page_footer .= array_reduce((array) $this->scripts['foot'], $merge_scripts);
// find page header
if ($hpos = stripos($output, '</head>')) {
$page_header .= "\n";
}
else {
if (!is_numeric($hpos)) {
$hpos = stripos($output, '<body');
}
if (!is_numeric($hpos) && ($hpos = stripos($output, '<html'))) {
while ($output[$hpos] != '>') {
$hpos++;
}
$hpos++;
}
$page_header = "<head>\n<title>$page_title</title>\n$page_header\n</head>\n";
}
// add page hader
if ($hpos) {
$output = substr_replace($output, $page_header, $hpos, 0);
}
else {
$output = $page_header . $output;
}
// add page footer
if (($fpos = strripos($output, '</body>')) || ($fpos = strripos($output, '</html>'))) {
// for Elastic: put footer content before "footer scripts"
while (($npos = strripos($output, "\n", -strlen($output) + $fpos - 1))
&& $npos != $fpos
&& ($chunk = substr($output, $npos, $fpos - $npos)) !== ''
&& (trim($chunk) === '' || preg_match('/\s*<script[^>]+><\/script>\s*/', $chunk))
) {
$fpos = $npos;
}
$output = substr_replace($output, $page_footer."\n", $fpos, 0);
}
else {
$output .= "\n".$page_footer;
}
// add css files in head, before scripts, for speed up with parallel downloads
if (!empty($this->css_files) && !$is_empty
&& (($pos = stripos($output, '<script ')) || ($pos = stripos($output, '</head>')))
) {
$css = '';
foreach ($this->css_files as $file) {
$is_less = substr_compare($file, '.less', -5, 5, true) === 0;
$css .= html::tag('link', array(
'rel' => $is_less ? 'stylesheet/less' : 'stylesheet',
'type' => 'text/css',
'href' => $file,
'nl' => true,
));
}
$output = substr_replace($output, $css, $pos, 0);
}
$output = $this->parse_with_globals($this->fix_paths($output));
if ($this->assets_path) {
$output = $this->fix_assets_paths($output);
}
$output = $this->postrender($output);
// trigger hook with final HTML content to be sent
$hook = $this->app->plugins->exec_hook("send_page", array('content' => $output));
if (!$hook['abort']) {
if ($this->charset != RCUBE_CHARSET) {
echo rcube_charset::convert($hook['content'], RCUBE_CHARSET, $this->charset);
}
else {
echo $hook['content'];
}
}
}
/**
* Returns iframe object, registers some related env variables
*
* @param array $attrib HTML attributes
* @param boolean $is_contentframe Register this iframe as the 'contentframe' gui object
*
* @return string IFRAME element
*/
public function frame($attrib, $is_contentframe = false)
{
static $idcount = 0;
if (!$attrib['id']) {
$attrib['id'] = 'rcmframe' . ++$idcount;
}
$attrib['name'] = $attrib['id'];
$attrib['src'] = $attrib['src'] ? $this->abs_url($attrib['src'], true) : 'about:blank';
// register as 'contentframe' object
if ($is_contentframe || $attrib['contentframe']) {
$this->set_env('contentframe', $attrib['contentframe'] ? $attrib['contentframe'] : $attrib['name']);
}
return html::iframe($attrib);
}
/* ************* common functions delivering gui objects ************** */
/**
* Create a form tag with the necessary hidden fields
*
* @param array $attrib Named tag parameters
* @param string $content HTML content of the form
*
* @return string HTML code for the form
*/
public function form_tag($attrib, $content = null)
{
if ($this->env['extwin']) {
$hiddenfield = new html_hiddenfield(array('name' => '_extwin', 'value' => '1'));
$hidden = $hiddenfield->show();
}
else if ($this->framed || $this->env['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' => $this->app->comm_path, 'method' => "get"),
$hidden . $content,
array('id','class','style','name','method','action','enctype','onsubmit')
);
}
/**
* Build a form tag with a unique request token
*
* @param array $attrib Named tag parameters including 'action' and 'task' values
* which will be put into hidden fields
* @param string $content 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']));
}
// we already have a <form> tag
if ($attrib['form']) {
if ($this->framed || $this->env['framed']) {
$hidden->add(array('name' => '_framed', 'value' => '1'));
}
return $hidden->show() . $content;
}
unset($attrib['task'], $attrib['request']);
$attrib['action'] = './';
return $this->form_tag($attrib, $hidden->show() . $content);
}
/**
* GUI object 'username'
* Showing IMAP username of the current session
*
* @param array $attrib 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;
}
// Current username is an e-mail address
if (strpos($_SESSION['username'], '@')) {
$username = $_SESSION['username'];
}
// get e-mail address from default identity
else if ($sql_arr = $this->app->user->get_identity()) {
$username = $sql_arr['email'];
}
else {
$username = $this->app->user->get_username();
}
return rcube_utils::idn_to_utf8($username);
}
/**
* GUI object 'loginform'
* Returns code for the webmail login form
*
* @param array $attrib Named parameters
*
* @return string HTML code for the gui object
*/
protected function login_form($attrib)
{
$default_host = $this->config->get('default_host');
$autocomplete = (int) $this->config->get('login_autocomplete');
$_SESSION['temp'] = true;
// save original url
$url = rcube_utils::get_input_value('_url', rcube_utils::INPUT_POST);
if (empty($url) && !preg_match('/_(task|action)=logout/', $_SERVER['QUERY_STRING']))
$url = $_SERVER['QUERY_STRING'];
// Disable autocapitalization on iPad/iPhone (#1488609)
$attrib['autocapitalize'] = 'off';
$form_name = !empty($attrib['form']) ? $attrib['form'] : 'form';
// set atocomplete attribute
$user_attrib = $autocomplete > 0 ? array() : array('autocomplete' => 'off');
$host_attrib = $autocomplete > 0 ? array() : array('autocomplete' => 'off');
$pass_attrib = $autocomplete > 1 ? array() : array('autocomplete' => 'off');
$input_task = new html_hiddenfield(array('name' => '_task', 'value' => 'login'));
$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_user = new html_inputfield(array('name' => '_user', 'id' => 'rcmloginuser', 'required' => 'required')
+ $attrib + $user_attrib);
$input_pass = new html_passwordfield(array('name' => '_pass', 'id' => 'rcmloginpwd', 'required' => 'required')
+ $attrib + $pass_attrib);
$input_host = null;
if (is_array($default_host) && count($default_host) > 1) {
$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 (is_array($default_host) && ($host = key($default_host)) !== null) {
$hide_host = true;
$input_host = new html_hiddenfield(array(
'name' => '_host', 'id' => 'rcmloginhost', 'value' => is_numeric($host) ? $default_host[$host] : $host) + $attrib);
}
else if (empty($default_host)) {
$input_host = new html_inputfield(array('name' => '_host', 'id' => 'rcmloginhost')
+ $attrib + $host_attrib);
}
$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', html::quote($this->app->gettext('username'))));
$table->add('input', $input_user->show(rcube_utils::get_input_value('_user', rcube_utils::INPUT_GPC)));
$table->add('title', html::label('rcmloginpwd', html::quote($this->app->gettext('password'))));
$table->add('input', $input_pass->show());
// add host selection row
if (is_object($input_host) && !$hide_host) {
$table->add('title', html::label('rcmloginhost', html::quote($this->app->gettext('server'))));
$table->add('input', $input_host->show(rcube_utils::get_input_value('_host', rcube_utils::INPUT_GPC)));
}
$out = $input_task->show();
$out .= $input_action->show();
$out .= $input_tzone->show();
$out .= $input_url->show();
$out .= $table->show();
if ($hide_host) {
$out .= $input_host->show();
}
if (rcube_utils::get_boolean($attrib['submit'])) {
$button_attr = array('type' => 'submit', 'id' => 'rcmloginsubmit', 'class' => 'button mainaction submit');
$out .= html::p('formbuttons', html::tag('button', $button_attr, $this->app->gettext('login')));
}
// surround html output with a form tag
if (empty($attrib['form'])) {
$out = $this->form_tag(array('name' => $form_name, 'method' => 'post'), $out);
}
// include script for timezone detection
$this->include_script('jstz.min.js');
return $out;
}
/**
* GUI object 'preloader'
* Loads javascript code for images preloading
*
* @param array $attrib Named parameters
* @return void
*/
protected function preloader($attrib)
{
$images = preg_split('/[\s\t\n,]+/', $attrib['images'], -1, PREG_SPLIT_NO_EMPTY);
$images = array_map(array($this, 'abs_url'), $images);
$images = array_map(array($this, 'asset_url'), $images);
if (empty($images) || $_REQUEST['_task'] == 'logout') {
return;
}
$this->add_script('var images = ' . self::json_serialize($images, $this->devel_mode) .';
for (var i=0; i<images.length; i++) {
img = new Image();
img.src = images[i];
}', 'docready');
}
/**
* GUI object 'searchform'
* Returns code for search function
*
* @param array $attrib Named parameters
*
* @return string HTML code for the gui object
*/
public function search_form($attrib)
{
// add some labels to client
$this->add_label('searching');
$attrib['name'] = '_q';
$attrib['class'] = trim($attrib['class'] . ' no-bs');
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmqsearchbox';
}
if ($attrib['type'] == 'search' && !$this->browser->khtml) {
unset($attrib['type'], $attrib['results']);
}
if (empty($attrib['placeholder'])) {
$attrib['placeholder'] = $this->app->gettext('searchplaceholder');
}
$label = html::label(array('for' => $attrib['id'], 'class' => 'voice'), rcube::Q($this->app->gettext('arialabelsearchterms')));
$input_q = new html_inputfield($attrib);
$out = $label . $input_q->show();
// Support for multiple searchforms on the same page
if ($attrib['gui-object'] !== false && $attrib['gui-object'] !== 'false') {
$this->add_gui_object($attrib['gui-object'] ?: 'qsearchbox', $attrib['id']);
}
// add form tag around text field
if (empty($attrib['form']) && empty($attrib['no-form'])) {
$out = $this->form_tag(array(
'name' => $attrib['form-name'] ?: 'rcmqsearchform',
'onsubmit' => sprintf("%s.command('%s'); return false", self::JS_OBJECT_NAME, $attrib['command'] ?: 'search'),
// 'style' => "display:inline"
), $out);
}
if (!empty($attrib['wrapper'])) {
$header = html::tag($attrib['ariatag'] ?: 'h2', array(
'id' => 'aria-label-' . $attrib['label'],
'class' => 'voice'
), rcube::Q($this->app->gettext('arialabel' . $attrib['label'], $attrib['label-domain'])));
if ($attrib['options']) {
$options_button = $this->button(array(
'type' => 'link',
'href' => '#search-filter',
'class' => 'button options',
'label' => 'options',
'title' => 'options',
'tabindex' => '0',
'innerclass' => 'inner',
'data-target' => $attrib['options']
));
}
$search_button = $this->button(array(
'type' => 'link',
'href' => '#search',
'class' => 'button search',
'label' => $attrib['buttontitle'],
'title' => $attrib['buttontitle'],
'tabindex' => '0',
'innerclass' => 'inner',
));
$reset_button = $this->button(array(
'type' => 'link',
'command' => $attrib['reset-command'] ?: 'reset-search',
'class' => 'button reset',
'label' => 'resetsearch',
'title' => 'resetsearch',
'tabindex' => '0',
'innerclass' => 'inner',
));
$out = html::div(array(
'role' => 'search',
'aria-labelledby' => $attrib['label'] ? 'aria-label-' . $attrib['label'] : null,
'class' => $attrib['wrapper'],
), "$header$out\n$reset_button\n$options_button\n$search_button");
}
return $out;
}
/**
* Builder for GUI object 'message'
*
* @param array Named tag parameters
* @return string HTML code for the gui object
*/
protected 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 $attrib Named parameters for the select tag
*
* @return string HTML code for the gui object
*/
public 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', 'name', 'class', 'style', 'size', 'tabindex'))) {
$field_attrib[$attr] = $value;
}
}
$charsets = array(
'UTF-8' => 'UTF-8 ('.$this->app->gettext('unicode').')',
'US-ASCII' => 'ASCII ('.$this->app->gettext('english').')',
'ISO-8859-1' => 'ISO-8859-1 ('.$this->app->gettext('westerneuropean').')',
'ISO-8859-2' => 'ISO-8859-2 ('.$this->app->gettext('easterneuropean').')',
'ISO-8859-4' => 'ISO-8859-4 ('.$this->app->gettext('baltic').')',
'ISO-8859-5' => 'ISO-8859-5 ('.$this->app->gettext('cyrillic').')',
'ISO-8859-6' => 'ISO-8859-6 ('.$this->app->gettext('arabic').')',
'ISO-8859-7' => 'ISO-8859-7 ('.$this->app->gettext('greek').')',
'ISO-8859-8' => 'ISO-8859-8 ('.$this->app->gettext('hebrew').')',
'ISO-8859-9' => 'ISO-8859-9 ('.$this->app->gettext('turkish').')',
'ISO-8859-10' => 'ISO-8859-10 ('.$this->app->gettext('nordic').')',
'ISO-8859-11' => 'ISO-8859-11 ('.$this->app->gettext('thai').')',
'ISO-8859-13' => 'ISO-8859-13 ('.$this->app->gettext('baltic').')',
'ISO-8859-14' => 'ISO-8859-14 ('.$this->app->gettext('celtic').')',
'ISO-8859-15' => 'ISO-8859-15 ('.$this->app->gettext('westerneuropean').')',
'ISO-8859-16' => 'ISO-8859-16 ('.$this->app->gettext('southeasterneuropean').')',
'WINDOWS-1250' => 'Windows-1250 ('.$this->app->gettext('easterneuropean').')',
'WINDOWS-1251' => 'Windows-1251 ('.$this->app->gettext('cyrillic').')',
'WINDOWS-1252' => 'Windows-1252 ('.$this->app->gettext('westerneuropean').')',
'WINDOWS-1253' => 'Windows-1253 ('.$this->app->gettext('greek').')',
'WINDOWS-1254' => 'Windows-1254 ('.$this->app->gettext('turkish').')',
'WINDOWS-1255' => 'Windows-1255 ('.$this->app->gettext('hebrew').')',
'WINDOWS-1256' => 'Windows-1256 ('.$this->app->gettext('arabic').')',
'WINDOWS-1257' => 'Windows-1257 ('.$this->app->gettext('baltic').')',
'WINDOWS-1258' => 'Windows-1258 ('.$this->app->gettext('vietnamese').')',
'ISO-2022-JP' => 'ISO-2022-JP ('.$this->app->gettext('japanese').')',
'ISO-2022-KR' => 'ISO-2022-KR ('.$this->app->gettext('korean').')',
'ISO-2022-CN' => 'ISO-2022-CN ('.$this->app->gettext('chinese').')',
'EUC-JP' => 'EUC-JP ('.$this->app->gettext('japanese').')',
'EUC-KR' => 'EUC-KR ('.$this->app->gettext('korean').')',
'EUC-CN' => 'EUC-CN ('.$this->app->gettext('chinese').')',
'BIG5' => 'BIG5 ('.$this->app->gettext('chinese').')',
'GB2312' => 'GB2312 ('.$this->app->gettext('chinese').')',
);
if ($post = rcube_utils::get_input_value('_charset', rcube_utils::INPUT_POST)) {
$set = $post;
}
else if (!empty($attrib['selected'])) {
$set = $attrib['selected'];
}
else {
$set = $this->get_charset();
}
$set = strtoupper($set);
if (!isset($charsets[$set]) && preg_match('/^[A-Z0-9-]+$/', $set)) {
$charsets[$set] = $set;
}
$select = new html_select($field_attrib);
$select->add(array_values($charsets), array_keys($charsets));
return $select->show($set);
}
/**
* Include content from config/about.<LANG>.html if available
*/
protected function about_content($attrib)
{
$content = '';
$filenames = array(
'about.' . $_SESSION['language'] . '.html',
'about.' . substr($_SESSION['language'], 0, 2) . '.html',
'about.html',
);
foreach ($filenames as $file) {
$fn = RCUBE_CONFIG_DIR . $file;
if (is_readable($fn)) {
$content = file_get_contents($fn);
$content = $this->parse_conditions($content);
$content = $this->parse_xml($content);
break;
}
}
return $content;
}
/**
* Get logo URL for current template based on skin_logo config option
*
* @param string $name Name of the logo to check for
* default is current template
* @param boolean $strict True if logo should only be returned for specific template
*
* @return string image URL
*/
protected function get_template_logo($name = null, $strict = false)
{
$template_logo = null;
// Use current template if none provided
if (!$name) {
$name = $this->template_name;
}
$template_names = array(
$this->skin_name . ':' . $name,
$this->skin_name . ':*',
$name,
'*',
);
// If strict matching then remove wildcard options
if ($strict) {
$template_names = preg_grep("/\*$/", $template_names, PREG_GREP_INVERT);
}
if ($logo = $this->config->get('skin_logo')) {
if (is_array($logo)) {
foreach ($template_names as $key) {
if (isset($logo[$key])) {
$template_logo = $logo[$key];
break;
}
}
}
else {
$template_logo = $logo;
}
}
return $template_logo;
}
}
diff --git a/program/include/rcmail_output_json.php b/program/include/rcmail_output_json.php
index 6530db735..38418421d 100644
--- a/program/include/rcmail_output_json.php
+++ b/program/include/rcmail_output_json.php
@@ -1,277 +1,276 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/include/rcmail_output_json.php |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2012, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Class to handle JSON (AJAX) output |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* View class to produce JSON responses
*
* @package Webmail
* @subpackage View
*/
class rcmail_output_json extends rcmail_output
{
protected $texts = array();
protected $commands = array();
protected $callbacks = array();
protected $message = null;
public $type = 'js';
public $ajax_call = true;
/**
* Object constructor
*/
public function __construct($task = null, $framed = false)
{
parent::__construct();
if (!empty($_SESSION['skin_config'])) {
foreach ($_SESSION['skin_config'] as $key => $value) {
$this->config->set($key, $value, true);
}
$value = array_merge((array) $this->config->get('dont_override'), array_keys($_SESSION['skin_config']));
$this->config->set('dont_override', $value, true);
}
}
/**
* Issue command to set page title
*
* @param string $title New page title
*/
public function set_pagetitle($title)
{
if ($this->config->get('devel_mode') && !empty($_SESSION['username'])) {
$name = $_SESSION['username'];
}
else {
$name = $this->config->get('product_name');
}
$this->command('set_pagetitle', empty($name) ? $title : $name . ' :: ' . $title);
}
/**
* Register a template object handler
*
* @param string $obj Object name
* @param string $func Function name to call
*/
public function add_handler($obj, $func)
{
// ignore
}
/**
* Register a list of template object handlers
*
* @param array $arr Hash array with object=>handler pairs
*/
public function add_handlers($arr)
{
// ignore
}
/**
* Call a client method
*
* @param string Method to call
* @param ... Additional arguments
*/
public function command()
{
$cmd = func_get_args();
if (strpos($cmd[0], 'plugin.') === 0) {
$this->callbacks[] = $cmd;
}
else {
$this->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->texts[$name] = $this->app->gettext($name);
}
}
/**
* Invoke display_message command
*
* @param string $message Message to display
* @param string $type Message type [notice|confirm|error]
* @param array $vars Key-value pairs to be replaced in localized text
* @param boolean $override Override last set message
* @param int $timeout Message displaying time in seconds
*
* @uses self::command()
*/
public function show_message($message, $type='notice', $vars=null, $override=true, $timeout=0)
{
if ($override || !$this->message) {
if ($this->app->text_exists($message)) {
if (!empty($vars)) {
$vars = array_map(array('rcmail', 'Q'), $vars);
}
$msgtext = $this->app->gettext(array('name' => $message, 'vars' => $vars));
}
else
$msgtext = $message;
$this->message = $message;
$this->command('display_message', $msgtext, $type, $timeout * 1000);
}
}
/**
* Delete all stored env variables and commands
*/
public function reset()
{
parent::reset();
$this->texts = array();
$this->commands = array();
}
/**
* Redirect to a certain url
*
* @param mixed $p Either a string with the action or url parameters as key-value pairs
* @param int $delay Delay in seconds
*
* @see rcmail::url()
*/
public function redirect($p = array(), $delay = 1)
{
$location = $this->app->url($p);
$this->remote_response(sprintf("window.setTimeout(function(){ %s.redirect('%s',true); }, %d);",
self::JS_OBJECT_NAME, $location, $delay));
exit;
}
/**
* Send an AJAX response to the client.
*/
public function send()
{
$this->remote_response();
exit;
}
/**
* Show error page and terminate script execution
*
* @param int $code Error code
* @param string $message Error message
*/
public function raise_error($code, $message)
{
if ($code == 403) {
header('HTTP/1.1 403 Forbidden');
die("Invalid Request");
}
$this->show_message("Application Error ($code): $message", 'error');
$this->remote_response();
exit;
}
/**
* Send an AJAX response with executable JS code
*
* @param string $add Additional JS code
*/
protected function remote_response($add = '')
{
static $s_header_sent = false;
if (!$s_header_sent) {
$s_header_sent = true;
$this->nocacheing_headers();
header('Content-Type: text/plain; charset=' . $this->get_charset());
}
// unset default env vars
unset($this->env['task'], $this->env['action'], $this->env['comm_path']);
$rcmail = rcmail::get_instance();
$response['action'] = $rcmail->action;
if ($unlock = rcube_utils::get_input_value('_unlock', rcube_utils::INPUT_GPC)) {
$response['unlock'] = $unlock;
}
if (!empty($this->env))
$response['env'] = $this->env;
if (!empty($this->texts))
$response['texts'] = $this->texts;
// send function calls
$response['exec'] = $this->get_js_commands() . $add;
if (!empty($this->callbacks))
$response['callbacks'] = $this->callbacks;
// trigger generic hook where plugins can put additional content to the response
$hook = $this->app->plugins->exec_hook("render_response", array('response' => $response));
// save some memory
$response = $hook['response'];
unset($hook['response']);
echo self::json_serialize($response, $this->devel_mode, false);
}
/**
* Return executable javascript code for all registered commands
*/
protected function get_js_commands()
{
$out = '';
foreach ($this->commands as $i => $args) {
$method = array_shift($args);
foreach ($args as $i => $arg) {
$args[$i] = self::json_serialize($arg, $this->devel_mode, false);
}
$out .= sprintf(
"this.%s(%s);\n",
preg_replace('/^parent\./', '', $method),
implode(',', $args)
);
}
return $out;
}
}
diff --git a/program/include/rcmail_resend_mail.php b/program/include/rcmail_resend_mail.php
index 95b785455..5b764cd98 100644
--- a/program/include/rcmail_resend_mail.php
+++ b/program/include/rcmail_resend_mail.php
@@ -1,187 +1,186 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/include/rcmail_resend_mail.php |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2017, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Bounce/resend an email message |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Mail_mime wrapper to handle mail resend/bounce
*
* @package Webmail
*/
class rcmail_resend_mail extends Mail_mime
{
protected $orig_head;
protected $orig_body;
/**
* Constructor function
*
* Added two parameters:
* 'bounce_message' - rcube_message object of the original message
* 'bounce_headers' - An array of headers to be added to the original message
*/
public function __construct($params = array())
{
// To make the code simpler always use delay_file_io=true
$params['delay_file_io'] = true;
$params['eol'] = "\r\n";
parent::__construct($params);
}
/**
* Returns/Sets message headers
*/
public function headers($headers = array(), $overwrite = false, $skip_content = false)
{
// headers() wrapper that returns Resent-Cc, Resent-Bcc instead of Cc,Bcc
// it's also called to re-add Resent-Bcc after it has been sent (to store in Sent)
if (array_key_exists('Bcc', $headers)) {
$this->build_params['bounce_headers']['Resent-Bcc'] = $headers['Bcc'];
}
foreach ($this->build_params['bounce_headers'] as $key => $val) {
$headers[str_replace('Resent-', '', $key)] = $val;
}
return $headers;
}
/**
* Returns all message headers as string
*/
public function txtHeaders($headers = array(), $overwrite = false, $skip_content = false)
{
// i.e. add Resent-* headers on top of the original message head
$this->init_message();
$result = array();
foreach ($this->build_params['bounce_headers'] as $name => $value) {
$key = str_replace('Resent-', '', $name);
// txtHeaders() can be used to unset Bcc header
if (array_key_exists($key, $headers)) {
$value = $headers[$key];
$this->build_params['bounce_headers']['Resent-'.$key] = $value;
}
if ($value) {
$result[] = "$name: $value";
}
}
$result = implode($this->build_params['eol'], $result);
if (strlen($this->orig_head)) {
$result .= $this->build_params['eol'] . $this->orig_head;
}
return $result;
}
/**
* Save the message body to a file (if delay_file_io=true)
*/
public function saveMessageBody($file, $params = null)
{
$this->init_message();
// this will be called only once, so let just move the file
rename($this->orig_body, $file);
$this->orig_head = null;
}
protected function init_message()
{
if ($this->orig_head !== null) {
return;
}
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$message = $this->build_params['bounce_message'];
$path = rcube_utils::temp_filename('bounce');
// We'll write the body to the file and the headers to a variable
if ($fp = fopen($path, 'w')) {
stream_filter_register('bounce_source', 'rcmail_bounce_stream_filter');
stream_filter_append($fp, 'bounce_source');
// message part
if ($message->context) {
$message->get_part_body($message->context, false, 0, $fp);
}
// complete message
else {
$storage->set_folder($message->folder);
$storage->get_raw_body($message->uid, $fp);
}
fclose($fp);
$this->orig_head = rcmail_bounce_stream_filter::$headers;
$this->orig_body = $path;
}
}
}
/**
* Stream filter to remove message headers from the streamed
* message source (and store them in a variable)
*
* @package Webmail
*/
class rcmail_bounce_stream_filter extends php_user_filter
{
public static $headers;
protected $in_body = false;
public function onCreate()
{
self::$headers = '';
}
public function filter($in, $out, &$consumed, $closing)
{
while ($bucket = stream_bucket_make_writeable($in)) {
if (!$this->in_body) {
self::$headers .= $bucket->data;
if (($pos = strpos(self::$headers, "\r\n\r\n")) === false) {
continue;
}
$bucket->data = substr(self::$headers, $pos + 4);
$bucket->datalen = strlen($bucket->data);
self::$headers = substr(self::$headers, 0, $pos);
$this->in_body = true;
}
$consumed += $bucket->datalen;
stream_bucket_append($out, $bucket);
}
return PSFS_PASS_ON;
}
}
diff --git a/program/include/rcmail_sendmail.php b/program/include/rcmail_sendmail.php
index 0b9df0f77..ef7376792 100644
--- a/program/include/rcmail_sendmail.php
+++ b/program/include/rcmail_sendmail.php
@@ -1,1529 +1,1528 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/include/rcmail_sendmail.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2017, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Common code for generating and saving/sending mail message |
| with support for common user interface elements |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Common code for generating and saving/sending mail message
* with support for common user interface elements.
*
* @package Webmail
*/
class rcmail_sendmail
{
public $data = array();
public $options = array();
protected $parse_data = array();
protected $compose_form;
// define constants for message compose mode
const MODE_REPLY = 'reply';
const MODE_FORWARD = 'forward';
const MODE_DRAFT = 'draft';
const MODE_EDIT = 'edit';
/**
* Object constructor
*
* @param array $data Compose data
* @param array $options Operation options:
* savedraft (bool) - Enable save-draft mode
* sendmail (bool) - Enable send-mail mode
* saveonly (bool) - Enable save-only mode
* message (object) - Message object to get some data from
* error_handler (callback) - Error handler
*/
public function __construct($data = array(), $options = array())
{
$this->rcmail = rcube::get_instance();
$this->data = (array) $data;
$this->options = (array) $options;
$this->options['sendmail_delay'] = (int) $this->rcmail->config->get('sendmail_delay');
if (empty($options['error_handler'])) {
$this->options['error_handler'] = function() { return false; };
}
if ($this->options['message']) {
$this->compose_init($this->options['message']);
}
}
/**
* Collect input data for message headers
*
* @return array Message headers
*/
public function headers_input()
{
if ($this->options['sendmail'] && $this->options['sendmail_delay']) {
$last_time = $this->rcmail->config->get('last_message_time');
$wait_sec = time() - $this->options['sendmail_delay'] - intval($last_time);
if ($wait_sec < 0) {
return $this->options['error_handler']('senttooquickly', 'error', array('sec' => $wait_sec * -1));
}
}
// set default charset
if (!($charset = $this->options['charset'])) {
$charset = rcube_utils::get_input_value('_charset', rcube_utils::INPUT_POST) ?: $this->rcmail->output->get_charset();
$this->options['charset'] = $charset;
}
$this->parse_data = array();
$mailto = $this->email_input_format(rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST, true, $charset), true);
$mailcc = $this->email_input_format(rcube_utils::get_input_value('_cc', rcube_utils::INPUT_POST, true, $charset), true);
$mailbcc = $this->email_input_format(rcube_utils::get_input_value('_bcc', rcube_utils::INPUT_POST, true, $charset), true);
if ($this->parse_data['INVALID_EMAIL'] && !$this->options['savedraft']) {
return $this->options['error_handler']('emailformaterror', 'error', array('email' => $this->parse_data['INVALID_EMAIL']));
}
if (($max_recipients = (int) $this->rcmail->config->get('max_recipients')) > 0) {
if ($this->parse_data['RECIPIENT_COUNT'] > $max_recipients) {
return $this->options['error_handler']('toomanyrecipients', 'error', array('max' => $max_recipients));
}
}
if (empty($mailto) && !empty($mailcc)) {
$mailto = $mailcc;
$mailcc = null;
}
else if (empty($mailto)) {
$mailto = 'undisclosed-recipients:;';
}
$dont_override = (array) $this->rcmail->config->get('dont_override');
$mdn_enabled = in_array('mdn_default', $dont_override) ? $this->rcmail->config->get('mdn_default') : !empty($_POST['_mdn']);
$dsn_enabled = in_array('dsn_default', $dont_override) ? $this->rcmail->config->get('dsn_default') : !empty($_POST['_dsn']);
$subject = rcube_utils::get_input_value('_subject', rcube_utils::INPUT_POST, true, $charset);
$from = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST, true, $charset);
$replyto = rcube_utils::get_input_value('_replyto', rcube_utils::INPUT_POST, true, $charset);
$followupto = rcube_utils::get_input_value('_followupto', rcube_utils::INPUT_POST, true, $charset);
// Get sender name and address from identity...
if (is_numeric($from)) {
if (is_array($identity_arr = $this->get_identity($from))) {
if ($identity_arr['mailto']) {
$from = $identity_arr['mailto'];
}
if ($identity_arr['string']) {
$from_string = $identity_arr['string'];
}
}
else {
$from = null;
}
}
// ... if there is no identity record, this might be a custom from
else if (($from_string = $this->email_input_format($from))
&& preg_match('/(\S+@\S+)/', $from_string, $m)
) {
$from = trim($m[1], '<>');
}
// ... otherwise it's empty or invalid
else {
$from = null;
}
// check 'From' address (identity may be incomplete)
if (!$this->options['savedraft'] && !$this->options['saveonly'] && empty($from)) {
return $this->options['error_handler']('nofromaddress', 'error');
}
if (!$from_string && $from) {
$from_string = $from;
}
$from_string = rcube_charset::convert($from_string, RCUBE_CHARSET, $charset);
$message_id = $this->data['param']['message-id'];
if (!$message_id) {
$message_id = $this->rcmail->gen_message_id($from);
}
$this->options['dsn_enabled'] = $dsn_enabled;
$this->options['from'] = $from;
$this->options['mailto'] = $mailto;
// compose headers array
$headers = array(
'Received' => $this->header_received(),
'Date' => $this->rcmail->user_date(),
'From' => $from_string,
'To' => $mailto,
'Cc' => $mailcc,
'Bcc' => $mailbcc,
'Subject' => trim($subject),
'Reply-To' => $this->email_input_format($replyto),
'Mail-Reply-To' => $headers['Reply-To'],
'Mail-Followup-To' => $this->email_input_format($followupto),
'In-Reply-To' => $this->data['reply_msgid'],
'References' => $this->data['references'],
'User-Agent' => $this->rcmail->config->get('useragent'),
'Message-ID' => $message_id,
'X-Sender' => $from,
);
if (!empty($identity_arr['organization'])) {
$headers['Organization'] = $identity_arr['organization'];
}
if ($mdn_enabled) {
$headers['Return-Receipt-To'] = $from_string;
$headers['Disposition-Notification-To'] = $from_string;
}
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));
}
}
// remember reply/forward UIDs in special headers
if ($this->options['savedraft']) {
// Note: We ignore <UID>.<PART> forwards/replies here
if (($uid = $this->data['reply_uid']) && !preg_match('/^\d+\.[0-9.]+$/', $uid)) {
$headers['X-Draft-Info'] = $this->draftinfo_encode(array(
'type' => 'reply',
'uid' => $uid,
'folder' => $this->data['mailbox']
));
}
else if (!empty($this->data['forward_uid'])
&& ($uid = rcube_imap_generic::compressMessageSet($this->data['forward_uid']))
&& !preg_match('/^\d+[0-9.]+$/', $uid)
) {
$headers['X-Draft-Info'] = $this->draftinfo_encode(array(
'type' => 'forward',
'uid' => $uid,
'folder' => $this->data['mailbox']
));
}
}
return array_filter($headers);
}
/**
* Set charset and transfer encoding on the message
*
* @param Mail_mime $message Message object
* @param bool $flowed Enable format=flowed
*/
public function set_message_encoding($message, $flowed = false)
{
$text_charset = $this->options['charset'];
$transfer_encoding = '7bit';
$head_encoding = 'quoted-printable';
// choose encodings for plain/text body and message headers
if (preg_match('/ISO-2022/i', $text_charset)) {
$head_encoding = 'base64'; // RFC1468
}
else if (preg_match('/[^\x00-\x7F]/', $message->getTXTBody())) {
$transfer_encoding = $this->rcmail->config->get('force_7bit') ? 'quoted-printable' : '8bit';
}
else if ($message_charset == 'UTF-8') {
$text_charset = 'US-ASCII';
}
if ($flowed) {
$text_charset .= ";\r\n format=flowed";
}
// encoding settings for mail composing
$message->setParam('text_encoding', $transfer_encoding);
$message->setParam('html_encoding', 'quoted-printable');
$message->setParam('head_encoding', $head_encoding);
$message->setParam('head_charset', $this->options['charset']);
$message->setParam('html_charset', $this->options['charset']);
$message->setParam('text_charset', $text_charset);
}
/**
* Create a message to be saved/sent
*
* @param array $headers Message headers
* @param string $body Message body
* @param bool $isHtml The body is HTML or not
* @param array $attachments Optional message attachments array
*
* @return Mail_mime Message object
*/
public function create_message($headers, $body, $isHtml = false, $attachments = array())
{
// set line length for body wrapping
$line_length = $this->rcmail->config->get('line_length', 72);
$charset = $this->options['charset'];
$flowed = $this->options['savedraft'] || $this->rcmail->config->get('send_format_flowed', true);
// create PEAR::Mail_mime instance
$MAIL_MIME = new Mail_mime("\r\n");
// Check if we have enough memory to handle the message in it
// It's faster than using files, so we'll do this if we only can
if (is_array($attachments) && ($mem_limit = parse_bytes(ini_get('memory_limit')))) {
$memory = 0;
foreach ($attachments as $id => $attachment) {
$memory += $attachment['size'];
}
// Yeah, Net_SMTP needs up to 12x more memory, 1.33 is for base64
if (!rcube_utils::mem_check($memory * 1.33 * 12)) {
$MAIL_MIME->setParam('delay_file_io', true);
}
}
$plugin = $this->rcmail->plugins->exec_hook('message_outgoing_body', array(
'body' => $body,
'type' => $isHtml ? 'html' : 'plain',
'message' => $MAIL_MIME
));
// For HTML-formatted messages, construct the MIME message with both
// the HTML part and the plain-text part
if ($isHtml) {
$MAIL_MIME->setHTMLBody($plugin['body']);
$plain_body = $this->rcmail->html2text($plugin['body'], array('width' => 0, 'charset' => $charset));
$plain_body = rcube_mime::wordwrap($plain_body, $line_length, "\r\n", false, $charset);
$plain_body = wordwrap($plain_body, 998, "\r\n", true);
// There's no sense to use multipart/alternative if the text/plain
// part would be blank. Completely blank text/plain part may confuse
// some mail clients (#5283)
if (strlen(trim($plain_body)) > 0) {
// make sure all line endings are CRLF (#1486712)
$plain_body = preg_replace('/\r?\n/', "\r\n", $plain_body);
$plugin = $this->rcmail->plugins->exec_hook('message_outgoing_body', array(
'body' => $plain_body,
'type' => 'alternative',
'message' => $MAIL_MIME
));
// add a plain text version of the e-mail as an alternative part.
$MAIL_MIME->setTXTBody($plugin['body']);
}
// Extract image Data URIs into message attachments (#1488502)
$this->extract_inline_images($MAIL_MIME, $this->options['from']);
}
else {
$body = $plugin['body'];
// compose format=flowed content if enabled
if ($flowed) {
$body = rcube_mime::format_flowed($body, min($line_length + 2, 79), $charset);
}
else {
$body = rcube_mime::wordwrap($body, $line_length, "\r\n", false, $charset);
}
$body = wordwrap($body, 998, "\r\n", true);
$MAIL_MIME->setTXTBody($body, false, true);
}
// encoding settings for mail composing
$this->set_message_encoding($MAIL_MIME, $flowed);
// pass headers to message object
$MAIL_MIME->headers($headers);
return $MAIL_MIME;
}
/**
* Message delivery, and setting Replied/Forwarded flag on success
*
* @param Mail_mime $message Message object
*/
public function deliver_message($message)
{
// Handle Delivery Status Notification request
$smtp_opts = array('dsn' => $this->options['dsn_enabled']);
$sent = $this->rcmail->deliver_message($message,
$this->options['from'],
$this->options['mailto'],
$smtp_error, $mailbody_file, $smtp_opts, true
);
// return to compose page if sending failed
if (!$sent) {
// remove temp file
if ($mailbody_file) {
unlink($mailbody_file);
}
if ($smtp_error && is_string($smtp_error)) {
return $this->options['error_handler']($smtp_error, 'error');
}
else if ($smtp_error && !empty($smtp_error['label'])) {
return $this->options['error_handler']($smtp_error['label'], 'error', $smtp_error['vars']);
}
else {
return $this->options['error_handler']('sendingfailed', 'error');
}
}
$message->mailbody_file = $mailbody_file;
// save message sent time
if ($this->options['sendmail_delay']) {
$this->rcmail->user->save_prefs(array('last_message_time' => time()));
}
// set replied/forwarded flag
if ($this->data['reply_uid']) {
foreach (rcmail::get_uids($this->data['reply_uid'], $this->data['mailbox']) as $mbox => $uids) {
// skip <UID>.<PART> replies
if (!preg_match('/^\d+\.[0-9.]+$/', implode(',', (array) $uids))) {
$this->rcmail->storage->set_flag($uids, 'ANSWERED', $mbox);
}
}
}
else if ($this->data['forward_uid']) {
foreach (rcmail::get_uids($this->data['forward_uid'], $this->data['mailbox']) as $mbox => $uids) {
// skip <UID>.<PART> forwards
if (!preg_match('/^\d+\.[0-9.]+$/', implode(',', (array) $uids))) {
$this->rcmail->storage->set_flag($uids, 'FORWARDED', $mbox);
}
}
}
}
/**
* Save the message into Drafts folder (in savedraft mode)
* or in Sent mailbox if specified/configured
*
* @param Mail_mime $message Message object
*
* @return mixed Operation status
*/
public function save_message($message)
{
// Determine which folder to save message
if ($this->options['savedraft']) {
$store_target = $this->rcmail->config->get('drafts_mbox');
}
else if (!$this->rcmail->config->get('no_save_sent_messages')) {
if (isset($_POST['_store_target'])) {
$store_target = rcube_utils::get_input_value('_store_target', rcube_utils::INPUT_POST, true);
}
else {
$store_target = $this->rcmail->config->get('sent_mbox');
}
}
if ($store_target) {
$storage = $this->rcmail->get_storage();
// check if folder is subscribed
if ($storage->folder_exists($store_target, true)) {
$store_folder = true;
}
// folder may be existing but not subscribed (#1485241)
else if (!$storage->folder_exists($store_target)) {
$store_folder = $storage->create_folder($store_target, true);
}
else if ($storage->subscribe($store_target)) {
$store_folder = true;
}
// append message to sent box
if ($store_folder) {
// message body in file
if ($message->mailbody_file || $message->getParam('delay_file_io')) {
$headers = $message->txtHeaders();
// file already created
if ($message->mailbody_file) {
$msg = $message->mailbody_file;
}
else {
$message->mailbody_file = rcube_utils::temp_filename('msg');
$msg = $message->saveMessageBody($message->mailbody_file);
if (!is_a($msg, 'PEAR_Error')) {
$msg = $message->mailbody_file;
}
}
}
else {
$msg = $message->getMessage();
$headers = '';
}
if (is_a($msg, 'PEAR_Error')) {
rcube::raise_error(array(
'code' => 650, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not create message: ".$msg->getMessage()),
true, false);
}
else {
$saved = $storage->save_message($store_target, $msg, $headers,
$message->mailbody_file ? true : false, array('SEEN'));
}
}
// raise error if saving failed
if (!$saved) {
rcube::raise_error(array('code' => 800, 'type' => 'imap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not save message in $store_target"), true, false);
}
}
if ($message->mailbody_file) {
unlink($message->mailbody_file);
unset($message->mailbody_file);
}
$this->options['store_target'] = $store_target;
$this->options['store_folder'] = $store_folder;
return $saved;
}
/**
* If enabled, returns Received header content to be prepended
* to message headers
*
* @return string Received header content
*/
public function header_received()
{
if ($this->rcmail->config->get('http_received_header')) {
$nldlm = "\r\n\t";
$http_header = 'from ';
// FROM/VIA
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$hosts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'], 2);
$http_header .= $this->received_host($hosts[0]) . $nldlm . ' via ';
}
$http_header .= $this->received_host($_SERVER['REMOTE_ADDR']);
// BY
$http_header .= $nldlm . 'by ' . rcube_utils::server_name('HTTP_HOST');
// WITH
$http_header .= $nldlm . 'with HTTP (' . $_SERVER['SERVER_PROTOCOL']
. ' ' . $_SERVER['REQUEST_METHOD'] . '); ' . date('r');
return wordwrap($http_header, 69, $nldlm);
}
}
/**
* Converts host address into host spec. for Received header
*/
protected function received_host($host)
{
$hostname = gethostbyaddr($host);
$result = $this->encrypt_host($hostname);
if ($host != $hostname) {
$result .= ' (' . $this->encrypt_host($host) . ')';
}
return $result;
}
/**
* Encrypt host IP or hostname for Received header
*/
protected function encrypt_host($host)
{
if ($this->rcmail->config->get('http_received_header_encrypt')) {
return $this->rcmail->encrypt($host);
}
if (!preg_match('/[^0-9:.]/', $host)) {
return "[$host]";
}
return $host;
}
/**
* Returns user identity record
*
* @param int $id Identity ID
*
* @return array User identity data
*/
public function get_identity($id)
{
if ($sql_arr = $this->rcmail->user->get_identity($id)) {
$out = $sql_arr;
if ($this->options['charset'] != RCUBE_CHARSET) {
foreach ($out as $k => $v) {
$out[$k] = rcube_charset::convert($v, RCUBE_CHARSET, $this->options['charset']);
}
}
$out['mailto'] = $sql_arr['email'];
$out['string'] = format_email_recipient($sql_arr['email'], $sql_arr['name']);
return $out;
}
return false;
}
/**
* Extract image attachments from HTML message (data URIs)
*
* @param Mail_mime $message Message object
* @param string $from Sender email address
*/
public static function extract_inline_images($message, $from)
{
$body = $message->getHTMLBody();
$offset = 0;
$list = array();
$domain = 'localhost';
$regexp = '#img[^>]+src=[\'"](data:([^;]*);base64,([a-z0-9+/=\r\n]+))([\'"])#i';
if (preg_match_all($regexp, $body, $matches, PREG_OFFSET_CAPTURE)) {
// get domain for the Content-ID, must be the same as in Mail_Mime::get()
if (preg_match('#@([0-9a-zA-Z\-\.]+)#', $from, $m)) {
$domain = $m[1];
}
foreach ($matches[1] as $idx => $m) {
$data = preg_replace('/\r\n/', '', $matches[3][$idx][0]);
$data = base64_decode($data);
if (empty($data)) {
continue;
}
$hash = md5($data) . '@' . $domain;
$mime_type = $matches[2][$idx][0];
$name = $list[$hash];
if (empty($mime_type)) {
$mime_type = rcube_mime::image_content_type($data);
}
// add the image to the MIME message
if (!$name) {
$ext = preg_replace('#^[^/]+/#', '', $mime_type);
$name = substr($hash, 0, 8) . '.' . $ext;
$list[$hash] = $name;
$message->addHTMLImage($data, $mime_type, $name, false, $hash);
}
$body = substr_replace($body, $name, $m[1] + $offset, strlen($m[0]));
$offset += strlen($name) - strlen($m[0]);
}
}
$message->setHTMLBody($body);
}
/**
* Parse and cleanup email address input (and count addresses)
*
* @param string $mailto Address input
* @param boolean $count Do count recipients (count saved in $this->parse_data['RECIPIENT_COUNT'])
* @param boolean $check Validate addresses (errors saved in $this->parse_data['INVALID_EMAIL'])
*
* @return string Canonical recipients string (comma separated)
*/
public function email_input_format($mailto, $count = false, $check = true)
{
// simplified email regexp, supporting quoted local part
$email_regexp = '(\S+|("[^"]+"))@\S+';
$delim = ',;';
$regexp = array("/[$delim]\s*[\r\n]+/", '/[\r\n]+/', "/[$delim]\s*\$/m", '/;/', '/(\S{1})(<'.$email_regexp.'>)/U');
$replace = array(', ', ', ', '', ',', '\\1 \\2');
// replace new lines and strip ending ', ', make address input more valid
$mailto = trim(preg_replace($regexp, $replace, $mailto));
$items = rcube_utils::explode_quoted_string("[$delim]", $mailto);
$result = array();
foreach ($items as $item) {
$item = trim($item);
// address in brackets without name (do nothing)
if (preg_match('/^<'.$email_regexp.'>$/', $item)) {
$item = rcube_utils::idn_to_ascii(trim($item, '<>'));
$result[] = $item;
}
// address without brackets and without name (add brackets)
else if (preg_match('/^'.$email_regexp.'$/', $item)) {
$item = rcube_utils::idn_to_ascii($item);
$result[] = $item;
}
// address with name (handle name)
else if (preg_match('/<*'.$email_regexp.'>*$/', $item, $matches)) {
$address = $matches[0];
$name = trim(str_replace($address, '', $item));
if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
$name = substr($name, 1, -1);
}
$name = stripcslashes($name);
$address = rcube_utils::idn_to_ascii(trim($address, '<>'));
$result[] = format_email_recipient($address, $name);
$item = $address;
}
// check address format
$item = trim($item, '<>');
if ($item && $check && !rcube_utils::check_email($item)) {
$this->parse_data['INVALID_EMAIL'] = $item;
return;
}
}
if ($count) {
$this->parse_data['RECIPIENT_COUNT'] += count($result);
}
return implode(', ', $result);
}
/**
* Returns configured generic message footer
*
* @param bool $isHtml Return HTML or Plain text version of the footer?
*
* @return string Footer content
*/
public function generic_message_footer($isHtml)
{
if ($isHtml && ($file = $this->rcmail->config->get('generic_message_footer_html'))) {
$html_footer = true;
}
else {
$file = $this->rcmail->config->get('generic_message_footer');
$html_footer = false;
}
if ($file && realpath($file)) {
// sanity check
if (!preg_match('/\.(php|ini|conf)$/', $file) && strpos($file, '/etc/') === false) {
$footer = file_get_contents($file);
if ($isHtml && !$html_footer) {
$t2h = new rcube_text2html($footer, false);
$footer = $t2h->get_html();
}
if ($this->options['charset'] && $this->options['charset'] != RCUBE_CHARSET) {
$footer = rcube_charset::convert($footer, RCUBE_CHARSET, $this->options['charset']);
}
return $footer;
}
}
return false;
}
/**
* Encode data array into a string for use in X-Draft-Info header
*
* @param array $data Data array
*
* @return string Decoded data as a string
*/
public static function draftinfo_encode($data)
{
$parts = array();
foreach ($data as $key => $val) {
$encode = $key == 'folder' || strpos($val, ';') !== false;
$parts[] = $key . '=' . ($encode ? 'B::' . base64_encode($val) : $val);
}
return join('; ', $parts);
}
/**
* Decode X-Draft-Info header value into an array
*
* @param string $str Encoded data string (see self::draftinfo_encode())
*
* @return array Decoded data
*/
public static function draftinfo_decode($str)
{
$info = array();
foreach (preg_split('/;\s+/', $str) as $part) {
list($key, $val) = explode('=', $part, 2);
if (strpos($val, 'B::') === 0) {
$val = base64_decode(substr($val, 3));
}
else if ($key == 'folder') {
$val = base64_decode($val);
}
$info[$key] = $val;
}
return $info;
}
/**
* Header (From, To, Cc, etc.) input object for templates
*/
public function headers_output($attrib)
{
list($form_start,) = $this->form_tags($attrib);
$out = '';
$part = strtolower($attrib['part']);
switch ($part) {
case 'from':
return $form_start . $this->compose_header_from($attrib);
case 'to':
case 'cc':
case 'bcc':
$fname = '_' . $part;
$header = $param = $part;
$allow_attrib = array('id', 'class', 'style', 'cols', 'rows', 'tabindex');
$field_type = 'html_textarea';
break;
case 'replyto':
case 'reply-to':
$fname = '_replyto';
$param = 'replyto';
$header = 'reply-to';
case 'followupto':
case 'followup-to':
if (!$fname) {
$fname = '_followupto';
$param = 'followupto';
$header = 'mail-followup-to';
}
$allow_attrib = array('id', 'class', 'style', 'size', 'tabindex');
$field_type = 'html_inputfield';
break;
}
if ($fname && $field_type) {
// pass the following attributes to the form class
$field_attrib = array('name' => $fname, 'spellcheck' => 'false');
foreach ($attrib as $attr => $value) {
if (stripos($attr, 'data-') === 0 || in_array($attr, $allow_attrib)) {
$field_attrib[$attr] = $value;
}
}
// create teaxtarea object
$input = new $field_type($field_attrib);
$out = $input->show($this->compose_header_value($param, $this->data['mode']));
}
if ($form_start) {
$out = $form_start . $out;
}
// configure autocompletion
$this->rcmail->autocomplete_init();
return $out;
}
/**
* Returns From header input element
*/
protected function compose_header_from($attrib)
{
// pass the following attributes to the form class
$field_attrib = array('name' => '_from');
foreach ($attrib as $attr => $value) {
if (in_array($attr, array('id', 'class', 'style', 'size', 'tabindex'))) {
$field_attrib[$attr] = $value;
}
}
if (!empty($this->options['message']->identities)) {
$a_signatures = array();
$identities = array();
$top_posting = intval($this->rcmail->config->get('reply_mode')) > 0
&& !$this->rcmail->config->get('sig_below')
&& ($this->data['mode'] == self::MODE_REPLY || $this->data['mode'] == self::MODE_FORWARD);
$separator = $top_posting ? '---' : '-- ';
$add_separator = (bool) $this->rcmail->config->get('sig_separator');
$field_attrib['onchange'] = rcmail_output::JS_OBJECT_NAME . ".change_identity(this)";
$select_from = new html_select($field_attrib);
// create SELECT element
foreach ($this->options['message']->identities as $sql_arr) {
$identity_id = $sql_arr['identity_id'];
$select_from->add(format_email_recipient($sql_arr['email'], $sql_arr['name']), $identity_id);
// add signature to array
if (!empty($sql_arr['signature']) && empty($this->data['param']['nosig'])) {
$text = $html = $sql_arr['signature'];
if ($sql_arr['html_signature']) {
$text = $this->rcmail->html2text($html, array('links' => false));
$text = trim($text, "\r\n");
}
else {
$t2h = new rcube_text2html($text, false);
$html = $t2h->get_html();
}
if ($add_separator && !preg_match('/^--[ -]\r?\n/m', $text)) {
$text = $separator . "\n" . ltrim($text, "\r\n");
$html = $separator . "<br>" . $html;
}
$a_signatures[$identity_id]['text'] = $text;
$a_signatures[$identity_id]['html'] = $html;
}
// add bcc and reply-to
if (!empty($sql_arr['reply-to'])) {
$identities[$identity_id]['replyto'] = $sql_arr['reply-to'];
}
if (!empty($sql_arr['bcc'])) {
$identities[$identity_id]['bcc'] = $sql_arr['bcc'];
}
$identities[$identity_id]['email'] = $sql_arr['email'];
}
$out = $select_from->show($this->options['message']->compose['from']);
// add signatures to client
$this->rcmail->output->set_env('signatures', $a_signatures);
$this->rcmail->output->set_env('identities', $identities);
}
// no identities, display text input field
else {
$field_attrib['class'] = 'from_address';
$input_from = new html_inputfield($field_attrib);
$out = $input_from->show($this->options['message']->compose['from']);
}
return $out;
}
/**
* Set the value of specified header depending on compose mode
*/
protected function compose_header_value($header, $mode)
{
$fvalue = '';
$decode_header = true;
$message = $this->options['message'];
$charset = $message->headers->charset;
$separator = ', ';
// we have a set of recipients stored is session
if ($header == 'to' && ($mailto_id = $this->data['param']['mailto'])
&& $_SESSION['mailto'][$mailto_id]
) {
$fvalue = urldecode($_SESSION['mailto'][$mailto_id]);
$decode_header = false;
$charset = $this->rcmail->output->charset;
// make session to not grow up too much
$this->rcmail->session->remove("mailto.$mailto_id");
}
else if (!empty($_POST['_' . $header])) {
$fvalue = rcube_utils::get_input_value('_' . $header, rcube_utils::INPUT_POST, true);
$charset = $this->rcmail->output->charset;
}
else if (!empty($this->data['param'][$header])) {
$fvalue = $this->data['param'][$header];
$charset = $this->rcmail->output->charset;
}
else if ($mode == self::MODE_REPLY) {
// get recipent address(es) out of the message headers
if ($header == 'to') {
$mailfollowup = $message->headers->others['mail-followup-to'];
$mailreplyto = $message->headers->others['mail-reply-to'];
// Reply to mailing list...
if ($message->reply_all == 'list' && $mailfollowup) {
$fvalue = $mailfollowup;
}
else if ($message->reply_all == 'list'
&& preg_match('/<mailto:([^>]+)>/i', $message->headers->others['list-post'], $m)
) {
$fvalue = $m[1];
}
// Reply to...
else if ($message->reply_all && $mailfollowup) {
$fvalue = $mailfollowup;
}
else if ($mailreplyto) {
$fvalue = $mailreplyto;
}
else if (!empty($message->headers->replyto)) {
$fvalue = $message->headers->replyto;
$replyto = true;
}
else if (!empty($message->headers->from)) {
$fvalue = $message->headers->from;
}
// Reply to message sent by yourself (#1487074, #1489230, #1490439)
// Reply-To address need to be unset (#1490233)
if (!empty($message->compose['ident']) && empty($replyto)) {
foreach (array($fvalue, $message->headers->from) as $sender) {
$senders = rcube_mime::decode_address_list($sender, null, false, $charset, true);
if (in_array($message->compose['ident']['email_ascii'], $senders)) {
$fvalue = $message->headers->to;
break;
}
}
}
}
// add recipient of original message if reply to all
else if ($header == 'cc' && !empty($message->reply_all) && $message->reply_all != 'list') {
if ($v = $message->headers->to) {
$fvalue .= $v;
}
if ($v = $message->headers->cc) {
$fvalue .= (!empty($fvalue) ? $separator : '') . $v;
}
// Deliberately ignore 'Sender' header (#6506)
// When To: and Reply-To: are the same we add From: address to the list (#1489037)
if ($v = $message->headers->from) {
$to = $message->headers->to;
$replyto = $message->headers->replyto;
$from = rcube_mime::decode_address_list($v, null, false, $charset, true);
$to = rcube_mime::decode_address_list($to, null, false, $charset, true);
$replyto = rcube_mime::decode_address_list($replyto, null, false, $charset, true);
if (!empty($replyto) && !count(array_diff($to, $replyto)) && count(array_diff($from, $to))) {
$fvalue .= (!empty($fvalue) ? $separator : '') . $v;
}
}
}
}
else if (in_array($mode, array(self::MODE_DRAFT, self::MODE_EDIT))) {
// get drafted headers
if ($header == 'to' && !empty($message->headers->to)) {
$fvalue = $message->get_header('to', true);
}
else if ($header == 'cc' && !empty($message->headers->cc)) {
$fvalue = $message->get_header('cc', true);
}
else if ($header == 'bcc' && !empty($message->headers->bcc)) {
$fvalue = $message->get_header('bcc', true);
}
else if ($header == 'replyto' && !empty($message->headers->others['mail-reply-to'])) {
$fvalue = $message->get_header('mail-reply-to');
}
else if ($header == 'replyto' && !empty($message->headers->replyto)) {
$fvalue = $message->get_header('reply-to');
}
else if ($header == 'followupto' && !empty($message->headers->others['mail-followup-to'])) {
$fvalue = $message->get_header('mail-followup-to');
}
}
// split recipients and put them back together in a unique way
if (!empty($fvalue) && in_array($header, array('to', 'cc', 'bcc'))) {
$from_email = @mb_strtolower($message->compose['ident']['email']);
$to_addresses = rcube_mime::decode_address_list($fvalue, null, $decode_header, $charset);
$fvalue = array();
foreach ($to_addresses as $addr_part) {
if (empty($addr_part['mailto'])) {
continue;
}
// According to RFC5321 local part of email address is case-sensitive
// however, here it is better to compare addresses in case-insensitive manner
$mailto = format_email(rcube_utils::idn_to_utf8($addr_part['mailto']));
$mailto_lc = mb_strtolower($addr_part['mailto']);
if (($header == 'to' || $mode != self::MODE_REPLY || $mailto_lc != $from_email)
&& !in_array($mailto_lc, (array) $message->recipients)
) {
if ($addr_part['name'] && $mailto != $addr_part['name']) {
$mailto = format_email_recipient($mailto, $addr_part['name']);
}
$fvalue[] = $mailto;
$message->recipients[] = $mailto_lc;
}
}
$fvalue = implode($separator, $fvalue);
}
return $fvalue;
}
/**
* Creates reply subject by removing common subject
* prefixes/suffixes from the original message subject
*
* @param string $subject Subject string
*
* @return string Modified subject string
*/
public static function reply_subject($subject)
{
$subject = trim($subject);
// replace Re:, Re[x]:, Re-x (#1490497)
$prefix = '/^(re:|re\[\d\]:|re-\d:)\s*/i';
do {
$subject = preg_replace($prefix, '', $subject, -1, $count);
}
while ($count);
// replace (was: ...) (#1489375)
$subject = preg_replace('/\s*\([wW]as:[^\)]+\)\s*$/', '', $subject);
return 'Re: ' . $subject;
}
/**
* Subject input object for templates
*/
public function compose_subject($attrib)
{
list($form_start, $form_end) = $this->form_tags($attrib);
unset($attrib['form']);
$attrib['name'] = '_subject';
$attrib['spellcheck'] = 'true';
$textfield = new html_inputfield($attrib);
$subject = '';
// use subject from post
if (isset($_POST['_subject'])) {
$subject = rcube_utils::get_input_value('_subject', rcube_utils::INPUT_POST, TRUE);
}
else if (!empty($this->data['param']['subject'])) {
$subject = $this->data['param']['subject'];
}
// create a reply-subject
else if ($this->data['mode'] == self::MODE_REPLY) {
$subject = self::reply_subject($this->options['message']->subject);
}
// create a forward-subject
else if ($this->data['mode'] == self::MODE_FORWARD) {
if (preg_match('/^fwd:/i', $this->options['message']->subject)) {
$subject = $this->options['message']->subject;
}
else {
$subject = 'Fwd: ' . $this->options['message']->subject;
}
}
// creeate a draft-subject
else if ($this->data['mode'] == self::MODE_DRAFT || $this->data['mode'] == self::MODE_EDIT) {
$subject = $this->options['message']->subject;
}
$out = $form_start ? "$form_start\n" : '';
$out .= $textfield->show($subject);
$out .= $form_end ? "\n$form_end" : '';
return $out;
}
/**
* Returns compose form tag (if not used already)
*/
public function form_tags($attrib)
{
if (rcube_utils::get_boolean((string) $attrib['noform'])) {
return array('', '');
}
$form_start = '';
if (!$this->message_form) {
$hiddenfields = new html_hiddenfield(array('name' => '_task', 'value' => $this->rcmail->task));
$hiddenfields->add(array('name' => '_action', 'value' => 'send'));
$hiddenfields->add(array('name' => '_id', 'value' => $this->data['id']));
$hiddenfields->add(array('name' => '_attachments'));
if (empty($attrib['form'])) {
$form_attr = array('name' => "form", 'method' => "post", 'class' => $attrib['class']);
$form_start = $this->rcmail->output->form_tag($form_attr);
}
$form_start .= $hiddenfields->show();
}
$form_end = ($this->message_form && !strlen($attrib['form'])) ? '</form>' : '';
$form_name = $attrib['form'] ?: 'form';
if (!$this->message_form) {
$this->rcmail->output->add_gui_object('messageform', $form_name);
}
$this->message_form = $form_name;
return array($form_start, $form_end);
}
/**
* Returns compose form "head"
*/
public function form_head($attrib)
{
list($form_start,) = $this->form_tags($attrib);
return $form_start;
}
/**
* Folder selector object for templates
*/
public function folder_selector($attrib)
{
$attrib['name'] = '_store_target';
$select = $this->rcmail->folder_selector(array_merge($attrib, array(
'noselection' => '- ' . $this->rcmail->gettext('dontsave') . ' -',
'folder_filter' => 'mail',
'folder_rights' => 'w',
)));
return $select->show(isset($_POST['_store_target']) ? $_POST['_store_target'] : $this->data['param']['sent_mbox'], $attrib);
}
/**
* Mail Disposition Notification checkbox object for templates
*/
public function mdn_checkbox($attrib)
{
list($form_start, $form_end) = $this->form_tags($attrib);
unset($attrib['form']);
if (!isset($attrib['id'])) {
$attrib['id'] = 'receipt';
}
$attrib['name'] = '_mdn';
$attrib['value'] = '1';
$checkbox = new html_checkbox($attrib);
if (isset($_POST['_mdn'])) {
$mdn_default = $_POST['_mdn'];
}
else if (in_array($this->data['mode'], array(self::MODE_DRAFT, self::MODE_EDIT))) {
$mdn_default = (bool) $this->options['message']->headers->mdn_to;
}
else {
$mdn_default = $this->rcmail->config->get('mdn_default');
}
$out = $form_start ? "$form_start\n" : '';
$out .= $checkbox->show($mdn_default);
$out .= $form_end ? "\n$form_end" : '';
return $out;
}
/**
* Delivery Status Notification checkbox object for templates
*/
public function dsn_checkbox($attrib)
{
list($form_start, $form_end) = $this->form_tags($attrib);
unset($attrib['form']);
if (!isset($attrib['id'])) {
$attrib['id'] = 'dsn';
}
$attrib['name'] = '_dsn';
$attrib['value'] = '1';
$checkbox = new html_checkbox($attrib);
if (isset($_POST['_dsn'])) {
$dsn_value = (int) $_POST['_dsn'];
}
else {
$dsn_value = $this->rcmail->config->get('dsn_default');
}
$out = $form_start ? "$form_start\n" : '';
$out .= $checkbox->show($dsn_value);
$out .= $form_end ? "\n$form_end" : '';
return $out;
}
/**
* Priority selector object for templates
*/
public function priority_selector($attrib)
{
list($form_start, $form_end) = $this->form_tags($attrib);
unset($attrib['form']);
$attrib['name'] = '_priority';
$prio_list = array(
$this->rcmail->gettext('lowest') => 5,
$this->rcmail->gettext('low') => 4,
$this->rcmail->gettext('normal') => 0,
$this->rcmail->gettext('high') => 2,
$this->rcmail->gettext('highest') => 1,
);
$selector = new html_select($attrib);
$selector->add(array_keys($prio_list), array_values($prio_list));
if (isset($_POST['_priority'])) {
$sel = (int) $_POST['_priority'];
}
else if (isset($this->options['message']->headers->priority)
&& intval($this->options['message']->headers->priority) != 3
) {
$sel = (int) $this->options['message']->headers->priority;
}
else {
$sel = 0;
}
$out = $form_start ? "$form_start\n" : '';
$out .= $selector->show((int) $sel);
$out .= $form_end ? "\n$form_end" : '';
return $out;
}
/**
* Helper to create Sent folder if not exists
*/
public static function check_sent_folder($folder, $create = false)
{
$rcmail = rcmail::get_instance();
// we'll not save the message, so it doesn't matter
if ($rcmail->config->get('no_save_sent_messages')) {
return true;
}
if ($rcmail->storage->folder_exists($folder, true)) {
return true;
}
// folder may exist but isn't subscribed (#1485241)
if ($create) {
if (!$rcmail->storage->folder_exists($folder))
return $rcmail->storage->create_folder($folder, true);
else
return $rcmail->storage->subscribe($folder);
}
return false;
}
/**
* Initialize mail compose UI elements
*/
protected function compose_init($message)
{
$message->compose = array();
// get user's identities
$message->identities = $this->rcmail->user->list_identities(null, true);
// Set From field value
if (!empty($_POST['_from'])) {
$message->compose['from'] = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST);
}
else if (!empty($this->data['param']['from'])) {
$message->compose['from'] = $this->data['param']['from'];
}
else if (!empty($message->identities)) {
$ident = self::identity_select($message, $message->identities, $this->data['mode']);
$message->compose['from'] = $ident['identity_id'];
$message->compose['ident'] = $ident;
}
$this->rcmail->output->add_handlers(array(
'storetarget' => array($this, 'folder_selector'),
'composeheaders' => array($this, 'headers_output'),
'composesubject' => array($this, 'compose_subject'),
'priorityselector' => array($this, 'priority_selector'),
'mdncheckbox' => array($this, 'mdn_checkbox'),
'dsncheckbox' => array($this, 'dsn_checkbox'),
'composeformhead' => array($this, 'form_head'),
));
// add some labels to client
$this->rcmail->output->add_label('nosubject', 'nosenderwarning', 'norecipientwarning',
'nosubjectwarning', 'cancel', 'nobodywarning', 'notsentwarning', 'savingmessage',
'sendingmessage', 'searching', 'disclosedrecipwarning', 'disclosedreciptitle',
'bccinstead', 'nosubjecttitle', 'sendmessage');
$this->rcmail->output->set_env('max_disclosed_recipients', (int) $this->rcmail->config->get('max_disclosed_recipients', 5));
}
/**
* Detect recipient identity from specified message
*
* @param rcube_message $message Message object
* @param array $identities User identities (if NULL all user identities will be used)
* @param string $mode Composing mode (see self::MODE_*)
*
* @return array Selected user identity (or the default identity) data
*/
public static function identity_select($message, $identities = null, $mode = null)
{
$a_recipients = array();
$a_names = array();
if ($identities === null) {
$identities = rcmail::get_instance()->user->list_identities(null, true);
}
if (!$mode) {
$mode = self::MODE_REPLY;
}
// extract all recipients of the reply-message
if (is_object($message->headers) && in_array($mode, array(self::MODE_REPLY, self::MODE_FORWARD))) {
$a_to = rcube_mime::decode_address_list($message->headers->to, null, true, $message->headers->charset);
foreach ($a_to as $addr) {
if (!empty($addr['mailto'])) {
$a_recipients[] = strtolower($addr['mailto']);
$a_names[] = $addr['name'];
}
}
if (!empty($message->headers->cc)) {
$a_cc = rcube_mime::decode_address_list($message->headers->cc, null, true, $message->headers->charset);
foreach ($a_cc as $addr) {
if (!empty($addr['mailto'])) {
$a_recipients[] = strtolower($addr['mailto']);
$a_names[] = $addr['name'];
}
}
}
}
// decode From: address
$from = rcube_mime::decode_address_list($message->headers->from, null, true, $message->headers->charset);
$from = array_shift($from);
$from['mailto'] = strtolower($from['mailto']);
$from_idx = null;
$found_idx = array('to' => null, 'from' => null);
$check_from = in_array($mode, array(self::MODE_DRAFT, self::MODE_EDIT, self::MODE_REPLY));
// Select identity
foreach ($identities as $idx => $ident) {
// use From: header when in edit/draft or reply-to-self
if ($check_from && $from['mailto'] == strtolower($ident['email_ascii'])) {
// remember first matching identity address
if ($found_idx['from'] === null) {
$found_idx['from'] = $idx;
}
// match identity name
if ($from['name'] && $ident['name'] && $from['name'] == $ident['name']) {
$from_idx = $idx;
break;
}
}
// use replied/forwarded message recipients
else if (($found = array_search(strtolower($ident['email_ascii']), $a_recipients)) !== false) {
// remember first matching identity address
if ($found_idx['to'] === null) {
$found_idx['to'] = $idx;
}
// match identity name
if ($a_names[$found] && $ident['name'] && $a_names[$found] == $ident['name']) {
$from_idx = $idx;
break;
}
}
}
// If matching by name+address didn't find any matches,
// get first found identity (address) if any
if ($from_idx === null) {
$from_idx = $found_idx['from'] !== null ? $found_idx['from'] : $found_idx['to'];
}
// Try Return-Path
if ($from_idx === null && ($return_path = $message->headers->others['return-path'])) {
$return_path = array_map('strtolower', (array) $return_path);
foreach ($identities as $idx => $ident) {
// Return-Path header contains an email address, but on some mailing list
// it can be e.g. <pear-dev-return-55250-local=domain.tld@lists.php.net>
// where local@domain.tld is the address we're looking for (#1489241)
$ident1 = strtolower($ident['email_ascii']);
$ident2 = str_replace('@', '=', $ident1);
$ident1 = '<' . $ident1 . '>';
$ident2 = '-' . $ident2 . '@';
foreach ($return_path as $path) {
if ($path == $ident1 || stripos($path, $ident2)) {
$from_idx = $idx;
break 2;
}
}
}
}
// See identity_select plugin for example usage of this hook
$plugin = rcmail::get_instance()->plugins->exec_hook('identity_select', array(
'message' => $message,
'identities' => $identities,
'selected' => $from_idx
));
$selected = $plugin['selected'];
// default identity is always first on the list
return $identities[$selected !== null ? $selected : 0];
}
}
diff --git a/program/include/rcmail_string_replacer.php b/program/include/rcmail_string_replacer.php
index c3b979719..4eae2ac45 100644
--- a/program/include/rcmail_string_replacer.php
+++ b/program/include/rcmail_string_replacer.php
@@ -1,64 +1,62 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/include/rcmail_string_replacer.php |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2012-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Turn URLs and email addresses into clickable links |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Helper class for turning URLs and email addresses in plaintext content
* into clickable links.
*
* @package Webmail
* @subpackage Utils
*/
class rcmail_string_replacer extends rcube_string_replacer
{
/**
* Callback function used to build mailto: links around e-mail strings
*
* This also adds an onclick-handler to open the Rouncube compose message screen on such links
*
* @param array $matches Matches result from preg_replace_callback
*
* @return int Index of saved string value
* @see rcube_string_replacer::mailto_callback()
*/
public function mailto_callback($matches)
{
$href = $matches[1];
$suffix = $this->parse_url_brackets($href);
$email = $href;
if (strpos($email, '?')) {
list($email,) = explode('?', $email);
}
// skip invalid emails
if (!rcube_utils::check_email($email, false)) {
return $matches[1];
}
$i = $this->add(html::a(array(
'href' => 'mailto:' . $href,
'onclick' => "return ".rcmail_output::JS_OBJECT_NAME.".command('compose','".rcube::JQ($href)."',this)",
),
rcube::Q($href)) . $suffix);
return $i >= 0 ? $this->get_replacement($i) : '';
}
}
diff --git a/program/include/rcmail_utils.php b/program/include/rcmail_utils.php
index 7c4170367..e15338686 100644
--- a/program/include/rcmail_utils.php
+++ b/program/include/rcmail_utils.php
@@ -1,375 +1,373 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/include/rcmail_utils.php |
+ | This file is part of the Roundcube Webmail client |
| |
- | This file is part of the Roundcube PHP suite |
- | Copyright (C) 2005-2015 The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| CONTENTS: |
| Roundcube utilities |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Roundcube utilities
*
* @package Webmail
* @subpackage Utils
*/
class rcmail_utils
{
public static $db;
/**
* Initialize database object and connect
*
* @return rcube_db Database instance
*/
public static function db()
{
if (self::$db === null) {
$rc = rcube::get_instance();
$db = rcube_db::factory($rc->config->get('db_dsnw'));
$db->set_debug((bool)$rc->config->get('sql_debug'));
// Connect to database
$db->db_connect('w');
if (!$db->is_connected()) {
rcube::raise_error("Error connecting to database: " . $db->is_error(), false, true);
}
self::$db = $db;
}
return self::$db;
}
/**
* Initialize database schema
*
* @param string $dir Directory with sql files
*/
public static function db_init($dir)
{
$db = self::db();
$file = $dir . '/' . $db->db_provider . '.initial.sql';
if (!file_exists($file)) {
rcube::raise_error("DDL file $file not found", false, true);
}
echo "Creating database schema... ";
if ($sql = file_get_contents($file)) {
if (!$db->exec_script($sql)) {
$error = $db->is_error();
}
}
else {
$error = "Unable to read file $file or it is empty";
}
if ($error) {
echo "[FAILED]\n";
rcube::raise_error($error, false, true);
}
else {
echo "[OK]\n";
}
}
/**
* Update database schema
*
* @param string $dir Directory with sql files
* @param string $package Component name
* @param string $ver Optional current version number
* @param array $opts Parameters (errors, quiet)
*
* @return True on success, False on failure
*/
public static function db_update($dir, $package, $ver = null, $opts = array())
{
// Check if directory exists
if (!file_exists($dir)) {
if ($opts['errors']) {
rcube::raise_error("Specified database schema directory doesn't exist.", false, true);
}
return false;
}
$db = self::db();
// Read DB schema version from database (if 'system' table exists)
if (in_array($db->table_name('system'), (array)$db->list_tables())) {
$db->query("SELECT `value`"
. " FROM " . $db->table_name('system', true)
. " WHERE `name` = ?",
$package . '-version');
$row = $db->fetch_array();
$version = preg_replace('/[^0-9]/', '', $row[0]);
}
// DB version not found, but release version is specified
if (!$version && $ver) {
// Map old release version string to DB schema version
// Note: This is for backward compat. only, do not need to be updated
$map = array(
'0.1-stable' => 1,
'0.1.1' => 2008030300,
'0.2-alpha' => 2008040500,
'0.2-beta' => 2008060900,
'0.2-stable' => 2008092100,
'0.2.1' => 2008092100,
'0.2.2' => 2008092100,
'0.3-stable' => 2008092100,
'0.3.1' => 2009090400,
'0.4-beta' => 2009103100,
'0.4' => 2010042300,
'0.4.1' => 2010042300,
'0.4.2' => 2010042300,
'0.5-beta' => 2010100600,
'0.5' => 2010100600,
'0.5.1' => 2010100600,
'0.5.2' => 2010100600,
'0.5.3' => 2010100600,
'0.5.4' => 2010100600,
'0.6-beta' => 2011011200,
'0.6' => 2011011200,
'0.7-beta' => 2011092800,
'0.7' => 2011111600,
'0.7.1' => 2011111600,
'0.7.2' => 2011111600,
'0.7.3' => 2011111600,
'0.7.4' => 2011111600,
'0.8-beta' => 2011121400,
'0.8-rc' => 2011121400,
'0.8.0' => 2011121400,
'0.8.1' => 2011121400,
'0.8.2' => 2011121400,
'0.8.3' => 2011121400,
'0.8.4' => 2011121400,
'0.8.5' => 2011121400,
'0.8.6' => 2011121400,
'0.9-beta' => 2012080700,
);
$version = $map[$ver];
}
// Assume last version before the 'system' table was added
if (empty($version)) {
$version = 2012080700;
}
$dir .= '/' . $db->db_provider;
if (!file_exists($dir)) {
if ($opts['errors']) {
rcube::raise_error("DDL Upgrade files for " . $db->db_provider . " driver not found.", false, true);
}
return false;
}
$dh = opendir($dir);
$result = array();
while ($file = readdir($dh)) {
if (preg_match('/^([0-9]+)\.sql$/', $file, $m) && $m[1] > $version) {
$result[] = $m[1];
}
}
sort($result, SORT_NUMERIC);
foreach ($result as $v) {
if (!$opts['quiet']) {
echo "Updating database schema ($v)... ";
}
// Ignore errors here to print the error only once
$db->set_option('ignore_errors', true);
$error = self::db_update_schema($package, $v, "$dir/$v.sql");
$db->set_option('ignore_errors', false);
if ($error) {
if (!$opts['quiet']) {
echo "[FAILED]\n";
}
if ($opts['errors']) {
rcube::raise_error("Error in DDL upgrade $v: $error", false, true);
}
return false;
}
else if (!$opts['quiet']) {
echo "[OK]\n";
}
}
return true;
}
/**
* Run database update from a single sql file
*/
protected static function db_update_schema($package, $version, $file)
{
$db = self::db();
// read DDL file
if ($sql = file_get_contents($file)) {
if (!$db->exec_script($sql)) {
return $db->is_error();
}
}
// escape if 'system' table does not exist
if ($version < 2013011000) {
return;
}
$system_table = $db->table_name('system', true);
$db->query("UPDATE " . $system_table
. " SET `value` = ?"
. " WHERE `name` = ?",
$version, $package . '-version');
if (!$db->is_error() && !$db->affected_rows()) {
$db->query("INSERT INTO " . $system_table
." (`name`, `value`) VALUES (?, ?)",
$package . '-version', $version);
}
return $db->is_error();
}
/**
* Removes all deleted records older than X days
*
* @param int $days Number of days
*/
public static function db_clean($days)
{
// mapping for table name => primary key
$primary_keys = array(
'contacts' => 'contact_id',
'contactgroups' => 'contactgroup_id',
);
$db = self::db();
$threshold = date('Y-m-d 00:00:00', time() - $days * 86400);
foreach (array('contacts','contactgroups','identities') as $table) {
$sqltable = $db->table_name($table, true);
// also delete linked records
// could be skipped for databases which respect foreign key constraints
if ($db->db_provider == 'sqlite' && ($table == 'contacts' || $table == 'contactgroups')) {
$pk = $primary_keys[$table];
$memberstable = $db->table_name('contactgroupmembers');
$db->query(
"DELETE FROM " . $db->quote_identifier($memberstable)
. " WHERE `$pk` IN ("
. "SELECT `$pk` FROM $sqltable"
. " WHERE `del` = 1 AND `changed` < ?"
. ")",
$threshold);
echo $db->affected_rows() . " records deleted from '$memberstable'\n";
}
// delete outdated records
$db->query("DELETE FROM $sqltable WHERE `del` = 1 AND `changed` < ?", $threshold);
echo $db->affected_rows() . " records deleted from '$table'\n";
}
}
/**
* Reindex contacts
*/
public static function indexcontacts()
{
$db = self::db();
// iterate over all users
$sql_result = $db->query("SELECT `user_id` FROM " . $db->table_name('users', true) . " ORDER BY `user_id`");
while ($sql_result && ($sql_arr = $db->fetch_assoc($sql_result))) {
echo "Indexing contacts for user " . $sql_arr['user_id'] . "...\n";
$contacts = new rcube_contacts($db, $sql_arr['user_id']);
$contacts->set_pagesize(9999);
$result = $contacts->list_records();
while ($result->count && ($row = $result->next())) {
unset($row['words']);
$contacts->update($row['ID'], $row);
}
}
echo "done.\n";
}
/**
* Modify user preferences
*
* @param string $name Option name
* @param string $value Option value
* @param int $userid Optional user identifier
* @param string $type Optional value type (bool, int, string)
*/
public static function mod_pref($name, $value, $userid = null, $type = 'string')
{
$db = self::db();
if ($userid) {
$query = '`user_id` = ' . intval($userid);
}
else {
$query = '1=1';
}
$type = strtolower($type);
if ($type == 'bool' || $type == 'boolean') {
$value = rcube_utils::get_boolean($value);
}
else if ($type == 'int' || $type == 'integer') {
$value = (int) $value;
}
// iterate over all users
$sql_result = $db->query("SELECT * FROM " . $db->table_name('users', true) . " WHERE $query");
while ($sql_result && ($sql_arr = $db->fetch_assoc($sql_result))) {
echo "Updating prefs for user " . $sql_arr['user_id'] . "...";
$user = new rcube_user($sql_arr['user_id'], $sql_arr);
$prefs = $old_prefs = $user->get_prefs();
$prefs[$name] = $value;
if ($prefs != $old_prefs) {
$user->save_prefs($prefs, true);
echo "saved.\n";
}
else {
echo "nothing changed.\n";
}
}
}
}
diff --git a/program/js/app.js b/program/js/app.js
index 7c12fe461..add7c1698 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -1,10083 +1,10083 @@
/**
* Roundcube Webmail Client Script
*
* This file is part of the Roundcube Webmail client
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
- * Copyright (C) 2005-2015, The Roundcube Dev Team
- * Copyright (C) 2011-2015, Kolab Systems AG
+ * Copyright (C) The Roundcube Dev Team
+ * Copyright (C) Kolab Systems AG
*
* The JavaScript code in this page is free software: you can
* redistribute it and/or modify it under the terms of the GNU
* General Public License (GNU GPL) as published by the Free Software
* Foundation, either version 3 of the License, or (at your option)
* any later version. The code is distributed WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
*
* As additional permission under GNU GPL version 3 section 7, you
* may distribute non-source (e.g., minimized or compacted) forms of
* that code without the copy of the GNU GPL normally required by
* section 4, provided you include this license notice and a URL
* through which recipients can access the Corresponding Source.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander 'A.L.E.C' Machniak <alec@alec.pl>
* @author Charles McNulty <charles@charlesmcnulty.com>
*
* @requires jquery.js, common.js, list.js
*/
function rcube_webmail()
{
this.labels = {};
this.buttons = {};
this.buttons_sel = {};
this.gui_objects = {};
this.gui_containers = {};
this.commands = {};
this.command_handlers = {};
this.onloads = [];
this.messages = {};
this.group2expand = {};
this.http_request_jobs = {};
this.menu_stack = [];
this.menu_buttons = {};
this.entity_selectors = [];
this.image_style = {};
this.uploads = {};
// webmail client settings
this.dblclick_time = 500;
this.message_time = 5000;
this.preview_delay_select = 400;
this.preview_delay_click = 60;
this.identifier_expr = /[^0-9a-z_-]/gi;
// environment defaults
this.env = {
request_timeout: 180, // seconds
draft_autosave: 0, // seconds
comm_path: './',
recipients_separator: ',', // @deprecated
recipients_delimiter: ', ', // @deprecated
popup_width: 1150,
popup_width_small: 900,
thread_padding: '15px'
};
// create protected reference to myself
this.ref = 'rcmail';
var ref = this;
// set jQuery ajax options
$.ajaxSetup({
cache: false,
timeout: this.env.request_timeout * 1000,
error: function(request, status, err){ ref.http_error(request, status, err); },
beforeSend: function(xmlhttp){ xmlhttp.setRequestHeader('X-Roundcube-Request', ref.env.request_token); }
});
// unload fix
$(window).on('beforeunload', function() { ref.unload = true; });
// set environment variable(s)
this.set_env = function(p, value)
{
if (p != null && typeof p === 'object' && !value)
for (var n in p)
this.env[n] = p[n];
else
this.env[p] = value;
};
// add a localized label to the client environment
this.add_label = function(p, value)
{
if (typeof p == 'string')
this.labels[p] = value;
else if (typeof p == 'object')
$.extend(this.labels, p);
};
// add a button to the button list
this.register_button = function(command, id, type, act, sel, over)
{
var button_prop = {id:id, type:type};
if (act) button_prop.act = act;
if (sel) button_prop.sel = sel;
if (over) button_prop.over = over;
if (!this.buttons[command])
this.buttons[command] = [];
this.buttons[command].push(button_prop);
if (this.loaded) {
this.init_button(command, button_prop);
this.set_button(command, (this.commands[command] ? 'act' : 'pas'));
}
};
// register a button with popup menu, to set its state according to the state of all commands in the menu
this.register_menu_button = function(button, menu_id)
{
if (this.menu_buttons[menu_id]) {
this.menu_buttons[menu_id][0].push(button);
}
else {
var commands = [];
$('#' + menu_id).find('a').each(function() {
var command, link = $(this), onclick = link.attr('onclick');
if (onclick && String(onclick).match(/rcmail\.command\(\'([^']+)/))
command = RegExp.$1;
else
command = function() { return link.is('.active'); };
commands.push(command);
});
if (commands.length)
this.menu_buttons[menu_id] = [[button], commands];
}
this.set_menu_buttons();
};
// set state of a menu button according to state of all menu actions
this.set_menu_buttons = function()
{
// Use timeouts to not block and set menu button states only once
clearTimeout(this.menu_buttons_timeout);
this.menu_buttons_timeout = setTimeout(function() {
$.each(ref.menu_buttons, function() {
var disabled = true;
$.each(this[1], function() {
var is_func = typeof(this) == 'function';
if ((is_func && this()) || (!is_func && ref.commands[this])) {
return disabled = false;
}
});
$(this[0]).add($(this[0]).parent('.dropbutton'))
.addClass(disabled ? 'disabled' : 'active')
.removeClass(disabled ? 'active' : 'disabled');
});
}, 50);
};
// register a specific gui object
this.gui_object = function(name, id)
{
this.gui_objects[name] = this.loaded ? rcube_find_object(id) : id;
};
// register a container object
this.gui_container = function(name, id)
{
this.gui_containers[name] = id;
};
// add a GUI element (html node) to a specified container
this.add_element = function(elm, container)
{
if (this.gui_containers[container] && this.gui_containers[container].jquery)
this.gui_containers[container].append(elm);
};
// register an external handler for a certain command
this.register_command = function(command, callback, enable)
{
this.command_handlers[command] = callback;
if (enable)
this.enable_command(command, true);
};
// execute the given script on load
this.add_onload = function(f)
{
this.onloads.push(f);
};
// initialize webmail client
this.init = function()
{
var n;
this.task = this.env.task;
// check browser capabilities (never use version checks here)
if (this.env.server_error != 409 && (!bw.dom || !bw.xmlhttp_test())) {
this.goto_url('error', '_code=0x199');
return;
}
if (!this.env.blankpage)
this.env.blankpage = 'about:blank';
// find all registered gui containers
for (n in this.gui_containers)
this.gui_containers[n] = $('#'+this.gui_containers[n]);
// find all registered gui objects
for (n in this.gui_objects)
this.gui_objects[n] = rcube_find_object(this.gui_objects[n]);
// init registered buttons
this.init_buttons();
// tell parent window that this frame is loaded
if (this.is_framed()) {
parent.rcmail.set_busy(false, null, parent.rcmail.env.frame_lock);
parent.rcmail.env.frame_lock = null;
}
// enable general commands
this.enable_command('close', 'logout', 'mail', 'addressbook', 'settings', 'save-pref',
'compose', 'undo', 'about', 'switch-task', 'menu-open', 'menu-close', 'menu-save', true);
// set active task button
this.set_button(this.task, 'sel');
if (this.env.permaurl)
this.enable_command('permaurl', 'extwin', true);
switch (this.task) {
case 'mail':
// enable mail commands
this.enable_command('list', 'checkmail', 'add-contact', 'search', 'reset-search', 'collapse-folder', 'import-messages', true);
if (this.gui_objects.messagelist) {
this.env.widescreen_list_template = [
{className: 'threads', cells: ['threads']},
{className: 'subject', cells: ['fromto', 'date', 'status', 'subject']},
{className: 'flags', cells: ['flag', 'attachment']}
];
this.message_list = new rcube_list_widget(this.gui_objects.messagelist, {
multiselect:true, multiexpand:true, draggable:true, keyboard:true,
column_movable:this.env.col_movable, dblclick_time:this.dblclick_time
});
this.message_list
.addEventListener('initrow', function(o) { ref.init_message_row(o); })
.addEventListener('dblclick', function(o) { ref.msglist_dbl_click(o); })
.addEventListener('keypress', function(o) { ref.msglist_keypress(o); })
.addEventListener('select', function(o) { ref.msglist_select(o); })
.addEventListener('dragstart', function(o) { ref.drag_start(o); })
.addEventListener('dragmove', function(e) { ref.drag_move(e); })
.addEventListener('dragend', function(e) { ref.drag_end(e); })
.addEventListener('expandcollapse', function(o) { ref.msglist_expand(o); })
.addEventListener('column_replace', function(o) { ref.msglist_set_coltypes(o); })
.init();
// TODO: this should go into the list-widget code
$(this.message_list.thead).on('click', 'a.sortcol', function(e){
return ref.command('sort', $(this).attr('rel'), this);
});
this.enable_command('toggle_status', 'toggle_flag', 'sort', true);
this.enable_command('set-listmode', this.env.threads && !this.is_multifolder_listing());
// load messages
var searchfilter = $(this.gui_objects.search_filter).val();
if (searchfilter && searchfilter != 'ALL')
this.filter_mailbox(searchfilter);
else
this.command('list');
$(this.gui_objects.qsearchbox).val(this.env.search_text).focusin(function() { ref.message_list.blur(); });
}
this.set_button_titles();
this.env.message_commands = ['show', 'reply', 'reply-all', 'reply-list',
'move', 'copy', 'delete', 'open', 'mark', 'edit', 'viewsource', 'bounce',
'print', 'load-attachment', 'download-attachment', 'show-headers', 'hide-headers', 'download',
'forward', 'forward-inline', 'forward-attachment', 'change-format'];
if (this.env.action == 'show' || this.env.action == 'preview') {
this.enable_command(this.env.message_commands, this.env.uid);
this.enable_command('reply-list', this.env.list_post);
if (this.env.action == 'show') {
this.http_request('pagenav', {_uid: this.env.uid, _mbox: this.env.mailbox, _search: this.env.search_request},
this.display_message('', 'loading'));
}
if (this.env.mail_read_time > 0)
setTimeout(function() {
ref.http_post('mark', {_uid: ref.env.uid, _flag: 'read', _mbox: ref.env.mailbox, _quiet: 1});
}, this.env.mail_read_time * 1000);
if (this.env.blockedobjects) {
$(this.gui_objects.remoteobjectsmsg).show();
this.enable_command('load-remote', true);
}
// make preview/message frame visible
if (this.env.action == 'preview' && this.is_framed()) {
this.enable_command('compose', 'add-contact', false);
parent.rcmail.show_contentframe(true);
}
if ($.inArray('flagged', this.env.message_flags) >= 0) {
$(document.body).addClass('status-flagged');
}
// initialize drag-n-drop on attachments, so they can e.g.
// be dropped into mail compose attachments in another window
if (this.gui_objects.attachments)
$('li > a', this.gui_objects.attachments).not('.drop').on('dragstart', function(e) {
var n, href = this.href, dt = e.originalEvent.dataTransfer;
if (dt) {
// inject username to the uri
href = href.replace(/^https?:\/\//, function(m) { return m + urlencode(ref.env.username) + '@'});
// cleanup the node to get filename without the size test
n = $(this).clone();
n.children().remove();
dt.setData('roundcube-uri', href);
dt.setData('roundcube-name', $.trim(n.text()));
}
});
}
else if (this.env.action == 'compose') {
this.env.address_group_stack = [];
this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel',
'toggle-editor', 'list-addresses', 'pushgroup', 'search', 'reset-search', 'extwin',
'insert-response', 'save-response', 'menu-open', 'menu-close', 'load-attachment',
'download-attachment', 'open-attachment', 'rename-attachment'];
if (this.env.drafts_mailbox)
this.env.compose_commands.push('savedraft')
this.enable_command(this.env.compose_commands, true);
// add more commands (not enabled)
$.merge(this.env.compose_commands, ['add-recipient', 'firstpage', 'previouspage', 'nextpage', 'lastpage']);
if (window.googie) {
this.env.editor_config.spellchecker = googie;
this.env.editor_config.spellcheck_observer = function(s) { ref.spellcheck_state(); };
this.env.compose_commands.push('spellcheck')
this.enable_command('spellcheck', true);
}
// initialize HTML editor
this.editor_init(this.env.editor_config, this.env.composebody);
// init canned response functions
if (this.gui_objects.responseslist) {
$('a.insertresponse', this.gui_objects.responseslist)
.attr('unselectable', 'on')
.mousedown(function(e) { return rcube_event.cancel(e); })
.on('mouseup keypress', function(e) {
if (e.type == 'mouseup' || rcube_event.get_keycode(e) == 13) {
ref.command('insert-response', $(this).attr('rel'));
$(document.body).trigger('mouseup'); // hides the menu
return rcube_event.cancel(e);
}
});
// avoid textarea loosing focus when hitting the save-response button/link
$.each(this.buttons['save-response'] || [], function (i, v) {
$('#' + v.id).mousedown(function(e){ return rcube_event.cancel(e); })
});
}
// init message compose form
this.init_messageform();
}
else if (this.env.action == 'bounce') {
this.init_messageform_inputs();
this.env.compose_commands = [];
}
else if (this.env.action == 'get') {
this.enable_command('download', true);
this.enable_command('image-scale', 'image-rotate', !!/^image\//.test(this.env.mimetype));
// Mozilla's PDF.js viewer does not allow printing from host page (#5125)
// to minimize user confusion we disable the Print button
if (bw.mz && this.env.mimetype == 'application/pdf') {
n = 0; // there will be two onload events, first for the preload page
$(this.gui_objects.messagepartframe).on('load', function() {
if (n++) try { if (this.contentWindow.document) ref.enable_command('print', true); }
catch (e) {/* ignore */}
});
}
else
this.enable_command('print', true);
if (this.env.is_message) {
this.enable_command('reply', 'reply-all', 'edit', 'viewsource',
'forward', 'forward-inline', 'forward-attachment', 'bounce', true);
if (this.env.list_post)
this.enable_command('reply-list', true);
}
// center and scale the image in preview frame
if (this.env.mimetype.startsWith('image/'))
$(this.gui_objects.messagepartframe).on('load', function() {
var css = 'img { max-width:100%; max-height:100%; } ' // scale
+ 'body { display:flex; align-items:center; justify-content:center; height:100%; margin:0; }'; // align
$(this).contents().find('head').append('<style type="text/css">'+ css + '</style>');
});
}
// show printing dialog
else if (this.env.action == 'print' && this.env.uid
&& !this.env.is_pgp_content && !this.env.pgp_mime_part
) {
this.print_dialog();
}
// get unread count for each mailbox
if (this.gui_objects.mailboxlist) {
this.env.unread_counts = {};
this.gui_objects.folderlist = this.gui_objects.mailboxlist;
this.http_request('getunread', {_page: this.env.current_page});
}
// init address book widget
if (this.gui_objects.contactslist) {
this.contact_list = new rcube_list_widget(this.gui_objects.contactslist,
{ multiselect:true, draggable:false, keyboard:true });
this.contact_list
.addEventListener('initrow', function(o) { ref.triggerEvent('insertrow', { cid:o.uid, row:o }); })
.addEventListener('select', function(o) { ref.compose_recipient_select(o); })
.addEventListener('dblclick', function(o) { ref.compose_add_recipient(); })
.addEventListener('keypress', function(o) {
if (o.key_pressed == o.ENTER_KEY) {
if (!ref.compose_add_recipient()) {
// execute link action on <enter> if not a recipient entry
if (o.last_selected && String(o.last_selected).charAt(0) == 'G') {
$(o.rows[o.last_selected].obj).find('a').first().click();
}
}
}
})
.init();
// remember last focused address field
$('#_to,#_cc,#_bcc').focus(function() { ref.env.focused_field = this; });
}
if (this.gui_objects.addressbookslist) {
this.gui_objects.folderlist = this.gui_objects.addressbookslist;
this.enable_command('list-addresses', true);
}
// ask user to send MDN
if (this.env.mdn_request && this.env.uid) {
var postact = 'sendmdn',
postdata = {_uid: this.env.uid, _mbox: this.env.mailbox};
if (!confirm(this.get_label('mdnrequest'))) {
postdata._flag = 'mdnsent';
postact = 'mark';
}
this.http_post(postact, postdata);
}
this.check_mailvelope(this.env.action);
// detect browser capabilities
if (!this.is_framed() && !this.env.extwin)
this.browser_capabilities_check();
break;
case 'addressbook':
this.env.address_group_stack = [];
if (this.gui_objects.folderlist)
this.env.contactfolders = $.extend($.extend({}, this.env.address_sources), this.env.contactgroups);
this.enable_command('add', 'import', this.env.writable_source);
this.enable_command('list', 'listgroup', 'pushgroup', 'popgroup', 'listsearch', 'search', 'reset-search', 'advanced-search', true);
if (this.gui_objects.contactslist) {
this.contact_list = new rcube_list_widget(this.gui_objects.contactslist,
{multiselect:true, draggable:this.gui_objects.folderlist?true:false, keyboard:true});
this.contact_list
.addEventListener('initrow', function(o) { ref.triggerEvent('insertrow', { cid:o.uid, row:o }); })
.addEventListener('keypress', function(o) { ref.contactlist_keypress(o); })
.addEventListener('select', function(o) { ref.contactlist_select(o); })
.addEventListener('dragstart', function(o) { ref.drag_start(o); })
.addEventListener('dragmove', function(e) { ref.drag_move(e); })
.addEventListener('dragend', function(e) { ref.drag_end(e); })
.init();
$(this.gui_objects.qsearchbox).focusin(function() { ref.contact_list.blur(); });
this.update_group_commands();
this.command('list');
}
if (this.gui_objects.savedsearchlist) {
this.savedsearchlist = new rcube_treelist_widget(this.gui_objects.savedsearchlist, {
id_prefix: 'rcmli',
id_encode: this.html_identifier_encode,
id_decode: this.html_identifier_decode
});
this.savedsearchlist.addEventListener('select', function(node) {
ref.triggerEvent('selectfolder', { folder:node.id, prefix:'rcmli' }); });
}
this.set_page_buttons();
if (this.env.cid) {
this.enable_command('show', 'edit', 'qrcode', true);
// register handlers for group assignment via checkboxes
if (this.gui_objects.editform) {
$('input.groupmember').change(function() {
ref.group_member_change(this.checked ? 'add' : 'del', ref.env.cid, ref.env.source, this.value);
});
}
}
if (this.gui_objects.editform) {
this.enable_command('save', true);
if (this.env.action == 'add' || this.env.action == 'edit' || this.env.action == 'search')
this.init_contact_form();
}
else if (this.env.action == 'print') {
this.print_dialog();
}
break;
case 'settings':
this.enable_command('show', 'save', true);
if (this.env.action == 'identities') {
this.enable_command('add', this.env.identities_level < 2);
}
else if (this.env.action == 'edit-identity' || this.env.action == 'add-identity') {
this.enable_command('save', 'edit', 'toggle-editor', true);
this.enable_command('delete', this.env.identities_level < 2);
// initialize HTML editor
this.editor_init(this.env.editor_config, 'rcmfd_signature');
this.check_mailvelope(this.env.action);
}
else if (this.env.action == 'folders') {
this.enable_command('subscribe', 'unsubscribe', 'create-folder', 'rename-folder', true);
}
else if (this.env.action == 'edit-folder' && this.gui_objects.editform) {
this.enable_command('save', 'folder-size', true);
parent.rcmail.env.exists = this.env.messagecount;
parent.rcmail.enable_command('purge', this.env.messagecount);
}
else if (this.env.action == 'responses') {
this.enable_command('add', true);
}
if (this.gui_objects.identitieslist) {
this.identity_list = new rcube_list_widget(this.gui_objects.identitieslist,
{multiselect:false, draggable:false, keyboard:true});
this.identity_list
.addEventListener('select', function(o) { ref.identity_select(o); })
.addEventListener('keypress', function(o) {
if (o.key_pressed == o.ENTER_KEY) {
ref.identity_select(o);
}
})
.init()
.focus();
}
else if (this.gui_objects.sectionslist) {
this.sections_list = new rcube_list_widget(this.gui_objects.sectionslist, {multiselect:false, draggable:false, keyboard:true});
this.sections_list
.addEventListener('select', function(o) { ref.section_select(o); })
.addEventListener('keypress', function(o) { if (o.key_pressed == o.ENTER_KEY) ref.section_select(o); })
.init()
.focus();
}
else if (this.gui_objects.subscriptionlist) {
this.init_subscription_list();
}
else if (this.gui_objects.responseslist) {
this.responses_list = new rcube_list_widget(this.gui_objects.responseslist, {multiselect:false, draggable:false, keyboard:true});
this.responses_list
.addEventListener('select', function(o) { ref.response_select(o); })
.init()
.focus();
}
break;
case 'login':
var tz, tz_name,
input_user = $('#rcmloginuser'),
input_tz = $('#rcmlogintz');
input_user.keyup(function(e) { return ref.login_user_keyup(e); });
if (input_user.val() == '')
input_user.focus();
else
$('#rcmloginpwd').focus();
// detect client timezone
if (window.jstz && (tz = jstz.determine()))
tz_name = tz.name();
input_tz.val(tz_name ? tz_name : (new Date().getStdTimezoneOffset() / -60));
// display 'loading' message on form submit, lock submit button
$('form').submit(function () {
$('[type=submit]', this).prop('disabled', true);
ref.clear_messages();
ref.display_message('', 'loading');
});
break;
}
// select first input field in an edit form
if (this.gui_objects.editform)
$("input,select,textarea", this.gui_objects.editform)
.not(':hidden').not(':disabled').first().select().focus();
// prevent from form submit with Enter key in file input fields
if (bw.ie)
$('input[type=file]').keydown(function(e) { if (e.keyCode == '13') e.preventDefault(); });
// flag object as complete
this.loaded = true;
this.env.lastrefresh = new Date();
// show message
if (this.pending_message)
this.display_message.apply(this, this.pending_message);
// init treelist widget
if (this.gui_objects.folderlist && window.rcube_treelist_widget
// some plugins may load rcube_treelist_widget and there's one case
// when this will cause problems - addressbook widget in compose,
// which already has been initialized using rcube_list_widget
&& this.gui_objects.folderlist != this.gui_objects.addressbookslist
) {
this.treelist = new rcube_treelist_widget(this.gui_objects.folderlist, {
selectable: true,
id_prefix: 'rcmli',
parent_focus: true,
id_encode: this.html_identifier_encode,
id_decode: this.html_identifier_decode,
check_droptarget: function(node) { return !node.virtual && ref.check_droptarget(node.id) }
});
this.treelist
.addEventListener('collapse', function(node) { ref.folder_collapsed(node) })
.addEventListener('expand', function(node) { ref.folder_collapsed(node) })
.addEventListener('beforeselect', function(node) { return !ref.busy; })
.addEventListener('select', function(node) {
ref.triggerEvent('selectfolder', { folder:node.id, prefix:'rcmli' });
ref.mark_all_read_state();
});
}
// activate html5 file drop feature (if browser supports it and if configured)
if (this.gui_objects.filedrop && this.env.filedrop && window.FormData) {
$(document.body).on('dragover dragleave drop', function(e) { return ref.document_drag_hover(e, e.type == 'dragover'); });
$(this.gui_objects.filedrop).addClass('droptarget')
.on('dragover dragleave', function(e) { return ref.file_drag_hover(e, e.type == 'dragover'); })
.get(0).addEventListener('drop', function(e) { return ref.file_dropped(e); }, false);
}
// catch document (and iframe) mouse clicks
var body_mouseup = function(e) { return ref.doc_mouse_up(e); };
$(document.body)
.mouseup(body_mouseup)
.keydown(function(e) { return ref.doc_keypress(e); });
rcube_webmail.set_iframe_events({mouseup: body_mouseup});
// trigger init event hook
this.triggerEvent('init', { task:this.task, action:this.env.action });
// execute all foreign onload scripts
// @deprecated
for (n in this.onloads) {
if (typeof this.onloads[n] === 'string')
eval(this.onloads[n]);
else if (typeof this.onloads[n] === 'function')
this.onloads[n]();
}
// register menu buttons
$('[data-popup]').each(function() { ref.register_menu_button(this, $(this).data('popup')); });
// start keep-alive and refresh intervals
this.start_refresh();
this.start_keepalive();
};
this.log = function(msg)
{
if (this.env.devel_mode && window.console && console.log)
console.log(msg);
};
/*********************************************************/
/********* client command interface *********/
/*********************************************************/
// execute a specific command on the web client
this.command = function(command, props, obj, event)
{
var ret, uid, cid, url, flag, aborted = false;
if (obj && obj.blur && !(event && rcube_event.is_keyboard(event)))
obj.blur();
// do nothing if interface is locked by another command
// with exception for searching reset and menu
if (this.busy && !(command == 'reset-search' && this.last_command == 'search') && !command.match(/^menu-/))
return false;
// let the browser handle this click (shift/ctrl usually opens the link in a new window/tab)
if ((obj && obj.href && String(obj.href).indexOf('#') < 0) && rcube_event.get_modifier(event)) {
return true;
}
// command not supported or allowed
if (!this.commands[command]) {
// pass command to parent window
if (this.is_framed())
parent.rcmail.command(command, props);
return false;
}
// check input before leaving compose step
if (this.task == 'mail' && this.env.action == 'compose' && !this.env.server_error && command != 'save-pref'
&& $.inArray(command, this.env.compose_commands) < 0 && !this.compose_skip_unsavedcheck
) {
if (!this.env.is_sent && this.cmp_hash != this.compose_field_hash()) {
this.confirm_dialog(this.get_label('notsentwarning'), 'discard', function() {
// remove copy from local storage if compose screen is left intentionally
ref.remove_compose_data(ref.env.compose_id);
ref.compose_skip_unsavedcheck = true;
ref.command(command, props, obj, event);
});
return false;
}
}
this.last_command = command;
// process external commands
if (typeof this.command_handlers[command] === 'function') {
ret = this.command_handlers[command](props, obj, event);
return ret !== undefined ? ret : (obj ? false : true);
}
else if (typeof this.command_handlers[command] === 'string') {
ret = window[this.command_handlers[command]](props, obj, event);
return ret !== undefined ? ret : (obj ? false : true);
}
// trigger plugin hooks
this.triggerEvent('actionbefore', {props:props, action:command, originalEvent:event});
ret = this.triggerEvent('before'+command, props || event);
if (ret !== undefined) {
// abort if one of the handlers returned false
if (ret === false)
return false;
else
props = ret;
}
ret = undefined;
// process internal command
switch (command) {
// commands to switch task
case 'logout':
case 'mail':
case 'addressbook':
case 'settings':
this.switch_task(command);
break;
case 'about':
this.redirect('?_task=settings&_action=about', false);
break;
case 'permaurl':
if (obj && obj.href && obj.target)
return true;
else if (this.env.permaurl)
parent.location.href = this.env.permaurl;
break;
case 'extwin':
if (this.env.action == 'compose') {
var form = this.gui_objects.messageform,
win = this.open_window('');
if (win) {
this.save_compose_form_local();
this.compose_skip_unsavedcheck = true;
$("[name='_action']", form).val('compose');
form.action = this.url('mail/compose', { _id: this.env.compose_id, _extwin: 1 });
form.target = win.name;
form.submit();
}
}
else {
this.open_window(this.env.permaurl, true);
}
break;
case 'change-format':
url = this.env.permaurl + '&_format=' + props;
if (this.env.action == 'preview')
url = url.replace(/_action=show/, '_action=preview') + '&_framed=1';
if (this.env.extwin)
url += '&_extwin=1';
location.href = url;
break;
case 'menu-open':
if (props && props.menu == 'attachmentmenu') {
var mimetype = this.env.attachments[props.id];
if (mimetype && mimetype.mimetype) // in compose format is different
mimetype = mimetype.mimetype;
this.enable_command('open-attachment', mimetype && this.env.mimetypes && $.inArray(mimetype, this.env.mimetypes) >= 0);
}
this.show_menu(props, props.show || undefined, event);
break;
case 'menu-close':
this.hide_menu(props, event);
break;
case 'menu-save':
this.triggerEvent(command, {props:props, originalEvent:event});
return false;
case 'open':
if (uid = this.get_single_uid()) {
obj.href = this.url('show', this.params_from_uid(uid, {_extwin: 1}));
return true;
}
break;
case 'close':
if (this.env.extwin)
window.close();
break;
case 'list':
if (props && props != '') {
this.reset_qsearch(true);
}
if (this.env.action == 'compose' && this.env.extwin) {
window.close();
}
else if (this.task == 'mail') {
this.list_mailbox(props);
this.set_button_titles();
}
else if (this.task == 'addressbook')
this.list_contacts(props);
break;
case 'set-listmode':
this.set_list_options(null, undefined, undefined, props == 'threads' ? 1 : 0);
break;
case 'sort':
var sort_order = this.env.sort_order,
sort_col = !this.env.disabled_sort_col ? props : this.env.sort_col;
if (!this.env.disabled_sort_order)
sort_order = this.env.sort_col == sort_col && sort_order == 'ASC' ? 'DESC' : 'ASC';
// set table header and update env
this.set_list_sorting(sort_col, sort_order);
// reload message list
this.list_mailbox('', '', sort_col+'_'+sort_order);
break;
case 'nextpage':
this.list_page('next');
break;
case 'lastpage':
this.list_page('last');
break;
case 'previouspage':
this.list_page('prev');
break;
case 'firstpage':
this.list_page('first');
break;
case 'expunge':
if (this.env.exists)
this.expunge_mailbox(this.env.mailbox);
break;
case 'purge':
case 'empty-mailbox':
if (this.env.exists)
this.purge_mailbox(this.env.mailbox);
break;
// common commands used in multiple tasks
case 'show':
if (this.task == 'mail') {
uid = this.get_single_uid();
if (uid && (!this.env.uid || uid != this.env.uid)) {
if (this.env.mailbox == this.env.drafts_mailbox)
this.open_compose_step({ _draft_uid: uid, _mbox: this.env.mailbox });
else
this.show_message(uid);
}
}
else if (this.task == 'addressbook') {
cid = props ? props : this.get_single_cid();
if (cid && !(this.env.action == 'show' && cid == this.env.cid))
this.load_contact(cid, 'show');
}
else if (this.task == 'settings') {
this.goto_url('settings/' + props, {_framed: 0});
}
break;
case 'add':
if (this.task == 'addressbook')
this.load_contact(0, 'add');
else if (this.task == 'settings' && this.env.action == 'responses')
this.load_response(0, 'add-response');
else if (this.task == 'settings')
this.load_identity(0, 'add-identity');
break;
case 'edit':
if (this.task == 'addressbook' && (cid = this.get_single_cid()))
this.load_contact(cid, 'edit');
else if (this.task == 'mail' && (uid = this.get_single_uid())) {
url = { _mbox: this.get_message_mailbox(uid) };
url[this.env.mailbox == this.env.drafts_mailbox && props != 'new' ? '_draft_uid' : '_uid'] = uid;
this.open_compose_step(url);
}
break;
case 'save':
var input, form = this.gui_objects.editform;
if (form) {
// user prefs
if ((input = $("[name='_pagesize']", form)) && input.length && isNaN(parseInt(input.val()))) {
this.alert_dialog(this.get_label('nopagesizewarning'), function() {
input.focus();
});
break;
}
// contacts/identities
else {
// reload form
if (props == 'reload') {
form.action += '&_reload=1';
}
else if (this.task == 'settings' && (this.env.identities_level % 2) == 0 &&
(input = $("[name='_email']", form)) && input.length && !rcube_check_email(input.val())
) {
this.alert_dialog(this.get_label('noemailwarning'), function() {
input.focus();
});
break;
}
}
// add selected source (on the list)
if (parent.rcmail && parent.rcmail.env.source)
form.action = this.add_url(form.action, '_orig_source', parent.rcmail.env.source);
form.submit();
}
break;
case 'delete':
// mail task
if (this.task == 'mail')
this.delete_messages(event);
// addressbook task
else if (this.task == 'addressbook')
this.delete_contacts();
// settings: canned response
else if (this.task == 'settings' && this.env.action == 'responses')
this.delete_response();
// settings: user identities
else if (this.task == 'settings')
this.delete_identity();
break;
// mail task commands
case 'move':
case 'moveto': // deprecated
if (this.task == 'mail')
this.move_messages(props, event);
else if (this.task == 'addressbook')
this.move_contacts(props, event);
break;
case 'copy':
if (this.task == 'mail')
this.copy_messages(props, event);
else if (this.task == 'addressbook')
this.copy_contacts(props, event);
break;
case 'mark':
if (props)
this.mark_message(props);
break;
case 'toggle_status':
case 'toggle_flag':
flag = command == 'toggle_flag' ? 'flagged' : 'read';
if (uid = props) {
// toggle flagged/unflagged
if (flag == 'flagged') {
if (this.message_list.rows[uid].flagged)
flag = 'unflagged';
}
// toggle read/unread
else if (this.message_list.rows[uid].deleted)
flag = 'undelete';
else if (!this.message_list.rows[uid].unread)
flag = 'unread';
this.mark_message(flag, uid);
}
break;
case 'add-contact':
this.add_contact(props);
break;
case 'load-remote':
if (this.env.uid) {
if (props && this.env.sender) {
this.add_contact(this.env.sender, true);
break;
}
this.show_message(this.env.uid, true, this.env.action == 'preview');
}
break;
case 'load-attachment':
case 'open-attachment':
case 'download-attachment':
var params, mimetype = this.env.attachments[props];
if (this.env.action == 'compose') {
params = {_file: props, _id: this.env.compose_id};
mimetype = mimetype ? mimetype.mimetype : '';
}
else {
params = {_mbox: this.env.mailbox, _uid: this.env.uid, _part: props};
}
// open attachment in frame if it's of a supported mimetype
if (command != 'download-attachment' && mimetype && this.env.mimetypes && $.inArray(mimetype, this.env.mimetypes) >= 0) {
// Note: We disable _framed for proper X-Frame-Options:deny support (#6688)
if (this.open_window(this.url('get', $.extend({_frame: 1, _framed: 0}, params))))
break;
}
params._download = 1;
// prevent from page unload warning in compose
this.compose_skip_unsavedcheck = 1;
this.goto_url('get', params, false, true);
this.compose_skip_unsavedcheck = 0;
break;
case 'select-all':
this.select_all_mode = props ? false : true;
this.dummy_select = true; // prevent msg opening if there's only one msg on the list
var list = this[this.task == 'addressbook' ? 'contact_list' : 'message_list'];
if (props == 'invert')
list.invert_selection();
else
list.select_all(props == 'page' ? '' : props);
this.dummy_select = null;
break;
case 'select-none':
this.select_all_mode = false;
this[this.task == 'addressbook' ? 'contact_list' : 'message_list'].clear_selection();
break;
case 'expand-all':
this.env.autoexpand_threads = 1;
this.message_list.expand_all();
break;
case 'expand-unread':
this.env.autoexpand_threads = 2;
this.message_list.collapse_all();
this.expand_unread();
break;
case 'collapse-all':
this.env.autoexpand_threads = 0;
this.message_list.collapse_all();
break;
case 'nextmessage':
if (this.env.next_uid)
this.show_message(this.env.next_uid, false, this.env.action == 'preview');
break;
case 'lastmessage':
if (this.env.last_uid)
this.show_message(this.env.last_uid);
break;
case 'previousmessage':
if (this.env.prev_uid)
this.show_message(this.env.prev_uid, false, this.env.action == 'preview');
break;
case 'firstmessage':
if (this.env.first_uid)
this.show_message(this.env.first_uid);
break;
case 'compose':
url = {};
if (this.task == 'mail') {
url = {_mbox: this.env.mailbox, _search: this.env.search_request};
if (props)
url._to = props;
}
// modify url if we're in addressbook
else if (this.task == 'addressbook') {
// switch to mail compose step directly
if (props && props.indexOf('@') > 0) {
url._to = props;
}
else {
var a_cids = [];
// use contact id passed as command parameter
if (props)
a_cids.push(props);
// get selected contacts
else if (this.contact_list)
a_cids = this.contact_list.get_selection();
if (a_cids.length) {
this.http_post('mailto', { _cid: a_cids.join(','), _source: this.env.source }, true);
break;
}
else if (this.env.group && this.env.pagecount) {
this.http_post('mailto', { _gid: this.env.group, _source: this.env.source }, true);
break;
}
}
}
else if (props && typeof props == 'string') {
url._to = props;
}
else if (props && typeof props == 'object') {
$.extend(url, props);
}
this.open_compose_step(url);
break;
case 'spellcheck':
if (this.spellcheck_state()) {
this.editor.spellcheck_stop();
}
else {
this.editor.spellcheck_start();
}
break;
case 'savedraft':
// Reset the auto-save timer
clearTimeout(this.save_timer);
// compose form did not change (and draft wasn't saved already)
if (this.env.draft_id && this.cmp_hash == this.compose_field_hash()) {
this.auto_save_start();
break;
}
this.submit_messageform(true);
break;
case 'send':
if (!props.nocheck && !this.env.is_sent && !this.check_compose_input(command))
break;
// Reset the auto-save timer
clearTimeout(this.save_timer);
this.submit_messageform();
break;
case 'send-attachment':
// Reset the auto-save timer
clearTimeout(this.save_timer);
if (!(flag = this.upload_file(props || this.gui_objects.uploadform, 'upload'))) {
if (flag !== false)
this.alert_dialog(this.get_label('selectimportfile'));
aborted = true;
}
break;
case 'insert-sig':
this.change_identity($("[name='_from']")[0], true);
break;
case 'list-addresses':
this.list_contacts(props);
this.enable_command('add-recipient', false);
break;
case 'add-recipient':
this.compose_add_recipient(props);
break;
case 'reply-all':
case 'reply-list':
case 'reply':
if (uid = this.get_single_uid()) {
url = {_reply_uid: uid, _mbox: this.get_message_mailbox(uid), _search: this.env.search_request};
if (command == 'reply-all')
// do reply-list, when list is detected and popup menu wasn't used
url._all = (!props && this.env.reply_all_mode == 1 && this.commands['reply-list'] ? 'list' : 'all');
else if (command == 'reply-list')
url._all = 'list';
this.open_compose_step(url);
}
break;
case 'forward-attachment':
case 'forward-inline':
case 'forward':
var uids = this.env.uid ? [this.env.uid] : (this.message_list ? this.message_list.get_selection() : []);
if (uids.length) {
url = { _forward_uid: this.uids_to_list(uids), _mbox: this.env.mailbox, _search: this.env.search_request };
if (command == 'forward-attachment' || (!props && this.env.forward_attachment) || uids.length > 1)
url._attachment = 1;
this.open_compose_step(url);
}
break;
case 'print':
if (this.task == 'addressbook') {
if (uid = this.get_single_cid()) {
url = '&_action=print&_cid=' + uid;
if (this.env.source)
url += '&_source=' + urlencode(this.env.source);
this.open_window(this.env.comm_path + url, true, true);
}
}
else if (this.env.action == 'get' && !this.env.is_message) {
this.gui_objects.messagepartframe.contentWindow.print();
}
else if (uid = this.get_single_uid()) {
url = this.url('print', this.params_from_uid(uid, {_safe: this.env.safemode ? 1 : 0}));
if (this.open_window(url, true, true)) {
if (this.env.action != 'show' && this.env.action != 'get')
this.mark_message('read', uid);
}
}
break;
case 'viewsource':
if (uid = this.get_single_uid())
this.open_window(this.url('viewsource', this.params_from_uid(uid)), true, true);
break;
case 'download':
if (this.env.action == 'get') {
location.href = this.secure_url(location.href.replace(/_frame=/, '_download='));
}
else if (uid = this.get_single_uid()) {
this.goto_url('viewsource', this.params_from_uid(uid, {_save: 1}), false, true);
}
break;
// quicksearch
case 'search':
ret = this.qsearch(props);
break;
// reset quicksearch
case 'reset-search':
var n, s = this.env.search_request || this.env.qsearch;
this.reset_qsearch(true);
if (s && this.env.action == 'compose') {
if (this.contact_list)
this.list_contacts_clear();
}
else if (s && this.env.mailbox) {
this.list_mailbox(this.env.mailbox, 1);
}
else if (s && this.task == 'addressbook') {
if (this.env.source == '') {
for (n in this.env.address_sources) break;
this.env.source = n;
this.env.group = '';
}
this.list_contacts(this.env.source, this.env.group, 1);
}
break;
case 'pushgroup':
// add group ID and current search to stack
var group = {
id: props.id,
search_request: this.env.search_request,
page: this.env.current_page,
search: this.env.search_request && this.gui_objects.qsearchbox ? this.gui_objects.qsearchbox.value : null
};
this.env.address_group_stack.push(group);
if (obj && event)
rcube_event.cancel(event);
case 'listgroup':
this.reset_qsearch();
this.list_contacts(props.source, props.id, 1, group);
break;
case 'popgroup':
if (this.env.address_group_stack.length) {
var old = this.env.address_group_stack.pop();
this.reset_qsearch();
if (old.search_request) {
// this code is executed when going back to the search result
if (old.search && this.gui_objects.qsearchbox)
$(this.gui_objects.qsearchbox).val(old.search);
this.env.search_request = old.search_request;
this.list_contacts_remote(null, null, this.env.current_page = old.page);
}
else
this.list_contacts(props.source, this.env.address_group_stack[this.env.address_group_stack.length-1].id);
}
break;
case 'import-messages':
var form = props || this.gui_objects.importform,
importlock = this.set_busy(true, 'importwait');
if (!(flag = this.upload_file(form, 'import', importlock))) {
this.set_busy(false, null, importlock);
if (flag !== false)
this.alert_dialog(this.get_label('selectimportfile'));
aborted = true;
}
break;
case 'import':
var dialog = $('<iframe>').attr('src', this.url('import', {_framed: 1, _target: this.env.source})),
import_func = function(e) {
var win = dialog[0].contentWindow,
form = win.rcmail.gui_objects.importform;
if (form) {
var lock, file = win.$('#rcmimportfile')[0];
if (file && !file.value) {
win.rcmail.alert_dialog(win.rcmail.get_label('selectimportfile'));
return;
}
lock = win.rcmail.set_busy(true, 'importwait');
$('[name="_unlock"]', form).val(lock);
form.submit();
win.rcmail.lock_form(form, true);
// disable Import button
$(e.target).attr('disabled', true).next().focus();
}
},
close_func = function(event, ui) {
$(this).remove();
if (ref.import_state == 'reload')
ref.command('list');
};
this.import_state = null;
this.import_dialog = this.simple_dialog(dialog, this.gettext('importcontacts'), import_func, {
close: close_func,
button: 'import',
width: 500,
height: 300
});
break;
case 'export':
if (this.contact_list.rowcount > 0) {
this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _search: this.env.search_request }, false, true);
}
break;
case 'export-selected':
if (this.contact_list.rowcount > 0) {
this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _cid: this.contact_list.get_selection().join(',') }, false, true);
}
break;
case 'upload-photo':
this.upload_contact_photo(props || this.gui_objects.uploadform);
break;
case 'delete-photo':
this.replace_contact_photo('-del-');
break;
case 'undo':
this.http_request('undo', '', this.display_message('', 'loading'));
break;
// unified command call (command name == function name)
default:
var func = command.replace(/-/g, '_');
if (this[func] && typeof this[func] === 'function') {
ret = this[func](props, obj, event);
}
break;
}
if (!aborted && this.triggerEvent('after'+command, props) === false)
ret = false;
this.triggerEvent('actionafter', { props:props, action:command, aborted:aborted, ret:ret, originalEvent:event});
if (ret === false)
return false;
if (obj || aborted === true)
return false;
return true;
};
// set command(s) enabled or disabled
this.enable_command = function()
{
var i, n, args = Array.prototype.slice.call(arguments),
enable = args.pop(), cmd;
for (n=0; n<args.length; n++) {
cmd = args[n];
// argument of type array
if (typeof cmd === 'string') {
this.commands[cmd] = enable;
this.set_button(cmd, (enable ? 'act' : 'pas'));
this.triggerEvent('enable-command', {command: cmd, status: enable});
}
// push array elements into commands array
else {
for (i in cmd)
args.push(cmd[i]);
}
}
this.set_menu_buttons();
};
this.command_enabled = function(cmd)
{
return this.commands[cmd];
};
// lock/unlock interface
this.set_busy = function(a, message, id)
{
if (a && message) {
var msg = this.get_label(message);
if (msg == message)
msg = 'Loading...';
id = this.display_message(msg, 'loading');
}
else if (!a && id) {
this.hide_message(id);
}
this.busy = a;
if (this.gui_objects.editform)
this.lock_form(this.gui_objects.editform, a);
return id;
};
// return a localized string
this.get_label = function(name, domain)
{
if (domain && this.labels[domain+'.'+name])
return this.labels[domain+'.'+name];
else if (this.labels[name])
return this.labels[name];
else
return name;
};
// alias for convenience reasons
this.gettext = this.get_label;
// switch to another application task
this.switch_task = function(task)
{
var action, path;
if ((path = task.split('/')).length == 2) {
task = path[0];
action = path[1];
}
if (this.task === task && task != 'mail')
return;
var url = this.get_task_url(task);
if (action)
url += '&_action=' + action;
if (task == 'mail')
url += '&_mbox=INBOX';
else if (task == 'logout') {
url = this.secure_url(url);
this.clear_compose_data();
}
this.redirect(url);
};
this.get_task_url = function(task, url)
{
if (!url)
url = this.env.comm_path;
if (url.match(/[?&]_task=[a-zA-Z0-9_-]+/))
return url.replace(/_task=[a-zA-Z0-9_-]+/, '_task=' + task);
else
return url.replace(/\?.*$/, '') + '?_task=' + task;
};
this.reload = function(delay)
{
if (this.is_framed())
parent.rcmail.reload(delay);
else if (delay)
setTimeout(function() { ref.reload(); }, delay);
else if (window.location)
location.href = this.url('', {_extwin: this.env.extwin});
};
// Add variable to GET string, replace old value if exists
this.add_url = function(url, name, value)
{
var urldata, datax, hash = '';
value = urlencode(value);
if (/(#[a-z0-9_-]+)$/.test(url)) {
hash = RegExp.$1;
url = url.substr(0, url.length - hash.length);
}
if (/(\?.*)$/.test(url)) {
urldata = RegExp.$1;
datax = RegExp('((\\?|&)'+RegExp.escape(name)+'=[^&]*)');
if (datax.test(urldata))
urldata = urldata.replace(datax, RegExp.$2 + name + '=' + value);
else
urldata += '&' + name + '=' + value;
return url.replace(/(\?.*)$/, urldata) + hash;
}
return url + '?' + name + '=' + value + hash;
};
// append CSRF protection token to the given url
this.secure_url = function(url)
{
return this.add_url(url, '_token', this.env.request_token);
},
this.is_framed = function()
{
return this.env.framed && parent.rcmail && parent.rcmail != this && typeof parent.rcmail.command == 'function';
};
this.save_pref = function(prop)
{
var request = {_name: prop.name, _value: prop.value};
if (prop.session)
request._session = prop.session;
if (prop.env)
this.env[prop.env] = prop.value;
this.http_post('save-pref', request);
};
this.html_identifier = function(str, encode)
{
return encode ? this.html_identifier_encode(str) : String(str).replace(this.identifier_expr, '_');
};
this.html_identifier_encode = function(str)
{
return Base64.encode(String(str)).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
};
this.html_identifier_decode = function(str)
{
str = String(str).replace(/-/g, '+').replace(/_/g, '/');
while (str.length % 4) str += '=';
return Base64.decode(str);
};
/*********************************************************/
/********* event handling methods *********/
/*********************************************************/
this.drag_menu = function(e, target)
{
var modkey = rcube_event.get_modifier(e),
menu = this.gui_objects.dragmenu;
if (menu && modkey == SHIFT_KEY && this.commands['copy']) {
var pos = rcube_event.get_mouse_pos(e);
this.env.drag_target = target;
this.show_menu(this.gui_objects.dragmenu.id, true, e);
$(menu).css({top: (pos.y-10)+'px', left: (pos.x-10)+'px'});
return true;
}
return false;
};
this.drag_menu_action = function(action)
{
var menu = this.gui_objects.dragmenu;
if (menu) {
$(menu).hide();
}
this.command(action, this.env.drag_target);
this.env.drag_target = null;
};
this.drag_start = function(list)
{
this.drag_active = true;
if (this.preview_timer)
clearTimeout(this.preview_timer);
// prepare treelist widget for dragging interactions
if (this.treelist)
this.treelist.drag_start();
};
this.drag_end = function(e)
{
var list, model;
if (this.treelist)
this.treelist.drag_end();
// execute drag & drop action when mouse was released
if (list = this.message_list)
model = this.env.mailboxes;
else if (list = this.contact_list)
model = this.env.contactfolders;
// Note: we accept only mouse events to ignore dragging aborts with ESC key (#6623)
if (this.drag_active && model && this.env.last_folder_target && !rcube_event.is_keyboard(e)) {
var target = model[this.env.last_folder_target];
list.draglayer.hide();
if (this.contact_list) {
if (!this.contacts_drag_menu(e, target))
this.command('move', target);
}
else if (!this.drag_menu(e, target))
this.command('move', target);
}
this.drag_active = false;
this.env.last_folder_target = null;
};
this.drag_move = function(e)
{
if (this.gui_objects.folderlist) {
var drag_target, oldclass,
layerclass = 'draglayernormal',
mouse = rcube_event.get_mouse_pos(e);
if (this.contact_list && this.contact_list.draglayer)
oldclass = this.contact_list.draglayer.attr('class');
// mouse intersects a valid drop target on the treelist
if (this.treelist && (drag_target = this.treelist.intersects(mouse, true))) {
this.env.last_folder_target = drag_target;
layerclass = 'draglayer' + (this.check_droptarget(drag_target) > 1 ? 'copy' : 'normal');
}
else {
// Clear target, otherwise drag end will trigger move into last valid droptarget
this.env.last_folder_target = null;
}
if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer)
this.contact_list.draglayer.attr('class', layerclass);
}
};
this.collapse_folder = function(name)
{
if (this.treelist)
this.treelist.toggle(name);
};
this.folder_collapsed = function(node)
{
var prefname = this.env.task == 'addressbook' ? 'collapsed_abooks' : 'collapsed_folders',
old = this.env[prefname];
if (node.collapsed) {
this.env[prefname] = this.env[prefname] + '&'+urlencode(node.id)+'&';
// select the folder if one of its childs is currently selected
// don't select if it's virtual (#1488346)
if (!node.virtual && this.env.mailbox && this.env.mailbox.startsWith(node.id + this.env.delimiter))
this.command('list', node.id);
}
else {
var reg = new RegExp('&'+urlencode(node.id)+'&');
this.env[prefname] = this.env[prefname].replace(reg, '');
}
if (!this.drag_active) {
if (old !== this.env[prefname])
this.command('save-pref', { name: prefname, value: this.env[prefname] });
if (this.env.unread_counts)
this.set_unread_count_display(node.id, false);
}
};
// global mouse-click handler to cleanup some UI elements
this.doc_mouse_up = function(e)
{
var list, id, target = rcube_event.get_target(e);
// ignore event if jquery UI dialog is open
if ($(target).closest('.ui-dialog, .ui-widget-overlay').length)
return;
// remove focus from list widgets
if (window.rcube_list_widget && rcube_list_widget._instances.length) {
$.each(rcube_list_widget._instances, function(i,list) {
if (list && !rcube_mouse_is_over(e, list.list.parentNode))
list.blur();
});
}
// reset 'pressed' buttons
if (this.buttons_sel) {
for (id in this.buttons_sel)
if (typeof id !== 'function')
this.button_out(this.buttons_sel[id], id);
this.buttons_sel = {};
}
// reset popup menus; delayed to have updated menu_stack data
setTimeout(function(e) {
var obj, skip, config, id, i, parents = $(target).parents();
for (i = ref.menu_stack.length - 1; i >= 0; i--) {
id = ref.menu_stack[i];
obj = $('#' + id);
if (obj.is(':visible')
&& target != obj.data('opener')
&& target != obj.get(0) // check if scroll bar was clicked (#1489832)
&& !parents.is(obj.data('opener'))
&& id != skip
&& (obj.attr('data-editable') != 'true' || !$(target).parents('#' + id).length)
&& (obj.attr('data-sticky') != 'true' || !rcube_mouse_is_over(e, obj.get(0)))
) {
ref.hide_menu(id, e);
}
skip = obj.data('parent');
}
}, 10, e);
};
// global keypress event handler
this.doc_keypress = function(e)
{
// Helper method to move focus to the next/prev active menu item
var focus_menu_item = function(dir) {
var obj, item, mod = dir < 0 ? 'prevAll' : 'nextAll', limit = dir < 0 ? 'last' : 'first';
if (ref.focused_menu && (obj = $('#'+ref.focused_menu))) {
item = obj.find(':focus').closest('li')[mod](':has(:not([aria-disabled=true]))').find('a,input')[limit]();
if (!item.length)
item = obj.find(':focus').closest('ul')[mod](':has(:not([aria-disabled=true]))').find('a,input')[limit]();
return item.focus().length;
}
return 0;
};
var target = e.target || {},
keyCode = rcube_event.get_keycode(e);
if (e.keyCode != 27 && (!this.menu_keyboard_active || target.nodeName == 'TEXTAREA' || target.nodeName == 'SELECT')) {
return true;
}
switch (keyCode) {
case 38:
case 40:
case 63232: // "up", in safari keypress
case 63233: // "down", in safari keypress
focus_menu_item(keyCode == 38 || keyCode == 63232 ? -1 : 1);
return rcube_event.cancel(e);
case 9: // tab
if (this.focused_menu) {
var mod = rcube_event.get_modifier(e);
if (!focus_menu_item(mod == SHIFT_KEY ? -1 : 1)) {
this.hide_menu(this.focused_menu, e);
}
}
return rcube_event.cancel(e);
case 27: // esc
if (this.menu_stack.length)
this.hide_menu(this.menu_stack[this.menu_stack.length-1], e);
break;
}
return true;
}
this.msglist_select = function(list)
{
if (this.preview_timer)
clearTimeout(this.preview_timer);
var selected = list.get_single_selection();
this.enable_command(this.env.message_commands, selected != null);
if (selected) {
// Hide certain command buttons when Drafts folder is selected
if (this.env.mailbox == this.env.drafts_mailbox)
this.enable_command('reply', 'reply-all', 'reply-list', 'forward', 'forward-attachment', 'forward-inline', false);
// Disable reply-list when List-Post header is not set
else {
var msg = this.env.messages[selected];
if (!msg.ml)
this.enable_command('reply-list', false);
}
}
// Multi-message commands
this.enable_command('delete', 'move', 'copy', 'mark', 'forward', 'forward-attachment', list.get_selection(false).length > 0);
// reset all-pages-selection
if (selected || (list.get_selection(false).length && list.get_selection(false).length != list.rowcount))
this.select_all_mode = false;
// start timer for message preview (wait for double click)
if (selected && this.env.contentframe && !list.multi_selecting && !this.dummy_select) {
// try to be responsive and try not to overload the server when user is pressing up/down key repeatedly
var now = new Date().getTime();
var time_diff = now - (this._last_msglist_select_time || 0);
var preview_pane_delay = this.preview_delay_click;
// user is selecting messages repeatedly, wait until this ends (use larger delay)
if (time_diff < this.preview_delay_select) {
preview_pane_delay = this.preview_delay_select;
if (this.preview_timer) {
clearTimeout(this.preview_timer);
}
if (this.env.contentframe) {
this.show_contentframe(false);
}
}
this._last_msglist_select_time = now;
this.preview_timer = setTimeout(function() { ref.msglist_get_preview(); }, preview_pane_delay);
}
else if (this.env.contentframe) {
this.show_contentframe(false);
}
};
this.msglist_dbl_click = function(list)
{
if (this.preview_timer)
clearTimeout(this.preview_timer);
var uid = list.get_single_selection();
if (uid && (this.env.messages[uid].mbox || this.env.mailbox) == this.env.drafts_mailbox)
this.open_compose_step({ _draft_uid: uid, _mbox: this.env.mailbox });
else if (uid)
this.show_message(uid, false, false);
};
this.msglist_keypress = function(list)
{
if (list.modkey == CONTROL_KEY)
return;
if (list.key_pressed == list.ENTER_KEY)
this.command('show');
else if (list.key_pressed == list.DELETE_KEY || list.key_pressed == list.BACKSPACE_KEY)
this.command('delete');
else if (list.key_pressed == 33)
this.command('previouspage');
else if (list.key_pressed == 34)
this.command('nextpage');
};
this.msglist_get_preview = function()
{
var uid = this.get_single_uid();
if (uid && this.env.contentframe && !this.drag_active)
this.show_message(uid, false, true);
else if (this.env.contentframe)
this.show_contentframe(false);
};
this.msglist_expand = function(row)
{
if (this.env.messages[row.uid])
this.env.messages[row.uid].expanded = row.expanded;
$(row.obj)[row.expanded?'addClass':'removeClass']('expanded');
};
this.msglist_set_coltypes = function(list)
{
var i, found, name, cols = list.thead.rows[0].cells;
this.env.listcols = [];
for (i=0; i<cols.length; i++)
if (cols[i].id && cols[i].id.startsWith('rcm')) {
name = cols[i].id.slice(3);
this.env.listcols.push(name);
}
if ((found = $.inArray('flag', this.env.listcols)) >= 0)
this.env.flagged_col = found;
if ((found = $.inArray('subject', this.env.listcols)) >= 0)
this.env.subject_col = found;
this.command('save-pref', { name: 'list_cols', value: this.env.listcols, session: 'list_attrib/columns' });
};
this.check_droptarget = function(id)
{
switch (this.task) {
case 'mail':
return (this.env.mailboxes[id]
&& !this.env.mailboxes[id].virtual
&& (this.env.mailboxes[id].id != this.env.mailbox || this.is_multifolder_listing())) ? 1 : 0;
case 'addressbook':
var target;
if (id != this.env.source && (target = this.env.contactfolders[id])) {
// droptarget is a group
if (target.type == 'group') {
if (target.id != this.env.group && !this.env.contactfolders[target.source].readonly) {
var is_other = this.env.selection_sources.length > 1 || $.inArray(target.source, this.env.selection_sources) == -1;
return !is_other || this.commands.move ? 1 : 2;
}
}
// droptarget is a (writable) addressbook and it's not the source
else if (!target.readonly && (this.env.selection_sources.length > 1 || $.inArray(id, this.env.selection_sources) == -1)) {
return this.commands.move ? 1 : 2;
}
}
}
return 0;
};
// open popup window
this.open_window = function(url, small, toolbar)
{
var wname = 'rcmextwin' + new Date().getTime();
url += (url.match(/\?/) ? '&' : '?') + '_extwin=1';
if (this.env.standard_windows)
var extwin = window.open(url, wname);
else {
var win = this.is_framed() ? parent.window : window,
page = $(win),
page_width = page.width(),
page_height = bw.mz ? $('body', win).height() : page.height(),
w = Math.min(small ? this.env.popup_width_small : this.env.popup_width, page_width),
h = page_height, // always use same height
l = (win.screenLeft || win.screenX) + 20,
t = (win.screenTop || win.screenY) + 20,
extwin = window.open(url, wname,
'width='+w+',height='+h+',top='+t+',left='+l+',resizable=yes,location=no,scrollbars=yes'
+(toolbar ? ',toolbar=yes,menubar=yes,status=yes' : ',toolbar=no,menubar=no,status=no'));
}
// detect popup blocker (#1489618)
// don't care this might not work with all browsers
if (!extwin || extwin.closed) {
this.display_message('windowopenerror', 'warning');
return;
}
// write loading... message to empty windows
if (!url && extwin.document) {
extwin.document.write('<html><body>' + this.get_label('loading') + '</body></html>');
}
// allow plugins to grab the window reference (#1489413)
this.triggerEvent('openwindow', { url:url, handle:extwin });
// focus window, delayed to bring to front
setTimeout(function() { extwin && extwin.focus(); }, 10);
return extwin;
};
/*********************************************************/
/********* (message) list functionality *********/
/*********************************************************/
this.init_message_row = function(row)
{
var i, fn = {}, uid = row.uid,
status_icon = (this.env.status_col != null ? 'status' : 'msg') + 'icn' + row.id;
if (uid && this.env.messages[uid])
$.extend(row, this.env.messages[uid]);
// set eventhandler to status icon
if (row.icon = document.getElementById(status_icon)) {
fn.icon = function(e) { ref.command('toggle_status', uid); };
}
// save message icon position too
if (this.env.status_col != null)
row.msgicon = document.getElementById('msgicn'+row.id);
else
row.msgicon = row.icon;
// set eventhandler to flag icon
if (this.env.flagged_col != null && (row.flagicon = document.getElementById('flagicn'+row.id))) {
fn.flagicon = function(e) { ref.command('toggle_flag', uid); };
}
// set event handler to thread expand/collapse icon
if (!row.depth && row.has_children && (row.expando = document.getElementById('rcmexpando'+row.id))) {
fn.expando = function(e) { ref.expand_message_row(e, uid); };
}
// attach events
$.each(fn, function(i, f) {
row[i].onclick = function(e) { f(e); return rcube_event.cancel(e); };
if (bw.touch && row[i].addEventListener) {
row[i].addEventListener('touchend', function(e) {
if (e.changedTouches.length == 1) {
f(e);
return rcube_event.cancel(e);
}
}, false);
}
});
this.triggerEvent('insertrow', { uid:uid, row:row });
};
// create a table row in the message list
this.add_message_row = function(uid, cols, flags, attop)
{
if (!this.gui_objects.messagelist || !this.message_list)
return false;
// Prevent from adding messages from different folder (#1487752)
if (flags.mbox != this.env.mailbox && !flags.skip_mbox_check)
return false;
// When deleting messages fast it may happen that the same message
// from the next page could be added many times, we prevent this here
if (this.message_list.rows[uid])
return false;
if (!this.env.messages[uid])
this.env.messages[uid] = {};
// merge flags over local message object
$.extend(this.env.messages[uid], {
deleted: flags.deleted?1:0,
replied: flags.answered?1:0,
unread: !flags.seen?1:0,
forwarded: flags.forwarded?1:0,
flagged: flags.flagged?1:0,
has_children: flags.has_children?1:0,
depth: flags.depth?flags.depth:0,
unread_children: flags.unread_children || 0,
flagged_children: flags.flagged_children || 0,
parent_uid: flags.parent_uid || 0,
selected: this.select_all_mode || this.message_list.in_selection(uid),
ml: flags.ml?1:0,
ctype: flags.ctype,
mbox: flags.mbox,
// flags from plugins
flags: flags.extra_flags
});
var c, n, col, html, css_class, label, status_class = '', status_label = '',
tree = '', expando = '',
list = this.message_list,
rows = list.rows,
message = this.env.messages[uid],
msg_id = this.html_identifier(uid,true),
row_class = 'message'
+ (!flags.seen ? ' unread' : '')
+ (flags.deleted ? ' deleted' : '')
+ (flags.flagged ? ' flagged' : '')
+ (message.selected ? ' selected' : ''),
row = { cols:[], style:{}, id:'rcmrow'+msg_id, uid:uid };
// message status icons
css_class = 'msgicon';
if (this.env.status_col === null) {
css_class += ' status';
if (flags.deleted) {
status_class += ' deleted';
status_label += this.get_label('deleted') + ' ';
}
else if (!flags.seen) {
status_class += ' unread';
status_label += this.get_label('unread') + ' ';
}
else if (flags.unread_children > 0) {
status_class += ' unreadchildren';
}
}
if (flags.answered) {
status_class += ' replied';
status_label += this.get_label('replied') + ' ';
}
if (flags.forwarded) {
status_class += ' forwarded';
status_label += this.get_label('forwarded') + ' ';
}
// update selection
if (message.selected && !list.in_selection(uid))
list.selection.push(uid);
// threads
if (this.env.threading) {
if (message.depth) {
// This assumes that div width is hardcoded to 15px,
tree += '<span id="rcmtab' + msg_id + '" class="branch" style="width:' + (message.depth * 15) + 'px;">&nbsp;&nbsp;</span>';
if ((rows[message.parent_uid] && rows[message.parent_uid].expanded === false)
|| ((this.env.autoexpand_threads == 0 || this.env.autoexpand_threads == 2) &&
(!rows[message.parent_uid] || !rows[message.parent_uid].expanded))
) {
row.style.display = 'none';
message.expanded = false;
}
else
message.expanded = true;
row_class += ' thread expanded';
}
else if (message.has_children) {
if (message.expanded === undefined && (this.env.autoexpand_threads == 1 || (this.env.autoexpand_threads == 2 && message.unread_children))) {
message.expanded = true;
}
expando = '<div id="rcmexpando' + row.id + '" class="' + (message.expanded ? 'expanded' : 'collapsed') + '">&nbsp;&nbsp;</div>';
row_class += ' thread' + (message.expanded ? ' expanded' : '');
}
if (flags.unread_children && flags.seen && !message.expanded)
row_class += ' unroot';
if (flags.flagged_children && !message.expanded)
row_class += ' flaggedroot';
}
tree += '<span id="msgicn'+row.id+'" class="'+css_class+status_class+'" title="'+status_label+'"></span>';
row.className = row_class;
// build subject link
if (cols.subject) {
var action = flags.mbox == this.env.drafts_mailbox ? 'compose' : 'show',
uid_param = flags.mbox == this.env.drafts_mailbox ? '_draft_uid' : '_uid',
query = { _mbox: flags.mbox };
query[uid_param] = uid;
cols.subject = '<a href="' + this.url(action, query) + '" onclick="return rcube_event.keyboard_only(event)"' +
' onmouseover="rcube_webmail.long_subject_title(this,'+(message.depth+1)+')" tabindex="-1"><span>'+cols.subject+'</span></a>';
}
// add each submitted col
for (n in this.env.listcols) {
c = this.env.listcols[n];
col = {className: String(c).toLowerCase(), events:{}};
if (this.env.coltypes[c] && this.env.coltypes[c].hidden) {
col.className += ' hidden';
}
if (c == 'flag') {
css_class = (flags.flagged ? 'flagged' : 'unflagged');
label = this.get_label(css_class);
html = '<span id="flagicn'+row.id+'" class="'+css_class+'" title="'+label+'"></span>';
}
else if (c == 'attachment') {
label = this.get_label('withattachment');
if (flags.attachmentClass)
html = '<span class="'+flags.attachmentClass+'" title="'+label+'"></span>';
else if (/application\/|multipart\/(m|signed)/.test(flags.ctype))
html = '<span class="attachment" title="'+label+'"></span>';
else if (/multipart\/report/.test(flags.ctype))
html = '<span class="report"></span>';
else if (flags.ctype == 'multipart/encrypted' || flags.ctype == 'application/pkcs7-mime')
html = '<span class="encrypted"></span>';
else
html = '&nbsp;';
}
else if (c == 'status') {
label = '';
if (flags.deleted) {
css_class = 'deleted';
label = this.get_label('deleted');
}
else if (!flags.seen) {
css_class = 'unread';
label = this.get_label('unread');
}
else if (flags.unread_children > 0) {
css_class = 'unreadchildren';
}
else
css_class = 'msgicon';
html = '<span id="statusicn'+row.id+'" class="'+css_class+status_class+'" title="'+label+'"></span>';
}
else if (c == 'threads')
html = expando;
else if (c == 'subject') {
html = tree + cols[c];
}
else if (c == 'priority') {
if (flags.prio > 0 && flags.prio < 6) {
label = this.get_label('priority') + ' ' + flags.prio;
html = '<span class="prio'+flags.prio+'" title="'+label+'"></span>';
}
else
html = '&nbsp;';
}
else if (c == 'folder') {
html = '<span onmouseover="rcube_webmail.long_subject_title(this)">' + cols[c] + '<span>';
}
else
html = cols[c];
col.innerHTML = html;
row.cols.push(col);
}
if (this.env.layout == 'widescreen')
row = this.widescreen_message_row(row, uid, message);
list.insert_row(row, attop);
// remove 'old' row
if (attop && this.env.pagesize && list.rowcount > this.env.pagesize) {
var uid = list.get_last_row();
list.remove_row(uid);
list.clear_selection(uid);
}
};
// Converts standard message list record into "widescreen" (3-column) layout
this.widescreen_message_row = function(row, uid, message)
{
var domrow = document.createElement('tr');
domrow.id = row.id;
domrow.uid = row.uid;
domrow.className = row.className;
if (row.style) $.extend(domrow.style, row.style);
$.each(this.env.widescreen_list_template, function() {
if (!ref.env.threading && this.className == 'threads')
return;
var i, n, e, col, domcol,
domcell = document.createElement('td');
if (this.className) domcell.className = this.className;
for (i=0; this.cells && i < this.cells.length; i++) {
for (n=0; row.cols && n < row.cols.length; n++) {
if (this.cells[i] == row.cols[n].className) {
col = row.cols[n];
domcol = document.createElement('span');
domcol.className = this.cells[i];
if (this.className == 'subject' && domcol.className != 'subject')
domcol.className += ' skip-on-drag';
if (col.innerHTML)
domcol.innerHTML = col.innerHTML;
domcell.appendChild(domcol);
break;
}
}
}
domrow.appendChild(domcell);
});
if (this.env.threading && message.depth) {
n = this.calculate_thread_padding(message.depth);
$('td.subject', domrow).attr('style', 'padding-left:' + n + ' !important');
$('span.branch', domrow).remove();
}
return domrow;
};
this.calculate_thread_padding = function(level)
{
ref.env.thread_padding.match(/^([0-9.]+)(.+)/);
return (Math.min(6, level) * parseFloat(RegExp.$1)) + RegExp.$2;
};
this.set_list_sorting = function(sort_col, sort_order)
{
var sort_old = this.env.sort_col == 'arrival' ? 'date' : this.env.sort_col,
sort_new = sort_col == 'arrival' ? 'date' : sort_col;
// set table header class
$('#rcm' + sort_old).removeClass('sorted' + this.env.sort_order.toUpperCase());
if (sort_new)
$('#rcm' + sort_new).addClass('sorted' + sort_order);
// if sorting by 'arrival' is selected, click on date column should not switch to 'date'
$('#rcmdate > a').prop('rel', sort_col == 'arrival' ? 'arrival' : 'date');
this.env.sort_col = sort_col;
this.env.sort_order = sort_order;
};
this.set_list_options = function(cols, sort_col, sort_order, threads, layout)
{
var update, post_data = {};
if (sort_col === undefined)
sort_col = this.env.sort_col;
if (!sort_order)
sort_order = this.env.sort_order;
if (this.env.sort_col != sort_col || this.env.sort_order != sort_order) {
update = 1;
this.set_list_sorting(sort_col, sort_order);
}
if (this.env.threading != threads) {
update = 1;
post_data._threads = threads;
}
if (layout && this.env.layout != layout) {
this.triggerEvent('layout-change', {old_layout: this.env.layout, new_layout: layout});
update = 1;
this.env.layout = post_data._layout = layout;
}
if (cols && cols.length) {
// make sure new columns are added at the end of the list
var i, idx, name, newcols = [], oldcols = this.env.listcols;
for (i=0; i<oldcols.length; i++) {
name = oldcols[i];
idx = $.inArray(name, cols);
if (idx != -1) {
newcols.push(name);
delete cols[idx];
}
}
for (i=0; i<cols.length; i++)
if (cols[i])
newcols.push(cols[i]);
if (newcols.join() != oldcols.join()) {
update = 1;
post_data._cols = newcols.join(',');
}
}
if (update)
this.list_mailbox('', '', sort_col+'_'+sort_order, post_data);
};
// when user double-clicks on a row
this.show_message = function(id, safe, preview)
{
if (!id)
return;
var win, target = window,
url = this.params_from_uid(id, {_caps: this.browser_capabilities()});
if (preview && (win = this.get_frame_window(this.env.contentframe))) {
target = win;
url._framed = 1;
}
if (safe)
url._safe = 1;
// also send search request to get the right messages
if (this.env.search_request)
url._search = this.env.search_request;
if (this.env.extwin)
url._extwin = 1;
url = this.url(preview ? 'preview': 'show', url);
if (preview && String(target.location.href).indexOf(url) >= 0) {
this.show_contentframe(true);
}
else {
if (!preview && this.env.message_extwin && !this.env.extwin)
this.open_window(url, true);
else
this.location_href(url, target, true);
}
};
// update message status and unread counter after marking a message as read
this.set_unread_message = function(id, folder)
{
var self = this;
// find window with messages list
if (!self.message_list)
self = self.opener();
if (!self && window.parent)
self = parent.rcmail;
if (!self || !self.message_list)
return;
// this may fail in multifolder mode
if (self.set_message(id, 'unread', false) === false)
self.set_message(id + '-' + folder, 'unread', false);
if (self.env.unread_counts[folder] > 0) {
self.env.unread_counts[folder] -= 1;
self.set_unread_count(folder, self.env.unread_counts[folder], folder == 'INBOX' && !self.is_multifolder_listing());
}
};
this.show_contentframe = function(show)
{
var frame, win, name = this.env.contentframe;
if (frame = this.get_frame_element(name)) {
if (!show && (win = this.get_frame_window(name))) {
if (win.location.href.indexOf(this.env.blankpage) < 0) {
if (win.stop)
win.stop();
else // IE
win.document.execCommand('Stop');
win.location.href = this.env.blankpage;
}
}
else if (!bw.safari && !bw.konq)
$(frame)[show ? 'show' : 'hide']();
}
if (!show && this.env.frame_lock)
this.set_busy(false, null, this.env.frame_lock);
};
this.get_frame_element = function(id)
{
var frame;
if (id && (frame = document.getElementById(id)))
return frame;
};
this.get_frame_window = function(id)
{
var frame = this.get_frame_element(id);
if (frame && frame.name && window.frames)
return window.frames[frame.name];
};
this.lock_frame = function()
{
if (!this.env.frame_lock)
(this.is_framed() ? parent.rcmail : this).env.frame_lock = this.set_busy(true, 'loading');
};
// list a specific page
this.list_page = function(page)
{
if (page == 'next')
page = this.env.current_page+1;
else if (page == 'last')
page = this.env.pagecount;
else if (page == 'prev' && this.env.current_page > 1)
page = this.env.current_page-1;
else if (page == 'first' && this.env.current_page > 1)
page = 1;
if (page > 0 && page <= this.env.pagecount) {
this.env.current_page = page;
if (this.task == 'addressbook' || this.contact_list)
this.list_contacts(this.env.source, this.env.group, page);
else if (this.task == 'mail')
this.list_mailbox(this.env.mailbox, page);
}
};
// sends request to check for recent messages
this.checkmail = function()
{
var lock = this.set_busy(true, 'checkingmail'),
params = this.check_recent_params();
this.http_post('check-recent', params, lock);
};
// list messages of a specific mailbox using filter
this.filter_mailbox = function(filter)
{
if (this.filter_disabled)
return;
var params = this.search_params(false, filter),
lock = this.set_busy(true, 'searching');
this.clear_message_list();
// reset vars
this.env.current_page = 1;
this.env.search_filter = filter;
this.http_request('search', params, lock);
this.update_state({_mbox: params._mbox, _filter: filter, _scope: params._scope});
};
// reload the current message listing
this.refresh_list = function()
{
this.list_mailbox(this.env.mailbox, this.env.current_page || 1, null, { _clear:1 }, true);
if (this.message_list)
this.message_list.clear_selection();
};
// list messages of a specific mailbox
this.list_mailbox = function(mbox, page, sort, url, update_only)
{
var win, target = window;
if (typeof url != 'object')
url = {};
if (!mbox)
mbox = this.env.mailbox ? this.env.mailbox : 'INBOX';
// add sort to url if set
if (sort)
url._sort = sort;
// folder change, reset page, search scope, etc.
if (this.env.mailbox != mbox) {
page = 1;
this.env.current_page = page;
this.env.search_scope = 'base';
this.select_all_mode = false;
this.reset_search_filter();
}
// also send search request to get the right messages
else if (this.env.search_request)
url._search = this.env.search_request;
if (!update_only) {
// unselect selected messages and clear the list and message data
this.clear_message_list();
if (mbox != this.env.mailbox || (mbox == this.env.mailbox && !page && !sort))
url._refresh = 1;
this.select_folder(mbox, '', true);
this.unmark_folder(mbox, 'recent', '', true);
this.env.mailbox = mbox;
}
// load message list remotely
if (this.gui_objects.messagelist) {
this.list_mailbox_remote(mbox, page, url);
return;
}
if (win = this.get_frame_window(this.env.contentframe)) {
target = win;
url._framed = 1;
}
if (this.env.uid)
url._uid = this.env.uid;
// load message list to target frame/window
if (mbox) {
url._mbox = mbox;
url._page = page;
this.set_busy(true, 'loading');
this.location_href(url, target);
}
};
this.clear_message_list = function()
{
this.env.messages = {};
this.show_contentframe(false);
if (this.message_list)
this.message_list.clear(true);
};
// send remote request to load message list
this.list_mailbox_remote = function(mbox, page, url)
{
var lock = this.set_busy(true, 'loading');
if (typeof url != 'object')
url = {};
url._layout = this.env.layout
url._mbox = mbox;
url._page = page;
this.http_request('list', url, lock);
this.update_state({ _mbox: mbox, _page: (page && page > 1 ? page : null) });
};
// removes messages that doesn't exists from list selection array
this.update_selection = function()
{
var list = this.message_list,
selected = list.selection,
rows = list.rows,
i, selection = [];
for (i in selected)
if (rows[selected[i]])
selection.push(selected[i]);
list.selection = selection;
// reset preview frame, if currently previewed message is not selected (has been removed)
try {
var win = this.get_frame_window(this.env.contentframe),
id = win.rcmail.env.uid;
if (id && !list.in_selection(id))
this.show_contentframe(false);
}
catch (e) {};
};
// expand all threads with unread children
this.expand_unread = function()
{
var r, tbody = this.message_list.tbody,
new_row = tbody.firstChild;
while (new_row) {
if (new_row.nodeType == 1 && (r = this.message_list.rows[new_row.uid]) && r.unread_children) {
this.message_list.expand_all(r);
this.set_unread_children(r.uid);
}
new_row = new_row.nextSibling;
}
return false;
};
// thread expanding/collapsing handler
this.expand_message_row = function(e, uid)
{
var row = this.message_list.rows[uid];
// handle unread_children/flagged_children mark
row.expanded = !row.expanded;
this.set_unread_children(uid);
this.set_flagged_children(uid);
row.expanded = !row.expanded;
this.message_list.expand_row(e, uid);
};
// message list expanding
this.expand_threads = function()
{
if (!this.env.threading || !this.env.autoexpand_threads || !this.message_list)
return;
switch (this.env.autoexpand_threads) {
case 2: this.expand_unread(); break;
case 1: this.message_list.expand_all(); break;
}
};
// Initializes threads indicators/expanders after list update
this.init_threads = function(roots, mbox)
{
// #1487752
if (mbox && mbox != this.env.mailbox)
return false;
for (var n=0, len=roots.length; n<len; n++)
this.add_tree_icons(roots[n]);
this.expand_threads();
};
// adds threads tree icons to the list (or specified thread)
this.add_tree_icons = function(root)
{
var i, l, r, n, len, pos, tmp = [], uid = [],
row, rows = this.message_list.rows;
if (root)
row = rows[root] ? rows[root].obj : null;
else
row = this.message_list.tbody.firstChild;
while (row) {
if (row.nodeType == 1 && (r = rows[row.uid])) {
if (r.depth) {
for (i=tmp.length-1; i>=0; i--) {
len = tmp[i].length;
if (len > r.depth) {
pos = len - r.depth;
if (!(tmp[i][pos] & 2))
tmp[i][pos] = tmp[i][pos] ? tmp[i][pos]+2 : 2;
}
else if (len == r.depth) {
if (!(tmp[i][0] & 2))
tmp[i][0] += 2;
}
if (r.depth > len)
break;
}
tmp.push(new Array(r.depth));
tmp[tmp.length-1][0] = 1;
uid.push(r.uid);
}
else {
if (tmp.length) {
for (i in tmp) {
this.set_tree_icons(uid[i], tmp[i]);
}
tmp = [];
uid = [];
}
if (root && row != rows[root].obj)
break;
}
}
row = row.nextSibling;
}
if (tmp.length) {
for (i in tmp) {
this.set_tree_icons(uid[i], tmp[i]);
}
}
};
// adds tree icons to specified message row
this.set_tree_icons = function(uid, tree)
{
var i, divs = [], html = '', len = tree.length;
for (i=0; i<len; i++) {
if (tree[i] > 2)
divs.push({'class': 'l3', width: 15});
else if (tree[i] > 1)
divs.push({'class': 'l2', width: 15});
else if (tree[i] > 0)
divs.push({'class': 'l1', width: 15});
// separator div
else if (divs.length && !divs[divs.length-1]['class'])
divs[divs.length-1].width += 15;
else
divs.push({'class': null, width: 15});
}
for (i=divs.length-1; i>=0; i--) {
if (divs[i]['class'])
html += '<div class="tree '+divs[i]['class']+'" />';
else
html += '<div style="width:'+divs[i].width+'px" />';
}
if (html)
$('#rcmtab'+this.html_identifier(uid, true)).html(html);
};
// update parent in a thread
this.update_thread_root = function(uid, flag)
{
if (!this.env.threading)
return;
var root = this.message_list.find_root(uid);
if (uid == root)
return;
var p = this.message_list.rows[root];
if (flag == 'read' && p.unread_children) {
p.unread_children--;
}
else if (flag == 'unread' && p.has_children) {
// unread_children may be undefined
p.unread_children = (p.unread_children || 0) + 1;
}
else if (flag == 'unflagged' && p.flagged_children) {
p.flagged_children--;
}
else if (flag == 'flagged' && p.has_children) {
p.flagged_children = (p.flagged_children || 0) + 1;
}
else {
return;
}
this.set_message_icon(root);
this.set_unread_children(root);
this.set_flagged_children(root);
};
// update thread indicators for all messages in a thread below the specified message
// return number of removed/added root level messages
this.update_thread = function(uid)
{
if (!this.env.threading || !this.message_list.rows[uid])
return 0;
var r, parent, count = 0,
list = this.message_list,
rows = list.rows,
row = rows[uid],
depth = rows[uid].depth,
roots = [];
if (!row.depth) // root message: decrease roots count
count--;
// update unread_children for thread root
if (row.depth && row.unread) {
parent = list.find_root(uid);
rows[parent].unread_children--;
this.set_unread_children(parent);
}
// update unread_children for thread root
if (row.depth && row.flagged) {
parent = list.find_root(uid);
rows[parent].flagged_children--;
this.set_flagged_children(parent);
}
parent = row.parent_uid;
// childrens
row = row.obj.nextSibling;
while (row) {
if (row.nodeType == 1 && (r = rows[row.uid])) {
if (!r.depth || r.depth <= depth)
break;
r.depth--; // move left
// reset width and clear the content of a tab, icons will be added later
$('#rcmtab'+r.id).width(r.depth * 15).html('');
if (!r.depth) { // a new root
count++; // increase roots count
r.parent_uid = 0;
if (r.has_children) {
// replace 'leaf' with 'collapsed'
$('#' + r.id + ' .leaf').first()
.attr('id', 'rcmexpando' + r.id)
.attr('class', (r.obj.style.display != 'none' ? 'expanded' : 'collapsed'))
.mousedown({uid: r.uid}, function(e) {
return ref.expand_message_row(e, e.data.uid);
});
r.unread_children = 0;
roots.push(r);
}
// show if it was hidden
if (r.obj.style.display == 'none')
$(r.obj).show();
}
else {
if (r.depth == depth)
r.parent_uid = parent;
if (r.unread && roots.length)
roots[roots.length-1].unread_children++;
}
}
row = row.nextSibling;
}
// update unread_children/flagged_children for roots
for (r=0; r<roots.length; r++) {
this.set_unread_children(roots[r].uid);
this.set_flagged_children(roots[r].uid);
}
return count;
};
this.delete_excessive_thread_rows = function()
{
var rows = this.message_list.rows,
tbody = this.message_list.tbody,
row = tbody.firstChild,
cnt = this.env.pagesize + 1;
while (row) {
if (row.nodeType == 1 && (r = rows[row.uid])) {
if (!r.depth && cnt)
cnt--;
if (!cnt)
this.message_list.remove_row(row.uid);
}
row = row.nextSibling;
}
};
// set message icon
this.set_message_icon = function(uid)
{
var css_class, label = '',
row = this.message_list.rows[uid];
if (!row)
return false;
if (row.icon) {
css_class = 'msgicon';
if (row.deleted) {
css_class += ' deleted';
label += this.get_label('deleted') + ' ';
}
else if (row.unread) {
css_class += ' unread';
label += this.get_label('unread') + ' ';
}
else if (row.unread_children)
css_class += ' unreadchildren';
if (row.msgicon == row.icon) {
if (row.replied) {
css_class += ' replied';
label += this.get_label('replied') + ' ';
}
if (row.forwarded) {
css_class += ' forwarded';
label += this.get_label('forwarded') + ' ';
}
css_class += ' status';
}
$(row.icon).attr('class', css_class).attr('title', label);
}
if (row.msgicon && row.msgicon != row.icon) {
label = '';
css_class = 'msgicon';
if (!row.unread && row.unread_children) {
css_class += ' unreadchildren';
}
if (row.replied) {
css_class += ' replied';
label += this.get_label('replied') + ' ';
}
if (row.forwarded) {
css_class += ' forwarded';
label += this.get_label('forwarded') + ' ';
}
$(row.msgicon).attr('class', css_class).attr('title', label);
}
if (row.flagicon) {
css_class = (row.flagged ? 'flagged' : 'unflagged');
label = this.get_label(css_class);
$(row.flagicon).attr('class', css_class)
.attr('aria-label', label)
.attr('title', label);
}
};
// set message status
this.set_message_status = function(uid, flag, status)
{
var row = this.message_list.rows[uid];
if (!row)
return false;
if (flag == 'unread') {
if (row.unread != status)
this.update_thread_root(uid, status ? 'unread' : 'read');
}
else if (flag == 'flagged') {
this.update_thread_root(uid, status ? 'flagged' : 'unflagged');
}
if ($.inArray(flag, ['unread', 'deleted', 'replied', 'forwarded', 'flagged']) > -1)
row[flag] = status;
};
// set message row status, class and icon
this.set_message = function(uid, flag, status)
{
var row = this.message_list && this.message_list.rows[uid];
if (!row)
return false;
if (flag)
this.set_message_status(uid, flag, status);
if ($.inArray(flag, ['unread', 'deleted', 'flagged']) > -1)
$(row.obj)[row[flag] ? 'addClass' : 'removeClass'](flag);
this.set_unread_children(uid);
this.set_message_icon(uid);
};
// sets unroot (unread_children) class of parent row
this.set_unread_children = function(uid)
{
var row = this.message_list.rows[uid];
if (row.parent_uid)
return;
var enable = !row.unread && row.unread_children && !row.expanded;
$(row.obj)[enable ? 'addClass' : 'removeClass']('unroot');
};
// sets flaggedroot (flagged_children) class of parent row
this.set_flagged_children = function(uid)
{
var row = this.message_list.rows[uid];
if (row.parent_uid)
return;
var enable = row.flagged_children && !row.expanded;
$(row.obj)[enable ? 'addClass' : 'removeClass']('flaggedroot');
};
// copy selected messages to the specified mailbox
this.copy_messages = function(mbox, event, uids)
{
if (mbox && typeof mbox === 'object') {
mbox = mbox.id;
}
else if (!mbox) {
uids = this.env.uid ? [this.env.uid] : this.message_list.get_selection();
return this.folder_selector(event, function(folder) {
ref.copy_messages(folder, null, uids);
});
}
// exit if current or no mailbox specified
if (!mbox || mbox == this.env.mailbox)
return;
var post_data = this.selection_post_data({_target_mbox: mbox, _uid: uids});
// exit if selection is empty
if (!post_data._uid)
return;
// send request to server
this.http_post('copy', post_data, this.display_message('copyingmessage', 'loading'));
};
// move selected messages to the specified mailbox
this.move_messages = function(mbox, event, uids)
{
if (mbox && typeof mbox === 'object') {
mbox = mbox.id;
}
else if (!mbox) {
uids = this.env.uid ? [this.env.uid] : this.message_list.get_selection();
return this.folder_selector(event, function(folder) {
ref.move_messages(folder, null, uids);
});
}
// exit if current or no mailbox specified
if (!mbox || (mbox == this.env.mailbox && !this.is_multifolder_listing()))
return;
var lock = false, post_data = this.selection_post_data({_target_mbox: mbox, _uid: uids});
// exit if selection is empty
if (!post_data._uid)
return;
// show wait message
if (this.env.action == 'show')
lock = this.set_busy(true, 'movingmessage');
else
this.show_contentframe(false);
// Hide message command buttons until a message is selected
this.enable_command(this.env.message_commands, false);
this.with_selected_messages('move', post_data, lock);
};
// delete selected messages from the current mailbox
this.delete_messages = function(event)
{
var list = this.message_list, trash = this.env.trash_mailbox;
// if config is set to flag for deletion
if (this.env.flag_for_deletion) {
this.mark_message('delete');
return false;
}
// if there isn't a defined trash mailbox or we are in it
else if (!trash || this.env.mailbox == trash)
this.permanently_remove_messages();
// we're in Junk folder and delete_junk is enabled
else if (this.env.delete_junk && this.env.junk_mailbox && this.env.mailbox == this.env.junk_mailbox)
this.permanently_remove_messages();
// if there is a trash mailbox defined and we're not currently in it
else {
// if shift was pressed delete it immediately
if ((list && list.modkey == SHIFT_KEY) || (event && rcube_event.get_modifier(event) == SHIFT_KEY)) {
this.confirm_dialog(this.get_label('deletemessagesconfirm'), 'delete', function() {
ref.permanently_remove_messages();
});
}
else
this.move_messages(trash);
}
return true;
};
// delete the selected messages permanently
this.permanently_remove_messages = function()
{
var post_data = this.selection_post_data();
// exit if selection is empty
if (!post_data._uid)
return;
this.show_contentframe(false);
this.with_selected_messages('delete', post_data);
};
// Send a specific move/delete request with UIDs of all selected messages
this.with_selected_messages = function(action, post_data, lock, http_action)
{
var count = 0, msg,
remove = (action == 'delete' || !this.is_multifolder_listing());
// update the list (remove rows, clear selection)
if (this.message_list) {
var n, len, id, root, roots = [],
selection = post_data._uid;
if (selection === '*')
selection = this.message_list.get_selection();
else if (typeof selection == 'string')
selection = selection.split(',');
for (n=0, len=selection.length; n<len; n++) {
id = selection[n];
if (this.env.threading) {
count += this.update_thread(id);
root = this.message_list.find_root(id);
if (root != id && $.inArray(root, roots) < 0) {
roots.push(root);
}
}
if (remove)
this.message_list.remove_row(id, (this.env.display_next && n == selection.length-1));
}
// make sure there are no selected rows
if (!this.env.display_next && remove)
this.message_list.clear_selection();
// update thread tree icons
for (n=0, len=roots.length; n<len; n++) {
this.add_tree_icons(roots[n]);
}
}
if (count < 0)
post_data._count = (count*-1);
// remove threads from the end of the list
else if (count > 0 && remove)
this.delete_excessive_thread_rows();
if (!remove)
post_data._refresh = 1;
if (!lock) {
msg = action == 'move' ? 'movingmessage' : 'deletingmessage';
lock = this.display_message(msg, 'loading');
}
// send request to server
this.http_post(http_action || action, post_data, lock);
};
// build post data for message delete/move/copy/flag requests
this.selection_post_data = function(data)
{
if (typeof(data) != 'object')
data = {};
if (!data._uid)
data._uid = this.env.uid ? [this.env.uid] : this.message_list.get_selection();
data._mbox = this.env.mailbox;
data._uid = this.uids_to_list(data._uid);
if (this.env.action)
data._from = this.env.action;
// also send search request to get the right messages
if (this.env.search_request)
data._search = this.env.search_request;
if (this.env.display_next && this.env.next_uid)
data._next_uid = this.env.next_uid;
return data;
};
// set a specific flag to one or more messages
this.mark_message = function(flag, uid)
{
var a_uids = [], r_uids = [], len, n, id,
list = this.message_list;
if (uid)
a_uids[0] = uid;
else if (this.env.uid)
a_uids[0] = this.env.uid;
else if (list)
a_uids = list.get_selection();
if (!list)
r_uids = a_uids;
else {
list.focus();
for (n=0, len=a_uids.length; n<len; n++) {
id = a_uids[n];
if ((flag == 'read' && list.rows[id].unread)
|| (flag == 'unread' && !list.rows[id].unread)
|| (flag == 'delete' && !list.rows[id].deleted)
|| (flag == 'undelete' && list.rows[id].deleted)
|| (flag == 'flagged' && !list.rows[id].flagged)
|| (flag == 'unflagged' && list.rows[id].flagged))
{
r_uids.push(id);
}
}
}
// nothing to do
if (!r_uids.length && !this.select_all_mode)
return;
switch (flag) {
case 'read':
case 'unread':
this.toggle_read_status(flag, r_uids);
break;
case 'delete':
case 'undelete':
this.toggle_delete_status(r_uids);
break;
case 'flagged':
case 'unflagged':
this.toggle_flagged_status(flag, a_uids);
break;
}
};
// set class to read/unread
this.toggle_read_status = function(flag, a_uids)
{
var i, len = a_uids.length,
post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: flag}),
lock = this.display_message('markingmessage', 'loading');
// mark all message rows as read/unread
for (i=0; i<len; i++)
this.set_message(a_uids[i], 'unread', (flag == 'unread' ? true : false));
this.http_post('mark', post_data, lock);
};
// set image to flagged or unflagged
this.toggle_flagged_status = function(flag, a_uids)
{
var i, len = a_uids.length,
win = this.env.contentframe ? this.get_frame_window(this.env.contentframe) : window,
post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: flag}),
lock = this.display_message('markingmessage', 'loading');
// mark all message rows as flagged/unflagged
for (i=0; i<len; i++)
this.set_message(a_uids[i], 'flagged', (flag == 'flagged' ? true : false));
$(win.document.body)[flag == 'flagged' ? 'addClass' : 'removeClass']('status-flagged');
this.http_post('mark', post_data, lock);
};
// mark all message rows as deleted/undeleted
this.toggle_delete_status = function(a_uids)
{
var i, uid, all_deleted = true,
len = a_uids.length,
rows = this.message_list ? this.message_list.rows : {};
if (len == 1) {
if (!this.message_list || (rows[a_uids[0]] && !rows[a_uids[0]].deleted))
this.flag_as_deleted(a_uids);
else
this.flag_as_undeleted(a_uids);
return true;
}
for (i=0; i<len; i++) {
uid = a_uids[i];
if (rows[uid] && !rows[uid].deleted) {
all_deleted = false;
break;
}
}
if (all_deleted)
this.flag_as_undeleted(a_uids);
else
this.flag_as_deleted(a_uids);
return true;
};
this.flag_as_undeleted = function(a_uids)
{
var i, len = a_uids.length,
post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: 'undelete'}),
lock = this.display_message('markingmessage', 'loading');
for (i=0; i<len; i++)
this.set_message(a_uids[i], 'deleted', false);
this.http_post('mark', post_data, lock);
};
this.flag_as_deleted = function(a_uids)
{
var r_uids = [],
post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: 'delete'}),
lock = this.display_message('markingmessage', 'loading'),
list = this.message_list,
rows = list ? list.rows : {},
count = 0;
for (var i=0, len=a_uids.length; i<len; i++) {
uid = a_uids[i];
if (rows[uid]) {
if (rows[uid].unread)
r_uids[r_uids.length] = uid;
if (this.env.skip_deleted) {
count += this.update_thread(uid);
list.remove_row(uid, (this.env.display_next && i == list.get_selection(false).length-1));
}
else
this.set_message(uid, 'deleted', true);
}
}
// make sure there are no selected rows
if (this.env.skip_deleted && list) {
if (!this.env.display_next || !list.rowcount)
list.clear_selection();
if (count < 0)
post_data._count = (count*-1);
else if (count > 0)
// remove threads from the end of the list
this.delete_excessive_thread_rows();
}
// set of messages to mark as seen
if (r_uids.length)
post_data._ruid = this.uids_to_list(r_uids);
if (this.env.skip_deleted && this.env.display_next && this.env.next_uid)
post_data._next_uid = this.env.next_uid;
this.http_post('mark', post_data, lock);
};
// flag as read without mark request (called from backend)
// argument should be a coma-separated list of uids
this.flag_deleted_as_read = function(uids)
{
var uid, i, len,
rows = this.message_list ? this.message_list.rows : {};
if (typeof uids == 'string')
uids = uids.split(',');
for (i=0, len=uids.length; i<len; i++) {
uid = uids[i];
if (rows[uid])
this.set_message(uid, 'unread', false);
}
};
// Converts array of message UIDs to comma-separated list for use in URL
// with select_all mode checking
this.uids_to_list = function(uids)
{
return this.select_all_mode ? '*' : ($.isArray(uids) ? uids.join(',') : uids);
};
// Sets title of the delete button
this.set_button_titles = function()
{
var label = 'deletemessage';
if (!this.env.flag_for_deletion
&& this.env.trash_mailbox && this.env.mailbox != this.env.trash_mailbox
&& (!this.env.delete_junk || !this.env.junk_mailbox || this.env.mailbox != this.env.junk_mailbox)
)
label = 'movemessagetotrash';
this.set_alttext('delete', label);
};
// Initialize input element for list page jump
this.init_pagejumper = function(element)
{
$(element).addClass('rcpagejumper')
.on('focus', function(e) {
// create and display popup with page selection
var i, html = '';
for (i = 1; i <= ref.env.pagecount; i++)
html += '<li>' + i + '</li>';
html = '<ul class="toolbarmenu">' + html + '</ul>';
if (!ref.pagejump) {
ref.pagejump = $('<div id="pagejump-selector" class="popupmenu"></div>')
.appendTo(document.body)
.on('click', 'li', function() {
if (!ref.busy)
$(element).val($(this).text()).change();
});
}
if (ref.pagejump.data('count') != i)
ref.pagejump.html(html);
ref.pagejump.attr('rel', '#' + this.id).data('count', i);
// display page selector
ref.show_menu('pagejump-selector', true, e);
$(this).keydown();
})
// keyboard navigation
.on('keydown keyup click', function(e) {
var current, selector = $('#pagejump-selector'),
ul = $('ul', selector),
list = $('li', ul),
height = ul.height(),
p = parseInt(this.value);
if (e.which != 27 && e.which != 9 && e.which != 13 && !selector.is(':visible'))
return ref.show_menu('pagejump-selector', true, e);
if (e.type == 'keydown') {
// arrow-down
if (e.which == 40) {
if (list.length > p)
this.value = (p += 1);
}
// arrow-up
else if (e.which == 38) {
if (p > 1 && list.length > p - 1)
this.value = (p -= 1);
}
// enter
else if (e.which == 13) {
return $(this).change();
}
// esc, tab
else if (e.which == 27 || e.which == 9) {
ref.hide_menu('pagejump-selector', e);
return $(element).val(ref.env.current_page);
}
}
$('li.selected', ul).removeClass('selected');
if ((current = $(list[p - 1])).length) {
current.addClass('selected');
$('#pagejump-selector').scrollTop(((ul.height() / list.length) * (p - 1)) - selector.height() / 2);
}
})
.on('change', function(e) {
// go to specified page
var p = parseInt(this.value);
if (p && p != ref.env.current_page && !ref.busy) {
ref.hide_menu('pagejump-selector', e);
ref.list_page(p);
}
});
};
// Update page-jumper state on list updates
this.update_pagejumper = function()
{
$('input.rcpagejumper').val(this.env.current_page).prop('disabled', this.env.pagecount < 2);
};
// check for mailvelope API
this.check_mailvelope = function(action)
{
if (typeof window.mailvelope !== 'undefined') {
this.mailvelope_load(action);
}
else {
$(window).on('mailvelope', function() {
ref.mailvelope_load(action);
});
}
};
// Load Mailvelope functionality (and initialize keyring if needed)
this.mailvelope_load = function(action)
{
if (this.env.browser_capabilities)
this.env.browser_capabilities['pgpmime'] = 1;
var keyring = this.env.user_id,
fn = function(kr) {
ref.mailvelope_keyring = kr;
ref.mailvelope_init(action, kr);
};
mailvelope.getVersion().then(function(v) {
mailvelope.VERSION = v;
mailvelope.VERSION_MAJOR = Math.floor(parseFloat(v));
return mailvelope.getKeyring(keyring);
}).then(fn, function(err) {
// attempt to create a new keyring for this app/user
mailvelope.createKeyring(keyring).then(fn, function(err) {
console.error(err);
});
});
};
// Initializes Mailvelope editor or display container
this.mailvelope_init = function(action, keyring)
{
if (!window.mailvelope)
return;
if (action == 'show' || action == 'preview' || action == 'print') {
// decrypt text body
if (this.env.is_pgp_content) {
var data = $(this.env.is_pgp_content).text();
ref.mailvelope_display_container(this.env.is_pgp_content, data, keyring);
}
// load pgp/mime message and pass it to the mailvelope display container
else if (this.env.pgp_mime_part) {
var msgid = this.display_message('loadingdata', 'loading'),
selector = this.env.pgp_mime_container;
$.ajax({
type: 'GET',
url: this.url('get', { '_mbox': this.env.mailbox, '_uid': this.env.uid, '_part': this.env.pgp_mime_part }),
error: function(o, status, err) {
ref.http_error(o, status, err, msgid);
},
success: function(data) {
ref.mailvelope_display_container(selector, data, keyring, msgid);
}
});
}
}
else if (action == 'compose') {
this.env.compose_commands.push('compose-encrypted');
var sign_supported = mailvelope.VERSION_MAJOR >= 2;
var is_html = $('[name="_is_html"]').val() > 0;
if (sign_supported)
this.env.compose_commands.push('compose-encrypted-signed');
if (this.env.pgp_mime_message) {
// fetch PGP/Mime part and open load into Mailvelope editor
var lock = this.set_busy(true, this.get_label('loadingdata'));
$.ajax({
type: 'GET',
url: this.url('get', this.env.pgp_mime_message),
error: function(o, status, err) {
ref.http_error(o, status, err, lock);
ref.enable_command('compose-encrypted', !is_html);
if (sign_supported)
ref.enable_command('compose-encrypted-signed', !is_html);
},
success: function(data) {
ref.set_busy(false, null, lock);
if (is_html) {
ref.command('toggle-editor', {html: false, noconvert: true});
$('#' + ref.env.composebody).val('');
}
ref.compose_encrypted({ quotedMail: data });
ref.enable_command('compose-encrypted', true);
ref.enable_command('compose-encrypted-signed', false);
}
});
}
else {
// enable encrypted compose toggle
this.enable_command('compose-encrypted', !is_html);
if (sign_supported)
this.enable_command('compose-encrypted-signed', !is_html);
}
// make sure to disable encryption button after toggling editor into HTML mode
this.addEventListener('actionafter', function(args) {
if (args.ret && args.action == 'toggle-editor') {
ref.enable_command('compose-encrypted', !args.props.html);
if (sign_supported)
ref.enable_command('compose-encrypted-signed', !args.props.html);
}
});
} else if (action == 'edit-identity') {
ref.mailvelope_identity_keygen();
}
};
// handler for the 'compose-encrypted-signed' command
this.compose_encrypted_signed = function(props)
{
props = props || {};
props.signMsg = true;
this.compose_encrypted(props);
};
// handler for the 'compose-encrypted' command
this.compose_encrypted = function(props)
{
var options, container = $('#' + this.env.composebody).parent();
// remove Mailvelope editor if active
if (ref.mailvelope_editor) {
ref.mailvelope_editor = null;
ref.compose_skip_unsavedcheck = false;
ref.set_button('compose-encrypted', 'act');
container.removeClass('mailvelope')
.find('iframe:not([aria-hidden=true])').remove();
$('#' + ref.env.composebody).show();
$("[name='_pgpmime']").remove();
// disable commands that operate on the compose body
ref.enable_command('spellcheck', 'insert-sig', 'toggle-editor', 'insert-response', 'save-response', true);
ref.triggerEvent('compose-encrypted', { active:false });
}
// embed Mailvelope editor container
else {
if (this.spellcheck_state())
this.editor.spellcheck_stop();
if (props.quotedMail) {
options = { quotedMail: props.quotedMail, quotedMailIndent: false };
}
else {
options = { predefinedText: $('#' + this.env.composebody).val() };
}
if (props.signMsg) {
options.signMsg = props.signMsg;
}
if (this.env.compose_mode == 'reply') {
options.quotedMailIndent = true;
options.quotedMailHeader = this.env.compose_reply_header;
}
mailvelope.createEditorContainer('#' + container.attr('id'), ref.mailvelope_keyring, options).then(function(editor) {
ref.mailvelope_editor = editor;
ref.compose_skip_unsavedcheck = true;
ref.set_button('compose-encrypted', 'sel');
container.addClass('mailvelope');
$('#' + ref.env.composebody).hide();
// disable commands that operate on the compose body
ref.enable_command('spellcheck', 'insert-sig', 'toggle-editor', 'insert-response', 'save-response', false);
ref.triggerEvent('compose-encrypted', { active:true });
// notify user about loosing attachments
if (ref.env.attachments && !$.isEmptyObject(ref.env.attachments)) {
ref.alert_dialog(ref.get_label('encryptnoattachments'));
$.each(ref.env.attachments, function(name, attach) {
ref.remove_from_attachment_list(name);
});
}
}, function(err) {
console.error(err);
console.log(options);
});
}
};
// callback to replace the message body with the full armored
this.mailvelope_submit_messageform = function(draft, saveonly)
{
// get recipients
var recipients = [];
$.each(['to', 'cc', 'bcc'], function(i,field) {
var pos, rcpt, val = $.trim($('[name="_' + field + '"]').val());
while (val.length && rcube_check_email(val, true)) {
rcpt = RegExp.$2;
recipients.push(rcpt);
val = val.substr(val.indexOf(rcpt) + rcpt.length + 1).replace(/^\s*,\s*/, '');
}
});
// check if we have keys for all recipients
var isvalid = recipients.length > 0;
ref.mailvelope_keyring.validKeyForAddress(recipients).then(function(status) {
var missing_keys = [];
$.each(status, function(k,v) {
if (v === false) {
isvalid = false;
missing_keys.push(k);
}
});
// list recipients with missing keys
if (!isvalid && missing_keys.length) {
// display dialog with missing keys
ref.simple_dialog(
ref.get_label('nopubkeyfor').replace('$email', missing_keys.join(', ')) +
'<p>' + ref.get_label('searchpubkeyservers') + '</p>',
'encryptedsendialog',
function() {
ref.mailvelope_search_pubkeys(missing_keys, function() {
return true; // close dialog
});
},
{button: 'search'}
);
return false;
}
if (!isvalid) {
if (!recipients.length) {
ref.alert_dialog(ref.get_label('norecipientwarning'), function() {
$("[name='_to']").focus();
});
}
return false;
}
// add sender identity to recipients to be able to decrypt our very own message
var senders = [], selected_sender = ref.env.identities[$("[name='_from'] option:selected").val()];
$.each(ref.env.identities, function(k, sender) {
senders.push(sender.email);
});
ref.mailvelope_keyring.validKeyForAddress(senders).then(function(status) {
valid_sender = null;
$.each(status, function(k,v) {
if (v !== false) {
valid_sender = k;
if (valid_sender == selected_sender) {
return false; // break
}
}
});
if (!valid_sender) {
if (!confirm(ref.get_label('nopubkeyforsender'))) {
return false;
}
}
recipients.push(valid_sender);
ref.mailvelope_editor.encrypt(recipients).then(function(armored) {
// all checks passed, send message
var form = ref.gui_objects.messageform,
hidden = $("[name='_pgpmime']", form),
msgid = ref.set_busy(true, draft || saveonly ? 'savingmessage' : 'sendingmessage');
form.target = ref.get_save_target();
form._draft.value = draft ? '1' : '';
form.action = ref.add_url(form.action, '_unlock', msgid);
form.action = ref.add_url(form.action, '_framed', 1);
if (saveonly) {
form.action = ref.add_url(form.action, '_saveonly', 1);
}
// send pgp conent via hidden field
if (!hidden.length) {
hidden = $('<input type="hidden" name="_pgpmime">').appendTo(form);
}
hidden.val(armored);
form.submit();
}, function(err) {
console.log(err);
}); // mailvelope_editor.encrypt()
}, function(err) {
console.error(err);
}); // mailvelope_keyring.validKeyForAddress(senders)
}, function(err) {
console.error(err);
}); // mailvelope_keyring.validKeyForAddress(recipients)
return false;
};
// wrapper for the mailvelope.createDisplayContainer API call
this.mailvelope_display_container = function(selector, data, keyring, msgid)
{
var error_handler = function(error) {
// remove mailvelope frame with the error message
$(selector + ' > iframe').remove();
ref.hide_message(msgid);
ref.display_message(error.message, 'error');
};
mailvelope.createDisplayContainer(selector, data, keyring, { showExternalContent: this.env.safemode }).then(function(status) {
if (status.error && status.error.message) {
return error_handler(status.error);
}
ref.hide_message(msgid);
$(selector).addClass('mailvelope').children().not('iframe').hide();
// on success we can remove encrypted part from the attachments list
if (ref.env.pgp_mime_part)
$('#attach' + ref.env.pgp_mime_part).remove();
setTimeout(function() { $(window).resize(); }, 10);
}, error_handler);
};
// subroutine to query keyservers for public keys
this.mailvelope_search_pubkeys = function(emails, resolve, import_handler)
{
// query with publickey.js
var deferreds = [],
pk = new PublicKey(),
lock = ref.display_message('', 'loading');
$.each(emails, function(i, email) {
var d = $.Deferred();
pk.search(email, function(results, errorCode) {
if (errorCode !== null) {
// rejecting would make all fail
// d.reject(email);
d.resolve([email]);
}
else {
d.resolve([email].concat(results));
}
});
deferreds.push(d);
});
$.when.apply($, deferreds).then(function() {
var missing_keys = [],
key_selection = [];
// alanyze results of all queries
$.each(arguments, function(i, result) {
var email = result.shift();
if (!result.length) {
missing_keys.push(email);
}
else {
key_selection = key_selection.concat(result);
}
});
ref.hide_message(lock);
resolve(true);
// show key import dialog
if (key_selection.length) {
ref.mailvelope_key_import_dialog(key_selection, import_handler);
}
// some keys could not be found
if (missing_keys.length) {
ref.display_message(ref.get_label('nopubkeyfor').replace('$email', missing_keys.join(', ')), 'warning');
}
}).fail(function() {
console.error('Pubkey lookup failed with', arguments);
ref.hide_message(lock);
ref.display_message('pubkeysearcherror', 'error');
resolve(false);
});
};
// list the given public keys in a dialog with options to import
// them into the local Maivelope keyring
this.mailvelope_key_import_dialog = function(candidates, import_handler)
{
var ul = $('<div>').addClass('listing pgpkeyimport');
$.each(candidates, function(i, keyrec) {
var li = $('<div>').addClass('key');
if (keyrec.revoked) li.addClass('revoked');
if (keyrec.disabled) li.addClass('disabled');
if (keyrec.expired) li.addClass('expired');
li.append($('<label>').addClass('keyid').text(ref.get_label('keyid')));
li.append($('<a>').text(keyrec.keyid.substr(-8).toUpperCase())
.attr('href', keyrec.info)
.attr('target', '_blank')
.attr('tabindex', '-1'));
li.append($('<label>').addClass('keylen').text(ref.get_label('keylength')));
li.append($('<span>').text(keyrec.keylen));
if (keyrec.expirationdate) {
li.append($('<label>').addClass('keyexpired').text(ref.get_label('keyexpired')));
li.append($('<span>').text(new Date(keyrec.expirationdate * 1000).toDateString()));
}
if (keyrec.revoked) {
li.append($('<span>').addClass('keyrevoked').text(ref.get_label('keyrevoked')));
}
var ul_ = $('<ul>').addClass('uids');
$.each(keyrec.uids, function(j, uid) {
var li_ = $('<li>').addClass('uid');
if (uid.revoked) li_.addClass('revoked');
if (uid.disabled) li_.addClass('disabled');
if (uid.expired) li_.addClass('expired');
ul_.append(li_.text(uid.uid));
});
li.append(ul_);
li.append($('<button>')
.attr('rel', keyrec.keyid)
.text(ref.get_label('import'))
.addClass('button import importkey')
.prop('disabled', keyrec.revoked || keyrec.disabled || keyrec.expired));
ul.append(li);
});
// display dialog with missing keys
ref.simple_dialog(
$('<div>')
.append($('<p>').html(ref.get_label('encryptpubkeysfound')))
.append(ul),
ref.get_label('importpubkeys'),
null,
{cancel_label: 'close', cancel_button: 'close'}
);
// delegate handler for import button clicks
ul.on('click', 'button.importkey', function() {
var btn = $(this),
keyid = btn.attr('rel'),
pk = new PublicKey(),
lock = ref.display_message('', 'loading');
// fetch from keyserver and import to Mailvelope keyring
pk.get(keyid, function(armored, errorCode) {
ref.hide_message(lock);
if (errorCode) {
ref.display_message('keyservererror', 'error');
return;
}
if (import_handler) {
import_handler(armored);
return;
}
// import to keyring
ref.mailvelope_keyring.importPublicKey(armored).then(function(status) {
if (status === 'REJECTED') {
// ref.alert_dialog(ref.get_label('Key import was rejected'));
}
else {
var $key = keyid.substr(-8).toUpperCase();
btn.closest('.key').fadeOut();
ref.display_message(ref.get_label('keyimportsuccess').replace('$key', $key), 'confirmation');
}
}, function(err) {
console.log(err);
});
});
});
};
// enable key management for identity
this.mailvelope_identity_keygen = function()
{
var container = $(this.gui_objects.editform).find('.identity-encryption').first();
var identity_email = $.trim($(this.gui_objects.editform).find('.ff_email').val());
if (!container.length || !identity_email || !this.mailvelope_keyring.createKeyGenContainer)
return;
var key_fingerprint;
this.mailvelope_keyring.validKeyForAddress([identity_email])
.then(function(keys) {
var private_keys = [];
if (keys && keys[identity_email] && Array.isArray(keys[identity_email].keys)) {
var checks = [];
for (var j=0; j < keys[identity_email].keys.length; j++) {
checks.push((function(key) {
return ref.mailvelope_keyring.hasPrivateKey(key.fingerprint)
.then(function(found) {
if (found) {
private_keys.push(key);
}
});
})(keys[identity_email].keys[j]));
}
return Promise.all(checks)
.then(function() {
return private_keys;
});
}
return private_keys;
}).then(function(private_keys) {
var content = container.find('.identity-encryption-block').empty();
if (private_keys && private_keys.length) {
// show private key information
$('<p>').text(ref.get_label('encryptionprivkeysinmailvelope').replace('$nr', private_keys.length)).appendTo(content);
var ul = $('<ul>').addClass('keylist').appendTo(content);
$.each(private_keys, function(i, key) {
$('<li>').appendTo(ul)
.append($('<strong>').addClass('fingerprint').text(String(key.fingerprint).toUpperCase()))
.append($('<span>').addClass('identity').text('<' + identity_email + '> '));
});
} else {
$('<p>').text(ref.get_label('encryptionnoprivkeysinmailvelope')).appendTo(content);
}
// show button to create a new key
$('<button>')
.attr('type', 'button')
.addClass('button create')
.text(ref.get_label('encryptioncreatekey'))
.appendTo(content)
.on('click', function() { ref.mailvelope_show_keygen_container(content, identity_email); });
$('<span>').addClass('space').html('&nbsp;').appendTo(content);
$('<button>')
.attr('type', 'button')
.addClass('button settings')
.text(ref.get_label('openmailvelopesettings'))
.appendTo(content)
.on('click', function() { ref.mailvelope_keyring.openSettings(); });
container.show();
ref.triggerEvent('identity-encryption-show', { container: container });
})
.catch(function(err) {
console.error('Mailvelope keyring error', err);
})
};
// start pgp key generation using Mailvelope
this.mailvelope_show_keygen_container = function(container, identity_email)
{
var cid = new Date().getTime();
var user_id = {email: identity_email, fullName: $.trim($(ref.gui_objects.editform).find('.ff_name').val())};
var options = {userIds: [user_id], keySize: 4096};
$('<div>').attr('id', 'mailvelope-keygen-container-' + cid)
.css({height: '245px', marginBottom: '10px'})
.appendTo(container.empty());
this.mailvelope_keyring.createKeyGenContainer('#mailvelope-keygen-container-' + cid, options)
.then(function(generator) {
if (generator instanceof Error) {
throw generator;
}
// append button to start key generation
$('<button>')
.attr('type', 'button')
.addClass('button mainaction generate')
.text(ref.get_label('generate'))
.appendTo(container)
.on('click', function() {
var btn = $(this).prop('disabled', true);
generator.generate()
.then(function(result) {
if (typeof result === 'string' && result.indexOf('BEGIN PGP') > 0) {
ref.display_message(ref.get_label('keypaircreatesuccess').replace('$identity', identity_email), 'confirmation');
// reset keygen view
ref.mailvelope_identity_keygen();
}
})
.catch(function(err) {
debugger;
ref.display_message(err.message || 'errortitle', 'error');
btn.prop('disabled', false);
});
});
$('<span>').addClass('space').html('&nbsp;').appendTo(container);
$('<button>')
.attr('type', 'button')
.addClass('button cancel')
.text(ref.get_label('cancel'))
.appendTo(container)
.on('click', function() { ref.mailvelope_identity_keygen(); });
ref.triggerEvent('identity-encryption-update', { container: container });
})
.catch(function(err) {
ref.display_message('errortitle', 'error');
// start over
ref.mailvelope_identity_keygen();
});
};
/*********************************************************/
/********* mailbox folders methods *********/
/*********************************************************/
this.expunge_mailbox = function(mbox)
{
var lock, post_data = {_mbox: mbox};
// lock interface if it's the active mailbox
if (mbox == this.env.mailbox) {
lock = this.set_busy(true, 'loading');
post_data._reload = 1;
if (this.env.search_request)
post_data._search = this.env.search_request;
}
// send request to server
this.http_post('expunge', post_data, lock);
};
this.purge_mailbox = function(mbox)
{
this.confirm_dialog(this.get_label('purgefolderconfirm'), 'delete', function() {
var lock, post_data = {_mbox: mbox};
// lock interface if it's the active mailbox
if (mbox == ref.env.mailbox) {
lock = ref.set_busy(true, 'loading');
post_data._reload = 1;
}
// send request to server
ref.http_post('purge', post_data, lock);
});
return false;
};
// test if purge command is allowed
this.purge_mailbox_test = function()
{
return (this.env.exists && (
this.env.mailbox == this.env.trash_mailbox
|| this.env.mailbox == this.env.junk_mailbox
|| this.env.mailbox.startsWith(this.env.trash_mailbox + this.env.delimiter)
|| this.env.mailbox.startsWith(this.env.junk_mailbox + this.env.delimiter)
));
};
// Mark all messages as read in:
// - selected folder (mode=cur)
// - selected folder and its subfolders (mode=sub)
// - all folders (mode=all)
this.mark_all_read = function(mbox, mode)
{
var state, content, nodes = [],
list = this.message_list,
folder = mbox || this.env.mailbox,
post_data = {_uid: '*', _flag: 'read', _mbox: folder, _folders: mode};
if (typeof mode != 'string') {
state = this.mark_all_read_state(folder);
if (!state)
return;
if (state > 1) {
// build content of the dialog
$.each({cur: 1, sub: 2, all: 4}, function(i, v) {
var id = 'readallmode' + i,
label = $('<label>').attr('for', id).text(ref.get_label('folders-' + i)),
input = $('<input>').attr({type: 'radio', value: i, name: 'mode', id: id, disabled: !(state & v)});
nodes.push($('<li>').append([input, label]));
});
content = $('<ul class="proplist">').append(nodes);
$('input:not([disabled])', content).first().attr('checked', true);
this.simple_dialog(content, this.get_label('markallread'),
function() {
ref.mark_all_read(folder, $('input:checked', content).val());
return true;
},
{button: 'mark', button_class: 'save'}
);
return;
}
post_data._folders = 'cur'; // only current folder has unread messages
}
// mark messages on the list
$.each(list ? list.rows : [], function(uid, row) {
if (!row.unread)
return;
var mbox = ref.env.messages[uid].mbox;
if (mode == 'all' || mbox == ref.env.mailbox
|| (mode == 'sub' && mbox.startsWith(ref.env.mailbox + ref.env.delimiter))
) {
ref.set_message(uid, 'unread', false);
}
});
// send the request
this.http_post('mark', post_data, this.display_message('markingmessage', 'loading'));
};
// Enable/disable mark-all-read action depending on folders state
this.mark_all_read_state = function(mbox)
{
var state = 0,
li = this.treelist.get_item(mbox || this.env.mailbox),
folder_item = $(li).is('.unread') ? 1 : 0,
subfolder_items = $('li.unread', li).length,
all_items = $('li.unread', ref.gui_objects.folderlist).length;
state += folder_item;
state += subfolder_items ? 2 : 0;
state += all_items > folder_item + subfolder_items ? 4 : 0;
this.enable_command('mark-all-read', state > 0);
return state;
};
// Display "bounce message" dialog
this.bounce = function(props, obj, event)
{
// get message uid and folder
var uid = this.get_single_uid(),
url = this.url('bounce', {_framed: 1, _uid: uid, _mbox: this.get_message_mailbox(uid)}),
dialog = $('<iframe>').attr('src', url),
get_form = function() {
var rc = $('iframe', dialog)[0].contentWindow.rcmail;
return {rc: rc, form: rc.gui_objects.messageform};
},
post_func = function() {
var post = {}, form = get_form();
$.each($(form.form).serializeArray(), function() { post[this.name] = this.value; });
post._uid = form.rc.env.uid;
post._mbox = form.rc.env.mailbox;
delete post._action;
delete post._task;
if (post._to || post._cc || post._bcc) {
ref.http_post('bounce', post, ref.set_busy(true, 'sendingmessage'));
dialog.dialog('close');
}
},
submit_func = function() {
var form = get_form();
if (typeof form.form != 'object')
return false;
if (!form.rc.check_compose_address_fields(post_func, form.form))
return false;
return post_func();
};
this.hide_menu('forwardmenu', event);
dialog = this.simple_dialog(dialog, this.gettext('bouncemsg'), submit_func, {
button: 'bounce',
width: 400,
height: 300
});
return true;
};
/*********************************************************/
/********* login form methods *********/
/*********************************************************/
// handler for keyboard events on the _user field
this.login_user_keyup = function(e)
{
var key = rcube_event.get_keycode(e),
passwd = $('#rcmloginpwd');
// enter
if (key == 13 && passwd.length && !passwd.val()) {
passwd.focus();
return rcube_event.cancel(e);
}
return true;
};
/*********************************************************/
/********* message compose methods *********/
/*********************************************************/
this.open_compose_step = function(p)
{
var url = this.url('mail/compose', p);
// open new compose window
if (this.env.compose_extwin && !this.env.extwin) {
this.open_window(url);
}
else {
this.redirect(url);
if (this.env.extwin)
window.resizeTo(Math.max(this.env.popup_width, $(window).width()), $(window).height() + 24);
}
};
// init message compose form: set focus and eventhandlers
this.init_messageform = function()
{
if (!this.gui_objects.messageform)
return false;
var elem, pos,
input_from = $("[name='_from']"),
input_to = $("[name='_to']"),
input_subject = $("[name='_subject']"),
input_message = $("[name='_message']").get(0),
html_mode = $("[name='_is_html']").val() == '1',
opener_rc = this.opener();
// close compose step in opener
if (opener_rc && opener_rc.env.action == 'compose') {
setTimeout(function(){
if (opener.history.length > 1)
opener.history.back();
else
opener_rc.redirect(opener_rc.get_task_url('mail'));
}, 100);
this.env.opened_extwin = true;
}
if (!html_mode) {
// On Back button Chrome will overwrite textarea with old content
// causing e.g. the same signature is added twice (#5809)
if (input_message.value && input_message.defaultValue !== undefined)
input_message.value = input_message.defaultValue;
pos = this.env.top_posting && this.env.compose_mode ? 0 : input_message.value.length;
// add signature according to selected identity
// if we have HTML editor, signature is added in a callback
if (input_from.prop('type') == 'select-one') {
// for some reason the caret initially is not at pos=0 in Firefox 51 (#5628)
this.set_caret_pos(input_message, 0);
this.change_identity(input_from[0]);
}
// set initial cursor position
this.set_caret_pos(input_message, pos);
// scroll to the bottom of the textarea (#1490114)
if (pos) {
$(input_message).scrollTop(input_message.scrollHeight);
}
}
// check for locally stored compose data
if (this.env.save_localstorage)
this.compose_restore_dialog(0, html_mode)
if (input_to.val() == '')
elem = input_to;
else if (input_subject.val() == '')
elem = input_subject;
else if (input_message)
elem = input_message;
this.env.compose_focus_elem = this.init_messageform_inputs(elem);
// get summary of all field values
this.compose_field_hash(true);
// start the auto-save timer
this.auto_save_start();
};
// init autocomplete events on compose form inputs
this.init_messageform_inputs = function(focused)
{
var i, ac_props,
input_to = $("[name='_to']"),
ac_fields = ['cc', 'bcc', 'replyto', 'followupto'];
// configure parallel autocompletion
if (this.env.autocomplete_threads > 0) {
ac_props = {
threads: this.env.autocomplete_threads,
sources: this.env.autocomplete_sources
};
}
// init live search events
this.init_address_input_events(input_to, ac_props);
for (i in ac_fields) {
this.init_address_input_events($("[name='_"+ac_fields[i]+"']"), ac_props);
}
if (!focused)
focused = input_to;
// focus first empty element (and return it)
return $(focused).focus().get(0);
};
this.compose_restore_dialog = function(j, html_mode)
{
var i, key, formdata, index = this.local_storage_get_item('compose.index', []);
var show_next = function(i) {
if (++i < index.length)
ref.compose_restore_dialog(i, html_mode)
}
for (i = j || 0; i < index.length; i++) {
key = index[i];
formdata = this.local_storage_get_item('compose.' + key, null, true);
if (!formdata) {
continue;
}
// restore saved copy of current compose_id
if (formdata.changed && key == this.env.compose_id) {
this.restore_compose_form(key, html_mode);
break;
}
// skip records from 'other' drafts
if (this.env.draft_id && formdata.draft_id && formdata.draft_id != this.env.draft_id) {
continue;
}
// skip records on reply
if (this.env.reply_msgid && formdata.reply_msgid != this.env.reply_msgid) {
continue;
}
// show dialog asking to restore the message
if (formdata.changed && formdata.session != this.env.session_id) {
this.show_popup_dialog(
this.get_label('restoresavedcomposedata')
.replace('$date', new Date(formdata.changed).toLocaleString())
.replace('$subject', formdata._subject)
.replace(/\n/g, '<br/>'),
this.get_label('restoremessage'),
[{
text: this.get_label('restore'),
'class': 'mainaction restore',
click: function(){
ref.restore_compose_form(key, html_mode);
ref.remove_compose_data(key); // remove old copy
ref.save_compose_form_local(); // save under current compose_id
$(this).dialog('close');
}
},
{
text: this.get_label('delete'),
'class': 'delete',
click: function(){
ref.remove_compose_data(key);
$(this).dialog('close');
show_next(i);
}
},
{
text: this.get_label('ignore'),
'class': 'cancel',
click: function(){
$(this).dialog('close');
show_next(i);
}
}]
);
break;
}
}
}
this.init_address_input_events = function(obj, props)
{
obj.keydown(function(e) { return ref.ksearch_keydown(e, this, props); })
.attr({autocomplete: 'off', 'aria-autocomplete': 'list', 'aria-expanded': 'false', role: 'combobox'});
// hide the popup on any click
var callback = function() { ref.ksearch_hide(); };
$(document).on('click', callback);
// and on scroll (that cannot be jQuery.on())
document.addEventListener('scroll', callback, true);
};
this.submit_messageform = function(draft, saveonly)
{
var form = this.gui_objects.messageform;
if (!form)
return;
// the message has been sent but not saved, ask the user what to do
if (!saveonly && this.env.is_sent) {
return this.simple_dialog(this.get_label('messageissent'), '', // TODO: dialog title
function() {
ref.submit_messageform(false, true);
return true;
}
);
}
// delegate sending to Mailvelope routine
if (this.mailvelope_editor) {
return this.mailvelope_submit_messageform(draft, saveonly);
}
// all checks passed, send message
var msgid = this.set_busy(true, draft || saveonly ? 'savingmessage' : 'sendingmessage'),
lang = this.spellcheck_lang(),
files = [];
// send files list
$('li', this.gui_objects.attachmentlist).each(function() { files.push(this.id.replace(/^rcmfile/, '')); });
$('[name="_attachments"]', form).val(files.join());
form.target = this.get_save_target();
form._draft.value = draft ? '1' : '';
form.action = this.add_url(form.action, '_unlock', msgid);
form.action = this.add_url(form.action, '_framed', 1);
if (lang)
form.action = this.add_url(form.action, '_lang', lang);
if (saveonly)
form.action = this.add_url(form.action, '_saveonly', 1);
// register timer to notify about connection timeout
this.submit_timer = setTimeout(function(){
ref.set_busy(false, null, msgid);
ref.display_message('requesttimedout', 'error');
}, this.env.request_timeout * 1000);
form.submit();
};
this.compose_recipient_select = function(list)
{
var id, n, recipients = 0, selection = list.get_selection();
for (n=0; n < selection.length; n++) {
id = selection[n];
if (this.env.contactdata[id])
recipients++;
}
this.enable_command('add-recipient', recipients);
};
this.compose_add_recipient = function(field)
{
// find last focused field name
if (!field) {
field = $(this.env.focused_field).filter(':visible');
field = field.length ? field.attr('id').replace('_', '') : 'to';
}
var recipients = [], input = $('#_' + field), selection = this.contact_list.get_selection();
if (this.contact_list && selection.length) {
for (var id, n=0; n < selection.length; n++) {
id = selection[n];
if (id && this.env.contactdata[id]) {
recipients.push(this.env.contactdata[id]);
// group is added, expand it
if (id.charAt(0) == 'E' && this.env.contactdata[id].indexOf('@') < 0 && input.length) {
var gid = id.substr(1);
this.group2expand[gid] = { name:this.env.contactdata[id], input:input.get(0) };
this.http_request('group-expand', {_source: this.env.source, _gid: gid}, false);
}
}
}
}
if (recipients.length && input.length) {
var oldval = input.val();
if (oldval && !/[,;]\s*$/.test(oldval))
oldval += ', ';
input.val(oldval + recipients.join(', ') + ', ').change();
this.triggerEvent('add-recipient', { field:field, recipients:recipients });
}
return recipients.length;
};
// checks the input fields before sending a message
this.check_compose_input = function(cmd)
{
var key,
input_subject = $("[name='_subject']");
// check if all files has been uploaded
for (key in this.env.attachments) {
if (typeof this.env.attachments[key] === 'object' && !this.env.attachments[key].complete) {
this.alert_dialog(this.get_label('notuploadedwarning'));
return false;
}
}
// display localized warning for missing subject
if (!this.env.nosubject_warned && input_subject.val() == '') {
var dialog,
prompt_value = $('<input>').attr({type: 'text', size: 40}),
myprompt = $('<div class="prompt">')
.append($('<div class="message">').text(this.get_label('nosubjectwarning')))
.append(prompt_value),
save_func = function() {
input_subject.val(prompt_value.val());
dialog.dialog('close');
if (ref.check_compose_input(cmd))
ref.command(cmd, { nocheck:true }); // repeat command which triggered this
};
dialog = this.show_popup_dialog(
myprompt,
this.get_label('nosubjecttitle'),
[{
text: this.get_label('sendmessage'),
'class': 'mainaction send',
click: function() { save_func(); }
}, {
text: this.get_label('cancel'),
'class': 'cancel',
click: function() {
input_subject.focus();
dialog.dialog('close');
}
}],
{dialogClass: 'warning'}
);
prompt_value.select().keydown(function(e) {
if (e.which == 13) save_func();
});
this.env.nosubject_warned = true;
return false;
}
// check for empty body (only possible if not mailvelope encrypted)
if (!this.mailvelope_editor && !this.editor.get_content() && !confirm(this.get_label('nobodywarning'))) {
this.editor.focus();
return false;
}
if (!this.check_compose_address_fields(cmd))
return false;
// move body from html editor to textarea (just to be sure, #1485860)
this.editor.save();
return true;
};
this.check_compose_address_fields = function(cmd, form)
{
if (!form)
form = window.document;
// check input fields
var key, recipients, dialog,
limit = this.env.max_disclosed_recipients,
input_to = $("[name='_to']", form),
input_cc = $("[name='_cc']", form),
input_bcc = $("[name='_bcc']", form),
input_from = $("[name='_from']", form),
get_recipients = function(fields) {
fields = $.map(fields, function(v) {
v = $.trim(v.val());
return v.length ? v : null;
});
return fields.join(',').replace(/^[\s,;]+/, '').replace(/[\s,;]+$/, '');
};
// check sender (if have no identities)
if (input_from.prop('type') == 'text' && !rcube_check_email(input_from.val(), true)) {
this.alert_dialog(this.get_label('nosenderwarning'), function() {
input_from.focus();
});
return false;
}
// check for empty recipient
if (!rcube_check_email(get_recipients([input_to, input_cc, input_bcc]), true)) {
this.alert_dialog(this.get_label('norecipientwarning'), function() {
input_to.focus();
});
return false;
}
// check disclosed recipients limit
if (limit && !this.env.disclosed_recipients_warned
&& rcube_check_email(recipients = get_recipients([input_to, input_cc]), true, true) > limit
) {
var save_func = function(move_to_bcc) {
if (move_to_bcc) {
var bcc = input_bcc.val();
input_bcc.val((bcc ? (bcc + ', ') : '') + recipients).change();
input_to.val('').change();
input_cc.val('').change();
}
dialog.dialog('close');
if (typeof cmd == 'function')
cmd();
else if (cmd)
ref.command(cmd, { nocheck:true }); // repeat command which triggered this
};
dialog = this.show_popup_dialog(
this.get_label('disclosedrecipwarning'),
this.get_label('disclosedreciptitle'),
[{
text: this.get_label('sendmessage'),
click: function() { save_func(false); },
'class': 'mainaction'
}, {
text: this.get_label('bccinstead'),
click: function() { save_func(true); }
}, {
text: this.get_label('cancel'),
click: function() { dialog.dialog('close'); },
'class': 'cancel'
}],
{dialogClass: 'warning'}
);
this.env.disclosed_recipients_warned = true;
return false;
}
return true;
};
this.toggle_editor = function(props, obj, e)
{
// @todo: this should work also with many editors on page
var result = this.editor.toggle(props.html, props.noconvert || false);
// satisfy the expectations of aftertoggle-editor event subscribers
props.mode = props.html ? 'html' : 'plain';
if (!result && e) {
// fix selector value if operation failed
props.mode = props.html ? 'plain' : 'html';
$(e.target).filter('select').val(props.mode);
}
if (result) {
// update internal format flag
$("[name='_is_html']").val(props.html ? 1 : 0);
}
return result;
};
// Inserts a predefined response to the compose editor
this.insert_response = function(key)
{
this.editor.replace(this.env.textresponses[key]);
this.display_message('responseinserted', 'confirmation');
};
/**
* Open the dialog to save a new canned response
*/
this.save_response = function()
{
// show dialog to enter a name and to modify the text to be saved
var buttons = {}, text = this.editor.get_content({selection: true, format: 'text', nosig: true}),
html = '<form class="propform">' +
'<div class="prop block"><label for="ffresponsename">' + this.get_label('responsename') + '</label>' +
'<input type="text" name="name" id="ffresponsename" size="40" /></div>' +
'<div class="prop block"><label for="ffresponsetext">' + this.get_label('responsetext') + '</label>' +
'<textarea name="text" id="ffresponsetext" cols="40" rows="8"></textarea></div>' +
'</form>';
buttons[this.get_label('save')] = function(e) {
var name = $('#ffresponsename').val(),
text = $('#ffresponsetext').val();
if (!text) {
$('#ffresponsetext').select();
return false;
}
if (!name)
name = text.replace(/[\r\n]+/g, ' ').substring(0,40);
var lock = ref.display_message('savingresponse', 'loading');
ref.http_post('settings/responses', { _insert:1, _name:name, _text:text }, lock);
$(this).dialog('close');
};
buttons[this.get_label('cancel')] = function() {
$(this).dialog('close');
};
this.show_popup_dialog(html, this.get_label('newresponse'), buttons, {button_classes: ['mainaction save', 'cancel']});
$('#ffresponsetext').val(text);
$('#ffresponsename').select();
};
this.add_response_item = function(response)
{
var key = response.key;
this.env.textresponses[key] = response;
// append to responses list
if (this.gui_objects.responseslist) {
var li = $('<li>').appendTo(this.gui_objects.responseslist);
$('<a>').addClass('insertresponse active')
.attr('href', '#')
.attr('rel', key)
.attr('tabindex', '0')
.html(this.quote_html(response.name))
.appendTo(li)
.mousedown(function(e) {
return rcube_event.cancel(e);
})
.on('mouseup keypress', function(e) {
if (e.type == 'mouseup' || rcube_event.get_keycode(e) == 13) {
ref.command('insert-response', $(this).attr('rel'));
$(document.body).trigger('mouseup'); // hides the menu
return rcube_event.cancel(e);
}
});
}
};
this.edit_responses = function()
{
// TODO: implement inline editing of responses
};
this.delete_response = function(key)
{
if (!key && this.responses_list) {
var selection = this.responses_list.get_selection();
key = selection[0];
}
// submit delete request
if (key) {
this.confirm_dialog(this.get_label('deleteresponseconfirm'), 'delete', function() {
ref.http_post('settings/delete-response', { _key: key }, false);
});
}
};
// updates spellchecker buttons on state change
this.spellcheck_state = function()
{
var active = this.editor.spellcheck_state();
$.each(this.buttons.spellcheck || [], function(i, v) {
$('#' + v.id)[active ? 'addClass' : 'removeClass']('selected');
});
return active;
};
// get selected language
this.spellcheck_lang = function()
{
return this.editor.get_language();
};
this.spellcheck_lang_set = function(lang)
{
this.editor.set_language(lang);
};
// resume spellchecking, highlight provided mispellings without new ajax request
this.spellcheck_resume = function(data)
{
this.editor.spellcheck_resume(data);
};
this.set_draft_id = function(id)
{
if (id && id != this.env.draft_id) {
var filter = {task: 'mail', action: ''},
rc = this.opener(false, filter) || this.opener(true, filter);
// refresh the drafts folder in the opener window
if (rc && rc.env.mailbox == this.env.drafts_mailbox)
rc.command('checkmail');
this.env.draft_id = id;
$("[name='_draft_saveid']").val(id);
}
// always remove local copy upon saving as draft
this.remove_compose_data(this.env.compose_id);
this.compose_skip_unsavedcheck = false;
};
// Create (attach) 'savetarget' iframe before use
this.get_save_target = function()
{
// Removing the frame on load/error to workaround issues with window history
this.dummy_iframe('savetarget', 'about:blank')
.on('load error', function() { $(this).remove(); });
return 'savetarget';
};
this.auto_save_start = function()
{
if (this.env.draft_autosave) {
this.save_timer = setTimeout(function() {
ref.command("savedraft");
}, this.env.draft_autosave * 1000);
}
// save compose form content to local storage every 5 seconds
if (!this.local_save_timer && window.localStorage && this.env.save_localstorage) {
// track typing activity and only save on changes
this.compose_type_activity = this.compose_type_activity_last = 0;
$(document).keypress(function(e) { ref.compose_type_activity++; });
this.local_save_timer = setInterval(function(){
if (ref.compose_type_activity > ref.compose_type_activity_last) {
ref.save_compose_form_local();
ref.compose_type_activity_last = ref.compose_type_activity;
}
}, 5000);
$(window).on('unload', function() {
// remove copy from local storage if compose screen is left after warning
if (!ref.env.server_error)
ref.remove_compose_data(ref.env.compose_id);
});
}
// check for unsaved changes before leaving the compose page
if (!window.onbeforeunload) {
window.onbeforeunload = function() {
if (!ref.compose_skip_unsavedcheck && ref.cmp_hash != ref.compose_field_hash()) {
return ref.get_label('notsentwarning');
}
};
}
// Unlock interface now that saving is complete
this.busy = false;
};
this.compose_field_hash = function(save)
{
// check input fields
var i, id, val, str = '', hash_fields = ['to', 'cc', 'bcc', 'subject'];
for (i=0; i<hash_fields.length; i++)
if (val = $('[name="_' + hash_fields[i] + '"]').val())
str += val + ':';
str += this.editor.get_content({refresh: false});
if (this.env.attachments)
for (id in this.env.attachments)
str += id;
// we can't detect changes in the Mailvelope editor so assume it changed
if (this.mailvelope_editor) {
str += ';' + new Date().getTime();
}
if (save)
this.cmp_hash = str;
return str;
};
// store the contents of the compose form to localstorage
this.save_compose_form_local = function()
{
// feature is disabled
if (!this.env.save_localstorage)
return;
var formdata = { session:this.env.session_id, changed:new Date().getTime() },
ed, empty = true;
// get fresh content from editor
this.editor.save();
if (this.env.draft_id) {
formdata.draft_id = this.env.draft_id;
}
if (this.env.reply_msgid) {
formdata.reply_msgid = this.env.reply_msgid;
}
$('input, select, textarea', this.gui_objects.messageform).each(function(i, elem) {
switch (elem.tagName.toLowerCase()) {
case 'input':
if (elem.type == 'button' || elem.type == 'submit' || (elem.type == 'hidden' && elem.name != '_is_html')) {
break;
}
formdata[elem.name] = elem.type != 'checkbox' || elem.checked ? $(elem).val() : '';
if (formdata[elem.name] != '' && elem.type != 'hidden')
empty = false;
break;
case 'select':
formdata[elem.name] = $('option:checked', elem).val();
break;
default:
formdata[elem.name] = $(elem).val();
if (formdata[elem.name] != '')
empty = false;
}
});
if (!empty) {
var index = this.local_storage_get_item('compose.index', []),
key = this.env.compose_id;
if ($.inArray(key, index) < 0) {
index.push(key);
}
this.local_storage_set_item('compose.' + key, formdata, true);
this.local_storage_set_item('compose.index', index);
}
};
// write stored compose data back to form
this.restore_compose_form = function(key, html_mode)
{
var ed, formdata = this.local_storage_get_item('compose.' + key, true);
if (formdata && typeof formdata == 'object') {
$.each(formdata, function(k, value) {
if (k[0] == '_') {
var elem = $("*[name='"+k+"']");
if (elem[0] && elem[0].type == 'checkbox') {
elem.prop('checked', value != '');
}
else {
elem.val(value);
}
}
});
// initialize HTML editor
if ((formdata._is_html == '1' && !html_mode) || (formdata._is_html != '1' && html_mode)) {
this.command('toggle-editor', {id: this.env.composebody, html: !html_mode, noconvert: true});
}
}
};
// remove stored compose data from localStorage
this.remove_compose_data = function(key)
{
var index = this.local_storage_get_item('compose.index', []);
if ($.inArray(key, index) >= 0) {
this.local_storage_remove_item('compose.' + key);
this.local_storage_set_item('compose.index', $.grep(index, function(val,i) { return val != key; }));
}
};
// clear all stored compose data of this user
this.clear_compose_data = function()
{
var i, index = this.local_storage_get_item('compose.index', []);
for (i=0; i < index.length; i++) {
this.local_storage_remove_item('compose.' + index[i]);
}
this.local_storage_remove_item('compose.index');
};
this.change_identity = function(obj, show)
{
if (!obj || !obj.options)
return false;
var id = $(obj).val(),
got_sig = this.env.signatures && this.env.signatures[id],
sig = this.env.identity,
show_sig = show ? show : this.env.show_sig;
// enable manual signature insert
if (got_sig) {
this.enable_command('insert-sig', true);
this.env.compose_commands.push('insert-sig');
got_sig = true;
}
else
this.enable_command('insert-sig', false);
// first function execution
if (!this.env.identities_initialized) {
this.env.identities_initialized = true;
if (this.env.show_sig_later)
this.env.show_sig = true;
if (this.env.opened_extwin)
return;
}
// update reply-to/bcc fields with addresses defined in identities
$.each(['replyto', 'bcc'], function() {
var rx, key = this,
old_val = sig && ref.env.identities[sig] ? ref.env.identities[sig][key] : '',
new_val = id && ref.env.identities[id] ? ref.env.identities[id][key] : '',
input = $('[name="_'+key+'"]'), input_val = input.val();
// remove old address(es)
if (old_val && input_val) {
rx = new RegExp('\\s*' + RegExp.escape(old_val) + '\\s*');
input_val = input_val.replace(rx, '');
}
// cleanup
input_val = String(input_val).replace(/[,;]\s*[,;]/g, ',').replace(/^[\s,;]+/, '');
// add new address(es)
if (new_val && input_val.indexOf(new_val) == -1 && input_val.indexOf(new_val.replace(/"/g, '')) == -1) {
if (input_val) {
input_val = input_val.replace(/[,;\s]+$/, '') + ', ';
}
input_val += new_val + ', ';
}
if (old_val || new_val)
input.val(input_val).change();
});
if (this.editor)
this.editor.change_signature(id, show_sig);
if (show && got_sig)
this.display_message('siginserted', 'confirmation');
this.env.identity = id;
this.triggerEvent('change_identity');
return true;
};
// Open file selection dialog for defined upload form
// Works only on click and only with smart-upload forms
this.upload_input = function(name)
{
$('#' + name + ' input[type="file"]').click();
};
// upload (attachment) file
this.upload_file = function(form, action, lock)
{
if (form) {
var fname, files = [];
$('input', form).each(function() {
if (this.files) {
fname = this.name;
for (var i=0; i < this.files.length; i++)
files.push(this.files[i]);
}
});
return this.file_upload(files, {_id: this.env.compose_id || ''}, {
name: fname,
action: action,
lock: lock
});
}
};
// add file name to attachment list
// called from upload page
this.add2attachment_list = function(name, att, upload_id)
{
if (upload_id)
this.triggerEvent('fileuploaded', {name: name, attachment: att, id: upload_id});
if (!this.env.attachments)
this.env.attachments = {};
if (upload_id && this.env.attachments[upload_id])
delete this.env.attachments[upload_id];
this.env.attachments[name] = att;
if (!this.gui_objects.attachmentlist)
return false;
var label, indicator, li = $('<li>');
if (!att.complete && att.html.indexOf('<') < 0)
att.html = '<span class="uploading">' + att.html + '</span>';
if (!att.complete && this.env.loadingicon)
att.html = '<img src="'+this.env.loadingicon+'" alt="" class="uploading" />' + att.html;
if (!att.complete) {
label = this.get_label('cancel');
att.html = '<a title="'+label+'" onclick="return rcmail.cancel_attachment_upload(\''+name+'\');" href="#cancelupload" class="cancelupload">'
+ (this.env.cancelicon ? '<img src="'+this.env.cancelicon+'" alt="'+label+'" />' : '<span class="inner">' + label + '</span>') + '</a>' + att.html;
}
li.attr('id', name).addClass(att.classname).html(att.html)
.find('.attachment-name').on('mouseover', function() { rcube_webmail.long_subject_title_ex(this); });
// replace indicator's li
if (upload_id && (indicator = document.getElementById(upload_id))) {
li.replaceAll(indicator);
}
else { // add new li
li.appendTo(this.gui_objects.attachmentlist);
}
// set tabindex attribute
var tabindex = $(this.gui_objects.attachmentlist).attr('data-tabindex') || '0';
li.find('a').attr('tabindex', tabindex);
this.triggerEvent('fileappended', {name: name, attachment: att, id: upload_id, item: li});
return true;
};
this.remove_from_attachment_list = function(name)
{
if (this.env.attachments) {
delete this.env.attachments[name];
$('#'+name).remove();
}
};
this.remove_attachment = function(name)
{
if (name && this.env.attachments[name])
this.http_post('remove-attachment', { _id:this.env.compose_id, _file:name });
return false;
};
this.cancel_attachment_upload = function(name)
{
if (!name || !this.uploads[name])
return false;
this.remove_from_attachment_list(name);
this.uploads[name].abort();
return false;
};
// rename uploaded attachment (in compose)
this.rename_attachment = function(id)
{
var attachment = this.env.attachments ? this.env.attachments[id] : null;
if (!attachment)
return;
var input = $('<input>').attr({type: 'text', size: 50}).val(attachment.name),
content = $('<label>').text(this.get_label('namex')).append(input);
this.simple_dialog(content, 'attachmentrename', function() {
var name;
if ((name = input.val()) && name != attachment.name) {
ref.http_post('rename-attachment', {_id: ref.env.compose_id, _file: id, _name: name},
ref.set_busy(true, 'loading'));
return true;
}
}
);
};
// update attachments list with the new name
this.rename_attachment_handler = function(id, name)
{
var attachment = this.env.attachments ? this.env.attachments[id] : null;
if (!attachment || !name)
return;
attachment.name = name;
$('#' + id + ' .attachment-name').text(name).attr('title', '');
};
// send remote request to add a new contact
this.add_contact = function(value, reload)
{
if (value)
this.http_post('addcontact', {_address: value, _reload: reload});
};
// send remote request to search mail or contacts
this.qsearch = function(value)
{
// Note: Some plugins would like to do search without value,
// so we keep value != '' check to allow that use-case. Which means
// e.g. that qsearch() with no argument will execute the search.
if (value != '' || $(this.gui_objects.qsearchbox).val() || $(this.gui_objects.search_interval).val()) {
var r, lock = this.set_busy(true, 'searching'),
url = this.search_params(value),
action = this.env.action == 'compose' && this.contact_list ? 'search-contacts' : 'search';
if (this.message_list)
this.clear_message_list();
else if (this.contact_list)
this.list_contacts_clear();
if (this.env.source)
url._source = this.env.source;
if (this.env.group)
url._gid = this.env.group;
// reset vars
this.env.current_page = 1;
r = this.http_request(action, url, lock);
this.env.qsearch = {lock: lock, request: r};
this.enable_command('set-listmode', this.env.threads && (this.env.search_scope || 'base') == 'base');
return true;
}
return false;
};
this.continue_search = function(request_id)
{
var lock = this.set_busy(true, 'stillsearching');
setTimeout(function() {
var url = ref.search_params();
url._continue = request_id;
ref.env.qsearch = { lock: lock, request: ref.http_request('search', url, lock) };
}, 100);
};
// build URL params for search
this.search_params = function(search, filter)
{
var n, url = {}, mods_arr = [],
mods = this.env.search_mods,
scope = this.env.search_scope || 'base',
mbox = scope == 'all' ? '*' : this.env.mailbox;
if (!filter && this.gui_objects.search_filter)
filter = this.gui_objects.search_filter.value;
if (!search && this.gui_objects.qsearchbox)
search = this.gui_objects.qsearchbox.value;
if (this.gui_objects.search_interval)
url._interval = $(this.gui_objects.search_interval).val();
if (search) {
url._q = search;
if (mods && this.message_list)
mods = mods[mbox] || mods['*'];
if (mods) {
for (n in mods)
mods_arr.push(n);
url._headers = mods_arr.join(',');
}
}
url._layout = this.env.layout;
url._filter = filter;
url._scope = scope;
if (mbox && scope != 'all')
url._mbox = mbox;
return url;
};
// reset search filter
this.reset_search_filter = function()
{
this.filter_disabled = true;
if (this.gui_objects.search_filter)
$(this.gui_objects.search_filter).val('ALL').change();
this.filter_disabled = false;
};
// reset quick-search form
this.reset_qsearch = function(all)
{
if (this.gui_objects.qsearchbox)
this.gui_objects.qsearchbox.value = '';
if (this.gui_objects.search_interval)
$(this.gui_objects.search_interval).val('');
if (this.env.qsearch)
this.abort_request(this.env.qsearch);
if (all) {
this.env.search_scope = 'base';
this.reset_search_filter();
}
this.env.qsearch = null;
this.env.search_request = null;
this.env.search_id = null;
this.select_all_mode = false;
this.enable_command('set-listmode', this.env.threads);
};
this.set_searchscope = function(scope)
{
var old = this.env.search_scope;
this.env.search_scope = scope;
// re-send search query with new scope
if (scope != old && this.env.search_request) {
if (!this.qsearch(this.gui_objects.qsearchbox.value) && this.env.search_filter && this.env.search_filter != 'ALL')
this.filter_mailbox(this.env.search_filter);
if (scope != 'all')
this.select_folder(this.env.mailbox, '', true);
}
};
this.set_searchinterval = function(interval)
{
var old = this.env.search_interval;
this.env.search_interval = interval;
// re-send search query with new interval
if (interval != old && this.env.search_request) {
if (!this.qsearch(this.gui_objects.qsearchbox.value) && this.env.search_filter && this.env.search_filter != 'ALL')
this.filter_mailbox(this.env.search_filter);
if (interval)
this.select_folder(this.env.mailbox, '', true);
}
};
this.set_searchmods = function(mods)
{
var mbox = this.env.mailbox,
scope = this.env.search_scope || 'base';
if (scope == 'all')
mbox = '*';
if (!this.env.search_mods)
this.env.search_mods = {};
if (mbox)
this.env.search_mods[mbox] = mods;
};
this.is_multifolder_listing = function()
{
return this.env.multifolder_listing !== undefined ? this.env.multifolder_listing :
(this.env.search_request && (this.env.search_scope || 'base') != 'base');
};
// action executed after mail is sent
this.sent_successfully = function(type, msg, folders, save_error)
{
this.display_message(msg, type);
this.compose_skip_unsavedcheck = true;
if (this.env.extwin) {
if (!save_error)
this.lock_form(this.gui_objects.messageform);
var filter = {task: 'mail', action: ''},
rc = this.opener(false, filter) || this.opener(true, filter);
if (rc) {
rc.display_message(msg, type);
// refresh the folder where sent message was saved or replied message comes from
if (folders && $.inArray(rc.env.mailbox, folders) >= 0) {
rc.command('checkmail');
}
}
if (!save_error)
setTimeout(function() { window.close(); }, 1000);
}
else if (!save_error) {
// before redirect we need to wait some time for Chrome (#1486177)
setTimeout(function() { ref.list_mailbox(); }, 500);
}
if (save_error)
this.env.is_sent = true;
};
this.image_rotate = function()
{
var curr = this.image_style ? (this.image_style.rotate || 0) : 0;
this.image_style.rotate = curr > 180 ? 0 : curr + 90;
this.apply_image_style();
};
this.image_scale = function(prop)
{
var curr = this.image_style ? (this.image_style.scale || 1) : 1;
this.image_style.scale = Math.max(0.1, curr + 0.1 * (prop == '-' ? -1 : 1));
this.apply_image_style();
};
this.apply_image_style = function()
{
var style = [],
head = $(this.gui_objects.messagepartframe).contents().find('head');
$('#image-style', head).remove();
$.each({scale: '', rotate: 'deg'}, function(i, v) {
var val = ref.image_style[i];
if (val)
style.push(i + '(' + val + v + ')');
});
if (style)
head.append($('<style id="image-style">').text('img { transform: ' + style.join(' ') + '}'));
};
// Update import dialog state
this.import_state_set = function(state)
{
if (this.import_dialog) {
this.import_state = state;
// activate Import button depending on state
$(this.import_dialog).parent().find('.ui-dialog-buttonset > button').first().attr('disabled', state != 'error');
}
};
/*********************************************************/
/********* keyboard live-search methods *********/
/*********************************************************/
// handler for keyboard events on address-fields
this.ksearch_keydown = function(e, obj, props)
{
if (this.ksearch_timer)
clearTimeout(this.ksearch_timer);
var key = rcube_event.get_keycode(e);
switch (key) {
case 38: // arrow up
case 40: // arrow down
if (!this.ksearch_visible())
return;
var dir = key == 38 ? 1 : 0,
highlight = this.ksearch_pane.find('li.selected')[0];
if (!highlight)
highlight = this.ksearch_pane.__ul.firstChild;
if (highlight)
this.ksearch_select(dir ? highlight.previousSibling : highlight.nextSibling);
return rcube_event.cancel(e);
case 9: // tab
if (rcube_event.get_modifier(e) == SHIFT_KEY || !this.ksearch_visible()) {
this.ksearch_hide();
return;
}
case 13: // enter
if (!this.ksearch_visible())
return false;
// insert selected address and hide ksearch pane
this.insert_recipient(this.ksearch_selected);
this.ksearch_hide();
// Don't cancel on Tab, we want to jump to the next field (#5659)
return key == 9 ? null : rcube_event.cancel(e);
case 27: // escape
this.ksearch_hide();
return;
case 37: // left
case 39: // right
return;
}
// start timer
this.ksearch_timer = setTimeout(function(){ ref.ksearch_get_results(props); }, 200);
this.ksearch_input = obj;
return true;
};
this.ksearch_visible = function()
{
return this.ksearch_selected !== null && this.ksearch_selected !== undefined && this.ksearch_value;
};
this.ksearch_select = function(node)
{
if (this.ksearch_pane && node) {
this.ksearch_pane.find('li.selected').removeClass('selected').removeAttr('aria-selected');
}
if (node) {
$(node).addClass('selected').attr('aria-selected', 'true');
this.ksearch_selected = node._rcm_id;
$(this.ksearch_input).attr('aria-activedescendant', 'rcmkSearchItem' + this.ksearch_selected);
}
};
this.insert_recipient = function(id)
{
if (id === null || !this.env.contacts[id] || !this.ksearch_input)
return;
var trigger = false, insert = '', delim = ', ';
this.ksearch_destroy();
// insert all members of a group
if (typeof this.env.contacts[id] === 'object' && this.env.contacts[id].type == 'group' && !this.env.contacts[id].email) {
insert += this.env.contacts[id].name + delim;
this.group2expand[this.env.contacts[id].id] = $.extend({ input: this.ksearch_input }, this.env.contacts[id]);
this.http_request('mail/group-expand', {_source: this.env.contacts[id].source, _gid: this.env.contacts[id].id}, false);
}
else if (typeof this.env.contacts[id] === 'object' && this.env.contacts[id].name) {
insert = this.env.contacts[id].name + delim;
trigger = true;
}
else if (typeof this.env.contacts[id] === 'string') {
insert = this.env.contacts[id] + delim;
trigger = true;
}
this.ksearch_input_replace(this.ksearch_value, insert);
if (trigger) {
this.triggerEvent('autocomplete_insert', { field:this.ksearch_input, insert:insert, data:this.env.contacts[id], search:this.ksearch_value_last, result_type:'person' });
this.ksearch_value_last = null;
this.compose_type_activity++;
}
};
this.replace_group_recipients = function(id, recipients)
{
if (this.group2expand[id]) {
this.ksearch_input_replace(this.group2expand[id].name, recipients, this.group2expand[id].input);
this.triggerEvent('autocomplete_insert', { field:this.group2expand[id].input, insert:recipients, data:this.group2expand[id], search:this.ksearch_value_last, result_type:'group' });
this.ksearch_value_last = null;
this.group2expand[id] = null;
this.compose_type_activity++;
}
};
// address search processor
this.ksearch_get_results = function(props)
{
if (this.ksearch_pane && this.ksearch_pane.is(":visible"))
this.ksearch_pane.hide();
// get string from cursor position back to the last comma or semicolon
var q = this.ksearch_input_get(),
min = this.env.autocomplete_min_length,
data = this.ksearch_data;
// trim query string
q = $.trim(q);
// Don't (re-)search if the last results are still active
if (q == this.ksearch_value)
return;
this.ksearch_destroy();
if (q.length && q.length < min) {
if (!this.ksearch_info) {
this.ksearch_info = this.display_message(this.get_label('autocompletechars').replace('$min', min));
}
return;
}
var old_value = this.ksearch_value;
this.ksearch_value = q;
this.ksearch_value_last = q; // Group expansion clears ksearch_value before calling autocomplete_insert trigger, therefore store it in separate variable for later consumption.
// ...string is empty
if (!q.length)
return;
// ...new search value contains old one and previous search was not finished or its result was empty
if (old_value && old_value.length && q.startsWith(old_value) && (!data || data.num <= 0) && this.env.contacts && !this.env.contacts.length)
return;
var sources = props && props.sources ? props.sources : [''];
var reqid = this.multi_thread_http_request({
items: sources,
threads: props && props.threads ? props.threads : 1,
action: props && props.action ? props.action : 'mail/autocomplete',
postdata: { _search:q, _source:'%s' },
lock: this.display_message('searching', 'loading')
});
this.ksearch_data = { id:reqid, sources:sources.slice(), num:sources.length };
};
this.ksearch_query_results = function(results, search, reqid)
{
// trigger multi-thread http response callback
this.multi_thread_http_response(results, reqid);
// search stopped in meantime?
if (!this.ksearch_value)
return;
// ignore this outdated search response
if (this.ksearch_input && search != this.ksearch_value)
return;
// display search results
var i, id, len, ul, text, type, init,
is_framed = this.is_framed(),
value = this.ksearch_value,
maxlen = this.env.autocomplete_max ? this.env.autocomplete_max : 15;
// create results pane if not present
if (!this.ksearch_pane) {
ul = $('<ul>');
this.ksearch_pane = $('<div>')
.attr({id: 'rcmKSearchpane', role: 'listbox'})
.css({position: 'absolute', 'z-index': 30000})
.append(ul)
.appendTo(is_framed ? parent.document.body : document.body);
this.ksearch_pane.__ul = ul[0];
this.triggerEvent('autocomplete_create', {obj: this.ksearch_pane});
}
ul = this.ksearch_pane.__ul;
// remove all search results or add to existing list if parallel search
if (reqid && this.ksearch_pane.data('reqid') == reqid) {
maxlen -= ul.childNodes.length;
}
else {
this.ksearch_pane.data('reqid', reqid);
init = 1;
// reset content
ul.innerHTML = '';
this.env.contacts = [];
// Calculate the results pane position and size
var pos = $(this.ksearch_input).offset();
// ... consider scroll position
pos.left -= $(document.documentElement).scrollLeft();
pos.top -= $(document.documentElement).scrollTop();
// ... consider iframe position
if (is_framed) {
try {
parent.$('iframe').each(function() {
if (this.contentWindow == window) {
var offset = $(this).offset();
pos.left += offset.left;
pos.top += offset.top;
}
});
}
catch(e) {}
}
var w = $(is_framed ? parent : window).width(),
left = w - pos.left > 200 ? pos.left : w - 200,
width = Math.min(400, w - left);
this.ksearch_pane.css({
left: left + 'px',
top: (pos.top + this.ksearch_input.offsetHeight + 1) + 'px',
maxWidth: width + 'px',
minWidth: '200px',
display: 'none'
});
}
// add each result line to list
if (results && (len = results.length)) {
for (i=0; i < len && maxlen > 0; i++) {
text = typeof results[i] === 'object' ? (results[i].display || results[i].name) : results[i];
type = typeof results[i] === 'object' ? results[i].type : '';
id = i + this.env.contacts.length;
$('<li>').attr('id', 'rcmkSearchItem' + id)
.attr('role', 'option')
.html('<i class="icon"></i>' + this.quote_html(text.replace(new RegExp('('+RegExp.escape(value)+')', 'ig'), '##$1%%')).replace(/##([^%]+)%%/g, '<b>$1</b>'))
.addClass(type || '')
.appendTo(ul)
.mouseover(function() { ref.ksearch_select(this); })
.mouseup(function() { ref.ksearch_click(this); })
.get(0)._rcm_id = id;
maxlen -= 1;
}
}
if (ul.childNodes.length) {
// set the right aria-* attributes to the input field
$(this.ksearch_input)
.attr('aria-haspopup', 'true')
.attr('aria-expanded', 'true')
.attr('aria-owns', 'rcmKSearchpane');
this.ksearch_pane.show();
// select the first
if (!this.env.contacts.length) {
this.ksearch_select($('li', ul)[0]);
}
}
if (len)
this.env.contacts = this.env.contacts.concat(results);
if (this.ksearch_data.id == reqid)
this.ksearch_data.num--;
};
// Getter for input value
// returns a string from the last comma to current cursor position
this.ksearch_input_get = function()
{
if (!this.ksearch_input)
return '';
var cp = this.get_caret_pos(this.ksearch_input);
return this.ksearch_input.value.substr(0, cp).split(/[,;]/).pop();
};
// Setter for input value
// replaces 'from' string with 'to' and sets cursor position at the end
this.ksearch_input_replace = function(from, to, input)
{
if (!this.ksearch_input && !input)
return;
if (!input)
input = this.ksearch_input;
var cpos = this.get_caret_pos(input),
p = input.value.lastIndexOf(from, cpos),
pre = input.value.substring(0, p),
end = input.value.substring(p + from.length, input.value.length);
input.value = pre + to + end;
// set caret to insert pos
this.set_caret_pos(input, p + to.length);
// run onchange action on the element
$(input).change();
};
this.ksearch_click = function(node)
{
if (this.ksearch_input)
this.ksearch_input.focus();
this.insert_recipient(node._rcm_id);
this.ksearch_hide();
};
this.ksearch_blur = function()
{
if (this.ksearch_timer)
clearTimeout(this.ksearch_timer);
this.ksearch_input = null;
this.ksearch_hide();
};
this.ksearch_hide = function()
{
this.ksearch_selected = null;
this.ksearch_value = '';
if (this.ksearch_pane)
this.ksearch_pane.hide();
$(this.ksearch_input)
.attr('aria-haspopup', 'false')
.attr('aria-expanded', 'false')
.removeAttr('aria-activedescendant')
.removeAttr('aria-owns');
this.ksearch_destroy();
};
// Clears autocomplete data/requests
this.ksearch_destroy = function()
{
if (this.ksearch_data)
this.multi_thread_request_abort(this.ksearch_data.id);
if (this.ksearch_info)
this.hide_message(this.ksearch_info);
if (this.ksearch_msg)
this.hide_message(this.ksearch_msg);
this.ksearch_data = null;
this.ksearch_info = null;
this.ksearch_msg = null;
};
/*********************************************************/
/********* address book methods *********/
/*********************************************************/
this.contactlist_keypress = function(list)
{
if (list.key_pressed == list.DELETE_KEY)
this.command('delete');
};
this.contactlist_select = function(list)
{
if (this.preview_timer)
clearTimeout(this.preview_timer);
var id, targets, groupcount = 0, writable = false, copy_writable = false,
selected = list.get_selection().length,
source = this.env.source ? this.env.address_sources[this.env.source] : null;
// we don't have dblclick handler here, so use 50 instead of this.dblclick_time
if (this.env.contentframe && !list.multi_selecting && (id = list.get_single_selection()))
this.preview_timer = setTimeout(function() { ref.load_contact(id, 'show'); }, this.preview_delay_click);
else if (this.env.contentframe)
this.show_contentframe(false);
if (selected) {
list.draggable = false;
// no source = search result, we'll need to detect if any of
// selected contacts are in writable addressbook to enable edit/delete
// we'll also need to know sources used in selection for copy
// and group-addmember operations (drag&drop)
this.env.selection_sources = [];
if (source) {
this.env.selection_sources.push(this.env.source);
}
$.each(list.get_selection(), function(i, v) {
var sid, contact = list.data[v];
if (!source) {
sid = String(v).replace(/^[^-]+-/, '');
if (sid && ref.env.address_sources[sid]) {
writable = writable || (!ref.env.address_sources[sid].readonly && !contact.readonly);
ref.env.selection_sources.push(sid);
}
}
else {
writable = writable || (!source.readonly && !contact.readonly);
}
if (contact._type != 'group')
list.draggable = true;
});
this.env.selection_sources = $.unique(this.env.selection_sources);
if (source && source.groups)
$.each(this.env.contactgroups, function() { if (this.source === ref.env.source) groupcount++; });
targets = $.map(this.env.address_sources, function(v, i) { return v.readonly ? null : i; });
copy_writable = $.grep(targets, function(v) { return jQuery.inArray(v, ref.env.selection_sources) < 0; }).length > 0;
}
// if a group is currently selected, and there is at least one contact selected
// we can enable the group-remove-selected command
this.enable_command('group-assign-selected', groupcount > 0 && writable);
this.enable_command('group-remove-selected', this.env.group && writable);
this.enable_command('print', 'qrcode', selected == 1);
this.enable_command('export-selected', selected > 0);
this.enable_command('edit', id && writable);
this.enable_command('delete', 'move', writable);
this.enable_command('copy', copy_writable);
return false;
};
this.list_contacts = function(src, group, page, search)
{
var win, folder, index = -1, url = {},
refresh = src === undefined && group === undefined && page === undefined,
target = window;
if (!src)
src = this.env.source;
if (refresh)
group = this.env.group;
if (src != this.env.source) {
page = this.env.current_page = 1;
this.reset_qsearch();
}
else if (!refresh && group != this.env.group)
page = this.env.current_page = 1;
if (this.env.search_id)
folder = 'S'+this.env.search_id;
else if (!this.env.search_request)
folder = group ? 'G'+src+group : src;
this.env.source = src;
this.env.group = group;
// truncate groups listing stack
$.each(this.env.address_group_stack, function(i, v) {
if (ref.env.group == v.id) {
index = i;
return false;
}
});
this.env.address_group_stack = index < 0 ? [] : this.env.address_group_stack.slice(0, index);
// remove cached contact group selector
this.destroy_entity_selector('contactgroup-selector');
// make sure the current group is on top of the stack
if (this.env.group) {
if (!search) search = {};
search.id = this.env.group;
this.env.address_group_stack.push(search);
// mark the first group on the stack as selected in the directory list
folder = 'G'+src+this.env.address_group_stack[0].id;
}
else if (this.gui_objects.addresslist_title) {
$(this.gui_objects.addresslist_title).text(this.get_label('contacts'));
}
if (!this.env.search_id)
this.select_folder(folder, '', true);
// load contacts remotely
if (this.gui_objects.contactslist) {
this.list_contacts_remote(src, group, page);
return;
}
if (win = this.get_frame_window(this.env.contentframe)) {
target = win;
url._framed = 1;
}
if (group)
url._gid = group;
if (page)
url._page = page;
if (src)
url._source = src;
// also send search request to get the correct listing
if (this.env.search_request)
url._search = this.env.search_request;
this.set_busy(true, 'loading');
this.location_href(url, target);
};
// send remote request to load contacts list
this.list_contacts_remote = function(src, group, page)
{
// clear message list first
this.list_contacts_clear();
// send request to server
var url = {}, lock = this.set_busy(true, 'loading');
if (src)
url._source = src;
if (page)
url._page = page;
if (group)
url._gid = group;
this.env.source = src;
this.env.group = group;
// also send search request to get the right records
if (this.env.search_request)
url._search = this.env.search_request;
this.http_request(this.env.task == 'mail' ? 'list-contacts' : 'list', url, lock);
if (this.env.task != 'mail')
this.update_state({_source: src, _page: page && page > 1 ? page : null, _gid: group});
};
this.list_contacts_clear = function()
{
this.contact_list.data = {};
this.contact_list.clear(true);
this.show_contentframe(false);
this.enable_command('delete', 'move', 'copy', 'print', false);
};
this.set_group_prop = function(prop)
{
if (this.gui_objects.addresslist_title) {
var boxtitle = $(this.gui_objects.addresslist_title).html(''); // clear contents
// add link to pop back to parent group
if (this.env.address_group_stack.length > 1
|| (this.env.address_group_stack.length == 1 && this.env.address_group_stack[0].search_request)
) {
$('<a href="#list">...</a>')
.attr('title', this.get_label('uponelevel'))
.addClass('poplink')
.appendTo(boxtitle)
.click(function(e){ return ref.command('popgroup','',this); });
boxtitle.append('&nbsp;&raquo;&nbsp;');
}
boxtitle.append($('<span>').text(prop ? prop.name : this.get_label('contacts')));
}
};
// load contact record
this.load_contact = function(cid, action, framed)
{
var win, url = {}, target = window,
rec = this.contact_list ? this.contact_list.data[cid] : null;
if (win = this.get_frame_window(this.env.contentframe)) {
url._framed = 1;
target = win;
this.show_contentframe(true);
// load dummy content, unselect selected row(s)
if (!cid)
this.contact_list.clear_selection();
this.enable_command('export-selected', 'print', rec && rec._type != 'group');
}
else if (framed)
return false;
if (action && (cid || action == 'add') && !this.drag_active) {
if (this.env.group)
url._gid = this.env.group;
if (this.env.search_request)
url._search = this.env.search_request;
url._action = action;
url._source = this.env.source;
url._cid = cid;
this.location_href(url, target, true);
}
return true;
};
// add/delete member to/from the group
this.group_member_change = function(what, cid, source, gid)
{
if (what != 'add')
what = 'del';
var lock = this.display_message(what == 'add' ? 'addingmember' : 'removingmember', 'loading'),
post_data = {_cid: cid, _source: source, _gid: gid};
this.http_post('group-'+what+'members', post_data, lock);
};
this.contacts_drag_menu = function(e, to)
{
var dest = to.type == 'group' ? to.source : to.id,
source = this.env.source;
if (!this.env.address_sources[dest] || this.env.address_sources[dest].readonly)
return true;
// search result may contain contacts from many sources, but if there is only one...
if (source == '' && this.env.selection_sources.length == 1)
source = this.env.selection_sources[0];
if (to.type == 'group' && dest == source) {
var cid = this.contact_list.get_selection().join(',');
this.group_member_change('add', cid, dest, to.id);
return true;
}
// move action is not possible, "redirect" to copy if menu wasn't requested
else if (!this.commands.move && rcube_event.get_modifier(e) != SHIFT_KEY) {
this.copy_contacts(to);
return true;
}
return this.drag_menu(e, to);
};
// copy contact(s) to the specified target (group or directory)
this.copy_contacts = function(to, event, cid)
{
if (!to) {
cid = this.contact_list.get_selection();
return this.addressbook_selector(event, function(to, obj) {
var to = $(obj).data('source') ? ref.env.contactgroups['G' + $(obj).data('source') + $(obj).data('gid')] : ref.env.address_sources[to];
ref.copy_contacts(to, null, cid);
});
}
var dest = to.type == 'group' ? to.source : to.id,
source = this.env.source,
group = this.env.group ? this.env.group : '';
cid = cid ? cid.join(',') : this.contact_list.get_selection().join(',');
if (!cid || !this.env.address_sources[dest] || this.env.address_sources[dest].readonly)
return;
// search result may contain contacts from many sources, but if there is only one...
if (source == '' && this.env.selection_sources.length == 1)
source = this.env.selection_sources[0];
// tagret is a group
if (to.type == 'group') {
if (dest == source)
return;
var lock = this.display_message('copyingcontact', 'loading'),
post_data = {_cid: cid, _source: this.env.source, _to: dest, _togid: to.id, _gid: group};
this.http_post('copy', post_data, lock);
}
// target is an addressbook
else if (to.id != source) {
var lock = this.display_message('copyingcontact', 'loading'),
post_data = {_cid: cid, _source: this.env.source, _to: to.id, _gid: group};
this.http_post('copy', post_data, lock);
}
};
// move contact(s) to the specified target (group or directory)
this.move_contacts = function(to, event, cid)
{
if (!to) {
cid = this.contact_list.get_selection();
return this.addressbook_selector(event, function(to, obj) {
var to = $(obj).data('source') ? ref.env.contactgroups['G' + $(obj).data('source') + $(obj).data('gid')] : ref.env.address_sources[to];
ref.move_contacts(to, null, cid);
});
}
var dest = to.type == 'group' ? to.source : to.id,
source = this.env.source,
group = this.env.group ? this.env.group : '';
if (!this.env.address_sources[dest] || this.env.address_sources[dest].readonly)
return;
// search result may contain contacts from many sources, but if there is only one...
if (source == '' && this.env.selection_sources.length == 1)
source = this.env.selection_sources[0];
if (to.type == 'group') {
if (dest == source)
return;
this._with_selected_contacts('move', {_to: dest, _togid: to.id, _cid: cid});
}
// target is an addressbook
else if (to.id != source)
this._with_selected_contacts('move', {_to: to.id, _cid: cid});
};
// delete contact(s)
this.delete_contacts = function()
{
var undelete = this.env.source && this.env.address_sources[this.env.source].undelete;
if (undelete) {
this._with_selected_contacts('delete', {_cid: this.contact_list.get_selection()});
}
else {
var cid = this.contact_list.get_selection();
this.confirm_dialog(this.get_label('deletecontactconfirm'), 'delete', function() {
ref._with_selected_contacts('delete', {_cid: cid});
});
}
};
this._with_selected_contacts = function(action, post_data)
{
var selection = post_data._cid;
// exit if no contact specified or if selection is empty
if (!selection.length && !this.env.cid)
return;
var n, a_cids = [],
label = action == 'delete' ? 'contactdeleting' : 'movingcontact',
lock = this.display_message(label, 'loading');
if (this.env.cid)
a_cids.push(this.env.cid);
else {
for (n=0; n<selection.length; n++) {
id = selection[n];
a_cids.push(id);
this.contact_list.remove_row(id, (n == selection.length-1));
}
// hide content frame if we delete the currently displayed contact
if (selection.length == 1)
this.show_contentframe(false);
}
if (!post_data)
post_data = {};
post_data._source = this.env.source;
post_data._from = this.env.action;
post_data._cid = a_cids.join(',');
if (this.env.group)
post_data._gid = this.env.group;
// also send search request to get the right records from the next page
if (this.env.search_request)
post_data._search = this.env.search_request;
// send request to server
this.http_post(action, post_data, lock)
return true;
};
// update a contact record in the list
this.update_contact_row = function(cid, cols_arr, newcid, source, data)
{
var list = this.contact_list;
cid = this.html_identifier(cid);
// when in searching mode, concat cid with the source name
if (!list.rows[cid]) {
cid = cid + '-' + source;
if (newcid)
newcid = newcid + '-' + source;
}
list.update_row(cid, cols_arr, newcid, true);
list.data[cid] = data;
};
// add row to contacts list
this.add_contact_row = function(cid, cols, classes, data)
{
if (!this.gui_objects.contactslist)
return false;
var c, col, list = this.contact_list,
row = { cols:[] };
row.id = 'rcmrow' + this.html_identifier(cid);
row.className = 'contact ' + (classes || '');
if (list.in_selection(cid))
row.className += ' selected';
// add each submitted col
for (c in cols) {
col = {};
col.className = String(c).toLowerCase();
col.innerHTML = cols[c];
row.cols.push(col);
}
// store data in list member
list.data[cid] = data;
list.insert_row(row);
this.enable_command('export', list.rowcount > 0);
};
this.init_contact_form = function()
{
var col;
if (this.env.coltypes) {
this.set_photo_actions($('#ff_photo').val());
for (col in this.env.coltypes)
this.init_edit_field(col, null);
}
$('.contactfieldgroup .row a.deletebutton').click(function() {
ref.delete_edit_field(this);
return false;
});
$('select.addfieldmenu').change(function() {
ref.insert_edit_field($(this).val(), $(this).attr('rel'), this);
this.selectedIndex = 0;
});
// enable date pickers on date fields
if ($.datepicker && this.env.date_format) {
$.datepicker.setDefaults({
dateFormat: this.env.date_format,
changeMonth: true,
changeYear: true,
yearRange: '-120:+10',
showOtherMonths: true,
selectOtherMonths: true
});
$('input.datepicker').datepicker();
}
// Submit search form on Enter
if (this.env.action == 'search')
$(this.gui_objects.editform).append($('<input type="submit">').hide())
.submit(function() { $('input.mainaction').click(); return false; });
};
// group creation dialog
this.group_create = function()
{
var input = $('<input>').attr('type', 'text'),
content = $('<label>').text(this.get_label('namex')).append(input),
source = this.env.source;
this.simple_dialog(content, 'newgroup', function() {
var name;
if (name = input.val()) {
ref.http_post('group-create', {_source: source, _name: name},
ref.set_busy(true, 'loading'));
return true;
}
});
};
// group rename dialog
this.group_rename = function()
{
if (!this.env.group)
return;
var group_name = this.env.contactgroups['G' + this.env.source + this.env.group].name,
input = $('<input>').attr('type', 'text').val(group_name),
content = $('<label>').text(this.get_label('namex')).append(input),
source = this.env.source,
group = this.env.group;
this.simple_dialog(content, 'grouprename', function() {
var name;
if ((name = input.val()) && name != group_name) {
ref.http_post('group-rename', {_source: source, _gid: group, _name: name},
ref.set_busy(true, 'loading'));
return true;
}
});
};
this.group_delete = function()
{
if (this.env.group) {
var group = this.env.group;
this.confirm_dialog(this.get_label('deletegroupconfirm'), 'delete', function() {
var lock = ref.set_busy(true, 'groupdeleting');
ref.http_post('group-delete', {_source: ref.env.source, _gid: group}, lock);
});
}
};
// callback from server upon group-delete command
this.remove_group_item = function(prop)
{
var key = 'G'+prop.source+prop.id;
if (this.treelist.remove(key)) {
// make sure there is no cached address book or contact group selectors
this.destroy_entity_selector('addressbook-selector');
this.destroy_entity_selector('contactgroup-selector');
this.triggerEvent('group_delete', { source:prop.source, id:prop.id });
delete this.env.contactfolders[key];
delete this.env.contactgroups[key];
}
if (prop.source == this.env.source && prop.id == this.env.group)
this.list_contacts(prop.source, 0);
};
//assign selected contacts to a group
this.group_assign_selected = function(props, obj, event)
{
var cid = ref.contact_list.get_selection();
var source = ref.env.source;
this.contactgroup_selector(event, function(to) { ref.group_member_change('add', cid, source, to); });
};
//remove selected contacts from current active group
this.group_remove_selected = function()
{
this.http_post('group-delmembers', {_cid: this.contact_list.get_selection(),
_source: this.env.source, _gid: this.env.group});
};
//callback after deleting contact(s) from current group
this.remove_group_contacts = function(props)
{
if (this.env.group !== undefined && (this.env.group === props.gid)) {
var n, selection = this.contact_list.get_selection();
for (n=0; n<selection.length; n++) {
id = selection[n];
this.contact_list.remove_row(id, (n == selection.length-1));
this.contact_list.clear_selection();
}
}
};
// callback for creating a new contact group
this.insert_contact_group = function(prop)
{
prop.type = 'group';
var key = 'G'+prop.source+prop.id,
link = $('<a>').attr('href', '#')
.attr('rel', prop.source+':'+prop.id)
.click(function() { return ref.command('listgroup', prop, this); })
.html(prop.name);
this.env.contactfolders[key] = this.env.contactgroups[key] = prop;
this.treelist.insert({ id:key, html:link, classes:['contactgroup'] }, prop.source, 'contactgroup');
// make sure there is no cached address book or contact group selectors
this.destroy_entity_selector('addressbook-selector');
this.destroy_entity_selector('contactgroup-selector');
this.triggerEvent('group_insert', { id:prop.id, source:prop.source, name:prop.name, li:this.treelist.get_item(key) });
};
// callback for renaming a contact group
this.update_contact_group = function(prop)
{
var key = 'G'+prop.source+prop.id,
newnode = {};
// group ID has changed, replace link node and identifiers
if (prop.newid) {
var newkey = 'G'+prop.source+prop.newid,
newprop = $.extend({}, prop);
this.env.contactfolders[newkey] = this.env.contactfolders[key];
this.env.contactfolders[newkey].id = prop.newid;
this.env.group = prop.newid;
delete this.env.contactfolders[key];
delete this.env.contactgroups[key];
newprop.id = prop.newid;
newprop.type = 'group';
newnode.id = newkey;
newnode.html = $('<a>').attr('href', '#')
.attr('rel', prop.source+':'+prop.newid)
.click(function() { return ref.command('listgroup', newprop, this); })
.html(prop.name);
}
// update displayed group name
else {
$(this.treelist.get_item(key)).children().first().html(prop.name);
this.env.contactfolders[key].name = this.env.contactgroups[key].name = prop.name;
if (prop.source == this.env.source && prop.id == this.env.group)
this.set_group_prop(prop);
}
// update list node and re-sort it
this.treelist.update(key, newnode, true);
// make sure there is no cached address book or contact group selectors
this.destroy_entity_selector('addressbook-selector');
this.destroy_entity_selector('contactgroup-selector');
this.triggerEvent('group_update', { id:prop.id, source:prop.source, name:prop.name, li:this.treelist.get_item(key), newid:prop.newid });
};
this.update_group_commands = function()
{
var source = this.env.source != '' ? this.env.address_sources[this.env.source] : null,
supported = source && source.groups && !source.readonly;
this.enable_command('group-create', supported);
this.enable_command('group-rename', 'group-delete', supported && this.env.group);
};
this.init_edit_field = function(col, elem)
{
var label = this.env.coltypes[col].label;
if (!elem)
elem = $('.ff_' + col);
if (label && !$('label[for="ff_' + col + '"]').length)
elem.placeholder(label);
};
this.insert_edit_field = function(col, section, menu)
{
// just make pre-defined input field visible
var elem = $('#ff_' + col);
if (elem.length) {
$('label[for="ff_' + col + '"]').parent().show();
elem.show().focus();
$(menu).children('option[value="' + col + '"]').prop('disabled', true);
}
else {
var lastelem = $('.ff_' + col),
appendcontainer = $('#contactsection'+section+' .contactcontroller'+col);
if (!appendcontainer.length) {
var sect = $('#contactsection'+section),
lastgroup = $('.contactfieldgroup', sect).last();
appendcontainer = $('<fieldset>').addClass('contactfieldgroup contactcontroller'+col);
if (lastgroup.length)
appendcontainer.insertAfter(lastgroup);
else
sect.prepend(appendcontainer);
}
if (appendcontainer.get(0).nodeName == 'FIELDSET') {
var label, input,
colprop = this.env.coltypes[col],
name_suffix = colprop.limit != 1 ? '[]' : '',
compact = $(menu).data('compact') ? true : false,
input_id = 'ff_' + col + (colprop.count || 0),
row = $('<div>').addClass('row input-group'),
cell = $('<div>').addClass('contactfieldcontent ' + colprop.type);
// Field label
if (colprop.subtypes_select) {
label = $(colprop.subtypes_select);
if (!compact)
label = $('<div>').addClass('contactfieldlabel label').append(label);
else
label.addClass('input-group-prepend');
}
else {
label = $('<label>').addClass('contactfieldlabel label input-group-text')
.attr('for', input_id).text(colprop.label);
if (compact)
label = $('<span class="input-group-prepend">').append(label);
}
// Field input
if (colprop.type == 'text' || colprop.type == 'date') {
input = $('<input>')
.addClass('form-control ff_' + col)
.attr({type: 'text', name: '_'+col+name_suffix, size: colprop.size, id: input_id});
this.init_edit_field(col, input);
if (colprop.type == 'date' && $.datepicker)
input.addClass('datepicker').datepicker();
}
else if (colprop.type == 'textarea') {
input = $('<textarea>')
.addClass('form-control ff_' + col)
.attr({ name: '_' + col + name_suffix, cols: colprop.size, rows: colprop.rows, id: input_id });
this.init_edit_field(col, input);
}
else if (colprop.type == 'composite') {
var i, childcol, cp, first, templ, cols = [], suffices = [], content = cell;
row.addClass('composite');
if (compact)
content = $('<div class="content input-group-text">');
// read template for composite field order
if (templ = this.env[col + '_template']) {
for (i=0; i < templ.length; i++) {
cols.push(templ[i][1]);
suffices.push(templ[i][2]);
}
}
else { // list fields according to appearance in colprop
for (childcol in colprop.childs)
cols.push(childcol);
}
for (i=0; i < cols.length; i++) {
childcol = cols[i];
cp = colprop.childs[childcol];
input = $('<input>')
.addClass('form-control ff_' + childcol)
.attr({ type: 'text', name: '_' + childcol + name_suffix, size: cp.size })
.appendTo(content);
if (!compact)
content.append(suffices[i] || " ");
this.init_edit_field(childcol, input);
if (!first) first = input;
}
if (compact)
input = content;
else
input = first; // set focus to the first of this composite fields
}
else if (colprop.type == 'select') {
input = $('<select>')
.addClass('custom-select ff_' + col)
.attr({ name: '_' + col + name_suffix, id: input_id });
var options = input.attr('options');
options[options.length] = new Option('---', '');
if (colprop.options)
$.each(colprop.options, function(i, val) { options[options.length] = new Option(val, i); });
}
if (input) {
var delbutton = $('<a href="#del"></a>')
.addClass('contactfieldbutton deletebutton input-group-text icon delete')
.attr({title: this.get_label('delete'), rel: col})
.html(this.env.delbutton)
.click(function() { ref.delete_edit_field(this); return false; });
row.append(label);
if (!compact) {
if (colprop.type != 'composite')
cell.append(input);
row.append(cell.append(delbutton));
}
else {
row.append(input).append(delbutton);
delbutton.wrap('<span class="input-group-append">');
}
row.appendTo(appendcontainer.show());
if (input.is('div'))
input.find('input').first().focus();
else
input.first().focus();
// disable option if limit reached
if (!colprop.count) colprop.count = 0;
if (++colprop.count == colprop.limit && colprop.limit)
$(menu).children('option[value="' + col + '"]').prop('disabled', true);
this.triggerEvent('insert-edit-field', input);
}
}
}
};
this.delete_edit_field = function(elem)
{
var col = $(elem).attr('rel'),
colprop = this.env.coltypes[col],
input_group = $(elem).parents('div.row'),
fieldset = $(elem).parents('fieldset.contactfieldgroup'),
addmenu = fieldset.parent().find('select.addfieldmenu');
// just clear input but don't hide the last field
if (--colprop.count <= 0 && colprop.visible)
input_group.find('input').val('').blur();
else {
input_group.remove();
// hide entire fieldset if no more rows
if (!fieldset.children('div.row').length)
fieldset.hide();
}
// enable option in add-field selector or insert it if necessary
if (addmenu.length) {
var option = addmenu.children('option[value="'+col+'"]');
if (option.length)
option.prop('disabled', false);
else
option = $('<option>').attr('value', col).html(colprop.label).appendTo(addmenu);
addmenu.show();
}
};
this.upload_contact_photo = function(form)
{
if (form && form.elements._photo.value) {
this.async_upload_form(form, 'upload-photo', function(e) {
ref.set_busy(false, null, ref.file_upload_id);
});
// display upload indicator
this.file_upload_id = this.set_busy(true, 'uploading');
}
};
this.replace_contact_photo = function(id)
{
var img_src = id == '-del-' ? this.env.photo_placeholder :
this.env.comm_path + '&_action=photo&_source=' + this.env.source + '&_cid=' + (this.env.cid || 0) + '&_photo=' + id;
this.set_photo_actions(id);
$(this.gui_objects.contactphoto).children('img').attr('src', img_src);
};
this.photo_upload_end = function()
{
this.set_busy(false, null, this.file_upload_id);
delete this.file_upload_id;
};
this.set_photo_actions = function(id)
{
var n, buttons = this.buttons['upload-photo'];
for (n=0; buttons && n < buttons.length; n++)
$('a#'+buttons[n].id).html(this.get_label(id == '-del-' ? 'addphoto' : 'replacephoto'));
$('#ff_photo').val(id);
this.enable_command('upload-photo', this.env.coltypes.photo ? true : false);
this.enable_command('delete-photo', this.env.coltypes.photo && id != '-del-');
};
// load advanced search page
this.advanced_search = function()
{
var dialog = $('<iframe>').attr('src', this.url('search', {_form: 1, _framed: 1})),
search_func = function() {
var valid = false, form = {_adv: 1};
$.each($(dialog[0].contentWindow.rcmail.gui_objects.editform).serializeArray(), function() {
if (this.name.match(/^_search/) && this.value != '') {
form[this.name] = this.value;
valid = true;
}
});
if (valid) {
ref.http_post('search', form, ref.set_busy(true, 'searching'));
return true;
}
};
this.simple_dialog(dialog, this.gettext('advsearch'), search_func, {
button: 'search',
width: 600,
height: 500
});
return true;
};
// unselect directory/group
this.unselect_directory = function()
{
this.select_folder('');
this.enable_command('search-delete', false);
};
// callback for creating a new saved search record
this.insert_saved_search = function(name, id)
{
var key = 'S'+id,
link = $('<a>').attr('href', '#')
.attr('rel', id)
.click(function() { return ref.command('listsearch', id, this); })
.html(name),
prop = { name:name, id:id };
this.savedsearchlist.insert({ id:key, html:link, classes:['contactsearch'] }, null, 'contactsearch');
this.select_folder(key,'',true);
this.enable_command('search-delete', true);
this.env.search_id = id;
this.triggerEvent('abook_search_insert', prop);
};
// creates a dialog for saved search
this.search_create = function()
{
var input = $('<input>').attr('type', 'text'),
content = $('<label>').text(this.get_label('namex')).append(input);
this.simple_dialog(content, 'searchsave',
function() {
var name;
if (name = input.val()) {
ref.http_post('search-create', {_search: ref.env.search_request, _name: name},
ref.set_busy(true, 'loading'));
return true;
}
}
);
};
this.search_delete = function()
{
if (this.env.search_request) {
var lock = this.set_busy(true, 'savedsearchdeleting');
this.http_post('search-delete', {_sid: this.env.search_id}, lock);
}
};
// callback from server upon search-delete command
this.remove_search_item = function(id)
{
var li, key = 'S'+id;
if (this.savedsearchlist.remove(key)) {
this.triggerEvent('search_delete', { id:id, li:li });
}
this.env.search_id = null;
this.env.search_request = null;
this.list_contacts_clear();
this.reset_qsearch();
this.enable_command('search-delete', 'search-create', false);
};
this.listsearch = function(id)
{
var lock = this.set_busy(true, 'searching');
if (this.contact_list) {
this.list_contacts_clear();
}
this.reset_qsearch();
if (this.savedsearchlist) {
this.treelist.select('');
this.savedsearchlist.select('S'+id);
}
else
this.select_folder('S'+id, '', true);
// reset vars
this.env.current_page = 1;
this.http_request('search', {_sid: id}, lock);
};
// display a dialog with QR code image
this.qrcode = function()
{
var title = this.get_label('qrcode'),
options = {button: false, cancel_button: 'close', width: 300, height: 300},
img = new Image(300, 300);
img.src = this.url('addressbook/qrcode', {_source: this.env.source, _cid: this.get_single_cid()});
return this.simple_dialog(img, title, null, options);
};
/*********************************************************/
/********* user settings methods *********/
/*********************************************************/
// preferences section select and load options frame
this.section_select = function(list)
{
var win, id = list.get_single_selection();
if (id && (win = this.get_frame_window(this.env.contentframe))) {
this.location_href({_action: 'edit-prefs', _section: id, _framed: 1}, win, true);
}
};
this.response_select = function(list)
{
var id = list.get_single_selection();
this.enable_command('delete', !!id && $.inArray(id, this.env.readonly_responses) < 0);
if (id) {
this.load_response(id, 'edit-response');
}
};
// load response record
this.load_response = function(id, action)
{
var win;
if (win = this.get_frame_window(this.env.contentframe)) {
if (id || action == 'add-response') {
if (!id)
this.responses_list.clear_selection();
this.location_href({_action: action, _key: id, _framed: 1}, win, true);
}
}
};
this.identity_select = function(list)
{
var id = list.get_single_selection();
this.enable_command('delete', !!id && list.rowcount > 1 && this.env.identities_level < 2);
if (id) {
this.load_identity(id, 'edit-identity');
}
};
// load identity record
this.load_identity = function(id, action)
{
var win;
if (win = this.get_frame_window(this.env.contentframe)) {
if (id || action == 'add-identity') {
if (!id)
this.identity_list.clear_selection();
this.location_href({_action: action, _iid: id, _framed: 1}, win, true);
}
}
};
this.delete_identity = function(id)
{
// exit if no identity is specified or if selection is empty
var selection = this.identity_list.get_selection();
if (!(selection.length || this.env.iid))
return;
if (!id)
id = this.env.iid ? this.env.iid : selection[0];
// submit request with appended token
if (id) {
this.confirm_dialog(this.get_label('deleteidentityconfirm'), 'delete', function() {
ref.http_post('settings/delete-identity', { _iid: id }, true);
});
}
};
this.update_identity_row = function(id, name, add)
{
var list = this.identity_list,
rid = this.html_identifier(id);
if (add) {
list.insert_row({ id:'rcmrow'+rid, cols:[ { className:'mail', innerHTML:name } ] });
list.select(rid);
}
else {
list.update_row(rid, [ name ]);
}
};
this.update_response_row = function(response, oldkey)
{
var list = this.responses_list;
if (list && oldkey) {
list.update_row(oldkey, [ response.name ], response.key, true);
}
else if (list) {
list.insert_row({ id:'rcmrow'+response.key, cols:[ { className:'name', innerHTML:response.name } ] });
list.select(response.key);
}
};
this.remove_response = function(key)
{
var frame;
if (this.env.textresponses) {
delete this.env.textresponses[key];
}
if (this.responses_list) {
this.responses_list.remove_row(key);
this.show_contentframe(false);
}
this.enable_command('delete', false);
};
this.remove_identity = function(id)
{
var frame, list = this.identity_list,
rid = this.html_identifier(id);
if (list && id) {
list.remove_row(rid);
this.show_contentframe(false);
}
this.enable_command('delete', false);
};
/*********************************************************/
/********* folder manager methods *********/
/*********************************************************/
this.init_subscription_list = function()
{
var delim = RegExp.escape(this.env.delimiter);
this.last_sub_rx = RegExp('['+delim+']?[^'+delim+']+$');
this.subscription_list = new rcube_treelist_widget(this.gui_objects.subscriptionlist, {
selectable: true,
tabexit: false,
parent_focus: true,
id_prefix: 'rcmli',
id_encode: this.html_identifier_encode,
id_decode: this.html_identifier_decode,
searchbox: '#foldersearch'
});
this.subscription_list
.addEventListener('select', function(node) { ref.subscription_select(node.id); })
.addEventListener('collapse', function(node) { ref.folder_collapsed(node) })
.addEventListener('expand', function(node) { ref.folder_collapsed(node) })
.addEventListener('search', function(p) { if (p.query) ref.subscription_select(); })
.draggable({cancel: 'li.mailbox.root,input,div.treetoggle'})
.droppable({
// @todo: find better way, accept callback is executed for every folder
// on the list when dragging starts (and stops), this is slow, but
// I didn't find a method to check droptarget on over event
accept: function(node) {
if (!node.is('.mailbox'))
return false;
var source_folder = ref.folder_id2name(node.attr('id')),
dest_folder = ref.folder_id2name(this.id),
source = ref.env.subscriptionrows[source_folder],
dest = ref.env.subscriptionrows[dest_folder];
return source && !source[2]
&& dest_folder != source_folder.replace(ref.last_sub_rx, '')
&& !dest_folder.startsWith(source_folder + ref.env.delimiter);
},
drop: function(e, ui) {
var source = ref.folder_id2name(ui.draggable.attr('id')),
dest = ref.folder_id2name(this.id);
ref.subscription_move_folder(source, dest);
}
});
};
this.folder_id2name = function(id)
{
return id ? ref.html_identifier_decode(id.replace(/^rcmli/, '')) : null;
};
this.subscription_select = function(id)
{
var folder;
if (id && id != '*' && (folder = this.env.subscriptionrows[id])) {
this.env.mailbox = id;
this.show_folder(id);
this.enable_command('delete-folder', !folder[2]);
}
else {
this.env.mailbox = null;
this.show_contentframe(false);
this.enable_command('delete-folder', 'purge', false);
}
};
this.subscription_move_folder = function(from, to)
{
if (from && to !== null && from != to && to != from.replace(this.last_sub_rx, '')) {
var path = from.split(this.env.delimiter),
basename = path.pop(),
newname = to === '' || to === '*' ? basename : to + this.env.delimiter + basename;
if (newname != from) {
this.confirm_dialog(this.get_label('movefolderconfirm'), 'move', function() {
ref.http_post('rename-folder', {_folder_oldname: from, _folder_newname: newname},
ref.set_busy(true, 'foldermoving'));
}, {button_class: 'save move'});
}
}
};
// tell server to create and subscribe a new mailbox
this.create_folder = function()
{
this.show_folder('', this.env.mailbox);
};
// delete a specific mailbox with all its messages
this.delete_folder = function(name)
{
if (!name)
name = this.env.mailbox;
if (name) {
this.confirm_dialog(this.get_label('deletefolderconfirm'), 'delete', function() {
ref.http_post('delete-folder', {_mbox: name}, ref.set_busy(true, 'folderdeleting'));
});
}
};
// Add folder row to the table and initialize it
this.add_folder_row = function (id, name, display_name, is_protected, subscribed, class_name, refrow, subfolders)
{
if (!this.gui_objects.subscriptionlist)
return false;
// reset searching
if (this.subscription_list.is_search()) {
this.subscription_select();
this.subscription_list.reset_search();
}
// disable drag-n-drop temporarily
// some skins disable dragging in mobile mode, so we have to check if it is still draggable
if (this.subscription_list.is_draggable())
this.subscription_list.draggable('destroy').droppable('destroy');
var row, n, tmp, tmp_name, rowid, collator, pos, p, parent = '',
folders = [], list = [], slist = [],
list_element = $(this.gui_objects.subscriptionlist);
row = refrow ? refrow : $($('li', list_element).get(1)).clone(true);
if (!row.length) {
// Refresh page if we don't have a table row to clone
this.goto_url('folders');
return false;
}
// set ID, reset css class
row.attr({id: 'rcmli' + this.html_identifier_encode(id), 'class': class_name});
if (!refrow || !refrow.length) {
// remove old data, subfolders and toggle
$('ul,div.treetoggle', row).remove();
row.removeData('filtered');
}
// set folder name
$('a', row).first().text(display_name).removeAttr('title');
// update subscription checkbox
$('input[name="_subscribed[]"]', row).first().val(id)
.prop({checked: subscribed ? true : false, disabled: is_protected ? true : false});
// add to folder/row-ID map
this.env.subscriptionrows[id] = [name, display_name, false];
// copy folders data to an array for sorting
$.each(this.env.subscriptionrows, function(k, v) { v[3] = k; folders.push(v); });
try {
// use collator if supported (FF29, IE11, Opera15, Chrome24)
collator = new Intl.Collator(this.env.locale.replace('_', '-'));
}
catch (e) {};
// sort folders
folders.sort(function(a, b) {
var i, f1, f2,
path1 = a[0].split(ref.env.delimiter),
path2 = b[0].split(ref.env.delimiter),
len = path1.length;
for (i=0; i<len; i++) {
f1 = path1[i];
f2 = path2[i];
if (f1 !== f2) {
if (f2 === undefined)
return 1;
if (collator)
return collator.compare(f1, f2);
else
return f1 < f2 ? -1 : 1;
}
else if (i == len-1) {
return -1
}
}
});
for (n in folders) {
p = folders[n][3];
// protected folder
if (folders[n][2]) {
tmp_name = p + this.env.delimiter;
// prefix namespace cannot have subfolders (#1488349)
if (tmp_name == this.env.prefix_ns)
continue;
slist.push(p);
tmp = tmp_name;
}
// protected folder's child
else if (tmp && p.startsWith(tmp))
slist.push(p);
// other
else {
list.push(p);
tmp = null;
}
}
// check if subfolder of a protected folder
for (n=0; n<slist.length; n++) {
if (id.startsWith(slist[n] + this.env.delimiter))
rowid = slist[n];
}
// find folder position after sorting
for (n=0; !rowid && n<list.length; n++) {
if (n && list[n] == id)
rowid = list[n-1];
}
// add row to the table
if (rowid && (n = this.subscription_list.get_item(rowid, true))) {
// find parent folder
if (pos = id.lastIndexOf(this.env.delimiter)) {
parent = id.substring(0, pos);
parent = this.subscription_list.get_item(parent, true);
// add required tree elements to the parent if not already there
if (!$('div.treetoggle', parent).length) {
$('<div>&nbsp;</div>').addClass('treetoggle collapsed').appendTo(parent);
}
if (!$('ul', parent).length) {
$('<ul>').css('display', 'none').appendTo(parent);
}
}
if (parent && n == parent) {
$('ul', parent).first().append(row);
}
else {
while (p = $(n).parent().parent().get(0)) {
if (parent && p == parent)
break;
if (!$(p).is('li.mailbox'))
break;
n = p;
}
$(n).after(row);
}
}
else {
list_element.append(row);
}
// add subfolders
$.extend(this.env.subscriptionrows, subfolders || {});
// update list widget
this.subscription_list.reset(true);
this.subscription_select();
// expand parent
if (parent) {
this.subscription_list.expand(this.folder_id2name(parent.id));
}
row = row.show().get(0);
if (row.scrollIntoView)
row.scrollIntoView(false);
return row;
};
// replace an existing table row with a new folder line (with subfolders)
this.replace_folder_row = function(oldid, id, name, display_name, is_protected, class_name)
{
if (!this.gui_objects.subscriptionlist) {
if (this.is_framed()) {
// @FIXME: for some reason this 'parent' variable need to be prefixed with 'window.'
return window.parent.rcmail.replace_folder_row(oldid, id, name, display_name, is_protected, class_name);
}
return false;
}
// reset searching
if (this.subscription_list.is_search()) {
this.subscription_select();
this.subscription_list.reset_search();
}
var subfolders = {},
row = this.subscription_list.get_item(oldid, true),
parent = $(row).parent(),
old_folder = this.env.subscriptionrows[oldid],
prefix_len_id = oldid.length,
prefix_len_name = old_folder[0].length,
subscribed = $('input[name="_subscribed[]"]', row).first().prop('checked');
// no renaming, only update class_name
if (oldid == id) {
$(row).attr('class', class_name || '');
return;
}
// update subfolders
$('li', row).each(function() {
var fname = ref.folder_id2name(this.id),
folder = ref.env.subscriptionrows[fname],
newid = id + fname.slice(prefix_len_id);
this.id = 'rcmli' + ref.html_identifier_encode(newid);
$('input[name="_subscribed[]"]', this).first().val(newid);
folder[0] = name + folder[0].slice(prefix_len_name);
subfolders[newid] = folder;
delete ref.env.subscriptionrows[fname];
});
// get row off the list
row = $(row).detach();
delete this.env.subscriptionrows[oldid];
// remove parent list/toggle elements if not needed
if (parent.get(0) != this.gui_objects.subscriptionlist && !$('li', parent).length) {
$('ul,div.treetoggle', parent.parent()).remove();
}
// move the existing table row
this.add_folder_row(id, name, display_name, is_protected, subscribed, class_name, row, subfolders);
};
// remove the table row of a specific mailbox from the table
this.remove_folder_row = function(folder)
{
// reset searching
if (this.subscription_list.is_search()) {
this.subscription_select();
this.subscription_list.reset_search();
}
var list = [], row = this.subscription_list.get_item(folder, true);
// get subfolders if any
$('li', row).each(function() { list.push(ref.folder_id2name(this.id)); });
// remove folder row (and subfolders)
this.subscription_list.remove(folder);
// update local list variable
list.push(folder);
$.each(list, function(i, v) { delete ref.env.subscriptionrows[v]; });
};
this.subscribe = function(folder)
{
if (folder) {
var lock = this.display_message('foldersubscribing', 'loading');
this.http_post('subscribe', {_mbox: folder}, lock);
}
};
this.unsubscribe = function(folder)
{
if (folder) {
var lock = this.display_message('folderunsubscribing', 'loading');
this.http_post('unsubscribe', {_mbox: folder}, lock);
}
};
// when user select a folder in manager
this.show_folder = function(folder, path, force)
{
var win, target = window,
action = folder === '' ? 'add' : 'edit',
url = '&_action=' + action + '-folder&_mbox=' + urlencode(folder);
if (path)
url += '&_path='+urlencode(path);
if (win = this.get_frame_window(this.env.contentframe)) {
target = win;
url += '&_framed=1';
}
if (String(target.location.href).indexOf(url) >= 0 && !force)
this.show_contentframe(true);
else
this.location_href(this.env.comm_path+url, target, true);
};
// disables subscription checkbox (for protected folder)
this.disable_subscription = function(folder)
{
var row = this.subscription_list.get_item(folder, true);
if (row)
$('input[name="_subscribed[]"]', row).first().prop('disabled', true);
};
// resets state of subscription checkbox (e.g. on error)
this.reset_subscription = function(folder, state)
{
var row = this.subscription_list.get_item(folder, true);
if (row)
$('input[name="_subscribed[]"]', row).first().prop('checked', state);
};
this.folder_size = function(folder)
{
var lock = this.set_busy(true, 'loading');
this.http_post('folder-size', {_mbox: folder}, lock);
};
this.folder_size_update = function(size)
{
$('#folder-size').replaceWith(size);
};
// filter folders by namespace
this.folder_filter = function(prefix)
{
this.subscription_list.reset_search();
this.subscription_list.container.children('li').each(function() {
var i, folder = ref.folder_id2name(this.id);
// show all folders
if (prefix == '---') {
}
// got namespace prefix
else if (prefix) {
if (folder !== prefix) {
$(this).data('filtered', true).hide();
return
}
}
// no namespace prefix, filter out all other namespaces
else {
// first get all namespace roots
for (i in ref.env.ns_roots) {
if (folder === ref.env.ns_roots[i]) {
$(this).data('filtered', true).hide();
return;
}
}
}
$(this).removeData('filtered').show();
});
};
/*********************************************************/
/********* GUI functionality *********/
/*********************************************************/
this.init_button = function(cmd, prop)
{
var elm = document.getElementById(prop.id);
if (!elm)
return;
var preload = false;
if (prop.type == 'image') {
elm = elm.parentNode;
preload = true;
}
elm._command = cmd;
elm._id = prop.id;
if (prop.sel) {
elm.onmousedown = function(e) { return ref.button_sel(this._command, this._id); };
elm.onmouseup = function(e) { return ref.button_out(this._command, this._id); };
if (preload)
new Image().src = prop.sel;
}
if (prop.over) {
elm.onmouseover = function(e) { return ref.button_over(this._command, this._id); };
elm.onmouseout = function(e) { return ref.button_out(this._command, this._id); };
if (preload)
new Image().src = prop.over;
}
};
// set event handlers on registered buttons
this.init_buttons = function()
{
for (var cmd in this.buttons) {
if (typeof cmd !== 'string')
continue;
for (var i=0; i<this.buttons[cmd].length; i++) {
this.init_button(cmd, this.buttons[cmd][i]);
}
}
};
// set button to a specific state
this.set_button = function(command, state)
{
var n, button, obj, $obj, a_buttons = this.buttons[command],
len = a_buttons ? a_buttons.length : 0;
for (n=0; n<len; n++) {
button = a_buttons[n];
obj = document.getElementById(button.id);
if (!obj || button.status === state)
continue;
// get default/passive setting of the button
if (button.type == 'image' && !button.status) {
button.pas = obj._original_src ? obj._original_src : obj.src;
// respect PNG fix on IE browsers
if (obj.runtimeStyle && obj.runtimeStyle.filter && obj.runtimeStyle.filter.match(/src=['"]([^'"]+)['"]/))
button.pas = RegExp.$1;
}
else if (!button.status)
button.pas = String(obj.className);
button.status = state;
// set image according to button state
if (button.type == 'image' && button[state]) {
obj.src = button[state];
}
// set class name according to button state
else if (button[state] !== undefined) {
obj.className = button[state];
}
// disable/enable input buttons
if (button.type == 'input' || button.type == 'button') {
obj.disabled = state == 'pas';
}
else {
$obj = $(obj);
$obj
.attr('tabindex', state == 'pas' || state == 'sel' ? '-1' : ($obj.attr('data-tabindex') || '0'))
.attr('aria-disabled', state == 'pas' || state == 'sel' ? 'true' : 'false');
}
}
};
// display a specific alttext
this.set_alttext = function(command, label)
{
var n, button, obj, link, a_buttons = this.buttons[command],
len = a_buttons ? a_buttons.length : 0;
for (n=0; n<len; n++) {
button = a_buttons[n];
obj = document.getElementById(button.id);
if (button.type == 'image' && obj) {
obj.setAttribute('alt', this.get_label(label));
if ((link = obj.parentNode) && link.tagName.toLowerCase() == 'a')
link.setAttribute('title', this.get_label(label));
}
else if (obj)
obj.setAttribute('title', this.get_label(label));
}
};
// mouse over button
this.button_over = function(command, id)
{
this.button_event(command, id, 'over');
};
// mouse down on button
this.button_sel = function(command, id)
{
this.button_event(command, id, 'sel');
};
// mouse out of button
this.button_out = function(command, id)
{
this.button_event(command, id, 'act');
};
// event of button
this.button_event = function(command, id, event)
{
var n, button, obj, a_buttons = this.buttons[command],
len = a_buttons ? a_buttons.length : 0;
for (n=0; n<len; n++) {
button = a_buttons[n];
if (button.id == id && button.status == 'act') {
if (button[event] && (obj = document.getElementById(button.id))) {
obj[button.type == 'image' ? 'src' : 'className'] = button[event];
}
if (event == 'sel') {
this.buttons_sel[id] = command;
}
}
}
};
// write to the document/window title
this.set_pagetitle = function(title)
{
if (title && document.title)
document.title = title;
};
// display a system message, list of types in common.css (below #message definition)
this.display_message = function(msg, type, timeout, key)
{
if (msg && msg.length && /^[a-z._]+$/.test(msg))
msg = this.get_label(msg);
// pass command to parent window
if (this.is_framed())
return parent.rcmail.display_message(msg, type, timeout);
if (!this.gui_objects.message) {
// save message in order to display after page loaded
if (type != 'loading')
this.pending_message = [msg, type, timeout, key];
return 1;
}
if (!type)
type = 'notice';
else if (type == 'loading') {
if (!key)
key = 'loading';
if (!timeout)
timeout = this.env.request_timeout * 1000;
if (!msg)
msg = this.get_label('loading');
}
if (!key)
key = this.html_identifier(msg);
var date = new Date(),
id = type + date.getTime();
if (!timeout) {
switch (type) {
case 'error':
case 'warning':
timeout = this.message_time * 2;
break;
case 'uploading':
timeout = 0;
break;
default:
timeout = this.message_time;
}
}
// The same message is already displayed
if (this.messages[key]) {
// replace label
if (this.messages[key].obj)
$('div.content', this.messages[key].obj).html(msg);
// store label in stack
if (type == 'loading') {
this.messages[key].labels.push({'id': id, 'msg': msg});
}
// add element and set timeout
this.messages[key].elements.push(id);
setTimeout(function() { ref.hide_message(id, type == 'loading'); }, timeout);
return id;
}
// create DOM object and display it
var obj = $('<div>').addClass(type + ' content').html(msg).data('key', key),
cont = $(this.gui_objects.message).append(obj).show();
this.messages[key] = {'obj': obj, 'elements': [id]};
if (type == 'loading') {
this.messages[key].labels = [{'id': id, 'msg': msg}];
}
else if (type != 'uploading') {
obj.click(function() { return ref.hide_message(obj); })
.attr('role', 'alert');
}
this.triggerEvent('message', { message:msg, type:type, timeout:timeout, object:obj });
if (timeout > 0)
setTimeout(function() { ref.hide_message(id, type != 'loading'); }, timeout);
return id;
};
// make a message to disapear
this.hide_message = function(obj, fade)
{
// pass command to parent window
if (this.is_framed())
return parent.rcmail.hide_message(obj, fade);
if (!this.gui_objects.message)
return;
var k, n, i, o, m = this.messages;
// Hide message by object, don't use for 'loading'!
if (typeof obj === 'object') {
o = $(obj);
k = o.data('key');
this.hide_message_object(o, fade);
if (m[k])
delete m[k];
}
// Hide message by id
else {
for (k in m) {
for (n in m[k].elements) {
if (m[k] && m[k].elements[n] == obj) {
m[k].elements.splice(n, 1);
// hide DOM element if last instance is removed
if (!m[k].elements.length) {
this.hide_message_object(m[k].obj, fade);
delete m[k];
}
// set pending action label for 'loading' message
else if (k == 'loading') {
for (i in m[k].labels) {
if (m[k].labels[i].id == obj) {
delete m[k].labels[i];
}
else {
o = m[k].labels[i].msg;
$('div.content', m[k].obj).html(o);
}
}
}
}
}
}
}
};
// hide message object and remove from the DOM
this.hide_message_object = function(o, fade)
{
if (fade)
o.fadeOut(600, function() { $(this).remove(); });
else
o.hide().remove();
};
// remove all messages immediately
this.clear_messages = function()
{
// pass command to parent window
if (this.is_framed())
return parent.rcmail.clear_messages();
var k, n, m = this.messages;
for (k in m)
for (n in m[k].elements)
if (m[k].obj)
this.hide_message_object(m[k].obj);
this.messages = {};
};
// display uploading message with progress indicator
// data should contain: name, total, current, percent, text
this.display_progress = function(data)
{
if (!data || !data.name)
return;
var msg = this.messages['progress' + data.name];
if (!data.label)
data.label = this.get_label('uploadingmany');
if (!msg) {
if (!data.percent || data.percent < 100)
this.display_message(data.label, 'uploading', 0, 'progress' + data.name);
return;
}
if (!data.total || data.percent >= 100) {
this.hide_message(msg.obj);
return;
}
if (data.text)
data.label += ' ' + data.text;
msg.obj.text(data.label);
};
// open a jquery UI dialog with the given content
this.show_popup_dialog = function(content, title, buttons, options)
{
// forward call to parent window
if (this.is_framed()) {
return parent.rcmail.show_popup_dialog(content, title, buttons, options);
}
var popup = $('<div class="popup">');
if (typeof content == 'object') {
popup.append(content);
if ($(content).is('iframe'))
popup.addClass('iframe');
}
else
popup.html(content);
// assign special classes to dialog buttons
var i = 0, fn = function(button, classes, idx) {
if (typeof button == 'function') {
button = {
click: button,
text: idx,
'class': classes
};
}
else {
buttons['class'] = classes;
}
return button;
};
if (options && options.button_classes)
$.each(buttons, function(idx, button) {
var cl = options.button_classes[i];
if (cl)
buttons[idx] = fn(button, cl, idx);
i++;
});
options = $.extend({
title: title,
buttons: buttons,
modal: true,
resizable: true,
width: 500,
close: function(event, ui) { $(this).remove(); }
}, options || {});
popup.dialog(options);
if (options.width)
popup.width(options.width);
if (options.height)
popup.height(options.height);
// resize and center popup
var win = $(window), w = win.width(), h = win.height(),
width = popup.width(),
height = options.height || (popup[0].scrollHeight + 20),
dialog = popup.parent(),
titlebar_height = $('.ui-dialog-titlebar', dialog).outerHeight() || 0,
buttonpane_height = $('.ui-dialog-buttonpane', dialog).outerHeight() || 0,
padding = (parseInt(dialog.css('padding-top')) + parseInt(popup.css('padding-top'))) * 2;
popup.dialog('option', {
height: Math.min(h - 40, height + titlebar_height + buttonpane_height + padding + 2),
width: Math.min(w - 20, width + 24)
});
// Don't propagate keyboard events to the UI below the dialog (#6055)
popup.parent().on('keydown keyup', function(e) { e.stopPropagation(); });
return popup;
};
// show_popup_dialog() wrapper for simple dialogs with action and Cancel buttons
this.simple_dialog = function(content, title, action_func, options)
{
if (!options)
options = {};
var title = this.get_label(title),
save_label = options.button || 'save',
save_class = options.button_class || save_label.replace(/^[^\.]+\./i, ''),
cancel_label = options.cancel_button || 'cancel',
cancel_class = options.cancel_class || cancel_label.replace(/^[^\.]+\./i, ''),
close_func = function(e, ui, dialog) {
(ref.is_framed() ? parent.$ : $)(dialog || this).dialog('close');
if (options.cancel_func) options.cancel_func(e, ref);
},
buttons = [{
text: this.get_label(cancel_label),
'class': cancel_class.replace(/close/i, 'cancel'),
click: close_func
}];
if (!action_func)
buttons[0]['class'] += ' mainaction';
else
buttons.unshift({
text: this.get_label(save_label),
'class': 'mainaction ' + save_class,
click: function(e, ui) { if (action_func(e, ref)) close_func(e, ui, this); }
});
return this.show_popup_dialog(content, title, buttons, options);
};
// show_popup_dialog() wrapper for alert() type dialogs
this.alert_dialog = function(content, action, options)
{
options = $.extend(options || {}, {
cancel_button: 'ok',
cancel_class: 'save',
cancel_func: action
});
return this.simple_dialog(content, options.title || 'alerttitle', null, options);
};
// simple_dialog() wrapper for confirm() type dialogs
this.confirm_dialog = function(content, button_label, action, options)
{
var action_func = function(e, ref) { action(e, ref); return true; };
options = $.extend(options || {}, {
button: button_label || 'continue'
});
return this.simple_dialog(content, options.title || 'confirmationtitle', action_func, options);
};
// enable/disable buttons for page shifting
this.set_page_buttons = function()
{
this.enable_command('nextpage', 'lastpage', this.env.pagecount > this.env.current_page);
this.enable_command('previouspage', 'firstpage', this.env.current_page > 1);
this.update_pagejumper();
};
// mark a mailbox as selected and set environment variable
this.select_folder = function(name, prefix, encode)
{
if (this.savedsearchlist) {
this.savedsearchlist.select('');
}
if (this.treelist) {
this.treelist.select(name);
}
else if (this.gui_objects.folderlist) {
$('li.selected', this.gui_objects.folderlist).removeClass('selected');
$(this.get_folder_li(name, prefix, encode)).addClass('selected');
// trigger event hook
this.triggerEvent('selectfolder', { folder:name, prefix:prefix });
}
};
// adds a class to selected folder
this.mark_folder = function(name, class_name, prefix, encode)
{
$(this.get_folder_li(name, prefix, encode)).addClass(class_name);
this.triggerEvent('markfolder', {folder: name, mark: class_name, status: true});
};
// adds a class to selected folder
this.unmark_folder = function(name, class_name, prefix, encode)
{
$(this.get_folder_li(name, prefix, encode)).removeClass(class_name);
this.triggerEvent('markfolder', {folder: name, mark: class_name, status: false});
};
// helper method to find a folder list item
this.get_folder_li = function(name, prefix, encode)
{
if (!prefix)
prefix = 'rcmli';
if (this.gui_objects.folderlist) {
name = this.html_identifier(name, encode);
return document.getElementById(prefix+name);
}
};
// for reordering column array (Konqueror workaround)
// and for setting some message list global variables
this.set_message_coltypes = function(listcols, repl, smart_col)
{
var list = this.message_list,
thead = list ? list.thead : null,
repl, cell, col, n, len, tr;
this.env.listcols = listcols;
if (!this.env.coltypes)
this.env.coltypes = {};
// replace old column headers
if (thead) {
if (repl) {
thead.innerHTML = '';
tr = document.createElement('tr');
for (c=0, len=repl.length; c < len; c++) {
cell = document.createElement('th');
cell.innerHTML = repl[c].html || '';
if (repl[c].id) cell.id = repl[c].id;
if (repl[c].className) cell.className = repl[c].className;
tr.appendChild(cell);
}
thead.appendChild(tr);
}
for (n=0, len=this.env.listcols.length; n<len; n++) {
col = this.env.listcols[n];
if ((cell = thead.rows[0].cells[n]) && (col == 'from' || col == 'to' || col == 'fromto')) {
$(cell).attr('rel', col).find('span,a').text(this.get_label(col == 'fromto' ? smart_col : col));
}
}
}
this.env.subject_col = null;
this.env.flagged_col = null;
this.env.status_col = null;
if (this.env.coltypes.folder)
this.env.coltypes.folder.hidden = !(this.env.search_request || this.env.search_id) || this.env.search_scope == 'base';
if ((n = $.inArray('subject', this.env.listcols)) >= 0) {
this.env.subject_col = n;
if (list)
list.subject_col = n;
}
if ((n = $.inArray('flag', this.env.listcols)) >= 0)
this.env.flagged_col = n;
if ((n = $.inArray('status', this.env.listcols)) >= 0)
this.env.status_col = n;
if (list) {
list.hide_column('folder', (this.env.coltypes.folder && this.env.coltypes.folder.hidden) || $.inArray('folder', this.env.listcols) < 0);
list.init_header();
}
};
// replace content of row count display
this.set_rowcount = function(text, mbox)
{
// #1487752
if (mbox && mbox != this.env.mailbox)
return false;
$(this.gui_objects.countdisplay).html(text);
// update page navigation buttons
this.set_page_buttons();
};
// replace content of mailboxname display
this.set_mailboxname = function(content)
{
if (this.gui_objects.mailboxname && content)
this.gui_objects.mailboxname.innerHTML = content;
};
// replace content of quota display
this.set_quota = function(content)
{
if (this.gui_objects.quotadisplay && content && content.type == 'text')
$(this.gui_objects.quotadisplay).text((content.percent||0) + '%').attr('title', content.title || '');
this.triggerEvent('setquota', content);
this.env.quota_content = content;
};
// update trash folder state
this.set_trash_count = function(count)
{
this[(count ? 'un' : '') + 'mark_folder'](this.env.trash_mailbox, 'empty', '', true);
};
// update the mailboxlist
this.set_unread_count = function(mbox, count, set_title, mark)
{
if (!this.gui_objects.mailboxlist)
return false;
this.env.unread_counts[mbox] = count;
this.set_unread_count_display(mbox, set_title);
if (mark)
this.mark_folder(mbox, mark, '', true);
else if (!count)
this.unmark_folder(mbox, 'recent', '', true);
this.mark_all_read_state();
};
// update the mailbox count display
this.set_unread_count_display = function(mbox, set_title)
{
var reg, link, text_obj, item, mycount, childcount, div;
if (item = this.get_folder_li(mbox, '', true)) {
mycount = this.env.unread_counts[mbox] ? this.env.unread_counts[mbox] : 0;
link = $(item).children('a').eq(0);
text_obj = link.children('span.unreadcount');
if (!text_obj.length && mycount)
text_obj = $('<span>').addClass('unreadcount').appendTo(link);
reg = /\s+\([0-9]+\)$/i;
childcount = 0;
if ((div = item.getElementsByTagName('div')[0]) &&
div.className.match(/collapsed/)) {
// add children's counters
for (var k in this.env.unread_counts)
if (k.startsWith(mbox + this.env.delimiter))
childcount += this.env.unread_counts[k];
}
if (mycount && text_obj.length)
text_obj.html(this.env.unreadwrap.replace(/%[sd]/, mycount));
else if (text_obj.length)
text_obj.remove();
// set parent's display
reg = new RegExp(RegExp.escape(this.env.delimiter) + '[^' + RegExp.escape(this.env.delimiter) + ']+$');
if (mbox.match(reg))
this.set_unread_count_display(mbox.replace(reg, ''), false);
// set the right classes
if ((mycount+childcount)>0)
$(item).addClass('unread');
else
$(item).removeClass('unread');
}
// set unread count to window title
reg = /^\([0-9]+\)\s+/i;
if (set_title && document.title) {
var new_title = '',
doc_title = String(document.title);
if (mycount && doc_title.match(reg))
new_title = doc_title.replace(reg, '('+mycount+') ');
else if (mycount)
new_title = '('+mycount+') '+doc_title;
else
new_title = doc_title.replace(reg, '');
this.set_pagetitle(new_title);
}
};
// display fetched raw headers
this.set_headers = function(content)
{
if (this.gui_objects.all_headers_box && content)
$(this.gui_objects.all_headers_box).html(content).show();
};
// display all-headers row and fetch raw message headers
this.show_headers = function(props, elem)
{
if (!this.gui_objects.all_headers_row || !this.gui_objects.all_headers_box || !this.env.uid)
return;
$(elem).removeClass('show-headers').addClass('hide-headers');
$(this.gui_objects.all_headers_row).show();
elem.onclick = function() { ref.command('hide-headers', '', elem); };
// fetch headers only once
if (!this.gui_objects.all_headers_box.innerHTML) {
this.http_request('headers', {_uid: this.env.uid, _mbox: this.env.mailbox},
this.display_message('', 'loading')
);
}
};
// hide all-headers row
this.hide_headers = function(props, elem)
{
if (!this.gui_objects.all_headers_row || !this.gui_objects.all_headers_box)
return;
$(elem).removeClass('hide-headers').addClass('show-headers');
$(this.gui_objects.all_headers_row).hide();
elem.onclick = function() { ref.command('show-headers', '', elem); };
};
// create folder selector popup
this.folder_selector = function(event, callback)
{
this.entity_selector('folder-selector', callback, this.env.mailboxes_list, function(obj, a) {
var n = 0, s = 0,
delim = ref.env.delimiter,
folder = ref.env.mailboxes[obj],
id = folder.id,
row = $('<li>');
if (folder.virtual)
a.addClass('virtual').attr({'aria-disabled': 'true', tabindex: '-1'});
else
a.addClass('active').data('id', folder.id);
if (folder['class'])
row.addClass(folder['class']);
// calculate/set indentation level
while ((s = id.indexOf(delim, s)) >= 0) {
n++; s++;
}
a.css('padding-left', n ? (n * 16) + 'px' : 0);
// add folder name element
a.append($('<span>').text(folder.name));
return row.append(a);
}, event);
};
// create addressbook selector popup
this.addressbook_selector = function(event, callback)
{
// build addressbook + groups list
var combined_sources = [];
// check we really need it before processing
if (!this.entity_selectors['addressbook-selector']) {
$.each(this.env.address_sources, function() {
if (!this.readonly) {
var source = this;
combined_sources.push(source);
$.each(ref.env.contactgroups, function() {
if (source.id === this.source) {
combined_sources.push(this);
}
});
}
});
}
this.entity_selector('addressbook-selector', callback, combined_sources, function(obj, a) {
if (obj.type == 'group') {
a.attr('rel', obj.source + ':' + obj.id)
.addClass('contactgroup active')
.data({source: obj.source, gid: obj.id, id: obj.source + ':' + obj.id})
.css('padding-left', '16px');
}
else {
a.addClass('addressbook active').data('id', obj.id);
}
a.append($('<span>').text(obj.name));
return $('<li>').append(a);
}, event);
};
// create contactgroup selector popup
this.contactgroup_selector = function(event, callback)
{
this.entity_selector('contactgroup-selector', callback, this.env.contactgroups, function(obj, a) {
if (ref.env.source === obj.source) {
a.addClass('contactgroup active')
.data({id: obj.id})
.append($('<span>').text(obj.name));
return $('<li>').append(a);
}
}, event);
};
// create selector popup (eg for folders or address books), position and display it
this.entity_selector = function(name, click_callback, entity_list, list_callback, event)
{
var container = this.entity_selectors[name];
if (!container) {
var rows = [],
container = $('<div>').attr('id', name).addClass('popupmenu'),
ul = $('<ul>').addClass('toolbarmenu'),
link = document.createElement('a');
link.href = '#';
link.className = 'icon';
// loop over entity list
$.each(entity_list, function(i) {
var a = $(link.cloneNode(false)).attr('rel', this.id);
rows.push(list_callback(this, a, i));
});
ul.append(rows).appendTo(container);
// temporarily show element to calculate its size
container.css({left: '-1000px', top: '-1000px'})
.appendTo(document.body).show();
// set max-height if the list is long
if (rows.length > 10)
container.css('max-height', $('li', container)[0].offsetHeight * 10 + 9);
// register delegate event handler for folder item clicks
container.on('click', 'a.active', function(e) {
container.data('callback')($(this).data('id'), this);
});
this.entity_selectors[name] = container;
}
container.data('callback', click_callback);
// position menu on the screen
this.show_menu(name, true, event);
};
this.destroy_entity_selector = function(name)
{
$("#" + name).remove();
delete this.entity_selectors[name];
};
/***********************************************/
/********* popup menu functions *********/
/***********************************************/
// Show/hide a specific popup menu
this.show_menu = function(prop, show, event)
{
var name = typeof prop == 'object' ? prop.menu : prop,
obj = $('#'+name),
ref = event && event.target ? $(event.target) : $(obj.attr('rel') || '#'+name+'link'),
keyboard = rcube_event.is_keyboard(event),
align = obj.attr('data-align') || '',
stack = false;
// find "real" button element
if (ref.get(0).tagName != 'A' && ref.closest('a').length)
ref = ref.closest('a');
if (typeof prop == 'string')
prop = { menu:name };
// let plugins or skins provide the menu element
if (!obj.length) {
obj = this.triggerEvent('menu-get', { name:name, props:prop, originalEvent:event });
}
if (!obj || !obj.length) {
// just delegate the action to subscribers
return this.triggerEvent(show === false ? 'menu-close' : 'menu-open', { name:name, props:prop, originalEvent:event });
}
// move element to top for proper absolute positioning
obj.appendTo(document.body);
if (typeof show == 'undefined')
show = obj.is(':visible') ? false : true;
if (show && ref.length) {
var win = $(window),
pos = ref.offset(),
above = align.indexOf('bottom') >= 0;
stack = ref.attr('role') == 'menuitem' || ref.closest('[role=menuitem]').length > 0;
ref.offsetWidth = ref.outerWidth();
ref.offsetHeight = ref.outerHeight();
if (!above && pos.top + ref.offsetHeight + obj.height() > win.height()) {
above = true;
}
if (align.indexOf('right') >= 0) {
pos.left = pos.left + ref.outerWidth() - obj.width();
}
else if (stack) {
pos.left = pos.left + ref.offsetWidth - 5;
pos.top -= ref.offsetHeight;
}
if (pos.left + obj.width() > win.width()) {
pos.left = win.width() - obj.width() - 12;
}
pos.top = Math.max(0, pos.top + (above ? -obj.height() : ref.offsetHeight));
obj.css({ left:pos.left+'px', top:pos.top+'px' });
}
// add menu to stack
if (show) {
// truncate stack down to the one containing the ref link
for (var i = this.menu_stack.length - 1; stack && i >= 0; i--) {
if (!$(ref).parents('#'+this.menu_stack[i]).length && $(event.target).parent().attr('role') != 'menuitem')
this.hide_menu(this.menu_stack[i], event);
}
if (stack && this.menu_stack.length) {
obj.data('parent', $.last(this.menu_stack));
obj.css('z-index', ($('#'+$.last(this.menu_stack)).css('z-index') || 0) + 1);
}
else if (!stack && this.menu_stack.length) {
this.hide_menu(this.menu_stack[0], event);
}
obj.show().attr('aria-hidden', 'false').data('opener', ref.attr('aria-expanded', 'true').get(0));
this.triggerEvent('menu-open', { name:name, obj:obj, props:prop, originalEvent:event });
this.menu_stack.push(name);
this.menu_keyboard_active = show && keyboard;
if (this.menu_keyboard_active) {
this.focused_menu = name;
obj.find('a,input:not(:disabled)').not('[aria-disabled=true]').first().focus();
}
}
else { // close menu
this.hide_menu(name, event);
}
return show;
};
// hide the given popup menu (and it's childs)
this.hide_menu = function(name, event)
{
if (!this.menu_stack.length) {
// delegate to subscribers
this.triggerEvent('menu-close', { name:name, props:{ menu:name }, originalEvent:event });
return;
}
var obj, keyboard = rcube_event.is_keyboard(event);
for (var j=this.menu_stack.length-1; j >= 0; j--) {
obj = $('#' + this.menu_stack[j]).hide().attr('aria-hidden', 'true').data('parent', false);
this.triggerEvent('menu-close', { name:this.menu_stack[j], obj:obj, props:{ menu:this.menu_stack[j] }, originalEvent:event });
if (this.menu_stack[j] == name) {
j = -1; // stop loop
if (obj.data('opener')) {
$(obj.data('opener')).attr('aria-expanded', 'false');
if (keyboard)
obj.data('opener').focus();
}
}
this.menu_stack.pop();
}
// focus previous menu in stack
if (this.menu_stack.length && keyboard) {
this.menu_keyboard_active = true;
this.focused_menu = $.last(this.menu_stack);
if (!obj || !obj.data('opener'))
$('#'+this.focused_menu).find('a,input:not(:disabled)').not('[aria-disabled=true]').first().focus();
}
else {
this.focused_menu = null;
this.menu_keyboard_active = false;
}
};
// position a menu element on the screen in relation to other object
this.element_position = function(element, obj)
{
var obj = $(obj), win = $(window),
width = obj.outerWidth(),
height = obj.outerHeight(),
menu_pos = obj.data('menu-pos'),
win_height = win.height(),
elem_height = $(element).height(),
elem_width = $(element).width(),
pos = obj.offset(),
top = pos.top,
left = pos.left + width;
if (menu_pos == 'bottom') {
top += height;
left -= width;
}
else
left -= 5;
if (top + elem_height > win_height) {
top -= elem_height - height;
if (top < 0)
top = Math.max(0, (win_height - elem_height) / 2);
}
if (left + elem_width > win.width())
left -= elem_width + width;
element.css({left: left + 'px', top: top + 'px'});
};
// initialize HTML editor
this.editor_init = function(config, id)
{
this.editor = new rcube_text_editor(config, id);
};
/********************************************************/
/********* html to text conversion functions *********/
/********************************************************/
this.html2plain = function(html, func)
{
return this.format_converter(html, 'html', func);
};
this.plain2html = function(plain, func)
{
return this.format_converter(plain, 'plain', func);
};
this.format_converter = function(text, format, func)
{
// warn the user (if converted content is not empty)
if (!text
|| (format == 'html' && !(text.replace(/<[^>]+>|&nbsp;|\xC2\xA0|\s/g, '')).length)
|| (format != 'html' && !(text.replace(/\xC2\xA0|\s/g, '')).length)
) {
// without setTimeout() here, textarea is filled with initial (onload) content
if (func)
setTimeout(function() { func(''); }, 50);
return true;
}
var confirmed = this.env.editor_warned || confirm(this.get_label('editorwarning'));
this.env.editor_warned = true;
if (!confirmed)
return false;
var url = '?_task=utils&_action=' + (format == 'html' ? 'html2text' : 'text2html'),
lock = this.set_busy(true, 'converting');
$.ajax({ type: 'POST', url: url, data: text, contentType: 'application/octet-stream',
error: function(o, status, err) { ref.http_error(o, status, err, lock); },
success: function(data) {
ref.set_busy(false, null, lock);
if (func) func(data);
}
});
return true;
};
/********************************************************/
/********* remote request methods *********/
/********************************************************/
// compose a valid url with the given parameters
this.url = function(action, query)
{
var querystring = typeof query === 'string' ? query : '';
if (typeof action !== 'string')
query = action;
else if (!query || typeof query !== 'object')
query = {};
if (action)
query._action = action;
else if (this.env.action)
query._action = this.env.action;
var url = this.env.comm_path, k, param = {};
// overwrite task name
if (action && action.match(/([a-z0-9_-]+)\/([a-z0-9-_.]+)/)) {
query._action = RegExp.$2;
url = url.replace(/\_task=[a-z0-9_-]+/, '_task=' + RegExp.$1);
}
// force _framed=0
if (query._framed === 0) {
url = url.replace('&_framed=1', '');
query._framed = null;
}
// remove undefined values
for (k in query) {
if (query[k] !== undefined && query[k] !== null)
param[k] = query[k];
}
if (param = $.param(param))
url += (url.indexOf('?') > -1 ? '&' : '?') + param;
if (querystring)
url += (url.indexOf('?') > -1 ? '&' : '?') + querystring;
return url;
};
this.redirect = function(url, lock)
{
if (lock !== false)
this.set_busy(true, 'loading');
if (this.is_framed()) {
url = url.replace(/&_framed=1/, '');
parent.rcmail.redirect(url, lock);
}
else {
if (this.env.extwin) {
if (typeof url == 'string')
url += (url.indexOf('?') < 0 ? '?' : '&') + '_extwin=1';
else
url._extwin = 1;
}
this.location_href(url, window);
}
};
this.goto_url = function(action, query, lock, secure)
{
var url = this.url(action, query)
if (secure) url = this.secure_url(url);
this.redirect(url, lock);
};
this.location_href = function(url, target, frame)
{
if (frame)
this.lock_frame();
if (typeof url == 'object')
url = this.env.comm_path + '&' + $.param(url);
// simulate real link click to force IE to send referer header
if (bw.ie && target == window)
$('<a>').attr('href', url).appendTo(document.body).get(0).click();
else
target.location.href = url;
// reset keep-alive interval
this.start_keepalive();
};
// update browser location to remember current view
this.update_state = function(query)
{
if (window.history.replaceState)
try {
// This may throw security exception in Firefox (#5400)
window.history.replaceState({}, document.title, rcmail.url('', query));
}
catch(e) { /* ignore */ };
};
// send a http request to the server
this.http_request = function(action, data, lock, type)
{
if (type != 'POST')
type = 'GET';
if (typeof data !== 'object')
data = rcube_parse_query(data);
data._remote = 1;
data._unlock = lock ? lock : 0;
// trigger plugin hook
var result = this.triggerEvent('request' + action, data);
// abort if one of the handlers returned false
if (result === false) {
if (data._unlock)
this.set_busy(false, null, data._unlock);
return false;
}
else if (result && result.getResponseHeader) {
return result;
}
else if (result !== undefined) {
data = result;
if (data._action) {
action = data._action;
delete data._action;
}
}
var url = this.url(action);
// reset keep-alive interval
this.start_keepalive();
// send request
return $.ajax({
type: type, url: url, data: data, dataType: 'json',
success: function(data) { ref.http_response(data); },
error: function(o, status, err) { ref.http_error(o, status, err, lock, action); }
});
};
// send a http GET request to the server
this.http_get = this.http_request;
// send a http POST request to the server
this.http_post = function(action, data, lock)
{
return this.http_request(action, data, lock, 'POST');
};
// aborts ajax request
this.abort_request = function(r)
{
if (r.request)
r.request.abort();
if (r.lock)
this.set_busy(false, null, r.lock);
};
// handle HTTP response
this.http_response = function(response)
{
if (!response)
return;
if (response.unlock)
this.set_busy(false, null, response.unlock);
this.triggerEvent('responsebefore', {response: response});
this.triggerEvent('responsebefore'+response.action, {response: response});
// set env vars
if (response.env)
this.set_env(response.env);
var i;
// we have labels to add
if (typeof response.texts === 'object') {
for (i in response.texts)
if (typeof response.texts[i] === 'string')
this.add_label(i, response.texts[i]);
}
// if we get javascript code from server -> execute it
if (response.exec) {
eval(response.exec);
}
// execute callback functions of plugins
if (response.callbacks && response.callbacks.length) {
for (i=0; i < response.callbacks.length; i++)
this.triggerEvent(response.callbacks[i][0], response.callbacks[i][1]);
}
// process the response data according to the sent action
switch (response.action) {
case 'mark':
// Mark the message as Seen also in the opener/parent
if ((this.env.action == 'show' || this.env.action == 'preview') && this.env.last_flag == 'SEEN')
this.set_unread_message(this.env.uid, this.env.mailbox);
break;
case 'delete':
if (this.task == 'addressbook') {
var sid, uid = this.contact_list.get_selection(), writable = false;
if (uid && this.contact_list.rows[uid]) {
// search results, get source ID from record ID
if (this.env.source == '') {
sid = String(uid).replace(/^[^-]+-/, '');
writable = sid && this.env.address_sources[sid] && !this.env.address_sources[sid].readonly;
}
else {
writable = !this.env.address_sources[this.env.source].readonly;
}
}
this.enable_command('delete', 'edit', writable);
this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0));
this.enable_command('export-selected', 'print', false);
}
case 'move':
if (this.env.action == 'show') {
// re-enable commands on move/delete error
this.enable_command(this.env.message_commands, true);
if (!this.env.list_post)
this.enable_command('reply-list', false);
}
else if (this.task == 'addressbook') {
this.triggerEvent('listupdate', { list:this.contact_list, folder:this.env.source, rowcount:this.contact_list.rowcount });
}
case 'purge':
case 'expunge':
if (this.task == 'mail') {
if (!this.env.exists) {
// clear preview pane content
if (this.env.contentframe)
this.show_contentframe(false);
// disable commands useless when mailbox is empty
this.enable_command(this.env.message_commands, 'purge', 'expunge',
'select-all', 'select-none', 'expand-all', 'expand-unread', 'collapse-all', false);
}
if (this.message_list)
this.triggerEvent('listupdate', { list:this.message_list, folder:this.env.mailbox, rowcount:this.message_list.rowcount });
}
break;
case 'refresh':
case 'check-recent':
// update message flags
$.each(this.env.recent_flags || {}, function(uid, flags) {
ref.set_message(uid, 'deleted', flags.deleted);
ref.set_message(uid, 'replied', flags.answered);
ref.set_message(uid, 'unread', !flags.seen);
ref.set_message(uid, 'forwarded', flags.forwarded);
ref.set_message(uid, 'flagged', flags.flagged);
});
delete this.env.recent_flags;
case 'getunread':
case 'search':
this.env.qsearch = null;
case 'list':
if (this.task == 'mail') {
var is_multifolder = this.is_multifolder_listing(),
list = this.message_list,
uid = this.env.list_uid;
this.enable_command('show', 'select-all', 'select-none', this.env.messagecount > 0);
this.enable_command('expunge', this.env.exists && !is_multifolder);
this.enable_command('purge', this.purge_mailbox_test() && !is_multifolder);
this.enable_command('import-messages', !is_multifolder);
this.enable_command('expand-all', 'expand-unread', 'collapse-all', this.env.threading && this.env.messagecount && !is_multifolder);
if (list) {
if (response.action == 'list' || response.action == 'search') {
// highlight message row when we're back from message page
if (uid) {
if (uid === 'FIRST') {
uid = list.get_first_row();
}
else if (uid === 'LAST') {
uid = list.get_last_row();
}
else if (!list.rows[uid]) {
uid += '-' + this.env.mailbox;
}
if (uid && list.rows[uid]) {
list.select(uid);
}
delete this.env.list_uid;
}
this.enable_command('set-listmode', this.env.threads && !is_multifolder);
if (list.rowcount > 0 && !$(document.activeElement).is('input,textarea'))
list.focus();
// trigger 'select' so all dependent actions update its state
// e.g. plugins use this event to activate buttons (#1490647)
list.triggerEvent('select');
}
if (response.action != 'getunread')
this.triggerEvent('listupdate', { list:list, folder:this.env.mailbox, rowcount:list.rowcount });
}
}
else if (this.task == 'addressbook') {
var list = this.contact_list,
uid = this.env.list_uid;
this.enable_command('export', 'select-all', 'select-none', (list && list.rowcount > 0));
if (response.action == 'list' || response.action == 'search') {
this.enable_command('search-create', this.env.source == '');
this.enable_command('search-delete', this.env.search_id);
this.update_group_commands();
if (list && uid) {
if (uid === 'FIRST') {
uid = list.get_first_row();
}
else if (uid === 'LAST') {
uid = list.get_last_row();
}
if (uid && list.rows[uid]) {
list.select(uid);
}
delete this.env.list_uid;
// trigger 'select' so all dependent actions update its state
list.triggerEvent('select');
}
if (list.rowcount > 0 && !$(document.activeElement).is('input,textarea'))
list.focus();
this.triggerEvent('listupdate', { list:list, folder:this.env.source, rowcount:list.rowcount });
}
}
break;
case 'list-contacts':
case 'search-contacts':
if (this.contact_list) {
if (this.contact_list.rowcount > 0)
this.contact_list.focus();
this.triggerEvent('listupdate', { list:this.contact_list, rowcount:this.contact_list.rowcount });
}
break;
}
if (response.unlock)
this.hide_message(response.unlock);
this.triggerEvent('responseafter', {response: response});
this.triggerEvent('responseafter'+response.action, {response: response});
// reset keep-alive interval
this.start_keepalive();
};
// handle HTTP request errors
this.http_error = function(request, status, err, lock, action)
{
var errmsg = request.statusText;
this.set_busy(false, null, lock);
request.abort();
// don't display error message on page unload (#1488547)
if (this.unload)
return;
if (request.status && errmsg)
this.display_message(this.get_label('servererror') + ' (' + errmsg + ')', 'error');
else if (status == 'timeout')
this.display_message('requesttimedout', 'error');
else if (request.status == 0 && status != 'abort')
this.display_message('connerror', 'error');
// redirect to url specified in location header if not empty
var location_url = request.getResponseHeader("Location");
if (location_url && this.env.action != 'compose') // don't redirect on compose screen, contents might get lost (#1488926)
this.redirect(location_url);
// 403 Forbidden response (CSRF prevention) - reload the page.
// In case there's a new valid session it will be used, otherwise
// login form will be presented (#1488960).
if (request.status == 403) {
(this.is_framed() ? parent : window).location.reload();
return;
}
// re-send keep-alive requests after 30 seconds
if (action == 'keep-alive')
setTimeout(function(){ ref.keep_alive(); ref.start_keepalive(); }, 30000);
};
// handler for session errors detected on the server
this.session_error = function(redirect_url)
{
this.env.server_error = 401;
// save message in local storage and do not redirect
if (this.env.action == 'compose') {
this.save_compose_form_local();
this.compose_skip_unsavedcheck = true;
// stop keep-alive and refresh processes
this.env.session_lifetime = 0;
if (this._keepalive)
clearInterval(this._keepalive);
if (this._refresh)
clearInterval(this._refresh);
}
else if (redirect_url) {
setTimeout(function(){ ref.redirect(redirect_url, true); }, 2000);
}
};
// callback when an iframe finished loading
this.iframe_loaded = function(unlock)
{
if (!unlock)
unlock = this.env.frame_lock;
this.set_busy(false, null, unlock);
if (this.submit_timer)
clearTimeout(this.submit_timer);
};
/**
Send multi-threaded parallel HTTP requests to the server for a list if items.
The string '%' in either a GET query or POST parameters will be replaced with the respective item value.
This is the argument object expected: {
items: ['foo','bar','gna'], // list of items to send requests for
action: 'task/some-action', // Roudncube action to call
query: { q:'%s' }, // GET query parameters
postdata: { source:'%s' }, // POST data (sends a POST request if present)
threads: 3, // max. number of concurrent requests
onresponse: function(data){ }, // Callback function called for every response received from server
whendone: function(alldata){ } // Callback function called when all requests have been sent
}
*/
this.multi_thread_http_request = function(prop)
{
var i, item, reqid = new Date().getTime(),
threads = prop.threads || 1;
prop.reqid = reqid;
prop.running = 0;
prop.requests = [];
prop.result = [];
prop._items = $.extend([], prop.items); // copy items
if (!prop.lock)
prop.lock = this.display_message('', 'loading');
// add the request arguments to the jobs pool
this.http_request_jobs[reqid] = prop;
// start n threads
for (i=0; i < threads; i++) {
item = prop._items.shift();
if (item === undefined)
break;
prop.running++;
prop.requests.push(this.multi_thread_send_request(prop, item));
}
return reqid;
};
// helper method to send an HTTP request with the given iterator value
this.multi_thread_send_request = function(prop, item)
{
var k, postdata, query;
// replace %s in post data
if (prop.postdata) {
postdata = {};
for (k in prop.postdata) {
postdata[k] = String(prop.postdata[k]).replace('%s', item);
}
postdata._reqid = prop.reqid;
}
// replace %s in query
else if (typeof prop.query == 'string') {
query = prop.query.replace('%s', item);
query += '&_reqid=' + prop.reqid;
}
else if (typeof prop.query == 'object' && prop.query) {
query = {};
for (k in prop.query) {
query[k] = String(prop.query[k]).replace('%s', item);
}
query._reqid = prop.reqid;
}
// send HTTP GET or POST request
return postdata ? this.http_post(prop.action, postdata) : this.http_request(prop.action, query);
};
// callback function for multi-threaded http responses
this.multi_thread_http_response = function(data, reqid)
{
var prop = this.http_request_jobs[reqid];
if (!prop || prop.running <= 0 || prop.cancelled)
return;
prop.running--;
// trigger response callback
if (prop.onresponse && typeof prop.onresponse == 'function') {
prop.onresponse(data);
}
prop.result = $.extend(prop.result, data);
// send next request if prop.items is not yet empty
var item = prop._items.shift();
if (item !== undefined) {
prop.running++;
prop.requests.push(this.multi_thread_send_request(prop, item));
}
// trigger whendone callback and mark this request as done
else if (prop.running == 0) {
if (prop.whendone && typeof prop.whendone == 'function') {
prop.whendone(prop.result);
}
this.set_busy(false, '', prop.lock);
// remove from this.http_request_jobs pool
delete this.http_request_jobs[reqid];
}
};
// abort a running multi-thread request with the given identifier
this.multi_thread_request_abort = function(reqid)
{
var prop = this.http_request_jobs[reqid];
if (prop) {
for (var i=0; prop.running > 0 && i < prop.requests.length; i++) {
if (prop.requests[i].abort)
prop.requests[i].abort();
}
prop.running = 0;
prop.cancelled = true;
this.set_busy(false, '', prop.lock);
}
};
// post the given form to a hidden iframe
this.async_upload_form = function(form, action, onload)
{
// create hidden iframe
var ts = new Date().getTime(),
frame_name = 'rcmupload' + ts,
frame = this.dummy_iframe(frame_name);
// handle upload errors by parsing iframe content in onload
frame.on('load', {ts:ts}, onload);
$(form).attr({
target: frame_name,
action: this.url(action, {_id: this.env.compose_id || '', _uploadid: ts, _from: this.env.action}),
method: 'POST'})
.attr(form.encoding ? 'encoding' : 'enctype', 'multipart/form-data')
.submit();
return frame_name;
};
// create hidden iframe element
this.dummy_iframe = function(name, src)
{
return $('<iframe>').attr({
name: name,
src: src,
style: 'width:0;height:0;visibility:hidden',
'aria-hidden': 'true'
})
.appendTo(document.body);
};
// html5 file-drop API
this.document_drag_hover = function(e, over)
{
// don't e.preventDefault() here to not block text dragging on the page (#1490619)
$(this.gui_objects.filedrop)[(over?'addClass':'removeClass')]('active');
};
this.file_drag_hover = function(e, over)
{
e.preventDefault();
e.stopPropagation();
$(this.gui_objects.filedrop)[(over?'addClass':'removeClass')]('hover');
};
// handler when files are dropped to a designated area.
// compose a multipart form data and submit it to the server
this.file_dropped = function(e)
{
// abort event and reset UI
this.file_drag_hover(e, false);
// prepare multipart form data composition
var uri,
files = e.target.files || e.dataTransfer.files,
args = {_id: this.env.compose_id || this.env.cid || '', _remote: 1, _from: this.env.action};
if (!files || !files.length) {
// Roundcube attachment, pass its uri to the backend and attach
if (uri = e.dataTransfer.getData('roundcube-uri')) {
var ts = 'upload' + new Date().getTime(),
// jQuery way to escape filename (#1490530)
content = $('<span>').text(e.dataTransfer.getData('roundcube-name') || this.get_label('attaching')).html();
args._uri = uri;
args._uploadid = ts;
// add to attachments list
if (!this.add2attachment_list(ts, {name: '', html: content, classname: 'uploading', complete: false}))
this.file_upload_id = this.set_busy(true, 'attaching');
this.http_post(this.env.filedrop.action || 'upload', args);
}
return;
}
this.file_upload(files, args, {
name: (this.env.filedrop.fieldname || '_file') + (this.env.filedrop.single ? '' : '[]'),
single: this.env.filedrop.single,
filter: this.env.filedrop.filter,
action: ref.env.filedrop.action
});
};
// Files upload using ajax
this.file_upload = function(files, post_args, props)
{
if (!window.FormData || !files || !files.length)
return false;
var f, i, fname, size = 0, numfiles = 0,
formdata = new FormData(),
fieldname = props.name || '_file[]',
limit = props.single ? 1 : files.length;
args = $.extend({_remote: 1, _from: this.env.action}, post_args || {});
// add files to form data
for (i=0; numfiles < limit && (f = files[i]); i++) {
// filter by file type if requested
if (props.filter && !f.type.match(new RegExp(props.filter))) {
// TODO: show message to user
continue;
}
formdata.append(fieldname, f);
size += f.size;
fname = f.name;
numfiles++;
}
if (numfiles) {
if (this.env.max_filesize && this.env.filesizeerror && size > this.env.max_filesize) {
this.display_message(this.env.filesizeerror, 'error');
return false;
}
if (this.env.max_filecount && this.env.filecounterror && numfiles > this.env.max_filecount) {
this.display_message(this.env.filecounterror, 'error');
return false;
}
var ts = 'upload' + new Date().getTime(),
label = numfiles > 1 ? this.get_label('uploadingmany') : fname,
// jQuery way to escape filename (#1490530)
content = $('<span>').text(label).html();
// add to attachments list
if (!this.add2attachment_list(ts, {name: '', html: content, classname: 'uploading', complete: false}) && !props.lock)
props.lock = this.file_upload_id = this.set_busy(true, 'uploading');
args._uploadid = ts;
args._unlock = props.lock;
this.uploads[ts] = $.ajax({
type: 'POST',
dataType: 'json',
url: this.url(props.action || 'upload', args),
contentType: false,
processData: false,
timeout: 0, // disable default timeout set in ajaxSetup()
data: formdata,
headers: {'X-Roundcube-Request': this.env.request_token},
xhr: function() {
var xhr = $.ajaxSettings.xhr();
if (xhr.upload && ref.labels.uploadprogress) {
xhr.upload.onprogress = function(e) {
var msg = ref.file_upload_msg(e.loaded, e.total);
if (msg) {
$('#' + ts).find('.uploading').text(msg);
}
};
}
return xhr;
},
success: function(data) {
delete ref.uploads[ts];
ref.http_response(data);
},
error: function(o, status, err) {
delete ref.uploads[ts];
ref.remove_from_attachment_list(ts);
ref.http_error(o, status, err, props.lock, 'attachment');
}
});
}
return true;
};
this.file_upload_msg = function(current, total)
{
if (total && current < total) {
var percent = Math.round(current/total * 100),
label = ref.get_label('uploadprogress');
if (total >= 1073741824) {
total = parseFloat(total/1073741824).toFixed(1) + ' ' . this.get_label('GB');
current = parseFloat(current/1073741824).toFixed(1);
}
else if (total >= 1048576) {
total = parseFloat(total/1048576).toFixed(1) + ' ' + this.get_label('MB');
current = parseFloat(current/1048576).toFixed(1);
}
else if (total >= 1024) {
total = parseInt(total/1024) + ' ' + this.get_label('KB');
current = parseInt(current/1024);
}
else {
total = total + ' ' + this.get_label('B');
}
return label.replace('$percent', percent + '%').replace('$current', current).replace('$total', total);
}
};
// starts interval for keep-alive signal
this.start_keepalive = function()
{
if (!this.env.session_lifetime || this.env.framed || this.env.extwin || this.task == 'login' || this.env.action == 'print')
return;
if (this._keepalive)
clearInterval(this._keepalive);
// use Math to prevent from an integer overflow (#5273)
// maximum interval is 15 minutes, minimum is 30 seconds
var interval = Math.min(1800, this.env.session_lifetime) * 0.5 * 1000;
this._keepalive = setInterval(function() { ref.keep_alive(); }, interval < 30000 ? 30000 : interval);
};
// starts interval for refresh signal
this.start_refresh = function()
{
if (!this.env.refresh_interval || this.env.framed || this.env.extwin || this.task == 'login' || this.env.action == 'print')
return;
if (this._refresh)
clearInterval(this._refresh);
this._refresh = setInterval(function(){ ref.refresh(); }, this.env.refresh_interval * 1000);
};
// sends keep-alive signal
this.keep_alive = function()
{
if (!this.busy)
this.http_request('keep-alive');
};
// sends refresh signal
this.refresh = function()
{
if (this.busy) {
// try again after 10 seconds
setTimeout(function(){ ref.refresh(); ref.start_refresh(); }, 10000);
return;
}
var params = {}, lock = this.set_busy(true, 'refreshing');
if (this.task == 'mail' && this.gui_objects.mailboxlist)
params = this.check_recent_params();
params._last = Math.floor(this.env.lastrefresh.getTime() / 1000);
this.env.lastrefresh = new Date();
// plugins should bind to 'requestrefresh' event to add own params
this.http_post('refresh', params, lock);
};
// returns check-recent request parameters
this.check_recent_params = function()
{
var params = {_mbox: this.env.mailbox};
if (this.gui_objects.mailboxlist)
params._folderlist = 1;
if (this.gui_objects.quotadisplay)
params._quota = 1;
if (this.env.search_request)
params._search = this.env.search_request;
if (this.gui_objects.messagelist) {
params._list = 1;
// message uids for flag updates check
params._uids = $.map(this.message_list.rows, function(row, uid) { return uid; }).join(',');
}
return params;
};
/********************************************************/
/********* helper methods *********/
/********************************************************/
/**
* Quote html entities
*/
this.quote_html = function(str)
{
return String(str).replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
};
// get window.opener.rcmail if available
this.opener = function(deep, filter)
{
var i, win = window.opener;
// catch Error: Permission denied to access property rcmail
try {
if (win && !win.closed && win !== window) {
// try parent of the opener window, e.g. preview frame
if (deep && (!win.rcmail || win.rcmail.env.framed) && win.parent && win.parent.rcmail)
win = win.parent;
if (win.rcmail && filter)
for (i in filter)
if (win.rcmail.env[i] != filter[i])
return;
return win.rcmail;
}
}
catch (e) {}
};
// check if we're in show mode or if we have a unique selection
// and return the message uid
this.get_single_uid = function()
{
var uid = this.env.uid || (this.message_list ? this.message_list.get_single_selection() : null);
var result = ref.triggerEvent('get_single_uid', { uid: uid });
return result || uid;
};
// same as above but for contacts
this.get_single_cid = function()
{
var cid = this.env.cid || (this.contact_list ? this.contact_list.get_single_selection() : null);
var result = ref.triggerEvent('get_single_cid', { cid: cid });
return result || cid;
};
// get the IMP mailbox of the message with the given UID
this.get_message_mailbox = function(uid)
{
var msg = (this.env.messages && uid ? this.env.messages[uid] : null) || {};
return msg.mbox || this.env.mailbox;
};
// build request parameters from single message id (maybe with mailbox name)
this.params_from_uid = function(uid, params)
{
if (!params)
params = {};
params._uid = String(uid).split('-')[0];
params._mbox = this.get_message_mailbox(uid);
return params;
};
// gets cursor position
this.get_caret_pos = function(obj)
{
if (obj.selectionEnd !== undefined)
return obj.selectionEnd;
return obj.value.length;
};
// moves cursor to specified position
this.set_caret_pos = function(obj, pos)
{
try {
if (obj.setSelectionRange)
obj.setSelectionRange(pos, pos);
}
catch(e) {} // catch Firefox exception if obj is hidden
};
// get selected text from an input field
this.get_input_selection = function(obj)
{
var start = 0, end = 0, normalizedValue = '';
if (typeof obj.selectionStart == "number" && typeof obj.selectionEnd == "number") {
normalizedValue = obj.value;
start = obj.selectionStart;
end = obj.selectionEnd;
}
return {start: start, end: end, text: normalizedValue.substr(start, end-start)};
};
// disable/enable all fields of a form
this.lock_form = function(form, lock)
{
if (!form || !form.elements)
return;
if (lock)
this.disabled_form_elements = [];
$.each(form.elements, function() {
if (this.type == 'hidden')
return;
// remember which elem was disabled before lock
if (lock && this.disabled)
ref.disabled_form_elements.push(this);
else if (lock || $.inArray(this, ref.disabled_form_elements) < 0)
this.disabled = lock;
});
};
this.mailto_handler_uri = function()
{
return location.href.split('?')[0] + '?_task=mail&_action=compose&_to=%s';
};
this.register_protocol_handler = function(name)
{
try {
window.navigator.registerProtocolHandler('mailto', this.mailto_handler_uri(), name);
}
catch(e) {
this.display_message(String(e), 'error');
}
};
this.check_protocol_handler = function(name, elem)
{
var nav = window.navigator;
if (!nav || (typeof nav.registerProtocolHandler != 'function')) {
$(elem).addClass('disabled').click(function() {
ref.display_message('nosupporterror', 'error');
return false;
});
}
else if (typeof nav.isProtocolHandlerRegistered == 'function') {
var status = nav.isProtocolHandlerRegistered('mailto', this.mailto_handler_uri());
if (status)
$(elem).parent().find('.mailtoprotohandler-status').html(status);
}
else {
$(elem).click(function() { ref.register_protocol_handler(name); return false; });
}
};
// Checks browser capabilities eg. PDF support, TIF support
this.browser_capabilities_check = function()
{
if (!this.env.browser_capabilities)
this.env.browser_capabilities = {};
$.each(['pdf', 'flash', 'tiff', 'webp'], function() {
if (ref.env.browser_capabilities[this] === undefined)
ref.env.browser_capabilities[this] = ref[this + '_support_check']();
});
};
// Returns browser capabilities string
this.browser_capabilities = function()
{
if (!this.env.browser_capabilities)
return '';
var n, ret = [];
for (n in this.env.browser_capabilities)
ret.push(n + '=' + this.env.browser_capabilities[n]);
return ret.join();
};
this.tiff_support_check = function()
{
this.image_support_check('tiff');
return 0;
};
this.webp_support_check = function()
{
this.image_support_check('webp');
return 0;
};
this.image_support_check = function(type)
{
window.setTimeout(function() {
var img = new Image();
img.onload = function() { ref.env.browser_capabilities[type] = 1; };
img.onerror = function() { ref.env.browser_capabilities[type] = 0; };
img.src = ref.assets_path('program/resources/blank.' + type);
}, 10);
};
this.pdf_support_check = function()
{
var i, plugin = navigator.mimeTypes ? navigator.mimeTypes["application/pdf"] : {},
plugins = navigator.plugins,
len = plugins.length,
regex = /Adobe Reader|PDF|Acrobat/i;
if (plugin && plugin.enabledPlugin)
return 1;
if ('ActiveXObject' in window) {
try {
if (plugin = new ActiveXObject("AcroPDF.PDF"))
return 1;
}
catch (e) {}
try {
if (plugin = new ActiveXObject("PDF.PdfCtrl"))
return 1;
}
catch (e) {}
}
for (i=0; i<len; i++) {
plugin = plugins[i];
if (typeof plugin === 'String') {
if (regex.test(plugin))
return 1;
}
else if (plugin.name && regex.test(plugin.name))
return 1;
}
window.setTimeout(function() {
$('<object>').attr({
data: ref.assets_path('program/resources/dummy.pdf'),
type: 'application/pdf',
style: 'position: "absolute"; top: -1000px; height: 1px; width: 1px'
})
.on('load error', function(e) {
ref.env.browser_capabilities.pdf = e.type == 'load' ? 1 : 0;
$(this).remove();
})
.appendTo(document.body);
}, 10);
return 0;
};
this.flash_support_check = function()
{
var plugin = navigator.mimeTypes ? navigator.mimeTypes["application/x-shockwave-flash"] : {};
if (plugin && plugin.enabledPlugin)
return 1;
if ('ActiveXObject' in window) {
try {
if (plugin = new ActiveXObject("ShockwaveFlash.ShockwaveFlash"))
return 1;
}
catch (e) {}
}
return 0;
};
this.assets_path = function(path)
{
if (this.env.assets_path && !path.startsWith(this.env.assets_path)) {
path = this.env.assets_path + path;
}
return path;
};
// Cookie setter
this.set_cookie = function(name, value, expires)
{
setCookie(name, value, expires, this.env.cookie_path, this.env.cookie_domain, this.env.cookie_secure);
};
this.get_local_storage_prefix = function()
{
if (!this.local_storage_prefix)
this.local_storage_prefix = 'roundcube.' + (this.env.user_id || 'anonymous') + '.';
return this.local_storage_prefix;
};
// wrapper for localStorage.getItem(key)
this.local_storage_get_item = function(key, deflt, encrypted)
{
var item, result;
// TODO: add encryption
try {
item = localStorage.getItem(this.get_local_storage_prefix() + key);
result = JSON.parse(item);
}
catch (e) { }
return result || deflt || null;
};
// wrapper for localStorage.setItem(key, data)
this.local_storage_set_item = function(key, data, encrypted)
{
// try/catch to handle no localStorage support, but also error
// in Safari-in-private-browsing-mode where localStorage exists
// but can't be used (#1489996)
try {
// TODO: add encryption
localStorage.setItem(this.get_local_storage_prefix() + key, JSON.stringify(data));
return true;
}
catch (e) {
return false;
}
};
// wrapper for localStorage.removeItem(key)
this.local_storage_remove_item = function(key)
{
try {
localStorage.removeItem(this.get_local_storage_prefix() + key);
return true;
}
catch (e) {
return false;
}
};
this.print_dialog = function()
{
if (bw.safari)
setTimeout('window.print()', 10);
else
window.print();
};
} // end object rcube_webmail
// some static methods
rcube_webmail.long_subject_title = function(elem, indent, text_elem)
{
if (!elem.title) {
var $elem = $(text_elem || elem);
if ($elem.width() + (indent || 0) * 15 > $elem.parent().width())
elem.title = rcube_webmail.subject_text($elem[0]);
}
};
rcube_webmail.long_subject_title_ex = function(elem)
{
if (!elem.title) {
var $elem = $(elem),
txt = $.trim($elem.text()),
indent = $('span.branch', $elem).width() || 0,
tmp = $('<span>').text(txt)
.css({position: 'absolute', 'float': 'left', visibility: 'hidden',
'font-size': $elem.css('font-size'), 'font-weight': $elem.css('font-weight')})
.appendTo(document.body),
w = tmp.width();
tmp.remove();
if (w + indent * 15 > $elem.width())
elem.title = rcube_webmail.subject_text(elem);
}
};
rcube_webmail.subject_text = function(elem)
{
var t = $(elem).clone();
t.find('.skip-on-drag,.skip-content,.voice').remove();
return $.trim(t.text());
};
// set event handlers on all iframe elements (and their contents)
rcube_webmail.set_iframe_events = function(events)
{
$('iframe').each(function() {
var frame = $(this);
$.each(events, function(event_name, event_handler) {
frame.on('load', function(e) {
try { $(this).contents().on(event_name, event_handler); }
catch (e) {/* catch possible permission error in IE */ }
});
try { frame.contents().on(event_name, event_handler); }
catch (e) {/* catch possible permission error in IE */ }
});
});
};
rcube_webmail.prototype.get_cookie = getCookie;
// copy event engine prototype
rcube_webmail.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;
rcube_webmail.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener;
rcube_webmail.prototype.triggerEvent = rcube_event_engine.prototype.triggerEvent;
diff --git a/program/js/common.js b/program/js/common.js
index 1422042ad..9eb10a8b1 100644
--- a/program/js/common.js
+++ b/program/js/common.js
@@ -1,798 +1,798 @@
/**
* Roundcube common js library
*
* This file is part of the Roundcube Webmail client
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
- * Copyright (c) 2005-2014, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The JavaScript code in this page is free software: you can
* redistribute it and/or modify it under the terms of the GNU
* General Public License (GNU GPL) as published by the Free Software
* Foundation, either version 3 of the License, or (at your option)
* any later version. The code is distributed WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
*
* As additional permission under GNU GPL version 3 section 7, you
* may distribute non-source (e.g., minimized or compacted) forms of
* that code without the copy of the GNU GPL normally required by
* section 4, provided you include this license notice and a URL
* through which recipients can access the Corresponding Source.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*/
// Constants
var CONTROL_KEY = 1;
var SHIFT_KEY = 2;
var CONTROL_SHIFT_KEY = 3;
/**
* Default browser check class
* @constructor
*/
function roundcube_browser()
{
var n = navigator;
this.agent = n.userAgent;
this.agent_lc = n.userAgent.toLowerCase();
this.name = n.appName;
this.vendor = n.vendor ? n.vendor : '';
this.vendver = n.vendorSub ? parseFloat(n.vendorSub) : 0;
this.product = n.product ? n.product : '';
this.platform = String(n.platform).toLowerCase();
this.lang = n.language ? n.language.substring(0,2) :
n.browserLanguage ? n.browserLanguage.substring(0,2) :
n.systemLanguage ? n.systemLanguage.substring(0,2) : 'en';
this.win = this.platform.indexOf('win') >= 0;
this.mac = this.platform.indexOf('mac') >= 0;
this.linux = this.platform.indexOf('linux') >= 0;
this.unix = this.platform.indexOf('unix') >= 0;
this.dom = document.getElementById ? true : false;
this.dom2 = document.addEventListener && document.removeEventListener;
this.edge = this.agent_lc.indexOf(' edge/') > 0;
this.webkit = !this.edge && this.agent_lc.indexOf('applewebkit') > 0;
this.ie = (document.all && !window.opera) || (this.win && this.agent_lc.indexOf('trident/') > 0);
if (window.opera) {
this.opera = true; // Opera < 15
this.vendver = opera.version();
}
else if (!this.ie && !this.edge) {
this.chrome = this.agent_lc.indexOf('chrome') > 0;
this.opera = this.webkit && this.agent.indexOf(' OPR/') > 0; // Opera >= 15
this.safari = !this.chrome && !this.opera && (this.webkit || this.agent_lc.indexOf('safari') > 0);
this.konq = this.agent_lc.indexOf('konqueror') > 0;
this.mz = this.dom && !this.chrome && !this.safari && !this.konq && !this.opera && this.agent.indexOf('Mozilla') >= 0;
this.iphone = this.safari && (this.agent_lc.indexOf('iphone') > 0 || this.agent_lc.indexOf('ipod') > 0 || this.platform == 'ipod' || this.platform == 'iphone');
this.ipad = this.safari && (this.agent_lc.indexOf('ipad') > 0 || this.platform == 'ipad');
}
if (!this.vendver) {
// common version strings
this.vendver = /(opera|opr|khtml|chrome|safari|applewebkit|msie)(\s|\/)([0-9\.]+)/.test(this.agent_lc) ? parseFloat(RegExp.$3) : 0;
// any other (Mozilla, Camino, IE>=11)
if (!this.vendver)
this.vendver = /rv:([0-9\.]+)/.test(this.agent) ? parseFloat(RegExp.$1) : 0;
}
// get real language out of safari's user agent
if (this.safari && (/;\s+([a-z]{2})-[a-z]{2}\)/.test(this.agent_lc)))
this.lang = RegExp.$1;
this.mobile = /iphone|ipod|blackberry|iemobile|opera mini|opera mobi|mobile/i.test(this.agent_lc);
this.tablet = !this.mobile && /ipad|android|xoom|sch-i800|playbook|tablet|kindle/i.test(this.agent_lc);
this.touch = this.mobile || this.tablet;
this.pointer = typeof window.PointerEvent == "function";
this.cookies = n.cookieEnabled;
// test for XMLHTTP support
this.xmlhttp_test = function()
{
var activeX_test = new Function("try{var o=new ActiveXObject('Microsoft.XMLHTTP');return true;}catch(err){return false;}");
this.xmlhttp = window.XMLHttpRequest || (('ActiveXObject' in window) && activeX_test());
return this.xmlhttp;
};
// set class names to html tag according to the current user agent detection
// this allows browser-specific css selectors like "html.chrome .someclass"
this.set_html_class = function()
{
var classname = ' js';
if (this.ie)
classname += ' ms ie ie'+parseInt(this.vendver);
else if (this.edge)
classname += ' ms edge';
else if (this.opera)
classname += ' opera';
else if (this.konq)
classname += ' konqueror';
else if (this.safari)
classname += ' chrome';
else if (this.chrome)
classname += ' chrome';
else if (this.mz)
classname += ' mozilla';
if (this.iphone)
classname += ' iphone';
else if (this.ipad)
classname += ' ipad';
else if (this.webkit)
classname += ' webkit';
if (this.mobile)
classname += ' mobile';
if (this.tablet)
classname += ' tablet';
if (document.documentElement)
document.documentElement.className += classname;
};
};
// static functions for DOM event handling
var rcube_event = {
/**
* returns the event target element
*/
get_target: function(e)
{
e = e || window.event;
return e && e.target ? e.target : e.srcElement || document;
},
/**
* returns the event key code
*/
get_keycode: function(e)
{
e = e || window.event;
return e && e.keyCode ? e.keyCode : (e && e.which ? e.which : 0);
},
/**
* returns the event key code
*/
get_button: function(e)
{
e = e || window.event;
return e && e.button !== undefined ? e.button : (e && e.which ? e.which : 0);
},
/**
* returns modifier key (constants defined at top of file)
*/
get_modifier: function(e)
{
var opcode = 0;
e = e || window.event;
if (bw.mac && e)
opcode += (e.metaKey && CONTROL_KEY) + (e.shiftKey && SHIFT_KEY);
else if (e)
opcode += (e.ctrlKey && CONTROL_KEY) + (e.shiftKey && SHIFT_KEY);
return opcode;
},
/**
* Return absolute mouse position of an event
*/
get_mouse_pos: function(e)
{
if (!e) e = window.event;
var mX = (e.pageX) ? e.pageX : e.clientX,
mY = (e.pageY) ? e.pageY : e.clientY;
if (document.body && document.all) {
mX += document.body.scrollLeft;
mY += document.body.scrollTop;
}
if (e._offset) {
mX += e._offset.left;
mY += e._offset.top;
}
return { x:mX, y:mY };
},
/**
* Add an object method as event listener to a certain element
*/
add_listener: function(p)
{
if (!p.object || !p.method) // not enough arguments
return;
if (!p.element)
p.element = document;
if (!p.object._rc_events)
p.object._rc_events = {};
var key = p.event + '*' + p.method;
if (!p.object._rc_events[key])
p.object._rc_events[key] = function(e){ return p.object[p.method](e); };
if (p.element.addEventListener)
p.element.addEventListener(p.event, p.object._rc_events[key], false);
else if (p.element.attachEvent) {
// IE allows multiple events with the same function to be applied to the same object
// forcibly detach the event, then attach
p.element.detachEvent('on'+p.event, p.object._rc_events[key]);
p.element.attachEvent('on'+p.event, p.object._rc_events[key]);
}
else
p.element['on'+p.event] = p.object._rc_events[key];
},
/**
* Remove event listener
*/
remove_listener: function(p)
{
if (!p.element)
p.element = document;
var key = p.event + '*' + p.method;
if (p.object && p.object._rc_events && p.object._rc_events[key]) {
if (p.element.removeEventListener)
p.element.removeEventListener(p.event, p.object._rc_events[key], false);
else if (p.element.detachEvent)
p.element.detachEvent('on'+p.event, p.object._rc_events[key]);
else
p.element['on'+p.event] = null;
}
},
/**
* Prevent event propagation and bubbling
*/
cancel: function(evt)
{
var e = evt ? evt : window.event;
if (e.preventDefault)
e.preventDefault();
else
e.returnValue = false;
if (e.stopPropagation)
e.stopPropagation();
e.cancelBubble = true;
return false;
},
/**
* Determine whether the given event was trigered from keyboard
*/
is_keyboard: function(e)
{
if (!e)
return false;
if (e.type)
return !!e.type.match(/^key/); // DOM3-compatible
return !e.pageX && (e.pageY || 0) <= 0 && !e.clientX && (e.clientY || 0) <= 0;
},
/**
* Accept event if triggered from keyboard action (e.g. <Enter>)
*/
keyboard_only: function(e)
{
return rcube_event.is_keyboard(e) ? true : rcube_event.cancel(e);
},
touchevent: function(e)
{
return { pageX:e.pageX, pageY:e.pageY, offsetX:e.pageX - e.target.offsetLeft, offsetY:e.pageY - e.target.offsetTop, target:e.target, istouch:true };
}
};
/**
* rcmail objects event interface
*/
function rcube_event_engine()
{
this._events = {};
};
rcube_event_engine.prototype = {
/**
* Setter for object event handlers
*
* @param {String} Event name
* @param {Function} Handler function
*/
addEventListener: function(evt, func, obj)
{
if (!this._events)
this._events = {};
if (!this._events[evt])
this._events[evt] = [];
this._events[evt].push({func:func, obj:obj ? obj : window});
return this; // chainable
},
/**
* Removes a specific event listener
*
* @param {String} Event name
* @param {Int} Listener ID to remove
*/
removeEventListener: function(evt, func, obj)
{
if (obj === undefined)
obj = window;
for (var h,i=0; this._events && this._events[evt] && i < this._events[evt].length; i++)
if ((h = this._events[evt][i]) && h.func == func && h.obj == obj)
this._events[evt][i] = null;
},
/**
* This will execute all registered event handlers
*
* @param {String} Event to trigger
* @param {Object} Event object/arguments
*/
triggerEvent: function(evt, e)
{
var ret, h,
reset_fn = function(o) {
try { if (o && o.event) delete o.event; } catch(err) { };
};
if (e === undefined)
e = this;
else if (typeof e === 'object')
e.event = evt;
if (!this._event_exec)
this._event_exec = {};
if (this._events && this._events[evt] && !this._event_exec[evt]) {
this._event_exec[evt] = true;
for (var i=0; i < this._events[evt].length; i++) {
if ((h = this._events[evt][i])) {
if (typeof h.func === 'function')
ret = h.func.call ? h.func.call(h.obj, e) : h.func(e);
else if (typeof h.obj[h.func] === 'function')
ret = h.obj[h.func](e);
// cancel event execution
if (ret !== undefined && !ret)
break;
}
}
reset_fn(ret);
}
delete this._event_exec[evt];
reset_fn(e);
return ret;
}
}; // end rcube_event_engine.prototype
// check if input is a valid email address
// By Cal Henderson <cal@iamcal.com>
// http://code.iamcal.com/php/rfc822/
function rcube_check_email(input, inline, count, strict)
{
if (!input)
return count ? 0 : false;
if (count) inline = true;
var qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]',
dtext = '[^\\x0d\\x5b-\\x5d\\x80-\\xff]',
atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+',
quoted_pair = '\\x5c[\\x00-\\x7f]',
quoted_string = '\\x22('+qtext+'|'+quoted_pair+')*\\x22',
ipv4 = '\\[(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}\\]',
ipv6 = '\\[IPv6:[0-9a-f:.]+\\]',
ip_addr = '(' + ipv4 + ')|(' + ipv6 + ')',
// Use simplified domain matching, because we need to allow Unicode characters here
// So, e-mail address should be validated also on server side after idn_to_ascii() use
//domain_literal = '\\x5b('+dtext+'|'+quoted_pair+')*\\x5d',
//sub_domain = '('+atom+'|'+domain_literal+')',
// allow punycode/unicode top-level domain
domain = '(('+ip_addr+')|(([^@\\x2e]+\\x2e)+([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,})))',
// ICANN e-mail test (http://idn.icann.org/E-mail_test)
icann_domains = [
'\\u0645\\u062b\\u0627\\u0644\\x2e\\u0625\\u062e\\u062a\\u0628\\u0627\\u0631',
'\\u4f8b\\u5b50\\x2e\\u6d4b\\u8bd5',
'\\u4f8b\\u5b50\\x2e\\u6e2c\\u8a66',
'\\u03c0\\u03b1\\u03c1\\u03ac\\u03b4\\u03b5\\u03b9\\u03b3\\u03bc\\u03b1\\x2e\\u03b4\\u03bf\\u03ba\\u03b9\\u03bc\\u03ae',
'\\u0909\\u0926\\u093e\\u0939\\u0930\\u0923\\x2e\\u092a\\u0930\\u0940\\u0915\\u094d\\u0937\\u093e',
'\\u4f8b\\u3048\\x2e\\u30c6\\u30b9\\u30c8',
'\\uc2e4\\ub840\\x2e\\ud14c\\uc2a4\\ud2b8',
'\\u0645\\u062b\\u0627\\u0644\\x2e\\u0622\\u0632\\u0645\\u0627\\u06cc\\u0634\u06cc',
'\\u043f\\u0440\\u0438\\u043c\\u0435\\u0440\\x2e\\u0438\\u0441\\u043f\\u044b\\u0442\\u0430\\u043d\\u0438\\u0435',
'\\u0b89\\u0ba4\\u0bbe\\u0bb0\\u0ba3\\u0bae\\u0bcd\\x2e\\u0baa\\u0bb0\\u0bbf\\u0b9f\\u0bcd\\u0b9a\\u0bc8',
'\\u05d1\\u05f2\\u05b7\\u05e9\\u05e4\\u05bc\\u05d9\\u05dc\\x2e\\u05d8\\u05e2\\u05e1\\u05d8'
],
icann_addr = 'mailtest\\x40('+icann_domains.join('|')+')',
word = strict ? '('+atom+'|'+quoted_string+')' : '[^\\u0000-\\u0020\\u002e\\u00a0\\u0040\\u007f\\u2028\\u2029]+',
delim = '[,;\\s\\n]',
local_part = word+'(\\x2e'+word+')*',
addr_spec = '(('+local_part+'\\x40'+domain+')|('+icann_addr+'))',
rx_flag = count ? 'ig' : 'i',
rx = inline ? new RegExp('(^|<|'+delim+')'+addr_spec+'($|>|'+delim+')', rx_flag) : new RegExp('^'+addr_spec+'$', 'i');
if (count)
return input.match(rx).length;
return rx.test(input);
};
// recursively copy an object
function rcube_clone_object(obj)
{
var out = {};
for (var key in obj) {
if (obj[key] && typeof obj[key] === 'object')
out[key] = rcube_clone_object(obj[key]);
else
out[key] = obj[key];
}
return out;
};
// make a string URL safe (and compatible with PHP's rawurlencode())
function urlencode(str)
{
if (window.encodeURIComponent)
return encodeURIComponent(str).replace('*', '%2A');
return escape(str)
.replace('+', '%2B')
.replace('*', '%2A')
.replace('/', '%2F')
.replace('@', '%40');
};
// get any type of html objects by id/name
function rcube_find_object(id, d)
{
var n, f, obj, e;
if (!d) d = document;
if (d.getElementById)
if (obj = d.getElementById(id))
return obj;
if (!obj && d.getElementsByName && (e = d.getElementsByName(id)))
obj = e[0];
if (!obj && d.all)
obj = d.all[id];
if (!obj && d.images.length)
obj = d.images[id];
if (!obj && d.forms.length) {
for (f=0; f<d.forms.length; f++) {
if (d.forms[f].name == id)
obj = d.forms[f];
else if(d.forms[f].elements[id])
obj = d.forms[f].elements[id];
}
}
if (!obj && d.layers) {
if (d.layers[id])
obj = d.layers[id];
for (n=0; !obj && n<d.layers.length; n++)
obj = rcube_find_object(id, d.layers[n].document);
}
return obj;
};
// determine whether the mouse is over the given object or not
function rcube_mouse_is_over(ev, obj)
{
var mouse = rcube_event.get_mouse_pos(ev),
pos = $(obj).offset();
return (mouse.x >= pos.left) && (mouse.x < (pos.left + obj.offsetWidth)) &&
(mouse.y >= pos.top) && (mouse.y < (pos.top + obj.offsetHeight));
};
// cookie functions by GoogieSpell
function setCookie(name, value, expires, path, domain, secure)
{
var curCookie = name + "=" + escape(value) +
(expires ? "; expires=" + expires.toGMTString() : "") +
(path ? "; path=" + path : "") +
(domain ? "; domain=" + domain : "") +
(secure ? "; secure" : "");
document.cookie = curCookie;
};
function getCookie(name)
{
var dc = document.cookie,
prefix = name + "=",
begin = dc.indexOf("; " + prefix);
if (begin == -1) {
begin = dc.indexOf(prefix);
if (begin != 0)
return null;
}
else {
begin += 2;
}
var end = dc.indexOf(";", begin);
if (end == -1)
end = dc.length;
return unescape(dc.substring(begin + prefix.length, end));
};
// deprecated aliases, to be removed, use rcmail.set_cookie/rcmail.get_cookie
roundcube_browser.prototype.set_cookie = setCookie;
roundcube_browser.prototype.get_cookie = getCookie;
var bw = new roundcube_browser();
bw.set_html_class();
// Add escape() method to RegExp object
// http://dev.rubyonrails.org/changeset/7271
RegExp.escape = function(str)
{
return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
};
// Extend Date prototype to detect Standard timezone without DST
// from http://www.michaelapproved.com/articles/timezone-detect-and-ignore-daylight-saving-time-dst/
Date.prototype.getStdTimezoneOffset = function()
{
var m = 12,
d = new Date(null, m, 1),
tzo = d.getTimezoneOffset();
while (--m) {
d.setUTCMonth(m);
if (tzo != d.getTimezoneOffset()) {
return Math.max(tzo, d.getTimezoneOffset());
}
}
return tzo;
}
// define String's startsWith() method for old browsers
if (!String.prototype.startsWith) {
String.prototype.startsWith = function(search, position) {
position = position || 0;
return this.slice(position, search.length) === search;
};
}
if (!String.prototype.endsWith) {
String.prototype.endsWith = function(searchString, position) {
var subjectString = this.toString();
if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) {
position = subjectString.length;
}
position -= searchString.length;
var lastIndex = subjectString.lastIndexOf(searchString, position);
return lastIndex !== -1 && lastIndex === position;
};
}
// array utility function
jQuery.last = function(arr) {
return arr && arr.length ? arr[arr.length-1] : undefined;
}
// jQuery plugin to set HTML5 placeholder and title attributes on input elements
jQuery.fn.placeholder = function(text) {
return this.each(function() {
$(this).prop({title: text, placeholder: text});
});
};
// function to parse query string into an object
var rcube_parse_query = function(query)
{
if (!query)
return {};
var params = {}, e, k, v,
re = /([^&=]+)=?([^&]*)/g,
decodeRE = /\+/g, // Regex for replacing addition symbol with a space
decode = function (str) { return decodeURIComponent(str.replace(decodeRE, ' ')); };
query = query.replace(/\?/, '');
while (e = re.exec(query)) {
k = decode(e[1]);
v = decode(e[2]);
if (k.substring(k.length - 2) === '[]') {
k = k.substring(0, k.length - 2);
(params[k] || (params[k] = [])).push(v);
}
else
params[k] = v;
}
return params;
};
// Base64 code from Tyler Akins -- http://rumkin.com
var Base64 = (function () {
var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
// private method for UTF-8 encoding
var utf8_encode = function(string) {
string = string.replace(/\r\n/g, "\n");
var utftext = '';
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
}
else if(c > 127 && c < 2048) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
}
else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
}
return utftext;
};
// private method for UTF-8 decoding
var utf8_decode = function (utftext) {
var i = 0, string = '', c = 0, c2 = 0, c3 = 0;
while (i < utftext.length) {
c = utftext.charCodeAt(i);
if (c < 128) {
string += String.fromCharCode(c);
i++;
}
else if (c > 191 && c < 224) {
c2 = utftext.charCodeAt(i + 1);
string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
i += 2;
}
else {
c2 = utftext.charCodeAt(i + 1);
c3 = utftext.charCodeAt(i + 2);
string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
i += 3;
}
}
return string;
};
var obj = {
/**
* Encodes a string in base64
* @param {String} input The string to encode in base64.
*/
encode: function (input) {
// encode UTF8 as btoa() may fail on some characters
input = utf8_encode(input);
if (typeof(window.btoa) === 'function') {
try {
return btoa(input);
}
catch (e) {};
}
var chr1, chr2, chr3, enc1, enc2, enc3, enc4, i = 0, output = '', len = input.length;
while (i < len) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2))
enc3 = enc4 = 64;
else if (isNaN(chr3))
enc4 = 64;
output = output
+ keyStr.charAt(enc1) + keyStr.charAt(enc2)
+ keyStr.charAt(enc3) + keyStr.charAt(enc4);
}
return output;
},
/**
* Decodes a base64 string.
* @param {String} input The string to decode.
*/
decode: function (input) {
if (typeof(window.atob) === 'function') {
try {
return utf8_decode(atob(input));
}
catch (e) {};
}
var chr1, chr2, chr3, enc1, enc2, enc3, enc4, len, i = 0, output = '';
// remove all characters that are not A-Z, a-z, 0-9, +, /, or =
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
len = input.length;
while (i < len) {
enc1 = keyStr.indexOf(input.charAt(i++));
enc2 = keyStr.indexOf(input.charAt(i++));
enc3 = keyStr.indexOf(input.charAt(i++));
enc4 = keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
if (enc3 != 64)
output = output + String.fromCharCode(chr2);
if (enc4 != 64)
output = output + String.fromCharCode(chr3);
}
return utf8_decode(output);
}
};
return obj;
})();
diff --git a/program/js/editor.js b/program/js/editor.js
index 994ba638d..9b88c89cd 100644
--- a/program/js/editor.js
+++ b/program/js/editor.js
@@ -1,849 +1,849 @@
/**
* Roundcube editor js library
*
* This file is part of the Roundcube Webmail client
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
- * Copyright (c) 2006-2014, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The JavaScript code in this page is free software: you can
* redistribute it and/or modify it under the terms of the GNU
* General Public License (GNU GPL) as published by the Free Software
* Foundation, either version 3 of the License, or (at your option)
* any later version. The code is distributed WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
*
* As additional permission under GNU GPL version 3 section 7, you
* may distribute non-source (e.g., minimized or compacted) forms of
* that code without the copy of the GNU GPL normally required by
* section 4, provided you include this license notice and a URL
* through which recipients can access the Corresponding Source.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*
* @author Eric Stadtherr <estadtherr@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
/**
* Roundcube Text Editor Widget class
* @constructor
*/
function rcube_text_editor(config, id)
{
var ref = this,
abs_url = location.href.replace(/[?#].*$/, '').replace(/\/$/, ''),
conf = {
selector: '#' + ($('#' + id).is('.mce_editor') ? id : 'fake-editor-id'),
cache_suffix: 's=4080200',
theme: 'modern',
language: config.lang,
content_css: rcmail.assets_path(config.content_css),
menubar: false,
statusbar: false,
toolbar_items_size: 'small',
extended_valid_elements: 'font[face|size|color|style],span[id|class|align|style]',
fontsize_formats: '8pt 9pt 10pt 11pt 12pt 14pt 18pt 24pt 36pt',
valid_children: '+body[style]',
relative_urls: false,
remove_script_host: false,
convert_urls: false, // #1486944
image_description: false,
paste_webkit_style: "color font-size font-family",
paste_data_images: true,
browser_spellcheck: true,
anchor_bottom: false,
anchor_top: false
};
// register spellchecker for plain text editor
this.spellcheck_observer = function() {};
if (config.spellchecker) {
this.spellchecker = config.spellchecker;
if (config.spellcheck_observer) {
this.spellchecker.spelling_state_observer = this.spellcheck_observer = config.spellcheck_observer;
}
}
// secure spellchecker requests with Roundcube token
// Note: must be registered only once (#1490311)
if (!tinymce.registered_request_token) {
tinymce.registered_request_token = true;
tinymce.util.XHR.on('beforeSend', function(e) {
e.xhr.setRequestHeader('X-Roundcube-Request', rcmail.env.request_token);
});
}
// minimal editor
if (config.mode == 'identity') {
$.extend(conf, {
plugins: 'autolink charmap code colorpicker hr image link paste tabfocus textcolor',
toolbar: 'bold italic underline alignleft aligncenter alignright alignjustify'
+ ' | outdent indent charmap hr link unlink image code forecolor'
+ ' | fontselect fontsizeselect',
file_browser_callback: function(name, url, type, win) { ref.file_browser_callback(name, url, type); },
file_browser_callback_types: 'image'
});
}
// full-featured editor
else {
$.extend(conf, {
plugins: 'autolink charmap code colorpicker directionality link lists image media nonbreaking'
+ ' paste table tabfocus textcolor searchreplace spellchecker',
toolbar: 'bold italic underline | alignleft aligncenter alignright alignjustify'
+ ' | bullist numlist outdent indent ltr rtl blockquote | forecolor backcolor | fontselect fontsizeselect'
+ ' | link unlink table | $extra charmap image media | code searchreplace undo redo',
spellchecker_rpc_url: abs_url + '/?_task=utils&_action=spell_html&_remote=1',
spellchecker_language: rcmail.env.spell_lang,
accessibility_focus: false,
file_browser_callback: function(name, url, type, win) { ref.file_browser_callback(name, url, type); },
// @todo: support more than image (types: file, image, media)
file_browser_callback_types: 'image media'
});
}
// add TinyMCE plugins/buttons from Roundcube plugin
$.each(config.extra_plugins || [], function() {
if (conf.plugins.indexOf(this) < 0)
conf.plugins = conf.plugins + ' ' + this;
});
$.each(config.extra_buttons || [], function() {
if (conf.toolbar.indexOf(this) < 0)
conf.toolbar = conf.toolbar.replace('$extra', '$extra ' + this);
});
// disable TinyMCE plugins/buttons from Roundcube plugin
$.each(config.disabled_plugins || [], function() {
conf.plugins = conf.plugins.replace(this, '');
});
$.each(config.disabled_buttons || [], function() {
conf.toolbar = conf.toolbar.replace(this, '');
});
conf.toolbar = conf.toolbar.replace('$extra', '').replace(/\|\s+\|/g, '|');
// support external configuration settings e.g. from skin
if (window.rcmail_editor_settings)
$.extend(conf, window.rcmail_editor_settings);
conf.setup = function(ed) {
ed.on('init', function(ed) { ref.init_callback(ed); });
// add handler for spellcheck button state update
ed.on('SpellcheckStart SpellcheckEnd', function(args) {
ref.spellcheck_active = args.type == 'spellcheckstart';
ref.spellcheck_observer();
});
ed.on('keypress', function() {
rcmail.compose_type_activity++;
});
// make links open on shift-click
ed.on('click', function(e) {
var link = $(e.target).closest('a');
if (link.length && e.shiftKey) {
window.open(link.get(0).href, '_blank');
return false;
}
});
ed.on('focus blur', function(e) {
$(ed.getContainer()).toggleClass('focused');
});
if (conf.setup_callback)
conf.setup_callback(ed);
};
rcmail.triggerEvent('editor-init', {config: conf, ref: ref});
// textarea identifier
this.id = id;
// reference to active editor (if in HTML mode)
this.editor = null;
tinymce.init(conf);
// react to real individual tinyMCE editor init
this.init_callback = function(event)
{
this.editor = event.target;
rcmail.triggerEvent('editor-load', {config: conf, ref: ref});
if (rcmail.env.action == 'compose') {
var area = $('#' + this.id),
height = $('div.mce-toolbar-grp', area.parent()).first().height();
// the editor might be still not fully loaded, making the editing area
// inaccessible, wait and try again (#1490310)
if (height > 200 || height > area.height()) {
return setTimeout(function () { ref.init_callback(event); }, 300);
}
var css = {},
elem = rcube_find_object('_from'),
fe = rcmail.env.compose_focus_elem;
if (rcmail.env.default_font)
css['font-family'] = rcmail.env.default_font;
if (rcmail.env.default_font_size)
css['font-size'] = rcmail.env.default_font_size;
if (css['font-family'] || css['font-size'])
$(this.editor.getBody()).css(css);
if (elem && elem.type == 'select-one') {
// insert signature (only for the first time)
if (!rcmail.env.identities_initialized)
rcmail.change_identity(elem);
// Focus previously focused element
if (fe && fe.id != this.id && fe.nodeName != 'BODY') {
window.focus(); // for WebKit (#1486674)
fe.focus();
rcmail.env.compose_focus_elem = null;
}
}
}
// set tabIndex and set focus to element that was focused before
ref.tabindex(ref.force_focus || (fe && fe.id == ref.id));
// Trigger resize (needed for proper editor resizing in some browsers)
$(window).resize();
};
// set tabIndex on tinymce editor
this.tabindex = function(focus)
{
if (rcmail.env.task == 'mail' && this.editor) {
var textarea = this.editor.getElement(),
node = this.editor.getContentAreaContainer().childNodes[0];
if (textarea && node)
node.tabIndex = textarea.tabIndex;
// find :prev and :next elements to get focus when tabbing away
if (textarea.tabIndex > 0) {
var x = null,
tabfocus_elements = [':prev',':next'],
el = tinymce.DOM.select('*[tabindex='+textarea.tabIndex+']:not(iframe)');
tinymce.each(el, function(e, i) { if (e.id == ref.id) { x = i; return false; } });
if (x !== null) {
if (el[x-1] && el[x-1].id) {
tabfocus_elements[0] = el[x-1].id;
}
if (el[x+1] && el[x+1].id) {
tabfocus_elements[1] = el[x+1].id;
}
this.editor.settings.tabfocus_elements = tabfocus_elements.join(',');
}
}
// ContentEditable reset fixes invisible cursor issue in Firefox < 25
if (bw.mz && bw.vendver < 25)
$(this.editor.getBody()).prop('contenteditable', false).prop('contenteditable', true);
}
if (focus)
this.focus();
};
// focus the editor
this.focus = function()
{
$(this.editor || ('#' + this.id)).focus();
this.force_focus = false;
};
// switch html/plain mode
this.toggle = function(ishtml, noconvert)
{
var curr, content, result,
// these non-printable chars are not removed on text2html and html2text
// we can use them as temp signature replacement
sig_mark = "\u0002\u0003",
input = $('#' + this.id),
signature = rcmail.env.identity ? rcmail.env.signatures[rcmail.env.identity] : null,
is_sig = signature && signature.text && signature.text.length > 1;
// apply spellcheck changes if spell checker is active
this.spellcheck_stop();
if (ishtml) {
content = input.val();
// replace current text signature with temp mark
if (is_sig) {
content = content.replace(/\r\n/, "\n");
content = content.replace(signature.text.replace(/\r\n/, "\n"), sig_mark);
}
var init_editor = function(data) {
// replace signature mark with html version of the signature
if (is_sig)
data = data.replace(sig_mark, '<div id="_rc_sig">' + signature.html + '</div>');
ref.force_focus = true;
input.val(data);
tinymce.execCommand('mceAddEditor', false, ref.id);
};
// convert to html
if (!noconvert) {
result = rcmail.plain2html(content, init_editor);
}
else {
init_editor(content);
result = true;
}
}
else if (this.editor) {
if (is_sig) {
// get current version of signature, we'll need it in
// case of html2text conversion abort
if (curr = this.editor.dom.get('_rc_sig'))
curr = curr.innerHTML;
// replace current signature with some non-printable characters
// we use non-printable characters, because this replacement
// is visible to the user
// doing this after getContent() would be hard
this.editor.dom.setHTML('_rc_sig', sig_mark);
}
// get html content
content = this.editor.getContent();
var init_plaintext = function(data) {
tinymce.execCommand('mceRemoveEditor', false, ref.id);
ref.editor = null;
// replace signture mark with text version of the signature
if (is_sig)
data = data.replace(sig_mark, "\n" + signature.text);
input.val(data).focus();
rcmail.set_caret_pos(input.get(0), 0);
};
// convert html to text
if (!noconvert) {
result = rcmail.html2plain(content, init_plaintext);
}
else {
init_plaintext(input.val());
result = true;
}
// bring back current signature
if (!result && curr)
this.editor.dom.setHTML('_rc_sig', curr);
}
return result;
};
// start spellchecker
this.spellcheck_start = function()
{
if (this.editor) {
tinymce.execCommand('mceSpellCheck', true);
this.spellcheck_observer();
}
else if (this.spellchecker && this.spellchecker.spellCheck) {
this.spellchecker.spellCheck();
}
};
// stop spellchecker
this.spellcheck_stop = function()
{
var ed = this.editor;
if (ed) {
if (ed.plugins && ed.plugins.spellchecker && this.spellcheck_active) {
ed.execCommand('mceSpellCheck', false);
this.spellcheck_observer();
}
}
else if (ed = this.spellchecker) {
if (ed.state && ed.state != 'ready' && ed.state != 'no_error_found')
$(ed.spell_span).trigger('click');
}
};
// spellchecker state
this.spellcheck_state = function()
{
var ed;
if (this.editor)
return this.spellcheck_active;
else if ((ed = this.spellchecker) && ed.state)
return ed.state != 'ready' && ed.state != 'no_error_found';
};
// resume spellchecking, highlight provided mispellings without a new ajax request
this.spellcheck_resume = function(data)
{
var ed = this.editor;
if (ed) {
ed.plugins.spellchecker.markErrors(data);
}
else if (ed = this.spellchecker) {
ed.prepare(false, true);
ed.processData(data);
}
};
// get selected (spellchecker) language
this.get_language = function()
{
if (this.editor) {
return this.editor.settings.spellchecker_language || rcmail.env.spell_lang;
}
else if (this.spellchecker) {
return GOOGIE_CUR_LANG;
}
};
// set language for spellchecking
this.set_language = function(lang)
{
var ed = this.editor;
if (ed) {
ed.settings.spellchecker_language = lang;
}
if (ed = this.spellchecker) {
ed.setCurrentLanguage(lang);
}
};
// replace selection with text snippet
// input can be a string or object with 'text' and 'html' properties
this.replace = function(input)
{
var format, ed = this.editor;
if (!input)
return false;
// insert into tinymce editor
if (ed) {
ed.getWin().focus(); // correct focus in IE & Chrome
if ($.type(input) == 'object' && ('html' in input)) {
input = input.html;
format = 'html';
}
else {
if ($.type(input) == 'object')
input = input.text || '';
input = rcmail.quote_html(input).replace(/\r?\n/g, '<br/>');
format = 'text';
}
ed.selection.setContent(input, {format: format});
}
// replace selection in compose textarea
else if (ed = rcube_find_object(this.id)) {
var selection = $(ed).is(':focus') ? rcmail.get_input_selection(ed) : {start: 0, end: 0},
value = ed.value,
pre = value.substring(0, selection.start),
end = value.substring(selection.end, value.length);
if ($.type(input) == 'object')
input = input.text || '';
// insert response text
ed.value = pre + input + end;
// set caret after inserted text
rcmail.set_caret_pos(ed, selection.start + input.length);
ed.focus();
}
};
// Fill the editor with specified content
// TODO: support format conversion
this.set_content = function(content)
{
if (this.editor) {
this.editor.setContent(content);
this.editor.getWin().focus();
}
else if (ed = rcube_find_object(this.id)) {
$(ed).val(content).focus();
}
};
// get selected text (if no selection returns all text) from the editor
this.get_content = function(args)
{
var sigstart, ed = this.editor, text = '', strip = false,
defaults = {refresh: true, selection: false, nosig: false, format: 'html'};
if (!args)
args = defaults;
else
args = $.extend(defaults, args);
// apply spellcheck changes if spell checker is active
if (args.refresh) {
this.spellcheck_stop();
}
// get selected text from tinymce editor
if (ed) {
if (args.selection)
text = ed.selection.getContent({format: args.format});
if (!text) {
text = ed.getContent({format: args.format});
// @todo: strip signature in html mode
strip = args.format == 'text';
}
}
// get selected text from compose textarea
else if (ed = rcube_find_object(this.id)) {
if (args.selection && $(ed).is(':focus')) {
text = rcmail.get_input_selection(ed).text;
}
if (!text) {
text = ed.value;
strip = true;
}
}
// strip off signature
// @todo: make this optional
if (strip && args.nosig) {
sigstart = text.indexOf('-- \n');
if (sigstart > 0) {
text = text.substring(0, sigstart);
}
}
return text;
};
// change user signature text
this.change_signature = function(id, show_sig)
{
var position_element, cursor_pos, p = -1,
input_message = $('#' + this.id),
message = input_message.val(),
sig = rcmail.env.identity;
if (!this.editor) { // plain text mode
// remove the 'old' signature
if (show_sig && sig && rcmail.env.signatures && rcmail.env.signatures[sig]) {
sig = rcmail.env.signatures[sig].text;
sig = sig.replace(/\r\n/g, '\n');
p = rcmail.env.top_posting ? message.indexOf(sig) : message.lastIndexOf(sig);
if (p >= 0)
message = message.substring(0, p) + message.substring(p+sig.length, message.length);
}
// add the new signature string
if (show_sig && rcmail.env.signatures && rcmail.env.signatures[id]) {
sig = rcmail.env.signatures[id].text;
sig = sig.replace(/\r\n/g, '\n');
// in place of removed signature
if (p >= 0) {
message = message.substring(0, p) + sig + message.substring(p, message.length);
cursor_pos = p - 1;
}
// empty message or new-message mode
else if (!message || !rcmail.env.compose_mode) {
cursor_pos = message.length;
message += '\n\n' + sig;
}
else if (rcmail.env.top_posting && !rcmail.env.sig_below) {
// at cursor position
if (pos = rcmail.get_caret_pos(input_message.get(0))) {
message = message.substring(0, pos) + '\n' + sig + '\n\n' + message.substring(pos, message.length);
cursor_pos = pos;
}
// on top
else {
message = '\n\n' + sig + '\n\n' + message.replace(/^[\r\n]+/, '');
cursor_pos = 0;
}
}
else {
message = message.replace(/[\r\n]+$/, '');
cursor_pos = !rcmail.env.top_posting && message.length ? message.length + 1 : 0;
message += '\n\n' + sig;
}
}
else {
cursor_pos = rcmail.env.top_posting ? 0 : message.length;
}
input_message.val(message);
// move cursor before the signature
rcmail.set_caret_pos(input_message.get(0), cursor_pos);
}
else if (show_sig && rcmail.env.signatures) { // html
var sigElem = this.editor.dom.get('_rc_sig');
// Append the signature as a div within the body
if (!sigElem) {
var body = this.editor.getBody();
sigElem = $('<div id="_rc_sig"></div>').get(0);
// insert at start or at cursor position in top-posting mode
// (but not if the content is empty and not in new-message mode)
if (rcmail.env.top_posting && !rcmail.env.sig_below
&& rcmail.env.compose_mode && (body.childNodes.length > 1 || $(body).text())
) {
this.editor.getWin().focus(); // correct focus in IE & Chrome
var node = this.editor.selection.getNode();
$(sigElem).insertBefore(node.nodeName == 'BODY' ? body.firstChild : node.nextSibling);
$('<p>').append($('<br>')).insertBefore(sigElem);
}
else {
body.appendChild(sigElem);
position_element = rcmail.env.top_posting && rcmail.env.compose_mode ? body.firstChild : $(sigElem).prev();
}
}
sigElem.innerHTML = rcmail.env.signatures[id] ? rcmail.env.signatures[id].html : '';
}
else if (!rcmail.env.top_posting) {
position_element = $(this.editor.getBody()).children().last();
}
// put cursor before signature and scroll the window
if (this.editor && position_element && position_element.length) {
this.editor.selection.setCursorLocation(position_element.get(0));
this.editor.getWin().scroll(0, position_element.offset().top);
}
};
// trigger content save
this.save = function()
{
if (this.editor) {
this.editor.save();
}
};
// focus the editing area
this.focus = function()
{
(this.editor || rcube_find_object(this.id)).focus();
};
// image selector
this.file_browser_callback = function(field_name, url, type)
{
var i, button, elem, cancel, dialog, fn, hint, list = [],
form = $('.upload-form').clone();
// open image selector dialog
this.editor.windowManager.open({
title: rcmail.get_label('select' + type),
width: 500,
html: '<div id="image-selector" class="image-selector file-upload"><ul id="image-selector-list" class="attachmentslist"></ul></div>',
buttons: [{text: 'Cancel', onclick: function() { ref.file_browser_close(); }}]
});
rcmail.env.file_browser_field = field_name;
rcmail.env.file_browser_type = type;
dialog = $('#image-selector');
if (!form.length)
form = this.file_upload_form(rcmail.gui_objects.uploadform);
else
form.find('button,a.button').slice(1).remove(); // need only the first button
button = dialog.prepend(form).find('button,a.button')
.text(rcmail.get_label('add' + type))
.focus();
// fill images list with available images
for (i in rcmail.env.attachments) {
if (elem = ref.file_browser_entry(i, rcmail.env.attachments[i])) {
list.push(elem);
}
}
cancel = dialog.parent().parent().find('button').last().parent();
// Add custom Tab key handlers, tabindex does not work
list = $('#image-selector-list').append(list).on('keydown', 'li', function(e) {
if (e.which == 9) {
if (rcube_event.get_modifier(e) == SHIFT_KEY) {
if (!$(this).prev().focus().length) {
button.focus();
}
}
else if (!$(this).next().focus().length) {
cancel.focus();
}
return false;
}
});
button.keydown(function(e) {
if (e.which == 9) { // Tab
if (rcube_event.get_modifier(e) == SHIFT_KEY || !list.find('li').first().focus().length) {
cancel.focus();
}
return false;
}
if (e.which == 13) { // Enter
this.click();
}
});
cancel.keydown(function(e) {
if (e.which == 9) {
if (rcube_event.get_modifier(e) != SHIFT_KEY || !list.find('li').last().focus().length) {
button.focus();
}
return false;
}
});
// enable drag-n-drop area
if (window.FormData) {
if (!rcmail.env.filedrop) {
rcmail.env.filedrop = {};
}
if (rcmail.gui_objects.filedrop) {
rcmail.env.old_file_drop = rcmail.gui_objects.filedrop;
}
rcmail.gui_objects.filedrop = $('#image-selector');
rcmail.gui_objects.filedrop.addClass('droptarget')
.on('dragover dragleave', function(e) {
e.preventDefault();
e.stopPropagation();
$(this)[(e.type == 'dragover' ? 'addClass' : 'removeClass')]('hover');
})
.get(0).addEventListener('drop', function(e) { return rcmail.file_dropped(e); }, false);
}
// register handler for successful file upload
if (!rcmail.env['file_dialog_event_' + type]) {
rcmail.env['file_dialog_event+' + type] = true;
rcmail.addEventListener('fileuploaded', function(attr) {
var elem;
if (elem = ref.file_browser_entry(attr.name, attr.attachment)) {
list.prepend(elem);
elem.focus();
}
});
}
// @todo: upload progress indicator
};
// close file browser window
this.file_browser_close = function(url)
{
var input = $('#' + rcmail.env.file_browser_field);
if (url)
input.val(url);
this.editor.windowManager.close();
input.focus();
if (rcmail.env.old_file_drop)
rcmail.gui_objects.filedrop = rcmail.env.old_file_drop;
};
// creates file browser entry
this.file_browser_entry = function(file_id, file)
{
if (!file.complete || !file.mimetype) {
return;
}
if (rcmail.file_upload_id) {
rcmail.set_busy(false, null, rcmail.file_upload_id);
}
var rx, img_src;
switch (rcmail.env.file_browser_type) {
case 'image':
rx = /^image\//i;
break;
case 'media':
rx = /^video\//i;
img_src = rcmail.assets_path('program/resources/tinymce/video.png');
break;
default:
return;
}
if (rx.test(file.mimetype)) {
var path = rcmail.env.comm_path + '&_from=' + rcmail.env.action,
action = rcmail.env.compose_id ? '&_id=' + rcmail.env.compose_id + '&_action=display-attachment' : '&_action=upload-display',
href = path + action + '&_file=' + file_id,
img = $('<img>').attr({title: file.name, src: img_src ? img_src : href + '&_thumbnail=1'});
return $('<li>').attr({tabindex: 0})
.data('url', href)
.append($('<span class="img">').append(img))
.append($('<span class="name">').text(file.name))
.click(function() { ref.file_browser_close($(this).data('url')); })
.keydown(function(e) {
if (e.which == 13) {
ref.file_browser_close($(this).data('url'));
}
});
}
};
this.file_upload_form = function(clone_form)
{
var hint = clone_form ? $(clone_form).find('.hint').text() : '',
form = $('<form id="imageuploadform">').attr({method: 'post', enctype: 'multipart/form-data'});
file = $('<input>').attr({name: '_file[]', type: 'file', multiple: true, style: 'opacity:0;height:1px;width:1px'})
.change(function() { rcmail.upload_file(form, 'upload'); }),
wrapper = $('<div class="upload-form">')
.append($('<button>').attr({'class': 'btn btn-secondary attach', href: '#', onclick: "rcmail.upload_input('imageuploadform')"}));
if (hint)
wrapper.prepend($('<div class="hint">').text(hint));
// clone existing upload form
if (clone_form) {
file.attr('name', $('input[type="file"]', clone_form).attr('name'));
form.attr('action', $(clone_form).attr('action'));
}
form.append(file).append($('<input>').attr({type: 'hidden', name: '_token', value: rcmail.env.request_token}));
return wrapper.append(form);
};
}
diff --git a/program/js/googiespell.js b/program/js/googiespell.js
index daa7698a0..d783eb658 100644
--- a/program/js/googiespell.js
+++ b/program/js/googiespell.js
@@ -1,1172 +1,1172 @@
/**
* Roundcube SpellCheck script
*
* jQuery'fied spell checker based on GoogieSpell 4.0
* (which was published under GPL "version 2 or any later version")
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
* Copyright (C) 2006 Amir Salihefendic
- * Copyright (C) 2009 The Roundcube Dev Team
- * Copyright (C) 2011 Kolab Systems AG
+ * Copyright (C) The Roundcube Dev Team
+ * Copyright (C) Kolab Systems AG
*
* The JavaScript code in this page is free software: you can
* redistribute it and/or modify it under the terms of the GNU
* General Public License (GNU GPL) as published by the Free Software
* Foundation, either version 3 of the License, or (at your option)
* any later version. The code is distributed WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
*
* As additional permission under GNU GPL version 3 section 7, you
* may distribute non-source (e.g., minimized or compacted) forms of
* that code without the copy of the GNU GPL normally required by
* section 4, provided you include this license notice and a URL
* through which recipients can access the Corresponding Source.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*
- * @author 4mir Salihefendic <amix@amix.dk>
+ * @author 4mir Salihefendic <amix@amix.dk>
* @author Aleksander Machniak - <alec [at] alec.pl>
*/
var GOOGIE_CUR_LANG,
GOOGIE_DEFAULT_LANG = 'en';
function GoogieSpell(img_dir, server_url, has_dict)
{
var ref = this,
cookie_value = rcmail.get_cookie('language');
GOOGIE_CUR_LANG = cookie_value != null ? cookie_value : GOOGIE_DEFAULT_LANG;
this.array_keys = function(arr) {
var res = [];
for (var key in arr) { res.push([key]); }
return res;
}
this.img_dir = img_dir;
this.server_url = server_url;
this.org_lang_to_word = {
"da": "Dansk", "de": "Deutsch", "en": "English",
"es": "Español", "fr": "Français", "it": "Italiano",
"nl": "Nederlands", "pl": "Polski", "pt": "Português",
"ru": "Русский", "fi": "Suomi", "sv": "Svenska"
};
this.lang_to_word = this.org_lang_to_word;
this.langlist_codes = this.array_keys(this.lang_to_word);
this.show_change_lang_pic = false; // roundcube mod.
this.change_lang_pic_placement = 'right';
this.report_state_change = true;
this.ta_scroll_top = 0;
this.el_scroll_top = 0;
this.lang_chck_spell = "Check spelling";
this.lang_revert = "Revert to";
this.lang_close = "Close";
this.lang_rsm_edt = "Resume editing";
this.lang_no_error_found = "No spelling errors found";
this.lang_no_suggestions = "No suggestions";
this.lang_learn_word = "Add to dictionary";
this.use_ok_pic = false; // added by roundcube
this.show_spell_img = false; // roundcube mod.
this.decoration = true;
this.use_close_btn = false;
this.edit_layer_dbl_click = true;
this.report_ta_not_found = true;
// Extensions
this.custom_ajax_error = null;
this.custom_no_spelling_error = null;
this.custom_menu_builder = []; // Should take an eval function and a build menu function
this.custom_item_evaulator = null; // Should take an eval function and a build menu function
this.extra_menu_items = [];
this.custom_spellcheck_starter = null;
this.main_controller = true;
this.has_dictionary = has_dict;
// Observers
this.lang_state_observer = null;
this.spelling_state_observer = null;
this.show_menu_observer = null;
this.all_errors_fixed_observer = null;
// Focus links - used to give the text box focus
this.use_focus = false;
this.focus_link_t = null;
this.focus_link_b = null;
// Counters
this.cnt_errors = 0;
this.cnt_errors_fixed = 0;
// Set document's onclick to hide the language and error menu
$(document).click(function(e) {
var target = $(e.target);
if (target.attr('googie_action_btn') != '1' && ref.isLangWindowShown())
ref.hideLangWindow();
if (target.attr('googie_action_btn') != '1' && ref.isErrorWindowShown())
ref.hideErrorWindow();
});
this.decorateTextarea = function(id)
{
this.text_area = typeof id === 'string' ? document.getElementById(id) : id;
if (this.text_area) {
if (!this.spell_container && this.decoration) {
var table = document.createElement('table'),
tbody = document.createElement('tbody'),
tr = document.createElement('tr'),
spell_container = document.createElement('td'),
r_width = this.isDefined(this.force_width) ? this.force_width : this.text_area.offsetWidth,
r_height = this.isDefined(this.force_height) ? this.force_height : 16;
tr.appendChild(spell_container);
tbody.appendChild(tr);
$(table).append(tbody).insertBefore(this.text_area).width('100%').height(r_height);
$(spell_container).height(r_height).width(r_width).css('text-align', 'right');
this.spell_container = spell_container;
}
this.checkSpellingState();
}
else if (this.report_ta_not_found) {
rcmail.alert_dialog('Text area not found');
}
};
//////
// API Functions (the ones that you can call)
/////
this.setSpellContainer = function(id)
{
this.spell_container = typeof id === 'string' ? document.getElementById(id) : id;
};
this.setLanguages = function(lang_dict)
{
this.lang_to_word = lang_dict;
this.langlist_codes = this.array_keys(lang_dict);
};
this.setCurrentLanguage = function(lan_code)
{
GOOGIE_CUR_LANG = lan_code;
//Set cookie
var now = new Date();
now.setTime(now.getTime() + 365 * 24 * 60 * 60 * 1000);
rcmail.set_cookie('language', lan_code, now);
};
this.setForceWidthHeight = function(width, height)
{
// Set to null if you want to use one of them
this.force_width = width;
this.force_height = height;
};
this.setDecoration = function(bool)
{
this.decoration = bool;
};
this.dontUseCloseButtons = function()
{
this.use_close_btn = false;
};
this.appendNewMenuItem = function(name, call_back_fn, checker)
{
this.extra_menu_items.push([name, call_back_fn, checker]);
};
this.appendCustomMenuBuilder = function(eval_fn, builder)
{
this.custom_menu_builder.push([eval_fn, builder]);
};
this.setFocus = function()
{
try {
this.focus_link_b.focus();
this.focus_link_t.focus();
return true;
}
catch(e) {
return false;
}
};
//////
// Set functions (internal)
/////
this.setStateChanged = function(current_state)
{
this.state = current_state;
if (this.spelling_state_observer != null && this.report_state_change)
this.spelling_state_observer(current_state, this);
};
this.setReportStateChange = function(bool)
{
this.report_state_change = bool;
};
//////
// Request functions
/////
this.getUrl = function()
{
return this.server_url + GOOGIE_CUR_LANG;
};
this.escapeSpecial = function(val)
{
return val ? val.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;") : '';
};
this.createXMLReq = function (text)
{
return '<?xml version="1.0" encoding="utf-8" ?>'
+ '<spellrequest textalreadyclipped="0" ignoredups="0" ignoredigits="1" ignoreallcaps="1">'
+ '<text>' + text + '</text></spellrequest>';
};
this.spellCheck = function(ignore)
{
this.prepare(ignore);
var req_text = this.escapeSpecial(this.orginal_text),
ref = this;
$.ajax({ type: 'POST', url: this.getUrl(), data: this.createXMLReq(req_text), dataType: 'text',
error: function(o) {
if (ref.custom_ajax_error) {
ref.custom_ajax_error(ref);
}
else {
rcmail.alert_dialog('An error was encountered on the server. Please try again later.');
}
if (ref.main_controller) {
$(ref.spell_span).remove();
ref.removeIndicator();
}
ref.checkSpellingState();
},
success: function(data) {
ref.processData(data);
if (!ref.results.length) {
if (!ref.custom_no_spelling_error)
ref.flashNoSpellingErrorState();
else
ref.custom_no_spelling_error(ref);
}
ref.removeIndicator();
}
});
};
this.learnWord = function(word, id)
{
word = this.escapeSpecial(word.innerHTML);
var ref = this,
req_text = '<?xml version="1.0" encoding="utf-8" ?><learnword><text>' + word + '</text></learnword>';
$.ajax({ type: 'POST', url: this.getUrl(), data: req_text, dataType: 'text',
error: function(o) {
if (ref.custom_ajax_error) {
ref.custom_ajax_error(ref);
}
else {
rcmail.alert_dialog('An error was encountered on the server. Please try again later.');
}
},
success: function(data) {
}
});
};
//////
// Spell checking functions
/////
this.prepare = function(ignore, no_indicator)
{
this.cnt_errors_fixed = 0;
this.cnt_errors = 0;
this.setStateChanged('checking_spell');
this.orginal_text = '';
if (!no_indicator && this.main_controller)
this.appendIndicator(this.spell_span);
this.error_links = [];
this.ta_scroll_top = this.text_area.scrollTop;
this.ignore = ignore;
this.hideLangWindow();
var area = $(this.text_area);
if (area.val() == '' || ignore) {
if (!this.custom_no_spelling_error)
this.flashNoSpellingErrorState();
else
this.custom_no_spelling_error(this);
this.removeIndicator();
return;
}
this.createEditLayer(area.width(), area.height());
this.createErrorWindow();
$('body').append(this.error_window);
try { netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead"); }
catch (e) { }
if (this.main_controller)
$(this.spell_span).off('click');
this.orginal_text = area.val();
};
this.parseResult = function(r_text)
{
// Returns an array: result[item] -> ['attrs'], ['suggestions']
var re_split_attr_c = /\w+="(\d+|true)"/g,
re_split_text = /\t/g,
matched_c = r_text.match(/<c[^>]*>[^<]*<\/c>/g),
results = [];
if (matched_c == null)
return results;
for (var i=0, len=matched_c.length; i < len; i++) {
var item = [];
this.errorFound();
// Get attributes
item['attrs'] = [];
var c_attr, val,
split_c = matched_c[i].match(re_split_attr_c);
for (var j=0; j < split_c.length; j++) {
c_attr = split_c[j].split(/=/);
val = c_attr[1].replace(/"/g, '');
item['attrs'][c_attr[0]] = val != 'true' ? parseInt(val) : val;
}
// Get suggestions
item['suggestions'] = [];
var only_text = matched_c[i].replace(/<[^>]*>/g, ''),
split_t = only_text.split(re_split_text);
for (var k=0; k < split_t.length; k++) {
if(split_t[k] != '')
item['suggestions'].push(split_t[k]);
}
results.push(item);
}
return results;
};
this.processData = function(data)
{
this.results = this.parseResult(data);
if (this.results.length) {
this.showErrorsInIframe();
this.resumeEditingState();
}
};
//////
// Error menu functions
/////
this.createErrorWindow = function()
{
this.error_window = document.createElement('div');
$(this.error_window).addClass('googie_window popupmenu').attr('googie_action_btn', '1');
};
this.isErrorWindowShown = function()
{
return $(this.error_window).is(':visible');
};
this.hideErrorWindow = function()
{
$(this.error_window).hide();
$(this.error_window_iframe).hide();
};
this.updateOrginalText = function(offset, old_value, new_value, id)
{
var part_1 = this.orginal_text.substring(0, offset),
part_2 = this.orginal_text.substring(offset+old_value.length),
add_2_offset = new_value.length - old_value.length;
this.orginal_text = part_1 + new_value + part_2;
$(this.text_area).val(this.orginal_text);
for (var j=0, len=this.results.length; j<len; j++) {
// Don't edit the offset of the current item
if (j != id && j > id)
this.results[j]['attrs']['o'] += add_2_offset;
}
};
this.saveOldValue = function(elm, old_value) {
elm.is_changed = true;
elm.old_value = old_value;
};
this.createListSeparator = function()
{
var td = document.createElement('td'),
tr = document.createElement('tr');
$(td).html(' ').attr('googie_action_btn', '1')
.css({'cursor': 'default', 'font-size': '3px', 'border-top': '1px solid #ccc', 'padding-top': '3px'});
tr.appendChild(td);
return tr;
};
this.correctError = function(id, elm, l_elm, rm_pre_space)
{
var old_value = elm.innerHTML,
new_value = l_elm.nodeType == 3 ? l_elm.nodeValue : l_elm.innerHTML,
offset = this.results[id]['attrs']['o'];
if (rm_pre_space) {
var pre_length = elm.previousSibling.innerHTML;
elm.previousSibling.innerHTML = pre_length.slice(0, pre_length.length-1);
old_value = " " + old_value;
offset--;
}
this.hideErrorWindow();
this.updateOrginalText(offset, old_value, new_value, id);
$(elm).html(new_value).css('color', 'green').attr('is_corrected', true);
this.results[id]['attrs']['l'] = new_value.length;
if (!this.isDefined(elm.old_value))
this.saveOldValue(elm, old_value);
this.errorFixed();
};
this.ignoreError = function(elm, id)
{
// @TODO: ignore all same words
$(elm).removeAttr('class').css('color', '').off();
this.hideErrorWindow();
};
this.showErrorWindow = function(elm, id)
{
if (this.show_menu_observer)
this.show_menu_observer(this);
var ref = this,
pos = $(elm).offset(),
table = document.createElement('table'),
list = document.createElement('tbody');
$(this.error_window).html('');
$(table).addClass('googie_list').attr('googie_action_btn', '1');
// Check if we should use custom menu builder, if not we use the default
var changed = false;
for (var k=0; k<this.custom_menu_builder.length; k++) {
var eb = this.custom_menu_builder[k];
if (eb[0](this.results[id])) {
changed = eb[1](this, list, elm);
break;
}
}
if (!changed) {
// Build up the result list
var suggestions = this.results[id]['suggestions'],
offset = this.results[id]['attrs']['o'],
len = this.results[id]['attrs']['l'],
row, item, dummy;
// [Add to dictionary] button
if (this.has_dictionary && !$(elm).attr('is_corrected')) {
row = document.createElement('tr'),
item = document.createElement('td'),
dummy = document.createElement('span');
$(dummy).text(this.lang_learn_word).addClass('googie_add_to_dict');
$(item).attr('googie_action_btn', '1').css('cursor', 'default')
.mouseover(ref.item_onmouseover)
.mouseout(ref.item_onmouseout)
.click(function(e) {
ref.learnWord(elm, id);
ref.ignoreError(elm, id);
});
item.appendChild(dummy);
row.appendChild(item);
list.appendChild(row);
}
/*
if (suggestions.length == 0) {
row = document.createElement('tr'),
item = document.createElement('td'),
dummy = document.createElement('span');
$(dummy).text(this.lang_no_suggestions);
$(item).attr('googie_action_btn', '1').css('cursor', 'default');
item.appendChild(dummy);
row.appendChild(item);
list.appendChild(row);
}
*/
for (var i=0, len=suggestions.length; i < len; i++) {
row = document.createElement('tr'),
item = document.createElement('td'),
dummy = document.createElement('span');
$(dummy).html(suggestions[i]);
$(item).mouseover(this.item_onmouseover).mouseout(this.item_onmouseout)
.click(function(e) { ref.correctError(id, elm, e.target.firstChild) });
item.appendChild(dummy);
row.appendChild(item);
list.appendChild(row);
}
// The element is changed, append the revert
if (elm.is_changed && elm.innerHTML != elm.old_value) {
var old_value = elm.old_value,
revert_row = document.createElement('tr'),
revert = document.createElement('td'),
rev_span = document.createElement('span');
$(rev_span).addClass('googie_list_revert').html(this.lang_revert + ' ' + old_value);
$(revert).mouseover(this.item_onmouseover).mouseout(this.item_onmouseout)
.click(function(e) {
ref.updateOrginalText(offset, elm.innerHTML, old_value, id);
$(elm).removeAttr('is_corrected').css('color', '#b91414').html(old_value);
ref.hideErrorWindow();
});
revert.appendChild(rev_span);
revert_row.appendChild(revert);
list.appendChild(revert_row);
}
// Append the edit box
var edit_row = document.createElement('tr'),
edit = document.createElement('td'),
edit_input = document.createElement('input'),
ok_pic = document.createElement('button'), // roundcube mod.
edit_form = document.createElement('form');
var onsub = function () {
if (edit_input.value != '') {
if (!ref.isDefined(elm.old_value))
ref.saveOldValue(elm, elm.innerHTML);
ref.updateOrginalText(offset, elm.innerHTML, edit_input.value, id);
$(elm).attr('is_corrected', true).css('color', 'green').text(edit_input.value);
ref.hideErrorWindow();
}
return false;
};
$(edit_input).width(120)
.css({'margin': 0, 'padding': 0})
.val($(elm).text()).attr('googie_action_btn', '1');
$(edit).css('cursor', 'default').attr('googie_action_btn', '1');
// roundcube modified image use
if (this.use_ok_pic) {
$('<img>').attr('src', this.img_dir + 'ok.gif')
.width(32).height(16)
.css({cursor: 'pointer', 'margin-left': '2px', 'margin-right': '2px'})
.appendTo(ok_pic);
}
else {
$(ok_pic).text('OK');
}
$(ok_pic).addClass('mainaction save googie_ok_button').click(onsub);
$(edit_form).attr('googie_action_btn', '1')
.css({'margin': 0, 'padding': 0, 'cursor': 'default', 'white-space': 'nowrap'})
.submit(onsub);
edit_form.appendChild(edit_input);
edit_form.appendChild(ok_pic);
edit.appendChild(edit_form);
edit_row.appendChild(edit);
list.appendChild(edit_row);
// Append extra menu items
if (this.extra_menu_items.length > 0)
list.appendChild(this.createListSeparator());
var loop = function(i) {
if (i < ref.extra_menu_items.length) {
var e_elm = ref.extra_menu_items[i];
if (!e_elm[2] || e_elm[2](elm, ref)) {
var e_row = document.createElement('tr'),
e_col = document.createElement('td');
$(e_col).html(e_elm[0])
.mouseover(ref.item_onmouseover)
.mouseout(ref.item_onmouseout)
.click(function() { return e_elm[1](elm, ref) });
e_row.appendChild(e_col);
list.appendChild(e_row);
}
loop(i+1);
}
};
loop(0);
loop = null;
//Close button
if (this.use_close_btn) {
list.appendChild(this.createCloseButton(this.hideErrorWindow));
}
}
table.appendChild(list);
this.error_window.appendChild(table);
// roundcube plugin api hook
rcmail.triggerEvent('googiespell_create', {obj: this.error_window});
// calculate and set position
var height = $(this.error_window).height(),
width = $(this.error_window).width(),
pageheight = $(document).height(),
pagewidth = $(document).width(),
top = pos.top + height + 20 < pageheight ? pos.top + 20 : pos.top - height,
left = pos.left + width < pagewidth ? pos.left : pos.left - width;
if (left < 0) left = 0;
if (top < 0) top = 0;
$(this.error_window).css({'top': top+'px', 'left': left+'px', position: 'absolute'}).show();
// Dummy for IE - dropdown bug fix
if (document.all && !window.opera) {
if (!this.error_window_iframe) {
var iframe = $('<iframe>').css({'position': 'absolute', 'z-index': -1});
$('body').append(iframe);
this.error_window_iframe = iframe;
}
$(this.error_window_iframe)
.css({'top': this.error_window.offsetTop, 'left': this.error_window.offsetLeft,
'width': this.error_window.offsetWidth, 'height': this.error_window.offsetHeight})
.show();
}
};
//////
// Edit layer (the layer where the suggestions are stored)
//////
this.createEditLayer = function(width, height)
{
this.edit_layer = document.createElement('div');
$(this.edit_layer).addClass('googie_edit_layer').attr('id', 'googie_edit_layer')
.width(width).height(height);
if (this.text_area.nodeName.toLowerCase() != 'input' || $(this.text_area).val() == '') {
$(this.edit_layer).css('overflow', 'auto');
} else {
$(this.edit_layer).css('overflow', 'hidden');
}
var ref = this;
if (this.edit_layer_dbl_click) {
$(this.edit_layer).dblclick(function(e) {
if (e.target.className != 'googie_link' && !ref.isErrorWindowShown()) {
ref.resumeEditing();
var fn1 = function() {
$(ref.text_area).focus();
fn1 = null;
};
window.setTimeout(fn1, 10);
}
return false;
});
}
};
this.resumeEditing = function()
{
this.setStateChanged('ready');
if (this.edit_layer)
this.el_scroll_top = this.edit_layer.scrollTop;
this.hideErrorWindow();
if (this.main_controller)
$(this.spell_span).removeClass().addClass('googie_no_style');
if (!this.ignore) {
if (this.use_focus) {
$(this.focus_link_t).remove();
$(this.focus_link_b).remove();
}
$(this.edit_layer).remove();
$(this.text_area).show();
if (this.el_scroll_top != undefined)
this.text_area.scrollTop = this.el_scroll_top;
}
this.checkSpellingState(false);
};
this.createErrorLink = function(text, id)
{
var elm = document.createElement('span'),
ref = this,
d = function (e) {
ref.showErrorWindow(elm, id);
d = null;
return false;
};
$(elm).html(text).addClass('googie_link').click(d).removeAttr('is_corrected')
.attr({'googie_action_btn' : '1', 'g_id' : id});
return elm;
};
this.createPart = function(txt_part)
{
if (txt_part == " ")
return document.createTextNode(" ");
txt_part = this.escapeSpecial(txt_part);
txt_part = txt_part.replace(/\n/g, "<br>");
txt_part = txt_part.replace(/ /g, " &nbsp;");
txt_part = txt_part.replace(/^ /g, "&nbsp;");
txt_part = txt_part.replace(/ $/g, "&nbsp;");
var span = document.createElement('span');
$(span).html(txt_part);
return span;
};
this.showErrorsInIframe = function()
{
var output = document.createElement('div'),
pointer = 0,
results = this.results;
if (results.length > 0) {
for (var i=0, length=results.length; i < length; i++) {
var offset = results[i]['attrs']['o'],
len = results[i]['attrs']['l'],
part_1_text = this.orginal_text.substring(pointer, offset),
part_1 = this.createPart(part_1_text);
output.appendChild(part_1);
pointer += offset - pointer;
// If the last child was an error, then insert some space
var err_link = this.createErrorLink(this.orginal_text.substr(offset, len), i);
this.error_links.push(err_link);
output.appendChild(err_link);
pointer += len;
}
// Insert the rest of the orginal text
var part_2_text = this.orginal_text.substr(pointer, this.orginal_text.length),
part_2 = this.createPart(part_2_text);
output.appendChild(part_2);
}
else
output.innerHTML = this.orginal_text;
$(output).css('text-align', 'left');
var me = this;
if (this.custom_item_evaulator)
$.map(this.error_links, function(elm){me.custom_item_evaulator(me, elm)});
$(this.edit_layer).append(output);
// Hide text area and show edit layer
$(this.text_area).hide();
$(this.edit_layer).insertBefore(this.text_area);
if (this.use_focus) {
this.focus_link_t = this.createFocusLink('focus_t');
this.focus_link_b = this.createFocusLink('focus_b');
$(this.focus_link_t).insertBefore(this.edit_layer);
$(this.focus_link_b).insertAfter(this.edit_layer);
}
// this.edit_layer.scrollTop = this.ta_scroll_top;
};
//////
// Choose language menu
//////
this.createLangWindow = function()
{
this.language_window = document.createElement('div');
$(this.language_window).addClass('googie_window popupmenu')
.width(100).attr('googie_action_btn', '1');
// Build up the result list
var table = document.createElement('table'),
list = document.createElement('tbody'),
ref = this,
row, item, span;
$(table).addClass('googie_list').width('100%');
this.lang_elms = [];
for (i=0; i < this.langlist_codes.length; i++) {
row = document.createElement('tr');
item = document.createElement('td');
span = document.createElement('span');
$(span).text(this.lang_to_word[this.langlist_codes[i]]);
this.lang_elms.push(item);
$(item).attr('googieId', this.langlist_codes[i])
.click(function(e) {
ref.deHighlightCurSel();
ref.setCurrentLanguage($(this).attr('googieId'));
if (ref.lang_state_observer != null) {
ref.lang_state_observer();
}
ref.highlightCurSel();
ref.hideLangWindow();
})
.mouseover(function(e) {
if (this.className != "googie_list_selected")
this.className = "googie_list_onhover";
})
.mouseout(function(e) {
if (this.className != "googie_list_selected")
this.className = "googie_list_onout";
});
item.appendChild(span);
row.appendChild(item);
list.appendChild(row);
}
// Close button
if (this.use_close_btn) {
list.appendChild(this.createCloseButton(function () { ref.hideLangWindow.apply(ref) }));
}
this.highlightCurSel();
table.appendChild(list);
this.language_window.appendChild(table);
};
this.isLangWindowShown = function()
{
return $(this.language_window).is(':visible');
};
this.hideLangWindow = function()
{
$(this.language_window).hide();
$(this.switch_lan_pic).removeClass().addClass('googie_lang_3d_on');
};
this.showLangWindow = function(elm)
{
if (this.show_menu_observer)
this.show_menu_observer(this);
this.createLangWindow();
$('body').append(this.language_window);
var pos = $(elm).offset(),
height = $(elm).height(),
width = $(elm).width(),
h = $(this.language_window).height(),
pageheight = $(document).height(),
left = this.change_lang_pic_placement == 'right' ?
pos.left - 100 + width : pos.left + width,
top = pos.top + h < pageheight ? pos.top + height : pos.top - h - 4;
$(this.language_window).css({'top' : top+'px','left' : left+'px'}).show();
this.highlightCurSel();
};
this.deHighlightCurSel = function()
{
$(this.lang_cur_elm).removeClass().addClass('googie_list_onout');
};
this.highlightCurSel = function()
{
if (GOOGIE_CUR_LANG == null)
GOOGIE_CUR_LANG = GOOGIE_DEFAULT_LANG;
for (var i=0; i < this.lang_elms.length; i++) {
if ($(this.lang_elms[i]).attr('googieId') == GOOGIE_CUR_LANG) {
this.lang_elms[i].className = 'googie_list_selected';
this.lang_cur_elm = this.lang_elms[i];
}
else {
this.lang_elms[i].className = 'googie_list_onout';
}
}
};
this.createChangeLangPic = function()
{
var img = $('<img>')
.attr({src: this.img_dir + 'change_lang.gif', 'alt': 'Change language', 'googie_action_btn': '1'}),
switch_lan = document.createElement('span');
ref = this;
$(switch_lan).addClass('googie_lang_3d_on')
.append(img)
.click(function(e) {
var elm = this.tagName.toLowerCase() == 'img' ? this.parentNode : this;
if($(elm).hasClass('googie_lang_3d_click')) {
elm.className = 'googie_lang_3d_on';
ref.hideLangWindow();
}
else {
elm.className = 'googie_lang_3d_click';
ref.showLangWindow(elm);
}
});
return switch_lan;
};
this.createSpellDiv = function()
{
var span = document.createElement('span');
$(span).addClass('googie_check_spelling_link').text(this.lang_chck_spell);
if (this.show_spell_img) {
$(span).append(' ').append($('<img>').attr('src', this.img_dir + 'spellc.gif'));
}
return span;
};
//////
// State functions
/////
this.flashNoSpellingErrorState = function(on_finish)
{
this.setStateChanged('no_error_found');
var ref = this;
if (this.main_controller) {
var no_spell_errors;
if (on_finish) {
var fn = function() {
on_finish();
ref.checkSpellingState();
};
no_spell_errors = fn;
}
else
no_spell_errors = function () { ref.checkSpellingState() };
var rsm = $('<span>').text(this.lang_no_error_found);
$(this.switch_lan_pic).hide();
$(this.spell_span).empty().append(rsm)
.removeClass().addClass('googie_check_spelling_ok');
window.setTimeout(no_spell_errors, 1000);
}
};
this.resumeEditingState = function()
{
this.setStateChanged('resume_editing');
//Change link text to resume
if (this.main_controller) {
var rsm = $('<span>').text(this.lang_rsm_edt);
var ref = this;
$(this.switch_lan_pic).hide();
$(this.spell_span).empty().off().append(rsm)
.click(function() { ref.resumeEditing(); })
.removeClass().addClass('googie_resume_editing');
}
try { this.edit_layer.scrollTop = this.ta_scroll_top; }
catch (e) {};
};
this.checkSpellingState = function(fire)
{
if (fire)
this.setStateChanged('ready');
if (this.show_change_lang_pic)
this.switch_lan_pic = this.createChangeLangPic();
else
this.switch_lan_pic = document.createElement('span');
var span_chck = this.createSpellDiv(),
ref = this;
if (this.custom_spellcheck_starter)
$(span_chck).click(function(e) { ref.custom_spellcheck_starter(); });
else {
$(span_chck).click(function(e) { ref.spellCheck(); });
}
if (this.main_controller) {
if (this.change_lang_pic_placement == 'left') {
$(this.spell_container).empty().append(this.switch_lan_pic).append(' ').append(span_chck);
} else {
$(this.spell_container).empty().append(span_chck).append(' ').append(this.switch_lan_pic);
}
}
this.spell_span = span_chck;
};
//////
// Misc. functions
/////
this.isDefined = function(o)
{
return (o !== undefined && o !== null)
};
this.errorFixed = function()
{
this.cnt_errors_fixed++;
if (this.all_errors_fixed_observer)
if (this.cnt_errors_fixed == this.cnt_errors) {
this.hideErrorWindow();
this.all_errors_fixed_observer();
}
};
this.errorFound = function()
{
this.cnt_errors++;
};
this.createCloseButton = function(c_fn)
{
return this.createButton(this.lang_close, 'googie_list_close', c_fn);
};
this.createButton = function(name, css_class, c_fn)
{
var btn_row = document.createElement('tr'),
btn = document.createElement('td'),
spn_btn;
if (css_class) {
spn_btn = document.createElement('span');
$(spn_btn).addClass(css_class).html(name);
} else {
spn_btn = document.createTextNode(name);
}
$(btn).click(c_fn)
.mouseover(this.item_onmouseover)
.mouseout(this.item_onmouseout);
btn.appendChild(spn_btn);
btn_row.appendChild(btn);
return btn_row;
};
this.removeIndicator = function(elm)
{
//$(this.indicator).remove();
// roundcube mod.
if (window.rcmail)
rcmail.set_busy(false, null, this.rc_msg_id);
};
this.appendIndicator = function(elm)
{
// modified by roundcube
if (window.rcmail)
this.rc_msg_id = rcmail.set_busy(true, 'checking');
/*
this.indicator = document.createElement('img');
$(this.indicator).attr('src', this.img_dir + 'indicator.gif')
.css({'margin-right': '5px', 'text-decoration': 'none'}).width(16).height(16);
if (elm)
$(this.indicator).insertBefore(elm);
else
$('body').append(this.indicator);
*/
}
this.createFocusLink = function(name)
{
var link = document.createElement('a');
$(link).attr({'href': 'javascript:;', 'name': name});
return link;
};
this.item_onmouseover = function(e)
{
if (this.className != 'googie_list_revert' && this.className != 'googie_list_close')
this.className = 'googie_list_onhover';
else
this.parentNode.className = 'googie_list_onhover';
};
this.item_onmouseout = function(e)
{
if (this.className != 'googie_list_revert' && this.className != 'googie_list_close')
this.className = 'googie_list_onout';
else
this.parentNode.className = 'googie_list_onout';
};
};
diff --git a/program/js/list.js b/program/js/list.js
index 75aa1f383..bfe7dd46f 100644
--- a/program/js/list.js
+++ b/program/js/list.js
@@ -1,1963 +1,1963 @@
/**
* Roundcube List Widget
*
* This file is part of the Roundcube Webmail client
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
- * Copyright (c) 2005-2014, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The JavaScript code in this page is free software: you can
* redistribute it and/or modify it under the terms of the GNU
* General Public License (GNU GPL) as published by the Free Software
* Foundation, either version 3 of the License, or (at your option)
* any later version. The code is distributed WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
*
* As additional permission under GNU GPL version 3 section 7, you
* may distribute non-source (e.g., minimized or compacted) forms of
* that code without the copy of the GNU GPL normally required by
* section 4, provided you include this license notice and a URL
* through which recipients can access the Corresponding Source.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Charles McNulty <charles@charlesmcnulty.com>
*
* @requires jquery.js, common.js
*/
/**
* Roundcube List Widget class
* @constructor
*/
function rcube_list_widget(list, p)
{
// static contants
this.ENTER_KEY = 13;
this.DELETE_KEY = 46;
this.BACKSPACE_KEY = 8;
this.list = list ? list : null;
this.tagname = this.list ? this.list.nodeName.toLowerCase() : 'table';
this.id_regexp = /^rcmrow([a-z0-9\-_=\+\/]+)/i;
this.rows = {};
this.selection = [];
this.rowcount = 0;
this.colcount = 0;
this.subject_col = 0;
this.modkey = 0;
this.multiselect = false;
this.multiexpand = false;
this.multi_selecting = false;
this.draggable = false;
this.column_movable = false;
this.keyboard = false;
this.toggleselect = false;
this.aria_listbox = false;
this.parent_focus = true;
this.checkbox_selection = false;
this.drag_active = false;
this.col_drag_active = false;
this.column_fixed = null;
this.last_selected = null;
this.shift_start = null;
this.focused = false;
this.drag_mouse_start = null;
this.dblclick_time = 500; // default value on MS Windows is 500
this.row_init = function(){}; // @deprecated; use list.addEventListener('initrow') instead
this.touch_start_time = 0; // start time of the touch event
this.touch_event_time = 500; // maximum time a touch should be considered a left mouse button event, after this its something else (eg contextmenu event)
// overwrite default paramaters
if (p && typeof p === 'object')
for (var n in p)
this[n] = p[n];
// register this instance
rcube_list_widget._instances.push(this);
};
rcube_list_widget.prototype = {
/**
* get all message rows from HTML table and init each row
*/
init: function()
{
if (this.tagname == 'table' && this.list && this.list.tBodies[0]) {
this.thead = this.list.tHead;
this.tbody = this.list.tBodies[0];
}
else if (this.tagname != 'table' && this.list) {
this.tbody = this.list;
}
if ($(this.list).attr('role') == 'listbox') {
this.aria_listbox = true;
if (this.multiselect)
$(this.list).attr('aria-multiselectable', 'true');
}
var me = this;
if (this.tbody) {
this.rows = {};
this.rowcount = 0;
var r, len, rows = this.tbody.childNodes;
for (r=0, len=rows.length; r<len; r++) {
if (rows[r].nodeType == 1)
this.rowcount += this.init_row(rows[r]) ? 1 : 0;
}
this.init_header();
this.frame = this.list.parentNode;
// set body events
if (this.keyboard) {
rcube_event.add_listener({event:'keydown', object:this, method:'key_press'});
// allow the table element to receive focus.
$(this.list).attr('tabindex', '0')
.on('focus', function(e) { me.focus(e); });
}
}
if (this.parent_focus) {
this.list.parentNode.onclick = function(e) { me.focus(); };
}
rcmail.triggerEvent('initlist', { obj: this.list });
return this;
},
/**
* Init list row and set mouse events on it
*/
init_row: function(row)
{
row.uid = this.get_row_uid(row);
// make references in internal array and set event handlers
if (row && row.uid) {
var self = this, uid = row.uid;
this.rows[uid] = {uid:uid, id:row.id, obj:row};
$(row).data('uid', uid)
// set eventhandlers to table row (only left-button-clicks in mouseup)
.mousedown(function(e) { return self.drag_row(e, this.uid); })
.mouseup(function(e) {
if (e.which == 1 && !self.drag_active && !$(e.currentTarget).is('.ui-droppable-active'))
return self.click_row(e, this.uid);
else
return true;
});
// for IE and Edge differentiate between touch, touch+hold using pointer events rather than touch
if ((bw.ie || bw.edge) && bw.pointer) {
$(row).on('pointerdown', function(e) {
if (e.pointerType == 'touch') {
self.touch_start_time = new Date().getTime();
return false;
}
})
.on('pointerup', function(e) {
if (e.pointerType == 'touch') {
var duration = (new Date().getTime() - self.touch_start_time);
if (duration <= self.touch_event_time) {
self.drag_row(e, this.uid);
return self.click_row(e, this.uid);
}
}
});
}
else if (bw.touch && row.addEventListener) {
row.addEventListener('touchstart', function(e) {
if (e.touches.length == 1) {
self.touchmoved = false;
self.drag_row(rcube_event.touchevent(e.touches[0]), this.uid);
self.touch_start_time = new Date().getTime();
}
}, false);
row.addEventListener('touchend', function(e) {
if (e.changedTouches.length == 1) {
var duration = (new Date().getTime() - self.touch_start_time);
if (!self.touchmoved && duration <= self.touch_event_time && !self.click_row(rcube_event.touchevent(e.changedTouches[0]), this.uid))
e.preventDefault();
}
}, false);
row.addEventListener('touchmove', function(e) {
if (e.changedTouches.length == 1) {
self.touchmoved = true;
if (self.drag_active)
e.preventDefault();
}
}, false);
}
// label the list row with the subject col as descriptive label
if (this.aria_listbox) {
var lbl_id = 'l:' + row.id;
$(row)
.attr('role', 'option')
.attr('aria-labelledby', lbl_id)
.find(this.col_tagname()).eq(this.subject_column()).attr('id', lbl_id);
}
if (document.all)
row.onselectstart = function() { return false; };
this.row_init(this.rows[uid]); // legacy support
this.triggerEvent('initrow', this.rows[uid]);
return true;
}
},
/**
* Init list column headers and set mouse events on them
*/
init_header: function()
{
if (this.thead) {
this.colcount = 0;
if (this.fixed_header) { // copy (modified) fixed header back to the actual table
$(this.list.tHead).replaceWith($(this.fixed_header).find('thead').clone());
$(this.list.tHead).find('th,td').attr('style', '').find('a').attr('tabindex', '-1'); // remove fixed widths
}
else if (!bw.touch && this.list.className.indexOf('fixedheader') >= 0) {
this.init_fixed_header();
}
var col, r, p = this;
// add events for list columns moving
if (this.column_movable && this.thead && this.thead.rows) {
for (r=0; r<this.thead.rows[0].cells.length; r++) {
if (this.column_fixed == r)
continue;
col = this.thead.rows[0].cells[r];
col.onmousedown = function(e) { return p.drag_column(e, this); };
this.colcount++;
}
}
}
},
init_fixed_header: function()
{
var clone = $(this.list.tHead).clone();
if (!this.fixed_header) {
this.fixed_header = $('<table>')
.attr('class', this.list.className + ' fixedcopy')
.attr('role', 'presentation')
.css({ position:'fixed' })
.append(clone)
.append('<tbody></tbody>');
$(this.list).before(this.fixed_header);
var me = this;
$(window).resize(function() { me.resize(); });
$(window).scroll(function() {
var w = $(window);
me.fixed_header.css({
marginLeft: -w.scrollLeft() + 'px',
marginTop: -w.scrollTop() + 'px'
});
});
}
else {
$(this.fixed_header).find('thead').replaceWith(clone);
}
// avoid scrolling header links being focused
$(this.list.tHead).find('a.sortcol').attr('tabindex', '-1');
// set tabindex to fixed header sort links
clone.find('a.sortcol').attr('tabindex', '0');
this.thead = clone.get(0);
this.resize();
},
resize: function()
{
if (!this.fixed_header)
return;
var column_widths = [];
// get column widths from original thead
$(this.tbody).parent().find('thead th,thead td').each(function(index) {
column_widths[index] = $(this).width();
});
// apply fixed widths to fixed table header
$(this.thead).parent().width($(this.tbody).parent().width());
$(this.thead).find('th,td').each(function(index) {
$(this).width(column_widths[index]);
});
$(window).scroll();
},
/**
* Remove all list rows
*/
clear: function(sel)
{
if (this.tagname == 'table') {
var tbody = document.createElement('tbody');
this.list.insertBefore(tbody, this.tbody);
this.list.removeChild(this.list.tBodies[1]);
this.tbody = tbody;
}
else {
$(this.row_tagname() + ':not(.thead)', this.tbody).remove();
}
this.rows = {};
this.rowcount = 0;
this.last_selected = null;
if (sel)
this.clear_selection();
// reset scroll position (in Opera)
if (this.frame)
this.frame.scrollTop = 0;
// fix list header after removing any rows
this.resize();
},
/**
* 'remove' message row from list (just hide it)
*/
remove_row: function(uid, sel_next)
{
var self = this, node = this.rows[uid] ? this.rows[uid].obj : null;
if (!node)
return;
node.style.display = 'none';
// Specify removed row uid in the select_next argument.
// It's needed because after removing a set of rows
// reference to the last selected message is lost.
if (sel_next)
this.select_next(uid);
delete this.rows[uid];
this.rowcount--;
// fix list header after removing any rows
clearTimeout(this.resize_timeout)
this.resize_timeout = setTimeout(function() { self.resize(); }, 50);
},
/**
* Add row to the list and initialize it
*/
insert_row: function(row, before)
{
var self = this, tbody = this.tbody;
// create a real dom node first
if (row.nodeName === undefined) {
// for performance reasons use DOM instead of jQuery here
var i, e, domcell, col,
domrow = document.createElement(this.row_tagname());
if (row.id) domrow.id = row.id;
if (row.uid) domrow.uid = row.uid;
if (row.className) domrow.className = row.className;
if (row.style) $.extend(domrow.style, row.style);
for (i=0; row.cols && i < row.cols.length; i++) {
col = row.cols[i];
domcell = col.dom;
if (!domcell) {
domcell = document.createElement(this.col_tagname());
if (col.className) domcell.className = col.className;
if (col.innerHTML) domcell.innerHTML = col.innerHTML;
for (e in col.events)
domcell['on' + e] = col.events[e];
}
domrow.appendChild(domcell);
}
row = domrow;
}
if (this.checkbox_selection) {
this.insert_checkbox(row);
}
if (before && tbody.childNodes.length)
tbody.insertBefore(row, (typeof before == 'object' && before.parentNode == tbody) ? before : tbody.firstChild);
else
tbody.appendChild(row);
this.init_row(row);
this.rowcount++;
// fix list header after adding any rows
clearTimeout(this.resize_timeout)
this.resize_timeout = setTimeout(function() { self.resize(); }, 50);
},
/**
* Update existing record
*/
update_row: function(id, cols, newid, select)
{
var row = this.rows[id];
if (!row) return false;
var i, domrow = row.obj;
for (i = 0; cols && i < cols.length; i++) {
this.get_cell(domrow, i).html(cols[i]);
}
if (newid) {
delete this.rows[id];
domrow.uid = newid;
domrow.id = 'rcmrow' + newid;
this.init_row(domrow);
if (select)
this.selection[0] = newid;
if (this.last_selected == id)
this.last_selected = newid;
}
},
/**
* Add selection checkbox to the list record
*/
insert_checkbox: function(row)
{
var key, self = this,
cell = document.createElement(this.col_tagname()),
chbox = document.createElement('input');
chbox.type = 'checkbox';
chbox.tabIndex = -1;
chbox.onchange = function(e) {
self.select_row(row.uid, key || CONTROL_KEY, true);
e.stopPropagation();
key = null;
};
chbox.onmousedown = function(e) {
key = rcube_event.get_modifier(e);
};
cell.className = 'selection';
// make the whole cell "touchable" for touch devices
cell.onclick = function(e) {
if (!$(e.target).is('input')) {
key = rcube_event.get_modifier(e);
$(chbox).prop('checked', !chbox.checked).change();
}
e.stopPropagation();
};
cell.appendChild(chbox);
row.insertBefore(cell, row.firstChild);
},
/**
* Enable checkbox selection
*/
enable_checkbox_selection: function()
{
this.checkbox_selection = true;
// Add checkbox to existing records if any
var r, len, cell, row_tag = this.row_tagname().toUpperCase(),
rows = this.tbody.childNodes;
for (r=0, len=rows.length; r<len; r++) {
if (rows[r].nodeName == row_tag && (cell = rows[r].firstChild)) {
if (cell.className == 'selection')
break;
this.insert_checkbox(rows[r]);
}
}
},
/**
* Set focus to the list
*/
focus: function(e)
{
if (this.focused)
return;
this.focused = true;
if (e)
rcube_event.cancel(e);
var focus_elem = null;
if (this.last_selected && this.rows[this.last_selected]) {
focus_elem = $(this.rows[this.last_selected].obj).find(this.col_tagname()).eq(this.subject_column()).attr('tabindex', '0');
}
// Un-focus already focused elements (#1487123, #1487316, #1488600, #1488620)
if (focus_elem && focus_elem.length) {
// We now fix this by explicitly assigning focus to a dedicated link element
this.focus_noscroll(focus_elem);
}
else {
// It looks that window.focus() does the job for all browsers, but not Firefox (#1489058)
$('iframe,:focus:not(body)').blur();
window.focus();
}
$(this.list).addClass('focus').removeAttr('tabindex');
// set internal focus pointer to first row
if (!this.last_selected)
this.select_first(CONTROL_KEY);
},
/**
* remove focus from the list
*/
blur: function(e)
{
this.focused = false;
// avoid the table getting focus right again (on Shift+Tab)
var me = this;
setTimeout(function() { $(me.list).attr('tabindex', '0'); }, 20);
if (this.last_selected && this.rows[this.last_selected]) {
$(this.rows[this.last_selected].obj)
.find(this.col_tagname()).eq(this.subject_column()).removeAttr('tabindex');
}
$(this.list).removeClass('focus');
},
/**
* Focus the given element without scrolling the list container
*/
focus_noscroll: function(elem)
{
var y = this.frame.scrollTop || this.frame.scrollY;
elem.focus();
this.frame.scrollTop = y;
},
/**
* Set/unset the given column as hidden
*/
hide_column: function(col, hide)
{
var method = hide ? 'addClass' : 'removeClass';
if (this.fixed_header)
$(this.row_tagname()+' '+this.col_tagname()+'.'+col, this.fixed_header)[method]('hidden');
$(this.row_tagname()+' '+this.col_tagname()+'.'+col, this.list)[method]('hidden');
},
/**
* onmousedown-handler of message list column
*/
drag_column: function(e, col)
{
if (this.colcount > 1) {
this.drag_start = true;
this.drag_mouse_start = rcube_event.get_mouse_pos(e);
rcube_event.add_listener({event:'mousemove', object:this, method:'column_drag_mouse_move'});
rcube_event.add_listener({event:'mouseup', object:this, method:'column_drag_mouse_up'});
// enable dragging over iframes
this.add_dragfix();
// find selected column number
for (var i=0; i<this.thead.rows[0].cells.length; i++) {
if (col == this.thead.rows[0].cells[i]) {
this.selected_column = i;
break;
}
}
}
return false;
},
/**
* onmousedown-handler of message list row
*/
drag_row: function(e, id)
{
// don't do anything (another action processed before)
if (!this.is_event_target(e))
return true;
// accept right-clicks
if (rcube_event.get_button(e) == 2)
return true;
this.in_selection_before = e && e.istouch || this.in_selection(id) ? id : false;
// selects currently unselected row
if (!this.in_selection_before) {
var mod_key = rcube_event.get_modifier(e);
this.select_row(id, mod_key, true);
}
if (this.draggable && this.selection.length && this.in_selection(id)) {
this.drag_start = true;
this.drag_mouse_start = rcube_event.get_mouse_pos(e);
rcube_event.add_listener({event:'mousemove', object:this, method:'drag_mouse_move'});
rcube_event.add_listener({event:'mouseup', object:this, method:'drag_mouse_up'});
if (bw.touch) {
rcube_event.add_listener({event:'touchmove', object:this, method:'drag_mouse_move'});
rcube_event.add_listener({event:'touchend', object:this, method:'drag_mouse_up'});
}
// enable dragging over iframes
this.add_dragfix();
this.focus();
}
return false;
},
/**
* onmouseup-handler of message list row
*/
click_row: function(e, id)
{
// sanity check
if (!id || !this.rows[id])
return false;
// don't do anything (another action processed before)
if (!this.is_event_target(e))
return true;
var now = new Date().getTime(),
dblclicked = now - this.rows[id].clicked < this.dblclick_time;
// unselects currently selected row
if (!this.drag_active && !dblclicked && this.in_selection_before == id)
this.select_row(id, rcube_event.get_modifier(e), true);
this.drag_start = false;
this.in_selection_before = false;
// row was double clicked
if (this.rowcount && dblclicked && this.in_selection(id)) {
this.triggerEvent('dblclick');
now = 0;
}
else
this.triggerEvent('click');
if (!this.drag_active) {
// remove temp divs
this.del_dragfix();
rcube_event.cancel(e);
}
this.rows[id].clicked = now;
this.focus();
return false;
},
/**
* Check target of the current event
*/
is_event_target: function(e)
{
var target = rcube_event.get_target(e),
tagname = target.tagName.toLowerCase();
return !(target && (tagname == 'input' || tagname == 'img' || (tagname != 'a' && target.onclick) || $(target).data('action-link')));
},
/*
* Returns thread root ID for specified row ID
*/
find_root: function(uid)
{
var r = this.rows[uid];
if (r && r.parent_uid)
return this.find_root(r.parent_uid);
else
return uid;
},
expand_row: function(e, id)
{
var row = this.rows[id],
evtarget = rcube_event.get_target(e),
mod_key = rcube_event.get_modifier(e);
// Don't treat double click on the expando as double click on the message.
row.clicked = 0;
if (row.expanded) {
evtarget.className = 'collapsed';
if (mod_key == CONTROL_KEY || this.multiexpand)
this.collapse_all(row);
else
this.collapse(row);
}
else {
evtarget.className = 'expanded';
if (mod_key == CONTROL_KEY || this.multiexpand)
this.expand_all(row);
else
this.expand(row);
}
},
collapse: function(row)
{
var r, depth = row.depth,
new_row = row ? row.obj.nextSibling : null;
row.expanded = false;
this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
while (new_row) {
if (new_row.nodeType == 1) {
r = this.rows[new_row.uid];
if (r && r.depth <= depth)
break;
$(new_row).css('display', 'none');
if (r.expanded) {
r.expanded = false;
this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded, obj:new_row });
}
}
new_row = new_row.nextSibling;
}
this.resize();
this.triggerEvent('listupdate');
return false;
},
expand: function(row)
{
var r, p, depth, new_row, last_expanded_parent_depth;
if (row) {
row.expanded = true;
depth = row.depth;
new_row = row.obj.nextSibling;
this.update_expando(row.id, true);
this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
}
else {
var tbody = this.tbody;
new_row = tbody.firstChild;
depth = 0;
last_expanded_parent_depth = 0;
}
while (new_row) {
if (new_row.nodeType == 1) {
r = this.rows[new_row.uid];
if (r) {
if (row && (!r.depth || r.depth <= depth))
break;
if (r.parent_uid) {
p = this.rows[r.parent_uid];
if (p && p.expanded) {
if ((row && p == row) || last_expanded_parent_depth >= p.depth - 1) {
last_expanded_parent_depth = p.depth;
$(new_row).css('display', '');
r.expanded = true;
this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded, obj:new_row });
}
}
else
if (row && (! p || p.depth <= depth))
break;
}
}
}
new_row = new_row.nextSibling;
}
this.resize();
this.triggerEvent('listupdate');
return false;
},
collapse_all: function(row)
{
var depth, new_row, r;
if (row) {
row.expanded = false;
depth = row.depth;
new_row = row.obj.nextSibling;
this.update_expando(row.id);
this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
// don't collapse sub-root tree in multiexpand mode
if (depth && this.multiexpand)
return false;
}
else {
new_row = this.tbody.firstChild;
depth = 0;
}
while (new_row) {
if (new_row.nodeType == 1) {
if (r = this.rows[new_row.uid]) {
if (row && (!r.depth || r.depth <= depth))
break;
if (row || r.depth)
$(new_row).css('display', 'none');
if (r.has_children && r.expanded) {
r.expanded = false;
this.update_expando(r.id, false);
this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded, obj:new_row });
}
}
}
new_row = new_row.nextSibling;
}
this.resize();
this.triggerEvent('listupdate');
return false;
},
expand_all: function(row)
{
var depth, new_row, r;
if (row) {
row.expanded = true;
depth = row.depth;
new_row = row.obj.nextSibling;
this.update_expando(row.id, true);
this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
}
else {
new_row = this.tbody.firstChild;
depth = 0;
}
while (new_row) {
if (new_row.nodeType == 1) {
if (r = this.rows[new_row.uid]) {
if (row && r.depth <= depth)
break;
$(new_row).css('display', '');
if (r.has_children && !r.expanded) {
r.expanded = true;
this.update_expando(r.id, true);
this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded, obj:new_row });
}
}
}
new_row = new_row.nextSibling;
}
this.resize();
this.triggerEvent('listupdate');
return false;
},
update_expando: function(id, expanded)
{
var expando = document.getElementById('rcmexpando' + id);
if (expando)
expando.className = expanded ? 'expanded' : 'collapsed';
},
get_row_uid: function(row)
{
if (!row)
return;
if (!row.uid) {
var uid = $(row).data('uid');
if (uid)
row.uid = uid;
else if (String(row.id).match(this.id_regexp))
row.uid = RegExp.$1;
}
return row.uid;
},
/**
* get first/next/previous/last rows that are not hidden
*/
get_next_row: function(uid)
{
if (!this.rowcount)
return false;
var last_selected_row = this.rows[uid || this.last_selected],
new_row = last_selected_row ? last_selected_row.obj.nextSibling : null;
while (new_row && (new_row.nodeType != 1 || new_row.style.display == 'none'))
new_row = new_row.nextSibling;
return new_row;
},
get_prev_row: function(uid)
{
if (!this.rowcount)
return false;
var last_selected_row = this.rows[uid || this.last_selected],
new_row = last_selected_row ? last_selected_row.obj.previousSibling : null;
while (new_row && (new_row.nodeType != 1 || new_row.style.display == 'none'))
new_row = new_row.previousSibling;
return new_row;
},
get_first_row: function()
{
if (this.rowcount) {
var i, uid, rows = this.tbody.childNodes;
for (i=0; i<rows.length; i++)
if (rows[i].id && (uid = this.get_row_uid(rows[i])) && this.rows[uid])
return uid;
}
return null;
},
get_last_row: function()
{
if (this.rowcount) {
var i, uid, rows = this.tbody.childNodes;
for (i=rows.length-1; i>=0; i--)
if (rows[i].id && (uid = this.get_row_uid(rows[i])) && this.rows[uid])
return uid;
}
return null;
},
get_next: function()
{
var row;
if (row = this.get_next_row()) {
return row.uid;
}
},
get_prev: function()
{
var row;
if (row = this.get_prev_row()) {
return row.uid;
}
},
row_tagname: function()
{
var row_tagnames = { table:'tr', ul:'li', '*':'div' };
return row_tagnames[this.tagname] || row_tagnames['*'];
},
col_tagname: function()
{
var col_tagnames = { table:'td', '*':'span' };
return col_tagnames[this.tagname] || col_tagnames['*'];
},
get_cell: function(row, index)
{
return $(this.col_tagname(), row).eq(index + (this.checkbox_selection ? 1 : 0));
},
/**
* selects or unselects the proper row depending on the modifier key pressed
*/
select_row: function(id, mod_key, with_mouse)
{
var select_before = this.selection.join(','),
in_selection_before = this.in_selection(id);
if (!this.multiselect && with_mouse)
mod_key = 0;
if (!this.shift_start)
this.shift_start = id
if (!mod_key) {
this.shift_start = id;
this.highlight_row(id, false);
this.multi_selecting = false;
}
else {
switch (mod_key) {
case SHIFT_KEY:
this.shift_select(id, false);
break;
case CONTROL_KEY:
if (with_mouse) {
this.shift_start = id;
this.highlight_row(id, true);
}
break;
case CONTROL_SHIFT_KEY:
this.shift_select(id, true);
break;
default:
this.highlight_row(id, false);
break;
}
this.multi_selecting = true;
}
if (this.last_selected && this.rows[this.last_selected]) {
$(this.rows[this.last_selected].obj).removeClass('focused')
.find(this.col_tagname()).eq(this.subject_column()).removeAttr('tabindex');
}
// unselect if toggleselect is active and the same row was clicked again
if (this.toggleselect && in_selection_before && !mod_key) {
this.clear_selection();
}
// trigger event if selection changed
else if (this.selection.join(',') != select_before) {
this.triggerEvent('select');
}
if (this.rows[id]) {
$(this.rows[id].obj).addClass('focused');
// set cursor focus to link inside selected row
if (this.focused)
this.focus_noscroll($(this.rows[id].obj).find(this.col_tagname()).eq(this.subject_column()).attr('tabindex', '0'));
}
if (!this.selection.length)
this.shift_start = null;
this.last_selected = id;
},
/**
* Alias method for select_row
*/
select: function(id)
{
this.select_row(id, false);
this.scrollto(id);
},
/**
* Select row next to the specified or last selected one
* Either below or above.
*/
select_next: function(uid)
{
var new_row = this.get_next_row(uid) || this.get_prev_row(uid);
if (new_row)
this.select_row(new_row.uid, false, false);
},
/**
* Select first row
*/
select_first: function(mod_key)
{
var row = this.get_first_row();
if (row) {
this.select_row(row, mod_key, false);
this.scrollto(row);
}
},
/**
* Select last row
*/
select_last: function(mod_key)
{
var row = this.get_last_row();
if (row) {
this.select_row(row, mod_key, false);
this.scrollto(row);
}
},
/**
* Add all childs of the given row to selection
*/
select_children: function(uid)
{
var i, children = this.row_children(uid), len = children.length;
for (i=0; i<len; i++)
if (!this.in_selection(children[i]))
this.select_row(children[i], CONTROL_KEY, true);
},
/**
* Perform selection when shift key is pressed
*/
shift_select: function(id, control)
{
if (!this.rows[this.shift_start] || !this.selection.length)
this.shift_start = id;
var n, i, j, to_row = this.rows[id],
from_rowIndex = this._rowIndex(this.rows[this.shift_start].obj),
to_rowIndex = this._rowIndex(to_row.obj);
// if we're going down the list, and we hit a thread, and it's closed, select the whole thread
if (from_rowIndex < to_rowIndex && !to_row.expanded && to_row.has_children)
if (to_row = this.rows[(this.row_children(id)).pop()])
to_rowIndex = this._rowIndex(to_row.obj);
i = ((from_rowIndex < to_rowIndex) ? from_rowIndex : to_rowIndex),
j = ((from_rowIndex > to_rowIndex) ? from_rowIndex : to_rowIndex);
// iterate through the entire message list
for (n in this.rows) {
if (this._rowIndex(this.rows[n].obj) >= i && this._rowIndex(this.rows[n].obj) <= j) {
if (!this.in_selection(n)) {
this.highlight_row(n, true);
}
}
else {
if (this.in_selection(n) && !control) {
this.highlight_row(n, true);
}
}
}
},
/**
* Helper method to emulate the rowIndex property of non-tr elements
*/
_rowIndex: function(obj)
{
return (obj.rowIndex !== undefined) ? obj.rowIndex : $(obj).prevAll().length;
},
/**
* Check if given id is part of the current selection
*/
in_selection: function(id, index)
{
for (var n in this.selection)
if (this.selection[n] == id)
return index ? parseInt(n) : true;
return false;
},
/**
* Select each row in list
*/
select_all: function(filter)
{
if (!this.rowcount)
return false;
// reset but remember selection first
var n, select_before = this.selection.join(',');
this.selection = [];
for (n in this.rows) {
if (!filter || this.rows[n][filter] == true) {
this.last_selected = n;
this.highlight_row(n, true, true);
}
else {
$(this.rows[n].obj).removeClass('selected').removeAttr('aria-selected');
}
}
// trigger event if selection changed
if (this.selection.join(',') != select_before)
this.triggerEvent('select');
this.focus();
return true;
},
/**
* Invert selection
*/
invert_selection: function()
{
if (!this.rowcount)
return false;
// remember old selection
var n, select_before = this.selection.join(',');
for (n in this.rows)
this.highlight_row(n, true);
// trigger event if selection changed
if (this.selection.join(',') != select_before)
this.triggerEvent('select');
this.focus();
return true;
},
/**
* Unselect selected row(s)
*/
clear_selection: function(id, no_event)
{
var n, num_select = this.selection.length;
// one row
if (id) {
for (n in this.selection)
if (this.selection[n] == id) {
this.selection.splice(n,1);
break;
}
}
// all rows
else {
for (n in this.selection)
if (this.rows[this.selection[n]]) {
$(this.rows[this.selection[n]].obj).removeClass('selected').removeAttr('aria-selected');
}
this.selection = [];
}
if (this.checkbox_selection)
$(this.row_tagname() + ':not(.selected) > .selection > input:checked', this.list).prop('checked', false);
if (num_select && !this.selection.length && !no_event) {
this.triggerEvent('select');
this.last_selected = null;
}
},
/**
* Getter for the selection array
*/
get_selection: function(deep)
{
var res = $.merge([], this.selection);
var props = {deep: deep, res: res};
if (this.triggerEvent('getselection', props) === false)
return props.res;
// return children of selected threads even if only root is selected
if (deep !== false && res.length) {
for (var uid, uids, i=0, len=res.length; i<len; i++) {
uid = res[i];
if (this.rows[uid] && this.rows[uid].has_children && !this.rows[uid].expanded) {
uids = this.row_children(uid);
for (var j=0, uids_len=uids.length; j<uids_len; j++) {
uid = uids[j];
if (!this.in_selection(uid))
res.push(uid);
}
}
}
}
return res;
},
/**
* Return the ID if only one row is selected
*/
get_single_selection: function()
{
var selection = this.get_selection(false);
if (selection.length == 1)
return selection[0];
else
return null;
},
/**
* Highlight/unhighlight a row
*/
highlight_row: function(id, multiple, norecur)
{
if (!this.rows[id])
return;
if (!multiple) {
if (this.selection.length > 1 || !this.in_selection(id)) {
this.clear_selection(null, true);
this.selection[0] = id;
$(this.rows[id].obj).addClass('selected').attr('aria-selected', 'true');
if (this.checkbox_selection)
$('.selection > input', this.rows[id].obj).prop('checked', true);
}
}
else {
var pre, post, p = this.in_selection(id, true);
if (p === false) { // select row
this.selection.push(id);
$(this.rows[id].obj).addClass('selected').attr('aria-selected', 'true');
if (this.checkbox_selection)
$('.selection > input', this.rows[id].obj).prop('checked', true);
if (!norecur && !this.rows[id].expanded)
this.highlight_children(id, true);
}
else { // unselect row
pre = this.selection.slice(0, p);
post = this.selection.slice(p+1, this.selection.length);
this.selection = pre.concat(post);
$(this.rows[id].obj).removeClass('selected').removeAttr('aria-selected');
if (this.checkbox_selection)
$('.selection > input', this.rows[id].obj).prop('checked', false);
if (!norecur && !this.rows[id].expanded)
this.highlight_children(id, false);
}
}
},
/**
* Highlight/unhighlight all childs of the given row
*/
highlight_children: function(id, status)
{
var i, selected,
children = this.row_children(id), len = children.length;
for (i=0; i<len; i++) {
selected = this.in_selection(children[i]);
if ((status && !selected) || (!status && selected))
this.highlight_row(children[i], true, true);
}
},
/**
* Handler for keyboard events
*/
key_press: function(e)
{
if (!this.focused || $(e.target).is('input,textarea,select'))
return true;
var keyCode = rcube_event.get_keycode(e),
mod_key = rcube_event.get_modifier(e);
switch (keyCode) {
case 40:
case 38:
case 63233: // "down", in safari keypress
case 63232: // "up", in safari keypress
// Stop propagation so that the browser doesn't scroll
rcube_event.cancel(e);
return this.use_arrow_key(keyCode, mod_key);
case 32:
rcube_event.cancel(e);
return this.select_row(this.last_selected, mod_key, true);
case 37: // Left arrow key
case 39: // Right arrow key
// Stop propagation
rcube_event.cancel(e);
var ret = this.use_arrow_key(keyCode, mod_key);
this.key_pressed = keyCode;
this.modkey = mod_key;
this.triggerEvent('keypress');
this.modkey = 0;
return ret;
case 36: // Home
this.select_first(mod_key);
return rcube_event.cancel(e);
case 35: // End
this.select_last(mod_key);
return rcube_event.cancel(e);
case 27:
if (this.drag_active)
return this.drag_mouse_up(e);
if (this.col_drag_active) {
this.selected_column = null;
return this.column_drag_mouse_up(e);
}
return rcube_event.cancel(e);
case 9: // Tab
this.blur();
break;
case 13: // Enter
if (!this.selection.length)
this.select_row(this.last_selected, mod_key, false);
default:
this.key_pressed = keyCode;
this.modkey = mod_key;
this.triggerEvent('keypress');
this.modkey = 0;
if (this.key_pressed == this.BACKSPACE_KEY)
return rcube_event.cancel(e);
}
return true;
},
/**
* Special handling method for arrow keys
*/
use_arrow_key: function(keyCode, mod_key)
{
var new_row,
selected_row = this.rows[this.last_selected];
// Safari uses the nonstandard keycodes 63232/63233 for up/down, if we're
// using the keypress event (but not the keydown or keyup event).
if (keyCode == 40 || keyCode == 63233) // down arrow key pressed
new_row = this.get_next_row();
else if (keyCode == 38 || keyCode == 63232) // up arrow key pressed
new_row = this.get_prev_row();
else {
if (!selected_row || !selected_row.has_children)
return;
// expand
if (keyCode == 39) {
if (selected_row.expanded)
return;
if (mod_key == CONTROL_KEY || this.multiexpand)
this.expand_all(selected_row);
else
this.expand(selected_row);
}
// collapse
else {
if (!selected_row.expanded)
return;
if (mod_key == CONTROL_KEY || this.multiexpand)
this.collapse_all(selected_row);
else
this.collapse(selected_row);
}
this.update_expando(selected_row.id, selected_row.expanded);
return false;
}
if (new_row) {
// simulate ctr-key if no rows are selected
if (!mod_key && !this.selection.length)
mod_key = CONTROL_KEY;
this.select_row(new_row.uid, mod_key, false);
this.scrollto(new_row.uid);
}
else if (!new_row && !selected_row) {
// select the first row if none selected yet
this.select_first(CONTROL_KEY);
}
return false;
},
/**
* Try to scroll the list to make the specified row visible
*/
scrollto: function(id)
{
var row = this.rows[id] ? this.rows[id].obj : null;
if (row && this.frame) {
var scroll_to = Number(row.offsetTop),
head_offset = 0;
// expand thread if target row is hidden (collapsed)
if (!scroll_to && this.rows[id].parent_uid) {
var parent = this.find_root(this.rows[id].uid);
this.expand_all(this.rows[parent]);
scroll_to = Number(row.offsetTop);
}
if (this.fixed_header)
head_offset = Number(this.thead.offsetHeight);
// if row is above the frame (or behind header)
if (scroll_to < Number(this.frame.scrollTop) + head_offset) {
// scroll window so that row isn't behind header
this.frame.scrollTop = scroll_to - head_offset;
}
else if (scroll_to + Number(row.offsetHeight) > Number(this.frame.scrollTop) + Number(this.frame.offsetHeight))
this.frame.scrollTop = (scroll_to + Number(row.offsetHeight)) - Number(this.frame.offsetHeight);
}
},
/**
* Handler for mouse move events
*/
drag_mouse_move: function(e)
{
// convert touch event
if (e.type == 'touchmove') {
if (e.touches.length == 1 && e.changedTouches.length == 1)
e = rcube_event.touchevent(e.changedTouches[0]);
else
return rcube_event.cancel(e);
}
if (this.drag_start) {
// check mouse movement, of less than 3 pixels, don't start dragging
var m = rcube_event.get_mouse_pos(e),
limit = 10, selection = [], self = this;
if (!this.drag_mouse_start || (Math.abs(m.x - this.drag_mouse_start.x) < 3 && Math.abs(m.y - this.drag_mouse_start.y) < 3))
return false;
// remember dragging start position
this.drag_start_pos = {left: m.x, top: m.y};
// initialize drag layer
if (!this.draglayer)
this.draglayer = $('<div>').attr('id', 'rcmdraglayer')
.css({position: 'absolute', display: 'none', 'z-index': 2000})
.appendTo(document.body);
else
this.draglayer.html('');
// get selected rows (in display order), don't use this.selection here
$(this.row_tagname() + '.selected', this.tbody).each(function() {
var uid = self.get_row_uid(this), row = self.rows[uid];
if (!row || $.inArray(uid, selection) > -1)
return;
selection.push(uid);
// also handle children of (collapsed) trees for dragging (they might be not selected)
if (row.has_children && !row.expanded)
$.each(self.row_children(uid), function() {
if ($.inArray(this, selection) > -1)
return;
selection.push(this);
});
// break the loop asap
if (selection.length > limit + 1)
return false;
});
// append subject (of every row up to the limit) to the drag layer
$.each(selection, function(i, uid) {
if (i > limit) {
self.draglayer.append($('<div>').text('...'));
return false;
}
var subject_col = self.subject_column();
$('> ' + self.col_tagname(), self.rows[uid].obj).each(function(n, cell) {
if (subject_col < 0 || (subject_col >= 0 && subject_col == n)) {
// remove elements marked with "skip-on-drag" class
cell = $(cell).clone();
$(cell).find('.skip-on-drag').remove();
var subject = cell.text();
if (subject) {
// remove leading spaces
subject = $.trim(subject);
// truncate line to 50 characters
subject = (subject.length > 50 ? subject.substring(0, 50) + '...' : subject);
self.draglayer.append($('<div>').text(subject));
return false;
}
}
});
});
this.draglayer.show();
this.drag_active = true;
this.triggerEvent('dragstart');
}
if (this.drag_active && this.draglayer) {
var pos = rcube_event.get_mouse_pos(e);
this.draglayer.css({ left:(pos.x+20)+'px', top:(pos.y-5 + (bw.ie ? document.documentElement.scrollTop : 0))+'px' });
this.triggerEvent('dragmove', e?e:window.event);
}
this.drag_start = false;
return false;
},
/**
* Handler for mouse up events
*/
drag_mouse_up: function(e)
{
document.onmousemove = null;
if (e.type == 'touchend') {
if (e.changedTouches.length != 1)
return rcube_event.cancel(e);
}
if (this.draglayer && this.draglayer.is(':visible')) {
if (this.drag_start_pos)
this.draglayer.animate(this.drag_start_pos, 300, 'swing').hide(20);
else
this.draglayer.hide();
}
if (this.drag_active)
this.focus();
this.drag_active = false;
rcube_event.remove_listener({event:'mousemove', object:this, method:'drag_mouse_move'});
rcube_event.remove_listener({event:'mouseup', object:this, method:'drag_mouse_up'});
if (bw.touch) {
rcube_event.remove_listener({event:'touchmove', object:this, method:'drag_mouse_move'});
rcube_event.remove_listener({event:'touchend', object:this, method:'drag_mouse_up'});
}
// remove temp divs
this.del_dragfix();
this.triggerEvent('dragend', e);
return rcube_event.cancel(e);
},
/**
* Handler for mouse move events for dragging list column
*/
column_drag_mouse_move: function(e)
{
if (this.drag_start) {
// check mouse movement, of less than 3 pixels, don't start dragging
var i, m = rcube_event.get_mouse_pos(e);
if (!this.drag_mouse_start || (Math.abs(m.x - this.drag_mouse_start.x) < 3 && Math.abs(m.y - this.drag_mouse_start.y) < 3))
return false;
if (!this.col_draglayer) {
var lpos = $(this.list).offset(),
cells = this.thead.rows[0].cells;
// fix layer position when list is scrolled
lpos.top += this.list.scrollTop + this.list.parentNode.scrollTop;
// create dragging layer
this.col_draglayer = $('<div>').attr('id', 'rcmcoldraglayer')
.css(lpos).css({ position:'absolute', 'z-index':2001,
'background-color':'white', opacity:0.75,
height: (this.frame.offsetHeight-2)+'px', width: (this.frame.offsetWidth-2)+'px' })
.appendTo(document.body)
// ... and column position indicator
.append($('<div>').attr('id', 'rcmcolumnindicator')
.css({ position:'absolute', 'border-right':'2px dotted #555',
'z-index':2002, height: (this.frame.offsetHeight-2)+'px' }));
this.cols = [];
this.list_pos = this.list_min_pos = lpos.left;
// save columns positions
for (i=0; i<cells.length; i++) {
this.cols[i] = cells[i].offsetWidth;
if (this.column_fixed !== null && i <= this.column_fixed) {
this.list_min_pos += this.cols[i];
}
}
}
this.col_draglayer.show();
this.col_drag_active = true;
this.triggerEvent('column_dragstart');
}
// set column indicator position
if (this.col_drag_active && this.col_draglayer) {
var i, cpos = 0, pos = rcube_event.get_mouse_pos(e);
for (i=0; i<this.cols.length; i++) {
if (pos.x >= this.cols[i]/2 + this.list_pos + cpos)
cpos += this.cols[i];
else
break;
}
// handle fixed columns on left
if (i == 0 && this.list_min_pos > pos.x)
cpos = this.list_min_pos - this.list_pos;
// empty list needs some assignment
else if (!this.list.rowcount && i == this.cols.length)
cpos -= 2;
$('#rcmcolumnindicator').css({ width: cpos+'px'});
this.triggerEvent('column_dragmove', e?e:window.event);
}
this.drag_start = false;
return false;
},
/**
* Handler for mouse up events for dragging list columns
*/
column_drag_mouse_up: function(e)
{
document.onmousemove = null;
if (this.col_draglayer) {
(this.col_draglayer).remove();
this.col_draglayer = null;
}
rcube_event.remove_listener({event:'mousemove', object:this, method:'column_drag_mouse_move'});
rcube_event.remove_listener({event:'mouseup', object:this, method:'column_drag_mouse_up'});
// remove temp divs
this.del_dragfix();
if (this.col_drag_active) {
this.col_drag_active = false;
this.focus();
this.triggerEvent('column_dragend', e);
if (this.selected_column !== null && this.cols && this.cols.length) {
var i, cpos = 0, pos = rcube_event.get_mouse_pos(e);
// find destination position
for (i=0; i<this.cols.length; i++) {
if (pos.x >= this.cols[i]/2 + this.list_pos + cpos)
cpos += this.cols[i];
else
break;
}
if (i != this.selected_column && i != this.selected_column+1) {
this.column_replace(this.selected_column, i);
}
}
}
return rcube_event.cancel(e);
},
/**
* Returns IDs of all rows in a thread (except root) for specified root
*/
row_children: function(uid)
{
if (!this.rows[uid] || !this.rows[uid].has_children)
return [];
var res = [], depth = this.rows[uid].depth,
row = this.rows[uid].obj.nextSibling;
while (row) {
if (row.nodeType == 1) {
if (r = this.rows[row.uid]) {
if (!r.depth || r.depth <= depth)
break;
res.push(r.uid);
}
}
row = row.nextSibling;
}
return res;
},
/**
* Creates a layer for drag&drop over iframes
*/
add_dragfix: function()
{
$('iframe').each(function() {
$('<div class="iframe-dragdrop-fix"></div>')
.css({background: '#fff',
width: this.offsetWidth+'px', height: this.offsetHeight+'px',
position: 'absolute', opacity: '0.001', zIndex: 1000
})
.css($(this).offset())
.appendTo(document.body);
});
},
/**
* Removes the layer for drag&drop over iframes
*/
del_dragfix: function()
{
$('div.iframe-dragdrop-fix').remove();
},
/**
* Replaces two columns
*/
column_replace: function(from, to)
{
// only supported for <table> lists
if (!this.thead || !this.thead.rows)
return;
var len, cells = this.thead.rows[0].cells,
elem = cells[from],
before = cells[to],
td = document.createElement('td');
// replace header cells
if (before)
cells[0].parentNode.insertBefore(td, before);
else
cells[0].parentNode.appendChild(td);
cells[0].parentNode.replaceChild(elem, td);
// replace list cells
for (r=0, len=this.tbody.rows.length; r<len; r++) {
row = this.tbody.rows[r];
elem = row.cells[from];
before = row.cells[to];
td = document.createElement('td');
if (before)
row.insertBefore(td, before);
else
row.appendChild(td);
row.replaceChild(elem, td);
}
// update subject column position
if (this.subject_col == from)
this.subject_col = to > from ? to - 1 : to;
else if (this.subject_col < from && to <= this.subject_col)
this.subject_col++;
else if (this.subject_col > from && to >= this.subject_col)
this.subject_col--;
if (this.fixed_header)
this.init_header();
this.triggerEvent('column_replace');
},
subject_column: function()
{
return this.subject_col + (this.checkbox_selection ? 1 : 0);
}
};
rcube_list_widget.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;
rcube_list_widget.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener;
rcube_list_widget.prototype.triggerEvent = rcube_event_engine.prototype.triggerEvent;
// static
rcube_list_widget._instances = [];
diff --git a/program/js/treelist.js b/program/js/treelist.js
index e079f2cb1..dfaf87472 100644
--- a/program/js/treelist.js
+++ b/program/js/treelist.js
@@ -1,1260 +1,1260 @@
/**
* Roundcube Treelist Widget
*
* This file is part of the Roundcube Webmail client
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
- * Copyright (c) 2013-2014, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The JavaScript code in this page is free software: you can
* redistribute it and/or modify it under the terms of the GNU
* General Public License (GNU GPL) as published by the Free Software
* Foundation, either version 3 of the License, or (at your option)
* any later version. The code is distributed WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
*
* As additional permission under GNU GPL version 3 section 7, you
* may distribute non-source (e.g., minimized or compacted) forms of
* that code without the copy of the GNU GPL normally required by
* section 4, provided you include this license notice and a URL
* through which recipients can access the Corresponding Source.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*
* @author Thomas Bruederli <roundcube@gmail.com>
* @requires jquery.js, common.js
*/
/**
* Roundcube Treelist widget class
* @constructor
*/
function rcube_treelist_widget(node, p)
{
// apply some defaults to p
p = $.extend({
id_prefix: '',
autoexpand: 1000,
selectable: false,
scroll_delay: 500,
scroll_step: 5,
scroll_speed: 20,
save_state: false,
keyboard: true,
tabexit: true,
parent_focus: false,
check_droptarget: function(node) { return !node.virtual; }
}, p || {});
var container = $(node),
data = p.data || [],
indexbyid = {},
selection = null,
drag_active = false,
search_active = false,
last_search = '',
has_focus = false,
box_coords = {},
item_coords = [],
autoexpand_timer,
autoexpand_item,
body_scroll_top = 0,
list_scroll_top = 0,
scroll_timer,
searchfield,
tree_state,
ui_droppable,
ui_draggable,
draggable_opts,
droppable_opts,
list_id = (container.attr('id') || p.id_prefix || '0'),
me = this;
/////// export public members and methods
this.container = container;
this.expand = expand;
this.collapse = collapse;
this.select = select;
this.render = render;
this.reset = reset;
this.drag_start = drag_start;
this.drag_end = drag_end;
this.intersects = intersects;
this.droppable = droppable;
this.draggable = draggable;
this.is_draggable = is_draggable;
this.update = update_node;
this.insert = insert;
this.remove = remove;
this.get_item = get_item;
this.get_node = get_node;
this.get_selection = get_selection;
this.get_next = get_next;
this.get_prev = get_prev;
this.get_single_selection = get_selection;
this.is_search = is_search;
this.reset_search = reset_search;
/////// startup code (constructor)
// abort if node not found
if (!container.length)
return;
if (p.data)
index_data({ children:data });
// load data from DOM
else
update_data();
// scroll to the selected item
if (selection) {
scroll_to_node(id2dom(selection, true));
}
container.attr('role', 'tree')
.on('focusin', function(e) {
// TODO: only accept focus on virtual nodes from keyboard events
has_focus = true;
})
.on('focusout', function(e) {
has_focus = false;
})
// register click handlers on list
.on('click', 'div.treetoggle', function(e) {
toggle(dom2id($(this).parent()));
e.stopPropagation();
})
.on('click', 'li', function(e) {
// do not select record on checkbox/input click
if ($(e.target).is('input'))
return true;
var node = p.selectable ? indexbyid[dom2id($(this))] : null;
if (node && !node.virtual) {
select(node.id);
e.stopPropagation();
}
})
// mute clicks on virtual folder links (they need tabindex="0" in order to be selectable by keyboard)
.on('mousedown', 'a', function(e) {
var link = $(e.target), node = indexbyid[dom2id(link.closest('li'))];
if (node && node.virtual && !link.attr('href')) {
e.preventDefault();
e.stopPropagation();
return false;
}
});
// activate search function
if (p.searchbox) {
searchfield = $(p.searchbox).off('keyup.treelist').on('keyup.treelist', function(e) {
var key = rcube_event.get_keycode(e),
mod = rcube_event.get_modifier(e);
switch (key) {
case 9: // tab
break;
case 13: // enter
search(this.value, true);
return rcube_event.cancel(e);
case 27: // escape
reset_search();
break;
case 38: // arrow up
case 37: // left
case 39: // right
case 40: // arrow down
return; // ignore arrow keys
default:
search(this.value, false);
break;
}
}).attr('autocomplete', 'off');
// find the reset button for this search field
searchfield.parent().find('a.reset').off('click.treelist').on('click.treelist', function(e) {
reset_search();
return false;
})
}
$(document.body).on('keydown', keypress);
// catch focus when clicking the list container area
if (p.parent_focus) {
container.parent(':not(body)').click(function(e) {
// click on a checkbox does not catch the focus
if ($(e.target).is('input'))
return true;
if (!has_focus && selection) {
$(get_item(selection)).find(':focusable').first().focus();
}
else if (!has_focus) {
container.children('li:has(:focusable)').first().find(':focusable').first().focus();
}
});
}
/////// private methods
/**
* Collaps a the node with the given ID
*/
function collapse(id, recursive, set)
{
var node;
if (node = indexbyid[id]) {
node.collapsed = typeof set == 'undefined' || set;
update_dom(node);
if (recursive && node.children) {
for (var i=0; i < node.children.length; i++) {
collapse(node.children[i].id, recursive, set);
}
}
me.triggerEvent(node.collapsed ? 'collapse' : 'expand', node);
save_state(id, node.collapsed);
}
}
/**
* Expand a the node with the given ID
*/
function expand(id, recursive)
{
collapse(id, recursive, false);
}
/**
* Toggle collapsed state of a list node
*/
function toggle(id, recursive)
{
var node;
if (node = indexbyid[id]) {
collapse(id, recursive, !node.collapsed);
}
}
/**
* Select a tree node by it's ID
*/
function select(id)
{
// allow subscribes to prevent selection change
if (me.triggerEvent('beforeselect', indexbyid[id]) === false) {
return;
}
if (selection) {
id2dom(selection, true).removeClass('selected').removeAttr('aria-selected');
if (search_active)
id2dom(selection).removeClass('selected').removeAttr('aria-selected');
selection = null;
}
if (!id)
return;
var li = id2dom(id, true);
if (li.length) {
li.addClass('selected').attr('aria-selected', 'true');
selection = id;
// TODO: expand all parent nodes if collapsed
if (search_active)
id2dom(id).addClass('selected').attr('aria-selected', 'true');
scroll_to_node(li);
}
me.triggerEvent('select', indexbyid[id]);
}
/**
* Getter for the currently selected node ID
*/
function get_selection()
{
return selection;
}
/**
* Return the DOM element of the list item with the given ID
*/
function get_node(id)
{
return indexbyid[id];
}
/**
* Return the DOM element of the list item with the given ID
*/
function get_item(id, real)
{
return id2dom(id, real).get(0);
}
/**
* Insert the given node
*/
function insert(node, parent_id, sort)
{
var li, parent_li,
parent_node = parent_id ? indexbyid[parent_id] : null
search_ = search_active;
// ignore, already exists
if (indexbyid[node.id]) {
return;
}
// apply saved state
state = get_state(node.id, node.collapsed);
if (state !== undefined) {
node.collapsed = state;
}
// insert as child of an existing node
if (parent_node) {
node.level = parent_node.level + 1;
if (!parent_node.children)
parent_node.children = [];
search_active = false;
parent_node.children.push(node);
parent_li = id2dom(parent_id);
// re-render the entire subtree
if (parent_node.children.length == 1) {
render_node(parent_node, null, parent_li);
li = id2dom(node.id);
}
else {
// append new node to parent's child list
li = render_node(node, parent_li.children('ul').first());
}
// list is in search mode
if (search_) {
search_active = search_;
// add clone to current search results (top level)
if (!li.is(':visible')) {
$('<li>')
.attr('id', li.attr('id') + '--xsR')
.attr('class', li.attr('class'))
.addClass('searchresult__')
.append(li.children().first().clone(true, true))
.appendTo(container);
}
}
}
// insert at top level
else {
node.level = 0;
data.push(node);
li = render_node(node, container);
}
indexbyid[node.id] = node;
// set new reference to node.html after insert
// will otherwise vanish in Firefox 3.6
if (typeof node.html == 'object') {
indexbyid[node.id].html = id2dom(node.id, true).children();
}
if (sort) {
resort_node(li, typeof sort == 'string' ? '[class~="' + sort + '"]' : '');
}
}
/**
* Update properties of an existing node
*/
function update_node(id, updates, sort)
{
var li, parent_ul, parent_node, old_parent,
node = indexbyid[id];
if (node) {
li = id2dom(id);
parent_ul = li.parent();
if (updates.id || updates.html || updates.children || updates.classes || updates.parent) {
if (updates.parent && (parent_node = indexbyid[updates.parent])) {
// remove reference from old parent's child list
if (parent_ul.closest('li').length && (old_parent = indexbyid[dom2id(parent_ul.closest('li'))])) {
old_parent.children = $.grep(old_parent.children, function(elem, i){ return elem.id != node.id; });
}
// append to new parent node
parent_ul = id2dom(updates.parent).children('ul').first();
if (!parent_node.children)
parent_node.children = [];
parent_node.children.push(node);
}
else if (updates.parent !== undefined) {
parent_ul = container;
}
$.extend(node, updates);
li = render_node(node, parent_ul, li);
}
if (node.id != id) {
delete indexbyid[id];
indexbyid[node.id] = node;
}
if (sort) {
resort_node(li, typeof sort == 'string' ? '[class~="' + sort + '"]' : '');
}
}
}
/**
* Helper method to sort the list of the given item
*/
function resort_node(li, filter)
{
var first, sibling,
myid = li.get(0).id,
sortname = li.children().first().text().toUpperCase();
li.parent().children('li' + filter).each(function(i, elem) {
if (i == 0)
first = elem;
if (elem.id == myid) {
// skip
}
else if (elem.id != myid && sortname >= $(elem).children().first().text().toUpperCase()) {
sibling = elem;
}
else {
return false;
}
});
if (sibling) {
li.insertAfter(sibling);
}
else if (first && first.id != myid) {
li.insertBefore(first);
}
// reload data from dom
update_data();
}
/**
* Remove the item with the given ID
*/
function remove(id)
{
var node, li, parent;
if (node = indexbyid[id]) {
li = id2dom(id, true);
parent = li.parent();
li.remove();
node.deleted = true;
delete indexbyid[id];
if (search_active) {
id2dom(id, false).remove();
}
// remove tree-toggle button and children list
if (!parent.children().length) {
parent.parent().find('div.treetoggle').remove();
parent.remove();
}
return true;
}
return false;
}
/**
* (Re-)read tree data from DOM
*/
function update_data()
{
data = walk_list(container, 0);
}
/**
* Apply the 'collapsed' status of the data node to the corresponding DOM element(s)
*/
function update_dom(node)
{
var li = id2dom(node.id, true);
li.attr('aria-expanded', node.collapsed ? 'false' : 'true');
li.children('ul').first()[(node.collapsed ? 'hide' : 'show')]();
li.children('div.treetoggle').removeClass('collapsed expanded').addClass(node.collapsed ? 'collapsed' : 'expanded');
me.triggerEvent('toggle', node);
}
/**
*
*/
function reset(keep_content, keep_selection)
{
if (!keep_selection)
select('');
data = [];
indexbyid = {};
drag_active = false;
if (keep_content) {
if (draggable_opts) {
if (ui_draggable)
draggable('destroy');
draggable(draggable_opts);
}
if (droppable_opts) {
if (ui_droppable)
droppable('destroy');
droppable(droppable_opts);
}
update_data();
}
else {
container.html('');
}
reset_search(keep_selection);
}
/**
*
*/
function search(q, enter)
{
q = String(q).toLowerCase();
if (!q.length)
return reset_search();
else if (q == last_search && !enter)
return 0;
var hits = [];
var search_tree = function(items) {
$.each(items, function(i, node) {
var li, sli;
if (!node.virtual && !node.deleted && String(node.text).toLowerCase().indexOf(q) >= 0 && hits.indexOf(node.id) < 0) {
li = id2dom(node.id);
// skip already filtered nodes
if (li.data('filtered'))
return;
sli = $('<li>')
.attr('id', li.attr('id') + '--xsR')
.attr('class', li.attr('class'))
.addClass('searchresult__')
// append all elements like links and inputs, but not sub-trees
.append(li.children(':not(div.treetoggle,ul)').clone(true, true))
.appendTo(container);
hits.push(node.id);
}
if (node.children && node.children.length) {
search_tree(node.children);
}
});
};
// reset old search results
if (search_active) {
$(container).children('li.searchresult__').remove();
search_active = false;
}
// hide all list items
$(container).children('li').hide().removeClass('selected');
// search recursively in tree (to keep sorting order)
search_tree(data);
search_active = true;
me.triggerEvent('search', { query: q, last: last_search, count: hits.length, ids: hits, execute: enter||false });
last_search = q;
return hits.count;
}
/**
*
*/
function reset_search(nosel)
{
if (searchfield)
searchfield.val('');
$(container).children('li.searchresult__').remove();
$(container).children('li').filter(function() { return !$(this).data('filtered'); }).show();
search_active = false;
me.triggerEvent('search', { query: false, last: last_search });
last_search = '';
if (selection && !nosel)
select(selection);
}
/**
*
*/
function is_search()
{
return search_active;
}
/**
* Render the tree list from the internal data structure
*/
function render()
{
if (me.triggerEvent('renderBefore', data) === false)
return;
// remove all child nodes
container.html('');
// render child nodes
for (var i=0; i < data.length; i++) {
data[i].level = 0;
render_node(data[i], container);
}
me.triggerEvent('renderAfter', container);
}
/**
* Render a specific node into the DOM list
*/
function render_node(node, parent, replace)
{
if (node.deleted)
return;
var li = $('<li>')
.attr('id', p.id_prefix + (p.id_encode ? p.id_encode(node.id) : node.id))
.attr('role', 'treeitem')
.addClass((node.classes || []).join(' '))
.data('id', node.id);
if (replace) {
replace.replaceWith(li);
if (parent)
li.appendTo(parent);
}
else
li.appendTo(parent);
if (typeof node.html == 'string')
li.html(node.html);
else if (typeof node.html == 'object')
li.append(node.html);
if (!node.text)
node.text = li.children().first().text();
if (node.virtual)
li.addClass('virtual');
if (node.id == selection)
li.addClass('selected');
// add child list and toggle icon
if (node.children && node.children.length) {
li.attr('aria-expanded', node.collapsed ? 'false' : 'true');
$('<div class="treetoggle '+(node.collapsed ? 'collapsed' : 'expanded') + '">&nbsp;</div>').appendTo(li);
var ul = $('<ul>').appendTo(li).attr('class', node.childlistclass).attr('role', 'group');
if (node.collapsed)
ul.hide();
for (var i=0; i < node.children.length; i++) {
node.children[i].level = node.level + 1;
render_node(node.children[i], ul);
}
}
return li;
}
/**
* Recursively walk the DOM tree and build an internal data structure
* representing the skeleton of this tree list.
*/
function walk_list(ul, level)
{
var result = [];
ul.children('li').each(function(i,e){
var state, li = $(e), sublist = li.children('ul');
var node = {
id: dom2id(li),
classes: String(li.attr('class')).split(' '),
virtual: li.hasClass('virtual'),
level: level,
html: li.children().first().get(0).outerHTML,
text: li.children().first().text(),
children: walk_list(sublist, level+1)
}
if (sublist.length) {
node.childlistclass = sublist.attr('class');
}
if (node.children.length) {
if (node.collapsed === undefined)
node.collapsed = sublist.css('display') == 'none';
// apply saved state
state = get_state(node.id, node.collapsed);
if (state !== undefined) {
node.collapsed = state;
sublist[(state?'hide':'show')]();
}
if (!li.children('div.treetoggle').length)
$('<div class="treetoggle '+(node.collapsed ? 'collapsed' : 'expanded') + '">&nbsp;</div>').appendTo(li);
li.attr('aria-expanded', node.collapsed ? 'false' : 'true');
}
if (li.hasClass('selected')) {
li.attr('aria-selected', 'true');
selection = node.id;
}
li.data('id', node.id);
// declare list item as treeitem
li.attr('role', 'treeitem').attr('aria-level', node.level+1);
// allow virtual nodes to receive focus
if (node.virtual) {
li.children('a').first().attr('tabindex', '0');
}
result.push(node);
indexbyid[node.id] = node;
});
ul.attr('role', level == 0 ? 'tree' : 'group');
return result;
}
/**
* Recursively walk the data tree and index nodes by their ID
*/
function index_data(node)
{
if (node.id) {
indexbyid[node.id] = node;
}
for (var c=0; node.children && c < node.children.length; c++) {
index_data(node.children[c]);
}
}
/**
* Get the (stripped) node ID from the given DOM element
*/
function dom2id(li)
{
var domid = String(li.attr('id')).replace(new RegExp('^' + (p.id_prefix) || '%'), '').replace(/--xsR$/, '');
return p.id_decode ? p.id_decode(domid) : domid;
}
/**
* Get the <li> element for the given node ID
*/
function id2dom(id, real)
{
var domid = p.id_encode ? p.id_encode(id) : id,
suffix = search_active && !real ? '--xsR' : '';
return $('#' + p.id_prefix + domid + suffix, container);
}
/**
* Scroll the parent container to make the given list item visible
*/
function scroll_to_node(li)
{
var scroller = container.parent(),
current_offset = scroller.scrollTop(),
rel_offset = li.offset().top - scroller.offset().top;
if (rel_offset < 0 || rel_offset + li.height() > scroller.height())
scroller.scrollTop(rel_offset + current_offset);
}
/**
* Save node collapse state to localStorage
*/
function save_state(id, collapsed)
{
if (p.save_state && window.rcmail) {
var key = 'treelist-' + list_id;
if (!tree_state) {
tree_state = rcmail.local_storage_get_item(key, {});
}
if (tree_state[id] != collapsed) {
tree_state[id] = collapsed;
rcmail.local_storage_set_item(key, tree_state);
}
}
}
/**
* Read node collapse state from localStorage
*/
function get_state(id)
{
if (p.save_state && window.rcmail) {
if (!tree_state) {
tree_state = rcmail.local_storage_get_item('treelist-' + list_id, {});
}
return tree_state[id];
}
return undefined;
}
/**
* Handler for keyboard events on treelist
*/
function keypress(e)
{
var target = e.target || {},
keyCode = rcube_event.get_keycode(e);
if (!has_focus || target.nodeName == 'INPUT' && keyCode != 38 && keyCode != 40 || target.nodeName == 'TEXTAREA' || target.nodeName == 'SELECT')
return true;
switch (keyCode) {
case 38:
case 40:
case 63232: // 'up', in safari keypress
case 63233: // 'down', in safari keypress
var li = p.keyboard ? container.find(':focus').closest('li') : [];
if (li.length) {
focus_next(li, (mod = keyCode == 38 || keyCode == 63232 ? -1 : 1));
}
return rcube_event.cancel(e);
case 37: // Left arrow key
case 39: // Right arrow key
var id, node, li = container.find(':focus').closest('li');
if (li.length) {
id = dom2id(li);
node = indexbyid[id];
if (node && node.children.length && node.collapsed != (keyCode == 37))
toggle(id, rcube_event.get_modifier(e) == SHIFT_KEY); // toggle subtree
}
return false;
case 9: // Tab
if (p.keyboard && p.tabexit) {
// jump to last/first item to move focus away from the treelist widget by tab
var limit = rcube_event.get_modifier(e) == SHIFT_KEY ? 'first' : 'last';
focus_noscroll(container.find('li[role=treeitem]:has(a)')[limit]().find('a:'+limit));
}
break;
}
return true;
}
function focus_next(li, dir, from_child)
{
var mod = dir < 0 ? 'prev' : 'next',
next = li[mod](), limit, parent;
if (dir > 0 && !from_child && li.children('ul[role=group]:visible').length) {
li.children('ul').children('li').first().find('a').first().focus();
}
else if (dir < 0 && !from_child && next.children('ul[role=group]:visible').length) {
next.children('ul').children('li').last().find('a').first().focus();
}
else if (next.length && next.find('a').first().focus().length) {
// focused
}
else {
parent = li.parent().closest('li[role=treeitem]');
if (parent.length)
if (dir < 0) {
parent.find('a').first().focus();
}
else {
focus_next(parent, dir, true);
}
}
}
/**
* Focus the given element without scrolling the list container
*/
function focus_noscroll(elem)
{
if (elem.length) {
var frame = container.parent().get(0) || { scrollTop:0 },
y = frame.scrollTop || frame.scrollY;
elem.focus();
frame.scrollTop = y;
}
}
function get_next()
{
var node, child;
if (selection && (node = id2dom(selection))) {
child = node.children('ul').children('li').first();
if (child.length) {
return dom2id(child);
}
child = node.next();
if (child.length) {
return dom2id(child);
}
while ((node = node.parent('ul').parent('li')) && node.length) {
child = node.next();
if (child.length) {
return dom2id(child);
}
}
}
}
function get_prev()
{
var node, prev, child;
if (selection && (node = id2dom(selection))) {
prev = node.prev();
child = prev.find('li').last();
if (child.length) {
return dom2id(child);
}
if (prev.length) {
return dom2id(prev);
}
node = node.parent().parent();
if (node.length && node.is('li')) {
return dom2id(node);
}
}
}
///// drag & drop support
/**
* When dragging starts, compute absolute bounding boxes of the list and it's items
* for faster comparisons while mouse is moving
*/
function drag_start(force)
{
if (!force && drag_active)
return;
drag_active = true;
var li, item, height,
pos = container.offset();
body_scroll_top = bw.ie ? 0 : window.pageYOffset;
list_scroll_top = container.parent().scrollTop();
pos.top += list_scroll_top;
box_coords = {
x1: pos.left,
y1: pos.top,
x2: pos.left + container.width(),
y2: pos.top + container.height()
};
item_coords = [];
for (var id in indexbyid) {
li = id2dom(id);
item = li.children().first().get(0);
if (item && (height = item.offsetHeight)) {
pos = $(item).offset();
pos.top += list_scroll_top;
item_coords[id] = {
x1: pos.left,
y1: pos.top,
x2: pos.left + item.offsetWidth,
y2: pos.top + height,
on: id == autoexpand_item
};
}
}
// enable auto-scrolling of list container
if (container.height() > container.parent().height()) {
container.parent().on('mousemove.treelist', function(e) {
var scroll = 0,
mouse = rcube_event.get_mouse_pos(e);
mouse.y -= container.parent().offset().top;
if (mouse.y < 25 && list_scroll_top > 0) {
scroll = -1; // up
}
else if (mouse.y > container.parent().height() - 25) {
scroll = 1; // down
}
if (drag_active && scroll != 0) {
if (!scroll_timer)
scroll_timer = window.setTimeout(function(){ drag_scroll(scroll); }, p.scroll_delay);
}
else if (scroll_timer) {
window.clearTimeout(scroll_timer);
scroll_timer = null;
}
})
.on('mouseleave.treelist', function() {
if (scroll_timer) {
window.clearTimeout(scroll_timer);
scroll_timer = null;
}
});
}
}
/**
* Signal that dragging has stopped
*/
function drag_end()
{
container.parent().off('.treelist');
$('li.droptarget', container).removeClass('droptarget');
if (!drag_active)
return;
drag_active = false;
scroll_timer = null;
if (autoexpand_timer) {
clearTimeout(autoexpand_timer);
autoexpand_timer = null;
autoexpand_item = null;
}
}
/**
* Scroll list container in the given direction
*/
function drag_scroll(dir)
{
if (!drag_active)
return;
var old_top = list_scroll_top;
container.parent().get(0).scrollTop += p.scroll_step * dir;
list_scroll_top = container.parent().scrollTop();
scroll_timer = null;
if (list_scroll_top != old_top)
scroll_timer = window.setTimeout(function(){ drag_scroll(dir); }, p.scroll_speed);
}
/**
* Determine if the given mouse coords intersect the list and one of its items
*/
function intersects(mouse, highlight)
{
// offsets to compensate for scrolling while dragging a message
var boffset = bw.ie ? -document.documentElement.scrollTop : body_scroll_top,
moffset = container.parent().scrollTop(),
result = null;
mouse.top = mouse.y + moffset - boffset;
// no intersection with list bounding box
if (mouse.x < box_coords.x1 || mouse.x >= box_coords.x2 || mouse.top < box_coords.y1 || mouse.top >= box_coords.y2) {
// TODO: optimize performance for this operation
if (highlight)
$('li.droptarget', container).removeClass('droptarget');
return result;
}
// check intersection with visible list items
var id, pos, node;
for (id in item_coords) {
pos = item_coords[id];
if (mouse.x >= pos.x1 && mouse.x < pos.x2 && mouse.top >= pos.y1 && mouse.top < pos.y2) {
node = indexbyid[id];
// if the folder is collapsed, expand it after the configured time
if (node.children && node.children.length && node.collapsed && p.autoexpand && autoexpand_item != id) {
if (autoexpand_timer)
clearTimeout(autoexpand_timer);
autoexpand_item = id;
autoexpand_timer = setTimeout(function() {
expand(autoexpand_item);
drag_start(true); // re-calculate item coords
autoexpand_item = null;
if (ui_droppable)
$.ui.ddmanager.prepareOffsets($.ui.ddmanager.current, null);
}, p.autoexpand);
}
else if (autoexpand_timer && autoexpand_item != id) {
clearTimeout(autoexpand_timer);
autoexpand_item = null;
autoexpand_timer = null;
}
// check if this item is accepted as drop target
if (p.check_droptarget(node)) {
if (highlight) {
id2dom(id).addClass('droptarget');
pos.on = true;
}
result = id;
}
else {
result = null;
}
}
else if (pos.on) {
id2dom(id).removeClass('droptarget');
pos.on = false;
}
}
return result;
}
/**
* Wrapper for jQuery.UI.droppable() activation on this widget
*
* @param object Options as passed to regular .droppable() function
*/
function droppable(opts)
{
if (!opts) opts = {};
if ($.type(opts) == 'string') {
if (opts == 'destroy') {
ui_droppable = null;
}
$('li:not(.virtual)', container).droppable(opts);
return this;
}
droppable_opts = opts;
var my_opts = $.extend({
greedy: true,
tolerance: 'pointer',
hoverClass: 'droptarget',
addClasses: false
}, opts);
my_opts.activate = function(e, ui) {
drag_start();
ui_droppable = ui;
if (opts.activate)
opts.activate(e, ui);
};
my_opts.deactivate = function(e, ui) {
drag_end();
ui_droppable = null;
if (opts.deactivate)
opts.deactivate(e, ui);
};
my_opts.over = function(e, ui) {
intersects(rcube_event.get_mouse_pos(e), false);
if (opts.over)
opts.over(e, ui);
};
$('li:not(.virtual)', container).droppable(my_opts);
return this;
}
/**
* Wrapper for jQuery.UI.draggable() activation on this widget
*
* @param object Options as passed to regular .draggable() function
*/
function draggable(opts)
{
if (!opts) opts = {};
if ($.type(opts) == 'string') {
if (opts == 'destroy') {
ui_draggable = null;
}
$('li:not(.virtual)', container).draggable(opts);
return this;
}
draggable_opts = opts;
var my_opts = $.extend({
appendTo: 'body',
revert: 'invalid',
iframeFix: true,
addClasses: false,
cursorAt: {left: -20, top: 5},
create: function(e, ui) { ui_draggable = ui; },
helper: function(e) {
return $('<div>').attr('id', 'rcmdraglayer')
.text($.trim($(e.target).first().text()));
}
}, opts);
$('li:not(.virtual)', container).draggable(my_opts);
return this;
}
function is_draggable()
{
return !!ui_draggable;
}
}
// use event processing functions from Roundcube's rcube_event_engine
rcube_treelist_widget.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;
rcube_treelist_widget.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener;
rcube_treelist_widget.prototype.triggerEvent = rcube_event_engine.prototype.triggerEvent;
diff --git a/program/lib/Roundcube/bootstrap.php b/program/lib/Roundcube/bootstrap.php
index 8868d5747..14886b6a9 100644
--- a/program/lib/Roundcube/bootstrap.php
+++ b/program/lib/Roundcube/bootstrap.php
@@ -1,457 +1,458 @@
<?php
/**
+-----------------------------------------------------------------------+
- | This file is part of the Roundcube PHP suite |
- | Copyright (C) 2005-2017, The Roundcube Dev Team |
+ | This file is part of the Roundcube webmail client |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| CONTENTS: |
| Roundcube Framework Initialization |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Roundcube Framework Initialization
*
* @package Framework
* @subpackage Core
*/
$config = array(
'error_reporting' => E_ALL & ~E_NOTICE & ~E_STRICT,
'display_errors' => false,
'log_errors' => true,
// Some users are not using Installer, so we'll check some
// critical PHP settings here. Only these, which doesn't provide
// an error/warning in the logs later. See (#1486307).
'mbstring.func_overload' => 0,
);
// check these additional ini settings if not called via CLI
if (php_sapi_name() != 'cli') {
$config += array(
'suhosin.session.encrypt' => false,
'file_uploads' => true,
'session.auto_start' => false,
'zlib.output_compression' => false,
);
}
foreach ($config as $optname => $optval) {
$ini_optval = filter_var(ini_get($optname), is_bool($optval) ? FILTER_VALIDATE_BOOLEAN : FILTER_VALIDATE_INT);
if ($optval != $ini_optval && @ini_set($optname, $optval) === false) {
$optval = !is_bool($optval) ? $optval : ($optval ? 'On' : 'Off');
$error = "ERROR: Wrong '$optname' option value and it wasn't possible to set it to required value ($optval).\n"
. "Check your PHP configuration (including php_admin_flag).";
if (defined('STDERR')) fwrite(STDERR, $error); else echo $error;
exit(1);
}
}
// framework constants
define('RCUBE_VERSION', '1.4-git');
define('RCUBE_CHARSET', 'UTF-8');
define('RCUBE_TEMP_FILE_PREFIX', 'RCMTEMP');
if (!defined('RCUBE_LIB_DIR')) {
define('RCUBE_LIB_DIR', __DIR__ . '/');
}
if (!defined('RCUBE_INSTALL_PATH')) {
define('RCUBE_INSTALL_PATH', RCUBE_LIB_DIR);
}
if (!defined('RCUBE_CONFIG_DIR')) {
define('RCUBE_CONFIG_DIR', RCUBE_INSTALL_PATH . 'config/');
}
if (!defined('RCUBE_PLUGINS_DIR')) {
define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'plugins/');
}
if (!defined('RCUBE_LOCALIZATION_DIR')) {
define('RCUBE_LOCALIZATION_DIR', RCUBE_INSTALL_PATH . 'localization/');
}
// set internal encoding for mbstring extension
mb_internal_encoding(RCUBE_CHARSET);
mb_regex_encoding(RCUBE_CHARSET);
// make sure the Roundcube lib directory is in the include_path
$rcube_path = realpath(RCUBE_LIB_DIR . '..');
$sep = PATH_SEPARATOR;
$regexp = "!(^|$sep)" . preg_quote($rcube_path, '!') . "($sep|\$)!";
$path = ini_get('include_path');
if (!preg_match($regexp, $path)) {
set_include_path($path . PATH_SEPARATOR . $rcube_path);
}
// Register autoloader
spl_autoload_register('rcube_autoload');
// set PEAR error handling (will also load the PEAR main class)
PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, function($err) { rcube::raise_error($err, true); });
/**
* Similar function as in_array() but case-insensitive with multibyte support.
*
* @param string $needle Needle value
* @param array $heystack Array to search in
*
* @return boolean True if found, False if not
*/
function in_array_nocase($needle, $haystack)
{
// use much faster method for ascii
if (is_ascii($needle)) {
foreach ((array) $haystack as $value) {
if (strcasecmp($value, $needle) === 0) {
return true;
}
}
}
else {
$needle = mb_strtolower($needle);
foreach ((array) $haystack as $value) {
if ($needle === mb_strtolower($value)) {
return true;
}
}
}
return false;
}
/**
* Parse a human readable string for a number of bytes.
*
* @param string $str Input string
*
* @return float Number of bytes
*/
function parse_bytes($str)
{
if (is_numeric($str)) {
return floatval($str);
}
if (preg_match('/([0-9\.]+)\s*([a-z]*)/i', $str, $regs)) {
$bytes = floatval($regs[1]);
switch (strtolower($regs[2])) {
case 'g':
case 'gb':
$bytes *= 1073741824;
break;
case 'm':
case 'mb':
$bytes *= 1048576;
break;
case 'k':
case 'kb':
$bytes *= 1024;
break;
}
}
return floatval($bytes);
}
/**
* Make sure the string ends with a slash
*/
function slashify($str)
{
return unslashify($str) . '/';
}
/**
* Remove slashes at the end of the string
*/
function unslashify($str)
{
return rtrim($str, '/');
}
/**
* Returns number of seconds for a specified offset string.
*
* @param string $str String representation of the offset (e.g. 20min, 5h, 2days, 1week)
*
* @return int Number of seconds
*/
function get_offset_sec($str)
{
if (preg_match('/^([0-9]+)\s*([smhdw])/i', $str, $regs)) {
$amount = (int) $regs[1];
$unit = strtolower($regs[2]);
}
else {
$amount = (int) $str;
$unit = 's';
}
switch ($unit) {
case 'w':
$amount *= 7;
case 'd':
$amount *= 24;
case 'h':
$amount *= 60;
case 'm':
$amount *= 60;
}
return $amount;
}
/**
* Create a unix timestamp with a specified offset from now.
*
* @param string $offset_str String representation of the offset (e.g. 20min, 5h, 2days)
* @param int $factor Factor to multiply with the offset
*
* @return int Unix timestamp
*/
function get_offset_time($offset_str, $factor = 1)
{
return time() + get_offset_sec($offset_str) * $factor;
}
/**
* Truncate string if it is longer than the allowed length.
* Replace the middle or the ending part of a string with a placeholder.
*
* @param string $str Input string
* @param int $maxlength Max. length
* @param string $placeholder Replace removed chars with this
* @param bool $ending Set to True if string should be truncated from the end
*
* @return string Abbreviated string
*/
function abbreviate_string($str, $maxlength, $placeholder = '...', $ending = false)
{
$length = mb_strlen($str);
if ($length > $maxlength) {
if ($ending) {
return mb_substr($str, 0, $maxlength) . $placeholder;
}
$placeholder_length = mb_strlen($placeholder);
$first_part_length = floor(($maxlength - $placeholder_length)/2);
$second_starting_location = $length - $maxlength + $first_part_length + $placeholder_length;
$prefix = mb_substr($str, 0, $first_part_length);
$suffix = mb_substr($str, $second_starting_location);
$str = $prefix . $placeholder . $suffix;
}
return $str;
}
/**
* Get all keys from array (recursive).
*
* @param array $array Input array
*
* @return array List of array keys
*/
function array_keys_recursive($array)
{
$keys = array();
if (!empty($array) && is_array($array)) {
foreach ($array as $key => $child) {
$keys[] = $key;
foreach (array_keys_recursive($child) as $val) {
$keys[] = $val;
}
}
}
return $keys;
}
/**
* Remove all non-ascii and non-word chars except ., -, _
*/
function asciiwords($str, $css_id = false, $replace_with = '')
{
$allowed = 'a-z0-9\_\-' . (!$css_id ? '\.' : '');
return preg_replace("/[^$allowed]/i", $replace_with, $str);
}
/**
* Check if a string contains only ascii characters
*
* @param string $str String to check
* @param bool $control_chars Includes control characters
*
* @return bool
*/
function is_ascii($str, $control_chars = true)
{
$regexp = $control_chars ? '/[^\x00-\x7F]/' : '/[^\x20-\x7E]/';
return preg_match($regexp, $str) ? false : true;
}
/**
* Compose a valid representation of name and e-mail address
*
* @param string $email E-mail address
* @param string $name Person name
*
* @return string Formatted string
*/
function format_email_recipient($email, $name = '')
{
$email = trim($email);
if ($name && $name != $email) {
// Special chars as defined by RFC 822 need to in quoted string (or escaped).
if (preg_match('/[\(\)\<\>\\\.\[\]@,;:"]/', $name)) {
$name = '"'.addcslashes($name, '"').'"';
}
return "$name <$email>";
}
return $email;
}
/**
* Format e-mail address
*
* @param string $email E-mail address
*
* @return string Formatted e-mail address
*/
function format_email($email)
{
$email = trim($email);
$parts = explode('@', $email);
$count = count($parts);
if ($count > 1) {
$parts[$count-1] = mb_strtolower($parts[$count-1]);
$email = implode('@', $parts);
}
return $email;
}
/**
* Fix version number so it can be used correctly in version_compare()
*
* @param string $version Version number string
*
* @param return Version number string
*/
function version_parse($version)
{
return str_replace(
array('-stable', '-git'),
array('.0', '.99'),
$version
);
}
/**
* intl replacement functions
*/
if (!function_exists('idn_to_utf8'))
{
function idn_to_utf8($domain)
{
static $idn, $loaded;
if (!$loaded) {
$idn = new Net_IDNA2();
$loaded = true;
}
if ($idn && $domain && preg_match('/(^|\.)xn--/i', $domain)) {
try {
$domain = $idn->decode($domain);
}
catch (Exception $e) {
}
}
return $domain;
}
}
if (!function_exists('idn_to_ascii'))
{
function idn_to_ascii($domain)
{
static $idn, $loaded;
if (!$loaded) {
$idn = new Net_IDNA2();
$loaded = true;
}
if ($idn && $domain && preg_match('/[^\x20-\x7E]/', $domain)) {
try {
$domain = $idn->encode($domain);
}
catch (Exception $e) {
}
}
return $domain;
}
}
/**
* Use PHP5 autoload for dynamic class loading
*
* @todo Make Zend, PEAR etc play with this
* @todo Make our classes conform to a more straight forward CS.
*/
function rcube_autoload($classname)
{
if (strpos($classname, 'rcube') === 0) {
$classname = preg_replace('/^rcube_(cache|db|session|spellchecker)_/', '\\1/', $classname);
$classname = 'Roundcube/' . $classname;
}
else if (strpos($classname, 'html_') === 0 || $classname === 'html') {
$classname = 'Roundcube/html';
}
else if (strpos($classname, 'Mail_') === 0) {
$classname = 'Mail/' . substr($classname, 5);
}
else if (strpos($classname, 'Net_') === 0) {
$classname = 'Net/' . substr($classname, 4);
}
else if (strpos($classname, 'Auth_') === 0) {
$classname = 'Auth/' . substr($classname, 5);
}
// Translate PHP namespaces into directories,
// i.e. use \Sabre\VObject; $vcf = VObject\Reader::read(...)
// -> Sabre/VObject/Reader.php
$classname = str_replace('\\', '/', $classname);
if ($fp = @fopen("$classname.php", 'r', true)) {
fclose($fp);
include_once "$classname.php";
return true;
}
return false;
}
diff --git a/program/lib/Roundcube/cache/apc.php b/program/lib/Roundcube/cache/apc.php
index 46a3b13b0..25fc076c6 100644
--- a/program/lib/Roundcube/cache/apc.php
+++ b/program/lib/Roundcube/cache/apc.php
@@ -1,146 +1,147 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2011-2018, The Roundcube Dev Team |
- | Copyright (C) 2011-2018, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Caching engine - APC |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Interface class for accessing APC cache
*
* @package Framework
* @subpackage Cache
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_cache_apc extends rcube_cache
{
/**
* Indicates if APC module is enabled and in a required version
*
* @var bool
*/
protected $enabled;
/**
* Object constructor.
*
* @param int $userid User identifier
* @param string $prefix Key name prefix
* @param string $ttl Expiration time of memcache/apc items
* @param bool $packed Enables/disabled data serialization.
* It's possible to disable data serialization if you're sure
* stored data will be always a safe string
*/
public function __construct($userid, $prefix = '', $ttl = 0, $packed = true)
{
parent::__construct($userid, $prefix, $ttl, $packed);
$rcube = rcube::get_instance();
$this->type = 'apc';
$this->enabled = function_exists('apc_exists'); // APC 3.1.4 required
$this->debug = $rcube->config->get('apc_debug');
}
/**
* Remove cache records older than ttl
*/
public function expunge()
{
// No need for GC, entries are expunged automatically
}
/**
* Remove expired records of all caches
*/
public static function gc()
{
// No need for GC, entries are expunged automatically
}
/**
* Reads cache entry.
*
* @param string $key Cache internal key name
*
* @return mixed Cached value
*/
protected function get_item($key)
{
if (!$this->enabled) {
return false;
}
$data = apc_fetch($key);
if ($this->debug) {
$this->debug('get', $key, $data);
}
return $data;
}
/**
* Adds entry into memcache/apc/redis DB.
*
* @param string $key Cache internal key name
* @param mixed $data Serialized cache data
*
* @param boolean True on success, False on failure
*/
protected function add_item($key, $data)
{
if (!$this->enabled) {
return false;
}
if (apc_exists($key)) {
apc_delete($key);
}
$result = apc_store($key, $data, $this->ttl);
if ($this->debug) {
$this->debug('set', $key, $data, $result);
}
return $result;
}
/**
* Deletes entry from memcache/apc/redis DB.
*
* @param string $key Cache internal key name
*
* @param boolean True on success, False on failure
*/
protected function delete_item($key)
{
if (!$this->enabled) {
return false;
}
$result = apc_delete($key);
if ($this->debug) {
$this->debug('delete', $key, null, $result);
}
return $result;
}
}
diff --git a/program/lib/Roundcube/cache/db.php b/program/lib/Roundcube/cache/db.php
index 23f2103eb..695f90074 100644
--- a/program/lib/Roundcube/cache/db.php
+++ b/program/lib/Roundcube/cache/db.php
@@ -1,271 +1,272 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2011-2018, The Roundcube Dev Team |
- | Copyright (C) 2011-2018, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Caching engine - SQL DB |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Interface class for accessing SQL Database cache
*
* @package Framework
* @subpackage Cache
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_cache_db extends rcube_cache
{
/**
* Instance of database handler
*
* @var rcube_db
*/
protected $db;
/**
* (Escaped) Cache table name (cache or cache_shared)
*
* @var string
*/
protected $table;
/**
* Object constructor.
*
* @param int $userid User identifier
* @param string $prefix Key name prefix
* @param string $ttl Expiration time of memcache/apc items
* @param bool $packed Enables/disabled data serialization.
* It's possible to disable data serialization if you're sure
* stored data will be always a safe string
*/
public function __construct($userid, $prefix = '', $ttl = 0, $packed = true)
{
parent::__construct($userid, $prefix, $ttl, $packed);
$rcube = rcube::get_instance();
$this->type = 'db';
$this->db = $rcube->get_dbh();
$this->table = $this->db->table_name($userid ? 'cache' : 'cache_shared', true);
}
/**
* Remove cache records older than ttl
*/
public function expunge()
{
if ($this->db && $this->ttl) {
$this->db->query(
"DELETE FROM {$this->table} WHERE "
. ($this->userid ? "`user_id` = {$this->userid} AND " : "")
. "`cache_key` LIKE ?"
. " AND `expires` < " . $this->db->now(),
$this->prefix . '.%');
}
}
/**
* Remove expired records of all caches
*/
public static function gc()
{
$rcube = rcube::get_instance();
$db = $rcube->get_dbh();
$db->query("DELETE FROM " . $db->table_name('cache', true) . " WHERE `expires` < " . $db->now());
$db->query("DELETE FROM " . $db->table_name('cache_shared', true) . " WHERE `expires` < " . $db->now());
}
/**
* Reads cache entry.
*
* @param string $key Cache key name
* @param boolean $nostore Enable to skip in-memory store
*
* @return mixed Cached value
*/
protected function read_record($key, $nostore=false)
{
if (!$this->db) {
return;
}
$sql_result = $this->db->query(
"SELECT `data`, `cache_key` FROM {$this->table} WHERE "
. ($this->userid ? "`user_id` = {$this->userid} AND " : "")
."`cache_key` = ?",
$this->prefix . '.' . $key);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
if (strlen($sql_arr['data']) > 0) {
$md5sum = md5($sql_arr['data']);
$data = $this->unserialize($sql_arr['data']);
}
$this->db->reset();
if ($nostore) {
return $data;
}
$this->cache[$key] = $data;
$this->cache_sums[$key] = $md5sum;
}
else {
$this->cache[$key] = null;
}
return $this->cache[$key];
}
/**
* Writes single cache record into DB.
*
* @param string $key Cache key name
* @param mixed $data Serialized cache data
*
* @param boolean True on success, False on failure
*/
protected function write_record($key, $data)
{
if (!$this->db) {
return false;
}
// don't attempt to write too big data sets
if (strlen($data) > $this->max_packet_size()) {
trigger_error("rcube_cache: max_packet_size ($this->max_packet) exceeded for key $key. Tried to write " . strlen($data) . " bytes", E_USER_WARNING);
return false;
}
$db_key = $this->prefix . '.' . $key;
// Remove NULL rows (here we don't need to check if the record exist)
if ($data == 'N;') {
$result = $this->db->query(
"DELETE FROM {$this->table} WHERE "
. ($this->userid ? "`user_id` = {$this->userid} AND " : "")
."`cache_key` = ?",
$db_key);
return !$this->db->is_error($result);
}
$key_exists = array_key_exists($key, $this->cache_sums);
$expires = $this->ttl ? $this->db->now($this->ttl) : 'NULL';
if (!$key_exists) {
// Try INSERT temporarily ignoring "duplicate key" errors
$this->db->set_option('ignore_key_errors', true);
if ($this->userid) {
$result = $this->db->query(
"INSERT INTO {$this->table} (`expires`, `user_id`, `cache_key`, `data`)"
. " VALUES ($expires, ?, ?, ?)",
$this->userid, $db_key, $data);
}
else {
$result = $this->db->query(
"INSERT INTO {$this->table} (`expires`, `cache_key`, `data`)"
. " VALUES ($expires, ?, ?)",
$db_key, $data);
}
$this->db->set_option('ignore_key_errors', false);
}
// otherwise try UPDATE
if (!isset($result) || !($count = $this->db->affected_rows($result))) {
$result = $this->db->query(
"UPDATE {$this->table} SET `expires` = $expires, `data` = ? WHERE "
. ($this->userid ? "`user_id` = {$this->userid} AND " : "")
. "`cache_key` = ?",
$data, $db_key);
$count = $this->db->affected_rows($result);
}
return $count > 0;
}
/**
* Deletes the cache record(s).
*
* @param string $key Cache key name or pattern
* @param boolean $prefix_mode Enable it to clear all keys starting
* with prefix specified in $key
*/
protected function remove_record($key = null, $prefix_mode = false)
{
if (!$this->db) {
return;
}
// Remove all keys (in specified cache)
if ($key === null) {
$where = "`cache_key` LIKE " . $this->db->quote($this->prefix . '.%');
}
// Remove keys by name prefix
else if ($prefix_mode) {
$where = "`cache_key` LIKE " . $this->db->quote($this->prefix . '.' . $key . '%');
}
// Remove one key by name
else {
$where = "`cache_key` = " . $this->db->quote($this->prefix . '.' . $key);
}
$this->db->query(
"DELETE FROM {$this->table} WHERE "
. ($this->userid ? "`user_id` = {$this->userid} AND " : "") . $where
);
}
/**
* Serializes data for storing
*/
protected function serialize($data)
{
return $this->db ? $this->db->encode($data, $this->packed) : false;
}
/**
* Unserializes serialized data
*/
protected function unserialize($data)
{
return $this->db ? $this->db->decode($data, $this->packed) : false;
}
/**
* Determine the maximum size for cache data to be written
*/
protected function max_packet_size()
{
if ($this->max_packet < 0) {
$this->max_packet = 2097152; // default/max is 2 MB
if ($value = $this->db->get_variable('max_allowed_packet', $this->max_packet)) {
$this->max_packet = $value;
}
$this->max_packet -= 2000;
}
return $this->max_packet;
}
}
diff --git a/program/lib/Roundcube/cache/memcache.php b/program/lib/Roundcube/cache/memcache.php
index f3450dd97..cf84f2bd0 100644
--- a/program/lib/Roundcube/cache/memcache.php
+++ b/program/lib/Roundcube/cache/memcache.php
@@ -1,217 +1,218 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2011-2018, The Roundcube Dev Team |
- | Copyright (C) 2011-2018, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Caching engine - Memcache |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Interface class for accessing Memcache cache
*
* @package Framework
* @subpackage Cache
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_cache_memcache extends rcube_cache
{
/**
* Instance of memcache handler
*
* @var Memcache
*/
protected static $memcache;
/**
* Object constructor.
*
* @param int $userid User identifier
* @param string $prefix Key name prefix
* @param string $ttl Expiration time of memcache/apc items
* @param bool $packed Enables/disabled data serialization.
* It's possible to disable data serialization if you're sure
* stored data will be always a safe string
*/
public function __construct($userid, $prefix = '', $ttl = 0, $packed = true)
{
parent::__construct($userid, $prefix, $ttl, $packed);
$this->type = 'memcache';
$this->debug = rcube::get_instance()->config->get('memcache_debug');
self::engine();
}
/**
* Get global handle for memcache access
*
* @return object Memcache
*/
public static function engine()
{
if (self::$memcache !== null) {
return self::$memcache;
}
// no memcache support in PHP
if (!class_exists('Memcache')) {
self::$memcache = false;
rcube::raise_error(array(
'code' => 604,
'type' => 'memcache',
'line' => __LINE__,
'file' => __FILE__,
'message' => "Failed to find Memcache. Make sure php-memcache is included"
),
true, true);
}
// add all configured hosts to pool
$rcube = rcube::get_instance();
$pconnect = $rcube->config->get('memcache_pconnect', true);
$timeout = $rcube->config->get('memcache_timeout', 1);
$retry_interval = $rcube->config->get('memcache_retry_interval', 15);
$seen = array();
$available = 0;
// Callback for memcache failure
$error_callback = function($host, $port) use ($seen, $available) {
// only report once
if (!$seen["$host:$port"]++) {
$available--;
rcube::raise_error(array(
'code' => 604, 'type' => 'db',
'line' => __LINE__, 'file' => __FILE__,
'message' => "Memcache failure on host $host:$port"),
true, false);
}
};
self::$memcache = new Memcache;
foreach ((array) $rcube->config->get('memcache_hosts') as $host) {
if (substr($host, 0, 7) != 'unix://') {
list($host, $port) = explode(':', $host);
if (!$port) $port = 11211;
}
else {
$port = 0;
}
$available += intval(self::$memcache->addServer(
$host, $port, $pconnect, 1, $timeout, $retry_interval, false, $error_callback));
}
// test connection and failover (will result in $available == 0 on complete failure)
self::$memcache->increment('__CONNECTIONTEST__', 1); // NOP if key doesn't exist
if (!$available) {
self::$memcache = false;
}
return self::$memcache;
}
/**
* Remove cache records older than ttl
*/
public function expunge()
{
// No need for GC, entries are expunged automatically
}
/**
* Remove expired records of all caches
*/
public static function gc()
{
// No need for GC, entries are expunged automatically
}
/**
* Reads cache entry.
*
* @param string $key Cache internal key name
*
* @return mixed Cached value
*/
protected function get_item($key)
{
if (!self::$memcache) {
return false;
}
$data = self::$memcache->get($key);
if ($this->debug) {
$this->debug('get', $key, $data);
}
return $data;
}
/**
* Adds entry into the cache.
*
* @param string $key Cache internal key name
* @param mixed $data Serialized cache data
*
* @param boolean True on success, False on failure
*/
protected function add_item($key, $data)
{
if (!self::$memcache) {
return false;
}
$result = self::$memcache->replace($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
if (!$result) {
$result = self::$memcache->set($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
}
if ($this->debug) {
$this->debug('set', $key, $data, $result);
}
return $result;
}
/**
* Deletes entry from the cache
*
* @param string $key Cache internal key name
*
* @param boolean True on success, False on failure
*/
protected function delete_item($key)
{
if (!self::$memcache) {
return false;
}
// #1488592: use 2nd argument
$result = self::$memcache->delete($key, 0);
if ($this->debug) {
$this->debug('delete', $key, null, $result);
}
return $result;
}
}
diff --git a/program/lib/Roundcube/cache/redis.php b/program/lib/Roundcube/cache/redis.php
index 4b4b3ae01..47c29edf6 100644
--- a/program/lib/Roundcube/cache/redis.php
+++ b/program/lib/Roundcube/cache/redis.php
@@ -1,254 +1,255 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2011-2018, The Roundcube Dev Team |
- | Copyright (C) 2011-2018, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Caching engine - Redis |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Interface class for accessing Redis cache
*
* @package Framework
* @subpackage Cache
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_cache_redis extends rcube_cache
{
/**
* Instance of Redis object
*
* @var Redis
*/
protected static $redis;
/**
* Object constructor.
*
* @param int $userid User identifier
* @param string $prefix Key name prefix
* @param string $ttl Expiration time of memcache/apc items
* @param bool $packed Enables/disabled data serialization.
* It's possible to disable data serialization if you're sure
* stored data will be always a safe string
*/
public function __construct($userid, $prefix = '', $ttl = 0, $packed = true)
{
parent::__construct($userid, $prefix, $ttl, $packed);
$rcube = rcube::get_instance();
$this->type = 'redis';
$this->debug = $rcube->config->get('redis_debug');
self::engine();
}
/**
* Get global handle for redis access
*
* @return object Redis
*/
public static function engine()
{
if (self::$redis !== null) {
return self::$redis;
}
if (!class_exists('Redis')) {
self::$redis = false;
rcube::raise_error(array(
'code' => 604,
'type' => 'redis',
'line' => __LINE__,
'file' => __FILE__,
'message' => "Failed to find Redis. Make sure php-redis is included"
),
true, true);
}
$rcube = rcube::get_instance();
$hosts = $rcube->config->get('redis_hosts');
// host config is wrong
if (!is_array($hosts) || empty($hosts)) {
rcube::raise_error(array(
'code' => 604,
'type' => 'redis',
'line' => __LINE__,
'file' => __FILE__,
'message' => "Redis host not configured"
),
true, true);
}
// only allow 1 host for now until we support clustering
if (count($hosts) > 1) {
rcube::raise_error(array(
'code' => 604,
'type' => 'redis',
'line' => __LINE__,
'file' => __FILE__,
'message' => "Redis cluster not yet supported"
),
true, true);
}
self::$redis = new Redis;
foreach ($hosts as $redis_host) {
// explode individual fields
list($host, $port, $database, $password) = array_pad(explode(':', $redis_host, 4), 4, null);
$params = parse_url($redis_host);
if ($params['scheme'] == 'redis') {
$host = (isset($params['host'])) ? $params['host'] : null;
$port = (isset($params['port'])) ? $params['port'] : null;
$database = (isset($params['database'])) ? $params['database'] : null;
$password = (isset($params['password'])) ? $params['password'] : null;
}
// set default values if not set
$host = ($host !== null) ? $host : '127.0.0.1';
$port = ($port !== null) ? $port : 6379;
$database = ($database !== null) ? $database : 0;
if (self::$redis->connect($host, $port) === false) {
rcube::raise_error(array(
'code' => 604,
'type' => 'redis',
'line' => __LINE__,
'file' => __FILE__,
'message' => "Could not connect to Redis server. Please check host and port"
),
true, true);
}
if ($password != null && self::$redis->auth($password) === false) {
rcube::raise_error(array(
'code' => 604,
'type' => 'redis',
'line' => __LINE__,
'file' => __FILE__,
'message' => "Could not authenticate with Redis server. Please check password."
),
true, true);
}
if ($database != 0 && self::$redis->select($database) === false) {
rcube::raise_error(array(
'code' => 604,
'type' => 'redis',
'line' => __LINE__,
'file' => __FILE__,
'message' => "Could not select Redis database. Please check database setting."
),
true, true);
}
}
if (self::$redis->ping() != "+PONG") {
self::$redis = false;
}
return self::$redis;
}
/**
* Remove cache records older than ttl
*/
public function expunge()
{
// No need for GC, entries are expunged automatically
}
/**
* Remove expired records
*/
public static function gc()
{
// No need for GC, entries are expunged automatically
}
/**
* Reads cache entry.
*
* @param string $key Cache internal key name
*
* @return mixed Cached value
*/
protected function get_item($key)
{
if (!self::$redis) {
return false;
}
$data = self::$redis->get($key);
if ($this->debug) {
$this->debug('get', $key, $data);
}
return $data;
}
/**
* Adds entry into Redis.
*
* @param string $key Cache internal key name
* @param mixed $data Serialized cache data
*
* @param boolean True on success, False on failure
*/
protected function add_item($key, $data)
{
if (!self::$redis) {
return false;
}
$result = self::$redis->setEx($key, $this->ttl, $data);
if ($this->debug) {
$this->debug('set', $key, $data, $result);
}
return $result;
}
/**
* Deletes entry from Redis.
*
* @param string $key Cache internal key name
*
* @param boolean True on success, False on failure
*/
protected function delete_item($key)
{
if (!self::$redis) {
return false;
}
$result = self::$redis->delete($key);
if ($this->debug) {
$this->debug('delete', $key, null, $result);
}
return $result;
}
}
diff --git a/program/lib/Roundcube/db/mssql.php b/program/lib/Roundcube/db/mssql.php
index 4138b1489..38edf037d 100644
--- a/program/lib/Roundcube/db/mssql.php
+++ b/program/lib/Roundcube/db/mssql.php
@@ -1,190 +1,191 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2012, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Database wrapper class that implements PHP PDO functions |
| for MS SQL Server database |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Database independent query interface
* This is a wrapper for the PHP PDO
*
* @package Framework
* @subpackage Database
*/
class rcube_db_mssql extends rcube_db
{
public $db_provider = 'mssql';
/**
* Object constructor
*
* @param string $db_dsnw DSN for read/write operations
* @param string $db_dsnr Optional DSN for read only operations
* @param bool $pconn Enables persistent connections
*/
public function __construct($db_dsnw, $db_dsnr = '', $pconn = false)
{
parent::__construct($db_dsnw, $db_dsnr, $pconn);
$this->options['identifier_start'] = '[';
$this->options['identifier_end'] = ']';
}
/**
* Driver-specific configuration of database connection
*
* @param array $dsn DSN for DB connections
* @param PDO $dbh Connection handler
*/
protected function conn_configure($dsn, $dbh)
{
// Set date format in case of non-default language (#1488918)
$dbh->query("SET DATEFORMAT ymd");
}
/**
* Return SQL function for current time and date
*
* @param int $interval Optional interval (in seconds) to add/subtract
*
* @return string SQL function to use in query
*/
public function now($interval = 0)
{
if ($interval) {
$interval = intval($interval);
return "dateadd(second, $interval, getdate())";
}
return "getdate()";
}
/**
* Return SQL statement to convert a field value into a unix timestamp
*
* @param string $field Field name
*
* @return string SQL statement to use in query
* @deprecated
*/
public function unixtimestamp($field)
{
return "DATEDIFF(second, '19700101', $field) + DATEDIFF(second, GETDATE(), GETUTCDATE())";
}
/**
* Abstract SQL statement for value concatenation
*
* @return string SQL statement to be used in query
*/
public function concat(/* col1, col2, ... */)
{
$args = func_get_args();
if (is_array($args[0])) {
$args = $args[0];
}
return '(' . join('+', $args) . ')';
}
/**
* Adds TOP (LIMIT,OFFSET) clause to the query
*
* @param string $query SQL query
* @param int $limit Number of rows
* @param int $offset Offset
*
* @return string SQL query
*/
protected function set_limit($query, $limit = 0, $offset = 0)
{
$limit = intval($limit);
$offset = intval($offset);
$end = $offset + $limit;
// query without OFFSET
if (!$offset) {
$query = preg_replace('/^SELECT\s/i', "SELECT TOP $limit ", $query);
return $query;
}
$orderby = stristr($query, 'ORDER BY');
$offset += 1;
if ($orderby !== false) {
$query = trim(substr($query, 0, -1 * strlen($orderby)));
}
else {
// it shouldn't happen, paging without sorting has not much sense
// @FIXME: I don't know how to build paging query without ORDER BY
$orderby = "ORDER BY 1";
}
$query = preg_replace('/^SELECT\s/i', '', $query);
$query = "WITH paging AS (SELECT ROW_NUMBER() OVER ($orderby) AS [RowNumber], $query)"
. " SELECT * FROM paging WHERE [RowNumber] BETWEEN $offset AND $end ORDER BY [RowNumber]";
return $query;
}
/**
* Returns PDO DSN string from DSN array
*/
protected function dsn_string($dsn)
{
$params = array();
$result = $dsn['phptype'] . ':';
if ($dsn['hostspec']) {
$host = $dsn['hostspec'];
if ($dsn['port']) {
$host .= ',' . $dsn['port'];
}
$params[] = 'host=' . $host;
}
if ($dsn['database']) {
$params[] = 'dbname=' . $dsn['database'];
}
if (!empty($params)) {
$result .= implode(';', $params);
}
return $result;
}
/**
* Parse SQL file and fix table names according to table prefix
*/
protected function fix_table_names($sql)
{
if (!$this->options['table_prefix']) {
return $sql;
}
// replace sequence names, and other postgres-specific commands
$sql = preg_replace_callback(
'/((TABLE|(?<!ON )UPDATE|INSERT INTO|FROM(?! deleted)| ON(?! (DELETE|UPDATE|\[PRIMARY\]))'
. '|REFERENCES|CONSTRAINT|TRIGGER|INDEX)\s+(\[dbo\]\.)?[\[\]]*)([^\[\]\( \r\n]+)/',
array($this, 'fix_table_names_callback'),
$sql
);
return $sql;
}
}
diff --git a/program/lib/Roundcube/db/mysql.php b/program/lib/Roundcube/db/mysql.php
index 06dac0d2c..06f27efdd 100644
--- a/program/lib/Roundcube/db/mysql.php
+++ b/program/lib/Roundcube/db/mysql.php
@@ -1,254 +1,255 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2012, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Database wrapper class that implements PHP PDO functions |
| for MySQL database |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Database independent query interface
*
* This is a wrapper for the PHP PDO
*
* @package Framework
* @subpackage Database
*/
class rcube_db_mysql extends rcube_db
{
public $db_provider = 'mysql';
/**
* Object constructor
*
* @param string $db_dsnw DSN for read/write operations
* @param string $db_dsnr Optional DSN for read only operations
* @param bool $pconn Enables persistent connections
*/
public function __construct($db_dsnw, $db_dsnr = '', $pconn = false)
{
parent::__construct($db_dsnw, $db_dsnr, $pconn);
// SQL identifiers quoting
$this->options['identifier_start'] = '`';
$this->options['identifier_end'] = '`';
}
/**
* Driver-specific configuration of database connection
*
* @param array $dsn DSN for DB connections
* @param PDO $dbh Connection handler
*/
protected function conn_configure($dsn, $dbh)
{
$dbh->query("SET NAMES 'utf8'");
}
/**
* Abstract SQL statement for value concatenation
*
* @return string SQL statement to be used in query
*/
public function concat(/* col1, col2, ... */)
{
$args = func_get_args();
if (is_array($args[0])) {
$args = $args[0];
}
return 'CONCAT(' . join(', ', $args) . ')';
}
/**
* Returns PDO DSN string from DSN array
*
* @param array $dsn DSN parameters
*
* @return string Connection string
*/
protected function dsn_string($dsn)
{
$params = array();
$result = 'mysql:';
if ($dsn['database']) {
$params[] = 'dbname=' . $dsn['database'];
}
if ($dsn['hostspec']) {
$params[] = 'host=' . $dsn['hostspec'];
}
if ($dsn['port']) {
$params[] = 'port=' . $dsn['port'];
}
if ($dsn['socket']) {
$params[] = 'unix_socket=' . $dsn['socket'];
}
$params[] = 'charset=utf8';
if (!empty($params)) {
$result .= implode(';', $params);
}
return $result;
}
/**
* Returns driver-specific connection options
*
* @param array $dsn DSN parameters
*
* @return array Connection options
*/
protected function dsn_options($dsn)
{
$result = parent::dsn_options($dsn);
if (!empty($dsn['key'])) {
$result[PDO::MYSQL_ATTR_SSL_KEY] = $dsn['key'];
}
if (!empty($dsn['cipher'])) {
$result[PDO::MYSQL_ATTR_SSL_CIPHER] = $dsn['cipher'];
}
if (!empty($dsn['cert'])) {
$result[PDO::MYSQL_ATTR_SSL_CERT] = $dsn['cert'];
}
if (!empty($dsn['capath'])) {
$result[PDO::MYSQL_ATTR_SSL_CAPATH] = $dsn['capath'];
}
if (!empty($dsn['ca'])) {
$result[PDO::MYSQL_ATTR_SSL_CA] = $dsn['ca'];
}
// Always return matching (not affected only) rows count
$result[PDO::MYSQL_ATTR_FOUND_ROWS] = true;
// Enable AUTOCOMMIT mode (#1488902)
$result[PDO::ATTR_AUTOCOMMIT] = true;
return $result;
}
/**
* Returns list of tables in a database
*
* @return array List of all tables of the current database
*/
public function list_tables()
{
// get tables if not cached
if ($this->tables === null) {
$q = $this->query("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES"
. " WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE'"
. " ORDER BY TABLE_NAME", $this->db_dsnw_array['database']);
$this->tables = $q ? $q->fetchAll(PDO::FETCH_COLUMN, 0) : array();
}
return $this->tables;
}
/**
* Returns list of columns in database table
*
* @param string $table Table name
*
* @return array List of table cols
*/
public function list_cols($table)
{
$q = $this->query("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS"
. " WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?",
$this->db_dsnw_array['database'], $table);
if ($q) {
return $q->fetchAll(PDO::FETCH_COLUMN, 0);
}
return array();
}
/**
* Get database runtime variables
*
* @param string $varname Variable name
* @param mixed $default Default value if variable is not set
*
* @return mixed Variable value or default
*/
public function get_variable($varname, $default = null)
{
if (!isset($this->variables)) {
$this->variables = array();
}
if (array_key_exists($varname, $this->variables)) {
return $this->variables[$varname];
}
// configured value has higher prio
$conf_value = rcube::get_instance()->config->get('db_' . $varname);
if ($conf_value !== null) {
return $this->variables[$varname] = $conf_value;
}
$result = $this->query('SHOW VARIABLES LIKE ?', $varname);
while ($row = $this->fetch_array($result)) {
$this->variables[$row[0]] = $row[1];
}
// not found, use default
if (!isset($this->variables[$varname])) {
$this->variables[$varname] = $default;
}
return $this->variables[$varname];
}
/**
* Handle DB errors, re-issue the query on deadlock errors from InnoDB row-level locking
*
* @param string Query that triggered the error
* @return mixed Result to be stored and returned
*/
protected function handle_error($query)
{
$error = $this->dbh->errorInfo();
// retry after "Deadlock found when trying to get lock" errors
$retries = 2;
while ($error[1] == 1213 && $retries >= 0) {
usleep(50000); // wait 50 ms
$result = $this->dbh->query($query);
if ($result !== false) {
return $result;
}
$error = $this->dbh->errorInfo();
$retries--;
}
return parent::handle_error($query);
}
}
diff --git a/program/lib/Roundcube/db/oracle.php b/program/lib/Roundcube/db/oracle.php
index b3ae58452..cc0e4d92f 100644
--- a/program/lib/Roundcube/db/oracle.php
+++ b/program/lib/Roundcube/db/oracle.php
@@ -1,622 +1,623 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2011-2014, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Database wrapper class that implements database functions |
| for Oracle database using OCI8 extension |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+-----------------------------------------------------------------------+
*/
/**
* Database independent query interface
*
* @package Framework
* @subpackage Database
*/
class rcube_db_oracle extends rcube_db
{
public $db_provider = 'oracle';
/**
* Create connection instance
*/
protected function conn_create($dsn)
{
// Get database specific connection options
$dsn_options = $this->dsn_options($dsn);
$function = $this->db_pconn ? 'oci_pconnect' : 'oci_connect';
if (!function_exists($function)) {
$this->db_error = true;
$this->db_error_msg = 'OCI8 extension not loaded. See http://php.net/manual/en/book.oci8.php';
rcube::raise_error(array('code' => 500, 'type' => 'db',
'line' => __LINE__, 'file' => __FILE__,
'message' => $this->db_error_msg), true, false);
return;
}
// connect
$dbh = @$function($dsn['username'], $dsn['password'], $dsn_options['database'], $dsn_options['charset']);
if (!$dbh) {
$error = oci_error();
$this->db_error = true;
$this->db_error_msg = $error['message'];
rcube::raise_error(array('code' => 500, 'type' => 'db',
'line' => __LINE__, 'file' => __FILE__,
'message' => $this->db_error_msg), true, false);
return;
}
// configure session
$this->conn_configure($dsn, $dbh);
return $dbh;
}
/**
* Driver-specific configuration of database connection
*
* @param array $dsn DSN for DB connections
* @param PDO $dbh Connection handler
*/
protected function conn_configure($dsn, $dbh)
{
$init_queries = array(
"ALTER SESSION SET nls_date_format = 'YYYY-MM-DD'",
"ALTER SESSION SET nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS'",
);
foreach ($init_queries as $query) {
$stmt = oci_parse($dbh, $query);
oci_execute($stmt);
}
}
/**
* Connection state checker
*
* @return boolean True if in connected state
*/
public function is_connected()
{
return empty($this->dbh) ? false : $this->db_connected;
}
/**
* Execute a SQL query with limits
*
* @param string $query SQL query to execute
* @param int $offset Offset for LIMIT statement
* @param int $numrows Number of rows for LIMIT statement
* @param array $params Values to be inserted in query
*
* @return PDOStatement|bool Query handle or False on error
*/
protected function _query($query, $offset, $numrows, $params)
{
$query = ltrim($query);
$this->db_connect($this->dsn_select($query), true);
// check connection before proceeding
if (!$this->is_connected()) {
return $this->last_result = false;
}
if ($numrows || $offset) {
$query = $this->set_limit($query, $numrows, $offset);
}
// replace self::DEFAULT_QUOTE with driver-specific quoting
$query = $this->query_parse($query);
// Because in Roundcube we mostly use queries that are
// executed only once, we will not use prepared queries
$pos = 0;
$idx = 0;
$args = array();
if (!empty($params)) {
while ($pos = strpos($query, '?', $pos)) {
if ($query[$pos+1] == '?') { // skip escaped '?'
$pos += 2;
}
else {
$val = $this->quote($params[$idx++]);
// long strings are not allowed inline, need to be parametrized
if (strlen($val) > 4000) {
$key = ':param' . (count($args) + 1);
$args[$key] = $params[$idx-1];
$val = $key;
}
unset($params[$idx-1]);
$query = substr_replace($query, $val, $pos, 1);
$pos += strlen($val);
}
}
}
$query = rtrim($query, " \t\n\r\0\x0B;");
// replace escaped '?' and quotes back to normal, see self::quote()
$query = str_replace(
array('??', self::DEFAULT_QUOTE.self::DEFAULT_QUOTE),
array('?', self::DEFAULT_QUOTE),
$query
);
// log query
$this->debug($query);
// destroy reference to previous result
$this->last_result = null;
$this->db_error_msg = null;
// prepare query
$result = @oci_parse($this->dbh, $query);
$mode = $this->in_transaction ? OCI_NO_AUTO_COMMIT : OCI_COMMIT_ON_SUCCESS;
if ($result) {
foreach (array_keys($args) as $param) {
oci_bind_by_name($result, $param, $args[$param], -1, SQLT_LNG);
}
}
// execute query
if (!$result || !@oci_execute($result, $mode)) {
$result = $this->handle_error($query, $result);
}
return $this->last_result = $result;
}
/**
* Helper method to handle DB errors.
* This by default logs the error but could be overridden by a driver implementation
*
* @param string Query that triggered the error
* @return mixed Result to be stored and returned
*/
protected function handle_error($query, $result = null)
{
$error = oci_error(is_resource($result) ? $result : $this->dbh);
// @TODO: Find error codes for key errors
if (empty($this->options['ignore_key_errors']) || !in_array($error['code'], array('23000', '23505'))) {
$this->db_error = true;
$this->db_error_msg = sprintf('[%s] %s', $error['code'], $error['message']);
rcube::raise_error(array('code' => 500, 'type' => 'db',
'line' => __LINE__, 'file' => __FILE__,
'message' => $this->db_error_msg . " (SQL Query: $query)"
), true, false);
}
return false;
}
/**
* Get last inserted record ID
*
* @param string $table Table name (to find the incremented sequence)
*
* @return mixed ID or false on failure
*/
public function insert_id($table = null)
{
if (!$this->db_connected || $this->db_mode == 'r' || empty($table)) {
return false;
}
$sequence = $this->quote_identifier($this->sequence_name($table));
$result = $this->query("SELECT $sequence.currval FROM dual");
$result = $this->fetch_array($result);
return $result[0] ?: false;
}
/**
* Get number of affected rows for the last query
*
* @param mixed $result Optional query handle
*
* @return int Number of (matching) rows
*/
public function affected_rows($result = null)
{
if ($result || ($result === null && ($result = $this->last_result))) {
return oci_num_rows($result);
}
return 0;
}
/**
* Get number of rows for a SQL query
* If no query handle is specified, the last query will be taken as reference
*
* @param mixed $result Optional query handle
* @return mixed Number of rows or false on failure
* @deprecated This method shows very poor performance and should be avoided.
*/
public function num_rows($result = null)
{
// not implemented
return false;
}
/**
* Get an associative array for one row
* If no query handle is specified, the last query will be taken as reference
*
* @param mixed $result Optional query handle
*
* @return mixed Array with col values or false on failure
*/
public function fetch_assoc($result = null)
{
return $this->_fetch_row($result, OCI_ASSOC);
}
/**
* Get an index array for one row
* If no query handle is specified, the last query will be taken as reference
*
* @param mixed $result Optional query handle
*
* @return mixed Array with col values or false on failure
*/
public function fetch_array($result = null)
{
return $this->_fetch_row($result, OCI_NUM);
}
/**
* Get col values for a result row
*
* @param mixed $result Optional query handle
* @param int $mode Fetch mode identifier
*
* @return mixed Array with col values or false on failure
*/
protected function _fetch_row($result, $mode)
{
if ($result || ($result === null && ($result = $this->last_result))) {
return oci_fetch_array($result, $mode + OCI_RETURN_NULLS + OCI_RETURN_LOBS);
}
return false;
}
/**
* Formats input so it can be safely used in a query
* PDO_OCI does not implement quote() method
*
* @param mixed $input Value to quote
* @param string $type Type of data (integer, bool, ident)
*
* @return string Quoted/converted string for use in query
*/
public function quote($input, $type = null)
{
// handle int directly for better performance
if ($type == 'integer' || $type == 'int') {
return intval($input);
}
if (is_null($input)) {
return 'NULL';
}
if ($input instanceof DateTime) {
return $this->quote($input->format($this->options['datetime_format']));
}
if ($type == 'ident') {
return $this->quote_identifier($input);
}
switch ($type) {
case 'bool':
case 'integer':
return intval($input);
default:
return "'" . strtr($input, array(
'?' => '??',
"'" => "''",
rcube_db::DEFAULT_QUOTE => rcube_db::DEFAULT_QUOTE . rcube_db::DEFAULT_QUOTE
)) . "'";
}
}
/**
* Return correct name for a specific database sequence
*
* @param string $table Table name
*
* @return string Translated sequence name
*/
protected function sequence_name($table)
{
// Note: we support only one sequence per table
// Note: The sequence name must be <table_name>_seq
$sequence = $table . '_seq';
// modify sequence name if prefix is configured
if ($prefix = $this->options['table_prefix']) {
return $prefix . $sequence;
}
return $sequence;
}
/**
* Return SQL statement for case insensitive LIKE
*
* @param string $column Field name
* @param string $value Search value
*
* @return string SQL statement to use in query
*/
public function ilike($column, $value)
{
return 'UPPER(' . $this->quote_identifier($column) . ') LIKE UPPER(' . $this->quote($value) . ')';
}
/**
* Return SQL function for current time and date
*
* @param int $interval Optional interval (in seconds) to add/subtract
*
* @return string SQL function to use in query
*/
public function now($interval = 0)
{
if ($interval) {
$interval = intval($interval);
return "current_timestamp + INTERVAL '$interval' SECOND";
}
return "current_timestamp";
}
/**
* Return SQL statement to convert a field value into a unix timestamp
*
* @param string $field Field name
*
* @return string SQL statement to use in query
* @deprecated
*/
public function unixtimestamp($field)
{
return "(($field - to_date('1970-01-01','YYYY-MM-DD')) * 60 * 60 * 24)";
}
/**
* Adds TOP (LIMIT,OFFSET) clause to the query
*
* @param string $query SQL query
* @param int $limit Number of rows
* @param int $offset Offset
*
* @return string SQL query
*/
protected function set_limit($query, $limit = 0, $offset = 0)
{
$limit = intval($limit);
$offset = intval($offset);
$end = $offset + $limit;
// @TODO: Oracle 12g has better OFFSET support
if (!$offset) {
$query = "SELECT * FROM ($query) a WHERE rownum <= $end";
}
else {
$query = "SELECT * FROM (SELECT a.*, rownum as rn FROM ($query) a WHERE rownum <= $end) b WHERE rn > $offset";
}
return $query;
}
/**
* Parse SQL file and fix table names according to table prefix
*/
protected function fix_table_names($sql)
{
if (!$this->options['table_prefix']) {
return $sql;
}
$sql = parent::fix_table_names($sql);
// replace sequence names, and other Oracle-specific commands
$sql = preg_replace_callback('/((SEQUENCE ["]?)([^" \r\n]+)/',
array($this, 'fix_table_names_callback'),
$sql
);
$sql = preg_replace_callback(
'/([ \r\n]+["]?)([^"\' \r\n\.]+)(["]?\.nextval)/',
array($this, 'fix_table_names_seq_callback'),
$sql
);
return $sql;
}
/**
* Preg_replace callback for fix_table_names()
*/
protected function fix_table_names_seq_callback($matches)
{
return $matches[1] . $this->options['table_prefix'] . $matches[2] . $matches[3];
}
/**
* Returns connection options from DSN array
*/
protected function dsn_options($dsn)
{
$params = array();
if ($dsn['hostspec']) {
$host = $dsn['hostspec'];
if ($dsn['port']) {
$host .= ':' . $dsn['port'];
}
$params['database'] = $host . '/' . $dsn['database'];
}
$params['charset'] = 'UTF8';
return $params;
}
/**
* Execute the given SQL script
*
* @param string $sql SQL queries to execute
*
* @return boolen True on success, False on error
*/
public function exec_script($sql)
{
$sql = $this->fix_table_names($sql);
$buff = '';
$body = false;
foreach (explode("\n", $sql) as $line) {
$tok = strtolower(trim($line));
if (preg_match('/^--/', $line) || $tok == '' || $tok == '/') {
continue;
}
$buff .= $line . "\n";
// detect PL/SQL function bodies, don't break on semicolon
if ($body && $tok == 'end;') {
$body = false;
}
else if (!$body && $tok == 'begin') {
$body = true;
}
if (!$body && substr($tok, -1) == ';') {
$this->query($buff);
$buff = '';
if ($this->db_error) {
break;
}
}
}
return !$this->db_error;
}
/**
* Start transaction
*
* @return bool True on success, False on failure
*/
public function startTransaction()
{
$this->db_connect('w', true);
// check connection before proceeding
if (!$this->is_connected()) {
return $this->last_result = false;
}
$this->debug('BEGIN TRANSACTION');
return $this->last_result = $this->in_transaction = true;
}
/**
* Commit transaction
*
* @return bool True on success, False on failure
*/
public function endTransaction()
{
$this->db_connect('w', true);
// check connection before proceeding
if (!$this->is_connected()) {
return $this->last_result = false;
}
$this->debug('COMMIT TRANSACTION');
if ($result = @oci_commit($this->dbh)) {
$this->in_transaction = true;
}
else {
$this->handle_error('COMMIT');
}
return $this->last_result = $result;
}
/**
* Rollback transaction
*
* @return bool True on success, False on failure
*/
public function rollbackTransaction()
{
$this->db_connect('w', true);
// check connection before proceeding
if (!$this->is_connected()) {
return $this->last_result = false;
}
$this->debug('ROLLBACK TRANSACTION');
if (@oci_rollback($this->dbh)) {
$this->in_transaction = false;
}
else {
$this->handle_error('ROLLBACK');
}
return $this->last_result = $this->dbh->rollBack();
}
/**
* Terminate database connection.
*/
public function closeConnection()
{
// release statement and close connection(s)
$this->last_result = null;
foreach ($this->dbhs as $dbh) {
oci_close($dbh);
}
parent::closeConnection();
}
}
diff --git a/program/lib/Roundcube/db/pgsql.php b/program/lib/Roundcube/db/pgsql.php
index 290c8a3e8..957d3f4e5 100644
--- a/program/lib/Roundcube/db/pgsql.php
+++ b/program/lib/Roundcube/db/pgsql.php
@@ -1,300 +1,301 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2017, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Database wrapper class that implements PHP PDO functions |
| for PostgreSQL database |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Database independent query interface
* This is a wrapper for the PHP PDO
*
* @package Framework
* @subpackage Database
*/
class rcube_db_pgsql extends rcube_db
{
public $db_provider = 'postgres';
// See https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-PARAMKEYWORDS
private static $libpq_connect_params = array("application_name", "sslmode", "sslcert", "sslkey", "sslrootcert", "sslcrl", "sslcompression", "service");
/**
* Object constructor
*
* @param string $db_dsnw DSN for read/write operations
* @param string $db_dsnr Optional DSN for read only operations
* @param bool $pconn Enables persistent connections
*/
public function __construct($db_dsnw, $db_dsnr = '', $pconn = false)
{
parent::__construct($db_dsnw, $db_dsnr, $pconn);
// use date/time input format with timezone spec.
$this->options['datetime_format'] = 'c';
}
/**
* Driver-specific configuration of database connection
*
* @param array $dsn DSN for DB connections
* @param PDO $dbh Connection handler
*/
protected function conn_configure($dsn, $dbh)
{
$dbh->query("SET NAMES 'utf8'");
$dbh->query("SET DATESTYLE TO ISO");
// if ?schema= is set in dsn, set the search_path
if ($dsn['schema']) {
$dbh->query("SET search_path TO " . $this->quote($dsn['schema']));
}
}
/**
* Get last inserted record ID
*
* @param string $table Table name (to find the incremented sequence)
*
* @return mixed ID or false on failure
*/
public function insert_id($table = null)
{
if (!$this->db_connected || $this->db_mode == 'r') {
return false;
}
if ($table) {
$table = $this->sequence_name($table);
}
$id = $this->dbh->lastInsertId($table);
return $id;
}
/**
* Return correct name for a specific database sequence
*
* @param string $table Table name
*
* @return string Translated sequence name
*/
protected function sequence_name($table)
{
// Note: we support only one sequence per table
// Note: The sequence name must be <table_name>_seq
$sequence = $table . '_seq';
// modify sequence name if prefix is configured
if ($prefix = $this->options['table_prefix']) {
return $prefix . $sequence;
}
return $sequence;
}
/**
* Return SQL statement to convert a field value into a unix timestamp
*
* @param string $field Field name
*
* @return string SQL statement to use in query
* @deprecated
*/
public function unixtimestamp($field)
{
return "EXTRACT (EPOCH FROM $field)";
}
/**
* Return SQL function for current time and date
*
* @param int $interval Optional interval (in seconds) to add/subtract
*
* @return string SQL function to use in query
*/
public function now($interval = 0)
{
if ($interval) {
$add = ' ' . ($interval > 0 ? '+' : '-') . " interval '";
$add .= $interval > 0 ? intval($interval) : intval($interval) * -1;
$add .= " seconds'";
}
return "now()" . $add;
}
/**
* Return SQL statement for case insensitive LIKE
*
* @param string $column Field name
* @param string $value Search value
*
* @return string SQL statement to use in query
*/
public function ilike($column, $value)
{
return $this->quote_identifier($column) . ' ILIKE ' . $this->quote($value);
}
/**
* Get database runtime variables
*
* @param string $varname Variable name
* @param mixed $default Default value if variable is not set
*
* @return mixed Variable value or default
*/
public function get_variable($varname, $default = null)
{
// There's a known case when max_allowed_packet is queried
// PostgreSQL doesn't have such limit, return immediately
if ($varname == 'max_allowed_packet') {
return rcube::get_instance()->config->get('db_' . $varname, $default);
}
$this->variables[$varname] = rcube::get_instance()->config->get('db_' . $varname);
if (!isset($this->variables)) {
$this->variables = array();
$result = $this->query('SHOW ALL');
while ($row = $this->fetch_array($result)) {
$this->variables[$row[0]] = $row[1];
}
}
return isset($this->variables[$varname]) ? $this->variables[$varname] : $default;
}
/**
* Returns list of tables in a database
*
* @return array List of all tables of the current database
*/
public function list_tables()
{
// get tables if not cached
if ($this->tables === null) {
if ($schema = $this->options['table_prefix']) {
$schema = str_replace('.', '', $schema);
$add = " AND TABLE_SCHEMA = " . $this->quote($schema);
}
else {
$add = " AND TABLE_SCHEMA NOT IN ('pg_catalog', 'information_schema')";
}
$q = $this->query("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES"
. " WHERE TABLE_TYPE = 'BASE TABLE'" . $add
. " ORDER BY TABLE_NAME");
$this->tables = $q ? $q->fetchAll(PDO::FETCH_COLUMN, 0) : array();
}
return $this->tables;
}
/**
* Returns list of columns in database table
*
* @param string $table Table name
*
* @return array List of table cols
*/
public function list_cols($table)
{
$args = array($table);
if ($schema = $this->options['table_prefix']) {
$add = " AND TABLE_SCHEMA = ?";
$args[] = str_replace('.', '', $schema);
}
else {
$add = " AND TABLE_SCHEMA NOT IN ('pg_catalog', 'information_schema')";
}
$q = $this->query("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS"
. " WHERE TABLE_NAME = ?" . $add, $args);
if ($q) {
return $q->fetchAll(PDO::FETCH_COLUMN, 0);
}
return array();
}
/**
* Returns PDO DSN string from DSN array
*
* @param array $dsn DSN parameters
*
* @return string DSN string
*/
protected function dsn_string($dsn)
{
$params = array();
$result = 'pgsql:';
if ($dsn['hostspec']) {
$params[] = 'host=' . $dsn['hostspec'];
}
else if ($dsn['socket']) {
$params[] = 'host=' . $dsn['socket'];
}
if ($dsn['port']) {
$params[] = 'port=' . $dsn['port'];
}
if ($dsn['database']) {
$params[] = 'dbname=' . $dsn['database'];
}
foreach (self::$libpq_connect_params as $param) {
if ($dsn[$param]) {
$params[] = $param . '=' . $dsn[$param];
}
}
if (!empty($params)) {
$result .= implode(';', $params);
}
return $result;
}
/**
* Parse SQL file and fix table names according to table prefix
*/
protected function fix_table_names($sql)
{
if (!$this->options['table_prefix']) {
return $sql;
}
$sql = parent::fix_table_names($sql);
// replace sequence names, and other postgres-specific commands
$sql = preg_replace_callback(
'/((SEQUENCE |RENAME TO |nextval\()["\']*)([^"\' \r\n]+)/',
array($this, 'fix_table_names_callback'),
$sql
);
return $sql;
}
}
diff --git a/program/lib/Roundcube/db/sqlite.php b/program/lib/Roundcube/db/sqlite.php
index b66c56097..546b6e9b0 100644
--- a/program/lib/Roundcube/db/sqlite.php
+++ b/program/lib/Roundcube/db/sqlite.php
@@ -1,161 +1,162 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2012, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Database wrapper class that implements PHP PDO functions |
| for SQLite database |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Database independent query interface
* This is a wrapper for the PHP PDO
*
* @package Framework
* @subpackage Database
*/
class rcube_db_sqlite extends rcube_db
{
public $db_provider = 'sqlite';
/**
* Prepare connection
*/
protected function conn_prepare($dsn)
{
// Create database file, required by PDO to exist on connection
if (!empty($dsn['database']) && !file_exists($dsn['database'])) {
$created = touch($dsn['database']);
// File mode setting, for compat. with MDB2
if (!empty($dsn['mode']) && $created) {
chmod($dsn['database'], octdec($dsn['mode']));
}
}
}
/**
* Configure connection, create database if not exists
*/
protected function conn_configure($dsn, $dbh)
{
// Initialize database structure in file is empty
if (!empty($dsn['database']) && !filesize($dsn['database'])) {
$data = file_get_contents(RCUBE_INSTALL_PATH . 'SQL/sqlite.initial.sql');
if (strlen($data)) {
$this->debug('INITIALIZE DATABASE');
$q = $dbh->exec($data);
if ($q === false) {
$error = $dbh->errorInfo();
$this->db_error = true;
$this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]);
rcube::raise_error(array('code' => 500, 'type' => 'db',
'line' => __LINE__, 'file' => __FILE__,
'message' => $this->db_error_msg), true, false);
}
}
}
}
/**
* Return SQL statement to convert a field value into a unix timestamp
*
* @param string $field Field name
*
* @return string SQL statement to use in query
* @deprecated
*/
public function unixtimestamp($field)
{
return "strftime('%s', $field)";
}
/**
* Return SQL function for current time and date
*
* @param int $interval Optional interval (in seconds) to add/subtract
*
* @return string SQL function to use in query
*/
public function now($interval = 0)
{
if ($interval) {
$add = ($interval > 0 ? '+' : '') . intval($interval) . ' seconds';
}
return "datetime('now'" . ($add ? ",'$add'" : "") . ")";
}
/**
* Returns list of tables in database
*
* @return array List of all tables of the current database
*/
public function list_tables()
{
if ($this->tables === null) {
$q = $this->query('SELECT name FROM sqlite_master'
.' WHERE type = \'table\' ORDER BY name');
$this->tables = $q ? $q->fetchAll(PDO::FETCH_COLUMN, 0) : array();
}
return $this->tables;
}
/**
* Returns list of columns in database table
*
* @param string $table Table name
*
* @return array List of table cols
*/
public function list_cols($table)
{
$q = $this->query('SELECT sql FROM sqlite_master WHERE type = ? AND name = ?',
array('table', $table));
$columns = array();
if ($sql = $this->fetch_array($q)) {
$sql = $sql[0];
$start_pos = strpos($sql, '(');
$end_pos = strrpos($sql, ')');
$sql = substr($sql, $start_pos+1, $end_pos-$start_pos-1);
$lines = explode(',', $sql);
foreach ($lines as $line) {
$line = explode(' ', trim($line));
if ($line[0] && strpos($line[0], '--') !== 0) {
$column = $line[0];
$columns[] = trim($column, '"');
}
}
}
return $columns;
}
/**
* Build DSN string for PDO constructor
*/
protected function dsn_string($dsn)
{
return $dsn['phptype'] . ':' . $dsn['database'];
}
}
diff --git a/program/lib/Roundcube/db/sqlsrv.php b/program/lib/Roundcube/db/sqlsrv.php
index 7b64ccea2..7d561e2d7 100644
--- a/program/lib/Roundcube/db/sqlsrv.php
+++ b/program/lib/Roundcube/db/sqlsrv.php
@@ -1,57 +1,58 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2012, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Database wrapper class that implements PHP PDO functions |
| for MS SQL Server database |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Database independent query interface
* This is a wrapper for the PHP PDO
*
* @package Framework
* @subpackage Database
*/
class rcube_db_sqlsrv extends rcube_db_mssql
{
/**
* Returns PDO DSN string from DSN array
*/
protected function dsn_string($dsn)
{
$params = array();
$result = 'sqlsrv:';
if ($dsn['hostspec']) {
$host = $dsn['hostspec'];
if ($dsn['port']) {
$host .= ',' . $dsn['port'];
}
$params[] = 'Server=' . $host;
}
if ($dsn['database']) {
$params[] = 'Database=' . $dsn['database'];
}
if (!empty($params)) {
$result .= implode(';', $params);
}
return $result;
}
}
diff --git a/program/lib/Roundcube/html.php b/program/lib/Roundcube/html.php
index 1b175b3fd..71ca68e3e 100644
--- a/program/lib/Roundcube/html.php
+++ b/program/lib/Roundcube/html.php
@@ -1,985 +1,986 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Helper class to create valid XHTML code |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Class for HTML code creation
*
* @package Framework
* @subpackage View
*/
class html
{
protected $tagname;
protected $content;
protected $attrib = array();
protected $allowed = array();
public static $doctype = 'xhtml';
public static $lc_tags = true;
public static $common_attrib = array('id','class','style','title','align','unselectable','tabindex','role');
public static $containers = array('iframe','div','span','p','h1','h2','h3','ul','form','textarea','table','thead','tbody','tr','th','td','style','script','a');
public static $bool_attrib = array('checked','multiple','disabled','selected','autofocus','readonly','required');
/**
* Constructor
*
* @param array $attrib Hash array with tag attributes
*/
public function __construct($attrib = array())
{
if (is_array($attrib)) {
$this->attrib = $attrib;
}
}
/**
* Return the tag code
*
* @return string The finally composed HTML tag
*/
public function show()
{
return self::tag($this->tagname, $this->attrib, $this->content, array_merge(self::$common_attrib, $this->allowed));
}
/****** STATIC METHODS *******/
/**
* Generic method to create a HTML tag
*
* @param string $tagname Tag name
* @param array $attrib Tag attributes as key/value pairs
* @param string $content Optional Tag content (creates a container tag)
* @param array $allowed List with allowed attributes, omit to allow all
*
* @return string The XHTML tag
*/
public static function tag($tagname, $attrib = array(), $content = null, $allowed = null)
{
if (is_string($attrib)) {
$attrib = array('class' => $attrib);
}
$inline_tags = array('a','span','img');
$suffix = $attrib['nl'] || ($content && $attrib['nl'] !== false && !in_array($tagname, $inline_tags)) ? "\n" : '';
$tagname = self::$lc_tags ? strtolower($tagname) : $tagname;
if (isset($content) || in_array($tagname, self::$containers)) {
$suffix = $attrib['noclose'] ? $suffix : '</' . $tagname . '>' . $suffix;
unset($attrib['noclose'], $attrib['nl']);
return '<' . $tagname . self::attrib_string($attrib, $allowed) . '>' . $content . $suffix;
}
else {
return '<' . $tagname . self::attrib_string($attrib, $allowed) . '>' . $suffix;
}
}
/**
* Return DOCTYPE tag of specified type
*
* @param string $type Document type (html5, xhtml, 'xhtml-trans, xhtml-strict)
*/
public static function doctype($type)
{
$doctypes = array(
'html5' => '<!DOCTYPE html>',
'xhtml' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
'xhtml-trans' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
'xhtml-strict' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
);
if ($doctypes[$type]) {
self::$doctype = preg_replace('/-\w+$/', '', $type);
return $doctypes[$type];
}
return '';
}
/**
* Derrived method for <div> containers
*
* @param mixed $attr Hash array with tag attributes or string with class name
* @param string $cont Div content
*
* @return string HTML code
* @see html::tag()
*/
public static function div($attr = null, $cont = null)
{
if (is_string($attr)) {
$attr = array('class' => $attr);
}
return self::tag('div', $attr, $cont, array_merge(self::$common_attrib, array('onclick')));
}
/**
* Derrived method for <p> blocks
*
* @param mixed $attr Hash array with tag attributes or string with class name
* @param string $cont Paragraph content
*
* @return string HTML code
* @see html::tag()
*/
public static function p($attr = null, $cont = null)
{
if (is_string($attr)) {
$attr = array('class' => $attr);
}
return self::tag('p', $attr, $cont, self::$common_attrib);
}
/**
* Derrived method to create <img />
*
* @param mixed $attr Hash array with tag attributes or string with image source (src)
*
* @return string HTML code
* @see html::tag()
*/
public static function img($attr = null)
{
if (is_string($attr)) {
$attr = array('src' => $attr);
}
return self::tag('img', $attr + array('alt' => ''), null, array_merge(self::$common_attrib,
array('src','alt','width','height','border','usemap','onclick','onerror','onload')));
}
/**
* Derrived method for link tags
*
* @param mixed $attr Hash array with tag attributes or string with link location (href)
* @param string $cont Link content
*
* @return string HTML code
* @see html::tag()
*/
public static function a($attr, $cont)
{
if (is_string($attr)) {
$attr = array('href' => $attr);
}
return self::tag('a', $attr, $cont, array_merge(self::$common_attrib,
array('href','target','name','rel','onclick','onmouseover','onmouseout','onmousedown','onmouseup')));
}
/**
* Derrived method for inline span tags
*
* @param mixed $attr Hash array with tag attributes or string with class name
* @param string $cont Tag content
*
* @return string HTML code
* @see html::tag()
*/
public static function span($attr, $cont)
{
if (is_string($attr)) {
$attr = array('class' => $attr);
}
return self::tag('span', $attr, $cont, self::$common_attrib);
}
/**
* Derrived method for form element labels
*
* @param mixed $attr Hash array with tag attributes or string with 'for' attrib
* @param string $cont Tag content
*
* @return string HTML code
* @see html::tag()
*/
public static function label($attr, $cont)
{
if (is_string($attr)) {
$attr = array('for' => $attr);
}
return self::tag('label', $attr, $cont, array_merge(self::$common_attrib,
array('for','onkeypress')));
}
/**
* Derrived method to create <iframe></iframe>
*
* @param mixed $attr Hash array with tag attributes or string with frame source (src)
*
* @return string HTML code
* @see html::tag()
*/
public static function iframe($attr = null, $cont = null)
{
if (is_string($attr)) {
$attr = array('src' => $attr);
}
return self::tag('iframe', $attr, $cont, array_merge(self::$common_attrib,
array('src','name','width','height','border','frameborder','onload','allowfullscreen')));
}
/**
* Derrived method to create <script> tags
*
* @param mixed $attr Hash array with tag attributes or string with script source (src)
* @param string $cont Javascript code to be placed as tag content
*
* @return string HTML code
* @see html::tag()
*/
public static function script($attr, $cont = null)
{
if (is_string($attr)) {
$attr = array('src' => $attr);
}
if ($cont) {
if (self::$doctype == 'xhtml')
$cont = "\n/* <![CDATA[ */\n" . $cont . "\n/* ]]> */\n";
else
$cont = "\n" . $cont . "\n";
}
return self::tag('script', $attr + array('type' => 'text/javascript', 'nl' => true),
$cont, array_merge(self::$common_attrib, array('src','type','charset')));
}
/**
* Derrived method for line breaks
*
* @param array $attrib Associative arry with tag attributes
*
* @return string HTML code
* @see html::tag()
*/
public static function br($attrib = array())
{
return self::tag('br', $attrib);
}
/**
* Create string with attributes
*
* @param array $attrib Associative array with tag attributes
* @param array $allowed List of allowed attributes
*
* @return string Valid attribute string
*/
public static function attrib_string($attrib = array(), $allowed = null)
{
if (empty($attrib)) {
return '';
}
$allowed_f = array_flip((array)$allowed);
$attrib_arr = array();
foreach ($attrib as $key => $value) {
// skip size if not numeric
if ($key == 'size' && !is_numeric($value)) {
continue;
}
// ignore "internal" or empty attributes
if ($key == 'nl' || $value === null) {
continue;
}
// ignore not allowed attributes, except aria-* and data-*
if (!empty($allowed)) {
$is_data_attr = @substr_compare($key, 'data-', 0, 5) === 0;
$is_aria_attr = @substr_compare($key, 'aria-', 0, 5) === 0;
if (!$is_aria_attr && !$is_data_attr && !isset($allowed_f[$key])) {
continue;
}
}
// skip empty eventhandlers
if (preg_match('/^on[a-z]+/', $key) && !$value) {
continue;
}
// attributes with no value
if (in_array($key, self::$bool_attrib)) {
if ($value) {
$value = $key;
if (self::$doctype == 'xhtml') {
$value .= '="' . $value . '"';
}
$attrib_arr[] = $value;
}
}
else {
$attrib_arr[] = $key . '="' . self::quote($value) . '"';
}
}
return count($attrib_arr) ? ' '.implode(' ', $attrib_arr) : '';
}
/**
* Convert a HTML attribute string attributes to an associative array (name => value)
*
* @param string $str Input string
*
* @return array Key-value pairs of parsed attributes
*/
public static function parse_attrib_string($str)
{
$attrib = array();
$html = '<html>'
. '<head><meta http-equiv="Content-Type" content="text/html; charset=' . RCUBE_CHARSET . '" /></head>'
. '<body><div ' . rtrim($str, '/ ') . ' /></body>'
. '</html>';
$document = new DOMDocument('1.0', RCUBE_CHARSET);
@$document->loadHTML($html);
if ($node = $document->getElementsByTagName('div')->item(0)) {
foreach ($node->attributes as $name => $attr) {
$attrib[strtolower($name)] = $attr->nodeValue;
}
}
return $attrib;
}
/**
* Replacing specials characters in html attribute value
*
* @param string $str Input string
*
* @return string The quoted string
*/
public static function quote($str)
{
return @htmlspecialchars($str, ENT_COMPAT | ENT_SUBSTITUTE, RCUBE_CHARSET);
}
}
/**
* Class to create an HTML input field
*
* @package Framework
* @subpackage View
*/
class html_inputfield extends html
{
protected $tagname = 'input';
protected $type = 'text';
protected $allowed = array(
'type','name','value','size','tabindex','autocapitalize','required',
'autocomplete','checked','onchange','onclick','disabled','readonly',
'spellcheck','results','maxlength','src','multiple','accept',
'placeholder','autofocus','pattern','oninput'
);
/**
* Object constructor
*
* @param array $attrib Associative array with tag attributes
*/
public function __construct($attrib = array())
{
if (is_array($attrib)) {
$this->attrib = $attrib;
}
if ($attrib['type']) {
$this->type = $attrib['type'];
}
}
/**
* Compose input tag
*
* @param string $value Field value
* @param array $attrib Additional attributes to override
*
* @return string HTML output
*/
public function show($value = null, $attrib = null)
{
// overwrite object attributes
if (is_array($attrib)) {
$this->attrib = array_merge($this->attrib, $attrib);
}
// set value attribute
if ($value !== null) {
$this->attrib['value'] = $value;
}
// set type
$this->attrib['type'] = $this->type;
return parent::show();
}
}
/**
* Class to create an HTML password field
*
* @package Framework
* @subpackage View
*/
class html_passwordfield extends html_inputfield
{
protected $type = 'password';
}
/**
* Class to create an hidden HTML input field
*
* @package Framework
* @subpackage View
*/
class html_hiddenfield extends html
{
protected $tagname = 'input';
protected $type = 'hidden';
protected $allowed = array('type','name','value','onchange','disabled','readonly');
protected $fields = array();
/**
* Constructor
*
* @param array $attrib Named tag attributes
*/
public function __construct($attrib = null)
{
if (is_array($attrib)) {
$this->add($attrib);
}
}
/**
* Add a hidden field to this instance
*
* @param array $attrib Named tag attributes
*/
public function add($attrib)
{
$this->fields[] = $attrib;
}
/**
* Create HTML code for the hidden fields
*
* @return string Final HTML code
*/
public function show()
{
$out = '';
foreach ($this->fields as $attrib) {
$out .= self::tag($this->tagname, array('type' => $this->type) + $attrib);
}
return $out;
}
}
/**
* Class to create HTML radio buttons
*
* @package Framework
* @subpackage View
*/
class html_radiobutton extends html_inputfield
{
protected $type = 'radio';
/**
* Get HTML code for this object
*
* @param string $value Value of the checked field
* @param array $attrib Additional attributes to override
*
* @return string HTML output
*/
public function show($value = '', $attrib = null)
{
// overwrite object attributes
if (is_array($attrib)) {
$this->attrib = array_merge($this->attrib, $attrib);
}
// set value attribute
$this->attrib['checked'] = ((string)$value == (string)$this->attrib['value']);
return parent::show();
}
}
/**
* Class to create HTML checkboxes
*
* @package Framework
* @subpackage View
*/
class html_checkbox extends html_inputfield
{
protected $type = 'checkbox';
/**
* Get HTML code for this object
*
* @param string $value Value of the checked field
* @param array $attrib Additional attributes to override
*
* @return string HTML output
*/
public function show($value = '', $attrib = null)
{
// overwrite object attributes
if (is_array($attrib)) {
$this->attrib = array_merge($this->attrib, $attrib);
}
// set value attribute
$this->attrib['checked'] = ((string)$value == (string)$this->attrib['value']);
return parent::show();
}
}
/**
* Class to create HTML button
*
* @package Framework
* @subpackage View
*/
class html_button extends html_inputfield
{
protected $tagname = 'button';
protected $type = 'button';
/**
* Get HTML code for this object
*
* @param string $content Text Content of the button
* @param array $attrib Additional attributes to override
*
* @return string HTML output
*/
public function show($content = '', $attrib = null)
{
// overwrite object attributes
if (is_array($attrib)) {
$this->attrib = array_merge($this->attrib, $attrib);
}
$this->content = $content;
return parent::show();
}
}
/**
* Class to create an HTML textarea
*
* @package Framework
* @subpackage View
*/
class html_textarea extends html
{
protected $tagname = 'textarea';
protected $allowed = array('name','rows','cols','wrap','tabindex',
'onchange','disabled','readonly','spellcheck');
/**
* Get HTML code for this object
*
* @param string $value Textbox value
* @param array $attrib Additional attributes to override
*
* @return string HTML output
*/
public function show($value = '', $attrib = null)
{
// overwrite object attributes
if (is_array($attrib)) {
$this->attrib = array_merge($this->attrib, $attrib);
}
// take value attribute as content
if (empty($value) && !empty($this->attrib['value'])) {
$value = $this->attrib['value'];
}
// make shure we don't print the value attribute
if (isset($this->attrib['value'])) {
unset($this->attrib['value']);
}
if (!empty($value) && empty($this->attrib['is_escaped'])) {
$value = self::quote($value);
}
return self::tag($this->tagname, $this->attrib, $value,
array_merge(self::$common_attrib, $this->allowed));
}
}
/**
* Builder for HTML drop-down menus
* Syntax:<pre>
* // create instance. arguments are used to set attributes of select-tag
* $select = new html_select(array('name' => 'fieldname'));
*
* // add one option
* $select->add('Switzerland', 'CH');
*
* // add multiple options
* $select->add(array('Switzerland','Germany'), array('CH','DE'));
*
* // generate pulldown with selection 'Switzerland' and return html-code
* // as second argument the same attributes available to instantiate can be used
* print $select->show('CH');
* </pre>
*
* @package Framework
* @subpackage View
*/
class html_select extends html
{
protected $tagname = 'select';
protected $options = array();
protected $allowed = array('name','size','tabindex','autocomplete',
'multiple','onchange','disabled','rel');
/**
* Add a new option to this drop-down
*
* @param mixed $names Option name or array with option names
* @param mixed $values Option value or array with option values
* @param array $attrib Additional attributes for the option entry
*/
public function add($names, $values = null, $attrib = array())
{
if (is_array($names)) {
foreach ($names as $i => $text) {
$this->options[] = array('text' => $text, 'value' => $values[$i]) + $attrib;
}
}
else {
$this->options[] = array('text' => $names, 'value' => $values) + $attrib;
}
}
/**
* Get HTML code for this object
*
* @param string $select Value of the selection option
* @param array $attrib Additional attributes to override
*
* @return string HTML output
*/
public function show($select = array(), $attrib = null)
{
// overwrite object attributes
if (is_array($attrib)) {
$this->attrib = array_merge($this->attrib, $attrib);
}
$this->content = "\n";
$select = (array)$select;
foreach ($this->options as $option) {
$attr = array(
'value' => $option['value'],
'selected' => (in_array($option['value'], $select, true) ||
in_array($option['text'], $select, true)) ? 1 : null);
$option_content = $option['text'];
if (empty($this->attrib['is_escaped'])) {
$option_content = self::quote($option_content);
}
$this->content .= self::tag('option', $attr + $option, $option_content, array('value','label','class','style','title','disabled','selected'));
}
return parent::show();
}
}
/**
* Class to build an HTML table
*
* @package Framework
* @subpackage View
*/
class html_table extends html
{
protected $tagname = 'table';
protected $allowed = array('id','class','style','width','summary',
'cellpadding','cellspacing','border');
private $header = array();
private $rows = array();
private $rowindex = 0;
private $colindex = 0;
/**
* Constructor
*
* @param array $attrib Named tag attributes
*/
public function __construct($attrib = array())
{
$default_attrib = self::$doctype == 'xhtml' ? array('summary' => '', 'border' => '0') : array();
$this->attrib = array_merge($attrib, $default_attrib);
if (!empty($attrib['tagname']) && $attrib['tagname'] != 'table') {
$this->tagname = $attrib['tagname'];
$this->allowed = self::$common_attrib;
}
}
/**
* Add a table cell
*
* @param array $attr Cell attributes
* @param string $cont Cell content
*/
public function add($attr, $cont)
{
if (is_string($attr)) {
$attr = array('class' => $attr);
}
$cell = new stdClass;
$cell->attrib = $attr;
$cell->content = $cont;
$this->rows[$this->rowindex]->cells[$this->colindex] = $cell;
$this->colindex += max(1, intval($attr['colspan']));
if ($this->attrib['cols'] && $this->colindex >= $this->attrib['cols']) {
$this->add_row();
}
}
/**
* Add a table header cell
*
* @param array $attr Cell attributes
* @param string $cont Cell content
*/
public function add_header($attr, $cont)
{
if (is_string($attr)) {
$attr = array('class' => $attr);
}
$cell = new stdClass;
$cell->attrib = $attr;
$cell->content = $cont;
$this->header[] = $cell;
}
/**
* Remove a column from a table
* Useful for plugins making alterations
*
* @param string $class Class name
*/
public function remove_column($class)
{
// Remove the header
foreach ($this->header as $index => $header){
if ($header->attrib['class'] == $class){
unset($this->header[$index]);
break;
}
}
// Remove cells from rows
foreach ($this->rows as $i => $row){
foreach ($row->cells as $j => $cell){
if ($cell->attrib['class'] == $class){
unset($this->rows[$i]->cells[$j]);
break;
}
}
}
}
/**
* Jump to next row
*
* @param array $attr Row attributes
*/
public function add_row($attr = array())
{
$this->rowindex++;
$this->colindex = 0;
$this->rows[$this->rowindex] = new stdClass;
$this->rows[$this->rowindex]->attrib = $attr;
$this->rows[$this->rowindex]->cells = array();
}
/**
* Set row attributes
*
* @param array $attr Row attributes
* @param int $index Optional row index (default current row index)
*/
public function set_row_attribs($attr = array(), $index = null)
{
if (is_string($attr)) {
$attr = array('class' => $attr);
}
if ($index === null) {
$index = $this->rowindex;
}
// make sure row object exists (#1489094)
if (!$this->rows[$index]) {
$this->rows[$index] = new stdClass;
}
$this->rows[$index]->attrib = $attr;
}
/**
* Get row attributes
*
* @param int $index Row index
*
* @return array Row attributes
*/
public function get_row_attribs($index = null)
{
if ($index === null) {
$index = $this->rowindex;
}
return $this->rows[$index] ? $this->rows[$index]->attrib : null;
}
/**
* Build HTML output of the table data
*
* @param array $attrib Table attributes
*
* @return string The final table HTML code
*/
public function show($attrib = null)
{
if (is_array($attrib)) {
$this->attrib = array_merge($this->attrib, $attrib);
}
$thead = '';
$tbody = '';
$col_tagname = $this->_col_tagname();
$row_tagname = $this->_row_tagname();
$head_tagname = $this->_head_tagname();
// include <thead>
if (!empty($this->header)) {
$rowcontent = '';
foreach ($this->header as $c => $col) {
$rowcontent .= self::tag($head_tagname, $col->attrib, $col->content);
}
$thead = $this->tagname == 'table' ? self::tag('thead', null, self::tag('tr', null, $rowcontent, parent::$common_attrib)) :
self::tag($row_tagname, array('class' => 'thead'), $rowcontent, parent::$common_attrib);
}
foreach ($this->rows as $r => $row) {
$rowcontent = '';
foreach ($row->cells as $c => $col) {
if ($row_tagname == 'li' && empty($col->attrib) && count($row->cells) == 1) {
$rowcontent .= $col->content;
}
else {
$rowcontent .= self::tag($col_tagname, $col->attrib, $col->content);
}
}
if ($r < $this->rowindex || count($row->cells)) {
$tbody .= self::tag($row_tagname, $row->attrib, $rowcontent, parent::$common_attrib);
}
}
if ($this->attrib['rowsonly']) {
return $tbody;
}
// add <tbody>
$this->content = $thead . ($this->tagname == 'table' ? self::tag('tbody', null, $tbody) : $tbody);
unset($this->attrib['cols'], $this->attrib['rowsonly']);
return parent::show();
}
/**
* Count number of rows
*
* @return The number of rows
*/
public function size()
{
return count($this->rows);
}
/**
* Remove table body (all rows)
*/
public function remove_body()
{
$this->rows = array();
$this->rowindex = 0;
}
/**
* Getter for the corresponding tag name for table row elements
*/
private function _row_tagname()
{
static $row_tagnames = array('table' => 'tr', 'ul' => 'li', '*' => 'div');
return $row_tagnames[$this->tagname] ?: $row_tagnames['*'];
}
/**
* Getter for the corresponding tag name for table row elements
*/
private function _head_tagname()
{
static $head_tagnames = array('table' => 'th', '*' => 'span');
return $head_tagnames[$this->tagname] ?: $head_tagnames['*'];
}
/**
* Getter for the corresponding tag name for table cell elements
*/
private function _col_tagname()
{
static $col_tagnames = array('table' => 'td', '*' => 'span');
return $col_tagnames[$this->tagname] ?: $col_tagnames['*'];
}
}
diff --git a/program/lib/Roundcube/rcube.php b/program/lib/Roundcube/rcube.php
index 2f77ea05d..af44fdf3e 100644
--- a/program/lib/Roundcube/rcube.php
+++ b/program/lib/Roundcube/rcube.php
@@ -1,1749 +1,1750 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2014, The Roundcube Dev Team |
- | Copyright (C) 2011-2014, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Framework base class providing core functions and holding |
| instances of all 'global' objects like db- and storage-connections |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Base class of the Roundcube Framework
* implemented as singleton
*
* @package Framework
* @subpackage Core
*/
class rcube
{
// Init options
const INIT_WITH_DB = 1;
const INIT_WITH_PLUGINS = 2;
// Request status
const REQUEST_VALID = 0;
const REQUEST_ERROR_URL = 1;
const REQUEST_ERROR_TOKEN = 2;
const DEBUG_LINE_LENGTH = 4096;
/**
* Singleton instance of rcube
*
* @var rcube
*/
static protected $instance;
/**
* Stores instance of rcube_config.
*
* @var rcube_config
*/
public $config;
/**
* Instance of database class.
*
* @var rcube_db
*/
public $db;
/**
* Instance of Memcache class.
*
* @var Memcache
*/
public $memcache;
/**
* Instance of Redis class.
*
* @var Redis
*/
public $redis;
/**
* Instance of rcube_session class.
*
* @var rcube_session
*/
public $session;
/**
* Instance of rcube_smtp class.
*
* @var rcube_smtp
*/
public $smtp;
/**
* Instance of rcube_storage class.
*
* @var rcube_storage
*/
public $storage;
/**
* Instance of rcube_output class.
*
* @var rcube_output
*/
public $output;
/**
* Instance of rcube_plugin_api.
*
* @var rcube_plugin_api
*/
public $plugins;
/**
* Instance of rcube_user class.
*
* @var rcube_user
*/
public $user;
/**
* Request status
*
* @var int
*/
public $request_status = 0;
/* private/protected vars */
protected $texts;
protected $caches = array();
protected $shutdown_functions = array();
/**
* This implements the 'singleton' design pattern
*
* @param integer $mode Options to initialize with this instance. See rcube::INIT_WITH_* constants
* @param string $env Environment name to run (e.g. live, dev, test)
*
* @return rcube The one and only instance
*/
static function get_instance($mode = 0, $env = '')
{
if (!self::$instance) {
self::$instance = new rcube($env);
self::$instance->init($mode);
}
return self::$instance;
}
/**
* Private constructor
*/
protected function __construct($env = '')
{
// load configuration
$this->config = new rcube_config($env);
$this->plugins = new rcube_dummy_plugin_api;
register_shutdown_function(array($this, 'shutdown'));
}
/**
* Initial startup function
*/
protected function init($mode = 0)
{
// 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);
}
// connect to database
if ($mode & self::INIT_WITH_DB) {
$this->get_dbh();
}
// create plugin API and load plugins
if ($mode & self::INIT_WITH_PLUGINS) {
$this->plugins = rcube_plugin_api::get_instance();
}
}
/**
* Get the current database connection
*
* @return rcube_db Database object
*/
public function get_dbh()
{
if (!$this->db) {
$this->db = rcube_db::factory(
$this->config->get('db_dsnw'),
$this->config->get('db_dsnr'),
$this->config->get('db_persistent')
);
$this->db->set_debug((bool)$this->config->get('sql_debug'));
}
return $this->db;
}
/**
* Get global handle for memcache access
*
* @return object Memcache
*/
public function get_memcache()
{
if (!isset($this->memcache)) {
$this->memcache = rcube_cache_memcache::engine();
}
return $this->memcache;
}
/**
* Get global handle for redis access
*
* @return object Redis
*/
public function get_redis()
{
if (!isset($this->redis)) {
$this->redis = rcube_cache_redis::engine();
}
return $this->redis;
}
/**
* Initialize and get user cache object
*
* @param string $name Cache identifier
* @param string $type Cache type ('db', 'apc', 'memcache', 'redis')
* @param string $ttl Expiration time for cache items
* @param bool $packed Enables/disables data serialization
*
* @return rcube_cache User cache object
*/
public function get_cache($name, $type='db', $ttl=0, $packed=true)
{
if (!isset($this->caches[$name]) && ($userid = $this->get_user_id())) {
$this->caches[$name] = rcube_cache::factory($type, $userid, $name, $ttl, $packed);
}
return $this->caches[$name];
}
/**
* Initialize and get shared cache object
*
* @param string $name Cache identifier
* @param bool $packed Enables/disables data serialization
*
* @return rcube_cache Shared cache object
*/
public function get_cache_shared($name, $packed = true)
{
$shared_name = "shared_$name";
if (!array_key_exists($shared_name, $this->caches)) {
$opt = strtolower($name) . '_cache';
$type = $this->config->get($opt);
$ttl = $this->config->get($opt . '_ttl');
if (!$type) {
// cache is disabled
return $this->caches[$shared_name] = null;
}
if ($ttl === null) {
$ttl = $this->config->get('shared_cache_ttl', '10d');
}
$this->caches[$shared_name] = rcube_cache::factory($type, null, $name, $ttl, $packed);
}
return $this->caches[$shared_name];
}
/**
* Create SMTP object and connect to server
*
* @param boolean $connect True if connection should be established
*/
public function smtp_init($connect = false)
{
$this->smtp = new rcube_smtp();
if ($connect) {
$this->smtp->connect();
}
}
/**
* Initialize and get storage object
*
* @return rcube_storage Storage object
*/
public function get_storage()
{
// already initialized
if (!is_object($this->storage)) {
$this->storage_init();
}
return $this->storage;
}
/**
* Initialize storage object
*/
public function storage_init()
{
// already initialized
if (is_object($this->storage)) {
return;
}
$driver = $this->config->get('storage_driver', 'imap');
$driver_class = "rcube_{$driver}";
if (!class_exists($driver_class)) {
self::raise_error(array(
'code' => 700, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Storage driver class ($driver) not found!"),
true, true);
}
// Initialize storage object
$this->storage = new $driver_class;
// for backward compat. (deprecated, will be removed)
$this->imap = $this->storage;
// set class options
$options = array(
'auth_type' => $this->config->get("{$driver}_auth_type", 'check'),
'auth_cid' => $this->config->get("{$driver}_auth_cid"),
'auth_pw' => $this->config->get("{$driver}_auth_pw"),
'debug' => (bool) $this->config->get("{$driver}_debug"),
'force_caps' => (bool) $this->config->get("{$driver}_force_caps"),
'disabled_caps' => $this->config->get("{$driver}_disabled_caps"),
'socket_options' => $this->config->get("{$driver}_conn_options"),
'timeout' => (int) $this->config->get("{$driver}_timeout"),
'skip_deleted' => (bool) $this->config->get('skip_deleted'),
'driver' => $driver,
);
if (!empty($_SESSION['storage_host'])) {
$options['language'] = $_SESSION['language'];
$options['host'] = $_SESSION['storage_host'];
$options['user'] = $_SESSION['username'];
$options['port'] = $_SESSION['storage_port'];
$options['ssl'] = $_SESSION['storage_ssl'];
$options['password'] = $this->decrypt($_SESSION['password']);
$_SESSION[$driver.'_host'] = $_SESSION['storage_host'];
}
$options = $this->plugins->exec_hook("storage_init", $options);
// for backward compat. (deprecated, to be removed)
$options = $this->plugins->exec_hook("imap_init", $options);
$this->storage->set_options($options);
$this->set_storage_prop();
// subscribe to 'storage_connected' hook for session logging
if ($this->config->get('imap_log_session', false)) {
$this->plugins->register_hook('storage_connected', array($this, 'storage_log_session'));
}
}
/**
* Set storage parameters.
*/
protected function set_storage_prop()
{
$storage = $this->get_storage();
// set pagesize from config
$pagesize = $this->config->get('mail_pagesize');
if (!$pagesize) {
$pagesize = $this->config->get('pagesize', 50);
}
$storage->set_pagesize($pagesize);
$storage->set_charset($this->config->get('default_charset', RCUBE_CHARSET));
// enable caching of mail data
$driver = $this->config->get('storage_driver', 'imap');
$storage_cache = $this->config->get("{$driver}_cache");
$messages_cache = $this->config->get('messages_cache');
// for backward compatybility
if ($storage_cache === null && $messages_cache === null && $this->config->get('enable_caching')) {
$storage_cache = 'db';
$messages_cache = true;
}
if ($storage_cache) {
$storage->set_caching($storage_cache);
}
if ($messages_cache) {
$storage->set_messages_caching(true);
}
}
/**
* Set special folders type association.
* This must be done AFTER connecting to the server!
*/
protected function set_special_folders()
{
$storage = $this->get_storage();
$folders = $storage->get_special_folders(true);
$prefs = array();
// check SPECIAL-USE flags on IMAP folders
foreach ($folders as $type => $folder) {
$idx = $type . '_mbox';
if ($folder !== $this->config->get($idx)) {
$prefs[$idx] = $folder;
}
}
// Some special folders differ, update user preferences
if (!empty($prefs) && $this->user) {
$this->user->save_prefs($prefs);
}
// create default folders (on login)
if ($this->config->get('create_default_folders')) {
$storage->create_default_folders();
}
}
/**
* Callback for IMAP connection events to log session identifiers
*/
public function storage_log_session($args)
{
if (!empty($args['session']) && session_id()) {
$this->write_log('imap_session', $args['session']);
}
}
/**
* Create session object and start the session.
*/
public function session_init()
{
// session started (Installer?)
if (session_id()) {
return;
}
$storage = $this->config->get('session_storage', 'db');
$sess_name = $this->config->get('session_name');
$sess_domain = $this->config->get('session_domain');
$sess_path = $this->config->get('session_path');
$lifetime = $this->config->get('session_lifetime', 0) * 60;
$is_secure = $this->config->get('use_https') || rcube_utils::https_check();
// set session domain
if ($sess_domain) {
ini_set('session.cookie_domain', $sess_domain);
}
// set session path
if ($sess_path) {
ini_set('session.cookie_path', $sess_path);
}
// set session garbage collecting time according to session_lifetime
if ($lifetime) {
ini_set('session.gc_maxlifetime', $lifetime * 2);
}
// set session cookie lifetime so it never expires (#5961)
ini_set('session.cookie_lifetime', 0);
ini_set('session.cookie_secure', $is_secure);
ini_set('session.name', $sess_name ?: 'roundcube_sessid');
ini_set('session.use_cookies', 1);
ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_httponly', 1);
// Make sure session garbage collector is enabled when using custom handlers (#6560)
// Note: Use session.gc_divisor to control accuracy
if ($storage != 'php' && !ini_get('session.gc_probability')) {
ini_set('session.gc_probability', 1);
}
// get session driver instance
$this->session = rcube_session::factory($this->config);
$this->session->register_gc_handler(array($this, 'gc'));
// start PHP session (if not in CLI mode)
if ($_SERVER['REMOTE_ADDR']) {
$this->session->start();
}
}
/**
* Garbage collector - cache/temp cleaner
*/
public function gc()
{
rcube_cache::gc();
$this->get_storage()->cache_gc();
$this->gc_temp();
}
/**
* Garbage collector function for temp files.
* Removes temporary files older than temp_dir_ttl.
*/
public function gc_temp()
{
$tmp = unslashify($this->config->get('temp_dir'));
// expire in 48 hours by default
$temp_dir_ttl = $this->config->get('temp_dir_ttl', '48h');
$temp_dir_ttl = get_offset_sec($temp_dir_ttl);
if ($temp_dir_ttl < 6*3600) {
$temp_dir_ttl = 6*3600; // 6 hours sensible lower bound.
}
$expire = time() - $temp_dir_ttl;
if ($tmp && ($dir = opendir($tmp))) {
while (($fname = readdir($dir)) !== false) {
if (strpos($fname, RCUBE_TEMP_FILE_PREFIX) !== 0) {
continue;
}
if (@filemtime("$tmp/$fname") < $expire) {
@unlink("$tmp/$fname");
}
}
closedir($dir);
}
}
/**
* Runs garbage collector with probability based on
* session settings. This is intended for environments
* without a session.
*/
public function gc_run()
{
$probability = (int) ini_get('session.gc_probability');
$divisor = (int) ini_get('session.gc_divisor');
if ($divisor > 0 && $probability > 0) {
$random = mt_rand(1, $divisor);
if ($random <= $probability) {
$this->gc();
}
}
}
/**
* Get localized text in the desired language
*
* @param mixed $attrib Named parameters array or label name
* @param string $domain Label domain (plugin) 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);
}
$name = (string) $attrib['name'];
// attrib contain text values: use them from now
if (($setval = $attrib[strtolower($_SESSION['language'])]) || ($setval = $attrib['en_us'])) {
$this->texts[$name] = $setval;
}
// check for text with domain
if ($domain && ($text = $this->texts[$domain.'.'.$name])) {
}
// text does not exist
else if (!($text = $this->texts[$name])) {
return "[$name]";
}
// 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);
}
}
// replace \n with real line break
$text = strtr($text, array('\n' => "\n"));
// case folding
if (($attrib['uppercase'] && strtolower($attrib['uppercase']) == 'first') || $attrib['ucfirst']) {
$case_mode = MB_CASE_TITLE;
}
else if ($attrib['uppercase']) {
$case_mode = MB_CASE_UPPER;
}
else if ($attrib['lowercase']) {
$case_mode = MB_CASE_LOWER;
}
if (isset($case_mode)) {
$text = mb_convert_case($text, $case_mode);
}
return $text;
}
/**
* Check if the given text label exists
*
* @param string $name Label name
* @param string $domain Label domain (plugin) name or '*' for all domains
* @param string $ref_domain Sets domain name if label is found
*
* @return boolean True if text exists (either in the current language or in en_US)
*/
public function text_exists($name, $domain = null, &$ref_domain = null)
{
// load localization files if not done yet
if (empty($this->texts)) {
$this->load_language();
}
if (isset($this->texts[$name])) {
$ref_domain = '';
return true;
}
// any of loaded domains (plugins)
if ($domain == '*') {
foreach ($this->plugins->loaded_plugins() as $domain) {
if (isset($this->texts[$domain.'.'.$name])) {
$ref_domain = $domain;
return true;
}
}
}
// specified domain
else if ($domain && isset($this->texts[$domain.'.'.$name])) {
$ref_domain = $domain;
return true;
}
return false;
}
/**
* Load a localization package
*
* @param string $lang Language ID
* @param array $add Additional text labels/messages
* @param array $merge Additional text labels/messages to merge
*/
public function load_language($lang = null, $add = array(), $merge = array())
{
$lang = $this->language_prop($lang ?: $_SESSION['language']);
// load localized texts
if (empty($this->texts) || $lang != $_SESSION['language']) {
$this->texts = array();
// handle empty lines after closing PHP tag in localization files
ob_start();
// get english labels (these should be complete)
@include(RCUBE_LOCALIZATION_DIR . 'en_US/labels.inc');
@include(RCUBE_LOCALIZATION_DIR . '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' && $lang != 'en_US' && is_dir(RCUBE_LOCALIZATION_DIR . $lang)) {
include_once(RCUBE_LOCALIZATION_DIR . $lang . '/labels.inc');
include_once(RCUBE_LOCALIZATION_DIR . $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);
}
ob_end_clean();
$_SESSION['language'] = $lang;
}
// append additional texts (from plugin)
if (is_array($add) && !empty($add)) {
$this->texts += $add;
}
// merge additional texts (from plugin)
if (is_array($merge) && !empty($merge)) {
$this->texts = array_merge($this->texts, $merge);
}
}
/**
* Read localized texts from an additional location (plugins, skins).
* Then you can use the result as 2nd arg to load_language().
*
* @param string $dir Directory to search in
*
* @return array Localization texts
*/
public function read_localization($dir)
{
$lang = $_SESSION['language'];
$langs = array_unique(array('en_US', $lang));
$locdir = slashify($dir);
$texts = array();
// Language aliases used to find localization in similar lang, see below
$aliases = array(
'de_CH' => 'de_DE',
'es_AR' => 'es_ES',
'fa_AF' => 'fa_IR',
'nl_BE' => 'nl_NL',
'pt_BR' => 'pt_PT',
'zh_CN' => 'zh_TW',
);
// use buffering to handle empty lines/spaces after closing PHP tag
ob_start();
foreach ($langs as $lng) {
$fpath = $locdir . $lng . '.inc';
if (is_file($fpath) && is_readable($fpath)) {
include $fpath;
$texts = (array) $labels + (array) $messages + (array) $texts;
}
else if ($lng != 'en_US') {
// Find localization in similar language (#1488401)
$alias = null;
if (!empty($aliases[$lng])) {
$alias = $aliases[$lng];
}
else if ($key = array_search($lng, $aliases)) {
$alias = $key;
}
if (!empty($alias)) {
$fpath = $locdir . $alias . '.inc';
if (is_file($fpath) && is_readable($fpath)) {
include $fpath;
$texts = (array) $labels + (array) $messages + (array) $texts;
}
}
}
}
ob_end_clean();
return $texts;
}
/**
* Check the given string and return a valid language code
*
* @param string $lang Language code
*
* @return string Valid language code
*/
protected 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 = $accept_langs[0];
if (preg_match('/^([a-z]+)[_-]([a-z]+)$/i', $lang, $m)) {
$lang = $m[1] . '_' . strtoupper($m[2]);
}
}
if (empty($rcube_languages)) {
@include(RCUBE_LOCALIZATION_DIR . '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(RCUBE_LOCALIZATION_DIR . $lang)) {
$lang = 'en_US';
}
return $lang;
}
/**
* 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 (!count($sa_languages)) {
@include(RCUBE_LOCALIZATION_DIR . 'index.inc');
if ($dh = @opendir(RCUBE_LOCALIZATION_DIR)) {
while (($name = readdir($dh)) !== false) {
if ($name[0] == '.' || !is_dir(RCUBE_LOCALIZATION_DIR . $name)) {
continue;
}
if ($label = $rcube_languages[$name]) {
$sa_languages[$name] = $label;
}
}
closedir($dh);
}
}
return $sa_languages;
}
/**
* Encrypt a string
*
* @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 (!is_string($clear) || !strlen($clear)) {
return '';
}
$ckey = $this->config->get_crypto_key($key);
$method = $this->config->get_crypto_method();
$opts = defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : true;
$iv = rcube_utils::random_bytes(openssl_cipher_iv_length($method), true);
$cipher = $iv . openssl_encrypt($clear, $method, $ckey, $opts, $iv);
return $base64 ? base64_encode($cipher) : $cipher;
}
/**
* Decrypt a 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;
$ckey = $this->config->get_crypto_key($key);
$method = $this->config->get_crypto_method();
$opts = defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : true;
$iv_size = openssl_cipher_iv_length($method);
$iv = substr($cipher, 0, $iv_size);
// session corruption? (#1485970)
if (strlen($iv) < $iv_size) {
return '';
}
$cipher = substr($cipher, $iv_size);
$clear = openssl_decrypt($cipher, $method, $ckey, $opts, $iv);
return $clear;
}
/**
* Returns session token for secure URLs
*
* @param bool $generate Generate token if not exists in session yet
*
* @return string|bool Token string, False when disabled
*/
public function get_secure_url_token($generate = false)
{
if ($len = $this->config->get('use_secure_urls')) {
if (empty($_SESSION['secure_token']) && $generate) {
// generate x characters long token
$length = $len > 1 ? $len : 16;
$token = rcube_utils::random_bytes($length);
$plugin = $this->plugins->exec_hook('secure_token',
array('value' => $token, 'length' => $length));
$_SESSION['secure_token'] = $plugin['value'];
}
return $_SESSION['secure_token'];
}
return false;
}
/**
* Generate a unique token to be used in a form request
*
* @return string The request token
*/
public function get_request_token()
{
if (empty($_SESSION['request_token'])) {
$plugin = $this->plugins->exec_hook('request_token', array(
'value' => rcube_utils::random_bytes(32)));
$_SESSION['request_token'] = $plugin['value'];
}
return $_SESSION['request_token'];
}
/**
* Check if the current request contains a valid token.
* Empty requests aren't checked until use_secure_urls is set.
*
* @param int $mode Request method
*
* @return boolean True if request token is valid false if not
*/
public function check_request($mode = rcube_utils::INPUT_POST)
{
// check secure token in URL if enabled
if ($token = $this->get_secure_url_token()) {
foreach (explode('/', preg_replace('/[?#&].*$/', '', $_SERVER['REQUEST_URI'])) as $tok) {
if ($tok == $token) {
return true;
}
}
$this->request_status = self::REQUEST_ERROR_URL;
return false;
}
$sess_tok = $this->get_request_token();
// ajax requests
if (rcube_utils::request_header('X-Roundcube-Request') === $sess_tok) {
return true;
}
// skip empty requests
if (($mode == rcube_utils::INPUT_POST && empty($_POST))
|| ($mode == rcube_utils::INPUT_GET && empty($_GET))
) {
return true;
}
// default method of securing requests
$token = rcube_utils::get_input_value('_token', $mode);
$sess_id = $_COOKIE[ini_get('session.name')];
if (empty($sess_id) || $token !== $sess_tok) {
$this->request_status = self::REQUEST_ERROR_TOKEN;
return false;
}
return true;
}
/**
* Build a valid URL to this instance of Roundcube
*
* @param mixed $p Either a string with the action or url parameters as key-value pairs
*
* @return string Valid application URL
*/
public function url($p)
{
// STUB: should be overloaded by the application
return '';
}
/**
* Function to be executed in script shutdown
* Registered with register_shutdown_function()
*/
public function shutdown()
{
foreach ($this->shutdown_functions as $function) {
call_user_func($function);
}
// write session data as soon as possible and before
// closing database connection, don't do this before
// registered shutdown functions, they may need the session
// Note: this will run registered gc handlers (ie. cache gc)
if ($_SERVER['REMOTE_ADDR'] && is_object($this->session)) {
$this->session->write_close();
}
if (is_object($this->smtp)) {
$this->smtp->disconnect();
}
foreach ($this->caches as $cache) {
if (is_object($cache)) {
$cache->close();
}
}
if (is_object($this->storage)) {
$this->storage->close();
}
if ($this->config->get('log_driver') == 'syslog') {
closelog();
}
}
/**
* Registers shutdown function to be executed on shutdown.
* The functions will be executed before destroying any
* objects like smtp, imap, session, etc.
*
* @param callback $function Function callback
*/
public function add_shutdown_function($function)
{
$this->shutdown_functions[] = $function;
}
/**
* When you're going to sleep the script execution for a longer time
* it is good to close all external connections (sql, memcache, redis, SMTP, IMAP).
*
* No action is required on wake up, all connections will be
* re-established automatically.
*/
public function sleep()
{
foreach ($this->caches as $cache) {
if (is_object($cache)) {
$cache->close();
}
}
if ($this->storage) {
$this->storage->close();
}
if ($this->db) {
$this->db->closeConnection();
}
if ($this->memcache) {
$this->memcache->close();
// after close() need to re-init memcache
$this->memcache_init();
}
if ($this->smtp) {
$this->smtp->disconnect();
}
if ($this->redis) {
$this->redis->close();
}
}
/**
* Quote a given string.
* Shortcut function for rcube_utils::rep_specialchars_output()
*
* @return string HTML-quoted string
*/
public static function Q($str, $mode = 'strict', $newlines = true)
{
return rcube_utils::rep_specialchars_output($str, 'html', $mode, $newlines);
}
/**
* Quote a given string for javascript output.
* Shortcut function for rcube_utils::rep_specialchars_output()
*
* @return string JS-quoted string
*/
public static function JQ($str)
{
return rcube_utils::rep_specialchars_output($str, 'js');
}
/**
* Construct shell command, execute it and return output as string.
* Keywords {keyword} are replaced with arguments
*
* @param $cmd Format string with {keywords} to be replaced
* @param $values (zero, one or more arrays can be passed)
*
* @return output of command. shell errors not detectable
*/
public static function exec(/* $cmd, $values1 = array(), ... */)
{
$args = func_get_args();
$cmd = array_shift($args);
$values = $replacements = array();
// merge values into one array
foreach ($args as $arg) {
$values += (array)$arg;
}
preg_match_all('/({(-?)([a-z]\w*)})/', $cmd, $matches, PREG_SET_ORDER);
foreach ($matches as $tags) {
list(, $tag, $option, $key) = $tags;
$parts = array();
if ($option) {
foreach ((array)$values["-$key"] as $key => $value) {
if ($value === true || $value === false || $value === null) {
$parts[] = $value ? $key : "";
}
else {
foreach ((array)$value as $val) {
$parts[] = "$key " . escapeshellarg($val);
}
}
}
}
else {
foreach ((array)$values[$key] as $value) {
$parts[] = escapeshellarg($value);
}
}
$replacements[$tag] = join(" ", $parts);
}
// use strtr behaviour of going through source string once
$cmd = strtr($cmd, $replacements);
return (string)shell_exec($cmd);
}
/**
* Print or write debug messages
*
* @param mixed Debug message or data
*/
public static function console()
{
$args = func_get_args();
if (class_exists('rcube', false)) {
$rcube = self::get_instance();
$plugin = $rcube->plugins->exec_hook('console', array('args' => $args));
if ($plugin['abort']) {
return;
}
$args = $plugin['args'];
}
$msg = array();
foreach ($args as $arg) {
$msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
}
self::write_log('console', join(";\n", $msg));
}
/**
* Append a line to a logfile in the logs directory.
* Date will be added automatically to the line.
*
* @param string $name Name of the log file
* @param mixed $line Line to append
*
* @return bool True on success, False on failure
*/
public static function write_log($name, $line)
{
if (!is_string($line)) {
$line = var_export($line, true);
}
$date_format = $log_driver = $session_key = null;
if (self::$instance) {
$date_format = self::$instance->config->get('log_date_format');
$log_driver = self::$instance->config->get('log_driver');
$session_key = intval(self::$instance->config->get('log_session_id', 8));
}
$date = rcube_utils::date_format($date_format);
// trigger logging hook
if (is_object(self::$instance) && is_object(self::$instance->plugins)) {
$log = self::$instance->plugins->exec_hook('write_log',
array('name' => $name, 'date' => $date, 'line' => $line));
$name = $log['name'];
$line = $log['line'];
$date = $log['date'];
if ($log['abort']) {
return true;
}
}
// add session ID to the log
if ($session_key > 0 && ($sess = session_id())) {
$line = '<' . substr($sess, 0, $session_key) . '> ' . $line;
}
if ($log_driver == 'syslog') {
$prio = $name == 'errors' ? LOG_ERR : LOG_INFO;
return syslog($prio, $line);
}
// write message with file name when configured to log to STDOUT
if ($log_driver == 'stdout') {
$stdout = "php://stdout";
$line = "$name: $line";
return file_put_contents($stdout, $line, FILE_APPEND) !== false;
}
// log_driver == 'file' is assumed here
$line = sprintf("[%s]: %s\n", $date, $line);
// per-user logging is activated
if (self::$instance && self::$instance->config->get('per_user_logging') && self::$instance->get_user_id()) {
$log_dir = self::$instance->get_user_log_dir();
if (empty($log_dir) && !in_array($name, array('errors', 'userlogins', 'sendmail'))) {
return false;
}
}
if (empty($log_dir)) {
if (!empty($log['dir'])) {
$log_dir = $log['dir'];
}
else if (self::$instance) {
$log_dir = self::$instance->config->get('log_dir');
}
}
if (empty($log_dir)) {
$log_dir = RCUBE_INSTALL_PATH . 'logs';
}
if (self::$instance) {
$name .= self::$instance->config->get('log_file_ext', '.log');
}
else {
$name .= '.log';
}
return file_put_contents("$log_dir/$name", $line, FILE_APPEND) !== false;
}
/**
* Throw system error (and show error page).
*
* @param array $arg Named parameters
* - code: Error code (required)
* - type: Error type [php|db|imap|javascript]
* - message: Error message
* - file: File where error occurred
* - line: Line where error occurred
* @param boolean $log True to log the error
* @param boolean $terminate Terminate script execution
*/
public static function raise_error($arg = array(), $log = false, $terminate = false)
{
// handle PHP exceptions
if ($arg instanceof Exception) {
$arg = array(
'code' => $arg->getCode(),
'line' => $arg->getLine(),
'file' => $arg->getFile(),
'message' => $arg->getMessage(),
);
}
else if ($arg instanceof PEAR_Error) {
$info = $arg->getUserInfo();
$arg = array(
'code' => $arg->getCode(),
'message' => $arg->getMessage() . ($info ? ': ' . $info : ''),
);
}
else if (is_string($arg)) {
$arg = array('message' => $arg);
}
if (empty($arg['code'])) {
$arg['code'] = 500;
}
$cli = php_sapi_name() == 'cli';
$arg['cli'] = $cli;
$arg['log'] = $log;
$arg['terminate'] = $terminate;
// send error to external error tracking tool
$arg = self::$instance->plugins->exec_hook('raise_error', $arg);
// installer
if (!$cli && class_exists('rcmail_install', false)) {
$rci = rcmail_install::get_instance();
$rci->raise_error($arg);
return;
}
if (($log || $terminate) && !$cli && $arg['message']) {
$arg['fatal'] = $terminate;
self::log_bug($arg);
}
// terminate script
if ($terminate) {
// display error page
if (is_object(self::$instance->output)) {
self::$instance->output->raise_error($arg['code'], $arg['message']);
}
else if ($cli) {
fwrite(STDERR, 'ERROR: ' . $arg['message']);
}
exit(1);
}
else if ($cli) {
fwrite(STDERR, 'ERROR: ' . $arg['message']);
}
}
/**
* Log an error
*
* @param array $arg_arr Named parameters
* @see self::raise_error()
*/
public static function log_bug($arg_arr)
{
$program = strtoupper($arg_arr['type'] ?: 'php');
// write error to local log file
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
foreach (array('_task', '_action') as $arg) {
if ($_POST[$arg] && !$_GET[$arg]) {
$post_query[$arg] = $_POST[$arg];
}
}
if (!empty($post_query)) {
$post_query = (strpos($_SERVER['REQUEST_URI'], '?') != false ? '&' : '?')
. http_build_query($post_query, '', '&');
}
}
$log_entry = sprintf("%s Error: %s%s (%s %s)",
$program,
$arg_arr['message'],
$arg_arr['file'] ? sprintf(' in %s on line %d', $arg_arr['file'], $arg_arr['line']) : '',
$_SERVER['REQUEST_METHOD'],
$_SERVER['REQUEST_URI'] . $post_query);
if (!self::write_log('errors', $log_entry)) {
// send error to PHPs error handler if write_log didn't succeed
trigger_error($arg_arr['message'], E_USER_WARNING);
}
}
/**
* Write debug info to the log
*
* @param string $engine Engine type - file name (memcache, apc, redis)
* @param string $data Data string to log
* @param bool $result Operation result
*/
public static function debug($engine, $data, $result = null)
{
static $debug_counter;
$line = '[' . (++$debug_counter[$engine]) . '] ' . $data;
if (($len = strlen($line)) > self::DEBUG_LINE_LENGTH) {
$diff = $len - self::DEBUG_LINE_LENGTH;
$line = substr($line, 0, self::DEBUG_LINE_LENGTH) . "... [truncated $diff bytes]";
}
if ($result !== null) {
$line .= ' [' . ($result ? 'TRUE' : 'FALSE') . ']';
}
self::write_log($engine, $line);
}
/**
* Returns current time (with microseconds).
*
* @return float Current time in seconds since the Unix
*/
public static function timer()
{
return microtime(true);
}
/**
* Logs time difference according to provided timer
*
* @param float $timer Timer (self::timer() result)
* @param string $label Log line prefix
* @param string $dest Log file name
*
* @see self::timer()
*/
public static function print_timer($timer, $label = 'Timer', $dest = 'console')
{
static $print_count = 0;
$print_count++;
$now = self::timer();
$diff = $now - $timer;
if (empty($label)) {
$label = 'Timer '.$print_count;
}
self::write_log($dest, sprintf("%s: %0.4f sec", $label, $diff));
}
/**
* Setter for system user object
*
* @param rcube_user Current user instance
*/
public function set_user($user)
{
if (is_object($user)) {
$this->user = $user;
// overwrite config with user preferences
$this->config->set_user_prefs((array)$this->user->get_prefs());
}
}
/**
* Getter for logged user ID.
*
* @return mixed User identifier
*/
public function get_user_id()
{
if (is_object($this->user)) {
return $this->user->ID;
}
else if (isset($_SESSION['user_id'])) {
return $_SESSION['user_id'];
}
}
/**
* Getter for logged user name.
*
* @return string User name
*/
public function get_user_name()
{
if (is_object($this->user)) {
return $this->user->get_username();
}
else if (isset($_SESSION['username'])) {
return $_SESSION['username'];
}
}
/**
* Getter for logged user email (derived from user name not identity).
*
* @return string User email address
*/
public function get_user_email()
{
if (is_object($this->user)) {
return $this->user->get_username('mail');
}
}
/**
* Getter for logged user password.
*
* @return string User password
*/
public function get_user_password()
{
if ($this->password) {
return $this->password;
}
else if ($_SESSION['password']) {
return $this->decrypt($_SESSION['password']);
}
}
/**
* Get the per-user log directory
*/
protected function get_user_log_dir()
{
$log_dir = $this->config->get('log_dir', RCUBE_INSTALL_PATH . 'logs');
$user_name = $this->get_user_name();
$user_log_dir = $log_dir . '/' . $user_name;
return !empty($user_name) && is_writable($user_log_dir) ? $user_log_dir : false;
}
/**
* Getter for logged user language code.
*
* @return string User language code
*/
public function get_user_language()
{
if (is_object($this->user)) {
return $this->user->language;
}
else if (isset($_SESSION['language'])) {
return $_SESSION['language'];
}
}
/**
* Unique Message-ID generator.
*
* @param string $sender Optional sender e-mail address
*
* @return string Message-ID
*/
public function gen_message_id($sender = null)
{
$local_part = md5(uniqid('rcube'.mt_rand(), true));
$domain_part = '';
if ($sender && preg_match('/@([^\s]+\.[a-z0-9-]+)/', $sender, $m)) {
$domain_part = $m[1];
}
else {
$domain_part = $this->user->get_username('domain');
}
// Try to find FQDN, some spamfilters doesn't like 'localhost' (#1486924)
if (!preg_match('/\.[a-z0-9-]+$/i', $domain_part)) {
foreach (array($_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']) as $host) {
$host = preg_replace('/:[0-9]+$/', '', $host);
if ($host && preg_match('/\.[a-z]+$/i', $host)) {
$domain_part = $host;
break;
}
}
}
return sprintf('<%s@%s>', $local_part, $domain_part);
}
/**
* Send the given message using the configured method.
*
* @param object $message Reference to Mail_MIME object
* @param string $from Sender address string
* @param array|string $mailto Either a comma-separated list of recipients (RFC822 compliant),
* or an array of recipients, each RFC822 valid
* @param array $error SMTP error array (reference)
* @param string $body_file Location of file with saved message body (reference),
* used when delay_file_io is enabled
* @param array $options SMTP options (e.g. DSN request)
* @param bool $disconnect Close SMTP connection ASAP
*
* @return boolean Send status.
*/
public function deliver_message(&$message, $from, $mailto, &$error,
&$body_file = null, $options = null, $disconnect = false)
{
$plugin = $this->plugins->exec_hook('message_before_send', array(
'message' => $message,
'from' => $from,
'mailto' => $mailto,
'options' => $options,
));
if ($plugin['abort']) {
if (!empty($plugin['error'])) {
$error = $plugin['error'];
}
if (!empty($plugin['body_file'])) {
$body_file = $plugin['body_file'];
}
return isset($plugin['result']) ? $plugin['result'] : false;
}
$from = $plugin['from'];
$mailto = $plugin['mailto'];
$options = $plugin['options'];
$message = $plugin['message'];
$headers = $message->headers();
// generate list of recipients
$a_recipients = (array) $mailto;
if (strlen($headers['Cc'])) {
$a_recipients[] = $headers['Cc'];
}
if (strlen($headers['Bcc'])) {
$a_recipients[] = $headers['Bcc'];
}
// remove Bcc header and get the whole head of the message as string
$smtp_headers = $message->txtHeaders(array('Bcc' => null), true);
if ($message->getParam('delay_file_io')) {
// use common temp dir
$body_file = rcube_utils::temp_filename('msg');
$mime_result = $message->saveMessageBody($body_file);
if (is_a($mime_result, 'PEAR_Error')) {
self::raise_error(array('code' => 650, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not create message: ".$mime_result->getMessage()),
true, false);
return false;
}
$msg_body = fopen($body_file, 'r');
}
else {
$msg_body = $message->get();
}
// initialize SMTP connection
if (!is_object($this->smtp)) {
$this->smtp_init(true);
}
// send message
$sent = $this->smtp->send_mail($from, $a_recipients, $smtp_headers, $msg_body, $options);
$response = $this->smtp->get_response();
$error = $this->smtp->get_error();
if (!$sent) {
self::raise_error(array('code' => 800, 'type' => 'smtp',
'line' => __LINE__, 'file' => __FILE__,
'message' => join("\n", $response)), true, false);
// allow plugins to catch sending errors with the same parameters as in 'message_before_send'
$this->plugins->exec_hook('message_send_error', $plugin + array('error' => $error));
}
else {
$this->plugins->exec_hook('message_sent', array('headers' => $headers, 'body' => $msg_body, 'message' => $message));
// remove MDN headers after sending
unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']);
if ($this->config->get('smtp_log')) {
// get all recipient addresses
$mailto = implode(',', $a_recipients);
$mailto = rcube_mime::decode_address_list($mailto, null, false, null, true);
self::write_log('sendmail', sprintf("User %s [%s]; Message %s for %s; %s",
$this->user->get_username(),
rcube_utils::remote_addr(),
$headers['Message-ID'],
implode(', ', $mailto),
!empty($response) ? join('; ', $response) : ''));
}
}
if (is_resource($msg_body)) {
fclose($msg_body);
}
if ($disconnect) {
$this->smtp->disconnect();
}
// Add Bcc header back
if (!empty($headers['Bcc'])) {
$message->headers(array('Bcc' => $headers['Bcc']), true);
}
return $sent;
}
}
/**
* Lightweight plugin API class serving as a dummy if plugins are not enabled
*
* @package Framework
* @subpackage Core
*/
class rcube_dummy_plugin_api
{
/**
* Triggers a plugin hook.
* @see rcube_plugin_api::exec_hook()
*/
public function exec_hook($hook, $args = array())
{
return $args;
}
}
diff --git a/program/lib/Roundcube/rcube_addressbook.php b/program/lib/Roundcube/rcube_addressbook.php
index 6c885b4fe..8b7d99bf5 100644
--- a/program/lib/Roundcube/rcube_addressbook.php
+++ b/program/lib/Roundcube/rcube_addressbook.php
@@ -1,716 +1,717 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2006-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Interface to the local address book database |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Abstract skeleton of an address book/repository
*
* @package Framework
* @subpackage Addressbook
*/
abstract class rcube_addressbook
{
// constants for error reporting
const ERROR_READ_ONLY = 1;
const ERROR_NO_CONNECTION = 2;
const ERROR_VALIDATE = 3;
const ERROR_SAVING = 4;
const ERROR_SEARCH = 5;
// search modes
const SEARCH_ALL = 0;
const SEARCH_STRICT = 1;
const SEARCH_PREFIX = 2;
const SEARCH_GROUPS = 4;
// public properties (mandatory)
public $primary_key;
public $groups = false;
public $export_groups = true;
public $readonly = true;
public $searchonly = false;
public $undelete = false;
public $ready = false;
public $group_id = null;
public $list_page = 1;
public $page_size = 10;
public $sort_col = 'name';
public $sort_order = 'ASC';
public $date_cols = array();
public $coltypes = array(
'name' => array('limit'=>1),
'firstname' => array('limit'=>1),
'surname' => array('limit'=>1),
'email' => array('limit'=>1)
);
protected $error;
/**
* Returns addressbook name (e.g. for addressbooks listing)
*/
abstract function get_name();
/**
* Save a search string for future listings
*
* @param mixed $filter Search params to use in listing method, obtained by get_search_set()
*/
abstract function set_search_set($filter);
/**
* Getter for saved search properties
*
* @return mixed Search properties used by this class
*/
abstract function get_search_set();
/**
* Reset saved results and search parameters
*/
abstract function reset();
/**
* Refresh saved search set after data has changed
*
* @return mixed New search set
*/
function refresh_search()
{
return $this->get_search_set();
}
/**
* List the current set of contact records
*
* @param array $cols List of cols to show
* @param int $subset Only return this number of records, use negative values for tail
*
* @return array Indexed list of contact records, each a hash array
*/
abstract function list_records($cols=null, $subset=0);
/**
* Search records
*
* @param array $fields List of fields to search in
* @param string $value Search value
* @param int $mode Search mode. Sum of self::SEARCH_*.
* @param boolean $select True if results are requested, False if count only
* @param boolean $nocount True to skip the count query (select only)
* @param array $required List of fields that cannot be empty
*
* @return object rcube_result_set List of contact records and 'count' value
*/
abstract function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array());
/**
* Count number of available contacts in database
*
* @return rcube_result_set Result set with values for 'count' and 'first'
*/
abstract function count();
/**
* Return the last result set
*
* @return rcube_result_set Current result set or NULL if nothing selected yet
*/
abstract function get_result();
/**
* Get a specific contact record
*
* @param mixed $id Record identifier(s)
* @param boolean $assoc True to return record as associative array, otherwise a result set is returned
*
* @return rcube_result_set|array Result object with all record fields
*/
abstract function get_record($id, $assoc=false);
/**
* Returns the last error occurred (e.g. when updating/inserting failed)
*
* @return array Hash array with the following fields: type, message
*/
function get_error()
{
return $this->error;
}
/**
* Setter for errors for internal use
*
* @param int $type Error type (one of this class' error constants)
* @param string $message Error message (name of a text label)
*/
protected function set_error($type, $message)
{
$this->error = array('type' => $type, 'message' => $message);
}
/**
* Close connection to source
* Called on script shutdown
*/
function close() { }
/**
* Set internal list page
*
* @param number $page Page number to list
*/
function set_page($page)
{
$this->list_page = (int)$page;
}
/**
* Set internal page size
*
* @param number $size Number of messages to display on one page
*/
function set_pagesize($size)
{
$this->page_size = (int)$size;
}
/**
* Set internal sort settings
*
* @param string $sort_col Sort column
* @param string $sort_order Sort order
*/
function set_sort_order($sort_col, $sort_order = null)
{
if ($sort_col != null && ($this->coltypes[$sort_col] || in_array($sort_col, $this->coltypes))) {
$this->sort_col = $sort_col;
}
if ($sort_order != null) {
$this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
}
}
/**
* Check the given data before saving.
* If input isn't valid, the message to display can be fetched using get_error()
*
* @param array &$save_data Associative array with data to save
* @param boolean $autofix Attempt to fix/complete record automatically
*
* @return boolean True if input is valid, False if not.
*/
public function validate(&$save_data, $autofix = false)
{
$rcube = rcube::get_instance();
$valid = true;
// check validity of email addresses
foreach ($this->get_col_values('email', $save_data, true) as $email) {
if (strlen($email)) {
if (!rcube_utils::check_email(rcube_utils::idn_to_ascii($email))) {
$error = $rcube->gettext(array('name' => 'emailformaterror', 'vars' => array('email' => $email)));
$this->set_error(self::ERROR_VALIDATE, $error);
$valid = false;
break;
}
}
}
// allow plugins to do contact validation and auto-fixing
$plugin = $rcube->plugins->exec_hook('contact_validate', array(
'record' => $save_data,
'autofix' => $autofix,
'valid' => $valid,
));
if ($valid && !$plugin['valid']) {
$this->set_error(self::ERROR_VALIDATE, $plugin['error']);
}
if (is_array($plugin['record'])) {
$save_data = $plugin['record'];
}
return $plugin['valid'];
}
/**
* Create a new contact record
*
* @param array $save_data Associative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
* @param boolean $check True to check for duplicates first
*
* @return mixed The created record ID on success, False on error
*/
function insert($save_data, $check=false)
{
/* empty for read-only address books */
}
/**
* Create new contact records for every item in the record set
*
* @param rcube_result_set $recset Recordset to insert
* @param boolean $check True to check for duplicates first
*
* @return array List of created record IDs
*/
function insertMultiple($recset, $check=false)
{
$ids = array();
if (is_object($recset) && is_a($recset, rcube_result_set)) {
while ($row = $recset->next()) {
if ($insert = $this->insert($row, $check))
$ids[] = $insert;
}
}
return $ids;
}
/**
* Update a specific contact record
*
* @param mixed $id Record identifier
* @param array $save_cols Associative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
*
* @return mixed On success if ID has been changed returns ID, otherwise True, False on error
*/
function update($id, $save_cols)
{
/* empty for read-only address books */
}
/**
* Mark one or more contact records as deleted
*
* @param array $ids Record identifiers
* @param bool $force Remove records irreversible (see self::undelete)
*/
function delete($ids, $force = true)
{
/* empty for read-only address books */
}
/**
* Unmark delete flag on contact record(s)
*
* @param array $ids Record identifiers
*/
function undelete($ids)
{
/* empty for read-only address books */
}
/**
* Mark all records in database as deleted
*
* @param bool $with_groups Remove also groups
*/
function delete_all($with_groups = false)
{
/* empty for read-only address books */
}
/**
* Setter for the current group
* (empty, has to be re-implemented by extending class)
*/
function set_group($group_id) { }
/**
* List all active contact groups of this source
*
* @param string $search Optional search string to match group name
* @param int $mode Search mode. Sum of self::SEARCH_*
*
* @return array Indexed list of contact groups, each a hash array
*/
function list_groups($search = null, $mode = 0)
{
/* empty for address books don't supporting groups */
return array();
}
/**
* Get group properties such as name and email address(es)
*
* @param string $group_id Group identifier
*
* @return array Group properties as hash array
*/
function get_group($group_id)
{
/* empty for address books don't supporting groups */
}
/**
* Create a contact group with the given name
*
* @param string $name The group name
*
* @return mixed False on error, array with record props in success
*/
function create_group($name)
{
/* empty for address books don't supporting groups */
return false;
}
/**
* Delete the given group and all linked group members
*
* @param string $group_id Group identifier
*
* @return boolean True on success, false if no data was changed
*/
function delete_group($group_id)
{
/* empty for address books don't supporting groups */
return false;
}
/**
* Rename a specific contact group
*
* @param string $group_id Group identifier
* @param string $newname New name to set for this group
* @param string &$newid New group identifier (if changed, otherwise don't set)
*
* @return boolean New name on success, false if no data was changed
*/
function rename_group($group_id, $newname, &$newid)
{
/* empty for address books don't supporting groups */
return false;
}
/**
* Add the given contact records the a certain group
*
* @param string $group_id Group identifier
* @param array|string $ids List of contact identifiers to be added
*
* @return int Number of contacts added
*/
function add_to_group($group_id, $ids)
{
/* empty for address books don't supporting groups */
return 0;
}
/**
* Remove the given contact records from a certain group
*
* @param string $group_id Group identifier
* @param array|string $ids List of contact identifiers to be removed
*
* @return int Number of deleted group members
*/
function remove_from_group($group_id, $ids)
{
/* empty for address books don't supporting groups */
return 0;
}
/**
* Get group assignments of a specific contact record
*
* @param mixed Record identifier
*
* @return array $id List of assigned groups as ID=>Name pairs
* @since 0.5-beta
*/
function get_record_groups($id)
{
/* empty for address books don't supporting groups */
return array();
}
/**
* Utility function to return all values of a certain data column
* either as flat list or grouped by subtype
*
* @param string $col Col name
* @param array $data Record data array as used for saving
* @param bool $flat True to return one array with all values,
* False for hash array with values grouped by type
*
* @return array List of column values
*/
public static function get_col_values($col, $data, $flat = false)
{
$out = array();
foreach ((array)$data as $c => $values) {
if ($c === $col || strpos($c, $col.':') === 0) {
if ($flat) {
$out = array_merge($out, (array)$values);
}
else {
list(, $type) = explode(':', $c);
$out[$type] = array_merge((array)$out[$type], (array)$values);
}
}
}
// remove duplicates
if ($flat && !empty($out)) {
$out = array_unique($out);
}
return $out;
}
/**
* Normalize the given string for fulltext search.
* Currently only optimized for Latin-1 characters; to be extended
*
* @param string $str Input string (UTF-8)
* @return string Normalized string
* @deprecated since 0.9-beta
*/
protected static function normalize_string($str)
{
return rcube_utils::normalize_string($str);
}
/**
* Compose a valid display name from the given structured contact data
*
* @param array $contact Hash array with contact data as key-value pairs
* @param bool $full_email Don't attempt to extract components from the email address
*
* @return string Display name
*/
public static function compose_display_name($contact, $full_email = false)
{
$contact = rcube::get_instance()->plugins->exec_hook('contact_displayname', $contact);
$fn = $contact['name'];
// default display name composition according to vcard standard
if (!$fn) {
$fn = join(' ', array_filter(array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix'])));
$fn = trim(preg_replace('/\s+/u', ' ', $fn));
}
// use email address part for name
$email = self::get_col_values('email', $contact, true);
$email = $email[0];
if ($email && (empty($fn) || $fn == $email)) {
// return full email
if ($full_email)
return $email;
list($emailname) = explode('@', $email);
if (preg_match('/(.*)[\.\-\_](.*)/', $emailname, $match))
$fn = trim(ucfirst($match[1]).' '.ucfirst($match[2]));
else
$fn = ucfirst($emailname);
}
return $fn;
}
/**
* Compose the name to display in the contacts list for the given contact record.
* This respects the settings parameter how to list conacts.
*
* @param array $contact Hash array with contact data as key-value pairs
*
* @return string List name
*/
public static function compose_list_name($contact)
{
static $compose_mode;
if (!isset($compose_mode)) // cache this
$compose_mode = rcube::get_instance()->config->get('addressbook_name_listing', 0);
if ($compose_mode == 3)
$fn = join(' ', array($contact['surname'] . ',', $contact['firstname'], $contact['middlename']));
else if ($compose_mode == 2)
$fn = join(' ', array($contact['surname'], $contact['firstname'], $contact['middlename']));
else if ($compose_mode == 1)
$fn = join(' ', array($contact['firstname'], $contact['middlename'], $contact['surname']));
else if ($compose_mode == 0)
$fn = $contact['name'] ?: join(' ', array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix']));
else {
$plugin = rcube::get_instance()->plugins->exec_hook('contact_listname', array('contact' => $contact));
$fn = $plugin['fn'];
}
$fn = trim($fn, ', ');
$fn = preg_replace('/\s+/u', ' ', $fn);
// fallbacks...
if ($fn === '') {
// ... display name
if ($name = trim($contact['name'])) {
$fn = $name;
}
// ... organization
else if ($org = trim($contact['organization'])) {
$fn = $org;
}
// ... email address
else if (($email = self::get_col_values('email', $contact, true)) && !empty($email)) {
$fn = $email[0];
}
}
return $fn;
}
/**
* Build contact display name for autocomplete listing
*
* @param array $contact Hash array with contact data as key-value pairs
* @param string $email Optional email address
* @param string $name Optional name (self::compose_list_name() result)
* @param string $templ Optional template to use (defaults to the 'contact_search_name' config option)
*
* @return string Display name
*/
public static function compose_search_name($contact, $email = null, $name = null, $templ = null)
{
static $template;
if (empty($templ) && !isset($template)) { // cache this
$template = rcube::get_instance()->config->get('contact_search_name');
if (empty($template)) {
$template = '{name} <{email}>';
}
}
$result = $templ ?: $template;
if (preg_match_all('/\{[a-z]+\}/', $result, $matches)) {
foreach ($matches[0] as $key) {
$key = trim($key, '{}');
$value = '';
switch ($key) {
case 'name':
$value = $name ?: self::compose_list_name($contact);
// If name(s) are undefined compose_list_name() may return an email address
// here we prevent from returning the same name and email
if ($name === $email && strpos($result, '{email}') !== false) {
$value = '';
}
break;
case 'email':
$value = $email;
break;
}
if (empty($value)) {
$value = strpos($key, ':') ? $contact[$key] : self::get_col_values($key, $contact, true);
if (is_array($value)) {
$value = $value[0];
}
}
$result = str_replace('{' . $key . '}', $value, $result);
}
}
$result = preg_replace('/\s+/u', ' ', $result);
$result = preg_replace('/\s*(<>|\(\)|\[\])/u', '', $result);
$result = trim($result, '/ ');
return $result;
}
/**
* Create a unique key for sorting contacts
*
* @param array $contact Contact record
* @param string $sort_col Sorting column name
*
* @return string Unique key
*/
public static function compose_contact_key($contact, $sort_col)
{
$key = $contact[$sort_col];
// add email to a key to not skip contacts with the same name (#1488375)
if (($email = self::get_col_values('email', $contact, true)) && !empty($email)) {
$key .= ':' . implode(':', (array)$email);
}
// Make the key really unique (as we e.g. support contacts with no email)
$key .= ':' . $contact['sourceid'] . ':' . $contact['ID'];
return $key;
}
/**
* Compare search value with contact data
*
* @param string $colname Data name
* @param string|array $value Data value
* @param string $search Search value
* @param int $mode Search mode
*
* @return bool Comparison result
*/
protected function compare_search_value($colname, $value, $search, $mode)
{
// The value is a date string, for date we'll
// use only strict comparison (mode = 1)
// @TODO: partial search, e.g. match only day and month
if (in_array($colname, $this->date_cols)) {
return (($value = rcube_utils::anytodatetime($value))
&& ($search = rcube_utils::anytodatetime($search))
&& $value->format('Ymd') == $search->format('Ymd'));
}
// Gender is a special value, must use strict comparison (#5757)
if ($colname == 'gender') {
$mode = self::SEARCH_STRICT;
}
// composite field, e.g. address
foreach ((array)$value as $val) {
$val = mb_strtolower($val);
if ($mode & self::SEARCH_STRICT) {
$got = ($val == $search);
}
else if ($mode & self::SEARCH_PREFIX) {
$got = ($search == substr($val, 0, strlen($search)));
}
else {
$got = (strpos($val, $search) !== false);
}
if ($got) {
return true;
}
}
return false;
}
}
diff --git a/program/lib/Roundcube/rcube_base_replacer.php b/program/lib/Roundcube/rcube_base_replacer.php
index a5d3f8a96..64369be51 100644
--- a/program/lib/Roundcube/rcube_base_replacer.php
+++ b/program/lib/Roundcube/rcube_base_replacer.php
@@ -1,123 +1,124 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2012, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide basic functions for base URL replacement |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Helper class to turn relative urls into absolute ones
* using a predefined base
*
* @package Framework
* @subpackage Utils
* @author Thomas Bruederli <roundcube@gmail.com>
*/
class rcube_base_replacer
{
private $base_url;
/**
* Class constructor
*
* @param string $base Base URL
*/
public function __construct($base)
{
$this->base_url = $base;
}
/**
* Replace callback
*
* @param array $matches Matching entries
*
* @return string Replaced text with absolute URL
*/
public function callback($matches)
{
return $matches[1] . '="' . self::absolute_url($matches[3], $this->base_url) . '"';
}
/**
* Convert base URLs to absolute ones
*
* @param string $body Text body
*
* @return string Replaced text
*/
public function replace($body)
{
$regexp = array(
'/(src|background|href)=(["\']?)([^"\'\s>]+)(\2|\s|>)/i',
'/(url\s*\()(["\']?)([^"\'\)\s]+)(\2)\)/i',
);
return preg_replace_callback($regexp, array($this, 'callback'), $body);
}
/**
* Convert paths like ../xxx to an absolute path using a base url
*
* @param string $path Relative path
* @param string $base_url Base URL
*
* @return string Absolute URL
*/
public static function absolute_url($path, $base_url)
{
// check if path is an absolute URL
if (preg_match('/^[fhtps]+:\/\//', $path)) {
return $path;
}
// check if path is a content-id scheme
if (strpos($path, 'cid:') === 0) {
return $path;
}
$host_url = $base_url;
$abs_path = $path;
// cut base_url to the last directory
if (strrpos($base_url, '/') > 7) {
$host_url = substr($base_url, 0, strpos($base_url, '/', 7));
$base_url = substr($base_url, 0, strrpos($base_url, '/'));
}
// $path is absolute
if ($path[0] == '/') {
$abs_path = $host_url.$path;
}
else {
// strip './' because its the same as ''
$path = preg_replace('/^\.\//', '', $path);
if (preg_match_all('/\.\.\//', $path, $matches, PREG_SET_ORDER)) {
$cnt = count($matches);
while ($cnt--) {
if ($pos = strrpos($base_url, '/')) {
$base_url = substr($base_url, 0, $pos);
}
$path = substr($path, 3);
}
}
$abs_path = $base_url.'/'.$path;
}
return $abs_path;
}
}
diff --git a/program/lib/Roundcube/rcube_browser.php b/program/lib/Roundcube/rcube_browser.php
index 6b1448fc5..4e0f9d0f1 100644
--- a/program/lib/Roundcube/rcube_browser.php
+++ b/program/lib/Roundcube/rcube_browser.php
@@ -1,69 +1,70 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2007-2015, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Class representing the client browser's properties |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Provide details about the client's browser based on the User-Agent header
*
* @package Framework
* @subpackage Utils
*/
class rcube_browser
{
function __construct()
{
$HTTP_USER_AGENT = strtolower($_SERVER['HTTP_USER_AGENT']);
$this->ver = 0;
$this->win = strpos($HTTP_USER_AGENT, 'win') != false;
$this->mac = strpos($HTTP_USER_AGENT, 'mac') != false;
$this->linux = strpos($HTTP_USER_AGENT, 'linux') != false;
$this->unix = strpos($HTTP_USER_AGENT, 'unix') != false;
$this->webkit = strpos($HTTP_USER_AGENT, 'applewebkit') !== false;
$this->opera = strpos($HTTP_USER_AGENT, 'opera') !== false || ($this->webkit && strpos($HTTP_USER_AGENT, 'opr/') !== false);
$this->ns = strpos($HTTP_USER_AGENT, 'netscape') !== false;
$this->chrome = !$this->opera && strpos($HTTP_USER_AGENT, 'chrome') !== false;
$this->ie = !$this->opera && (strpos($HTTP_USER_AGENT, 'compatible; msie') !== false || strpos($HTTP_USER_AGENT, 'trident/') !== false);
$this->safari = !$this->opera && !$this->chrome && ($this->webkit || strpos($HTTP_USER_AGENT, 'safari') !== false);
$this->mz = !$this->ie && !$this->safari && !$this->chrome && !$this->ns && !$this->opera && strpos($HTTP_USER_AGENT, 'mozilla') !== false;
if ($this->opera) {
if (preg_match('/(opera|opr)\/([0-9.]+)/', $HTTP_USER_AGENT, $regs)) {
$this->ver = (float) $regs[2];
}
}
else if (preg_match('/(chrome|msie|version|khtml)(\s*|\/)([0-9.]+)/', $HTTP_USER_AGENT, $regs)) {
$this->ver = (float) $regs[3];
}
else if (preg_match('/rv:([0-9.]+)/', $HTTP_USER_AGENT, $regs)) {
$this->ver = (float) $regs[1];
}
if (preg_match('/ ([a-z]{2})-([a-z]{2})/', $HTTP_USER_AGENT, $regs)) {
$this->lang = $regs[1];
}
else {
$this->lang = 'en';
}
$this->dom = $this->mz || $this->safari || ($this->ie && $this->ver>=5) || ($this->opera && $this->ver>=7);
$this->pngalpha = $this->mz || $this->safari || ($this->ie && $this->ver>=5.5) ||
($this->ie && $this->ver>=5 && $this->mac) || ($this->opera && $this->ver>=7);
$this->imgdata = !$this->ie;
}
}
diff --git a/program/lib/Roundcube/rcube_cache.php b/program/lib/Roundcube/rcube_cache.php
index 895b76fb6..4782d750d 100644
--- a/program/lib/Roundcube/rcube_cache.php
+++ b/program/lib/Roundcube/rcube_cache.php
@@ -1,504 +1,505 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2011, The Roundcube Dev Team |
- | Copyright (C) 2011, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Caching engine |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Interface class for accessing Roundcube cache
*
* @package Framework
* @subpackage Cache
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_cache
{
protected $type;
protected $userid;
protected $prefix;
protected $ttl;
protected $packed;
protected $index;
protected $index_changed = false;
protected $debug = false;
protected $cache = array();
protected $cache_changes = array();
protected $cache_sums = array();
protected $max_packet = -1;
/**
* Object factory
*
* @param string $type Engine type ('db', 'memcache', 'apc', 'redis')
* @param int $userid User identifier
* @param string $prefix Key name prefix
* @param string $ttl Expiration time of memcache/apc items
* @param bool $packed Enables/disabled data serialization.
* It's possible to disable data serialization if you're sure
* stored data will be always a safe string
*
* @param rcube_cache Cache object
*/
public static function factory($type, $userid, $prefix = '', $ttl = 0, $packed = true)
{
$driver = strtolower($type) ?: 'db';
$class = "rcube_cache_$driver";
if (!$driver || !class_exists($class)) {
rcube::raise_error(array('code' => 600, 'type' => 'db',
'line' => __LINE__, 'file' => __FILE__,
'message' => "Configuration error. Unsupported cache driver: $driver"),
true, true);
}
return new $class($userid, $prefix, $ttl, $packed);
}
/**
* Object constructor.
*
* @param int $userid User identifier
* @param string $prefix Key name prefix
* @param string $ttl Expiration time of memcache/apc items
* @param bool $packed Enables/disabled data serialization.
* It's possible to disable data serialization if you're sure
* stored data will be always a safe string
*/
public function __construct($userid, $prefix = '', $ttl = 0, $packed = true)
{
// convert ttl string to seconds
$ttl = get_offset_sec($ttl);
if ($ttl > 2592000) $ttl = 2592000;
$this->userid = (int) $userid;
$this->ttl = $ttl;
$this->packed = $packed;
$this->prefix = $prefix;
}
/**
* Returns cached value.
*
* @param string $key Cache key name
*
* @return mixed Cached value
*/
public function get($key)
{
if (!array_key_exists($key, $this->cache)) {
return $this->read_record($key);
}
return $this->cache[$key];
}
/**
* Sets (add/update) value in cache.
*
* @param string $key Cache key name
* @param mixed $data Cache data
*/
public function set($key, $data)
{
$this->cache[$key] = $data;
$this->cache_changes[$key] = true;
}
/**
* Returns cached value without storing it in internal memory.
*
* @param string $key Cache key name
*
* @return mixed Cached value
*/
public function read($key)
{
if (array_key_exists($key, $this->cache)) {
return $this->cache[$key];
}
return $this->read_record($key, true);
}
/**
* Sets (add/update) value in cache and immediately saves
* it in the backend, no internal memory will be used.
*
* @param string $key Cache key name
* @param mixed $data Cache data
*
* @param boolean True on success, False on failure
*/
public function write($key, $data)
{
return $this->write_record($key, $this->serialize($data));
}
/**
* Clears the cache.
*
* @param string $key Cache key name or pattern
* @param boolean $prefix_mode Enable it to clear all keys starting
* with prefix specified in $key
*/
public function remove($key=null, $prefix_mode=false)
{
// Remove all keys
if ($key === null) {
$this->cache = array();
$this->cache_changes = array();
$this->cache_sums = array();
}
// Remove keys by name prefix
else if ($prefix_mode) {
foreach (array_keys($this->cache) as $k) {
if (strpos($k, $key) === 0) {
$this->cache[$k] = null;
$this->cache_changes[$k] = false;
unset($this->cache_sums[$k]);
}
}
}
// Remove one key by name
else {
$this->cache[$key] = null;
$this->cache_changes[$key] = false;
unset($this->cache_sums[$key]);
}
// Remove record(s) from the backend
$this->remove_record($key, $prefix_mode);
}
/**
* Remove cache records older than ttl
*/
public function expunge()
{
// to be overwritten by engine class
}
/**
* Remove expired records of all caches
*/
public static function gc()
{
// Only DB cache requires an action to remove expired entries
rcube_cache_db::gc();
}
/**
* Writes the cache back to the DB.
*/
public function close()
{
foreach ($this->cache as $key => $data) {
// The key has been used
if ($this->cache_changes[$key]) {
// Make sure we're not going to write unchanged data
// by comparing current md5 sum with the sum calculated on DB read
$data = $this->serialize($data);
if (!$this->cache_sums[$key] || $this->cache_sums[$key] != md5($data)) {
$this->write_record($key, $data);
}
}
}
if ($this->index_changed) {
$this->write_index();
}
// reset internal cache index, thanks to this we can force index reload
$this->index = null;
$this->index_changed = false;
$this->cache = array();
$this->cache_sums = array();
$this->cache_changes = array();
}
/**
* Reads cache entry.
*
* @param string $key Cache key name
* @param boolean $nostore Enable to skip in-memory store
*
* @return mixed Cached value
*/
protected function read_record($key, $nostore = false)
{
$this->load_index();
// Consistency check (#1490390)
if (!in_array($key, $this->index)) {
// we always check if the key exist in the index
// to have data in consistent state. Keeping the index consistent
// is needed for keys delete operation when we delete all keys or by prefix.
}
else {
$ckey = $this->ckey($key);
$data = $this->get_item($ckey);
}
if ($data !== false) {
$md5sum = md5($data);
$data = $this->unserialize($data);
if ($nostore) {
return $data;
}
$this->cache_sums[$key] = $md5sum;
$this->cache[$key] = $data;
}
else {
$this->cache[$key] = null;
}
return $this->cache[$key];
}
/**
* Writes single cache record into DB.
*
* @param string $key Cache key name
* @param mixed $data Serialized cache data
*
* @param boolean True on success, False on failure
*/
protected function write_record($key, $data)
{
// don't attempt to write too big data sets
if (strlen($data) > $this->max_packet_size()) {
trigger_error("rcube_cache: max_packet_size ($this->max_packet) exceeded for key $key. Tried to write " . strlen($data) . " bytes", E_USER_WARNING);
return false;
}
$result = $this->add_item($this->ckey($key), $data);
// make sure index will be updated
if ($result) {
if (!array_key_exists($key, $this->cache_sums)) {
$this->cache_sums[$key] = true;
}
$this->load_index();
if (!$this->index_changed && !in_array($key, $this->index)) {
$this->index_changed = true;
}
}
return $result;
}
/**
* Deletes the cache record(s).
*
* @param string $key Cache key name or pattern
* @param boolean $prefix_mode Enable it to clear all keys starting
* with prefix specified in $key
*/
protected function remove_record($key = null, $prefix_mode = false)
{
$this->load_index();
// Remove all keys
if ($key === null) {
foreach ($this->index as $key) {
$this->delete_item($this->ckey($key));
}
$this->index = array();
}
// Remove keys by name prefix
else if ($prefix_mode) {
foreach ($this->index as $idx => $k) {
if (strpos($k, $key) === 0) {
$this->delete_item($this->ckey($k));
unset($this->index[$idx]);
}
}
}
// Remove one key by name
else {
$this->delete_item($this->ckey($key));
if (($idx = array_search($key, $this->index)) !== false) {
unset($this->index[$idx]);
}
}
$this->index_changed = true;
}
/**
* Fetches cache entry.
*
* @param string $key Cache internal key name
*
* @return mixed Cached value
*/
protected function get_item($key)
{
// to be overwritten by engine class
}
/**
* Adds entry into memcache/apc/redis DB.
*
* @param string $key Cache internal key name
* @param mixed $data Serialized cache data
*
* @param boolean True on success, False on failure
*/
protected function add_item($key, $data)
{
// to be overwritten by engine class
}
/**
* Deletes entry from memcache/apc/redis DB.
*
* @param string $key Cache internal key name
*
* @param boolean True on success, False on failure
*/
protected function delete_item($key)
{
// to be overwritten by engine class
}
/**
* Writes the index entry into memcache/apc/redis DB.
*/
protected function write_index()
{
$this->load_index();
// Make sure index contains new keys
foreach ($this->cache as $key => $value) {
if ($value !== null && !in_array($key, $this->index)) {
$this->index[] = $key;
}
}
// new keys added using self::write()
foreach ($this->cache_sums as $key => $value) {
if ($value === true && !in_array($key, $this->index)) {
$this->index[] = $key;
}
}
$data = serialize(array_values($this->index));
$this->add_item($this->ikey(), $data);
}
/**
* Gets the index entry from memcache/apc/redis DB.
*/
protected function load_index()
{
if ($this->index !== null) {
return;
}
$index_key = $this->ikey();
$data = $this->get_item($index_key);
$this->index = $data ? unserialize($data) : array();
}
/**
* Creates per-user cache key name (for memcache, apc, redis)
*
* @param string $key Cache key name
*
* @return string Cache key
*/
protected function ckey($key)
{
$key = $this->prefix . ':' . $key;
if ($this->userid) {
$key = $this->userid . ':' . $key;
}
return $key;
}
/**
* Creates per-user index cache key name (for memcache, apc, redis)
*
* @return string Cache key
*/
protected function ikey()
{
$key = $this->prefix . 'INDEX';
if ($this->userid) {
$key = $this->userid . ':' . $key;
}
return $key;
}
/**
* Serializes data for storing
*/
protected function serialize($data)
{
return $this->packed ? serialize($data) : $data;
}
/**
* Unserializes serialized data
*/
protected function unserialize($data)
{
return $this->packed ? @unserialize($data) : $data;
}
/**
* Determine the maximum size for cache data to be written
*/
protected function max_packet_size()
{
if ($this->max_packet < 0) {
$config = rcube::get_instance()->config;
$max_packet = $config->get($this->type . '_max_allowed_packet');
$this->max_packet = parse_bytes($max_packet) ?: 2097152; // default/max is 2 MB
}
return $this->max_packet;
}
/**
* Write memcache/apc/redis debug info to the log
*/
protected function debug($type, $key, $data = null, $result = null)
{
$line = strtoupper($type) . ' ' . $key;
if ($data !== null) {
$line .= ' ' . ($this->packed ? $data : serialize($data));
}
rcube::debug($this->type, $line, $result);
}
}
diff --git a/program/lib/Roundcube/rcube_charset.php b/program/lib/Roundcube/rcube_charset.php
index 86e2e17d4..b106965e2 100644
--- a/program/lib/Roundcube/rcube_charset.php
+++ b/program/lib/Roundcube/rcube_charset.php
@@ -1,910 +1,911 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2012, The Roundcube Dev Team |
- | Copyright (C) 2011-2012, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| Copyright (C) 2000 Edmund Grimley Evans <edmundo@rano.org> |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide charset conversion functionality |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Character sets conversion functionality
*
* @package Framework
* @subpackage Core
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
* @author Edmund Grimley Evans <edmundo@rano.org>
*/
class rcube_charset
{
// Aliases: some of them from HTML5 spec.
static public $aliases = array(
'USASCII' => 'WINDOWS-1252',
'ANSIX31101983' => 'WINDOWS-1252',
'ANSIX341968' => 'WINDOWS-1252',
'UNKNOWN8BIT' => 'ISO-8859-15',
'UNKNOWN' => 'ISO-8859-15',
'USERDEFINED' => 'ISO-8859-15',
'KSC56011987' => 'EUC-KR',
'GB2312' => 'GBK',
'GB231280' => 'GBK',
'UNICODE' => 'UTF-8',
'UTF7IMAP' => 'UTF7-IMAP',
'TIS620' => 'WINDOWS-874',
'ISO88599' => 'WINDOWS-1254',
'ISO885911' => 'WINDOWS-874',
'MACROMAN' => 'MACINTOSH',
'77' => 'MAC',
'128' => 'SHIFT-JIS',
'129' => 'CP949',
'130' => 'CP1361',
'134' => 'GBK',
'136' => 'BIG5',
'161' => 'WINDOWS-1253',
'162' => 'WINDOWS-1254',
'163' => 'WINDOWS-1258',
'177' => 'WINDOWS-1255',
'178' => 'WINDOWS-1256',
'186' => 'WINDOWS-1257',
'204' => 'WINDOWS-1251',
'222' => 'WINDOWS-874',
'238' => 'WINDOWS-1250',
'MS950' => 'CP950',
'WINDOWS949' => 'UHC',
);
/**
* Windows codepages
*
* @var array
*/
static public $windows_codepages = array(
37 => 'IBM037', // IBM EBCDIC US-Canada
437 => 'IBM437', // OEM United States
500 => 'IBM500', // IBM EBCDIC International
708 => 'ASMO-708', // Arabic (ASMO 708)
720 => 'DOS-720', // Arabic (Transparent ASMO); Arabic (DOS)
737 => 'IBM737', // OEM Greek (formerly 437G); Greek (DOS)
775 => 'IBM775', // OEM Baltic; Baltic (DOS)
850 => 'IBM850', // OEM Multilingual Latin 1; Western European (DOS)
852 => 'IBM852', // OEM Latin 2; Central European (DOS)
855 => 'IBM855', // OEM Cyrillic (primarily Russian)
857 => 'IBM857', // OEM Turkish; Turkish (DOS)
858 => 'IBM00858', // OEM Multilingual Latin 1 + Euro symbol
860 => 'IBM860', // OEM Portuguese; Portuguese (DOS)
861 => 'IBM861', // OEM Icelandic; Icelandic (DOS)
862 => 'DOS-862', // OEM Hebrew; Hebrew (DOS)
863 => 'IBM863', // OEM French Canadian; French Canadian (DOS)
864 => 'IBM864', // OEM Arabic; Arabic (864)
865 => 'IBM865', // OEM Nordic; Nordic (DOS)
866 => 'cp866', // OEM Russian; Cyrillic (DOS)
869 => 'IBM869', // OEM Modern Greek; Greek, Modern (DOS)
870 => 'IBM870', // IBM EBCDIC Multilingual/ROECE (Latin 2); IBM EBCDIC Multilingual Latin 2
874 => 'windows-874', // ANSI/OEM Thai (ISO 8859-11); Thai (Windows)
875 => 'cp875', // IBM EBCDIC Greek Modern
932 => 'shift_jis', // ANSI/OEM Japanese; Japanese (Shift-JIS)
936 => 'gb2312', // ANSI/OEM Simplified Chinese (PRC, Singapore); Chinese Simplified (GB2312)
950 => 'big5', // ANSI/OEM Traditional Chinese (Taiwan; Hong Kong SAR, PRC); Chinese Traditional (Big5)
1026 => 'IBM1026', // IBM EBCDIC Turkish (Latin 5)
1047 => 'IBM01047', // IBM EBCDIC Latin 1/Open System
1140 => 'IBM01140', // IBM EBCDIC US-Canada (037 + Euro symbol); IBM EBCDIC (US-Canada-Euro)
1141 => 'IBM01141', // IBM EBCDIC Germany (20273 + Euro symbol); IBM EBCDIC (Germany-Euro)
1142 => 'IBM01142', // IBM EBCDIC Denmark-Norway (20277 + Euro symbol); IBM EBCDIC (Denmark-Norway-Euro)
1143 => 'IBM01143', // IBM EBCDIC Finland-Sweden (20278 + Euro symbol); IBM EBCDIC (Finland-Sweden-Euro)
1144 => 'IBM01144', // IBM EBCDIC Italy (20280 + Euro symbol); IBM EBCDIC (Italy-Euro)
1145 => 'IBM01145', // IBM EBCDIC Latin America-Spain (20284 + Euro symbol); IBM EBCDIC (Spain-Euro)
1146 => 'IBM01146', // IBM EBCDIC United Kingdom (20285 + Euro symbol); IBM EBCDIC (UK-Euro)
1147 => 'IBM01147', // IBM EBCDIC France (20297 + Euro symbol); IBM EBCDIC (France-Euro)
1148 => 'IBM01148', // IBM EBCDIC International (500 + Euro symbol); IBM EBCDIC (International-Euro)
1149 => 'IBM01149', // IBM EBCDIC Icelandic (20871 + Euro symbol); IBM EBCDIC (Icelandic-Euro)
1200 => 'UTF-16', // Unicode UTF-16, little endian byte order (BMP of ISO 10646); available only to managed applications
1201 => 'UTF-16BE', // Unicode UTF-16, big endian byte order; available only to managed applications
1250 => 'windows-1250', // ANSI Central European; Central European (Windows)
1251 => 'windows-1251', // ANSI Cyrillic; Cyrillic (Windows)
1252 => 'windows-1252', // ANSI Latin 1; Western European (Windows)
1253 => 'windows-1253', // ANSI Greek; Greek (Windows)
1254 => 'windows-1254', // ANSI Turkish; Turkish (Windows)
1255 => 'windows-1255', // ANSI Hebrew; Hebrew (Windows)
1256 => 'windows-1256', // ANSI Arabic; Arabic (Windows)
1257 => 'windows-1257', // ANSI Baltic; Baltic (Windows)
1258 => 'windows-1258', // ANSI/OEM Vietnamese; Vietnamese (Windows)
10000 => 'macintosh', // MAC Roman; Western European (Mac)
12000 => 'UTF-32', // Unicode UTF-32, little endian byte order; available only to managed applications
12001 => 'UTF-32BE', // Unicode UTF-32, big endian byte order; available only to managed applications
20127 => 'US-ASCII', // US-ASCII (7-bit)
20273 => 'IBM273', // IBM EBCDIC Germany
20277 => 'IBM277', // IBM EBCDIC Denmark-Norway
20278 => 'IBM278', // IBM EBCDIC Finland-Sweden
20280 => 'IBM280', // IBM EBCDIC Italy
20284 => 'IBM284', // IBM EBCDIC Latin America-Spain
20285 => 'IBM285', // IBM EBCDIC United Kingdom
20290 => 'IBM290', // IBM EBCDIC Japanese Katakana Extended
20297 => 'IBM297', // IBM EBCDIC France
20420 => 'IBM420', // IBM EBCDIC Arabic
20423 => 'IBM423', // IBM EBCDIC Greek
20424 => 'IBM424', // IBM EBCDIC Hebrew
20838 => 'IBM-Thai', // IBM EBCDIC Thai
20866 => 'koi8-r', // Russian (KOI8-R); Cyrillic (KOI8-R)
20871 => 'IBM871', // IBM EBCDIC Icelandic
20880 => 'IBM880', // IBM EBCDIC Cyrillic Russian
20905 => 'IBM905', // IBM EBCDIC Turkish
20924 => 'IBM00924', // IBM EBCDIC Latin 1/Open System (1047 + Euro symbol)
20932 => 'EUC-JP', // Japanese (JIS 0208-1990 and 0212-1990)
20936 => 'cp20936', // Simplified Chinese (GB2312); Chinese Simplified (GB2312-80)
20949 => 'cp20949', // Korean Wansung
21025 => 'cp1025', // IBM EBCDIC Cyrillic Serbian-Bulgarian
21866 => 'koi8-u', // Ukrainian (KOI8-U); Cyrillic (KOI8-U)
28591 => 'iso-8859-1', // ISO 8859-1 Latin 1; Western European (ISO)
28592 => 'iso-8859-2', // ISO 8859-2 Central European; Central European (ISO)
28593 => 'iso-8859-3', // ISO 8859-3 Latin 3
28594 => 'iso-8859-4', // ISO 8859-4 Baltic
28595 => 'iso-8859-5', // ISO 8859-5 Cyrillic
28596 => 'iso-8859-6', // ISO 8859-6 Arabic
28597 => 'iso-8859-7', // ISO 8859-7 Greek
28598 => 'iso-8859-8', // ISO 8859-8 Hebrew; Hebrew (ISO-Visual)
28599 => 'iso-8859-9', // ISO 8859-9 Turkish
28603 => 'iso-8859-13', // ISO 8859-13 Estonian
28605 => 'iso-8859-15', // ISO 8859-15 Latin 9
38598 => 'iso-8859-8-i', // ISO 8859-8 Hebrew; Hebrew (ISO-Logical)
50220 => 'iso-2022-jp', // ISO 2022 Japanese with no halfwidth Katakana; Japanese (JIS)
50221 => 'csISO2022JP', // ISO 2022 Japanese with halfwidth Katakana; Japanese (JIS-Allow 1 byte Kana)
50222 => 'iso-2022-jp', // ISO 2022 Japanese JIS X 0201-1989; Japanese (JIS-Allow 1 byte Kana - SO/SI)
50225 => 'iso-2022-kr', // ISO 2022 Korean
51932 => 'EUC-JP', // EUC Japanese
51936 => 'EUC-CN', // EUC Simplified Chinese; Chinese Simplified (EUC)
51949 => 'EUC-KR', // EUC Korean
52936 => 'hz-gb-2312', // HZ-GB2312 Simplified Chinese; Chinese Simplified (HZ)
54936 => 'GB18030', // Windows XP and later: GB18030 Simplified Chinese (4 byte); Chinese Simplified (GB18030)
65000 => 'UTF-7',
65001 => 'UTF-8',
);
/**
* Catch an error and throw an exception.
*
* @param int $errno Level of the error
* @param string $errstr Error message
*/
public static function error_handler($errno, $errstr)
{
throw new ErrorException($errstr, 0, $errno);
}
/**
* Parse and validate charset name string (see #1485758).
* Sometimes charset string is malformed, there are also charset aliases
* but we need strict names for charset conversion (specially utf8 class)
*
* @param string $input Input charset name
*
* @return string The validated charset name
*/
public static function parse_charset($input)
{
static $charsets = array();
$charset = strtoupper($input);
if (isset($charsets[$input])) {
return $charsets[$input];
}
$charset = preg_replace(array(
'/^[^0-9A-Z]+/', // e.g. _ISO-8859-JP$SIO
'/\$.*$/', // e.g. _ISO-8859-JP$SIO
'/UNICODE-1-1-*/', // RFC1641/1642
'/^X-/', // X- prefix (e.g. X-ROMAN8 => ROMAN8)
), '', $charset);
if ($charset == 'BINARY') {
return $charsets[$input] = null;
}
// allow A-Z and 0-9 only
$str = preg_replace('/[^A-Z0-9]/', '', $charset);
if (isset(self::$aliases[$str])) {
$result = self::$aliases[$str];
}
// UTF
else if (preg_match('/U[A-Z][A-Z](7|8|16|32)(BE|LE)*/', $str, $m)) {
$result = 'UTF-' . $m[1] . $m[2];
}
// ISO-8859
else if (preg_match('/ISO8859([0-9]{0,2})/', $str, $m)) {
$iso = 'ISO-8859-' . ($m[1] ?: 1);
// some clients sends windows-1252 text as latin1,
// it is safe to use windows-1252 for all latin1
$result = $iso == 'ISO-8859-1' ? 'WINDOWS-1252' : $iso;
}
// handle broken charset names e.g. WINDOWS-1250HTTP-EQUIVCONTENT-TYPE
else if (preg_match('/(WIN|WINDOWS)([0-9]+)/', $str, $m)) {
$result = 'WINDOWS-' . $m[2];
}
// LATIN
else if (preg_match('/LATIN(.*)/', $str, $m)) {
$aliases = array('2' => 2, '3' => 3, '4' => 4, '5' => 9, '6' => 10,
'7' => 13, '8' => 14, '9' => 15, '10' => 16,
'ARABIC' => 6, 'CYRILLIC' => 5, 'GREEK' => 7, 'GREEK1' => 7, 'HEBREW' => 8
);
// some clients sends windows-1252 text as latin1,
// it is safe to use windows-1252 for all latin1
if ($m[1] == 1) {
$result = 'WINDOWS-1252';
}
// if iconv is not supported we need ISO labels, it's also safe for iconv
else if (!empty($aliases[$m[1]])) {
$result = 'ISO-8859-'.$aliases[$m[1]];
}
// iconv requires conversion of e.g. LATIN-1 to LATIN1
else {
$result = $str;
}
}
else {
$result = $charset;
}
$charsets[$input] = $result;
return $result;
}
/**
* Convert a string from one charset to another.
* Uses mbstring and iconv functions if possible
*
* @param string $str Input string
* @param string $from Suspected charset of the input string
* @param string $to Target charset to convert to; defaults to RCUBE_CHARSET
*
* @return string Converted string
*/
public static function convert($str, $from, $to = null)
{
static $iconv_options = null;
static $mbstring_sc = null;
$to = empty($to) ? RCUBE_CHARSET : strtoupper($to);
$from = self::parse_charset($from);
// It is a common case when UTF-16 charset is used with US-ASCII content (#1488654)
// In that case we can just skip the conversion (use UTF-8)
if ($from == 'UTF-16' && !preg_match('/[^\x00-\x7F]/', $str)) {
$from = 'UTF-8';
}
if ($from == $to || empty($str) || empty($from)) {
return $str;
}
if ($iconv_options === null) {
if (function_exists('iconv')) {
// ignore characters not available in output charset
$iconv_options = '//IGNORE';
if (iconv('', $iconv_options, '') === false) {
// iconv implementation does not support options
$iconv_options = '';
}
}
else {
$iconv_options = false;
}
}
// convert charset using iconv module
if ($iconv_options !== false && $from != 'UTF7-IMAP' && $to != 'UTF7-IMAP'
&& $from !== 'ISO-2022-JP'
) {
// throw an exception if iconv reports an illegal character in input
// it means that input string has been truncated
set_error_handler(array('rcube_charset', 'error_handler'), E_NOTICE);
try {
$out = iconv($from, $to . $iconv_options, $str);
}
catch (ErrorException $e) {
$out = false;
}
restore_error_handler();
if ($out !== false) {
return $out;
}
}
if ($mbstring_sc === null) {
$mbstring_sc = extension_loaded('mbstring') ? mb_substitute_character() : false;
}
// convert charset using mbstring module
if ($mbstring_sc !== false) {
$aliases = array(
'WINDOWS-1257' => 'ISO-8859-13',
'US-ASCII' => 'ASCII',
'ISO-2022-JP' => 'ISO-2022-JP-MS',
);
$mb_from = $aliases[$from] ?: $from;
$mb_to = $aliases[$to] ?: $to;
// Do the same as //IGNORE with iconv
mb_substitute_character('none');
// throw an exception if mbstring reports an illegal character in input
// using mb_check_encoding() is much slower
set_error_handler(array('rcube_charset', 'error_handler'), E_WARNING);
try {
$out = mb_convert_encoding($str, $mb_to, $mb_from);
}
catch (ErrorException $e) {
$out = false;
}
restore_error_handler();
mb_substitute_character($mbstring_sc);
if ($out !== false) {
return $out;
}
}
// convert charset using bundled classes/functions
if ($to == 'UTF-8') {
if ($from == 'UTF7-IMAP') {
if ($out = self::utf7imap_to_utf8($str)) {
return $out;
}
}
else if ($from == 'UTF-7') {
if ($out = self::utf7_to_utf8($str)) {
return $out;
}
}
}
// encode string for output
if ($from == 'UTF-8') {
// @TODO: we need a function for UTF-7 (RFC2152) conversion
if ($to == 'UTF7-IMAP' || $to == 'UTF-7') {
if ($out = self::utf8_to_utf7imap($str)) {
return $out;
}
}
}
if (!isset($out)) {
trigger_error("No suitable function found for '$from' to '$to' conversion");
}
// return original string
return $str;
}
/**
* Converts string from standard UTF-7 (RFC 2152) to UTF-8.
*
* @param string $str Input string (UTF-7)
*
* @return string Converted string (UTF-8)
*/
public static function utf7_to_utf8($str)
{
$Index_64 = array(
0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
0,0,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0,
1,1,1,1, 1,1,1,1, 1,1,0,0, 0,0,0,0,
0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1,
1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0,
0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1,
1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0,
);
$u7len = strlen($str);
$str = strval($str);
$res = '';
for ($i=0; $u7len > 0; $i++, $u7len--) {
$u7 = $str[$i];
if ($u7 == '+') {
$i++;
$u7len--;
$ch = '';
for (; $u7len > 0; $i++, $u7len--) {
$u7 = $str[$i];
if (!$Index_64[ord($u7)]) {
break;
}
$ch .= $u7;
}
if ($ch == '') {
if ($u7 == '-') {
$res .= '+';
}
continue;
}
$res .= self::utf16_to_utf8(base64_decode($ch));
}
else {
$res .= $u7;
}
}
return $res;
}
/**
* Converts string from UTF-16 to UTF-8 (helper for utf-7 to utf-8 conversion)
*
* @param string $str Input string
*
* @return string The converted string
*/
public static function utf16_to_utf8($str)
{
$len = strlen($str);
$dec = '';
for ($i = 0; $i < $len; $i += 2) {
$c = ord($str[$i]) << 8 | ord($str[$i + 1]);
if ($c >= 0x0001 && $c <= 0x007F) {
$dec .= chr($c);
}
else if ($c > 0x07FF) {
$dec .= chr(0xE0 | (($c >> 12) & 0x0F));
$dec .= chr(0x80 | (($c >> 6) & 0x3F));
$dec .= chr(0x80 | (($c >> 0) & 0x3F));
}
else {
$dec .= chr(0xC0 | (($c >> 6) & 0x1F));
$dec .= chr(0x80 | (($c >> 0) & 0x3F));
}
}
return $dec;
}
/**
* Convert the data ($str) from RFC 2060's UTF-7 to UTF-8.
* If input data is invalid, return the original input string.
* RFC 2060 obviously intends the encoding to be unique (see
* point 5 in section 5.1.3), so we reject any non-canonical
* form, such as &ACY- (instead of &-) or &AMA-&AMA- (instead
* of &AMAAwA-).
*
* Translated from C to PHP by Thomas Bruederli <roundcube@gmail.com>
*
* @param string $str Input string (UTF7-IMAP)
*
* @return string Output string (UTF-8)
*/
public static function utf7imap_to_utf8($str)
{
$Index_64 = array(
-1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, 63,-1,-1,-1,
52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1,-1,-1,-1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
-1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
);
$u7len = strlen($str);
$str = strval($str);
$p = '';
$err = '';
for ($i=0; $u7len > 0; $i++, $u7len--) {
$u7 = $str[$i];
if ($u7 == '&') {
$i++;
$u7len--;
$u7 = $str[$i];
if ($u7len && $u7 == '-') {
$p .= '&';
continue;
}
$ch = 0;
$k = 10;
for (; $u7len > 0; $i++, $u7len--) {
$u7 = $str[$i];
if ((ord($u7) & 0x80) || ($b = $Index_64[ord($u7)]) == -1) {
break;
}
if ($k > 0) {
$ch |= $b << $k;
$k -= 6;
}
else {
$ch |= $b >> (-$k);
if ($ch < 0x80) {
// Printable US-ASCII
if (0x20 <= $ch && $ch < 0x7f) {
return $err;
}
$p .= chr($ch);
}
else if ($ch < 0x800) {
$p .= chr(0xc0 | ($ch >> 6));
$p .= chr(0x80 | ($ch & 0x3f));
}
else {
$p .= chr(0xe0 | ($ch >> 12));
$p .= chr(0x80 | (($ch >> 6) & 0x3f));
$p .= chr(0x80 | ($ch & 0x3f));
}
$ch = ($b << (16 + $k)) & 0xffff;
$k += 10;
}
}
// Non-zero or too many extra bits
if ($ch || $k < 6) {
return $err;
}
// BASE64 not properly terminated
if (!$u7len || $u7 != '-') {
return $err;
}
// Adjacent BASE64 sections
if ($u7len > 2 && $str[$i+1] == '&' && $str[$i+2] != '-') {
return $err;
}
}
// Not printable US-ASCII
else if (ord($u7) < 0x20 || ord($u7) >= 0x7f) {
return $err;
}
else {
$p .= $u7;
}
}
return $p;
}
/**
* Convert the data ($str) from UTF-8 to RFC 2060's UTF-7.
* Unicode characters above U+FFFF are replaced by U+FFFE.
* If input data is invalid, return an empty string.
*
* Translated from C to PHP by Thomas Bruederli <roundcube@gmail.com>
*
* @param string $str Input string (UTF-8)
*
* @return string Output string (UTF7-IMAP)
*/
public static function utf8_to_utf7imap($str)
{
$B64Chars = array(
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', '+', ','
);
$u8len = strlen($str);
$base64 = 0;
$i = 0;
$p = '';
$err = '';
while ($u8len) {
$u8 = $str[$i];
$c = ord($u8);
if ($c < 0x80) {
$ch = $c;
$n = 0;
}
else if ($c < 0xc2) {
return $err;
}
else if ($c < 0xe0) {
$ch = $c & 0x1f;
$n = 1;
}
else if ($c < 0xf0) {
$ch = $c & 0x0f;
$n = 2;
}
else if ($c < 0xf8) {
$ch = $c & 0x07;
$n = 3;
}
else if ($c < 0xfc) {
$ch = $c & 0x03;
$n = 4;
}
else if ($c < 0xfe) {
$ch = $c & 0x01;
$n = 5;
}
else {
return $err;
}
$i++;
$u8len--;
if ($n > $u8len) {
return $err;
}
for ($j=0; $j < $n; $j++) {
$o = ord($str[$i+$j]);
if (($o & 0xc0) != 0x80) {
return $err;
}
$ch = ($ch << 6) | ($o & 0x3f);
}
if ($n > 1 && !($ch >> ($n * 5 + 1))) {
return $err;
}
$i += $n;
$u8len -= $n;
if ($ch < 0x20 || $ch >= 0x7f) {
if (!$base64) {
$p .= '&';
$base64 = 1;
$b = 0;
$k = 10;
}
if ($ch & ~0xffff) {
$ch = 0xfffe;
}
$p .= $B64Chars[($b | $ch >> $k)];
$k -= 6;
for (; $k >= 0; $k -= 6) {
$p .= $B64Chars[(($ch >> $k) & 0x3f)];
}
$b = ($ch << (-$k)) & 0x3f;
$k += 16;
}
else {
if ($base64) {
if ($k > 10) {
$p .= $B64Chars[$b];
}
$p .= '-';
$base64 = 0;
}
$p .= chr($ch);
if (chr($ch) == '&') {
$p .= '-';
}
}
}
if ($base64) {
if ($k > 10) {
$p .= $B64Chars[$b];
}
$p .= '-';
}
return $p;
}
/**
* A method to guess character set of a string.
*
* @param string $string String
* @param string $failover Default result for failover
* @param string $language User language
*
* @return string Charset name
*/
public static function detect($string, $failover = null, $language = null)
{
if (substr($string, 0, 4) == "\0\0\xFE\xFF") return 'UTF-32BE'; // Big Endian
if (substr($string, 0, 4) == "\xFF\xFE\0\0") return 'UTF-32LE'; // Little Endian
if (substr($string, 0, 2) == "\xFE\xFF") return 'UTF-16BE'; // Big Endian
if (substr($string, 0, 2) == "\xFF\xFE") return 'UTF-16LE'; // Little Endian
if (substr($string, 0, 3) == "\xEF\xBB\xBF") return 'UTF-8';
// heuristics
if ($string[0] == "\0" && $string[1] == "\0" && $string[2] == "\0" && $string[3] != "\0") return 'UTF-32BE';
if ($string[0] != "\0" && $string[1] == "\0" && $string[2] == "\0" && $string[3] == "\0") return 'UTF-32LE';
if ($string[0] == "\0" && $string[1] != "\0" && $string[2] == "\0" && $string[3] != "\0") return 'UTF-16BE';
if ($string[0] != "\0" && $string[1] == "\0" && $string[2] != "\0" && $string[3] == "\0") return 'UTF-16LE';
if (empty($language)) {
$rcube = rcube::get_instance();
$language = $rcube->get_user_language();
}
// Prioritize charsets according to current language (#1485669)
switch ($language) {
case 'ja_JP':
$prio = array('ISO-2022-JP', 'JIS', 'UTF-8', 'EUC-JP', 'eucJP-win', 'SJIS', 'SJIS-win');
break;
case 'zh_CN':
case 'zh_TW':
$prio = array('UTF-8', 'BIG-5', 'GB2312', 'EUC-TW');
break;
case 'ko_KR':
$prio = array('UTF-8', 'EUC-KR', 'ISO-2022-KR');
break;
case 'ru_RU':
$prio = array('UTF-8', 'WINDOWS-1251', 'KOI8-R');
break;
case 'tr_TR':
$prio = array('UTF-8', 'ISO-8859-9', 'WINDOWS-1254');
break;
}
// mb_detect_encoding() is not reliable for some charsets (#1490135)
// use mb_check_encoding() to make charset priority lists really working
if ($prio && function_exists('mb_check_encoding')) {
foreach ($prio as $encoding) {
if (mb_check_encoding($string, $encoding)) {
return $encoding;
}
}
}
if (function_exists('mb_detect_encoding')) {
if (!$prio) {
$prio = array('UTF-8', 'SJIS', 'GB2312',
'ISO-8859-1', 'ISO-8859-2', 'ISO-8859-3', 'ISO-8859-4',
'ISO-8859-5', 'ISO-8859-6', 'ISO-8859-7', 'ISO-8859-8', 'ISO-8859-9',
'ISO-8859-10', 'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15', 'ISO-8859-16',
'WINDOWS-1252', 'WINDOWS-1251', 'EUC-JP', 'EUC-TW', 'KOI8-R', 'BIG-5',
'ISO-2022-KR', 'ISO-2022-JP',
);
}
$encodings = array_unique(array_merge($prio, mb_list_encodings()));
if ($encoding = mb_detect_encoding($string, $encodings)) {
return $encoding;
}
}
// No match, check for UTF-8
// from http://w3.org/International/questions/qa-forms-utf-8.html
if (preg_match('/\A(
[\x09\x0A\x0D\x20-\x7E]
| [\xC2-\xDF][\x80-\xBF]
| \xE0[\xA0-\xBF][\x80-\xBF]
| [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}
| \xED[\x80-\x9F][\x80-\xBF]
| \xF0[\x90-\xBF][\x80-\xBF]{2}
| [\xF1-\xF3][\x80-\xBF]{3}
| \xF4[\x80-\x8F][\x80-\xBF]{2}
)*\z/xs', substr($string, 0, 2048))
) {
return 'UTF-8';
}
return $failover;
}
/**
* Removes non-unicode characters from input.
*
* @param mixed $input String or array.
*
* @return mixed String or array
*/
public static function clean($input)
{
// handle input of type array
if (is_array($input)) {
foreach ($input as $idx => $val) {
$input[$idx] = self::clean($val);
}
return $input;
}
if (!is_string($input) || $input == '') {
return $input;
}
// iconv/mbstring are much faster (especially with long strings)
if (function_exists('mb_convert_encoding')) {
$msch = mb_substitute_character();
mb_substitute_character('none');
$res = mb_convert_encoding($input, 'UTF-8', 'UTF-8');
mb_substitute_character($msch);
if ($res !== false) {
return $res;
}
}
if (function_exists('iconv')) {
if (($res = @iconv('UTF-8', 'UTF-8//IGNORE', $input)) !== false) {
return $res;
}
}
$seq = '';
$out = '';
$regexp = '/^('.
// '[\x00-\x7F]'. // UTF8-1
'|[\xC2-\xDF][\x80-\xBF]'. // UTF8-2
'|\xE0[\xA0-\xBF][\x80-\xBF]'. // UTF8-3
'|[\xE1-\xEC][\x80-\xBF][\x80-\xBF]'. // UTF8-3
'|\xED[\x80-\x9F][\x80-\xBF]'. // UTF8-3
'|[\xEE-\xEF][\x80-\xBF][\x80-\xBF]'. // UTF8-3
'|\xF0[\x90-\xBF][\x80-\xBF][\x80-\xBF]'. // UTF8-4
'|[\xF1-\xF3][\x80-\xBF][\x80-\xBF][\x80-\xBF]'.// UTF8-4
'|\xF4[\x80-\x8F][\x80-\xBF][\x80-\xBF]'. // UTF8-4
')$/';
for ($i = 0, $len = strlen($input); $i < $len; $i++) {
$chr = $input[$i];
$ord = ord($chr);
// 1-byte character
if ($ord <= 0x7F) {
if ($seq !== '') {
$out .= preg_match($regexp, $seq) ? $seq : '';
$seq = '';
}
$out .= $chr;
}
// first byte of multibyte sequence
else if ($ord >= 0xC0) {
if ($seq !== '') {
$out .= preg_match($regexp, $seq) ? $seq : '';
$seq = '';
}
$seq = $chr;
}
// next byte of multibyte sequence
else if ($seq !== '') {
$seq .= $chr;
}
}
if ($seq !== '') {
$out .= preg_match($regexp, $seq) ? $seq : '';
}
return $out;
}
}
diff --git a/program/lib/Roundcube/rcube_config.php b/program/lib/Roundcube/rcube_config.php
index 62093aa6a..378e91e18 100644
--- a/program/lib/Roundcube/rcube_config.php
+++ b/program/lib/Roundcube/rcube_config.php
@@ -1,884 +1,885 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2014, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Class to read configuration settings |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Configuration class for Roundcube
*
* @package Framework
* @subpackage Core
*/
class rcube_config
{
const DEFAULT_SKIN = 'larry';
private $env = '';
private $paths = array();
private $prop = array();
private $errors = array();
private $userprefs = array();
private $immutable = array();
private $client_tz;
/**
* Renamed options
*
* @var array
*/
private $legacy_props = array(
// new name => old name
'mail_pagesize' => 'pagesize',
'addressbook_pagesize' => 'pagesize',
'reply_mode' => 'top_posting',
'refresh_interval' => 'keep_alive',
'min_refresh_interval' => 'min_keep_alive',
'messages_cache_ttl' => 'message_cache_lifetime',
'mail_read_time' => 'preview_pane_mark_read',
'redundant_attachments_cache_ttl' => 'redundant_attachments_memcache_ttl',
);
/**
* Object constructor
*
* @param string $env Environment suffix for config files to load
*/
public function __construct($env = '')
{
$this->env = $env;
if ($paths = getenv('RCUBE_CONFIG_PATH')) {
$this->paths = explode(PATH_SEPARATOR, $paths);
// make all paths absolute
foreach ($this->paths as $i => $path) {
if (!rcube_utils::is_absolute_path($path)) {
if ($realpath = realpath(RCUBE_INSTALL_PATH . $path)) {
$this->paths[$i] = unslashify($realpath) . '/';
}
else {
unset($this->paths[$i]);
}
}
else {
$this->paths[$i] = unslashify($path) . '/';
}
}
}
if (defined('RCUBE_CONFIG_DIR') && !in_array(RCUBE_CONFIG_DIR, $this->paths)) {
$this->paths[] = RCUBE_CONFIG_DIR;
}
if (empty($this->paths)) {
$this->paths[] = RCUBE_INSTALL_PATH . 'config/';
}
$this->load();
// Defaults, that we do not require you to configure,
// but contain information that is used in various locations in the code:
if (empty($this->prop['contactlist_fields'])) {
$this->set('contactlist_fields', array('name', 'firstname', 'surname', 'email'));
}
}
/**
* @brief Guess the type the string may fit into.
*
* Look inside the string to determine what type might be best as a container.
*
* @param mixed $value The value to inspect
*
* @return The guess at the type.
*/
private function guess_type($value)
{
$type = 'string';
// array requires hint to be passed.
if (preg_match('/^[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?$/', $value)) {
$type = 'double';
}
else if (preg_match('/^\d+$/', $value)) {
$type = 'integer';
}
else if (preg_match('/^(t(rue)?)|(f(alse)?)$/i', $value)) {
$type = 'boolean';
}
return $type;
}
/**
* @brief Parse environment variable into PHP type.
*
* Perform an appropriate parsing of the string to create the desired PHP type.
*
* @param string $string String to parse into PHP type
* @param string $type Type of value to return
*
* @return Appropriately typed interpretation of $string.
*/
private function parse_env($string, $type)
{
$_ = $string;
switch ($type) {
case 'boolean':
$_ = (boolean) $_;
break;
case 'integer':
$_ = (integer) $_;
break;
case 'double':
$_ = (double) $_;
break;
case 'string':
break;
case 'array':
$_ = json_decode($_, true);
break;
case 'object':
$_ = json_decode($_, false);
break;
case 'resource':
case 'NULL':
default:
$_ = $this->parse_env($_, $this->guess_type($_));
}
return $_;
}
/**
* @brief Get environment variable value.
*
* Retrieve an environment variable's value or if it's not found, return the
* provided default value.
*
* @param string $varname Environment variable name
* @param mixed $default_value Default value to return if necessary
* @param string $type Type of value to return
*
* @return Value of the environment variable or default if not found.
*/
private function getenv_default($varname, $default_value, $type = null)
{
$value = getenv($varname);
if ($value === false) {
$value = $default_value;
}
else {
$value = $this->parse_env($value, $type ?: gettype($default_value));
}
return $value;
}
/**
* Load config from local config file
*/
private function load()
{
// Load default settings
if (!$this->load_from_file('defaults.inc.php')) {
$this->errors[] = 'defaults.inc.php was not found.';
}
// load main config file
if (!$this->load_from_file('config.inc.php')) {
// Old configuration files
if (!$this->load_from_file('main.inc.php') || !$this->load_from_file('db.inc.php')) {
$this->errors[] = 'config.inc.php was not found.';
}
else if (rand(1,100) == 10) { // log warning on every 100th request (average)
trigger_error("config.inc.php was not found. Please migrate your config by running bin/update.sh", E_USER_WARNING);
}
}
// load host-specific configuration
$this->load_host_config();
// set skin (with fallback to old 'skin_path' property)
if (empty($this->prop['skin'])) {
if (!empty($this->prop['skin_path'])) {
$this->prop['skin'] = str_replace('skins/', '', unslashify($this->prop['skin_path']));
}
else {
$this->prop['skin'] = self::DEFAULT_SKIN;
}
}
// larry is the new default skin :-)
if ($this->prop['skin'] == 'default') {
$this->prop['skin'] = self::DEFAULT_SKIN;
}
// fix paths
foreach (array('log_dir' => 'logs', 'temp_dir' => 'temp') as $key => $dir) {
foreach (array($this->prop[$key], '../' . $this->prop[$key], RCUBE_INSTALL_PATH . $dir) as $path) {
if ($path && ($realpath = realpath(unslashify($path)))) {
$this->prop[$key] = $realpath;
break;
}
}
}
// 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], RCUBE_CHARSET, 'UTF7-IMAP');
}
// set PHP error logging according to config
$error_log = $this->prop['log_driver'] ?: 'file';
if ($error_log == 'file') {
$error_log = $this->prop['log_dir'] . '/errors';
$error_log .= isset($this->prop['log_file_ext']) ? $this->prop['log_file_ext'] : '.log';
}
if ($error_log && $error_log != 'stdout') {
ini_set('error_log', $error_log);
}
// remove deprecated properties
unset($this->prop['dst_active']);
}
/**
* Load a host-specific config file if configured
* This will merge the host specific configuration with the given one
*/
private function load_host_config()
{
if (empty($this->prop['include_host_config'])) {
return;
}
foreach (array('HTTP_HOST', 'SERVER_NAME', 'SERVER_ADDR') as $key) {
$fname = null;
$name = $_SERVER[$key];
if (!$name) {
continue;
}
if (is_array($this->prop['include_host_config'])) {
$fname = $this->prop['include_host_config'][$name];
}
else {
$fname = preg_replace('/[^a-z0-9\.\-_]/i', '', $name) . '.inc.php';
}
if ($fname && $this->load_from_file($fname)) {
return;
}
}
}
/**
* Read configuration from a file
* and merge with the already stored config values
*
* @param string $file Name of the config file to be loaded
*
* @return booelan True on success, false on failure
*/
public function load_from_file($file)
{
$success = false;
foreach ($this->resolve_paths($file) as $fpath) {
if ($fpath && is_file($fpath) && is_readable($fpath)) {
// use output buffering, we don't need any output here
ob_start();
include($fpath);
ob_end_clean();
if (is_array($config)) {
$this->merge($config);
$success = true;
}
// deprecated name of config variable
if (is_array($rcmail_config)) {
$this->merge($rcmail_config);
$success = true;
}
}
}
return $success;
}
/**
* Helper method to resolve absolute paths to the given config file.
* This also takes the 'env' property into account.
*
* @param string $file Filename or absolute file path
* @param boolean $use_env Return -$env file path if exists
*
* @return array List of candidates in config dir path(s)
*/
public function resolve_paths($file, $use_env = true)
{
$files = array();
$abs_path = rcube_utils::is_absolute_path($file);
foreach ($this->paths as $basepath) {
$realpath = $abs_path ? $file : realpath($basepath . '/' . $file);
// check if <file>-env.ini exists
if ($realpath && $use_env && !empty($this->env)) {
$envfile = preg_replace('/\.(inc.php)$/', '-' . $this->env . '.\\1', $realpath);
if (is_file($envfile)) {
$realpath = $envfile;
}
}
if ($realpath) {
$files[] = $realpath;
// no need to continue the loop if an absolute file path is given
if ($abs_path) {
break;
}
}
}
return $files;
}
/**
* Getter for a specific config parameter
*
* @param string $name Parameter name
* @param mixed $def Default value if not set
*
* @return mixed The requested config value
*/
public function get($name, $def = null)
{
if (isset($this->prop[$name])) {
$result = $this->prop[$name];
}
else {
$result = $def;
}
$result = $this->getenv_default('ROUNDCUBE_' . strtoupper($name), $result);
$rcube = rcube::get_instance();
if ($name == 'timezone') {
if (empty($result) || $result == 'auto') {
$result = $this->client_timezone();
}
}
else if ($name == 'client_mimetypes') {
if (!$result && !$def) {
$result = 'text/plain,text/html,text/xml'
. ',image/jpeg,image/gif,image/png,image/bmp,image/tiff,image/webp'
. ',application/x-javascript,application/pdf,application/x-shockwave-flash';
}
if ($result && is_string($result)) {
$result = explode(',', $result);
}
}
$plugin = $rcube->plugins->exec_hook('config_get', array(
'name' => $name, 'default' => $def, 'result' => $result));
return $plugin['result'];
}
/**
* Setter for a config parameter
*
* @param string $name Parameter name
* @param mixed $value Parameter value
* @param bool $immutable Make the value immutable
*/
public function set($name, $value, $immutable = false)
{
$this->prop[$name] = $value;
if ($immutable) {
$this->immutable[$name] = $value;
}
}
/**
* Override config options with the given values (eg. user prefs)
*
* @param array $prefs Hash array with config props to merge over
*/
public function merge($prefs)
{
$prefs = $this->fix_legacy_props($prefs);
$this->prop = array_merge($this->prop, $prefs, $this->userprefs, $this->immutable);
}
/**
* Merge the given prefs over the current config
* and make sure that they survive further merging.
*
* @param array $prefs Hash array with user prefs
*/
public function set_user_prefs($prefs)
{
$prefs = $this->fix_legacy_props($prefs);
// Honor the dont_override setting for any existing user preferences
$dont_override = $this->get('dont_override');
if (is_array($dont_override) && !empty($dont_override)) {
foreach ($dont_override as $key) {
unset($prefs[$key]);
}
}
// larry is the new default skin :-)
if ($prefs['skin'] == 'default') {
$prefs['skin'] = self::DEFAULT_SKIN;
}
$this->userprefs = $prefs;
$this->prop = array_merge($this->prop, $prefs);
}
/**
* Getter for all config options
*
* @return array Hash array containing all config properties
*/
public function all()
{
$props = $this->prop;
foreach ($props as $prop_name => $prop_value) {
$props[$prop_name] = $this->getenv_default('ROUNDCUBE_' . strtoupper($prop_name), $prop_value);
}
$rcube = rcube::get_instance();
$plugin = $rcube->plugins->exec_hook('config_get', array(
'name' => '*', 'result' => $props));
return $plugin['result'];
}
/**
* Some options set as immutable that are also listed
* in dont_override should not be stored permanently
* in user preferences. Here's the list of these
*
* @return array List of transient options
*/
public function transient_options()
{
return array_intersect(array_keys($this->immutable), (array) $this->get('dont_override'));
}
/**
* Special getter for user's timezone offset including DST
*
* @return float Timezone offset (in hours)
* @deprecated
*/
public function get_timezone()
{
if ($tz = $this->get('timezone')) {
try {
$tz = new DateTimeZone($tz);
return $tz->getOffset(new DateTime('now')) / 3600;
}
catch (Exception $e) {
}
}
return 0;
}
/**
* Return requested DES crypto key.
*
* @param string $key 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) || empty($this->prop[$key])) {
rcube::raise_error(array(
'code' => 500, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Request for unconfigured crypto key \"$key\""
), true, true);
}
return $this->prop[$key];
}
/**
* Return configured crypto method.
*
* @return string Crypto method
*/
public function get_crypto_method()
{
return $this->get('cipher_method') ?: 'DES-EDE3-CBC';
}
/**
* Try to autodetect operating system and find the correct line endings
*
* @return string The appropriate mail header delimiter
* @deprecated Since 1.3 we don't use mail()
*/
public function header_delimiter()
{
// use the configured delimiter for headers
if (!empty($this->prop['mail_header_delimiter'])) {
$delim = $this->prop['mail_header_delimiter'];
if ($delim == "\n" || $delim == "\r\n") {
return $delim;
}
else {
rcube::raise_error(array(
'code' => 500, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Invalid mail_header_delimiter setting"
), true, false);
}
}
$php_os = strtolower(substr(PHP_OS, 0, 3));
if ($php_os == 'win')
return "\r\n";
if ($php_os == 'mac')
return "\r\n";
return "\n";
}
/**
* Return the mail domain configured for the given host
*
* @param string $host IMAP host
* @param boolean $encode If true, domain name will be converted to IDN ASCII
*
* @return string Resolved SMTP host
*/
public function mail_domain($host, $encode=true)
{
$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 = rcube_utils::parse_host($this->prop['mail_domain']);
}
if ($encode) {
$domain = rcube_utils::idn_to_ascii($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);
}
/**
* Internal getter for client's (browser) timezone identifier
*/
private function client_timezone()
{
if ($this->client_tz) {
return $this->client_tz;
}
// @TODO: remove this legacy timezone handling in the future
$props = $this->fix_legacy_props(array('timezone' => $_SESSION['timezone']));
if (!empty($props['timezone'])) {
// Prevent from using deprecated timezone names
$props['timezone'] = $this->resolve_timezone_alias($props['timezone']);
try {
$tz = new DateTimeZone($props['timezone']);
return $this->client_tz = $tz->getName();
}
catch (Exception $e) { /* gracefully ignore */ }
}
// fallback to server's timezone
return date_default_timezone_get();
}
/**
* Convert legacy options into new ones
*
* @param array $props Hash array with config props
*
* @return array Converted config props
*/
private function fix_legacy_props($props)
{
foreach ($this->legacy_props as $new => $old) {
if (isset($props[$old])) {
if (!isset($props[$new])) {
$props[$new] = $props[$old];
}
unset($props[$old]);
}
}
// convert deprecated numeric timezone value
if (isset($props['timezone']) && is_numeric($props['timezone'])) {
if ($tz = self::timezone_name_from_abbr($props['timezone'])) {
$props['timezone'] = $tz;
}
else {
unset($props['timezone']);
}
}
// translate old `preview_pane` settings to `layout`
if (isset($props['preview_pane']) && !isset($props['layout'])) {
$props['layout'] = $props['preview_pane'] ? 'desktop' : 'list';
unset($props['preview_pane']);
}
// translate old `display_version` settings to `display_product_info`
if (isset($props['display_version']) && !isset($props['display_product_info'])) {
$props['display_product_info'] = $props['display_version'] ? 2 : 1;
unset($props['display_version']);
}
return $props;
}
/**
* timezone_name_from_abbr() replacement. Converts timezone offset
* into timezone name abbreviation.
*
* @param float $offset Timezone offset (in hours)
*
* @return string Timezone abbreviation
*/
static public function timezone_name_from_abbr($offset)
{
// List of timezones here is not complete - https://bugs.php.net/bug.php?id=44780
if ($tz = timezone_name_from_abbr('', $offset * 3600, 0)) {
return $tz;
}
// try with more complete list (#1489261)
$timezones = array(
'-660' => "Pacific/Apia",
'-600' => "Pacific/Honolulu",
'-570' => "Pacific/Marquesas",
'-540' => "America/Anchorage",
'-480' => "America/Los_Angeles",
'-420' => "America/Denver",
'-360' => "America/Chicago",
'-300' => "America/New_York",
'-270' => "America/Caracas",
'-240' => "America/Halifax",
'-210' => "Canada/Newfoundland",
'-180' => "America/Sao_Paulo",
'-60' => "Atlantic/Azores",
'0' => "Europe/London",
'60' => "Europe/Paris",
'120' => "Europe/Helsinki",
'180' => "Europe/Moscow",
'210' => "Asia/Tehran",
'240' => "Asia/Dubai",
'270' => "Asia/Kabul",
'300' => "Asia/Karachi",
'330' => "Asia/Kolkata",
'345' => "Asia/Katmandu",
'360' => "Asia/Yekaterinburg",
'390' => "Asia/Rangoon",
'420' => "Asia/Krasnoyarsk",
'480' => "Asia/Shanghai",
'525' => "Australia/Eucla",
'540' => "Asia/Tokyo",
'570' => "Australia/Adelaide",
'600' => "Australia/Melbourne",
'630' => "Australia/Lord_Howe",
'660' => "Asia/Vladivostok",
'690' => "Pacific/Norfolk",
'720' => "Pacific/Auckland",
'765' => "Pacific/Chatham",
'780' => "Pacific/Enderbury",
'840' => "Pacific/Kiritimati",
);
return $timezones[(string) intval($offset * 60)];
}
/**
* Replace deprecated timezone name with a valid one.
*
* @param string $tzname Timezone name
*
* @return string Timezone name
*/
static public function resolve_timezone_alias($tzname)
{
// http://www.php.net/manual/en/timezones.others.php
// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
$deprecated_timezones = array(
'Australia/ACT' => 'Australia/Sydney',
'Australia/LHI' => 'Australia/Lord_Howe',
'Australia/North' => 'Australia/Darwin',
'Australia/NSW' => 'Australia/Sydney',
'Australia/Queensland' => 'Australia/Brisbane',
'Australia/South' => 'Australia/Adelaide',
'Australia/Adelaide' => 'Australia/Hobart',
'Australia/Tasmania' => 'Australia/Hobart',
'Australia/Victoria' => 'Australia/Melbourne',
'Australia/West' => 'Australia/Perth',
'Brazil/Acre' => 'America/Rio_Branco',
'Brazil/DeNoronha' => 'America/Noronha',
'Brazil/East' => 'America/Sao_Paulo',
'Brazil/West' => 'America/Manaus',
'Canada/Atlantic' => 'America/Halifax',
'Canada/Central' => 'America/Winnipeg',
'Canada/Eastern' => 'America/Toronto',
'Canada/Mountain' => 'America/Edmonton',
'Canada/Newfoundland' => 'America/St_Johns',
'Canada/Pacific' => 'America/Vancouver',
'Canada/Saskatchewan' => 'America/Regina',
'Canada/Yukon' => 'America/Whitehorse',
'CET' => 'Europe/Berlin',
'Chile/Continental' => 'America/Santiago',
'Chile/EasterIsland' => 'Pacific/Easter',
'CST6CDT' => 'America/Chicago',
'Cuba' => ' America/Havana',
'EET' => 'Europe/Berlin',
'Egypt' => 'Africa/Cairo',
'Eire' => 'Europe/Dublin',
'EST' => 'America/New_York',
'EST5EDT' => 'America/New_York',
'Factory' => 'UTC', // ?
'GB' => 'Europe/London',
'GB-Eire' => 'Europe/London',
'GMT' => 'UTC',
'GMT+0' => 'UTC',
'GMT-0' => 'UTC',
'GMT0' => 'UTC',
'Greenwich' => 'UTC',
'Hongkong' => 'Asia/Hong_Kong',
'HST' => 'Pacific/Honolulu',
'Iceland' => 'Atlantic/Reykjavik',
'Iran' => 'Asia/Tehran',
'Israel' => 'Asia/Jerusalem',
'Jamaica' => 'America/Jamaica',
'Japan' => 'Asia/Tokyo',
'Kwajalein' => 'Pacific/Kwajalein',
'Libya' => 'Africa/Tripoli',
'MET' => 'Europe/Berlin',
'Mexico/BajaNorte' => 'America/Tijuana',
'Mexico/BajaSur' => 'America/Mazatlan',
'Mexico/General' => 'America/Mexico_City',
'MST' => 'America/Denver',
'MST7MDT' => 'America/Denver',
'Navajo' => 'America/Denver',
'NZ' => 'Pacific/Auckland',
'NZ-CHAT' => 'Pacific/Chatham',
'Poland' => 'Europe/Warsaw',
'Portugal' => 'Europe/Lisbon',
'PRC' => 'Asia/Shanghai',
'PST8PDT' => 'America/Los_Angeles',
'ROC' => 'Asia/Taipei',
'ROK' => 'Asia/Seoul',
'Singapore' => 'Asia/Singapore',
'Turkey' => 'Europe/Istanbul',
'UCT' => 'UTC',
'Universal' => 'UTC',
'US/Alaska' => 'America/Anchorage',
'US/Aleutian' => 'America/Adak',
'US/Arizona' => 'America/Phoenix',
'US/Central' => 'America/Chicago',
'US/East-Indiana' => 'America/Indiana/Indianapolis',
'US/Eastern' => 'America/New_York',
'US/Hawaii' => 'Pacific/Honolulu',
'US/Indiana-Starke' => 'America/Indiana/Knox',
'US/Michigan' => 'America/Detroit',
'US/Mountain' => 'America/Denver',
'US/Pacific' => 'America/Los_Angeles',
'US/Pacific-New' => 'America/Los_Angeles',
'US/Samoa' => 'Pacific/Pago_Pago',
'W-SU' => 'Europe/Moscow',
'WET' => 'Europe/Berlin',
'Z' => 'UTC',
'Zulu' => 'UTC',
// Some of these Etc/X zones are not deprecated, but still problematic
'Etc/GMT' => 'UTC',
'Etc/GMT+0' => 'UTC',
'Etc/GMT+1' => 'Atlantic/Azores',
'Etc/GMT+10' => 'Pacific/Honolulu',
'Etc/GMT+11' => 'Pacific/Midway',
'Etc/GMT+12' => 'Pacific/Auckland',
'Etc/GMT+2' => 'America/Noronha',
'Etc/GMT+3' => 'America/Argentina/Buenos_Aires',
'Etc/GMT+4' => 'America/Manaus',
'Etc/GMT+5' => 'America/New_York',
'Etc/GMT+6' => 'America/Chicago',
'Etc/GMT+7' => 'America/Denver',
'Etc/GMT+8' => 'America/Los_Angeles',
'Etc/GMT+9' => 'America/Anchorage',
'Etc/GMT-0' => 'UTC',
'Etc/GMT-1' => 'Europe/Berlin',
'Etc/GMT-10' => 'Australia/Sydney',
'Etc/GMT-11' => 'Pacific/Norfolk',
'Etc/GMT-12' => 'Pacific/Auckland',
'Etc/GMT-13' => 'Pacific/Apia',
'Etc/GMT-14' => 'Pacific/Kiritimati',
'Etc/GMT-2' => 'Africa/Cairo',
'Etc/GMT-3' => 'Europe/Moscow',
'Etc/GMT-4' => 'Europe/Samara',
'Etc/GMT-5' => 'Asia/Yekaterinburg',
'Etc/GMT-6' => 'Asia/Almaty',
'Etc/GMT-7' => 'Asia/Bangkok',
'Etc/GMT-8' => 'Asia/Hong_Kong',
'Etc/GMT-9' => 'Asia/Tokyo',
'Etc/GMT0' => 'UTC',
'Etc/Greenwich' => 'UTC',
'Etc/UCT' => 'UTC',
'Etc/Universal' => 'UTC',
'Etc/UTC' => 'UTC',
'Etc/Zulu' => 'UTC',
);
return $deprecated_timezones[$tzname] ?: $tzname;
}
}
diff --git a/program/lib/Roundcube/rcube_contacts.php b/program/lib/Roundcube/rcube_contacts.php
index 55e5f6fb3..6e0de82d9 100644
--- a/program/lib/Roundcube/rcube_contacts.php
+++ b/program/lib/Roundcube/rcube_contacts.php
@@ -1,1042 +1,1043 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2006-2012, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Interface to the local address book database |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Model class for the local address book database
*
* @package Framework
* @subpackage Addressbook
*/
class rcube_contacts extends rcube_addressbook
{
// protected for backward compat. with some plugins
protected $db_name = 'contacts';
protected $db_groups = 'contactgroups';
protected $db_groupmembers = 'contactgroupmembers';
protected $vcard_fieldmap = array();
/**
* Store database connection.
*
* @var rcube_db
*/
private $db = null;
private $user_id = 0;
private $filter = null;
private $result = null;
private $cache;
private $table_cols = array('name', 'email', 'firstname', 'surname');
private $fulltext_cols = array('name', 'firstname', 'surname', 'middlename', 'nickname',
'jobtitle', 'organization', 'department', 'maidenname', 'email', 'phone',
'address', 'street', 'locality', 'zipcode', 'region', 'country', 'website', 'im', 'notes');
// public properties
public $primary_key = 'contact_id';
public $name;
public $readonly = false;
public $groups = true;
public $undelete = true;
public $list_page = 1;
public $page_size = 10;
public $group_id = 0;
public $ready = false;
public $coltypes = array('name', 'firstname', 'surname', 'middlename', 'prefix', 'suffix', 'nickname',
'jobtitle', 'organization', 'department', 'assistant', 'manager',
'gender', 'maidenname', 'spouse', 'email', 'phone', 'address',
'birthday', 'anniversary', 'website', 'im', 'notes', 'photo');
public $date_cols = array('birthday', 'anniversary');
const SEPARATOR = ',';
/**
* Object constructor
*
* @param object $dbconn Instance of the rcube_db class
* @param integer $user User-ID
*/
function __construct($dbconn, $user)
{
$this->db = $dbconn;
$this->user_id = $user;
$this->ready = $this->db && !$this->db->is_error();
}
/**
* Returns addressbook name
*/
function get_name()
{
return $this->name;
}
/**
* Save a search string for future listings
*
* @param string $filter SQL params to use in listing method
*/
function set_search_set($filter)
{
$this->filter = $filter;
$this->cache = null;
}
/**
* Getter for saved search properties
*
* @return mixed Search properties used by this class
*/
function get_search_set()
{
return $this->filter;
}
/**
* Setter for the current group
* (empty, has to be re-implemented by extending class)
*/
function set_group($gid)
{
$this->group_id = $gid;
$this->cache = null;
}
/**
* Reset all saved results and search parameters
*/
function reset()
{
$this->result = null;
$this->filter = null;
$this->cache = null;
}
/**
* List all active contact groups of this source
*
* @param string $search Search string to match group name
* @param int $mode Matching mode. Sum of rcube_addressbook::SEARCH_*
*
* @return array Indexed list of contact groups, each a hash array
*/
function list_groups($search = null, $mode = 0)
{
$results = array();
if (!$this->groups) {
return $results;
}
if ($search) {
if ($mode & rcube_addressbook::SEARCH_STRICT) {
$sql_filter = $this->db->ilike('name', $search);
}
else if ($mode & rcube_addressbook::SEARCH_PREFIX) {
$sql_filter = $this->db->ilike('name', $search . '%');
}
else {
$sql_filter = $this->db->ilike('name', '%' . $search . '%');
}
$sql_filter = " AND $sql_filter";
}
$sql_result = $this->db->query(
"SELECT * FROM " . $this->db->table_name($this->db_groups, true)
. " WHERE `del` <> 1 AND `user_id` = ?" . $sql_filter
. " ORDER BY `name`",
$this->user_id);
while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
$sql_arr['ID'] = $sql_arr['contactgroup_id'];
$results[] = $sql_arr;
}
return $results;
}
/**
* Get group properties such as name and email address(es)
*
* @param string $group_id Group identifier
*
* @return array Group properties as hash array
*/
function get_group($group_id)
{
$sql_result = $this->db->query(
"SELECT * FROM " . $this->db->table_name($this->db_groups, true)
. " WHERE `del` <> 1 AND `contactgroup_id` = ? AND `user_id` = ?",
$group_id, $this->user_id);
if ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
$sql_arr['ID'] = $sql_arr['contactgroup_id'];
return $sql_arr;
}
return null;
}
/**
* List the current set of contact records
*
* @param array List of cols to show, Null means all
* @param int Only return this number of records, use negative values for tail
* @param boolean True to skip the count query (select only)
*
* @return array Indexed list of contact records, each a hash array
*/
function list_records($cols = null, $subset = 0, $nocount = false)
{
if ($nocount || $this->list_page <= 1) {
// create dummy result, we don't need a count now
$this->result = new rcube_result_set();
} else {
// count all records
$this->result = $this->count();
}
$start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
$length = $subset != 0 ? abs($subset) : $this->page_size;
if ($this->group_id)
$join = " LEFT JOIN " . $this->db->table_name($this->db_groupmembers, true) . " AS m".
" ON (m.`contact_id` = c.`".$this->primary_key."`)";
$order_col = (in_array($this->sort_col, $this->table_cols) ? $this->sort_col : 'name');
$order_cols = array("c.`$order_col`");
if ($order_col == 'firstname')
$order_cols[] = 'c.`surname`';
else if ($order_col == 'surname')
$order_cols[] = 'c.`firstname`';
if ($order_col != 'name')
$order_cols[] = 'c.`name`';
$order_cols[] = 'c.`email`';
$sql_result = $this->db->limitquery(
"SELECT * FROM " . $this->db->table_name($this->db_name, true) . " AS c" .
$join .
" WHERE c.`del` <> 1" .
" AND c.`user_id` = ?" .
($this->group_id ? " AND m.`contactgroup_id` = ?" : "").
($this->filter ? " AND ".$this->filter : "") .
" ORDER BY ". $this->db->concat($order_cols) .
" " . $this->sort_order,
$start_row,
$length,
$this->user_id,
$this->group_id);
// determine whether we have to parse the vcard or if only db cols are requested
$read_vcard = !$cols || count(array_intersect($cols, $this->table_cols)) < count($cols);
while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
$sql_arr['ID'] = $sql_arr[$this->primary_key];
if ($read_vcard)
$sql_arr = $this->convert_db_data($sql_arr);
else {
$sql_arr['email'] = $sql_arr['email'] ? explode(self::SEPARATOR, $sql_arr['email']) : array();
$sql_arr['email'] = array_map('trim', $sql_arr['email']);
}
$this->result->add($sql_arr);
}
$cnt = count($this->result->records);
// update counter
if ($nocount)
$this->result->count = $cnt;
else if ($this->list_page <= 1) {
if ($cnt < $this->page_size && $subset == 0)
$this->result->count = $cnt;
else if (isset($this->cache['count']))
$this->result->count = $this->cache['count'];
else
$this->result->count = $this->_count();
}
return $this->result;
}
/**
* Search contacts
*
* @param mixed $fields The field name or array of field names to search in
* @param mixed $value Search value (or array of values when $fields is array)
* @param int $mode Search mode. Sum of rcube_addressbook::SEARCH_*
* @param boolean $select True if results are requested, False if count only
* @param boolean $nocount True to skip the count query (select only)
* @param array $required List of fields that cannot be empty
*
* @return object rcube_result_set Contact records and 'count' value
*/
function search($fields, $value, $mode = 0, $select = true, $nocount = false, $required = array())
{
if (!is_array($required) && !empty($required)) {
$required = array($required);
}
$where = $and_where = $post_search = array();
$mode = intval($mode);
$WS = ' ';
$AS = self::SEPARATOR;
// direct ID search
if ($fields == 'ID' || $fields == $this->primary_key) {
$ids = !is_array($value) ? explode(self::SEPARATOR, $value) : $value;
$ids = $this->db->array2list($ids, 'integer');
$where[] = 'c.' . $this->primary_key.' IN ('.$ids.')';
}
else if (is_array($value)) {
foreach ((array)$fields as $idx => $col) {
$val = $value[$idx];
if (!strlen($val)) {
continue;
}
// table column
if (in_array($col, $this->table_cols)) {
$where[] = $this->fulltext_sql_where($val, $mode, $col);
}
// vCard field
else {
if (in_array($col, $this->fulltext_cols)) {
$where[] = $this->fulltext_sql_where($val, $mode, 'words');
}
$post_search[$col] = mb_strtolower($val);
}
}
}
// fulltext search in all fields
else if ($fields == '*') {
$where[] = $this->fulltext_sql_where($value, $mode, 'words');
}
else {
// require each word in to be present in one of the fields
$words = ($mode & rcube_addressbook::SEARCH_STRICT) ? array($value) : rcube_utils::tokenize_string($value, 1);
foreach ($words as $word) {
$groups = array();
foreach ((array)$fields as $idx => $col) {
$groups[] = $this->fulltext_sql_where($word, $mode, $col);
}
$where[] = '(' . join(' OR ', $groups) . ')';
}
}
foreach (array_intersect($required, $this->table_cols) as $col) {
$where[] = $this->db->quote_identifier($col).' <> '.$this->db->quote('');
}
$required = array_diff($required, $this->table_cols);
if (!empty($where)) {
// use AND operator for advanced searches
$where = join(" AND ", $where);
}
// Post-searching in vCard data fields
// we will search in all records and then build a where clause for their IDs
if (!empty($post_search) || !empty($required)) {
$ids = array(0);
// build key name regexp
$regexp = '/^(' . implode(array_keys($post_search), '|') . ')(?:.*)$/';
// use initial WHERE clause, to limit records number if possible
if (!empty($where))
$this->set_search_set($where);
// count result pages
$cnt = $this->count()->count;
$pages = ceil($cnt / $this->page_size);
$scnt = !empty($post_search) ? count($post_search) : 0;
// get (paged) result
for ($i=0; $i<$pages; $i++) {
$this->list_records(null, $i, true);
while ($row = $this->result->next()) {
$id = $row[$this->primary_key];
$found = array();
if (!empty($post_search)) {
foreach (preg_grep($regexp, array_keys($row)) as $col) {
$pos = strpos($col, ':');
$colname = $pos ? substr($col, 0, $pos) : $col;
$search = $post_search[$colname];
foreach ((array)$row[$col] as $value) {
if ($this->compare_search_value($colname, $value, $search, $mode)) {
$found[$colname] = true;
break;
}
}
}
}
// check if required fields are present
if (!empty($required)) {
foreach ($required as $req) {
$hit = false;
foreach ($row as $c => $values) {
if ($c === $req || strpos($c, $req.':') === 0) {
if ((is_string($row[$c]) && strlen($row[$c])) || !empty($row[$c])) {
$hit = true;
break;
}
}
}
if (!$hit) {
continue 2;
}
}
}
// all fields match
if (count($found) >= $scnt) {
$ids[] = $id;
}
}
}
// build WHERE clause
$ids = $this->db->array2list($ids, 'integer');
$where = 'c.`' . $this->primary_key.'` IN ('.$ids.')';
// reset counter
unset($this->cache['count']);
// when we know we have an empty result
if ($ids == '0') {
$this->set_search_set($where);
return ($this->result = new rcube_result_set(0, 0));
}
}
if (!empty($where)) {
$this->set_search_set($where);
if ($select)
$this->list_records(null, 0, $nocount);
else
$this->result = $this->count();
}
return $this->result;
}
/**
* Helper method to compose SQL where statements for fulltext searching
*/
private function fulltext_sql_where($value, $mode, $col = 'words', $bool = 'AND')
{
$WS = ' ';
$AS = $col == 'words' ? $WS : self::SEPARATOR;
$words = $col == 'words' ? rcube_utils::normalize_string($value, true) : array($value);
$where = array();
foreach ($words as $word) {
if ($mode & rcube_addressbook::SEARCH_STRICT) {
$where[] = '(' . $this->db->ilike($col, $word)
. ' OR ' . $this->db->ilike($col, $word . $AS . '%')
. ' OR ' . $this->db->ilike($col, '%' . $AS . $word . $AS . '%')
. ' OR ' . $this->db->ilike($col, '%' . $AS . $word) . ')';
}
else if ($mode & rcube_addressbook::SEARCH_PREFIX) {
$where[] = '(' . $this->db->ilike($col, $word . '%')
. ' OR ' . $this->db->ilike($col, '%' . $AS . $word . '%') . ')';
}
else {
$where[] = $this->db->ilike($col, '%' . $word . '%');
}
}
return count($where) ? '(' . join(" $bool ", $where) . ')' : '';
}
/**
* Count number of available contacts in database
*
* @return rcube_result_set Result object
*/
function count()
{
$count = isset($this->cache['count']) ? $this->cache['count'] : $this->_count();
return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
}
/**
* Count number of available contacts in database
*
* @return int Contacts count
*/
private function _count()
{
if ($this->group_id)
$join = " LEFT JOIN " . $this->db->table_name($this->db_groupmembers, true) . " AS m".
" ON (m.`contact_id` = c.`".$this->primary_key."`)";
// count contacts for this user
$sql_result = $this->db->query(
"SELECT COUNT(c.`contact_id`) AS cnt".
" FROM " . $this->db->table_name($this->db_name, true) . " AS c".
$join.
" WHERE c.`del` <> 1".
" AND c.`user_id` = ?".
($this->group_id ? " AND m.`contactgroup_id` = ?" : "").
($this->filter ? " AND (".$this->filter.")" : ""),
$this->user_id,
$this->group_id
);
$sql_arr = $this->db->fetch_assoc($sql_result);
$this->cache['count'] = (int) $sql_arr['cnt'];
return $this->cache['count'];
}
/**
* Return the last result set
*
* @return mixed Result array or NULL if nothing selected yet
*/
function get_result()
{
return $this->result;
}
/**
* Get a specific contact record
*
* @param mixed $id Record identifier(s)
* @param bool $assoc Enables returning associative array
*
* @return rcube_result_set|array Result object with all record fields
*/
function get_record($id, $assoc = false)
{
// return cached result
if ($this->result && ($first = $this->result->first()) && $first[$this->primary_key] == $id) {
return $assoc ? $first : $this->result;
}
$this->db->query(
"SELECT * FROM " . $this->db->table_name($this->db_name, true).
" WHERE `contact_id` = ?".
" AND `user_id` = ?".
" AND `del` <> 1",
$id,
$this->user_id
);
$this->result = null;
if ($sql_arr = $this->db->fetch_assoc()) {
$record = $this->convert_db_data($sql_arr);
$this->result = new rcube_result_set(1);
$this->result->add($record);
}
return $assoc && $record ? $record : $this->result;
}
/**
* Get group assignments of a specific contact record
*
* @param mixed $id Record identifier
*
* @return array List of assigned groups as ID=>Name pairs
*/
function get_record_groups($id)
{
$results = array();
if (!$this->groups) {
return $results;
}
$sql_result = $this->db->query(
"SELECT cgm.`contactgroup_id`, cg.`name` "
. " FROM " . $this->db->table_name($this->db_groupmembers, true) . " AS cgm"
. " LEFT JOIN " . $this->db->table_name($this->db_groups, true) . " AS cg"
. " ON (cgm.`contactgroup_id` = cg.`contactgroup_id` AND cg.`del` <> 1)"
. " WHERE cgm.`contact_id` = ?",
$id
);
while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
$results[$sql_arr['contactgroup_id']] = $sql_arr['name'];
}
return $results;
}
/**
* Check the given data before saving.
* If input not valid, the message to display can be fetched using get_error()
*
* @param array &$save_data Associative array with data to save
* @param boolean $autofix Try to fix/complete record automatically
*
* @return boolean True if input is valid, False if not.
*/
public function validate(&$save_data, $autofix = false)
{
// validate e-mail addresses
$valid = parent::validate($save_data, $autofix);
// require at least some name or email
if ($valid
&& !strlen($save_data['firstname'].$save_data['surname'].$save_data['name'])
&& !count(array_filter($this->get_col_values('email', $save_data, true)))
) {
$this->set_error(self::ERROR_VALIDATE, 'nonamewarning');
$valid = false;
}
return $valid;
}
/**
* Create a new contact record
*
* @param array $save_data Associative array with save data
* @param bool $check Enables validity checks
*
* @return integer|boolean The created record ID on success, False on error
*/
function insert($save_data, $check = false)
{
if (!is_array($save_data)) {
return false;
}
$insert_id = $existing = false;
if ($check) {
foreach ($save_data as $col => $values) {
if (strpos($col, 'email') === 0) {
foreach ((array)$values as $email) {
if ($existing = $this->search('email', $email, false, false))
break 2;
}
}
}
}
$save_data = $this->convert_save_data($save_data);
$a_insert_cols = $a_insert_values = array();
foreach ($save_data as $col => $value) {
$a_insert_cols[] = $this->db->quote_identifier($col);
$a_insert_values[] = $this->db->quote($value);
}
if (!$existing->count && !empty($a_insert_cols)) {
$this->db->query(
"INSERT INTO " . $this->db->table_name($this->db_name, true).
" (`user_id`, `changed`, `del`, ".join(', ', $a_insert_cols).")".
" VALUES (".intval($this->user_id).", ".$this->db->now().", 0, ".join(', ', $a_insert_values).")"
);
$insert_id = $this->db->insert_id($this->db_name);
}
$this->cache = null;
return $insert_id;
}
/**
* Update a specific contact record
*
* @param mixed $id Record identifier
* @param array $save_cols Associative array with save data
*
* @return boolean True on success, False on error
*/
function update($id, $save_cols)
{
$updated = false;
$write_sql = array();
$record = $this->get_record($id, true);
$save_cols = $this->convert_save_data($save_cols, $record);
foreach ($save_cols as $col => $value) {
$write_sql[] = sprintf("%s=%s", $this->db->quote_identifier($col), $this->db->quote($value));
}
if (!empty($write_sql)) {
$this->db->query(
"UPDATE " . $this->db->table_name($this->db_name, true).
" SET `changed` = ".$this->db->now().", ".join(', ', $write_sql).
" WHERE `contact_id` = ?".
" AND `user_id` = ?".
" AND `del` <> 1",
$id,
$this->user_id
);
$updated = $this->db->affected_rows();
$this->result = null; // clear current result (from get_record())
}
return !empty($updated);
}
/**
* Convert data stored in the database into output format
*/
private function convert_db_data($sql_arr)
{
$record = array();
$record['ID'] = $sql_arr[$this->primary_key];
if ($sql_arr['vcard']) {
unset($sql_arr['email']);
$vcard = new rcube_vcard($sql_arr['vcard'], RCUBE_CHARSET, false, $this->vcard_fieldmap);
$record += $vcard->get_assoc() + $sql_arr;
}
else {
$record += $sql_arr;
$record['email'] = explode(self::SEPARATOR, $record['email']);
$record['email'] = array_map('trim', $record['email']);
}
return $record;
}
/**
* Convert input data for storing in the database
*/
private function convert_save_data($save_data, $record = array())
{
$out = array();
$words = '';
// copy values into vcard object
$vcard = new rcube_vcard($record['vcard'] ?: $save_data['vcard'], RCUBE_CHARSET, false, $this->vcard_fieldmap);
$vcard->reset();
// don't store groups in vCard (#1490277)
$vcard->set('groups', null);
unset($save_data['groups']);
foreach ($save_data as $key => $values) {
list($field, $section) = explode(':', $key);
$fulltext = in_array($field, $this->fulltext_cols);
// avoid casting DateTime objects to array
if (is_object($values) && is_a($values, 'DateTime')) {
$values = array(0 => $values);
}
foreach ((array)$values as $value) {
if (isset($value))
$vcard->set($field, $value, $section);
if ($fulltext && is_array($value))
$words .= ' ' . rcube_utils::normalize_string(join(" ", $value));
else if ($fulltext && strlen($value) >= 3)
$words .= ' ' . rcube_utils::normalize_string($value);
}
}
$out['vcard'] = $vcard->export(false);
foreach ($this->table_cols as $col) {
$key = $col;
if (!isset($save_data[$key]))
$key .= ':home';
if (isset($save_data[$key])) {
if (is_array($save_data[$key]))
$out[$col] = join(self::SEPARATOR, $save_data[$key]);
else
$out[$col] = $save_data[$key];
}
}
// save all e-mails in database column
$out['email'] = join(self::SEPARATOR, $vcard->email);
// join words for fulltext search
$out['words'] = join(" ", array_unique(explode(" ", $words)));
return $out;
}
/**
* Mark one or more contact records as deleted
*
* @param array $ids Record identifiers
* @param boolean $force Remove record(s) irreversible (unsupported)
*/
function delete($ids, $force = true)
{
if (!is_array($ids)) {
$ids = explode(self::SEPARATOR, $ids);
}
$ids = $this->db->array2list($ids, 'integer');
// flag record as deleted (always)
$this->db->query(
"UPDATE " . $this->db->table_name($this->db_name, true).
" SET `del` = 1, `changed` = ".$this->db->now().
" WHERE `user_id` = ?".
" AND `contact_id` IN ($ids)",
$this->user_id
);
$this->cache = null;
return $this->db->affected_rows();
}
/**
* Undelete one or more contact records
*
* @param array $ids Record identifiers
*/
function undelete($ids)
{
if (!is_array($ids)) {
$ids = explode(self::SEPARATOR, $ids);
}
$ids = $this->db->array2list($ids, 'integer');
// clear deleted flag
$this->db->query(
"UPDATE " . $this->db->table_name($this->db_name, true).
" SET `del` = 0, `changed` = ".$this->db->now().
" WHERE `user_id` = ?".
" AND `contact_id` IN ($ids)",
$this->user_id
);
$this->cache = null;
return $this->db->affected_rows();
}
/**
* Remove all records from the database
*
* @param bool $with_groups Remove also groups
*
* @return int Number of removed records
*/
function delete_all($with_groups = false)
{
$this->cache = null;
$now = $this->db->now();
$this->db->query("UPDATE " . $this->db->table_name($this->db_name, true)
. " SET `del` = 1, `changed` = $now"
. " WHERE `user_id` = ?", $this->user_id);
$count = $this->db->affected_rows();
if ($with_groups) {
$this->db->query("UPDATE " . $this->db->table_name($this->db_groups, true)
. " SET `del` = 1, `changed` = $now"
. " WHERE `user_id` = ?", $this->user_id);
$count += $this->db->affected_rows();
}
return $count;
}
/**
* Create a contact group with the given name
*
* @param string $name The group name
*
* @return mixed False on error, array with record props in success
*/
function create_group($name)
{
$result = false;
// make sure we have a unique name
$name = $this->unique_groupname($name);
$this->db->query(
"INSERT INTO " . $this->db->table_name($this->db_groups, true).
" (`user_id`, `changed`, `name`)".
" VALUES (".intval($this->user_id).", ".$this->db->now().", ".$this->db->quote($name).")"
);
if ($insert_id = $this->db->insert_id($this->db_groups)) {
$result = array('id' => $insert_id, 'name' => $name);
}
return $result;
}
/**
* Delete the given group (and all linked group members)
*
* @param string $gid Group identifier
*
* @return boolean True on success, false if no data was changed
*/
function delete_group($gid)
{
// flag group record as deleted
$this->db->query(
"UPDATE " . $this->db->table_name($this->db_groups, true)
. " SET `del` = 1, `changed` = " . $this->db->now()
. " WHERE `contactgroup_id` = ?"
. " AND `user_id` = ?",
$gid, $this->user_id
);
$this->cache = null;
return $this->db->affected_rows();
}
/**
* Rename a specific contact group
*
* @param string $gid Group identifier
* @param string $name New name to set for this group
* @param string $new_gid (not used)
*
* @return boolean New name on success, false if no data was changed
*/
function rename_group($gid, $name, &$new_gid)
{
// make sure we have a unique name
$name = $this->unique_groupname($name);
$sql_result = $this->db->query(
"UPDATE " . $this->db->table_name($this->db_groups, true).
" SET `name` = ?, `changed` = ".$this->db->now().
" WHERE `contactgroup_id` = ?".
" AND `user_id` = ?",
$name, $gid, $this->user_id
);
return $this->db->affected_rows($sql_result) ? $name : false;
}
/**
* Add the given contact records the a certain group
*
* @param string Group identifier
* @param array|string List of contact identifiers to be added
*
* @return int Number of contacts added
*/
function add_to_group($group_id, $ids)
{
if (!is_array($ids)) {
$ids = explode(self::SEPARATOR, $ids);
}
$added = 0;
$exists = array();
// get existing assignments ...
$sql_result = $this->db->query(
"SELECT `contact_id` FROM " . $this->db->table_name($this->db_groupmembers, true).
" WHERE `contactgroup_id` = ?".
" AND `contact_id` IN (".$this->db->array2list($ids, 'integer').")",
$group_id
);
while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
$exists[] = $sql_arr['contact_id'];
}
// ... and remove them from the list
$ids = array_diff($ids, $exists);
foreach ($ids as $contact_id) {
$this->db->query(
"INSERT INTO " . $this->db->table_name($this->db_groupmembers, true).
" (`contactgroup_id`, `contact_id`, `created`)".
" VALUES (?, ?, ".$this->db->now().")",
$group_id,
$contact_id
);
if ($error = $this->db->is_error()) {
$this->set_error(self::ERROR_SAVING, $error);
}
else {
$added++;
}
}
return $added;
}
/**
* Remove the given contact records from a certain group
*
* @param string $group_id Group identifier
* @param array|string $ids List of contact identifiers to be removed
*
* @return int Number of deleted group members
*/
function remove_from_group($group_id, $ids)
{
if (!is_array($ids))
$ids = explode(self::SEPARATOR, $ids);
$ids = $this->db->array2list($ids, 'integer');
$sql_result = $this->db->query(
"DELETE FROM " . $this->db->table_name($this->db_groupmembers, true).
" WHERE `contactgroup_id` = ?".
" AND `contact_id` IN ($ids)",
$group_id
);
return $this->db->affected_rows($sql_result);
}
/**
* Check for existing groups with the same name
*
* @param string $name Name to check
*
* @return string A group name which is unique for the current use
*/
private function unique_groupname($name)
{
$checkname = $name;
$num = 2;
$hit = false;
do {
$sql_result = $this->db->query(
"SELECT 1 FROM " . $this->db->table_name($this->db_groups, true).
" WHERE `del` <> 1".
" AND `user_id` = ?".
" AND `name` = ?",
$this->user_id,
$checkname);
// append number to make name unique
if ($hit = $this->db->fetch_array($sql_result)) {
$checkname = $name . ' ' . $num++;
}
}
while ($hit);
return $checkname;
}
}
diff --git a/program/lib/Roundcube/rcube_content_filter.php b/program/lib/Roundcube/rcube_content_filter.php
index 88f780666..59441639d 100644
--- a/program/lib/Roundcube/rcube_content_filter.php
+++ b/program/lib/Roundcube/rcube_content_filter.php
@@ -1,57 +1,58 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2011, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| PHP stream filter to detect evil content in mail attachments |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* PHP stream filter to detect html/javascript code in attachments
*
* @package Framework
* @subpackage Utils
*/
class rcube_content_filter extends php_user_filter
{
private $buffer = '';
private $cutoff = 2048;
function onCreate()
{
$this->cutoff = rand(2048, 3027);
return true;
}
function filter($in, $out, &$consumed, $closing)
{
while ($bucket = stream_bucket_make_writeable($in)) {
$this->buffer .= $bucket->data;
// check for evil content and abort
if (preg_match('/<(script|iframe|object)/i', $this->buffer)) {
return PSFS_ERR_FATAL;
}
// keep buffer small enough
if (strlen($this->buffer) > 4096) {
$this->buffer = substr($this->buffer, $this->cutoff);
}
$consumed += $bucket->datalen;
stream_bucket_append($out, $bucket);
}
return PSFS_PASS_ON;
}
}
diff --git a/program/lib/Roundcube/rcube_csv2vcard.php b/program/lib/Roundcube/rcube_csv2vcard.php
index 50e2313cd..e7353f0b3 100644
--- a/program/lib/Roundcube/rcube_csv2vcard.php
+++ b/program/lib/Roundcube/rcube_csv2vcard.php
@@ -1,685 +1,686 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2012, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| CSV to vCard data conversion |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* CSV to vCard data converter
*
* @package Framework
* @subpackage Addressbook
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_csv2vcard
{
/**
* CSV to vCard fields mapping
*
* @var array
*/
protected $csv2vcard_map = array(
// MS Outlook 2010
'anniversary' => 'anniversary',
'assistants_name' => 'assistant',
'assistants_phone' => 'phone:assistant',
'birthday' => 'birthday',
'business_city' => 'locality:work',
'business_countryregion' => 'country:work',
'business_fax' => 'phone:work,fax',
'business_phone' => 'phone:work',
'business_phone_2' => 'phone:work2',
'business_postal_code' => 'zipcode:work',
'business_state' => 'region:work',
'business_street' => 'street:work',
//'business_street_2' => '',
//'business_street_3' => '',
'car_phone' => 'phone:car',
'categories' => 'groups',
//'children' => '',
'company' => 'organization',
//'company_main_phone' => '',
'department' => 'department',
'email_2_address' => 'email:other',
//'email_2_type' => '',
'email_3_address' => 'email:other',
//'email_3_type' => '',
'email_address' => 'email:pref',
//'email_type' => '',
'first_name' => 'firstname',
'gender' => 'gender',
'home_city' => 'locality:home',
'home_countryregion' => 'country:home',
'home_fax' => 'phone:home,fax',
'home_phone' => 'phone:home',
'home_phone_2' => 'phone:home2',
'home_postal_code' => 'zipcode:home',
'home_state' => 'region:home',
'home_street' => 'street:home',
//'home_street_2' => '',
//'home_street_3' => '',
//'initials' => '',
//'isdn' => '',
'job_title' => 'jobtitle',
//'keywords' => '',
//'language' => '',
'last_name' => 'surname',
//'location' => '',
'managers_name' => 'manager',
'middle_name' => 'middlename',
//'mileage' => '',
'mobile_phone' => 'phone:cell',
'notes' => 'notes',
//'office_location' => '',
'other_city' => 'locality:other',
'other_countryregion' => 'country:other',
'other_fax' => 'phone:other,fax',
'other_phone' => 'phone:other',
'other_postal_code' => 'zipcode:other',
'other_state' => 'region:other',
'other_street' => 'street:other',
//'other_street_2' => '',
//'other_street_3' => '',
'pager' => 'phone:pager',
'primary_phone' => 'phone:pref',
//'profession' => '',
//'radio_phone' => '',
'spouse' => 'spouse',
'suffix' => 'suffix',
'title' => 'title',
'web_page' => 'website:homepage',
// Thunderbird
'birth_day' => 'birthday-d',
'birth_month' => 'birthday-m',
'birth_year' => 'birthday-y',
'display_name' => 'displayname',
'fax_number' => 'phone:fax',
'home_address' => 'street:home',
//'home_address_2' => '',
'home_country' => 'country:home',
'home_zipcode' => 'zipcode:home',
'mobile_number' => 'phone:cell',
'nickname' => 'nickname',
'organization' => 'organization',
'pager_number' => 'phone:pager',
'primary_email' => 'email:pref',
'secondary_email' => 'email:other',
'web_page_1' => 'website:homepage',
'web_page_2' => 'website:other',
'work_phone' => 'phone:work',
'work_address' => 'street:work',
//'work_address_2' => '',
'work_country' => 'country:work',
'work_zipcode' => 'zipcode:work',
'last' => 'surname',
'first' => 'firstname',
'work_city' => 'locality:work',
'work_state' => 'region:work',
'home_city_short' => 'locality:home',
'home_state_short' => 'region:home',
// Atmail
'date_of_birth' => 'birthday',
'email' => 'email:pref',
'home_mobile' => 'phone:cell',
'home_zip' => 'zipcode:home',
'info' => 'notes',
'user_photo' => 'photo',
'url' => 'website:homepage',
'work_company' => 'organization',
'work_dept' => 'departament',
'work_fax' => 'phone:work,fax',
'work_mobile' => 'phone:work,cell',
'work_title' => 'jobtitle',
'work_zip' => 'zipcode:work',
'group' => 'groups',
// GMail
'groups' => 'groups',
'group_membership' => 'groups',
'given_name' => 'firstname',
'additional_name' => 'middlename',
'family_name' => 'surname',
'name' => 'displayname',
'name_prefix' => 'prefix',
'name_suffix' => 'suffix',
// Format of Letter Hub test files from
// https://letterhub.com/sample-csv-file-with-contacts/
'company_name' => 'organization',
'address' => 'street:home',
'city' => 'locality:home',
//'county' => '',
'state' => 'region:home',
'zip' => 'zipcode:home',
'phone1' => 'phone:home',
'phone' => 'phone:work',
'email' => 'email:home',
);
/**
* CSV label to text mapping for English
*
* @var array
*/
protected $label_map = array(
// MS Outlook 2010
'anniversary' => "Anniversary",
'assistants_name' => "Assistant's Name",
'assistants_phone' => "Assistant's Phone",
'birthday' => "Birthday",
'business_city' => "Business City",
'business_countryregion' => "Business Country/Region",
'business_fax' => "Business Fax",
'business_phone' => "Business Phone",
'business_phone_2' => "Business Phone 2",
'business_postal_code' => "Business Postal Code",
'business_state' => "Business State",
'business_street' => "Business Street",
//'business_street_2' => "Business Street 2",
//'business_street_3' => "Business Street 3",
'car_phone' => "Car Phone",
'categories' => "Categories",
//'children' => "Children",
'company' => "Company",
//'company_main_phone' => "Company Main Phone",
'department' => "Department",
//'directory_server' => "Directory Server",
'email_2_address' => "E-mail 2 Address",
//'email_2_type' => "E-mail 2 Type",
'email_3_address' => "E-mail 3 Address",
//'email_3_type' => "E-mail 3 Type",
'email_address' => "E-mail Address",
//'email_type' => "E-mail Type",
'first_name' => "First Name",
'gender' => "Gender",
'home_city' => "Home City",
'home_countryregion' => "Home Country/Region",
'home_fax' => "Home Fax",
'home_phone' => "Home Phone",
'home_phone_2' => "Home Phone 2",
'home_postal_code' => "Home Postal Code",
'home_state' => "Home State",
'home_street' => "Home Street",
//'home_street_2' => "Home Street 2",
//'home_street_3' => "Home Street 3",
//'initials' => "Initials",
//'isdn' => "ISDN",
'job_title' => "Job Title",
//'keywords' => "Keywords",
//'language' => "Language",
'last_name' => "Last Name",
//'location' => "Location",
'managers_name' => "Manager's Name",
'middle_name' => "Middle Name",
//'mileage' => "Mileage",
'mobile_phone' => "Mobile Phone",
'notes' => "Notes",
//'office_location' => "Office Location",
'other_city' => "Other City",
'other_countryregion' => "Other Country/Region",
'other_fax' => "Other Fax",
'other_phone' => "Other Phone",
'other_postal_code' => "Other Postal Code",
'other_state' => "Other State",
'other_street' => "Other Street",
//'other_street_2' => "Other Street 2",
//'other_street_3' => "Other Street 3",
'pager' => "Pager",
'primary_phone' => "Primary Phone",
//'profession' => "Profession",
//'radio_phone' => "Radio Phone",
'spouse' => "Spouse",
'suffix' => "Suffix",
'title' => "Title",
'web_page' => "Web Page",
// Thunderbird
'birth_day' => "Birth Day",
'birth_month' => "Birth Month",
'birth_year' => "Birth Year",
'display_name' => "Display Name",
'fax_number' => "Fax Number",
'home_address' => "Home Address",
//'home_address_2' => "Home Address 2",
'home_country' => "Home Country",
'home_zipcode' => "Home ZipCode",
'mobile_number' => "Mobile Number",
'nickname' => "Nickname",
'organization' => "Organization",
'pager_number' => "Pager Namber",
'primary_email' => "Primary Email",
'secondary_email' => "Secondary Email",
'web_page_1' => "Web Page 1",
'web_page_2' => "Web Page 2",
'work_phone' => "Work Phone",
'work_address' => "Work Address",
//'work_address_2' => "Work Address 2",
'work_city' => "Work City",
'work_country' => "Work Country",
'work_state' => "Work State",
'work_zipcode' => "Work ZipCode",
// Atmail
'date_of_birth' => "Date of Birth",
'email' => "Email",
//'email_2' => "Email2",
//'email_3' => "Email3",
//'email_4' => "Email4",
//'email_5' => "Email5",
'home_mobile' => "Home Mobile",
'home_zip' => "Home Zip",
'info' => "Info",
'user_photo' => "User Photo",
'url' => "URL",
'work_company' => "Work Company",
'work_dept' => "Work Dept",
'work_fax' => "Work Fax",
'work_mobile' => "Work Mobile",
'work_title' => "Work Title",
'work_zip' => "Work Zip",
'group' => "Group",
// GMail
'groups' => "Groups",
'group_membership' => "Group Membership",
'given_name' => "Given Name",
'additional_name' => "Additional Name",
'family_name' => "Family Name",
'name' => "Name",
'name_prefix' => "Name Prefix",
'name_suffix' => "Name Suffix",
);
/**
* Special fields map for GMail format
*
* @var array
*/
protected $gmail_label_map = array(
'E-mail' => array(
'Value' => array(
'home' => 'email:home',
'work' => 'email:work',
'*' => 'email:other',
),
),
'Phone' => array(
'Value' => array(
'home' => 'phone:home',
'homefax' => 'phone:homefax',
'main' => 'phone:pref',
'pager' => 'phone:pager',
'mobile' => 'phone:cell',
'work' => 'phone:work',
'workfax' => 'phone:workfax',
),
),
'Relation' => array(
'Value' => array(
'spouse' => 'spouse',
),
),
'Website' => array(
'Value' => array(
'profile' => 'website:profile',
'blog' => 'website:blog',
'homepage' => 'website:homepage',
'work' => 'website:work',
),
),
'Address' => array(
'Street' => array(
'home' => 'street:home',
'work' => 'street:work',
),
'City' => array(
'home' => 'locality:home',
'work' => 'locality:work',
),
'Region' => array(
'home' => 'region:home',
'work' => 'region:work',
),
'Postal Code' => array(
'home' => 'zipcode:home',
'work' => 'zipcode:work',
),
'Country' => array(
'home' => 'country:home',
'work' => 'country:work',
),
),
'Organization' => array(
'Name' => array(
'' => 'organization',
),
'Title' => array(
'' => 'jobtitle',
),
'Department' => array(
'' => 'department',
),
),
);
protected $local_label_map = array();
protected $vcards = array();
protected $map = array();
protected $gmail_map = array();
/**
* Class constructor
*
* @param string $lang File language
*/
public function __construct($lang = 'en_US')
{
// Localize fields map
if ($lang && $lang != 'en_US') {
if (file_exists(RCUBE_LOCALIZATION_DIR . "$lang/csv2vcard.inc")) {
include RCUBE_LOCALIZATION_DIR . "$lang/csv2vcard.inc";
}
if (!empty($map)) {
$this->local_label_map = array_merge($this->label_map, $map);
}
}
$this->label_map = array_flip($this->label_map);
$this->local_label_map = array_flip($this->local_label_map);
}
/**
* Import contacts from CSV file
*
* @param string $csv Content of the CSV file
*/
public function import($csv)
{
// convert to UTF-8
$head = substr($csv, 0, 4096);
$charset = rcube_charset::detect($head, RCUBE_CHARSET);
$csv = rcube_charset::convert($csv, $charset);
$csv = preg_replace(array('/^[\xFE\xFF]{2}/', '/^\xEF\xBB\xBF/', '/^\x00+/'), '', $csv); // also remove BOM
$head = '';
$prev_line = false;
$this->map = array();
$this->gmail_map = array();
// Parse file
foreach (preg_split("/[\r\n]+/", $csv) as $line) {
if (!empty($prev_line)) {
$line = '"' . $line;
}
$elements = $this->parse_line($line);
if (empty($elements)) {
continue;
}
// Parse header
if (empty($this->map)) {
$this->parse_header($elements);
if (empty($this->map)) {
break;
}
}
// Parse data row
else {
// handle multiline elements (e.g. Gmail)
if (!empty($prev_line)) {
$first = array_shift($elements);
if ($first[0] == '"') {
$prev_line[count($prev_line)-1] = '"' . $prev_line[count($prev_line)-1] . "\n" . substr($first, 1);
}
else {
$prev_line[count($prev_line)-1] .= "\n" . $first;
}
$elements = array_merge($prev_line, $elements);
}
$last_element = $elements[count($elements)-1];
if ($last_element[0] == '"') {
$elements[count($elements)-1] = substr($last_element, 1);
$prev_line = $elements;
continue;
}
$this->csv_to_vcard($elements);
$prev_line = false;
}
}
}
/**
* Export vCards
*
* @return array rcube_vcard List of vcards
*/
public function export()
{
return $this->vcards;
}
/**
* Parse CSV file line
*/
protected function parse_line($line)
{
$line = trim($line);
if (empty($line)) {
return null;
}
$fields = rcube_utils::explode_quoted_string(',', $line);
// remove quotes if needed
if (!empty($fields)) {
foreach ($fields as $idx => $value) {
if (($len = strlen($value)) > 1 && $value[0] == '"' && $value[$len-1] == '"') {
// remove surrounding quotes
$value = substr($value, 1, -1);
// replace doubled quotes inside the string with single quote
$value = str_replace('""', '"', $value);
$fields[$idx] = $value;
}
}
}
return $fields;
}
/**
* Parse CSV header line, detect fields mapping
*/
protected function parse_header($elements)
{
$map1 = array();
$map2 = array();
$size = count($elements);
// check English labels
for ($i = 0; $i < $size; $i++) {
$label = $this->label_map[$elements[$i]];
if ($label && !empty($this->csv2vcard_map[$label])) {
$map1[$i] = $this->csv2vcard_map[$label];
}
}
// check localized labels
if (!empty($this->local_label_map)) {
for ($i = 0; $i < $size; $i++) {
$label = $this->local_label_map[$elements[$i]];
// special localization label
if ($label && $label[0] == '_') {
$label = substr($label, 1);
}
if ($label && !empty($this->csv2vcard_map[$label])) {
$map2[$i] = $this->csv2vcard_map[$label];
}
}
}
// If nothing recognized fallback to simple non-localized labels
if (empty($map1) && empty($map2)) {
for ($i = 0; $i < $size; $i++) {
$label = str_replace(' ', '_', strtolower($elements[$i]));
if (!empty($this->csv2vcard_map[$label])) {
$map1[$i] = $this->csv2vcard_map[$label];
}
}
}
$this->map = count($map1) >= count($map2) ? $map1 : $map2;
// support special Gmail format
foreach ($this->gmail_label_map as $key => $items) {
$num = 1;
while (($_key = "$key $num - Type") && ($found = array_search($_key, $elements)) !== false) {
$this->gmail_map["$key:$num"] = array('_key' => $key, '_idx' => $found);
foreach (array_keys($items) as $item_key) {
$_key = "$key $num - $item_key";
if (($found = array_search($_key, $elements)) !== false) {
$this->gmail_map["$key:$num"][$item_key] = $found;
}
}
$num++;
}
}
}
/**
* Convert CSV data row to vCard
*/
protected function csv_to_vcard($data)
{
$contact = array();
foreach ($this->map as $idx => $name) {
$value = $data[$idx];
if ($value !== null && $value !== '') {
if (!empty($contact[$name])) {
$contact[$name] = (array) $contact[$name];
$contact[$name][] = $value;
}
else {
$contact[$name] = $value;
}
}
}
// Gmail format support
foreach ($this->gmail_map as $idx => $item) {
$type = preg_replace('/[^a-z]/', '', strtolower($data[$item['_idx']]));
$key = $item['_key'];
unset($item['_idx']);
unset($item['_key']);
foreach ($item as $item_key => $item_idx) {
$value = $data[$item_idx];
if ($value !== null && $value !== '') {
foreach (array($type, '*') as $_type) {
if ($data_idx = $this->gmail_label_map[$key][$item_key][$_type]) {
$value = explode(' ::: ', $value);
if (!empty($contact[$data_idx])) {
$contact[$data_idx] = array_merge((array) $contact[$data_idx], $value);
}
else {
$contact[$data_idx] = $value;
}
break;
}
}
}
}
}
if (empty($contact)) {
return;
}
// Handle special values
if (!empty($contact['birthday-d']) && !empty($contact['birthday-m']) && !empty($contact['birthday-y'])) {
$contact['birthday'] = $contact['birthday-y'] .'-' .$contact['birthday-m'] . '-' . $contact['birthday-d'];
}
if (!empty($contact['groups'])) {
// categories/groups separator in vCard is ',' not ';'
$contact['groups'] = str_replace(',', '', $contact['groups']);
$contact['groups'] = str_replace(';', ',', $contact['groups']);
if (!empty($this->gmail_map)) {
// remove "* " added by GMail
$contact['groups'] = str_replace('* ', '', $contact['groups']);
// replace strange delimiter
$contact['groups'] = str_replace(' ::: ', ',', $contact['groups']);
}
}
// Empty dates, e.g. "0/0/00", "0000-00-00 00:00:00"
foreach (array('birthday', 'anniversary') as $key) {
if (!empty($contact[$key])) {
$date = preg_replace('/[0[:^word:]]/', '', $contact[$key]);
if (empty($date)) {
unset($contact[$key]);
}
}
}
if (!empty($contact['gender']) && ($gender = strtolower($contact['gender']))) {
if (!in_array($gender, array('male', 'female'))) {
unset($contact['gender']);
}
}
// Convert address(es) to rcube_vcard data
foreach ($contact as $idx => $value) {
$name = explode(':', $idx);
if (in_array($name[0], array('street', 'locality', 'region', 'zipcode', 'country'))) {
$contact['address:'.$name[1]][$name[0]] = $value;
unset($contact[$idx]);
}
}
// Create vcard object
$vcard = new rcube_vcard();
foreach ($contact as $name => $value) {
$name = explode(':', $name);
if (is_array($value) && $name[0] != 'address') {
foreach ((array) $value as $val) {
$vcard->set($name[0], $val, $name[1]);
}
}
else {
$vcard->set($name[0], $value, $name[1]);
}
}
// add to the list
$this->vcards[] = $vcard;
}
}
diff --git a/program/lib/Roundcube/rcube_db.php b/program/lib/Roundcube/rcube_db.php
index e106b41f9..12e3be6a7 100644
--- a/program/lib/Roundcube/rcube_db.php
+++ b/program/lib/Roundcube/rcube_db.php
@@ -1,1388 +1,1389 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2012, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Database wrapper class that implements PHP PDO functions |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Database independent query interface.
* This is a wrapper for the PHP PDO.
*
* @package Framework
* @subpackage Database
*/
class rcube_db
{
public $db_provider;
protected $db_dsnw; // DSN for write operations
protected $db_dsnr; // DSN for read operations
protected $db_connected = false; // Already connected ?
protected $db_mode; // Connection mode
protected $dbh; // Connection handle
protected $dbhs = array();
protected $table_connections = array();
protected $db_error = false;
protected $db_error_msg = '';
protected $conn_failure = false;
protected $db_index = 0;
protected $last_result;
protected $tables;
protected $variables;
protected $options = array(
// column/table quotes
'identifier_start' => '"',
'identifier_end' => '"',
// date/time input format
'datetime_format' => 'Y-m-d H:i:s',
);
const DEBUG_LINE_LENGTH = 4096;
const DEFAULT_QUOTE = '`';
/**
* Factory, returns driver-specific instance of the class
*
* @param string $db_dsnw DSN for read/write operations
* @param string $db_dsnr Optional DSN for read only operations
* @param bool $pconn Enables persistent connections
*
* @return rcube_db Object instance
*/
public static function factory($db_dsnw, $db_dsnr = '', $pconn = false)
{
$driver = strtolower(substr($db_dsnw, 0, strpos($db_dsnw, ':')));
$driver_map = array(
'sqlite2' => 'sqlite',
'sybase' => 'mssql',
'dblib' => 'mssql',
'mysqli' => 'mysql',
'oci' => 'oracle',
'oci8' => 'oracle',
);
$driver = isset($driver_map[$driver]) ? $driver_map[$driver] : $driver;
$class = "rcube_db_$driver";
if (!$driver || !class_exists($class)) {
rcube::raise_error(array('code' => 600, 'type' => 'db',
'line' => __LINE__, 'file' => __FILE__,
'message' => "Configuration error. Unsupported database driver: $driver"),
true, true);
}
return new $class($db_dsnw, $db_dsnr, $pconn);
}
/**
* Object constructor
*
* @param string $db_dsnw DSN for read/write operations
* @param string $db_dsnr Optional DSN for read only operations
* @param bool $pconn Enables persistent connections
*/
public function __construct($db_dsnw, $db_dsnr = '', $pconn = false)
{
if (empty($db_dsnr)) {
$db_dsnr = $db_dsnw;
}
$this->db_dsnw = $db_dsnw;
$this->db_dsnr = $db_dsnr;
$this->db_pconn = $pconn;
$this->db_dsnw_array = self::parse_dsn($db_dsnw);
$this->db_dsnr_array = self::parse_dsn($db_dsnr);
$config = rcube::get_instance()->config;
$this->options['table_prefix'] = $config->get('db_prefix');
$this->options['dsnw_noread'] = $config->get('db_dsnw_noread', false);
$this->options['table_dsn_map'] = array_map(array($this, 'table_name'), $config->get('db_table_dsn', array()));
}
/**
* Connect to specific database
*
* @param array $dsn DSN for DB connections
* @param string $mode Connection mode (r|w)
*/
protected function dsn_connect($dsn, $mode)
{
$this->db_error = false;
$this->db_error_msg = null;
// return existing handle
if ($this->dbhs[$mode]) {
$this->dbh = $this->dbhs[$mode];
$this->db_mode = $mode;
return $this->dbh;
}
// connect to database
if ($dbh = $this->conn_create($dsn)) {
$this->dbhs[$mode] = $dbh;
$this->db_mode = $mode;
$this->db_connected = true;
}
}
/**
* Create PDO connection
*/
protected function conn_create($dsn)
{
// Get database specific connection options
$dsn_string = $this->dsn_string($dsn);
$dsn_options = $this->dsn_options($dsn);
// Connect
try {
// with this check we skip fatal error on PDO object creation
if (!class_exists('PDO', false)) {
throw new Exception('PDO extension not loaded. See http://php.net/manual/en/intro.pdo.php');
}
$this->conn_prepare($dsn);
$this->dbh = new PDO($dsn_string, $dsn['username'], $dsn['password'], $dsn_options);
// don't throw exceptions or warnings
$this->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
$this->conn_configure($dsn, $this->dbh);
}
catch (Exception $e) {
$this->db_error = true;
$this->db_error_msg = $e->getMessage();
rcube::raise_error(array('code' => 500, 'type' => 'db',
'line' => __LINE__, 'file' => __FILE__,
'message' => $this->db_error_msg), true, false);
return null;
}
return $this->dbh;
}
/**
* Driver-specific preparation of database connection
*
* @param array $dsn DSN for DB connections
*/
protected function conn_prepare($dsn)
{
}
/**
* Driver-specific configuration of database connection
*
* @param array $dsn DSN for DB connections
* @param PDO $dbh Connection handler
*/
protected function conn_configure($dsn, $dbh)
{
}
/**
* Connect to appropriate database depending on the operation
*
* @param string $mode Connection mode (r|w)
* @param boolean $force Enforce using the given mode
*/
public function db_connect($mode, $force = false)
{
// previous connection failed, don't attempt to connect again
if ($this->conn_failure) {
return;
}
// no replication
if ($this->db_dsnw == $this->db_dsnr) {
$mode = 'w';
}
// Already connected
if ($this->db_connected) {
// connected to db with the same or "higher" mode (if allowed)
if ($this->db_mode == $mode || $this->db_mode == 'w' && !$force && !$this->options['dsnw_noread']) {
return;
}
}
$dsn = ($mode == 'r') ? $this->db_dsnr_array : $this->db_dsnw_array;
$this->dsn_connect($dsn, $mode);
// use write-master when read-only fails
if (!$this->db_connected && $mode == 'r' && $this->is_replicated()) {
$this->dsn_connect($this->db_dsnw_array, 'w');
}
$this->conn_failure = !$this->db_connected;
}
/**
* Analyze the given SQL statement and select the appropriate connection to use
*/
protected function dsn_select($query)
{
// no replication
if ($this->db_dsnw == $this->db_dsnr) {
return 'w';
}
// Read or write ?
$mode = preg_match('/^(select|show|set)/i', $query) ? 'r' : 'w';
$start = '[' . $this->options['identifier_start'] . self::DEFAULT_QUOTE . ']';
$end = '[' . $this->options['identifier_end'] . self::DEFAULT_QUOTE . ']';
$regex = '/(?:^|\s)(from|update|into|join)\s+'.$start.'?([a-z0-9._]+)'.$end.'?\s+/i';
// find tables involved in this query
if (preg_match_all($regex, $query, $matches, PREG_SET_ORDER)) {
foreach ($matches as $m) {
$table = $m[2];
// always use direct mapping
if ($this->options['table_dsn_map'][$table]) {
$mode = $this->options['table_dsn_map'][$table];
break; // primary table rules
}
else if ($mode == 'r') {
// connected to db with the same or "higher" mode for this table
$db_mode = $this->table_connections[$table];
if ($db_mode == 'w' && !$this->options['dsnw_noread']) {
$mode = $db_mode;
}
}
}
// remember mode chosen (for primary table)
$table = $matches[0][2];
$this->table_connections[$table] = $mode;
}
return $mode;
}
/**
* Activate/deactivate debug mode
*
* @param boolean $dbg True if SQL queries should be logged
*/
public function set_debug($dbg = true)
{
$this->options['debug_mode'] = $dbg;
}
/**
* Writes debug information/query to 'sql' log file
*
* @param string $query SQL query
*/
protected function debug($query)
{
if ($this->options['debug_mode']) {
if (($len = strlen($query)) > self::DEBUG_LINE_LENGTH) {
$diff = $len - self::DEBUG_LINE_LENGTH;
$query = substr($query, 0, self::DEBUG_LINE_LENGTH)
. "... [truncated $diff bytes]";
}
rcube::write_log('sql', '[' . (++$this->db_index) . '] ' . $query . ';');
}
}
/**
* Getter for error state
*
* @param mixed $result Optional query result
*
* @return string Error message
*/
public function is_error($result = null)
{
if ($result !== null) {
return $result === false ? $this->db_error_msg : null;
}
return $this->db_error ? $this->db_error_msg : null;
}
/**
* Connection state checker
*
* @return boolean True if in connected state
*/
public function is_connected()
{
return !is_object($this->dbh) ? false : $this->db_connected;
}
/**
* Is database replication configured?
*
* @return bool Returns true if dsnw != dsnr
*/
public function is_replicated()
{
return !empty($this->db_dsnr) && $this->db_dsnw != $this->db_dsnr;
}
/**
* Get database runtime variables
*
* @param string $varname Variable name
* @param mixed $default Default value if variable is not set
*
* @return mixed Variable value or default
*/
public function get_variable($varname, $default = null)
{
// to be implemented by driver class
return rcube::get_instance()->config->get('db_' . $varname, $default);
}
/**
* Execute a SQL query
*
* @param string SQL query to execute
* @param mixed Values to be inserted in query
*
* @return number Query handle identifier
*/
public function query()
{
$params = func_get_args();
$query = array_shift($params);
// Support one argument of type array, instead of n arguments
if (count($params) == 1 && is_array($params[0])) {
$params = $params[0];
}
return $this->_query($query, 0, 0, $params);
}
/**
* Execute a SQL query with limits
*
* @param string SQL query to execute
* @param int Offset for LIMIT statement
* @param int Number of rows for LIMIT statement
* @param mixed Values to be inserted in query
*
* @return PDOStatement|bool Query handle or False on error
*/
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 $query SQL query to execute
* @param int $offset Offset for LIMIT statement
* @param int $numrows Number of rows for LIMIT statement
* @param array $params Values to be inserted in query
*
* @return PDOStatement|bool Query handle or False on error
*/
protected function _query($query, $offset, $numrows, $params)
{
$query = ltrim($query);
$this->db_connect($this->dsn_select($query), true);
// check connection before proceeding
if (!$this->is_connected()) {
return $this->last_result = false;
}
if ($numrows || $offset) {
$query = $this->set_limit($query, $numrows, $offset);
}
// replace self::DEFAULT_QUOTE with driver-specific quoting
$query = $this->query_parse($query);
// Because in Roundcube we mostly use queries that are
// executed only once, we will not use prepared queries
$pos = 0;
$idx = 0;
if (count($params)) {
while ($pos = strpos($query, '?', $pos)) {
if ($query[$pos+1] == '?') { // skip escaped '?'
$pos += 2;
}
else {
$val = $this->quote($params[$idx++]);
unset($params[$idx-1]);
$query = substr_replace($query, $val, $pos, 1);
$pos += strlen($val);
}
}
}
$query = rtrim($query, " \t\n\r\0\x0B;");
// replace escaped '?' and quotes back to normal, see self::quote()
$query = str_replace(
array('??', self::DEFAULT_QUOTE.self::DEFAULT_QUOTE),
array('?', self::DEFAULT_QUOTE),
$query
);
// log query
$this->debug($query);
return $this->query_execute($query);
}
/**
* Query execution
*/
protected function query_execute($query)
{
// destroy reference to previous result, required for SQLite driver (#1488874)
$this->last_result = null;
$this->db_error_msg = null;
// send query
$result = $this->dbh->query($query);
if ($result === false) {
$result = $this->handle_error($query);
}
return $this->last_result = $result;
}
/**
* Parse SQL query and replace identifier quoting
*
* @param string $query SQL query
*
* @return string SQL query
*/
protected function query_parse($query)
{
$start = $this->options['identifier_start'];
$end = $this->options['identifier_end'];
$quote = self::DEFAULT_QUOTE;
if ($start == $quote) {
return $query;
}
$pos = 0;
$in = false;
while ($pos = strpos($query, $quote, $pos)) {
if ($query[$pos+1] == $quote) { // skip escaped quote
$pos += 2;
}
else {
if ($in) {
$q = $end;
$in = false;
}
else {
$q = $start;
$in = true;
}
$query = substr_replace($query, $q, $pos, 1);
$pos++;
}
}
return $query;
}
/**
* Helper method to handle DB errors.
* This by default logs the error but could be overridden by a driver implementation
*
* @param string $query Query that triggered the error
*
* @return mixed Result to be stored and returned
*/
protected function handle_error($query)
{
$error = $this->dbh->errorInfo();
if (empty($this->options['ignore_key_errors']) || !in_array($error[0], array('23000', '23505'))) {
$this->db_error = true;
$this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]);
if (empty($this->options['ignore_errors'])) {
rcube::raise_error(array(
'code' => 500, 'type' => 'db', 'line' => __LINE__, 'file' => __FILE__,
'message' => $this->db_error_msg . " (SQL Query: $query)"
), true, false);
}
}
return false;
}
/**
* Get number of affected rows for the last query
*
* @param mixed $result Optional query handle
*
* @return int Number of (matching) rows
*/
public function affected_rows($result = null)
{
if ($result || ($result === null && ($result = $this->last_result))) {
if ($result !== true) {
return $result->rowCount();
}
}
return 0;
}
/**
* Get number of rows for a SQL query
* If no query handle is specified, the last query will be taken as reference
*
* @param mixed $result Optional query handle
*
* @return mixed Number of rows or false on failure
* @deprecated This method shows very poor performance and should be avoided.
*/
public function num_rows($result = null)
{
if (($result || ($result === null && ($result = $this->last_result))) && $result !== true) {
// repeat query with SELECT COUNT(*) ...
if (preg_match('/^SELECT\s+(?:ALL\s+|DISTINCT\s+)?(?:.*?)\s+FROM\s+(.*)$/ims', $result->queryString, $m)) {
$query = $this->dbh->query('SELECT COUNT(*) FROM ' . $m[1], PDO::FETCH_NUM);
return $query ? intval($query->fetchColumn(0)) : false;
}
else {
$num = count($result->fetchAll());
$result->execute(); // re-execute query because there's no seek(0)
return $num;
}
}
return false;
}
/**
* Get last inserted record ID
*
* @param string $table Table name (to find the incremented sequence)
*
* @return mixed ID or false on failure
*/
public function insert_id($table = '')
{
if (!$this->db_connected || $this->db_mode == 'r') {
return false;
}
if ($table) {
// resolve table name
$table = $this->table_name($table);
}
$id = $this->dbh->lastInsertId($table);
return $id;
}
/**
* Get an associative array for one row
* If no query handle is specified, the last query will be taken as reference
*
* @param mixed $result Optional query handle
*
* @return mixed Array with col values or false on failure
*/
public function fetch_assoc($result = null)
{
return $this->_fetch_row($result, PDO::FETCH_ASSOC);
}
/**
* Get an index array for one row
* If no query handle is specified, the last query will be taken as reference
*
* @param mixed $result Optional query handle
*
* @return mixed Array with col values or false on failure
*/
public function fetch_array($result = null)
{
return $this->_fetch_row($result, PDO::FETCH_NUM);
}
/**
* Get col values for a result row
*
* @param mixed $result Optional query handle
* @param int $mode Fetch mode identifier
*
* @return mixed Array with col values or false on failure
*/
protected function _fetch_row($result, $mode)
{
if ($result || ($result === null && ($result = $this->last_result))) {
if ($result !== true) {
return $result->fetch($mode);
}
}
return false;
}
/**
* Adds LIMIT,OFFSET clauses to the query
*
* @param string $query SQL query
* @param int $limit Number of rows
* @param int $offset Offset
*
* @return string SQL query
*/
protected function set_limit($query, $limit = 0, $offset = 0)
{
if ($limit) {
$query .= ' LIMIT ' . intval($limit);
}
if ($offset) {
$query .= ' OFFSET ' . intval($offset);
}
return $query;
}
/**
* Returns list of tables in a database
*
* @return array List of all tables of the current database
*/
public function list_tables()
{
// get tables if not cached
if ($this->tables === null) {
$q = $this->query("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES"
. " WHERE TABLE_TYPE = 'BASE TABLE'"
. " ORDER BY TABLE_NAME");
$this->tables = $q ? $q->fetchAll(PDO::FETCH_COLUMN, 0) : array();
}
return $this->tables;
}
/**
* Returns list of columns in database table
*
* @param string $table Table name
*
* @return array List of table cols
*/
public function list_cols($table)
{
$q = $this->query('SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ?',
array($table));
if ($q) {
return $q->fetchAll(PDO::FETCH_COLUMN, 0);
}
return array();
}
/**
* Start transaction
*
* @return bool True on success, False on failure
*/
public function startTransaction()
{
$this->db_connect('w', true);
// check connection before proceeding
if (!$this->is_connected()) {
return $this->last_result = false;
}
$this->debug('BEGIN TRANSACTION');
return $this->last_result = $this->dbh->beginTransaction();
}
/**
* Commit transaction
*
* @return bool True on success, False on failure
*/
public function endTransaction()
{
$this->db_connect('w', true);
// check connection before proceeding
if (!$this->is_connected()) {
return $this->last_result = false;
}
$this->debug('COMMIT TRANSACTION');
return $this->last_result = $this->dbh->commit();
}
/**
* Rollback transaction
*
* @return bool True on success, False on failure
*/
public function rollbackTransaction()
{
$this->db_connect('w', true);
// check connection before proceeding
if (!$this->is_connected()) {
return $this->last_result = false;
}
$this->debug('ROLLBACK TRANSACTION');
return $this->last_result = $this->dbh->rollBack();
}
/**
* Release resources related to the last query result.
* When we know we don't need to access the last query result we can destroy it
* and release memory. Useful especially if the query returned big chunk of data.
*/
public function reset()
{
$this->last_result = null;
}
/**
* Terminate database connection.
*/
public function closeConnection()
{
$this->db_connected = false;
$this->db_index = 0;
// release statement and connection resources
$this->last_result = null;
$this->dbh = null;
$this->dbhs = array();
}
/**
* Formats input so it can be safely used in a query
*
* @param mixed $input Value to quote
* @param string $type Type of data (integer, bool, ident)
*
* @return string Quoted/converted string for use in query
*/
public function quote($input, $type = null)
{
// handle int directly for better performance
if ($type == 'integer' || $type == 'int') {
return intval($input);
}
if (is_null($input)) {
return 'NULL';
}
if ($input instanceof DateTime) {
return $this->quote($input->format($this->options['datetime_format']));
}
if ($type == 'ident') {
return $this->quote_identifier($input);
}
// create DB handle if not available
if (!$this->dbh) {
$this->db_connect('r');
}
if ($this->dbh) {
$map = array(
'bool' => PDO::PARAM_BOOL,
'integer' => PDO::PARAM_INT,
);
$type = isset($map[$type]) ? $map[$type] : PDO::PARAM_STR;
return strtr($this->dbh->quote($input, $type),
// escape ? and `
array('?' => '??', self::DEFAULT_QUOTE => self::DEFAULT_QUOTE.self::DEFAULT_QUOTE)
);
}
return 'NULL';
}
/**
* Escapes a string so it can be safely used in a query
*
* @param string $str A string to escape
*
* @return string Escaped string for use in a query
*/
public function escape($str)
{
if (is_null($str)) {
return 'NULL';
}
return substr($this->quote($str), 1, -1);
}
/**
* Quotes a string so it can be safely used as a table or column name
*
* @param string $str Value to quote
*
* @return string Quoted string for use in query
* @deprecated Replaced by rcube_db::quote_identifier
* @see rcube_db::quote_identifier
*/
public function quoteIdentifier($str)
{
return $this->quote_identifier($str);
}
/**
* Escapes a string so it can be safely used in a query
*
* @param string $str A string to escape
*
* @return string Escaped string for use in a query
* @deprecated Replaced by rcube_db::escape
* @see rcube_db::escape
*/
public function escapeSimple($str)
{
return $this->escape($str);
}
/**
* Quotes a string so it can be safely used as a table or column name
*
* @param string $str Value to quote
*
* @return string Quoted string for use in query
*/
public function quote_identifier($str)
{
$start = $this->options['identifier_start'];
$end = $this->options['identifier_end'];
$name = array();
foreach (explode('.', $str) as $elem) {
$elem = str_replace(array($start, $end), '', $elem);
$name[] = $start . $elem . $end;
}
return implode($name, '.');
}
/**
* Return SQL function for current time and date
*
* @param int $interval Optional interval (in seconds) to add/subtract
*
* @return string SQL function to use in query
*/
public function now($interval = 0)
{
if ($interval) {
$add = ' ' . ($interval > 0 ? '+' : '-') . ' INTERVAL ';
$add .= $interval > 0 ? intval($interval) : intval($interval) * -1;
$add .= ' SECOND';
}
return "now()" . $add;
}
/**
* Return list of elements for use with SQL's IN clause
*
* @param array $arr Input array
* @param string $type Type of data (integer, bool, ident)
*
* @return string Comma-separated list of quoted values for use in query
*/
public function array2list($arr, $type = null)
{
if (!is_array($arr)) {
return $this->quote($arr, $type);
}
foreach ($arr as $idx => $item) {
$arr[$idx] = $this->quote($item, $type);
}
return implode(',', $arr);
}
/**
* Return SQL statement to convert a field value into a unix timestamp
*
* This method is deprecated and should not be used anymore due to limitations
* of timestamp functions in Mysql (year 2038 problem)
*
* @param string $field Field name
*
* @return string SQL statement to use in query
* @deprecated
*/
public function unixtimestamp($field)
{
return "UNIX_TIMESTAMP($field)";
}
/**
* Return SQL statement to convert from a unix timestamp
*
* @param int $timestamp Unix timestamp
*
* @return string Date string in db-specific format
* @deprecated
*/
public function fromunixtime($timestamp)
{
return $this->quote(date($this->options['datetime_format'], $timestamp));
}
/**
* Return SQL statement for case insensitive LIKE
*
* @param string $column Field name
* @param string $value Search value
*
* @return string SQL statement to use in query
*/
public function ilike($column, $value)
{
return $this->quote_identifier($column).' LIKE '.$this->quote($value);
}
/**
* Abstract SQL statement for value concatenation
*
* @return string SQL statement to be used in query
*/
public function concat(/* col1, col2, ... */)
{
$args = func_get_args();
if (is_array($args[0])) {
$args = $args[0];
}
return '(' . join(' || ', $args) . ')';
}
/**
* Encodes non-UTF-8 characters in string/array/object (recursive)
*
* @param mixed $input Data to fix
* @param bool $serialized Enable serialization
*
* @return mixed Properly UTF-8 encoded data
*/
public static function encode($input, $serialized = false)
{
// use Base64 encoding to workaround issues with invalid
// or null characters in serialized string (#1489142)
if ($serialized) {
return base64_encode(serialize($input));
}
if (is_object($input)) {
foreach (get_object_vars($input) as $idx => $value) {
$input->$idx = self::encode($value);
}
return $input;
}
else if (is_array($input)) {
foreach ($input as $idx => $value) {
$input[$idx] = self::encode($value);
}
return $input;
}
return utf8_encode($input);
}
/**
* Decodes encoded UTF-8 string/object/array (recursive)
*
* @param mixed $input Input data
* @param bool $serialized Enable serialization
*
* @return mixed Decoded data
*/
public static function decode($input, $serialized = false)
{
// use Base64 encoding to workaround issues with invalid
// or null characters in serialized string (#1489142)
if ($serialized) {
// Keep backward compatybility where base64 wasn't used
if (strpos(substr($input, 0, 16), ':') !== false) {
return self::decode(@unserialize($input));
}
return @unserialize(base64_decode($input));
}
if (is_object($input)) {
foreach (get_object_vars($input) as $idx => $value) {
$input->$idx = self::decode($value);
}
return $input;
}
else if (is_array($input)) {
foreach ($input as $idx => $value) {
$input[$idx] = self::decode($value);
}
return $input;
}
return utf8_decode($input);
}
/**
* Return correct name for a specific database table
*
* @param string $table Table name
* @param bool $quoted Quote table identifier
*
* @return string Translated table name
*/
public function table_name($table, $quoted = false)
{
// let plugins alter the table name (#1489837)
$plugin = rcube::get_instance()->plugins->exec_hook('db_table_name', array('table' => $table));
$table = $plugin['table'];
// add prefix to the table name if configured
if (($prefix = $this->options['table_prefix']) && strpos($table, $prefix) !== 0) {
$table = $prefix . $table;
}
if ($quoted) {
$table = $this->quote_identifier($table);
}
return $table;
}
/**
* Set class option value
*
* @param string $name Option name
* @param mixed $value Option value
*/
public function set_option($name, $value)
{
$this->options[$name] = $value;
}
/**
* Set DSN connection to be used for the given table
*
* @param string $table Table name
* @param string $mode DSN connection ('r' or 'w') to be used
*/
public function set_table_dsn($table, $mode)
{
$this->options['table_dsn_map'][$this->table_name($table)] = $mode;
}
/**
* MDB2 DSN string parser
*
* @param string $sequence Secuence name
*
* @return array DSN parameters
*/
public static function parse_dsn($dsn)
{
if (empty($dsn)) {
return null;
}
// Find phptype and dbsyntax
if (($pos = strpos($dsn, '://')) !== false) {
$str = substr($dsn, 0, $pos);
$dsn = substr($dsn, $pos + 3);
}
else {
$str = $dsn;
$dsn = null;
}
// Get phptype and dbsyntax
// $str => phptype(dbsyntax)
if (preg_match('|^(.+?)\((.*?)\)$|', $str, $arr)) {
$parsed['phptype'] = $arr[1];
$parsed['dbsyntax'] = !$arr[2] ? $arr[1] : $arr[2];
}
else {
$parsed['phptype'] = $str;
$parsed['dbsyntax'] = $str;
}
if (empty($dsn)) {
return $parsed;
}
// Get (if found): username and password
// $dsn => username:password@protocol+hostspec/database
if (($at = strrpos($dsn,'@')) !== false) {
$str = substr($dsn, 0, $at);
$dsn = substr($dsn, $at + 1);
if (($pos = strpos($str, ':')) !== false) {
$parsed['username'] = rawurldecode(substr($str, 0, $pos));
$parsed['password'] = rawurldecode(substr($str, $pos + 1));
}
else {
$parsed['username'] = rawurldecode($str);
}
}
// Find protocol and hostspec
// $dsn => proto(proto_opts)/database
if (preg_match('|^([^(]+)\((.*?)\)/?(.*?)$|', $dsn, $match)) {
$proto = $match[1];
$proto_opts = $match[2] ? $match[2] : false;
$dsn = $match[3];
}
// $dsn => protocol+hostspec/database (old format)
else {
if (strpos($dsn, '+') !== false) {
list($proto, $dsn) = explode('+', $dsn, 2);
}
if ( strpos($dsn, '//') === 0
&& strpos($dsn, '/', 2) !== false
&& $parsed['phptype'] == 'oci8'
) {
//oracle's "Easy Connect" syntax:
//"username/password@[//]host[:port][/service_name]"
//e.g. "scott/tiger@//mymachine:1521/oracle"
$proto_opts = $dsn;
$pos = strrpos($proto_opts, '/');
$dsn = substr($proto_opts, $pos + 1);
$proto_opts = substr($proto_opts, 0, $pos);
}
else if (strpos($dsn, '/') !== false) {
list($proto_opts, $dsn) = explode('/', $dsn, 2);
}
else {
$proto_opts = $dsn;
$dsn = null;
}
}
// process the different protocol options
$parsed['protocol'] = $proto ?: 'tcp';
$proto_opts = rawurldecode($proto_opts);
if (strpos($proto_opts, ':') !== false) {
list($proto_opts, $parsed['port']) = explode(':', $proto_opts);
}
if ($parsed['protocol'] == 'tcp') {
$parsed['hostspec'] = $proto_opts;
}
else if ($parsed['protocol'] == 'unix') {
$parsed['socket'] = $proto_opts;
}
// Get dabase if any
// $dsn => database
if ($dsn) {
// /database
if (($pos = strpos($dsn, '?')) === false) {
$parsed['database'] = rawurldecode($dsn);
// /database?param1=value1&param2=value2
}
else {
$parsed['database'] = rawurldecode(substr($dsn, 0, $pos));
$dsn = substr($dsn, $pos + 1);
if (strpos($dsn, '&') !== false) {
$opts = explode('&', $dsn);
}
else { // database?param1=value1
$opts = array($dsn);
}
foreach ($opts as $opt) {
list($key, $value) = explode('=', $opt);
if (!array_key_exists($key, $parsed) || false === $parsed[$key]) {
// don't allow params overwrite
$parsed[$key] = rawurldecode($value);
}
}
}
}
return $parsed;
}
/**
* Returns PDO DSN string from DSN array
*
* @param array $dsn DSN parameters
*
* @return string DSN string
*/
protected function dsn_string($dsn)
{
$params = array();
$result = $dsn['phptype'] . ':';
if ($dsn['hostspec']) {
$params[] = 'host=' . $dsn['hostspec'];
}
if ($dsn['port']) {
$params[] = 'port=' . $dsn['port'];
}
if ($dsn['database']) {
$params[] = 'dbname=' . $dsn['database'];
}
if (!empty($params)) {
$result .= implode(';', $params);
}
return $result;
}
/**
* Returns driver-specific connection options
*
* @param array $dsn DSN parameters
*
* @return array Connection options
*/
protected function dsn_options($dsn)
{
$result = array();
if ($this->db_pconn) {
$result[PDO::ATTR_PERSISTENT] = true;
}
if (!empty($dsn['prefetch'])) {
$result[PDO::ATTR_PREFETCH] = (int) $dsn['prefetch'];
}
if (!empty($dsn['timeout'])) {
$result[PDO::ATTR_TIMEOUT] = (int) $dsn['timeout'];
}
return $result;
}
/**
* Execute the given SQL script
*
* @param string $sql SQL queries to execute
*
* @return boolen True on success, False on error
*/
public function exec_script($sql)
{
$sql = $this->fix_table_names($sql);
$buff = '';
$exec = '';
foreach (explode("\n", $sql) as $line) {
$trimmed = trim($line);
if ($trimmed == '' || preg_match('/^--/', $trimmed)) {
continue;
}
if ($trimmed == 'GO') {
$exec = $buff;
}
else if ($trimmed[strlen($trimmed)-1] == ';') {
$exec = $buff . substr(rtrim($line), 0, -1);
}
if ($exec) {
$this->query($exec);
$buff = '';
$exec = '';
if ($this->db_error) {
break;
}
}
else {
$buff .= $line . "\n";
}
}
return !$this->db_error;
}
/**
* Parse SQL file and fix table names according to table prefix
*/
protected function fix_table_names($sql)
{
if (!$this->options['table_prefix']) {
return $sql;
}
$sql = preg_replace_callback(
'/((TABLE|TRUNCATE|(?<!ON )UPDATE|INSERT INTO|FROM'
. '| ON(?! (DELETE|UPDATE))|REFERENCES|CONSTRAINT|FOREIGN KEY|INDEX)'
. '\s+(IF (NOT )?EXISTS )?[`"]*)([^`"\( \r\n]+)/',
array($this, 'fix_table_names_callback'),
$sql
);
return $sql;
}
/**
* Preg_replace callback for fix_table_names()
*/
protected function fix_table_names_callback($matches)
{
return $matches[1] . $this->options['table_prefix'] . $matches[count($matches)-1];
}
}
diff --git a/program/lib/Roundcube/rcube_enriched.php b/program/lib/Roundcube/rcube_enriched.php
index b9de5693a..8c4d0e800 100644
--- a/program/lib/Roundcube/rcube_enriched.php
+++ b/program/lib/Roundcube/rcube_enriched.php
@@ -1,150 +1,151 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2012, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Helper class to convert Enriched to HTML format (RFC 1523, 1896) |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
| Author: Ryo Chijiiwa (IlohaMail) |
+-----------------------------------------------------------------------+
*/
/**
* Class for Enriched to HTML conversion
*
* @package Framework
* @subpackage Utils
*/
class rcube_enriched
{
protected static function convert_newlines($body)
{
// remove single newlines, convert N newlines to N-1
$body = str_replace("\r\n", "\n", $body);
$len = strlen($body);
$nl = 0;
$out = '';
for ($i=0; $i<$len; $i++) {
$c = $body[$i];
if (ord($c) == 10)
$nl++;
if ($nl && ord($c) != 10)
$nl = 0;
if ($nl != 1)
$out .= $c;
else
$out .= ' ';
}
return $out;
}
protected static function convert_formatting($body)
{
$replace = array(
'<bold>' => '<b>', '</bold>' => '</b>',
'<italic>' => '<i>', '</italic>' => '</i>',
'<fixed>' => '<tt>', '</fixed>' => '</tt>',
'<smaller>' => '<font size=-1>', '</smaller>'=> '</font>',
'<bigger>' => '<font size=+1>', '</bigger>' => '</font>',
'<underline>' => '<span style="text-decoration: underline">', '</underline>' => '</span>',
'<flushleft>' => '<span style="text-align: left">', '</flushleft>' => '</span>',
'<flushright>' => '<span style="text-align: right">', '</flushright>' => '</span>',
'<flushboth>' => '<span style="text-align: justified">', '</flushboth>' => '</span>',
'<indent>' => '<span style="padding-left: 20px">', '</indent>' => '</span>',
'<indentright>' => '<span style="padding-right: 20px">', '</indentright>' => '</span>',
);
return str_ireplace(array_keys($replace), array_values($replace), $body);
}
protected static function convert_font($body)
{
$pattern = '/(.*)\<fontfamily\>\<param\>(.*)\<\/param\>(.*)\<\/fontfamily\>(.*)/ims';
while (preg_match($pattern, $body, $a)) {
if (count($a) != 5)
continue;
$body = $a[1].'<span style="font-family: '.$a[2].'">'.$a[3].'</span>'.$a[4];
}
return $body;
}
protected static function convert_color($body)
{
$pattern = '/(.*)\<color\>\<param\>(.*)\<\/param\>(.*)\<\/color\>(.*)/ims';
while (preg_match($pattern, $body, $a)) {
if (count($a) != 5)
continue;
// extract color (either by name, or ####,####,####)
if (strpos($a[2],',')) {
$rgb = explode(',',$a[2]);
$color = '#';
for ($i=0; $i<3; $i++)
$color .= substr($rgb[$i], 0, 2); // just take first 2 bytes
}
else {
$color = $a[2];
}
// put it all together
$body = $a[1].'<span style="color: '.$color.'">'.$a[3].'</span>'.$a[4];
}
return $body;
}
protected static function convert_excerpt($body)
{
$pattern = '/(.*)\<excerpt\>(.*)\<\/excerpt\>(.*)/i';
while (preg_match($pattern, $body, $a)) {
if (count($a) != 4)
continue;
$quoted = '';
$lines = explode('<br>', $a[2]);
foreach ($lines as $line)
$quoted .= '&gt;'.$line.'<br>';
$body = $a[1].'<span class="quotes">'.$quoted.'</span>'.$a[3];
}
return $body;
}
/**
* Converts Enriched text into HTML format
*
* @param string $body Enriched text
*
* @return string HTML text
*/
public static function to_html($body)
{
$body = str_replace('<<','&lt;',$body);
$body = self::convert_newlines($body);
$body = str_replace("\n", '<br>', $body);
$body = self::convert_formatting($body);
$body = self::convert_color($body);
$body = self::convert_font($body);
$body = self::convert_excerpt($body);
//$body = nl2br($body);
return $body;
}
}
diff --git a/program/lib/Roundcube/rcube_html2text.php b/program/lib/Roundcube/rcube_html2text.php
index 6562de8cc..371c8f611 100644
--- a/program/lib/Roundcube/rcube_html2text.php
+++ b/program/lib/Roundcube/rcube_html2text.php
@@ -1,735 +1,736 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2012, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| Copyright (c) 2005-2007, Jon Abernathy <jon@chuggnutt.com> |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Converts HTML to formatted plain text (based on html2text class) |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
| Author: Jon Abernathy <jon@chuggnutt.com> |
+-----------------------------------------------------------------------+
*/
/**
* Takes HTML and converts it to formatted, plain text.
*
* Thanks to Alexander Krug (http://www.krugar.de/) to pointing out and
* correcting an error in the regexp search array. Fixed 7/30/03.
*
* Updated set_html() function's file reading mechanism, 9/25/03.
*
* Thanks to Joss Sanglier (http://www.dancingbear.co.uk/) for adding
* several more HTML entity codes to the $search and $replace arrays.
* Updated 11/7/03.
*
* Thanks to Darius Kasperavicius (http://www.dar.dar.lt/) for
* suggesting the addition of $allowed_tags and its supporting function
* (which I slightly modified). Updated 3/12/04.
*
* Thanks to Justin Dearing for pointing out that a replacement for the
* <TH> tag was missing, and suggesting an appropriate fix.
* Updated 8/25/04.
*
* Thanks to Mathieu Collas (http://www.myefarm.com/) for finding a
* display/formatting bug in the _build_link_list() function: email
* readers would show the left bracket and number ("[1") as part of the
* rendered email address.
* Updated 12/16/04.
*
* Thanks to Wojciech Bajon (http://histeria.pl/) for submitting code
* to handle relative links, which I hadn't considered. I modified his
* code a bit to handle normal HTTP links and MAILTO links. Also for
* suggesting three additional HTML entity codes to search for.
* Updated 03/02/05.
*
* Thanks to Jacob Chandler for pointing out another link condition
* for the _build_link_list() function: "https".
* Updated 04/06/05.
*
* Thanks to Marc Bertrand (http://www.dresdensky.com/) for
* suggesting a revision to the word wrapping functionality; if you
* specify a $width of 0 or less, word wrapping will be ignored.
* Updated 11/02/06.
*
* *** Big housecleaning updates below:
*
* Thanks to Colin Brown (http://www.sparkdriver.co.uk/) for
* suggesting the fix to handle </li> and blank lines (whitespace).
* Christian Basedau (http://www.movetheweb.de/) also suggested the
* blank lines fix.
*
* Special thanks to Marcus Bointon (http://www.synchromedia.co.uk/),
* Christian Basedau, Norbert Laposa (http://ln5.co.uk/),
* Bas van de Weijer, and Marijn van Butselaar
* for pointing out my glaring error in the <th> handling. Marcus also
* supplied a host of fixes.
*
* Thanks to Jeffrey Silverman (http://www.newtnotes.com/) for pointing
* out that extra spaces should be compressed--a problem addressed with
* Marcus Bointon's fixes but that I had not yet incorporated.
*
* Thanks to Daniel Schledermann (http://www.typoconsult.dk/) for
* suggesting a valuable fix with <a> tag handling.
*
* Thanks to Wojciech Bajon (again!) for suggesting fixes and additions,
* including the <a> tag handling that Daniel Schledermann pointed
* out but that I had not yet incorporated. I haven't (yet)
* incorporated all of Wojciech's changes, though I may at some
* future time.
*
* *** End of the housecleaning updates. Updated 08/08/07.
*/
/**
* Converts HTML to formatted plain text
*
* @package Framework
* @subpackage Utils
*/
class rcube_html2text
{
/**
* Contains the HTML content to convert.
*
* @var string $html
*/
protected $html;
/**
* Contains the converted, formatted text.
*
* @var string $text
*/
protected $text;
/**
* Maximum width of the formatted text, in columns.
*
* Set this value to 0 (or less) to ignore word wrapping
* and not constrain text to a fixed-width column.
*
* @var integer $width
*/
protected $width = 70;
/**
* Target character encoding for output text
*
* @var string $charset
*/
protected $charset = 'UTF-8';
/**
* List of preg* regular expression patterns to search for,
* used in conjunction with $replace.
*
* @var array $search
* @see $replace
*/
protected $search = array(
'/\r/', // Non-legal carriage return
'/^.*<body[^>]*>\n*/is', // Anything before <body>
'/<head[^>]*>.*?<\/head>/is', // <head>
'/<script[^>]*>.*?<\/script>/is', // <script>
'/<style[^>]*>.*?<\/style>/is', // <style>
'/[\n\t]+/', // Newlines and tabs
'/<p[^>]*>/i', // <p>
'/<\/p>[\s\n\t]*<div[^>]*>/i', // </p> before <div>
'/<br[^>]*>[\s\n\t]*<div[^>]*>/i', // <br> before <div>
'/<br[^>]*>\s*/i', // <br>
'/<i[^>]*>(.*?)<\/i>/i', // <i>
'/<em[^>]*>(.*?)<\/em>/i', // <em>
'/(<ul[^>]*>|<\/ul>)/i', // <ul> and </ul>
'/(<ol[^>]*>|<\/ol>)/i', // <ol> and </ol>
'/<li[^>]*>(.*?)<\/li>/i', // <li> and </li>
'/<li[^>]*>/i', // <li>
'/<hr[^>]*>/i', // <hr>
'/<div[^>]*>/i', // <div>
'/(<table[^>]*>|<\/table>)/i', // <table> and </table>
'/(<tr[^>]*>|<\/tr>)/i', // <tr> and </tr>
'/<td[^>]*>(.*?)<\/td>/i', // <td> and </td>
);
/**
* List of pattern replacements corresponding to patterns searched.
*
* @var array $replace
* @see $search
*/
protected $replace = array(
'', // Non-legal carriage return
'', // Anything before <body>
'', // <head>
'', // <script>
'', // <style>
' ', // Newlines and tabs
"\n\n", // <p>
"\n<div>", // </p> before <div>
'<div>', // <br> before <div>
"\n", // <br>
'_\\1_', // <i>
'_\\1_', // <em>
"\n\n", // <ul> and </ul>
"\n\n", // <ol> and </ol>
"\t* \\1\n", // <li> and </li>
"\n\t* ", // <li>
"\n-------------------------\n", // <hr>
"<div>\n", // <div>
"\n\n", // <table> and </table>
"\n", // <tr> and </tr>
"\t\t\\1\n", // <td> and </td>
);
/**
* List of preg* regular expression patterns to search for,
* used in conjunction with $ent_replace.
*
* @var array $ent_search
* @see $ent_replace
*/
protected $ent_search = array(
'/&(nbsp|#160);/i', // Non-breaking space
'/&(quot|rdquo|ldquo|#8220|#8221|#147|#148);/i',
// Double quotes
'/&(apos|rsquo|lsquo|#8216|#8217);/i', // Single quotes
'/&gt;/i', // Greater-than
'/&lt;/i', // Less-than
'/&(copy|#169);/i', // Copyright
'/&(trade|#8482|#153);/i', // Trademark
'/&(reg|#174);/i', // Registered
'/&(mdash|#151|#8212);/i', // mdash
'/&(ndash|minus|#8211|#8722);/i', // ndash
'/&(bull|#149|#8226);/i', // Bullet
'/&(pound|#163);/i', // Pound sign
'/&(euro|#8364);/i', // Euro sign
'/&(amp|#38);/i', // Ampersand: see _converter()
'/[ ]{2,}/', // Runs of spaces, post-handling
);
/**
* List of pattern replacements corresponding to patterns searched.
*
* @var array $ent_replace
* @see $ent_search
*/
protected $ent_replace = array(
"\xC2\xA0", // Non-breaking space
'"', // Double quotes
"'", // Single quotes
'>',
'<',
'(c)',
'(tm)',
'(R)',
'--',
'-',
'*',
'£',
'EUR', // Euro sign. €
'|+|amp|+|', // Ampersand: see _converter()
' ', // Runs of spaces, post-handling
);
/**
* List of preg* regular expression patterns to search for
* and replace using callback function.
*
* @var array $callback_search
*/
protected $callback_search = array(
'/<(a) [^>]*href=("|\')([^"\']+)\2[^>]*>(.*?)<\/a>/i', // <a href="">
'/<(h)[123456]( [^>]*)?>(.*?)<\/h[123456]>/i', // h1 - h6
'/<(b)( [^>]*)?>(.*?)<\/b>/i', // <b>
'/<(strong)( [^>]*)?>(.*?)<\/strong>/i', // <strong>
'/<(th)( [^>]*)?>(.*?)<\/th>/i', // <th> and </th>
);
/**
* List of preg* regular expression patterns to search for in PRE body,
* used in conjunction with $pre_replace.
*
* @var array $pre_search
* @see $pre_replace
*/
protected $pre_search = array(
"/\n/",
"/\t/",
'/ /',
'/<pre[^>]*>/',
'/<\/pre>/'
);
/**
* List of pattern replacements corresponding to patterns searched for PRE body.
*
* @var array $pre_replace
* @see $pre_search
*/
protected $pre_replace = array(
'<br>',
'&nbsp;&nbsp;&nbsp;&nbsp;',
'&nbsp;',
'',
''
);
/**
* Contains a list of HTML tags to allow in the resulting text.
*
* @var string $allowed_tags
* @see set_allowed_tags()
*/
protected $allowed_tags = '';
/**
* Contains the base URL that relative links should resolve to.
*
* @var string $url
*/
protected $url;
/**
* Indicates whether content in the $html variable has been converted yet.
*
* @var boolean $_converted
* @see $html, $text
*/
protected $_converted = false;
/**
* Contains URL addresses from links to be rendered in plain text.
*
* @var array $_link_list
* @see _build_link_list()
*/
protected $_link_list = array();
/**
* Boolean flag, true if a table of link URLs should be listed after the text.
*
* @var boolean $_do_links
* @see __construct()
*/
protected $_do_links = true;
/**
* Constructor.
*
* If the HTML source string (or file) is supplied, the class
* will instantiate with that source propagated, all that has
* to be done it to call get_text().
*
* @param string $source HTML content
* @param boolean $from_file Indicates $source is a file to pull content from
* @param boolean $do_links Indicate whether a table of link URLs is desired
* @param integer $width Maximum width of the formatted text, 0 for no limit
*/
function __construct($source = '', $from_file = false, $do_links = true, $width = 75, $charset = 'UTF-8')
{
if (!empty($source)) {
$this->set_html($source, $from_file);
}
$this->set_base_url();
$this->_do_links = $do_links;
$this->width = $width;
$this->charset = $charset;
}
/**
* Loads source HTML into memory, either from $source string or a file.
*
* @param string $source HTML content
* @param boolean $from_file Indicates $source is a file to pull content from
*/
function set_html($source, $from_file = false)
{
if ($from_file && file_exists($source)) {
$this->html = file_get_contents($source);
}
else {
$this->html = $source;
}
$this->_converted = false;
}
/**
* Returns the text, converted from HTML.
*
* @return string Plain text
*/
function get_text()
{
if (!$this->_converted) {
$this->_convert();
}
return $this->text;
}
/**
* Prints the text, converted from HTML.
*/
function print_text()
{
print $this->get_text();
}
/**
* Sets the allowed HTML tags to pass through to the resulting text.
*
* Tags should be in the form "<p>", with no corresponding closing tag.
*/
function set_allowed_tags($allowed_tags = '')
{
if (!empty($allowed_tags)) {
$this->allowed_tags = $allowed_tags;
}
}
/**
* Sets a base URL to handle relative links.
*/
function set_base_url($url = '')
{
if (empty($url)) {
if (!empty($_SERVER['HTTP_HOST'])) {
$this->url = 'http://' . $_SERVER['HTTP_HOST'];
}
else {
$this->url = '';
}
}
else {
// Strip any trailing slashes for consistency (relative
// URLs may already start with a slash like "/file.html")
if (substr($url, -1) == '/') {
$url = substr($url, 0, -1);
}
$this->url = $url;
}
}
/**
* Workhorse function that does actual conversion (calls _converter() method).
*/
protected function _convert()
{
// Variables used for building the link list
$this->_link_list = array();
$text = $this->html;
// Convert HTML to TXT
$this->_converter($text);
// Add link list
if (!empty($this->_link_list)) {
$text .= "\n\nLinks:\n------\n";
foreach ($this->_link_list as $idx => $url) {
$text .= '[' . ($idx+1) . '] ' . $url . "\n";
}
}
$this->text = $text;
$this->_converted = true;
}
/**
* Workhorse function that does actual conversion.
*
* First performs custom tag replacement specified by $search and
* $replace arrays. Then strips any remaining HTML tags, reduces whitespace
* and newlines to a readable format, and word wraps the text to
* $width characters.
*
* @param string &$text Reference to HTML content string
*/
protected function _converter(&$text)
{
// Convert <BLOCKQUOTE> (before PRE!)
$this->_convert_blockquotes($text);
// Convert <PRE>
$this->_convert_pre($text);
// Run our defined tags search-and-replace
$text = preg_replace($this->search, $this->replace, $text);
// Run our defined tags search-and-replace with callback
$text = preg_replace_callback($this->callback_search, array($this, 'tags_preg_callback'), $text);
// Strip any other HTML tags
$text = strip_tags($text, $this->allowed_tags);
// Run our defined entities/characters search-and-replace
$text = preg_replace($this->ent_search, $this->ent_replace, $text);
// Replace known html entities
$text = html_entity_decode($text, ENT_QUOTES, $this->charset);
// Replace unicode nbsp to regular spaces
$text = preg_replace('/\xC2\xA0/', ' ', $text);
// Remove unknown/unhandled entities (this cannot be done in search-and-replace block)
$text = preg_replace('/&([a-zA-Z0-9]{2,6}|#[0-9]{2,4});/', '', $text);
// Convert "|+|amp|+|" into "&", need to be done after handling of unknown entities
// This properly handles situation of "&amp;quot;" in input string
$text = str_replace('|+|amp|+|', '&', $text);
// Bring down number of empty lines to 2 max
$text = preg_replace("/\n\s+\n/", "\n\n", $text);
$text = preg_replace("/[\n]{3,}/", "\n\n", $text);
// remove leading empty lines (can be produced by eg. P tag on the beginning)
$text = ltrim($text, "\n");
// Wrap the text to a readable format
// for PHP versions >= 4.0.2. Default width is 75
// If width is 0 or less, don't wrap the text.
if ( $this->width > 0 ) {
$text = wordwrap($text, $this->width);
}
}
/**
* Helper function called by preg_replace() on link replacement.
*
* Maintains an internal list of links to be displayed at the end of the
* text, with numeric indices to the original point in the text they
* appeared. Also makes an effort at identifying and handling absolute
* and relative links.
*
* @param string $link URL of the link
* @param string $display Part of the text to associate number with
*/
protected function _build_link_list($link, $display)
{
if (empty($link)) {
return $display;
}
// Ignored link types
if (preg_match('!^(javascript:|mailto:|#)!i', $link)) {
return $display;
}
// skip links with href == content (#1490434)
if ($link === $display) {
return $display;
}
if (preg_match('!^([a-z][a-z0-9.+-]+:)!i', $link)) {
$url = $link;
}
else {
$url = $this->url;
if (substr($link, 0, 1) != '/') {
$url .= '/';
}
$url .= "$link";
}
if (!$this->_do_links) {
// When not using link list use URL if there's no content (#5795)
// The content here is HTML, convert it to text first
$h2t = new rcube_html2text($display, false, false, 1024, $this->charset);
$display = $h2t->get_text();
if (empty($display) && preg_match('!^([a-z][a-z0-9.+-]+://)!i', $link)) {
return $link;
}
return $display;
}
if (($index = array_search($url, $this->_link_list)) === false) {
$index = count($this->_link_list);
$this->_link_list[] = $url;
}
return $display . ' [' . ($index+1) . ']';
}
/**
* Helper function for PRE body conversion.
*
* @param string &$text HTML content
*/
protected function _convert_pre(&$text)
{
// get the content of PRE element
while (preg_match('/<pre[^>]*>(.*)<\/pre>/ismU', $text, $matches)) {
$this->pre_content = $matches[1];
// Run our defined tags search-and-replace with callback
$this->pre_content = preg_replace_callback($this->callback_search,
array($this, 'tags_preg_callback'), $this->pre_content);
// convert the content
$this->pre_content = sprintf('<div><br>%s<br></div>',
preg_replace($this->pre_search, $this->pre_replace, $this->pre_content));
// replace the content (use callback because content can contain $0 variable)
$text = preg_replace_callback('/<pre[^>]*>.*<\/pre>/ismU',
array($this, 'pre_preg_callback'), $text, 1);
// free memory
$this->pre_content = '';
}
}
/**
* Helper function for BLOCKQUOTE body conversion.
*
* @param string &$text HTML content
*/
protected function _convert_blockquotes(&$text)
{
$level = 0;
$offset = 0;
while (($start = stripos($text, '<blockquote', $offset)) !== false) {
$offset = $start + 12;
do {
$end = stripos($text, '</blockquote>', $offset);
$next = stripos($text, '<blockquote', $offset);
// nested <blockquote>, skip
if ($next !== false && $next < $end) {
$offset = $next + 12;
$level++;
}
// nested </blockquote> tag
if ($end !== false && $level > 0) {
$offset = $end + 12;
$level--;
}
// found matching end tag
else if ($end !== false && $level == 0) {
$taglen = strpos($text, '>', $start) - $start;
$startpos = $start + $taglen + 1;
// get blockquote content
$body = trim(substr($text, $startpos, $end - $startpos));
// adjust text wrapping width
$p_width = $this->width;
if ($this->width > 0) $this->width -= 2;
// replace content with inner blockquotes
$this->_converter($body);
// resore text width
$this->width = $p_width;
// Add citation markers and create <pre> block
$body = preg_replace_callback('/((?:^|\n)>*)([^\n]*)/', array($this, 'blockquote_citation_callback'), trim($body));
$body = '<pre>' . htmlspecialchars($body, ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, $this->charset) . '</pre>';
$text = substr_replace($text, $body . "\n", $start, $end + 13 - $start);
$offset = 0;
break;
}
// abort on invalid tag structure (e.g. no closing tag found)
else {
break;
}
}
while ($end || $next);
}
}
/**
* Callback function to correctly add citation markers for blockquote contents
*/
public function blockquote_citation_callback($m)
{
$line = ltrim($m[2]);
$space = $line[0] == '>' ? '' : ' ';
return $m[1] . '>' . $space . $line;
}
/**
* Callback function for preg_replace_callback use.
*
* @param array $matches PREG matches
* @return string
*/
public function tags_preg_callback($matches)
{
switch (strtolower($matches[1])) {
case 'b':
case 'strong':
return $this->_toupper($matches[3]);
case 'th':
return $this->_toupper("\t\t". $matches[3] ."\n");
case 'h':
return $this->_toupper("\n\n". $matches[3] ."\n\n");
case 'a':
// Remove spaces in URL (#1487805)
$url = str_replace(' ', '', $matches[3]);
return $this->_build_link_list($url, $matches[4]);
}
}
/**
* Callback function for preg_replace_callback use in PRE content handler.
*
* @param array $matches PREG matches
* @return string
*/
public function pre_preg_callback($matches)
{
return $this->pre_content;
}
/**
* Strtoupper function with HTML tags and entities handling.
*
* @param string $str Text to convert
* @return string Converted text
*/
private function _toupper($str)
{
// string can containing HTML tags
$chunks = preg_split('/(<[^>]*>)/', $str, null, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
// convert toupper only the text between HTML tags
foreach ($chunks as $idx => $chunk) {
if ($chunk[0] != '<') {
$chunks[$idx] = $this->_strtoupper($chunk);
}
}
return implode($chunks);
}
/**
* Strtoupper multibyte wrapper function with HTML entities handling.
*
* @param string $str Text to convert
* @return string Converted text
*/
private function _strtoupper($str)
{
$str = html_entity_decode($str, ENT_COMPAT, $this->charset);
$str = mb_strtoupper($str);
$str = htmlspecialchars($str, ENT_COMPAT, $this->charset);
return $str;
}
}
diff --git a/program/lib/Roundcube/rcube_image.php b/program/lib/Roundcube/rcube_image.php
index 45c48c94e..2d10578ce 100644
--- a/program/lib/Roundcube/rcube_image.php
+++ b/program/lib/Roundcube/rcube_image.php
@@ -1,458 +1,459 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2012, The Roundcube Dev Team |
- | Copyright (C) 2011-2012, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Image resizer and converter |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Image resizer and converter
*
* @package Framework
* @subpackage Utils
*/
class rcube_image
{
private $image_file;
const TYPE_GIF = 1;
const TYPE_JPG = 2;
const TYPE_PNG = 3;
const TYPE_TIF = 4;
public static $extensions = array(
self::TYPE_GIF => 'gif',
self::TYPE_JPG => 'jpg',
self::TYPE_PNG => 'png',
self::TYPE_TIF => 'tif',
);
/**
* Class constructor
*
* @param string $filename Image file name/path
*/
function __construct($filename)
{
$this->image_file = $filename;
}
/**
* Get image properties.
*
* @return mixed Hash array with image props like type, width, height
*/
public function props()
{
// use GD extension
if (function_exists('getimagesize') && ($imsize = @getimagesize($this->image_file))) {
$width = $imsize[0];
$height = $imsize[1];
$gd_type = $imsize[2];
$type = image_type_to_extension($gd_type, false);
$channels = $imsize['channels'];
}
// use ImageMagick
if (!$type && ($data = $this->identify())) {
list($type, $width, $height) = $data;
$channels = null;
}
if ($type) {
return array(
'type' => $type,
'gd_type' => $gd_type,
'width' => $width,
'height' => $height,
'channels' => $channels,
);
}
}
/**
* Resize image to a given size. Use only to shrink an image.
* If an image is smaller than specified size it will be not resized.
*
* @param int $size Max width/height size
* @param string $filename Output filename
* @param boolean $browser_compat Convert to image type displayable by any browser
*
* @return mixed Output type on success, False on failure
*/
public function resize($size, $filename = null, $browser_compat = false)
{
$result = false;
$rcube = rcube::get_instance();
$convert = $rcube->config->get('im_convert_path', false);
$props = $this->props();
if (empty($props)) {
return false;
}
if (!$filename) {
$filename = $this->image_file;
}
// use Imagemagick
if ($convert || class_exists('Imagick', false)) {
$p['out'] = $filename;
$p['in'] = $this->image_file;
$type = $props['type'];
if (!$type && ($data = $this->identify())) {
$type = $data[0];
}
$type = strtr($type, array("jpeg" => "jpg", "tiff" => "tif", "ps" => "eps", "ept" => "eps"));
$p['intype'] = $type;
// convert to an image format every browser can display
if ($browser_compat && !in_array($type, array('jpg','gif','png'))) {
$type = 'jpg';
}
// If only one dimension is greater than the limit convert doesn't
// work as expected, we need to calculate new dimensions
$scale = $size / max($props['width'], $props['height']);
// if file is smaller than the limit, we do nothing
// but copy original file to destination file
if ($scale >= 1 && $p['intype'] == $type) {
$result = ($this->image_file == $filename || copy($this->image_file, $filename)) ? '' : false;
}
else {
$valid_types = "bmp,eps,gif,jp2,jpg,png,svg,tif";
if (in_array($type, explode(',', $valid_types))) { // Valid type?
if ($scale >= 1) {
$width = $props['width'];
$height = $props['height'];
}
else {
$width = intval($props['width'] * $scale);
$height = intval($props['height'] * $scale);
}
// use ImageMagick in command line
if ($convert) {
$p += array(
'type' => $type,
'quality' => 75,
'size' => $width . 'x' . $height,
);
$result = rcube::exec($convert . ' 2>&1 -flatten -auto-orient -colorspace sRGB -strip'
. ' -quality {quality} -resize {size} {intype}:{in} {type}:{out}', $p);
}
// use PHP's Imagick class
else {
try {
$image = new Imagick($this->image_file);
try {
// it throws exception on formats not supporting these features
$image->setImageBackgroundColor('white');
$image->setImageAlphaChannel(11);
$image->mergeImageLayers(Imagick::LAYERMETHOD_FLATTEN);
}
catch (Exception $e) {
// ignore errors
}
$image->setImageColorspace(Imagick::COLORSPACE_SRGB);
$image->setImageCompressionQuality(75);
$image->setImageFormat($type);
$image->stripImage();
$image->scaleImage($width, $height);
if ($image->writeImage($filename)) {
$result = '';
}
}
catch (Exception $e) {
rcube::raise_error($e, true, false);
}
}
}
}
if ($result === '') {
@chmod($filename, 0600);
return $type;
}
}
// do we have enough memory? (#1489937)
if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && !$this->mem_check($props)) {
return false;
}
// use GD extension
if ($props['gd_type']) {
if ($props['gd_type'] == IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) {
$image = imagecreatefromjpeg($this->image_file);
$type = 'jpg';
}
else if ($props['gd_type'] == IMAGETYPE_GIF && function_exists('imagecreatefromgif')) {
$image = imagecreatefromgif($this->image_file);
$type = 'gif';
}
else if ($props['gd_type'] == IMAGETYPE_PNG && function_exists('imagecreatefrompng')) {
$image = imagecreatefrompng($this->image_file);
$type = 'png';
}
else {
// @TODO: print error to the log?
return false;
}
if ($image === false) {
return false;
}
$scale = $size / max($props['width'], $props['height']);
// Imagemagick resize is implemented in shrinking mode (see -resize argument above)
// we do the same here, if an image is smaller than specified size
// we do nothing but copy original file to destination file
if ($scale >= 1) {
$result = $this->image_file == $filename || copy($this->image_file, $filename);
}
else {
$width = intval($props['width'] * $scale);
$height = intval($props['height'] * $scale);
$new_image = imagecreatetruecolor($width, $height);
if ($new_image === false) {
return false;
}
// Fix transparency of gif/png image
if ($props['gd_type'] != IMAGETYPE_JPEG) {
imagealphablending($new_image, false);
imagesavealpha($new_image, true);
$transparent = imagecolorallocatealpha($new_image, 255, 255, 255, 127);
imagefilledrectangle($new_image, 0, 0, $width, $height, $transparent);
}
imagecopyresampled($new_image, $image, 0, 0, 0, 0, $width, $height, $props['width'], $props['height']);
$image = $new_image;
// fix orientation of image if EXIF data exists and specifies orientation (GD strips the EXIF data)
if ($this->image_file && $type == 'jpg' && function_exists('exif_read_data')) {
$exif = @exif_read_data($this->image_file);
if ($exif && $exif['Orientation']) {
switch ($exif['Orientation']) {
case 3:
$image = imagerotate($image, 180, 0);
break;
case 6:
$image = imagerotate($image, -90, 0);
break;
case 8:
$image = imagerotate($image, 90, 0);
break;
}
}
}
if ($props['gd_type'] == IMAGETYPE_JPEG) {
$result = imagejpeg($image, $filename, 75);
}
elseif($props['gd_type'] == IMAGETYPE_GIF) {
$result = imagegif($image, $filename);
}
elseif($props['gd_type'] == IMAGETYPE_PNG) {
$result = imagepng($image, $filename, 6, PNG_ALL_FILTERS);
}
}
if ($result) {
@chmod($filename, 0600);
return $type;
}
}
// @TODO: print error to the log?
return false;
}
/**
* Convert image to a given type
*
* @param int $type Destination file type (see class constants)
* @param string $filename Output filename (if empty, original file will be used
* and filename extension will be modified)
*
* @return bool True on success, False on failure
*/
public function convert($type, $filename = null)
{
$rcube = rcube::get_instance();
$convert = $rcube->config->get('im_convert_path', false);
if (!$filename) {
$filename = $this->image_file;
// modify extension
if ($extension = self::$extensions[$type]) {
$filename = preg_replace('/\.[^.]+$/', '', $filename) . '.' . $extension;
}
}
// use ImageMagick in command line
if ($convert) {
$p['in'] = $this->image_file;
$p['out'] = $filename;
$p['type'] = self::$extensions[$type];
$result = rcube::exec($convert . ' 2>&1 -colorspace sRGB -strip -quality 75 {in} {type}:{out}', $p);
if ($result === '') {
chmod($filename, 0600);
return true;
}
}
// use PHP's Imagick class
if (class_exists('Imagick', false)) {
try {
$image = new Imagick($this->image_file);
$image->setImageColorspace(Imagick::COLORSPACE_SRGB);
$image->setImageCompressionQuality(75);
$image->setImageFormat(self::$extensions[$type]);
$image->stripImage();
if ($image->writeImage($filename)) {
@chmod($filename, 0600);
return true;
}
}
catch (Exception $e) {
rcube::raise_error($e, true, false);
}
}
// use GD extension (TIFF isn't supported)
$props = $this->props();
// do we have enough memory? (#1489937)
if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && !$this->mem_check($props)) {
return false;
}
if ($props['gd_type']) {
if ($props['gd_type'] == IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) {
$image = imagecreatefromjpeg($this->image_file);
}
else if ($props['gd_type'] == IMAGETYPE_GIF && function_exists('imagecreatefromgif')) {
$image = imagecreatefromgif($this->image_file);
}
else if ($props['gd_type'] == IMAGETYPE_PNG && function_exists('imagecreatefrompng')) {
$image = imagecreatefrompng($this->image_file);
}
else {
// @TODO: print error to the log?
return false;
}
if ($type == self::TYPE_JPG) {
$result = imagejpeg($image, $filename, 75);
}
else if ($type == self::TYPE_GIF) {
$result = imagegif($image, $filename);
}
else if ($type == self::TYPE_PNG) {
$result = imagepng($image, $filename, 6, PNG_ALL_FILTERS);
}
if ($result) {
@chmod($filename, 0600);
return true;
}
}
// @TODO: print error to the log?
return false;
}
/**
* Checks if image format conversion is supported
*
* @return boolean True if specified format can be converted to another format
*/
public static function is_convertable($mimetype = null)
{
$rcube = rcube::get_instance();
// @TODO: check if specified mimetype is really supported
return class_exists('Imagick', false) || $rcube->config->get('im_convert_path');
}
/**
* ImageMagick based image properties read.
*/
private function identify()
{
$rcube = rcube::get_instance();
// use ImageMagick in command line
if ($cmd = $rcube->config->get('im_identify_path')) {
$args = array('in' => $this->image_file, 'format' => "%m %[fx:w] %[fx:h]");
$id = rcube::exec($cmd. ' 2>/dev/null -format {format} {in}', $args);
if ($id) {
return explode(' ', strtolower($id));
}
}
// use PHP's Imagick class
if (class_exists('Imagick', false)) {
try {
$image = new Imagick($this->image_file);
return array(
strtolower($image->getImageFormat()),
$image->getImageWidth(),
$image->getImageHeight(),
);
}
catch (Exception $e) {}
}
}
/**
* Check if we have enough memory to load specified image
*/
private function mem_check($props)
{
// image size is unknown, we can't calculate required memory
if (!$props['width']) {
return true;
}
// channels: CMYK - 4, RGB - 3
$multip = ($props['channels'] ?: 3) + 1;
// calculate image size in memory (in bytes)
$size = $props['width'] * $props['height'] * $multip;
return rcube_utils::mem_check($size);
}
}
diff --git a/program/lib/Roundcube/rcube_imap.php b/program/lib/Roundcube/rcube_imap.php
index 18f1a561d..e5ac6e274 100644
--- a/program/lib/Roundcube/rcube_imap.php
+++ b/program/lib/Roundcube/rcube_imap.php
@@ -1,4571 +1,4572 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2012, The Roundcube Dev Team |
- | Copyright (C) 2011-2012, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| IMAP Storage Engine |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Interface class for accessing an IMAP server
*
* @package Framework
* @subpackage Storage
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_imap extends rcube_storage
{
/**
* Instance of rcube_imap_generic
*
* @var rcube_imap_generic
*/
public $conn;
/**
* Instance of rcube_imap_cache
*
* @var rcube_imap_cache
*/
protected $mcache;
/**
* Instance of rcube_cache
*
* @var rcube_cache
*/
protected $cache;
/**
* Internal (in-memory) cache
*
* @var array
*/
protected $icache = array();
protected $plugins;
protected $delimiter;
protected $namespace;
protected $struct_charset;
protected $search_set;
protected $search_string = '';
protected $search_charset = '';
protected $search_sort_field = '';
protected $search_threads = false;
protected $search_sorted = false;
protected $sort_field = '';
protected $sort_order = 'DESC';
protected $options = array('auth_type' => 'check');
protected $caching = false;
protected $messages_caching = false;
protected $threading = false;
protected $list_excludes = array();
protected $list_root;
/**
* Object constructor.
*/
public function __construct()
{
$this->conn = new rcube_imap_generic();
$this->plugins = rcube::get_instance()->plugins;
// Set namespace and delimiter from session,
// so some methods would work before connection
if (isset($_SESSION['imap_namespace'])) {
$this->namespace = $_SESSION['imap_namespace'];
}
if (isset($_SESSION['imap_delimiter'])) {
$this->delimiter = $_SESSION['imap_delimiter'];
}
if (!empty($_SESSION['imap_list_conf'])) {
list($this->list_root, $this->list_excludes) = $_SESSION['imap_list_conf'];
}
}
/**
* Magic getter for backward compat.
*
* @deprecated.
*/
public function __get($name)
{
if (isset($this->{$name})) {
return $this->{$name};
}
}
/**
* Connect to an IMAP server
*
* @param string $host Host to connect
* @param string $user Username for IMAP account
* @param string $pass Password for IMAP account
* @param integer $port Port to connect to
* @param string $use_ssl SSL schema (either ssl or tls) or null if plain connection
*
* @return boolean True on success, False on failure
*/
public function connect($host, $user, $pass, $port=143, $use_ssl=null)
{
// check for OpenSSL support in PHP build
if ($use_ssl && extension_loaded('openssl')) {
$this->options['ssl_mode'] = $use_ssl == 'imaps' ? 'ssl' : $use_ssl;
}
else if ($use_ssl) {
rcube::raise_error(array('code' => 403, 'type' => 'imap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "OpenSSL not available"), true, false);
$port = 143;
}
$this->options['port'] = $port;
if ($this->options['debug']) {
$this->set_debug(true);
$this->options['ident'] = array(
'name' => 'Roundcube',
'version' => RCUBE_VERSION,
'php' => PHP_VERSION,
'os' => PHP_OS,
'command' => $_SERVER['REQUEST_URI'],
);
}
$attempt = 0;
do {
$data = $this->plugins->exec_hook('storage_connect',
array_merge($this->options, array('host' => $host, 'user' => $user,
'attempt' => ++$attempt)));
if (!empty($data['pass'])) {
$pass = $data['pass'];
}
// Handle per-host socket options
rcube_utils::parse_socket_options($data['socket_options'], $data['host']);
$this->conn->connect($data['host'], $data['user'], $pass, $data);
} while(!$this->conn->connected() && $data['retry']);
$config = array(
'host' => $data['host'],
'user' => $data['user'],
'password' => $pass,
'port' => $port,
'ssl' => $use_ssl,
);
$this->options = array_merge($this->options, $config);
$this->connect_done = true;
if ($this->conn->connected()) {
// check for session identifier
$session = null;
if (preg_match('/\s+SESSIONID=([^=\s]+)/', $this->conn->result, $m)) {
$session = $m[1];
}
// get namespace and delimiter
$this->set_env();
// trigger post-connect hook
$this->plugins->exec_hook('storage_connected', array(
'host' => $host, 'user' => $user, 'session' => $session
));
return true;
}
// write error log
else if ($this->conn->error) {
if ($pass && $user) {
$message = sprintf("Login failed for %s against %s from %s. %s",
$user, $host, rcube_utils::remote_ip(), $this->conn->error);
rcube::raise_error(array('code' => 403, 'type' => 'imap',
'file' => __FILE__, 'line' => __LINE__,
'message' => $message), true, false);
}
}
return false;
}
/**
* Close IMAP connection.
* Usually done on script shutdown
*/
public function close()
{
$this->connect_done = false;
$this->conn->closeConnection();
if ($this->mcache) {
$this->mcache->close();
}
}
/**
* Check connection state, connect if not connected.
*
* @return bool Connection state.
*/
public function check_connection()
{
// Establish connection if it wasn't done yet
if (!$this->connect_done && !empty($this->options['user'])) {
return $this->connect(
$this->options['host'],
$this->options['user'],
$this->options['password'],
$this->options['port'],
$this->options['ssl']
);
}
return $this->is_connected();
}
/**
* Checks IMAP connection.
*
* @return boolean TRUE on success, FALSE on failure
*/
public function is_connected()
{
return $this->conn->connected();
}
/**
* Returns code of last error
*
* @return int Error code
*/
public function get_error_code()
{
return $this->conn->errornum;
}
/**
* Returns text of last error
*
* @return string Error string
*/
public function get_error_str()
{
return $this->conn->error;
}
/**
* Returns code of last command response
*
* @return int Response code
*/
public function get_response_code()
{
switch ($this->conn->resultcode) {
case 'NOPERM':
return self::NOPERM;
case 'READ-ONLY':
return self::READONLY;
case 'TRYCREATE':
return self::TRYCREATE;
case 'INUSE':
return self::INUSE;
case 'OVERQUOTA':
return self::OVERQUOTA;
case 'ALREADYEXISTS':
return self::ALREADYEXISTS;
case 'NONEXISTENT':
return self::NONEXISTENT;
case 'CONTACTADMIN':
return self::CONTACTADMIN;
default:
return self::UNKNOWN;
}
}
/**
* Activate/deactivate debug mode
*
* @param boolean $dbg True if IMAP conversation should be logged
*/
public function set_debug($dbg = true)
{
$this->options['debug'] = $dbg;
$this->conn->setDebug($dbg, array($this, 'debug_handler'));
}
/**
* Set internal folder reference.
* All operations will be performed on this folder.
*
* @param string $folder Folder name
*/
public function set_folder($folder)
{
$this->folder = $folder;
}
/**
* Save a search result for future message listing methods
*
* @param array $set Search set, result from rcube_imap::get_search_set():
* 0 - searching criteria, string
* 1 - search result, rcube_result_index|rcube_result_thread
* 2 - searching character set, string
* 3 - sorting field, string
* 4 - true if sorted, bool
*/
public function set_search_set($set)
{
$set = (array)$set;
$this->search_string = $set[0];
$this->search_set = $set[1];
$this->search_charset = $set[2];
$this->search_sort_field = $set[3];
$this->search_sorted = $set[4];
$this->search_threads = is_a($this->search_set, 'rcube_result_thread');
if (is_a($this->search_set, 'rcube_result_multifolder')) {
$this->set_threading(false);
}
}
/**
* Return the saved search set as hash array
*
* @return array Search set
*/
public function get_search_set()
{
if (empty($this->search_set)) {
return null;
}
return array(
$this->search_string,
$this->search_set,
$this->search_charset,
$this->search_sort_field,
$this->search_sorted,
);
}
/**
* Returns the IMAP server's capability.
*
* @param string $cap Capability name
*
* @return mixed Capability value or TRUE if supported, FALSE if not
*/
public function get_capability($cap)
{
$cap = strtoupper($cap);
$sess_key = "STORAGE_$cap";
if (!isset($_SESSION[$sess_key])) {
if (!$this->check_connection()) {
return false;
}
if ($cap == rcube_storage::DUAL_USE_FOLDERS) {
$_SESSION[$sess_key] = $this->detect_dual_use_folders();
}
else {
$_SESSION[$sess_key] = $this->conn->getCapability($cap);
}
}
return $_SESSION[$sess_key];
}
/**
* Checks the PERMANENTFLAGS capability of the current folder
* and returns true if the given flag is supported by the IMAP server
*
* @param string $flag Permanentflag name
*
* @return boolean True if this flag is supported
*/
public function check_permflag($flag)
{
$flag = strtoupper($flag);
$perm_flags = $this->get_permflags($this->folder);
$imap_flag = $this->conn->flags[$flag];
return $imap_flag && !empty($perm_flags) && in_array_nocase($imap_flag, $perm_flags);
}
/**
* Returns PERMANENTFLAGS of the specified folder
*
* @param string $folder Folder name
*
* @return array Flags
*/
public function get_permflags($folder)
{
if (!strlen($folder)) {
return array();
}
if (!$this->check_connection()) {
return array();
}
if ($this->conn->select($folder)) {
$permflags = $this->conn->data['PERMANENTFLAGS'];
}
else {
return array();
}
if (!is_array($permflags)) {
$permflags = array();
}
return $permflags;
}
/**
* Returns the delimiter that is used by the IMAP server for folder separation
*
* @return string Delimiter string
*/
public function get_hierarchy_delimiter()
{
return $this->delimiter;
}
/**
* Get namespace
*
* @param string $name Namespace array index: personal, other, shared, prefix
*
* @return array Namespace data
*/
public function get_namespace($name = null)
{
$ns = $this->namespace;
if ($name) {
// an alias for BC
if ($name == 'prefix') {
$name = 'prefix_in';
}
return isset($ns[$name]) ? $ns[$name] : null;
}
unset($ns['prefix_in'], $ns['prefix_out']);
return $ns;
}
/**
* Sets delimiter and namespaces
*/
protected function set_env()
{
if ($this->delimiter !== null && $this->namespace !== null) {
return;
}
$config = rcube::get_instance()->config;
$imap_personal = $config->get('imap_ns_personal');
$imap_other = $config->get('imap_ns_other');
$imap_shared = $config->get('imap_ns_shared');
$imap_delimiter = $config->get('imap_delimiter');
if (!$this->check_connection()) {
return;
}
$ns = $this->conn->getNamespace();
// Set namespaces (NAMESPACE supported)
if (is_array($ns)) {
$this->namespace = $ns;
}
else {
$this->namespace = array(
'personal' => null,
'other' => null,
'shared' => null,
);
}
if ($imap_delimiter) {
$this->delimiter = $imap_delimiter;
}
if (empty($this->delimiter)) {
$this->delimiter = $this->namespace['personal'][0][1];
}
if (empty($this->delimiter)) {
$this->delimiter = $this->conn->getHierarchyDelimiter();
}
if (empty($this->delimiter)) {
$this->delimiter = '/';
}
$this->list_root = null;
$this->list_excludes = array();
// Overwrite namespaces
if ($imap_personal !== null) {
$this->namespace['personal'] = null;
foreach ((array)$imap_personal as $dir) {
$this->namespace['personal'][] = array($dir, $this->delimiter);
}
}
if ($imap_other === false) {
foreach ((array) $this->namespace['other'] as $dir) {
if (is_array($dir) && $dir[0]) {
$this->list_excludes[] = $dir[0];
}
}
$this->namespace['other'] = null;
}
else if ($imap_other !== null) {
$this->namespace['other'] = null;
foreach ((array)$imap_other as $dir) {
if ($dir) {
$this->namespace['other'][] = array($dir, $this->delimiter);
}
}
}
if ($imap_shared === false) {
foreach ((array) $this->namespace['shared'] as $dir) {
if (is_array($dir) && $dir[0]) {
$this->list_excludes[] = $dir[0];
}
}
$this->namespace['shared'] = null;
}
else if ($imap_shared !== null) {
$this->namespace['shared'] = null;
foreach ((array)$imap_shared as $dir) {
if ($dir) {
$this->namespace['shared'][] = array($dir, $this->delimiter);
}
}
}
// Performance optimization for case where we have no shared/other namespace
// and personal namespace has one prefix (#5073)
// In such a case we can tell the server to return only content of the
// specified folder in LIST/LSUB, no post-filtering
if (empty($this->namespace['other']) && empty($this->namespace['shared'])
&& !empty($this->namespace['personal']) && count($this->namespace['personal']) === 1
&& strlen($this->namespace['personal'][0][0]) > 1
) {
$this->list_root = $this->namespace['personal'][0][0];
$this->list_excludes = array();
}
// Find personal namespace prefix(es) for self::mod_folder()
if (is_array($this->namespace['personal']) && !empty($this->namespace['personal'])) {
// There can be more than one namespace root,
// - for prefix_out get the first one but only
// if there is only one root
// - for prefix_in get the first one but only
// if there is no non-prefixed namespace root (#5403)
$roots = array();
foreach ($this->namespace['personal'] as $ns) {
$roots[] = $ns[0];
}
if (!in_array('', $roots)) {
$this->namespace['prefix_in'] = $roots[0];
}
if (count($roots) == 1) {
$this->namespace['prefix_out'] = $roots[0];
}
}
$_SESSION['imap_namespace'] = $this->namespace;
$_SESSION['imap_delimiter'] = $this->delimiter;
$_SESSION['imap_list_conf'] = array($this->list_root, $this->list_excludes);
}
/**
* Returns IMAP server vendor name
*
* @return string Vendor name
* @since 1.2
*/
public function get_vendor()
{
if ($_SESSION['imap_vendor'] !== null) {
return $_SESSION['imap_vendor'];
}
$config = rcube::get_instance()->config;
$imap_vendor = $config->get('imap_vendor');
if ($imap_vendor) {
return $imap_vendor;
}
if (!$this->check_connection()) {
return;
}
if (($ident = $this->conn->data['ID']) === null) {
$ident = $this->conn->id(array(
'name' => 'Roundcube',
'version' => RCUBE_VERSION,
'php' => PHP_VERSION,
'os' => PHP_OS,
));
}
$vendor = (string) (!empty($ident) ? $ident['name'] : '');
$ident = strtolower($vendor . ' ' . $this->conn->data['GREETING']);
$vendors = array('cyrus', 'dovecot', 'uw-imap', 'gmail', 'hmail');
foreach ($vendors as $v) {
if (strpos($ident, $v) !== false) {
$vendor = $v;
break;
}
}
return $_SESSION['imap_vendor'] = $vendor;
}
/**
* Get message count for a specific folder
*
* @param string $folder Folder name
* @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT|EXISTS]
* @param boolean $force Force reading from server and update cache
* @param boolean $status Enables storing folder status info (max UID/count),
* required for folder_status()
*
* @return int Number of messages
*/
public function count($folder='', $mode='ALL', $force=false, $status=true)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
return $this->countmessages($folder, $mode, $force, $status);
}
/**
* Protected method for getting number of messages
*
* @param string $folder Folder name
* @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT|EXISTS]
* @param boolean $force Force reading from server and update cache
* @param boolean $status Enables storing folder status info (max UID/count),
* required for folder_status()
* @param boolean $no_search Ignore current search result
*
* @return int Number of messages
* @see rcube_imap::count()
*/
protected function countmessages($folder, $mode = 'ALL', $force = false, $status = true, $no_search = false)
{
$mode = strtoupper($mode);
// Count search set, assume search set is always up-to-date (don't check $force flag)
// @TODO: this could be handled in more reliable way, e.g. a separate method
// maybe in rcube_imap_search
if (!$no_search && $this->search_string && $folder == $this->folder) {
if ($mode == 'ALL') {
return $this->search_set->count_messages();
}
else if ($mode == 'THREADS') {
return $this->search_set->count();
}
}
// EXISTS is a special alias for ALL, it allows to get the number
// of all messages in a folder also when search is active and with
// any skip_deleted setting
$a_folder_cache = $this->get_cache('messagecount');
// return cached value
if (!$force && is_array($a_folder_cache[$folder]) && isset($a_folder_cache[$folder][$mode])) {
return $a_folder_cache[$folder][$mode];
}
if (!is_array($a_folder_cache[$folder])) {
$a_folder_cache[$folder] = array();
}
if ($mode == 'THREADS') {
$res = $this->threads($folder);
$count = $res->count();
if ($status) {
$msg_count = $res->count_messages();
$this->set_folder_stats($folder, 'cnt', $msg_count);
$this->set_folder_stats($folder, 'maxuid', $msg_count ? $this->id2uid($msg_count, $folder) : 0);
}
}
// Need connection here
else if (!$this->check_connection()) {
return 0;
}
// RECENT count is fetched a bit different
else if ($mode == 'RECENT') {
$count = $this->conn->countRecent($folder);
}
// use SEARCH for message counting
else if ($mode != 'EXISTS' && !empty($this->options['skip_deleted'])) {
$search_str = "ALL UNDELETED";
$keys = array('COUNT');
if ($mode == 'UNSEEN') {
$search_str .= " UNSEEN";
}
else {
if ($this->messages_caching) {
$keys[] = 'ALL';
}
if ($status) {
$keys[] = 'MAX';
}
}
// @TODO: if $mode == 'ALL' we could try to use cache index here
// get message count using (E)SEARCH
// not very performant but more precise (using UNDELETED)
$index = $this->conn->search($folder, $search_str, true, $keys);
$count = $index->count();
if ($mode == 'ALL') {
// Cache index data, will be used in index_direct()
$this->icache['undeleted_idx'] = $index;
if ($status) {
$this->set_folder_stats($folder, 'cnt', $count);
$this->set_folder_stats($folder, 'maxuid', $index->max());
}
}
}
else {
if ($mode == 'UNSEEN') {
$count = $this->conn->countUnseen($folder);
}
else {
$count = $this->conn->countMessages($folder);
if ($status && $mode == 'ALL') {
$this->set_folder_stats($folder, 'cnt', $count);
$this->set_folder_stats($folder, 'maxuid', $count ? $this->id2uid($count, $folder) : 0);
}
}
}
$a_folder_cache[$folder][$mode] = (int)$count;
// write back to cache
$this->update_cache('messagecount', $a_folder_cache);
return (int)$count;
}
/**
* Public method for listing message flags
*
* @param string $folder Folder name
* @param array $uids Message UIDs
* @param int $mod_seq Optional MODSEQ value (of last flag update)
*
* @return array Indexed array with message flags
*/
public function list_flags($folder, $uids, $mod_seq = null)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
if (!$this->check_connection()) {
return array();
}
// @TODO: when cache was synchronized in this request
// we might already have asked for flag updates, use it.
$flags = $this->conn->fetch($folder, $uids, true, array('FLAGS'), $mod_seq);
$result = array();
if (!empty($flags)) {
foreach ($flags as $message) {
$result[$message->uid] = $message->flags;
}
}
return $result;
}
/**
* Public method for listing headers
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param string $sort_field Header field to sort by
* @param string $sort_order Sort order [ASC|DESC]
* @param int $slice Number of slice items to extract from result array
*
* @return array Indexed array with message header objects
*/
public function list_messages($folder='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
return $this->_list_messages($folder, $page, $sort_field, $sort_order, $slice);
}
/**
* protected method for listing message headers
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param string $sort_field Header field to sort by
* @param string $sort_order Sort order [ASC|DESC]
* @param int $slice Number of slice items to extract from result array
*
* @return array Indexed array with message header objects
* @see rcube_imap::list_messages
*/
protected function _list_messages($folder='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
{
if (!strlen($folder)) {
return array();
}
$this->set_sort_order($sort_field, $sort_order);
$page = $page ? $page : $this->list_page;
// use saved message set
if ($this->search_string) {
return $this->list_search_messages($folder, $page, $slice);
}
if ($this->threading) {
return $this->list_thread_messages($folder, $page, $slice);
}
// get UIDs of all messages in the folder, sorted
$index = $this->index($folder, $this->sort_field, $this->sort_order);
if ($index->is_empty()) {
return array();
}
$from = ($page-1) * $this->page_size;
$to = $from + $this->page_size;
$index->slice($from, $to - $from);
if ($slice) {
$index->slice(-$slice, $slice);
}
// fetch reqested messages headers
$a_index = $index->get();
$a_msg_headers = $this->fetch_headers($folder, $a_index);
return array_values($a_msg_headers);
}
/**
* protected method for listing message headers using threads
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param int $slice Number of slice items to extract from result array
*
* @return array Indexed array with message header objects
* @see rcube_imap::list_messages
*/
protected function list_thread_messages($folder, $page, $slice=0)
{
// get all threads (not sorted)
if ($mcache = $this->get_mcache_engine()) {
$threads = $mcache->get_thread($folder);
}
else {
$threads = $this->threads($folder);
}
return $this->fetch_thread_headers($folder, $threads, $page, $slice);
}
/**
* Method for fetching threads data
*
* @param string $folder Folder name
*
* @return rcube_imap_thread Thread data object
*/
function threads($folder)
{
if ($mcache = $this->get_mcache_engine()) {
// don't store in self's internal cache, cache has it's own internal cache
return $mcache->get_thread($folder);
}
if (!empty($this->icache['threads'])) {
if ($this->icache['threads']->get_parameters('MAILBOX') == $folder) {
return $this->icache['threads'];
}
}
// get all threads
$result = $this->threads_direct($folder);
// add to internal (fast) cache
return $this->icache['threads'] = $result;
}
/**
* Method for direct fetching of threads data
*
* @param string $folder Folder name
*
* @return rcube_imap_thread Thread data object
*/
function threads_direct($folder)
{
if (!$this->check_connection()) {
return new rcube_result_thread();
}
// get all threads
return $this->conn->thread($folder, $this->threading,
$this->options['skip_deleted'] ? 'UNDELETED' : '', true);
}
/**
* protected method for fetching threaded messages headers
*
* @param string $folder Folder name
* @param rcube_result_thread $threads Threads data object
* @param int $page List page number
* @param int $slice Number of threads to slice
*
* @return array Messages headers
*/
protected function fetch_thread_headers($folder, $threads, $page, $slice=0)
{
// Sort thread structure
$this->sort_threads($threads);
$from = ($page-1) * $this->page_size;
$to = $from + $this->page_size;
$threads->slice($from, $to - $from);
if ($slice) {
$threads->slice(-$slice, $slice);
}
// Get UIDs of all messages in all threads
$a_index = $threads->get();
// fetch reqested headers from server
$a_msg_headers = $this->fetch_headers($folder, $a_index);
unset($a_index);
// Set depth, has_children and unread_children fields in headers
$this->set_thread_flags($a_msg_headers, $threads);
return array_values($a_msg_headers);
}
/**
* protected method for setting threaded messages flags:
* depth, has_children, unread_children, flagged_children
*
* @param array $headers Reference to headers array indexed by message UID
* @param rcube_result_thread $threads Threads data object
*
* @return array Message headers array indexed by message UID
*/
protected function set_thread_flags(&$headers, $threads)
{
$parents = array();
list ($msg_depth, $msg_children) = $threads->get_thread_data();
foreach ($headers as $uid => $header) {
$depth = $msg_depth[$uid];
$parents = array_slice($parents, 0, $depth);
if (!empty($parents)) {
$headers[$uid]->parent_uid = end($parents);
if (empty($header->flags['SEEN'])) {
$headers[$parents[0]]->unread_children++;
}
if (!empty($header->flags['FLAGGED'])) {
$headers[$parents[0]]->flagged_children++;
}
}
array_push($parents, $uid);
$headers[$uid]->depth = $depth;
$headers[$uid]->has_children = $msg_children[$uid];
}
}
/**
* protected method for listing a set of message headers (search results)
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param int $slice Number of slice items to extract from result array
*
* @return array Indexed array with message header objects
*/
protected function list_search_messages($folder, $page, $slice=0)
{
if (!strlen($folder) || empty($this->search_set) || $this->search_set->is_empty()) {
return array();
}
// gather messages from a multi-folder search
if ($this->search_set->multi) {
$page_size = $this->page_size;
$sort_field = $this->sort_field;
$search_set = $this->search_set;
// prepare paging
$cnt = $search_set->count();
$from = ($page-1) * $page_size;
$to = $from + $page_size;
$slice_length = min($page_size, $cnt - $from);
// fetch resultset headers, sort and slice them
if (!empty($sort_field) && $search_set->get_parameters('SORT') != $sort_field) {
$this->sort_field = null;
$this->page_size = 1000; // fetch up to 1000 matching messages per folder
$this->threading = false;
$a_msg_headers = array();
foreach ($search_set->sets as $resultset) {
if (!$resultset->is_empty()) {
$this->search_set = $resultset;
$this->search_threads = $resultset instanceof rcube_result_thread;
$a_headers = $this->list_search_messages($resultset->get_parameters('MAILBOX'), 1);
$a_msg_headers = array_merge($a_msg_headers, $a_headers);
unset($a_headers);
}
}
// sort headers
if (!empty($a_msg_headers)) {
$a_msg_headers = rcube_imap_generic::sortHeaders($a_msg_headers, $sort_field, $this->sort_order);
}
// store (sorted) message index
$search_set->set_message_index($a_msg_headers, $sort_field, $this->sort_order);
// only return the requested part of the set
$a_msg_headers = array_slice(array_values($a_msg_headers), $from, $slice_length);
}
else {
if ($this->sort_order != $search_set->get_parameters('ORDER')) {
$search_set->revert();
}
// slice resultset first...
$fetch = array();
foreach (array_slice($search_set->get(), $from, $slice_length) as $msg_id) {
list($uid, $folder) = explode('-', $msg_id, 2);
$fetch[$folder][] = $uid;
}
// ... and fetch the requested set of headers
$a_msg_headers = array();
foreach ($fetch as $folder => $a_index) {
$a_msg_headers = array_merge($a_msg_headers, array_values($this->fetch_headers($folder, $a_index)));
}
}
if ($slice) {
$a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
}
// restore members
$this->sort_field = $sort_field;
$this->page_size = $page_size;
$this->search_set = $search_set;
return $a_msg_headers;
}
// use saved messages from searching
if ($this->threading) {
return $this->list_search_thread_messages($folder, $page, $slice);
}
// search set is threaded, we need a new one
if ($this->search_threads) {
$this->search('', $this->search_string, $this->search_charset, $this->sort_field);
}
$index = clone $this->search_set;
$from = ($page-1) * $this->page_size;
$to = $from + $this->page_size;
// return empty array if no messages found
if ($index->is_empty()) {
return array();
}
// quickest method (default sorting)
if (!$this->search_sort_field && !$this->sort_field) {
$got_index = true;
}
// sorted messages, so we can first slice array and then fetch only wanted headers
else if ($this->search_sorted) { // SORT searching result
$got_index = true;
// reset search set if sorting field has been changed
if ($this->sort_field && $this->search_sort_field != $this->sort_field) {
$this->search('', $this->search_string, $this->search_charset, $this->sort_field);
$index = clone $this->search_set;
// return empty array if no messages found
if ($index->is_empty()) {
return array();
}
}
}
if ($got_index) {
if ($this->sort_order != $index->get_parameters('ORDER')) {
$index->revert();
}
// get messages uids for one page
$index->slice($from, $to-$from);
if ($slice) {
$index->slice(-$slice, $slice);
}
// fetch headers
$a_index = $index->get();
$a_msg_headers = $this->fetch_headers($folder, $a_index);
return array_values($a_msg_headers);
}
// SEARCH result, need sorting
$cnt = $index->count();
// 300: experimantal value for best result
if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) {
// use memory less expensive (and quick) method for big result set
$index = clone $this->index('', $this->sort_field, $this->sort_order);
// get messages uids for one page...
$index->slice($from, min($cnt-$from, $this->page_size));
if ($slice) {
$index->slice(-$slice, $slice);
}
// ...and fetch headers
$a_index = $index->get();
$a_msg_headers = $this->fetch_headers($folder, $a_index);
return array_values($a_msg_headers);
}
else {
// for small result set we can fetch all messages headers
$a_index = $index->get();
$a_msg_headers = $this->fetch_headers($folder, $a_index, false);
// 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 = rcube_imap_generic::sortHeaders(
$a_msg_headers, $this->sort_field, $this->sort_order);
// only return the requested part of the set
$slice_length = min($this->page_size, $cnt - ($to > $cnt ? $from : $to));
$a_msg_headers = array_slice(array_values($a_msg_headers), $from, $slice_length);
if ($slice) {
$a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
}
return $a_msg_headers;
}
}
/**
* protected method for listing a set of threaded message headers (search results)
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param int $slice Number of slice items to extract from result array
*
* @return array Indexed array with message header objects
* @see rcube_imap::list_search_messages()
*/
protected function list_search_thread_messages($folder, $page, $slice=0)
{
// update search_set if previous data was fetched with disabled threading
if (!$this->search_threads) {
if ($this->search_set->is_empty()) {
return array();
}
$this->search('', $this->search_string, $this->search_charset, $this->sort_field);
}
return $this->fetch_thread_headers($folder, clone $this->search_set, $page, $slice);
}
/**
* Fetches messages headers (by UID)
*
* @param string $folder Folder name
* @param array $msgs Message UIDs
* @param bool $sort Enables result sorting by $msgs
* @param bool $force Disables cache use
*
* @return array Messages headers indexed by UID
*/
function fetch_headers($folder, $msgs, $sort = true, $force = false)
{
if (empty($msgs)) {
return array();
}
if (!$force && ($mcache = $this->get_mcache_engine())) {
$headers = $mcache->get_messages($folder, $msgs);
}
else if (!$this->check_connection()) {
return array();
}
else {
// fetch reqested headers from server
$headers = $this->conn->fetchHeaders(
$folder, $msgs, true, false, $this->get_fetch_headers());
}
if (empty($headers)) {
return array();
}
foreach ($headers as $h) {
$h->folder = $folder;
$a_msg_headers[$h->uid] = $h;
}
if ($sort) {
// use this class for message sorting
$sorter = new rcube_message_header_sorter();
$sorter->set_index($msgs);
$sorter->sort_headers($a_msg_headers);
}
return $a_msg_headers;
}
/**
* Returns current status of a folder (compared to the last time use)
*
* We compare the maximum UID to determine the number of
* new messages because the RECENT flag is not reliable.
*
* @param string $folder Folder name
* @param array $diff Difference data
*
* @return int Folder status
*/
public function folder_status($folder = null, &$diff = array())
{
if (!strlen($folder)) {
$folder = $this->folder;
}
$old = $this->get_folder_stats($folder);
// refresh message count -> will update
$this->countmessages($folder, 'ALL', true, true, true);
$result = 0;
if (empty($old)) {
return $result;
}
$new = $this->get_folder_stats($folder);
// got new messages
if ($new['maxuid'] > $old['maxuid']) {
$result += 1;
// get new message UIDs range, that can be used for example
// to get the data of these messages
$diff['new'] = ($old['maxuid'] + 1 < $new['maxuid'] ? ($old['maxuid']+1).':' : '') . $new['maxuid'];
}
// some messages has been deleted
if ($new['cnt'] < $old['cnt']) {
$result += 2;
}
// @TODO: optional checking for messages flags changes (?)
// @TODO: UIDVALIDITY checking
return $result;
}
/**
* Stores folder statistic data in session
* @TODO: move to separate DB table (cache?)
*
* @param string $folder Folder name
* @param string $name Data name
* @param mixed $data Data value
*/
protected function set_folder_stats($folder, $name, $data)
{
$_SESSION['folders'][$folder][$name] = $data;
}
/**
* Gets folder statistic data
*
* @param string $folder Folder name
*
* @return array Stats data
*/
protected function get_folder_stats($folder)
{
if ($_SESSION['folders'][$folder]) {
return (array) $_SESSION['folders'][$folder];
}
return array();
}
/**
* Return sorted list of message UIDs
*
* @param string $folder Folder to get index from
* @param string $sort_field Sort column
* @param string $sort_order Sort order [ASC, DESC]
* @param bool $no_threads Get not threaded index
* @param bool $no_search Get index not limited to search result (optionally)
*
* @return rcube_result_index|rcube_result_thread List of messages (UIDs)
*/
public function index($folder = '', $sort_field = NULL, $sort_order = NULL,
$no_threads = false, $no_search = false
) {
if (!$no_threads && $this->threading) {
return $this->thread_index($folder, $sort_field, $sort_order);
}
$this->set_sort_order($sort_field, $sort_order);
if (!strlen($folder)) {
$folder = $this->folder;
}
// we have a saved search result, get index from there
if ($this->search_string) {
if ($this->search_set->is_empty()) {
return new rcube_result_index($folder, '* SORT');
}
if ($this->search_set instanceof rcube_result_multifolder) {
$index = $this->search_set;
$index->folder = $folder;
// TODO: handle changed sorting
}
// search result is an index with the same sorting?
else if (($this->search_set instanceof rcube_result_index)
&& ((!$this->sort_field && !$this->search_sorted) ||
($this->search_sorted && $this->search_sort_field == $this->sort_field))
) {
$index = $this->search_set;
}
// $no_search is enabled when we are not interested in
// fetching index for search result, e.g. to sort
// threaded search result we can use full mailbox index.
// This makes possible to use index from cache
else if (!$no_search) {
if (!$this->sort_field) {
// No sorting needed, just build index from the search result
// @TODO: do we need to sort by UID here?
$search = $this->search_set->get_compressed();
$index = new rcube_result_index($folder, '* ESEARCH ALL ' . $search);
}
else {
$index = $this->index_direct($folder, $this->sort_field, $this->sort_order, $this->search_set);
}
}
if (isset($index)) {
if ($this->sort_order != $index->get_parameters('ORDER')) {
$index->revert();
}
return $index;
}
}
// check local cache
if ($mcache = $this->get_mcache_engine()) {
return $mcache->get_index($folder, $this->sort_field, $this->sort_order);
}
// fetch from IMAP server
return $this->index_direct($folder, $this->sort_field, $this->sort_order);
}
/**
* Return sorted list of message UIDs ignoring current search settings.
* Doesn't uses cache by default.
*
* @param string $folder Folder to get index from
* @param string $sort_field Sort column
* @param string $sort_order Sort order [ASC, DESC]
* @param rcube_result_* $search Optional messages set to limit the result
*
* @return rcube_result_index Sorted list of message UIDs
*/
public function index_direct($folder, $sort_field = null, $sort_order = null, $search = null)
{
if (!empty($search)) {
$search = $search->get_compressed();
}
// use message index sort as default sorting
if (!$sort_field) {
// use search result from count() if possible
if (empty($search) && $this->options['skip_deleted']
&& !empty($this->icache['undeleted_idx'])
&& $this->icache['undeleted_idx']->get_parameters('ALL') !== null
&& $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder
) {
$index = $this->icache['undeleted_idx'];
}
else if (!$this->check_connection()) {
return new rcube_result_index();
}
else {
$query = $this->options['skip_deleted'] ? 'UNDELETED' : '';
if ($search) {
$query = trim($query . ' UID ' . $search);
}
$index = $this->conn->search($folder, $query, true);
}
}
else if (!$this->check_connection()) {
return new rcube_result_index();
}
// fetch complete message index
else {
if ($this->get_capability('SORT')) {
$query = $this->options['skip_deleted'] ? 'UNDELETED' : '';
if ($search) {
$query = trim($query . ' UID ' . $search);
}
$index = $this->conn->sort($folder, $sort_field, $query, true);
}
if (empty($index) || $index->is_error()) {
$index = $this->conn->index($folder, $search ? $search : "1:*",
$sort_field, $this->options['skip_deleted'],
$search ? true : false, true);
}
}
if ($sort_order != $index->get_parameters('ORDER')) {
$index->revert();
}
return $index;
}
/**
* Return index of threaded message UIDs
*
* @param string $folder Folder to get index from
* @param string $sort_field Sort column
* @param string $sort_order Sort order [ASC, DESC]
*
* @return rcube_result_thread Message UIDs
*/
public function thread_index($folder='', $sort_field=NULL, $sort_order=NULL)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
// we have a saved search result, get index from there
if ($this->search_string && $this->search_threads && $folder == $this->folder) {
$threads = $this->search_set;
}
else {
// get all threads (default sort order)
$threads = $this->threads($folder);
}
$this->set_sort_order($sort_field, $sort_order);
$this->sort_threads($threads);
return $threads;
}
/**
* Sort threaded result, using THREAD=REFS method if available.
* If not, use any method and re-sort the result in THREAD=REFS way.
*
* @param rcube_result_thread $threads Threads result set
*/
protected function sort_threads($threads)
{
if ($threads->is_empty()) {
return;
}
// THREAD=ORDEREDSUBJECT: sorting by sent date of root message
// THREAD=REFERENCES: sorting by sent date of root message
// THREAD=REFS: sorting by the most recent date in each thread
if ($this->threading != 'REFS' || ($this->sort_field && $this->sort_field != 'date')) {
$sortby = $this->sort_field ? $this->sort_field : 'date';
$index = $this->index($this->folder, $sortby, $this->sort_order, true, true);
if (!$index->is_empty()) {
$threads->sort($index);
}
}
else if ($this->sort_order != $threads->get_parameters('ORDER')) {
$threads->revert();
}
}
/**
* Invoke search request to IMAP server
*
* @param string $folder Folder name to search in
* @param string $search Search criteria
* @param string $charset Search charset
* @param string $sort_field Header field to sort by
*
* @return rcube_result_index Search result object
* @todo: Search criteria should be provided in non-IMAP format, eg. array
*/
public function search($folder = '', $search = 'ALL', $charset = null, $sort_field = null)
{
if (!$search) {
$search = 'ALL';
}
if ((is_array($folder) && empty($folder)) || (!is_array($folder) && !strlen($folder))) {
$folder = $this->folder;
}
$plugin = $this->plugins->exec_hook('imap_search_before', array(
'folder' => $folder,
'search' => $search,
'charset' => $charset,
'sort_field' => $sort_field,
'threading' => $this->threading,
));
$folder = $plugin['folder'];
$search = $plugin['search'];
$charset = $plugin['charset'];
$sort_field = $plugin['sort_field'];
$results = $plugin['result'];
// multi-folder search
if (!$results && is_array($folder) && count($folder) > 1 && $search != 'ALL') {
// connect IMAP to have all the required classes and settings loaded
$this->check_connection();
// disable threading
$this->threading = false;
$searcher = new rcube_imap_search($this->options, $this->conn);
// set limit to not exceed the client's request timeout
$searcher->set_timelimit(60);
// continue existing incomplete search
if (!empty($this->search_set) && $this->search_set->incomplete && $search == $this->search_string) {
$searcher->set_results($this->search_set);
}
// execute the search
$results = $searcher->exec(
$folder,
$search,
$charset ? $charset : $this->default_charset,
$sort_field && $this->get_capability('SORT') ? $sort_field : null,
$this->threading
);
}
else if (!$results) {
$folder = is_array($folder) ? $folder[0] : $folder;
$search = is_array($search) ? $search[$folder] : $search;
$results = $this->search_index($folder, $search, $charset, $sort_field);
}
$sorted = $this->threading || $this->search_sorted || $plugin['search_sorted'] ? true : false;
$this->set_search_set(array($search, $results, $charset, $sort_field, $sorted));
return $results;
}
/**
* Direct (real and simple) SEARCH request (without result sorting and caching).
*
* @param string $mailbox Mailbox name to search in
* @param string $str Search string
*
* @return rcube_result_index Search result (UIDs)
*/
public function search_once($folder = null, $str = 'ALL')
{
if (!$this->check_connection()) {
return new rcube_result_index();
}
if (!$str) {
$str = 'ALL';
}
// multi-folder search
if (is_array($folder) && count($folder) > 1) {
$searcher = new rcube_imap_search($this->options, $this->conn);
$index = $searcher->exec($folder, $str, $this->default_charset);
}
else {
$folder = is_array($folder) ? $folder[0] : $folder;
if (!strlen($folder)) {
$folder = $this->folder;
}
$index = $this->conn->search($folder, $str, true);
}
return $index;
}
/**
* protected search method
*
* @param string $folder Folder name
* @param string $criteria Search criteria
* @param string $charset Charset
* @param string $sort_field Sorting field
*
* @return rcube_result_index|rcube_result_thread Search results (UIDs)
* @see rcube_imap::search()
*/
protected function search_index($folder, $criteria='ALL', $charset=NULL, $sort_field=NULL)
{
if (!$this->check_connection()) {
if ($this->threading) {
return new rcube_result_thread();
}
else {
return new rcube_result_index();
}
}
if ($this->options['skip_deleted'] && !preg_match('/UNDELETED/', $criteria)) {
$criteria = 'UNDELETED '.$criteria;
}
// unset CHARSET if criteria string is ASCII, this way
// SEARCH won't be re-sent after "unsupported charset" response
if ($charset && $charset != 'US-ASCII' && is_ascii($criteria)) {
$charset = 'US-ASCII';
}
if ($this->threading) {
$threads = $this->conn->thread($folder, $this->threading, $criteria, true, $charset);
// Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
// but I've seen that Courier doesn't support UTF-8)
if ($threads->is_error() && $charset && $charset != 'US-ASCII') {
$threads = $this->conn->thread($folder, $this->threading,
self::convert_criteria($criteria, $charset), true, 'US-ASCII');
}
return $threads;
}
if ($sort_field && $this->get_capability('SORT')) {
$charset = $charset ? $charset : $this->default_charset;
$messages = $this->conn->sort($folder, $sort_field, $criteria, true, $charset);
// Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
// but I've seen Courier with disabled UTF-8 support)
if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
$messages = $this->conn->sort($folder, $sort_field,
self::convert_criteria($criteria, $charset), true, 'US-ASCII');
}
if (!$messages->is_error()) {
$this->search_sorted = true;
return $messages;
}
}
$messages = $this->conn->search($folder,
($charset && $charset != 'US-ASCII' ? "CHARSET $charset " : '') . $criteria, true);
// Error, try with US-ASCII (some servers may support only US-ASCII)
if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
$messages = $this->conn->search($folder,
self::convert_criteria($criteria, $charset), true);
}
$this->search_sorted = false;
return $messages;
}
/**
* Converts charset of search criteria string
*
* @param string $str Search string
* @param string $charset Original charset
* @param string $dest_charset Destination charset (default US-ASCII)
*
* @return string Search string
*/
public static function convert_criteria($str, $charset, $dest_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, $dest_charset);
if ($string === false || !strlen($string)) {
continue;
}
$res .= substr($str, $last, $m[1] - $last - 1) . rcube_imap_generic::escape($string);
$last = $m[0] + $string_offset - 1;
}
if ($last < strlen($str)) {
$res .= substr($str, $last, strlen($str)-$last);
}
}
// strings for conversion not found
else {
$res = $str;
}
return $res;
}
/**
* Refresh saved search set
*
* @return array Current search set
*/
public function refresh_search()
{
if (!empty($this->search_string)) {
$this->search(
is_object($this->search_set) ? $this->search_set->get_parameters('MAILBOX') : '',
$this->search_string,
$this->search_charset,
$this->search_sort_field
);
}
return $this->get_search_set();
}
/**
* Flag certain result subsets as 'incomplete'.
* For subsequent refresh_search() calls to only refresh the updated parts.
*/
protected function set_search_dirty($folder)
{
if ($this->search_set && is_a($this->search_set, 'rcube_result_multifolder')) {
if ($subset = $this->search_set->get_set($folder)) {
$subset->incomplete = $this->search_set->incomplete = true;
}
}
}
/**
* Return message headers object of a specific message
*
* @param int $id Message UID
* @param string $folder Folder to read from
* @param bool $force True to skip cache
*
* @return rcube_message_header Message headers
*/
public function get_message_headers($uid, $folder = null, $force = false)
{
// decode combined UID-folder identifier
if (preg_match('/^\d+-.+/', $uid)) {
list($uid, $folder) = explode('-', $uid, 2);
}
if (!strlen($folder)) {
$folder = $this->folder;
}
// get cached headers
if (!$force && $uid && ($mcache = $this->get_mcache_engine())) {
$headers = $mcache->get_message($folder, $uid);
}
else if (!$this->check_connection()) {
$headers = false;
}
else {
$headers = $this->conn->fetchHeader(
$folder, $uid, true, true, $this->get_fetch_headers());
if (is_object($headers))
$headers->folder = $folder;
}
return $headers;
}
/**
* Fetch message headers and body structure from the IMAP server and build
* an object structure.
*
* @param int $uid Message UID to fetch
* @param string $folder Folder to read from
*
* @return object rcube_message_header Message data
*/
public function get_message($uid, $folder = null)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
// decode combined UID-folder identifier
if (preg_match('/^\d+-.+/', $uid)) {
list($uid, $folder) = explode('-', $uid, 2);
}
// Check internal cache
if (!empty($this->icache['message'])) {
if (($headers = $this->icache['message']) && $headers->uid == $uid) {
return $headers;
}
}
$headers = $this->get_message_headers($uid, $folder);
// message doesn't exist?
if (empty($headers)) {
return null;
}
// structure might be cached
if (!empty($headers->structure)) {
return $headers;
}
$this->msg_uid = $uid;
if (!$this->check_connection()) {
return $headers;
}
if (empty($headers->bodystructure)) {
$headers->bodystructure = $this->conn->getStructure($folder, $uid, true);
}
$structure = $headers->bodystructure;
if (empty($structure)) {
return $headers;
}
// set message charset from message headers
if ($headers->charset) {
$this->struct_charset = $headers->charset;
}
else {
$this->struct_charset = $this->structure_charset($structure);
}
$headers->ctype = @strtolower($headers->ctype);
// 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" NIL NIL NIL "7bit" 2154 70 NIL NIL NIL)
if ($headers->ctype && !is_array($structure[0]) && $headers->ctype != 'text/plain'
&& strtolower($structure[0].'/'.$structure[1]) == 'text/plain'
) {
// A special known case "Content-type: text" (#1488968)
if ($headers->ctype == 'text') {
$structure[1] = 'plain';
$headers->ctype = 'text/plain';
}
// we can handle single-part messages, by simple fix in structure (#1486898)
else if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) {
$structure[0] = $m[1];
$structure[1] = $m[2];
}
else {
// Try to parse the message using rcube_mime_decode.
// We need a better solution, it parses message
// in memory, which wouldn't work for very big messages,
// (it uses up to 10x more memory than the message size)
// it's also buggy and not actively developed
if ($headers->size && rcube_utils::mem_check($headers->size * 10)) {
$raw_msg = $this->get_raw_body($uid);
$struct = rcube_mime::parse_message($raw_msg);
}
else {
return $headers;
}
}
}
if (empty($struct)) {
$struct = $this->structure_part($structure, 0, '', $headers);
}
// some workarounds on simple messages...
if (empty($struct->parts)) {
// ...don't trust given content-type
if (!empty($headers->ctype)) {
$struct->mime_id = '1';
$struct->mimetype = strtolower($headers->ctype);
list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype);
}
// ...and charset (there's a case described in #1488968 where invalid content-type
// results in invalid charset in BODYSTRUCTURE)
if (!empty($headers->charset) && $headers->charset != $struct->ctype_parameters['charset']) {
$struct->charset = $headers->charset;
$struct->ctype_parameters['charset'] = $headers->charset;
}
}
$headers->structure = $struct;
return $this->icache['message'] = $headers;
}
/**
* Build message part object
*
* @param array $part
* @param int $count
* @param string $parent
*/
protected function structure_part($part, $count = 0, $parent = '', $mime_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';
/* RFC3501: BODYSTRUCTURE fields of multipart part
part1 array
part2 array
part3 array
....
1. subtype
2. parameters (optional)
3. description (optional)
4. language (optional)
5. location (optional)
*/
// find first non-array entry
for ($i=1; $i<count($part); $i++) {
if (!is_array($part[$i])) {
$struct->ctype_secondary = strtolower($part[$i]);
// read content type parameters
if (is_array($part[$i+1])) {
$struct->ctype_parameters = array();
for ($j=0; $j<count($part[$i+1]); $j+=2) {
$param = strtolower($part[$i+1][$j]);
$struct->ctype_parameters[$param] = $part[$i+1][$j+1];
}
}
break;
}
}
$struct->mimetype = 'multipart/'.$struct->ctype_secondary;
// build parts list for headers pre-fetching
for ($i=0; $i<count($part); $i++) {
if (!is_array($part[$i])) {
break;
}
// 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') {
$mime_part_headers[] = $tmp_part_id;
}
else if (in_array('name', (array)$part[$i][2]) && empty($part[$i][3])) {
$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 = $this->conn->fetchMIMEHeaders($this->folder,
$this->msg_uid, $mime_part_headers);
}
$struct->parts = array();
for ($i=0, $count=0; $i<count($part); $i++) {
if (!is_array($part[$i])) {
break;
}
$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]);
}
return $struct;
}
/* RFC3501: BODYSTRUCTURE fields of non-multipart part
0. type
1. subtype
2. parameters
3. id
4. description
5. encoding
6. size
-- text
7. lines
-- message/rfc822
7. envelope structure
8. body structure
9. lines
--
x. md5 (optional)
x. disposition (optional)
x. language (optional)
x. location (optional)
*/
// 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'];
}
}
// #1487700: workaround for lack of charset in malformed structure
if (empty($struct->charset) && !empty($mime_headers) && $mime_headers->charset) {
$struct->charset = $mime_headers->charset;
}
// read content encoding
if (!empty($part[5])) {
$struct->encoding = strtolower($part[5]);
$struct->headers['content-transfer-encoding'] = $struct->encoding;
}
// get part size
if (!empty($part[6])) {
$struct->size = intval($part[6]);
}
// read part disposition
$di = 8;
if ($struct->ctype_primary == 'text') {
$di += 1;
}
else if ($struct->mimetype == 'message/rfc822') {
$di += 3;
}
if (is_array($part[$di]) && count($part[$di]) == 2) {
$struct->disposition = strtolower($part[$di][0]);
if ($struct->disposition && $struct->disposition !== 'inline' && $struct->disposition !== 'attachment') {
// RFC2183, Section 2.8 - unrecognized type should be treated as "attachment"
$struct->disposition = 'attachment';
}
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 message/rfc822's 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])) {
break;
}
$struct->parts[] = $this->structure_part($part[8][$i], ++$count, $struct->mime_id);
}
}
// get part ID
if (!empty($part[3])) {
$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 = $this->conn->fetchPartHeader(
$this->folder, $this->msg_uid, true, $struct->mime_id);
}
if (is_string($mime_headers)) {
$struct->headers = rcube_mime::parse_headers($mime_headers) + $struct->headers;
}
else if (is_object($mime_headers)) {
$struct->headers = get_object_vars($mime_headers) + $struct->headers;
}
// get real content-type of message/rfc822
if ($struct->mimetype == 'message/rfc822') {
// single-part
if (!is_array($part[8][0])) {
$struct->real_mimetype = strtolower($part[8][0] . '/' . $part[8][1]);
}
// multi-part
else {
for ($n=0; $n<count($part[8]); $n++) {
if (!is_array($part[8][$n])) {
break;
}
}
$struct->real_mimetype = 'multipart/' . strtolower($part[8][$n]);
}
}
if ($struct->ctype_primary == 'message' && empty($struct->parts)) {
if (is_array($part[8]) && $di != 8) {
$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
*
* @param rcube_message_part $part Part object
* @param string $headers Part's raw headers
*/
protected 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 = $this->conn->fetchPartHeader(
$this->folder, $this->msg_uid, true, $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 = $this->conn->fetchPartHeader(
$this->folder, $this->msg_uid, true, $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 = $this->conn->fetchPartHeader(
$this->folder, $this->msg_uid, true, $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 = $this->conn->fetchPartHeader(
$this->folder, $this->msg_uid, true, $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)) {
if (!empty($part->charset)) {
$charset = $part->charset;
}
else if (!empty($this->struct_charset)) {
$charset = $this->struct_charset;
}
else {
$charset = rcube_charset::detect($filename_mime, $this->default_charset);
}
$part->filename = rcube_mime::decode_mime_string($filename_mime, $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)
*
* @param array $structure Message structure
*
* @return string Charset name
*/
protected 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 rcube_message_part Part object created by get_structure()
* @param mixed True to print part, resource to write part contents in
* @param resource File pointer to save the message part
* @param boolean Disables charset conversion
* @param int Only read this number of bytes
* @param boolean Enables formatting of text/* parts bodies
*
* @return string Message/part body if not printed
*/
public function get_message_part($uid, $part = 1, $o_part = null, $print = null, $fp = null,
$skip_charset_conv = false, $max_bytes = 0, $formatted = true)
{
if (!$this->check_connection()) {
return null;
}
// get part data if not provided
if (!is_object($o_part)) {
$structure = $this->conn->getStructure($this->folder, $uid, true);
$part_data = rcube_imap_generic::getStructurePartData($structure, $part);
$o_part = new rcube_message_part;
$o_part->ctype_primary = $part_data['type'];
$o_part->encoding = $part_data['encoding'];
$o_part->charset = $part_data['charset'];
$o_part->size = $part_data['size'];
}
if ($o_part && $o_part->size) {
$formatted = $formatted && $o_part->ctype_primary == 'text';
$body = $this->conn->handlePartBody($this->folder, $uid, true,
$part ? $part : 'TEXT', $o_part->encoding, $print, $fp, $formatted, $max_bytes);
}
if ($fp || $print) {
return true;
}
// convert charset (if text or message part)
if ($body && preg_match('/^(text|message)$/', $o_part->ctype_primary)) {
// Remove NULL characters if any (#1486189)
if ($formatted && strpos($body, "\x00") !== false) {
$body = str_replace("\x00", '', $body);
}
if (!$skip_charset_conv) {
if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') {
// try to extract charset information from HTML meta tag (#1488125)
if ($o_part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m)) {
$o_part->charset = strtoupper($m[1]);
}
else {
$o_part->charset = $this->default_charset;
}
}
$body = rcube_charset::convert($body, $o_part->charset);
}
}
return $body;
}
/**
* Returns the whole message source as string (or saves to a file)
*
* @param int $uid Message UID
* @param resource $fp File pointer to save the message
* @param string $part Optional message part ID
*
* @return string Message source string
*/
public function get_raw_body($uid, $fp=null, $part = null)
{
if (!$this->check_connection()) {
return null;
}
return $this->conn->handlePartBody($this->folder, $uid,
true, $part, null, false, $fp);
}
/**
* Returns the message headers as string
*
* @param int $uid Message UID
* @param string $part Optional message part ID
*
* @return string Message headers string
*/
public function get_raw_headers($uid, $part = null)
{
if (!$this->check_connection()) {
return null;
}
return $this->conn->fetchPartHeader($this->folder, $uid, true, $part);
}
/**
* Sends the whole message source to stdout
*
* @param int $uid Message UID
* @param bool $formatted Enables line-ending formatting
*/
public function print_raw_body($uid, $formatted = true)
{
if (!$this->check_connection()) {
return;
}
$this->conn->handlePartBody($this->folder, $uid, true, null, null, true, null, $formatted);
}
/**
* Set message flag to one or several messages
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $flag Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
* @param string $folder Folder name
* @param boolean $skip_cache True to skip message cache clean up
*
* @return boolean Operation status
*/
public function set_flag($uids, $flag, $folder=null, $skip_cache=false)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
if (!$this->check_connection()) {
return false;
}
$flag = strtoupper($flag);
list($uids, $all_mode) = $this->parse_uids($uids);
if (strpos($flag, 'UN') === 0) {
$result = $this->conn->unflag($folder, $uids, substr($flag, 2));
}
else {
$result = $this->conn->flag($folder, $uids, $flag);
}
if ($result && !$skip_cache) {
// reload message headers if cached
// update flags instead removing from cache
if ($mcache = $this->get_mcache_engine()) {
$status = strpos($flag, 'UN') !== 0;
$mflag = preg_replace('/^UN/', '', $flag);
$mcache->change_flag($folder, $all_mode ? null : explode(',', $uids),
$mflag, $status);
}
// clear cached counters
if ($flag == 'SEEN' || $flag == 'UNSEEN') {
$this->clear_messagecount($folder, array('SEEN', 'UNSEEN'));
}
else if ($flag == 'DELETED' || $flag == 'UNDELETED') {
$this->clear_messagecount($folder, array('ALL', 'THREADS'));
if ($this->options['skip_deleted']) {
// remove cached messages
$this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
}
}
$this->set_search_dirty($folder);
}
return $result;
}
/**
* Append a mail message (source) to a specific folder
*
* @param string $folder Target folder
* @param string|array $message The message source string or filename
* or array (of strings and file pointers)
* @param string $headers Headers string if $message contains only the body
* @param boolean $is_file True if $message is a filename
* @param array $flags Message flags
* @param mixed $date Message internal date
* @param bool $binary Enables BINARY append
*
* @return int|bool Appended message UID or True on success, False on error
*/
public function save_message($folder, &$message, $headers='', $is_file=false, $flags = array(), $date = null, $binary = false)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
if (!$this->check_connection()) {
return false;
}
// make sure folder exists
if (!$this->folder_exists($folder)) {
return false;
}
$date = $this->date_format($date);
if ($is_file) {
$saved = $this->conn->appendFromFile($folder, $message, $headers, $flags, $date, $binary);
}
else {
$saved = $this->conn->append($folder, $message, $flags, $date, $binary);
}
if ($saved) {
// increase messagecount of the target folder
$this->set_messagecount($folder, 'ALL', 1);
$this->plugins->exec_hook('message_saved', array(
'folder' => $folder,
'message' => $message,
'headers' => $headers,
'is_file' => $is_file,
'flags' => $flags,
'date' => $date,
'binary' => $binary,
'result' => $saved,
));
}
return $saved;
}
/**
* Move a message from one folder to another
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $to_mbox Target folder
* @param string $from_mbox Source folder
*
* @return boolean True on success, False on error
*/
public function move_message($uids, $to_mbox, $from_mbox='')
{
if (!strlen($from_mbox)) {
$from_mbox = $this->folder;
}
if ($to_mbox === $from_mbox) {
return false;
}
list($uids, $all_mode) = $this->parse_uids($uids);
// exit if no message uids are specified
if (empty($uids)) {
return false;
}
if (!$this->check_connection()) {
return false;
}
$config = rcube::get_instance()->config;
$to_trash = $to_mbox == $config->get('trash_mbox');
// flag messages as read before moving them
if ($to_trash && $config->get('read_when_deleted')) {
// don't flush cache (4th argument)
$this->set_flag($uids, 'SEEN', $from_mbox, true);
}
// move messages
$moved = $this->conn->move($uids, $from_mbox, $to_mbox);
// when moving to Trash we make sure the folder exists
// as it's uncommon scenario we do this when MOVE fails, not before
if (!$moved && $to_trash && $this->get_response_code() == rcube_storage::TRYCREATE) {
if ($this->create_folder($to_mbox, true, 'trash')) {
$moved = $this->conn->move($uids, $from_mbox, $to_mbox);
}
}
if ($moved) {
$this->clear_messagecount($from_mbox);
$this->clear_messagecount($to_mbox);
$this->set_search_dirty($from_mbox);
$this->set_search_dirty($to_mbox);
}
// moving failed
else if ($to_trash && $config->get('delete_always', false)) {
$moved = $this->delete_message($uids, $from_mbox);
}
if ($moved) {
// unset threads internal cache
unset($this->icache['threads']);
// remove message ids from search set
if ($this->search_set && $from_mbox == $this->folder) {
// threads are too complicated to just remove messages from set
if ($this->search_threads || $all_mode) {
$this->refresh_search();
}
else if (!$this->search_set->incomplete) {
$this->search_set->filter(explode(',', $uids), $this->folder);
}
}
// remove cached messages
// @TODO: do cache update instead of clearing it
$this->clear_message_cache($from_mbox, $all_mode ? null : explode(',', $uids));
}
return $moved;
}
/**
* Copy a message from one folder to another
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $to_mbox Target folder
* @param string $from_mbox Source folder
*
* @return boolean True on success, False on error
*/
public function copy_message($uids, $to_mbox, $from_mbox='')
{
if (!strlen($from_mbox)) {
$from_mbox = $this->folder;
}
list($uids, $all_mode) = $this->parse_uids($uids);
// exit if no message uids are specified
if (empty($uids)) {
return false;
}
if (!$this->check_connection()) {
return false;
}
// copy messages
$copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
if ($copied) {
$this->clear_messagecount($to_mbox);
}
return $copied;
}
/**
* Mark messages as deleted and expunge them
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $folder Source folder
*
* @return boolean True on success, False on error
*/
public function delete_message($uids, $folder='')
{
if (!strlen($folder)) {
$folder = $this->folder;
}
list($uids, $all_mode) = $this->parse_uids($uids);
// exit if no message uids are specified
if (empty($uids)) {
return false;
}
if (!$this->check_connection()) {
return false;
}
$deleted = $this->conn->flag($folder, $uids, 'DELETED');
if ($deleted) {
// send expunge command in order to have the deleted message
// really deleted from the folder
$this->expunge_message($uids, $folder, false);
$this->clear_messagecount($folder);
// unset threads internal cache
unset($this->icache['threads']);
$this->set_search_dirty($folder);
// remove message ids from search set
if ($this->search_set && $folder == $this->folder) {
// threads are too complicated to just remove messages from set
if ($this->search_threads || $all_mode) {
$this->refresh_search();
}
else if (!$this->search_set->incomplete) {
$this->search_set->filter(explode(',', $uids));
}
}
// remove cached messages
$this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
}
return $deleted;
}
/**
* Send IMAP expunge command and clear cache
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $folder Folder name
* @param boolean $clear_cache False if cache should not be cleared
*
* @return boolean True on success, False on failure
*/
public function expunge_message($uids, $folder = null, $clear_cache = true)
{
if ($uids && $this->get_capability('UIDPLUS')) {
list($uids, $all_mode) = $this->parse_uids($uids);
}
else {
$uids = null;
}
if (!strlen($folder)) {
$folder = $this->folder;
}
if (!$this->check_connection()) {
return false;
}
// force folder selection and check if folder is writeable
// to prevent a situation when CLOSE is executed on closed
// or EXPUNGE on read-only folder
$result = $this->conn->select($folder);
if (!$result) {
return false;
}
if (!$this->conn->data['READ-WRITE']) {
$this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Folder is read-only");
return false;
}
// CLOSE(+SELECT) should be faster than EXPUNGE
if (empty($uids) || $all_mode) {
$result = $this->conn->close();
}
else {
$result = $this->conn->expunge($folder, $uids);
}
if ($result && $clear_cache) {
$this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
$this->clear_messagecount($folder);
}
return $result;
}
/* --------------------------------
* folder management
* --------------------------------*/
/**
* Public method for listing subscribed folders.
*
* @param string $root Optional root folder
* @param string $name Optional name pattern
* @param string $filter Optional filter
* @param string $rights Optional ACL requirements
* @param bool $skip_sort Enable to return unsorted list (for better performance)
*
* @return array List of folders
*/
public function list_folders_subscribed($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
{
$cache_key = $root.':'.$name;
if (!empty($filter)) {
$cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
}
$cache_key .= ':'.$rights;
$cache_key = 'mailboxes.'.md5($cache_key);
// get cached folder list
$a_mboxes = $this->get_cache($cache_key);
if (is_array($a_mboxes)) {
return $a_mboxes;
}
// Give plugins a chance to provide a list of folders
$data = $this->plugins->exec_hook('storage_folders',
array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB'));
if (isset($data['folders'])) {
$a_mboxes = $data['folders'];
}
else {
$a_mboxes = $this->list_folders_subscribed_direct($root, $name);
}
if (!is_array($a_mboxes)) {
return array();
}
// filter folders list according to rights requirements
if ($rights && $this->get_capability('ACL')) {
$a_mboxes = $this->filter_rights($a_mboxes, $rights);
}
// INBOX should always be available
if (in_array_nocase($root . $name, array('*', '%', 'INBOX', 'INBOX*'))
&& (!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)
) {
array_unshift($a_mboxes, 'INBOX');
}
// sort folders (always sort for cache)
if (!$skip_sort || $this->cache) {
$a_mboxes = $this->sort_folder_list($a_mboxes);
}
// write folders list to cache
$this->update_cache($cache_key, $a_mboxes);
return $a_mboxes;
}
/**
* Method for direct folders listing (LSUB)
*
* @param string $root Optional root folder
* @param string $name Optional name pattern
*
* @return array List of subscribed folders
* @see rcube_imap::list_folders_subscribed()
*/
public function list_folders_subscribed_direct($root = '', $name = '*')
{
if (!$this->check_connection()) {
return null;
}
$config = rcube::get_instance()->config;
$list_root = $root === '' && $this->list_root ? $this->list_root : $root;
// Server supports LIST-EXTENDED, we can use selection options
// #1486225: Some dovecot versions return wrong result using LIST-EXTENDED
$list_extended = !$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED');
if ($list_extended) {
// This will also set folder options, LSUB doesn't do that
$result = $this->conn->listMailboxes($list_root, $name,
NULL, array('SUBSCRIBED'));
}
else {
// retrieve list of folders from IMAP server using LSUB
$result = $this->conn->listSubscribed($list_root, $name);
}
if (!is_array($result)) {
return array();
}
// Add/Remove folders according to some configuration options
$this->list_folders_filter($result, $root . $name, ($list_extended ? 'ext-' : '') . 'subscribed');
if ($list_extended) {
// unsubscribe non-existent folders, remove from the list
if ($name == '*' && !empty($this->conn->data['LIST'])) {
foreach ($result as $idx => $folder) {
if (($opts = $this->conn->data['LIST'][$folder])
&& in_array_nocase('\\NonExistent', $opts)
) {
$this->conn->unsubscribe($folder);
unset($result[$idx]);
}
}
}
}
else {
// unsubscribe non-existent folders, remove them from the list
if (!empty($result) && $name == '*') {
$existing = $this->list_folders($root, $name);
// Try to make sure the list of existing folders is not malformed,
// we don't want to unsubscribe existing folders on error
if (is_array($existing) && (!empty($root) || count($existing) > 1)) {
$nonexisting = array_diff($result, $existing);
$result = array_diff($result, $nonexisting);
foreach ($nonexisting as $folder) {
$this->conn->unsubscribe($folder);
}
}
}
}
return $result;
}
/**
* Get a list of all folders available on the server
*
* @param string $root IMAP root dir
* @param string $name Optional name pattern
* @param mixed $filter Optional filter
* @param string $rights Optional ACL requirements
* @param bool $skip_sort Enable to return unsorted list (for better performance)
*
* @return array Indexed array with folder names
*/
public function list_folders($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
{
$cache_key = $root.':'.$name;
if (!empty($filter)) {
$cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
}
$cache_key .= ':'.$rights;
$cache_key = 'mailboxes.list.'.md5($cache_key);
// get cached folder list
$a_mboxes = $this->get_cache($cache_key);
if (is_array($a_mboxes)) {
return $a_mboxes;
}
// Give plugins a chance to provide a list of folders
$data = $this->plugins->exec_hook('storage_folders',
array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
if (isset($data['folders'])) {
$a_mboxes = $data['folders'];
}
else {
// retrieve list of folders from IMAP server
$a_mboxes = $this->list_folders_direct($root, $name);
}
if (!is_array($a_mboxes)) {
$a_mboxes = array();
}
// INBOX should always be available
if (in_array_nocase($root . $name, array('*', '%', 'INBOX', 'INBOX*'))
&& (!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)
) {
array_unshift($a_mboxes, 'INBOX');
}
// cache folder attributes
if ($root == '' && $name == '*' && empty($filter) && !empty($this->conn->data)) {
$this->update_cache('mailboxes.attributes', $this->conn->data['LIST']);
}
// filter folders list according to rights requirements
if ($rights && $this->get_capability('ACL')) {
$a_mboxes = $this->filter_rights($a_mboxes, $rights);
}
// filter folders and sort them
if (!$skip_sort) {
$a_mboxes = $this->sort_folder_list($a_mboxes);
}
// write folders list to cache
$this->update_cache($cache_key, $a_mboxes);
return $a_mboxes;
}
/**
* Method for direct folders listing (LIST)
*
* @param string $root Optional root folder
* @param string $name Optional name pattern
*
* @return array List of folders
* @see rcube_imap::list_folders()
*/
public function list_folders_direct($root = '', $name = '*')
{
if (!$this->check_connection()) {
return null;
}
$list_root = $root === '' && $this->list_root ? $this->list_root : $root;
$result = $this->conn->listMailboxes($list_root, $name);
if (!is_array($result)) {
return array();
}
// Add/Remove folders according to some configuration options
$this->list_folders_filter($result, $root . $name);
return $result;
}
/**
* Apply configured filters on folders list
*/
protected function list_folders_filter(&$result, $root, $update_type = null)
{
$config = rcube::get_instance()->config;
// #1486796: some server configurations doesn't return folders in all namespaces
if ($root === '*' && $config->get('imap_force_ns')) {
$this->list_folders_update($result, $update_type);
}
// Remove hidden folders
if ($config->get('imap_skip_hidden_folders')) {
$result = array_filter($result, function($v) { return $v[0] != '.'; });
}
// Remove folders in shared namespaces (if configured, see self::set_env())
if ($root === '*' && !empty($this->list_excludes)) {
$result = array_filter($result, function($v) {
foreach ($this->list_excludes as $prefix) {
if (strpos($v, $prefix) === 0) {
return false;
}
}
return true;
});
}
}
/**
* Fix folders list by adding folders from other namespaces.
* Needed on some servers eg. Courier IMAP
*
* @param array $result Reference to folders list
* @param string $type Listing type (ext-subscribed, subscribed or all)
*/
protected function list_folders_update(&$result, $type = null)
{
$namespace = $this->get_namespace();
$search = array();
// build list of namespace prefixes
foreach ((array)$namespace as $ns) {
if (is_array($ns)) {
foreach ($ns as $ns_data) {
if (strlen($ns_data[0])) {
$search[] = $ns_data[0];
}
}
}
}
if (!empty($search)) {
// go through all folders detecting namespace usage
foreach ($result as $folder) {
foreach ($search as $idx => $prefix) {
if (strpos($folder, $prefix) === 0) {
unset($search[$idx]);
}
}
if (empty($search)) {
break;
}
}
// get folders in hidden namespaces and add to the result
foreach ($search as $prefix) {
if ($type == 'ext-subscribed') {
$list = $this->conn->listMailboxes('', $prefix . '*', null, array('SUBSCRIBED'));
}
else if ($type == 'subscribed') {
$list = $this->conn->listSubscribed('', $prefix . '*');
}
else {
$list = $this->conn->listMailboxes('', $prefix . '*');
}
if (!empty($list)) {
$result = array_merge($result, $list);
}
}
}
}
/**
* Filter the given list of folders according to access rights
*
* For performance reasons we assume user has full rights
* on all personal folders.
*/
protected function filter_rights($a_folders, $rights)
{
$regex = '/('.$rights.')/';
foreach ($a_folders as $idx => $folder) {
if ($this->folder_namespace($folder) == 'personal') {
continue;
}
$myrights = join('', (array)$this->my_rights($folder));
if ($myrights !== null && !preg_match($regex, $myrights)) {
unset($a_folders[$idx]);
}
}
return $a_folders;
}
/**
* Get mailbox quota information
*
* @param string $folder Folder name
*
* @return mixed Quota info or False if not supported
*/
public function get_quota($folder = null)
{
if ($this->get_capability('QUOTA') && $this->check_connection()) {
return $this->conn->getQuota($folder);
}
return false;
}
/**
* Get folder size (size of all messages in a folder)
*
* @param string $folder Folder name
*
* @return int Folder size in bytes, False on error
*/
public function folder_size($folder)
{
if (!strlen($folder)) {
return false;
}
if (!$this->check_connection()) {
return 0;
}
// On Cyrus we can use special folder annotation, which should be much faster
if ($this->get_vendor() == 'cyrus') {
$idx = '/shared/vendor/cmu/cyrus-imapd/size';
$result = $this->get_metadata($folder, $idx, array(), true);
if (!empty($result) && is_numeric($result[$folder][$idx])) {
return $result[$folder][$idx];
}
}
// @TODO: could we try to use QUOTA here?
$result = $this->conn->fetchHeaderIndex($folder, '1:*', 'SIZE', false);
if (is_array($result)) {
$result = array_sum($result);
}
return $result;
}
/**
* Subscribe to a specific folder(s)
*
* @param array $folders Folder name(s)
*
* @return boolean True on success
*/
public function subscribe($folders)
{
// let this common function do the main work
return $this->change_subscription($folders, 'subscribe');
}
/**
* Unsubscribe folder(s)
*
* @param array $a_mboxes Folder name(s)
*
* @return boolean True on success
*/
public function unsubscribe($folders)
{
// let this common function do the main work
return $this->change_subscription($folders, 'unsubscribe');
}
/**
* Create a new folder on the server and register it in local cache
*
* @param string $folder New folder name
* @param boolean $subscribe True if the new folder should be subscribed
* @param string $type Optional folder type (junk, trash, drafts, sent, archive)
* @param boolean $noselect Make the folder a \NoSelect folder by adding hierarchy
* separator at the end (useful for server that do not support
* both folders and messages as folder children)
*
* @return boolean True on success
*/
public function create_folder($folder, $subscribe = false, $type = null, $noselect = false)
{
if (!$this->check_connection()) {
return false;
}
if ($noselect) {
$folder .= $this->delimiter;
}
$result = $this->conn->createFolder($folder, $type ? array("\\" . ucfirst($type)) : null);
// try to subscribe it
if ($result) {
// clear cache
$this->clear_cache('mailboxes', true);
if ($subscribe && !$noselect) {
$this->subscribe($folder);
}
}
return $result;
}
/**
* Set a new name to an existing folder
*
* @param string $folder Folder to rename
* @param string $new_name New folder name
*
* @return boolean True on success
*/
public function rename_folder($folder, $new_name)
{
if (!strlen($new_name)) {
return false;
}
if (!$this->check_connection()) {
return false;
}
$delm = $this->get_hierarchy_delimiter();
// get list of subscribed folders
if ((strpos($folder, '%') === false) && (strpos($folder, '*') === false)) {
$a_subscribed = $this->list_folders_subscribed('', $folder . $delm . '*');
$subscribed = $this->folder_exists($folder, true);
}
else {
$a_subscribed = $this->list_folders_subscribed();
$subscribed = in_array($folder, $a_subscribed);
}
$result = $this->conn->renameFolder($folder, $new_name);
if ($result) {
// unsubscribe the old folder, subscribe the new one
if ($subscribed) {
$this->conn->unsubscribe($folder);
$this->conn->subscribe($new_name);
}
// check if folder children are subscribed
foreach ($a_subscribed as $c_subscribed) {
if (strpos($c_subscribed, $folder.$delm) === 0) {
$this->conn->unsubscribe($c_subscribed);
$this->conn->subscribe(preg_replace('/^'.preg_quote($folder, '/').'/',
$new_name, $c_subscribed));
// clear cache
$this->clear_message_cache($c_subscribed);
}
}
// clear cache
$this->clear_message_cache($folder);
$this->clear_cache('mailboxes', true);
}
return $result;
}
/**
* Remove folder (with subfolders) from the server
*
* @param string $folder Folder name
*
* @return boolean True on success, False on failure
*/
public function delete_folder($folder)
{
if (!$this->check_connection()) {
return false;
}
$delm = $this->get_hierarchy_delimiter();
// get list of sub-folders or all folders
// if folder name contains special characters
$path = strspn($folder, '%*') > 0 ? ($folder . $delm) : '';
$sub_mboxes = $this->list_folders('', $path . '*');
// According to RFC3501 deleting a \Noselect folder
// with subfolders may fail. To workaround this we delete
// subfolders first (in reverse order) (#5466)
if (!empty($sub_mboxes)) {
foreach (array_reverse($sub_mboxes) as $mbox) {
if (strpos($mbox, $folder . $delm) === 0) {
if ($this->conn->deleteFolder($mbox)) {
$this->conn->unsubscribe($mbox);
$this->clear_message_cache($mbox);
}
}
}
}
// delete the folder
if ($result = $this->conn->deleteFolder($folder)) {
// and unsubscribe it
$this->conn->unsubscribe($folder);
$this->clear_message_cache($folder);
}
$this->clear_cache('mailboxes', true);
return $result;
}
/**
* Detect special folder associations stored in storage backend
*/
public function get_special_folders($forced = false)
{
$result = parent::get_special_folders();
$rcube = rcube::get_instance();
// Lock SPECIAL-USE after user preferences change (#4782)
if ($rcube->config->get('lock_special_folders')) {
return $result;
}
if (isset($this->icache['special-use'])) {
return array_merge($result, $this->icache['special-use']);
}
if (!$forced || !$this->get_capability('SPECIAL-USE')) {
return $result;
}
if (!$this->check_connection()) {
return $result;
}
$types = array_map(function($value) { return "\\" . ucfirst($value); }, rcube_storage::$folder_types);
$special = array();
// request \Subscribed flag in LIST response as performance improvement for folder_exists()
$folders = $this->conn->listMailboxes('', '*', array('SUBSCRIBED'), array('SPECIAL-USE'));
if (!empty($folders)) {
foreach ($folders as $folder) {
if ($flags = $this->conn->data['LIST'][$folder]) {
foreach ($types as $type) {
if (in_array($type, $flags)) {
$type = strtolower(substr($type, 1));
$special[$type] = $folder;
}
}
}
}
}
$this->icache['special-use'] = $special;
unset($this->icache['special-folders']);
return array_merge($result, $special);
}
/**
* Set special folder associations stored in storage backend
*/
public function set_special_folders($specials)
{
if (!$this->get_capability('SPECIAL-USE') || !$this->get_capability('METADATA')) {
return false;
}
if (!$this->check_connection()) {
return false;
}
$folders = $this->get_special_folders(true);
$old = (array) $this->icache['special-use'];
foreach ($specials as $type => $folder) {
if (in_array($type, rcube_storage::$folder_types)) {
$old_folder = $old[$type];
if ($old_folder !== $folder) {
// unset old-folder metadata
if ($old_folder !== null) {
$this->delete_metadata($old_folder, array('/private/specialuse'));
}
// set new folder metadata
if ($folder) {
$this->set_metadata($folder, array('/private/specialuse' => "\\" . ucfirst($type)));
}
}
}
}
$this->icache['special-use'] = $specials;
unset($this->icache['special-folders']);
return true;
}
/**
* Checks if folder exists and is subscribed
*
* @param string $folder Folder name
* @param boolean $subscription Enable subscription checking
*
* @return boolean TRUE or FALSE
*/
public function folder_exists($folder, $subscription = false)
{
if ($folder == 'INBOX') {
return true;
}
$key = $subscription ? 'subscribed' : 'existing';
if (is_array($this->icache[$key]) && in_array($folder, $this->icache[$key])) {
return true;
}
if (!$this->check_connection()) {
return false;
}
if ($subscription) {
// It's possible we already called LIST command, check LIST data
if (!empty($this->conn->data['LIST']) && !empty($this->conn->data['LIST'][$folder])
&& in_array_nocase('\\Subscribed', $this->conn->data['LIST'][$folder])
) {
$a_folders = array($folder);
}
else {
$a_folders = $this->conn->listSubscribed('', $folder);
}
}
else {
// It's possible we already called LIST command, check LIST data
if (!empty($this->conn->data['LIST']) && isset($this->conn->data['LIST'][$folder])) {
$a_folders = array($folder);
}
else {
$a_folders = $this->conn->listMailboxes('', $folder);
}
}
if (is_array($a_folders) && in_array($folder, $a_folders)) {
$this->icache[$key][] = $folder;
return true;
}
return false;
}
/**
* Returns the namespace where the folder is in
*
* @param string $folder Folder name
*
* @return string One of 'personal', 'other' or 'shared'
*/
public function folder_namespace($folder)
{
if ($folder == 'INBOX') {
return 'personal';
}
foreach ($this->namespace as $type => $namespace) {
if (is_array($namespace)) {
foreach ($namespace as $ns) {
if ($len = strlen($ns[0])) {
if (($len > 1 && $folder == substr($ns[0], 0, -1))
|| strpos($folder, $ns[0]) === 0
) {
return $type;
}
}
}
}
}
return 'personal';
}
/**
* Modify folder name according to personal namespace prefix.
* For output it removes prefix of the personal namespace if it's possible.
* For input it adds the prefix. Use it before creating a folder in root
* of the folders tree.
*
* @param string $folder Folder name
* @param string $mode Mode name (out/in)
*
* @return string Folder name
*/
public function mod_folder($folder, $mode = 'out')
{
$prefix = $this->namespace['prefix_' . $mode]; // see set_env()
if ($prefix === null || $prefix === ''
|| !($prefix_len = strlen($prefix)) || !strlen($folder)
) {
return $folder;
}
// remove prefix for output
if ($mode == 'out') {
if (substr($folder, 0, $prefix_len) === $prefix) {
return substr($folder, $prefix_len);
}
return $folder;
}
// add prefix for input (e.g. folder creation)
return $prefix . $folder;
}
/**
* Gets folder attributes from LIST response, e.g. \Noselect, \Noinferiors
*
* @param string $folder Folder name
* @param bool $force Set to True if attributes should be refreshed
*
* @return array Options list
*/
public function folder_attributes($folder, $force=false)
{
// get attributes directly from LIST command
if (!empty($this->conn->data['LIST']) && is_array($this->conn->data['LIST'][$folder])) {
$opts = $this->conn->data['LIST'][$folder];
}
// get cached folder attributes
else if (!$force) {
$opts = $this->get_cache('mailboxes.attributes');
$opts = $opts[$folder];
}
if (!is_array($opts)) {
if (!$this->check_connection()) {
return array();
}
$this->conn->listMailboxes('', $folder);
$opts = $this->conn->data['LIST'][$folder];
}
return is_array($opts) ? $opts : array();
}
/**
* Gets connection (and current folder) data: UIDVALIDITY, EXISTS, RECENT,
* PERMANENTFLAGS, UIDNEXT, UNSEEN
*
* @param string $folder Folder name
*
* @return array Data
*/
public function folder_data($folder)
{
if (!strlen($folder)) {
$folder = $this->folder !== null ? $this->folder : 'INBOX';
}
if ($this->conn->selected != $folder) {
if (!$this->check_connection()) {
return array();
}
if ($this->conn->select($folder)) {
$this->folder = $folder;
}
else {
return null;
}
}
$data = $this->conn->data;
// add (E)SEARCH result for ALL UNDELETED query
if (!empty($this->icache['undeleted_idx'])
&& $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder
) {
$data['UNDELETED'] = $this->icache['undeleted_idx'];
}
// dovecot does not return HIGHESTMODSEQ until requested, we use it though in our caching system
// calling STATUS is needed only once, after first use mod-seq db will be maintained
if (!isset($data['HIGHESTMODSEQ']) && empty($data['NOMODSEQ'])
&& ($this->get_capability('QRESYNC') || $this->get_capability('CONDSTORE'))
) {
if ($add_data = $this->conn->status($folder, array('HIGHESTMODSEQ'))) {
$data = array_merge($data, $add_data);
}
}
return $data;
}
/**
* Returns extended information about the folder
*
* @param string $folder Folder name
*
* @return array Data
*/
public function folder_info($folder)
{
if ($this->icache['options'] && $this->icache['options']['name'] == $folder) {
return $this->icache['options'];
}
// get cached metadata
$cache_key = 'mailboxes.folder-info.' . $folder;
$cached = $this->get_cache($cache_key);
if (is_array($cached)) {
return $cached;
}
$acl = $this->get_capability('ACL');
$namespace = $this->get_namespace();
$options = array();
// check if the folder is a namespace prefix
if (!empty($namespace)) {
$mbox = $folder . $this->delimiter;
foreach ($namespace as $ns) {
if (!empty($ns)) {
foreach ($ns as $item) {
if ($item[0] === $mbox) {
$options['is_root'] = true;
break 2;
}
}
}
}
}
// check if the folder is other user virtual-root
if (!$options['is_root'] && !empty($namespace) && !empty($namespace['other'])) {
$parts = explode($this->delimiter, $folder);
if (count($parts) == 2) {
$mbox = $parts[0] . $this->delimiter;
foreach ($namespace['other'] as $item) {
if ($item[0] === $mbox) {
$options['is_root'] = true;
break;
}
}
}
}
$options['name'] = $folder;
$options['attributes'] = $this->folder_attributes($folder, true);
$options['namespace'] = $this->folder_namespace($folder);
$options['special'] = $this->is_special_folder($folder);
// Set 'noselect' flag
if (is_array($options['attributes'])) {
foreach ($options['attributes'] as $attrib) {
$attrib = strtolower($attrib);
if ($attrib == '\noselect' || $attrib == '\nonexistent') {
$options['noselect'] = true;
}
}
}
else {
$options['noselect'] = true;
}
// Get folder rights (MYRIGHTS)
if ($acl && ($rights = $this->my_rights($folder))) {
$options['rights'] = $rights;
}
// Set 'norename' flag
if (!empty($options['rights'])) {
$rfc_4314 = is_array($this->get_capability('RIGHTS'));
$options['norename'] = ($rfc_4314 && !in_array('x', $options['rights']))
|| (!$rfc_4314 && !in_array('d', $options['rights']));
if (!$options['noselect']) {
$options['noselect'] = !in_array('r', $options['rights']);
}
}
else {
$options['norename'] = $options['is_root'] || $options['namespace'] != 'personal';
}
// update caches
$this->icache['options'] = $options;
$this->update_cache($cache_key, $options);
return $options;
}
/**
* Synchronizes messages cache.
*
* @param string $folder Folder name
*/
public function folder_sync($folder)
{
if ($mcache = $this->get_mcache_engine()) {
$mcache->synchronize($folder);
}
}
/**
* Check if the folder name is valid
*
* @param string $folder Folder name (UTF-8)
* @param string &$char First forbidden character found
*
* @return bool True if the name is valid, False otherwise
*/
public function folder_validate($folder, &$char = null)
{
if (parent::folder_validate($folder, $char)) {
$vendor = $this->get_vendor();
$regexp = '\\x00-\\x1F\\x7F%*';
if ($vendor == 'cyrus') {
// List based on testing Kolab's Cyrus-IMAP 2.5
$regexp .= '!`@(){}|\\?<;"';
}
if (!preg_match("/[$regexp]/", $folder, $m)) {
return true;
}
$char = $m[0];
}
return false;
}
/**
* Get message header names for rcube_imap_generic::fetchHeader(s)
*
* @return string Space-separated list of header names
*/
protected function get_fetch_headers()
{
if (!empty($this->options['fetch_headers'])) {
$headers = explode(' ', $this->options['fetch_headers']);
}
else {
$headers = array();
}
if ($this->messages_caching || $this->options['all_headers']) {
$headers = array_merge($headers, $this->all_headers);
}
return $headers;
}
/* -----------------------------------------
* ACL and METADATA/ANNOTATEMORE methods
* ----------------------------------------*/
/**
* Changes the ACL on the specified folder (SETACL)
*
* @param string $folder Folder name
* @param string $user User name
* @param string $acl ACL string
*
* @return boolean True on success, False on failure
* @since 0.5-beta
*/
public function set_acl($folder, $user, $acl)
{
if (!$this->get_capability('ACL')) {
return false;
}
if (!$this->check_connection()) {
return false;
}
$this->clear_cache('mailboxes.folder-info.' . $folder);
return $this->conn->setACL($folder, $user, $acl);
}
/**
* Removes any <identifier,rights> pair for the
* specified user from the ACL for the specified
* folder (DELETEACL)
*
* @param string $folder Folder name
* @param string $user User name
*
* @return boolean True on success, False on failure
* @since 0.5-beta
*/
public function delete_acl($folder, $user)
{
if (!$this->get_capability('ACL')) {
return false;
}
if (!$this->check_connection()) {
return false;
}
return $this->conn->deleteACL($folder, $user);
}
/**
* Returns the access control list for folder (GETACL)
*
* @param string $folder Folder name
*
* @return array User-rights array on success, NULL on error
* @since 0.5-beta
*/
public function get_acl($folder)
{
if (!$this->get_capability('ACL')) {
return null;
}
if (!$this->check_connection()) {
return null;
}
return $this->conn->getACL($folder);
}
/**
* Returns information about what rights can be granted to the
* user (identifier) in the ACL for the folder (LISTRIGHTS)
*
* @param string $folder Folder name
* @param string $user User name
*
* @return array List of user rights
* @since 0.5-beta
*/
public function list_rights($folder, $user)
{
if (!$this->get_capability('ACL')) {
return null;
}
if (!$this->check_connection()) {
return null;
}
return $this->conn->listRights($folder, $user);
}
/**
* Returns the set of rights that the current user has to
* folder (MYRIGHTS)
*
* @param string $folder Folder name
*
* @return array MYRIGHTS response on success, NULL on error
* @since 0.5-beta
*/
public function my_rights($folder)
{
if (!$this->get_capability('ACL')) {
return null;
}
if (!$this->check_connection()) {
return null;
}
return $this->conn->myRights($folder);
}
/**
* Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
*
* @param string $folder Folder name (empty for server metadata)
* @param array $entries Entry-value array (use NULL value as NIL)
*
* @return boolean True on success, False on failure
* @since 0.5-beta
*/
public function set_metadata($folder, $entries)
{
if (!$this->check_connection()) {
return false;
}
$this->clear_cache('mailboxes.metadata.', true);
if ($this->get_capability('METADATA') ||
(!strlen($folder) && $this->get_capability('METADATA-SERVER'))
) {
return $this->conn->setMetadata($folder, $entries);
}
else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
foreach ((array)$entries as $entry => $value) {
list($ent, $attr) = $this->md2annotate($entry);
$entries[$entry] = array($ent, $attr, $value);
}
return $this->conn->setAnnotation($folder, $entries);
}
return false;
}
/**
* Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
*
* @param string $folder Folder name (empty for server metadata)
* @param array $entries Entry names array
*
* @return boolean True on success, False on failure
* @since 0.5-beta
*/
public function delete_metadata($folder, $entries)
{
if (!$this->check_connection()) {
return false;
}
$this->clear_cache('mailboxes.metadata.', true);
if ($this->get_capability('METADATA') ||
(!strlen($folder) && $this->get_capability('METADATA-SERVER'))
) {
return $this->conn->deleteMetadata($folder, $entries);
}
else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
foreach ((array)$entries as $idx => $entry) {
list($ent, $attr) = $this->md2annotate($entry);
$entries[$idx] = array($ent, $attr, NULL);
}
return $this->conn->setAnnotation($folder, $entries);
}
return false;
}
/**
* Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
*
* @param string $folder Folder name (empty for server metadata)
* @param array $entries Entries
* @param array $options Command options (with MAXSIZE and DEPTH keys)
* @param bool $force Disables cache use
*
* @return array Metadata entry-value hash array on success, NULL on error
* @since 0.5-beta
*/
public function get_metadata($folder, $entries, $options = array(), $force = false)
{
$entries = (array) $entries;
if (!$force) {
// create cache key
// @TODO: this is the simplest solution, but we do the same with folders list
// maybe we should store data per-entry and merge on request
sort($options);
sort($entries);
$cache_key = 'mailboxes.metadata.' . $folder;
$cache_key .= '.' . md5(serialize($options).serialize($entries));
// get cached data
$cached_data = $this->get_cache($cache_key);
if (is_array($cached_data)) {
return $cached_data;
}
}
if (!$this->check_connection()) {
return null;
}
if ($this->get_capability('METADATA') ||
(!strlen($folder) && $this->get_capability('METADATA-SERVER'))
) {
$res = $this->conn->getMetadata($folder, $entries, $options);
}
else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
$queries = array();
$res = array();
// Convert entry names
foreach ($entries as $entry) {
list($ent, $attr) = $this->md2annotate($entry);
$queries[$attr][] = $ent;
}
// @TODO: Honor MAXSIZE and DEPTH options
foreach ($queries as $attrib => $entry) {
$result = $this->conn->getAnnotation($folder, $entry, $attrib);
// an error, invalidate any previous getAnnotation() results
if (!is_array($result)) {
return null;
}
else {
foreach ($result as $fldr => $data) {
$res[$fldr] = array_merge((array) $res[$fldr], $data);
}
}
}
}
if (isset($res)) {
if (!$force) {
$this->update_cache($cache_key, $res);
}
return $res;
}
}
/**
* Converts the METADATA extension entry name into the correct
* entry-attrib names for older ANNOTATEMORE version.
*
* @param string $entry Entry name
*
* @return array Entry-attribute list, NULL if not supported (?)
*/
protected function md2annotate($entry)
{
if (substr($entry, 0, 7) == '/shared') {
return array(substr($entry, 7), 'value.shared');
}
else if (substr($entry, 0, 8) == '/private') {
return array(substr($entry, 8), 'value.priv');
}
// @TODO: log error
}
/* --------------------------------
* internal caching methods
* --------------------------------*/
/**
* Enable or disable indexes caching
*
* @param string $type Cache type (@see rcube::get_cache)
*/
public function set_caching($type)
{
if ($type) {
$this->caching = $type;
}
else {
if ($this->cache) {
$this->cache->close();
}
$this->cache = null;
$this->caching = false;
}
}
/**
* Getter for IMAP cache object
*/
protected function get_cache_engine()
{
if ($this->caching && !$this->cache) {
$rcube = rcube::get_instance();
$ttl = $rcube->config->get('imap_cache_ttl', '10d');
$this->cache = $rcube->get_cache('IMAP', $this->caching, $ttl);
}
return $this->cache;
}
/**
* Returns cached value
*
* @param string $key Cache key
*
* @return mixed
*/
public function get_cache($key)
{
if ($cache = $this->get_cache_engine()) {
return $cache->get($key);
}
}
/**
* Update cache
*
* @param string $key Cache key
* @param mixed $data Data
*/
public function update_cache($key, $data)
{
if ($cache = $this->get_cache_engine()) {
$cache->set($key, $data);
}
}
/**
* Clears the cache.
*
* @param string $key Cache key name or pattern
* @param boolean $prefix_mode Enable it to clear all keys starting
* with prefix specified in $key
*/
public function clear_cache($key = null, $prefix_mode = false)
{
if ($cache = $this->get_cache_engine()) {
$cache->remove($key, $prefix_mode);
}
}
/* --------------------------------
* message caching methods
* --------------------------------*/
/**
* Enable or disable messages caching
*
* @param boolean $set Flag
* @param int $mode Cache mode
*/
public function set_messages_caching($set, $mode = null)
{
if ($set) {
$this->messages_caching = true;
if ($mode && ($cache = $this->get_mcache_engine())) {
$cache->set_mode($mode);
}
}
else {
if ($this->mcache) {
$this->mcache->close();
}
$this->mcache = null;
$this->messages_caching = false;
}
}
/**
* Getter for messages cache object
*/
protected function get_mcache_engine()
{
if ($this->messages_caching && !$this->mcache) {
$rcube = rcube::get_instance();
if (($dbh = $rcube->get_dbh()) && ($userid = $rcube->get_user_id())) {
$ttl = $rcube->config->get('messages_cache_ttl', '10d');
$threshold = $rcube->config->get('messages_cache_threshold', 50);
$this->mcache = new rcube_imap_cache(
$dbh, $this, $userid, $this->options['skip_deleted'], $ttl, $threshold);
}
}
return $this->mcache;
}
/**
* Clears the messages cache.
*
* @param string $folder Folder name
* @param array $uids Optional message UIDs to remove from cache
*/
protected function clear_message_cache($folder = null, $uids = null)
{
if ($mcache = $this->get_mcache_engine()) {
$mcache->clear($folder, $uids);
}
}
/**
* Delete outdated cache entries
*/
function cache_gc()
{
rcube_imap_cache::gc();
}
/* --------------------------------
* protected methods
* --------------------------------*/
/**
* Determines if server supports dual use folders (those can
* contain both sub-folders and messages).
*
* @return bool
*/
protected function detect_dual_use_folders()
{
$val = rcube::get_instance()->config->get('imap_dual_use_folders');
if ($val !== null) {
return (bool) $val;
}
if (!$this->check_connection()) {
return false;
}
$folder = str_replace('.', '', 'foldertest' . microtime(true));
$folder = $this->mod_folder($folder, 'in');
$subfolder = $folder . $this->delimiter . 'foldertest';
if ($this->conn->createFolder($folder)) {
if ($created = $this->conn->createFolder($subfolder)) {
$this->conn->deleteFolder($subfolder);
}
$this->conn->deleteFolder($folder);
return $created;
}
}
/**
* Validate the given input and save to local properties
*
* @param string $sort_field Sort column
* @param string $sort_order Sort order
*/
protected 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 folders first by default folders and then in alphabethical order
*
* @param array $a_folders Folders list
* @param bool $skip_default Skip default folders handling
*
* @return array Sorted list
*/
public function sort_folder_list($a_folders, $skip_default = false)
{
$specials = array_merge(array('INBOX'), array_values($this->get_special_folders()));
$folders = array();
// convert names to UTF-8
foreach ($a_folders as $folder) {
// for better performance skip encoding conversion
// if the string does not look like UTF7-IMAP
$folders[$folder] = strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP');
}
// sort folders
// asort($folders, SORT_LOCALE_STRING) is not properly sorting case sensitive names
uasort($folders, array($this, 'sort_folder_comparator'));
$folders = array_keys($folders);
if ($skip_default) {
return $folders;
}
// force the type of folder name variable (#1485527)
$folders = array_map('strval', $folders);
$out = array();
// finally we must put special folders on top and rebuild the list
// to move their subfolders where they belong...
$specials = array_unique(array_intersect($specials, $folders));
$folders = array_merge($specials, array_diff($folders, $specials));
$this->sort_folder_specials(null, $folders, $specials, $out);
return $out;
}
/**
* Recursive function to put subfolders of special folders in place
*/
protected function sort_folder_specials($folder, &$list, &$specials, &$out)
{
foreach ($list as $key => $name) {
if ($folder === null || strpos($name, $folder.$this->delimiter) === 0) {
$out[] = $name;
unset($list[$key]);
if (!empty($specials) && ($found = array_search($name, $specials)) !== false) {
unset($specials[$found]);
$this->sort_folder_specials($name, $list, $specials, $out);
}
}
}
reset($list);
}
/**
* Callback for uasort() that implements correct
* locale-aware case-sensitive sorting
*/
protected function sort_folder_comparator($str1, $str2)
{
if ($this->sort_folder_collator === null) {
$this->sort_folder_collator = false;
// strcoll() does not work with UTF8 locale on Windows,
// use Collator from the intl extension
if (stripos(PHP_OS, 'win') === 0 && function_exists('collator_compare')) {
$locale = $this->options['language'] ?: 'en_US';
$this->sort_folder_collator = collator_create($locale) ?: false;
}
}
$path1 = explode($this->delimiter, $str1);
$path2 = explode($this->delimiter, $str2);
foreach ($path1 as $idx => $folder1) {
$folder2 = $path2[$idx];
if ($folder1 === $folder2) {
continue;
}
if ($this->sort_folder_collator) {
return collator_compare($this->sort_folder_collator, $folder1, $folder2);
}
return strcoll($folder1, $folder2);
}
}
/**
* Find UID of the specified message sequence ID
*
* @param int $id Message (sequence) ID
* @param string $folder Folder name
*
* @return int Message UID
*/
public function id2uid($id, $folder = null)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
if (!$this->check_connection()) {
return null;
}
return $this->conn->ID2UID($folder, $id);
}
/**
* Subscribe/unsubscribe a list of folders and update local cache
*/
protected function change_subscription($folders, $mode)
{
$updated = 0;
$folders = (array) $folders;
if (!empty($folders)) {
if (!$this->check_connection()) {
return false;
}
foreach ($folders as $folder) {
$updated += (int) $this->conn->{$mode}($folder);
}
}
// clear cached folders list(s)
if ($updated) {
$this->clear_cache('mailboxes', true);
}
return $updated == count($folders);
}
/**
* Increde/decrese messagecount for a specific folder
*/
protected function set_messagecount($folder, $mode, $increment)
{
if (!is_numeric($increment)) {
return false;
}
$mode = strtoupper($mode);
$a_folder_cache = $this->get_cache('messagecount');
if (!is_array($a_folder_cache[$folder]) || !isset($a_folder_cache[$folder][$mode])) {
return false;
}
// add incremental value to messagecount
$a_folder_cache[$folder][$mode] += $increment;
// there's something wrong, delete from cache
if ($a_folder_cache[$folder][$mode] < 0) {
unset($a_folder_cache[$folder][$mode]);
}
// write back to cache
$this->update_cache('messagecount', $a_folder_cache);
return true;
}
/**
* Remove messagecount of a specific folder from cache
*/
protected function clear_messagecount($folder, $mode = array())
{
$a_folder_cache = $this->get_cache('messagecount');
if (is_array($a_folder_cache[$folder])) {
if (!empty($mode)) {
foreach ((array) $mode as $key) {
unset($a_folder_cache[$folder][$key]);
}
}
else {
unset($a_folder_cache[$folder]);
}
$this->update_cache('messagecount', $a_folder_cache);
}
}
/**
* Converts date string/object into IMAP date/time format
*/
protected function date_format($date)
{
if (empty($date)) {
return null;
}
if (!is_object($date) || !is_a($date, 'DateTime')) {
try {
$timestamp = rcube_utils::strtotime($date);
$date = new DateTime("@".$timestamp);
}
catch (Exception $e) {
return null;
}
}
return $date->format('d-M-Y H:i:s O');
}
/**
* This is our own debug handler for the IMAP connection
*/
public function debug_handler(&$imap, $message)
{
rcube::write_log('imap', $message);
}
}
diff --git a/program/lib/Roundcube/rcube_imap_cache.php b/program/lib/Roundcube/rcube_imap_cache.php
index 4cca3ac60..1ef238aee 100644
--- a/program/lib/Roundcube/rcube_imap_cache.php
+++ b/program/lib/Roundcube/rcube_imap_cache.php
@@ -1,1317 +1,1318 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2012, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Caching of IMAP folder contents (messages and index) |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Interface class for accessing Roundcube messages cache
*
* @package Framework
* @subpackage Storage
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_imap_cache
{
const MODE_INDEX = 1;
const MODE_MESSAGE = 2;
/**
* Instance of rcube_imap
*
* @var rcube_imap
*/
private $imap;
/**
* Instance of rcube_db
*
* @var rcube_db
*/
private $db;
/**
* User ID
*
* @var int
*/
private $userid;
/**
* Expiration time in seconds
*
* @var int
*/
private $ttl;
/**
* Maximum cached message size
*
* @var int
*/
private $threshold;
/**
* Internal (in-memory) cache
*
* @var array
*/
private $icache = array();
private $skip_deleted = false;
private $mode;
/**
* List of known flags. Thanks to this we can handle flag changes
* with good performance. Bad thing is we need to know used flags.
*/
public $flags = array(
1 => 'SEEN', // RFC3501
2 => 'DELETED', // RFC3501
4 => 'ANSWERED', // RFC3501
8 => 'FLAGGED', // RFC3501
16 => 'DRAFT', // RFC3501
32 => 'MDNSENT', // RFC3503
64 => 'FORWARDED', // RFC5550
128 => 'SUBMITPENDING', // RFC5550
256 => 'SUBMITTED', // RFC5550
512 => 'JUNK',
1024 => 'NONJUNK',
2048 => 'LABEL1',
4096 => 'LABEL2',
8192 => 'LABEL3',
16384 => 'LABEL4',
32768 => 'LABEL5',
);
/**
* Object constructor.
*
* @param rcube_db $db DB handler
* @param rcube_imap $imap IMAP handler
* @param int $userid User identifier
* @param bool $skip_deleted skip_deleted flag
* @param string $ttl Expiration time of memcache/apc items
* @param int $threshold Maximum cached message size
*/
function __construct($db, $imap, $userid, $skip_deleted, $ttl=0, $threshold=0)
{
// convert ttl string to seconds
$ttl = get_offset_sec($ttl);
if ($ttl > 2592000) $ttl = 2592000;
$this->db = $db;
$this->imap = $imap;
$this->userid = $userid;
$this->skip_deleted = $skip_deleted;
$this->ttl = $ttl;
$this->threshold = $threshold;
// cache all possible information by default
$this->mode = self::MODE_INDEX | self::MODE_MESSAGE;
// database tables
$this->index_table = $db->table_name('cache_index', true);
$this->thread_table = $db->table_name('cache_thread', true);
$this->messages_table = $db->table_name('cache_messages', true);
}
/**
* Cleanup actions (on shutdown).
*/
public function close()
{
$this->save_icache();
$this->icache = null;
}
/**
* Set cache mode
*
* @param int $mode Cache mode
*/
public function set_mode($mode)
{
$this->mode = $mode;
}
/**
* Return (sorted) messages index (UIDs).
* If index doesn't exist or is invalid, will be updated.
*
* @param string $mailbox Folder name
* @param string $sort_field Sorting column
* @param string $sort_order Sorting order (ASC|DESC)
* @param bool $exiting Skip index initialization if it doesn't exist in DB
*
* @return array Messages index
*/
function get_index($mailbox, $sort_field = null, $sort_order = null, $existing = false)
{
if (empty($this->icache[$mailbox])) {
$this->icache[$mailbox] = array();
}
$sort_order = strtoupper($sort_order) == 'ASC' ? 'ASC' : 'DESC';
// Seek in internal cache
if (array_key_exists('index', $this->icache[$mailbox])) {
// The index was fetched from database already, but not validated yet
if (empty($this->icache[$mailbox]['index']['validated'])) {
$index = $this->icache[$mailbox]['index'];
}
// We've got a valid index
else if ($sort_field == 'ANY' || $this->icache[$mailbox]['index']['sort_field'] == $sort_field) {
$result = $this->icache[$mailbox]['index']['object'];
if ($result->get_parameters('ORDER') != $sort_order) {
$result->revert();
}
return $result;
}
}
// Get index from DB (if DB wasn't already queried)
if (empty($index) && empty($this->icache[$mailbox]['index_queried'])) {
$index = $this->get_index_row($mailbox);
// set the flag that DB was already queried for index
// this way we'll be able to skip one SELECT, when
// get_index() is called more than once
$this->icache[$mailbox]['index_queried'] = true;
}
$data = null;
// @TODO: Think about skipping validation checks.
// If we could check only every 10 minutes, we would be able to skip
// expensive checks, mailbox selection or even IMAP connection, this would require
// additional logic to force cache invalidation in some cases
// and many rcube_imap changes to connect when needed
// Entry exists, check cache status
if (!empty($index)) {
$exists = true;
if ($sort_field == 'ANY') {
$sort_field = $index['sort_field'];
}
if ($sort_field != $index['sort_field']) {
$is_valid = false;
}
else {
$is_valid = $this->validate($mailbox, $index, $exists);
}
if ($is_valid) {
$data = $index['object'];
// revert the order if needed
if ($data->get_parameters('ORDER') != $sort_order) {
$data->revert();
}
}
}
else {
if ($existing) {
return null;
}
else if ($sort_field == 'ANY') {
$sort_field = '';
}
// Got it in internal cache, so the row already exist
$exists = array_key_exists('index', $this->icache[$mailbox]);
}
// Index not found, not valid or sort field changed, get index from IMAP server
if ($data === null) {
// Get mailbox data (UIDVALIDITY, counters, etc.) for status check
$mbox_data = $this->imap->folder_data($mailbox);
$data = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data);
// insert/update
$this->add_index_row($mailbox, $sort_field, $data, $mbox_data, $exists, $index['modseq']);
}
$this->icache[$mailbox]['index'] = array(
'validated' => true,
'object' => $data,
'sort_field' => $sort_field,
'modseq' => !empty($index['modseq']) ? $index['modseq'] : $mbox_data['HIGHESTMODSEQ']
);
return $data;
}
/**
* Return messages thread.
* If threaded index doesn't exist or is invalid, will be updated.
*
* @param string $mailbox Folder name
*
* @return array Messages threaded index
*/
function get_thread($mailbox)
{
if (empty($this->icache[$mailbox])) {
$this->icache[$mailbox] = array();
}
// Seek in internal cache
if (array_key_exists('thread', $this->icache[$mailbox])) {
return $this->icache[$mailbox]['thread']['object'];
}
// Get thread from DB (if DB wasn't already queried)
if (empty($this->icache[$mailbox]['thread_queried'])) {
$index = $this->get_thread_row($mailbox);
// set the flag that DB was already queried for thread
// this way we'll be able to skip one SELECT, when
// get_thread() is called more than once or after clear()
$this->icache[$mailbox]['thread_queried'] = true;
}
// Entry exist, check cache status
if (!empty($index)) {
$exists = true;
$is_valid = $this->validate($mailbox, $index, $exists);
if (!$is_valid) {
$index = null;
}
}
// Index not found or not valid, get index from IMAP server
if ($index === null) {
// Get mailbox data (UIDVALIDITY, counters, etc.) for status check
$mbox_data = $this->imap->folder_data($mailbox);
// Get THREADS result
$index['object'] = $this->get_thread_data($mailbox, $mbox_data);
// insert/update
$this->add_thread_row($mailbox, $index['object'], $mbox_data, $exists);
}
$this->icache[$mailbox]['thread'] = $index;
return $index['object'];
}
/**
* Returns list of messages (headers). See rcube_imap::fetch_headers().
*
* @param string $mailbox Folder name
* @param array $msgs Message UIDs
*
* @return array The list of messages (rcube_message_header) indexed by UID
*/
function get_messages($mailbox, $msgs = array())
{
if (empty($msgs)) {
return array();
}
$result = array();
if ($this->mode & self::MODE_MESSAGE) {
// Fetch messages from cache
$sql_result = $this->db->query(
"SELECT `uid`, `data`, `flags`"
." FROM {$this->messages_table}"
." WHERE `user_id` = ?"
." AND `mailbox` = ?"
." AND `uid` IN (".$this->db->array2list($msgs, 'integer').")",
$this->userid, $mailbox);
$msgs = array_flip($msgs);
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$uid = intval($sql_arr['uid']);
$result[$uid] = $this->build_message($sql_arr);
if (!empty($result[$uid])) {
// save memory, we don't need message body here (?)
$result[$uid]->body = null;
unset($msgs[$uid]);
}
}
$this->db->reset();
$msgs = array_flip($msgs);
}
// Fetch not found messages from IMAP server
if (!empty($msgs)) {
$messages = $this->imap->fetch_headers($mailbox, $msgs, false, true);
// Insert to DB and add to result list
if (!empty($messages)) {
foreach ($messages as $msg) {
if ($this->mode & self::MODE_MESSAGE) {
$this->add_message($mailbox, $msg, !array_key_exists($msg->uid, $result));
}
$result[$msg->uid] = $msg;
}
}
}
return $result;
}
/**
* Returns message data.
*
* @param string $mailbox Folder name
* @param int $uid Message UID
* @param bool $update If message doesn't exists in cache it will be fetched
* from IMAP server
* @param bool $no_cache Enables internal cache usage
*
* @return rcube_message_header Message data
*/
function get_message($mailbox, $uid, $update = true, $cache = true)
{
// Check internal cache
if ($this->icache['__message']
&& $this->icache['__message']['mailbox'] == $mailbox
&& $this->icache['__message']['object']->uid == $uid
) {
return $this->icache['__message']['object'];
}
if ($this->mode & self::MODE_MESSAGE) {
$sql_result = $this->db->query(
"SELECT `flags`, `data`"
." FROM {$this->messages_table}"
." WHERE `user_id` = ?"
." AND `mailbox` = ?"
." AND `uid` = ?",
$this->userid, $mailbox, (int)$uid);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$message = $this->build_message($sql_arr);
$found = true;
}
}
// Get the message from IMAP server
if (empty($message) && $update) {
$message = $this->imap->get_message_headers($uid, $mailbox, true);
// cache will be updated in close(), see below
}
if (!($this->mode & self::MODE_MESSAGE)) {
return $message;
}
// Save the message in internal cache, will be written to DB in close()
// Common scenario: user opens unseen message
// - get message (SELECT)
// - set message headers/structure (INSERT or UPDATE)
// - set \Seen flag (UPDATE)
// This way we can skip one UPDATE
if (!empty($message) && $cache) {
// Save current message from internal cache
$this->save_icache();
$this->icache['__message'] = array(
'object' => $message,
'mailbox' => $mailbox,
'exists' => $found,
'md5sum' => md5(serialize($message)),
);
}
return $message;
}
/**
* Saves the message in cache.
*
* @param string $mailbox Folder name
* @param rcube_message_header $message Message data
* @param bool $force Skips message in-cache existence check
*/
function add_message($mailbox, $message, $force = false)
{
if (!is_object($message) || empty($message->uid)) {
return;
}
if (!($this->mode & self::MODE_MESSAGE)) {
return;
}
$flags = 0;
$msg = clone $message;
if (!empty($message->flags)) {
foreach ($this->flags as $idx => $flag) {
if (!empty($message->flags[$flag])) {
$flags += $idx;
}
}
}
unset($msg->flags);
$msg = $this->db->encode($msg, true);
// update cache record (even if it exists, the update
// here will work as select, assume row exist if affected_rows=0)
if (!$force) {
$res = $this->db->query(
"UPDATE {$this->messages_table}"
." SET `flags` = ?, `data` = ?, `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
." WHERE `user_id` = ?"
." AND `mailbox` = ?"
." AND `uid` = ?",
$flags, $msg, $this->userid, $mailbox, (int) $message->uid);
if ($this->db->affected_rows($res)) {
return;
}
}
$this->db->set_option('ignore_key_errors', true);
// insert new record
$res = $this->db->query(
"INSERT INTO {$this->messages_table}"
." (`user_id`, `mailbox`, `uid`, `flags`, `expires`, `data`)"
." VALUES (?, ?, ?, ?, ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL') . ", ?)",
$this->userid, $mailbox, (int) $message->uid, $flags, $msg);
// race-condition, insert failed so try update (#1489146)
// thanks to ignore_key_errors "duplicate row" errors will be ignored
if ($force && !$res && !$this->db->is_error($res)) {
$this->db->query(
"UPDATE {$this->messages_table}"
." SET `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
.", `flags` = ?, `data` = ?"
." WHERE `user_id` = ?"
." AND `mailbox` = ?"
." AND `uid` = ?",
$flags, $msg, $this->userid, $mailbox, (int) $message->uid);
}
$this->db->set_option('ignore_key_errors', false);
}
/**
* Sets the flag for specified message.
*
* @param string $mailbox Folder name
* @param array $uids Message UIDs or null to change flag
* of all messages in a folder
* @param string $flag The name of the flag
* @param bool $enabled Flag state
*/
function change_flag($mailbox, $uids, $flag, $enabled = false)
{
if (empty($uids)) {
return;
}
if (!($this->mode & self::MODE_MESSAGE)) {
return;
}
$flag = strtoupper($flag);
$idx = (int) array_search($flag, $this->flags);
$uids = (array) $uids;
if (!$idx) {
return;
}
// Internal cache update
if (($message = $this->icache['__message'])
&& $message['mailbox'] === $mailbox
&& in_array($message['object']->uid, $uids)
) {
$message['object']->flags[$flag] = $enabled;
if (count($uids) == 1) {
return;
}
}
$binary_check = $this->db->db_provider == 'oracle' ? "BITAND(`flags`, %d)" : "(`flags` & %d)";
$this->db->query(
"UPDATE {$this->messages_table}"
." SET `expires` = ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
.", `flags` = `flags` ".($enabled ? "+ $idx" : "- $idx")
." WHERE `user_id` = ?"
." AND `mailbox` = ?"
.(!empty($uids) ? " AND `uid` IN (".$this->db->array2list($uids, 'integer').")" : "")
." AND " . sprintf($binary_check, $idx) . ($enabled ? " = 0" : " = $idx"),
$this->userid, $mailbox);
}
/**
* Removes message(s) from cache.
*
* @param string $mailbox Folder name
* @param array $uids Message UIDs, NULL removes all messages
*/
function remove_message($mailbox = null, $uids = null)
{
if (!($this->mode & self::MODE_MESSAGE)) {
return;
}
if (!strlen($mailbox)) {
$this->db->query(
"DELETE FROM {$this->messages_table}"
." WHERE `user_id` = ?",
$this->userid);
}
else {
// Remove the message from internal cache
if (!empty($uids) && ($message = $this->icache['__message'])
&& $message['mailbox'] === $mailbox
&& in_array($message['object']->uid, (array)$uids)
) {
$this->icache['__message'] = null;
}
$this->db->query(
"DELETE FROM {$this->messages_table}"
." WHERE `user_id` = ?"
." AND `mailbox` = ?"
.($uids !== null ? " AND `uid` IN (".$this->db->array2list((array)$uids, 'integer').")" : ""),
$this->userid, $mailbox);
}
}
/**
* Clears index cache.
*
* @param string $mailbox Folder name
* @param bool $remove Enable to remove the DB row
*/
function remove_index($mailbox = null, $remove = false)
{
if (!($this->mode & self::MODE_INDEX)) {
return;
}
// The index should be only removed from database when
// UIDVALIDITY was detected or the mailbox is empty
// otherwise use 'valid' flag to not loose HIGHESTMODSEQ value
if ($remove) {
$this->db->query(
"DELETE FROM {$this->index_table}"
." WHERE `user_id` = ?"
.(strlen($mailbox) ? " AND `mailbox` = ".$this->db->quote($mailbox) : ""),
$this->userid
);
}
else {
$this->db->query(
"UPDATE {$this->index_table}"
." SET `valid` = 0"
." WHERE `user_id` = ?"
.(strlen($mailbox) ? " AND `mailbox` = ".$this->db->quote($mailbox) : ""),
$this->userid
);
}
if (strlen($mailbox)) {
unset($this->icache[$mailbox]['index']);
// Index removed, set flag to skip SELECT query in get_index()
$this->icache[$mailbox]['index_queried'] = true;
}
else {
$this->icache = array();
}
}
/**
* Clears thread cache.
*
* @param string $mailbox Folder name
*/
function remove_thread($mailbox = null)
{
if (!($this->mode & self::MODE_INDEX)) {
return;
}
$this->db->query(
"DELETE FROM {$this->thread_table}"
." WHERE `user_id` = ?"
.(strlen($mailbox) ? " AND `mailbox` = ".$this->db->quote($mailbox) : ""),
$this->userid
);
if (strlen($mailbox)) {
unset($this->icache[$mailbox]['thread']);
// Thread data removed, set flag to skip SELECT query in get_thread()
$this->icache[$mailbox]['thread_queried'] = true;
}
else {
$this->icache = array();
}
}
/**
* Clears the cache.
*
* @param string $mailbox Folder name
* @param array $uids Message UIDs, NULL removes all messages in a folder
*/
function clear($mailbox = null, $uids = null)
{
$this->remove_index($mailbox, true);
$this->remove_thread($mailbox);
$this->remove_message($mailbox, $uids);
}
/**
* Delete expired cache entries
*/
static function gc()
{
$rcube = rcube::get_instance();
$db = $rcube->get_dbh();
$now = $db->now();
$db->query("DELETE FROM " . $db->table_name('cache_messages', true)
." WHERE `expires` < $now");
$db->query("DELETE FROM " . $db->table_name('cache_index', true)
." WHERE `expires` < $now");
$db->query("DELETE FROM ".$db->table_name('cache_thread', true)
." WHERE `expires` < $now");
}
/**
* Fetches index data from database
*/
private function get_index_row($mailbox)
{
if (!($this->mode & self::MODE_INDEX)) {
return;
}
// Get index from DB
$sql_result = $this->db->query(
"SELECT `data`, `valid`"
." FROM {$this->index_table}"
." WHERE `user_id` = ?"
." AND `mailbox` = ?",
$this->userid, $mailbox);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$data = explode('@', $sql_arr['data']);
$index = $this->db->decode($data[0], true);
unset($data[0]);
if (empty($index)) {
$index = new rcube_result_index($mailbox);
}
return array(
'valid' => $sql_arr['valid'],
'object' => $index,
'sort_field' => $data[1],
'deleted' => $data[2],
'validity' => $data[3],
'uidnext' => $data[4],
'modseq' => $data[5],
);
}
}
/**
* Fetches thread data from database
*/
private function get_thread_row($mailbox)
{
if (!($this->mode & self::MODE_INDEX)) {
return;
}
// Get thread from DB
$sql_result = $this->db->query(
"SELECT `data`"
." FROM {$this->thread_table}"
." WHERE `user_id` = ?"
." AND `mailbox` = ?",
$this->userid, $mailbox);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$data = explode('@', $sql_arr['data']);
$thread = $this->db->decode($data[0], true);
unset($data[0]);
if (empty($thread)) {
$thread = new rcube_result_thread($mailbox);
}
return array(
'object' => $thread,
'deleted' => $data[1],
'validity' => $data[2],
'uidnext' => $data[3],
);
}
}
/**
* Saves index data into database
*/
private function add_index_row($mailbox, $sort_field,
$data, $mbox_data = array(), $exists = false, $modseq = null)
{
if (!($this->mode & self::MODE_INDEX)) {
return;
}
$data = array(
$this->db->encode($data, true),
$sort_field,
(int) $this->skip_deleted,
(int) $mbox_data['UIDVALIDITY'],
(int) $mbox_data['UIDNEXT'],
$modseq ? $modseq : $mbox_data['HIGHESTMODSEQ'],
);
$data = implode('@', $data);
$expires = $this->ttl ? $this->db->now($this->ttl) : 'NULL';
if ($exists) {
$res = $this->db->query(
"UPDATE {$this->index_table}"
." SET `data` = ?, `valid` = 1, `expires` = $expires"
." WHERE `user_id` = ?"
." AND `mailbox` = ?",
$data, $this->userid, $mailbox);
if ($this->db->affected_rows($res)) {
return;
}
}
$this->db->set_option('ignore_key_errors', true);
$res = $this->db->query(
"INSERT INTO {$this->index_table}"
." (`user_id`, `mailbox`, `valid`, `expires`, `data`)"
." VALUES (?, ?, 1, $expires, ?)",
$this->userid, $mailbox, $data);
// race-condition, insert failed so try update (#1489146)
// thanks to ignore_key_errors "duplicate row" errors will be ignored
if (!$exists && !$res && !$this->db->is_error($res)) {
$res = $this->db->query(
"UPDATE {$this->index_table}"
." SET `data` = ?, `valid` = 1, `expires` = $expires"
." WHERE `user_id` = ?"
." AND `mailbox` = ?",
$data, $this->userid, $mailbox);
}
$this->db->set_option('ignore_key_errors', false);
}
/**
* Saves thread data into database
*/
private function add_thread_row($mailbox, $data, $mbox_data = array(), $exists = false)
{
if (!($this->mode & self::MODE_INDEX)) {
return;
}
$data = array(
$this->db->encode($data, true),
(int) $this->skip_deleted,
(int) $mbox_data['UIDVALIDITY'],
(int) $mbox_data['UIDNEXT'],
);
$data = implode('@', $data);
$expires = $this->ttl ? $this->db->now($this->ttl) : 'NULL';
if ($exists) {
$res = $this->db->query(
"UPDATE {$this->thread_table}"
." SET `data` = ?, `expires` = $expires"
." WHERE `user_id` = ?"
." AND `mailbox` = ?",
$data, $this->userid, $mailbox);
if ($this->db->affected_rows($res)) {
return;
}
}
$this->db->set_option('ignore_key_errors', true);
$res = $this->db->query(
"INSERT INTO {$this->thread_table}"
." (`user_id`, `mailbox`, `expires`, `data`)"
." VALUES (?, ?, $expires, ?)",
$this->userid, $mailbox, $data);
// race-condition, insert failed so try update (#1489146)
// thanks to ignore_key_errors "duplicate row" errors will be ignored
if (!$exists && !$res && !$this->db->is_error($res)) {
$this->db->query(
"UPDATE {$this->thread_table}"
." SET `expires` = $expires, `data` = ?"
." WHERE `user_id` = ?"
." AND `mailbox` = ?",
$data, $this->userid, $mailbox);
}
$this->db->set_option('ignore_key_errors', false);
}
/**
* Checks index/thread validity
*/
private function validate($mailbox, $index, &$exists = true)
{
$object = $index['object'];
$is_thread = is_a($object, 'rcube_result_thread');
// sanity check
if (empty($object)) {
return false;
}
$index['validated'] = true;
// Get mailbox data (UIDVALIDITY, counters, etc.) for status check
$mbox_data = $this->imap->folder_data($mailbox);
// @TODO: Think about skipping validation checks.
// If we could check only every 10 minutes, we would be able to skip
// expensive checks, mailbox selection or even IMAP connection, this would require
// additional logic to force cache invalidation in some cases
// and many rcube_imap changes to connect when needed
// Check UIDVALIDITY
if ($index['validity'] != $mbox_data['UIDVALIDITY']) {
$this->clear($mailbox);
$exists = false;
return false;
}
// Folder is empty but cache isn't
if (empty($mbox_data['EXISTS'])) {
if (!$object->is_empty()) {
$this->clear($mailbox);
$exists = false;
return false;
}
}
// Folder is not empty but cache is
else if ($object->is_empty()) {
unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']);
return false;
}
// Validation flag
if (!$is_thread && empty($index['valid'])) {
unset($this->icache[$mailbox]['index']);
return false;
}
// Index was created with different skip_deleted setting
if ($this->skip_deleted != $index['deleted']) {
return false;
}
// Check HIGHESTMODSEQ
if (!empty($index['modseq']) && !empty($mbox_data['HIGHESTMODSEQ'])
&& $index['modseq'] == $mbox_data['HIGHESTMODSEQ']
) {
return true;
}
// Check UIDNEXT
if ($index['uidnext'] != $mbox_data['UIDNEXT']) {
unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']);
return false;
}
// @TODO: find better validity check for threaded index
if ($is_thread) {
// check messages number...
if (!$this->skip_deleted && $mbox_data['EXISTS'] != $object->count_messages()) {
return false;
}
return true;
}
// The rest of checks, more expensive
if (!empty($this->skip_deleted)) {
// compare counts if available
if (!empty($mbox_data['UNDELETED'])
&& $mbox_data['UNDELETED']->count() != $object->count()
) {
return false;
}
// compare UID sets
if (!empty($mbox_data['UNDELETED'])) {
$uids_new = $mbox_data['UNDELETED']->get();
$uids_old = $object->get();
if (count($uids_new) != count($uids_old)) {
return false;
}
sort($uids_new, SORT_NUMERIC);
sort($uids_old, SORT_NUMERIC);
if ($uids_old != $uids_new)
return false;
}
else {
// get all undeleted messages excluding cached UIDs
$ids = $this->imap->search_once($mailbox, 'ALL UNDELETED NOT UID '.
rcube_imap_generic::compressMessageSet($object->get()));
if (!$ids->is_empty()) {
return false;
}
}
}
else {
// check messages number...
if ($mbox_data['EXISTS'] != $object->count()) {
return false;
}
// ... and max UID
if ($object->max() != $this->imap->id2uid($mbox_data['EXISTS'], $mailbox)) {
return false;
}
}
return true;
}
/**
* Synchronizes the mailbox.
*
* @param string $mailbox Folder name
*/
function synchronize($mailbox)
{
// RFC4549: Synchronization Operations for Disconnected IMAP4 Clients
// RFC4551: IMAP Extension for Conditional STORE Operation
// or Quick Flag Changes Resynchronization
// RFC5162: IMAP Extensions for Quick Mailbox Resynchronization
// @TODO: synchronize with other methods?
$qresync = $this->imap->get_capability('QRESYNC');
$condstore = $qresync ? true : $this->imap->get_capability('CONDSTORE');
if (!$qresync && !$condstore) {
return;
}
// Get stored index
$index = $this->get_index_row($mailbox);
// database is empty
if (empty($index)) {
// set the flag that DB was already queried for index
// this way we'll be able to skip one SELECT in get_index()
$this->icache[$mailbox]['index_queried'] = true;
return;
}
$this->icache[$mailbox]['index'] = $index;
// no last HIGHESTMODSEQ value
if (empty($index['modseq'])) {
return;
}
if (!$this->imap->check_connection()) {
return;
}
// Enable QRESYNC
$res = $this->imap->conn->enable($qresync ? 'QRESYNC' : 'CONDSTORE');
if ($res === false) {
return;
}
// Close mailbox if already selected to get most recent data
if ($this->imap->conn->selected == $mailbox) {
$this->imap->conn->close();
}
// Get mailbox data (UIDVALIDITY, HIGHESTMODSEQ, counters, etc.)
$mbox_data = $this->imap->folder_data($mailbox);
if (empty($mbox_data)) {
return;
}
// Check UIDVALIDITY
if ($index['validity'] != $mbox_data['UIDVALIDITY']) {
$this->clear($mailbox);
return;
}
// QRESYNC not supported on specified mailbox
if (!empty($mbox_data['NOMODSEQ']) || empty($mbox_data['HIGHESTMODSEQ'])) {
return;
}
// Nothing new
if ($mbox_data['HIGHESTMODSEQ'] == $index['modseq']) {
return;
}
$uids = array();
$removed = array();
// Get known UIDs
if ($this->mode & self::MODE_MESSAGE) {
$sql_result = $this->db->query(
"SELECT `uid`"
." FROM {$this->messages_table}"
." WHERE `user_id` = ?"
." AND `mailbox` = ?",
$this->userid, $mailbox);
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$uids[] = $sql_arr['uid'];
}
}
// Synchronize messages data
if (!empty($uids)) {
// Get modified flags and vanished messages
// UID FETCH 1:* (FLAGS) (CHANGEDSINCE 0123456789 VANISHED)
$result = $this->imap->conn->fetch($mailbox,
$uids, true, array('FLAGS'), $index['modseq'], $qresync);
if (!empty($result)) {
foreach ($result as $msg) {
$uid = $msg->uid;
// Remove deleted message
if ($this->skip_deleted && !empty($msg->flags['DELETED'])) {
$removed[] = $uid;
// Invalidate index
$index['valid'] = false;
continue;
}
$flags = 0;
if (!empty($msg->flags)) {
foreach ($this->flags as $idx => $flag) {
if (!empty($msg->flags[$flag])) {
$flags += $idx;
}
}
}
$this->db->query(
"UPDATE {$this->messages_table}"
." SET `flags` = ?, `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
." WHERE `user_id` = ?"
." AND `mailbox` = ?"
." AND `uid` = ?"
." AND `flags` <> ?",
$flags, $this->userid, $mailbox, $uid, $flags);
}
}
// VANISHED found?
if ($qresync) {
$mbox_data = $this->imap->folder_data($mailbox);
// Removed messages found
$uids = rcube_imap_generic::uncompressMessageSet($mbox_data['VANISHED']);
if (!empty($uids)) {
$removed = array_merge($removed, $uids);
// Invalidate index
$index['valid'] = false;
}
}
// remove messages from database
if (!empty($removed)) {
$this->remove_message($mailbox, $removed);
}
}
$sort_field = $index['sort_field'];
$sort_order = $index['object']->get_parameters('ORDER');
$exists = true;
// Validate index
if (!$this->validate($mailbox, $index, $exists)) {
// Invalidate (remove) thread index
// if $exists=false it was already removed in validate()
if ($exists) {
$this->remove_thread($mailbox);
}
// Update index
$data = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data);
}
else {
$data = $index['object'];
}
// update index and/or HIGHESTMODSEQ value
$this->add_index_row($mailbox, $sort_field, $data, $mbox_data, $exists);
// update internal cache for get_index()
$this->icache[$mailbox]['index']['object'] = $data;
}
/**
* Converts cache row into message object.
*
* @param array $sql_arr Message row data
*
* @return rcube_message_header Message object
*/
private function build_message($sql_arr)
{
$message = $this->db->decode($sql_arr['data'], true);
if ($message) {
$message->flags = array();
foreach ($this->flags as $idx => $flag) {
if (($sql_arr['flags'] & $idx) == $idx) {
$message->flags[$flag] = true;
}
}
}
return $message;
}
/**
* Saves message stored in internal cache
*/
private function save_icache()
{
// Save current message from internal cache
if ($message = $this->icache['__message']) {
// clean up some object's data
$this->message_object_prepare($message['object']);
// calculate current md5 sum
$md5sum = md5(serialize($message['object']));
if ($message['md5sum'] != $md5sum) {
$this->add_message($message['mailbox'], $message['object'], !$message['exists']);
}
$this->icache['__message']['md5sum'] = $md5sum;
}
}
/**
* Prepares message object to be stored in database.
*
* @param rcube_message_header|rcube_message_part
*/
private function message_object_prepare(&$msg, &$size = 0)
{
// Remove body too big
if (isset($msg->body)) {
$length = strlen($msg->body);
if ($msg->body_modified || $size + $length > $this->threshold * 1024) {
unset($msg->body);
}
else {
$size += $length;
}
}
// Fix mimetype which might be broken by some code when message is displayed
// Another solution would be to use object's copy in rcube_message class
// to prevent related issues, however I'm not sure which is better
if ($msg->mimetype) {
list($msg->ctype_primary, $msg->ctype_secondary) = explode('/', $msg->mimetype);
}
unset($msg->replaces);
if (is_object($msg->structure)) {
$this->message_object_prepare($msg->structure, $size);
}
if (is_array($msg->parts)) {
foreach ($msg->parts as $part) {
$this->message_object_prepare($part, $size);
}
}
}
/**
* Fetches index data from IMAP server
*/
private function get_index_data($mailbox, $sort_field, $sort_order, $mbox_data = array())
{
if (empty($mbox_data)) {
$mbox_data = $this->imap->folder_data($mailbox);
}
if ($mbox_data['EXISTS']) {
// fetch sorted sequence numbers
$index = $this->imap->index_direct($mailbox, $sort_field, $sort_order);
}
else {
$index = new rcube_result_index($mailbox, '* SORT');
}
return $index;
}
/**
* Fetches thread data from IMAP server
*/
private function get_thread_data($mailbox, $mbox_data = array())
{
if (empty($mbox_data)) {
$mbox_data = $this->imap->folder_data($mailbox);
}
if ($mbox_data['EXISTS']) {
// get all threads (default sort order)
return $this->imap->threads_direct($mailbox);
}
return new rcube_result_thread($mailbox, '* THREAD');
}
}
// for backward compat.
class rcube_mail_header extends rcube_message_header { }
diff --git a/program/lib/Roundcube/rcube_imap_generic.php b/program/lib/Roundcube/rcube_imap_generic.php
index 17cc3a93f..3fe3f7b62 100644
--- a/program/lib/Roundcube/rcube_imap_generic.php
+++ b/program/lib/Roundcube/rcube_imap_generic.php
@@ -1,4111 +1,4112 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2015, The Roundcube Dev Team |
- | Copyright (C) 2011-2012, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide alternative IMAP library that doesn't rely on the standard |
| C-Client based version. This allows to function regardless |
| of whether or not the PHP build it's running on has IMAP |
| functionality built-in. |
| |
| Based on Iloha IMAP Library. See http://ilohamail.org/ for details |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
| Author: Ryo Chijiiwa <Ryo@IlohaMail.org> |
+-----------------------------------------------------------------------+
*/
/**
* PHP based wrapper class to connect to an IMAP server
*
* @package Framework
* @subpackage Storage
*/
class rcube_imap_generic
{
public $error;
public $errornum;
public $result;
public $resultcode;
public $selected;
public $data = array();
public $flags = array(
'SEEN' => '\\Seen',
'DELETED' => '\\Deleted',
'ANSWERED' => '\\Answered',
'DRAFT' => '\\Draft',
'FLAGGED' => '\\Flagged',
'FORWARDED' => '$Forwarded',
'MDNSENT' => '$MDNSent',
'*' => '\\*',
);
protected $fp;
protected $host;
protected $cmd_tag;
protected $cmd_num = 0;
protected $resourceid;
protected $prefs = array();
protected $logged = false;
protected $capability = array();
protected $capability_readed = false;
protected $debug = false;
protected $debug_handler = false;
const ERROR_OK = 0;
const ERROR_NO = -1;
const ERROR_BAD = -2;
const ERROR_BYE = -3;
const ERROR_UNKNOWN = -4;
const ERROR_COMMAND = -5;
const ERROR_READONLY = -6;
const COMMAND_NORESPONSE = 1;
const COMMAND_CAPABILITY = 2;
const COMMAND_LASTLINE = 4;
const COMMAND_ANONYMIZED = 8;
const DEBUG_LINE_LENGTH = 4098; // 4KB + 2B for \r\n
/**
* Send simple (one line) command to the connection stream
*
* @param string $string Command string
* @param bool $endln True if CRLF need to be added at the end of command
* @param bool $anonymized Don't write the given data to log but a placeholder
*
* @param int Number of bytes sent, False on error
*/
protected function putLine($string, $endln = true, $anonymized = false)
{
if (!$this->fp) {
return false;
}
if ($this->debug) {
// anonymize the sent command for logging
$cut = $endln ? 2 : 0;
if ($anonymized && preg_match('/^(A\d+ (?:[A-Z]+ )+)(.+)/', $string, $m)) {
$log = $m[1] . sprintf('****** [%d]', strlen($m[2]) - $cut);
}
else if ($anonymized) {
$log = sprintf('****** [%d]', strlen($string) - $cut);
}
else {
$log = rtrim($string);
}
$this->debug('C: ' . $log);
}
if ($endln) {
$string .= "\r\n";
}
$res = fwrite($this->fp, $string);
if ($res === false) {
$this->closeSocket();
}
return $res;
}
/**
* Send command to the connection stream with Command Continuation
* Requests (RFC3501 7.5) and LITERAL+ (RFC2088) support
*
* @param string $string Command string
* @param bool $endln True if CRLF need to be added at the end of command
* @param bool $anonymized Don't write the given data to log but a placeholder
*
* @return int|bool Number of bytes sent, False on error
*/
protected function putLineC($string, $endln=true, $anonymized=false)
{
if (!$this->fp) {
return false;
}
if ($endln) {
$string .= "\r\n";
}
$res = 0;
if ($parts = preg_split('/(\{[0-9]+\}\r\n)/m', $string, -1, PREG_SPLIT_DELIM_CAPTURE)) {
for ($i=0, $cnt=count($parts); $i<$cnt; $i++) {
if (preg_match('/^\{([0-9]+)\}\r\n$/', $parts[$i+1], $matches)) {
// LITERAL+ support
if ($this->prefs['literal+']) {
$parts[$i+1] = sprintf("{%d+}\r\n", $matches[1]);
}
$bytes = $this->putLine($parts[$i].$parts[$i+1], false, $anonymized);
if ($bytes === false) {
return false;
}
$res += $bytes;
// don't wait if server supports LITERAL+ capability
if (!$this->prefs['literal+']) {
$line = $this->readLine(1000);
// handle error in command
if ($line[0] != '+') {
return false;
}
}
$i++;
}
else {
$bytes = $this->putLine($parts[$i], false, $anonymized);
if ($bytes === false) {
return false;
}
$res += $bytes;
}
}
}
return $res;
}
/**
* Reads line from the connection stream
*
* @param int $size Buffer size
*
* @return string Line of text response
*/
protected function readLine($size = 1024)
{
$line = '';
if (!$size) {
$size = 1024;
}
do {
if ($this->eof()) {
return $line ?: null;
}
$buffer = fgets($this->fp, $size);
if ($buffer === false) {
$this->closeSocket();
break;
}
if ($this->debug) {
$this->debug('S: '. rtrim($buffer));
}
$line .= $buffer;
}
while (substr($buffer, -1) != "\n");
return $line;
}
/**
* Reads more data from the connection stream when provided
* data contain string literal
*
* @param string $line Response text
* @param bool $escape Enables escaping
*
* @return string Line of text response
*/
protected function multLine($line, $escape = false)
{
$line = rtrim($line);
if (preg_match('/\{([0-9]+)\}$/', $line, $m)) {
$out = '';
$str = substr($line, 0, -strlen($m[0]));
$bytes = $m[1];
while (strlen($out) < $bytes) {
$line = $this->readBytes($bytes);
if ($line === null) {
break;
}
$out .= $line;
}
$line = $str . ($escape ? $this->escape($out) : $out);
}
return $line;
}
/**
* Reads specified number of bytes from the connection stream
*
* @param int $bytes Number of bytes to get
*
* @return string Response text
*/
protected function readBytes($bytes)
{
$data = '';
$len = 0;
while ($len < $bytes && !$this->eof()) {
$d = fread($this->fp, $bytes-$len);
if ($this->debug) {
$this->debug('S: '. $d);
}
$data .= $d;
$data_len = strlen($data);
if ($len == $data_len) {
break; // nothing was read -> exit to avoid apache lockups
}
$len = $data_len;
}
return $data;
}
/**
* Reads complete response to the IMAP command
*
* @param array $untagged Will be filled with untagged response lines
*
* @return string Response text
*/
protected function readReply(&$untagged = null)
{
do {
$line = trim($this->readLine(1024));
// store untagged response lines
if ($line[0] == '*') {
$untagged[] = $line;
}
}
while ($line[0] == '*');
if ($untagged) {
$untagged = join("\n", $untagged);
}
return $line;
}
/**
* Response parser.
*
* @param string $string Response text
* @param string $err_prefix Error message prefix
*
* @return int Response status
*/
protected function parseResult($string, $err_prefix = '')
{
if (preg_match('/^[a-z0-9*]+ (OK|NO|BAD|BYE)(.*)$/i', trim($string), $matches)) {
$res = strtoupper($matches[1]);
$str = trim($matches[2]);
if ($res == 'OK') {
$this->errornum = self::ERROR_OK;
}
else if ($res == 'NO') {
$this->errornum = self::ERROR_NO;
}
else if ($res == 'BAD') {
$this->errornum = self::ERROR_BAD;
}
else if ($res == 'BYE') {
$this->closeSocket();
$this->errornum = self::ERROR_BYE;
}
if ($str) {
$str = trim($str);
// get response string and code (RFC5530)
if (preg_match("/^\[([a-z-]+)\]/i", $str, $m)) {
$this->resultcode = strtoupper($m[1]);
$str = trim(substr($str, strlen($m[1]) + 2));
}
else {
$this->resultcode = null;
// parse response for [APPENDUID 1204196876 3456]
if (preg_match("/^\[APPENDUID [0-9]+ ([0-9]+)\]/i", $str, $m)) {
$this->data['APPENDUID'] = $m[1];
}
// parse response for [COPYUID 1204196876 3456:3457 123:124]
else if (preg_match("/^\[COPYUID [0-9]+ ([0-9,:]+) ([0-9,:]+)\]/i", $str, $m)) {
$this->data['COPYUID'] = array($m[1], $m[2]);
}
}
$this->result = $str;
if ($this->errornum != self::ERROR_OK) {
$this->error = $err_prefix ? $err_prefix.$str : $str;
}
}
return $this->errornum;
}
return self::ERROR_UNKNOWN;
}
/**
* Checks connection stream state.
*
* @return bool True if connection is closed
*/
protected function eof()
{
if (!is_resource($this->fp)) {
return true;
}
// If a connection opened by fsockopen() wasn't closed
// by the server, feof() will hang.
$start = microtime(true);
if (feof($this->fp) ||
($this->prefs['timeout'] && (microtime(true) - $start > $this->prefs['timeout']))
) {
$this->closeSocket();
return true;
}
return false;
}
/**
* Closes connection stream.
*/
protected function closeSocket()
{
@fclose($this->fp);
$this->fp = null;
}
/**
* Error code/message setter.
*/
protected function setError($code, $msg = '')
{
$this->errornum = $code;
$this->error = $msg;
return $code;
}
/**
* Checks response status.
* Checks if command response line starts with specified prefix (or * BYE/BAD)
*
* @param string $string Response text
* @param string $match Prefix to match with (case-sensitive)
* @param bool $error Enables BYE/BAD checking
* @param bool $nonempty Enables empty response checking
*
* @return bool True any check is true or connection is closed.
*/
protected function startsWith($string, $match, $error = false, $nonempty = false)
{
if (!$this->fp) {
return true;
}
if (strncmp($string, $match, strlen($match)) == 0) {
return true;
}
if ($error && preg_match('/^\* (BYE|BAD) /i', $string, $m)) {
if (strtoupper($m[1]) == 'BYE') {
$this->closeSocket();
}
return true;
}
if ($nonempty && !strlen($string)) {
return true;
}
return false;
}
/**
* Capabilities checker
*/
protected function hasCapability($name)
{
if (empty($this->capability) || $name == '') {
return false;
}
if (in_array($name, $this->capability)) {
return true;
}
else if (strpos($name, '=')) {
return false;
}
$result = array();
foreach ($this->capability as $cap) {
$entry = explode('=', $cap);
if ($entry[0] == $name) {
$result[] = $entry[1];
}
}
return $result ?: false;
}
/**
* Capabilities checker
*
* @param string $name Capability name
*
* @return mixed Capability values array for key=value pairs, true/false for others
*/
public function getCapability($name)
{
$result = $this->hasCapability($name);
if (!empty($result)) {
return $result;
}
else if ($this->capability_readed) {
return false;
}
// get capabilities (only once) because initial
// optional CAPABILITY response may differ
$result = $this->execute('CAPABILITY');
if ($result[0] == self::ERROR_OK) {
$this->parseCapability($result[1]);
}
$this->capability_readed = true;
return $this->hasCapability($name);
}
/**
* Clears detected server capabilities
*/
public function clearCapability()
{
$this->capability = array();
$this->capability_readed = false;
}
/**
* DIGEST-MD5/CRAM-MD5/PLAIN Authentication
*
* @param string $user Username
* @param string $pass Password
* @param string $type Authentication type (PLAIN/CRAM-MD5/DIGEST-MD5)
*
* @return resource Connection resourse on success, error code on error
*/
protected function authenticate($user, $pass, $type = 'PLAIN')
{
if ($type == 'CRAM-MD5' || $type == 'DIGEST-MD5') {
if ($type == 'DIGEST-MD5' && !class_exists('Auth_SASL')) {
return $this->setError(self::ERROR_BYE,
"The Auth_SASL package is required for DIGEST-MD5 authentication");
}
$this->putLine($this->nextTag() . " AUTHENTICATE $type");
$line = trim($this->readReply());
if ($line[0] == '+') {
$challenge = substr($line, 2);
}
else {
return $this->parseResult($line);
}
if ($type == 'CRAM-MD5') {
// RFC2195: CRAM-MD5
$ipad = '';
$opad = '';
$xor = function($str1, $str2) {
$result = '';
$size = strlen($str1);
for ($i=0; $i<$size; $i++) {
$result .= chr(ord($str1[$i]) ^ ord($str2[$i]));
}
return $result;
};
// initialize ipad, opad
for ($i=0; $i<64; $i++) {
$ipad .= chr(0x36);
$opad .= chr(0x5C);
}
// pad $pass so it's 64 bytes
$pass = str_pad($pass, 64, chr(0));
// generate hash
$hash = md5($xor($pass, $opad) . pack("H*",
md5($xor($pass, $ipad) . base64_decode($challenge))));
$reply = base64_encode($user . ' ' . $hash);
// send result
$this->putLine($reply, true, true);
}
else {
// RFC2831: DIGEST-MD5
// proxy authorization
if (!empty($this->prefs['auth_cid'])) {
$authc = $this->prefs['auth_cid'];
$pass = $this->prefs['auth_pw'];
}
else {
$authc = $user;
$user = '';
}
$auth_sasl = new Auth_SASL;
$auth_sasl = $auth_sasl->factory('digestmd5');
$reply = base64_encode($auth_sasl->getResponse($authc, $pass,
base64_decode($challenge), $this->host, 'imap', $user));
// send result
$this->putLine($reply, true, true);
$line = trim($this->readReply());
if ($line[0] != '+') {
return $this->parseResult($line);
}
// check response
$challenge = substr($line, 2);
$challenge = base64_decode($challenge);
if (strpos($challenge, 'rspauth=') === false) {
return $this->setError(self::ERROR_BAD,
"Unexpected response from server to DIGEST-MD5 response");
}
$this->putLine('');
}
$line = $this->readReply();
$result = $this->parseResult($line);
}
else if ($type == 'GSSAPI') {
if (!extension_loaded('krb5')) {
return $this->setError(self::ERROR_BYE,
"The krb5 extension is required for GSSAPI authentication");
}
if (empty($this->prefs['gssapi_cn'])) {
return $this->setError(self::ERROR_BYE,
"The gssapi_cn parameter is required for GSSAPI authentication");
}
if (empty($this->prefs['gssapi_context'])) {
return $this->setError(self::ERROR_BYE,
"The gssapi_context parameter is required for GSSAPI authentication");
}
putenv('KRB5CCNAME=' . $this->prefs['gssapi_cn']);
try {
$ccache = new KRB5CCache();
$ccache->open($this->prefs['gssapi_cn']);
$gssapicontext = new GSSAPIContext();
$gssapicontext->acquireCredentials($ccache);
$token = '';
$success = $gssapicontext->initSecContext($this->prefs['gssapi_context'], null, null, null, $token);
$token = base64_encode($token);
}
catch (Exception $e) {
trigger_error($e->getMessage(), E_USER_WARNING);
return $this->setError(self::ERROR_BYE, "GSSAPI authentication failed");
}
$this->putLine($this->nextTag() . " AUTHENTICATE GSSAPI " . $token);
$line = trim($this->readReply());
if ($line[0] != '+') {
return $this->parseResult($line);
}
try {
$itoken = base64_decode(substr($line, 2));
if (!$gssapicontext->unwrap($itoken, $itoken)) {
throw new Exception("GSSAPI SASL input token unwrap failed");
}
if (strlen($itoken) < 4) {
throw new Exception("GSSAPI SASL input token invalid");
}
// Integrity/encryption layers are not supported. The first bit
// indicates that the server supports "no security layers".
// 0x00 should not occur, but support broken implementations.
$server_layers = ord($itoken[0]);
if ($server_layers && ($server_layers & 0x1) != 0x1) {
throw new Exception("Server requires GSSAPI SASL integrity/encryption");
}
// Construct output token. 0x01 in the first octet = SASL layer "none",
// zero in the following three octets = no data follows.
// See https://github.com/cyrusimap/cyrus-sasl/blob/e41cfb986c1b1935770de554872247453fdbb079/plugins/gssapi.c#L1284
if (!$gssapicontext->wrap(pack("CCCC", 0x1, 0, 0, 0), $otoken, true)) {
throw new Exception("GSSAPI SASL output token wrap failed");
}
}
catch (Exception $e) {
trigger_error($e->getMessage(), E_USER_WARNING);
return $this->setError(self::ERROR_BYE, "GSSAPI authentication failed");
}
$this->putLine(base64_encode($otoken));
$line = $this->readReply();
$result = $this->parseResult($line);
}
else if ($type == 'PLAIN') {
// proxy authorization
if (!empty($this->prefs['auth_cid'])) {
$authc = $this->prefs['auth_cid'];
$pass = $this->prefs['auth_pw'];
}
else {
$authc = $user;
$user = '';
}
$reply = base64_encode($user . chr(0) . $authc . chr(0) . $pass);
// RFC 4959 (SASL-IR): save one round trip
if ($this->getCapability('SASL-IR')) {
list($result, $line) = $this->execute("AUTHENTICATE PLAIN", array($reply),
self::COMMAND_LASTLINE | self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED);
}
else {
$this->putLine($this->nextTag() . " AUTHENTICATE PLAIN");
$line = trim($this->readReply());
if ($line[0] != '+') {
return $this->parseResult($line);
}
// send result, get reply and process it
$this->putLine($reply, true, true);
$line = $this->readReply();
$result = $this->parseResult($line);
}
}
else if ($type == 'LOGIN') {
$this->putLine($this->nextTag() . " AUTHENTICATE LOGIN");
$line = trim($this->readReply());
if ($line[0] != '+') {
return $this->parseResult($line);
}
$this->putLine(base64_encode($user), true, true);
$line = trim($this->readReply());
if ($line[0] != '+') {
return $this->parseResult($line);
}
// send result, get reply and process it
$this->putLine(base64_encode($pass), true, true);
$line = $this->readReply();
$result = $this->parseResult($line);
}
if ($result === self::ERROR_OK) {
// optional CAPABILITY response
if ($line && preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
$this->parseCapability($matches[1], true);
}
return $this->fp;
}
return $this->setError($result, "AUTHENTICATE $type: $line");
}
/**
* LOGIN Authentication
*
* @param string $user Username
* @param string $pass Password
*
* @return resource Connection resourse on success, error code on error
*/
protected function login($user, $password)
{
// Prevent from sending credentials in plain text when connection is not secure
if ($this->getCapability('LOGINDISABLED')) {
return $this->setError(self::ERROR_BAD, "Login disabled by IMAP server");
}
list($code, $response) = $this->execute('LOGIN', array(
$this->escape($user), $this->escape($password)), self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED);
// re-set capabilities list if untagged CAPABILITY response provided
if (preg_match('/\* CAPABILITY (.+)/i', $response, $matches)) {
$this->parseCapability($matches[1], true);
}
if ($code == self::ERROR_OK) {
return $this->fp;
}
return $code;
}
/**
* Detects hierarchy delimiter
*
* @return string The delimiter
*/
public function getHierarchyDelimiter()
{
if ($this->prefs['delimiter']) {
return $this->prefs['delimiter'];
}
// try (LIST "" ""), should return delimiter (RFC2060 Sec 6.3.8)
list($code, $response) = $this->execute('LIST',
array($this->escape(''), $this->escape('')));
if ($code == self::ERROR_OK) {
$args = $this->tokenizeResponse($response, 4);
$delimiter = $args[3];
if (strlen($delimiter) > 0) {
return ($this->prefs['delimiter'] = $delimiter);
}
}
}
/**
* NAMESPACE handler (RFC 2342)
*
* @return array Namespace data hash (personal, other, shared)
*/
public function getNamespace()
{
if (array_key_exists('namespace', $this->prefs)) {
return $this->prefs['namespace'];
}
if (!$this->getCapability('NAMESPACE')) {
return self::ERROR_BAD;
}
list($code, $response) = $this->execute('NAMESPACE');
if ($code == self::ERROR_OK && preg_match('/^\* NAMESPACE /', $response)) {
$response = substr($response, 11);
$data = $this->tokenizeResponse($response);
}
if (!is_array($data)) {
return $code;
}
$this->prefs['namespace'] = array(
'personal' => $data[0],
'other' => $data[1],
'shared' => $data[2],
);
return $this->prefs['namespace'];
}
/**
* Connects to IMAP server and authenticates.
*
* @param string $host Server hostname or IP
* @param string $user User name
* @param string $password Password
* @param array $options Connection and class options
*
* @return bool True on success, False on failure
*/
public function connect($host, $user, $password, $options = array())
{
// configure
$this->set_prefs($options);
$this->host = $host;
$this->user = $user;
$this->logged = false;
$this->selected = null;
// check input
if (empty($host)) {
$this->setError(self::ERROR_BAD, "Empty host");
return false;
}
if (empty($user)) {
$this->setError(self::ERROR_NO, "Empty user");
return false;
}
if (empty($password) && empty($options['gssapi_cn'])) {
$this->setError(self::ERROR_NO, "Empty password");
return false;
}
// Connect
if (!$this->_connect($host)) {
return false;
}
// Send ID info
if (!empty($this->prefs['ident']) && $this->getCapability('ID')) {
$this->data['ID'] = $this->id($this->prefs['ident']);
}
$auth_method = $this->prefs['auth_type'];
$auth_methods = array();
$result = null;
// check for supported auth methods
if (!$auth_method || $auth_method == 'CHECK') {
if ($auth_caps = $this->getCapability('AUTH')) {
$auth_methods = $auth_caps;
}
// Use best (for security) supported authentication method
$all_methods = array('DIGEST-MD5', 'CRAM-MD5', 'CRAM_MD5', 'PLAIN', 'LOGIN');
if (!empty($this->prefs['gssapi_cn'])) {
array_unshift($all_methods, 'GSSAPI');
}
foreach ($all_methods as $auth_method) {
if (in_array($auth_method, $auth_methods)) {
break;
}
}
// Prefer LOGIN over AUTHENTICATE LOGIN for performance reasons
if ($auth_method == 'LOGIN' && !$this->getCapability('LOGINDISABLED')) {
$auth_method = 'IMAP';
}
}
// pre-login capabilities can be not complete
$this->capability_readed = false;
// Authenticate
switch ($auth_method) {
case 'CRAM_MD5':
$auth_method = 'CRAM-MD5';
case 'CRAM-MD5':
case 'DIGEST-MD5':
case 'GSSAPI':
case 'PLAIN':
case 'LOGIN':
$result = $this->authenticate($user, $password, $auth_method);
break;
case 'IMAP':
$result = $this->login($user, $password);
break;
default:
$this->setError(self::ERROR_BAD, "Configuration error. Unknown auth method: $auth_method");
}
// Connected and authenticated
if (is_resource($result)) {
if ($this->prefs['force_caps']) {
$this->clearCapability();
}
$this->logged = true;
return true;
}
$this->closeConnection();
return false;
}
/**
* Connects to IMAP server.
*
* @param string $host Server hostname or IP
*
* @return bool True on success, False on failure
*/
protected function _connect($host)
{
// initialize connection
$this->error = '';
$this->errornum = self::ERROR_OK;
if (!$this->prefs['port']) {
$this->prefs['port'] = 143;
}
// check for SSL
if ($this->prefs['ssl_mode'] && $this->prefs['ssl_mode'] != 'tls') {
$host = $this->prefs['ssl_mode'] . '://' . $host;
}
if ($this->prefs['timeout'] <= 0) {
$this->prefs['timeout'] = max(0, intval(ini_get('default_socket_timeout')));
}
if ($this->debug) {
// set connection identifier for debug output
$this->resourceid = strtoupper(substr(md5(microtime() . $host . $this->user), 0, 4));
$_host = ($this->prefs['ssl_mode'] == 'tls' ? 'tls://' : '') . $host . ':' . $this->prefs['port'];
$this->debug("Connecting to $_host...");
}
if (!empty($this->prefs['socket_options'])) {
$context = stream_context_create($this->prefs['socket_options']);
$this->fp = stream_socket_client($host . ':' . $this->prefs['port'], $errno, $errstr,
$this->prefs['timeout'], STREAM_CLIENT_CONNECT, $context);
}
else {
$this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']);
}
if (!$this->fp) {
$this->setError(self::ERROR_BAD, sprintf("Could not connect to %s:%d: %s",
$host, $this->prefs['port'], $errstr ?: "Unknown reason"));
return false;
}
if ($this->prefs['timeout'] > 0) {
stream_set_timeout($this->fp, $this->prefs['timeout']);
}
$line = trim(fgets($this->fp, 8192));
if ($this->debug && $line) {
$this->debug('S: '. $line);
}
// Connected to wrong port or connection error?
if (!preg_match('/^\* (OK|PREAUTH)/i', $line)) {
if ($line)
$error = sprintf("Wrong startup greeting (%s:%d): %s", $host, $this->prefs['port'], $line);
else
$error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']);
$this->setError(self::ERROR_BAD, $error);
$this->closeConnection();
return false;
}
$this->data['GREETING'] = trim(preg_replace('/\[[^\]]+\]\s*/', '', $line));
// RFC3501 [7.1] optional CAPABILITY response
if (preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
$this->parseCapability($matches[1], true);
}
// TLS connection
if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) {
$res = $this->execute('STARTTLS');
if ($res[0] != self::ERROR_OK) {
$this->closeConnection();
return false;
}
if (isset($this->prefs['socket_options']['ssl']['crypto_method'])) {
$crypto_method = $this->prefs['socket_options']['ssl']['crypto_method'];
}
else {
// There is no flag to enable all TLS methods. Net_SMTP
// handles enabling TLS similarly.
$crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT
| @STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT
| @STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
}
if (!stream_socket_enable_crypto($this->fp, true, $crypto_method)) {
$this->setError(self::ERROR_BAD, "Unable to negotiate TLS");
$this->closeConnection();
return false;
}
// Now we're secure, capabilities need to be reread
$this->clearCapability();
}
return true;
}
/**
* Initializes environment
*/
protected function set_prefs($prefs)
{
// set preferences
if (is_array($prefs)) {
$this->prefs = $prefs;
}
// set auth method
if (!empty($this->prefs['auth_type'])) {
$this->prefs['auth_type'] = strtoupper($this->prefs['auth_type']);
}
else {
$this->prefs['auth_type'] = 'CHECK';
}
// disabled capabilities
if (!empty($this->prefs['disabled_caps'])) {
$this->prefs['disabled_caps'] = array_map('strtoupper', (array)$this->prefs['disabled_caps']);
}
// additional message flags
if (!empty($this->prefs['message_flags'])) {
$this->flags = array_merge($this->flags, $this->prefs['message_flags']);
unset($this->prefs['message_flags']);
}
}
/**
* Checks connection status
*
* @return bool True if connection is active and user is logged in, False otherwise.
*/
public function connected()
{
return $this->fp && $this->logged;
}
/**
* Closes connection with logout.
*/
public function closeConnection()
{
if ($this->logged && $this->putLine($this->nextTag() . ' LOGOUT')) {
$this->readReply();
}
$this->closeSocket();
}
/**
* Executes SELECT command (if mailbox is already not in selected state)
*
* @param string $mailbox Mailbox name
* @param array $qresync_data QRESYNC data (RFC5162)
*
* @return boolean True on success, false on error
*/
public function select($mailbox, $qresync_data = null)
{
if (!strlen($mailbox)) {
return false;
}
if ($this->selected === $mailbox) {
return true;
}
$params = array($this->escape($mailbox));
// QRESYNC data items
// 0. the last known UIDVALIDITY,
// 1. the last known modification sequence,
// 2. the optional set of known UIDs, and
// 3. an optional parenthesized list of known sequence ranges and their
// corresponding UIDs.
if (!empty($qresync_data)) {
if (!empty($qresync_data[2])) {
$qresync_data[2] = self::compressMessageSet($qresync_data[2]);
}
$params[] = array('QRESYNC', $qresync_data);
}
list($code, $response) = $this->execute('SELECT', $params);
if ($code == self::ERROR_OK) {
$this->clear_mailbox_cache();
$response = explode("\r\n", $response);
foreach ($response as $line) {
if (preg_match('/^\* OK \[/i', $line)) {
$pos = strcspn($line, ' ]', 6);
$token = strtoupper(substr($line, 6, $pos));
$pos += 7;
switch ($token) {
case 'UIDNEXT':
case 'UIDVALIDITY':
case 'UNSEEN':
if ($len = strspn($line, '0123456789', $pos)) {
$this->data[$token] = (int) substr($line, $pos, $len);
}
break;
case 'HIGHESTMODSEQ':
if ($len = strspn($line, '0123456789', $pos)) {
$this->data[$token] = (string) substr($line, $pos, $len);
}
break;
case 'NOMODSEQ':
$this->data[$token] = true;
break;
case 'PERMANENTFLAGS':
$start = strpos($line, '(', $pos);
$end = strrpos($line, ')');
if ($start && $end) {
$flags = substr($line, $start + 1, $end - $start - 1);
$this->data[$token] = explode(' ', $flags);
}
break;
}
}
else if (preg_match('/^\* ([0-9]+) (EXISTS|RECENT|FETCH)/i', $line, $match)) {
$token = strtoupper($match[2]);
switch ($token) {
case 'EXISTS':
case 'RECENT':
$this->data[$token] = (int) $match[1];
break;
case 'FETCH':
// QRESYNC FETCH response (RFC5162)
$line = substr($line, strlen($match[0]));
$fetch_data = $this->tokenizeResponse($line, 1);
$data = array('id' => $match[1]);
for ($i=0, $size=count($fetch_data); $i<$size; $i+=2) {
$data[strtolower($fetch_data[$i])] = $fetch_data[$i+1];
}
$this->data['QRESYNC'][$data['uid']] = $data;
break;
}
}
// QRESYNC VANISHED response (RFC5162)
else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) {
$line = substr($line, strlen($match[0]));
$v_data = $this->tokenizeResponse($line, 1);
$this->data['VANISHED'] = $v_data;
}
}
$this->data['READ-WRITE'] = $this->resultcode != 'READ-ONLY';
$this->selected = $mailbox;
return true;
}
return false;
}
/**
* Executes STATUS command
*
* @param string $mailbox Mailbox name
* @param array $items Additional requested item names. By default
* MESSAGES and UNSEEN are requested. Other defined
* in RFC3501: UIDNEXT, UIDVALIDITY, RECENT
*
* @return array Status item-value hash
* @since 0.5-beta
*/
public function status($mailbox, $items = array())
{
if (!strlen($mailbox)) {
return false;
}
if (!in_array('MESSAGES', $items)) {
$items[] = 'MESSAGES';
}
if (!in_array('UNSEEN', $items)) {
$items[] = 'UNSEEN';
}
list($code, $response) = $this->execute('STATUS',
array($this->escape($mailbox), '(' . implode(' ', $items) . ')'), 0, '/^\* STATUS /i');
if ($code == self::ERROR_OK && $response) {
$result = array();
$response = substr($response, 9); // remove prefix "* STATUS "
list($mbox, $items) = $this->tokenizeResponse($response, 2);
// Fix for #1487859. Some buggy server returns not quoted
// folder name with spaces. Let's try to handle this situation
if (!is_array($items) && ($pos = strpos($response, '(')) !== false) {
$response = substr($response, $pos);
$items = $this->tokenizeResponse($response, 1);
}
if (!is_array($items)) {
return $result;
}
for ($i=0, $len=count($items); $i<$len; $i += 2) {
$result[$items[$i]] = $items[$i+1];
}
$this->data['STATUS:'.$mailbox] = $result;
return $result;
}
return false;
}
/**
* Executes EXPUNGE command
*
* @param string $mailbox Mailbox name
* @param string|array $messages Message UIDs to expunge
*
* @return boolean True on success, False on error
*/
public function expunge($mailbox, $messages = null)
{
if (!$this->select($mailbox)) {
return false;
}
if (!$this->data['READ-WRITE']) {
$this->setError(self::ERROR_READONLY, "Mailbox is read-only");
return false;
}
// Clear internal status cache
$this->clear_status_cache($mailbox);
if (!empty($messages) && $messages != '*' && $this->hasCapability('UIDPLUS')) {
$messages = self::compressMessageSet($messages);
$result = $this->execute('UID EXPUNGE', array($messages), self::COMMAND_NORESPONSE);
}
else {
$result = $this->execute('EXPUNGE', null, self::COMMAND_NORESPONSE);
}
if ($result == self::ERROR_OK) {
$this->selected = null; // state has changed, need to reselect
return true;
}
return false;
}
/**
* Executes CLOSE command
*
* @return boolean True on success, False on error
* @since 0.5
*/
public function close()
{
$result = $this->execute('CLOSE', null, self::COMMAND_NORESPONSE);
if ($result == self::ERROR_OK) {
$this->selected = null;
return true;
}
return false;
}
/**
* Folder subscription (SUBSCRIBE)
*
* @param string $mailbox Mailbox name
*
* @return boolean True on success, False on error
*/
public function subscribe($mailbox)
{
$result = $this->execute('SUBSCRIBE', array($this->escape($mailbox)),
self::COMMAND_NORESPONSE);
return $result == self::ERROR_OK;
}
/**
* Folder unsubscription (UNSUBSCRIBE)
*
* @param string $mailbox Mailbox name
*
* @return boolean True on success, False on error
*/
public function unsubscribe($mailbox)
{
$result = $this->execute('UNSUBSCRIBE', array($this->escape($mailbox)),
self::COMMAND_NORESPONSE);
return $result == self::ERROR_OK;
}
/**
* Folder creation (CREATE)
*
* @param string $mailbox Mailbox name
* @param array $types Optional folder types (RFC 6154)
*
* @return bool True on success, False on error
*/
public function createFolder($mailbox, $types = null)
{
$args = array($this->escape($mailbox));
// RFC 6154: CREATE-SPECIAL-USE
if (!empty($types) && $this->getCapability('CREATE-SPECIAL-USE')) {
$args[] = '(USE (' . implode(' ', $types) . '))';
}
$result = $this->execute('CREATE', $args, self::COMMAND_NORESPONSE);
return $result == self::ERROR_OK;
}
/**
* Folder renaming (RENAME)
*
* @param string $mailbox Mailbox name
*
* @return bool True on success, False on error
*/
public function renameFolder($from, $to)
{
$result = $this->execute('RENAME', array($this->escape($from), $this->escape($to)),
self::COMMAND_NORESPONSE);
return $result == self::ERROR_OK;
}
/**
* Executes DELETE command
*
* @param string $mailbox Mailbox name
*
* @return boolean True on success, False on error
*/
public function deleteFolder($mailbox)
{
$result = $this->execute('DELETE', array($this->escape($mailbox)),
self::COMMAND_NORESPONSE);
return $result == self::ERROR_OK;
}
/**
* Removes all messages in a folder
*
* @param string $mailbox Mailbox name
*
* @return boolean True on success, False on error
*/
public function clearFolder($mailbox)
{
if ($this->countMessages($mailbox) > 0) {
$res = $this->flag($mailbox, '1:*', 'DELETED');
}
if ($res) {
if ($this->selected === $mailbox) {
$res = $this->close();
}
else {
$res = $this->expunge($mailbox);
}
}
return $res;
}
/**
* Returns list of mailboxes
*
* @param string $ref Reference name
* @param string $mailbox Mailbox name
* @param array $return_opts (see self::_listMailboxes)
* @param array $select_opts (see self::_listMailboxes)
*
* @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
* is requested, False on error.
*/
public function listMailboxes($ref, $mailbox, $return_opts = array(), $select_opts = array())
{
return $this->_listMailboxes($ref, $mailbox, false, $return_opts, $select_opts);
}
/**
* Returns list of subscribed mailboxes
*
* @param string $ref Reference name
* @param string $mailbox Mailbox name
* @param array $return_opts (see self::_listMailboxes)
*
* @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
* is requested, False on error.
*/
public function listSubscribed($ref, $mailbox, $return_opts = array())
{
return $this->_listMailboxes($ref, $mailbox, true, $return_opts, null);
}
/**
* IMAP LIST/LSUB command
*
* @param string $ref Reference name
* @param string $mailbox Mailbox name
* @param bool $subscribed Enables returning subscribed mailboxes only
* @param array $return_opts List of RETURN options (RFC5819: LIST-STATUS, RFC5258: LIST-EXTENDED)
* Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN,
* MYRIGHTS, SUBSCRIBED, CHILDREN
* @param array $select_opts List of selection options (RFC5258: LIST-EXTENDED)
* Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE,
* SPECIAL-USE (RFC6154)
*
* @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
* is requested, False on error.
*/
protected function _listMailboxes($ref, $mailbox, $subscribed=false,
$return_opts=array(), $select_opts=array())
{
if (!strlen($mailbox)) {
$mailbox = '*';
}
$args = array();
$rets = array();
if (!empty($select_opts) && $this->getCapability('LIST-EXTENDED')) {
$select_opts = (array) $select_opts;
$args[] = '(' . implode(' ', $select_opts) . ')';
}
$args[] = $this->escape($ref);
$args[] = $this->escape($mailbox);
if (!empty($return_opts) && $this->getCapability('LIST-EXTENDED')) {
$ext_opts = array('SUBSCRIBED', 'CHILDREN');
$rets = array_intersect($return_opts, $ext_opts);
$return_opts = array_diff($return_opts, $rets);
}
if (!empty($return_opts) && $this->getCapability('LIST-STATUS')) {
$lstatus = true;
$status_opts = array('MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN');
$opts = array_diff($return_opts, $status_opts);
$status_opts = array_diff($return_opts, $opts);
if (!empty($status_opts)) {
$rets[] = 'STATUS (' . implode(' ', $status_opts) . ')';
}
if (!empty($opts)) {
$rets = array_merge($rets, $opts);
}
}
if (!empty($rets)) {
$args[] = 'RETURN (' . implode(' ', $rets) . ')';
}
list($code, $response) = $this->execute($subscribed ? 'LSUB' : 'LIST', $args);
if ($code == self::ERROR_OK) {
$folders = array();
$last = 0;
$pos = 0;
$response .= "\r\n";
while ($pos = strpos($response, "\r\n", $pos+1)) {
// literal string, not real end-of-command-line
if ($response[$pos-1] == '}') {
continue;
}
$line = substr($response, $last, $pos - $last);
$last = $pos + 2;
if (!preg_match('/^\* (LIST|LSUB|STATUS|MYRIGHTS) /i', $line, $m)) {
continue;
}
$cmd = strtoupper($m[1]);
$line = substr($line, strlen($m[0]));
// * LIST (<options>) <delimiter> <mailbox>
if ($cmd == 'LIST' || $cmd == 'LSUB') {
list($opts, $delim, $mailbox) = $this->tokenizeResponse($line, 3);
// Remove redundant separator at the end of folder name, UW-IMAP bug? (#1488879)
if ($delim) {
$mailbox = rtrim($mailbox, $delim);
}
// Add to result array
if (!$lstatus) {
$folders[] = $mailbox;
}
else {
$folders[$mailbox] = array();
}
// store folder options
if ($cmd == 'LIST') {
// Add to options array
if (empty($this->data['LIST'][$mailbox])) {
$this->data['LIST'][$mailbox] = $opts;
}
else if (!empty($opts)) {
$this->data['LIST'][$mailbox] = array_unique(array_merge(
$this->data['LIST'][$mailbox], $opts));
}
}
}
else if ($lstatus) {
// * STATUS <mailbox> (<result>)
if ($cmd == 'STATUS') {
list($mailbox, $status) = $this->tokenizeResponse($line, 2);
for ($i=0, $len=count($status); $i<$len; $i += 2) {
list($name, $value) = $this->tokenizeResponse($status, 2);
$folders[$mailbox][$name] = $value;
}
}
// * MYRIGHTS <mailbox> <acl>
else if ($cmd == 'MYRIGHTS') {
list($mailbox, $acl) = $this->tokenizeResponse($line, 2);
$folders[$mailbox]['MYRIGHTS'] = $acl;
}
}
}
return $folders;
}
return false;
}
/**
* Returns count of all messages in a folder
*
* @param string $mailbox Mailbox name
*
* @return int Number of messages, False on error
*/
public function countMessages($mailbox)
{
if ($this->selected === $mailbox && isset($this->data['EXISTS'])) {
return $this->data['EXISTS'];
}
// Check internal cache
$cache = $this->data['STATUS:'.$mailbox];
if (!empty($cache) && isset($cache['MESSAGES'])) {
return (int) $cache['MESSAGES'];
}
// Try STATUS (should be faster than SELECT)
$counts = $this->status($mailbox);
if (is_array($counts)) {
return (int) $counts['MESSAGES'];
}
return false;
}
/**
* Returns count of messages with \Recent flag in a folder
*
* @param string $mailbox Mailbox name
*
* @return int Number of messages, False on error
*/
public function countRecent($mailbox)
{
if ($this->selected === $mailbox && isset($this->data['RECENT'])) {
return $this->data['RECENT'];
}
// Check internal cache
$cache = $this->data['STATUS:'.$mailbox];
if (!empty($cache) && isset($cache['RECENT'])) {
return (int) $cache['RECENT'];
}
// Try STATUS (should be faster than SELECT)
$counts = $this->status($mailbox, array('RECENT'));
if (is_array($counts)) {
return (int) $counts['RECENT'];
}
return false;
}
/**
* Returns count of messages without \Seen flag in a specified folder
*
* @param string $mailbox Mailbox name
*
* @return int Number of messages, False on error
*/
public function countUnseen($mailbox)
{
// Check internal cache
$cache = $this->data['STATUS:'.$mailbox];
if (!empty($cache) && isset($cache['UNSEEN'])) {
return (int) $cache['UNSEEN'];
}
// Try STATUS (should be faster than SELECT+SEARCH)
$counts = $this->status($mailbox);
if (is_array($counts)) {
return (int) $counts['UNSEEN'];
}
// Invoke SEARCH as a fallback
$index = $this->search($mailbox, 'ALL UNSEEN', false, array('COUNT'));
if (!$index->is_error()) {
return $index->count();
}
return false;
}
/**
* Executes ID command (RFC2971)
*
* @param array $items Client identification information key/value hash
*
* @return array Server identification information key/value hash
* @since 0.6
*/
public function id($items = array())
{
if (is_array($items) && !empty($items)) {
foreach ($items as $key => $value) {
$args[] = $this->escape($key, true);
$args[] = $this->escape($value, true);
}
}
list($code, $response) = $this->execute('ID',
array(!empty($args) ? '(' . implode(' ', (array) $args) . ')' : $this->escape(null)),
0, '/^\* ID /i');
if ($code == self::ERROR_OK && $response) {
$response = substr($response, 5); // remove prefix "* ID "
$items = $this->tokenizeResponse($response, 1);
$result = null;
for ($i=0, $len=count($items); $i<$len; $i += 2) {
$result[$items[$i]] = $items[$i+1];
}
return $result;
}
return false;
}
/**
* Executes ENABLE command (RFC5161)
*
* @param mixed $extension Extension name to enable (or array of names)
*
* @return array|bool List of enabled extensions, False on error
* @since 0.6
*/
public function enable($extension)
{
if (empty($extension)) {
return false;
}
if (!$this->hasCapability('ENABLE')) {
return false;
}
if (!is_array($extension)) {
$extension = array($extension);
}
if (!empty($this->extensions_enabled)) {
// check if all extensions are already enabled
$diff = array_diff($extension, $this->extensions_enabled);
if (empty($diff)) {
return $extension;
}
// Make sure the mailbox isn't selected, before enabling extension(s)
if ($this->selected !== null) {
$this->close();
}
}
list($code, $response) = $this->execute('ENABLE', $extension, 0, '/^\* ENABLED /i');
if ($code == self::ERROR_OK && $response) {
$response = substr($response, 10); // remove prefix "* ENABLED "
$result = (array) $this->tokenizeResponse($response);
$this->extensions_enabled = array_unique(array_merge((array)$this->extensions_enabled, $result));
return $this->extensions_enabled;
}
return false;
}
/**
* Executes SORT command
*
* @param string $mailbox Mailbox name
* @param string $field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
* @param string $criteria Searching criteria
* @param bool $return_uid Enables UID SORT usage
* @param string $encoding Character set
*
* @return rcube_result_index Response data
*/
public function sort($mailbox, $field = 'ARRIVAL', $criteria = '', $return_uid = false, $encoding = 'US-ASCII')
{
$old_sel = $this->selected;
$supported = array('ARRIVAL', 'CC', 'DATE', 'FROM', 'SIZE', 'SUBJECT', 'TO');
$field = strtoupper($field);
if ($field == 'INTERNALDATE') {
$field = 'ARRIVAL';
}
if (!in_array($field, $supported)) {
return new rcube_result_index($mailbox);
}
if (!$this->select($mailbox)) {
return new rcube_result_index($mailbox);
}
// return empty result when folder is empty and we're just after SELECT
if ($old_sel != $mailbox && !$this->data['EXISTS']) {
return new rcube_result_index($mailbox, '* SORT');
}
// RFC 5957: SORT=DISPLAY
if (($field == 'FROM' || $field == 'TO') && $this->getCapability('SORT=DISPLAY')) {
$field = 'DISPLAY' . $field;
}
$encoding = $encoding ? trim($encoding) : 'US-ASCII';
$criteria = $criteria ? 'ALL ' . trim($criteria) : 'ALL';
list($code, $response) = $this->execute($return_uid ? 'UID SORT' : 'SORT',
array("($field)", $encoding, $criteria));
if ($code != self::ERROR_OK) {
$response = null;
}
return new rcube_result_index($mailbox, $response);
}
/**
* Executes THREAD command
*
* @param string $mailbox Mailbox name
* @param string $algorithm Threading algorithm (ORDEREDSUBJECT, REFERENCES, REFS)
* @param string $criteria Searching criteria
* @param bool $return_uid Enables UIDs in result instead of sequence numbers
* @param string $encoding Character set
*
* @return rcube_result_thread Thread data
*/
public function thread($mailbox, $algorithm = 'REFERENCES', $criteria = '', $return_uid = false, $encoding = 'US-ASCII')
{
$old_sel = $this->selected;
if (!$this->select($mailbox)) {
return new rcube_result_thread($mailbox);
}
// return empty result when folder is empty and we're just after SELECT
if ($old_sel != $mailbox && !$this->data['EXISTS']) {
return new rcube_result_thread($mailbox, '* THREAD');
}
$encoding = $encoding ? trim($encoding) : 'US-ASCII';
$algorithm = $algorithm ? trim($algorithm) : 'REFERENCES';
$criteria = $criteria ? 'ALL '.trim($criteria) : 'ALL';
list($code, $response) = $this->execute($return_uid ? 'UID THREAD' : 'THREAD',
array($algorithm, $encoding, $criteria));
if ($code != self::ERROR_OK) {
$response = null;
}
return new rcube_result_thread($mailbox, $response);
}
/**
* Executes SEARCH command
*
* @param string $mailbox Mailbox name
* @param string $criteria Searching criteria
* @param bool $return_uid Enable UID in result instead of sequence ID
* @param array $items Return items (MIN, MAX, COUNT, ALL)
*
* @return rcube_result_index Result data
*/
public function search($mailbox, $criteria, $return_uid = false, $items = array())
{
$old_sel = $this->selected;
if (!$this->select($mailbox)) {
return new rcube_result_index($mailbox);
}
// return empty result when folder is empty and we're just after SELECT
if ($old_sel != $mailbox && !$this->data['EXISTS']) {
return new rcube_result_index($mailbox, '* SEARCH');
}
// If ESEARCH is supported always use ALL
// but not when items are specified or using simple id2uid search
if (empty($items) && preg_match('/[^0-9]/', $criteria)) {
$items = array('ALL');
}
$esearch = empty($items) ? false : $this->getCapability('ESEARCH');
$criteria = trim($criteria);
$params = '';
// RFC4731: ESEARCH
if (!empty($items) && $esearch) {
$params .= 'RETURN (' . implode(' ', $items) . ')';
}
if (!empty($criteria)) {
$params .= ($params ? ' ' : '') . $criteria;
}
else {
$params .= 'ALL';
}
list($code, $response) = $this->execute($return_uid ? 'UID SEARCH' : 'SEARCH',
array($params));
if ($code != self::ERROR_OK) {
$response = null;
}
return new rcube_result_index($mailbox, $response);
}
/**
* Simulates SORT command by using FETCH and sorting.
*
* @param string $mailbox Mailbox name
* @param string|array $message_set Searching criteria (list of messages to return)
* @param string $index_field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
* @param bool $skip_deleted Makes that DELETED messages will be skipped
* @param bool $uidfetch Enables UID FETCH usage
* @param bool $return_uid Enables returning UIDs instead of IDs
*
* @return rcube_result_index Response data
*/
public function index($mailbox, $message_set, $index_field='', $skip_deleted=true,
$uidfetch=false, $return_uid=false)
{
$msg_index = $this->fetchHeaderIndex($mailbox, $message_set,
$index_field, $skip_deleted, $uidfetch, $return_uid);
if (!empty($msg_index)) {
asort($msg_index); // ASC
$msg_index = array_keys($msg_index);
$msg_index = '* SEARCH ' . implode(' ', $msg_index);
}
else {
$msg_index = is_array($msg_index) ? '* SEARCH' : null;
}
return new rcube_result_index($mailbox, $msg_index);
}
/**
* Fetches specified header/data value for a set of messages.
*
* @param string $mailbox Mailbox name
* @param string|array $message_set Searching criteria (list of messages to return)
* @param string $index_field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
* @param bool $skip_deleted Makes that DELETED messages will be skipped
* @param bool $uidfetch Enables UID FETCH usage
* @param bool $return_uid Enables returning UIDs instead of IDs
*
* @return array|bool List of header values or False on failure
*/
public function fetchHeaderIndex($mailbox, $message_set, $index_field = '', $skip_deleted = true,
$uidfetch = false, $return_uid = false)
{
if (is_array($message_set)) {
if (!($message_set = $this->compressMessageSet($message_set))) {
return false;
}
}
else {
list($from_idx, $to_idx) = explode(':', $message_set);
if (empty($message_set) ||
(isset($to_idx) && $to_idx != '*' && (int)$from_idx > (int)$to_idx)
) {
return false;
}
}
$index_field = empty($index_field) ? 'DATE' : strtoupper($index_field);
$fields_a['DATE'] = 1;
$fields_a['INTERNALDATE'] = 4;
$fields_a['ARRIVAL'] = 4;
$fields_a['FROM'] = 1;
$fields_a['REPLY-TO'] = 1;
$fields_a['SENDER'] = 1;
$fields_a['TO'] = 1;
$fields_a['CC'] = 1;
$fields_a['SUBJECT'] = 1;
$fields_a['UID'] = 2;
$fields_a['SIZE'] = 2;
$fields_a['SEEN'] = 3;
$fields_a['RECENT'] = 3;
$fields_a['DELETED'] = 3;
if (!($mode = $fields_a[$index_field])) {
return false;
}
// Select the mailbox
if (!$this->select($mailbox)) {
return false;
}
// build FETCH command string
$key = $this->nextTag();
$cmd = $uidfetch ? 'UID FETCH' : 'FETCH';
$fields = array();
if ($return_uid) {
$fields[] = 'UID';
}
if ($skip_deleted) {
$fields[] = 'FLAGS';
}
if ($mode == 1) {
if ($index_field == 'DATE') {
$fields[] = 'INTERNALDATE';
}
$fields[] = "BODY.PEEK[HEADER.FIELDS ($index_field)]";
}
else if ($mode == 2) {
if ($index_field == 'SIZE') {
$fields[] = 'RFC822.SIZE';
}
else if (!$return_uid || $index_field != 'UID') {
$fields[] = $index_field;
}
}
else if ($mode == 3 && !$skip_deleted) {
$fields[] = 'FLAGS';
}
else if ($mode == 4) {
$fields[] = 'INTERNALDATE';
}
$request = "$key $cmd $message_set (" . implode(' ', $fields) . ")";
if (!$this->putLine($request)) {
$this->setError(self::ERROR_COMMAND, "Failed to send $cmd command");
return false;
}
$result = array();
do {
$line = rtrim($this->readLine(200));
$line = $this->multLine($line);
if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) {
$id = $m[1];
$flags = null;
if ($return_uid) {
if (preg_match('/UID ([0-9]+)/', $line, $matches)) {
$id = (int) $matches[1];
}
else {
continue;
}
}
if ($skip_deleted && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) {
$flags = explode(' ', strtoupper($matches[1]));
if (in_array('\\DELETED', $flags)) {
continue;
}
}
if ($mode == 1 && $index_field == 'DATE') {
if (preg_match('/BODY\[HEADER\.FIELDS \("*DATE"*\)\] (.*)/', $line, $matches)) {
$value = preg_replace(array('/^"*[a-z]+:/i'), '', $matches[1]);
$value = trim($value);
$result[$id] = rcube_utils::strtotime($value);
}
// non-existent/empty Date: header, use INTERNALDATE
if (empty($result[$id])) {
if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches)) {
$result[$id] = rcube_utils::strtotime($matches[1]);
}
else {
$result[$id] = 0;
}
}
}
else if ($mode == 1) {
if (preg_match('/BODY\[HEADER\.FIELDS \("?(FROM|REPLY-TO|SENDER|TO|SUBJECT)"?\)\] (.*)/', $line, $matches)) {
$value = preg_replace(array('/^"*[a-z]+:/i', '/\s+$/sm'), array('', ''), $matches[2]);
$result[$id] = trim($value);
}
else {
$result[$id] = '';
}
}
else if ($mode == 2) {
if (preg_match('/' . $index_field . ' ([0-9]+)/', $line, $matches)) {
$result[$id] = trim($matches[1]);
}
else {
$result[$id] = 0;
}
}
else if ($mode == 3) {
if (!$flags && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) {
$flags = explode(' ', $matches[1]);
}
$result[$id] = in_array("\\".$index_field, (array) $flags) ? 1 : 0;
}
else if ($mode == 4) {
if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches)) {
$result[$id] = rcube_utils::strtotime($matches[1]);
}
else {
$result[$id] = 0;
}
}
}
}
while (!$this->startsWith($line, $key, true, true));
return $result;
}
/**
* Returns message sequence identifier
*
* @param string $mailbox Mailbox name
* @param int $uid Message unique identifier (UID)
*
* @return int Message sequence identifier
*/
public function UID2ID($mailbox, $uid)
{
if ($uid > 0) {
$index = $this->search($mailbox, "UID $uid");
if ($index->count() == 1) {
$arr = $index->get();
return (int) $arr[0];
}
}
}
/**
* Returns message unique identifier (UID)
*
* @param string $mailbox Mailbox name
* @param int $uid Message sequence identifier
*
* @return int Message unique identifier
*/
public function ID2UID($mailbox, $id)
{
if (empty($id) || $id < 0) {
return null;
}
if (!$this->select($mailbox)) {
return null;
}
if ($uid = $this->data['UID-MAP'][$id]) {
return $uid;
}
if (isset($this->data['EXISTS']) && $id > $this->data['EXISTS']) {
return null;
}
$index = $this->search($mailbox, $id, true);
if ($index->count() == 1) {
$arr = $index->get();
return $this->data['UID-MAP'][$id] = (int) $arr[0];
}
}
/**
* Sets flag of the message(s)
*
* @param string $mailbox Mailbox name
* @param string|array $messages Message UID(s)
* @param string $flag Flag name
*
* @return bool True on success, False on failure
*/
public function flag($mailbox, $messages, $flag)
{
return $this->modFlag($mailbox, $messages, $flag, '+');
}
/**
* Unsets flag of the message(s)
*
* @param string $mailbox Mailbox name
* @param string|array $messages Message UID(s)
* @param string $flag Flag name
*
* @return bool True on success, False on failure
*/
public function unflag($mailbox, $messages, $flag)
{
return $this->modFlag($mailbox, $messages, $flag, '-');
}
/**
* Changes flag of the message(s)
*
* @param string $mailbox Mailbox name
* @param string|array $messages Message UID(s)
* @param string $flag Flag name
* @param string $mod Modifier [+|-]. Default: "+".
*
* @return bool True on success, False on failure
*/
protected function modFlag($mailbox, $messages, $flag, $mod = '+')
{
if (!$flag) {
return false;
}
if (!$this->select($mailbox)) {
return false;
}
if (!$this->data['READ-WRITE']) {
$this->setError(self::ERROR_READONLY, "Mailbox is read-only");
return false;
}
if ($this->flags[strtoupper($flag)]) {
$flag = $this->flags[strtoupper($flag)];
}
// if PERMANENTFLAGS is not specified all flags are allowed
if (!empty($this->data['PERMANENTFLAGS'])
&& !in_array($flag, (array) $this->data['PERMANENTFLAGS'])
&& !in_array('\\*', (array) $this->data['PERMANENTFLAGS'])
) {
return false;
}
// Clear internal status cache
if ($flag == 'SEEN') {
unset($this->data['STATUS:'.$mailbox]['UNSEEN']);
}
if ($mod != '+' && $mod != '-') {
$mod = '+';
}
$result = $this->execute('UID STORE', array(
$this->compressMessageSet($messages), $mod . 'FLAGS.SILENT', "($flag)"),
self::COMMAND_NORESPONSE);
return $result == self::ERROR_OK;
}
/**
* Copies message(s) from one folder to another
*
* @param string|array $messages Message UID(s)
* @param string $from Mailbox name
* @param string $to Destination mailbox name
*
* @return bool True on success, False on failure
*/
public function copy($messages, $from, $to)
{
// Clear last COPYUID data
unset($this->data['COPYUID']);
if (!$this->select($from)) {
return false;
}
// Clear internal status cache
unset($this->data['STATUS:'.$to]);
$result = $this->execute('UID COPY', array(
$this->compressMessageSet($messages), $this->escape($to)),
self::COMMAND_NORESPONSE);
return $result == self::ERROR_OK;
}
/**
* Moves message(s) from one folder to another.
*
* @param string|array $messages Message UID(s)
* @param string $from Mailbox name
* @param string $to Destination mailbox name
*
* @return bool True on success, False on failure
*/
public function move($messages, $from, $to)
{
if (!$this->select($from)) {
return false;
}
if (!$this->data['READ-WRITE']) {
$this->setError(self::ERROR_READONLY, "Mailbox is read-only");
return false;
}
// use MOVE command (RFC 6851)
if ($this->hasCapability('MOVE')) {
// Clear last COPYUID data
unset($this->data['COPYUID']);
// Clear internal status cache
unset($this->data['STATUS:'.$to]);
$this->clear_status_cache($from);
$result = $this->execute('UID MOVE', array(
$this->compressMessageSet($messages), $this->escape($to)),
self::COMMAND_NORESPONSE);
return $result == self::ERROR_OK;
}
// use COPY + STORE +FLAGS.SILENT \Deleted + EXPUNGE
$result = $this->copy($messages, $from, $to);
if ($result) {
// Clear internal status cache
unset($this->data['STATUS:'.$from]);
$result = $this->flag($from, $messages, 'DELETED');
if ($messages == '*') {
// CLOSE+SELECT should be faster than EXPUNGE
$this->close();
}
else {
$this->expunge($from, $messages);
}
}
return $result;
}
/**
* FETCH command (RFC3501)
*
* @param string $mailbox Mailbox name
* @param mixed $message_set Message(s) sequence identifier(s) or UID(s)
* @param bool $is_uid True if $message_set contains UIDs
* @param array $query_items FETCH command data items
* @param string $mod_seq Modification sequence for CHANGEDSINCE (RFC4551) query
* @param bool $vanished Enables VANISHED parameter (RFC5162) for CHANGEDSINCE query
*
* @return array List of rcube_message_header elements, False on error
* @since 0.6
*/
public function fetch($mailbox, $message_set, $is_uid = false, $query_items = array(),
$mod_seq = null, $vanished = false)
{
if (!$this->select($mailbox)) {
return false;
}
$message_set = $this->compressMessageSet($message_set);
$result = array();
$key = $this->nextTag();
$cmd = ($is_uid ? 'UID ' : '') . 'FETCH';
$request = "$key $cmd $message_set (" . implode(' ', $query_items) . ")";
if ($mod_seq !== null && $this->hasCapability('CONDSTORE')) {
$request .= " (CHANGEDSINCE $mod_seq" . ($vanished ? " VANISHED" : '') .")";
}
if (!$this->putLine($request)) {
$this->setError(self::ERROR_COMMAND, "Failed to send $cmd command");
return false;
}
do {
$line = $this->readLine(4096);
if (!$line) {
break;
}
// Sample reply line:
// * 321 FETCH (UID 2417 RFC822.SIZE 2730 FLAGS (\Seen)
// INTERNALDATE "16-Nov-2008 21:08:46 +0100" BODYSTRUCTURE (...)
// BODY[HEADER.FIELDS ...
if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) {
$id = intval($m[1]);
$result[$id] = new rcube_message_header;
$result[$id]->id = $id;
$result[$id]->subject = '';
$result[$id]->messageID = 'mid:' . $id;
$headers = null;
$lines = array();
$line = substr($line, strlen($m[0]) + 2);
$ln = 0;
// get complete entry
while (preg_match('/\{([0-9]+)\}\r\n$/', $line, $m)) {
$bytes = $m[1];
$out = '';
while (strlen($out) < $bytes) {
$out = $this->readBytes($bytes);
if ($out === null) {
break;
}
$line .= $out;
}
$str = $this->readLine(4096);
if ($str === false) {
break;
}
$line .= $str;
}
// Tokenize response and assign to object properties
while (list($name, $value) = $this->tokenizeResponse($line, 2)) {
if ($name == 'UID') {
$result[$id]->uid = intval($value);
}
else if ($name == 'RFC822.SIZE') {
$result[$id]->size = intval($value);
}
else if ($name == 'RFC822.TEXT') {
$result[$id]->body = $value;
}
else if ($name == 'INTERNALDATE') {
$result[$id]->internaldate = $value;
$result[$id]->date = $value;
$result[$id]->timestamp = rcube_utils::strtotime($value);
}
else if ($name == 'FLAGS') {
if (!empty($value)) {
foreach ((array)$value as $flag) {
$flag = str_replace(array('$', "\\"), '', $flag);
$flag = strtoupper($flag);
$result[$id]->flags[$flag] = true;
}
}
}
else if ($name == 'MODSEQ') {
$result[$id]->modseq = $value[0];
}
else if ($name == 'ENVELOPE') {
$result[$id]->envelope = $value;
}
else if ($name == 'BODYSTRUCTURE' || ($name == 'BODY' && count($value) > 2)) {
if (!is_array($value[0]) && (strtolower($value[0]) == 'message' && strtolower($value[1]) == 'rfc822')) {
$value = array($value);
}
$result[$id]->bodystructure = $value;
}
else if ($name == 'RFC822') {
$result[$id]->body = $value;
}
else if (stripos($name, 'BODY[') === 0) {
$name = str_replace(']', '', substr($name, 5));
if ($name == 'HEADER.FIELDS') {
// skip ']' after headers list
$this->tokenizeResponse($line, 1);
$headers = $this->tokenizeResponse($line, 1);
}
else if (strlen($name)) {
$result[$id]->bodypart[$name] = $value;
}
else {
$result[$id]->body = $value;
}
}
}
// create array with header field:data
if (!empty($headers)) {
$headers = explode("\n", trim($headers));
foreach ($headers as $resln) {
if (ord($resln[0]) <= 32) {
$lines[$ln] .= (empty($lines[$ln]) ? '' : "\n") . trim($resln);
}
else {
$lines[++$ln] = trim($resln);
}
}
foreach ($lines as $str) {
list($field, $string) = explode(':', $str, 2);
$field = strtolower($field);
$string = preg_replace('/\n[\t\s]*/', ' ', trim($string));
switch ($field) {
case 'date';
$string = substr($string, 0, 128);
$result[$id]->date = $string;
$result[$id]->timestamp = rcube_utils::strtotime($string);
break;
case 'to':
$result[$id]->to = preg_replace('/undisclosed-recipients:[;,]*/', '', $string);
break;
case 'from':
case 'subject':
$string = substr($string, 0, 2048);
case 'cc':
case 'bcc':
case 'references':
$result[$id]->{$field} = $string;
break;
case 'reply-to':
$result[$id]->replyto = $string;
break;
case 'content-transfer-encoding':
$result[$id]->encoding = substr($string, 0, 32);
break;
case 'content-type':
$ctype_parts = preg_split('/[; ]+/', $string);
$result[$id]->ctype = strtolower(array_shift($ctype_parts));
if (preg_match('/charset\s*=\s*"?([a-z0-9\-\.\_]+)"?/i', $string, $regs)) {
$result[$id]->charset = $regs[1];
}
break;
case 'in-reply-to':
$result[$id]->in_reply_to = str_replace(array("\n", '<', '>'), '', $string);
break;
case 'return-receipt-to':
case 'disposition-notification-to':
case 'x-confirm-reading-to':
$result[$id]->mdn_to = substr($string, 0, 2048);
break;
case 'message-id':
$result[$id]->messageID = substr($string, 0, 2048);
break;
case 'x-priority':
if (preg_match('/^(\d+)/', $string, $matches)) {
$result[$id]->priority = intval($matches[1]);
}
break;
default:
if (strlen($field) < 3) {
break;
}
if ($result[$id]->others[$field]) {
$string = array_merge((array)$result[$id]->others[$field], (array)$string);
}
$result[$id]->others[$field] = $string;
}
}
}
}
// VANISHED response (QRESYNC RFC5162)
// Sample: * VANISHED (EARLIER) 300:310,405,411
else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) {
$line = substr($line, strlen($match[0]));
$v_data = $this->tokenizeResponse($line, 1);
$this->data['VANISHED'] = $v_data;
}
}
while (!$this->startsWith($line, $key, true));
return $result;
}
/**
* Returns message(s) data (flags, headers, etc.)
*
* @param string $mailbox Mailbox name
* @param mixed $message_set Message(s) sequence identifier(s) or UID(s)
* @param bool $is_uid True if $message_set contains UIDs
* @param bool $bodystr Enable to add BODYSTRUCTURE data to the result
* @param array $add_headers List of additional headers
*
* @return bool|array List of rcube_message_header elements, False on error
*/
public function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = false, $add_headers = array())
{
$query_items = array('UID', 'RFC822.SIZE', 'FLAGS', 'INTERNALDATE');
$headers = array('DATE', 'FROM', 'TO', 'SUBJECT', 'CONTENT-TYPE', 'CC', 'REPLY-TO',
'LIST-POST', 'DISPOSITION-NOTIFICATION-TO', 'X-PRIORITY');
if (!empty($add_headers)) {
$add_headers = array_map('strtoupper', $add_headers);
$headers = array_unique(array_merge($headers, $add_headers));
}
if ($bodystr) {
$query_items[] = 'BODYSTRUCTURE';
}
$query_items[] = 'BODY.PEEK[HEADER.FIELDS (' . implode(' ', $headers) . ')]';
return $this->fetch($mailbox, $message_set, $is_uid, $query_items);
}
/**
* Returns message data (flags, headers, etc.)
*
* @param string $mailbox Mailbox name
* @param int $id Message sequence identifier or UID
* @param bool $is_uid True if $id is an UID
* @param bool $bodystr Enable to add BODYSTRUCTURE data to the result
* @param array $add_headers List of additional headers
*
* @return bool|rcube_message_header Message data, False on error
*/
public function fetchHeader($mailbox, $id, $is_uid = false, $bodystr = false, $add_headers = array())
{
$a = $this->fetchHeaders($mailbox, $id, $is_uid, $bodystr, $add_headers);
if (is_array($a)) {
return array_shift($a);
}
return false;
}
/**
* Sort messages by specified header field
*
* @param array $messages Array of rcube_message_header objects
* @param string $field Name of the property to sort by
* @param string $flag Sorting order (ASC|DESC)
*
* @return array Sorted input array
*/
public static function sortHeaders($messages, $field, $flag)
{
// Strategy: First, we'll create an "index" array.
// Then, we'll use sort() on that array, and use that to sort the main array.
$field = empty($field) ? 'uid' : strtolower($field);
$flag = empty($flag) ? 'ASC' : strtoupper($flag);
$index = array();
$result = array();
reset($messages);
foreach ($messages as $key => $headers) {
$value = null;
switch ($field) {
case 'arrival':
$field = 'internaldate';
case 'date':
case 'internaldate':
case 'timestamp':
$value = rcube_utils::strtotime($headers->$field);
if (!$value && $field != 'timestamp') {
$value = $headers->timestamp;
}
break;
default:
// @TODO: decode header value, convert to UTF-8
$value = $headers->$field;
if (is_string($value)) {
$value = str_replace('"', '', $value);
if ($field == 'subject') {
$value = preg_replace('/^(Re:\s*|Fwd:\s*|Fw:\s*)+/i', '', $value);
}
$data = strtoupper($value);
}
}
$index[$key] = $value;
}
if (!empty($index)) {
// sort index
if ($flag == 'ASC') {
asort($index);
}
else {
arsort($index);
}
// form new array based on index
foreach ($index as $key => $val) {
$result[$key] = $messages[$key];
}
}
return $result;
}
/**
* Fetch MIME headers of specified message parts
*
* @param string $mailbox Mailbox name
* @param int $uid Message UID
* @param array $parts Message part identifiers
* @param bool $mime Use MIME instad of HEADER
*
* @return array|bool Array containing headers string for each specified body
* False on failure.
*/
public function fetchMIMEHeaders($mailbox, $uid, $parts, $mime = true)
{
if (!$this->select($mailbox)) {
return false;
}
$result = false;
$parts = (array) $parts;
$key = $this->nextTag();
$peeks = array();
$type = $mime ? 'MIME' : 'HEADER';
// format request
foreach ($parts as $part) {
$peeks[] = "BODY.PEEK[$part.$type]";
}
$request = "$key UID FETCH $uid (" . implode(' ', $peeks) . ')';
// send request
if (!$this->putLine($request)) {
$this->setError(self::ERROR_COMMAND, "Failed to send UID FETCH command");
return false;
}
do {
$line = $this->readLine(1024);
if (preg_match('/^\* [0-9]+ FETCH [0-9UID( ]+/', $line, $m)) {
$line = ltrim(substr($line, strlen($m[0])));
while (preg_match('/^BODY\[([0-9\.]+)\.'.$type.'\]/', $line, $matches)) {
$line = substr($line, strlen($matches[0]));
$result[$matches[1]] = trim($this->multLine($line));
$line = $this->readLine(1024);
}
}
}
while (!$this->startsWith($line, $key, true));
return $result;
}
/**
* Fetches message part header
*/
public function fetchPartHeader($mailbox, $id, $is_uid = false, $part = null)
{
$part = empty($part) ? 'HEADER' : $part.'.MIME';
return $this->handlePartBody($mailbox, $id, $is_uid, $part);
}
/**
* Fetches body of the specified message part
*/
public function handlePartBody($mailbox, $id, $is_uid=false, $part='', $encoding=null, $print=null, $file=null, $formatted=false, $max_bytes=0)
{
if (!$this->select($mailbox)) {
return false;
}
$binary = true;
do {
if (!$initiated) {
switch ($encoding) {
case 'base64':
$mode = 1;
break;
case 'quoted-printable':
$mode = 2;
break;
case 'x-uuencode':
case 'x-uue':
case 'uue':
case 'uuencode':
$mode = 3;
break;
default:
$mode = 0;
}
// Use BINARY extension when possible (and safe)
$binary = $binary && $mode && preg_match('/^[0-9.]+$/', $part) && $this->hasCapability('BINARY');
$fetch_mode = $binary ? 'BINARY' : 'BODY';
$partial = $max_bytes ? sprintf('<0.%d>', $max_bytes) : '';
// format request
$key = $this->nextTag();
$cmd = ($is_uid ? 'UID ' : '') . 'FETCH';
$request = "$key $cmd $id ($fetch_mode.PEEK[$part]$partial)";
$result = false;
$found = false;
$initiated = true;
// send request
if (!$this->putLine($request)) {
$this->setError(self::ERROR_COMMAND, "Failed to send $cmd command");
return false;
}
if ($binary) {
// WARNING: Use $formatted argument with care, this may break binary data stream
$mode = -1;
}
}
$line = trim($this->readLine(1024));
if (!$line) {
break;
}
// handle UNKNOWN-CTE response - RFC 3516, try again with standard BODY request
if ($binary && !$found && preg_match('/^' . $key . ' NO \[(UNKNOWN-CTE|PARSE)\]/i', $line)) {
$binary = $initiated = false;
continue;
}
// skip irrelevant untagged responses (we have a result already)
if ($found || !preg_match('/^\* ([0-9]+) FETCH (.*)$/', $line, $m)) {
continue;
}
$line = $m[2];
// handle one line response
if ($line[0] == '(' && substr($line, -1) == ')') {
// tokenize content inside brackets
// the content can be e.g.: (UID 9844 BODY[2.4] NIL)
$tokens = $this->tokenizeResponse(preg_replace('/(^\(|\)$)/', '', $line));
for ($i=0; $i<count($tokens); $i+=2) {
if (preg_match('/^(BODY|BINARY)/i', $tokens[$i])) {
$result = $tokens[$i+1];
$found = true;
break;
}
}
if ($result !== false) {
if ($mode == 1) {
$result = base64_decode($result);
}
else if ($mode == 2) {
$result = quoted_printable_decode($result);
}
else if ($mode == 3) {
$result = convert_uudecode($result);
}
}
}
// response with string literal
else if (preg_match('/\{([0-9]+)\}$/', $line, $m)) {
$bytes = (int) $m[1];
$prev = '';
$found = true;
// empty body
if (!$bytes) {
$result = '';
}
else while ($bytes > 0) {
$line = $this->readLine(8192);
if ($line === null) {
break;
}
$len = strlen($line);
if ($len > $bytes) {
$line = substr($line, 0, $bytes);
$len = strlen($line);
}
$bytes -= $len;
// BASE64
if ($mode == 1) {
$line = preg_replace('|[^a-zA-Z0-9+=/]|', '', $line);
// create chunks with proper length for base64 decoding
$line = $prev.$line;
$length = strlen($line);
if ($length % 4) {
$length = floor($length / 4) * 4;
$prev = substr($line, $length);
$line = substr($line, 0, $length);
}
else {
$prev = '';
}
$line = base64_decode($line);
}
// QUOTED-PRINTABLE
else if ($mode == 2) {
$line = rtrim($line, "\t\r\0\x0B");
$line = quoted_printable_decode($line);
}
// UUENCODE
else if ($mode == 3) {
$line = rtrim($line, "\t\r\n\0\x0B");
if ($line == 'end' || preg_match('/^begin\s+[0-7]+\s+.+$/', $line)) {
continue;
}
$line = convert_uudecode($line);
}
// default
else if ($formatted) {
$line = rtrim($line, "\t\r\n\0\x0B") . "\n";
}
if ($file) {
if (fwrite($file, $line) === false) {
break;
}
}
else if ($print) {
echo $line;
}
else {
$result .= $line;
}
}
}
}
while (!$this->startsWith($line, $key, true) || !$initiated);
if ($result !== false) {
if ($file) {
return fwrite($file, $result);
}
else if ($print) {
echo $result;
return true;
}
return $result;
}
return false;
}
/**
* Handler for IMAP APPEND command
*
* @param string $mailbox Mailbox name
* @param string|array $message The message source string or array (of strings and file pointers)
* @param array $flags Message flags
* @param string $date Message internal date
* @param bool $binary Enable BINARY append (RFC3516)
*
* @return string|bool On success APPENDUID response (if available) or True, False on failure
*/
public function append($mailbox, &$message, $flags = array(), $date = null, $binary = false)
{
unset($this->data['APPENDUID']);
if ($mailbox === null || $mailbox === '') {
return false;
}
$binary = $binary && $this->getCapability('BINARY');
$literal_plus = !$binary && $this->prefs['literal+'];
$len = 0;
$msg = is_array($message) ? $message : array(&$message);
$chunk_size = 512000;
for ($i=0, $cnt=count($msg); $i<$cnt; $i++) {
if (is_resource($msg[$i])) {
$stat = fstat($msg[$i]);
if ($stat === false) {
return false;
}
$len += $stat['size'];
}
else {
if (!$binary) {
$msg[$i] = str_replace("\r", '', $msg[$i]);
$msg[$i] = str_replace("\n", "\r\n", $msg[$i]);
}
$len += strlen($msg[$i]);
}
}
if (!$len) {
return false;
}
// build APPEND command
$key = $this->nextTag();
$request = "$key APPEND " . $this->escape($mailbox) . ' (' . $this->flagsToStr($flags) . ')';
if (!empty($date)) {
$request .= ' ' . $this->escape($date);
}
$request .= ' ' . ($binary ? '~' : '') . '{' . $len . ($literal_plus ? '+' : '') . '}';
// send APPEND command
if (!$this->putLine($request)) {
$this->setError(self::ERROR_COMMAND, "Failed to send APPEND command");
return false;
}
// Do not wait when LITERAL+ is supported
if (!$literal_plus) {
$line = $this->readReply();
if ($line[0] != '+') {
$this->parseResult($line, 'APPEND: ');
return false;
}
}
foreach ($msg as $msg_part) {
// file pointer
if (is_resource($msg_part)) {
rewind($msg_part);
while (!feof($msg_part) && $this->fp) {
$buffer = fread($msg_part, $chunk_size);
$this->putLine($buffer, false);
}
fclose($msg_part);
}
// string
else {
$size = strlen($msg_part);
// Break up the data by sending one chunk (up to 512k) at a time.
// This approach reduces our peak memory usage
for ($offset = 0; $offset < $size; $offset += $chunk_size) {
$chunk = substr($msg_part, $offset, $chunk_size);
if (!$this->putLine($chunk, false)) {
return false;
}
}
}
}
if (!$this->putLine('')) { // \r\n
return false;
}
do {
$line = $this->readLine();
} while (!$this->startsWith($line, $key, true, true));
// Clear internal status cache
unset($this->data['STATUS:'.$mailbox]);
if ($this->parseResult($line, 'APPEND: ') != self::ERROR_OK) {
return false;
}
if (!empty($this->data['APPENDUID'])) {
return $this->data['APPENDUID'];
}
return true;
}
/**
* Handler for IMAP APPEND command.
*
* @param string $mailbox Mailbox name
* @param string $path Path to the file with message body
* @param string $headers Message headers
* @param array $flags Message flags
* @param string $date Message internal date
* @param bool $binary Enable BINARY append (RFC3516)
*
* @return string|bool On success APPENDUID response (if available) or True, False on failure
*/
public function appendFromFile($mailbox, $path, $headers=null, $flags = array(), $date = null, $binary = false)
{
// open message file
if (file_exists(realpath($path))) {
$fp = fopen($path, 'r');
}
if (!$fp) {
$this->setError(self::ERROR_UNKNOWN, "Couldn't open $path for reading");
return false;
}
$message = array();
if ($headers) {
$message[] = trim($headers, "\r\n") . "\r\n\r\n";
}
$message[] = $fp;
return $this->append($mailbox, $message, $flags, $date, $binary);
}
/**
* Returns QUOTA information
*
* @param string $mailbox Mailbox name
*
* @return array Quota information
*/
public function getQuota($mailbox = null)
{
if ($mailbox === null || $mailbox === '') {
$mailbox = 'INBOX';
}
// a0001 GETQUOTAROOT INBOX
// * QUOTAROOT INBOX user/sample
// * QUOTA user/sample (STORAGE 654 9765)
// a0001 OK Completed
list($code, $response) = $this->execute('GETQUOTAROOT', array($this->escape($mailbox)), 0, '/^\* QUOTA /i');
$result = false;
$min_free = PHP_INT_MAX;
$all = array();
if ($code == self::ERROR_OK) {
foreach (explode("\n", $response) as $line) {
list(, , $quota_root) = $this->tokenizeResponse($line, 3);
$quotas = $this->tokenizeResponse($line, 1);
if (empty($quotas)) {
continue;
}
foreach (array_chunk($quotas, 3) as $quota) {
list($type, $used, $total) = $quota;
$type = strtolower($type);
if ($type && $total) {
$all[$quota_root][$type]['used'] = intval($used);
$all[$quota_root][$type]['total'] = intval($total);
}
}
if (empty($all[$quota_root]['storage'])) {
continue;
}
$used = $all[$quota_root]['storage']['used'];
$total = $all[$quota_root]['storage']['total'];
$free = $total - $used;
// calculate lowest available space from all storage quotas
if ($free < $min_free) {
$min_free = $free;
$result['used'] = $used;
$result['total'] = $total;
$result['percent'] = min(100, round(($used/max(1,$total))*100));
$result['free'] = 100 - $result['percent'];
}
}
}
if (!empty($result)) {
$result['all'] = $all;
}
return $result;
}
/**
* Send the SETACL command (RFC4314)
*
* @param string $mailbox Mailbox name
* @param string $user User name
* @param mixed $acl ACL string or array
*
* @return boolean True on success, False on failure
*
* @since 0.5-beta
*/
public function setACL($mailbox, $user, $acl)
{
if (is_array($acl)) {
$acl = implode('', $acl);
}
$result = $this->execute('SETACL', array(
$this->escape($mailbox), $this->escape($user), strtolower($acl)),
self::COMMAND_NORESPONSE);
return ($result == self::ERROR_OK);
}
/**
* Send the DELETEACL command (RFC4314)
*
* @param string $mailbox Mailbox name
* @param string $user User name
*
* @return boolean True on success, False on failure
*
* @since 0.5-beta
*/
public function deleteACL($mailbox, $user)
{
$result = $this->execute('DELETEACL', array(
$this->escape($mailbox), $this->escape($user)),
self::COMMAND_NORESPONSE);
return ($result == self::ERROR_OK);
}
/**
* Send the GETACL command (RFC4314)
*
* @param string $mailbox Mailbox name
*
* @return array User-rights array on success, NULL on error
* @since 0.5-beta
*/
public function getACL($mailbox)
{
list($code, $response) = $this->execute('GETACL', array($this->escape($mailbox)), 0, '/^\* ACL /i');
if ($code == self::ERROR_OK && $response) {
// Parse server response (remove "* ACL ")
$response = substr($response, 6);
$ret = $this->tokenizeResponse($response);
$mbox = array_shift($ret);
$size = count($ret);
// Create user-rights hash array
// @TODO: consider implementing fixACL() method according to RFC4314.2.1.1
// so we could return only standard rights defined in RFC4314,
// excluding 'c' and 'd' defined in RFC2086.
if ($size % 2 == 0) {
for ($i=0; $i<$size; $i++) {
$ret[$ret[$i]] = str_split($ret[++$i]);
unset($ret[$i-1]);
unset($ret[$i]);
}
return $ret;
}
$this->setError(self::ERROR_COMMAND, "Incomplete ACL response");
}
}
/**
* Send the LISTRIGHTS command (RFC4314)
*
* @param string $mailbox Mailbox name
* @param string $user User name
*
* @return array List of user rights
* @since 0.5-beta
*/
public function listRights($mailbox, $user)
{
list($code, $response) = $this->execute('LISTRIGHTS',
array($this->escape($mailbox), $this->escape($user)), 0, '/^\* LISTRIGHTS /i');
if ($code == self::ERROR_OK && $response) {
// Parse server response (remove "* LISTRIGHTS ")
$response = substr($response, 13);
$ret_mbox = $this->tokenizeResponse($response, 1);
$ret_user = $this->tokenizeResponse($response, 1);
$granted = $this->tokenizeResponse($response, 1);
$optional = trim($response);
return array(
'granted' => str_split($granted),
'optional' => explode(' ', $optional),
);
}
}
/**
* Send the MYRIGHTS command (RFC4314)
*
* @param string $mailbox Mailbox name
*
* @return array MYRIGHTS response on success, NULL on error
* @since 0.5-beta
*/
public function myRights($mailbox)
{
list($code, $response) = $this->execute('MYRIGHTS', array($this->escape($mailbox)), 0, '/^\* MYRIGHTS /i');
if ($code == self::ERROR_OK && $response) {
// Parse server response (remove "* MYRIGHTS ")
$response = substr($response, 11);
$ret_mbox = $this->tokenizeResponse($response, 1);
$rights = $this->tokenizeResponse($response, 1);
return str_split($rights);
}
}
/**
* Send the SETMETADATA command (RFC5464)
*
* @param string $mailbox Mailbox name
* @param array $entries Entry-value array (use NULL value as NIL)
*
* @return boolean True on success, False on failure
* @since 0.5-beta
*/
public function setMetadata($mailbox, $entries)
{
if (!is_array($entries) || empty($entries)) {
$this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command");
return false;
}
foreach ($entries as $name => $value) {
$entries[$name] = $this->escape($name) . ' ' . $this->escape($value, true);
}
$entries = implode(' ', $entries);
$result = $this->execute('SETMETADATA', array(
$this->escape($mailbox), '(' . $entries . ')'),
self::COMMAND_NORESPONSE);
return ($result == self::ERROR_OK);
}
/**
* Send the SETMETADATA command with NIL values (RFC5464)
*
* @param string $mailbox Mailbox name
* @param array $entries Entry names array
*
* @return boolean True on success, False on failure
*
* @since 0.5-beta
*/
public function deleteMetadata($mailbox, $entries)
{
if (!is_array($entries) && !empty($entries)) {
$entries = explode(' ', $entries);
}
if (empty($entries)) {
$this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command");
return false;
}
foreach ($entries as $entry) {
$data[$entry] = null;
}
return $this->setMetadata($mailbox, $data);
}
/**
* Send the GETMETADATA command (RFC5464)
*
* @param string $mailbox Mailbox name
* @param array $entries Entries
* @param array $options Command options (with MAXSIZE and DEPTH keys)
*
* @return array GETMETADATA result on success, NULL on error
*
* @since 0.5-beta
*/
public function getMetadata($mailbox, $entries, $options=array())
{
if (!is_array($entries)) {
$entries = array($entries);
}
// create entries string
foreach ($entries as $idx => $name) {
$entries[$idx] = $this->escape($name);
}
$optlist = '';
$entlist = '(' . implode(' ', $entries) . ')';
// create options string
if (is_array($options)) {
$options = array_change_key_case($options, CASE_UPPER);
$opts = array();
if (!empty($options['MAXSIZE'])) {
$opts[] = 'MAXSIZE '.intval($options['MAXSIZE']);
}
if (!empty($options['DEPTH'])) {
$opts[] = 'DEPTH '.intval($options['DEPTH']);
}
if ($opts) {
$optlist = '(' . implode(' ', $opts) . ')';
}
}
$optlist .= ($optlist ? ' ' : '') . $entlist;
list($code, $response) = $this->execute('GETMETADATA', array(
$this->escape($mailbox), $optlist));
if ($code == self::ERROR_OK) {
$result = array();
$data = $this->tokenizeResponse($response);
// The METADATA response can contain multiple entries in a single
// response or multiple responses for each entry or group of entries
for ($i = 0, $size = count($data); $i < $size; $i++) {
if ($data[$i] === '*'
&& $data[++$i] === 'METADATA'
&& is_string($mbox = $data[++$i])
&& is_array($data[++$i])
) {
for ($x = 0, $size2 = count($data[$i]); $x < $size2; $x += 2) {
if ($data[$i][$x+1] !== null) {
$result[$mbox][$data[$i][$x]] = $data[$i][$x+1];
}
}
}
}
return $result;
}
}
/**
* Send the SETANNOTATION command (draft-daboo-imap-annotatemore)
*
* @param string $mailbox Mailbox name
* @param array $data Data array where each item is an array with
* three elements: entry name, attribute name, value
*
* @return boolean True on success, False on failure
* @since 0.5-beta
*/
public function setAnnotation($mailbox, $data)
{
if (!is_array($data) || empty($data)) {
$this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command");
return false;
}
foreach ($data as $entry) {
// ANNOTATEMORE drafts before version 08 require quoted parameters
$entries[] = sprintf('%s (%s %s)', $this->escape($entry[0], true),
$this->escape($entry[1], true), $this->escape($entry[2], true));
}
$entries = implode(' ', $entries);
$result = $this->execute('SETANNOTATION', array(
$this->escape($mailbox), $entries), self::COMMAND_NORESPONSE);
return ($result == self::ERROR_OK);
}
/**
* Send the SETANNOTATION command with NIL values (draft-daboo-imap-annotatemore)
*
* @param string $mailbox Mailbox name
* @param array $data Data array where each item is an array with
* two elements: entry name and attribute name
*
* @return boolean True on success, False on failure
*
* @since 0.5-beta
*/
public function deleteAnnotation($mailbox, $data)
{
if (!is_array($data) || empty($data)) {
$this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command");
return false;
}
return $this->setAnnotation($mailbox, $data);
}
/**
* Send the GETANNOTATION command (draft-daboo-imap-annotatemore)
*
* @param string $mailbox Mailbox name
* @param array $entries Entries names
* @param array $attribs Attribs names
*
* @return array Annotations result on success, NULL on error
*
* @since 0.5-beta
*/
public function getAnnotation($mailbox, $entries, $attribs)
{
if (!is_array($entries)) {
$entries = array($entries);
}
// create entries string
// ANNOTATEMORE drafts before version 08 require quoted parameters
foreach ($entries as $idx => $name) {
$entries[$idx] = $this->escape($name, true);
}
$entries = '(' . implode(' ', $entries) . ')';
if (!is_array($attribs)) {
$attribs = array($attribs);
}
// create attributes string
foreach ($attribs as $idx => $name) {
$attribs[$idx] = $this->escape($name, true);
}
$attribs = '(' . implode(' ', $attribs) . ')';
list($code, $response) = $this->execute('GETANNOTATION', array(
$this->escape($mailbox), $entries, $attribs));
if ($code == self::ERROR_OK) {
$result = array();
$data = $this->tokenizeResponse($response);
// Here we returns only data compatible with METADATA result format
if (!empty($data) && ($size = count($data))) {
for ($i=0; $i<$size; $i++) {
$entry = $data[$i];
if (isset($mbox) && is_array($entry)) {
$attribs = $entry;
$entry = $last_entry;
}
else if ($entry == '*') {
if ($data[$i+1] == 'ANNOTATION') {
$mbox = $data[$i+2];
unset($data[$i]); // "*"
unset($data[++$i]); // "ANNOTATION"
unset($data[++$i]); // Mailbox
}
// get rid of other untagged responses
else {
unset($mbox);
unset($data[$i]);
}
continue;
}
else if (isset($mbox)) {
$attribs = $data[++$i];
}
else {
unset($data[$i]);
continue;
}
if (!empty($attribs)) {
for ($x=0, $len=count($attribs); $x<$len;) {
$attr = $attribs[$x++];
$value = $attribs[$x++];
if ($attr == 'value.priv' && $value !== null) {
$result[$mbox]['/private' . $entry] = $value;
}
else if ($attr == 'value.shared' && $value !== null) {
$result[$mbox]['/shared' . $entry] = $value;
}
}
}
$last_entry = $entry;
unset($data[$i]);
}
}
return $result;
}
}
/**
* Returns BODYSTRUCTURE for the specified message.
*
* @param string $mailbox Folder name
* @param int $id Message sequence number or UID
* @param bool $is_uid True if $id is an UID
*
* @return array/bool Body structure array or False on error.
* @since 0.6
*/
public function getStructure($mailbox, $id, $is_uid = false)
{
$result = $this->fetch($mailbox, $id, $is_uid, array('BODYSTRUCTURE'));
if (is_array($result)) {
$result = array_shift($result);
return $result->bodystructure;
}
return false;
}
/**
* Returns data of a message part according to specified structure.
*
* @param array $structure Message structure (getStructure() result)
* @param string $part Message part identifier
*
* @return array Part data as hash array (type, encoding, charset, size)
*/
public static function getStructurePartData($structure, $part)
{
$part_a = self::getStructurePartArray($structure, $part);
$data = array();
if (empty($part_a)) {
return $data;
}
// content-type
if (is_array($part_a[0])) {
$data['type'] = 'multipart';
}
else {
$data['type'] = strtolower($part_a[0]);
$data['encoding'] = strtolower($part_a[5]);
// charset
if (is_array($part_a[2])) {
foreach ($part_a[2] as $key => $val) {
if (strcasecmp($val, 'charset') == 0) {
$data['charset'] = $part_a[2][$key+1];
break;
}
}
}
}
// size
$data['size'] = intval($part_a[6]);
return $data;
}
public static function getStructurePartArray($a, $part)
{
if (!is_array($a)) {
return false;
}
if (empty($part)) {
return $a;
}
$ctype = is_string($a[0]) && is_string($a[1]) ? $a[0] . '/' . $a[1] : '';
if (strcasecmp($ctype, 'message/rfc822') == 0) {
$a = $a[8];
}
if (strpos($part, '.') > 0) {
$orig_part = $part;
$pos = strpos($part, '.');
$rest = substr($orig_part, $pos+1);
$part = substr($orig_part, 0, $pos);
return self::getStructurePartArray($a[$part-1], $rest);
}
else if ($part > 0) {
return (is_array($a[$part-1])) ? $a[$part-1] : $a;
}
}
/**
* Creates next command identifier (tag)
*
* @return string Command identifier
* @since 0.5-beta
*/
public function nextTag()
{
$this->cmd_num++;
$this->cmd_tag = sprintf('A%04d', $this->cmd_num);
return $this->cmd_tag;
}
/**
* Sends IMAP command and parses result
*
* @param string $command IMAP command
* @param array $arguments Command arguments
* @param int $options Execution options
* @param string $filter Line filter (regexp)
*
* @return mixed Response code or list of response code and data
* @since 0.5-beta
*/
public function execute($command, $arguments = array(), $options = 0, $filter = null)
{
$tag = $this->nextTag();
$query = $tag . ' ' . $command;
$noresp = ($options & self::COMMAND_NORESPONSE);
$response = $noresp ? null : '';
if (!empty($arguments)) {
foreach ($arguments as $arg) {
$query .= ' ' . self::r_implode($arg);
}
}
// Send command
if (!$this->putLineC($query, true, ($options & self::COMMAND_ANONYMIZED))) {
preg_match('/^[A-Z0-9]+ ((UID )?[A-Z]+)/', $query, $matches);
$cmd = $matches[1] ?: 'UNKNOWN';
$this->setError(self::ERROR_COMMAND, "Failed to send $cmd command");
return $noresp ? self::ERROR_COMMAND : array(self::ERROR_COMMAND, '');
}
// Parse response
do {
$line = $this->readLine(4096);
if ($response !== null) {
// TODO: Better string literals handling with filter
if (!$filter || preg_match($filter, $line)) {
$response .= $line;
}
}
// parse untagged response for [COPYUID 1204196876 3456:3457 123:124] (RFC6851)
if ($line && $command == 'UID MOVE') {
if (preg_match("/^\* OK \[COPYUID [0-9]+ ([0-9,:]+) ([0-9,:]+)\]/i", $line, $m)) {
$this->data['COPYUID'] = array($m[1], $m[2]);
}
}
}
while (!$this->startsWith($line, $tag . ' ', true, true));
$code = $this->parseResult($line, $command . ': ');
// Remove last line from response
if ($response) {
if (!$filter) {
$line_len = min(strlen($response), strlen($line));
$response = substr($response, 0, -$line_len);
}
$response = rtrim($response, "\r\n");
}
// optional CAPABILITY response
if (($options & self::COMMAND_CAPABILITY) && $code == self::ERROR_OK
&& preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)
) {
$this->parseCapability($matches[1], true);
}
// return last line only (without command tag, result and response code)
if ($line && ($options & self::COMMAND_LASTLINE)) {
$response = preg_replace("/^$tag (OK|NO|BAD|BYE|PREAUTH)?\s*(\[[a-z-]+\])?\s*/i", '', trim($line));
}
return $noresp ? $code : array($code, $response);
}
/**
* Splits IMAP response into string tokens
*
* @param string &$str The IMAP's server response
* @param int $num Number of tokens to return
*
* @return mixed Tokens array or string if $num=1
* @since 0.5-beta
*/
public static function tokenizeResponse(&$str, $num=0)
{
$result = array();
while (!$num || count($result) < $num) {
// remove spaces from the beginning of the string
$str = ltrim($str);
switch ($str[0]) {
// String literal
case '{':
if (($epos = strpos($str, "}\r\n", 1)) == false) {
// error
}
if (!is_numeric(($bytes = substr($str, 1, $epos - 1)))) {
// error
}
$result[] = $bytes ? substr($str, $epos + 3, $bytes) : '';
$str = substr($str, $epos + 3 + $bytes);
break;
// Quoted string
case '"':
$len = strlen($str);
for ($pos=1; $pos<$len; $pos++) {
if ($str[$pos] == '"') {
break;
}
if ($str[$pos] == "\\") {
if ($str[$pos + 1] == '"' || $str[$pos + 1] == "\\") {
$pos++;
}
}
}
// we need to strip slashes for a quoted string
$result[] = stripslashes(substr($str, 1, $pos - 1));
$str = substr($str, $pos + 1);
break;
// Parenthesized list
case '(':
$str = substr($str, 1);
$result[] = self::tokenizeResponse($str);
break;
case ')':
$str = substr($str, 1);
return $result;
// String atom, number, astring, NIL, *, %
default:
// empty string
if ($str === '' || $str === null) {
break 2;
}
// excluded chars: SP, CTL, ), DEL
// we do not exclude [ and ] (#1489223)
if (preg_match('/^([^\x00-\x20\x29\x7F]+)/', $str, $m)) {
$result[] = $m[1] == 'NIL' ? null : $m[1];
$str = substr($str, strlen($m[1]));
}
break;
}
}
return $num == 1 ? $result[0] : $result;
}
/**
* Joins IMAP command line elements (recursively)
*/
protected static function r_implode($element)
{
$string = '';
if (is_array($element)) {
reset($element);
foreach ($element as $value) {
$string .= ' ' . self::r_implode($value);
}
}
else {
return $element;
}
return '(' . trim($string) . ')';
}
/**
* Converts message identifiers array into sequence-set syntax
*
* @param array $messages Message identifiers
* @param bool $force Forces compression of any size
*
* @return string Compressed sequence-set
*/
public static function compressMessageSet($messages, $force=false)
{
// given a comma delimited list of independent mid's,
// compresses by grouping sequences together
if (!is_array($messages)) {
// if less than 255 bytes long, let's not bother
if (!$force && strlen($messages) < 255) {
return preg_match('/[^0-9:,*]/', $messages) ? 'INVALID' : $messages;
}
// see if it's already been compressed
if (strpos($messages, ':') !== false) {
return preg_match('/[^0-9:,*]/', $messages) ? 'INVALID' : $messages;
}
// separate, then sort
$messages = explode(',', $messages);
}
sort($messages);
$result = array();
$start = $prev = $messages[0];
foreach ($messages as $id) {
$incr = $id - $prev;
if ($incr > 1) { // found a gap
if ($start == $prev) {
$result[] = $prev; // push single id
}
else {
$result[] = $start . ':' . $prev; // push sequence as start_id:end_id
}
$start = $id; // start of new sequence
}
$prev = $id;
}
// handle the last sequence/id
if ($start == $prev) {
$result[] = $prev;
}
else {
$result[] = $start.':'.$prev;
}
// return as comma separated string
$result = implode(',', $result);
return preg_match('/[^0-9:,*]/', $result) ? 'INVALID' : $result;
}
/**
* Converts message sequence-set into array
*
* @param string $messages Message identifiers
*
* @return array List of message identifiers
*/
public static function uncompressMessageSet($messages)
{
if (empty($messages)) {
return array();
}
$result = array();
$messages = explode(',', $messages);
foreach ($messages as $idx => $part) {
$items = explode(':', $part);
$max = max($items[0], $items[1]);
for ($x=$items[0]; $x<=$max; $x++) {
$result[] = (int)$x;
}
unset($messages[$idx]);
}
return $result;
}
/**
* Clear internal status cache
*/
protected function clear_status_cache($mailbox)
{
unset($this->data['STATUS:' . $mailbox]);
$keys = array('EXISTS', 'RECENT', 'UNSEEN', 'UID-MAP');
foreach ($keys as $key) {
unset($this->data[$key]);
}
}
/**
* Clear internal cache of the current mailbox
*/
protected function clear_mailbox_cache()
{
$this->clear_status_cache($this->selected);
$keys = array('UIDNEXT', 'UIDVALIDITY', 'HIGHESTMODSEQ', 'NOMODSEQ',
'PERMANENTFLAGS', 'QRESYNC', 'VANISHED', 'READ-WRITE');
foreach ($keys as $key) {
unset($this->data[$key]);
}
}
/**
* Converts flags array into string for inclusion in IMAP command
*
* @param array $flags Flags (see self::flags)
*
* @return string Space-separated list of flags
*/
protected function flagsToStr($flags)
{
foreach ((array)$flags as $idx => $flag) {
if ($flag = $this->flags[strtoupper($flag)]) {
$flags[$idx] = $flag;
}
}
return implode(' ', (array)$flags);
}
/**
* CAPABILITY response parser
*/
protected function parseCapability($str, $trusted=false)
{
$str = preg_replace('/^\* CAPABILITY /i', '', $str);
$this->capability = explode(' ', strtoupper($str));
if (!empty($this->prefs['disabled_caps'])) {
$this->capability = array_diff($this->capability, $this->prefs['disabled_caps']);
}
if (!isset($this->prefs['literal+']) && in_array('LITERAL+', $this->capability)) {
$this->prefs['literal+'] = true;
}
if ($trusted) {
$this->capability_readed = true;
}
}
/**
* Escapes a string when it contains special characters (RFC3501)
*
* @param string $string IMAP string
* @param boolean $force_quotes Forces string quoting (for atoms)
*
* @return string String atom, quoted-string or string literal
* @todo lists
*/
public static function escape($string, $force_quotes=false)
{
if ($string === null) {
return 'NIL';
}
if ($string === '') {
return '""';
}
// atom-string (only safe characters)
if (!$force_quotes && !preg_match('/[\x00-\x20\x22\x25\x28-\x2A\x5B-\x5D\x7B\x7D\x80-\xFF]/', $string)) {
return $string;
}
// quoted-string
if (!preg_match('/[\r\n\x00\x80-\xFF]/', $string)) {
return '"' . addcslashes($string, '\\"') . '"';
}
// literal-string
return sprintf("{%d}\r\n%s", strlen($string), $string);
}
/**
* Set the value of the debugging flag.
*
* @param boolean $debug New value for the debugging flag.
* @param callback $handler Logging handler function
*
* @since 0.5-stable
*/
public function setDebug($debug, $handler = null)
{
$this->debug = $debug;
$this->debug_handler = $handler;
}
/**
* Write the given debug text to the current debug output handler.
*
* @param string $message Debug message text.
*
* @since 0.5-stable
*/
protected function debug($message)
{
if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) {
$diff = $len - self::DEBUG_LINE_LENGTH;
$message = substr($message, 0, self::DEBUG_LINE_LENGTH)
. "... [truncated $diff bytes]";
}
if ($this->resourceid) {
$message = sprintf('[%s] %s', $this->resourceid, $message);
}
if ($this->debug_handler) {
call_user_func_array($this->debug_handler, array(&$this, $message));
}
else {
echo "DEBUG: $message\n";
}
}
}
diff --git a/program/lib/Roundcube/rcube_imap_search.php b/program/lib/Roundcube/rcube_imap_search.php
index 86b10c90a..cc1a86667 100644
--- a/program/lib/Roundcube/rcube_imap_search.php
+++ b/program/lib/Roundcube/rcube_imap_search.php
@@ -1,229 +1,229 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
- | Copyright (C) 2013, The Roundcube Dev Team |
- | Copyright (C) 2014, Kolab Systems AG |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Execute (multi-threaded) searches in multiple IMAP folders |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Class to control search jobs on multiple IMAP folders.
*
* @package Framework
* @subpackage Storage
* @author Thomas Bruederli <roundcube@gmail.com>
*/
class rcube_imap_search
{
public $options = array();
protected $jobs = array();
protected $timelimit = 0;
protected $results;
protected $conn;
/**
* Default constructor
*/
public function __construct($options, $conn)
{
$this->options = $options;
$this->conn = $conn;
}
/**
* Invoke search request to IMAP server
*
* @param array $folders List of IMAP folders to search in
* @param string $str Search criteria
* @param string $charset Search charset
* @param string $sort_field Header field to sort by
* @param boolean $threading True if threaded listing is active
*/
public function exec($folders, $str, $charset = null, $sort_field = null, $threading=null)
{
$start = floor(microtime(true));
$results = new rcube_result_multifolder($folders);
// start a search job for every folder to search in
foreach ($folders as $folder) {
// a complete result for this folder already exists
$result = $this->results ? $this->results->get_set($folder) : false;
if ($result && !$result->incomplete) {
$results->add($result);
}
else {
$search = is_array($str) && $str[$folder] ? $str[$folder] : $str;
$job = new rcube_imap_search_job($folder, $search, $charset, $sort_field, $threading);
$job->worker = $this;
$this->jobs[] = $job;
}
}
// execute jobs and gather results
foreach ($this->jobs as $job) {
// only run search if within the configured time limit
// TODO: try to estimate the required time based on folder size and previous search performance
if (!$this->timelimit || floor(microtime(true)) - $start < $this->timelimit) {
$job->run();
}
// add result (may have ->incomplete flag set)
$results->add($job->get_result());
}
return $results;
}
/**
* Setter for timelimt property
*/
public function set_timelimit($seconds)
{
$this->timelimit = $seconds;
}
/**
* Setter for previous (potentially incomplete) search results
*/
public function set_results($res)
{
$this->results = $res;
}
/**
* Get connection to the IMAP server
* (used for single-thread mode)
*/
public function get_imap()
{
return $this->conn;
}
}
/**
* Stackable item to run the search on a specific IMAP folder
*/
class rcube_imap_search_job /* extends Stackable */
{
private $folder;
private $search;
private $charset;
private $sort_field;
private $threading;
private $result;
public function __construct($folder, $str, $charset = null, $sort_field = null, $threading=false)
{
$this->folder = $folder;
$this->search = $str;
$this->charset = $charset;
$this->sort_field = $sort_field;
$this->threading = $threading;
$this->result = new rcube_result_index($folder);
$this->result->incomplete = true;
}
public function run()
{
$this->result = $this->search_index();
}
/**
* Copy of rcube_imap::search_index()
*/
protected function search_index()
{
$criteria = $this->search;
$charset = $this->charset;
$imap = $this->worker->get_imap();
if (!$imap->connected()) {
trigger_error("No IMAP connection for $this->folder", E_USER_WARNING);
if ($this->threading) {
return new rcube_result_thread($this->folder);
}
else {
return new rcube_result_index($this->folder);
}
}
if ($this->worker->options['skip_deleted'] && !preg_match('/UNDELETED/', $criteria)) {
$criteria = 'UNDELETED '.$criteria;
}
// unset CHARSET if criteria string is ASCII, this way
// SEARCH won't be re-sent after "unsupported charset" response
if ($charset && $charset != 'US-ASCII' && is_ascii($criteria)) {
$charset = 'US-ASCII';
}
if ($this->threading) {
$threads = $imap->thread($this->folder, $this->threading, $criteria, true, $charset);
// Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
// but I've seen that Courier doesn't support UTF-8)
if ($threads->is_error() && $charset && $charset != 'US-ASCII') {
$threads = $imap->thread($this->folder, $this->threading,
rcube_imap::convert_criteria($criteria, $charset), true, 'US-ASCII');
}
return $threads;
}
if ($this->sort_field) {
$messages = $imap->sort($this->folder, $this->sort_field, $criteria, true, $charset);
// Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
// but I've seen Courier with disabled UTF-8 support)
if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
$messages = $imap->sort($this->folder, $this->sort_field,
rcube_imap::convert_criteria($criteria, $charset), true, 'US-ASCII');
}
}
if (!$messages || $messages->is_error()) {
$messages = $imap->search($this->folder,
($charset && $charset != 'US-ASCII' ? "CHARSET $charset " : '') . $criteria, true);
// Error, try with US-ASCII (some servers may support only US-ASCII)
if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
$messages = $imap->search($this->folder,
rcube_imap::convert_criteria($criteria, $charset), true);
}
}
return $messages;
}
public function get_search_set()
{
return array(
$this->search,
$this->result,
$this->charset,
$this->sort_field,
$this->threading,
);
}
public function get_result()
{
return $this->result;
}
}
diff --git a/program/lib/Roundcube/rcube_ldap.php b/program/lib/Roundcube/rcube_ldap.php
index defb92d36..ef2aeb7c3 100644
--- a/program/lib/Roundcube/rcube_ldap.php
+++ b/program/lib/Roundcube/rcube_ldap.php
@@ -1,2165 +1,2166 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2006-2013, The Roundcube Dev Team |
- | Copyright (C) 2011-2013, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Interface to an LDAP address directory |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Andreas Dick <andudi (at) gmx (dot) ch> |
| Aleksander Machniak <machniak@kolabsys.com> |
+-----------------------------------------------------------------------+
*/
/**
* Model class to access an LDAP address directory
*
* @package Framework
* @subpackage Addressbook
*/
class rcube_ldap extends rcube_addressbook
{
// public properties
public $primary_key = 'ID';
public $groups = false;
public $readonly = true;
public $ready = false;
public $group_id = 0;
public $coltypes = array();
public $export_groups = false;
// private properties
protected $ldap;
protected $formats = array();
protected $prop = array();
protected $fieldmap = array();
protected $filter = '';
protected $sub_filter;
protected $result;
protected $ldap_result;
protected $mail_domain = '';
protected $debug = false;
/**
* Group objectclass (lowercase) to member attribute mapping
*
* @var array
*/
private $group_types = array(
'group' => 'member',
'groupofnames' => 'member',
'kolabgroupofnames' => 'member',
'groupofuniquenames' => 'uniqueMember',
'kolabgroupofuniquenames' => 'uniqueMember',
'univentiongroup' => 'uniqueMember',
'groupofurls' => null,
);
private $base_dn = '';
private $groups_base_dn = '';
private $group_data;
private $group_search_cache;
private $cache;
/**
* Object constructor
*
* @param array $p LDAP connection properties
* @param boolean $debug Enables debug mode
* @param string $mail_domain Current user mail domain name
*/
function __construct($p, $debug = false, $mail_domain = null)
{
$this->prop = $p;
$fetch_attributes = array('objectClass');
// check if groups are configured
if (is_array($p['groups']) && count($p['groups'])) {
$this->groups = true;
// set member field
if (!empty($p['groups']['member_attr']))
$this->prop['member_attr'] = strtolower($p['groups']['member_attr']);
else if (empty($p['member_attr']))
$this->prop['member_attr'] = 'member';
// set default name attribute to cn
if (empty($this->prop['groups']['name_attr']))
$this->prop['groups']['name_attr'] = 'cn';
if (empty($this->prop['groups']['scope']))
$this->prop['groups']['scope'] = 'sub';
// extend group objectclass => member attribute mapping
if (!empty($this->prop['groups']['class_member_attr']))
$this->group_types = array_merge($this->group_types, $this->prop['groups']['class_member_attr']);
// add group name attrib to the list of attributes to be fetched
$fetch_attributes[] = $this->prop['groups']['name_attr'];
}
if (is_array($p['group_filters'])) {
$this->groups = $this->groups || count($p['group_filters']);
foreach ($p['group_filters'] as $k => $group_filter) {
// set default name attribute to cn
if (empty($group_filter['name_attr']) && empty($this->prop['groups']['name_attr']))
$this->prop['group_filters'][$k]['name_attr'] = $group_filter['name_attr'] = 'cn';
if ($group_filter['name_attr'])
$fetch_attributes[] = $group_filter['name_attr'];
}
}
// fieldmap property is given
if (is_array($p['fieldmap'])) {
$p['fieldmap'] = array_filter($p['fieldmap']);
foreach ($p['fieldmap'] as $rf => $lf)
$this->fieldmap[$rf] = $this->_attr_name($lf);
}
else if (!empty($p)) {
// read deprecated *_field properties to remain backwards compatible
foreach ($p as $prop => $value)
if (!empty($value) && preg_match('/^(.+)_field$/', $prop, $matches))
$this->fieldmap[$matches[1]] = $this->_attr_name($value);
}
// use fieldmap to advertise supported coltypes to the application
foreach ($this->fieldmap as $colv => $lfv) {
list($col, $type) = explode(':', $colv);
$params = explode(':', $lfv);
$lf = array_shift($params);
$limit = 1;
foreach ($params as $idx => $param) {
// field format specification
if (preg_match('/^(date)\[(.+)\]$/i', $param, $m)) {
$this->formats[$lf] = array('type' => strtolower($m[1]), 'format' => $m[2]);
}
// first argument is a limit
else if ($idx === 0) {
if ($param == '*') $limit = null;
else $limit = max(1, intval($param));
}
// second is a composite field separator
else if ($idx === 1 && $param) {
$this->coltypes[$col]['serialized'][$type] = $param;
}
}
if (!is_array($this->coltypes[$col])) {
$subtypes = $type ? array($type) : null;
$this->coltypes[$col] = array('limit' => $limit, 'subtypes' => $subtypes, 'attributes' => array($lf));
}
elseif ($type) {
$this->coltypes[$col]['subtypes'][] = $type;
$this->coltypes[$col]['attributes'][] = $lf;
$this->coltypes[$col]['limit'] += $limit;
}
$this->fieldmap[$colv] = $lf;
}
// support for composite address
if ($this->coltypes['street'] && $this->coltypes['locality']) {
$this->coltypes['address'] = array(
'limit' => max(1, $this->coltypes['locality']['limit'] + $this->coltypes['address']['limit']),
'subtypes' => array_merge((array)$this->coltypes['address']['subtypes'], (array)$this->coltypes['locality']['subtypes']),
'childs' => array(),
'attributes' => array(),
) + (array)$this->coltypes['address'];
foreach (array('street','locality','zipcode','region','country') as $childcol) {
if ($this->coltypes[$childcol]) {
$this->coltypes['address']['childs'][$childcol] = array('type' => 'text');
$this->coltypes['address']['attributes'] = array_merge($this->coltypes['address']['attributes'], $this->coltypes[$childcol]['attributes']);
unset($this->coltypes[$childcol]); // remove address child col from global coltypes list
}
}
// at least one address type must be specified
if (empty($this->coltypes['address']['subtypes'])) {
$this->coltypes['address']['subtypes'] = array('home');
}
}
else if ($this->coltypes['address']) {
$this->coltypes['address'] += array('type' => 'textarea', 'childs' => null, 'size' => 40);
// 'serialized' means the UI has to present a composite address field
if ($this->coltypes['address']['serialized']) {
$childprop = array('type' => 'text');
$this->coltypes['address']['type'] = 'composite';
$this->coltypes['address']['childs'] = array('street' => $childprop, 'locality' => $childprop, 'zipcode' => $childprop, 'country' => $childprop);
}
}
// make sure 'required_fields' is an array
if (!is_array($this->prop['required_fields'])) {
$this->prop['required_fields'] = (array) $this->prop['required_fields'];
}
// make sure LDAP_rdn field is required
if (!empty($this->prop['LDAP_rdn']) && !in_array($this->prop['LDAP_rdn'], $this->prop['required_fields'])
&& !in_array($this->prop['LDAP_rdn'], array_keys((array)$this->prop['autovalues']))) {
$this->prop['required_fields'][] = $this->prop['LDAP_rdn'];
}
foreach ($this->prop['required_fields'] as $key => $val) {
$this->prop['required_fields'][$key] = $this->_attr_name($val);
}
// Build sub_fields filter
if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) {
$this->sub_filter = '';
foreach ($this->prop['sub_fields'] as $class) {
if (!empty($class)) {
$class = is_array($class) ? array_pop($class) : $class;
$this->sub_filter .= '(objectClass=' . $class . ')';
}
}
if (count($this->prop['sub_fields']) > 1) {
$this->sub_filter = '(|' . $this->sub_filter . ')';
}
}
$this->sort_col = is_array($p['sort']) ? $p['sort'][0] : $p['sort'];
$this->debug = $debug;
$this->mail_domain = $this->prop['mail_domain'] = $mail_domain;
// initialize cache
$rcube = rcube::get_instance();
if ($cache_type = $rcube->config->get('ldap_cache', 'db')) {
$cache_ttl = $rcube->config->get('ldap_cache_ttl', '10m');
$cache_name = 'LDAP.' . asciiwords($this->prop['name']);
$this->cache = $rcube->get_cache($cache_name, $cache_type, $cache_ttl);
}
// determine which attributes to fetch
$this->prop['list_attributes'] = array_unique($fetch_attributes);
$this->prop['attributes'] = array_merge(array_values($this->fieldmap), $fetch_attributes);
foreach ($rcube->config->get('contactlist_fields') as $col) {
$this->prop['list_attributes'] = array_merge($this->prop['list_attributes'], $this->_map_field($col));
}
// initialize ldap wrapper object
$this->ldap = new rcube_ldap_generic($this->prop);
$this->ldap->config_set(array('cache' => $this->cache, 'debug' => $this->debug));
$this->_connect();
}
/**
* Establish a connection to the LDAP server
*/
private function _connect()
{
$rcube = rcube::get_instance();
if ($this->ready) {
return true;
}
if (!is_array($this->prop['hosts'])) {
$this->prop['hosts'] = array($this->prop['hosts']);
}
// try to connect + bind for every host configured
// with OpenLDAP 2.x ldap_connect() always succeeds but ldap_bind will fail if host isn't reachable
// see http://www.php.net/manual/en/function.ldap-connect.php
foreach ($this->prop['hosts'] as $host) {
// skip host if connection failed
if (!$this->ldap->connect($host)) {
continue;
}
// See if the directory is writeable.
if ($this->prop['writable']) {
$this->readonly = false;
}
// trigger post-connect hook
$rcube = rcube::get_instance();
$conf = $rcube->plugins->exec_hook('ldap_connected', $this->prop + array('host' => $host));
$bind_pass = $conf['bind_pass'];
$bind_user = $conf['bind_user'];
$bind_dn = $conf['bind_dn'];
$auth_method = $conf['auth_method'];
$this->base_dn = $conf['base_dn'];
$this->groups_base_dn = $conf['groups']['base_dn'] ?: $this->base_dn;
// User specific access, generate the proper values to use.
if ($conf['user_specific']) {
// No password set, use the session password
if (empty($bind_pass)) {
$bind_pass = $rcube->get_user_password();
}
// Get the pieces needed for variable replacement.
if ($fu = ($rcube->get_user_email() ?: $conf['username'])) {
list($u, $d) = explode('@', $fu);
}
else {
$d = $this->mail_domain;
}
$dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string
// resolve $dc through LDAP
if (!empty($conf['domain_filter']) && !empty($conf['search_bind_dn']) &&
method_exists($this->ldap, 'domain_root_dn')) {
$this->ldap->bind($conf['search_bind_dn'], $conf['search_bind_pw']);
$dc = $this->ldap->domain_root_dn($d);
}
$replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
// Search for the dn to use to authenticate
if ($conf['search_base_dn'] && $conf['search_filter']
&& (strstr($bind_dn, '%dn') || strstr($this->base_dn, '%dn') || strstr($this->groups_base_dn, '%dn'))
) {
$search_attribs = array('uid');
if ($search_bind_attrib = (array) $conf['search_bind_attrib']) {
foreach ($search_bind_attrib as $r => $attr) {
$search_attribs[] = $attr;
$replaces[$r] = '';
}
}
$search_bind_dn = strtr($conf['search_bind_dn'], $replaces);
$search_base_dn = strtr($conf['search_base_dn'], $replaces);
$search_filter = strtr($conf['search_filter'], $replaces);
$cache_key = 'DN.' . md5("$host:$search_bind_dn:$search_base_dn:$search_filter:" . $conf['search_bind_pw']);
if ($this->cache && ($dn = $this->cache->get($cache_key))) {
$replaces['%dn'] = $dn;
}
else {
$ldap = $this->ldap;
if (!empty($search_bind_dn) && !empty($conf['search_bind_pw'])) {
// To protect from "Critical extension is unavailable" error
// we need to use a separate LDAP connection
if (!empty($conf['vlv'])) {
$ldap = new rcube_ldap_generic($conf);
$ldap->config_set(array('cache' => $this->cache, 'debug' => $this->debug));
if (!$ldap->connect($host)) {
continue;
}
}
if (!$ldap->bind($search_bind_dn, $conf['search_bind_pw'])) {
continue; // bind failed, try next host
}
}
$res = $ldap->search($search_base_dn, $search_filter, 'sub', $search_attribs);
if ($res) {
$res->rewind();
$replaces['%dn'] = key($res->entries(TRUE));
// add more replacements from 'search_bind_attrib' config
if ($search_bind_attrib) {
$res = $res->current();
foreach ($search_bind_attrib as $r => $attr) {
$replaces[$r] = $res[$attr][0];
}
}
}
if ($ldap != $this->ldap) {
$ldap->close();
}
}
// DN not found
if (empty($replaces['%dn'])) {
if (!empty($conf['search_dn_default']))
$replaces['%dn'] = $conf['search_dn_default'];
else {
rcube::raise_error(array(
'code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "DN not found using LDAP search."), true);
continue;
}
}
if ($this->cache && !empty($replaces['%dn'])) {
$this->cache->set($cache_key, $replaces['%dn']);
}
}
// Replace the bind_dn and base_dn variables.
$bind_dn = strtr($bind_dn, $replaces);
$this->base_dn = strtr($this->base_dn, $replaces);
$this->groups_base_dn = strtr($this->groups_base_dn, $replaces);
// replace placeholders in filter settings
if (!empty($conf['filter'])) {
$this->prop['filter'] = strtr($conf['filter'], $replaces);
}
foreach (array('base_dn','filter','member_filter') as $k) {
if (!empty($conf['groups'][$k])) {
$this->prop['groups'][$k] = strtr($conf['groups'][$k], $replaces);
}
}
if (is_array($conf['group_filters'])) {
foreach ($conf['group_filters'] as $i => $gf) {
if (!empty($gf['base_dn'])) {
$this->prop['group_filters'][$i]['base_dn'] = strtr($gf['base_dn'], $replaces);
}
if (!empty($gf['filter'])) {
$this->prop['group_filters'][$i]['filter'] = strtr($gf['filter'], $replaces);
}
}
}
if (empty($bind_user)) {
$bind_user = $u;
}
}
if (empty($bind_pass) && strcasecmp($auth_method, 'GSSAPI') != 0) {
$this->ready = true;
}
else {
if (!empty($conf['auth_cid'])) {
$this->ready = $this->ldap->sasl_bind($conf['auth_cid'], $bind_pass, $bind_dn);
}
else if (!empty($bind_dn)) {
$this->ready = $this->ldap->bind($bind_dn, $bind_pass);
}
else {
$this->ready = $this->ldap->sasl_bind($bind_user, $bind_pass);
}
}
// connection established, we're done here
if ($this->ready) {
break;
}
} // end foreach hosts
if (!is_resource($this->ldap->conn)) {
rcube::raise_error(array('code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not connect to any LDAP server, last tried $host"), true);
return false;
}
return $this->ready;
}
/**
* Close connection to LDAP server
*/
function close()
{
if ($this->ldap) {
$this->ldap->close();
}
}
/**
* Returns address book name
*
* @return string Address book name
*/
function get_name()
{
return $this->prop['name'];
}
/**
* Set internal list page
*
* @param number Page number to list
*/
function set_page($page)
{
$this->list_page = (int)$page;
$this->ldap->set_vlv_page($this->list_page, $this->page_size);
}
/**
* Set internal page size
*
* @param number Number of records to display on one page
*/
function set_pagesize($size)
{
$this->page_size = (int)$size;
$this->ldap->set_vlv_page($this->list_page, $this->page_size);
}
/**
* Set internal sort settings
*
* @param string $sort_col Sort column
* @param string $sort_order Sort order
*/
function set_sort_order($sort_col, $sort_order = null)
{
if ($this->coltypes[$sort_col]['attributes'])
$this->sort_col = $this->coltypes[$sort_col]['attributes'][0];
}
/**
* Save a search string for future listings
*
* @param string $filter 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)
{
if ($this->prop['searchonly'] && empty($this->filter) && !$this->group_id) {
$this->result = new rcube_result_set(0);
$this->result->searchonly = true;
return $this->result;
}
// fetch group members recursively
if ($this->group_id && $this->group_data['dn']) {
$entries = $this->list_group_members($this->group_data['dn']);
// make list of entries unique and sort it
$seen = array();
foreach ($entries as $i => $rec) {
if ($seen[$rec['dn']]++)
unset($entries[$i]);
}
usort($entries, array($this, '_entry_sort_cmp'));
$entries['count'] = count($entries);
$this->result = new rcube_result_set($entries['count'], ($this->list_page-1) * $this->page_size);
}
else {
// exec LDAP search if no result resource is stored
if ($this->ready && $this->ldap_result === null) {
$this->ldap_result = $this->extended_search();
}
// count contacts for this user
$this->result = $this->count();
$entries = $this->ldap_result;
} // end else
// start and end of the page
$start_row = $this->ldap->vlv_active ? 0 : $this->result->first;
$start_row = $subset < 0 ? $start_row + $this->page_size + $subset : $start_row;
$last_row = $this->result->first + $this->page_size;
$last_row = $subset != 0 ? $start_row + abs($subset) : $last_row;
// filter entries for this page
for ($i = $start_row; $i < min($entries['count'], $last_row); $i++)
if ($entries[$i])
$this->result->add($this->_ldap2result($entries[$i]));
return $this->result;
}
/**
* Get all members of the given group
*
* @param string Group DN
* @param boolean Count only
* @param array Group entries (if called recursively)
* @return array Accumulated group members
*/
function list_group_members($dn, $count = false, $entries = null)
{
$group_members = array();
// fetch group object
if (empty($entries)) {
$attribs = array_merge(array('dn','objectClass','memberURL'), array_values($this->group_types));
$entries = $this->ldap->read_entries($dn, '(objectClass=*)', $attribs);
if ($entries === false) {
return $group_members;
}
}
for ($i=0; $i < $entries['count']; $i++) {
$entry = $entries[$i];
$attrs = array();
foreach ((array)$entry['objectclass'] as $objectclass) {
if (($member_attr = $this->get_group_member_attr(array($objectclass), ''))
&& ($member_attr = strtolower($member_attr)) && !in_array($member_attr, $attrs)
) {
$members = $this->_list_group_members($dn, $entry, $member_attr, $count);
$group_members = array_merge($group_members, $members);
$attrs[] = $member_attr;
}
else if (!empty($entry['memberurl'])) {
$members = $this->_list_group_memberurl($dn, $entry, $count);
$group_members = array_merge($group_members, $members);
}
if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit']) {
break 2;
}
}
}
return array_filter($group_members);
}
/**
* Fetch members of the given group entry from server
*
* @param string Group DN
* @param array Group entry
* @param string Member attribute to use
* @param boolean Count only
* @return array Accumulated group members
*/
private function _list_group_members($dn, $entry, $attr, $count)
{
// Use the member attributes to return an array of member ldap objects
// NOTE that the member attribute is supposed to contain a DN
$group_members = array();
if (empty($entry[$attr])) {
return $group_members;
}
// read these attributes for all members
$attrib = $count ? array('dn','objectClass') : $this->prop['list_attributes'];
$attrib = array_merge($attrib, array_values($this->group_types));
$attrib[] = 'memberURL';
$filter = $this->prop['groups']['member_filter'] ?: '(objectclass=*)';
for ($i=0; $i < $entry[$attr]['count']; $i++) {
if (empty($entry[$attr][$i]))
continue;
$members = $this->ldap->read_entries($entry[$attr][$i], $filter, $attrib);
if ($members == false) {
$members = array();
}
// for nested groups, call recursively
$nested_group_members = $this->list_group_members($entry[$attr][$i], $count, $members);
unset($members['count']);
$group_members = array_merge($group_members, array_filter($members), $nested_group_members);
}
return $group_members;
}
/**
* List members of group class groupOfUrls
*
* @param string Group DN
* @param array Group entry
* @param boolean True if only used for counting
* @return array Accumulated group members
*/
private function _list_group_memberurl($dn, $entry, $count)
{
$group_members = array();
for ($i=0; $i < $entry['memberurl']['count']; $i++) {
// extract components from url
if (!preg_match('!ldap://[^/]*/([^\?]+)\?\?(\w+)\?(.*)$!', $entry['memberurl'][$i], $m)) {
continue;
}
// add search filter if any
$filter = $this->filter ? '(&(' . $m[3] . ')(' . $this->filter . '))' : $m[3];
$attrs = $count ? array('dn','objectClass') : $this->prop['list_attributes'];
if ($result = $this->ldap->search($m[1], $filter, $m[2], $attrs, $this->group_data)) {
$entries = $result->entries();
for ($j = 0; $j < $entries['count']; $j++) {
if ($this->is_group_entry($entries[$j]) && ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count)))
$group_members = array_merge($group_members, $nested_group_members);
else
$group_members[] = $entries[$j];
}
}
}
return $group_members;
}
/**
* Callback for sorting entries
*/
function _entry_sort_cmp($a, $b)
{
return strcmp($a[$this->sort_col][0], $b[$this->sort_col][0]);
}
/**
* Search contacts
*
* @param mixed $fields The field name of array of field names to search in
* @param mixed $value Search value (or array of values when $fields is array)
* @param int $mode Matching mode. Sum of rcube_addressbook::SEARCH_*
* @param boolean $select True if results are requested, False if count only
* @param boolean $nocount (Not used)
* @param array $required List of fields that cannot be empty
*
* @return rcube_result_set List of contact records
*/
function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array())
{
$mode = intval($mode);
// special treatment for ID-based search
if ($fields == 'ID' || $fields == $this->primary_key) {
$ids = !is_array($value) ? explode(',', $value) : $value;
$result = new rcube_result_set();
foreach ($ids as $id) {
if ($rec = $this->get_record($id, true)) {
$result->add($rec);
$result->count++;
}
}
return $result;
}
// use VLV pseudo-search for autocompletion
$rcube = rcube::get_instance();
$list_fields = $rcube->config->get('contactlist_fields');
if ($this->prop['vlv_search'] && $this->ready && join(',', (array)$fields) == join(',', $list_fields)) {
$this->result = new rcube_result_set(0);
$this->ldap->config_set('fuzzy_search', intval($this->prop['fuzzy_search'] && !($mode & rcube_addressbook::SEARCH_STRICT)));
$ldap_data = $this->ldap->search($this->base_dn, $this->prop['filter'], $this->prop['scope'], $this->prop['attributes'],
array('search' => $value /*, 'sort' => $this->prop['sort'] */));
if ($ldap_data === false) {
return $this->result;
}
// get all entries of this page and post-filter those that really match the query
$search = mb_strtolower($value);
foreach ($ldap_data as $entry) {
$rec = $this->_ldap2result($entry);
foreach ($fields as $f) {
foreach ((array)$rec[$f] as $val) {
if ($this->compare_search_value($f, $val, $search, $mode)) {
$this->result->add($rec);
$this->result->count++;
break 2;
}
}
}
}
return $this->result;
}
// advanced per-attribute search
if (is_array($value)) {
// use AND operator for advanced searches
$filter = '(&';
// set wildcards
$wp = $ws = '';
if (!empty($this->prop['fuzzy_search']) && !($mode & rcube_addressbook::SEARCH_STRICT)) {
$ws = '*';
if (!($mode & rcube_addressbook::SEARCH_PREFIX)) {
$wp = '*';
}
}
foreach ((array)$fields as $idx => $field) {
$val = $value[$idx];
if (!strlen($val))
continue;
if ($attrs = $this->_map_field($field)) {
if (count($attrs) > 1)
$filter .= '(|';
foreach ($attrs as $f)
$filter .= "($f=$wp" . rcube_ldap_generic::quote_string($val) . "$ws)";
if (count($attrs) > 1)
$filter .= ')';
}
}
$filter .= ')';
}
else {
if ($fields == '*') {
// search_fields are required for fulltext search
if (empty($this->prop['search_fields'])) {
$this->set_error(self::ERROR_SEARCH, 'nofulltextsearch');
$this->result = new rcube_result_set();
return $this->result;
}
$attributes = (array)$this->prop['search_fields'];
}
else {
// map address book fields into ldap attributes
$attributes = array();
foreach ((array) $fields as $field) {
if ($this->coltypes[$field] && ($attrs = $this->coltypes[$field]['attributes'])) {
$attributes = array_merge($attributes, (array) $attrs);
}
}
}
// compose a full-text-like search filter
$filter = rcube_ldap_generic::fulltext_search_filter($value, $attributes, $mode & ~rcube_addressbook::SEARCH_GROUPS);
}
// add required (non empty) fields filter
$req_filter = '';
foreach ((array)$required as $field) {
if (in_array($field, (array)$fields)) // required field is already in search filter
continue;
if ($attrs = $this->_map_field($field)) {
if (count($attrs) > 1)
$req_filter .= '(|';
foreach ($attrs as $f)
$req_filter .= "($f=*)";
if (count($attrs) > 1)
$req_filter .= ')';
}
}
if (!empty($req_filter))
$filter = '(&' . $req_filter . $filter . ')';
// avoid double-wildcard if $value is empty
$filter = preg_replace('/\*+/', '*', $filter);
if ($mode & rcube_addressbook::SEARCH_GROUPS) {
$filter = 'e:' . $filter;
}
// set filter string and execute search
$this->set_search_set($filter);
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 (!empty($this->ldap_result)) {
$count = $this->ldap_result['count'];
}
else if ($this->group_id && $this->group_data['dn']) {
$count = count($this->list_group_members($this->group_data['dn'], true));
}
// We have a connection but no result set, attempt to get one.
else if ($this->ready) {
$count = $this->extended_search(true);
}
return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
}
/**
* Wrapper on LDAP searches with group_filters support, which
* allows searching for contacts AND groups.
*
* @param bool $count Return count instead of the records
*
* @return int|array Count of records or the result array (with 'count' item)
*/
protected function extended_search($count = false)
{
$prop = $this->group_id ? $this->group_data : $this->prop;
$base_dn = $this->group_id ? $prop['base_dn'] : $this->base_dn;
$attrs = $count ? array('dn') : $this->prop['attributes'];
$entries = array();
// Use global search filter
if ($filter = $this->filter) {
if ($filter[0] == 'e' && $filter[1] == ':') {
$filter = substr($filter, 2);
$is_extended_search = !$this->group_id;
}
$prop['filter'] = $filter;
// add general filter to query
if (!empty($this->prop['filter'])) {
$prop['filter'] = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['filter']) . ')' . $prop['filter'] . ')';
}
}
$result = $this->ldap->search($base_dn, $prop['filter'], $prop['scope'], $attrs, $prop, $count);
// we have a search result resource, get all entries
if (!$count && $result) {
$result_count = $result->count();
$result = $result->entries();
unset($result['count']);
}
// search for groups
if ($is_extended_search
&& is_array($this->prop['group_filters'])
&& !empty($this->prop['groups']['filter'])
) {
$filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['groups']['filter']) . ')' . $filter . ')';
// for groups we may use cn instead of displayname...
if ($this->prop['fieldmap']['name'] != $this->prop['groups']['name_attr']) {
$filter = str_replace(strtolower($this->prop['fieldmap']['name']) . '=', $this->prop['groups']['name_attr'] . '=', $filter);
}
$name_attr = $this->prop['groups']['name_attr'];
$email_attr = $this->prop['groups']['email_attr'] ?: 'mail';
$attrs = array_unique(array('dn', 'objectClass', $name_attr, $email_attr));
$res = $this->ldap->search($this->groups_base_dn, $filter, $this->prop['groups']['scope'], $attrs, $prop, $count);
if ($count && $res) {
$result += $res;
}
else if (!$count && $res && ($res_count = $res->count())) {
$res = $res->entries();
unset($res['count']);
$result = array_merge($result, $res);
$result_count += $res_count;
}
}
if (!$count && $result) {
// sorting
if ($this->sort_col && $prop['scope'] !== 'base' && !$this->ldap->vlv_active) {
usort($result, array($this, '_entry_sort_cmp'));
}
$result['count'] = $result_count;
$this->result_entries = $result;
}
return $result;
}
/**
* 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 = $this->result = null;
if ($this->ready && $dn) {
$dn = self::dn_decode($dn);
if ($rec = $this->ldap->get_entry($dn, $this->prop['attributes'])) {
$rec = array_change_key_case($rec, CASE_LOWER);
}
// Use ldap_list to get subentries like country (c) attribute (#1488123)
if (!empty($rec) && $this->sub_filter) {
if ($entries = $this->ldap->list_entries($dn, $this->sub_filter, array_keys($this->prop['sub_fields']))) {
foreach ($entries as $entry) {
$lrec = array_change_key_case($entry, CASE_LOWER);
$rec = array_merge($lrec, $rec);
}
}
}
if (!empty($rec)) {
// 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;
}
/**
* Returns the last error occurred (e.g. when updating/inserting failed)
*
* @return array Hash array with the following fields: type, message
*/
function get_error()
{
$err = $this->error;
// check ldap connection for errors
if (!$err && $this->ldap->get_error()) {
$err = array(self::ERROR_SEARCH, $this->ldap->get_error());
}
return $err;
}
/**
* Check the given data before saving.
* If input not valid, the message to display can be fetched using get_error()
*
* @param array Assoziative array with data to save
* @param boolean Try to fix/complete record automatically
* @return boolean True if input is valid, False if not.
*/
public function validate(&$save_data, $autofix = false)
{
// validate e-mail addresses
if (!parent::validate($save_data, $autofix)) {
return false;
}
// check for name input
if (empty($save_data['name'])) {
$this->set_error(self::ERROR_VALIDATE, 'nonamewarning');
return false;
}
// Verify that the required fields are set.
$missing = null;
$ldap_data = $this->_map_data($save_data);
foreach ($this->prop['required_fields'] as $fld) {
if (!isset($ldap_data[$fld]) || $ldap_data[$fld] === '') {
$missing[$fld] = 1;
}
}
if ($missing) {
// try to complete record automatically
if ($autofix) {
$sn_field = $this->fieldmap['surname'];
$fn_field = $this->fieldmap['firstname'];
$mail_field = $this->fieldmap['email'];
// try to extract surname and firstname from displayname
$name_parts = preg_split('/[\s,.]+/', $save_data['name']);
if ($sn_field && $missing[$sn_field]) {
$save_data['surname'] = array_pop($name_parts);
unset($missing[$sn_field]);
}
if ($fn_field && $missing[$fn_field]) {
$save_data['firstname'] = array_shift($name_parts);
unset($missing[$fn_field]);
}
// try to fix missing e-mail, very often on import
// from vCard we have email:other only defined
if ($mail_field && $missing[$mail_field]) {
$emails = $this->get_col_values('email', $save_data, true);
if (!empty($emails) && ($email = array_shift($emails))) {
$save_data['email'] = $email;
unset($missing[$mail_field]);
}
}
}
// TODO: generate message saying which fields are missing
if (!empty($missing)) {
$this->set_error(self::ERROR_VALIDATE, 'formincomplete');
return false;
}
}
return true;
}
/**
* Create a new contact record
*
* @param array Associative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
* @param boolean True to check for duplicates first
*
* @return mixed The created record ID on success, False on error
*/
function insert($save_cols, $check = false)
{
// Map out the column names to their LDAP ones to build the new entry.
$newentry = $this->_map_data($save_cols);
$newentry['objectClass'] = $this->prop['LDAP_Object_Classes'];
// add automatically generated attributes
$this->add_autovalues($newentry);
// Verify that the required fields are set.
$missing = null;
foreach ($this->prop['required_fields'] as $fld) {
if (!isset($newentry[$fld])) {
$missing[] = $fld;
}
}
// abort process if requiered fields are missing
// TODO: generate message saying which fields are missing
if ($missing) {
$this->set_error(self::ERROR_VALIDATE, 'formincomplete');
return false;
}
// Build the new entries DN.
$dn = $this->prop['LDAP_rdn'].'='.rcube_ldap_generic::quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn;
// Remove attributes that need to be added separately (child objects)
$xfields = array();
if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) {
foreach (array_keys($this->prop['sub_fields']) as $xf) {
if (!empty($newentry[$xf])) {
$xfields[$xf] = $newentry[$xf];
unset($newentry[$xf]);
}
}
}
if (!$this->ldap->add_entry($dn, $newentry)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
foreach ($xfields as $xidx => $xf) {
$xdn = $xidx.'='.rcube_ldap_generic::quote_string($xf).','.$dn;
$xf = array(
$xidx => $xf,
'objectClass' => (array) $this->prop['sub_fields'][$xidx],
);
$this->ldap->add_entry($xdn, $xf);
}
$dn = self::dn_encode($dn);
// add new contact to the selected group
if ($this->group_id)
$this->add_to_group($this->group_id, $dn);
return $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);
$newdata = array();
$replacedata = array();
$deletedata = array();
$subdata = array();
$subdeldata = array();
$subnewdata = array();
$ldap_data = $this->_map_data($save_cols);
$old_data = $record['_raw_attrib'];
// special handling of photo col
if ($photo_fld = $this->fieldmap['photo']) {
// undefined means keep old photo
if (!array_key_exists('photo', $save_cols)) {
$ldap_data[$photo_fld] = $record['photo'];
}
}
foreach ($this->fieldmap as $fld) {
if ($fld) {
$val = $ldap_data[$fld];
$old = $old_data[$fld];
// remove empty array values
if (is_array($val))
$val = array_filter($val);
// $this->_map_data() result and _raw_attrib use different format
// make sure comparing array with one element with a string works as expected
if (is_array($old) && count($old) == 1 && !is_array($val)) {
$old = array_pop($old);
}
if (is_array($val) && count($val) == 1 && !is_array($old)) {
$val = array_pop($val);
}
// Subentries must be handled separately
if (!empty($this->prop['sub_fields']) && isset($this->prop['sub_fields'][$fld])) {
if ($old != $val) {
if ($old !== null) {
$subdeldata[$fld] = $old;
}
if ($val) {
$subnewdata[$fld] = $val;
}
}
else if ($old !== null) {
$subdata[$fld] = $old;
}
continue;
}
// The field does exist compare it to the ldap record.
if ($old != $val) {
// Changed, but find out how.
if ($old === null) {
// Field was not set prior, need to add it.
$newdata[$fld] = $val;
}
else if ($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.
// #1488420: Workaround "ldap_mod_del(): Modify: Inappropriate matching in..."
// jpegPhoto attribute require an array() here. It looks to me that it works for other attribs too
$deletedata[$fld] = array();
//$deletedata[$fld] = $old_data[$fld];
}
}
else {
// The data was modified, save it out.
$replacedata[$fld] = $val;
}
} // end if
} // end if
} // end foreach
// console($old_data, $ldap_data, '----', $newdata, $replacedata, $deletedata, '----', $subdata, $subnewdata, $subdeldata);
$dn = self::dn_decode($id);
// Update the entry as required.
if (!empty($deletedata)) {
// Delete the fields.
if (!$this->ldap->mod_del($dn, $deletedata)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
} // end if
if (!empty($replacedata)) {
// Handle RDN change
if ($replacedata[$this->prop['LDAP_rdn']]) {
$newdn = $this->prop['LDAP_rdn'].'='
.rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true)
.','.$this->base_dn;
if ($dn != $newdn) {
$newrdn = $this->prop['LDAP_rdn'].'='
.rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true);
unset($replacedata[$this->prop['LDAP_rdn']]);
}
}
// Replace the fields.
if (!empty($replacedata)) {
if (!$this->ldap->mod_replace($dn, $replacedata)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
}
} // end if
// RDN change, we need to remove all sub-entries
if (!empty($newrdn)) {
$subdeldata = array_merge($subdeldata, $subdata);
$subnewdata = array_merge($subnewdata, $subdata);
}
// remove sub-entries
if (!empty($subdeldata)) {
foreach ($subdeldata as $fld => $val) {
$subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn;
if (!$this->ldap->delete_entry($subdn)) {
return false;
}
}
}
if (!empty($newdata)) {
// Add the fields.
if (!$this->ldap->mod_add($dn, $newdata)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
} // end if
// Handle RDN change
if (!empty($newrdn)) {
if (!$this->ldap->rename($dn, $newrdn, null, true)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
$dn = self::dn_encode($dn);
$newdn = self::dn_encode($newdn);
// change the group membership of the contact
if ($this->groups) {
$group_ids = $this->get_record_groups($dn);
foreach (array_keys($group_ids) as $group_id) {
$this->remove_from_group($group_id, $dn);
$this->add_to_group($group_id, $newdn);
}
}
$dn = self::dn_decode($newdn);
}
// add sub-entries
if (!empty($subnewdata)) {
foreach ($subnewdata as $fld => $val) {
$subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn;
$xf = array(
$fld => $val,
'objectClass' => (array) $this->prop['sub_fields'][$fld],
);
$this->ldap->add_entry($subdn, $xf);
}
}
return $newdn ?: true;
}
/**
* Mark one or more contact records as deleted
*
* @param array Record identifiers
* @param boolean Remove record(s) irreversible (unsupported)
*
* @return boolean True on success, False on error
*/
function delete($ids, $force=true)
{
if (!is_array($ids)) {
// Not an array, break apart the encoded DNs.
$ids = explode(',', $ids);
} // end if
foreach ($ids as $id) {
$dn = self::dn_decode($id);
// Need to delete all sub-entries first
if ($this->sub_filter) {
if ($entries = $this->ldap->list_entries($dn, $this->sub_filter)) {
foreach ($entries as $entry) {
if (!$this->ldap->delete_entry($entry['dn'])) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
}
}
}
// Delete the record.
if (!$this->ldap->delete_entry($dn)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
// remove contact from all groups where he was a member
if ($this->groups) {
$dn = self::dn_encode($dn);
$group_ids = $this->get_record_groups($dn);
foreach (array_keys($group_ids) as $group_id) {
$this->remove_from_group($group_id, $dn);
}
}
} // end foreach
return count($ids);
}
/**
* Remove all contact records
*
* @param bool $with_groups Delete also groups if enabled
*/
function delete_all($with_groups = false)
{
// searching for contact entries
$dn_list = $this->ldap->list_entries($this->base_dn, $this->prop['filter'] ?: '(objectclass=*)');
if (!empty($dn_list)) {
foreach ($dn_list as $idx => $entry) {
$dn_list[$idx] = self::dn_encode($entry['dn']);
}
$this->delete($dn_list);
}
if ($with_groups && $this->groups && ($groups = $this->_fetch_groups()) && count($groups)) {
foreach ($groups as $group) {
$this->ldap->delete_entry($group['dn']);
}
if ($this->cache) {
$this->cache->remove('groups');
}
}
}
/**
* Generate missing attributes as configured
*
* @param array LDAP record attributes
*/
protected function add_autovalues(&$attrs)
{
if (empty($this->prop['autovalues'])) {
return;
}
$attrvals = array();
foreach ($attrs as $k => $v) {
$attrvals['{'.$k.'}'] = is_array($v) ? $v[0] : $v;
}
foreach ((array)$this->prop['autovalues'] as $lf => $templ) {
if (empty($attrs[$lf])) {
if (strpos($templ, '(') !== false) {
// replace {attr} placeholders with (escaped!) attribute values to be safely eval'd
$code = preg_replace('/\{\w+\}/', '', strtr($templ, array_map('addslashes', $attrvals)));
$res = false;
try {
$res = eval("return ($code);");
}
catch (ParseError $e) {
// ignore
}
if ($res === false) {
rcube::raise_error(array(
'code' => 505, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Expression parse error on: ($code)"), true, false);
continue;
}
$attrs[$lf] = $res;
}
else {
// replace {attr} placeholders with concrete attribute values
$attrs[$lf] = preg_replace('/\{\w+\}/', '', strtr($templ, $attrvals));
}
}
}
}
/**
* Converts LDAP entry into an array
*/
private function _ldap2result($rec)
{
$out = array('_type' => 'person');
$fieldmap = $this->fieldmap;
if ($rec['dn'])
$out[$this->primary_key] = self::dn_encode($rec['dn']);
// determine record type
if ($this->is_group_entry($rec)) {
$out['_type'] = 'group';
$out['readonly'] = true;
$fieldmap['name'] = $this->group_data['name_attr'] ?: $this->prop['groups']['name_attr'];
}
// assign object type from object class mapping
if (!empty($this->prop['class_type_map'])) {
foreach (array_map('strtolower', (array)$rec['objectclass']) as $objcls) {
if (!empty($this->prop['class_type_map'][$objcls])) {
$out['_type'] = $this->prop['class_type_map'][$objcls];
break;
}
}
}
foreach ($fieldmap as $rf => $lf)
{
// we might be dealing with normalized and non-normalized data
$entry = $rec[$lf];
if (!is_array($entry) || !isset($entry['count'])) {
$entry = (array) $entry;
$entry['count'] = count($entry);
}
for ($i=0; $i < $entry['count']; $i++) {
if (!($value = $entry[$i]))
continue;
list($col, $subtype) = explode(':', $rf);
$out['_raw_attrib'][$lf][$i] = $value;
if ($col == 'email' && $this->mail_domain && !strpos($value, '@'))
$out[$rf][] = sprintf('%s@%s', $value, $this->mail_domain);
else if (in_array($col, array('street','zipcode','locality','country','region')))
$out['address' . ($subtype ? ':' : '') . $subtype][$i][$col] = $value;
else if ($col == 'address' && strpos($value, '$') !== false) // address data is represented as string separated with $
list($out[$rf][$i]['street'], $out[$rf][$i]['locality'], $out[$rf][$i]['zipcode'], $out[$rf][$i]['country']) = explode('$', $value);
else if ($entry['count'] > 1)
$out[$rf][] = $value;
else
$out[$rf] = $value;
}
// Make sure name fields aren't arrays (#1488108)
if (is_array($out[$rf]) && in_array($rf, array('name', 'surname', 'firstname', 'middlename', 'nickname'))) {
$out[$rf] = $out['_raw_attrib'][$lf] = $out[$rf][0];
}
}
return $out;
}
/**
* Return LDAP attribute(s) for the given field
*/
private function _map_field($field)
{
return (array)$this->coltypes[$field]['attributes'];
}
/**
* Convert a record data set into LDAP field attributes
*/
private function _map_data($save_cols)
{
// flatten composite fields first
foreach ($this->coltypes as $col => $colprop) {
if (is_array($colprop['childs']) && ($values = $this->get_col_values($col, $save_cols, false))) {
foreach ($values as $subtype => $childs) {
$subtype = $subtype ? ':'.$subtype : '';
foreach ($childs as $i => $child_values) {
foreach ((array)$child_values as $childcol => $value) {
$save_cols[$childcol.$subtype][$i] = $value;
}
}
}
}
// if addresses are to be saved as serialized string, do so
if (is_array($colprop['serialized'])) {
foreach ($colprop['serialized'] as $subtype => $delim) {
$key = $col.':'.$subtype;
foreach ((array)$save_cols[$key] as $i => $val) {
$values = array($val['street'], $val['locality'], $val['zipcode'], $val['country']);
$save_cols[$key][$i] = count(array_filter($values)) ? join($delim, $values) : null;
}
}
}
}
$ldap_data = array();
foreach ($this->fieldmap as $rf => $fld) {
$val = $save_cols[$rf];
// check for value in base field (eg.g email instead of email:foo)
list($col, $subtype) = explode(':', $rf);
if (!$val && !empty($save_cols[$col])) {
$val = $save_cols[$col];
unset($save_cols[$col]); // only use this value once
}
else if (!$val && !$subtype) { // extract values from subtype cols
$val = $this->get_col_values($col, $save_cols, true);
}
if (is_array($val))
$val = array_filter($val); // remove empty entries
if ($fld && $val) {
// The field does exist, add it to the entry.
$ldap_data[$fld] = $val;
}
}
foreach ($this->formats as $fld => $format) {
if (empty($ldap_data[$fld])) {
continue;
}
switch ($format['type']) {
case 'date':
if ($dt = rcube_utils::anytodatetime($ldap_data[$fld])) {
$ldap_data[$fld] = $dt->format($format['format']);
}
break;
}
}
return $ldap_data;
}
/**
* Returns unified attribute name (resolving aliases)
*/
private static function _attr_name($namev)
{
// list of known attribute aliases
static $aliases = array(
'gn' => 'givenname',
'rfc822mailbox' => 'email',
'userid' => 'uid',
'emailaddress' => 'email',
'pkcs9email' => 'email',
);
list($name, $limit) = explode(':', $namev, 2);
$suffix = $limit ? ':'.$limit : '';
$name = strtolower($name);
return (isset($aliases[$name]) ? $aliases[$name] : $name) . $suffix;
}
/**
* Determines whether the given LDAP entry is a group record
*/
private function is_group_entry($entry)
{
$classes = array_map('strtolower', (array)$entry['objectclass']);
return count(array_intersect(array_keys($this->group_types), $classes)) > 0;
}
/**
* Activate/deactivate debug mode
*
* @param boolean $dbg True if LDAP commands should be logged
*/
function set_debug($dbg = true)
{
$this->debug = $dbg;
if ($this->ldap) {
$this->ldap->config_set('debug', $dbg);
}
}
/**
* Setter for the current group
*/
function set_group($group_id)
{
if ($group_id) {
$this->group_id = $group_id;
$this->group_data = $this->get_group_entry($group_id);
}
else {
$this->group_id = 0;
$this->group_data = null;
}
}
/**
* List all active contact groups of this source
*
* @param string Optional search string to match group name
* @param int Matching mode. Sum of rcube_addressbook::SEARCH_*
*
* @return array Indexed list of contact groups, each a hash array
*/
function list_groups($search = null, $mode = 0)
{
if (!$this->groups) {
return array();
}
$group_cache = $this->_fetch_groups($search, $mode);
$groups = array();
if ($search) {
foreach ($group_cache as $group) {
if ($this->compare_search_value('name', $group['name'], mb_strtolower($search), $mode)) {
$groups[] = $group;
}
}
}
else {
$groups = $group_cache;
}
return array_values($groups);
}
/**
* Fetch groups from server
*/
private function _fetch_groups($search = null, $mode = 0, $vlv_page = null)
{
// reset group search cache
if ($search !== null && $vlv_page === null) {
$this->group_search_cache = null;
}
// return in-memory cache from previous search results
else if (is_array($this->group_search_cache) && $vlv_page === null) {
return $this->group_search_cache;
}
// special case: list groups from 'group_filters' config
if ($vlv_page === null && $search === null && is_array($this->prop['group_filters'])) {
$groups = array();
$rcube = rcube::get_instance();
// list regular groups configuration as special filter
if (!empty($this->prop['groups']['filter'])) {
$id = '__groups__';
$groups[$id] = array('ID' => $id, 'name' => $rcube->gettext('groups'), 'virtual' => true) + $this->prop['groups'];
}
foreach ($this->prop['group_filters'] as $id => $prop) {
$groups[$id] = $prop + array('ID' => $id, 'name' => ucfirst($id), 'virtual' => true, 'base_dn' => $this->base_dn);
}
return $groups;
}
if ($this->cache && $search === null && $vlv_page === null && ($groups = $this->cache->get('groups')) !== null) {
return $groups;
}
$base_dn = $this->groups_base_dn;
$filter = $this->prop['groups']['filter'];
$scope = $this->prop['groups']['scope'];
$name_attr = $this->prop['groups']['name_attr'];
$email_attr = $this->prop['groups']['email_attr'] ?: 'mail';
$sort_attrs = $this->prop['groups']['sort'] ? (array)$this->prop['groups']['sort'] : array($name_attr);
$sort_attr = $sort_attrs[0];
$ldap = $this->ldap;
// use vlv to list groups
if ($this->prop['groups']['vlv']) {
$page_size = 200;
if (!$this->prop['groups']['sort']) {
$this->prop['groups']['sort'] = $sort_attrs;
}
$ldap = clone $this->ldap;
$ldap->config_set($this->prop['groups']);
$ldap->set_vlv_page($vlv_page+1, $page_size);
}
$props = array('sort' => $this->prop['groups']['sort']);
$attrs = array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr));
// add search filter
if ($search !== null) {
// set wildcards
$wp = $ws = '';
if (!empty($this->prop['fuzzy_search']) && !($mode & rcube_addressbook::SEARCH_STRICT)) {
$ws = '*';
if (!($mode & rcube_addressbook::SEARCH_PREFIX)) {
$wp = '*';
}
}
$filter = "(&$filter($name_attr=$wp" . rcube_ldap_generic::quote_string($search) . "$ws))";
$props['search'] = $wp . $search . $ws;
}
$ldap_data = $ldap->search($base_dn, $filter, $scope, $attrs, $props);
if ($ldap_data === false) {
return array();
}
$groups = array();
$group_sortnames = array();
$group_count = $ldap_data->count();
foreach ($ldap_data as $entry) {
if (!$entry['dn']) // DN is mandatory
$entry['dn'] = $ldap_data->get_dn();
$group_name = is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr];
$group_id = self::dn_encode($entry['dn']);
$groups[$group_id]['ID'] = $group_id;
$groups[$group_id]['dn'] = $entry['dn'];
$groups[$group_id]['name'] = $group_name;
$groups[$group_id]['member_attr'] = $this->get_group_member_attr($entry['objectclass']);
// list email attributes of a group
for ($j=0; $entry[$email_attr] && $j < $entry[$email_attr]['count']; $j++) {
if (strpos($entry[$email_attr][$j], '@') > 0)
$groups[$group_id]['email'][] = $entry[$email_attr][$j];
}
$group_sortnames[] = mb_strtolower($entry[$sort_attr][0]);
}
// recursive call can exit here
if ($vlv_page > 0) {
return $groups;
}
// call recursively until we have fetched all groups
while ($this->prop['groups']['vlv'] && $group_count == $page_size) {
$next_page = $this->_fetch_groups($search, $mode, ++$vlv_page);
$groups = array_merge($groups, $next_page);
$group_count = count($next_page);
}
// when using VLV the list of groups is already sorted
if (!$this->prop['groups']['vlv']) {
array_multisort($group_sortnames, SORT_ASC, SORT_STRING, $groups);
}
// cache this
if ($this->cache && $search === null) {
$this->cache->set('groups', $groups);
}
else if ($search !== null) {
$this->group_search_cache = $groups;
}
return $groups;
}
/**
* Fetch a group entry from LDAP and save in local cache
*/
private function get_group_entry($group_id)
{
$group_cache = $this->_fetch_groups();
// add group record to cache if it isn't yet there
if (!isset($group_cache[$group_id])) {
$name_attr = $this->prop['groups']['name_attr'];
$dn = self::dn_decode($group_id);
if ($list = $this->ldap->read_entries($dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL',$name_attr,$this->fieldmap['email']))) {
$entry = $list[0];
$group_name = is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr];
$group_cache[$group_id]['ID'] = $group_id;
$group_cache[$group_id]['dn'] = $dn;
$group_cache[$group_id]['name'] = $group_name;
$group_cache[$group_id]['member_attr'] = $this->get_group_member_attr($entry['objectclass']);
}
else {
$group_cache[$group_id] = false;
}
if ($this->cache) {
$this->cache->set('groups', $group_cache);
}
}
return $group_cache[$group_id];
}
/**
* Get group properties such as name and email address(es)
*
* @param string Group identifier
* @return array Group properties as hash array
*/
function get_group($group_id)
{
$group_data = $this->get_group_entry($group_id);
unset($group_data['dn'], $group_data['member_attr']);
return $group_data;
}
/**
* Create a contact group with the given name
*
* @param string The group name
* @return mixed False on error, array with record props in success
*/
function create_group($group_name)
{
$new_dn = 'cn=' . rcube_ldap_generic::quote_string($group_name, true) . ',' . $this->groups_base_dn;
$new_gid = self::dn_encode($new_dn);
$member_attr = $this->get_group_member_attr();
$name_attr = $this->prop['groups']['name_attr'] ?: 'cn';
$new_entry = array(
'objectClass' => $this->prop['groups']['object_classes'],
$name_attr => $group_name,
$member_attr => '',
);
if (!$this->ldap->add_entry($new_dn, $new_entry)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
if ($this->cache) {
$this->cache->remove('groups');
}
return array('id' => $new_gid, 'name' => $group_name);
}
/**
* Delete the given group and all linked group members
*
* @param string Group identifier
* @return boolean True on success, false if no data was changed
*/
function delete_group($group_id)
{
$group_cache = $this->_fetch_groups();
$del_dn = $group_cache[$group_id]['dn'];
if (!$this->ldap->delete_entry($del_dn)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
if ($this->cache) {
unset($group_cache[$group_id]);
$this->cache->set('groups', $group_cache);
}
return true;
}
/**
* Rename a specific contact group
*
* @param string Group identifier
* @param string New name to set for this group
* @param string New group identifier (if changed, otherwise don't set)
* @return boolean New name on success, false if no data was changed
*/
function rename_group($group_id, $new_name, &$new_gid)
{
$group_cache = $this->_fetch_groups();
$old_dn = $group_cache[$group_id]['dn'];
$new_rdn = "cn=" . rcube_ldap_generic::quote_string($new_name, true);
$new_gid = self::dn_encode($new_rdn . ',' . $this->groups_base_dn);
if (!$this->ldap->rename($old_dn, $new_rdn, null, true)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
if ($this->cache) {
$this->cache->remove('groups');
}
return $new_name;
}
/**
* Add the given contact records the a certain group
*
* @param string Group identifier
* @param array|string List of contact identifiers to be added
*
* @return int Number of contacts added
*/
function add_to_group($group_id, $contact_ids)
{
$group_cache = $this->_fetch_groups();
$member_attr = $group_cache[$group_id]['member_attr'];
$group_dn = $group_cache[$group_id]['dn'];
$new_attrs = array();
if (!is_array($contact_ids)) {
$contact_ids = explode(',', $contact_ids);
}
foreach ($contact_ids as $id) {
$new_attrs[$member_attr][] = self::dn_decode($id);
}
if (!$this->ldap->mod_add($group_dn, $new_attrs)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return 0;
}
if ($this->cache) {
$this->cache->remove('groups');
}
return count($new_attrs[$member_attr]);
}
/**
* Remove the given contact records from a certain group
*
* @param string Group identifier
* @param array|string List of contact identifiers to be removed
*
* @return int Number of deleted group members
*/
function remove_from_group($group_id, $contact_ids)
{
$group_cache = $this->_fetch_groups();
$member_attr = $group_cache[$group_id]['member_attr'];
$group_dn = $group_cache[$group_id]['dn'];
$del_attrs = array();
if (!is_array($contact_ids)) {
$contact_ids = explode(',', $contact_ids);
}
foreach ($contact_ids as $id) {
$del_attrs[$member_attr][] = self::dn_decode($id);
}
if (!$this->ldap->mod_del($group_dn, $del_attrs)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return 0;
}
if ($this->cache) {
$this->cache->remove('groups');
}
return count($del_attrs[$member_attr]);
}
/**
* Get group assignments of a specific contact record
*
* @param mixed Record identifier
*
* @return array List of assigned groups as ID=>Name pairs
* @since 0.5-beta
*/
function get_record_groups($contact_id)
{
if (!$this->groups) {
return array();
}
$base_dn = $this->groups_base_dn;
$contact_dn = self::dn_decode($contact_id);
$name_attr = $this->prop['groups']['name_attr'] ?: 'cn';
$member_attr = $this->get_group_member_attr();
$add_filter = '';
if ($member_attr != 'member' && $member_attr != 'uniqueMember')
$add_filter = "($member_attr=$contact_dn)";
$filter = strtr("(|(member=$contact_dn)(uniqueMember=$contact_dn)$add_filter)", array('\\' => '\\\\'));
$ldap_data = $this->ldap->search($base_dn, $filter, 'sub', array('dn', $name_attr));
if ($ldap_data === false) {
return array();
}
$groups = array();
foreach ($ldap_data as $entry) {
if (!$entry['dn'])
$entry['dn'] = $ldap_data->get_dn();
$group_name = $entry[$name_attr][0];
$group_id = self::dn_encode($entry['dn']);
$groups[$group_id] = $group_name;
}
return $groups;
}
/**
* Detects group member attribute name
*/
private function get_group_member_attr($object_classes = array(), $default = 'member')
{
if (empty($object_classes)) {
$object_classes = $this->prop['groups']['object_classes'];
}
if (!empty($object_classes)) {
foreach ((array)$object_classes as $oc) {
if ($attr = $this->group_types[strtolower($oc)]) {
return $attr;
}
}
}
if (!empty($this->prop['groups']['member_attr'])) {
return $this->prop['groups']['member_attr'];
}
return $default;
}
/**
* HTML-safe DN string encoding
*
* @param string $str DN string
*
* @return string Encoded HTML identifier string
*/
static function dn_encode($str)
{
// @TODO: to make output string shorter we could probably
// remove dc=* items from it
return rtrim(strtr(base64_encode($str), '+/', '-_'), '=');
}
/**
* Decodes DN string encoded with _dn_encode()
*
* @param string $str Encoded HTML identifier string
*
* @return string DN string
*/
static function dn_decode($str)
{
$str = str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT);
return base64_decode($str);
}
}
diff --git a/program/lib/Roundcube/rcube_ldap_generic.php b/program/lib/Roundcube/rcube_ldap_generic.php
index ab7b6784a..09ac70bf7 100644
--- a/program/lib/Roundcube/rcube_ldap_generic.php
+++ b/program/lib/Roundcube/rcube_ldap_generic.php
@@ -1,353 +1,352 @@
<?php
/**
+-----------------------------------------------------------------------+
- | Roundcube/rcube_ldap_generic.php |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2006-2014, The Roundcube Dev Team |
- | Copyright (C) 2012-2015, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide basic functionality for accessing LDAP directories |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Aleksander Machniak <machniak@kolabsys.com> |
+-----------------------------------------------------------------------+
*/
/**
* Model class to access an LDAP directories
*
* @package Framework
* @subpackage LDAP
*/
class rcube_ldap_generic extends Net_LDAP3
{
/** private properties */
protected $cache = null;
protected $attributes = array('dn');
protected $error;
function __construct($config = null)
{
parent::__construct($config);
$this->config_set('log_hook', array($this, 'log'));
}
/**
* Establish a connection to the LDAP server
*/
public function connect($host = null)
{
// Net_LDAP3 does not support IDNA yet
// also parse_host() here is very Roundcube specific
$host = rcube_utils::parse_host($host, $this->config['mail_domain']);
$host = rcube_utils::idn_to_ascii($host);
return parent::connect($host);
}
/**
* Prints debug/error info to the log
*/
public function log($level, $msg)
{
$msg = implode("\n", $msg);
switch ($level) {
case LOG_DEBUG:
case LOG_INFO:
case LOG_NOTICE:
if ($this->config['debug']) {
rcube::write_log('ldap', $msg);
}
break;
case LOG_EMERG:
case LOG_ALERT:
case LOG_CRIT:
rcube::raise_error($msg, true, true);
break;
case LOG_ERR:
case LOG_WARNING:
$this->error = $msg;
rcube::raise_error($msg, true, false);
break;
}
}
/**
* Returns the last LDAP error occurred
*
* @return mixed Error message string or null if no error occurred
*/
function get_error()
{
return $this->error;
}
/**
* @deprecated
*/
public function set_debug($dbg = true)
{
$this->config['debug'] = (bool) $dbg;
}
/**
* @deprecated
*/
public function set_cache($cache_engine)
{
$this->config['cache'] = $cache_engine;
}
/**
* @deprecated
*/
public static function scope2func($scope, &$ns_function = null)
{
return self::scope_to_function($scope, $ns_function);
}
/**
* @deprecated
*/
public function set_config($opt, $val = null)
{
$this->config_set($opt, $val);
}
/**
* @deprecated
*/
public function add($dn, $entry)
{
return $this->add_entry($dn, $entry);
}
/**
* @deprecated
*/
public function delete($dn)
{
return $this->delete_entry($dn);
}
/**
* Wrapper for ldap_mod_replace()
*
* @see ldap_mod_replace()
*/
public function mod_replace($dn, $entry)
{
$this->_debug("C: Replace $dn: ".print_r($entry, true));
if (!ldap_mod_replace($this->conn, $dn, $entry)) {
$this->_error("ldap_mod_replace() failed with " . ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return true;
}
/**
* Wrapper for ldap_mod_add()
*
* @see ldap_mod_add()
*/
public function mod_add($dn, $entry)
{
$this->_debug("C: Add $dn: ".print_r($entry, true));
if (!ldap_mod_add($this->conn, $dn, $entry)) {
$this->_error("ldap_mod_add() failed with " . ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return true;
}
/**
* Wrapper for ldap_mod_del()
*
* @see ldap_mod_del()
*/
public function mod_del($dn, $entry)
{
$this->_debug("C: Delete $dn: ".print_r($entry, true));
if (!ldap_mod_del($this->conn, $dn, $entry)) {
$this->_error("ldap_mod_del() failed with " . ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return true;
}
/**
* Wrapper for ldap_rename()
*
* @see ldap_rename()
*/
public function rename($dn, $newrdn, $newparent = null, $deleteoldrdn = true)
{
$this->_debug("C: Rename $dn to $newrdn");
if (!ldap_rename($this->conn, $dn, $newrdn, $newparent, $deleteoldrdn)) {
$this->_error("ldap_rename() failed with " . ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return true;
}
/**
* Wrapper for ldap_list() + ldap_get_entries()
*
* @see ldap_list()
* @see ldap_get_entries()
*/
public function list_entries($dn, $filter, $attributes = array('dn'))
{
$list = array();
$this->_debug("C: List $dn [{$filter}]");
if ($result = ldap_list($this->conn, $dn, $filter, $attributes)) {
$list = ldap_get_entries($this->conn, $result);
if ($list === false) {
$this->_error("ldap_get_entries() failed with " . ldap_error($this->conn));
return array();
}
$count = $list['count'];
unset($list['count']);
$this->_debug("S: $count record(s)");
}
else {
$this->_error("ldap_list() failed with " . ldap_error($this->conn));
}
return $list;
}
/**
* Wrapper for ldap_read() + ldap_get_entries()
*
* @see ldap_read()
* @see ldap_get_entries()
*/
public function read_entries($dn, $filter, $attributes = null)
{
$this->_debug("C: Read $dn [{$filter}]");
if ($this->conn && $dn) {
$result = @ldap_read($this->conn, $dn, $filter, $attributes, 0, (int)$this->config['sizelimit'], (int)$this->config['timelimit']);
if ($result === false) {
$this->_error("ldap_read() failed with " . ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return ldap_get_entries($this->conn, $result);
}
return false;
}
/**
* Turn an LDAP entry into a regular PHP array with attributes as keys.
*
* @param array $entry Attributes array as retrieved from ldap_get_attributes() or ldap_get_entries()
* @param bool $flat Convert one-element-array values into strings (not implemented)
*
* @return array Hash array with attributes as keys
*/
public static function normalize_entry($entry, $flat = false)
{
if (!isset($entry['count'])) {
return $entry;
}
$rec = array();
for ($i=0; $i < $entry['count']; $i++) {
$attr = $entry[$i];
if ($entry[$attr]['count'] == 1) {
switch ($attr) {
case 'objectclass':
$rec[$attr] = array(strtolower($entry[$attr][0]));
break;
default:
$rec[$attr] = $entry[$attr][0];
break;
}
}
else {
for ($j=0; $j < $entry[$attr]['count']; $j++) {
$rec[$attr][$j] = $entry[$attr][$j];
}
}
}
return $rec;
}
/**
* Compose an LDAP filter string matching all words from the search string
* in the given list of attributes.
*
* @param string $value Search value
* @param mixed $attrs List of LDAP attributes to search
* @param int $mode Matching mode:
* 0 - partial (*abc*),
* 1 - strict (=),
* 2 - prefix (abc*)
* @return string LDAP filter
*/
public static function fulltext_search_filter($value, $attributes, $mode = 1)
{
if (empty($attributes)) {
$attributes = array('cn');
}
$groups = array();
$value = str_replace('*', '', $value);
$words = $mode == 0 ? rcube_utils::tokenize_string($value, 1) : array($value);
// set wildcards
$wp = $ws = '';
if ($mode != 1) {
$ws = '*';
$wp = !$mode ? '*' : '';
}
// search each word in all listed attributes
foreach ($words as $word) {
$parts = array();
foreach ($attributes as $attr) {
$parts[] = "($attr=$wp" . self::quote_string($word) . "$ws)";
}
$groups[] = '(|' . join('', $parts) . ')';
}
return count($groups) > 1 ? '(&' . join('', $groups) . ')' : join('', $groups);
}
}
// for backward compat.
class rcube_ldap_result extends Net_LDAP3_Result {}
diff --git a/program/lib/Roundcube/rcube_message.php b/program/lib/Roundcube/rcube_message.php
index aa8d6114c..fddbf702f 100644
--- a/program/lib/Roundcube/rcube_message.php
+++ b/program/lib/Roundcube/rcube_message.php
@@ -1,1129 +1,1130 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2014, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Logical representation of a mail message with all its data |
| and related functions |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Logical representation of a mail message with all its data
* and related functions
*
* @package Framework
* @subpackage Storage
* @author Thomas Bruederli <roundcube@gmail.com>
*/
class rcube_message
{
/**
* Instance of framework class.
*
* @var rcube
*/
private $app;
/**
* Instance of storage class
*
* @var rcube_storage
*/
private $storage;
/**
* Instance of mime class
*
* @var rcube_mime
*/
private $mime;
private $opt = array();
private $parse_alternative = false;
public $uid;
public $folder;
public $headers;
public $sender;
public $context;
public $parts = array();
public $mime_parts = array();
public $inline_parts = array();
public $attachments = array();
public $subject = '';
public $is_safe = false;
const BODY_MAX_SIZE = 1048576; // 1MB
/**
* __construct
*
* Provide a uid, and parse message structure.
*
* @param string $uid The message UID.
* @param string $folder Folder name
* @param bool $is_safe Security flag
*
* @see self::$app, self::$storage, self::$opt, self::$parts
*/
function __construct($uid, $folder = null, $is_safe = false)
{
// decode combined UID-folder identifier
if (preg_match('/^[0-9.]+-.+/', $uid)) {
list($uid, $folder) = explode('-', $uid, 2);
}
if (preg_match('/^([0-9]+)\.([0-9.]+)$/', $uid, $matches)) {
$uid = $matches[1];
$context = $matches[2];
}
$this->uid = $uid;
$this->context = $context;
$this->app = rcube::get_instance();
$this->storage = $this->app->get_storage();
$this->folder = strlen($folder) ? $folder : $this->storage->get_folder();
// Set current folder
$this->storage->set_folder($this->folder);
$this->storage->set_options(array('all_headers' => true));
$this->headers = $this->storage->get_message($uid);
if (!$this->headers) {
return;
}
$this->set_safe($is_safe || $_SESSION['safe_messages'][$this->folder.':'.$uid]);
$this->opt = array(
'safe' => $this->is_safe,
'prefer_html' => $this->app->config->get('prefer_html'),
'get_url' => $this->app->url(array(
'action' => 'get',
'mbox' => $this->folder,
'uid' => $uid),
false, false, true)
);
if (!empty($this->headers->structure)) {
$this->get_mime_numbers($this->headers->structure);
$this->parse_structure($this->headers->structure);
}
else if ($this->context === null) {
$this->body = $this->storage->get_body($uid);
}
$this->mime = new rcube_mime($this->headers->charset);
$this->subject = $this->headers->get('subject');
$from = $this->mime->decode_address_list($this->headers->from, 1);
$this->sender = current($from);
// notify plugins and let them analyze this structured message object
$this->app->plugins->exec_hook('message_load', array('object' => $this));
}
/**
* Return a (decoded) message header
*
* @param string $name Header name
* @param bool $row Don't mime-decode the value
* @return string Header value
*/
public function get_header($name, $raw = false)
{
if (empty($this->headers)) {
return null;
}
return $this->headers->get($name, !$raw);
}
/**
* Set is_safe var and session data
*
* @param bool $safe enable/disable
*/
public function set_safe($safe = true)
{
$_SESSION['safe_messages'][$this->folder.':'.$this->uid] = $this->is_safe = $safe;
}
/**
* Compose a valid URL for getting a message part
*
* @param string $mime_id Part MIME-ID
* @param mixed $embed Mimetype class for parts to be embedded
* @return string URL or false if part does not exist
*/
public function get_part_url($mime_id, $embed = false)
{
if ($this->mime_parts[$mime_id])
return $this->opt['get_url'] . '&_part=' . $mime_id . ($embed ? '&_embed=1&_mimeclass=' . $embed : '');
else
return false;
}
/**
* Get content of a specific part of this message
*
* @param string $mime_id Part MIME-ID
* @param resource $fp File pointer to save the message part
* @param boolean $skip_charset_conv Disables charset conversion
* @param int $max_bytes Only read this number of bytes
* @param boolean $formatted Enables formatting of text/* parts bodies
*
* @return string Part content
* @deprecated
*/
public function get_part_content($mime_id, $fp = null, $skip_charset_conv = false, $max_bytes = 0, $formatted = true)
{
if ($part = $this->mime_parts[$mime_id]) {
// stored in message structure (winmail/inline-uuencode)
if (!empty($part->body) || $part->encoding == 'stream') {
if ($fp) {
fwrite($fp, $part->body);
}
return $fp ? true : $part->body;
}
// get from IMAP
$this->storage->set_folder($this->folder);
return $this->storage->get_message_part($this->uid, $mime_id, $part,
NULL, $fp, $skip_charset_conv, $max_bytes, $formatted);
}
}
/**
* Get content of a specific part of this message
*
* @param string $mime_id Part ID
* @param boolean $formatted Enables formatting of text/* parts bodies
* @param int $max_bytes Only return/read this number of bytes
* @param mixed $mode NULL to return a string, -1 to print body
* or file pointer to save the body into
*
* @return string|bool Part content or operation status
*/
public function get_part_body($mime_id, $formatted = false, $max_bytes = 0, $mode = null)
{
if (!($part = $this->mime_parts[$mime_id])) {
return;
}
// allow plugins to modify part body
$plugin = $this->app->plugins->exec_hook('message_part_body',
array('object' => $this, 'part' => $part));
// only text parts can be formatted
$formatted = $formatted && $part->ctype_primary == 'text';
// part body not fetched yet... save in memory if it's small enough
if ($part->body === null && is_numeric($mime_id) && $part->size < self::BODY_MAX_SIZE) {
$this->storage->set_folder($this->folder);
// Warning: body here should be always unformatted
$part->body = $this->storage->get_message_part($this->uid, $mime_id, $part,
null, null, true, 0, false);
}
// body stored in message structure (winmail/inline-uuencode)
if ($part->body !== null || $part->encoding == 'stream') {
$body = $part->body;
if ($formatted && $body) {
$body = self::format_part_body($body, $part, $this->headers->charset);
}
if ($max_bytes && strlen($body) > $max_bytes) {
$body = substr($body, 0, $max_bytes);
}
if (is_resource($mode)) {
if ($body !== false) {
fwrite($mode, $body);
@rewind($mode);
}
return $body !== false;
}
if ($mode === -1) {
if ($body !== false) {
print($body);
}
return $body !== false;
}
return $body;
}
// get the body from IMAP
$this->storage->set_folder($this->folder);
$body = $this->storage->get_message_part($this->uid, $mime_id, $part,
$mode === -1, is_resource($mode) ? $mode : null,
!($mode && $formatted), $max_bytes, $mode && $formatted);
if (is_resource($mode)) {
@rewind($mode);
return $body !== false;
}
if (!$mode && $body && $formatted) {
$body = self::format_part_body($body, $part, $this->headers->charset);
}
return $body;
}
/**
* Format text message part for display
*
* @param string $body Part body
* @param rcube_message_part $part Part object
* @param string $default_charset Fallback charset if part charset is not specified
*
* @return string Formatted body
*/
public static function format_part_body($body, $part, $default_charset = null)
{
// remove useless characters
$body = preg_replace('/[\t\r\0\x0B]+\n/', "\n", $body);
// remove NULL characters if any (#1486189)
if (strpos($body, "\x00") !== false) {
$body = str_replace("\x00", '', $body);
}
// detect charset...
if (!$part->charset || strtoupper($part->charset) == 'US-ASCII') {
// try to extract charset information from HTML meta tag (#1488125)
if ($part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m)) {
$part->charset = strtoupper($m[1]);
}
else if ($default_charset) {
$part->charset = $default_charset;
}
else {
$rcube = rcube::get_instance();
$part->charset = $rcube->config->get('default_charset', RCUBE_CHARSET);
}
}
// ..convert charset encoding
$body = rcube_charset::convert($body, $part->charset);
return $body;
}
/**
* Determine if the message contains a HTML part. This must to be
* a real part not an attachment (or its part)
*
* @param bool $enriched Enables checking for text/enriched parts too
* @param rcube_message_part &$part Reference to the part if found
*
* @return bool True if a HTML is available, False if not
*/
public function has_html_part($enriched = false, &$part = null)
{
// check all message parts
foreach ($this->mime_parts as $part) {
if ($part->mimetype == 'text/html' || ($enriched && $part->mimetype == 'text/enriched')) {
// Skip if part is an attachment, don't use is_attachment() here
if ($part->filename) {
continue;
}
if (!$part->size) {
continue;
}
if (!$this->check_context($part)) {
continue;
}
$level = explode('.', $part->mime_id);
$depth = count($level);
$last = '';
// Check if the part belongs to higher-level's multipart part
// this can be alternative/related/signed/encrypted or mixed
while (array_pop($level) !== null) {
$parent_depth = count($level);
if (!$parent_depth) {
return true;
}
$parent = $this->mime_parts[join('.', $level)];
if (!$this->check_context($parent)) {
return true;
}
$max_delta = $depth - (1 + ($last == 'multipart/alternative' ? 1 : 0));
$last = $parent->real_mimetype ?: $parent->mimetype;
if (!preg_match('/^multipart\/(alternative|related|signed|encrypted|mixed)$/', $last)
|| ($last == 'multipart/mixed' && $parent_depth < $max_delta)) {
continue 2;
}
}
return true;
}
}
$part = null;
return false;
}
/**
* Determine if the message contains a text/plain part. This must to be
* a real part not an attachment (or its part)
*
* @param rcube_message_part &$part Reference to the part if found
*
* @return bool True if a plain text part is available, False if not
*/
public function has_text_part(&$part = null)
{
// check all message parts
foreach ($this->mime_parts as $part) {
if ($part->mimetype == 'text/plain') {
// Skip if part is an attachment, don't use is_attachment() here
if ($part->filename) {
continue;
}
if (!$part->size) {
continue;
}
if (!$this->check_context($part)) {
continue;
}
$level = explode('.', $part->mime_id);
// Check if the part belongs to higher-level's alternative/related
while (array_pop($level) !== null) {
if (!count($level)) {
return true;
}
$parent = $this->mime_parts[join('.', $level)];
if (!$this->check_context($parent)) {
return true;
}
if ($parent->mimetype != 'multipart/alternative' && $parent->mimetype != 'multipart/related') {
continue 2;
}
}
return true;
}
}
$part = null;
return false;
}
/**
* Return the first HTML part of this message
*
* @param rcube_message_part &$part Reference to the part if found
* @param bool $enriched Enables checking for text/enriched parts too
*
* @return string HTML message part content
*/
public function first_html_part(&$part = null, $enriched = false)
{
if ($this->has_html_part($enriched, $part)) {
$body = $this->get_part_body($part->mime_id, true);
if ($part->mimetype == 'text/enriched') {
$body = rcube_enriched::to_html($body);
}
return $body;
}
}
/**
* Return the first text part of this message.
* If there's no text/plain part but $strict=true and text/html part
* exists, it will be returned in text/plain format.
*
* @param rcube_message_part &$part Reference to the part if found
* @param bool $strict Check only text/plain parts
*
* @return string Plain text message/part content
*/
public function first_text_part(&$part = null, $strict = false)
{
// no message structure, return complete body
if (empty($this->parts)) {
return $this->body;
}
if ($this->has_text_part($part)) {
return $this->get_part_body($part->mime_id, true);
}
if (!$strict && ($body = $this->first_html_part($part, true))) {
// create instance of html2text class
$h2t = new rcube_html2text($body);
return $h2t->get_text();
}
}
/**
* Return message parts in current context
*/
public function mime_parts()
{
if ($this->context === null) {
return $this->mime_parts;
}
$parts = array();
foreach ($this->mime_parts as $part_id => $part) {
if ($this->check_context($part)) {
$parts[$part_id] = $part;
}
}
return $parts;
}
/**
* Checks if part of the message is an attachment (or part of it)
*
* @param rcube_message_part $part Message part
*
* @return bool True if the part is an attachment part
*/
public function is_attachment($part)
{
foreach ($this->attachments as $att_part) {
if ($att_part->mime_id == $part->mime_id) {
return true;
}
// check if the part is a subpart of another attachment part (message/rfc822)
if ($att_part->mimetype == 'message/rfc822') {
if (in_array($part, (array)$att_part->parts)) {
return true;
}
}
}
return false;
}
/**
* In a multipart/encrypted encrypted message,
* find the encrypted message payload part.
*
* @return rcube_message_part
*/
public function get_multipart_encrypted_part()
{
foreach ($this->mime_parts as $mime_id => $mpart) {
if ($mpart->mimetype == 'multipart/encrypted') {
$this->pgp_mime = true;
}
if ($this->pgp_mime && ($mpart->mimetype == 'application/octet-stream' ||
(!empty($mpart->filename) && $mpart->filename != 'version.txt'))) {
$this->encrypted_part = $mime_id;
return $mpart;
}
}
return false;
}
/**
* Read the message structure returend by the IMAP server
* and build flat lists of content parts and attachments
*
* @param rcube_message_part $structure Message structure node
* @param bool $recursive True when called recursively
*/
private function parse_structure($structure, $recursive = false)
{
// real content-type of message/rfc822 part
if ($structure->mimetype == 'message/rfc822' && $structure->real_mimetype) {
$mimetype = $structure->real_mimetype;
// parse headers from message/rfc822 part
if (!isset($structure->headers['subject']) && !isset($structure->headers['from'])) {
list($headers, ) = explode("\r\n\r\n", $this->get_part_body($structure->mime_id, false, 32768));
$structure->headers = rcube_mime::parse_headers($headers);
if ($this->context == $structure->mime_id) {
$this->headers = rcube_message_header::from_array($structure->headers);
}
}
}
else {
$mimetype = $structure->mimetype;
}
// show message headers
if ($recursive && is_array($structure->headers) &&
(isset($structure->headers['subject']) || $structure->headers['from'] || $structure->headers['to'])
) {
$c = new stdClass;
$c->type = 'headers';
$c->headers = $structure->headers;
$this->add_part($c);
}
// Allow plugins to handle message parts
$plugin = $this->app->plugins->exec_hook('message_part_structure',
array('object' => $this, 'structure' => $structure,
'mimetype' => $mimetype, 'recursive' => $recursive));
if ($plugin['abort']) {
return;
}
$structure = $plugin['structure'];
$mimetype = $plugin['mimetype'];
$recursive = $plugin['recursive'];
list($message_ctype_primary, $message_ctype_secondary) = explode('/', $mimetype);
// print body if message doesn't have multiple parts
if ($message_ctype_primary == 'text' && !$recursive) {
// parts with unsupported type add to attachments list
if (!in_array($message_ctype_secondary, array('plain', 'html', 'enriched'))) {
$this->add_part($structure, 'attachment');
return;
}
$structure->type = 'content';
$this->add_part($structure);
// Parse simple (plain text) message body
if ($message_ctype_secondary == 'plain') {
foreach ((array)$this->uu_decode($structure) as $uupart) {
$this->mime_parts[$uupart->mime_id] = $uupart;
$this->add_part($uupart, 'attachment');
}
}
}
// the same for pgp signed messages
else if ($mimetype == 'application/pgp' && !$recursive) {
$structure->type = 'content';
$this->add_part($structure);
}
// message contains (more than one!) alternative parts
else if ($mimetype == 'multipart/alternative'
&& is_array($structure->parts) && count($structure->parts) > 1
) {
// get html/plaintext parts, other add to attachments list
foreach ($structure->parts as $p => $sub_part) {
$sub_mimetype = $sub_part->mimetype;
$is_multipart = preg_match('/^multipart\/(related|relative|mixed|alternative)/', $sub_mimetype);
// skip empty text parts
if (!$sub_part->size && !$is_multipart) {
continue;
}
// We've encountered (malformed) messages with more than
// one text/plain or text/html part here. There's no way to choose
// which one is better, so we'll display first of them and add
// others as attachments (#1489358)
// check if sub part is
if ($is_multipart)
$related_part = $p;
else if ($sub_mimetype == 'text/plain' && !$plain_part)
$plain_part = $p;
else if ($sub_mimetype == 'text/html' && !$html_part) {
$html_part = $p;
$this->got_html_part = true;
}
else if ($sub_mimetype == 'text/enriched' && !$enriched_part)
$enriched_part = $p;
else {
// add unsupported/unrecognized parts to attachments list
$this->add_part($sub_part, 'attachment');
}
}
// parse related part (alternative part could be in here)
if ($related_part !== null && !$this->parse_alternative) {
$this->parse_alternative = true;
$this->parse_structure($structure->parts[$related_part], true);
$this->parse_alternative = false;
// if plain part was found, we should unset it if html is preferred
if ($this->opt['prefer_html'] && count($this->parts)) {
$plain_part = null;
}
}
// choose html/plain part to print
if ($html_part !== null && $this->opt['prefer_html']) {
$print_part = $structure->parts[$html_part];
}
else if ($enriched_part !== null) {
$print_part = $structure->parts[$enriched_part];
}
else if ($plain_part !== null) {
$print_part = $structure->parts[$plain_part];
}
// add the right message body
if (is_object($print_part)) {
$print_part->type = 'content';
// Allow plugins to handle also this part
$plugin = $this->app->plugins->exec_hook('message_part_structure',
array('object' => $this, 'structure' => $print_part,
'mimetype' => $print_part->mimetype, 'recursive' => true));
if (!$plugin['abort']) {
$this->add_part($print_part);
}
}
// show plaintext warning
else if ($html_part !== null && empty($this->parts)) {
$c = new stdClass;
$c->type = 'content';
$c->ctype_primary = 'text';
$c->ctype_secondary = 'plain';
$c->mimetype = 'text/plain';
$c->realtype = 'text/html';
$this->add_part($c);
}
}
// this is an ecrypted message -> create a plaintext body with the according message
else if ($mimetype == 'multipart/encrypted') {
$p = new stdClass;
$p->type = 'content';
$p->ctype_primary = 'text';
$p->ctype_secondary = 'plain';
$p->mimetype = 'text/plain';
$p->realtype = 'multipart/encrypted';
$p->mime_id = $structure->mime_id;
$this->add_part($p);
// add encrypted payload part as attachment
if (is_array($structure->parts)) {
for ($i=0; $i < count($structure->parts); $i++) {
$subpart = $structure->parts[$i];
if ($subpart->mimetype == 'application/octet-stream' || !empty($subpart->filename)) {
$this->add_part($subpart, 'attachment');
}
}
}
}
// this is an S/MIME ecrypted message -> create a plaintext body with the according message
else if ($mimetype == 'application/pkcs7-mime') {
$p = new stdClass;
$p->type = 'content';
$p->ctype_primary = 'text';
$p->ctype_secondary = 'plain';
$p->mimetype = 'text/plain';
$p->realtype = 'application/pkcs7-mime';
$p->mime_id = $structure->mime_id;
$this->add_part($p);
if (!empty($structure->filename)) {
$this->add_part($structure, 'attachment');
}
}
// message contains multiple parts
else if (is_array($structure->parts) && !empty($structure->parts)) {
// iterate over parts
for ($i=0; $i < count($structure->parts); $i++) {
$mail_part = &$structure->parts[$i];
$primary_type = $mail_part->ctype_primary;
$secondary_type = $mail_part->ctype_secondary;
$part_mimetype = $mail_part->mimetype;
// multipart/alternative or message/rfc822
if ($primary_type == 'multipart' || $part_mimetype == 'message/rfc822') {
// list message/rfc822 as attachment as well
if ($part_mimetype == 'message/rfc822') {
$this->add_part($mail_part, 'attachment');
}
$this->parse_structure($mail_part, true);
}
// part text/[plain|html] or delivery status
else if ((($part_mimetype == 'text/plain' || $part_mimetype == 'text/html') && $mail_part->disposition != 'attachment') ||
in_array($part_mimetype, array('message/delivery-status', 'text/rfc822-headers', 'message/disposition-notification'))
) {
// Allow plugins to handle also this part
$plugin = $this->app->plugins->exec_hook('message_part_structure',
array('object' => $this, 'structure' => $mail_part,
'mimetype' => $part_mimetype, 'recursive' => true));
if ($plugin['abort']) {
continue;
}
if ($part_mimetype == 'text/html' && $mail_part->size) {
$this->got_html_part = true;
}
$mail_part = $plugin['structure'];
list($primary_type, $secondary_type) = explode('/', $plugin['mimetype']);
// add text part if it matches the prefs
if (!$this->parse_alternative ||
($secondary_type == 'html' && $this->opt['prefer_html']) ||
($secondary_type == 'plain' && !$this->opt['prefer_html'])
) {
$mail_part->type = 'content';
$this->add_part($mail_part);
}
// list as attachment as well
if (!empty($mail_part->filename)) {
$this->add_part($mail_part, 'attachment');
}
}
// ignore "virtual" protocol parts
else if ($primary_type == 'protocol') {
continue;
}
// part is Microsoft Outlook TNEF (winmail.dat)
else if ($part_mimetype == 'application/ms-tnef') {
$tnef_parts = (array) $this->tnef_decode($mail_part);
foreach ($tnef_parts as $tpart) {
$this->mime_parts[$tpart->mime_id] = $tpart;
$this->add_part($tpart, 'attachment');
}
// add winmail.dat to the list if it's content is unknown
if (empty($tnef_parts) && !empty($mail_part->filename)) {
$this->mime_parts[$mail_part->mime_id] = $mail_part;
$this->add_part($mail_part, 'attachment');
}
}
// part is a file/attachment
else if (preg_match('/^(inline|attach)/', $mail_part->disposition) ||
$mail_part->headers['content-id'] ||
($mail_part->filename &&
(empty($mail_part->disposition) || preg_match('/^[a-z0-9!#$&.+^_-]+$/i', $mail_part->disposition)))
) {
// skip apple resource forks
if ($message_ctype_secondary == 'appledouble' && $secondary_type == 'applefile') {
continue;
}
if ($mail_part->headers['content-id']) {
$mail_part->content_id = preg_replace(array('/^</', '/>$/'), '', $mail_part->headers['content-id']);
}
if ($mail_part->headers['content-location']) {
$mail_part->content_location = $mail_part->headers['content-base'] . $mail_part->headers['content-location'];
}
// part belongs to a related message and is linked
// Note: mixed is not supposed to contain inline images, but we've found such examples (#5905)
if (preg_match('/^multipart\/(related|relative|mixed)/', $mimetype)
&& ($mail_part->content_id || $mail_part->content_location)
) {
$this->add_part($mail_part, 'inline');
}
// Any non-inline attachment
if (!preg_match('/^inline/i', $mail_part->disposition) || empty($mail_part->headers['content-id'])) {
// Content-Type name regexp according to RFC4288.4.2
if (!preg_match('/^[a-z0-9!#$&.+^_-]+\/[a-z0-9!#$&.+^_-]+$/i', $part_mimetype)) {
// replace malformed content type with application/octet-stream (#1487767)
$mail_part->ctype_primary = 'application';
$mail_part->ctype_secondary = 'octet-stream';
$mail_part->mimetype = 'application/octet-stream';
}
$this->add_part($mail_part, 'attachment');
}
}
// calendar part not marked as attachment (#1490325)
else if ($part_mimetype == 'text/calendar') {
if (!$mail_part->filename) {
$mail_part->filename = 'calendar.ics';
}
$this->add_part($mail_part, 'attachment');
}
}
// if this is a related part try to resolve references
// Note: mixed is not supposed to contain inline images, but we've found such examples (#5905)
if (preg_match('/^multipart\/(related|relative|mixed)/', $mimetype) && count($this->inline_parts)) {
$a_replaces = array();
$img_regexp = '/^image\/(gif|jpe?g|png|tiff|bmp|svg)/';
foreach ($this->inline_parts as $inline_object) {
$part_url = $this->get_part_url($inline_object->mime_id, $inline_object->ctype_primary);
if (isset($inline_object->content_id)) {
$a_replaces['cid:'.$inline_object->content_id] = $part_url;
}
if ($inline_object->content_location) {
$a_replaces[$inline_object->content_location] = $part_url;
}
if (!empty($inline_object->filename)) {
// MS Outlook sends sometimes non-related attachments as related
// In this case multipart/related message has only one text part
// We'll add all such attachments to the attachments list
if (!isset($this->got_html_part)) {
$this->add_part($inline_object, 'attachment');
}
// MS Outlook sometimes also adds non-image attachments as related
// We'll add all such attachments to the attachments list
// Warning: some browsers support pdf in <img/>
else if (!preg_match($img_regexp, $inline_object->mimetype)) {
$this->add_part($inline_object, 'attachment');
}
// @TODO: we should fetch HTML body and find attachment's content-id
// to handle also image attachments without reference in the body
// @TODO: should we list all image attachments in text mode?
}
}
// add replace array to each content part
// (will be applied later when part body is available)
foreach ($this->parts as $i => $part) {
if ($part->type == 'content') {
$this->parts[$i]->replaces = $a_replaces;
}
}
}
}
// message is a single part non-text
else if ($structure->filename || preg_match('/^application\//i', $mimetype)) {
$this->add_part($structure, 'attachment');
}
}
/**
* Fill a flat array with references to all parts, indexed by part numbers
*
* @param rcube_message_part $part Message body structure
*/
private function get_mime_numbers(&$part)
{
if (strlen($part->mime_id))
$this->mime_parts[$part->mime_id] = &$part;
if (is_array($part->parts))
for ($i=0; $i<count($part->parts); $i++)
$this->get_mime_numbers($part->parts[$i]);
}
/**
* Add a part to object parts array(s) (with context check)
*/
private function add_part($part, $type = null)
{
if ($this->check_context($part)) {
// It may happen that we add the same part to the array many times
// use part ID index to prevent from duplicates
switch ($type) {
case 'inline': $this->inline_parts[(string) $part->mime_id] = $part; break;
case 'attachment': $this->attachments[(string) $part->mime_id] = $part; break;
default: $this->parts[] = $part; break;
}
}
}
/**
* Check if specified part belongs to the current context
*/
private function check_context($part)
{
return $this->context === null || strpos($part->mime_id, $this->context . '.') === 0;
}
/**
* Decode a Microsoft Outlook TNEF part (winmail.dat)
*
* @param rcube_message_part $part Message part to decode
* @return array
*/
function tnef_decode(&$part)
{
// @TODO: attachment may be huge, handle body via file
$body = $this->get_part_body($part->mime_id);
$tnef = new rcube_tnef_decoder;
$tnef_arr = $tnef->decompress($body);
$parts = array();
unset($body);
foreach ($tnef_arr['attachments'] as $pid => $winatt) {
$tpart = new rcube_message_part;
$tpart->filename = $this->fix_attachment_name(trim($winatt['name']), $part);
$tpart->encoding = 'stream';
$tpart->ctype_primary = trim(strtolower($winatt['type']));
$tpart->ctype_secondary = trim(strtolower($winatt['subtype']));
$tpart->mimetype = $tpart->ctype_primary . '/' . $tpart->ctype_secondary;
$tpart->mime_id = 'winmail.' . $part->mime_id . '.' . $pid;
$tpart->size = $winatt['size'];
$tpart->body = $winatt['stream'];
$parts[] = $tpart;
unset($tnef_arr[$pid]);
}
return $parts;
}
/**
* Parse message body for UUencoded attachments bodies
*
* @param rcube_message_part $part Message part to decode
* @return array
*/
function uu_decode(&$part)
{
// @TODO: messages may be huge, handle body via file
$part->body = $this->get_part_body($part->mime_id);
$parts = array();
$pid = 0;
// FIXME: line length is max.65?
$uu_regexp_begin = '/begin [0-7]{3,4} ([^\r\n]+)\r?\n/s';
$uu_regexp_end = '/`\r?\nend((\r?\n)|($))/s';
while (preg_match($uu_regexp_begin, $part->body, $matches, PREG_OFFSET_CAPTURE)) {
$startpos = $matches[0][1];
if (!preg_match($uu_regexp_end, $part->body, $m, PREG_OFFSET_CAPTURE, $startpos)) {
break;
}
$endpos = $m[0][1];
$begin_len = strlen($matches[0][0]);
$end_len = strlen($m[0][0]);
// extract attachment body
$filebody = substr($part->body, $startpos + $begin_len, $endpos - $startpos - $begin_len - 1);
$filebody = str_replace("\r\n", "\n", $filebody);
// remove attachment body from the message body
$part->body = substr_replace($part->body, '', $startpos, $endpos + $end_len - $startpos);
// mark body as modified so it will not be cached by rcube_imap_cache
$part->body_modified = true;
// add attachments to the structure
$uupart = new rcube_message_part;
$uupart->filename = trim($matches[1][0]);
$uupart->encoding = 'stream';
$uupart->body = convert_uudecode($filebody);
$uupart->size = strlen($uupart->body);
$uupart->mime_id = 'uu.' . $part->mime_id . '.' . $pid;
$ctype = rcube_mime::file_content_type($uupart->body, $uupart->filename, 'application/octet-stream', true);
$uupart->mimetype = $ctype;
list($uupart->ctype_primary, $uupart->ctype_secondary) = explode('/', $ctype);
$parts[] = $uupart;
$pid++;
}
return $parts;
}
/**
* Fix attachment name encoding if needed/possible
*/
protected function fix_attachment_name($name, $part)
{
if ($name == rcube_charset::clean($name)) {
return $name;
}
// find charset from part or its parent(s)
if ($part->charset) {
$charsets[] = $part->charset;
}
else {
// check first part (common case)
$n = strpos($part->mime_id, '.') ? preg_replace('/\.[0-9]+$/', '', $part->mime_id) . '.1' : 1;
if (($_part = $this->mime_parts[$n]) && $_part->charset) {
$charsets[] = $_part->charset;
}
// check parents' charset
$items = explode('.', $part->mime_id);
for ($i = count($items)-1; $i > 0; $i--) {
$last = array_pop($items);
$parent = $this->mime_parts[join('.', $items)];
if ($parent && $parent->charset) {
$charsets[] = $parent->charset;
}
}
}
if ($this->headers->charset) {
$charsets[] = $this->headers->charset;
}
if (empty($charsets)) {
$rcube = rcube::get_instance();
$charsets[] = rcube_charset::detect($name, $rcube->config->get('default_charset', RCUBE_CHARSET));
}
foreach (array_unique($charsets) as $charset) {
$_name = rcube_charset::convert($name, $charset);
if ($_name == rcube_charset::clean($_name)) {
if (!$part->charset) {
$part->charset = $charset;
}
return $_name;
}
}
return $name;
}
/**
* Deprecated methods (to be removed)
*/
public static function unfold_flowed($text)
{
return rcube_mime::unfold_flowed($text);
}
public static function format_flowed($text, $length = 72)
{
return rcube_mime::format_flowed($text, $length);
}
}
diff --git a/program/lib/Roundcube/rcube_message_header.php b/program/lib/Roundcube/rcube_message_header.php
index b0b958eb6..9dc2d9c58 100644
--- a/program/lib/Roundcube/rcube_message_header.php
+++ b/program/lib/Roundcube/rcube_message_header.php
@@ -1,325 +1,326 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2012, The Roundcube Dev Team |
- | Copyright (C) 2011-2012, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| E-mail message headers representation |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Struct representing an e-mail message header
*
* @package Framework
* @subpackage Storage
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_message_header
{
/**
* Message sequence number
*
* @var int
*/
public $id;
/**
* Message unique identifier
*
* @var int
*/
public $uid;
/**
* Message subject
*
* @var string
*/
public $subject;
/**
* Message sender (From)
*
* @var string
*/
public $from;
/**
* Message recipient (To)
*
* @var string
*/
public $to;
/**
* Message additional recipients (Cc)
*
* @var string
*/
public $cc;
/**
* Message Reply-To header
*
* @var string
*/
public $replyto;
/**
* Message In-Reply-To header
*
* @var string
*/
public $in_reply_to;
/**
* Message date (Date)
*
* @var string
*/
public $date;
/**
* Message identifier (Message-ID)
*
* @var string
*/
public $messageID;
/**
* Message size
*
* @var int
*/
public $size;
/**
* Message encoding
*
* @var string
*/
public $encoding;
/**
* Message charset
*
* @var string
*/
public $charset;
/**
* Message Content-type
*
* @var string
*/
public $ctype;
/**
* Message timestamp (based on message date)
*
* @var int
*/
public $timestamp;
/**
* IMAP bodystructure string
*
* @var string
*/
public $bodystructure;
/**
* IMAP internal date
*
* @var string
*/
public $internaldate;
/**
* Message References header
*
* @var string
*/
public $references;
/**
* Message priority (X-Priority)
*
* @var int
*/
public $priority;
/**
* Message receipt recipient
*
* @var string
*/
public $mdn_to;
/**
* IMAP folder this message is stored in
*
* @var string
*/
public $folder;
/**
* Other message headers
*
* @var array
*/
public $others = array();
/**
* Message flags
*
* @var array
*/
public $flags = array();
// map header to rcube_message_header object property
private $obj_headers = array(
'date' => 'date',
'from' => 'from',
'to' => 'to',
'subject' => 'subject',
'reply-to' => 'replyto',
'cc' => 'cc',
'bcc' => 'bcc',
'mbox' => 'folder',
'folder' => 'folder',
'content-transfer-encoding' => 'encoding',
'in-reply-to' => 'in_reply_to',
'content-type' => 'ctype',
'charset' => 'charset',
'references' => 'references',
'return-receipt-to' => 'mdn_to',
'disposition-notification-to' => 'mdn_to',
'x-confirm-reading-to' => 'mdn_to',
'message-id' => 'messageID',
'x-priority' => 'priority',
);
/**
* Returns header value
*/
public function get($name, $decode = true)
{
$name = strtolower($name);
if (isset($this->obj_headers[$name])) {
$value = $this->{$this->obj_headers[$name]};
}
else {
$value = $this->others[$name];
}
if ($decode) {
if (is_array($value)) {
foreach ($value as $key => $val) {
$val = rcube_mime::decode_header($val, $this->charset);
$value[$key] = rcube_charset::clean($val);
}
}
else {
$value = rcube_mime::decode_header($value, $this->charset);
$value = rcube_charset::clean($value);
}
}
return $value;
}
/**
* Sets header value
*/
public function set($name, $value)
{
$name = strtolower($name);
if (isset($this->obj_headers[$name])) {
$this->{$this->obj_headers[$name]} = $value;
}
else {
$this->others[$name] = $value;
}
}
/**
* Factory method to instantiate headers from a data array
*
* @param array Hash array with header values
* @return object rcube_message_header instance filled with headers values
*/
public static function from_array($arr)
{
$obj = new rcube_message_header;
foreach ($arr as $k => $v)
$obj->set($k, $v);
return $obj;
}
}
/**
* Class for sorting an array of rcube_message_header objects in a predetermined order.
*
* @package Framework
* @subpackage Storage
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_message_header_sorter
{
private $uids = array();
/**
* Set the predetermined sort order.
*
* @param array $index Numerically indexed array of IMAP UIDs
*/
function set_index($index)
{
$index = array_flip($index);
$this->uids = $index;
}
/**
* Sort the array of header objects
*
* @param array $headers Array of rcube_message_header objects indexed by UID
*/
function sort_headers(&$headers)
{
uksort($headers, array($this, "compare_uids"));
}
/**
* Sort method called by uksort()
*
* @param int $a Array key (UID)
* @param int $b Array key (UID)
*/
function compare_uids($a, $b)
{
// then find each sequence number in my ordered list
$posa = isset($this->uids[$a]) ? intval($this->uids[$a]) : -1;
$posb = isset($this->uids[$b]) ? intval($this->uids[$b]) : -1;
// return the relative position as the comparison value
return $posa - $posb;
}
}
diff --git a/program/lib/Roundcube/rcube_message_part.php b/program/lib/Roundcube/rcube_message_part.php
index 82f3e2411..bf8504a9a 100644
--- a/program/lib/Roundcube/rcube_message_part.php
+++ b/program/lib/Roundcube/rcube_message_part.php
@@ -1,94 +1,95 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2012, The Roundcube Dev Team |
- | Copyright (C) 2011-2012, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Class representing a message part |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class representing a message part
*
* @package Framework
* @subpackage Storage
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_message_part
{
/**
* Part MIME identifier
*
* @var string
*/
public $mime_id = '';
/**
* Content main type
*
* @var string
*/
public $ctype_primary = 'text';
/**
* Content subtype
*
* @var string
*/
public $ctype_secondary = 'plain';
/**
* Complete content type
*
* @var string
*/
public $mimetype = 'text/plain';
/**
* Part size in bytes
*
* @var int
*/
public $size = 0;
/**
* Part headers
*
* @var array
*/
public $headers = array();
public $disposition = '';
public $filename = '';
public $encoding = '8bit';
public $charset = '';
public $d_parameters = array();
public $ctype_parameters = array();
/**
* Clone handler.
*/
function __clone()
{
if (isset($this->parts)) {
foreach ($this->parts as $idx => $part) {
if (is_object($part)) {
$this->parts[$idx] = clone $part;
}
}
}
}
}
diff --git a/program/lib/Roundcube/rcube_mime.php b/program/lib/Roundcube/rcube_mime.php
index e020c5faf..f1db70acd 100644
--- a/program/lib/Roundcube/rcube_mime.php
+++ b/program/lib/Roundcube/rcube_mime.php
@@ -1,902 +1,903 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2016, The Roundcube Dev Team |
- | Copyright (C) 2011-2016, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| MIME message parsing utilities |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class for parsing MIME messages
*
* @package Framework
* @subpackage Storage
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_mime
{
private static $default_charset;
/**
* Object constructor.
*/
function __construct($default_charset = null)
{
self::$default_charset = $default_charset;
}
/**
* Returns message/object character set name
*
* @return string Character set name
*/
public static function get_charset()
{
if (self::$default_charset) {
return self::$default_charset;
}
if ($charset = rcube::get_instance()->config->get('default_charset')) {
return $charset;
}
return RCUBE_CHARSET;
}
/**
* Parse the given raw message source and return a structure
* of rcube_message_part objects.
*
* It makes use of the rcube_mime_decode library
*
* @param string $raw_body The message source
*
* @return object rcube_message_part The message structure
*/
public static function parse_message($raw_body)
{
$conf = array(
'include_bodies' => true,
'decode_bodies' => true,
'decode_headers' => false,
'default_charset' => self::get_charset(),
);
$mime = new rcube_mime_decode($conf);
return $mime->decode($raw_body);
}
/**
* Split an address list into a structured array list
*
* @param string|array $input Input string (or list of strings)
* @param int $max List only this number of addresses
* @param boolean $decode Decode address strings
* @param string $fallback Fallback charset if none specified
* @param boolean $addronly Return flat array with e-mail addresses only
*
* @return array Indexed list of addresses
*/
static function decode_address_list($input, $max = null, $decode = true, $fallback = null, $addronly = false)
{
// A common case when the same header is used many times in a mail message
if (is_array($input)) {
$input = implode(', ', $input);
}
$a = self::parse_address_list($input, $decode, $fallback);
$out = array();
$j = 0;
// Special chars as defined by RFC 822 need to in quoted string (or escaped).
$special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
if (!is_array($a)) {
return $out;
}
foreach ($a as $val) {
$j++;
$address = trim($val['address']);
if ($addronly) {
$out[$j] = $address;
}
else {
$name = 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 message header value
*
* @param string $input Header value
* @param string $fallback Fallback charset if none specified
*
* @return string Decoded string
*/
public static function decode_header($input, $fallback = null)
{
$str = self::decode_mime_string((string)$input, $fallback);
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
*/
public static function decode_mime_string($input, $fallback = null)
{
$default_charset = $fallback ?: self::get_charset();
// 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);
// encoded-word regexp
$re = '/=\?([^?]+)\?([BbQq])\?([^\n]*?)\?=/';
// Find all RFC2047's encoded words
if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
// Initialize variables
$tmp = array();
$out = '';
$start = 0;
foreach ($matches as $idx => $m) {
$pos = $m[0][1];
$charset = $m[1][0];
$encoding = $m[2][0];
$text = $m[3][0];
$length = strlen($m[0][0]);
// Append everything that is before the text to be decoded
if ($start != $pos) {
$substr = substr($input, $start, $pos-$start);
$out .= rcube_charset::convert($substr, $default_charset);
$start = $pos;
}
$start += $length;
// Per RFC2047, each string part "MUST represent an integral number
// of characters . A multi-octet character may not be split across
// adjacent encoded-words." However, some mailers break this, so we
// try to handle characters spanned across parts anyway by iterating
// through and aggregating sequential encoded parts with the same
// character set and encoding, then perform the decoding on the
// aggregation as a whole.
$tmp[] = $text;
if ($next_match = $matches[$idx+1]) {
if ($next_match[0][1] == $start
&& $next_match[1][0] == $charset
&& $next_match[2][0] == $encoding
) {
continue;
}
}
$count = count($tmp);
$text = '';
// Decode and join encoded-word's chunks
if ($encoding == 'B' || $encoding == 'b') {
$rest = '';
// base64 must be decoded a segment at a time.
// However, there are broken implementations that continue
// in the following word, we'll handle that (#6048)
for ($i=0; $i<$count; $i++) {
$chunk = $rest . $tmp[$i];
$length = strlen($chunk);
if ($length % 4) {
$length = floor($length / 4) * 4;
$rest = substr($chunk, $length);
$chunk = substr($chunk, 0, $length);
}
$text .= base64_decode($chunk);
}
}
else { //if ($encoding == 'Q' || $encoding == 'q') {
// quoted printable can be combined and processed at once
for ($i=0; $i<$count; $i++)
$text .= $tmp[$i];
$text = str_replace('_', ' ', $text);
$text = quoted_printable_decode($text);
}
$out .= rcube_charset::convert($text, $charset);
$tmp = array();
}
// add the last part of the input string
if ($start != strlen($input)) {
$out .= rcube_charset::convert(substr($input, $start), $default_charset);
}
// return the results
return $out;
}
// no encoding information, use fallback
return rcube_charset::convert($input, $default_charset);
}
/**
* Decode a mime part
*
* @param string $input Input string
* @param string $encoding Part encoding
*
* @return string Decoded string
*/
public static function decode($input, $encoding = '7bit')
{
switch (strtolower($encoding)) {
case 'quoted-printable':
return quoted_printable_decode($input);
case 'base64':
return base64_decode($input);
case 'x-uuencode':
case 'x-uue':
case 'uue':
case 'uuencode':
return convert_uudecode($input);
case '7bit':
default:
return $input;
}
}
/**
* Split RFC822 header string into an associative array
*/
public static function parse_headers($headers)
{
$a_headers = array();
$headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
$lines = explode("\n", $headers);
$count = count($lines);
for ($i=0; $i<$count; $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;
}
/**
* E-mail address list parser
*/
private static function parse_address_list($str, $decode = true, $fallback = null)
{
// remove any newlines and carriage returns before
$str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
// extract list items, remove comments
$str = self::explode_header_string(',;', $str, true);
$result = array();
// simplified regexp, supporting quoted local part
$email_rx = '(\S+|("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+"))@\S+';
foreach ($str as $key => $val) {
$name = '';
$address = '';
$val = trim($val);
if (preg_match('/(.*)<('.$email_rx.')>$/', $val, $m)) {
$address = $m[2];
$name = trim($m[1]);
}
else if (preg_match('/^('.$email_rx.')$/', $val, $m)) {
$address = $m[1];
$name = '';
}
// special case (#1489092)
else if (preg_match('/(\s*<MAILER-DAEMON>)$/', $val, $m)) {
$address = 'MAILER-DAEMON';
$name = substr($val, 0, -strlen($m[1]));
}
else if (preg_match('/('.$email_rx.')/', $val, $m)) {
$name = $m[1];
}
else {
$name = $val;
}
// dequote and/or decode name
if ($name) {
if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
$name = substr($name, 1, -1);
$name = stripslashes($name);
}
if ($decode) {
$name = self::decode_header($name, $fallback);
// some clients encode addressee name with quotes around it
if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
$name = substr($name, 1, -1);
}
}
}
if (!$address && $name) {
$address = $name;
$name = '';
}
if ($address) {
$address = self::fix_email($address);
$result[$key] = array('name' => $name, 'address' => $address);
}
}
return $result;
}
/**
* Explodes header (e.g. address-list) string into array of strings
* using specified separator characters with proper handling
* of quoted-strings and comments (RFC2822)
*
* @param string $separator String containing separator characters
* @param string $str Header string
* @param bool $remove_comments Enable to remove comments
*
* @return array Header items
*/
public static function explode_header_string($separator, $str, $remove_comments = false)
{
$length = strlen($str);
$result = array();
$quoted = false;
$comment = 0;
$out = '';
for ($i=0; $i<$length; $i++) {
// we're inside a quoted string
if ($quoted) {
if ($str[$i] == '"') {
$quoted = false;
}
else if ($str[$i] == "\\") {
if ($comment <= 0) {
$out .= "\\";
}
$i++;
}
}
// we are inside a comment string
else if ($comment > 0) {
if ($str[$i] == ')') {
$comment--;
}
else if ($str[$i] == '(') {
$comment++;
}
else if ($str[$i] == "\\") {
$i++;
}
continue;
}
// separator, add to result array
else if (strpos($separator, $str[$i]) !== false) {
if ($out) {
$result[] = $out;
}
$out = '';
continue;
}
// start of quoted string
else if ($str[$i] == '"') {
$quoted = true;
}
// start of comment
else if ($remove_comments && $str[$i] == '(') {
$comment++;
}
if ($comment <= 0) {
$out .= $str[$i];
}
}
if ($out && $comment <= 0) {
$result[] = $out;
}
return $result;
}
/**
* Interpret a format=flowed message body according to RFC 2646
*
* @param string $text Raw body formatted as flowed text
* @param string $mark Mark each flowed line with specified character
* @param boolean $delsp Remove the trailing space of each flowed line
*
* @return string Interpreted text with unwrapped lines and stuffed space removed
*/
public static function unfold_flowed($text, $mark = null, $delsp = false)
{
$text = preg_split('/\r?\n/', $text);
$last = -1;
$q_level = 0;
$marks = array();
foreach ($text as $idx => $line) {
if ($q = strspn($line, '>')) {
// remove quote chars
$line = substr($line, $q);
// remove (optional) space-staffing
if ($line[0] === ' ') $line = substr($line, 1);
// The same paragraph (We join current line with the previous one) when:
// - the same level of quoting
// - previous line was flowed
// - previous line contains more than only one single space (and quote char(s))
if ($q == $q_level
&& isset($text[$last]) && $text[$last][strlen($text[$last])-1] == ' '
&& !preg_match('/^>+ {0,1}$/', $text[$last])
) {
if ($delsp) {
$text[$last] = substr($text[$last], 0, -1);
}
$text[$last] .= $line;
unset($text[$idx]);
if ($mark) {
$marks[$last] = true;
}
}
else {
$last = $idx;
}
}
else {
if ($line == '-- ') {
$last = $idx;
}
else {
// remove space-stuffing
if ($line[0] === ' ') $line = substr($line, 1);
if (isset($text[$last]) && $line && !$q_level
&& $text[$last] != '-- '
&& $text[$last][strlen($text[$last])-1] == ' '
) {
if ($delsp) {
$text[$last] = substr($text[$last], 0, -1);
}
$text[$last] .= $line;
unset($text[$idx]);
if ($mark) {
$marks[$last] = true;
}
}
else {
$text[$idx] = $line;
$last = $idx;
}
}
}
$q_level = $q;
}
if (!empty($marks)) {
foreach (array_keys($marks) as $mk) {
$text[$mk] = $mark . $text[$mk];
}
}
return implode("\r\n", $text);
}
/**
* Wrap the given text to comply with RFC 2646
*
* @param string $text Text to wrap
* @param int $length Length
* @param string $charset Character encoding of $text
*
* @return string Wrapped text
*/
public static function format_flowed($text, $length = 72, $charset=null)
{
$text = preg_split('/\r?\n/', $text);
foreach ($text as $idx => $line) {
if ($line != '-- ') {
if ($level = strspn($line, '>')) {
// remove quote chars
$line = substr($line, $level);
// remove (optional) space-staffing and spaces before the line end
$line = rtrim($line, ' ');
if ($line[0] === ' ') $line = substr($line, 1);
$prefix = str_repeat('>', $level) . ' ';
$line = $prefix . self::wordwrap($line, $length - $level - 2, " \r\n$prefix", false, $charset);
}
else if ($line) {
$line = self::wordwrap(rtrim($line), $length - 2, " \r\n", false, $charset);
// space-stuffing
$line = preg_replace('/(^|\r\n)(From| |>)/', '\\1 \\2', $line);
}
$text[$idx] = $line;
}
}
return implode("\r\n", $text);
}
/**
* Improved wordwrap function with multibyte support.
* The code is based on Zend_Text_MultiByte::wordWrap().
*
* @param string $string Text to wrap
* @param int $width Line width
* @param string $break Line separator
* @param bool $cut Enable to cut word
* @param string $charset Charset of $string
* @param bool $wrap_quoted When enabled quoted lines will not be wrapped
*
* @return string Text
*/
public static function wordwrap($string, $width=75, $break="\n", $cut=false, $charset=null, $wrap_quoted=true)
{
// Note: Never try to use iconv instead of mbstring functions here
// Iconv's substr/strlen are 100x slower (#1489113)
if ($charset && $charset != RCUBE_CHARSET) {
mb_internal_encoding($charset);
}
// Convert \r\n to \n, this is our line-separator
$string = str_replace("\r\n", "\n", $string);
$separator = "\n"; // must be 1 character length
$result = array();
while (($stringLength = mb_strlen($string)) > 0) {
$breakPos = mb_strpos($string, $separator, 0);
// quoted line (do not wrap)
if ($wrap_quoted && $string[0] == '>') {
if ($breakPos === $stringLength - 1 || $breakPos === false) {
$subString = $string;
$cutLength = null;
}
else {
$subString = mb_substr($string, 0, $breakPos);
$cutLength = $breakPos + 1;
}
}
// next line found and current line is shorter than the limit
else if ($breakPos !== false && $breakPos < $width) {
if ($breakPos === $stringLength - 1) {
$subString = $string;
$cutLength = null;
}
else {
$subString = mb_substr($string, 0, $breakPos);
$cutLength = $breakPos + 1;
}
}
else {
$subString = mb_substr($string, 0, $width);
// last line
if ($breakPos === false && $subString === $string) {
$cutLength = null;
}
else {
$nextChar = mb_substr($string, $width, 1);
if ($nextChar === ' ' || $nextChar === $separator) {
$afterNextChar = mb_substr($string, $width + 1, 1);
// Note: mb_substr() does never return False
if ($afterNextChar === false || $afterNextChar === '') {
$subString .= $nextChar;
}
$cutLength = mb_strlen($subString) + 1;
}
else {
$spacePos = mb_strrpos($subString, ' ', 0);
if ($spacePos !== false) {
$subString = mb_substr($subString, 0, $spacePos);
$cutLength = $spacePos + 1;
}
else if ($cut === false) {
$spacePos = mb_strpos($string, ' ', 0);
if ($spacePos !== false && ($breakPos === false || $spacePos < $breakPos)) {
$subString = mb_substr($string, 0, $spacePos);
$cutLength = $spacePos + 1;
}
else if ($breakPos === false) {
$subString = $string;
$cutLength = null;
}
else {
$subString = mb_substr($string, 0, $breakPos);
$cutLength = $breakPos + 1;
}
}
else {
$cutLength = $width;
}
}
}
}
$result[] = $subString;
if ($cutLength !== null) {
$string = mb_substr($string, $cutLength, ($stringLength - $cutLength));
}
else {
break;
}
}
if ($charset && $charset != RCUBE_CHARSET) {
mb_internal_encoding(RCUBE_CHARSET);
}
return implode($break, $result);
}
/**
* A method to guess the mime_type of an attachment.
*
* @param string $path Path to the file or file contents
* @param string $name File name (with suffix)
* @param string $failover Mime type supplied for failover
* @param boolean $is_stream Set to True if $path contains file contents
* @param boolean $skip_suffix Set to True if the config/mimetypes.php mappig should be ignored
*
* @return string
* @author Till Klampaeckel <till@php.net>
* @see http://de2.php.net/manual/en/ref.fileinfo.php
* @see http://de2.php.net/mime_content_type
*/
public static function file_content_type($path, $name, $failover = 'application/octet-stream', $is_stream = false, $skip_suffix = false)
{
static $mime_ext = array();
$mime_type = null;
$config = rcube::get_instance()->config;
$mime_magic = $config->get('mime_magic');
if (!$skip_suffix && empty($mime_ext)) {
foreach ($config->resolve_paths('mimetypes.php') as $fpath) {
$mime_ext = array_merge($mime_ext, (array) @include($fpath));
}
}
// use file name suffix with hard-coded mime-type map
if (!$skip_suffix && is_array($mime_ext) && $name) {
if ($suffix = substr($name, strrpos($name, '.')+1)) {
$mime_type = $mime_ext[strtolower($suffix)];
}
}
// try fileinfo extension if available
if (!$mime_type && function_exists('finfo_open')) {
// null as a 2nd argument should be the same as no argument
// this however is not true on all systems/versions
if ($mime_magic) {
$finfo = finfo_open(FILEINFO_MIME, $mime_magic);
}
else {
$finfo = finfo_open(FILEINFO_MIME);
}
if ($finfo) {
if ($is_stream)
$mime_type = finfo_buffer($finfo, $path);
else
$mime_type = finfo_file($finfo, $path);
finfo_close($finfo);
}
}
// try PHP's mime_content_type
if (!$mime_type && !$is_stream && function_exists('mime_content_type')) {
$mime_type = @mime_content_type($path);
}
// fall back to user-submitted string
if (!$mime_type) {
$mime_type = $failover;
}
else {
// Sometimes (PHP-5.3?) content-type contains charset definition,
// Remove it (#1487122) also "charset=binary" is useless
$mime_type = array_shift(preg_split('/[; ]/', $mime_type));
}
return $mime_type;
}
/**
* Get mimetype => file extension mapping
*
* @param string Mime-Type to get extensions for
*
* @return array List of extensions matching the given mimetype or a hash array
* with ext -> mimetype mappings if $mimetype is not given
*/
public static function get_mime_extensions($mimetype = null)
{
static $mime_types, $mime_extensions;
// return cached data
if (is_array($mime_types)) {
return $mimetype ? $mime_types[$mimetype] : $mime_extensions;
}
// load mapping file
$file_paths = array();
if ($mime_types = rcube::get_instance()->config->get('mime_types')) {
$file_paths[] = $mime_types;
}
// try common locations
if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
$file_paths[] = 'C:/xampp/apache/conf/mime.types.';
}
else {
$file_paths[] = '/etc/mime.types';
$file_paths[] = '/etc/httpd/mime.types';
$file_paths[] = '/etc/httpd2/mime.types';
$file_paths[] = '/etc/apache/mime.types';
$file_paths[] = '/etc/apache2/mime.types';
$file_paths[] = '/etc/nginx/mime.types';
$file_paths[] = '/usr/local/etc/httpd/conf/mime.types';
$file_paths[] = '/usr/local/etc/apache/conf/mime.types';
$file_paths[] = '/usr/local/etc/apache24/mime.types';
}
foreach ($file_paths as $fp) {
if (@is_readable($fp)) {
$lines = file($fp, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
break;
}
}
$mime_types = $mime_extensions = array();
$regex = "/([\w\+\-\.\/]+)\s+([\w\s]+)/i";
foreach ((array)$lines as $line) {
// skip comments or mime types w/o any extensions
if ($line[0] == '#' || !preg_match($regex, $line, $matches))
continue;
$mime = $matches[1];
foreach (explode(' ', $matches[2]) as $ext) {
$ext = trim($ext);
$mime_types[$mime][] = $ext;
$mime_extensions[$ext] = $mime;
}
}
// fallback to some well-known types most important for daily emails
if (empty($mime_types)) {
foreach (rcube::get_instance()->config->resolve_paths('mimetypes.php') as $fpath) {
$mime_extensions = array_merge($mime_extensions, (array) @include($fpath));
}
foreach ($mime_extensions as $ext => $mime) {
$mime_types[$mime][] = $ext;
}
}
// Add some known aliases that aren't included by some mime.types (#1488891)
// the order is important here so standard extensions have higher prio
$aliases = array(
'image/gif' => array('gif'),
'image/png' => array('png'),
'image/x-png' => array('png'),
'image/jpeg' => array('jpg', 'jpeg', 'jpe'),
'image/jpg' => array('jpg', 'jpeg', 'jpe'),
'image/pjpeg' => array('jpg', 'jpeg', 'jpe'),
'image/tiff' => array('tif'),
'message/rfc822' => array('eml'),
'text/x-mail' => array('eml'),
);
foreach ($aliases as $mime => $exts) {
$mime_types[$mime] = array_unique(array_merge((array) $mime_types[$mime], $exts));
foreach ($exts as $ext) {
if (!isset($mime_extensions[$ext])) {
$mime_extensions[$ext] = $mime;
}
}
}
return $mimetype ? $mime_types[$mimetype] : $mime_extensions;
}
/**
* Detect image type of the given binary data by checking magic numbers.
*
* @param string $data Binary file content
*
* @return string Detected mime-type or jpeg as fallback
*/
public static function image_content_type($data)
{
$type = 'jpeg';
if (preg_match('/^\x89\x50\x4E\x47/', $data)) $type = 'png';
else if (preg_match('/^\x47\x49\x46\x38/', $data)) $type = 'gif';
else if (preg_match('/^\x00\x00\x01\x00/', $data)) $type = 'ico';
// else if (preg_match('/^\xFF\xD8\xFF\xE0/', $data)) $type = 'jpeg';
return 'image/' . $type;
}
/**
* Try to fix invalid email addresses
*/
public static function fix_email($email)
{
$parts = rcube_utils::explode_quoted_string('@', $email);
foreach ($parts as $idx => $part) {
// remove redundant quoting (#1490040)
if ($part[0] == '"' && preg_match('/^"([a-zA-Z0-9._+=-]+)"$/', $part, $m)) {
$parts[$idx] = $m[1];
}
}
return implode('@', $parts);
}
}
diff --git a/program/lib/Roundcube/rcube_mime_decode.php b/program/lib/Roundcube/rcube_mime_decode.php
index ebdbbb753..739987fac 100644
--- a/program/lib/Roundcube/rcube_mime_decode.php
+++ b/program/lib/Roundcube/rcube_mime_decode.php
@@ -1,400 +1,401 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2015, The Roundcube Dev Team |
- | Copyright (C) 2011-2015, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| MIME message parsing utilities derived from Mail_mimeDecode |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
| Author: Richard Heyes <richard@phpguru.org> |
+-----------------------------------------------------------------------+
*/
/**
* Class for parsing MIME messages
*
* @package Framework
* @subpackage Storage
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_mime_decode
{
/**
* Class configuration parameters.
*
* @var array
*/
protected $params = array(
'include_bodies' => true,
'decode_bodies' => true,
'decode_headers' => true,
'crlf' => "\r\n",
'default_charset' => RCUBE_CHARSET,
);
/**
* Constructor.
*
* Sets up the object, initialise the variables, and splits and
* stores the header and body of the input.
*
* @param array $params An array of various parameters that determine
* various things:
* include_bodies - Whether to include the body in the returned
* object.
* decode_bodies - Whether to decode the bodies
* of the parts. (Transfer encoding)
* decode_headers - Whether to decode headers
* crlf - CRLF type to use (CRLF/LF/CR)
*/
public function __construct($params = array())
{
if (!empty($params)) {
$this->params = array_merge($this->params, (array) $params);
}
}
/**
* Performs the decoding process.
*
* @param string $input The input to decode
* @param bool $convert Convert result to rcube_message_part structure
*
* @return object|bool Decoded results or False on failure
*/
public function decode($input, $convert = true)
{
list($header, $body) = $this->splitBodyHeader($input);
$struct = $this->do_decode($header, $body);
if ($struct && $convert) {
$struct = $this->structure_part($struct);
}
return $struct;
}
/**
* Performs the decoding. Decodes the body string passed to it
* If it finds certain content-types it will call itself in a
* recursive fashion
*
* @param string $headers Header section
* @param string $body Body section
* @param string $default_ctype Default content type
*
* @return object|bool Decoded results or False on error
*/
protected function do_decode($headers, $body, $default_ctype = 'text/plain')
{
$return = new stdClass;
$headers = $this->parseHeaders($headers);
foreach ($headers as $value) {
$header_name = strtolower($value['name']);
if (isset($return->headers[$header_name]) && !is_array($return->headers[$header_name])) {
$return->headers[$header_name] = array($return->headers[$header_name]);
$return->headers[$header_name][] = $value['value'];
}
else if (isset($return->headers[$header_name])) {
$return->headers[$header_name][] = $value['value'];
}
else {
$return->headers[$header_name] = $value['value'];
}
switch ($header_name) {
case 'content-type':
$content_type = $this->parseHeaderValue($value['value']);
if (preg_match('/([0-9a-z+.-]+)\/([0-9a-z+.-]+)/i', $content_type['value'], $regs)) {
$return->ctype_primary = $regs[1];
$return->ctype_secondary = $regs[2];
}
if (!empty($content_type['other'])) {
$return->ctype_parameters = array_merge((array) $return->ctype_parameters, (array) $content_type['other']);
}
break;
case 'content-disposition';
$content_disposition = $this->parseHeaderValue($value['value']);
$return->disposition = $content_disposition['value'];
if (!empty($content_disposition['other'])) {
$return->d_parameters = array_merge((array) $return->d_parameters, (array) $content_disposition['other']);
}
break;
case 'content-transfer-encoding':
$content_transfer_encoding = $this->parseHeaderValue($value['value']);
break;
}
}
if (isset($content_type)) {
$ctype = strtolower($content_type['value']);
switch ($ctype) {
case 'text/plain':
$encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit';
if ($this->params['include_bodies']) {
$return->body = $this->params['decode_bodies'] ? rcube_mime::decode($body, $encoding) : $body;
}
break;
case 'text/html':
$encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit';
if ($this->params['include_bodies']) {
$return->body = $this->params['decode_bodies'] ? rcube_mime::decode($body, $encoding) : $body;
}
break;
case 'multipart/digest':
case 'multipart/alternative':
case 'multipart/related':
case 'multipart/mixed':
case 'multipart/signed':
case 'multipart/encrypted':
if (!isset($content_type['other']['boundary'])) {
return false;
}
$default_ctype = $ctype === 'multipart/digest' ? 'message/rfc822' : 'text/plain';
$parts = $this->boundarySplit($body, $content_type['other']['boundary']);
for ($i = 0; $i < count($parts); $i++) {
list($part_header, $part_body) = $this->splitBodyHeader($parts[$i]);
$return->parts[] = $this->do_decode($part_header, $part_body, $default_ctype);
}
break;
case 'message/rfc822':
$obj = new rcube_mime_decode($this->params);
$return->parts[] = $obj->decode($body, false);
unset($obj);
if ($this->params['include_bodies']) {
$return->body = $this->params['decode_bodies'] ? rcube_mime::decode($body, $encoding) : $body;
}
break;
default:
if ($this->params['include_bodies']) {
$return->body = $this->params['decode_bodies'] ? rcube_mime::decode($body, $content_transfer_encoding['value']) : $body;
}
break;
}
}
else {
$ctype = explode('/', $default_ctype);
$return->ctype_primary = $ctype[0];
$return->ctype_secondary = $ctype[1];
if ($this->params['include_bodies']) {
$return->body = $this->params['decode_bodies'] ? rcube_mime::decode($body) : $body;
}
}
return $return;
}
/**
* Given a string containing a header and body
* section, this function will split them (at the first
* blank line) and return them.
*
* @param string $input Input to split apart
*
* @return array Contains header and body section
*/
protected function splitBodyHeader($input)
{
$pos = strpos($input, $this->params['crlf'] . $this->params['crlf']);
if ($pos === false) {
return false;
}
$crlf_len = strlen($this->params['crlf']);
$header = substr($input, 0, $pos);
$body = substr($input, $pos + 2 * $crlf_len);
if (substr_compare($body, $this->params['crlf'], -$crlf_len) === 0) {
$body = substr($body, 0, -$crlf_len);
}
return array($header, $body);
}
/**
* Parse headers given in $input and return as assoc array.
*
* @param string $input Headers to parse
*
* @return array Contains parsed headers
*/
protected function parseHeaders($input)
{
if ($input !== '') {
// Unfold the input
$input = preg_replace('/' . $this->params['crlf'] . "(\t| )/", ' ', $input);
$headers = explode($this->params['crlf'], trim($input));
foreach ($headers as $value) {
$hdr_name = substr($value, 0, $pos = strpos($value, ':'));
$hdr_value = substr($value, $pos+1);
if ($hdr_value[0] == ' ') {
$hdr_value = substr($hdr_value, 1);
}
$return[] = array(
'name' => $hdr_name,
'value' => $this->params['decode_headers'] ? $this->decodeHeader($hdr_value) : $hdr_value,
);
}
}
else {
$return = array();
}
return $return;
}
/**
* Function to parse a header value, extract first part, and any secondary
* parts (after ;) This function is not as robust as it could be.
* Eg. header comments in the wrong place will probably break it.
*
* @param string $input Header value to parse
*
* @return array Contains parsed result
*/
protected function parseHeaderValue($input)
{
$parts = preg_split('/;\s*/', $input);
if (!empty($parts)) {
$return['value'] = trim($parts[0]);
for ($n = 1; $n < count($parts); $n++) {
if (preg_match_all('/(([[:alnum:]]+)="?([^"]*)"?\s?;?)+/i', $parts[$n], $matches)) {
for ($i = 0; $i < count($matches[2]); $i++) {
$return['other'][strtolower($matches[2][$i])] = $matches[3][$i];
}
}
}
}
else {
$return['value'] = trim($input);
}
return $return;
}
/**
* This function splits the input based on the given boundary
*
* @param string $input Input to parse
* @param string $boundary Boundary
*
* @return array Contains array of resulting mime parts
*/
protected function boundarySplit($input, $boundary)
{
$tmp = explode('--' . $boundary, $input);
for ($i = 1; $i < count($tmp)-1; $i++) {
$parts[] = $tmp[$i];
}
return $parts;
}
/**
* Given a header, this function will decode it according to RFC2047.
* Probably not *exactly* conformant, but it does pass all the given
* examples (in RFC2047).
*
* @param string $input Input header value to decode
*
* @return string Decoded header value
*/
protected function decodeHeader($input)
{
return rcube_mime::decode_mime_string($input, $this->params['default_charset']);
}
/**
* Recursive method to convert a rcube_mime_decode structure
* into a rcube_message_part object.
*
* @param object $part A message part struct
* @param int $count Part count
* @param string $parent Parent MIME ID
*
* @return object rcube_message_part
* @see self::decode()
*/
protected function structure_part($part, $count = 0, $parent = '')
{
$struct = new rcube_message_part;
$struct->mime_id = $part->mime_id ?: (empty($parent) ? (string)$count : "$parent.$count");
$struct->headers = $part->headers;
$struct->mimetype = $part->ctype_primary . '/' . $part->ctype_secondary;
$struct->ctype_primary = $part->ctype_primary;
$struct->ctype_secondary = $part->ctype_secondary;
$struct->ctype_parameters = $part->ctype_parameters;
if ($part->headers['content-transfer-encoding']) {
$struct->encoding = $part->headers['content-transfer-encoding'];
}
if ($part->ctype_parameters['charset']) {
$struct->charset = $part->ctype_parameters['charset'];
}
$part_charset = $struct->charset ?: $this->params['default_charset'];
// determine filename
if (($filename = $part->d_parameters['filename']) || ($filename = $part->ctype_parameters['name'])) {
if (!$this->params['decode_headers']) {
$filename = $this->decodeHeader($filename);
}
$struct->filename = $filename;
}
$struct->body = $part->body;
$struct->size = strlen($part->body);
$struct->disposition = $part->disposition;
$count = 0;
foreach ((array)$part->parts as $child_part) {
$struct->parts[] = $this->structure_part($child_part, ++$count, $struct->mime_id);
}
return $struct;
}
}
diff --git a/program/lib/Roundcube/rcube_output.php b/program/lib/Roundcube/rcube_output.php
index ab8fabeb1..27acc7075 100644
--- a/program/lib/Roundcube/rcube_output.php
+++ b/program/lib/Roundcube/rcube_output.php
@@ -1,367 +1,368 @@
<?php
/**
+-----------------------------------------------------------------------+
- | This file is part of the Roundcube PHP suite |
- | Copyright (C) 2005-2014 The Roundcube Dev Team |
+ | This file is part of the Roundcube webmail client |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| CONTENTS: |
| Abstract class for output generation |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class for output generation
*
* @package Framework
* @subpackage View
*/
abstract class rcube_output
{
public $browser;
protected $app;
protected $config;
protected $charset = RCUBE_CHARSET;
protected $env = array();
protected $skins = array();
/**
* Object constructor
*/
public function __construct()
{
$this->app = rcube::get_instance();
$this->config = $this->app->config;
$this->browser = new rcube_browser();
}
/**
* Magic getter
*/
public function __get($var)
{
// allow read-only access to some members
switch ($var) {
case 'env': return $this->env;
case 'skins': return $this->skins;
case 'charset': return $this->charset;
}
}
/**
* Setter for output charset.
* To be specified in a meta tag and sent as http-header
*
* @param string $charset Charset name
*/
public function set_charset($charset)
{
$this->charset = $charset;
}
/**
* Getter for output charset
*
* @return string Output charset name
*/
public function get_charset()
{
return $this->charset;
}
/**
* Set environment variable
*
* @param string $name Property name
* @param mixed $value Property value
*/
public function set_env($name, $value)
{
$this->env[$name] = $value;
}
/**
* Environment variable getter.
*
* @param string $name Property name
*
* @return mixed Property value
*/
public function get_env($name)
{
return $this->env[$name];
}
/**
* Delete all stored env variables and commands
*/
public function reset()
{
$this->env = array();
}
/**
* Invoke display_message command
*
* @param string $message Message to display
* @param string $type Message type [notice|confirm|error]
* @param array $vars Key-value pairs to be replaced in localized text
* @param boolean $override Override last set message
* @param int $timeout Message displaying time in seconds
*/
abstract function show_message($message, $type = 'notice', $vars = null, $override = true, $timeout = 0);
/**
* Redirect to a certain url.
*
* @param mixed $p Either a string with the action or url parameters as key-value pairs
* @param int $delay Delay in seconds
*/
abstract function redirect($p = array(), $delay = 1);
/**
* Send output to the client.
*/
abstract function send();
/**
* Send HTTP headers to prevent caching a page
*/
public function nocacheing_headers()
{
if (headers_sent()) {
return;
}
header("Expires: ".gmdate("D, d M Y H:i:s")." GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
// We need to set the following headers to make downloads work using IE in HTTPS mode.
if ($this->browser->ie && rcube_utils::https_check()) {
header('Pragma: private');
header("Cache-Control: private, must-revalidate");
}
else {
header("Cache-Control: private, no-cache, no-store, must-revalidate, post-check=0, pre-check=0");
header("Pragma: no-cache");
}
}
/**
* Send header with expire date 30 days in future
*
* @param int Expiration time in seconds
*/
public function future_expire_header($offset = 2600000)
{
if (headers_sent()) {
return;
}
header("Expires: " . gmdate("D, d M Y H:i:s", time()+$offset) . " GMT");
header("Cache-Control: max-age=$offset");
header("Pragma: ");
}
/**
* Send browser compatibility/security/privacy headers
*
* @param bool $privacy Enable privacy headers
*/
public function common_headers($privacy = true)
{
if (headers_sent()) {
return;
}
$headers = array();
// Unlock IE compatibility mode
if ($this->browser->ie) {
$headers['X-UA-Compatible'] = 'IE=edge';
}
if ($privacy) {
// Request browser to disable DNS prefetching (CVE-2010-0464)
$headers['X-DNS-Prefetch-Control'] = 'off';
// Request browser disable Referer (sic) header
$headers['Referrer-Policy'] = 'same-origin';
}
// send CSRF and clickjacking protection headers
if ($xframe = $this->app->config->get('x_frame_options', 'sameorigin')) {
$headers['X-Frame-Options'] = $xframe;
}
$plugin = $this->app->plugins->exec_hook('common_headers', array('headers' => $headers, 'privacy' => $privacy));
foreach ($plugin['headers'] as $header => $value) {
header("$header: $value");
}
}
/**
* Send headers related to file downloads
*
* @param string $filename File name
* @param array $params Optional parameters:
* type - File content type (default: 'application/octet-stream')
* disposition - Download type: 'inline' or 'attachment' (default)
* length - Content length
* charset - File name character set
* type_charset - Content character set
* time_limit - Script execution limit (default: 3600)
*/
public function download_headers($filename, $params = array())
{
if (empty($params['disposition'])) {
$params['disposition'] = 'attachment';
}
if ($params['disposition'] == 'inline' && stripos($params['type'], 'text') === 0) {
$params['type'] .= '; charset=' . ($params['type_charset'] ?: $this->charset);
}
header("Content-Type: " . ($params['type'] ?: "application/octet-stream"));
if ($params['disposition'] == 'attachment' && $this->browser->ie) {
header("Content-Type: application/force-download");
}
$disposition = "Content-Disposition: " . $params['disposition'];
// For non-ascii characters we'll use RFC2231 syntax
if (!preg_match('/[^a-zA-Z0-9_.:,?;@+ -]/', $filename)) {
$disposition .= sprintf("; filename=\"%s\"", $filename);
}
else {
$disposition .= sprintf("; filename*=%s''%s", $params['charset'] ?: $this->charset, rawurlencode($filename));
}
header($disposition);
if (isset($params['length'])) {
header("Content-Length: " . $params['length']);
}
// don't kill the connection if download takes more than 30 sec.
if (!array_key_exists('time_limit', $params)) {
$params['time_limit'] = 3600;
}
if (is_numeric($params['time_limit'])) {
@set_time_limit($params['time_limit']);
}
}
/**
* Show error page and terminate script execution
*
* @param int $code Error code
* @param string $message Error message
*/
public function raise_error($code, $message)
{
// STUB: to be overloaded by specific output classes
fwrite(STDERR, "Error $code: $message\n");
exit(-1);
}
/**
* Create an edit field for inclusion on a form
*
* @param string $col Field name
* @param string $value Field value
* @param array $attrib HTML element attributes for the field
* @param string $type HTML element type (default 'text')
*
* @return string HTML field definition
*/
public static function get_edit_field($col, $value, $attrib, $type = 'text')
{
static $colcounts = array();
$fname = '_'.$col;
$attrib['name'] = $fname . ($attrib['array'] ? '[]' : '');
$attrib['class'] = trim($attrib['class'] . ' ff_' . $col);
if ($type == 'checkbox') {
$attrib['value'] = '1';
$input = new html_checkbox($attrib);
}
else if ($type == 'textarea') {
$attrib['cols'] = $attrib['size'];
$input = new html_textarea($attrib);
}
else if ($type == 'select') {
$input = new html_select($attrib);
$input->add('---', '');
$input->add(array_values($attrib['options']), array_keys($attrib['options']));
}
else if ($type == 'password' || $attrib['type'] == 'password') {
$input = new html_passwordfield($attrib);
}
else {
if ($attrib['type'] != 'text' && $attrib['type'] != 'hidden') {
$attrib['type'] = 'text';
}
$input = new html_inputfield($attrib);
}
// use value from post
if (isset($_POST[$fname])) {
$postvalue = rcube_utils::get_input_value($fname, rcube_utils::INPUT_POST, true);
$value = $attrib['array'] ? $postvalue[intval($colcounts[$col]++)] : $postvalue;
}
$out = $input->show($value);
return $out;
}
/**
* Convert a variable into a javascript object notation
*
* @param mixed $input Input value
* @param boolean $pretty Enable JSON formatting
* @param boolean $inline Enable inline mode (generates output safe for use inside HTML)
*
* @return string Serialized JSON string
*/
public static function json_serialize($input, $pretty = false, $inline = true)
{
// The input need to be valid UTF-8 to use with json_encode()
$input = rcube_charset::clean($input);
$options = JSON_UNESCAPED_SLASHES;
// JSON_HEX_TAG is needed for inlining JSON inside of the <script> tag
// if input contains a html tag it will cause issues (#6207)
if ($inline) {
$options |= JSON_HEX_TAG;
}
// JSON_UNESCAPED_UNICODE in PHP < 7.1.0 does not escape U+2028 and U+2029
// which causes issues (#6187)
if (PHP_VERSION_ID >= 70100) {
$options |= JSON_UNESCAPED_UNICODE;
}
if ($pretty) {
$options |= JSON_PRETTY_PRINT;
}
// sometimes even using rcube_charset::clean() the input contains invalid UTF-8 sequences
// that's why we have @ here
return @json_encode($input, $options);
}
}
diff --git a/program/lib/Roundcube/rcube_plugin.php b/program/lib/Roundcube/rcube_plugin.php
index 42786a67b..b4a645442 100644
--- a/program/lib/Roundcube/rcube_plugin.php
+++ b/program/lib/Roundcube/rcube_plugin.php
@@ -1,409 +1,410 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2014, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Abstract plugins interface/class |
| All plugins need to extend this class |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Plugin interface class
*
* @package Framework
* @subpackage PluginAPI
*/
abstract class rcube_plugin
{
/**
* Class name of the plugin instance
*
* @var string
*/
public $ID;
/**
* Instance of Plugin API
*
* @var rcube_plugin_api
*/
public $api;
/**
* Regular expression defining task(s) to bind with
*
* @var string
*/
public $task;
/**
* Disables plugin in AJAX requests
*
* @var boolean
*/
public $noajax = false;
/**
* Disables plugin in framed mode
*
* @var boolean
*/
public $noframe = false;
/**
* A list of config option names that can be modified
* by the user via user interface (with save-prefs command)
*
* @var array
*/
public $allowed_prefs;
protected $home;
protected $urlbase;
private $mytask;
private $loaded_config = array();
/**
* Default constructor.
*
* @param rcube_plugin_api $api Plugin API
*/
public function __construct($api)
{
$this->ID = get_class($this);
$this->api = $api;
$this->home = $api->dir . $this->ID;
$this->urlbase = $api->url . $this->ID . '/';
}
/**
* Initialization method, needs to be implemented by the plugin itself
*/
abstract function init();
/**
* Provide information about this
*
* @return array Meta information about a plugin or false if not implemented:
* As hash array with the following keys:
* name: The plugin name
* vendor: Name of the plugin developer
* version: Plugin version name
* license: License name (short form according to http://spdx.org/licenses/)
* uri: The URL to the plugin homepage or source repository
* src_uri: Direct download URL to the source code of this plugin
* require: List of plugins required for this one (as array of plugin names)
*/
public static function info()
{
return false;
}
/**
* Attempt to load the given plugin which is required for the current plugin
*
* @param string Plugin name
* @return boolean True on success, false on failure
*/
public function require_plugin($plugin_name)
{
return $this->api->load_plugin($plugin_name, true);
}
/**
* Attempt to load the given plugin which is optional for the current plugin
*
* @param string Plugin name
* @return boolean True on success, false on failure
*/
public function include_plugin($plugin_name)
{
return $this->api->load_plugin($plugin_name, true, false);
}
/**
* Load local config file from plugins directory.
* The loaded values are patched over the global configuration.
*
* @param string $fname 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')
{
if (in_array($fname, $this->loaded_config)) {
return true;
}
$this->loaded_config[] = $fname;
$fpath = $this->home.'/'.$fname;
$rcube = rcube::get_instance();
if (($is_local = is_file($fpath)) && !$rcube->config->load_from_file($fpath)) {
rcube::raise_error(array(
'code' => 527, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to load config from $fpath"), true, false);
return false;
}
else if (!$is_local) {
// Search plugin_name.inc.php file in any configured path
return $rcube->config->load_from_file($this->ID . '.inc.php');
}
return true;
}
/**
* Register a callback function for a specific (server-side) hook
*
* @param string $hook Hook name
* @param mixed $callback Callback function as string or array
* with object reference and method name
*/
public function add_hook($hook, $callback)
{
$this->api->register_hook($hook, $callback);
}
/**
* Unregister a callback function for a specific (server-side) hook.
*
* @param string $hook Hook name
* @param mixed $callback Callback function as string or array
* with object reference and method name
*/
public function remove_hook($hook, $callback)
{
$this->api->unregister_hook($hook, $callback);
}
/**
* Load localized texts from the plugins dir
*
* @param string $dir Directory to search in
* @param mixed $add2client Make texts also available on the client
* (array with list or true for all)
*/
public function add_texts($dir, $add2client = false)
{
$rcube = rcube::get_instance();
$texts = $rcube->read_localization(realpath(slashify($this->home) . $dir));
// prepend domain to text keys and add to the application texts repository
if (!empty($texts)) {
$domain = $this->ID;
$add = array();
foreach ($texts as $key => $value) {
$add[$domain.'.'.$key] = $value;
}
$rcube->load_language($_SESSION['language'], $add);
// add labels to client
if ($add2client && method_exists($rcube->output, 'add_label')) {
if (is_array($add2client)) {
$js_labels = array_map(array($this, 'label_map_callback'), $add2client);
}
else {
$js_labels = array_keys($add);
}
$rcube->output->add_label($js_labels);
}
}
}
/**
* Wrapper for add_label() adding the plugin ID as domain
*/
public function add_label()
{
$rcube = rcube::get_instance();
if (method_exists($rcube->output, 'add_label')) {
$args = func_get_args();
if (count($args) == 1 && is_array($args[0])) {
$args = $args[0];
}
$args = array_map(array($this, 'label_map_callback'), $args);
$rcube->output->add_label($args);
}
}
/**
* Wrapper for rcube::gettext() adding the plugin ID as domain
*
* @param string $p Message identifier
*
* @return string Localized text
* @see rcube::gettext()
*/
public function gettext($p)
{
return rcube::get_instance()->gettext($p, $this->ID);
}
/**
* Register this plugin to be responsible for a specific task
*
* @param string $task Task name (only characters [a-z0-9_-] are allowed)
*/
public function register_task($task)
{
if ($this->api->register_task($task, $this->ID)) {
$this->mytask = $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 Action name (should be unique)
* @param mixed $callback 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, $this->mytask);
}
/**
* 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 $name Object name (should be unique and start with 'plugin.')
* @param mixed $callback 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 $fn 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 $fn File path; absolute or relative to the plugin directory
*/
public function include_stylesheet($fn)
{
$this->api->include_stylesheet($this->resource_url($fn), $if_exists);
}
/**
* Append a button to a certain container
*
* @param array $p Hash array with named parameters (as used in skin templates)
* @param string $container 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 $fn 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
*
* @param string $fn Filename
*/
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
*/
public function local_skin_path()
{
$rcube = rcube::get_instance();
$skins = array_keys((array)$rcube->output->skins);
if (empty($skins)) {
$skins = (array) $rcube->config->get('skin');
}
foreach ($skins as $skin) {
$skin_path = 'skins/' . $skin;
if (is_dir(realpath(slashify($this->home) . $skin_path))) {
break;
}
}
return $skin_path;
}
/**
* Callback function for array_map
*
* @param string $key Array key.
* @return string
*/
private function label_map_callback($key)
{
if (strpos($key, $this->ID.'.') === 0) {
return $key;
}
return $this->ID.'.'.$key;
}
}
diff --git a/program/lib/Roundcube/rcube_plugin_api.php b/program/lib/Roundcube/rcube_plugin_api.php
index 5575935dc..cea6b46d8 100644
--- a/program/lib/Roundcube/rcube_plugin_api.php
+++ b/program/lib/Roundcube/rcube_plugin_api.php
@@ -1,697 +1,698 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2012, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Plugins repository |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
// location where plugins are loade from
if (!defined('RCUBE_PLUGINS_DIR')) {
define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'plugins/');
}
/**
* The plugin loader and global API
*
* @package Framework
* @subpackage PluginAPI
*/
class rcube_plugin_api
{
static protected $instance;
public $dir;
public $url = 'plugins/';
public $task = '';
public $initialized = false;
public $output;
public $handlers = array();
public $allowed_prefs = array();
public $allowed_session_prefs = array();
public $active_plugins = array();
protected $plugins = array();
protected $plugins_initialized = array();
protected $tasks = array();
protected $actions = array();
protected $actionmap = array();
protected $objectsmap = array();
protected $template_contents = array();
protected $exec_stack = array();
protected $deprecated_hooks = array();
/**
* This implements the 'singleton' design pattern
*
* @return 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
*/
protected function __construct()
{
$this->dir = slashify(RCUBE_PLUGINS_DIR);
}
/**
* Initialize plugin engine
*
* This has to be done after rcmail::load_gui() or rcmail::json_init()
* was called because plugins need to have access to rcmail->output
*
* @param object rcube Instance of the rcube base class
* @param string Current application task (used for conditional plugin loading)
*/
public function init($app, $task = '')
{
$this->task = $task;
$this->output = $app->output;
// 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
foreach ($this->plugins as $plugin) {
// ... task, request type and framed mode
if (!$this->plugins_initialized[$plugin->ID] && !$this->filter($plugin)) {
$plugin->init();
$this->plugins_initialized[$plugin->ID] = $plugin;
}
}
// we have finished initializing all plugins
$this->initialized = true;
}
/**
* Load and init all enabled plugins
*
* This has to be done after rcmail::load_gui() or rcmail::json_init()
* was called because plugins need to have access to rcmail->output
*
* @param array List of configured plugins to load
* @param array List of plugins required by the application
*/
public function load_plugins($plugins_enabled, $required_plugins = array())
{
foreach ($plugins_enabled as $plugin_name) {
$this->load_plugin($plugin_name);
}
// check existance of all required core plugins
foreach ($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) {
$loaded = $this->load_plugin($plugin_name);
}
// trigger fatal error if still not loaded
if (!$loaded) {
rcube::raise_error(array(
'code' => 520, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Requried plugin $plugin_name was not loaded"), true, true);
}
}
}
/**
* Load the specified plugin
*
* @param string Plugin name
* @param boolean Force loading of the plugin even if it doesn't match the filter
* @param boolean Require loading of the plugin, error if it doesn't exist
*
* @return boolean True on success, false if not loaded or failure
*/
public function load_plugin($plugin_name, $force = false, $require = true)
{
static $plugins_dir;
if (!$plugins_dir) {
$dir = dir($this->dir);
$plugins_dir = unslashify($dir->path);
}
// plugin already loaded?
if (!$this->plugins[$plugin_name]) {
$fn = "$plugins_dir/$plugin_name/$plugin_name.php";
if (!is_readable($fn)) {
if ($require) {
rcube::raise_error(array('code' => 520, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to load plugin file $fn"), true, false);
}
return false;
}
if (!class_exists($plugin_name, false)) {
include $fn;
}
// instantiate class if exists
if (!class_exists($plugin_name, false)) {
rcube::raise_error(array('code' => 520, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "No plugin class $plugin_name found in $fn"),
true, false);
return false;
}
$plugin = new $plugin_name($this);
$this->active_plugins[] = $plugin_name;
// check inheritance...
if (is_subclass_of($plugin, 'rcube_plugin')) {
// call onload method on plugin if it exists.
// this is useful if you want to be called early in the boot process
if (method_exists($plugin, 'onload')) {
$plugin->onload();
}
if (!empty($plugin->allowed_prefs)) {
$this->allowed_prefs = array_merge($this->allowed_prefs, $plugin->allowed_prefs);
}
$this->plugins[$plugin_name] = $plugin;
}
}
if ($plugin = $this->plugins[$plugin_name]) {
// init a plugin only if $force is set or if we're called after initialization
if (($force || $this->initialized) && !$this->plugins_initialized[$plugin_name] && ($force || !$this->filter($plugin))) {
$plugin->init();
$this->plugins_initialized[$plugin_name] = $plugin;
}
}
return true;
}
/**
* check if we should prevent this plugin from initialising
*
* @param $plugin
* @return bool
*/
private function filter($plugin)
{
return ($plugin->noajax && !(is_object($this->output) && $this->output->type == 'html'))
|| ($plugin->task && !preg_match('/^('.$plugin->task.')$/i', $this->task))
|| ($plugin->noframe && !empty($_REQUEST['_framed']));
}
/**
* Get information about a specific plugin.
* This is either provided my a plugin's info() method or extracted from a package.xml or a composer.json file
*
* @param string Plugin name
* @return array Meta information about a plugin or False if plugin was not found
*/
public function get_info($plugin_name)
{
static $composer_lock, $license_uris = array(
'Apache' => 'http://www.apache.org/licenses/LICENSE-2.0.html',
'Apache-2' => 'http://www.apache.org/licenses/LICENSE-2.0.html',
'Apache-1' => 'http://www.apache.org/licenses/LICENSE-1.0',
'Apache-1.1' => 'http://www.apache.org/licenses/LICENSE-1.1',
'GPL' => 'http://www.gnu.org/licenses/gpl.html',
'GPLv2' => 'http://www.gnu.org/licenses/gpl-2.0.html',
'GPL-2.0' => 'http://www.gnu.org/licenses/gpl-2.0.html',
'GPLv3' => 'http://www.gnu.org/licenses/gpl-3.0.html',
'GPLv3+' => 'http://www.gnu.org/licenses/gpl-3.0.html',
'GPL-3.0' => 'http://www.gnu.org/licenses/gpl-3.0.html',
'GPL-3.0+' => 'http://www.gnu.org/licenses/gpl.html',
'GPL-2.0+' => 'http://www.gnu.org/licenses/gpl.html',
'AGPLv3' => 'http://www.gnu.org/licenses/agpl.html',
'AGPLv3+' => 'http://www.gnu.org/licenses/agpl.html',
'AGPL-3.0' => 'http://www.gnu.org/licenses/agpl.html',
'LGPL' => 'http://www.gnu.org/licenses/lgpl.html',
'LGPLv2' => 'http://www.gnu.org/licenses/lgpl-2.0.html',
'LGPLv2.1' => 'http://www.gnu.org/licenses/lgpl-2.1.html',
'LGPLv3' => 'http://www.gnu.org/licenses/lgpl.html',
'LGPL-2.0' => 'http://www.gnu.org/licenses/lgpl-2.0.html',
'LGPL-2.1' => 'http://www.gnu.org/licenses/lgpl-2.1.html',
'LGPL-3.0' => 'http://www.gnu.org/licenses/lgpl.html',
'LGPL-3.0+' => 'http://www.gnu.org/licenses/lgpl.html',
'BSD' => 'http://opensource.org/licenses/bsd-license.html',
'BSD-2-Clause' => 'http://opensource.org/licenses/BSD-2-Clause',
'BSD-3-Clause' => 'http://opensource.org/licenses/BSD-3-Clause',
'FreeBSD' => 'http://opensource.org/licenses/BSD-2-Clause',
'MIT' => 'http://www.opensource.org/licenses/mit-license.php',
'PHP' => 'http://opensource.org/licenses/PHP-3.0',
'PHP-3' => 'http://www.php.net/license/3_01.txt',
'PHP-3.0' => 'http://www.php.net/license/3_0.txt',
'PHP-3.01' => 'http://www.php.net/license/3_01.txt',
);
$dir = dir($this->dir);
$fn = unslashify($dir->path) . "/$plugin_name/$plugin_name.php";
$info = false;
if (!class_exists($plugin_name, false)) {
if (is_readable($fn)) {
include($fn);
}
else {
return false;
}
}
if (class_exists($plugin_name)) {
$info = $plugin_name::info();
}
// fall back to composer.json file
if (!$info) {
$composer = INSTALL_PATH . "/plugins/$plugin_name/composer.json";
if (is_readable($composer) && ($json = @json_decode(file_get_contents($composer), true))) {
list($info['vendor'], $info['name']) = explode('/', $json['name']);
$info['version'] = $json['version'];
$info['license'] = $json['license'];
$info['uri'] = $json['homepage'];
$info['require'] = array_filter(array_keys((array)$json['require']), function($pname) {
if (strpos($pname, '/') == false) {
return false;
}
list($vendor, $name) = explode('/', $pname);
return !($name == 'plugin-installer' || $vendor == 'pear-pear');
});
}
// read local composer.lock file (once)
if (!isset($composer_lock)) {
$composer_lock = @json_decode(@file_get_contents(INSTALL_PATH . "/composer.lock"), true);
if ($composer_lock['packages']) {
foreach ($composer_lock['packages'] as $i => $package) {
$composer_lock['installed'][$package['name']] = $package;
}
}
}
// load additional information from local composer.lock file
if ($lock = $composer_lock['installed'][$json['name']]) {
$info['version'] = $lock['version'];
$info['uri'] = $lock['homepage'] ?: $lock['source']['uri'];
$info['src_uri'] = $lock['dist']['uri'] ?: $lock['source']['uri'];
}
}
// fall back to package.xml file
if (!$info) {
$package = INSTALL_PATH . "/plugins/$plugin_name/package.xml";
if (is_readable($package) && ($file = file_get_contents($package))) {
$doc = new DOMDocument();
$doc->loadXML($file);
$xpath = new DOMXPath($doc);
$xpath->registerNamespace('rc', "http://pear.php.net/dtd/package-2.0");
// XPaths of plugin metadata elements
$metadata = array(
'name' => 'string(//rc:package/rc:name)',
'version' => 'string(//rc:package/rc:version/rc:release)',
'license' => 'string(//rc:package/rc:license)',
'license_uri' => 'string(//rc:package/rc:license/@uri)',
'src_uri' => 'string(//rc:package/rc:srcuri)',
'uri' => 'string(//rc:package/rc:uri)',
);
foreach ($metadata as $key => $path) {
$info[$key] = $xpath->evaluate($path);
}
// dependent required plugins (can be used, but not included in config)
$deps = $xpath->evaluate('//rc:package/rc:dependencies/rc:required/rc:package/rc:name');
for ($i = 0; $i < $deps->length; $i++) {
$dn = $deps->item($i)->nodeValue;
$info['require'][] = $dn;
}
}
}
// At least provide the name
if (!$info && class_exists($plugin_name)) {
$info = array('name' => $plugin_name, 'version' => '--');
}
else if ($info['license'] && empty($info['license_uri']) && ($license_uri = $license_uris[$info['license']])) {
$info['license_uri'] = $license_uri;
}
return $info;
}
/**
* Allows a plugin object to register a callback for a certain hook
*
* @param string $hook Hook name
* @param mixed $callback String with global function name or array($obj, 'methodname')
*/
public function register_hook($hook, $callback)
{
if (is_callable($callback)) {
if (isset($this->deprecated_hooks[$hook])) {
rcube::raise_error(array('code' => 522, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Deprecated hook name. "
. $hook . ' -> ' . $this->deprecated_hooks[$hook]), true, false);
$hook = $this->deprecated_hooks[$hook];
}
$this->handlers[$hook][] = $callback;
}
else {
rcube::raise_error(array('code' => 521, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Invalid callback function for $hook"), true, false);
}
}
/**
* Allow a plugin object to unregister a callback.
*
* @param string $hook Hook name
* @param mixed $callback String with global function name or array($obj, 'methodname')
*/
public function unregister_hook($hook, $callback)
{
$callback_id = array_search($callback, (array) $this->handlers[$hook]);
if ($callback_id !== false) {
// array_splice() removes the element and re-indexes keys
// that is required by the 'for' loop in exec_hook() below
array_splice($this->handlers[$hook], $callback_id, 1);
}
}
/**
* Triggers a plugin hook.
* This is called from the application and executes all registered handlers
*
* @param string $hook Hook name
* @param array $args 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);
}
// TODO: avoid recursion by checking in_array($hook, $this->exec_stack) ?
$args += array('abort' => false);
array_push($this->exec_stack, $hook);
// Use for loop here, so handlers added in the hook will be executed too
if (!empty($this->handlers[$hook])) {
for ($i = 0; $i < count($this->handlers[$hook]); $i++) {
$ret = call_user_func($this->handlers[$hook][$i], $args);
if ($ret && is_array($ret)) {
$args = $ret + $args;
}
if ($args['break']) {
break;
}
}
}
array_pop($this->exec_stack);
return $args;
}
/**
* Let a plugin register a handler for a specific request
*
* @param string $action Action name (_task=mail&_action=plugin.foo)
* @param string $owner Plugin name that registers this action
* @param mixed $callback Callback: string with global function name or array($obj, 'methodname')
* @param string $task Task name registered by this plugin
*/
public function register_action($action, $owner, $callback, $task = null)
{
// check action name
if ($task)
$action = $task.'.'.$action;
else 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 {
rcube::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 Action name
*/
public function exec_action($action)
{
if (isset($this->actions[$action])) {
call_user_func($this->actions[$action]);
}
else if (rcube::get_instance()->action != 'refresh') {
rcube::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 $name Object name
* @param string $owner Plugin name that registers this action
* @param mixed $callback 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 (is_object($this->output)
&& (!isset($this->objectsmap[$name]) || $this->objectsmap[$name] == $owner)
) {
$this->output->add_handler($name, $callback);
$this->objectsmap[$name] = $owner;
}
else {
rcube::raise_error(array('code' => 525, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Cannot register template handler $name;"
." already taken by another plugin or no output object available"), true, false);
}
}
/**
* Register this plugin to be responsible for a specific task
*
* @param string $task Task name (only characters [a-z0-9_-] are allowed)
* @param string $owner Plugin name that registers this action
*/
public function register_task($task, $owner)
{
// tasks are irrelevant in framework mode
if (!class_exists('rcmail', false)) {
return true;
}
if ($task != asciiwords($task, true)) {
rcube::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($task, rcmail::$main_tasks)) {
rcube::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 {
$this->tasks[$task] = $owner;
rcmail::$main_tasks[] = $task;
return true;
}
return false;
}
/**
* Checks whether the given task is registered by a plugin
*
* @param string $task Task name
*
* @return boolean True if registered, otherwise false
*/
public function is_plugin_task($task)
{
return $this->tasks[$task] ? true : false;
}
/**
* Check if a plugin hook is currently processing.
* Mainly used to prevent loops and recursion.
*
* @param string $hook Hook to check (optional)
*
* @return boolean True if any/the given hook is currently processed, otherwise false
*/
public function is_processing($hook = null)
{
return count($this->exec_stack) > 0 && (!$hook || in_array($hook, $this->exec_stack));
}
/**
* Include a plugin script file in the current HTML page
*
* @param string $fn Path to script
*/
public function include_script($fn)
{
if (is_object($this->output) && $this->output->type == 'html') {
$src = $this->resource_url($fn);
$this->output->include_script($src, 'head_bottom', false);
}
}
/**
* Include a plugin stylesheet in the current HTML page
*
* @param string $fn Path to stylesheet
*/
public function include_stylesheet($fn)
{
if (is_object($this->output) && $this->output->type == 'html') {
if ($fn[0] != '/' && !preg_match('|^https?://|i', $fn)) {
$rcube = rcube::get_instance();
$devel_mode = $rcube->config->get('devel_mode');
$assets_dir = $rcube->config->get('assets_dir');
$path = unslashify($assets_dir ?: RCUBE_INSTALL_PATH);
// Prefer .less files in devel_mode (assume less.js is loaded)
if ($devel_mode) {
$less = preg_replace('/\.css$/i', '.less', $fn);
if ($less != $fn && is_file("$path/plugins/$less")) {
$fn = $less;
}
}
else if (!preg_match('/\.min\.css$/', $fn)) {
$min = preg_replace('/\.css$/i', '.min.css', $fn);
if (is_file("$path/plugins/$min")) {
$fn = $min;
}
}
if (!is_file("$path/plugins/$fn")) {
return;
}
}
$src = $this->resource_url($fn);
$this->output->include_css($src);
}
}
/**
* Save the given HTML content to be added to a template container
*
* @param string $html HTML content
* @param string $container Template container identifier
*/
public function add_content($html, $container)
{
$this->template_contents[$container] .= $html . "\n";
}
/**
* Returns list of loaded plugins names
*
* @return array List of plugin names
*/
public function loaded_plugins()
{
return array_keys($this->plugins);
}
/**
* Returns loaded plugin
*
* @return rcube_plugin Plugin instance
*/
public function get_plugin($name)
{
return $this->plugins[$name];
}
/**
* Callback for template_container hooks
*
* @param array $attrib
* @return array
*/
protected 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
*
* @param string $fn Filename
* @return string
*/
protected function resource_url($fn)
{
if ($fn[0] != '/' && !preg_match('|^https?://|i', $fn))
return $this->url . $fn;
else
return $fn;
}
}
diff --git a/program/lib/Roundcube/rcube_result_index.php b/program/lib/Roundcube/rcube_result_index.php
index 6320bac2f..d18b63b2e 100644
--- a/program/lib/Roundcube/rcube_result_index.php
+++ b/program/lib/Roundcube/rcube_result_index.php
@@ -1,419 +1,420 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2011, The Roundcube Dev Team |
- | Copyright (C) 2011, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| SORT/SEARCH/ESEARCH response handler |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class for accessing IMAP's SORT/SEARCH/ESEARCH result
*
* @package Framework
* @subpackage Storage
*/
class rcube_result_index
{
public $incomplete = false;
protected $raw_data;
protected $mailbox;
protected $meta = array();
protected $params = array();
protected $order = 'ASC';
const SEPARATOR_ELEMENT = ' ';
/**
* Object constructor.
*/
public function __construct($mailbox = null, $data = null, $order = null)
{
$this->mailbox = $mailbox;
$this->order = $order == 'DESC' ? 'DESC' : 'ASC';
$this->init($data);
}
/**
* Initializes object with SORT command response
*
* @param string $data IMAP response string
*/
public function init($data = null)
{
$this->meta = array();
$data = explode('*', (string)$data);
// ...skip unilateral untagged server responses
for ($i=0, $len=count($data); $i<$len; $i++) {
$data_item = &$data[$i];
if (preg_match('/^ SORT/i', $data_item)) {
// valid response, initialize raw_data for is_error()
$this->raw_data = '';
$data_item = substr($data_item, 5);
break;
}
else if (preg_match('/^ (E?SEARCH)/i', $data_item, $m)) {
// valid response, initialize raw_data for is_error()
$this->raw_data = '';
$data_item = substr($data_item, strlen($m[0]));
if (strtoupper($m[1]) == 'ESEARCH') {
$data_item = trim($data_item);
// remove MODSEQ response
if (preg_match('/\(MODSEQ ([0-9]+)\)$/i', $data_item, $m)) {
$data_item = substr($data_item, 0, -strlen($m[0]));
$this->params['MODSEQ'] = $m[1];
}
// remove TAG response part
if (preg_match('/^\(TAG ["a-z0-9]+\)\s*/i', $data_item, $m)) {
$data_item = substr($data_item, strlen($m[0]));
}
// remove UID
$data_item = preg_replace('/^UID\s*/i', '', $data_item);
// ESEARCH parameters
while (preg_match('/^([a-z]+) ([0-9:,]+)\s*/i', $data_item, $m)) {
$param = strtoupper($m[1]);
$value = $m[2];
$this->params[$param] = $value;
$data_item = substr($data_item, strlen($m[0]));
if (in_array($param, array('COUNT', 'MIN', 'MAX'))) {
$this->meta[strtolower($param)] = (int) $value;
}
}
// @TODO: Implement compression using compressMessageSet() in __sleep() and __wakeup() ?
// @TODO: work with compressed result?!
if (isset($this->params['ALL'])) {
$data_item = implode(self::SEPARATOR_ELEMENT,
rcube_imap_generic::uncompressMessageSet($this->params['ALL']));
}
}
break;
}
unset($data[$i]);
}
$data = array_filter($data);
if (empty($data)) {
return;
}
$data = array_shift($data);
$data = trim($data);
$data = preg_replace('/[\r\n]/', '', $data);
$data = preg_replace('/\s+/', ' ', $data);
$this->raw_data = $data;
}
/**
* Checks the result from IMAP command
*
* @return bool True if the result is an error, False otherwise
*/
public function is_error()
{
return $this->raw_data === null;
}
/**
* Checks if the result is empty
*
* @return bool True if the result is empty, False otherwise
*/
public function is_empty()
{
return empty($this->raw_data);
}
/**
* Returns number of elements in the result
*
* @return int Number of elements
*/
public function count()
{
if ($this->meta['count'] !== null)
return $this->meta['count'];
if (empty($this->raw_data)) {
$this->meta['count'] = 0;
$this->meta['length'] = 0;
}
else {
$this->meta['count'] = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT);
}
return $this->meta['count'];
}
/**
* Returns number of elements in the result.
* Alias for count() for compatibility with rcube_result_thread
*
* @return int Number of elements
*/
public function count_messages()
{
return $this->count();
}
/**
* Returns maximal message identifier in the result
*
* @return int Maximal message identifier
*/
public function max()
{
if (!isset($this->meta['max'])) {
$this->meta['max'] = (int) @max($this->get());
}
return $this->meta['max'];
}
/**
* Returns minimal message identifier in the result
*
* @return int Minimal message identifier
*/
public function min()
{
if (!isset($this->meta['min'])) {
$this->meta['min'] = (int) @min($this->get());
}
return $this->meta['min'];
}
/**
* Slices data set.
*
* @param $offset Offset (as for PHP's array_slice())
* @param $length Number of elements (as for PHP's array_slice())
*/
public function slice($offset, $length)
{
$data = $this->get();
$data = array_slice($data, $offset, $length);
$this->meta = array();
$this->meta['count'] = count($data);
$this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
}
/**
* Filters data set. Removes elements not listed in $ids list.
*
* @param array $ids List of IDs to remove.
*/
public function filter($ids = array())
{
$data = $this->get();
$data = array_intersect($data, $ids);
$this->meta = array();
$this->meta['count'] = count($data);
$this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
}
/**
* Reverts order of elements in the result
*/
public function revert()
{
$this->order = $this->order == 'ASC' ? 'DESC' : 'ASC';
if (empty($this->raw_data)) {
return;
}
$data = $this->get();
$data = array_reverse($data);
$this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
$this->meta['pos'] = array();
}
/**
* Check if the given message ID exists in the object
*
* @param int $msgid Message ID
* @param bool $get_index When enabled element's index will be returned.
* Elements are indexed starting with 0
*
* @return mixed False if message ID doesn't exist, True if exists or
* index of the element if $get_index=true
*/
public function exists($msgid, $get_index = false)
{
if (empty($this->raw_data)) {
return false;
}
$msgid = (int) $msgid;
$begin = implode('|', array('^', preg_quote(self::SEPARATOR_ELEMENT, '/')));
$end = implode('|', array('$', preg_quote(self::SEPARATOR_ELEMENT, '/')));
if (preg_match("/($begin)$msgid($end)/", $this->raw_data, $m,
$get_index ? PREG_OFFSET_CAPTURE : null)
) {
if ($get_index) {
$idx = 0;
if ($m[0][1]) {
$idx = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT, 0, $m[0][1]);
}
// cache position of this element, so we can use it in get_element()
$this->meta['pos'][$idx] = (int)$m[0][1];
return $idx;
}
return true;
}
return false;
}
/**
* Return all messages in the result.
*
* @return array List of message IDs
*/
public function get()
{
if (empty($this->raw_data)) {
return array();
}
return explode(self::SEPARATOR_ELEMENT, $this->raw_data);
}
/**
* Return all messages in the result.
*
* @return array List of message IDs
*/
public function get_compressed()
{
if (empty($this->raw_data)) {
return '';
}
return rcube_imap_generic::compressMessageSet($this->get());
}
/**
* Return result element at specified index
*
* @param int|string $index Element's index or "FIRST" or "LAST"
*
* @return int Element value
*/
public function get_element($index)
{
$count = $this->count();
if (!$count) {
return null;
}
// first element
if ($index === 0 || $index === '0' || $index === 'FIRST') {
$pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT);
if ($pos === false)
$result = (int) $this->raw_data;
else
$result = (int) substr($this->raw_data, 0, $pos);
return $result;
}
// last element
if ($index === 'LAST' || $index == $count-1) {
$pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT);
if ($pos === false)
$result = (int) $this->raw_data;
else
$result = (int) substr($this->raw_data, $pos);
return $result;
}
// do we know the position of the element or the neighbour of it?
if (!empty($this->meta['pos'])) {
if (isset($this->meta['pos'][$index]))
$pos = $this->meta['pos'][$index];
else if (isset($this->meta['pos'][$index-1]))
$pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT,
$this->meta['pos'][$index-1] + 1);
else if (isset($this->meta['pos'][$index+1]))
$pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT,
$this->meta['pos'][$index+1] - $this->length() - 1);
if (isset($pos) && preg_match('/([0-9]+)/', $this->raw_data, $m, null, $pos)) {
return (int) $m[1];
}
}
// Finally use less effective method
$data = explode(self::SEPARATOR_ELEMENT, $this->raw_data);
return $data[$index];
}
/**
* Returns response parameters, e.g. ESEARCH's MIN/MAX/COUNT/ALL/MODSEQ
* or internal data e.g. MAILBOX, ORDER
*
* @param string $param Parameter name
*
* @return array|string Response parameters or parameter value
*/
public function get_parameters($param=null)
{
$params = $this->params;
$params['MAILBOX'] = $this->mailbox;
$params['ORDER'] = $this->order;
if ($param !== null) {
return $params[$param];
}
return $params;
}
/**
* Returns length of internal data representation
*
* @return int Data length
*/
protected function length()
{
if (!isset($this->meta['length'])) {
$this->meta['length'] = strlen($this->raw_data);
}
return $this->meta['length'];
}
}
diff --git a/program/lib/Roundcube/rcube_result_multifolder.php b/program/lib/Roundcube/rcube_result_multifolder.php
index 1bb153f7b..974cd76d3 100644
--- a/program/lib/Roundcube/rcube_result_multifolder.php
+++ b/program/lib/Roundcube/rcube_result_multifolder.php
@@ -1,348 +1,349 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2011, The Roundcube Dev Team |
- | Copyright (C) 2011, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| SORT/SEARCH/ESEARCH response handler |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Class holding a set of rcube_result_index instances that together form a
* result set of a multi-folder search
*
* @package Framework
* @subpackage Storage
*/
class rcube_result_multifolder
{
public $multi = true;
public $sets = array();
public $incomplete = false;
public $folder;
protected $meta = array();
protected $index = array();
protected $folders = array();
protected $sdata = array();
protected $order = 'ASC';
protected $sorting;
/**
* Object constructor.
*/
public function __construct($folders = array())
{
$this->folders = $folders;
$this->meta = array('count' => 0);
}
/**
* Initializes object with SORT command response
*
* @param string $data IMAP response string
*/
public function add($result)
{
$this->sets[] = $result;
if ($result->count()) {
$this->append_result($result);
}
else if ($result->incomplete) {
$this->incomplete = true;
}
}
/**
* Append message UIDs from the given result to our index
*/
protected function append_result($result)
{
$this->meta['count'] += $result->count();
// append UIDs to global index
$folder = $result->get_parameters('MAILBOX');
$index = array_map(function($uid) use ($folder) { return $uid . '-' . $folder; }, $result->get());
$this->index = array_merge($this->index, $index);
}
/**
* Store a global index of (sorted) message UIDs
*/
public function set_message_index($headers, $sort_field, $sort_order)
{
$this->index = array();
foreach ($headers as $header) {
$this->index[] = $header->uid . '-' . $header->folder;
}
$this->sorting = $sort_field;
$this->order = $sort_order;
}
/**
* Checks the result from IMAP command
*
* @return bool True if the result is an error, False otherwise
*/
public function is_error()
{
return false;
}
/**
* Checks if the result is empty
*
* @return bool True if the result is empty, False otherwise
*/
public function is_empty()
{
return empty($this->sets) || $this->meta['count'] == 0;
}
/**
* Returns number of elements in the result
*
* @return int Number of elements
*/
public function count()
{
return $this->meta['count'];
}
/**
* Returns number of elements in the result.
* Alias for count() for compatibility with rcube_result_thread
*
* @return int Number of elements
*/
public function count_messages()
{
return $this->count();
}
/**
* Reverts order of elements in the result
*/
public function revert()
{
$this->order = $this->order == 'ASC' ? 'DESC' : 'ASC';
$this->index = array_reverse($this->index);
// revert order in all sub-sets
foreach ($this->sets as $set) {
if ($this->order != $set->get_parameters('ORDER')) {
$set->revert();
}
}
}
/**
* Check if the given message ID exists in the object
*
* @param int $msgid Message ID
* @param bool $get_index When enabled element's index will be returned.
* Elements are indexed starting with 0
* @return mixed False if message ID doesn't exist, True if exists or
* index of the element if $get_index=true
*/
public function exists($msgid, $get_index = false)
{
if (!empty($this->folder)) {
$msgid .= '-' . $this->folder;
}
return array_search($msgid, $this->index);
}
/**
* Filters data set. Removes elements listed in $ids list.
*
* @param array $ids List of IDs to remove.
* @param string $folder IMAP folder
*/
public function filter($ids = array(), $folder = null)
{
$this->meta['count'] = 0;
foreach ($this->sets as $set) {
if ($set->get_parameters('MAILBOX') == $folder) {
$set->filter($ids);
}
$this->meta['count'] += $set->count();
}
}
/**
* Slices data set.
*
* @param int $offset Offset (as for PHP's array_slice())
* @param int $length Number of elements (as for PHP's array_slice())
*/
public function slice($offset, $length)
{
$data = array_slice($this->get(), $offset, $length);
$this->index = $data;
$this->meta['count'] = count($data);
}
/**
* Filters data set. Removes elements not listed in $ids list.
*
* @param array $ids List of IDs to keep.
*/
public function intersect($ids = array())
{
// not implemented
}
/**
* Return all messages in the result.
*
* @return array List of message IDs
*/
public function get()
{
return $this->index;
}
/**
* Return all messages in the result in compressed form
*
* @return string List of message IDs in compressed form
*/
public function get_compressed()
{
return '';
}
/**
* Return result element at specified index
*
* @param int|string $index Element's index or "FIRST" or "LAST"
*
* @return int Element value
*/
public function get_element($idx)
{
switch ($idx) {
case 'FIRST': return $this->index[0];
case 'LAST': return end($this->index);
default: return $this->index[$idx];
}
}
/**
* Returns response parameters, e.g. ESEARCH's MIN/MAX/COUNT/ALL/MODSEQ
* or internal data e.g. MAILBOX, ORDER
*
* @param string $param Parameter name
*
* @return array|string Response parameters or parameter value
*/
public function get_parameters($param=null)
{
$params = array(
'SORT' => $this->sorting,
'ORDER' => $this->order,
'MAILBOX' => $this->folders,
);
if ($param !== null) {
return $params[$param];
}
return $params;
}
/**
* Returns the stored result object for a particular folder
*
* @param string $folder Folder name
*
* @return false|object rcube_result_* instance of false if none found
*/
public function get_set($folder)
{
foreach ($this->sets as $set) {
if ($set->get_parameters('MAILBOX') == $folder) {
return $set;
}
}
return false;
}
/**
* Returns length of internal data representation
*
* @return int Data length
*/
protected function length()
{
return $this->count();
}
/* Serialize magic methods */
public function __sleep()
{
$this->sdata = array('incomplete' => array(), 'error' => array());
foreach ($this->sets as $set) {
if ($set->incomplete) {
$this->sdata['incomplete'][] = $set->get_parameters('MAILBOX');
}
else if ($set->is_error()) {
$this->sdata['error'][] = $set->get_parameters('MAILBOX');
}
}
return array('sdata', 'index', 'folders', 'sorting', 'order');
}
public function __wakeup()
{
$this->meta = array('count' => count($this->index));
$this->incomplete = count($this->sdata['incomplete']) > 0;
// restore result sets from saved index
$data = array();
foreach ($this->index as $item) {
list($uid, $folder) = explode('-', $item, 2);
$data[$folder] .= ' ' . $uid;
}
foreach ($this->folders as $folder) {
if (in_array($folder, $this->sdata['error'])) {
$data_str = null;
}
else {
$data_str = '* SORT' . $data[$folder];
}
$set = new rcube_result_index($folder, $data_str, strtoupper($this->order));
if (in_array($folder, $this->sdata['incomplete'])) {
$set->incomplete = true;
}
$this->sets[] = $set;
}
}
}
diff --git a/program/lib/Roundcube/rcube_result_set.php b/program/lib/Roundcube/rcube_result_set.php
index e4b260ad0..4494e2331 100644
--- a/program/lib/Roundcube/rcube_result_set.php
+++ b/program/lib/Roundcube/rcube_result_set.php
@@ -1,118 +1,119 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2006-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Class representing an address directory result set |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Roundcube result set class
*
* Representing an address directory result set.
* Implenets Iterator and thus be used in foreach() loops.
*
* @package Framework
* @subpackage Addressbook
*/
class rcube_result_set implements Iterator, ArrayAccess
{
public $count = 0;
public $first = 0;
public $searchonly = false;
public $records = array();
private $current = 0;
function __construct($c=0, $f=0)
{
$this->count = (int)$c;
$this->first = (int)$f;
}
function add($rec)
{
$this->records[] = $rec;
}
function iterate()
{
return $this->records[$this->current++];
}
function first()
{
$this->current = 0;
return $this->records[$this->current];
}
function seek($i)
{
$this->current = $i;
}
/*** Implement PHP ArrayAccess interface ***/
public function offsetSet($offset, $value)
{
if (is_null($offset)) {
$offset = count($this->records);
$this->records[] = $value;
}
else {
$this->records[$offset] = $value;
}
}
public function offsetExists($offset)
{
return isset($this->records[$offset]);
}
public function offsetUnset($offset)
{
unset($this->records[$offset]);
}
public function offsetGet($offset)
{
return $this->records[$offset];
}
/*** PHP 5 Iterator interface ***/
function rewind()
{
$this->current = 0;
}
function current()
{
return $this->records[$this->current];
}
function key()
{
return $this->current;
}
function next()
{
return $this->iterate();
}
function valid()
{
return isset($this->records[$this->current]);
}
}
diff --git a/program/lib/Roundcube/rcube_result_thread.php b/program/lib/Roundcube/rcube_result_thread.php
index 394fdeb01..2ebd48ca8 100644
--- a/program/lib/Roundcube/rcube_result_thread.php
+++ b/program/lib/Roundcube/rcube_result_thread.php
@@ -1,649 +1,650 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2011, The Roundcube Dev Team |
- | Copyright (C) 2011, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| THREAD response handler |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class for accessing IMAP's THREAD result
*
* @package Framework
* @subpackage Storage
*/
class rcube_result_thread
{
public $incomplete = false;
protected $raw_data;
protected $mailbox;
protected $meta = array();
protected $order = 'ASC';
const SEPARATOR_ELEMENT = ' ';
const SEPARATOR_ITEM = '~';
const SEPARATOR_LEVEL = ':';
/**
* Object constructor.
*/
public function __construct($mailbox = null, $data = null)
{
$this->mailbox = $mailbox;
$this->init($data);
}
/**
* Initializes object with IMAP command response
*
* @param string $data IMAP response string
*/
public function init($data = null)
{
$this->meta = array();
$data = explode('*', (string)$data);
// ...skip unilateral untagged server responses
for ($i=0, $len=count($data); $i<$len; $i++) {
if (preg_match('/^ THREAD/i', $data[$i])) {
// valid response, initialize raw_data for is_error()
$this->raw_data = '';
$data[$i] = substr($data[$i], 7);
break;
}
unset($data[$i]);
}
if (empty($data)) {
return;
}
$data = array_shift($data);
$data = trim($data);
$data = preg_replace('/[\r\n]/', '', $data);
$data = preg_replace('/\s+/', ' ', $data);
$this->raw_data = $this->parse_thread($data);
}
/**
* Checks the result from IMAP command
*
* @return bool True if the result is an error, False otherwise
*/
public function is_error()
{
return $this->raw_data === null;
}
/**
* Checks if the result is empty
*
* @return bool True if the result is empty, False otherwise
*/
public function is_empty()
{
return empty($this->raw_data);
}
/**
* Returns number of elements (threads) in the result
*
* @return int Number of elements
*/
public function count()
{
if ($this->meta['count'] !== null)
return $this->meta['count'];
if (empty($this->raw_data)) {
$this->meta['count'] = 0;
}
else {
$this->meta['count'] = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT);
}
if (!$this->meta['count'])
$this->meta['messages'] = 0;
return $this->meta['count'];
}
/**
* Returns number of all messages in the result
*
* @return int Number of elements
*/
public function count_messages()
{
if ($this->meta['messages'] !== null)
return $this->meta['messages'];
if (empty($this->raw_data)) {
$this->meta['messages'] = 0;
}
else {
$this->meta['messages'] = 1
+ substr_count($this->raw_data, self::SEPARATOR_ELEMENT)
+ substr_count($this->raw_data, self::SEPARATOR_ITEM);
}
if ($this->meta['messages'] == 0 || $this->meta['messages'] == 1)
$this->meta['count'] = $this->meta['messages'];
return $this->meta['messages'];
}
/**
* Returns maximum message identifier in the result
*
* @return int Maximum message identifier
*/
public function max()
{
if (!isset($this->meta['max'])) {
$this->meta['max'] = (int) @max($this->get());
}
return $this->meta['max'];
}
/**
* Returns minimum message identifier in the result
*
* @return int Minimum message identifier
*/
public function min()
{
if (!isset($this->meta['min'])) {
$this->meta['min'] = (int) @min($this->get());
}
return $this->meta['min'];
}
/**
* Slices data set.
*
* @param $offset Offset (as for PHP's array_slice())
* @param $length Number of elements (as for PHP's array_slice())
*/
public function slice($offset, $length)
{
$data = explode(self::SEPARATOR_ELEMENT, $this->raw_data);
$data = array_slice($data, $offset, $length);
$this->meta = array();
$this->meta['count'] = count($data);
$this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
}
/**
* Filters data set. Removes threads not listed in $roots list.
*
* @param array $roots List of IDs of thread roots.
*/
public function filter($roots)
{
$datalen = strlen($this->raw_data);
$roots = array_flip($roots);
$result = '';
$start = 0;
$this->meta = array();
$this->meta['count'] = 0;
while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start))
|| ($start < $datalen && ($pos = $datalen))
) {
$len = $pos - $start;
$elem = substr($this->raw_data, $start, $len);
$start = $pos + 1;
// extract root message ID
if ($npos = strpos($elem, self::SEPARATOR_ITEM)) {
$root = (int) substr($elem, 0, $npos);
}
else {
$root = $elem;
}
if (isset($roots[$root])) {
$this->meta['count']++;
$result .= self::SEPARATOR_ELEMENT . $elem;
}
}
$this->raw_data = ltrim($result, self::SEPARATOR_ELEMENT);
}
/**
* Reverts order of elements in the result
*/
public function revert()
{
$this->order = $this->order == 'ASC' ? 'DESC' : 'ASC';
if (empty($this->raw_data)) {
return;
}
$data = explode(self::SEPARATOR_ELEMENT, $this->raw_data);
$data = array_reverse($data);
$this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
$this->meta['pos'] = array();
}
/**
* Check if the given message ID exists in the object
*
* @param int $msgid Message ID
* @param bool $get_index When enabled element's index will be returned.
* Elements are indexed starting with 0
*
* @return boolean True on success, False if message ID doesn't exist
*/
public function exists($msgid, $get_index = false)
{
$msgid = (int) $msgid;
$begin = implode('|', array(
'^',
preg_quote(self::SEPARATOR_ELEMENT, '/'),
preg_quote(self::SEPARATOR_LEVEL, '/'),
));
$end = implode('|', array(
'$',
preg_quote(self::SEPARATOR_ELEMENT, '/'),
preg_quote(self::SEPARATOR_ITEM, '/'),
));
if (preg_match("/($begin)$msgid($end)/", $this->raw_data, $m,
$get_index ? PREG_OFFSET_CAPTURE : null)
) {
if ($get_index) {
$idx = 0;
if ($m[0][1]) {
$idx = substr_count($this->raw_data, self::SEPARATOR_ELEMENT, 0, $m[0][1]+1)
+ substr_count($this->raw_data, self::SEPARATOR_ITEM, 0, $m[0][1]+1);
}
// cache position of this element, so we can use it in get_element()
$this->meta['pos'][$idx] = (int)$m[0][1];
return $idx;
}
return true;
}
return false;
}
/**
* Return IDs of all messages in the result. Threaded data will be flattened.
*
* @return array List of message identifiers
*/
public function get()
{
if (empty($this->raw_data)) {
return array();
}
$regexp = '/(' . preg_quote(self::SEPARATOR_ELEMENT, '/')
. '|' . preg_quote(self::SEPARATOR_ITEM, '/') . '[0-9]+' . preg_quote(self::SEPARATOR_LEVEL, '/')
.')/';
return preg_split($regexp, $this->raw_data);
}
/**
* Return all messages in the result.
*
* @return array List of message identifiers
*/
public function get_compressed()
{
if (empty($this->raw_data)) {
return '';
}
return rcube_imap_generic::compressMessageSet($this->get());
}
/**
* Return result element at specified index (all messages, not roots)
*
* @param int|string $index Element's index or "FIRST" or "LAST"
*
* @return int Element value
*/
public function get_element($index)
{
$count = $this->count();
if (!$count) {
return null;
}
// first element
if ($index === 0 || $index === '0' || $index === 'FIRST') {
preg_match('/^([0-9]+)/', $this->raw_data, $m);
$result = (int) $m[1];
return $result;
}
// last element
if ($index === 'LAST' || $index == $count-1) {
preg_match('/([0-9]+)$/', $this->raw_data, $m);
$result = (int) $m[1];
return $result;
}
// do we know the position of the element or the neighbour of it?
if (!empty($this->meta['pos'])) {
$element = preg_quote(self::SEPARATOR_ELEMENT, '/');
$item = preg_quote(self::SEPARATOR_ITEM, '/') . '[0-9]+' . preg_quote(self::SEPARATOR_LEVEL, '/') .'?';
$regexp = '(' . $element . '|' . $item . ')';
if (isset($this->meta['pos'][$index])) {
if (preg_match('/([0-9]+)/', $this->raw_data, $m, null, $this->meta['pos'][$index]))
$result = $m[1];
}
else if (isset($this->meta['pos'][$index-1])) {
// get chunk of data after previous element
$data = substr($this->raw_data, $this->meta['pos'][$index-1]+1, 50);
$data = preg_replace('/^[0-9]+/', '', $data); // remove UID at $index position
$data = preg_replace("/^$regexp/", '', $data); // remove separator
if (preg_match('/^([0-9]+)/', $data, $m))
$result = $m[1];
}
else if (isset($this->meta['pos'][$index+1])) {
// get chunk of data before next element
$pos = max(0, $this->meta['pos'][$index+1] - 50);
$len = min(50, $this->meta['pos'][$index+1]);
$data = substr($this->raw_data, $pos, $len);
$data = preg_replace("/$regexp\$/", '', $data); // remove separator
if (preg_match('/([0-9]+)$/', $data, $m))
$result = $m[1];
}
if (isset($result)) {
return (int) $result;
}
}
// Finally use less effective method
$data = $this->get();
return $data[$index];
}
/**
* Returns response parameters e.g. MAILBOX, ORDER
*
* @param string $param Parameter name
*
* @return array|string Response parameters or parameter value
*/
public function get_parameters($param=null)
{
$params = array();
$params['MAILBOX'] = $this->mailbox;
$params['ORDER'] = $this->order;
if ($param !== null) {
return $params[$param];
}
return $params;
}
/**
* THREAD=REFS sorting implementation (based on provided index)
*
* @param rcube_result_index $index Sorted message identifiers
*/
public function sort($index)
{
$this->sort_order = $index->get_parameters('ORDER');
if (empty($this->raw_data)) {
return;
}
// when sorting search result it's good to make the index smaller
if ($index->count() != $this->count_messages()) {
$index->filter($this->get());
}
$result = array_fill_keys($index->get(), null);
$datalen = strlen($this->raw_data);
$start = 0;
// Here we're parsing raw_data twice, we want only one big array
// in memory at a time
// Assign roots
while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start))
|| ($start < $datalen && ($pos = $datalen))
) {
$len = $pos - $start;
$elem = substr($this->raw_data, $start, $len);
$start = $pos + 1;
$items = explode(self::SEPARATOR_ITEM, $elem);
$root = (int) array_shift($items);
if ($root) {
$result[$root] = $root;
foreach ($items as $item) {
list($lv, $id) = explode(self::SEPARATOR_LEVEL, $item);
$result[$id] = $root;
}
}
}
// get only unique roots
$result = array_filter($result); // make sure there are no nulls
$result = array_unique($result);
// Re-sort raw data
$result = array_fill_keys($result, null);
$start = 0;
while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start))
|| ($start < $datalen && ($pos = $datalen))
) {
$len = $pos - $start;
$elem = substr($this->raw_data, $start, $len);
$start = $pos + 1;
$npos = strpos($elem, self::SEPARATOR_ITEM);
$root = (int) ($npos ? substr($elem, 0, $npos) : $elem);
$result[$root] = $elem;
}
$this->raw_data = implode(self::SEPARATOR_ELEMENT, $result);
}
/**
* Returns data as tree
*
* @return array Data tree
*/
public function get_tree()
{
$datalen = strlen($this->raw_data);
$result = array();
$start = 0;
while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start))
|| ($start < $datalen && ($pos = $datalen))
) {
$len = $pos - $start;
$elem = substr($this->raw_data, $start, $len);
$items = explode(self::SEPARATOR_ITEM, $elem);
$result[array_shift($items)] = $this->build_thread($items);
$start = $pos + 1;
}
return $result;
}
/**
* Returns thread depth and children data
*
* @return array Thread data
*/
public function get_thread_data()
{
$data = $this->get_tree();
$depth = array();
$children = array();
$this->build_thread_data($data, $depth, $children);
return array($depth, $children);
}
/**
* Creates 'depth' and 'children' arrays from stored thread 'tree' data.
*/
protected function build_thread_data($data, &$depth, &$children, $level = 0)
{
foreach ((array)$data as $key => $val) {
$empty = empty($val) || !is_array($val);
$children[$key] = !$empty;
$depth[$key] = $level;
if (!$empty) {
$this->build_thread_data($val, $depth, $children, $level + 1);
}
}
}
/**
* Converts part of the raw thread into an array
*/
protected function build_thread($items, $level = 1, &$pos = 0)
{
$result = array();
for ($len=count($items); $pos < $len; $pos++) {
list($lv, $id) = explode(self::SEPARATOR_LEVEL, $items[$pos]);
if ($level == $lv) {
$pos++;
$result[$id] = $this->build_thread($items, $level+1, $pos);
}
else {
$pos--;
break;
}
}
return $result;
}
/**
* IMAP THREAD response parser
*/
protected function parse_thread($str, $begin = 0, $end = 0, $depth = 0)
{
// Don't be tempted to change $str to pass by reference to speed this up - it will slow it down by about
// 7 times instead :-) See comments on http://uk2.php.net/references and this article:
// http://derickrethans.nl/files/phparch-php-variables-article.pdf
$node = '';
if (!$end) {
$end = strlen($str);
}
// Let's try to store data in max. compacted stracture as a string,
// arrays handling is much more expensive
// For the following structure: THREAD (2)(3 6 (4 23)(44 7 96))
// -- 2
// -- 3
// \-- 6
// |-- 4
// | \-- 23
// |
// \-- 44
// \-- 7
// \-- 96
//
// The output will be: 2,3^1:6^2:4^3:23^2:44^3:7^4:96
if ($str[$begin] != '(') {
// find next bracket
$stop = $begin + strcspn($str, '()', $begin, $end - $begin);
$messages = explode(' ', trim(substr($str, $begin, $stop - $begin)));
if (empty($messages)) {
return $node;
}
foreach ($messages as $msg) {
if ($msg) {
$node .= ($depth ? self::SEPARATOR_ITEM.$depth.self::SEPARATOR_LEVEL : '').$msg;
$this->meta['messages']++;
$depth++;
}
}
if ($stop < $end) {
$node .= $this->parse_thread($str, $stop, $end, $depth);
}
}
else {
$off = $begin;
while ($off < $end) {
$start = $off;
$off++;
$n = 1;
while ($n > 0) {
$p = strpos($str, ')', $off);
if ($p === false) {
// error, wrong structure, mismatched brackets in IMAP THREAD response
// @TODO: write error to the log or maybe set $this->raw_data = null;
return $node;
}
$p1 = strpos($str, '(', $off);
if ($p1 !== false && $p1 < $p) {
$off = $p1 + 1;
$n++;
}
else {
$off = $p + 1;
$n--;
}
}
$thread = $this->parse_thread($str, $start + 1, $off - 1, $depth);
if ($thread) {
if (!$depth) {
if ($node) {
$node .= self::SEPARATOR_ELEMENT;
}
}
$node .= $thread;
}
}
}
return $node;
}
}
diff --git a/program/lib/Roundcube/rcube_session.php b/program/lib/Roundcube/rcube_session.php
index 11f2fc749..d43aa1bd8 100644
--- a/program/lib/Roundcube/rcube_session.php
+++ b/program/lib/Roundcube/rcube_session.php
@@ -1,698 +1,699 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2014, The Roundcube Dev Team |
- | Copyright (C) 2011, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide database supported session management |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
| Author: Cor Bosman <cor@roundcu.be> |
+-----------------------------------------------------------------------+
*/
/**
* Abstract class to provide database supported session storage
*
* @package Framework
* @subpackage Core
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
abstract class rcube_session
{
protected $config;
protected $key;
protected $ip;
protected $changed;
protected $start;
protected $vars;
protected $now;
protected $time_diff = 0;
protected $reloaded = false;
protected $appends = array();
protected $unsets = array();
protected $gc_enabled = 0;
protected $gc_handlers = array();
protected $cookiename = 'roundcube_sessauth';
protected $ip_check = false;
protected $logging = false;
protected $ignore_write = false;
/**
* Blocks session data from being written to database.
* Can be used if write-race conditions are to be expected
* @var boolean
*/
public $nowrite = false;
/**
* Factory, returns driver-specific instance of the class
*
* @param object $config
* @return Object rcube_session
*/
public static function factory($config)
{
// get session storage driver
$storage = $config->get('session_storage', 'db');
// class name for this storage
$class = "rcube_session_" . $storage;
// try to instantiate class
if (class_exists($class)) {
return new $class($config);
}
// no storage found, raise error
rcube::raise_error(array('code' => 604, 'type' => 'session',
'line' => __LINE__, 'file' => __FILE__,
'message' => "Failed to find session driver. Check session_storage config option"),
true, true);
}
/**
* @param Object $config
*/
public function __construct($config)
{
$this->config = $config;
// set ip check
$this->set_ip_check($this->config->get('ip_check'));
// set cookie name
if ($this->config->get('session_auth_name')) {
$this->set_cookiename($this->config->get('session_auth_name'));
}
}
/**
* register session handler
*/
public function register_session_handler()
{
if (session_id()) {
// Session already exists, skip
return;
}
ini_set('session.serialize_handler', 'php');
// set custom functions for PHP session management
session_set_save_handler(
array($this, 'open'),
array($this, 'close'),
array($this, 'read'),
array($this, 'sess_write'),
array($this, 'destroy'),
array($this, 'gc')
);
}
/**
* Wrapper for session_start()
*/
public function start()
{
$this->start = microtime(true);
$this->ip = rcube_utils::remote_addr();
$this->logging = $this->config->get('log_session', false);
$lifetime = $this->config->get('session_lifetime', 1) * 60;
$this->set_lifetime($lifetime);
session_start();
}
/**
* Abstract methods should be implemented by driver classes
*/
abstract function open($save_path, $session_name);
abstract function close();
abstract function destroy($key);
abstract function read($key);
abstract function write($key, $vars);
abstract function update($key, $newvars, $oldvars);
/**
* session write handler. This calls the implementation methods for write/update after some initial checks.
*
* @param $key
* @param $vars
*
* @return bool
*/
public function sess_write($key, $vars)
{
if ($this->nowrite) {
return true;
}
// check cache
$oldvars = $this->get_cache($key);
// if there are cached vars, update store, else insert new data
if ($oldvars) {
$newvars = $this->_fixvars($vars, $oldvars);
return $this->update($key, $newvars, $oldvars);
}
else {
return $this->write($key, $vars);
}
}
/**
* Wrapper for session_write_close()
*/
public function write_close()
{
session_write_close();
// write_close() is called on script shutdown, see rcube::shutdown()
// execute cleanup functionality if enabled by session gc handler
// we do this after closing the session for better performance
$this->gc_shutdown();
}
/**
* Creates a new (separate) session
*
* @param array Session data
*
* @return string Session identifier (on success)
*/
public function create($data)
{
$length = strlen(session_id());
$key = rcube_utils::random_bytes($length);
// create new session
if ($this->write($key, $this->serialize($data))) {
return $key;
}
}
/**
* Merge vars with old vars and apply unsets
*/
protected function _fixvars($vars, $oldvars)
{
if ($oldvars !== null) {
$a_oldvars = $this->unserialize($oldvars);
if (is_array($a_oldvars)) {
// remove unset keys on oldvars
foreach ((array)$this->unsets as $var) {
if (isset($a_oldvars[$var])) {
unset($a_oldvars[$var]);
}
else {
$path = explode('.', $var);
$k = array_pop($path);
$node = &$this->get_node($path, $a_oldvars);
unset($node[$k]);
}
}
$newvars = $this->serialize(array_merge(
(array)$a_oldvars, (array)$this->unserialize($vars)));
}
else {
$newvars = $vars;
}
}
$this->unsets = array();
return $newvars;
}
/**
* Execute registered garbage collector routines
*/
public function gc($maxlifetime)
{
// move gc execution to the script shutdown function
// see rcube::shutdown() and rcube_session::write_close()
$this->gc_enabled = $maxlifetime;
return true;
}
/**
* Register additional garbage collector functions
*
* @param mixed Callback function
*/
public function register_gc_handler($func)
{
foreach ($this->gc_handlers as $handler) {
if ($handler == $func) {
return;
}
}
$this->gc_handlers[] = $func;
}
/**
* Garbage collector handler to run on script shutdown
*/
protected function gc_shutdown()
{
if ($this->gc_enabled) {
foreach ($this->gc_handlers as $fct) {
call_user_func($fct);
}
}
}
/**
* Generate and set new session id
*
* @param boolean $destroy If enabled the current session will be destroyed
*
* @return bool
*/
public function regenerate_id($destroy = true)
{
// Since PHP 7.0 session_regenerate_id() will cause the old
// session data update, we don't need this
$this->ignore_write = true;
session_regenerate_id($destroy);
$this->ignore_write = false;
$this->vars = null;
$this->key = session_id();
return true;
}
/**
* See if we have vars of this key already cached, and if so, return them.
*
* @param string $key Session ID
*
* @return string
*/
protected function get_cache($key)
{
// no session data in cache (read() returns false)
if (!$this->key) {
$cache = null;
}
// use internal data for fast requests (up to 0.5 sec.)
else if ($key == $this->key && (!$this->vars || microtime(true) - $this->start < 0.5)) {
$cache = $this->vars;
}
else { // else read data again
$cache = $this->read($key);
}
return $cache;
}
/**
* Append the given value to the certain node in the session data array
*
* Warning: Do not use if you already modified $_SESSION in the same request (#1490608)
*
* @param string Path denoting the session variable where to append the value
* @param string Key name under which to append the new value (use null for appending to an indexed list)
* @param mixed Value to append to the session data array
*/
public function append($path, $key, $value)
{
// re-read session data from DB because it might be outdated
if (!$this->reloaded && microtime(true) - $this->start > 0.5) {
$this->reload();
$this->reloaded = true;
$this->start = microtime(true);
}
$node = &$this->get_node(explode('.', $path), $_SESSION);
if ($key !== null) {
$node[$key] = $value;
$path .= '.' . $key;
}
else {
$node[] = $value;
}
$this->appends[] = $path;
// when overwriting a previously unset variable
if ($this->unsets[$path]) {
unset($this->unsets[$path]);
}
}
/**
* Unset a session variable
*
* @param string Variable name (can be a path denoting a certain node in the session array, e.g. compose.attachments.5)
* @return boolean True on success
*/
public function remove($var=null)
{
if (empty($var)) {
return $this->destroy(session_id());
}
$this->unsets[] = $var;
if (isset($_SESSION[$var])) {
unset($_SESSION[$var]);
}
else {
$path = explode('.', $var);
$key = array_pop($path);
$node = &$this->get_node($path, $_SESSION);
unset($node[$key]);
}
return true;
}
/**
* Kill this session
*/
public function kill()
{
$this->vars = null;
$this->ip = rcube_utils::remote_addr(); // update IP (might have changed)
$this->destroy(session_id());
rcube_utils::setcookie($this->cookiename, '-del-', time() - 60);
}
/**
* Re-read session data from storage backend
*/
public function reload()
{
// collect updated data from previous appends
$merge_data = array();
foreach ((array)$this->appends as $var) {
$path = explode('.', $var);
$value = $this->get_node($path, $_SESSION);
$k = array_pop($path);
$node = &$this->get_node($path, $merge_data);
$node[$k] = $value;
}
if ($this->key) {
$data = $this->read($this->key);
}
if ($data) {
session_decode($data);
// apply appends and unsets to reloaded data
$_SESSION = array_merge_recursive($_SESSION, $merge_data);
foreach ((array)$this->unsets as $var) {
if (isset($_SESSION[$var])) {
unset($_SESSION[$var]);
}
else {
$path = explode('.', $var);
$k = array_pop($path);
$node = &$this->get_node($path, $_SESSION);
unset($node[$k]);
}
}
}
}
/**
* Returns a reference to the node in data array referenced by the given path.
* e.g. ['compose','attachments'] will return $_SESSION['compose']['attachments']
*/
protected function &get_node($path, &$data_arr)
{
$node = &$data_arr;
if (!empty($path)) {
foreach ((array)$path as $key) {
if (!isset($node[$key]))
$node[$key] = array();
$node = &$node[$key];
}
}
return $node;
}
/**
* Serialize session data
*/
protected function serialize($vars)
{
$data = '';
if (is_array($vars)) {
foreach ($vars as $var=>$value)
$data .= $var.'|'.serialize($value);
}
else {
$data = 'b:0;';
}
return $data;
}
/**
* Unserialize session data
* http://www.php.net/manual/en/function.session-decode.php#56106
*/
public static function unserialize($str)
{
$str = (string)$str;
$endptr = strlen($str);
$p = 0;
$serialized = '';
$items = 0;
$level = 0;
while ($p < $endptr) {
$q = $p;
while ($str[$q] != '|')
if (++$q >= $endptr)
break 2;
if ($str[$p] == '!') {
$p++;
$has_value = false;
}
else {
$has_value = true;
}
$name = substr($str, $p, $q - $p);
$q++;
$serialized .= 's:' . strlen($name) . ':"' . $name . '";';
if ($has_value) {
for (;;) {
$p = $q;
switch (strtolower($str[$q])) {
case 'n': // null
case 'b': // boolean
case 'i': // integer
case 'd': // decimal
do $q++;
while ( ($q < $endptr) && ($str[$q] != ';') );
$q++;
$serialized .= substr($str, $p, $q - $p);
if ($level == 0)
break 2;
break;
case 'r': // reference
$q+= 2;
for ($id = ''; ($q < $endptr) && ($str[$q] != ';'); $q++)
$id .= $str[$q];
$q++;
// increment pointer because of outer array
$serialized .= 'R:' . ($id + 1) . ';';
if ($level == 0)
break 2;
break;
case 's': // string
$q+=2;
for ($length=''; ($q < $endptr) && ($str[$q] != ':'); $q++)
$length .= $str[$q];
$q+=2;
$q+= (int)$length + 2;
$serialized .= substr($str, $p, $q - $p);
if ($level == 0)
break 2;
break;
case 'a': // array
case 'o': // object
do $q++;
while ($q < $endptr && $str[$q] != '{');
$q++;
$level++;
$serialized .= substr($str, $p, $q - $p);
break;
case '}': // end of array|object
$q++;
$serialized .= substr($str, $p, $q - $p);
if (--$level == 0)
break 2;
break;
default:
return false;
}
}
}
else {
$serialized .= 'N;';
$q += 2;
}
$items++;
$p = $q;
}
return unserialize( 'a:' . $items . ':{' . $serialized . '}' );
}
/**
* Setter for session lifetime
*/
public function set_lifetime($lifetime)
{
$this->lifetime = max(120, $lifetime);
// valid time range is now - 1/2 lifetime to now + 1/2 lifetime
$now = time();
$this->now = $now - ($now % ($this->lifetime / 2));
}
/**
* Getter for remote IP saved with this session
*/
public function get_ip()
{
return $this->ip;
}
/**
* Setter for cookie encryption secret
*/
function set_secret($secret = null)
{
// generate random hash and store in session
if (!$secret) {
if (!empty($_SESSION['auth_secret'])) {
$secret = $_SESSION['auth_secret'];
}
else {
$secret = rcube_utils::random_bytes(strlen($this->key));
}
}
$_SESSION['auth_secret'] = $secret;
}
/**
* Enable/disable IP check
*/
function set_ip_check($check)
{
$this->ip_check = $check;
}
/**
* Setter for the cookie name used for session cookie
*/
function set_cookiename($cookiename)
{
if ($cookiename) {
$this->cookiename = $cookiename;
}
}
/**
* Check session authentication cookie
*
* @return boolean True if valid, False if not
*/
function check_auth()
{
$this->cookie = $_COOKIE[$this->cookiename];
$result = $this->ip_check ? rcube_utils::remote_addr() == $this->ip : true;
if (!$result) {
$this->log("IP check failed for " . $this->key . "; expected " . $this->ip . "; got " . rcube_utils::remote_addr());
}
if ($result && $this->_mkcookie($this->now) != $this->cookie) {
$this->log("Session auth check failed for " . $this->key . "; timeslot = " . date('Y-m-d H:i:s', $this->now));
$result = false;
// Check if using id from a previous time slot
for ($i = 1; $i <= 2; $i++) {
$prev = $this->now - ($this->lifetime / 2) * $i;
if ($this->_mkcookie($prev) == $this->cookie) {
$this->log("Send new auth cookie for " . $this->key . ": " . $this->cookie);
$this->set_auth_cookie();
$result = true;
}
}
}
if (!$result) {
$this->log("Session authentication failed for " . $this->key
. "; invalid auth cookie sent; timeslot = " . date('Y-m-d H:i:s', $prev));
}
return $result;
}
/**
* Set session authentication cookie
*/
public function set_auth_cookie()
{
$this->cookie = $this->_mkcookie($this->now);
rcube_utils::setcookie($this->cookiename, $this->cookie, 0);
$_COOKIE[$this->cookiename] = $this->cookie;
}
/**
* Create session cookie for specified time slot.
*
* @param int Time slot to use
*
* @return string
*/
protected function _mkcookie($timeslot)
{
// make sure the secret key exists
$this->set_secret();
// no need to hash this, it's just a random string
return $_SESSION['auth_secret'] . '-' . $timeslot;
}
/**
* Writes debug information to the log
*/
function log($line)
{
if ($this->logging) {
rcube::write_log('session', $line);
}
}
}
diff --git a/program/lib/Roundcube/rcube_smtp.php b/program/lib/Roundcube/rcube_smtp.php
index 4831caed3..3c76c3c5f 100644
--- a/program/lib/Roundcube/rcube_smtp.php
+++ b/program/lib/Roundcube/rcube_smtp.php
@@ -1,522 +1,523 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2012, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide SMTP functionality using socket connections |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Class to provide SMTP functionality using PEAR Net_SMTP
*
* @package Framework
* @subpackage Mail
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_smtp
{
private $conn;
private $response;
private $error;
private $anonymize_log = 0;
// define headers delimiter
const SMTP_MIME_CRLF = "\r\n";
const DEBUG_LINE_LENGTH = 4098; // 4KB + 2B for \r\n
/**
* SMTP Connection and authentication
*
* @param string Server host
* @param string Server port
* @param string User name
* @param string Password
*
* @return bool Returns true on success, or false on error
*/
public function connect($host = null, $port = null, $user = null, $pass = null)
{
$rcube = rcube::get_instance();
// disconnect/destroy $this->conn
$this->disconnect();
// reset error/response var
$this->error = $this->response = null;
// let plugins alter smtp connection config
$CONFIG = $rcube->plugins->exec_hook('smtp_connect', array(
'smtp_server' => $host ?: $rcube->config->get('smtp_server'),
'smtp_port' => $port ?: $rcube->config->get('smtp_port', 587),
'smtp_user' => $user !== null ? $user : $rcube->config->get('smtp_user', '%u'),
'smtp_pass' => $pass !== null ? $pass : $rcube->config->get('smtp_pass', '%p'),
'smtp_auth_cid' => $rcube->config->get('smtp_auth_cid'),
'smtp_auth_pw' => $rcube->config->get('smtp_auth_pw'),
'smtp_auth_type' => $rcube->config->get('smtp_auth_type'),
'smtp_helo_host' => $rcube->config->get('smtp_helo_host'),
'smtp_timeout' => $rcube->config->get('smtp_timeout'),
'smtp_conn_options' => $rcube->config->get('smtp_conn_options'),
'smtp_auth_callbacks' => array(),
));
$smtp_host = rcube_utils::parse_host($CONFIG['smtp_server']);
// when called from Installer it's possible to have empty $smtp_host here
if (!$smtp_host) $smtp_host = 'localhost';
$smtp_port = is_numeric($CONFIG['smtp_port']) ? $CONFIG['smtp_port'] : 25;
$smtp_host_url = parse_url($smtp_host);
// overwrite port
if (isset($smtp_host_url['host']) && isset($smtp_host_url['port'])) {
$smtp_host = $smtp_host_url['host'];
$smtp_port = $smtp_host_url['port'];
}
// re-write smtp host
if (isset($smtp_host_url['host']) && isset($smtp_host_url['scheme'])) {
$smtp_host = sprintf('%s://%s', $smtp_host_url['scheme'], $smtp_host_url['host']);
}
// remove TLS prefix and set flag for use in Net_SMTP::auth()
if (preg_match('#^tls://#i', $smtp_host)) {
$smtp_host = preg_replace('#^tls://#i', '', $smtp_host);
$use_tls = true;
}
// Handle per-host socket options
rcube_utils::parse_socket_options($CONFIG['smtp_conn_options'], $smtp_host);
// Use valid EHLO/HELO host (#6408)
$helo_host = $CONFIG['smtp_helo_host'] ?: rcube_utils::server_name();
$helo_host = rcube_utils::idn_to_ascii($helo_host);
if (!preg_match('/^[a-zA-Z0-9.:-]+$/', $helo_host)) {
$helo_host = 'localhost';
}
// IDNA Support
$smtp_host = rcube_utils::idn_to_ascii($smtp_host);
$this->conn = new Net_SMTP($smtp_host, $smtp_port, $helo_host, false, 0, $CONFIG['smtp_conn_options'],
$CONFIG['gssapi_context'], $CONFIG['gssapi_cn']);
if ($rcube->config->get('smtp_debug')) {
$this->conn->setDebug(true, array($this, 'debug_handler'));
$this->anonymize_log = 0;
$_host = ($use_tls ? 'tls://' : '') . $smtp_host . ':' . $smtp_port;
$this->debug_handler($this->conn, "Connecting to $_host...");
}
// register authentication methods
if (!empty($CONFIG['smtp_auth_callbacks']) && method_exists($this->conn, 'setAuthMethod')) {
foreach ($CONFIG['smtp_auth_callbacks'] as $callback) {
$this->conn->setAuthMethod($callback['name'], $callback['function'],
isset($callback['prepend']) ? $callback['prepend'] : true);
}
}
// try to connect to server and exit on failure
$result = $this->conn->connect($CONFIG['smtp_timeout']);
if (is_a($result, 'PEAR_Error')) {
$this->response[] = "Connection failed: " . $result->getMessage();
list($code,) = $this->conn->getResponse();
$this->error = array('label' => 'smtpconnerror', 'vars' => array('code' => $code));
$this->conn = null;
return false;
}
// workaround for timeout bug in Net_SMTP 1.5.[0-1] (#1487843)
if (method_exists($this->conn, 'setTimeout')
&& ($timeout = ini_get('default_socket_timeout'))
) {
$this->conn->setTimeout($timeout);
}
$smtp_user = str_replace('%u', $rcube->get_user_name(), $CONFIG['smtp_user']);
$smtp_pass = str_replace('%p', $rcube->get_user_password(), $CONFIG['smtp_pass']);
$smtp_auth_type = $CONFIG['smtp_auth_type'] ?: null;
if (!empty($CONFIG['smtp_auth_cid'])) {
$smtp_authz = $smtp_user;
$smtp_user = $CONFIG['smtp_auth_cid'];
$smtp_pass = $CONFIG['smtp_auth_pw'];
}
// attempt to authenticate to the SMTP server
if (($smtp_user && $smtp_pass) || ($smtp_auth_type == 'GSSAPI')) {
// IDNA Support
if (strpos($smtp_user, '@')) {
$smtp_user = rcube_utils::idn_to_ascii($smtp_user);
}
$result = $this->conn->auth($smtp_user, $smtp_pass, $smtp_auth_type, $use_tls, $smtp_authz);
if (is_a($result, 'PEAR_Error')) {
list($code,) = $this->conn->getResponse();
$this->error = array('label' => 'smtpautherror', 'vars' => array('code' => $code));
$this->response[] = 'Authentication failure: ' . $result->getMessage()
. ' (Code: ' . $result->getCode() . ')';
$this->reset();
$this->disconnect();
return false;
}
}
return true;
}
/**
* Function for sending mail
*
* @param string Sender e-Mail address
*
* @param mixed Either a comma-separated list of recipients
* (RFC822 compliant), or an array of recipients,
* each RFC822 valid. This may contain recipients not
* specified in the headers, for Bcc:, resending
* messages, etc.
* @param mixed The message headers to send with the mail
* Either as an associative array or a finally
* formatted string
* @param mixed The full text of the message body, including any Mime parts
* or file handle
* @param array Delivery options (e.g. DSN request)
*
* @return bool Returns true on success, or false on error
*/
public function send_mail($from, $recipients, &$headers, &$body, $opts=null)
{
if (!is_object($this->conn)) {
return false;
}
// prepare message headers as string
if (is_array($headers)) {
if (!($headerElements = $this->_prepare_headers($headers))) {
$this->reset();
return false;
}
list($from, $text_headers) = $headerElements;
}
else if (is_string($headers)) {
$text_headers = $headers;
}
// exit if no from address is given
if (!isset($from)) {
$this->reset();
$this->response[] = "No From address has been provided";
return false;
}
// prepare list of recipients
$recipients = $this->_parse_rfc822($recipients);
if (is_a($recipients, 'PEAR_Error')) {
$this->error = array('label' => 'smtprecipientserror');
$this->reset();
return false;
}
$exts = $this->conn->getServiceExtensions();
// RFC3461: Delivery Status Notification
if ($opts['dsn']) {
if (isset($exts['DSN'])) {
$from_params = 'RET=HDRS';
$recipient_params = 'NOTIFY=SUCCESS,FAILURE';
}
}
// RFC6531: request SMTPUTF8 if needed
if (preg_match('/[^\x00-\x7F]/', $from . implode('', $recipients))) {
if (isset($exts['SMTPUTF8'])) {
$from_params = ltrim($from_params . ' SMTPUTF8');
}
else {
$this->error = array('label' => 'smtputf8error');
$this->response[] = "SMTP server does not support unicode in email addresses";
$this->reset();
return false;
}
}
// RFC2298.3: remove envelope sender address
if (empty($opts['mdn_use_from'])
&& preg_match('/Content-Type: multipart\/report/', $text_headers)
&& preg_match('/report-type=disposition-notification/', $text_headers)
) {
$from = '';
}
// set From: address
$result = $this->conn->mailFrom($from, $from_params);
if (is_a($result, 'PEAR_Error')) {
$err = $this->conn->getResponse();
$this->error = array('label' => 'smtpfromerror', 'vars' => array(
'from' => $from, 'code' => $err[0], 'msg' => $err[1]));
$this->response[] = "Failed to set sender '$from'. "
. $err[1] . ' (Code: ' . $err[0] . ')';
$this->reset();
return false;
}
// set mail recipients
foreach ($recipients as $recipient) {
$result = $this->conn->rcptTo($recipient, $recipient_params);
if (is_a($result, 'PEAR_Error')) {
$err = $this->conn->getResponse();
$this->error = array('label' => 'smtptoerror', 'vars' => array(
'to' => $recipient, 'code' => $err[0], 'msg' => $err[1]));
$this->response[] = "Failed to add recipient '$recipient'. "
. $err[1] . ' (Code: ' . $err[0] . ')';
$this->reset();
return false;
}
}
if (is_resource($body)) {
// file handle
$data = $body;
if ($text_headers) {
$text_headers = preg_replace('/[\r\n]+$/', '', $text_headers);
}
}
else {
// Concatenate headers and body so it can be passed by reference to SMTP_CONN->data
// so preg_replace in SMTP_CONN->quotedata will store a reference instead of a copy.
// We are still forced to make another copy here for a couple ticks so we don't really
// get to save a copy in the method call.
$data = $text_headers . "\r\n" . $body;
// unset old vars to save data and so we can pass into SMTP_CONN->data by reference.
unset($text_headers, $body);
}
// Send the message's headers and the body as SMTP data.
$result = $this->conn->data($data, $text_headers);
if (is_a($result, 'PEAR_Error')) {
$err = $this->conn->getResponse();
$err_label = 'smtperror';
$err_vars = array();
if (!in_array($err[0], array(354, 250, 221))) {
$msg = sprintf('[%d] %s', $err[0], $err[1]);
}
else {
$msg = $result->getMessage();
if (strpos($msg, 'size exceeds')) {
$err_label = 'smtpsizeerror';
$exts = $this->conn->getServiceExtensions();
$limit = $exts['SIZE'];
if ($limit) {
$msg .= " (Limit: $limit)";
$rcube = rcube::get_instance();
if (method_exists($rcube, 'show_bytes')) {
$limit = $rcube->show_bytes($limit);
}
$err_vars['limit'] = $limit;
$err_label = 'smtpsizeerror';
}
}
}
$err_vars['msg'] = $msg;
$this->error = array('label' => $err_label, 'vars' => $err_vars);
$this->response[] = "Failed to send data. " . $msg;
$this->reset();
return false;
}
$this->response[] = join(': ', $this->conn->getResponse());
return true;
}
/**
* Reset the global SMTP connection
*/
public function reset()
{
if (is_object($this->conn)) {
$this->conn->rset();
}
}
/**
* Disconnect the global SMTP connection
*/
public function disconnect()
{
if (is_object($this->conn)) {
$this->conn->disconnect();
$this->conn = null;
}
}
/**
* This is our own debug handler for the SMTP connection
*/
public function debug_handler(&$smtp, $message)
{
// catch AUTH commands and set anonymization flag for subsequent sends
if (preg_match('/^Send: AUTH ([A-Z]+)/', $message, $m)) {
$this->anonymize_log = $m[1] == 'LOGIN' ? 2 : 1;
}
// anonymize this log entry
else if ($this->anonymize_log > 0 && strpos($message, 'Send:') === 0 && --$this->anonymize_log == 0) {
$message = sprintf('Send: ****** [%d]', strlen($message) - 8);
}
if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) {
$diff = $len - self::DEBUG_LINE_LENGTH;
$message = substr($message, 0, self::DEBUG_LINE_LENGTH)
. "... [truncated $diff bytes]";
}
rcube::write_log('smtp', preg_replace('/\r\n$/', '', $message));
}
/**
* Get error message
*/
public function get_error()
{
return $this->error;
}
/**
* Get server response messages array
*/
public function get_response()
{
return $this->response;
}
/**
* Take an array of mail headers and return a string containing
* text usable in sending a message.
*
* @param array $headers The array of headers to prepare, in an associative
* array, where the array key is the header name (ie,
* 'Subject'), and the array value is the header
* value (ie, 'test'). The header produced from those
* values would be 'Subject: test'.
*
* @return mixed Returns false if it encounters a bad address,
* otherwise returns an array containing two
* elements: Any From: address found in the headers,
* and the plain text version of the headers.
*/
private function _prepare_headers($headers)
{
$lines = array();
$from = null;
foreach ($headers as $key => $value) {
if (strcasecmp($key, 'From') === 0) {
$addresses = $this->_parse_rfc822($value);
if (is_array($addresses)) {
$from = $addresses[0];
}
// Reject envelope From: addresses with spaces.
if (strpos($from, ' ') !== false) {
return false;
}
$lines[] = $key . ': ' . $value;
}
else if (strcasecmp($key, 'Received') === 0) {
$received = array();
if (is_array($value)) {
foreach ($value as $line) {
$received[] = $key . ': ' . $line;
}
}
else {
$received[] = $key . ': ' . $value;
}
// Put Received: headers at the top. Spam detectors often
// flag messages with Received: headers after the Subject:
// as spam.
$lines = array_merge($received, $lines);
}
else {
// If $value is an array (i.e., a list of addresses), convert
// it to a comma-delimited string of its elements (addresses).
if (is_array($value)) {
$value = implode(', ', $value);
}
$lines[] = $key . ': ' . $value;
}
}
return array($from, join(self::SMTP_MIME_CRLF, $lines) . self::SMTP_MIME_CRLF);
}
/**
* Take a set of recipients and parse them, returning an array of
* bare addresses (forward paths) that can be passed to sendmail
* or an smtp server with the rcpt to: command.
*
* @param mixed Either a comma-separated list of recipients
* (RFC822 compliant), or an array of recipients,
* each RFC822 valid.
*
* @return array An array of forward paths (bare addresses).
*/
private function _parse_rfc822($recipients)
{
// if we're passed an array, assume addresses are valid and implode them before parsing.
if (is_array($recipients)) {
$recipients = implode(', ', $recipients);
}
$addresses = array();
$recipients = preg_replace('/[\s\t]*\r?\n/', '', $recipients);
$recipients = rcube_utils::explode_quoted_string(',', $recipients);
reset($recipients);
foreach ($recipients as $recipient) {
$a = rcube_utils::explode_quoted_string(' ', $recipient);
foreach ($a as $word) {
$word = trim($word);
$len = strlen($word);
if ($len && strpos($word, "@") > 0 && $word[$len-1] != '"') {
$word = preg_replace('/^<|>$/', '', $word);
if (!in_array($word, $addresses)) {
array_push($addresses, $word);
}
}
}
}
return $addresses;
}
}
diff --git a/program/lib/Roundcube/rcube_spellchecker.php b/program/lib/Roundcube/rcube_spellchecker.php
index 88d8282c7..c70a7e82c 100644
--- a/program/lib/Roundcube/rcube_spellchecker.php
+++ b/program/lib/Roundcube/rcube_spellchecker.php
@@ -1,418 +1,419 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2011-2013, Kolab Systems AG |
- | Copyright (C) 2008-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Spellchecking using different backends |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Helper class for spellchecking with Googielspell and PSpell support.
*
* @package Framework
* @subpackage Utils
*/
class rcube_spellchecker
{
private $matches = array();
private $engine;
private $backend;
private $lang;
private $rc;
private $error;
private $options = array();
private $dict;
private $have_dict;
/**
* Constructor
*
* @param string $lang Language code
*/
function __construct($lang = 'en')
{
$this->rc = rcube::get_instance();
$this->engine = $this->rc->config->get('spellcheck_engine', 'googie');
$this->lang = $lang ?: 'en';
$this->options = array(
'ignore_syms' => $this->rc->config->get('spellcheck_ignore_syms'),
'ignore_nums' => $this->rc->config->get('spellcheck_ignore_nums'),
'ignore_caps' => $this->rc->config->get('spellcheck_ignore_caps'),
'dictionary' => $this->rc->config->get('spellcheck_dictionary'),
);
$cls = 'rcube_spellchecker_' . $this->engine;
if (class_exists($cls)) {
$this->backend = new $cls($this, $this->lang);
$this->backend->options = $this->options;
}
else {
$this->error = "Unknown spellcheck engine '$this->engine'";
}
}
/**
* Return a list of supported languages
*/
function languages()
{
// trust configuration
$configured = $this->rc->config->get('spellcheck_languages');
if (!empty($configured) && is_array($configured) && !$configured[0]) {
return $configured;
}
else if (!empty($configured)) {
$langs = (array)$configured;
}
else if ($this->backend) {
$langs = $this->backend->languages();
}
// load index
@include(RCUBE_LOCALIZATION_DIR . 'index.inc');
// add correct labels
$languages = array();
foreach ((array) $langs as $lang) {
$langc = strtolower(substr($lang, 0, 2));
$alias = $rcube_language_aliases[$langc];
if (!$alias) {
$alias = $langc.'_'.strtoupper($langc);
}
if ($rcube_languages[$lang]) {
$languages[$lang] = $rcube_languages[$lang];
}
else if ($rcube_languages[$alias]) {
$languages[$lang] = $rcube_languages[$alias];
}
else {
$languages[$lang] = ucfirst($lang);
}
}
// remove possible duplicates (#1489395)
$languages = array_unique($languages);
asort($languages);
return $languages;
}
/**
* Set content and check spelling
*
* @param string $text Text content for spellchecking
* @param bool $is_html Enables HTML-to-Text conversion
*
* @return bool True when no mispelling found, otherwise false
*/
function check($text, $is_html = false)
{
// convert to plain text
if ($is_html) {
$this->content = $this->html2text($text);
}
else {
$this->content = $text;
}
if ($this->backend) {
$this->matches = $this->backend->check($this->content);
}
return $this->found() == 0;
}
/**
* Number of mispellings found (after check)
*
* @return int Number of mispellings
*/
function found()
{
return count($this->matches);
}
/**
* Returns suggestions for the specified word
*
* @param string $word The word
*
* @return array Suggestions list
*/
function get_suggestions($word)
{
if ($this->backend) {
return $this->backend->get_suggestions($word);
}
return array();
}
/**
* Returns misspelled words
*
* @param string $text The content for spellchecking. If empty content
* used for check() method will be used.
*
* @return array List of misspelled words
*/
function get_words($text = null, $is_html=false)
{
if ($is_html) {
$text = $this->html2text($text);
}
if ($this->backend) {
return $this->backend->get_words($text);
}
return array();
}
/**
* Returns checking result in XML (Googiespell) format
*
* @return string XML content
*/
function get_xml()
{
// send output
$out = '<?xml version="1.0" encoding="'.RCUBE_CHARSET.'"?><spellresult charschecked="'.mb_strlen($this->content).'">';
foreach ((array)$this->matches as $item) {
$out .= '<c o="'.$item[1].'" l="'.$item[2].'">';
$out .= is_array($item[4]) ? implode("\t", $item[4]) : $item[4];
$out .= '</c>';
}
$out .= '</spellresult>';
return $out;
}
/**
* Returns checking result (misspelled words with suggestions)
*
* @return array Spellchecking result. An array indexed by word.
*/
function get()
{
$result = array();
foreach ((array)$this->matches as $item) {
if ($this->engine == 'pspell') {
$word = $item[0];
}
else {
$word = mb_substr($this->content, $item[1], $item[2], RCUBE_CHARSET);
}
if (is_array($item[4])) {
$suggestions = $item[4];
}
else if (empty($item[4])) {
$suggestions = array();
}
else {
$suggestions = explode("\t", $item[4]);
}
$result[$word] = $suggestions;
}
return $result;
}
/**
* Returns error message
*
* @return string Error message
*/
function error()
{
return $this->error ?: ($this->backend ? $this->backend->error() : false);
}
private function html2text($text)
{
$h2t = new rcube_html2text($text, false, false, 0);
return $h2t->get_text();
}
/**
* Check if the specified word is an exception according to
* spellcheck options.
*
* @param string $word The word
*
* @return bool True if the word is an exception, False otherwise
*/
public function is_exception($word)
{
// Contain only symbols (e.g. "+9,0", "2:2")
if (!$word || preg_match('/^[0-9@#$%^&_+~*<>=:;?!,.-]+$/', $word))
return true;
// Contain symbols (e.g. "g@@gle"), all symbols excluding separators
if (!empty($this->options['ignore_syms']) && preg_match('/[@#$%^&_+~*=-]/', $word))
return true;
// Contain numbers (e.g. "g00g13")
if (!empty($this->options['ignore_nums']) && preg_match('/[0-9]/', $word))
return true;
// Blocked caps (e.g. "GOOGLE")
if (!empty($this->options['ignore_caps']) && $word == mb_strtoupper($word))
return true;
// Use exceptions from dictionary
if (!empty($this->options['dictionary'])) {
$this->load_dict();
// @TODO: should dictionary be case-insensitive?
if (!empty($this->dict) && in_array($word, $this->dict))
return true;
}
return false;
}
/**
* Add a word to dictionary
*
* @param string $word The word to add
*/
public function add_word($word)
{
$this->load_dict();
foreach (explode(' ', $word) as $word) {
// sanity check
if (strlen($word) < 512) {
$this->dict[] = $word;
$valid = true;
}
}
if ($valid) {
$this->dict = array_unique($this->dict);
$this->update_dict();
}
}
/**
* Remove a word from dictionary
*
* @param string $word The word to remove
*/
public function remove_word($word)
{
$this->load_dict();
if (($key = array_search($word, $this->dict)) !== false) {
unset($this->dict[$key]);
$this->update_dict();
}
}
/**
* Update dictionary row in DB
*/
private function update_dict()
{
if (strcasecmp($this->options['dictionary'], 'shared') != 0) {
$userid = $this->rc->get_user_id();
}
$plugin = $this->rc->plugins->exec_hook('spell_dictionary_save', array(
'userid' => $userid, 'language' => $this->lang, 'dictionary' => $this->dict));
if (!empty($plugin['abort'])) {
return;
}
if ($this->have_dict) {
if (!empty($this->dict)) {
$this->rc->db->query(
"UPDATE " . $this->rc->db->table_name('dictionary', true)
." SET `data` = ?"
." WHERE `user_id` " . ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
." AND `language` = ?",
implode(' ', $plugin['dictionary']), $plugin['language']);
}
// don't store empty dict
else {
$this->rc->db->query(
"DELETE FROM " . $this->rc->db->table_name('dictionary', true)
." WHERE `user_id` " . ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
." AND `language` = ?",
$plugin['language']);
}
}
else if (!empty($this->dict)) {
$this->rc->db->query(
"INSERT INTO " . $this->rc->db->table_name('dictionary', true)
." (`user_id`, `language`, `data`) VALUES (?, ?, ?)",
$plugin['userid'], $plugin['language'], implode(' ', $plugin['dictionary']));
}
}
/**
* Get dictionary from DB
*/
private function load_dict()
{
if (is_array($this->dict)) {
return $this->dict;
}
if (strcasecmp($this->options['dictionary'], 'shared') != 0) {
$userid = $this->rc->get_user_id();
}
$plugin = $this->rc->plugins->exec_hook('spell_dictionary_get', array(
'userid' => $userid, 'language' => $this->lang, 'dictionary' => array()));
if (empty($plugin['abort'])) {
$dict = array();
$sql_result = $this->rc->db->query(
"SELECT `data` FROM " . $this->rc->db->table_name('dictionary', true)
." WHERE `user_id` ". ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
." AND `language` = ?",
$plugin['language']);
if ($sql_arr = $this->rc->db->fetch_assoc($sql_result)) {
$this->have_dict = true;
if (!empty($sql_arr['data'])) {
$dict = explode(' ', $sql_arr['data']);
}
}
$plugin['dictionary'] = array_merge((array)$plugin['dictionary'], $dict);
}
if (!empty($plugin['dictionary']) && is_array($plugin['dictionary'])) {
$this->dict = $plugin['dictionary'];
}
else {
$this->dict = array();
}
return $this->dict;
}
}
diff --git a/program/lib/Roundcube/rcube_storage.php b/program/lib/Roundcube/rcube_storage.php
index be0987669..b7f8b2c8c 100644
--- a/program/lib/Roundcube/rcube_storage.php
+++ b/program/lib/Roundcube/rcube_storage.php
@@ -1,1006 +1,1007 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2012, The Roundcube Dev Team |
- | Copyright (C) 2012, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Mail Storage Engine |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Abstract class for accessing mail messages storage server
*
* @package Framework
* @subpackage Storage
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
abstract class rcube_storage
{
/**
* Instance of connection object e.g. rcube_imap_generic
*
* @var mixed
*/
public $conn;
/**
* List of supported special folder types
*
* @var array
*/
public static $folder_types = array('drafts', 'sent', 'junk', 'trash');
protected $folder = 'INBOX';
protected $default_charset = 'ISO-8859-1';
protected $options = array('auth_type' => 'check', 'language' => 'en_US');
protected $page_size = 10;
protected $list_page = 1;
protected $threading = false;
protected $search_set;
/**
* All (additional) headers used (in any way) by Roundcube
* Not listed here: DATE, FROM, TO, CC, REPLY-TO, SUBJECT, CONTENT-TYPE, LIST-POST
* (used for messages listing) are hardcoded in rcube_imap_generic::fetchHeaders()
*
* @var array
*/
protected $all_headers = array(
'IN-REPLY-TO',
'BCC',
'SENDER',
'MESSAGE-ID',
'CONTENT-TRANSFER-ENCODING',
'REFERENCES',
'X-DRAFT-INFO',
'MAIL-FOLLOWUP-TO',
'MAIL-REPLY-TO',
'RETURN-PATH',
);
const UNKNOWN = 0;
const NOPERM = 1;
const READONLY = 2;
const TRYCREATE = 3;
const INUSE = 4;
const OVERQUOTA = 5;
const ALREADYEXISTS = 6;
const NONEXISTENT = 7;
const CONTACTADMIN = 8;
const DUAL_USE_FOLDERS = 'X-DUAL-USE-FOLDERS';
/**
* Connect to the server
*
* @param string $host Host to connect
* @param string $user Username for IMAP account
* @param string $pass Password for IMAP account
* @param integer $port Port to connect to
* @param string $use_ssl SSL schema (either ssl or tls) or null if plain connection
*
* @return boolean TRUE on success, FALSE on failure
*/
abstract function connect($host, $user, $pass, $port = 143, $use_ssl = null);
/**
* Close connection. Usually done on script shutdown
*/
abstract function close();
/**
* Checks connection state.
*
* @return boolean TRUE on success, FALSE on failure
*/
abstract function is_connected();
/**
* Check connection state, connect if not connected.
*
* @return bool Connection state.
*/
abstract function check_connection();
/**
* Returns code of last error
*
* @return int Error code
*/
abstract function get_error_code();
/**
* Returns message of last error
*
* @return string Error message
*/
abstract function get_error_str();
/**
* Returns code of last command response
*
* @return int Response code (class constant)
*/
abstract function get_response_code();
/**
* Set connection and class options
*
* @param array $opt Options array
*/
public function set_options($opt)
{
$this->options = array_merge($this->options, (array)$opt);
}
/**
* Get connection/class option
*
* @param string $name Option name
*
* @param mixed Option value
*/
public function get_option($name)
{
return $this->options[$name];
}
/**
* Activate/deactivate debug mode.
*
* @param boolean $dbg True if conversation with the server should be logged
*/
abstract function set_debug($dbg = true);
/**
* Set default message charset.
*
* This will be used for message decoding if a charset specification is not available
*
* @param string $cs Charset string
*/
public function set_charset($cs)
{
$this->default_charset = $cs;
}
/**
* Set internal folder reference.
* All operations will be performed on this folder.
*
* @param string $folder Folder name
*/
public function set_folder($folder)
{
if ($this->folder === $folder) {
return;
}
$this->folder = $folder;
}
/**
* Returns the currently used folder name
*
* @return string Name of the folder
*/
public function get_folder()
{
return $this->folder;
}
/**
* Set internal list page number.
*
* @param int $page Page number to list
*/
public function set_page($page)
{
if ($page = intval($page)) {
$this->list_page = $page;
}
}
/**
* Gets internal list page number.
*
* @return int Page number
*/
public function get_page()
{
return $this->list_page;
}
/**
* Set internal page size
*
* @param int $size Number of messages to display on one page
*/
public function set_pagesize($size)
{
$this->page_size = (int) $size;
}
/**
* Get internal page size
*
* @return int Number of messages to display on one page
*/
public function get_pagesize()
{
return $this->page_size;
}
/**
* Save a search result for future message listing methods.
*
* @param mixed $set Search set in driver specific format
*/
abstract function set_search_set($set);
/**
* Return the saved search set.
*
* @return array Search set in driver specific format, NULL if search wasn't initialized
*/
abstract function get_search_set();
/**
* Returns the storage server's (IMAP) capability
*
* @param string $cap Capability name
*
* @return mixed Capability value or TRUE if supported, FALSE if not
*/
abstract function get_capability($cap);
/**
* Sets threading flag to the best supported THREAD algorithm.
* Enable/Disable threaded mode.
*
* @param boolean $enable TRUE to enable and FALSE
*
* @return mixed Threading algorithm or False if THREAD is not supported
*/
public function set_threading($enable = false)
{
$this->threading = false;
if ($enable && ($caps = $this->get_capability('THREAD'))) {
$methods = array('REFS', 'REFERENCES', 'ORDEREDSUBJECT');
$methods = array_intersect($methods, $caps);
$this->threading = array_shift($methods);
}
return $this->threading;
}
/**
* Get current threading flag.
*
* @return mixed Threading algorithm or False if THREAD is not supported or disabled
*/
public function get_threading()
{
return $this->threading;
}
/**
* Checks the PERMANENTFLAGS capability of the current folder
* and returns true if the given flag is supported by the server.
*
* @param string $flag Permanentflag name
*
* @return boolean True if this flag is supported
*/
abstract function check_permflag($flag);
/**
* Returns the delimiter that is used by the server
* for folder hierarchy separation.
*
* @return string Delimiter string
*/
abstract function get_hierarchy_delimiter();
/**
* Get namespace
*
* @param string $name Namespace array index: personal, other, shared, prefix
*
* @return array Namespace data
*/
abstract function get_namespace($name = null);
/**
* Get messages count for a specific folder.
*
* @param string $folder Folder name
* @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT|EXISTS]
* @param boolean $force Force reading from server and update cache
* @param boolean $status Enables storing folder status info (max UID/count),
* required for folder_status()
*
* @return int Number of messages
*/
abstract function count($folder = null, $mode = 'ALL', $force = false, $status = true);
/**
* Public method for listing message flags
*
* @param string $folder Folder name
* @param array $uids Message UIDs
* @param int $mod_seq Optional MODSEQ value
*
* @return array Indexed array with message flags
*/
abstract function list_flags($folder, $uids, $mod_seq = null);
/**
* Public method for listing headers.
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param string $sort_field Header field to sort by
* @param string $sort_order Sort order [ASC|DESC]
* @param int $slice Number of slice items to extract from result array
*
* @return array Indexed array with message header objects
*/
abstract function list_messages($folder = null, $page = null, $sort_field = null, $sort_order = null, $slice = 0);
/**
* Return sorted list of message UIDs
*
* @param string $folder Folder to get index from
* @param string $sort_field Sort column
* @param string $sort_order Sort order [ASC, DESC]
*
* @return rcube_result_index|rcube_result_thread List of messages (UIDs)
*/
abstract function index($folder = null, $sort_field = null, $sort_order = null);
/**
* Invoke search request to the server.
*
* @param string $folder Folder name to search in
* @param string $str Search criteria
* @param string $charset Search charset
* @param string $sort_field Header field to sort by
*
* @todo: Search criteria should be provided in non-IMAP format, eg. array
*/
abstract function search($folder = null, $str = 'ALL', $charset = null, $sort_field = null);
/**
* Direct (real and simple) search request (without result sorting and caching).
*
* @param string $folder Folder name to search in
* @param string $str Search string
*
* @return rcube_result_index Search result (UIDs)
*/
abstract function search_once($folder = null, $str = 'ALL');
/**
* Refresh saved search set
*
* @return array Current search set
*/
abstract function refresh_search();
/* --------------------------------
* messages management
* --------------------------------*/
/**
* Fetch message headers and body structure from the server and build
* an object structure.
*
* @param int $uid Message UID to fetch
* @param string $folder Folder to read from
*
* @return object rcube_message_header Message data
*/
abstract function get_message($uid, $folder = null);
/**
* Return message headers object of a specific message
*
* @param int $id Message sequence ID or UID
* @param string $folder Folder to read from
* @param bool $force True to skip cache
*
* @return rcube_message_header Message headers
*/
abstract function get_message_headers($uid, $folder = null, $force = false);
/**
* Fetch message body of a specific message from the server
*
* @param int $uid Message UID
* @param string $part Part number
* @param rcube_message_part $o_part Part object created by get_structure()
* @param mixed $print True to print part, resource to write part contents in
* @param resource $fp File pointer to save the message part
* @param boolean $skip_charset_conv Disables charset conversion
*
* @return string Message/part body if not printed
*/
abstract function get_message_part($uid, $part = 1, $o_part = null, $print = null, $fp = null, $skip_charset_conv = false);
/**
* Fetch message body of a specific message from the server
*
* @param int $uid Message UID
*
* @return string $part Message/part body
* @see rcube_imap::get_message_part()
*/
public function get_body($uid, $part = 1)
{
$headers = $this->get_message_headers($uid);
return rcube_charset::convert($this->get_message_part($uid, $part, null),
$headers->charset ?: $this->default_charset);
}
/**
* Returns the whole message source as string (or saves to a file)
*
* @param int $uid Message UID
* @param resource $fp File pointer to save the message
* @param string $part Optional message part ID
*
* @return string Message source string
*/
abstract function get_raw_body($uid, $fp = null, $part = null);
/**
* Returns the message headers as string
*
* @param int $uid Message UID
* @param string $part Optional message part ID
*
* @return string Message headers string
*/
abstract function get_raw_headers($uid, $part = null);
/**
* Sends the whole message source to stdout
*
* @param int $uid Message UID
* @param bool $formatted Enables line-ending formatting
*/
abstract function print_raw_body($uid, $formatted = true);
/**
* Set message flag to one or several messages
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $flag Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
* @param string $folder Folder name
* @param boolean $skip_cache True to skip message cache clean up
*
* @return bool Operation status
*/
abstract function set_flag($uids, $flag, $folder = null, $skip_cache = false);
/**
* Remove message flag for one or several messages
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $flag Flag to unset: SEEN, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
* @param string $folder Folder name
*
* @return bool Operation status
* @see set_flag
*/
public function unset_flag($uids, $flag, $folder = null)
{
return $this->set_flag($uids, 'UN'.$flag, $folder);
}
/**
* Append a mail message (source) to a specific folder.
*
* @param string $folder Target folder
* @param string|array $message The message source string or filename
* or array (of strings and file pointers)
* @param string $headers Headers string if $message contains only the body
* @param boolean $is_file True if $message is a filename
* @param array $flags Message flags
* @param mixed $date Message internal date
*
* @return int|bool Appended message UID or True on success, False on error
*/
abstract function save_message($folder, &$message, $headers = '', $is_file = false, $flags = array(), $date = null);
/**
* Move message(s) from one folder to another.
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $to Target folder
* @param string $from Source folder
*
* @return boolean True on success, False on error
*/
abstract function move_message($uids, $to, $from = null);
/**
* Copy message(s) from one mailbox to another.
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $to Target folder
* @param string $from Source folder
*
* @return boolean True on success, False on error
*/
abstract function copy_message($uids, $to, $from = null);
/**
* Mark message(s) as deleted and expunge.
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $folder Source folder
*
* @return boolean True on success, False on error
*/
abstract function delete_message($uids, $folder = null);
/**
* Expunge message(s) and clear the cache.
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $folder Folder name
* @param boolean $clear_cache False if cache should not be cleared
*
* @return boolean True on success, False on error
*/
abstract function expunge_message($uids, $folder = null, $clear_cache = true);
/**
* Parse message UIDs input
*
* @param mixed $uids UIDs array or comma-separated list or '*' or '1:*'
*
* @return array Two elements array with UIDs converted to list and ALL flag
*/
protected function parse_uids($uids)
{
if ($uids === '*' || $uids === '1:*') {
if (empty($this->search_set)) {
$uids = '1:*';
$all = true;
}
// get UIDs from current search set
else {
$uids = join(',', $this->search_set->get());
}
}
else {
if (is_array($uids)) {
$uids = join(',', $uids);
}
else if (strpos($uids, ':')) {
$uids = join(',', rcube_imap_generic::uncompressMessageSet($uids));
}
if (preg_match('/[^0-9,]/', $uids)) {
$uids = '';
}
}
return array($uids, (bool) $all);
}
/* --------------------------------
* folder management
* --------------------------------*/
/**
* Get a list of subscribed folders.
*
* @param string $root Optional root folder
* @param string $name Optional name pattern
* @param string $filter Optional filter
* @param string $rights Optional ACL requirements
* @param bool $skip_sort Enable to return unsorted list (for better performance)
*
* @return array List of folders
*/
abstract function list_folders_subscribed($root = '', $name = '*', $filter = null, $rights = null, $skip_sort = false);
/**
* Get a list of all folders available on the server.
*
* @param string $root IMAP root dir
* @param string $name Optional name pattern
* @param mixed $filter Optional filter
* @param string $rights Optional ACL requirements
* @param bool $skip_sort Enable to return unsorted list (for better performance)
*
* @return array Indexed array with folder names
*/
abstract function list_folders($root = '', $name = '*', $filter = null, $rights = null, $skip_sort = false);
/**
* Subscribe to a specific folder(s)
*
* @param array $folders Folder name(s)
*
* @return boolean True on success
*/
abstract function subscribe($folders);
/**
* Unsubscribe folder(s)
*
* @param array $folders Folder name(s)
*
* @return boolean True on success
*/
abstract function unsubscribe($folders);
/**
* Create a new folder on the server.
*
* @param string $folder New folder name
* @param boolean $subscribe True if the newvfolder should be subscribed
* @param string $type Optional folder type (junk, trash, drafts, sent, archive)
* @param boolean $noselect Make the folder \NoSelect folder by adding hierarchy
* separator at the end (useful for server that do not support
* both folders and messages as folder children)
*
* @return boolean True on success, False on error
*/
abstract function create_folder($folder, $subscribe = false, $type = null, $noselect = false);
/**
* Set a new name to an existing folder
*
* @param string $folder Folder to rename
* @param string $new_name New folder name
*
* @return boolean True on success, False on error
*/
abstract function rename_folder($folder, $new_name);
/**
* Remove a folder from the server.
*
* @param string $folder Folder name
*
* @return boolean True on success, False on error
*/
abstract function delete_folder($folder);
/**
* Send expunge command and clear the cache.
*
* @param string $folder Folder name
* @param boolean $clear_cache False if cache should not be cleared
*
* @return boolean True on success, False on error
*/
public function expunge_folder($folder = null, $clear_cache = true)
{
return $this->expunge_message('*', $folder, $clear_cache);
}
/**
* Remove all messages in a folder..
*
* @param string $folder Folder name
*
* @return boolean True on success, False on error
*/
public function clear_folder($folder = null)
{
return $this->delete_message('*', $folder);
}
/**
* Checks if folder exists and is subscribed
*
* @param string $folder Folder name
* @param boolean $subscription Enable subscription checking
*
* @return boolean True if folder exists, False otherwise
*/
abstract function folder_exists($folder, $subscription = false);
/**
* Get folder size (size of all messages in a folder)
*
* @param string $folder Folder name
*
* @return int Folder size in bytes, False on error
*/
abstract function folder_size($folder);
/**
* Returns the namespace where the folder is in
*
* @param string $folder Folder name
*
* @return string One of 'personal', 'other' or 'shared'
*/
abstract function folder_namespace($folder);
/**
* Gets folder attributes (from LIST response, e.g. \Noselect, \Noinferiors).
*
* @param string $folder Folder name
* @param bool $force Set to True if attributes should be refreshed
*
* @return array Options list
*/
abstract function folder_attributes($folder, $force = false);
/**
* Gets connection (and current folder) data: UIDVALIDITY, EXISTS, RECENT,
* PERMANENTFLAGS, UIDNEXT, UNSEEN
*
* @param string $folder Folder name
*
* @return array Data
*/
abstract function folder_data($folder);
/**
* Returns extended information about the folder.
*
* @param string $folder Folder name
*
* @return array Data
*/
abstract function folder_info($folder);
/**
* Returns current status of a folder (compared to the last time use)
*
* @param string $folder Folder name
* @param array $diff Difference data
*
* @return int Folder status
*/
abstract function folder_status($folder = null, &$diff = array());
/**
* Synchronizes messages cache.
*
* @param string $folder Folder name
*/
abstract function folder_sync($folder);
/**
* Modify folder name according to namespace.
* For output it removes prefix of the personal namespace if it's possible.
* For input it adds the prefix. Use it before creating a folder in root
* of the folders tree.
*
* @param string $folder Folder name
* @param string $mode Mode name (out/in)
*
* @return string Folder name
*/
abstract function mod_folder($folder, $mode = 'out');
/**
* Check if the folder name is valid
*
* @param string $folder Folder name (UTF-8)
* @param string &$char First forbidden character found
*
* @return bool True if the name is valid, False otherwise
*/
public function folder_validate($folder, &$char = null)
{
$delim = $this->get_hierarchy_delimiter();
if (strpos($folder, $delim) !== false) {
$char = $delim;
return false;
}
return true;
}
/**
* Create all folders specified as default
*/
public function create_default_folders()
{
$rcube = rcube::get_instance();
// create default folders if they do not exist
foreach (self::$folder_types as $type) {
if ($folder = $rcube->config->get($type . '_mbox')) {
if (!$this->folder_exists($folder)) {
$this->create_folder($folder, true, $type);
}
else if (!$this->folder_exists($folder, true)) {
$this->subscribe($folder);
}
}
}
}
/**
* Check if specified folder is a special folder
*/
public function is_special_folder($name)
{
return $name == 'INBOX' || in_array($name, $this->get_special_folders());
}
/**
* Return configured special folders
*/
public function get_special_folders($forced = false)
{
// getting config might be expensive, store special folders in memory
if (!isset($this->icache['special-folders'])) {
$rcube = rcube::get_instance();
$this->icache['special-folders'] = array();
foreach (self::$folder_types as $type) {
if ($folder = $rcube->config->get($type . '_mbox')) {
$this->icache['special-folders'][$type] = $folder;
}
}
}
return $this->icache['special-folders'];
}
/**
* Set special folder associations stored in backend
*/
public function set_special_folders($specials)
{
// should be overridden by storage class if backend supports special folders (SPECIAL-USE)
unset($this->icache['special-folders']);
}
/**
* Get mailbox quota information.
*
* @param string $folder Folder name
*
* @return mixed Quota info or False if not supported
*/
abstract function get_quota($folder = null);
/* -----------------------------------------
* ACL and METADATA methods
* ----------------------------------------*/
/**
* Changes the ACL on the specified folder (SETACL)
*
* @param string $folder Folder name
* @param string $user User name
* @param string $acl ACL string
*
* @return boolean True on success, False on failure
*/
abstract function set_acl($folder, $user, $acl);
/**
* Removes any <identifier,rights> pair for the
* specified user from the ACL for the specified
* folder (DELETEACL).
*
* @param string $folder Folder name
* @param string $user User name
*
* @return boolean True on success, False on failure
*/
abstract function delete_acl($folder, $user);
/**
* Returns the access control list for a folder (GETACL).
*
* @param string $folder Folder name
*
* @return array User-rights array on success, NULL on error
*/
abstract function get_acl($folder);
/**
* Returns information about what rights can be granted to the
* user (identifier) in the ACL for the folder (LISTRIGHTS).
*
* @param string $folder Folder name
* @param string $user User name
*
* @return array List of user rights
*/
abstract function list_rights($folder, $user);
/**
* Returns the set of rights that the current user has to a folder (MYRIGHTS).
*
* @param string $folder Folder name
*
* @return array MYRIGHTS response on success, NULL on error
*/
abstract function my_rights($folder);
/**
* Sets metadata/annotations (SETMETADATA/SETANNOTATION)
*
* @param string $folder Folder name (empty for server metadata)
* @param array $entries Entry-value array (use NULL value as NIL)
*
* @return boolean True on success, False on failure
*/
abstract function set_metadata($folder, $entries);
/**
* Unsets metadata/annotations (SETMETADATA/SETANNOTATION)
*
* @param string $folder Folder name (empty for server metadata)
* @param array $entries Entry names array
*
* @return boolean True on success, False on failure
*/
abstract function delete_metadata($folder, $entries);
/**
* Returns folder metadata/annotations (GETMETADATA/GETANNOTATION).
*
* @param string $folder Folder name (empty for server metadata)
* @param array $entries Entries
* @param array $options Command options (with MAXSIZE and DEPTH keys)
* @param bool $force Disables cache use
*
* @return array Metadata entry-value hash array on success, NULL on error
*/
abstract function get_metadata($folder, $entries, $options = array(), $force = false);
/* -----------------------------------------
* Cache related functions
* ----------------------------------------*/
/**
* Clears the cache.
*
* @param string $key Cache key name or pattern
* @param boolean $prefix_mode Enable it to clear all keys starting
* with prefix specified in $key
*/
abstract function clear_cache($key = null, $prefix_mode = false);
/**
* Returns cached value
*
* @param string $key Cache key
*
* @return mixed Cached value
*/
abstract function get_cache($key);
/**
* Delete outdated cache entries
*/
abstract function cache_gc();
}
diff --git a/program/lib/Roundcube/rcube_string_replacer.php b/program/lib/Roundcube/rcube_string_replacer.php
index 284d58547..2768bbc5b 100644
--- a/program/lib/Roundcube/rcube_string_replacer.php
+++ b/program/lib/Roundcube/rcube_string_replacer.php
@@ -1,257 +1,258 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2009-2012, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Handle string replacements based on preg_replace_callback |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Helper class for string replacements based on preg_replace_callback
*
* @package Framework
* @subpackage Utils
*/
class rcube_string_replacer
{
public static $pattern = '/##str_replacement_(\d+)##/';
public $mailto_pattern;
public $link_pattern;
public $linkref_index;
public $linkref_pattern;
protected $values = array();
protected $options = array();
protected $linkrefs = array();
protected $urls = array();
protected $noword = '[^\w@.#-]';
function __construct($options = array())
{
// Simplified domain expression for UTF8 characters handling
// Support unicode/punycode in top-level domain part
$utf_domain = '[^?&@"\'\\/()<>\s\r\t\n]+\\.?([^\\x00-\\x2f\\x3b-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-zA-Z0-9]{2,})';
$url1 = '.:;,';
$url2 = 'a-zA-Z0-9%=#$@+?|!&\\/_~\\[\\]\\(\\){}\*\x80-\xFE-';
// Supported link prefixes
$link_prefix = "([\w]+:\/\/|{$this->noword}[Ww][Ww][Ww]\.|^[Ww][Ww][Ww]\.)";
$this->options = $options;
$this->linkref_index = '/\[([^\]#]+)\](:?\s*##str_replacement_(\d+)##)/';
$this->linkref_pattern = '/\[([^\]#]+)\]/';
$this->link_pattern = "/$link_prefix($utf_domain([$url1]*[$url2]+)*)/";
$this->mailto_pattern = "/("
."[-\w!\#\$%&\'*+~\/^`|{}=]+(?:\.[-\w!\#\$%&\'*+~\/^`|{}=]+)*" // local-part
."@$utf_domain" // domain-part
."(\?[$url1$url2]+)?" // e.g. ?subject=test...
.")/";
}
/**
* Add a string to the internal list
*
* @param string String value
*
* @return int Index of value for retrieval
*/
public function add($str)
{
$i = count($this->values);
$this->values[$i] = $str;
return $i;
}
/**
* Build replacement string
*/
public function get_replacement($i)
{
return '##str_replacement_' . $i . '##';
}
/**
* Callback function used to build HTML links around URL strings
*
* @param array Matches result from preg_replace_callback
* @return int Index of saved string value
*/
public function link_callback($matches)
{
$i = -1;
$scheme = strtolower($matches[1]);
if (preg_match('!^(http|ftp|file)s?://!i', $scheme)) {
$url = $matches[1] . $matches[2];
}
else if (preg_match("/^({$this->noword}*)(www\.)$/i", $matches[1], $m)) {
$url = $m[2] . $matches[2];
$url_prefix = 'http://';
$prefix = $m[1];
}
if ($url) {
$suffix = $this->parse_url_brackets($url);
$attrib = (array)$this->options['link_attribs'];
$attrib['href'] = $url_prefix . $url;
$i = $this->add(html::a($attrib, rcube::Q($url)) . $suffix);
$this->urls[$i] = $attrib['href'];
}
// Return valid link for recognized schemes, otherwise
// return the unmodified string for unrecognized schemes.
return $i >= 0 ? $prefix . $this->get_replacement($i) : $matches[0];
}
/**
* Callback to add an entry to the link index
*/
public function linkref_addindex($matches)
{
$key = $matches[1];
$this->linkrefs[$key] = $this->urls[$matches[3]];
return $this->get_replacement($this->add('['.$key.']')) . $matches[2];
}
/**
* Callback to replace link references with real links
*/
public function linkref_callback($matches)
{
$i = 0;
if ($url = $this->linkrefs[$matches[1]]) {
$attrib = (array)$this->options['link_attribs'];
$attrib['href'] = $url;
$i = $this->add(html::a($attrib, rcube::Q($matches[1])));
}
return $i > 0 ? '['.$this->get_replacement($i).']' : $matches[0];
}
/**
* Callback function used to build mailto: links around e-mail strings
*
* @param array Matches result from preg_replace_callback
*
* @return int Index of saved string value
*/
public function mailto_callback($matches)
{
$href = $matches[1];
$suffix = $this->parse_url_brackets($href);
$i = $this->add(html::a('mailto:' . $href, rcube::Q($href)) . $suffix);
return $i >= 0 ? $this->get_replacement($i) : '';
}
/**
* Look up the index from the preg_replace matches array
* and return the substitution value.
*
* @param array Matches result from preg_replace_callback
* @return string Value at index $matches[1]
*/
public function replace_callback($matches)
{
return $this->values[$matches[1]];
}
/**
* Replace all defined (link|mailto) patterns with replacement string
*
* @param string $str Text
*
* @return string Text
*/
public function replace($str)
{
// search for patterns like links and e-mail addresses
$str = preg_replace_callback($this->link_pattern, array($this, 'link_callback'), $str);
$str = preg_replace_callback($this->mailto_pattern, array($this, 'mailto_callback'), $str);
// resolve link references
$str = preg_replace_callback($this->linkref_index, array($this, 'linkref_addindex'), $str);
$str = preg_replace_callback($this->linkref_pattern, array($this, 'linkref_callback'), $str);
return $str;
}
/**
* Replace substituted strings with original values
*/
public function resolve($str)
{
return preg_replace_callback(self::$pattern, array($this, 'replace_callback'), $str);
}
/**
* Fixes bracket characters in URL handling
*/
public static function parse_url_brackets(&$url)
{
// #1487672: special handling of square brackets,
// URL regexp allows [] characters in URL, for example:
// "http://example.com/?a[b]=c". However we need to handle
// properly situation when a bracket is placed at the end
// of the link e.g. "[http://example.com]"
// Yes, this is not perfect handles correctly only paired characters
// but it should work for common cases
if (preg_match('/(\\[|\\])/', $url)) {
$in = false;
for ($i=0, $len=strlen($url); $i<$len; $i++) {
if ($url[$i] == '[') {
if ($in)
break;
$in = true;
}
else if ($url[$i] == ']') {
if (!$in)
break;
$in = false;
}
}
if ($i < $len) {
$suffix = substr($url, $i);
$url = substr($url, 0, $i);
}
}
// Do the same for parentheses
if (preg_match('/(\\(|\\))/', $url)) {
$in = false;
for ($i=0, $len=strlen($url); $i<$len; $i++) {
if ($url[$i] == '(') {
if ($in)
break;
$in = true;
}
else if ($url[$i] == ')') {
if (!$in)
break;
$in = false;
}
}
if ($i < $len) {
$suffix = substr($url, $i);
$url = substr($url, 0, $i);
}
}
return $suffix;
}
}
diff --git a/program/lib/Roundcube/rcube_text2html.php b/program/lib/Roundcube/rcube_text2html.php
index ce8c37799..594de816c 100644
--- a/program/lib/Roundcube/rcube_text2html.php
+++ b/program/lib/Roundcube/rcube_text2html.php
@@ -1,320 +1,321 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2014, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Converts plain text to HTML |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Converts plain text to HTML
*
* @package Framework
* @subpackage Utils
*/
class rcube_text2html
{
/**
* Contains the HTML content after conversion.
*
* @var string $html
*/
protected $html;
/**
* Contains the plain text.
*
* @var string $text
*/
protected $text;
/**
* Configuration
*
* @var array $config
*/
protected $config = array(
// non-breaking space
'space' => "\xC2\xA0",
// enables format=flowed parser
'flowed' => false,
// enables delsp=yes parser
'delsp' => false,
// enables wrapping for non-flowed text
'wrap' => true,
// line-break tag
'break' => "<br>\n",
// prefix and suffix (wrapper element)
'begin' => '<div class="pre">',
'end' => '</div>',
// enables links replacement
'links' => true,
// string replacer class
'replacer' => 'rcube_string_replacer',
// prefix and suffix of unwrappable line
'nobr_start' => '<span style="white-space:nowrap">',
'nobr_end' => '</span>',
);
/**
* Constructor.
*
* If the plain text source string (or file) is supplied, the class
* will instantiate with that source propagated, all that has
* to be done it to call get_html().
*
* @param string $source Plain text
* @param boolean $from_file Indicates $source is a file to pull content from
* @param array $config Class configuration
*/
function __construct($source = '', $from_file = false, $config = array())
{
if (!empty($source)) {
$this->set_text($source, $from_file);
}
if (!empty($config) && is_array($config)) {
$this->config = array_merge($this->config, $config);
}
}
/**
* Loads source text into memory, either from $source string or a file.
*
* @param string $source Plain text
* @param boolean $from_file Indicates $source is a file to pull content from
*/
function set_text($source, $from_file = false)
{
if ($from_file && file_exists($source)) {
$this->text = file_get_contents($source);
}
else {
$this->text = $source;
}
$this->_converted = false;
}
/**
* Returns the HTML content.
*
* @return string HTML content
*/
function get_html()
{
if (!$this->_converted) {
$this->_convert();
}
return $this->html;
}
/**
* Prints the HTML.
*/
function print_html()
{
print $this->get_html();
}
/**
* Workhorse function that does actual conversion (calls _converter() method).
*/
protected function _convert()
{
// Convert TXT to HTML
$this->html = $this->_converter($this->text);
$this->_converted = true;
}
/**
* Workhorse function that does actual conversion.
*
* @param string Plain text
*/
protected function _converter($text)
{
// make links and email-addresses clickable
$attribs = array('link_attribs' => array('rel' => 'noreferrer', 'target' => '_blank'));
$replacer = new $this->config['replacer']($attribs);
if ($this->config['flowed']) {
$flowed_char = 0x01;
$delsp = $this->config['delsp'];
$text = rcube_mime::unfold_flowed($text, chr($flowed_char), $delsp);
}
// search for patterns like links and e-mail addresses and replace with tokens
if ($this->config['links']) {
$text = $replacer->replace($text);
}
// split body into single lines
$text = preg_split('/\r?\n/', $text);
$quote_level = 0;
$last = null;
// wrap quoted lines with <blockquote>
for ($n = 0, $cnt = count($text); $n < $cnt; $n++) {
$flowed = false;
if ($this->config['flowed'] && ord($text[$n][0]) == $flowed_char) {
$flowed = true;
$text[$n] = substr($text[$n], 1);
}
if ($text[$n][0] == '>' && preg_match('/^(>+ {0,1})+/', $text[$n], $regs)) {
$q = substr_count($regs[0], '>');
$text[$n] = substr($text[$n], strlen($regs[0]));
$text[$n] = $this->_convert_line($text[$n], $flowed || $this->config['wrap']);
$_length = strlen(str_replace(' ', '', $text[$n]));
if ($q > $quote_level) {
if ($last !== null) {
$text[$last] .= (!$length ? "\n" : '')
. $replacer->get_replacement($replacer->add(
str_repeat('<blockquote>', $q - $quote_level)))
. $text[$n];
unset($text[$n]);
}
else {
$text[$n] = $replacer->get_replacement($replacer->add(
str_repeat('<blockquote>', $q - $quote_level))) . $text[$n];
$last = $n;
}
}
else if ($q < $quote_level) {
$text[$last] .= (!$length ? "\n" : '')
. $replacer->get_replacement($replacer->add(
str_repeat('</blockquote>', $quote_level - $q)))
. $text[$n];
unset($text[$n]);
}
else {
$last = $n;
}
}
else {
$text[$n] = $this->_convert_line($text[$n], $flowed || $this->config['wrap']);
$q = 0;
$_length = strlen(str_replace(' ', '', $text[$n]));
if ($quote_level > 0) {
$text[$last] .= (!$length ? "\n" : '')
. $replacer->get_replacement($replacer->add(
str_repeat('</blockquote>', $quote_level)))
. $text[$n];
unset($text[$n]);
}
else {
$last = $n;
}
}
$quote_level = $q;
$length = $_length;
}
if ($quote_level > 0) {
$text[$last] .= $replacer->get_replacement($replacer->add(
str_repeat('</blockquote>', $quote_level)));
}
$text = join("\n", $text);
// colorize signature (up to <sig_max_lines> lines)
$len = strlen($text);
$sig_sep = "--" . $this->config['space'] . "\n";
$sig_max_lines = rcube::get_instance()->config->get('sig_max_lines', 15);
while (($sp = strrpos($text, $sig_sep, $sp ? -$len+$sp-1 : 0)) !== false) {
if ($sp == 0 || $text[$sp-1] == "\n") {
// do not touch blocks with more that X lines
if (substr_count($text, "\n", $sp) < $sig_max_lines) {
$text = substr($text, 0, max(0, $sp))
.'<span class="sig">'.substr($text, $sp).'</span>';
}
break;
}
}
// insert url/mailto links and citation tags
$text = $replacer->resolve($text);
// replace line breaks
$text = str_replace("\n", $this->config['break'], $text);
return $this->config['begin'] . $text . $this->config['end'];
}
/**
* Converts spaces in line of text
*/
protected function _convert_line($text, $is_flowed)
{
static $table;
if (empty($table)) {
$table = get_html_translation_table(HTML_SPECIALCHARS);
unset($table['?']);
// replace some whitespace characters
$table["\r"] = '';
$table["\t"] = ' ';
}
// skip signature separator
if ($text == '-- ') {
return '--' . $this->config['space'];
}
// replace HTML special and whitespace characters
$text = strtr($text, $table);
$nbsp = $this->config['space'];
// replace spaces with non-breaking spaces
if ($is_flowed) {
$pos = 0;
$diff = 0;
$len = strlen($nbsp);
$copy = $text;
while (($pos = strpos($text, ' ', $pos)) !== false) {
if ($pos == 0 || $text[$pos-1] == ' ') {
$copy = substr_replace($copy, $nbsp, $pos + $diff, 1);
$diff += $len - 1;
}
$pos++;
}
$text = $copy;
}
// make the whole line non-breakable if needed
else if ($text !== '' && preg_match('/[^a-zA-Z0-9_]/', $text)) {
// use non-breakable spaces to correctly display
// trailing/leading spaces and multi-space inside
$text = str_replace(' ', $nbsp, $text);
// wrap in nobr element, so it's not wrapped on e.g. - or /
$text = $this->config['nobr_start'] . $text . $this->config['nobr_end'];
}
return $text;
}
}
diff --git a/program/lib/Roundcube/rcube_tnef_decoder.php b/program/lib/Roundcube/rcube_tnef_decoder.php
index 12b291344..2cd920c4c 100644
--- a/program/lib/Roundcube/rcube_tnef_decoder.php
+++ b/program/lib/Roundcube/rcube_tnef_decoder.php
@@ -1,752 +1,753 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2017, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| Copyright (C) 2002-2010, The Horde Project (http://www.horde.org/) |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| MS-TNEF format decoder |
+-----------------------------------------------------------------------+
| Author: Jan Schneider <jan@horde.org> |
| Author: Michael Slusarz <slusarz@horde.org> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* MS-TNEF format decoder based on code by:
* Graham Norbury <gnorbury@bondcar.com>
* Original design by:
* Thomas Boll <tb@boll.ch>, Mark Simpson <damned@world.std.com>
*
* @package Framework
* @subpackage Storage
*/
class rcube_tnef_decoder
{
const SIGNATURE = 0x223e9f78;
const LVL_MESSAGE = 0x01;
const LVL_ATTACHMENT = 0x02;
const AFROM = 0x08000;
const ASUBJECT = 0x18004;
const AMESSAGEID = 0x18009;
const AFILENAME = 0x18010;
const APARENTID = 0x1800a;
const ACONVERSATIONID = 0x1800b;
const ABODY = 0x2800c;
const ADATESENT = 0x38005;
const ADATERECEIVED = 0x38006;
const ADATEMODIFIED = 0x38020;
const APRIORITY = 0x4800d;
const AOWNER = 0x60000;
const ASENTFOR = 0x60001;
const ASTATUS = 0x68007;
const ATTACHDATA = 0x6800f;
const ATTACHMETAFILE = 0x68011;
const ATTACHCREATEDATE = 0x38012;
const ARENDDATA = 0x69002;
const AMAPIPROPS = 0x69003;
const ARECIPIENTTABLE = 0x69004;
const AMAPIATTRS = 0x69005;
const AOEMCODEPAGE = 0x69007;
const AORIGINALMCLASS = 0x70006;
const AMCLASS = 0x78008;
const AVERSION = 0x89006;
const MAPI_TYPE_UNSET = 0x0000;
const MAPI_NULL = 0x0001;
const MAPI_SHORT = 0x0002;
const MAPI_INT = 0x0003;
const MAPI_FLOAT = 0x0004;
const MAPI_DOUBLE = 0x0005;
const MAPI_CURRENCY = 0x0006;
const MAPI_APPTIME = 0x0007;
const MAPI_ERROR = 0x000a;
const MAPI_BOOLEAN = 0x000b;
const MAPI_OBJECT = 0x000d;
const MAPI_INT8BYTE = 0x0014;
const MAPI_STRING = 0x001e;
const MAPI_UNICODE_STRING = 0x001f;
const MAPI_SYSTIME = 0x0040;
const MAPI_CLSID = 0x0048;
const MAPI_BINARY = 0x0102;
const MAPI_BODY = 0x1000;
const MAPI_RTF_COMPRESSED = 0x1009;
const MAPI_BODY_HTML = 0x1013;
const MAPI_NATIVE_BODY = 0x1016;
const MAPI_DISPLAY_NAME = 0x3001;
const MAPI_ADDRTYPE = 0x3002;
const MAPI_EMAIL_ADDRESS = 0x3003;
const MAPI_COMMENT = 0x3004;
const MAPI_DEPTH = 0x3005;
const MAPI_PROVIDER_DISPLAY = 0x3006;
const MAPI_CREATION_TIME = 0x3007;
const MAPI_LAST_MODIFICATION_TIME = 0x3008;
const MAPI_RESOURCE_FLAGS = 0x3009;
const MAPI_PROVIDER_DLL_NAME = 0x300A;
const MAPI_SEARCH_KEY = 0x300B;
const MAPI_ATTACHMENT_X400_PARAMETERS = 0x3700;
const MAPI_ATTACH_DATA = 0x3701;
const MAPI_ATTACH_ENCODING = 0x3702;
const MAPI_ATTACH_EXTENSION = 0x3703;
const MAPI_ATTACH_FILENAME = 0x3704;
const MAPI_ATTACH_METHOD = 0x3705;
const MAPI_ATTACH_LONG_FILENAME = 0x3707;
const MAPI_ATTACH_PATHNAME = 0x3708;
const MAPI_ATTACH_RENDERING = 0x3709;
const MAPI_ATTACH_TAG = 0x370A;
const MAPI_RENDERING_POSITION = 0x370B;
const MAPI_ATTACH_TRANSPORT_NAME = 0x370C;
const MAPI_ATTACH_LONG_PATHNAME = 0x370D;
const MAPI_ATTACH_MIME_TAG = 0x370E;
const MAPI_ATTACH_ADDITIONAL_INFO = 0x370F;
const MAPI_ATTACH_MIME_SEQUENCE = 0x3710;
const MAPI_ATTACH_CONTENT_ID = 0x3712;
const MAPI_ATTACH_CONTENT_LOCATION = 0x3713;
const MAPI_ATTACH_FLAGS = 0x3714;
const MAPI_NAMED_TYPE_ID = 0x0000;
const MAPI_NAMED_TYPE_STRING = 0x0001;
const MAPI_NAMED_TYPE_NONE = 0xff;
const MAPI_MV_FLAG = 0x1000;
const RTF_UNCOMPRESSED = 0x414c454d;
const RTF_COMPRESSED = 0x75465a4c;
/**
* Decompress the data.
*
* @param string $data The data to decompress.
*
* @return mixed The decompressed data.
*/
public function decompress($data)
{
$attachments = array();
$message = array();
if ($this->_geti($data, 32) == self::SIGNATURE) {
$this->_geti($data, 16);
// Version
$this->_geti($data, 8); // lvl_message
$this->_geti($data, 32); // idTnefVersion
$this->_getx($data, $this->_geti($data, 32));
$this->_geti($data, 16); // checksum
while (strlen($data) > 0) {
switch ($this->_geti($data, 8)) {
case self::LVL_MESSAGE:
$this->_decodeMessage($data, $message);
break;
case self::LVL_ATTACHMENT:
$this->_decodeAttachment($data, $attachments);
break;
}
}
}
return array(
'message' => $message,
'attachments' => array_reverse($attachments),
);
}
/**
* Pop specified number of bytes from the buffer.
*
* @param string &$data The data string.
* @param integer $bytes How many bytes to retrieve.
*
* @return string Extracted data
*/
protected function _getx(&$data, $bytes)
{
$value = null;
if (strlen($data) >= $bytes) {
$value = substr($data, 0, $bytes);
$data = substr($data, $bytes);
}
return $value;
}
/**
* Pop specified number of bits from the buffer
*
* @param string &$data The data string.
* @param integer $bits How many bits to retrieve.
*
* @return int
*/
protected function _geti(&$data, $bits)
{
$bytes = $bits / 8;
$value = null;
if (strlen($data) >= $bytes) {
$value = ord($data[0]);
if ($bytes >= 2) {
$value += (ord($data[1]) << 8);
}
if ($bytes >= 4) {
$value += (ord($data[2]) << 16) + (ord($data[3]) << 24);
}
$data = substr($data, $bytes);
}
return $value;
}
/**
* Decode a single attribute
*
* @param string &$data The data string.
*/
protected function _decodeAttribute(&$data)
{
// Data.
$value = $this->_getx($data, $this->_geti($data, 32));
// Checksum.
$this->_geti($data, 16);
return $value;
}
/**
* TODO
*
* @param string $data The data string.
* @param array &result TODO
*/
protected function _extractMapiAttributes($data, &$result)
{
// Number of attributes.
$number = $this->_geti($data, 32);
while ((strlen($data) > 0) && $number--) {
$have_mval = false;
$num_mval = 1;
$value = null;
$attr_type = $this->_geti($data, 16);
$attr_name = $this->_geti($data, 16);
if (($attr_type & self::MAPI_MV_FLAG) != 0) {
$have_mval = true;
$attr_type = $attr_type & ~self::MAPI_MV_FLAG;
}
if (($attr_name >= 0x8000) && ($attr_name < 0xFFFE)) {
$this->_getx($data, 16);
$named_type = $this->_geti($data, 32);
switch ($named_type) {
case self::MAPI_NAMED_TYPE_ID:
$attr_name = $this->_geti($data, 32);
break;
case self::MAPI_NAMED_TYPE_STRING:
$attr_name = 0x9999;
$idlen = $this->_geti($data, 32);
$name = $this->_getx($data, $idlen + ((4 - ($idlen % 4)) % 4));
// $name = $this->convertString(substr($name, 0, $idlen));
break;
case self::MAPI_NAMED_TYPE_NONE:
default:
continue 2;
}
}
if ($have_mval) {
$num_mval = $this->_geti($data, 32);
}
switch ($attr_type) {
case self::MAPI_NULL:
case self::MAPI_TYPE_UNSET:
break;
case self::MAPI_SHORT:
$value = $this->_geti($data, 16);
$this->_geti($data, 16);
break;
case self::MAPI_INT:
case self::MAPI_BOOLEAN:
for ($i = 0; $i < $num_mval; $i++) {
$value = $this->_geti($data, 32);
}
break;
case self::MAPI_FLOAT:
case self::MAPI_ERROR:
$value = $this->_getx($data, 4);
break;
case self::MAPI_DOUBLE:
case self::MAPI_APPTIME:
case self::MAPI_CURRENCY:
case self::MAPI_INT8BYTE:
case self::MAPI_SYSTIME:
$value = $this->_getx($data, 8);
break;
case self::MAPI_STRING:
case self::MAPI_UNICODE_STRING:
case self::MAPI_BINARY:
case self::MAPI_OBJECT:
$num_vals = $have_mval ? $num_mval : $this->_geti($data, 32);
for ($i = 0; $i < $num_vals; $i++) {
$length = $this->_geti($data, 32);
// Pad to next 4 byte boundary.
$datalen = $length + ((4 - ($length % 4)) % 4);
// Read and truncate to length.
$value = $this->_getx($data, $datalen);
}
if ($attr_type == self::MAPI_UNICODE_STRING) {
$value = $this->convertString($value);
}
break;
}
// Store any interesting attributes.
switch ($attr_name) {
case self::MAPI_RTF_COMPRESSED:
$result['type'] = 'application';
$result['subtype'] = 'rtf';
$result['name'] = ($result['name'] ?: 'Untitled') . '.rtf';
$result['stream'] = $this->_decodeRTF($value);
$result['size'] = strlen($result['stream']);
break;
case self::MAPI_BODY:
case self::MAPI_BODY_HTML:
$result['type'] = 'text';
$result['subtype'] = $attr_name == self::MAPI_BODY ? 'plain' : 'html';
$result['name'] = ($result['name'] ?: 'Untitled') . ($attr_name == self::MAPI_BODY ? '.txt' : '.html');
$result['stream'] = $value;
$result['size'] = strlen($value);
break;
case self::MAPI_ATTACH_LONG_FILENAME:
// Used in preference to AFILENAME value.
$result['name'] = trim(preg_replace('/.*[\/](.*)$/', '\1', $value));
break;
case self::MAPI_ATTACH_MIME_TAG:
// Is this ever set, and what is format?
$value = explode('/', $value);
$result['type'] = $value[0];
$result['subtype'] = $value[1];
break;
case self::MAPI_ATTACH_DATA:
$this->_getx($value, 16);
$att = new rcube_tnef_decoder;
$res = $att->decompress($value);
$result = array_merge($result, $res['message']);
break;
}
}
}
/**
* Decodes TNEF message attributes
*
* @param string &$data The data string.
* @param array &$message Message data
*/
protected function _decodeMessage(&$data, &$message)
{
$attribute = $this->_geti($data, 32);
$value = $this->_decodeAttribute($data);
switch ($attribute) {
case self::AOEMCODEPAGE:
// Find codepage of the message
$value = unpack('V', $value);
$this->codepage = $value[1];
break;
case self::AMCLASS:
$value = trim(str_replace('Microsoft Mail v3.0 ', '', $value));
// Normal message will be that with prefix 'IPM.Microsoft Mail.
break;
case self::ASUBJECT:
$message['name'] = $value;
break;
case self::AMAPIPROPS:
$this->_extractMapiAttributes($value, $message);
break;
}
}
/**
* Decodes TNEF attachment attributes
*
* @param string &$data The data string.
* @param array &$attachment Attachments data
*/
protected function _decodeAttachment(&$data, &$attachment)
{
$attribute = $this->_geti($data, 32);
$size = $this->_geti($data, 32);
$value = $this->_getx($data, $size);
$this->_geti($data, 16); // checksum
switch ($attribute) {
case self::ARENDDATA:
// Add a new default data block to hold details of this
// attachment. Reverse order is easier to handle later!
array_unshift($attachment, array(
'type' => 'application',
'subtype' => 'octet-stream',
'name' => 'unknown',
'stream' => ''
));
break;
case self::AFILENAME:
$value = $this->convertString($value, true);
// Strip path
$attachment[0]['name'] = trim(preg_replace('/.*[\/](.*)$/', '\1', $value));
break;
case self::ATTACHDATA:
// The attachment itself
$attachment[0]['size'] = $size;
$attachment[0]['stream'] = $value;
break;
case self::AMAPIATTRS:
$this->_extractMapiAttributes($value, $attachment[0]);
break;
}
}
/**
* Convert string value to system charset according to defined codepage
*/
protected function convertString($str, $use_codepage = false)
{
if ($use_codepage && $this->codepage
&& ($charset = rcube_charset::$windows_codepages[$this->codepage])
) {
$str = rcube_charset::convert($str, $charset, RCUBE_CHARSET);
}
else if (($pos = strpos($str, "\0")) !== false && $pos != strlen($str)-1) {
$str = rcube_charset::convert($str, 'UTF-16LE', RCUBE_CHARSET);
}
return trim($str);
}
/**
* Decodes TNEF RTF
*/
protected function _decodeRTF($data)
{
$c_size = $this->_geti($data, 32);
$size = $this->_geti($data, 32);
$magic = $this->_geti($data, 32);
$crc = $this->_geti($data, 32);
if ($magic == self::RTF_COMPRESSED) {
$data = $this->_decompressRTF($data, $size);
}
return $data;
}
/**
* Decompress compressed RTF. Logic taken from Horde.
*/
protected function _decompressRTF($data, $size)
{
$in = $out = $flags = $flag_count = 0;
$uncomp = '';
$preload = "{\\rtf1\\ansi\\mac\\deff0\\deftab720{\\fonttbl;}{\\f0\\fnil \\froman \\fswiss \\fmodern \\fscript \\fdecor MS Sans SerifSymbolArialTimes New RomanCourier{\\colortbl\\red0\\green0\\blue0\n\r\\par \\pard\\plain\\f0\\fs20\\b\\i\\u\\tab\\tx";
$length_preload = strlen($preload);
for ($cnt = 0; $cnt < $length_preload; $cnt++) {
$uncomp .= $preload{$cnt};
++$out;
}
while ($out < ($size + $length_preload)) {
if (($flag_count++ % 8) == 0) {
$flags = ord($data{$in++});
}
else {
$flags = $flags >> 1;
}
if (($flags & 1) != 0) {
$offset = ord($data{$in++});
$length = ord($data{$in++});
$offset = ($offset << 4) | ($length >> 4);
$length = ($length & 0xF) + 2;
$offset = ((int)($out / 4096)) * 4096 + $offset;
if ($offset >= $out) {
$offset -= 4096;
}
$end = $offset + $length;
while ($offset < $end) {
$uncomp.= $uncomp[$offset++];
++$out;
}
}
else {
$uncomp .= $data{$in++};
++$out;
}
}
return substr($uncomp, $length_preload);
}
/**
* Parse RTF data and return the best plaintext representation we can.
* Adapted from:
* http://webcheatsheet.com/php/reading_the_clean_text_from_rtf.php
*
* @param string $text The RTF (uncompressed) text.
*
* @return string The plain text.
*/
public static function rtf2text($text)
{
$document = '';
$stack = array();
$j = -1;
// Read the data character-by- character…
for ($i = 0, $len = strlen($text); $i < $len; $i++) {
$c = $text[$i];
switch ($c) {
case "\\":
// Key Word
$nextChar = $text[$i + 1];
// If it is another backslash or nonbreaking space or hyphen,
// then the character is plain text and add it to the output stream.
if ($nextChar == "\\" && self::_rtfIsPlain($stack[$j])) {
$document .= "\\";
}
elseif ($nextChar == '~' && self::_rtfIsPlain($stack[$j])) {
$document .= ' ';
}
elseif ($nextChar == '_' && self::_rtfIsPlain($stack[$j])) {
$document .= '-';
}
elseif ($nextChar == '*') {
// Add to the stack.
$stack[$j]['*'] = true;
}
elseif ($nextChar == "'") {
// If it is a single quote, read next two characters that
// are the hexadecimal notation of a character we should add
// to the output stream.
$hex = substr($text, $i + 2, 2);
if (self::_rtfIsPlain($stack[$j])) {
$document .= html_entity_decode('&#' . hexdec($hex) .';');
}
//Shift the pointer.
$i += 2;
}
elseif ($nextChar >= 'a' && $nextChar <= 'z' || $nextChar >= 'A' && $nextChar <= 'Z') {
// Since, we’ve found the alphabetic character, the next
// characters are control words and, possibly, some digit
// parameter.
$word = '';
$param = null;
// Start reading characters after the backslash.
for ($k = $i + 1, $m = 0; $k < strlen($text); $k++, $m++) {
$nextChar = $text[$k];
// If the current character is a letter and there were
// no digits before it, then we’re still reading the
// control word. If there were digits, we should stop
// since we reach the end of the control word.
if ($nextChar >= 'a' && $nextChar <= 'z'
|| $nextChar >= 'A' && $nextChar <= 'Z') {
if (!empty($param)) {
break;
}
$word .= $nextChar;
}
elseif ($nextChar >= '0' && $nextChar <= '9') {
// If it is a digit, store the parameter.
$param .= $nextChar;
}
elseif ($nextChar == '-') {
// Since minus sign may occur only before a digit
// parameter, check whether $param is empty.
// Otherwise, we reach the end of the control word.
if (!empty($param)) {
break;
}
$param .= $nextChar;
}
else {
break;
}
}
// Shift the pointer on the number of read characters.
$i += $m - 1;
// Start analyzing.We are interested mostly in control words
$toText = '';
switch (strtolower($word)) {
// If the control word is "u", then its parameter is
// the decimal notation of the Unicode character that
// should be added to the output stream. We need to
// check whether the stack contains \ucN control word.
// If it does, we should remove the N characters from
// the output stream.
case 'u':
$toText .= html_entity_decode('&#x' . dechex($param) .';');
$ucDelta = @$stack[$j]['uc'];
if ($ucDelta > 0) {
$i += $ucDelta;
}
break;
case 'par':
case 'page':
case 'column':
case 'line':
case 'lbr':
$toText .= "\n";
break;
case 'emspace':
case 'enspace':
case 'qmspace':
$toText .= ' ';
break;
case 'tab':
$toText .= "\t";
break;
case 'chdate':
$toText .= date('m.d.Y');
break;
case 'chdpl':
$toText .= date('l, j F Y');
break;
case 'chdpa':
$toText .= date('D, j M Y');
break;
case 'chtime':
$toText .= date('H:i:s');
break;
case 'emdash':
$toText .= html_entity_decode('&mdash;');
break;
case 'endash':
$toText .= html_entity_decode('&ndash;');
break;
case 'bullet':
$toText .= html_entity_decode('&#149;');
break;
case 'lquote':
$toText .= html_entity_decode('&lsquo;');
break;
case 'rquote':
$toText .= html_entity_decode('&rsquo;');
break;
case 'ldblquote':
$toText .= html_entity_decode('&laquo;');
break;
case 'rdblquote':
$toText .= html_entity_decode('&raquo;');
break;
default:
$stack[$j][strtolower($word)] = empty($param) ? true : $param;
break;
}
// Add data to the output stream if required.
if (self::_rtfIsPlain($stack[$j])) {
$document .= $toText;
}
}
$i++;
break;
case '{':
// New subgroup starts, add new stack element and write the data
// from previous stack element to it.
if (!empty($stack[$j])) {
array_push($stack, $stack[$j++]);
}
else {
$j++;
}
break;
case '}':
array_pop($stack);
$j--;
break;
case '\0':
case '\r':
case '\f':
case '\n':
// Junk
break;
default:
// Add other data to the output stream if required.
if (!empty($stack[$j]) && self::_rtfIsPlain($stack[$j])) {
$document .= $c;
}
break;
}
}
return $document;
}
protected static function _rtfIsPlain($s)
{
$notPlain = array('*', 'fonttbl', 'colortbl', 'datastore', 'themedata', 'stylesheet');
for ($i = 0; $i < count($notPlain); $i++) {
if (!empty($s[$notPlain[$i]])) {
return false;
}
}
return true;
}
}
diff --git a/program/lib/Roundcube/rcube_user.php b/program/lib/Roundcube/rcube_user.php
index caa2e086e..931be420f 100644
--- a/program/lib/Roundcube/rcube_user.php
+++ b/program/lib/Roundcube/rcube_user.php
@@ -1,866 +1,867 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2012, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| This class represents a system user linked and provides access |
| to the related database records. |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class representing a system user
*
* @package Framework
* @subpackage Core
*/
class rcube_user
{
public $ID;
public $data;
public $language;
public $prefs;
/**
* Holds database connection.
*
* @var rcube_db
*/
private $db;
/**
* Framework object.
*
* @var rcube
*/
private $rc;
/**
* Internal identities cache
*
* @var array
*/
private $identities = array();
/**
* Internal emails cache
*
* @var array
*/
private $emails;
const SEARCH_ADDRESSBOOK = 1;
const SEARCH_MAIL = 2;
/**
* Object constructor
*
* @param int $id User id
* @param array $sql_arr SQL result set
*/
function __construct($id = null, $sql_arr = null)
{
$this->rc = rcube::get_instance();
$this->db = $this->rc->get_dbh();
if ($id && !$sql_arr) {
$sql_result = $this->db->query(
"SELECT * FROM " . $this->db->table_name('users', true)
. " WHERE `user_id` = ?", $id);
$sql_arr = $this->db->fetch_assoc($sql_result);
}
if (!empty($sql_arr)) {
$this->ID = $sql_arr['user_id'];
$this->data = $sql_arr;
$this->language = $sql_arr['language'];
}
}
/**
* Build a user name string (as e-mail address)
*
* @param string $part Username part (empty or 'local' or 'domain', 'mail')
*
* @return string Full user name or its part
*/
function get_username($part = null)
{
if ($this->data['username']) {
// return real name
if (!$part) {
return $this->data['username'];
}
list($local, $domain) = explode('@', $this->data['username']);
// at least we should always have the local part
if ($part == 'local') {
return $local;
}
// if no domain was provided...
if (empty($domain)) {
$domain = $this->rc->config->mail_domain($this->data['mail_host']);
}
if ($part == 'domain') {
return $domain;
}
if (!empty($domain)) {
return $local . '@' . $domain;
}
return $local;
}
}
/**
* Get the preferences saved for this user
*
* @return array Hash array with prefs
*/
function get_prefs()
{
if (isset($this->prefs)) {
return $this->prefs;
}
$this->prefs = array();
if (!empty($this->language))
$this->prefs['language'] = $this->language;
if ($this->ID) {
// Preferences from session (write-master is unavailable)
if (!empty($_SESSION['preferences'])) {
// Check last write attempt time, try to write again (every 5 minutes)
if ($_SESSION['preferences_time'] < time() - 5 * 60) {
$saved_prefs = unserialize($_SESSION['preferences']);
$this->rc->session->remove('preferences');
$this->rc->session->remove('preferences_time');
$this->save_prefs($saved_prefs);
}
else {
$this->data['preferences'] = $_SESSION['preferences'];
}
}
if ($this->data['preferences']) {
$this->prefs += (array)unserialize($this->data['preferences']);
}
}
return $this->prefs;
}
/**
* Write the given user prefs to the user's record
*
* @param array $a_user_prefs User prefs to save
* @param bool $no_session Simplified language/preferences handling
*
* @return boolean True on success, False on failure
*/
function save_prefs($a_user_prefs, $no_session = false)
{
if (!$this->ID) {
return false;
}
$config = $this->rc->config;
$transient = $config->transient_options();
$a_user_prefs = array_diff_key($a_user_prefs, array_flip($transient));
if (empty($a_user_prefs)) {
return true;
}
$plugin = $this->rc->plugins->exec_hook('preferences_update', array(
'userid' => $this->ID,
'prefs' => $a_user_prefs,
'old' => (array)$this->get_prefs()
));
if (!empty($plugin['abort'])) {
return false;
}
$a_user_prefs = $plugin['prefs'];
$old_prefs = $plugin['old'];
$defaults = $config->all();
// merge (partial) prefs array with existing settings
$this->prefs = $save_prefs = $a_user_prefs + $old_prefs;
unset($save_prefs['language']);
// don't save prefs with default values if they haven't been changed yet
// Warning: we use result of rcube_config::all() here instead of just get() (#5782)
foreach ($a_user_prefs as $key => $value) {
if ($value === null || (!isset($old_prefs[$key]) && $value === $defaults[$key])) {
unset($save_prefs[$key]);
}
}
$save_prefs = serialize($save_prefs);
if (!$no_session) {
$this->language = $_SESSION['language'];
}
$this->db->query(
"UPDATE ".$this->db->table_name('users', true).
" SET `preferences` = ?, `language` = ?".
" WHERE `user_id` = ?",
$save_prefs,
$this->language,
$this->ID);
// Update success
if ($this->db->affected_rows() !== false) {
$this->data['preferences'] = $save_prefs;
if (!$no_session) {
$config->set_user_prefs($this->prefs);
if (isset($_SESSION['preferences'])) {
$this->rc->session->remove('preferences');
$this->rc->session->remove('preferences_time');
}
}
return true;
}
// Update error, but we are using replication (we have read-only DB connection)
// and we are storing session not in the SQL database
// we can store preferences in session and try to write later (see get_prefs())
else if (!$no_session && $this->db->is_replicated()
&& $config->get('session_storage', 'db') != 'db'
) {
$_SESSION['preferences'] = $save_prefs;
$_SESSION['preferences_time'] = time();
$config->set_user_prefs($this->prefs);
$this->data['preferences'] = $save_prefs;
}
return false;
}
/**
* Generate a unique hash to identify this user whith
*/
function get_hash()
{
$prefs = $this->get_prefs();
// generate a random hash and store it in user prefs
if (empty($prefs['client_hash'])) {
$prefs['client_hash'] = rcube_utils::random_bytes(16);
$this->save_prefs(array('client_hash' => $prefs['client_hash']));
}
return $prefs['client_hash'];
}
/**
* Return a list of all user emails (from identities)
*
* @param bool $default Return only default identity
*
* @return array List of emails (identity_id, name, email)
*/
function list_emails($default = false)
{
if ($this->emails === null) {
$this->emails = array();
$sql_result = $this->db->query(
"SELECT `identity_id`, `name`, `email`"
." FROM " . $this->db->table_name('identities', true)
." WHERE `user_id` = ? AND `del` <> 1"
." ORDER BY `standard` DESC, `name` ASC, `email` ASC, `identity_id` ASC",
$this->ID);
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$this->emails[] = $sql_arr;
}
}
return $default ? $this->emails[0] : $this->emails;
}
/**
* Get default identity of this user
*
* @param int $id Identity ID. If empty, the default identity is returned
*
* @return array Hash array with all cols of the identity record
*/
function get_identity($id = null)
{
$id = (int)$id;
// cache identities for better performance
if (!array_key_exists($id, $this->identities)) {
$result = $this->list_identities($id ? "AND `identity_id` = $id" : '');
$this->identities[$id] = $result[0];
}
return $this->identities[$id];
}
/**
* Return a list of all identities linked with this user
*
* @param string $sql_add Optional WHERE clauses
* @param bool $formatted Format identity email and name
*
* @return array List of identities
*/
function list_identities($sql_add = '', $formatted = false)
{
$result = array();
$sql_result = $this->db->query(
"SELECT * FROM ".$this->db->table_name('identities', true).
" WHERE `del` <> 1 AND `user_id` = ?".
($sql_add ? " ".$sql_add : "").
" ORDER BY `standard` DESC, `name` ASC, `email` ASC, `identity_id` ASC",
$this->ID);
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
if ($formatted) {
$ascii_email = format_email($sql_arr['email']);
$utf8_email = format_email(rcube_utils::idn_to_utf8($ascii_email));
$sql_arr['email_ascii'] = $ascii_email;
$sql_arr['email'] = $utf8_email;
$sql_arr['ident'] = format_email_recipient($ascii_email, $sql_arr['name']);
}
$result[] = $sql_arr;
}
return $result;
}
/**
* Update a specific identity record
*
* @param int $iid Identity ID
* @param array $data Hash array with col->value pairs to save
*
* @return boolean True if saved successfully, false if nothing changed
*/
function update_identity($iid, $data)
{
if (!$this->ID) {
return false;
}
$query_cols = $query_params = array();
foreach ((array)$data as $col => $value) {
$query_cols[] = $this->db->quote_identifier($col) . ' = ?';
$query_params[] = $value;
}
$query_params[] = $iid;
$query_params[] = $this->ID;
$sql = "UPDATE ".$this->db->table_name('identities', true).
" SET `changed` = ".$this->db->now().", ".join(', ', $query_cols).
" WHERE `identity_id` = ?".
" AND `user_id` = ?".
" AND `del` <> 1";
call_user_func_array(array($this->db, 'query'),
array_merge(array($sql), $query_params));
// clear the cache
$this->identities = array();
$this->emails = null;
return $this->db->affected_rows() > 0;
}
/**
* Create a new identity record linked with this user
*
* @param array $data Hash array with col->value pairs to save
*
* @return int The inserted identity ID or false on error
*/
function insert_identity($data)
{
if (!$this->ID) {
return false;
}
unset($data['user_id']);
$insert_cols = array();
$insert_values = array();
foreach ((array)$data as $col => $value) {
$insert_cols[] = $this->db->quote_identifier($col);
$insert_values[] = $value;
}
$insert_cols[] = $this->db->quote_identifier('user_id');
$insert_values[] = $this->ID;
$sql = "INSERT INTO ".$this->db->table_name('identities', true).
" (`changed`, ".join(', ', $insert_cols).")".
" VALUES (".$this->db->now().", ".join(', ', array_pad(array(), count($insert_values), '?')).")";
$insert = $this->db->query($sql, $insert_values);
// clear the cache
$this->identities = array();
$this->emails = null;
return $this->db->affected_rows($insert) ? $this->db->insert_id('identities') : false;
}
/**
* Mark the given identity as deleted
*
* @param int $iid Identity ID
*
* @return boolean True if deleted successfully, false if nothing changed
*/
function delete_identity($iid)
{
if (!$this->ID) {
return false;
}
$sql_result = $this->db->query(
"SELECT count(*) AS ident_count FROM ".$this->db->table_name('identities', true).
" WHERE `user_id` = ? AND `del` <> 1",
$this->ID);
$sql_arr = $this->db->fetch_assoc($sql_result);
// we'll not delete last identity
if ($sql_arr['ident_count'] <= 1) {
return false;
}
$this->db->query(
"UPDATE ".$this->db->table_name('identities', true).
" SET `del` = 1, `changed` = ".$this->db->now().
" WHERE `user_id` = ?".
" AND `identity_id` = ?",
$this->ID,
$iid);
// clear the cache
$this->identities = array();
$this->emails = null;
return $this->db->affected_rows() > 0;
}
/**
* Make this identity the default one for this user
*
* @param int $iid The identity ID
*/
function set_default($iid)
{
if ($this->ID && $iid) {
$this->db->query(
"UPDATE ".$this->db->table_name('identities', true).
" SET `standard` = '0'".
" WHERE `user_id` = ? AND `identity_id` <> ?",
$this->ID,
$iid);
unset($this->identities[0]);
}
}
/**
* Update user's last_login timestamp
*/
function touch()
{
if ($this->ID) {
$this->db->query(
"UPDATE ".$this->db->table_name('users', true).
" SET `last_login` = ".$this->db->now().
" WHERE `user_id` = ?",
$this->ID);
}
}
/**
* Update user's failed_login timestamp and counter
*/
function failed_login()
{
if ($this->ID && ($rate = (int) $this->rc->config->get('login_rate_limit', 3))) {
if (empty($this->data['failed_login'])) {
$failed_login = new DateTime('now');
$counter = 1;
}
else {
$failed_login = new DateTime($this->data['failed_login']);
$threshold = new DateTime('- 60 seconds');
if ($failed_login < $threshold) {
$failed_login = new DateTime('now');
$counter = 1;
}
}
$this->db->query(
"UPDATE " . $this->db->table_name('users', true)
. " SET `failed_login` = ?"
. ", `failed_login_counter` = " . ($counter ?: "`failed_login_counter` + 1")
. " WHERE `user_id` = ?",
$failed_login, $this->ID);
}
}
/**
* Checks if the account is locked, e.g. as a result of brute-force prevention
*/
function is_locked()
{
if (empty($this->data['failed_login'])) {
return false;
}
if ($rate = (int) $this->rc->config->get('login_rate_limit', 3)) {
$last_failed = new DateTime($this->data['failed_login']);
$threshold = new DateTime('- 60 seconds');
if ($last_failed > $threshold && $this->data['failed_login_counter'] >= $rate) {
return true;
}
}
return false;
}
/**
* Clear the saved object state
*/
function reset()
{
$this->ID = null;
$this->data = null;
}
/**
* Find a user record matching the given name and host
*
* @param string $user IMAP user name
* @param string $host IMAP host name
*
* @return rcube_user New user instance
*/
static function query($user, $host)
{
$dbh = rcube::get_instance()->get_dbh();
$config = rcube::get_instance()->config;
// query for matching user name
$sql_result = $dbh->query("SELECT * FROM " . $dbh->table_name('users', true)
." WHERE `mail_host` = ? AND `username` = ?", $host, $user);
$sql_arr = $dbh->fetch_assoc($sql_result);
// username not found, try aliases from identities
if (empty($sql_arr) && $config->get('user_aliases') && strpos($user, '@')) {
$sql_result = $dbh->limitquery("SELECT u.*"
." FROM " . $dbh->table_name('users', true) . " u"
." JOIN " . $dbh->table_name('identities', true) . " i ON (i.`user_id` = u.`user_id`)"
." WHERE `email` = ? AND `del` <> 1", 0, 1, $user);
$sql_arr = $dbh->fetch_assoc($sql_result);
}
// user already registered -> overwrite username
if ($sql_arr) {
return new rcube_user($sql_arr['user_id'], $sql_arr);
}
}
/**
* Create a new user record and return a rcube_user instance
*
* @param string $user IMAP user name
* @param string $host IMAP host
*
* @return rcube_user New user instance
*/
static function create($user, $host)
{
$user_name = '';
$user_email = '';
$rcube = rcube::get_instance();
$dbh = $rcube->get_dbh();
// try to resolve user in virtuser table and file
if ($email_list = self::user2email($user, false, true)) {
$user_email = is_array($email_list[0]) ? $email_list[0]['email'] : $email_list[0];
}
$data = $rcube->plugins->exec_hook('user_create', array(
'host' => $host,
'user' => $user,
'user_name' => $user_name,
'user_email' => $user_email,
'email_list' => $email_list,
'language' => $_SESSION['language'],
));
// plugin aborted this operation
if ($data['abort']) {
return;
}
$insert = $dbh->query(
"INSERT INTO ".$dbh->table_name('users', true).
" (`created`, `last_login`, `username`, `mail_host`, `language`)".
" VALUES (".$dbh->now().", ".$dbh->now().", ?, ?, ?)",
$data['user'],
$data['host'],
$data['language']);
if ($dbh->affected_rows($insert) && ($user_id = $dbh->insert_id('users'))) {
// create rcube_user instance to make plugin hooks work
$user_instance = new rcube_user($user_id, array(
'user_id' => $user_id,
'username' => $data['user'],
'mail_host' => $data['host'],
'language' => $data['language'],
));
$rcube->user = $user_instance;
$mail_domain = $rcube->config->mail_domain($data['host']);
$user_name = $data['user_name'];
$user_email = $data['user_email'];
$email_list = $data['email_list'];
if (empty($email_list)) {
if (empty($user_email)) {
$user_email = strpos($data['user'], '@') ? $user : sprintf('%s@%s', $data['user'], $mail_domain);
}
$email_list[] = $user_email;
}
// identities_level check
else if (count($email_list) > 1 && $rcube->config->get('identities_level', 0) > 1) {
$email_list = array($email_list[0]);
}
if (empty($user_name)) {
$user_name = $data['user'];
}
// create new identities records
$standard = 1;
foreach ($email_list as $row) {
$record = array();
if (is_array($row)) {
if (empty($row['email'])) {
continue;
}
$record = $row;
}
else {
$record['email'] = $row;
}
if (empty($record['name'])) {
$record['name'] = $user_name != $record['email'] ? $user_name : '';
}
$record['user_id'] = $user_id;
$record['standard'] = $standard;
$plugin = $rcube->plugins->exec_hook('identity_create',
array('login' => true, 'record' => $record));
if (!$plugin['abort'] && $plugin['record']['email']) {
$rcube->user->insert_identity($plugin['record']);
}
$standard = 0;
}
}
else {
rcube::raise_error(array(
'code' => 500,
'type' => 'php',
'line' => __LINE__,
'file' => __FILE__,
'message' => "Failed to create new user"), true, false);
}
return $user_id ? $user_instance : null;
}
/**
* Resolve username using a virtuser plugins
*
* @param string $email E-mail address to resolve
*
* @return string Resolved IMAP username
*/
static function email2user($email)
{
$rcube = rcube::get_instance();
$plugin = $rcube->plugins->exec_hook('email2user',
array('email' => $email, 'user' => null));
return $plugin['user'];
}
/**
* Resolve e-mail address from virtuser plugins
*
* @param string $user User name
* @param boolean $first If true returns first found entry
* @param boolean $extended If true returns email as array (email and name for identity)
*
* @return mixed Resolved e-mail address string or array of strings
*/
static function user2email($user, $first=true, $extended=false)
{
$rcube = rcube::get_instance();
$plugin = $rcube->plugins->exec_hook('user2email',
array('email' => null, 'user' => $user,
'first' => $first, 'extended' => $extended));
return empty($plugin['email']) ? null : $plugin['email'];
}
/**
* Return a list of saved searches linked with this user
*
* @param int $type Search type
*
* @return array List of saved searches indexed by search ID
*/
function list_searches($type)
{
$plugin = $this->rc->plugins->exec_hook('saved_search_list', array('type' => $type));
if ($plugin['abort']) {
return (array) $plugin['result'];
}
$result = array();
$sql_result = $this->db->query(
"SELECT `search_id` AS id, `name`"
." FROM ".$this->db->table_name('searches', true)
." WHERE `user_id` = ? AND `type` = ?"
." ORDER BY `name`",
(int) $this->ID, (int) $type);
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$sql_arr['data'] = unserialize($sql_arr['data']);
$result[$sql_arr['id']] = $sql_arr;
}
return $result;
}
/**
* Return saved search data.
*
* @param int $id Row identifier
*
* @return array Data
*/
function get_search($id)
{
$plugin = $this->rc->plugins->exec_hook('saved_search_get', array('id' => $id));
if ($plugin['abort']) {
return $plugin['result'];
}
$sql_result = $this->db->query(
"SELECT `name`, `data`, `type`"
. " FROM ".$this->db->table_name('searches', true)
. " WHERE `user_id` = ?"
." AND `search_id` = ?",
(int) $this->ID, (int) $id);
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
return array(
'id' => $id,
'name' => $sql_arr['name'],
'type' => $sql_arr['type'],
'data' => unserialize($sql_arr['data']),
);
}
}
/**
* Deletes given saved search record
*
* @param int $sid Search ID
*
* @return boolean True if deleted successfully, false if nothing changed
*/
function delete_search($sid)
{
if (!$this->ID) {
return false;
}
$this->db->query(
"DELETE FROM ".$this->db->table_name('searches', true)
." WHERE `user_id` = ?"
." AND `search_id` = ?",
(int) $this->ID, $sid);
return $this->db->affected_rows() > 0;
}
/**
* Create a new saved search record linked with this user
*
* @param array $data Hash array with col->value pairs to save
*
* @return int The inserted search ID or false on error
*/
function insert_search($data)
{
if (!$this->ID) {
return false;
}
$insert_cols[] = 'user_id';
$insert_values[] = (int) $this->ID;
$insert_cols[] = $this->db->quote_identifier('type');
$insert_values[] = (int) $data['type'];
$insert_cols[] = $this->db->quote_identifier('name');
$insert_values[] = $data['name'];
$insert_cols[] = $this->db->quote_identifier('data');
$insert_values[] = serialize($data['data']);
$sql = "INSERT INTO ".$this->db->table_name('searches', true)
." (".join(', ', $insert_cols).")"
." VALUES (".join(', ', array_pad(array(), count($insert_values), '?')).")";
$insert = $this->db->query($sql, $insert_values);
return $this->db->affected_rows($insert) ? $this->db->insert_id('searches') : false;
}
}
diff --git a/program/lib/Roundcube/rcube_utils.php b/program/lib/Roundcube/rcube_utils.php
index 95e2897cb..e86f3c780 100644
--- a/program/lib/Roundcube/rcube_utils.php
+++ b/program/lib/Roundcube/rcube_utils.php
@@ -1,1425 +1,1426 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2012, The Roundcube Dev Team |
- | Copyright (C) 2011-2012, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Utility class providing common functions |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Utility class providing common functions
*
* @package Framework
* @subpackage Utils
*/
class rcube_utils
{
// define constants for input reading
const INPUT_GET = 1;
const INPUT_POST = 2;
const INPUT_COOKIE = 4;
const INPUT_GP = 3; // GET + POST
const INPUT_GPC = 7; // GET + POST + COOKIE
/**
* Helper method to set a cookie with the current path and host settings
*
* @param string $name Cookie name
* @param string $value Cookie value
* @param int $exp Expiration time
* @param bool $http_only HTTP Only
*/
public static function setcookie($name, $value, $exp = 0, $http_only = true)
{
if (headers_sent()) {
return;
}
$cookie = session_get_cookie_params();
$secure = $cookie['secure'] || self::https_check();
setcookie($name, $value, $exp, $cookie['path'], $cookie['domain'], $secure, $http_only);
}
/**
* E-mail address validation.
*
* @param string $email Email address
* @param boolean $dns_check True to check dns
*
* @return boolean True on success, False if address is invalid
*/
public static function check_email($email, $dns_check=true)
{
// Check for invalid (control) characters
if (preg_match('/\p{Cc}/u', $email)) {
return false;
}
// Check for length limit specified by RFC 5321 (#1486453)
if (strlen($email) > 254) {
return false;
}
$pos = strrpos($email, '@');
if (!$pos) {
return false;
}
$domain_part = substr($email, $pos + 1);
$local_part = substr($email, 0, $pos);
// quoted-string, make sure all backslashes and quotes are
// escaped
if (substr($local_part,0,1) == '"') {
$local_quoted = preg_replace('/\\\\(\\\\|\")/','', substr($local_part, 1, -1));
if (preg_match('/\\\\|"/', $local_quoted)) {
return false;
}
}
// dot-atom portion, make sure there's no prohibited characters
else if (preg_match('/(^\.|\.\.|\.$)/', $local_part)
|| preg_match('/[\\ ",:;<>@]/', $local_part)
) {
return false;
}
// Validate domain part
if (preg_match('/^\[((IPv6:[0-9a-f:.]+)|([0-9.]+))\]$/i', $domain_part, $matches)) {
return self::check_ip(preg_replace('/^IPv6:/i', '', $matches[1])); // valid IPv4 or IPv6 address
}
else {
// If not an IP address
$domain_array = explode('.', $domain_part);
// Not enough parts to be a valid domain
if (count($domain_array) < 2) {
return false;
}
foreach ($domain_array as $part) {
if (!preg_match('/^((xn--)?([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]))$/', $part)) {
return false;
}
}
// last domain part
$last_part = array_pop($domain_array);
if (strpos($last_part, 'xn--') !== 0 && preg_match('/[^a-zA-Z]/', $last_part)) {
return false;
}
$rcube = rcube::get_instance();
if (!$dns_check || !function_exists('checkdnsrr') || !$rcube->config->get('email_dns_check')) {
return true;
}
// Check DNS record(s)
// Note: We can't use ANY (#6581)
foreach (array('A', 'MX', 'CNAME', 'AAAA') as $type) {
if (checkdnsrr($domain_part, $type)) {
return true;
}
}
}
return false;
}
/**
* Validates IPv4 or IPv6 address
*
* @param string $ip IP address in v4 or v6 format
*
* @return bool True if the address is valid
*/
public static function check_ip($ip)
{
return filter_var($ip, FILTER_VALIDATE_IP) !== false;
}
/**
* Replacing specials characters to a specific encoding type
*
* @param string Input string
* @param string Encoding type: text|html|xml|js|url
* @param string Replace mode for tags: show|remove|strict
* @param boolean Convert newlines
*
* @return string The quoted string
*/
public static function rep_specialchars_output($str, $enctype = '', $mode = '', $newlines = true)
{
static $html_encode_arr = false;
static $js_rep_table = false;
static $xml_rep_table = false;
if (!is_string($str)) {
$str = strval($str);
}
// encode for HTML output
if ($enctype == 'html') {
if (!$html_encode_arr) {
$html_encode_arr = get_html_translation_table(HTML_SPECIALCHARS);
unset($html_encode_arr['?']);
}
$encode_arr = $html_encode_arr;
if ($mode == 'remove') {
$str = strip_tags($str);
}
else if ($mode != 'strict') {
// don't replace quotes and html tags
$ltpos = strpos($str, '<');
if ($ltpos !== false && strpos($str, '>', $ltpos) !== false) {
unset($encode_arr['"']);
unset($encode_arr['<']);
unset($encode_arr['>']);
unset($encode_arr['&']);
}
}
$out = strtr($str, $encode_arr);
return $newlines ? nl2br($out) : $out;
}
// if the replace tables for XML and JS are not yet defined
if ($js_rep_table === false) {
$js_rep_table = $xml_rep_table = array();
$xml_rep_table['&'] = '&amp;';
// can be increased to support more charsets
for ($c=160; $c<256; $c++) {
$xml_rep_table[chr($c)] = "&#$c;";
}
$xml_rep_table['"'] = '&quot;';
$js_rep_table['"'] = '\\"';
$js_rep_table["'"] = "\\'";
$js_rep_table["\\"] = "\\\\";
// Unicode line and paragraph separators (#1486310)
$js_rep_table[chr(hexdec('E2')).chr(hexdec('80')).chr(hexdec('A8'))] = '&#8232;';
$js_rep_table[chr(hexdec('E2')).chr(hexdec('80')).chr(hexdec('A9'))] = '&#8233;';
}
// encode for javascript use
if ($enctype == 'js') {
return preg_replace(array("/\r?\n/", "/\r/", '/<\\//'), array('\n', '\n', '<\\/'), strtr($str, $js_rep_table));
}
// encode for plaintext
if ($enctype == 'text') {
return str_replace("\r\n", "\n", $mode == 'remove' ? strip_tags($str) : $str);
}
if ($enctype == 'url') {
return rawurlencode($str);
}
// encode for XML
if ($enctype == 'xml') {
return strtr($str, $xml_rep_table);
}
// no encoding given -> return original string
return $str;
}
/**
* Read input value and convert it for internal use
* Performs stripslashes() and charset conversion if necessary
*
* @param string Field name to read
* @param int Source to get value from (see self::INPUT_*)
* @param boolean Allow HTML tags in field value
* @param string Charset to convert into
*
* @return string Field value or NULL if not available
*/
public static function get_input_value($fname, $source, $allow_html = false, $charset = null)
{
$value = null;
if (($source & self::INPUT_GET) && isset($_GET[$fname])) {
$value = $_GET[$fname];
}
if (($source & self::INPUT_POST) && isset($_POST[$fname])) {
$value = $_POST[$fname];
}
if (($source & self::INPUT_COOKIE) && isset($_COOKIE[$fname])) {
$value = $_COOKIE[$fname];
}
return self::parse_input_value($value, $allow_html, $charset);
}
/**
* Parse/validate input value. See self::get_input_value()
* Performs stripslashes() and charset conversion if necessary
*
* @param string Input value
* @param boolean Allow HTML tags in field value
* @param string Charset to convert into
*
* @return string Parsed value
*/
public static function parse_input_value($value, $allow_html = false, $charset = null)
{
global $OUTPUT;
if (empty($value)) {
return $value;
}
if (is_array($value)) {
foreach ($value as $idx => $val) {
$value[$idx] = self::parse_input_value($val, $allow_html, $charset);
}
return $value;
}
// remove HTML tags if not allowed
if (!$allow_html) {
$value = strip_tags($value);
}
$output_charset = is_object($OUTPUT) ? $OUTPUT->get_charset() : null;
// remove invalid characters (#1488124)
if ($output_charset == 'UTF-8') {
$value = rcube_charset::clean($value);
}
// convert to internal charset
if ($charset && $output_charset) {
$value = rcube_charset::convert($value, $output_charset, $charset);
}
return $value;
}
/**
* Convert array of request parameters (prefixed with _)
* to a regular array with non-prefixed keys.
*
* @param int $mode Source to get value from (GPC)
* @param string $ignore PCRE expression to skip parameters by name
* @param boolean $allow_html Allow HTML tags in field value
*
* @return array Hash array with all request parameters
*/
public static function request2param($mode = null, $ignore = 'task|action', $allow_html = false)
{
$out = array();
$src = $mode == self::INPUT_GET ? $_GET : ($mode == self::INPUT_POST ? $_POST : $_REQUEST);
foreach (array_keys($src) as $key) {
$fname = $key[0] == '_' ? substr($key, 1) : $key;
if ($ignore && !preg_match('/^(' . $ignore . ')$/', $fname)) {
$out[$fname] = self::get_input_value($key, $mode, $allow_html);
}
}
return $out;
}
/**
* Convert the given string into a valid HTML identifier
* Same functionality as done in app.js with rcube_webmail.html_identifier()
*/
public static function html_identifier($str, $encode=false)
{
if ($encode) {
return rtrim(strtr(base64_encode($str), '+/', '-_'), '=');
}
else {
return asciiwords($str, true, '_');
}
}
/**
* Replace all css definitions with #container [def]
* and remove css-inlined scripting, make position style safe
*
* @param string CSS source code
* @param string Container ID to use as prefix
* @param bool Allow remote content
* @param string Prefix to be added to id/class identifier
*
* @return string Modified CSS source
*/
public static function mod_css_styles($source, $container_id, $allow_remote = false, $prefix = '')
{
$last_pos = 0;
$replacements = new rcube_string_replacer;
// ignore the whole block if evil styles are detected
$source = self::xss_entity_decode($source);
$stripped = preg_replace('/[^a-z\(:;]/i', '', $source);
$evilexpr = 'expression|behavior|javascript:|import[^a]' . (!$allow_remote ? '|url\((?!data:image)' : '');
if (preg_match("/$evilexpr/i", $stripped)) {
return '/* evil! */';
}
$strict_url_regexp = '!url\s*\([ "\'](https?:)//[a-z0-9/._+-]+["\' ]\)!Uims';
// cut out all contents between { and }
while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos))) {
$nested = strpos($source, '{', $pos+1);
if ($nested && $nested < $pos2) // when dealing with nested blocks (e.g. @media), take the inner one
$pos = $nested;
$length = $pos2 - $pos - 1;
$styles = substr($source, $pos+1, $length);
// Convert position:fixed to position:absolute (#5264)
$styles = preg_replace('/position:[\s\r\n]*fixed/i', 'position: absolute', $styles);
// check every line of a style block...
if ($allow_remote) {
$a_styles = preg_split('/;[\r\n]*/', $styles, -1, PREG_SPLIT_NO_EMPTY);
for ($i=0, $len=count($a_styles); $i < $len; $i++) {
$line = $a_styles[$i];
$stripped = preg_replace('/[^a-z\(:;]/i', '', $line);
// allow data:image uri, join with continuation
if (stripos($stripped, 'url(data:image')) {
$a_styles[$i] .= ';' . $a_styles[$i+1];
unset($a_styles[$i+1]);
}
// allow strict url() values only
else if (stripos($stripped, 'url(') && !preg_match($strict_url_regexp, $line)) {
$a_styles = array('/* evil! */');
break;
}
}
$styles = join(";\n", $a_styles);
}
$key = $replacements->add($styles);
$repl = $replacements->get_replacement($key);
$source = substr_replace($source, $repl, $pos+1, $length);
$last_pos = $pos2 - ($length - strlen($repl));
}
// remove html comments
$source = preg_replace('/(^\s*<\!--)|(-->\s*$)/m', '', $source);
// add #container to each tag selector and prefix to id/class identifiers
if ($container_id || $prefix) {
// (?!##str) below is to not match with ##str_replacement_0##
// from rcube_string_replacer used above, this is needed for
// cases like @media { body { position: fixed; } } (#5811)
$regexp = '/(^\s*|,\s*|\}\s*|\{\s*)((?!##str)[a-z0-9\._#\*\[][a-z0-9\._:\(\)#=~ \[\]"\|\>\+\$\^-]*)/im';
$callback = function($matches) use ($container_id, $prefix) {
$replace = $matches[2];
if ($prefix) {
$replace = str_replace(array('.', '#'), array(".$prefix", "#$prefix"), $replace);
}
if ($container_id) {
$replace = "#$container_id " . $replace;
}
return str_replace($matches[2], $replace, $matches[0]);
};
$source = preg_replace_callback($regexp, $callback, $source);
}
// replace body definition because we also stripped off the <body> tag
if ($container_id) {
$regexp = '/#' . preg_quote($container_id, '/') . '\s+body/i';
$source = preg_replace($regexp, "#$container_id", $source);
}
// put block contents back in
$source = $replacements->resolve($source);
return $source;
}
/**
* Generate CSS classes from mimetype and filename extension
*
* @param string $mimetype Mimetype
* @param string $filename Filename
*
* @return string CSS classes separated by space
*/
public static function file2class($mimetype, $filename)
{
$mimetype = strtolower($mimetype);
$filename = strtolower($filename);
list($primary, $secondary) = explode('/', $mimetype);
$classes = array($primary ?: 'unknown');
if ($secondary) {
$classes[] = $secondary;
}
if (preg_match('/\.([a-z0-9]+)$/', $filename, $m)) {
if (!in_array($m[1], $classes)) {
$classes[] = $m[1];
}
}
return join(" ", $classes);
}
/**
* Decode escaped entities used by known XSS exploits.
* See http://downloads.securityfocus.com/vulnerabilities/exploits/26800.eml for examples
*
* @param string CSS content to decode
*
* @return string Decoded string
*/
public static function xss_entity_decode($content)
{
$callback = function($matches) { return chr(hexdec($matches[1])); };
$out = html_entity_decode(html_entity_decode($content));
$out = trim(preg_replace('/(^<!--|-->$)/', '', trim($out)));
$out = preg_replace_callback('/\\\([0-9a-f]{2,6})\s*/i', $callback, $out);
$out = preg_replace('/\\\([^0-9a-f])/i', '\\1', $out);
$out = preg_replace('#/\*.*\*/#Ums', '', $out);
$out = strip_tags($out);
return $out;
}
/**
* Check if we can process not exceeding memory_limit
*
* @param integer Required amount of memory
*
* @return boolean True if memory won't be exceeded, False otherwise
*/
public static function mem_check($need)
{
$mem_limit = parse_bytes(ini_get('memory_limit'));
$memory = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024; // safe value: 16MB
return $mem_limit > 0 && $memory + $need > $mem_limit ? false : true;
}
/**
* Check if working in SSL mode
*
* @param integer $port HTTPS port number
* @param boolean $use_https Enables 'use_https' option checking
*
* @return boolean
*/
public static function https_check($port=null, $use_https=true)
{
if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off') {
return true;
}
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])
&& strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https'
&& in_array($_SERVER['REMOTE_ADDR'], rcube::get_instance()->config->get('proxy_whitelist', array()))
) {
return true;
}
if ($port && $_SERVER['SERVER_PORT'] == $port) {
return true;
}
if ($use_https && rcube::get_instance()->config->get('use_https')) {
return true;
}
return false;
}
/**
* Replaces hostname variables.
*
* @param string $name Hostname
* @param string $host Optional IMAP hostname
*
* @return string Hostname
*/
public static function parse_host($name, $host = '')
{
if (!is_string($name)) {
return $name;
}
// %n - host
$n = self::server_name();
// %t - host name without first part, e.g. %n=mail.domain.tld, %t=domain.tld
$t = preg_replace('/^[^\.]+\./', '', $n);
// %d - domain name without first part
$d = preg_replace('/^[^\.]+\./', '', self::server_name('HTTP_HOST'));
// %h - IMAP host
$h = $_SESSION['storage_host'] ?: $host;
// %z - IMAP domain without first part, e.g. %h=imap.domain.tld, %z=domain.tld
$z = preg_replace('/^[^\.]+\./', '', $h);
// %s - domain name after the '@' from e-mail address provided at login screen.
// Returns FALSE if an invalid email is provided
if (strpos($name, '%s') !== false) {
$user_email = self::idn_to_ascii(self::get_input_value('_user', self::INPUT_POST));
$matches = preg_match('/(.*)@([a-z0-9\.\-\[\]\:]+)/i', $user_email, $s);
if ($matches < 1 || filter_var($s[1]."@".$s[2], FILTER_VALIDATE_EMAIL) === false) {
return false;
}
}
return str_replace(array('%n', '%t', '%d', '%h', '%z', '%s'), array($n, $t, $d, $h, $z, $s[2]), $name);
}
/**
* Returns the host name after checking it against trusted hostname
* patterns, otherwise returns localhost (and logs a warning)
*
* @param string $type The $_SERVER key, e.g. 'HTTP_HOST', Default: 'SERVER_NAME'.
* @param boolean $strip_port Strip port from the host name
*
* @return string Server name
*/
public static function server_name($type = null, $strip_port = true)
{
$name = $_SERVER[$type ?: 'SERVER_NAME'];
$rcube = rcube::get_instance();
$patterns = (array) $rcube->config->get('trusted_host_patterns');
if ($strip_port) {
$name = preg_replace('/:\d+$/', '', $name);
}
if (empty($patterns) || in_array_nocase($name, $patterns)) {
return $name;
}
if (!empty($name)) {
foreach ($patterns as $pattern) {
if (preg_match("/$pattern/", $name)) {
return $name;
}
}
$rcube->raise_error(array('file' => __FILE__, 'line' => __LINE__,
'message' => "Specified host is not trusted. Using 'localhost'."), true, false);
}
return 'localhost';
}
/**
* Returns remote IP address and forwarded addresses if found
*
* @return string Remote IP address(es)
*/
public static function remote_ip()
{
$address = $_SERVER['REMOTE_ADDR'];
// append the NGINX X-Real-IP header, if set
if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
$remote_ip[] = 'X-Real-IP: ' . $_SERVER['HTTP_X_REAL_IP'];
}
// append the X-Forwarded-For header, if set
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$remote_ip[] = 'X-Forwarded-For: ' . $_SERVER['HTTP_X_FORWARDED_FOR'];
}
if (!empty($remote_ip)) {
$address .= '(' . implode(',', $remote_ip) . ')';
}
return $address;
}
/**
* Returns the real remote IP address
*
* @return string Remote IP address
*/
public static function remote_addr()
{
// Check if any of the headers are set first to improve performance
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR']) || !empty($_SERVER['HTTP_X_REAL_IP'])) {
$proxy_whitelist = rcube::get_instance()->config->get('proxy_whitelist', array());
if (in_array($_SERVER['REMOTE_ADDR'], $proxy_whitelist)) {
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
foreach (array_reverse(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])) as $forwarded_ip) {
if (!in_array($forwarded_ip, $proxy_whitelist)) {
return $forwarded_ip;
}
}
}
if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
return $_SERVER['HTTP_X_REAL_IP'];
}
}
}
if (!empty($_SERVER['REMOTE_ADDR'])) {
return $_SERVER['REMOTE_ADDR'];
}
return '';
}
/**
* Read a specific HTTP request header.
*
* @param string $name Header name
*
* @return mixed Header value or null if not available
*/
public static function request_header($name)
{
if (function_exists('getallheaders')) {
$hdrs = array_change_key_case(getallheaders(), CASE_UPPER);
$key = strtoupper($name);
}
else {
$key = 'HTTP_' . strtoupper(strtr($name, '-', '_'));
$hdrs = array_change_key_case($_SERVER, CASE_UPPER);
}
return $hdrs[$key];
}
/**
* Explode quoted string
*
* @param string Delimiter expression string for preg_match()
* @param string Input string
*
* @return array String items
*/
public static function explode_quoted_string($delimiter, $string)
{
$result = array();
$strlen = strlen($string);
for ($q=$p=$i=0; $i < $strlen; $i++) {
if ($string[$i] == "\"" && $string[$i-1] != "\\") {
$q = $q ? false : true;
}
else if (!$q && preg_match("/$delimiter/", $string[$i])) {
$result[] = substr($string, $p, $i - $p);
$p = $i + 1;
}
}
$result[] = (string) substr($string, $p);
return $result;
}
/**
* Improved equivalent to strtotime()
*
* @param string $date Date string
* @param DateTimeZone $timezone Timezone to use for DateTime object
*
* @return int Unix timestamp
*/
public static function strtotime($date, $timezone = null)
{
$date = self::clean_datestr($date);
$tzname = $timezone ? ' ' . $timezone->getName() : '';
// unix timestamp
if (is_numeric($date)) {
return (int) $date;
}
// It can be very slow when provided string is not a date and very long
if (strlen($date) > 128) {
$date = substr($date, 0, 128);
}
// if date parsing fails, we have a date in non-rfc format.
// remove token from the end and try again
while (($ts = @strtotime($date . $tzname)) === false || $ts < 0) {
if (($pos = strrpos($date, ' ')) === false) {
break;
}
$date = rtrim(substr($date, 0, $pos));
}
return (int) $ts;
}
/**
* Date parsing function that turns the given value into a DateTime object
*
* @param string $date Date string
* @param DateTimeZone $timezone Timezone to use for DateTime object
*
* @return DateTime instance or false on failure
*/
public static function anytodatetime($date, $timezone = null)
{
if ($date instanceof DateTime) {
return $date;
}
$dt = false;
$date = self::clean_datestr($date);
// try to parse string with DateTime first
if (!empty($date)) {
try {
$_date = preg_match('/^[0-9]+$/', $date) ? "@$date" : $date;
$dt = $timezone ? new DateTime($_date, $timezone) : new DateTime($_date);
}
catch (Exception $e) {
// ignore
}
}
// try our advanced strtotime() method
if (!$dt && ($timestamp = self::strtotime($date, $timezone))) {
try {
$dt = new DateTime("@".$timestamp);
if ($timezone) {
$dt->setTimezone($timezone);
}
}
catch (Exception $e) {
// ignore
}
}
return $dt;
}
/**
* Clean up date string for strtotime() input
*
* @param string $date Date string
*
* @return string Date string
*/
public static function clean_datestr($date)
{
$date = trim($date);
// check for MS Outlook vCard date format YYYYMMDD
if (preg_match('/^([12][90]\d\d)([01]\d)([0123]\d)$/', $date, $m)) {
return sprintf('%04d-%02d-%02d 00:00:00', intval($m[1]), intval($m[2]), intval($m[3]));
}
// Clean malformed data
$date = preg_replace(
array(
'/\(.*\)/', // remove RFC comments
'/GMT\s*([+-][0-9]+)/', // support non-standard "GMTXXXX" literal
'/[^a-z0-9\x20\x09:\/\.+-]/i', // remove any invalid characters
'/\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*/i', // remove weekday names
),
array(
'',
'\\1',
'',
'',
), $date);
$date = trim($date);
// try to fix dd/mm vs. mm/dd discrepancy, we can't do more here
if (preg_match('/^(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{4})(\s.*)?$/', $date, $m)) {
$mdy = $m[2] > 12 && $m[1] <= 12;
$day = $mdy ? $m[2] : $m[1];
$month = $mdy ? $m[1] : $m[2];
$date = sprintf('%04d-%02d-%02d%s', $m[3], $month, $day, $m[4] ?: ' 00:00:00');
}
// I've found that YYYY.MM.DD is recognized wrong, so here's a fix
else if (preg_match('/^(\d{4})\.(\d{1,2})\.(\d{1,2})(\s.*)?$/', $date, $m)) {
$date = sprintf('%04d-%02d-%02d%s', $m[1], $m[2], $m[3], $m[4] ?: ' 00:00:00');
}
return $date;
}
/**
* Turns the given date-only string in defined format into YYYY-MM-DD format.
*
* Supported formats: 'Y/m/d', 'Y.m.d', 'd-m-Y', 'd/m/Y', 'd.m.Y', 'j.n.Y'
*
* @param string $date Date string
* @param string $format Input date format
*
* @return strin Date string in YYYY-MM-DD format, or the original string
* if format is not supported
*/
public static function format_datestr($date, $format)
{
$format_items = preg_split('/[.-\/\\\\]/', $format);
$date_items = preg_split('/[.-\/\\\\]/', $date);
$iso_format = '%04d-%02d-%02d';
if (count($format_items) == 3 && count($date_items) == 3) {
if ($format_items[0] == 'Y') {
$date = sprintf($iso_format, $date_items[0], $date_items[1], $date_items[2]);
}
else if (strpos('dj', $format_items[0]) !== false) {
$date = sprintf($iso_format, $date_items[2], $date_items[1], $date_items[0]);
}
else if (strpos('mn', $format_items[0]) !== false) {
$date = sprintf($iso_format, $date_items[2], $date_items[0], $date_items[1]);
}
}
return $date;
}
/**
* Wrapper for idn_to_ascii with support for e-mail address.
*
* Warning: Domain names may be lowercase'd.
* Warning: An empty string may be returned on invalid domain.
*
* @param string $str Decoded e-mail address
*
* @return string Encoded e-mail address
*/
public static function idn_to_ascii($str)
{
return self::idn_convert($str, true);
}
/**
* Wrapper for idn_to_utf8 with support for e-mail address
*
* @param string $str Decoded e-mail address
*
* @return string Encoded e-mail address
*/
public static function idn_to_utf8($str)
{
return self::idn_convert($str, false);
}
/**
* Convert a string to ascii or utf8 (using IDNA standard)
*
* @param string $input Decoded e-mail address
* @param boolean $is_utf Convert by idn_to_ascii if true and idn_to_utf8 if false
*
* @return string Encoded e-mail address
*/
public static function idn_convert($input, $is_utf = false)
{
if ($at = strpos($input, '@')) {
$user = substr($input, 0, $at);
$domain = substr($input, $at + 1);
}
else {
$user = '';
$domain = $input;
}
// Note that in PHP 7.2/7.3 calling idn_to_* functions with default arguments
// throws a warning, so we have to set the variant explicitely (#6075)
$variant = defined('INTL_IDNA_VARIANT_UTS46') ? INTL_IDNA_VARIANT_UTS46 : null;
$options = 0;
// Because php-intl extension lowercases domains and return false
// on invalid input (#6224), we skip conversion when not needed
// for compatibility with our Net_IDNA2 wrappers in bootstrap.php
if ($is_utf) {
if (preg_match('/[^\x20-\x7E]/', $domain)) {
$domain = idn_to_ascii($domain, $options, $variant);
}
}
else if (preg_match('/(^|\.)xn--/i', $domain)) {
$domain = idn_to_utf8($domain, $options, $variant);
}
if ($domain === false) {
return '';
}
return $at ? $user . '@' . $domain : $domain;
}
/**
* Split the given string into word tokens
*
* @param string Input to tokenize
* @param integer Minimum length of a single token
* @return array List of tokens
*/
public static function tokenize_string($str, $minlen = 2)
{
$expr = array('/[\s;,"\'\/+-]+/ui', '/(\d)[-.\s]+(\d)/u');
$repl = array(' ', '\\1\\2');
if ($minlen > 1) {
$minlen--;
$expr[] = "/(^|\s+)\w{1,$minlen}(\s+|$)/u";
$repl[] = ' ';
}
return array_filter(explode(" ", preg_replace($expr, $repl, $str)));
}
/**
* Normalize the given string for fulltext search.
* Currently only optimized for ISO-8859-1 and ISO-8859-2 characters; to be extended
*
* @param string Input string (UTF-8)
* @param boolean True to return list of words as array
* @param integer Minimum length of tokens
*
* @return mixed Normalized string or a list of normalized tokens
*/
public static function normalize_string($str, $as_array = false, $minlen = 2)
{
// replace 4-byte unicode characters with '?' character,
// these are not supported in default utf-8 charset on mysql,
// the chance we'd need them in searching is very low
$str = preg_replace('/('
. '\xF0[\x90-\xBF][\x80-\xBF]{2}'
. '|[\xF1-\xF3][\x80-\xBF]{3}'
. '|\xF4[\x80-\x8F][\x80-\xBF]{2}'
. ')/', '?', $str);
// split by words
$arr = self::tokenize_string($str, $minlen);
// detect character set
if (utf8_encode(utf8_decode($str)) == $str) {
// ISO-8859-1 (or ASCII)
preg_match_all('/./u', 'äâàåáãæçéêëèïîìíñöôòøõóüûùúýÿ', $keys);
preg_match_all('/./', 'aaaaaaaceeeeiiiinoooooouuuuyy', $values);
$mapping = array_combine($keys[0], $values[0]);
$mapping = array_merge($mapping, array('ß' => 'ss', 'ae' => 'a', 'oe' => 'o', 'ue' => 'u'));
}
else if (rcube_charset::convert(rcube_charset::convert($str, 'UTF-8', 'ISO-8859-2'), 'ISO-8859-2', 'UTF-8') == $str) {
// ISO-8859-2
preg_match_all('/./u', 'ąáâäćçčéęëěíîłľĺńňóôöŕřśšşťţůúűüźžżý', $keys);
preg_match_all('/./', 'aaaaccceeeeiilllnnooorrsssttuuuuzzzy', $values);
$mapping = array_combine($keys[0], $values[0]);
$mapping = array_merge($mapping, array('ß' => 'ss', 'ae' => 'a', 'oe' => 'o', 'ue' => 'u'));
}
foreach ($arr as $i => $part) {
$part = mb_strtolower($part);
if (!empty($mapping)) {
$part = strtr($part, $mapping);
}
$arr[$i] = $part;
}
return $as_array ? $arr : join(" ", $arr);
}
/**
* Compare two strings for matching words (order not relevant)
*
* @param string Haystack
* @param string Needle
*
* @return boolean True if match, False otherwise
*/
public static function words_match($haystack, $needle)
{
$a_needle = self::tokenize_string($needle, 1);
$_haystack = join(" ", self::tokenize_string($haystack, 1));
$valid = strlen($_haystack) > 0;
$hits = 0;
foreach ($a_needle as $w) {
if ($valid) {
if (stripos($_haystack, $w) !== false) {
$hits++;
}
}
else if (stripos($haystack, $w) !== false) {
$hits++;
}
}
return $hits >= count($a_needle);
}
/**
* Parse commandline arguments into a hash array
*
* @param array $aliases Argument alias names
*
* @return array Argument values hash
*/
public static function get_opt($aliases = array())
{
$args = array();
$bool = array();
// find boolean (no value) options
foreach ($aliases as $key => $alias) {
if ($pos = strpos($alias, ':')) {
$aliases[$key] = substr($alias, 0, $pos);
$bool[] = $key;
$bool[] = $aliases[$key];
}
}
for ($i=1; $i < count($_SERVER['argv']); $i++) {
$arg = $_SERVER['argv'][$i];
$value = true;
$key = null;
if ($arg[0] == '-') {
$key = preg_replace('/^-+/', '', $arg);
$sp = strpos($arg, '=');
if ($sp > 0) {
$key = substr($key, 0, $sp - 2);
$value = substr($arg, $sp+1);
}
else if (in_array($key, $bool)) {
$value = true;
}
else if (strlen($_SERVER['argv'][$i+1]) && $_SERVER['argv'][$i+1][0] != '-') {
$value = $_SERVER['argv'][++$i];
}
$args[$key] = is_string($value) ? preg_replace(array('/^["\']/', '/["\']$/'), '', $value) : $value;
}
else {
$args[] = $arg;
}
if ($alias = $aliases[$key]) {
$args[$alias] = $args[$key];
}
}
return $args;
}
/**
* Safe password prompt for command line
* from http://blogs.sitepoint.com/2009/05/01/interactive-cli-password-prompt-in-php/
*
* @return string Password
*/
public static function prompt_silent($prompt = "Password:")
{
if (preg_match('/^win/i', PHP_OS)) {
$vbscript = sys_get_temp_dir() . 'prompt_password.vbs';
$vbcontent = 'wscript.echo(InputBox("' . addslashes($prompt) . '", "", "password here"))';
file_put_contents($vbscript, $vbcontent);
$command = "cscript //nologo " . escapeshellarg($vbscript);
$password = rtrim(shell_exec($command));
unlink($vbscript);
return $password;
}
else {
$command = "/usr/bin/env bash -c 'echo OK'";
if (rtrim(shell_exec($command)) !== 'OK') {
echo $prompt;
$pass = trim(fgets(STDIN));
echo chr(8)."\r" . $prompt . str_repeat("*", strlen($pass))."\n";
return $pass;
}
$command = "/usr/bin/env bash -c 'read -s -p \"" . addslashes($prompt) . "\" mypassword && echo \$mypassword'";
$password = rtrim(shell_exec($command));
echo "\n";
return $password;
}
}
/**
* Find out if the string content means true or false
*
* @param string $str Input value
*
* @return boolean Boolean value
*/
public static function get_boolean($str)
{
$str = strtolower($str);
return !in_array($str, array('false', '0', 'no', 'off', 'nein', ''), true);
}
/**
* OS-dependent absolute path detection
*/
public static function is_absolute_path($path)
{
if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
return (bool) preg_match('!^[a-z]:[\\\\/]!i', $path);
}
else {
return $path[0] == '/';
}
}
/**
* Resolve relative URL
*
* @param string $url Relative URL
*
* @return string Absolute URL
*/
public static function resolve_url($url)
{
// prepend protocol://hostname:port
if (!preg_match('|^https?://|', $url)) {
$schema = 'http';
$default_port = 80;
if (self::https_check()) {
$schema = 'https';
$default_port = 443;
}
$prefix = $schema . '://' . preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']);
if ($_SERVER['SERVER_PORT'] != $default_port && $_SERVER['SERVER_PORT'] != 80) {
$prefix .= ':' . $_SERVER['SERVER_PORT'];
}
$url = $prefix . ($url[0] == '/' ? '' : '/') . $url;
}
return $url;
}
/**
* Generate a random string
*
* @param int $length String length
* @param bool $raw Return RAW data instead of ascii
*
* @return string The generated random string
*/
public static function random_bytes($length, $raw = false)
{
$hextab = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
$tabsize = strlen($hextab);
// Use PHP7 true random generator
if ($raw && function_exists('random_bytes')) {
return random_bytes($length);
}
if (!$raw && function_exists('random_int')) {
$result = '';
while ($length-- > 0) {
$result .= $hextab[random_int(0, $tabsize - 1)];
}
return $result;
}
$random = openssl_random_pseudo_bytes($length);
if ($random === false && $length > 0) {
throw new Exception("Failed to get random bytes");
}
if (!$raw) {
for ($x = 0; $x < $length; $x++) {
$random[$x] = $hextab[ord($random[$x]) % $tabsize];
}
}
return $random;
}
/**
* Convert binary data into readable form (containing a-zA-Z0-9 characters)
*
* @param string $input Binary input
*
* @return string Readable output (Base62)
* @deprecated since 1.3.1
*/
public static function bin2ascii($input)
{
$hextab = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
$result = '';
for ($x = 0; $x < strlen($input); $x++) {
$result .= $hextab[ord($input[$x]) % 62];
}
return $result;
}
/**
* Format current date according to specified format.
* This method supports microseconds (u).
*
* @param string $format Date format (default: 'd-M-Y H:i:s O')
*
* @return string Formatted date
*/
public static function date_format($format = null)
{
if (empty($format)) {
$format = 'd-M-Y H:i:s O';
}
if (strpos($format, 'u') !== false) {
$dt = number_format(microtime(true), 6, '.', '');
$dt .= '.' . date_default_timezone_get();
if ($date = date_create_from_format('U.u.e', $dt)) {
return $date->format($format);
}
}
return date($format);
}
/**
* Parses socket options and returns options for specified hostname.
*
* @param array &$options Configured socket options
* @param string $host Hostname
*/
public static function parse_socket_options(&$options, $host = null)
{
if (empty($host) || empty($options)) {
return $options;
}
// get rid of schema and port from the hostname
$host_url = parse_url($host);
if (isset($host_url['host'])) {
$host = $host_url['host'];
}
// find per-host options
if (array_key_exists($host, $options)) {
$options = $options[$host];
}
}
/**
* Get maximum upload size
*
* @return int Maximum size in bytes
*/
public static function max_upload_size()
{
// find max filesize value
$max_filesize = parse_bytes(ini_get('upload_max_filesize'));
$max_postsize = parse_bytes(ini_get('post_max_size'));
if ($max_postsize && $max_postsize < $max_filesize) {
$max_filesize = $max_postsize;
}
return $max_filesize;
}
/**
* Detect and log last PREG operation error
*
* @param array $error Error data (line, file, code, message)
* @param bool $terminate Stop script execution
*
* @return bool True on error, False otherwise
*/
public static function preg_error($error = array(), $terminate = false)
{
if (($preg_error = preg_last_error()) != PREG_NO_ERROR) {
$errstr = "PCRE Error: $preg_error.";
if ($preg_error == PREG_BACKTRACK_LIMIT_ERROR) {
$errstr .= " Consider raising pcre.backtrack_limit!";
}
if ($preg_error == PREG_RECURSION_LIMIT_ERROR) {
$errstr .= " Consider raising pcre.recursion_limit!";
}
$error = array_merge(array('code' => 620, 'line' => __LINE__, 'file' => __FILE__), $error);
if (!empty($error['message'])) {
$error['message'] .= ' ' . $errstr;
}
else {
$error['message'] = $errstr;
}
rcube::raise_error($error, true, $terminate);
return true;
}
return false;
}
/**
* Generate a temporary file path in the Roundcube temp directory
*
* @param string $file_name String identifier for the type of temp file
* @param bool $unique Generate unique file names based on $file_name
* @param bool $create Create the temp file or not
*
* @return string temporary file path
*/
public static function temp_filename($file_name, $unique = true, $create = true)
{
$temp_dir = rcube::get_instance()->config->get('temp_dir');
// Fall back to system temp dir if configured dir is not writable
if (!is_writable($temp_dir)) {
$temp_dir = sys_get_temp_dir();
}
// On Windows tempnam() uses only the first three characters of prefix so use uniqid() and manually add the prefix
// Full prefix is required for garbage collection to recognise the file
$temp_file = $unique ? str_replace('.', '', uniqid($file_name, true)) : $file_name;
$temp_path = unslashify($temp_dir) . '/' . RCUBE_TEMP_FILE_PREFIX . $temp_file;
// Sanity check for unique file name
if ($unique && file_exists($temp_path)) {
return self::temp_filename($file_name, $unique, $create);
}
// Create the file to prevent possible race condition like tempnam() does
if ($create) {
touch($temp_path);
}
return $temp_path;
}
}
diff --git a/program/lib/Roundcube/rcube_vcard.php b/program/lib/Roundcube/rcube_vcard.php
index 251c1644a..edc5a8c8b 100644
--- a/program/lib/Roundcube/rcube_vcard.php
+++ b/program/lib/Roundcube/rcube_vcard.php
@@ -1,881 +1,882 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2012, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Logical representation of a vcard address record |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Logical representation of a vcard-based address record
* Provides functions to parse and export vCard data format
*
* @package Framework
* @subpackage Addressbook
*/
class rcube_vcard
{
private static $values_decoded = false;
private $raw = array(
'FN' => array(),
'N' => array(array('','','','','')),
);
private static $fieldmap = array(
'phone' => 'TEL',
'birthday' => 'BDAY',
'website' => 'URL',
'notes' => 'NOTE',
'email' => 'EMAIL',
'address' => 'ADR',
'jobtitle' => 'TITLE',
'department' => 'X-DEPARTMENT',
'gender' => 'X-GENDER',
'maidenname' => 'X-MAIDENNAME',
'anniversary' => 'X-ANNIVERSARY',
'assistant' => 'X-ASSISTANT',
'manager' => 'X-MANAGER',
'spouse' => 'X-SPOUSE',
'edit' => 'X-AB-EDIT',
'groups' => 'CATEGORIES',
);
private $typemap = array(
'IPHONE' => 'mobile',
'CELL' => 'mobile',
'WORK,FAX' => 'workfax',
);
private $phonetypemap = array(
'HOME1' => 'HOME',
'BUSINESS1' => 'WORK',
'BUSINESS2' => 'WORK2',
'BUSINESSFAX' => 'WORK,FAX',
'MOBILE' => 'CELL',
);
private $addresstypemap = array(
'BUSINESS' => 'WORK',
);
private $immap = array(
'X-JABBER' => 'jabber',
'X-ICQ' => 'icq',
'X-MSN' => 'msn',
'X-AIM' => 'aim',
'X-YAHOO' => 'yahoo',
'X-SKYPE' => 'skype',
'X-SKYPE-USERNAME' => 'skype',
);
public $business = false;
public $displayname;
public $surname;
public $firstname;
public $middlename;
public $nickname;
public $organization;
public $email = array();
public static $eol = "\r\n";
/**
* Constructor
*/
public function __construct($vcard = null, $charset = RCUBE_CHARSET, $detect = false, $fieldmap = array())
{
if (!empty($fieldmap)) {
$this->extend_fieldmap($fieldmap);
}
if (!empty($vcard)) {
$this->load($vcard, $charset, $detect);
}
}
/**
* Load record from (internal, unfolded) vcard 3.0 format
*
* @param string vCard string to parse
* @param string Charset of string values
* @param boolean True if loading a 'foreign' vcard and extra heuristics for charset detection is required
*/
public function load($vcard, $charset = RCUBE_CHARSET, $detect = false)
{
self::$values_decoded = false;
$this->raw = self::vcard_decode(self::cleanup($vcard));
// resolve charset parameters
if ($charset == null) {
$this->raw = self::charset_convert($this->raw);
}
// vcard has encoded values and charset should be detected
else if ($detect && self::$values_decoded
&& ($detected_charset = self::detect_encoding(self::vcard_encode($this->raw)))
&& $detected_charset != RCUBE_CHARSET
) {
$this->raw = self::charset_convert($this->raw, $detected_charset);
}
// find well-known address fields
$this->displayname = $this->raw['FN'][0][0];
$this->surname = $this->raw['N'][0][0];
$this->firstname = $this->raw['N'][0][1];
$this->middlename = $this->raw['N'][0][2];
$this->nickname = $this->raw['NICKNAME'][0][0];
$this->organization = $this->raw['ORG'][0][0];
$this->business = ($this->raw['X-ABSHOWAS'][0][0] == 'COMPANY') || (join('', (array)$this->raw['N'][0]) == '' && !empty($this->organization));
foreach ((array)$this->raw['EMAIL'] as $i => $raw_email) {
$this->email[$i] = is_array($raw_email) ? $raw_email[0] : $raw_email;
}
// make the pref e-mail address the first entry in $this->email
$pref_index = $this->get_type_index('EMAIL', 'pref');
if ($pref_index > 0) {
$tmp = $this->email[0];
$this->email[0] = $this->email[$pref_index];
$this->email[$pref_index] = $tmp;
}
// fix broken vcards from Outlook that only supply ORG but not the required N or FN properties
if (!strlen(trim($this->displayname . $this->surname . $this->firstname)) && strlen($this->organization)) {
$this->displayname = $this->organization;
}
}
/**
* Return vCard data as associative array to be unsed in Roundcube address books
*
* @return array Hash array with key-value pairs
*/
public function get_assoc()
{
$out = array('name' => $this->displayname);
$typemap = $this->typemap;
// copy name fields to output array
foreach (array('firstname','surname','middlename','nickname','organization') as $col) {
if (strlen($this->$col)) {
$out[$col] = $this->$col;
}
}
if ($this->raw['N'][0][3])
$out['prefix'] = $this->raw['N'][0][3];
if ($this->raw['N'][0][4])
$out['suffix'] = $this->raw['N'][0][4];
// convert from raw vcard data into associative data for Roundcube
foreach (array_flip(self::$fieldmap) as $tag => $col) {
foreach ((array)$this->raw[$tag] as $i => $raw) {
if (is_array($raw)) {
$k = -1;
$key = $col;
$subtype = '';
if (!empty($raw['type'])) {
$raw['type'] = array_map('strtolower', $raw['type']);
$combined = join(',', array_diff($raw['type'], array('internet', 'pref')));
$combined = strtoupper($combined);
if ($typemap[$combined]) {
$subtype = $typemap[$combined];
}
else if ($typemap[$raw['type'][++$k]]) {
$subtype = $typemap[$raw['type'][$k]];
}
else {
$subtype = $raw['type'][$k];
}
while ($k < count($raw['type']) && ($subtype == 'internet' || $subtype == 'pref')) {
$subtype = $typemap[$raw['type'][++$k]] ?: $raw['type'][$k];
}
}
// read vcard 2.1 subtype
if (!$subtype) {
foreach ($raw as $k => $v) {
if (!is_numeric($k) && $v === true && ($k = strtolower($k))
&& !in_array($k, array('pref','internet','voice','base64'))
) {
$k_uc = strtoupper($k);
$subtype = $typemap[$k_uc] ?: $k;
break;
}
}
}
// force subtype if none set
if (!$subtype && preg_match('/^(email|phone|address|website)/', $key)) {
$subtype = 'other';
}
if ($subtype) {
$key .= ':' . $subtype;
}
// split ADR values into assoc array
if ($tag == 'ADR') {
list(,, $value['street'], $value['locality'], $value['region'], $value['zipcode'], $value['country']) = $raw;
$out[$key][] = $value;
}
else {
$out[$key][] = $raw[0];
}
}
else {
$out[$col][] = $raw;
}
}
}
// handle special IM fields as used by Apple
foreach ($this->immap as $tag => $type) {
foreach ((array)$this->raw[$tag] as $i => $raw) {
$out['im:'.$type][] = $raw[0];
}
}
// copy photo data
if ($this->raw['PHOTO']) {
$out['photo'] = $this->raw['PHOTO'][0][0];
}
return $out;
}
/**
* Convert the data structure into a vcard 3.0 string
*/
public function export($folded = true)
{
$vcard = self::vcard_encode($this->raw);
return $folded ? self::rfc2425_fold($vcard) : $vcard;
}
/**
* Clear the given fields in the loaded vcard data
*
* @param array List of field names to be reset
*/
public function reset($fields = null)
{
if (!$fields) {
$fields = array_merge(array_values(self::$fieldmap), array_keys($this->immap),
array('FN','N','ORG','NICKNAME','EMAIL','ADR','BDAY'));
}
foreach ($fields as $f) {
unset($this->raw[$f]);
}
if (!$this->raw['N']) {
$this->raw['N'] = array(array('','','','',''));
}
if (!$this->raw['FN']) {
$this->raw['FN'] = array();
}
$this->email = array();
}
/**
* Setter for address record fields
*
* @param string Field name
* @param string Field value
* @param string Type/section name
*/
public function set($field, $value, $type = 'HOME')
{
$field = strtolower($field);
$type_uc = strtoupper($type);
switch ($field) {
case 'name':
case 'displayname':
$this->raw['FN'][0][0] = $this->displayname = $value;
break;
case 'surname':
$this->raw['N'][0][0] = $this->surname = $value;
break;
case 'firstname':
$this->raw['N'][0][1] = $this->firstname = $value;
break;
case 'middlename':
$this->raw['N'][0][2] = $this->middlename = $value;
break;
case 'prefix':
$this->raw['N'][0][3] = $value;
break;
case 'suffix':
$this->raw['N'][0][4] = $value;
break;
case 'nickname':
$this->raw['NICKNAME'][0][0] = $this->nickname = $value;
break;
case 'organization':
$this->raw['ORG'][0][0] = $this->organization = $value;
break;
case 'photo':
if (strpos($value, 'http:') === 0) {
// TODO: fetch file from URL and save it locally?
$this->raw['PHOTO'][0] = array(0 => $value, 'url' => true);
}
else {
$this->raw['PHOTO'][0] = array(0 => $value, 'base64' => (bool) preg_match('![^a-z0-9/=+-]!i', $value));
}
break;
case 'email':
$this->raw['EMAIL'][] = array(0 => $value, 'type' => array_filter(array('INTERNET', $type_uc)));
$this->email[] = $value;
break;
case 'im':
// save IM subtypes into extension fields
$typemap = array_flip($this->immap);
if ($field = $typemap[strtolower($type)]) {
$this->raw[$field][] = array(0 => $value);
}
break;
case 'birthday':
case 'anniversary':
if (($val = rcube_utils::anytodatetime($value)) && ($fn = self::$fieldmap[$field])) {
$this->raw[$fn][] = array(0 => $val->format('Y-m-d'), 'value' => array('date'));
}
break;
case 'address':
if ($this->addresstypemap[$type_uc]) {
$type = $this->addresstypemap[$type_uc];
}
$value = $value[0] ? $value : array('', '', $value['street'], $value['locality'], $value['region'], $value['zipcode'], $value['country']);
// fall through if not empty
if (!strlen(join('', $value))) {
break;
}
default:
if ($field == 'phone' && $this->phonetypemap[$type_uc]) {
$type = $this->phonetypemap[$type_uc];
}
if (($tag = self::$fieldmap[$field]) && (is_array($value) || strlen($value))) {
$this->raw[$tag][] = (array) $value;
if ($type) {
$index = count($this->raw[$tag]) - 1;
$typemap = array_flip($this->typemap);
$this->raw[$tag][$index]['type'] = explode(',', $typemap[$type_uc] ?: $type);
}
}
else {
unset($this->raw[$tag]);
}
break;
}
}
/**
* Setter for individual vcard properties
*
* @param string VCard tag name
* @param array Value-set of this vcard property
* @param boolean Set to true if the value-set should be appended instead of replacing any existing value-set
*/
public function set_raw($tag, $value, $append = false)
{
$index = $append && isset($this->raw[$tag]) ? count($this->raw[$tag]) : 0;
$this->raw[$tag][$index] = (array)$value;
}
/**
* Find index with the '$type' attribute
*
* @param string Field name
*
* @return int Field index having $type set
*/
private function get_type_index($field)
{
$result = 0;
if ($this->raw[$field]) {
foreach ($this->raw[$field] as $i => $data) {
if (is_array($data['type']) && in_array_nocase('pref', $data['type'])) {
$result = $i;
}
}
}
return $result;
}
/**
* Convert a whole vcard (array) to UTF-8.
* If $force_charset is null, each member value that has a charset parameter will be converted
*/
private static function charset_convert($card, $force_charset = null)
{
foreach ($card as $key => $node) {
foreach ($node as $i => $subnode) {
if (is_array($subnode) && (($charset = $force_charset) || ($subnode['charset'] && ($charset = $subnode['charset'][0])))) {
foreach ($subnode as $j => $value) {
if (is_numeric($j) && is_string($value)) {
$card[$key][$i][$j] = rcube_charset::convert($value, $charset);
}
}
unset($card[$key][$i]['charset']);
}
}
}
return $card;
}
/**
* Extends fieldmap definition
*/
public function extend_fieldmap($map)
{
if (is_array($map)) {
self::$fieldmap = array_merge($map, self::$fieldmap);
}
}
/**
* Factory method to import a vcard file
*
* @param string vCard file content
*
* @return array List of rcube_vcard objects
*/
public static function import($data)
{
$out = array();
// check if charsets are specified (usually vcard version < 3.0 but this is not reliable)
if (preg_match('/charset=/i', substr($data, 0, 2048))) {
$charset = null;
}
// detect charset and convert to utf-8
else if (($charset = self::detect_encoding($data)) && $charset != RCUBE_CHARSET) {
$data = rcube_charset::convert($data, $charset);
$data = preg_replace(array('/^[\xFE\xFF]{2}/', '/^\xEF\xBB\xBF/', '/^\x00+/'), '', $data); // also remove BOM
$charset = RCUBE_CHARSET;
}
$vcard_block = '';
$in_vcard_block = false;
foreach (preg_split("/[\r\n]+/", $data) as $line) {
if ($in_vcard_block && !empty($line)) {
$vcard_block .= $line . "\n";
}
$line = trim($line);
if (preg_match('/^END:VCARD$/i', $line)) {
// parse vcard
$obj = new rcube_vcard($vcard_block, $charset, true, self::$fieldmap);
// FN and N is required by vCard format (RFC 2426)
// on import we can be less restrictive, let's addressbook decide
if (!empty($obj->displayname) || !empty($obj->surname) || !empty($obj->firstname) || !empty($obj->email)) {
$out[] = $obj;
}
$in_vcard_block = false;
}
else if (preg_match('/^BEGIN:VCARD$/i', $line)) {
$vcard_block = $line . "\n";
$in_vcard_block = true;
}
}
return $out;
}
/**
* Normalize vcard data for better parsing
*
* @param string vCard block
*
* @return string Cleaned vcard block
*/
public static function cleanup($vcard)
{
// convert Apple X-ABRELATEDNAMES into X-* fields for better compatibility
$vcard = preg_replace_callback(
'/item(\d+)\.(X-ABRELATEDNAMES)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w() -]*)(?:>!\$_)?./s',
array('self', 'x_abrelatednames_callback'),
$vcard);
// Cleanup
$vcard = preg_replace(array(
// convert special types (like Skype) to normal type='skype' classes with this simple regex ;)
'/item(\d+)\.(TEL|EMAIL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w() -]*)(?:>!\$_)?./si',
'/^item\d*\.X-AB.*$/mi', // remove cruft like item1.X-AB*
'/^item\d*\./mi', // remove item1.ADR instead of ADR
'/\n+/', // remove empty lines
'/^(N:[^;\R]*)$/m', // if N doesn't have any semicolons, add some
),
array(
'\2;type=\5\3:\4',
'',
'',
"\n",
'\1;;;;',
), $vcard);
// convert X-WAB-GENDER to X-GENDER
if (preg_match('/X-WAB-GENDER:(\d)/', $vcard, $matches)) {
$value = $matches[1] == '2' ? 'male' : 'female';
$vcard = preg_replace('/X-WAB-GENDER:\d/', 'X-GENDER:' . $value, $vcard);
}
return $vcard;
}
private static function x_abrelatednames_callback($matches)
{
return 'X-' . strtoupper($matches[5]) . $matches[3] . ':'. $matches[4];
}
private static function rfc2425_fold_callback($matches)
{
// chunk_split string and avoid lines breaking multibyte characters
$c = 71;
$out .= substr($matches[1], 0, $c);
for ($n = $c; $c < strlen($matches[1]); $c++) {
// break if length > 75 or mutlibyte character starts after position 71
if ($n > 75 || ($n > 71 && ord($matches[1][$c]) >> 6 == 3)) {
$out .= "\r\n ";
$n = 0;
}
$out .= $matches[1][$c];
$n++;
}
return $out;
}
public static function rfc2425_fold($val)
{
return preg_replace_callback('/([^\n]{72,})/', array('self', 'rfc2425_fold_callback'), $val);
}
/**
* Decodes a vcard block (vcard 3.0 format, unfolded)
* into an array structure
*
* @param string vCard block to parse
*
* @return array Raw data structure
*/
private static function vcard_decode($vcard)
{
// Perform RFC2425 line unfolding and split lines
$vcard = preg_replace(array("/\r/", "/\n\s+/"), '', $vcard);
$lines = explode("\n", $vcard);
$result = array();
for ($i=0; $i < count($lines); $i++) {
if (!($pos = strpos($lines[$i], ':'))) {
continue;
}
$prefix = substr($lines[$i], 0, $pos);
$data = substr($lines[$i], $pos+1);
if (preg_match('/^(BEGIN|END)$/i', $prefix)) {
continue;
}
// convert 2.1-style "EMAIL;internet;home:" to 3.0-style "EMAIL;TYPE=internet;TYPE=home:"
if ($result['VERSION'][0] == "2.1"
&& preg_match('/^([^;]+);([^:]+)/', $prefix, $regs2)
&& !preg_match('/^TYPE=/i', $regs2[2])
) {
$prefix = $regs2[1];
foreach (explode(';', $regs2[2]) as $prop) {
$prefix .= ';' . (strpos($prop, '=') ? $prop : 'TYPE='.$prop);
}
}
if (preg_match_all('/([^\\;]+);?/', $prefix, $regs2)) {
$entry = array();
$field = strtoupper($regs2[1][0]);
$enc = null;
foreach ($regs2[1] as $attrid => $attr) {
$attr = preg_replace('/[\s\t\n\r\0\x0B]/', '', $attr);
if ((list($key, $value) = explode('=', $attr)) && $value) {
if ($key == 'ENCODING') {
$value = strtoupper($value);
// add next line(s) to value string if QP line end detected
if ($value == 'QUOTED-PRINTABLE') {
while (preg_match('/=$/', $lines[$i])) {
$data .= "\n" . $lines[++$i];
}
}
$enc = $value == 'BASE64' ? 'B' : $value;
}
else {
$lc_key = strtolower($key);
$entry[$lc_key] = array_merge((array)$entry[$lc_key], (array)self::vcard_unquote($value, ','));
}
}
else if ($attrid > 0) {
$entry[strtolower($key)] = true; // true means attr without =value
}
}
// decode value
if ($enc || !empty($entry['base64'])) {
// save encoding type (#1488432)
if ($enc == 'B') {
$entry['encoding'] = 'B';
// should we use vCard 3.0 instead?
// $entry['base64'] = true;
}
$data = self::decode_value($data, $enc ?: 'base64');
}
else if ($field == 'PHOTO') {
// vCard 4.0 data URI, "PHOTO:data:image/jpeg;base64,..."
if (preg_match('/^data:[a-z\/_-]+;base64,/i', $data, $m)) {
$entry['encoding'] = $enc = 'B';
$data = substr($data, strlen($m[0]));
$data = self::decode_value($data, 'base64');
}
}
if ($enc != 'B' && empty($entry['base64'])) {
$data = self::vcard_unquote($data);
}
if (is_array($data) || (is_string($data) && strlen($data))) {
$entry = array_merge($entry, (array) $data);
$result[$field][] = $entry;
}
}
}
unset($result['VERSION']);
return $result;
}
/**
* Decode a given string with the encoding rule from ENCODING attributes
*
* @param string String to decode
* @param string Encoding type (quoted-printable and base64 supported)
*
* @return string Decoded 8bit value
*/
private static function decode_value($value, $encoding)
{
switch (strtolower($encoding)) {
case 'quoted-printable':
self::$values_decoded = true;
return quoted_printable_decode($value);
case 'base64':
case 'b':
self::$values_decoded = true;
return base64_decode($value);
default:
return $value;
}
}
/**
* Encodes an entry for storage in our database (vcard 3.0 format, unfolded)
*
* @param array Raw data structure to encode
*
* @return string vCard encoded string
*/
static function vcard_encode($data)
{
foreach ((array)$data as $type => $entries) {
// valid N has 5 properties
while ($type == "N" && is_array($entries[0]) && count($entries[0]) < 5) {
$entries[0][] = "";
}
// make sure FN is not empty (required by RFC2426)
if ($type == "FN" && empty($entries)) {
$entries[0] = $data['EMAIL'][0][0];
}
foreach ((array)$entries as $entry) {
$attr = '';
if (is_array($entry)) {
$value = array();
foreach ($entry as $attrname => $attrvalues) {
if (is_int($attrname)) {
if (!empty($entry['base64']) || $entry['encoding'] == 'B') {
$attrvalues = base64_encode($attrvalues);
}
$value[] = $attrvalues;
}
else if (is_bool($attrvalues)) {
// true means just a tag, not tag=value, as in PHOTO;BASE64:...
if ($attrvalues) {
// vCard v3 uses ENCODING=b (#1489183)
if ($attrname == 'base64') {
$attr .= ";ENCODING=b";
}
else {
$attr .= strtoupper(";$attrname");
}
}
}
else {
foreach ((array)$attrvalues as $attrvalue) {
$attr .= strtoupper(";$attrname=") . self::vcard_quote($attrvalue, ',');
}
}
}
}
else {
$value = $entry;
}
// skip empty entries
if (self::is_empty($value)) {
continue;
}
$vcard .= self::vcard_quote($type) . $attr . ':' . self::vcard_quote($value) . self::$eol;
}
}
return 'BEGIN:VCARD' . self::$eol . 'VERSION:3.0' . self::$eol . $vcard . 'END:VCARD';
}
/**
* Join indexed data array to a vcard quoted string
*
* @param array Field data
* @param string Separator
*
* @return string Joined and quoted string
*/
public static function vcard_quote($s, $sep = ';')
{
if (is_array($s)) {
foreach ($s as $part) {
$r[] = self::vcard_quote($part, $sep);
}
return(implode($sep, (array)$r));
}
return strtr($s, array('\\' => '\\\\', "\r" => '', "\n" => '\n', $sep => '\\'.$sep));
}
/**
* Split quoted string
*
* @param string vCard string to split
* @param string Separator char/string
*
* @return array List with splited values
*/
private static function vcard_unquote($s, $sep = ';')
{
// break string into parts separated by $sep
if (!empty($sep)) {
// Handle properly backslash escaping (#1488896)
$rep1 = array("\\\\" => "\010", "\\$sep" => "\007");
$rep2 = array("\007" => "\\$sep", "\010" => "\\\\");
if (count($parts = explode($sep, strtr($s, $rep1))) > 1) {
foreach ($parts as $s) {
$result[] = self::vcard_unquote(strtr($s, $rep2));
}
return $result;
}
$s = trim(strtr($s, $rep2));
}
// some implementations (GMail) use non-standard backslash before colon (#1489085)
// we will handle properly any backslashed character - removing dummy backslahes
// return strtr($s, array("\r" => '', '\\\\' => '\\', '\n' => "\n", '\N' => "\n", '\,' => ',', '\;' => ';'));
$s = str_replace("\r", '', $s);
$pos = 0;
while (($pos = strpos($s, '\\', $pos)) !== false) {
$next = substr($s, $pos + 1, 1);
if ($next == 'n' || $next == 'N') {
$s = substr_replace($s, "\n", $pos, 2);
}
else {
$s = substr_replace($s, '', $pos, 1);
}
$pos += 1;
}
return $s;
}
/**
* Check if vCard entry is empty: empty string or an array with
* all entries empty.
*
* @param string|array $value Attribute value
*
* @return bool True if the value is empty, False otherwise
*/
private static function is_empty($value)
{
foreach ((array)$value as $v) {
if (strval($v) !== '') {
return false;
}
}
return true;
}
/**
* Returns UNICODE type based on BOM (Byte Order Mark)
*
* @param string Input string to test
*
* @return string Detected encoding
*/
private static function detect_encoding($string)
{
$fallback = rcube::get_instance()->config->get('default_charset', 'ISO-8859-1'); // fallback to Latin-1
return rcube_charset::detect($string, $fallback);
}
}
diff --git a/program/lib/Roundcube/rcube_washtml.php b/program/lib/Roundcube/rcube_washtml.php
index 8e4edde65..513bacef4 100644
--- a/program/lib/Roundcube/rcube_washtml.php
+++ b/program/lib/Roundcube/rcube_washtml.php
@@ -1,847 +1,846 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2012, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Utility class providing HTML sanityzer (based on Washtml class) |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
| Author: Frederic Motte <fmotte@ubixis.com> |
+-----------------------------------------------------------------------+
- */
-/*
- * Washtml, a HTML sanityzer.
- *
- * Copyright (c) 2007 Frederic Motte <fmotte@ubixis.com>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
- * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
- * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * OVERVIEW:
- *
- * Wahstml take an untrusted HTML and return a safe html string.
- *
- * SYNOPSIS:
- *
- * $washer = new washtml($config);
- * $washer->wash($html);
- * It return a sanityzed string of the $html parameter without html and head tags.
- * $html is a string containing the html code to wash.
- * $config is an array containing options:
- * $config['allow_remote'] is a boolean to allow link to remote resources (images/css).
- * $config['blocked_src'] string with image-src to be used for blocked remote images
- * $config['show_washed'] is a boolean to include washed out attributes as x-washed
- * $config['cid_map'] is an array where cid urls index urls to replace them.
- * $config['charset'] is a string containing the charset of the HTML document if it is not defined in it.
- * $washer->extlinks is a reference to a boolean that is set to true if remote images were removed. (FE: show remote images link)
- *
- * INTERNALS:
- *
- * Only tags and attributes in the static lists $html_elements and $html_attributes
- * are kept, inline styles are also filtered: all style identifiers matching
- * /[a-z\-]/i are allowed. Values matching colors, sizes, /[a-z\-]/i and safe
- * urls if allowed and cid urls if mapped are kept.
- *
- * Roundcube Changes:
- * - added $block_elements
- * - changed $ignore_elements behaviour
- * - added RFC2397 support
- * - base URL support
- * - invalid HTML comments removal before parsing
- * - "fixing" unitless CSS values for XHTML output
- * - SVG and MathML support
- */
+ Washtml, a HTML sanityzer.
+
+ Copyright (c) 2007 Frederic Motte <fmotte@ubixis.com>
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+ OVERVIEW:
+
+ Wahstml take an untrusted HTML and return a safe html string.
+
+ SYNOPSIS:
+
+ $washer = new washtml($config);
+ $washer->wash($html);
+ It return a sanityzed string of the $html parameter without html and head tags.
+ $html is a string containing the html code to wash.
+ $config is an array containing options:
+ $config['allow_remote'] is a boolean to allow link to remote resources (images/css).
+ $config['blocked_src'] string with image-src to be used for blocked remote images
+ $config['show_washed'] is a boolean to include washed out attributes as x-washed
+ $config['cid_map'] is an array where cid urls index urls to replace them.
+ $config['charset'] is a string containing the charset of the HTML document if it is not defined in it.
+ $washer->extlinks is a reference to a boolean that is set to true if remote images were removed. (FE: show remote images link)
+
+ INTERNALS:
+
+ Only tags and attributes in the static lists $html_elements and $html_attributes
+ are kept, inline styles are also filtered: all style identifiers matching
+ /[a-z\-]/i are allowed. Values matching colors, sizes, /[a-z\-]/i and safe
+ urls if allowed and cid urls if mapped are kept.
+
+ Roundcube Changes:
+ - added $block_elements
+ - changed $ignore_elements behaviour
+ - added RFC2397 support
+ - base URL support
+ - invalid HTML comments removal before parsing
+ - "fixing" unitless CSS values for XHTML output
+ - SVG and MathML support
+*/
/**
* Utility class providing HTML sanityzer
*
* @package Framework
* @subpackage Utils
*/
class rcube_washtml
{
/* Allowed HTML elements (default) */
static $html_elements = array('a', 'abbr', 'acronym', 'address', 'area', 'b',
'basefont', 'bdo', 'big', 'blockquote', 'br', 'caption', 'center',
'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl',
'dt', 'em', 'fieldset', 'font', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i',
'ins', 'label', 'legend', 'li', 'map', 'menu', 'nobr', 'ol', 'p', 'pre', 'q',
's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'table',
'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'wbr', 'img',
'video', 'source',
// form elements
'button', 'input', 'textarea', 'select', 'option', 'optgroup',
// SVG
'svg', 'altglyph', 'altglyphdef', 'altglyphitem', 'animate',
'animatecolor', 'animatetransform', 'circle', 'clippath', 'defs', 'desc',
'ellipse', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'line',
'lineargradient', 'marker', 'mask', 'mpath', 'path', 'pattern',
'polygon', 'polyline', 'radialgradient', 'rect', 'set', 'stop', 'switch', 'symbol',
'text', 'textpath', 'tref', 'tspan', 'use', 'view', 'vkern', 'filter',
// SVG Filters
'feblend', 'fecolormatrix', 'fecomponenttransfer', 'fecomposite',
'feconvolvematrix', 'fediffuselighting', 'fedisplacementmap',
'feflood', 'fefunca', 'fefuncb', 'fefuncg', 'fefuncr', 'fegaussianblur',
'feimage', 'femerge', 'femergenode', 'femorphology', 'feoffset',
'fespecularlighting', 'fetile', 'feturbulence',
// MathML
'math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr',
'mmuliscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow',
'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd',
'mtext', 'mtr', 'munder', 'munderover', 'maligngroup', 'malignmark',
'mprescripts', 'semantics', 'annotation', 'annotation-xml', 'none',
'infinity', 'matrix', 'matrixrow', 'ci', 'cn', 'sep', 'apply',
'plus', 'minus', 'eq', 'power', 'times', 'divide', 'csymbol', 'root',
'bvar', 'lowlimit', 'uplimit',
);
/* Ignore these HTML tags and their content */
static $ignore_elements = array('script', 'applet', 'embed', 'object', 'style');
/* Allowed HTML attributes */
static $html_attribs = array('name', 'class', 'title', 'alt', 'width', 'height',
'align', 'nowrap', 'col', 'row', 'id', 'rowspan', 'colspan', 'cellspacing',
'cellpadding', 'valign', 'bgcolor', 'color', 'border', 'bordercolorlight',
'bordercolordark', 'face', 'marginwidth', 'marginheight', 'axis', 'border',
'abbr', 'char', 'charoff', 'clear', 'compact', 'coords', 'vspace', 'hspace',
'cellborder', 'size', 'lang', 'dir', 'usemap', 'shape', 'media',
'background', 'src', 'poster', 'href', 'headers',
// attributes of form elements
'type', 'rows', 'cols', 'disabled', 'readonly', 'checked', 'multiple', 'value', 'for',
// SVG
'accent-height', 'accumulate', 'additive', 'alignment-baseline', 'alphabetic',
'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseprofile',
'baseline-shift', 'begin', 'bias', 'by', 'clip', 'clip-path', 'clip-rule',
'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile',
'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction',
'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'fill', 'fill-opacity',
'fill-rule', 'filter', 'flood-color', 'flood-opacity', 'font-family', 'font-size',
'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'from',
'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform',
'image-rendering', 'in', 'in2', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints',
'keysplines', 'keytimes', 'lengthadjust', 'letter-spacing', 'kernelmatrix',
'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid',
'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits',
'maskunits', 'max', 'mask', 'mode', 'min', 'numoctaves', 'offset', 'operator',
'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order',
'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits',
'points', 'preservealpha', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount',
'repeatdur', 'restart', 'rotate', 'scale', 'seed', 'shape-rendering', 'show', 'specularconstant',
'specularexponent', 'spreadmethod', 'stddeviation', 'stitchtiles', 'stop-color',
'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap',
'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width',
'surfacescale', 'targetx', 'targety', 'transform', 'text-anchor', 'text-decoration',
'text-rendering', 'textlength', 'to', 'u1', 'u2', 'unicode', 'values', 'viewbox',
'visibility', 'vert-adv-y', 'version', 'vert-origin-x', 'vert-origin-y', 'word-spacing',
'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2',
'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan',
// MathML
'accent', 'accentunder', 'bevelled', 'close', 'columnalign', 'columnlines',
'columnspan', 'denomalign', 'depth', 'display', 'displaystyle', 'encoding', 'fence',
'frame', 'largeop', 'length', 'linethickness', 'lspace', 'lquote',
'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize',
'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign',
'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel',
'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator',
'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset',
'fontsize', 'fontweight', 'fontstyle', 'fontfamily', 'groupalign', 'edge', 'side',
);
/* Elements which could be empty and be returned in short form (<tag />) */
static $void_elements = array('area', 'base', 'br', 'col', 'command', 'embed', 'hr',
'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr',
// MathML
'sep', 'infinity', 'in', 'plus', 'eq', 'power', 'times', 'divide', 'root',
'maligngroup', 'none', 'mprescripts',
);
/* State for linked objects in HTML */
public $extlinks = false;
/* Current settings */
private $config = array();
/* Registered callback functions for tags */
private $handlers = array();
/* Allowed HTML elements */
private $_html_elements = array();
/* Ignore these HTML tags but process their content */
private $_ignore_elements = array();
/* Elements which could be empty and be returned in short form (<tag />) */
private $_void_elements = array();
/* Allowed HTML attributes */
private $_html_attribs = array();
/* A prefix to be added to id/class/for attribute values */
private $_css_prefix;
/* Max nesting level */
private $max_nesting_level;
private $is_xml = false;
/**
* Class constructor
*/
public function __construct($p = array())
{
$this->_html_elements = array_flip((array)$p['html_elements']) + array_flip(self::$html_elements);
$this->_html_attribs = array_flip((array)$p['html_attribs']) + array_flip(self::$html_attribs);
$this->_ignore_elements = array_flip((array)$p['ignore_elements']) + array_flip(self::$ignore_elements);
$this->_void_elements = array_flip((array)$p['void_elements']) + array_flip(self::$void_elements);
$this->_css_prefix = is_string($p['css_prefix']) && strlen($p['css_prefix']) ? $p['css_prefix'] : null;
unset($p['html_elements'], $p['html_attribs'], $p['ignore_elements'], $p['void_elements']);
$this->config = $p + array('show_washed' => true, 'allow_remote' => false, 'cid_map' => array());
}
/**
* Register a callback function for a certain tag
*/
public function add_callback($tagName, $callback)
{
$this->handlers[$tagName] = $callback;
}
/**
* Check CSS style
*/
private function wash_style($style)
{
$result = array();
// Remove unwanted white-space characters so regular expressions below work better
$style = preg_replace('/[\n\r\s\t]+/', ' ', $style);
// Decode insecure character sequences
$style = rcube_utils::xss_entity_decode($style);
foreach (explode(';', $style) as $declaration) {
if (preg_match('/^\s*([a-z\\\-]+)\s*:\s*(.*)\s*$/i', $declaration, $match)) {
$cssid = $match[1];
$str = $match[2];
$value = '';
foreach ($this->explode_style($str) as $val) {
if (preg_match('/^url\(/i', $val)) {
if (preg_match('/^url\(\s*[\'"]?([^\'"\)]*)[\'"]?\s*\)/iu', $val, $match)) {
if ($url = $this->wash_uri($match[1])) {
$value .= ' url(' . htmlspecialchars($url, ENT_QUOTES, $this->config['charset']) . ')';
}
}
}
else if (!preg_match('/^(behavior|expression)/i', $val)) {
// Set position:fixed to position:absolute for security (#5264)
if (!strcasecmp($cssid, 'position') && !strcasecmp($val, 'fixed')) {
$val = 'absolute';
}
// whitelist ?
$value .= ' ' . $val;
// #1488535: Fix size units, so width:800 would be changed to width:800px
if (preg_match('/^(left|right|top|bottom|width|height)/i', $cssid)
&& preg_match('/^[0-9]+$/', $val)
) {
$value .= 'px';
}
}
}
if (isset($value[0])) {
$result[] = $cssid . ':' . $value;
}
}
}
return implode('; ', $result);
}
/**
* Take a node and return allowed attributes and check values
*/
private function wash_attribs($node)
{
$result = '';
$washed = array();
foreach ($node->attributes as $name => $attr) {
$key = strtolower($name);
$value = $attr->nodeValue;
if ($key == 'style' && ($style = $this->wash_style($value))) {
// replace double quotes to prevent syntax error and XSS issues (#1490227)
$result .= ' style="' . str_replace('"', '&quot;', $style) . '"';
}
else if (isset($this->_html_attribs[$key])) {
$value = trim($value);
$out = null;
// in SVG to/from attribs may contain anything, including URIs
if ($key == 'to' || $key == 'from') {
$key = strtolower($node->getAttribute('attributeName'));
if ($key && !isset($this->_html_attribs[$key])) {
$key = null;
}
}
if ($this->is_image_attribute($node->nodeName, $key)) {
$out = $this->wash_uri($value, true);
}
else if ($this->is_link_attribute($node->nodeName, $key)) {
if (!preg_match('!^(javascript|vbscript|data:text)!i', $value)
&& preg_match('!^([a-z][a-z0-9.+-]+:|//|#).+!i', $value)
) {
$out = $value;
}
}
else if ($this->is_funciri_attribute($node->nodeName, $key)) {
if (preg_match('/^[a-z:]*url\(/i', $val)) {
if (preg_match('/^([a-z:]*url)\(\s*[\'"]?([^\'"\)]*)[\'"]?\s*\)/iu', $value, $match)) {
if ($url = $this->wash_uri($match[2])) {
$result .= ' ' . $attr->nodeName . '="' . $match[1]
. '(' . htmlspecialchars($url, ENT_QUOTES, $this->config['charset']) . ')'
. substr($val, strlen($match[0])) . '"';
continue;
}
}
else {
$out = $value;
}
}
else {
$out = $value;
}
}
else if ($this->_css_prefix !== null && in_array($key, array('id', 'class', 'for'))) {
$out = preg_replace('/(\S+)/', $this->_css_prefix . '\1', $value);
}
else if ($key) {
$out = $value;
}
if ($out !== null && $out !== '') {
$result .= ' ' . $attr->nodeName . '="' . htmlspecialchars($out, ENT_QUOTES | ENT_SUBSTITUTE, $this->config['charset']) . '"';
}
else if ($value) {
$washed[] = htmlspecialchars($attr->nodeName, ENT_QUOTES, $this->config['charset']);
}
}
else {
$washed[] = htmlspecialchars($attr->nodeName, ENT_QUOTES, $this->config['charset']);
}
}
if (!empty($washed) && $this->config['show_washed']) {
$result .= ' x-washed="' . implode(' ', $washed) . '"';
}
return $result;
}
/**
* Wash URI value
*/
private function wash_uri($uri, $blocked_source = false, $is_image = true)
{
if (($src = $this->config['cid_map'][$uri])
|| ($src = $this->config['cid_map'][$this->config['base_url'].$uri])
) {
return $src;
}
// allow url(#id) used in SVG
if ($uri[0] == '#') {
return $uri;
}
if (preg_match('/^(http|https|ftp):.+/i', $uri)) {
if ($this->config['allow_remote']) {
return $uri;
}
$this->extlinks = true;
if ($is_image && $blocked_source && $this->config['blocked_src']) {
return $this->config['blocked_src'];
}
}
else if ($is_image && preg_match('/^data:image.+/i', $uri)) { // RFC2397
return $uri;
}
}
/**
* Check it the tag/attribute may contain an URI
*/
private function is_link_attribute($tag, $attr)
{
return ($tag == 'a' || $tag == 'area') && $attr == 'href';
}
/**
* Check it the tag/attribute may contain an image URI
*/
private function is_image_attribute($tag, $attr)
{
return $attr == 'background'
|| $attr == 'color-profile' // SVG
|| ($attr == 'poster' && $tag == 'video')
|| ($attr == 'src' && preg_match('/^(img|image|source|input|video|audio)$/i', $tag))
|| ($tag == 'image' && $attr == 'href'); // SVG
}
/**
* Check it the tag/attribute may contain a FUNCIRI value
*/
private function is_funciri_attribute($tag, $attr)
{
return in_array($attr, array('fill', 'filter', 'stroke', 'marker-start',
'marker-end', 'marker-mid', 'clip-path', 'mask', 'cursor'));
}
/**
* The main loop that recurse on a node tree.
* It output only allowed tags with allowed attributes and allowed inline styles
*
* @param DOMNode $node HTML element
* @param int $level Recurrence level (safe initial value found empirically)
*/
private function dumpHtml($node, $level = 20)
{
if (!$node->hasChildNodes()) {
return '';
}
$level++;
if ($this->max_nesting_level > 0 && $level == $this->max_nesting_level - 1) {
// log error message once
if (!$this->max_nesting_level_error) {
$this->max_nesting_level_error = true;
rcube::raise_error(array('code' => 500, 'type' => 'php',
'line' => __LINE__, 'file' => __FILE__,
'message' => "Maximum nesting level exceeded (xdebug.max_nesting_level={$this->max_nesting_level})"),
true, false);
}
return '<!-- ignored -->';
}
$node = $node->firstChild;
$dump = '';
do {
switch ($node->nodeType) {
case XML_ELEMENT_NODE: //Check element
$tagName = strtolower($node->nodeName);
if ($tagName == 'link') {
$uri = $this->wash_uri($node->getAttribute('href'), false, false);
if (!$uri) {
$dump .= '<!-- link ignored -->';
break;
}
$node->setAttribute('href', (string) $uri);
}
if ($callback = $this->handlers[$tagName]) {
$dump .= call_user_func($callback, $tagName,
$this->wash_attribs($node), $this->dumpHtml($node, $level), $this);
}
else if (isset($this->_html_elements[$tagName])) {
$content = $this->dumpHtml($node, $level);
$dump .= '<' . $node->nodeName;
if ($tagName == 'svg') {
$xpath = new DOMXPath($node->ownerDocument);
foreach ($xpath->query('namespace::*') as $ns) {
if ($ns->nodeName != 'xmlns:xml') {
$dump .= ' ' . $ns->nodeName . '="' . $ns->nodeValue . '"';
}
}
}
else if ($tagName == 'textarea' && strpos($content, '<') !== false) {
$content = htmlspecialchars($content, ENT_QUOTES | ENT_SUBSTITUTE, $this->config['charset']);
}
$dump .= $this->wash_attribs($node);
if ($content === '' && ($this->is_xml || isset($this->_void_elements[$tagName]))) {
$dump .= ' />';
}
else {
$dump .= '>' . $content . '</' . $node->nodeName . '>';
}
}
else if (isset($this->_ignore_elements[$tagName])) {
$dump .= '<!-- ' . htmlspecialchars($node->nodeName, ENT_QUOTES, $this->config['charset']) . ' not allowed -->';
}
else {
$dump .= '<!-- ' . htmlspecialchars($node->nodeName, ENT_QUOTES, $this->config['charset']) . ' ignored -->';
$dump .= $this->dumpHtml($node, $level); // ignore tags not its content
}
break;
case XML_CDATA_SECTION_NODE:
$dump .= $node->nodeValue;
break;
case XML_TEXT_NODE:
$dump .= htmlspecialchars($node->nodeValue, ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, $this->config['charset']);
break;
case XML_HTML_DOCUMENT_NODE:
$dump .= $this->dumpHtml($node, $level);
break;
}
}
while($node = $node->nextSibling);
return $dump;
}
/**
* Main function, give it untrusted HTML, tell it if you allow loading
* remote images and give it a map to convert "cid:" urls.
*/
public function wash($html)
{
$this->extlinks = false;
$html = $this->cleanup($html);
// Find base URL for images
if (preg_match('/<base\s+href=[\'"]*([^\'"]+)/is', $html, $matches)) {
$this->config['base_url'] = $matches[1];
}
else {
$this->config['base_url'] = '';
}
// Detect max nesting level (for dumpHTML) (#1489110)
$this->max_nesting_level = (int) @ini_get('xdebug.max_nesting_level');
// SVG need to be parsed as XML
$this->is_xml = stripos($html, '<html') === false && stripos($html, '<svg') !== false;
$method = $this->is_xml ? 'loadXML' : 'loadHTML';
// DOMDocument does not support HTML5, try Masterminds parser if available
if (!$this->is_xml && class_exists('Masterminds\HTML5')) {
try {
$html5 = new Masterminds\HTML5();
$node = $html5->loadHTML($this->fix_html5($html));
}
catch (Exception $e) {
// ignore, fallback to DOMDocument
}
}
if (empty($node)) {
// Charset seems to be ignored (probably if defined in the HTML document)
$node = new DOMDocument('1.0', $this->config['charset']);
@$node->{$method}($html, LIBXML_PARSEHUGE | LIBXML_COMPACT | LIBXML_NONET);
}
return $this->dumpHtml($node);
}
/**
* Getter for config parameters
*/
public function get_config($prop)
{
return $this->config[$prop];
}
/**
* Clean HTML input
*/
private function cleanup($html)
{
$html = trim($html);
// special replacements (not properly handled by washtml class)
$html_search = array(
// space(s) between <NOBR>
'/(<\/nobr>)(\s+)(<nobr>)/i',
// PHP bug #32547 workaround: remove title tag
'/<title[^>]*>[^<]*<\/title>/i',
// remove <!doctype> before BOM (#1490291)
'/<\!doctype[^>]+>[^<]*/im',
// byte-order mark (only outlook?)
'/^(\0\0\xFE\xFF|\xFF\xFE\0\0|\xFE\xFF|\xFF\xFE|\xEF\xBB\xBF)/',
// washtml/DOMDocument cannot handle xml namespaces
'/<html\s[^>]+>/i',
// washtml/DOMDocument cannot handle xml namespaces
'/<\?xml:namespace\s[^>]+>/i',
);
$html_replace = array(
'\\1'.' &nbsp; '.'\\3',
'',
'',
'',
'<html>',
'',
);
$html = preg_replace($html_search, $html_replace, $html);
$err = array('line' => __LINE__, 'file' => __FILE__, 'message' => "Could not clean up HTML!");
if ($html === null && rcube_utils::preg_error($err)) {
return '';
}
// Replace all of those weird MS Word quotes and other high characters
$badwordchars = array(
"\xe2\x80\x98", // left single quote
"\xe2\x80\x99", // right single quote
"\xe2\x80\x9c", // left double quote
"\xe2\x80\x9d", // right double quote
"\xe2\x80\x94", // em dash
"\xe2\x80\xa6" // elipses
);
$fixedwordchars = array(
"'",
"'",
'"',
'"',
'&mdash;',
'...'
);
$html = str_replace($badwordchars, $fixedwordchars, $html);
// FIXME: HTML comments handling could be better. The code below can break comments (#6464),
// we should probably do not modify content inside comments at all.
// fix (unknown/malformed) HTML tags before "wash"
$html = preg_replace_callback('/(<(?!\!)[\/]*)([^\s>]+)([^>]*)/', array($this, 'html_tag_callback'), $html);
// Remove invalid HTML comments (#1487759)
// Note: We don't want to remove valid comments, conditional comments
// and MSOutlook comments (<!-->)
$html = preg_replace('/<!--[a-zA-Z0-9]+>/', '', $html);
// fix broken nested lists
self::fix_broken_lists($html);
// turn relative into absolute urls
$html = self::resolve_base($html);
return $html;
}
/**
* Callback function for HTML tags fixing
*/
public static function html_tag_callback($matches)
{
// It might be an ending of a comment, ignore (#6464)
if (substr($matches[3], -2) == '--') {
$matches[0] = '';
return implode('', $matches);
}
$tagname = $matches[2];
$tagname = preg_replace(array(
'/:.*$/', // Microsoft's Smart Tags <st1:xxxx>
'/[^a-z0-9_\[\]\!?-]/i', // forbidden characters
), '', $tagname);
// fix invalid closing tags - remove any attributes (#1489446)
if ($matches[1] == '</') {
$matches[3] = '';
}
return $matches[1] . $tagname . $matches[3];
}
/**
* Convert all relative URLs according to a <base> in HTML
*/
public static function resolve_base($body)
{
// check for <base href=...>
if (preg_match('!(<base.*href=["\']?)([hftps]{3,5}://[a-z0-9/.%-]+)!i', $body, $regs)) {
$replacer = new rcube_base_replacer($regs[2]);
$body = $replacer->replace($body);
}
return $body;
}
/**
* Fix broken nested lists, they are not handled properly by DOMDocument (#1488768)
*/
public static function fix_broken_lists(&$html)
{
// do two rounds, one for <ol>, one for <ul>
foreach (array('ol', 'ul') as $tag) {
$pos = 0;
while (($pos = stripos($html, '<' . $tag, $pos)) !== false) {
$pos++;
// make sure this is an ol/ul tag
if (!in_array($html[$pos+2], array(' ', '>'))) {
continue;
}
$p = $pos;
$in_li = false;
$li_pos = 0;
while (($p = strpos($html, '<', $p)) !== false) {
$tt = strtolower(substr($html, $p, 4));
// li open tag
if ($tt == '<li>' || $tt == '<li ') {
$in_li = true;
$p += 4;
}
// li close tag
else if ($tt == '</li' && in_array($html[$p+4], array(' ', '>'))) {
$li_pos = $p;
$p += 4;
$in_li = false;
}
// ul/ol closing tag
else if ($tt == '</' . $tag && in_array($html[$p+4], array(' ', '>'))) {
break;
}
// nested ol/ul element out of li
else if (!$in_li && $li_pos && ($tt == '<ol>' || $tt == '<ol ' || $tt == '<ul>' || $tt == '<ul ')) {
// find closing tag of this ul/ol element
$element = substr($tt, 1, 2);
$cpos = $p;
do {
$tpos = stripos($html, '<' . $element, $cpos+1);
$cpos = stripos($html, '</' . $element, $cpos+1);
}
while ($tpos !== false && $cpos !== false && $cpos > $tpos);
// not found, this is invalid HTML, skip it
if ($cpos === false) {
break;
}
// get element content
$end = strpos($html, '>', $cpos);
$len = $end - $p + 1;
$element = substr($html, $p, $len);
// move element to the end of the last li
$html = substr_replace($html, '', $p, $len);
$html = substr_replace($html, $element, $li_pos, 0);
$p = $end;
}
else {
$p++;
}
}
}
}
}
/**
* Cleanup and workarounds on input to Masterminds/HTML5
*/
protected function fix_html5($html)
{
// HTML5 requires <head> or <body> (#6713)
// https://github.com/Masterminds/html5-php/issues/166
if (!preg_match('/<(head|body)/i', $html)) {
$pos = stripos($html, '<html');
if ($pos === false) {
$html = '<html><body>' . $html;
}
else {
$pos = strpos($html, '>', $pos);
$html = substr_replace($html, '<body>', $pos + 1, 0);
}
}
return $html;
}
/**
* Explode css style value
*/
protected function explode_style($style)
{
$pos = 0;
// first remove comments
while (($pos = strpos($style, '/*', $pos)) !== false) {
$end = strpos($style, '*/', $pos+2);
if ($end === false) {
$style = substr($style, 0, $pos);
}
else {
$style = substr_replace($style, '', $pos, $end - $pos + 2);
}
}
$style = trim($style);
$strlen = strlen($style);
$result = array();
// explode value
for ($p=$i=0; $i < $strlen; $i++) {
if (($style[$i] == "\"" || $style[$i] == "'") && $style[$i-1] != "\\") {
if ($q == $style[$i]) {
$q = false;
}
else if (!$q) {
$q = $style[$i];
}
}
if (!$q && $style[$i] == ' ' && !preg_match('/[,\(]/', $style[$i-1])) {
$result[] = substr($style, $p, $i - $p);
$p = $i + 1;
}
}
$result[] = (string) substr($style, $p);
return $result;
}
}
diff --git a/program/lib/Roundcube/session/db.php b/program/lib/Roundcube/session/db.php
index 269a80c12..a6f8d87a1 100644
--- a/program/lib/Roundcube/session/db.php
+++ b/program/lib/Roundcube/session/db.php
@@ -1,182 +1,183 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2014, The Roundcube Dev Team |
- | Copyright (C) 2011, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide database supported session management |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
| Author: Cor Bosman <cor@roundcu.be> |
+-----------------------------------------------------------------------+
*/
/**
* Class to provide database session storage
*
* @package Framework
* @subpackage Core
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
* @author Cor Bosman <cor@roundcu.be>
*/
class rcube_session_db extends rcube_session
{
private $db;
private $table_name;
/**
* @param Object $config
*/
public function __construct($config)
{
parent::__construct($config);
// get db instance
$this->db = rcube::get_instance()->get_dbh();
// session table name
$this->table_name = $this->db->table_name('session', true);
// register sessions handler
$this->register_session_handler();
// register db gc handler
$this->register_gc_handler(array($this, 'gc_db'));
}
/**
* @param $save_path
* @param $session_name
* @return bool
*/
public function open($save_path, $session_name)
{
return true;
}
/**
* @return bool
*/
public function close()
{
return true;
}
/**
* Handler for session_destroy()
*
* @param $key
* @return bool
*/
public function destroy($key)
{
if ($key) {
$this->db->query("DELETE FROM {$this->table_name} WHERE `sess_id` = ?", $key);
}
return true;
}
/**
* Read session data from database
*
* @param string Session ID
*
* @return string Session vars
*/
public function read($key)
{
$sql_result = $this->db->query(
"SELECT `vars`, `ip`, `changed`, " . $this->db->now() . " AS ts"
. " FROM {$this->table_name} WHERE `sess_id` = ?", $key);
if ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
$this->time_diff = time() - strtotime($sql_arr['ts']);
$this->changed = strtotime($sql_arr['changed']);
$this->ip = $sql_arr['ip'];
$this->vars = base64_decode($sql_arr['vars']);
$this->key = $key;
$this->db->reset();
return !empty($this->vars) ? (string) $this->vars : '';
}
return '';
}
/**
* insert new data into db session store
*
* @param $key
* @param $vars
* @return bool
*/
public function write($key, $vars)
{
if ($this->ignore_write) {
return true;
}
$now = $this->db->now();
$this->db->query("INSERT INTO {$this->table_name}"
. " (`sess_id`, `vars`, `ip`, `changed`)"
. " VALUES (?, ?, ?, $now)",
$key, base64_encode($vars), (string)$this->ip);
return true;
}
/**
* update session data
*
* @param $key
* @param $newvars
* @param $oldvars
*
* @return bool
*/
public function update($key, $newvars, $oldvars)
{
$now = $this->db->now();
$ts = microtime(true);
// if new and old data are not the same, update data
// else update expire timestamp only when certain conditions are met
if ($newvars !== $oldvars) {
$this->db->query("UPDATE {$this->table_name} "
. "SET `changed` = $now, `vars` = ? WHERE `sess_id` = ?",
base64_encode($newvars), $key);
}
else if ($ts - $this->changed + $this->time_diff > $this->lifetime / 2) {
$this->db->query("UPDATE {$this->table_name} SET `changed` = $now"
. " WHERE `sess_id` = ?", $key);
}
return true;
}
/**
* Clean up db sessions.
*/
public function gc_db()
{
// just clean all old sessions when this GC is called
$this->db->query("DELETE FROM " . $this->db->table_name('session')
. " WHERE changed < " . $this->db->now(-$this->gc_enabled));
$this->log("Session GC (DB): remove records < "
. date('Y-m-d H:i:s', time() - $this->gc_enabled)
. '; rows = ' . intval($this->db->affected_rows()));
}
}
diff --git a/program/lib/Roundcube/session/memcache.php b/program/lib/Roundcube/session/memcache.php
index 8210bd6d2..b4d875415 100644
--- a/program/lib/Roundcube/session/memcache.php
+++ b/program/lib/Roundcube/session/memcache.php
@@ -1,183 +1,184 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2014, The Roundcube Dev Team |
- | Copyright (C) 2011, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide database supported session management |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
| Author: Cor Bosman <cor@roundcu.bet> |
+-----------------------------------------------------------------------+
*/
/**
* Class to provide memcache session storage
*
* @package Framework
* @subpackage Core
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
* @author Cor Bosman <cor@roundcu.be>
*/
class rcube_session_memcache extends rcube_session
{
private $memcache;
private $debug;
/**
* @param Object $config
*/
public function __construct($config)
{
parent::__construct($config);
$this->memcache = rcube::get_instance()->get_memcache();
$this->debug = $config->get('memcache_debug');
if (!$this->memcache) {
rcube::raise_error(array(
'code' => 604, 'type' => 'db',
'line' => __LINE__, 'file' => __FILE__,
'message' => "Failed to connect to memcached. Please check configuration"),
true, true);
}
// register sessions handler
$this->register_session_handler();
}
/**
* @param $save_path
* @param $session_name
* @return bool
*/
public function open($save_path, $session_name)
{
return true;
}
/**
* @return bool
*/
public function close()
{
return true;
}
/**
* Handler for session_destroy() with memcache backend
*
* @param $key
* @return bool
*/
public function destroy($key)
{
if ($key) {
// #1488592: use 2nd argument
$result = $this->memcache->delete($key, 0);
if ($this->debug) {
$this->debug('delete', $key, null, $result);
}
}
return true;
}
/**
* Read session data from memcache
*
* @param $key
* @return null|string
*/
public function read($key)
{
if ($value = $this->memcache->get($key)) {
$arr = unserialize($value);
$this->changed = $arr['changed'];
$this->ip = $arr['ip'];
$this->vars = $arr['vars'];
$this->key = $key;
}
if ($this->debug) {
$this->debug('get', $key, $value);
}
return $this->vars ?: '';
}
/**
* Write data to memcache storage
*
* @param $key
* @param $vars
*
* @return bool
*/
public function write($key, $vars)
{
if ($this->ignore_write) {
return true;
}
$data = serialize(array('changed' => time(), 'ip' => $this->ip, 'vars' => $vars));
$result = $this->memcache->set($key, $data, MEMCACHE_COMPRESSED, $this->lifetime + 60);
if ($this->debug) {
$this->debug('set', $key, $data, $result);
}
return $result;
}
/**
* Update memcache session data
*
* @param $key
* @param $newvars
* @param $oldvars
*
* @return bool
*/
public function update($key, $newvars, $oldvars)
{
$ts = microtime(true);
if ($newvars !== $oldvars || $ts - $this->changed > $this->lifetime / 3) {
$data = serialize(array('changed' => time(), 'ip' => $this->ip, 'vars' => $newvars));
$result = $this->memcache->set($key, $data, MEMCACHE_COMPRESSED, $this->lifetime + 60);
if ($this->debug) {
$this->debug('set', $key, $data, $result);
}
return $result;
}
return true;
}
/**
* Write memcache debug info to the log
*/
protected function debug($type, $key, $data = null, $result = null)
{
$line = strtoupper($type) . ' ' . $key;
if ($data !== null) {
$line .= ' ' . $data;
}
rcube::debug('memcache', $line, $result);
}
}
diff --git a/program/lib/Roundcube/session/php.php b/program/lib/Roundcube/session/php.php
index 67b1dce7a..3f5af7d2d 100644
--- a/program/lib/Roundcube/session/php.php
+++ b/program/lib/Roundcube/session/php.php
@@ -1,76 +1,77 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2014, The Roundcube Dev Team |
- | Copyright (C) 2011, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide database supported session management |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
| Author: Cor Bosman <cor@roundcu.be> |
+-----------------------------------------------------------------------+
*/
/**
* Class to provide native php session storage
*
* @package Framework
* @subpackage Core
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
* @author Cor Bosman <cor@roundcu.be>
*/
class rcube_session_php extends rcube_session {
/**
* native php sessions don't need a save handler
* we do need to define abstract function implementations but they are not used.
*/
public function open($save_path, $session_name) {}
public function close() {}
public function destroy($key) {}
public function read($key) {}
public function write($key, $vars) {}
public function update($key, $newvars, $oldvars) {}
/**
* @param Object $config
*/
public function __construct($config)
{
parent::__construct($config);
}
/**
* Wrapper for session_write_close()
*/
public function write_close()
{
$_SESSION['__IP'] = $this->ip;
$_SESSION['__MTIME'] = time();
parent::write_close();
}
/**
* Wrapper for session_start()
*/
public function start()
{
parent::start();
$this->key = session_id();
$this->ip = $_SESSION['__IP'];
$this->changed = $_SESSION['__MTIME'];
}
}
diff --git a/program/lib/Roundcube/session/redis.php b/program/lib/Roundcube/session/redis.php
index 8a810571d..898d97ae2 100644
--- a/program/lib/Roundcube/session/redis.php
+++ b/program/lib/Roundcube/session/redis.php
@@ -1,168 +1,169 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2018, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide redis supported session management |
+-----------------------------------------------------------------------+
| Author: Cor Bosman <cor@roundcu.be> |
+-----------------------------------------------------------------------+
*/
/**
* Class to provide redis session storage
*
* @package Framework
* @subpackage Core
* @author Cor Bosman <cor@roundcu.be>
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_session_redis extends rcube_session {
private $redis;
private $debug;
/**
* @param rcube_config $config
*/
public function __construct($config)
{
parent::__construct($config);
$this->redis = rcube::get_instance()->get_redis();
$this->debug = $config->get('redis_debug');
// register sessions handler
$this->register_session_handler();
}
/**
* @param $save_path
* @param $session_name
* @return bool
*/
public function open($save_path, $session_name)
{
return true;
}
/**
* @return bool
*/
public function close()
{
return true;
}
/**
* remove data from store
*
* @param $key
* @return bool
*/
public function destroy($key)
{
if ($key) {
$result = $this->redis->del($key);
if ($this->debug) {
$this->debug('delete', $key, null, $result);
}
}
return true;
}
/**
* read data from redis store
*
* @param $key
* @return string
*/
public function read($key)
{
if ($value = $this->redis->get($key)) {
$arr = unserialize($value);
$this->changed = $arr['changed'];
$this->ip = $arr['ip'];
$this->vars = $arr['vars'];
$this->key = $key;
}
if ($this->debug) {
$this->debug('get', $key, $value);
}
return $this->vars ?: '';
}
/**
* write data to redis store
*
* @param $key
* @param $newvars
* @param $oldvars
* @return bool
*/
public function update($key, $newvars, $oldvars)
{
$ts = microtime(true);
if ($newvars !== $oldvars || $ts - $this->changed > $this->lifetime / 3) {
$data = serialize(array('changed' => time(), 'ip' => $this->ip, 'vars' => $newvars));
$result = $this->redis->setex($key, $this->lifetime + 60, $data);
if ($this->debug) {
$this->debug('set', $key, $data, $result);
}
return $result;
}
return true;
}
/**
* write data to redis store
*
* @param $key
* @param $vars
* @return bool
*/
public function write($key, $vars)
{
if ($this->ignore_write) {
return true;
}
$data = serialize(array('changed' => time(), 'ip' => $this->ip, 'vars' => $vars));
$result = $this->redis->setex($key, $this->lifetime + 60, $data);
if ($this->debug) {
$this->debug('set', $key, $data, $result);
}
return $result;
}
/**
* Write memcache debug info to the log
*/
protected function debug($type, $key, $data = null, $result = null)
{
$line = strtoupper($type) . ' ' . $key;
if ($data !== null) {
$line .= ' ' . $data;
}
rcube::debug('redis', $line, $result);
}
}
diff --git a/program/lib/Roundcube/spellchecker/atd.php b/program/lib/Roundcube/spellchecker/atd.php
index c8e924355..4708e70c7 100644
--- a/program/lib/Roundcube/spellchecker/atd.php
+++ b/program/lib/Roundcube/spellchecker/atd.php
@@ -1,202 +1,202 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
- | Copyright (C) 2013, The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Spellchecking backend implementation for afterthedeadline services |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Spellchecking backend implementation to work with an After the Deadline service
* See http://www.afterthedeadline.com/ for more information
*
* @package Framework
* @subpackage Utils
*/
class rcube_spellchecker_atd extends rcube_spellchecker_engine
{
const SERVICE_HOST = 'service.afterthedeadline.com';
const SERVICE_PORT = 80;
private $matches = array();
private $content;
private $langhosts = array(
'fr' => 'fr.',
'de' => 'de.',
'pt' => 'pt.',
'es' => 'es.',
);
/**
* Return a list of languages supported by this backend
*
* @see rcube_spellchecker_engine::languages()
*/
function languages()
{
$langs = array_values($this->langhosts);
$langs[] = 'en';
return $langs;
}
/**
* Set content and check spelling
*
* @see rcube_spellchecker_engine::check()
*/
function check($text)
{
$this->content = $text;
// spell check uri is configured
$rcube = rcube::get_instance();
$url = $rcube->config->get('spellcheck_uri');
$key = $rcube->config->get('spellcheck_atd_key');
if ($url) {
$a_uri = parse_url($url);
$ssl = ($a_uri['scheme'] == 'https' || $a_uri['scheme'] == 'ssl');
$port = $a_uri['port'] ?: ($ssl ? 443 : 80);
$host = ($ssl ? 'ssl://' : '') . $a_uri['host'];
$path = $a_uri['path'] . ($a_uri['query'] ? '?'.$a_uri['query'] : '') . $this->lang;
}
else {
$host = self::SERVICE_HOST;
$port = self::SERVICE_PORT;
$path = '/checkDocument';
// prefix host for other languages than 'en'
$lang = substr($this->lang, 0, 2);
if ($this->langhosts[$lang])
$host = $this->langhosts[$lang] . $host;
}
$postdata = 'data=' . urlencode($text);
if (!empty($key))
$postdata .= '&key=' . urlencode($key);
$response = $headers = '';
$in_header = true;
if ($fp = fsockopen($host, $port, $errno, $errstr, 30)) {
$out = "POST $path HTTP/1.0\r\n";
$out .= "Host: " . str_replace('ssl://', '', $host) . "\r\n";
$out .= "Content-Length: " . strlen($postdata) . "\r\n";
$out .= "Content-Type: application/x-www-form-urlencoded\r\n";
$out .= "Connection: Close\r\n\r\n";
$out .= $postdata;
fwrite($fp, $out);
while (!feof($fp)) {
if ($in_header) {
$line = fgets($fp, 512);
$headers .= $line;
if (trim($line) == '')
$in_header = false;
}
else {
$response .= fgets($fp, 1024);
}
}
fclose($fp);
}
// parse HTTP response headers
if (preg_match('!^HTTP/1.\d (\d+)(.+)!', $headers, $m)) {
$http_status = $m[1];
if ($http_status != '200')
$this->error = 'HTTP ' . $m[1] . $m[2];
}
if (!$response) {
$this->error = "Empty result from spelling engine";
}
try {
$result = new SimpleXMLElement($response);
}
catch (Exception $e) {
$this->error = "Unexpected response from server: " . $response;
return array();
}
foreach ($result->error as $error) {
if (strval($error->type) == 'spelling') {
$word = strval($error->string);
// skip exceptions
if ($this->dictionary->is_exception($word)) {
continue;
}
$prefix = strval($error->precontext);
$start = $prefix ? mb_strpos($text, $prefix) : 0;
$pos = mb_strpos($text, $word, $start);
$len = mb_strlen($word);
$num = 0;
$match = array($word, $pos, $len, null, array());
foreach ($error->suggestions->option as $option) {
$match[4][] = strval($option);
if (++$num == self::MAX_SUGGESTIONS)
break;
}
$matches[] = $match;
}
}
$this->matches = $matches;
return $matches;
}
/**
* Returns suggestions for the specified word
*
* @see rcube_spellchecker_engine::get_words()
*/
function get_suggestions($word)
{
$matches = $word ? $this->check($word) : $this->matches;
if ($matches[0][4]) {
return $matches[0][4];
}
return array();
}
/**
* Returns misspelled words
*
* @see rcube_spellchecker_engine::get_suggestions()
*/
function get_words($text = null)
{
if ($text) {
$matches = $this->check($text);
}
else {
$matches = $this->matches;
$text = $this->content;
}
$result = array();
foreach ($matches as $m) {
$result[] = mb_substr($text, $m[1], $m[2], RCUBE_CHARSET);
}
return $result;
}
}
diff --git a/program/lib/Roundcube/spellchecker/enchant.php b/program/lib/Roundcube/spellchecker/enchant.php
index b0bdf7313..4287a9753 100644
--- a/program/lib/Roundcube/spellchecker/enchant.php
+++ b/program/lib/Roundcube/spellchecker/enchant.php
@@ -1,185 +1,185 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
- | Copyright (C) 2011-2013, Kolab Systems AG |
- | Copyright (C) 20011-2013, The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Spellchecking backend implementation to work with Enchant |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+-----------------------------------------------------------------------+
*/
/**
* Spellchecking backend implementation to work with Pspell
*
* @package Framework
* @subpackage Utils
*/
class rcube_spellchecker_enchant extends rcube_spellchecker_engine
{
private $enchant_broker;
private $enchant_dictionary;
private $matches = array();
/**
* Return a list of languages supported by this backend
*
* @see rcube_spellchecker_engine::languages()
*/
function languages()
{
$this->init();
if (!$this->enchant_broker) {
return;
}
$langs = array();
if ($dicts = enchant_broker_list_dicts($this->enchant_broker)) {
foreach ($dicts as $dict) {
$langs[] = preg_replace('/-.*$/', '', $dict['lang_tag']);
}
}
return array_unique($langs);
}
/**
* Initializes Enchant dictionary
*/
private function init()
{
if (!$this->enchant_broker) {
if (!extension_loaded('enchant')) {
$this->error = "Enchant extension not available";
return;
}
$this->enchant_broker = enchant_broker_init();
}
if (!enchant_broker_dict_exists($this->enchant_broker, $this->lang)) {
$this->error = "Unable to load dictionary for selected language using Enchant";
return;
}
$this->enchant_dictionary = enchant_broker_request_dict($this->enchant_broker, $this->lang);
}
/**
* Set content and check spelling
*
* @see rcube_spellchecker_engine::check()
*/
function check($text)
{
$this->init();
if (!$this->enchant_dictionary) {
return array();
}
// tokenize
$text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
$diff = 0;
$matches = array();
foreach ($text as $w) {
$word = trim($w[0]);
$pos = $w[1] - $diff;
$len = mb_strlen($word);
// skip exceptions
if ($this->dictionary->is_exception($word)) {
}
else if (!enchant_dict_check($this->enchant_dictionary, $word)) {
$suggestions = enchant_dict_suggest($this->enchant_dictionary, $word);
if (is_array($suggestions) && count($suggestions) > self::MAX_SUGGESTIONS) {
$suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
}
$matches[] = array($word, $pos, $len, null, $suggestions);
}
$diff += (strlen($word) - $len);
}
$this->matches = $matches;
return $matches;
}
/**
* Returns suggestions for the specified word
*
* @see rcube_spellchecker_engine::get_words()
*/
function get_suggestions($word)
{
$this->init();
if (!$this->enchant_dictionary) {
return array();
}
$suggestions = enchant_dict_suggest($this->enchant_dictionary, $word);
if (is_array($suggestions) && count($suggestions) > self::MAX_SUGGESTIONS)
$suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
return is_array($suggestions) ? $suggestions : array();
}
/**
* Returns misspelled words
*
* @see rcube_spellchecker_engine::get_suggestions()
*/
function get_words($text = null)
{
$result = array();
if ($text) {
// init spellchecker
$this->init();
if (!$this->enchant_dictionary) {
return array();
}
// With Enchant we don't need to get suggestions to return misspelled words
$text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
foreach ($text as $w) {
$word = trim($w[0]);
// skip exceptions
if ($this->dictionary->is_exception($word)) {
continue;
}
if (!enchant_dict_check($this->enchant_dictionary, $word)) {
$result[] = $word;
}
}
return $result;
}
foreach ($this->matches as $m) {
$result[] = $m[0];
}
return $result;
}
}
diff --git a/program/lib/Roundcube/spellchecker/engine.php b/program/lib/Roundcube/spellchecker/engine.php
index 3fc9f4371..acf99cfe1 100644
--- a/program/lib/Roundcube/spellchecker/engine.php
+++ b/program/lib/Roundcube/spellchecker/engine.php
@@ -1,89 +1,89 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
- | Copyright (C) 2011-2013, Kolab Systems AG |
- | Copyright (C) 2008-2013, The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Interface class for a spell-checking backend |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Interface class for a spell-checking backend
*
* @package Framework
* @subpackage Utils
*/
abstract class rcube_spellchecker_engine
{
const MAX_SUGGESTIONS = 10;
protected $lang;
protected $error;
protected $dictionary;
protected $separator = '/[\s\r\n\t\(\)\/\[\]{}<>\\"]+|[:;?!,\.](?=\W|$)/';
/**
* Default constructor
*/
public function __construct($dict, $lang)
{
$this->dictionary = $dict;
$this->lang = $lang;
}
/**
* Return a list of languages supported by this backend
*
* @return array Indexed list of language codes
*/
abstract function languages();
/**
* Set content and check spelling
*
* @param string $text Text content for spellchecking
*
* @return bool True when no mispelling found, otherwise false
*/
abstract function check($text);
/**
* Returns suggestions for the specified word
*
* @param string $word The word
*
* @return array Suggestions list
*/
abstract function get_suggestions($word);
/**
* Returns misspelled words
*
* @param string $text The content for spellchecking. If empty content
* used for check() method will be used.
*
* @return array List of misspelled words
*/
abstract function get_words($text = null);
/**
* Returns error message
*
* @return string Error message
*/
public function error()
{
return $this->error;
}
}
diff --git a/program/lib/Roundcube/spellchecker/googie.php b/program/lib/Roundcube/spellchecker/googie.php
index a1387256b..d1144e778 100644
--- a/program/lib/Roundcube/spellchecker/googie.php
+++ b/program/lib/Roundcube/spellchecker/googie.php
@@ -1,177 +1,177 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
- | Copyright (C) 2008-2013, The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Spellchecking backend implementation to work with Googiespell |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Spellchecking backend implementation to work with a Googiespell service
*
* @package Framework
* @subpackage Utils
*/
class rcube_spellchecker_googie extends rcube_spellchecker_engine
{
const GOOGIE_HOST = 'ssl://spell.roundcube.net';
const GOOGIE_PORT = 443;
private $matches = array();
private $content;
/**
* Return a list of languages supported by this backend
*
* @see rcube_spellchecker_engine::languages()
*/
function languages()
{
return array('am','ar','ar','bg','br','ca','cs','cy','da',
'de_CH','de_DE','el','en_GB','en_US',
'eo','es','et','eu','fa','fi','fr_FR','ga','gl','gl',
'he','hr','hu','hy','is','it','ku','lt','lv','nl',
'pl','pt_BR','pt_PT','ro','ru',
'sk','sl','sv','uk');
}
/**
* Set content and check spelling
*
* @see rcube_spellchecker_engine::check()
*/
function check($text)
{
$this->content = $text;
if (empty($text)) {
return $this->matches = array();
}
// spell check uri is configured
$url = rcube::get_instance()->config->get('spellcheck_uri');
if ($url) {
$a_uri = parse_url($url);
$ssl = ($a_uri['scheme'] == 'https' || $a_uri['scheme'] == 'ssl');
$port = $a_uri['port'] ? $a_uri['port'] : ($ssl ? 443 : 80);
$host = ($ssl ? 'ssl://' : '') . $a_uri['host'];
$path = $a_uri['path'] . ($a_uri['query'] ? '?'.$a_uri['query'] : '') . $this->lang;
}
else {
$host = self::GOOGIE_HOST;
$port = self::GOOGIE_PORT;
$path = '/tbproxy/spell?lang=' . $this->lang;
}
$path .= sprintf('&key=%06d', $_SESSION['user_id']);
$gtext = '<?xml version="1.0" encoding="utf-8" ?>'
.'<spellrequest textalreadyclipped="0" ignoredups="0" ignoredigits="1" ignoreallcaps="1">'
.'<text>' . htmlspecialchars($text, ENT_QUOTES, RCUBE_CHARSET) . '</text>'
.'</spellrequest>';
$store = '';
if ($fp = fsockopen($host, $port, $errno, $errstr, 30)) {
$out = "POST $path HTTP/1.0\r\n";
$out .= "Host: " . str_replace('ssl://', '', $host) . "\r\n";
$out .= "User-Agent: Roundcube Webmail/" . RCUBE_VERSION . " (Googiespell Wrapper)\r\n";
$out .= "Content-Length: " . strlen($gtext) . "\r\n";
$out .= "Content-Type: text/xml\r\n";
$out .= "Connection: Close\r\n\r\n";
$out .= $gtext;
fwrite($fp, $out);
while (!feof($fp))
$store .= fgets($fp, 128);
fclose($fp);
}
// parse HTTP response
if (preg_match('!^HTTP/1.\d (\d+)(.+)!', $store, $m)) {
$http_status = $m[1];
if ($http_status != '200') {
$this->error = 'HTTP ' . $m[1] . rtrim($m[2]);
}
}
if (!$store) {
$this->error = "Empty result from spelling engine";
}
else if (preg_match('/<spellresult error="([^"]+)"/', $store, $m) && $m[1]) {
$this->error = "Error code $m[1] returned";
$this->error .= preg_match('/<errortext>([^<]+)/', $store, $m) ? ": " . html_entity_decode($m[1]) : '';
}
preg_match_all('/<c o="([^"]*)" l="([^"]*)" s="([^"]*)">([^<]*)<\/c>/', $store, $matches, PREG_SET_ORDER);
// skip exceptions (if appropriate options are enabled)
foreach ($matches as $idx => $m) {
$word = mb_substr($text, $m[1], $m[2], RCUBE_CHARSET);
// skip exceptions
if ($this->dictionary->is_exception($word)) {
unset($matches[$idx]);
}
}
$this->matches = $matches;
return $matches;
}
/**
* Returns suggestions for the specified word
*
* @see rcube_spellchecker_engine::get_words()
*/
function get_suggestions($word)
{
$matches = $word ? $this->check($word) : $this->matches;
if ($matches[0][4]) {
$suggestions = explode("\t", $matches[0][4]);
if (count($suggestions) > self::MAX_SUGGESTIONS) {
$suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
}
return $suggestions;
}
return array();
}
/**
* Returns misspelled words
*
* @see rcube_spellchecker_engine::get_suggestions()
*/
function get_words($text = null)
{
if ($text) {
$matches = $this->check($text);
}
else {
$matches = $this->matches;
$text = $this->content;
}
$result = array();
foreach ($matches as $m) {
$result[] = mb_substr($text, $m[1], $m[2], RCUBE_CHARSET);
}
return $result;
}
}
diff --git a/program/lib/Roundcube/spellchecker/pspell.php b/program/lib/Roundcube/spellchecker/pspell.php
index d5f9235d4..a91c53f56 100644
--- a/program/lib/Roundcube/spellchecker/pspell.php
+++ b/program/lib/Roundcube/spellchecker/pspell.php
@@ -1,191 +1,191 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
- | Copyright (C) 2008-2013, The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Spellchecking backend implementation to work with Pspell |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Spellchecking backend implementation to work with Pspell
*
* @package Framework
* @subpackage Utils
*/
class rcube_spellchecker_pspell extends rcube_spellchecker_engine
{
private $plink;
private $matches = array();
/**
* Return a list of languages supported by this backend
*
* @see rcube_spellchecker_engine::languages()
*/
function languages()
{
$defaults = array('en');
$langs = array();
// get aspell dictionaries
exec('aspell dump dicts', $dicts);
if (!empty($dicts)) {
$seen = array();
foreach ($dicts as $lang) {
$lang = preg_replace('/-.*$/', '', $lang);
$langc = strlen($lang) == 2 ? $lang.'_'.strtoupper($lang) : $lang;
if (!$seen[$langc]++) {
$langs[] = $lang;
}
}
$langs = array_unique($langs);
}
else {
$langs = $defaults;
}
return $langs;
}
/**
* Initializes PSpell dictionary
*/
private function init()
{
if (!$this->plink) {
if (!extension_loaded('pspell')) {
$this->error = "Pspell extension not available";
return;
}
$this->plink = pspell_new($this->lang, null, null, RCUBE_CHARSET, PSPELL_FAST);
}
if (!$this->plink) {
$this->error = "Unable to load Pspell engine for selected language";
}
}
/**
* Set content and check spelling
*
* @see rcube_spellchecker_engine::check()
*/
function check($text)
{
$this->init();
if (!$this->plink) {
return array();
}
// tokenize
$text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
$diff = 0;
$matches = array();
foreach ($text as $w) {
$word = trim($w[0]);
$pos = $w[1] - $diff;
$len = mb_strlen($word);
// skip exceptions
if ($this->dictionary->is_exception($word)) {
}
else if (!pspell_check($this->plink, $word)) {
$suggestions = pspell_suggest($this->plink, $word);
if (count($suggestions) > self::MAX_SUGGESTIONS) {
$suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
}
$matches[] = array($word, $pos, $len, null, $suggestions);
}
$diff += (strlen($word) - $len);
}
$this->matches = $matches;
return $matches;
}
/**
* Returns suggestions for the specified word
*
* @see rcube_spellchecker_engine::get_words()
*/
function get_suggestions($word)
{
$this->init();
if (!$this->plink) {
return array();
}
$suggestions = pspell_suggest($this->plink, $word);
if (count($suggestions) > self::MAX_SUGGESTIONS) {
$suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
}
return is_array($suggestions) ? $suggestions : array();
}
/**
* Returns misspelled words
*
* @see rcube_spellchecker_engine::get_suggestions()
*/
function get_words($text = null)
{
$result = array();
if ($text) {
// init spellchecker
$this->init();
if (!$this->plink) {
return array();
}
// With PSpell we don't need to get suggestions to return misspelled words
$text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
foreach ($text as $w) {
$word = trim($w[0]);
// skip exceptions
if ($this->dictionary->is_exception($word)) {
continue;
}
if (!pspell_check($this->plink, $word)) {
$result[] = $word;
}
}
return $result;
}
foreach ($this->matches as $m) {
$result[] = $m[0];
}
return $result;
}
}
diff --git a/program/localization/en_US/csv2vcard.inc b/program/localization/en_US/csv2vcard.inc
index 67a1757d6..34231f97a 100644
--- a/program/localization/en_US/csv2vcard.inc
+++ b/program/localization/en_US/csv2vcard.inc
@@ -1,112 +1,108 @@
<?php
/*
+-----------------------------------------------------------------------+
- | localization/<lang>/csv2vcard.inc |
- | |
| Localization file of the Roundcube Webmail client |
- | Copyright (C) 2005-2015, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
- +-----------------------------------------------------------------------+
- | Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
// This is a list of CSV column names specified in CSV file header
// These must be original texts used in Outlook/Thunderbird exported csv files
// Encoding UTF-8
$map = array();
// MS Outlook 2010
$map['anniversary'] = "Anniversary";
$map['assistants_name'] = "Assistant's Name";
$map['assistants_phone'] = "Assistant's Phone";
$map['birthday'] = "Birthday";
$map['business_city'] = "Business City";
$map['business_countryregion'] = "Business Country/Region";
$map['business_fax'] = "Business Fax";
$map['business_phone'] = "Business Phone";
$map['business_phone_2'] = "Business Phone 2";
$map['business_postal_code'] = "Business Postal Code";
$map['business_state'] = "Business State";
$map['business_street'] = "Business Street";
$map['car_phone'] = "Car Phone";
$map['categories'] = "Categories";
$map['company'] = "Company";
$map['department'] = "Department";
$map['email_address'] = "E-mail Address";
$map['email_2_address'] = "E-mail 2 Address";
$map['email_3_address'] = "E-mail 3 Address";
$map['first_name'] = "First Name";
$map['gender'] = "Gender";
$map['home_city'] = "Home City";
$map['home_countryregion'] = "Home Country/Region";
$map['home_fax'] = "Home Fax";
$map['home_phone'] = "Home Phone";
$map['home_phone_2'] = "Home Phone 2";
$map['home_postal_code'] = "Home Postal Code";
$map['home_state'] = "Home State";
$map['home_street'] = "Home Street";
$map['job_title'] = "Job Title";
$map['last_name'] = "Last Name";
$map['managers_name'] = "Manager's Name";
$map['middle_name'] = "Middle Name";
$map['mobile_phone'] = "Mobile Phone";
$map['notes'] = "Notes";
$map['other_city'] = "Other City";
$map['other_countryregion'] = "Other Country/Region";
$map['other_fax'] = "Other Fax";
$map['other_phone'] = "Other Phone";
$map['other_postal_code'] = "Other Postal Code";
$map['other_state'] = "Other State";
$map['other_street'] = "Other Street";
$map['pager'] = "Pager";
$map['primary_phone'] = "Primary Phone";
$map['spouse'] = "Spouse";
$map['suffix'] = "Suffix";
$map['title'] = "Title";
$map['web_page'] = "Web Page";
// Thunderbird
$map['birth_day'] = "Birth Day";
$map['birth_month'] = "Birth Month";
$map['birth_year'] = "Birth Year";
$map['display_name'] = "Display Name";
$map['fax_number'] = "Fax Number";
$map['home_address'] = "Home Address";
$map['home_country'] = "Home Country";
$map['home_zipcode'] = "Home ZipCode";
$map['mobile_number'] = "Mobile Number";
$map['nickname'] = "Nickname";
$map['organization'] = "Organization";
$map['pager_number'] = "Pager Namber";
$map['primary_email'] = "Primary Email";
$map['secondary_email'] = "Secondary Email";
$map['web_page_1'] = "Web Page 1";
$map['web_page_2'] = "Web Page 2";
$map['work_phone'] = "Work Phone";
$map['work_address'] = "Work Address";
$map['work_country'] = "Work Country";
$map['work_zipcode'] = "Work ZipCode";
// Atmail
$map['date_of_birth'] = "Date of Birth";
$map['email'] = "Email";
$map['home_mobile'] = "Home Mobile";
$map['home_zip'] = "Home Zip";
$map['info'] = "Info";
$map['user_photo'] = "User Photo";
$map['url'] = "URL";
$map['work_city'] = "Work City";
$map['work_company'] = "Work Company";
$map['work_dept'] = "Work Dept";
$map['work_fax'] = "Work Fax";
$map['work_mobile'] = "Work Mobile";
$map['work_state'] = "Work State";
$map['work_title'] = "Work Title";
$map['work_zip'] = "Work Zip";
diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc
index 046103c4e..3c46bf390 100644
--- a/program/localization/en_US/labels.inc
+++ b/program/localization/en_US/labels.inc
@@ -1,752 +1,750 @@
<?php
/*
+-----------------------------------------------------------------------+
- | localization/<lang>/labels.inc |
- | |
| Localization file of the Roundcube Webmail client |
- | Copyright (C) 2005-2015, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-----------------------------------------------------------------------+
For translation see https://www.transifex.com/projects/p/roundcube-webmail/resource/labels/
*/
$labels = array();
// login page
$labels['welcome'] = 'Welcome to $product';
$labels['username'] = 'Username';
$labels['password'] = 'Password';
$labels['server'] = 'Server';
$labels['login'] = 'Login';
// taskbar
$labels['menu'] = 'Menu';
$labels['logout'] = 'Logout';
$labels['mail'] = 'Mail';
$labels['settings'] = 'Settings';
$labels['addressbook'] = 'Address Book';
// mailbox names
$labels['inbox'] = 'Inbox';
$labels['drafts'] = 'Drafts';
$labels['sent'] = 'Sent';
$labels['trash'] = 'Trash';
$labels['junk'] = 'Junk';
$labels['show_real_foldernames'] = 'Show real names for special folders';
// message listing
$labels['subject'] = 'Subject';
$labels['from'] = 'From';
$labels['sender'] = 'Sender';
$labels['to'] = 'To';
$labels['cc'] = 'Cc';
$labels['bcc'] = 'Bcc';
$labels['replyto'] = 'Reply-To';
$labels['followupto'] = 'Followup-To';
$labels['date'] = 'Date';
$labels['size'] = 'Size';
$labels['priority'] = 'Priority';
$labels['organization'] = 'Organization';
$labels['readstatus'] = 'Read status';
$labels['listoptions'] = 'List options...';
$labels['listoptionstitle'] = 'List options';
$labels['mailboxlist'] = 'Folders';
$labels['messagesfromto'] = 'Messages $from to $to of $count';
$labels['threadsfromto'] = 'Threads $from to $to of $count';
$labels['messagenrof'] = 'Message $nr of $count';
$labels['fromtoshort'] = '$from – $to of $count';
$labels['copy'] = 'Copy';
$labels['move'] = 'Move';
$labels['moveto'] = 'Move to...';
$labels['copyto'] = 'Copy to...';
$labels['download'] = 'Download';
$labels['open'] = 'Open';
$labels['showattachment'] = 'Show';
$labels['showanyway'] = 'Show it anyway';
$labels['filename'] = 'File name';
$labels['filesize'] = 'File size';
$labels['addtoaddressbook'] = 'Add to address book';
// weekdays short
$labels['sun'] = 'Sun';
$labels['mon'] = 'Mon';
$labels['tue'] = 'Tue';
$labels['wed'] = 'Wed';
$labels['thu'] = 'Thu';
$labels['fri'] = 'Fri';
$labels['sat'] = 'Sat';
// weekdays long
$labels['sunday'] = 'Sunday';
$labels['monday'] = 'Monday';
$labels['tuesday'] = 'Tuesday';
$labels['wednesday'] = 'Wednesday';
$labels['thursday'] = 'Thursday';
$labels['friday'] = 'Friday';
$labels['saturday'] = 'Saturday';
// months short
$labels['jan'] = 'Jan';
$labels['feb'] = 'Feb';
$labels['mar'] = 'Mar';
$labels['apr'] = 'Apr';
$labels['may'] = 'May';
$labels['jun'] = 'Jun';
$labels['jul'] = 'Jul';
$labels['aug'] = 'Aug';
$labels['sep'] = 'Sep';
$labels['oct'] = 'Oct';
$labels['nov'] = 'Nov';
$labels['dec'] = 'Dec';
// months long
$labels['longjan'] = 'January';
$labels['longfeb'] = 'February';
$labels['longmar'] = 'March';
$labels['longapr'] = 'April';
$labels['longmay'] = 'May';
$labels['longjun'] = 'June';
$labels['longjul'] = 'July';
$labels['longaug'] = 'August';
$labels['longsep'] = 'September';
$labels['longoct'] = 'October';
$labels['longnov'] = 'November';
$labels['longdec'] = 'December';
$labels['today'] = 'Today';
// toolbar buttons
$labels['refresh'] = 'Refresh';
$labels['checkmail'] = 'Check for new messages';
$labels['compose'] = 'Compose';
$labels['writenewmessage'] = 'Create a new message';
$labels['reply'] = 'Reply';
$labels['replytomessage'] = 'Reply to sender';
$labels['replytoallmessage'] = 'Reply to list or to sender and all recipients';
$labels['replyall'] = 'Reply all';
$labels['replylist'] = 'Reply list';
$labels['forward'] = 'Forward';
$labels['forwardinline'] = 'Forward inline';
$labels['forwardattachment'] = 'Forward as attachment';
$labels['forwardmessage'] = 'Forward the message';
$labels['bouncemsg'] = 'Resend (bounce)';
$labels['bounce'] = 'Resend';
$labels['deletemessage'] = 'Delete message';
$labels['movemessagetotrash'] = 'Move message to trash';
$labels['printmessage'] = 'Print this message';
$labels['previousmessage'] = 'Show previous message';
$labels['firstmessage'] = 'Show first message';
$labels['nextmessage'] = 'Show next message';
$labels['lastmessage'] = 'Show last message';
$labels['backtolist'] = 'Back to message list';
$labels['viewsource'] = 'Show source';
$labels['mark'] = 'Mark';
$labels['markmessages'] = 'Mark messages';
$labels['markread'] = 'As read';
$labels['markunread'] = 'As unread';
$labels['markflagged'] = 'As flagged';
$labels['markunflagged'] = 'As unflagged';
$labels['moreactions'] = 'More actions...';
$labels['markallread'] = 'Mark all as read';
$labels['folders-cur'] = 'Selected folder only';
$labels['folders-sub'] = 'Selected folder and its subfolders';
$labels['folders-all'] = 'All folders';
$labels['more'] = 'More';
$labels['back'] = 'Back';
$labels['options'] = 'Options';
$labels['composeoptions'] = 'Compose options';
$labels['optionsandattachments'] = 'Options and attachments';
$labels['actions'] = 'Actions';
$labels['first'] = 'First';
$labels['last'] = 'Last';
$labels['previous'] = 'Previous';
$labels['next'] = 'Next';
$labels['select'] = 'Select';
$labels['selection'] = 'Selection';
$labels['all'] = 'All';
$labels['none'] = 'None';
$labels['currpage'] = 'Current page';
$labels['isread'] = 'Read';
$labels['unread'] = 'Unread';
$labels['flagged'] = 'Flagged';
$labels['unflagged'] = 'Not Flagged';
$labels['unanswered'] = 'Unanswered';
$labels['withattachment'] = 'With attachment';
$labels['deleted'] = 'Deleted';
$labels['undeleted'] = 'Not deleted';
$labels['replied'] = 'Replied';
$labels['forwarded'] = 'Forwarded';
$labels['invert'] = 'Invert';
$labels['filter'] = 'Filter';
$labels['list'] = 'List';
$labels['threads'] = 'Threads';
$labels['expand-all'] = 'Expand All';
$labels['expand-unread'] = 'Expand Unread';
$labels['collapse-all'] = 'Collapse All';
$labels['threaded'] = 'Threaded';
$labels['autoexpand_threads'] = 'Expand message threads';
$labels['do_expand'] = 'all threads';
$labels['expand_only_unread'] = 'only with unread messages';
$labels['fromto'] = 'From/To';
$labels['flag'] = 'Flag';
$labels['attachment'] = 'Attachment';
$labels['nonesort'] = 'None';
$labels['sentdate'] = 'Sent date';
$labels['arrival'] = 'Arrival date';
$labels['asc'] = 'ascending';
$labels['desc'] = 'descending';
$labels['listcolumns'] = 'List columns';
$labels['listsorting'] = 'Sorting column';
$labels['listorder'] = 'Sorting order';
$labels['listmode'] = 'List view mode';
$labels['lmode'] = 'List mode';
$labels['layout'] = 'Layout';
$labels['layoutwidescreen'] = 'Widescreen';
$labels['layoutdesktop'] = 'Desktop';
$labels['layoutlist'] = 'List';
$labels['layoutwidescreendesc'] = 'Widescreen (3-column view)';
$labels['layoutdesktopdesc'] = 'Desktop (wide list and mail preview below)';
$labels['layoutlistdesc'] = 'List (no mail preview)';
$labels['folderactions'] = 'Folder actions...';
$labels['compact'] = 'Compact';
$labels['empty'] = 'Empty';
$labels['importmessages'] = 'Import messages';
$labels['mailimportdesc'] = 'You can upload mail using files in <a href="https://en.wikipedia.org/wiki/Email#Message_format">MIME</a> or <a href="https://en.wikipedia.org/wiki/Mbox">Mbox</a> format.';
$labels['mailimportzip'] = 'Multiple files can be compressed into zip archives.';
$labels['quota'] = 'Disk usage';
$labels['unknown'] = 'unknown';
$labels['unlimited'] = 'unlimited';
$labels['quotatype'] = 'Quota type';
$labels['quotatotal'] = 'Limit';
$labels['quotaused'] = 'Used';
$labels['quotastorage'] = 'Disk space';
$labels['quotamessage'] = 'Messages count';
$labels['shortheaderdate'] = 'On $date';
$labels['shortheaderto'] = 'To $to on $date';
$labels['shortheaderfrom'] = 'From $from on $date';
$labels['quicksearch'] = 'Quick search';
$labels['searchplaceholder'] = 'Search...';
$labels['resetsearch'] = 'Reset search';
$labels['searchmod'] = 'Search modifiers';
$labels['msgtext'] = 'Entire message';
$labels['body'] = 'Body';
$labels['type'] = 'Type';
$labels['namex'] = 'Name';
$labels['searchscope'] = 'Scope';
$labels['currentfolder'] = 'Current folder';
$labels['subfolders'] = 'This and subfolders';
$labels['allfolders'] = 'All folders';
$labels['searchinterval-1W'] = 'older than a week';
$labels['searchinterval-1M'] = 'older than a month';
$labels['searchinterval-1Y'] = 'older than a year';
$labels['searchinterval1W'] = 'younger than a week';
$labels['searchinterval1M'] = 'younger than a month';
$labels['searchinterval1Y'] = 'younger than a year';
$labels['openinextwin'] = 'Open in new window';
$labels['emlsave'] = 'Download (.eml)';
$labels['changeformattext'] = 'Display in plain text format';
$labels['changeformathtml'] = 'Display in HTML format';
// message compose
$labels['editasnew'] = 'Edit as new';
$labels['send'] = 'Send';
$labels['sendmessage'] = 'Send message';
$labels['savemessage'] = 'Save as draft';
$labels['addattachment'] = 'Attach a file';
$labels['charset'] = 'Charset';
$labels['editortype'] = 'Editor type';
$labels['returnreceipt'] = 'Return receipt';
$labels['dsn'] = 'Delivery status notification';
$labels['mailreplyintro'] = 'On $date, $sender wrote:';
$labels['originalmessage'] = 'Original Message';
$labels['selectimage'] = 'Select image';
$labels['addimage'] = 'Add image';
$labels['selectmedia'] = 'Select movie';
$labels['addmedia'] = 'Add movie';
$labels['encrypt'] = 'Encrypt';
$labels['encryptmessage'] = 'Encrypt message';
$labels['encryptmessagemailvelope'] = 'Encrypt message with Mailvelope';
$labels['importpubkeys'] = 'Import public keys';
$labels['encryptedsendialog'] = 'Sending encrypted message';
$labels['encryptandsign'] = 'Encrypt and sign';
$labels['keyid'] = 'Key ID';
$labels['keylength'] = 'Bits';
$labels['keyexpired'] = 'Expired';
$labels['keyrevoked'] = 'Revoked';
$labels['bccinstead'] = 'Use Bcc';
$labels['addheader'] = 'Add recipient (header)';
$labels['insert'] = 'Insert';
$labels['insertcontact'] = 'Insert contact(s)';
$labels['recipient'] = 'Recipient';
$labels['recipientedit'] = 'Recipient edit';
$labels['editidents'] = 'Edit identities';
$labels['spellcheck'] = 'Spell';
$labels['checkspelling'] = 'Check spelling';
$labels['resumeediting'] = 'Resume editing';
$labels['revertto'] = 'Revert to';
$labels['restore'] = 'Restore';
$labels['restoremessage'] = 'Restore message?';
$labels['ignore'] = 'Ignore';
$labels['responses'] = 'Responses';
$labels['insertresponse'] = 'Insert a response';
$labels['manageresponses'] = 'Manage responses';
$labels['newresponse'] = 'Create new response';
$labels['addresponse'] = 'Add response';
$labels['editresponse'] = 'Edit response';
$labels['editresponses'] = 'Edit responses';
$labels['responsename'] = 'Name';
$labels['responsetext'] = 'Response Text';
$labels['attach'] = 'Attach';
$labels['attachments'] = 'Attachments';
$labels['upload'] = 'Upload';
$labels['uploadprogress'] = '$percent ($current of $total)';
$labels['close'] = 'Close';
$labels['messageoptions'] = 'Message options...';
$labels['togglecomposeoptions'] = 'Toggle composition options';
$labels['attachmentrename'] = 'Rename attachment';
$labels['low'] = 'Low';
$labels['lowest'] = 'Lowest';
$labels['normal'] = 'Normal';
$labels['high'] = 'High';
$labels['highest'] = 'Highest';
$labels['nosubject'] = '(no subject)';
$labels['showimages'] = 'Display images';
$labels['allow'] = 'Allow';
$labels['alwaysshow'] = 'Always show images from $sender';
$labels['alwaysallow'] = 'Always allow from $sender';
$labels['isdraft'] = 'This is a draft message.';
$labels['andnmore'] = '$nr more...';
$labels['details'] = 'Details';
$labels['headers'] = 'Headers';
$labels['allheaders'] = 'All headers...';
$labels['togglemoreheaders'] = 'Show more message headers';
$labels['togglefullheaders'] = 'Toggle raw message headers';
$labels['htmltoggle'] = 'HTML';
$labels['plaintoggle'] = 'Plain text';
$labels['savesentmessagein'] = 'Save sent message in';
$labels['dontsave'] = 'don\'t save';
$labels['maxuploadsize'] = 'Maximum allowed file size is $size';
$labels['addcc'] = 'Add Cc';
$labels['addbcc'] = 'Add Bcc';
$labels['addreplyto'] = 'Add Reply-To';
$labels['addfollowupto'] = 'Add Followup-To';
// mdn
$labels['mdnrequest'] = 'The sender of this message has asked to be notified when you read this message. Do you wish to notify the sender?';
$labels['receiptread'] = 'Return Receipt (read)';
$labels['yourmessage'] = 'This is a Return Receipt for your message';
$labels['receiptnote'] = 'Note: This receipt only acknowledges that the message was displayed on the recipient\'s computer. There is no guarantee that the recipient has read or understood the message contents.';
$labels['zoomin'] = 'Zoom In';
$labels['zoomout'] = 'Zoom Out';
$labels['rotate'] = 'Rotate';
$labels['increaseimage'] = 'Increase image size';
$labels['decreaseimage'] = 'Decrease image size';
$labels['rotateimage'] = 'Rotate image';
$labels['showtools'] = 'Show image tools';
$labels['hidetools'] = 'Hide image tools';
// address boook
$labels['name'] = 'Display Name';
$labels['firstname'] = 'First Name';
$labels['surname'] = 'Last Name';
$labels['middlename'] = 'Middle Name';
$labels['nameprefix'] = 'Prefix';
$labels['namesuffix'] = 'Suffix';
$labels['nickname'] = 'Nickname';
$labels['jobtitle'] = 'Job Title';
$labels['department'] = 'Department';
$labels['gender'] = 'Gender';
$labels['maidenname'] = 'Maiden Name';
$labels['email'] = 'Email';
$labels['phone'] = 'Phone';
$labels['address'] = 'Address';
$labels['street'] = 'Street';
$labels['locality'] = 'City';
$labels['zipcode'] = 'ZIP Code';
$labels['region'] = 'State/Province';
$labels['country'] = 'Country';
$labels['birthday'] = 'Birthday';
$labels['anniversary'] = 'Anniversary';
$labels['website'] = 'Website';
$labels['instantmessenger'] = 'IM';
$labels['notes'] = 'Notes';
$labels['male'] = 'male';
$labels['female'] = 'female';
$labels['manager'] = 'Manager';
$labels['assistant'] = 'Assistant';
$labels['spouse'] = 'Spouse';
$labels['allfields'] = 'All fields';
$labels['search'] = 'Search';
$labels['searchresult'] = 'Search result';
$labels['advsearch'] = 'Advanced Search';
$labels['advanced'] = 'Advanced';
$labels['other'] = 'Other';
$labels['printcontact'] = 'Print contact';
$labels['qrcode'] = 'QR Code';
$labels['typehome'] = 'Home';
$labels['typework'] = 'Work';
$labels['typeother'] = 'Other';
$labels['typemobile'] = 'Mobile';
$labels['typemain'] = 'Main';
$labels['typehomefax'] = 'Home Fax';
$labels['typeworkfax'] = 'Work Fax';
$labels['typecar'] = 'Car';
$labels['typepager'] = 'Pager';
$labels['typevideo'] = 'Video';
$labels['typeassistant'] = 'Assistant';
$labels['typehomepage'] = 'Home Page';
$labels['typeblog'] = 'Blog';
$labels['typeprofile'] = 'Profile';
$labels['addfield'] = 'Add field...';
$labels['addcontact'] = 'Add contact';
$labels['editcontact'] = 'Edit contact';
$labels['contacts'] = 'Contacts';
$labels['contactproperties'] = 'Contact properties';
$labels['contactnameandorg'] = 'Name and Organization';
$labels['personalinfo'] = 'Personal information';
$labels['personal'] = 'Personal';
$labels['contactphoto'] = 'Contact photo';
$labels['edit'] = 'Edit';
$labels['cancel'] = 'Cancel';
$labels['save'] = 'Save';
$labels['delete'] = 'Delete';
$labels['discard'] = 'Discard';
$labels['continue'] = 'Continue';
$labels['ok'] = 'OK';
$labels['rename'] = 'Rename';
$labels['addphoto'] = 'Add';
$labels['replacephoto'] = 'Replace';
$labels['uploadphoto'] = 'Upload photo';
$labels['choosefile'] = 'Choose file...';
$labels['choosefiles'] = 'Choose files...';
$labels['browse'] = 'Browse';
$labels['newcontact'] = 'Create new contact';
$labels['deletecontact'] = 'Delete selected contacts';
$labels['composeto'] = 'Compose mail to';
$labels['contactsfromto'] = 'Contacts $from to $to of $count';
$labels['print'] = 'Print';
$labels['export'] = 'Export';
$labels['exportall'] = 'Export all';
$labels['exportsel'] = 'Export selected';
$labels['exportvcards'] = 'Export contacts in vCard format';
$labels['newgroup'] = 'Create new group';
$labels['addgroup'] = 'Add group';
$labels['grouprename'] = 'Rename group';
$labels['groupdelete'] = 'Delete group';
$labels['groupassign'] = 'Assign to group...';
$labels['groupremove'] = 'Remove from group';
$labels['groupremoveselected'] = 'Remove selected contacts from group';
$labels['uponelevel'] = 'Up one level';
$labels['previouspage'] = 'Show previous page';
$labels['firstpage'] = 'Show first page';
$labels['nextpage'] = 'Show next page';
$labels['lastpage'] = 'Show last page';
$labels['group'] = 'Group';
$labels['groups'] = 'Groups';
$labels['listgroup'] = 'List group members';
$labels['personaladrbook'] = 'Personal Addresses';
$labels['searchsave'] = 'Save search';
$labels['searchdelete'] = 'Delete search';
$labels['import'] = 'Import';
$labels['importcontacts'] = 'Import contacts';
$labels['importfromfile'] = 'Import from file';
$labels['importtarget'] = 'Add contacts to';
$labels['importreplace'] = 'Replace the entire address book';
$labels['importgroups'] = 'Import group assignments';
$labels['importgroupsall'] = 'All (create groups if necessary)';
$labels['importgroupsexisting'] = 'Only for existing groups';
$labels['importdesc'] = 'You can upload contacts from an existing address book.<br/>We currently support importing addresses from the <a href="https://en.wikipedia.org/wiki/VCard">vCard</a> or CSV (comma-separated) data format.';
$labels['done'] = 'Done';
// settings
$labels['settingsfor'] = 'Settings for';
$labels['about'] = 'About';
$labels['preferences'] = 'Preferences';
$labels['userpreferences'] = 'User preferences';
$labels['editpreferences'] = 'Edit user preferences';
$labels['identities'] = 'Identities';
$labels['manageidentities'] = 'Manage identities';
$labels['newidentity'] = 'Create new identity';
$labels['addidentity'] = 'Add identity';
$labels['editidentity'] = 'Edit identity';
$labels['identityencryption'] = 'Encryption';
$labels['preferhtml'] = 'Display HTML';
$labels['defaultcharset'] = 'Default Character Set';
$labels['htmlmessage'] = 'HTML Message';
$labels['messagepart'] = 'Part';
$labels['digitalsig'] = 'Digital Signature';
$labels['dateformat'] = 'Date format';
$labels['timeformat'] = 'Time format';
$labels['prettydate'] = 'Pretty dates';
$labels['setdefault'] = 'Set default';
$labels['autodetect'] = 'Auto';
$labels['language'] = 'Language';
$labels['timezone'] = 'Time zone';
$labels['pagesize'] = 'Rows per page';
$labels['signature'] = 'Signature';
$labels['dstactive'] = 'Daylight saving time';
$labels['showinextwin'] = 'Open message in a new window';
$labels['composeextwin'] = 'Compose in a new window';
$labels['htmleditor'] = 'Compose HTML messages';
$labels['htmlonreply'] = 'on reply to HTML message';
$labels['htmlonreplyandforward'] = 'on forward or reply to HTML message';
$labels['htmlsignature'] = 'HTML signature';
$labels['showemail'] = 'Show email address with display name';
$labels['previewpane'] = 'Show preview pane';
$labels['skin'] = 'Interface skin';
$labels['logoutclear'] = 'Clear Trash on logout';
$labels['logoutcompact'] = 'Compact Inbox on logout';
$labels['uisettings'] = 'User Interface';
$labels['serversettings'] = 'Server Settings';
$labels['mailboxview'] = 'Mailbox View';
$labels['mdnrequests'] = 'On request for return receipt';
$labels['askuser'] = 'ask me';
$labels['autosend'] = 'send receipt';
$labels['autosendknown'] = 'send receipt to my contacts, otherwise ask me';
$labels['autosendknownignore'] = 'send receipt to my contacts, otherwise ignore';
$labels['ignorerequest'] = 'ignore request';
$labels['readwhendeleted'] = 'Mark the message as read on delete';
$labels['flagfordeletion'] = 'Flag the message for deletion instead of delete';
$labels['skipdeleted'] = 'Do not show deleted messages';
$labels['deletealways'] = 'If moving messages to Trash fails, delete them';
$labels['deletejunk'] = 'Directly delete messages in Junk';
$labels['showremoteimages'] = 'Display remote inline images';
$labels['allowremoteresources'] = 'Allow remote resources (images, styles)';
$labels['fromknownsenders'] = 'from known senders';
$labels['always'] = 'always';
$labels['alwaysbutplain'] = 'always, except when replying to plain text';
$labels['showinlineimages'] = 'Display attached images below the message';
$labels['autosavedraft'] = 'Automatically save draft';
$labels['everynminutes'] = 'every $n minute(s)';
$labels['refreshinterval'] = 'Refresh (check for new messages, etc.)';
$labels['never'] = 'never';
$labels['immediately'] = 'immediately';
$labels['messagesdisplaying'] = 'Displaying Messages';
$labels['messagescomposition'] = 'Composing Messages';
$labels['mimeparamfolding'] = 'Attachment names';
$labels['2231folding'] = 'Full RFC 2231 (Thunderbird)';
$labels['miscfolding'] = 'RFC 2047/2231 (MS Outlook)';
$labels['2047folding'] = 'Full RFC 2047 (other)';
$labels['force7bit'] = 'Use MIME encoding for 8-bit characters';
$labels['savelocalstorage'] = "Save in the browser's local storage (temporarily)";
$labels['advancedoptions'] = 'Advanced options';
$labels['toggleadvancedoptions'] = 'Toggle advanced options';
$labels['focusonnewmessage'] = 'Focus browser window on new message';
$labels['checkallfolders'] = 'Check all folders for new messages';
$labels['displaynext'] = 'After message delete/move display the next message';
$labels['defaultfont'] = 'Default font of HTML message';
$labels['mainoptions'] = 'Main Options';
$labels['browseroptions'] = 'Browser Options';
$labels['section'] = 'Section';
$labels['maintenance'] = 'Maintenance';
$labels['newmessage'] = 'New Message';
$labels['signatureoptions'] = 'Signature Options';
$labels['whenreplying'] = 'When replying';
$labels['replyempty'] = 'do not quote the original message';
$labels['replytopposting'] = 'start new message above the quote';
$labels['replytoppostingnoindent'] = 'start new message above the quote (no indentation)';
$labels['replybottomposting'] = 'start new message below the quote';
$labels['replyremovesignature'] = 'When replying remove original signature from message';
$labels['autoaddsignature'] = 'Automatically add signature';
$labels['newmessageonly'] = 'new message only';
$labels['replyandforwardonly'] = 'replies and forwards only';
$labels['insertsignature'] = 'Insert signature';
$labels['sigbelow'] = 'Place signature below the quoted message';
$labels['sigseparator'] = 'Force standard separator in signatures';
$labels['automarkread'] = 'Mark messages as read';
$labels['afternseconds'] = 'after $n seconds';
$labels['reqmdn'] = 'Always request a return receipt';
$labels['reqdsn'] = 'Always request a delivery status notification';
$labels['replysamefolder'] = 'Place replies in the folder of the message being replied to';
$labels['defaultabook'] = 'Default address book';
$labels['autocompletesingle'] = 'Skip alternative email addresses in autocompletion';
$labels['listnamedisplay'] = 'List contacts as';
$labels['spellcheckbeforesend'] = 'Check spelling before sending a message';
$labels['spellcheckoptions'] = 'Spellcheck Options';
$labels['spellcheckignoresyms'] = 'Ignore words with symbols';
$labels['spellcheckignorenums'] = 'Ignore words with numbers';
$labels['spellcheckignorecaps'] = 'Ignore words with all letters capitalized';
$labels['addtodict'] = 'Add to dictionary';
$labels['mailtoprotohandler'] = 'Register protocol handler for mailto: links';
$labels['standardwindows'] = 'Handle popups as standard windows';
$labels['forwardmode'] = 'Messages forwarding';
$labels['inline'] = 'inline';
$labels['asattachment'] = 'as attachment';
$labels['replyallmode'] = 'Default action of [Reply all] button';
$labels['replyalldefault'] = 'reply to all';
$labels['replyalllist'] = 'reply to mailing list only (if found)';
$labels['folder'] = 'Folder';
$labels['folders'] = 'Folders';
$labels['foldername'] = 'Folder name';
$labels['subscribed'] = 'Subscribed';
$labels['messagecount'] = 'Messages';
$labels['create'] = 'Create';
$labels['createfolder'] = 'Create new folder';
$labels['managefolders'] = 'Manage folders';
$labels['specialfolders'] = 'Special Folders';
$labels['properties'] = 'Properties';
$labels['folderproperties'] = 'Folder properties';
$labels['parentfolder'] = 'Parent folder';
$labels['location'] = 'Location';
$labels['info'] = 'Information';
$labels['getfoldersize'] = 'Click to get folder size';
$labels['changesubscription'] = 'Click to change subscription';
$labels['foldertype'] = 'Folder Type';
$labels['personalfolder'] = 'Private Folder';
$labels['otherfolder'] = 'Other User\'s Folder';
$labels['sharedfolder'] = 'Public Folder';
$labels['findfolders'] = 'Find folders';
$labels['findcontacts'] = 'Find contacts';
$labels['findmail'] = 'Find mail messages';
$labels['namespace.personal'] = 'Personal';
$labels['namespace.other'] = 'Other users';
$labels['namespace.shared'] = 'Shared';
$labels['dualuselabel'] = 'Can contain only';
$labels['dualusemail'] = 'messages';
$labels['dualusefolder'] = 'folders';
$labels['generate'] = 'Generate';
$labels['encryptioncreatekey'] = 'Create a new key pair';
$labels['openmailvelopesettings'] = 'Open Mailvelope Settings';
$labels['encryptionprivkeysinmailvelope'] = 'You have $nr matching private keys stored in your Mailvelope keyring:';
$labels['encryptionnoprivkeysinmailvelope'] = 'This sender identity doesn\'t yet have a PGP private key stored in your Mailvelope extension. Would you like to create one in order to enable encrypted messaging?';
$labels['sortby'] = 'Sort by';
$labels['sortasc'] = 'Sort ascending';
$labels['sortdesc'] = 'Sort descending';
$labels['undo'] = 'Undo';
$labels['installedplugins'] = 'Installed plugins';
$labels['plugin'] = 'Plugin';
$labels['version'] = 'Version';
$labels['source'] = 'Source';
$labels['license'] = 'License';
$labels['support'] = 'Get support';
$labels['savedsearches'] = 'Saved searches';
$labels['dateformatY'] = 'YYYY';
$labels['dateformaty'] = 'YY';
$labels['dateformatm'] = 'MM';
$labels['dateformatd'] = 'DD';
$labels['dateformatj'] = 'D';
$labels['dateformatn'] = 'M';
// units
$labels['B'] = 'B';
$labels['KB'] = 'KB';
$labels['MB'] = 'MB';
$labels['GB'] = 'GB';
// character sets
$labels['unicode'] = 'Unicode';
$labels['english'] = 'English';
$labels['westerneuropean'] = 'Western European';
$labels['easterneuropean'] = 'Eastern European';
$labels['southeasterneuropean'] = 'South-Eastern European';
$labels['baltic'] = 'Baltic';
$labels['cyrillic'] = 'Cyrillic';
$labels['arabic'] = 'Arabic';
$labels['greek'] = 'Greek';
$labels['hebrew'] = 'Hebrew';
$labels['turkish'] = 'Turkish';
$labels['nordic'] = 'Nordic';
$labels['thai'] = 'Thai';
$labels['celtic'] = 'Celtic';
$labels['vietnamese'] = 'Vietnamese';
$labels['japanese'] = 'Japanese';
$labels['korean'] = 'Korean';
$labels['chinese'] = 'Chinese';
// accessibility (voice-only) headings and descriptions
$labels['arialabeltopnav'] = 'Window control';
$labels['arialabeltasknav'] = 'Application tasks';
$labels['arialabeltoolbar'] = 'Application toolbar';
$labels['arialabelactivetask'] = 'Active task';
$labels['arialabelmessagessearchfilter'] = 'Email listing filter';
$labels['arialabelmailsearchform'] = 'Email message search form';
$labels['arialabelcontactsearchform'] = 'Contacts search form';
$labels['arialabelmailquicksearchbox'] = 'Email search input';
$labels['arialabelquicksearchbox'] = 'Search input';
$labels['arialabelfoldersearchfilter'] = 'Folder listing filter';
$labels['arialabelfoldersearchform'] = 'Folder search form';
$labels['arialabelfolderlist'] = 'Email folder selection';
$labels['arialabelmessagelist'] = 'Email Messages Listing';
$labels['arialabelmailpreviewframe'] = 'Message preview';
$labels['arialabelmailboxmenu'] = 'Folder actions menu';
$labels['arialabellistselectmenu'] = 'List selection menu';
$labels['arialabelthreadselectmenu'] = 'Threads listing menu';
$labels['arialabelmessagelistoptions'] = 'Message list display and sorting options';
$labels['arialabelmailimportdialog'] = 'Message import dialog';
$labels['arialabelmessagenav'] = 'Message navigation';
$labels['arialabelmessagebody'] = 'Message Body';
$labels['arialabelmessageactions'] = 'Message actions';
$labels['arialabelcontactquicksearch'] = 'Contacts search form';
$labels['arialabelcontactsearchbox'] = 'Contact search input';
$labels['arialabelmessageheaders'] = 'Message headers';
$labels['arialabelforwardingoptions'] = 'Forwarding options';
$labels['arialabelreplyalloptions'] = 'Reply-all options';
$labels['arialabelmoremessageactions'] = 'More message actions';
$labels['arialabelmorecontactactions'] = 'More contact actions';
$labels['arialabelmarkmessagesas'] = 'Mark selected messages as...';
$labels['arialabelcomposeoptions'] = 'Composition options';
$labels['arialabelresponsesmenu'] = 'Canned responses menu';
$labels['arialabelattachmentuploadform'] = 'Attachment upload form';
$labels['arialabelattachmentmenu'] = 'Attachment options';
$labels['arialabelmailtomenu'] = 'Email address options';
$labels['arialabelattachmentpreview'] = 'Attachment preview';
$labels['ariasummarycomposecontacts'] = 'List of contacts and groups to select as recipients';
$labels['arialabelcontactexportoptions'] = 'Contact export options';
$labels['arialabelabookgroupoptions'] = 'Addressbook/group options';
$labels['arialabelpreferencesform'] = 'Preferences form';
$labels['arialabelidentityeditfrom'] = 'Identity edit form';
$labels['arialabelresonseeditfrom'] = 'Response edit form';
$labels['arialabelsearchterms'] = 'Search terms';
$labels['arialabeldropactionmenu'] = 'Drag-n-Drop action menu';
$labels['arialabelheadersmenu'] = 'Recipient (header) adding menu';
$labels['arialabelimagetools'] = 'Image tools';
$labels['helplistnavigation'] = 'List keyboard navigation';
$labels['helplistkeyboardnavigation'] = "Arrows up/down: Move row focus/selection.
Space: Select focused row.
Shift + up/down: Select additional row above/below.
Ctrl + Space: Add focused row to selection/remove from selection.";
$labels['helplistkeyboardnavmessages'] = "Arrows right/left: expand/collapse message thread (in threads mode only).
Enter: Open the selected/focused message.
Delete: Move selected messages to Trash.";
$labels['helplistkeyboardnavcontacts'] = "Enter: Open the selected/focused contact.";
diff --git a/program/localization/en_US/messages.inc b/program/localization/en_US/messages.inc
index 7b3356354..5001c266e 100644
--- a/program/localization/en_US/messages.inc
+++ b/program/localization/en_US/messages.inc
@@ -1,226 +1,224 @@
<?php
/*
+-----------------------------------------------------------------------+
- | localization/<lang>/messages.inc |
- | |
| Localization file of the Roundcube Webmail client |
- | Copyright (C) 2005-2015, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
- | |
+-----------------------------------------------------------------------+
For translation see https://www.transifex.com/projects/p/roundcube-webmail/resource/messages/
*/
$messages = array();
$messages['errortitle'] = 'An error occurred!';
$messages['loginfailed'] = 'Login failed.';
$messages['cookiesdisabled'] = 'Your browser does not accept cookies.';
$messages['sessionerror'] = 'Your session is invalid or expired.';
$messages['storageerror'] = 'Connection to storage server failed.';
$messages['servererror'] = 'Server Error!';
$messages['servererrormsg'] = 'Server Error: $msg';
$messages['accountlocked'] = 'Too many failed login attempts. Try again later.';
$messages['connerror'] = 'Connection Error (Failed to reach the server)!';
$messages['dberror'] = 'Database Error!';
$messages['windowopenerror'] = 'The popup window was blocked!';
$messages['requesttimedout'] = 'Request timed out';
$messages['errorreadonly'] = 'Unable to perform operation. Folder is read-only.';
$messages['errornoperm'] = 'Unable to perform operation. Permission denied.';
$messages['erroroverquota'] = 'Unable to perform operation. No free disk space.';
$messages['erroroverquotadelete'] = 'No free disk space. Use SHIFT+DEL to delete a message.';
$messages['invalidrequest'] = 'Invalid request! No data was saved.';
$messages['invalidhost'] = 'Invalid server name.';
$messages['nomessagesfound'] = 'No messages found in this mailbox.';
$messages['loggedout'] = 'You have successfully terminated the session. Goodbye!';
$messages['mailboxempty'] = 'Mailbox is empty';
$messages['nomessages'] = 'No messages';
$messages['refreshing'] = 'Refreshing...';
$messages['loading'] = 'Loading...';
$messages['uploading'] = 'Uploading file...';
$messages['attaching'] = 'Attaching file...';
$messages['uploadingmany'] = 'Uploading files...';
$messages['loadingdata'] = 'Loading data...';
$messages['checkingmail'] = 'Checking for new messages...';
$messages['sendingmessage'] = 'Sending message...';
$messages['messagesent'] = 'Message sent successfully.';
$messages['savingmessage'] = 'Saving message...';
$messages['messagesaved'] = 'Message saved to Drafts.';
$messages['successfullysaved'] = 'Successfully saved.';
$messages['savingresponse'] = 'Saving response text...';
$messages['deleteresponseconfirm'] = 'Do you really want to delete this response text?';
$messages['nocontactselected'] = 'You have not selected a contact yet.';
$messages['addedsuccessfully'] = 'Contact added successfully to address book.';
$messages['contactexists'] = 'A contact with the same email address already exists.';
$messages['contactnameexists'] = 'A contact with the same name already exists.';
$messages['blockedimages'] = 'To protect your privacy, remote images are blocked in this message.';
$messages['blockedresources'] = 'To protect your privacy remote resources have been blocked.';
$messages['encryptedmessage'] = 'This is an encrypted message and can not be displayed. Sorry!';
$messages['externalmessagedecryption'] = 'This is an encrypted message and can be decrypted with your browser extension.';
$messages['nopubkeyfor'] = 'No valid public key found for $email';
$messages['nopubkeyforsender'] = 'No valid public key found for your sender identity. Do you want to encrypt the message for the recipients only?';
$messages['encryptnoattachments'] = 'Already uploaded attachments cannot be encrypted. Please re-add them in the encryption editor.';
$messages['searchpubkeyservers'] = 'Do you want to search public key servers for the missing keys?';
$messages['encryptpubkeysfound'] = 'The following public keys have been found:';
$messages['keyservererror'] = 'Failed to get key from keyserver';
$messages['keyimportsuccess'] = 'Public key $key successfully imported into your key ring';
$messages['nocontactsfound'] = 'No contacts found.';
$messages['contactnotfound'] = 'The requested contact was not found.';
$messages['contactsearchonly'] = 'Enter some search terms to find contacts';
$messages['sendingfailed'] = 'Failed to send message.';
$messages['senttooquickly'] = 'Please wait $sec sec(s). before sending this message.';
$messages['errorsavingsent'] = 'An error occurred while saving sent message.';
$messages['errorsaving'] = 'An error occurred while saving.';
$messages['errormoving'] = 'Could not move the message(s).';
$messages['errorcopying'] = 'Could not copy the message(s).';
$messages['errordeleting'] = 'Could not delete the message(s).';
$messages['errormarking'] = 'Could not mark the message(s).';
$messages['alerttitle'] = 'Attention';
$messages['confirmationtitle'] = 'Are you sure...';
$messages['deletecontactconfirm'] = 'Do you really want to delete selected contact(s)?';
$messages['deletegroupconfirm'] = 'Do you really want to delete selected group?';
$messages['deletemessagesconfirm'] = 'Do you really want to delete selected message(s)?';
$messages['deletefolderconfirm'] = 'Do you really want to delete this folder?';
$messages['movefolderconfirm'] = 'Do you really want to move this folder?';
$messages['purgefolderconfirm'] = 'Do you really want to delete all messages in this folder?';
$messages['contactdeleting'] = 'Deleting contact(s)...';
$messages['groupdeleting'] = 'Deleting group...';
$messages['folderdeleting'] = 'Deleting folder...';
$messages['foldermoving'] = 'Moving folder...';
$messages['foldersubscribing'] = 'Subscribing folder...';
$messages['folderunsubscribing'] = 'Unsubscribing folder...';
$messages['formincomplete'] = 'The form was not completely filled out.';
$messages['noemailwarning'] = 'Please enter a valid email address.';
$messages['nonamewarning'] = 'Please enter a name.';
$messages['nopagesizewarning'] = 'Please enter a page size.';
$messages['nosenderwarning'] = 'Please enter sender email address.';
$messages['recipientsadded'] = 'Recipient(s) added successfully.';
$messages['norecipientwarning'] = 'Please enter at least one recipient.';
$messages['disclosedrecipwarning'] = 'All recipients will see each others e-mail addresses. To prevent this and protect their privacy you can use the Bcc field.';
$messages['disclosedreciptitle'] = 'Too many public recipients';
$messages['nosubjectwarning'] = 'The "Subject" field is empty. Would you like to enter one now?';
$messages['nosubjecttitle'] = 'No subject';
$messages['nobodywarning'] = 'Send this message without text?';
$messages['notsentwarning'] = 'The message has not been sent and has unsaved changes. Do you want to discard your changes?';
$messages['restoresavedcomposedata'] = 'A previously composed but unsent message was found.\n\nSubject: $subject\nSaved: $date\n\nDo you want to restore this message?';
$messages['nosearchname'] = 'Please enter a contact name or email address.';
$messages['notuploadedwarning'] = 'Not all attachments have been uploaded yet. Please wait or cancel the upload.';
$messages['searchsuccessful'] = '$nr messages found.';
$messages['contactsearchsuccessful'] = '$nr contacts found.';
$messages['searchnomatch'] = 'Search returned no matches.';
$messages['searching'] = 'Searching...';
$messages['checking'] = 'Checking...';
$messages['stillsearching'] = 'Still searching...';
$messages['nospellerrors'] = 'No spelling errors found.';
$messages['folderdeleted'] = 'Folder successfully deleted.';
$messages['foldersubscribed'] = 'Folder successfully subscribed.';
$messages['folderunsubscribed'] = 'Folder successfully unsubscribed.';
$messages['folderpurged'] = 'Folder has successfully been emptied.';
$messages['folderexpunged'] = 'Folder has successfully been compacted.';
$messages['deletedsuccessfully'] = 'Successfully deleted.';
$messages['converting'] = 'Removing formatting...';
$messages['messageopenerror'] = 'Could not load message from server.';
$messages['filelinkerror'] = 'Attaching the file failed.';
$messages['fileuploaderror'] = 'File upload failed.';
$messages['filesizeerror'] = 'The uploaded file exceeds the maximum size of $size.';
$messages['filecounterror'] = 'You can upload maximum $count files at once.';
$messages['msgsizeerror'] = 'Failed to attach a file. Maximum size of a message ($size) exceeded.';
$messages['msgsizeerrorfwd'] = 'Maximum size of a message ($size) exceeded. $num message(s) have not been attached.';
$messages['copysuccess'] = 'Successfully copied $nr contacts.';
$messages['movesuccess'] = 'Successfully moved $nr contacts.';
$messages['copyerror'] = 'Could not copy any contacts.';
$messages['moveerror'] = 'Could not move any contacts.';
$messages['sourceisreadonly'] = 'This address source is read only.';
$messages['errorsavingcontact'] = 'Could not save the contact address.';
$messages['movingmessage'] = 'Moving message(s)...';
$messages['copyingmessage'] = 'Copying message(s)...';
$messages['copyingcontact'] = 'Copying contact(s)...';
$messages['movingcontact'] = 'Moving contact(s)...';
$messages['deletingmessage'] = 'Deleting message(s)...';
$messages['markingmessage'] = 'Marking message(s)...';
$messages['addingmember'] = 'Adding contact(s) to the group...';
$messages['removingmember'] = 'Removing contact(s) from the group...';
$messages['receiptsent'] = 'Successfully sent a read receipt.';
$messages['errorsendingreceipt'] = 'Could not send the receipt.';
$messages['deleteidentityconfirm'] = 'Do you really want to delete this identity?';
$messages['nodeletelastidentity'] = 'You cannot delete this identity, it\'s your last one.';
$messages['forbiddencharacter'] = 'Folder name contains a forbidden character.';
$messages['selectimportfile'] = 'Please select a file to upload.';
$messages['addresswriterror'] = 'The selected address book is not writeable.';
$messages['contactaddedtogroup'] = 'Successfully added the contacts to this group.';
$messages['contactremovedfromgroup'] = 'Successfully removed contacts from this group.';
$messages['nogroupassignmentschanged'] = 'No group assignments changed.';
$messages['importwait'] = 'Importing, please wait...';
$messages['importformaterror'] = 'Import failed! The uploaded file is not a valid import data file.';
$messages['importconfirm'] = '<b>Successfully imported $inserted contacts</b>';
$messages['importconfirmskipped'] = '<b>Skipped $skipped existing entries</b>';
$messages['importmessagesuccess'] = 'Successfully imported $nr messages';
$messages['importmessageerror'] = 'Import failed! The uploaded file is not a valid message or mailbox file';
$messages['opnotpermitted'] = 'Operation not permitted!';
$messages['nofromaddress'] = 'Missing email address in selected identity.';
$messages['editorwarning'] = 'Switching editor type may cause text formatting to be lost. Do you wish to continue?';
$messages['httpreceivedencrypterror'] = 'A fatal configuration error occurred. Contact your administrator immediately. <b>Your message can not be sent.</b>';
$messages['smtpconnerror'] = 'SMTP Error ($code): Connection to server failed.';
$messages['smtpautherror'] = 'SMTP Error ($code): Authentication failed.';
$messages['smtpfromerror'] = 'SMTP Error ($code): Failed to set sender "$from" ($msg).';
$messages['smtptoerror'] = 'SMTP Error ($code): Failed to add recipient "$to" ($msg).';
$messages['smtprecipientserror'] = 'SMTP Error: Unable to parse recipients list.';
$messages['smtputf8error'] = 'SMTP Error: Server does not support Unicode in email address.';
$messages['smtpsizeerror'] = 'SMTP Error: Message size exceeds server limit ($limit)';
$messages['smtperror'] = 'SMTP Error: $msg';
$messages['emailformaterror'] = 'Invalid email address: $email';
$messages['toomanyrecipients'] = 'Too many recipients. Reduce the number of recipients to $max.';
$messages['maxgroupmembersreached'] = 'The number of group members exceeds the maximum of $max.';
$messages['internalerror'] = 'An internal error occurred. Please try again.';
$messages['contactdelerror'] = 'Could not delete contact(s).';
$messages['contactdeleted'] = 'Contact(s) deleted successfully.';
$messages['contactrestoreerror'] = 'Could not restore deleted contact(s).';
$messages['contactrestored'] = 'Contact(s) restored successfully.';
$messages['groupdeleted'] = 'Group deleted successfully.';
$messages['grouprenamed'] = 'Group renamed successfully.';
$messages['groupcreated'] = 'Group created successfully.';
$messages['savedsearchdeleted'] = 'Saved search deleted successfully.';
$messages['savedsearchdeleteerror'] = 'Could not delete saved search.';
$messages['savedsearchcreated'] = 'Saved search created successfully.';
$messages['savedsearchcreateerror'] = 'Could not create saved search.';
$messages['messagedeleted'] = 'Message(s) deleted successfully.';
$messages['messagemoved'] = 'Message(s) moved successfully.';
$messages['messagemovedtotrash'] = 'Message(s) moved to Trash successfully.';
$messages['messagecopied'] = 'Message(s) copied successfully.';
$messages['messagemarked'] = 'Message(s) marked successfully.';
$messages['autocompletechars'] = 'Enter at least $min characters for autocompletion.';
$messages['autocompletemore'] = 'More matching entries found. Please type more characters.';
$messages['namecannotbeempty'] = 'Name cannot be empty.';
$messages['nametoolong'] = 'Name is too long.';
$messages['namedotforbidden'] = 'Folder name cannot start with a dot.';
$messages['folderupdated'] = 'Folder updated successfully.';
$messages['foldercreated'] = 'Folder created successfully.';
$messages['invalidimageformat'] = 'Not a valid image format.';
$messages['mispellingsfound'] = 'Spelling errors detected in the message.';
$messages['parentnotwritable'] = 'Unable to create/move folder into selected parent folder. No access rights.';
$messages['messagetoobig'] = 'The message part is too big to process it.';
$messages['attachmentvalidationerror'] = 'This attachment is suspicious because its type doesn\'t match the type declared in the message. If you do not trust the sender, you shouldn\'t open it in the browser because it may contain malicious contents.<br/><br/><em>Expected: $expected; found: $detected</em>';
$messages['noscriptwarning'] = 'Warning: This webmail service requires Javascript! In order to use it please enable Javascript in your browser\'s settings.';
$messages['messageissent'] = 'The message was already sent, but not saved yet. Do you want to save it now?';
$messages['errnotfound'] = 'File Not Found';
$messages['errnotfoundexplain'] = 'The requested resource was not found!';
$messages['errfailedrequest'] = 'Failed request';
$messages['errauthorizationfailed'] = 'Authorization Failed';
$messages['errunauthorizedexplain'] = 'Could not verify that you are authorized to access this service!';
$messages['errrequestcheckfailed'] = 'Request Check Failed';
$messages['errcsrfprotectionexplain'] = "For your protection, access to this resource is secured against CSRF.\nIf you see this, you probably didn't log out before leaving the web application.\n\nHuman interaction is now required to continue.";
$messages['errcontactserveradmin'] = 'Please contact your server-administrator.';
$messages['clicktoresumesession'] = 'Click here to resume your previous session';
$messages['errcomposesession'] = 'Compose session error';
$messages['errcomposesessionexplain'] = 'Requested compose session not found.';
$messages['clicktocompose'] = 'Click here to compose a new message';
$messages['nosupporterror'] = 'This feature is not supported by your web browser.';
$messages['siginserted'] = 'Signature inserted successfully.';
$messages['responseinserted'] = 'Response inserted successfully.';
$messages['listempty'] = 'The list is empty.';
$messages['listusebutton'] = 'Use the Create button to add a new record.';
$messages['keypaircreatesuccess'] = 'A new key pair has been successfully created for $identity.';
diff --git a/program/localization/en_US/timezones.inc b/program/localization/en_US/timezones.inc
index 9ff3cc255..0e58adbb9 100644
--- a/program/localization/en_US/timezones.inc
+++ b/program/localization/en_US/timezones.inc
@@ -1,457 +1,456 @@
<?php
/*
+-----------------------------------------------------------------------+
- | localization/<lang>/timezones.inc |
- | |
| Localization file of the Roundcube Webmail client |
- | Copyright (C) 2005-2017, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
+-----------------------------------------------------------------------+
For translation see https://www.transifex.com/projects/p/roundcube-webmail/resource/timezones/
*/
$labels = array();
$labels['tz:utc'] = 'UTC';
$labels['tz:africa'] = 'Africa';
$labels['tz:africa:abidjan'] = 'Abidjan';
$labels['tz:africa:accra'] = 'Accra';
$labels['tz:africa:addis_ababa'] = 'Addis Ababa';
$labels['tz:africa:algiers'] = 'Algiers';
$labels['tz:africa:asmara'] = 'Asmara';
$labels['tz:africa:bamako'] = 'Bamako';
$labels['tz:africa:bangui'] = 'Bangui';
$labels['tz:africa:banjul'] = 'Banjul';
$labels['tz:africa:bissau'] = 'Bissau';
$labels['tz:africa:blantyre'] = 'Blantyre';
$labels['tz:africa:brazzaville'] = 'Brazzaville';
$labels['tz:africa:bujumbura'] = 'Bujumbura';
$labels['tz:africa:cairo'] = 'Cairo';
$labels['tz:africa:casablanca'] = 'Casablanca';
$labels['tz:africa:ceuta'] = 'Ceuta';
$labels['tz:africa:conakry'] = 'Conakry';
$labels['tz:africa:dakar'] = 'Dakar';
$labels['tz:africa:dar_es_salaam'] = 'Dar es Salaam';
$labels['tz:africa:djibouti'] = 'Djibouti';
$labels['tz:africa:douala'] = 'Douala';
$labels['tz:africa:el_aaiun'] = 'El Aaiun';
$labels['tz:africa:freetown'] = 'Freetown';
$labels['tz:africa:gaborone'] = 'Gaborone';
$labels['tz:africa:harare'] = 'Harare';
$labels['tz:africa:johannesburg'] = 'Johannesburg';
$labels['tz:africa:juba'] = 'Juba';
$labels['tz:africa:kampala'] = 'Kampala';
$labels['tz:africa:khartoum'] = 'Khartoum';
$labels['tz:africa:kigali'] = 'Kigali';
$labels['tz:africa:kinshasa'] = 'Kinshasa';
$labels['tz:africa:lagos'] = 'Lagos';
$labels['tz:africa:libreville'] = 'Libreville';
$labels['tz:africa:lome'] = 'Lomé';
$labels['tz:africa:luanda'] = 'Luanda';
$labels['tz:africa:lubumbashi'] = 'Lubumbashi';
$labels['tz:africa:lusaka'] = 'Lusaka';
$labels['tz:africa:malabo'] = 'Malabo';
$labels['tz:africa:maputo'] = 'Maputo';
$labels['tz:africa:maseru'] = 'Maseru';
$labels['tz:africa:mbabane'] = 'Mbabane';
$labels['tz:africa:mogadishu'] = 'Mogadishu';
$labels['tz:africa:monrovia'] = 'Monrovia';
$labels['tz:africa:nairobi'] = 'Nairobi';
$labels['tz:africa:ndjamena'] = 'Ndjamena';
$labels['tz:africa:niamey'] = 'Niamey';
$labels['tz:africa:nouakchott'] = 'Nouakchott';
$labels['tz:africa:ouagadougou'] = 'Ouagadougou';
$labels['tz:africa:porto-novo'] = 'Porto-Novo';
$labels['tz:africa:sao_tome'] = 'São Tomé';
$labels['tz:africa:tripoli'] = 'Tripoli';
$labels['tz:africa:tunis'] = 'Tunis';
$labels['tz:africa:windhoek'] = 'Windhoek';
$labels['tz:america'] = 'America';
$labels['tz:america:adak'] = 'Adak';
$labels['tz:america:anchorage'] = 'Anchorage';
$labels['tz:america:anguilla'] = 'Anguilla';
$labels['tz:america:antigua'] = 'Antigua';
$labels['tz:america:araguaina'] = 'Araguaina';
$labels['tz:america:argentina'] = 'Argentina';
$labels['tz:america:argentina:buenos_aires'] = 'Buenos Aires';
$labels['tz:america:argentina:catamarca'] = 'Catamarca';
$labels['tz:america:argentina:cordoba'] = 'Cordoba';
$labels['tz:america:argentina:jujuy'] = 'Jujuy';
$labels['tz:america:argentina:la_rioja'] = 'La Rioja';
$labels['tz:america:argentina:mendoza'] = 'Mendoza';
$labels['tz:america:argentina:rio_gallegos'] = 'Rio Gallegos';
$labels['tz:america:argentina:salta'] = 'Salta';
$labels['tz:america:argentina:san_juan'] = 'San Juan';
$labels['tz:america:argentina:san_luis'] = 'San Luis';
$labels['tz:america:argentina:tucuman'] = 'Tucuman';
$labels['tz:america:argentina:ushuaia'] = 'Ushuaia';
$labels['tz:america:aruba'] = 'Aruba';
$labels['tz:america:asuncion'] = 'Asunción';
$labels['tz:america:atikokan'] = 'Atikokan';
$labels['tz:america:bahia'] = 'Bahia';
$labels['tz:america:bahia_banderas'] = 'Bahia Banderas';
$labels['tz:america:barbados'] = 'Barbados';
$labels['tz:america:belem'] = 'Belem';
$labels['tz:america:belize'] = 'Belize';
$labels['tz:america:blanc-sablon'] = 'Blanc-Sablon';
$labels['tz:america:boa_vista'] = 'Boa Vista';
$labels['tz:america:bogota'] = 'Bogota';
$labels['tz:america:boise'] = 'Boise';
$labels['tz:america:cambridge_bay'] = 'Cambridge Bay';
$labels['tz:america:campo_grande'] = 'Campo Grande';
$labels['tz:america:cancun'] = 'Cancun';
$labels['tz:america:caracas'] = 'Caracas';
$labels['tz:america:cayenne'] = 'Cayenne';
$labels['tz:america:cayman'] = 'Cayman';
$labels['tz:america:chicago'] = 'Chicago';
$labels['tz:america:chihuahua'] = 'Chihuahua';
$labels['tz:america:costa_rica'] = 'Costa Rica';
$labels['tz:america:creston'] = 'Creston';
$labels['tz:america:cuiaba'] = 'Cuiaba';
$labels['tz:america:curacao'] = 'Curaçao';
$labels['tz:america:danmarkshavn'] = 'Danmarkshavn';
$labels['tz:america:dawson'] = 'Dawson';
$labels['tz:america:dawson_creek'] = 'Dawson Creek';
$labels['tz:america:denver'] = 'Denver';
$labels['tz:america:detroit'] = 'Detroit';
$labels['tz:america:dominica'] = 'Dominica';
$labels['tz:america:edmonton'] = 'Edmonton';
$labels['tz:america:eirunepe'] = 'Eirunepe';
$labels['tz:america:el_salvador'] = 'El Salvador';
$labels['tz:america:fort_nelson'] = 'Fort Nelson';
$labels['tz:america:fortaleza'] = 'Fortaleza';
$labels['tz:america:glace_bay'] = 'Glace Bay';
$labels['tz:america:godthab'] = 'Godthab';
$labels['tz:america:goose_bay'] = 'Goose Bay';
$labels['tz:america:grand_turk'] = 'Grand Turk';
$labels['tz:america:grenada'] = 'Grenada';
$labels['tz:america:guadeloupe'] = 'Guadeloupe';
$labels['tz:america:guatemala'] = 'Guatemala';
$labels['tz:america:guayaquil'] = 'Guayaquil';
$labels['tz:america:guyana'] = 'Guyana';
$labels['tz:america:halifax'] = 'Halifax';
$labels['tz:america:havana'] = 'Havana';
$labels['tz:america:hermosillo'] = 'Hermosillo';
$labels['tz:america:indiana'] = 'Indiana';
$labels['tz:america:indiana:indianapolis'] = 'Indianapolis';
$labels['tz:america:indiana:knox'] = 'Knox';
$labels['tz:america:indiana:marengo'] = 'Marengo';
$labels['tz:america:indiana:petersburg'] = 'Petersburg';
$labels['tz:america:indiana:tell_city'] = 'Tell City';
$labels['tz:america:indiana:vevay'] = 'Vevay';
$labels['tz:america:indiana:vincennes'] = 'Vincennes';
$labels['tz:america:indiana:winamac'] = 'Winamac';
$labels['tz:america:inuvik'] = 'Inuvik';
$labels['tz:america:iqaluit'] = 'Iqaluit';
$labels['tz:america:jamaica'] = 'Jamaica';
$labels['tz:america:juneau'] = 'Juneau';
$labels['tz:america:kentucky'] = 'Kentucky';
$labels['tz:america:kentucky:louisville'] = 'Louisville';
$labels['tz:america:kentucky:monticello'] = 'Monticello';
$labels['tz:america:kralendijk'] = 'Kralendijk';
$labels['tz:america:la_paz'] = 'La Paz';
$labels['tz:america:lima'] = 'Lima';
$labels['tz:america:los_angeles'] = 'Los Angeles';
$labels['tz:america:lower_princes'] = 'Lower Princes';
$labels['tz:america:maceio'] = 'Maceio';
$labels['tz:america:managua'] = 'Managua';
$labels['tz:america:manaus'] = 'Manaus';
$labels['tz:america:marigot'] = 'Marigot';
$labels['tz:america:martinique'] = 'Martinique';
$labels['tz:america:matamoros'] = 'Matamoros';
$labels['tz:america:mazatlan'] = 'Mazatlan';
$labels['tz:america:menominee'] = 'Menominee';
$labels['tz:america:merida'] = 'Merida';
$labels['tz:america:metlakatla'] = 'Metlakatla';
$labels['tz:america:mexico_city'] = 'Mexico City';
$labels['tz:america:miquelon'] = 'Miquelon';
$labels['tz:america:moncton'] = 'Moncton';
$labels['tz:america:monterrey'] = 'Monterrey';
$labels['tz:america:montevideo'] = 'Montevideo';
$labels['tz:america:montserrat'] = 'Montserrat';
$labels['tz:america:nassau'] = 'Nassau';
$labels['tz:america:new_york'] = 'New York';
$labels['tz:america:nipigon'] = 'Nipigon';
$labels['tz:america:nome'] = 'Nome';
$labels['tz:america:noronha'] = 'Noronha';
$labels['tz:america:north_dakota'] = 'North Dakota';
$labels['tz:america:north_dakota:beulah'] = 'Beulah';
$labels['tz:america:north_dakota:center'] = 'Center';
$labels['tz:america:north_dakota:new_salem'] = 'New Salem';
$labels['tz:america:ojinaga'] = 'Ojinaga';
$labels['tz:america:panama'] = 'Panama';
$labels['tz:america:pangnirtung'] = 'Pangnirtung';
$labels['tz:america:paramaribo'] = 'Paramaribo';
$labels['tz:america:phoenix'] = 'Phoenix';
$labels['tz:america:port-au-prince'] = 'Port-au-Prince';
$labels['tz:america:port_of_spain'] = 'Port of Spain';
$labels['tz:america:porto_velho'] = 'Porto Velho';
$labels['tz:america:puerto_rico'] = 'Puerto Rico';
$labels['tz:america:rainy_river'] = 'Rainy River';
$labels['tz:america:rankin_inlet'] = 'Rankin Inlet';
$labels['tz:america:recife'] = 'Recife';
$labels['tz:america:regina'] = 'Regina';
$labels['tz:america:resolute'] = 'Resolute';
$labels['tz:america:rio_branco'] = 'Rio Branco';
$labels['tz:america:santarem'] = 'Santarem';
$labels['tz:america:santiago'] = 'Santiago';
$labels['tz:america:santo_domingo'] = 'Santo Domingo';
$labels['tz:america:sao_paulo'] = 'Sao Paulo';
$labels['tz:america:scoresbysund'] = 'Scoresbysund';
$labels['tz:america:sitka'] = 'Sitka';
$labels['tz:america:st_barthelemy'] = 'Saint Barthélemy';
$labels['tz:america:st_johns'] = "Saint John's";
$labels['tz:america:st_kitts'] = 'Saint Kitts';
$labels['tz:america:st_lucia'] = 'Saint Lucia';
$labels['tz:america:st_thomas'] = 'Saint Thomas';
$labels['tz:america:st_vincent'] = 'Saint Vincent';
$labels['tz:america:swift_current'] = 'Swift Current';
$labels['tz:america:tegucigalpa'] = 'Tegucigalpa';
$labels['tz:america:thule'] = 'Thule';
$labels['tz:america:thunder_bay'] = 'Thunder Bay';
$labels['tz:america:tijuana'] = 'Tijuana';
$labels['tz:america:toronto'] = 'Toronto';
$labels['tz:america:tortola'] = 'Tortola';
$labels['tz:america:vancouver'] = 'Vancouver';
$labels['tz:america:whitehorse'] = 'Whitehorse';
$labels['tz:america:winnipeg'] = 'Winnipeg';
$labels['tz:america:yakutat'] = 'Yakutat';
$labels['tz:america:yellowknife'] = 'Yellowknife';
$labels['tz:antarctica'] = 'Antarctica';
$labels['tz:antarctica:casey'] = 'Casey';
$labels['tz:antarctica:davis'] = 'Davis';
$labels['tz:antarctica:dumontdurville'] = "Dumont d'Urville";
$labels['tz:antarctica:macquarie'] = 'Macquarie';
$labels['tz:antarctica:mawson'] = 'Mawson';
$labels['tz:antarctica:mcmurdo'] = 'McMurdo';
$labels['tz:antarctica:palmer'] = 'Palmer';
$labels['tz:antarctica:rothera'] = 'Rothera';
$labels['tz:antarctica:syowa'] = 'Syowa';
$labels['tz:antarctica:troll'] = 'Troll';
$labels['tz:antarctica:vostok'] = 'Vostok';
$labels['tz:arctic'] = 'Arctic';
$labels['tz:arctic:longyearbyen'] = 'Longyearbyen';
$labels['tz:asia'] = 'Asia';
$labels['tz:asia:aden'] = 'Aden';
$labels['tz:asia:almaty'] = 'Almaty';
$labels['tz:asia:amman'] = 'Amman';
$labels['tz:asia:anadyr'] = 'Anadyr';
$labels['tz:asia:aqtau'] = 'Aqtau';
$labels['tz:asia:aqtobe'] = 'Aqtobe';
$labels['tz:asia:ashgabat'] = 'Ashgabat';
$labels['tz:asia:atyrau'] = 'Atyrau';
$labels['tz:asia:baghdad'] = 'Baghdad';
$labels['tz:asia:bahrain'] = 'Bahrain';
$labels['tz:asia:baku'] = 'Baku';
$labels['tz:asia:bangkok'] = 'Bangkok';
$labels['tz:asia:barnaul'] = 'Barnaul';
$labels['tz:asia:beirut'] = 'Beirut';
$labels['tz:asia:bishkek'] = 'Bishkek';
$labels['tz:asia:brunei'] = 'Brunei';
$labels['tz:asia:chita'] = 'Chita';
$labels['tz:asia:choibalsan'] = 'Choibalsan';
$labels['tz:asia:colombo'] = 'Colombo';
$labels['tz:asia:damascus'] = 'Damascus';
$labels['tz:asia:dhaka'] = 'Dhaka';
$labels['tz:asia:dili'] = 'Dili';
$labels['tz:asia:dubai'] = 'Dubai';
$labels['tz:asia:dushanbe'] = 'Dushanbe';
$labels['tz:asia:famagusta'] = 'Famagusta';
$labels['tz:asia:gaza'] = 'Gaza';
$labels['tz:asia:hebron'] = 'Hebron';
$labels['tz:asia:ho_chi_minh'] = 'Ho Chi Minh';
$labels['tz:asia:hong_kong'] = 'Hong Kong';
$labels['tz:asia:hovd'] = 'Hovd';
$labels['tz:asia:irkutsk'] = 'Irkutsk';
$labels['tz:asia:jakarta'] = 'Jakarta';
$labels['tz:asia:jayapura'] = 'Jayapura';
$labels['tz:asia:jerusalem'] = 'Jerusalem';
$labels['tz:asia:kabul'] = 'Kabul';
$labels['tz:asia:kamchatka'] = 'Kamchatka';
$labels['tz:asia:karachi'] = 'Karachi';
$labels['tz:asia:kathmandu'] = 'Kathmandu';
$labels['tz:asia:khandyga'] = 'Khandyga';
$labels['tz:asia:kolkata'] = 'Kolkata';
$labels['tz:asia:krasnoyarsk'] = 'Krasnoyarsk';
$labels['tz:asia:kuala_lumpur'] = 'Kuala Lumpur';
$labels['tz:asia:kuching'] = 'Kuching';
$labels['tz:asia:kuwait'] = 'Kuwait';
$labels['tz:asia:macau'] = 'Macau';
$labels['tz:asia:magadan'] = 'Magadan';
$labels['tz:asia:makassar'] = 'Makassar';
$labels['tz:asia:manila'] = 'Manila';
$labels['tz:asia:muscat'] = 'Muscat';
$labels['tz:asia:nicosia'] = 'Nicosia';
$labels['tz:asia:novokuznetsk'] = 'Novokuznetsk';
$labels['tz:asia:novosibirsk'] = 'Novosibirsk';
$labels['tz:asia:omsk'] = 'Omsk';
$labels['tz:asia:oral'] = 'Oral';
$labels['tz:asia:phnom_penh'] = 'Phnom Penh';
$labels['tz:asia:pontianak'] = 'Pontianak';
$labels['tz:asia:pyongyang'] = 'Pyongyang';
$labels['tz:asia:qatar'] = 'Qatar';
$labels['tz:asia:qyzylorda'] = 'Qyzylorda';
$labels['tz:asia:riyadh'] = 'Riyadh';
$labels['tz:asia:sakhalin'] = 'Sakhalin';
$labels['tz:asia:samarkand'] = 'Samarkand';
$labels['tz:asia:seoul'] = 'Seoul';
$labels['tz:asia:shanghai'] = 'Shanghai';
$labels['tz:asia:singapore'] = 'Singapore';
$labels['tz:asia:srednekolymsk'] = 'Srednekolymsk';
$labels['tz:asia:taipei'] = 'Taipei';
$labels['tz:asia:tashkent'] = 'Tashkent';
$labels['tz:asia:tbilisi'] = 'Tbilisi';
$labels['tz:asia:tehran'] = 'Tehran';
$labels['tz:asia:thimphu'] = 'Thimphu';
$labels['tz:asia:tokyo'] = 'Tokyo';
$labels['tz:asia:tomsk'] = 'Tomsk';
$labels['tz:asia:ulaanbaatar'] = 'Ulaanbaatar';
$labels['tz:asia:urumqi'] = 'Urumqi';
$labels['tz:asia:ust-nera'] = 'Ust-Nera';
$labels['tz:asia:vientiane'] = 'Vientiane';
$labels['tz:asia:vladivostok'] = 'Vladivostok';
$labels['tz:asia:yakutsk'] = 'Yakutsk';
$labels['tz:asia:yangon'] = 'Yangon';
$labels['tz:asia:yekaterinburg'] = 'Yekaterinburg';
$labels['tz:asia:yerevan'] = 'Yerevan';
$labels['tz:atlantic'] = 'Atlantic';
$labels['tz:atlantic:azores'] = 'Azores';
$labels['tz:atlantic:bermuda'] = 'Bermuda';
$labels['tz:atlantic:canary'] = 'Canary';
$labels['tz:atlantic:cape_verde'] = 'Cape Verde';
$labels['tz:atlantic:faroe'] = 'Faroe';
$labels['tz:atlantic:madeira'] = 'Madeira';
$labels['tz:atlantic:reykjavik'] = 'Reykjavik';
$labels['tz:atlantic:south_georgia'] = 'South Georgia';
$labels['tz:atlantic:st_helena'] = 'St Helena';
$labels['tz:atlantic:stanley'] = 'Stanley';
$labels['tz:australia'] = 'Australia';
$labels['tz:australia:adelaide'] = 'Adelaide';
$labels['tz:australia:brisbane'] = 'Brisbane';
$labels['tz:australia:broken_hill'] = 'Broken Hill';
$labels['tz:australia:currie'] = 'Currie';
$labels['tz:australia:darwin'] = 'Darwin';
$labels['tz:australia:eucla'] = 'Eucla';
$labels['tz:australia:hobart'] = 'Hobart';
$labels['tz:australia:lindeman'] = 'Lindeman';
$labels['tz:australia:lord_howe'] = 'Lord Howe';
$labels['tz:australia:melbourne'] = 'Melbourne';
$labels['tz:australia:perth'] = 'Perth';
$labels['tz:australia:sydney'] = 'Sydney';
$labels['tz:europe'] = 'Europe';
$labels['tz:europe:amsterdam'] = 'Amsterdam';
$labels['tz:europe:andorra'] = 'Andorra';
$labels['tz:europe:astrakhan'] = 'Astrakhan';
$labels['tz:europe:athens'] = 'Athens';
$labels['tz:europe:belgrade'] = 'Belgrade';
$labels['tz:europe:berlin'] = 'Berlin';
$labels['tz:europe:bratislava'] = 'Bratislava';
$labels['tz:europe:brussels'] = 'Brussels';
$labels['tz:europe:bucharest'] = 'Bucharest';
$labels['tz:europe:budapest'] = 'Budapest';
$labels['tz:europe:busingen'] = 'Busingen';
$labels['tz:europe:chisinau'] = 'Chisinau';
$labels['tz:europe:copenhagen'] = 'Copenhagen';
$labels['tz:europe:dublin'] = 'Dublin';
$labels['tz:europe:gibraltar'] = 'Gibraltar';
$labels['tz:europe:guernsey'] = 'Guernsey';
$labels['tz:europe:helsinki'] = 'Helsinki';
$labels['tz:europe:isle_of_man'] = 'Isle of Man';
$labels['tz:europe:istanbul'] = 'Istanbul';
$labels['tz:europe:jersey'] = 'Jersey';
$labels['tz:europe:kaliningrad'] = 'Kaliningrad';
$labels['tz:europe:kiev'] = 'Kiev';
$labels['tz:europe:kirov'] = 'Kirov';
$labels['tz:europe:lisbon'] = 'Lisbon';
$labels['tz:europe:ljubljana'] = 'Ljubljana';
$labels['tz:europe:london'] = 'London';
$labels['tz:europe:luxembourg'] = 'Luxembourg';
$labels['tz:europe:madrid'] = 'Madrid';
$labels['tz:europe:malta'] = 'Malta';
$labels['tz:europe:mariehamn'] = 'Mariehamn';
$labels['tz:europe:minsk'] = 'Minsk';
$labels['tz:europe:monaco'] = 'Monaco';
$labels['tz:europe:moscow'] = 'Moscow';
$labels['tz:europe:oslo'] = 'Oslo';
$labels['tz:europe:paris'] = 'Paris';
$labels['tz:europe:podgorica'] = 'Podgorica';
$labels['tz:europe:prague'] = 'Prague';
$labels['tz:europe:riga'] = 'Riga';
$labels['tz:europe:rome'] = 'Rome';
$labels['tz:europe:samara'] = 'Samara';
$labels['tz:europe:san_marino'] = 'San Marino';
$labels['tz:europe:sarajevo'] = 'Sarajevo';
$labels['tz:europe:saratov'] = 'Saratov';
$labels['tz:europe:simferopol'] = 'Simferopol';
$labels['tz:europe:skopje'] = 'Skopje';
$labels['tz:europe:sofia'] = 'Sofia';
$labels['tz:europe:stockholm'] = 'Stockholm';
$labels['tz:europe:tallinn'] = 'Tallinn';
$labels['tz:europe:tirane'] = 'Tirane';
$labels['tz:europe:ulyanovsk'] = 'Ulyanovsk';
$labels['tz:europe:uzhgorod'] = 'Uzhgorod';
$labels['tz:europe:vaduz'] = 'Vaduz';
$labels['tz:europe:vatican'] = 'Vatican';
$labels['tz:europe:vienna'] = 'Vienna';
$labels['tz:europe:vilnius'] = 'Vilnius';
$labels['tz:europe:volgograd'] = 'Volgograd';
$labels['tz:europe:warsaw'] = 'Warsaw';
$labels['tz:europe:zagreb'] = 'Zagreb';
$labels['tz:europe:zaporozhye'] = 'Zaporozhye';
$labels['tz:europe:zurich'] = 'Zurich';
$labels['tz:indian'] = 'Indian';
$labels['tz:indian:antananarivo'] = 'Antananarivo';
$labels['tz:indian:chagos'] = 'Chagos';
$labels['tz:indian:christmas'] = 'Christmas';
$labels['tz:indian:cocos'] = 'Cocos';
$labels['tz:indian:comoro'] = 'Comoro';
$labels['tz:indian:kerguelen'] = 'Kerguelen';
$labels['tz:indian:mahe'] = 'Mahe';
$labels['tz:indian:maldives'] = 'Maldives';
$labels['tz:indian:mauritius'] = 'Mauritius';
$labels['tz:indian:mayotte'] = 'Mayotte';
$labels['tz:indian:reunion'] = 'Reunion';
$labels['tz:pacific'] = 'Pacific';
$labels['tz:pacific:apia'] = 'Apia';
$labels['tz:pacific:auckland'] = 'Auckland';
$labels['tz:pacific:bougainville'] = 'Bougainville';
$labels['tz:pacific:chatham'] = 'Chatham';
$labels['tz:pacific:chuuk'] = 'Chuuk';
$labels['tz:pacific:easter'] = 'Easter';
$labels['tz:pacific:efate'] = 'Efate';
$labels['tz:pacific:enderbury'] = 'Enderbury';
$labels['tz:pacific:fakaofo'] = 'Fakaofo';
$labels['tz:pacific:fiji'] = 'Fiji';
$labels['tz:pacific:funafuti'] = 'Funafuti';
$labels['tz:pacific:galapagos'] = 'Galapagos';
$labels['tz:pacific:gambier'] = 'Gambier';
$labels['tz:pacific:guadalcanal'] = 'Guadalcanal';
$labels['tz:pacific:guam'] = 'Guam';
$labels['tz:pacific:honolulu'] = 'Honolulu';
$labels['tz:pacific:johnston'] = 'Johnston';
$labels['tz:pacific:kiritimati'] = 'Kiritimati';
$labels['tz:pacific:kosrae'] = 'Kosrae';
$labels['tz:pacific:kwajalein'] = 'Kwajalein';
$labels['tz:pacific:majuro'] = 'Majuro';
$labels['tz:pacific:marquesas'] = 'Marquesas';
$labels['tz:pacific:midway'] = 'Midway';
$labels['tz:pacific:nauru'] = 'Nauru';
$labels['tz:pacific:niue'] = 'Niue';
$labels['tz:pacific:norfolk'] = 'Norfolk';
$labels['tz:pacific:noumea'] = 'Noumea';
$labels['tz:pacific:pago_pago'] = 'Pago Pago';
$labels['tz:pacific:palau'] = 'Palau';
$labels['tz:pacific:pitcairn'] = 'Pitcairn';
$labels['tz:pacific:pohnpei'] = 'Pohnpei';
$labels['tz:pacific:port_moresby'] = 'Port Moresby';
$labels['tz:pacific:rarotonga'] = 'Rarotonga';
$labels['tz:pacific:saipan'] = 'Saipan';
$labels['tz:pacific:tahiti'] = 'Tahiti';
$labels['tz:pacific:tarawa'] = 'Tarawa';
$labels['tz:pacific:tongatapu'] = 'Tongatapu';
$labels['tz:pacific:wake'] = 'Wake';
$labels['tz:pacific:wallis'] = 'Wallis';
diff --git a/program/steps/addressbook/copy.inc b/program/steps/addressbook/copy.inc
index 927521e74..4bf896822 100644
--- a/program/steps/addressbook/copy.inc
+++ b/program/steps/addressbook/copy.inc
@@ -1,128 +1,126 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/addressbook/copy.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2007-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Copy a contact record from one direcotry to another |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
// only process ajax requests
if (!$OUTPUT->ajax_call)
return;
$cids = rcmail_get_cids();
$target = rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST);
$target_group = rcube_utils::get_input_value('_togid', rcube_utils::INPUT_POST);
$success = 0;
$errormsg = 'copyerror';
$maxnum = $RCMAIL->config->get('max_group_members', 0);
foreach ($cids as $source => $cid) {
// Something wrong, target not specified
if (!strlen($target)) {
break;
}
// It maight happen when copying records from search result
// Do nothing, go to next source
if ((string)$target == (string)$source) {
continue;
}
$CONTACTS = $RCMAIL->get_address_book($source);
$TARGET = $RCMAIL->get_address_book($target);
if (!$TARGET || !$TARGET->ready || $TARGET->readonly) {
break;
}
$ids = array();
foreach ($cid as $cid) {
$a_record = $CONTACTS->get_record($cid, true);
// avoid copying groups
if ($a_record['_type'] == 'group')
continue;
// Check if contact exists, if so, we'll need it's ID
// Note: Some addressbooks allows empty email address field
// @TODO: should we check all email addresses?
$email = $CONTACTS->get_col_values('email', $a_record, true);
if (!empty($email))
$result = $TARGET->search('email', $email[0], 1, true, true);
else if (!empty($a_record['name']))
$result = $TARGET->search('name', $a_record['name'], 1, true, true);
else
$result = new rcube_result_set();
// insert contact record
if (!$result->count) {
$plugin = $RCMAIL->plugins->exec_hook('contact_create', array(
'record' => $a_record, 'source' => $target, 'group' => $target_group));
if (!$plugin['abort']) {
if ($insert_id = $TARGET->insert($plugin['record'], false)) {
$ids[] = $insert_id;
$success++;
}
}
else if ($plugin['result']) {
$ids = array_merge($ids, $plugin['result']);
$success++;
}
}
else {
$record = $result->first();
$ids[] = $record['ID'];
$errormsg = empty($email) ? 'contactnameexists' : 'contactexists';
}
}
// assign to group
if ($target_group && $TARGET->groups && !empty($ids)) {
$plugin = $RCMAIL->plugins->exec_hook('group_addmembers', array(
'group_id' => $target_group, 'ids' => $ids, 'source' => $target));
if (!$plugin['abort']) {
$TARGET->reset();
$TARGET->set_group($target_group);
if ($maxnum && ($TARGET->count()->count + count($plugin['ids']) > $maxnum)) {
$OUTPUT->show_message('maxgroupmembersreached', 'warning', array('max' => $maxnum));
$OUTPUT->send();
}
if (($cnt = $TARGET->add_to_group($target_group, $plugin['ids'])) && $cnt > $success)
$success = $cnt;
}
else if ($plugin['result']) {
$success = $plugin['result'];
}
$errormsg = $plugin['message'] ?: 'copyerror';
}
}
if (!$success)
$OUTPUT->show_message($errormsg, 'error');
else
$OUTPUT->show_message('copysuccess', 'confirmation', array('nr' => $success));
// send response
$OUTPUT->send();
diff --git a/program/steps/addressbook/delete.inc b/program/steps/addressbook/delete.inc
index d600c4a88..32e8ce8be 100644
--- a/program/steps/addressbook/delete.inc
+++ b/program/steps/addressbook/delete.inc
@@ -1,154 +1,152 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/addressbook/delete.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Delete the submitted contacts (CIDs) from the users address book |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
// process ajax requests only
if (!$OUTPUT->ajax_call) {
return;
}
$cids = rcmail_get_cids(null, rcube_utils::INPUT_POST);
$delcnt = 0;
// remove previous deletes
$undo_time = $RCMAIL->config->get('undo_timeout', 0);
$RCMAIL->session->remove('contact_undo');
foreach ($cids as $source => $cid) {
$CONTACTS = rcmail_contact_source($source);
if ($CONTACTS->readonly) {
// more sources? do nothing, probably we have search results from
// more than one source, some of these sources can be readonly
if (count($cids) == 1) {
$OUTPUT->show_message('contactdelerror', 'error');
$OUTPUT->command('list_contacts');
$OUTPUT->send();
}
continue;
}
$plugin = $RCMAIL->plugins->exec_hook('contact_delete', array(
'id' => $cid, 'source' => $source));
$deleted = !$plugin['abort'] ? $CONTACTS->delete($cid, $undo_time < 1) : $plugin['result'];
if (!$deleted) {
if ($plugin['message']) {
$error = $plugin['message'];
}
else if (($error = $CONTACTS->get_error()) && $error['message']) {
$error = $error['message'];
}
else {
$error = 'contactdelerror';
}
$source = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GP);
$group = rcube_utils::get_input_value('_gid', rcube_utils::INPUT_GP);
$OUTPUT->show_message($error, 'error');
$OUTPUT->command('list_contacts', $source, $group);
$OUTPUT->send();
}
else {
$delcnt += $deleted;
// store deleted contacts IDs in session for undo action
if ($undo_time > 0 && $CONTACTS->undelete) {
$_SESSION['contact_undo']['data'][$source] = $cid;
}
}
}
if (!empty($_SESSION['contact_undo'])) {
$_SESSION['contact_undo']['ts'] = time();
$msg = html::span(null, $RCMAIL->gettext('contactdeleted'))
. ' ' . html::a(array('onclick' => rcmail_output::JS_OBJECT_NAME.".command('undo', '', this)"), $RCMAIL->gettext('undo'));
$OUTPUT->show_message($msg, 'confirmation', null, true, $undo_time);
}
else {
$OUTPUT->show_message('contactdeleted', 'confirmation');
}
$page = isset($_SESSION['page']) ? $_SESSION['page'] : 1;
// update saved search after data changed
if (($records = rcmail_search_update(true)) !== false) {
// create resultset object
$count = count($records);
$first = ($page-1) * $PAGE_SIZE;
$result = new rcube_result_set($count, $first);
$pages = ceil((count($records) + $delcnt) / $PAGE_SIZE);
// last page and it's empty, display previous one
if ($result->count && $result->count <= ($PAGE_SIZE * ($page - 1))) {
$OUTPUT->command('list_page', 'prev');
$rowcount = $RCMAIL->gettext('loading');
}
// get records from the next page to add to the list
else if ($pages > 1 && $page < $pages) {
// sort the records
ksort($records, SORT_LOCALE_STRING);
$first += $PAGE_SIZE;
// create resultset object
$res = new rcube_result_set($count, $first - $delcnt);
if ($PAGE_SIZE < $count) {
$records = array_slice($records, $first - $delcnt, $delcnt);
}
$res->records = array_values($records);
$records = $res;
}
else {
unset($records);
}
}
else {
// count contacts for this user
$result = $CONTACTS->count();
$pages = ceil(($result->count + $delcnt) / $PAGE_SIZE);
// last page and it's empty, display previous one
if ($result->count && $result->count <= ($PAGE_SIZE * ($page - 1))) {
$OUTPUT->command('list_page', 'prev');
$rowcount = $RCMAIL->gettext('loading');
}
// get records from the next page to add to the list
else if ($pages > 1 && $page < $pages) {
$CONTACTS->set_page($page);
$records = $CONTACTS->list_records(null, -$delcnt);
}
}
// update message count display
$OUTPUT->set_env('pagecount', ceil($result->count / $PAGE_SIZE));
$OUTPUT->command('set_rowcount', $rowcount ?: rcmail_get_rowcount_text($result));
// add new rows from next page (if any)
if (!empty($records)) {
rcmail_js_contacts_list($records);
}
// send response
$OUTPUT->send();
diff --git a/program/steps/addressbook/edit.inc b/program/steps/addressbook/edit.inc
index 878a6080d..78d3c2ad1 100644
--- a/program/steps/addressbook/edit.inc
+++ b/program/steps/addressbook/edit.inc
@@ -1,278 +1,276 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/addressbook/edit.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Show edit form for a contact entry or to add a new one |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
if ($RCMAIL->action == 'edit') {
// Get contact ID and source ID from request
$cids = rcmail_get_cids();
$source = key($cids);
$cid = array_shift($cids[$source]);
// Initialize addressbook
$CONTACTS = rcmail_contact_source($source, true);
// Contact edit
if ($cid && ($record = $CONTACTS->get_record($cid, true))) {
$OUTPUT->set_env('cid', $record['ID']);
}
// editing not allowed here
if ($CONTACTS->readonly || $record['readonly']) {
$OUTPUT->show_message('sourceisreadonly');
$RCMAIL->overwrite_action('show');
return;
}
}
else {
$source = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
if (strlen($source)) {
$CONTACTS = $RCMAIL->get_address_book($source, true);
}
if (!$CONTACTS || $CONTACTS->readonly) {
$CONTACTS = $RCMAIL->get_address_book(-1, true);
$source = $RCMAIL->get_address_book_id($CONTACTS);
}
// Initialize addressbook
$CONTACTS = rcmail_contact_source($source, true);
}
$SOURCE_ID = $source;
rcmail_set_sourcename($CONTACTS);
$OUTPUT->add_handlers(array(
'contactedithead' => 'rcmail_contact_edithead',
'contacteditform' => 'rcmail_contact_editform',
'contactphoto' => 'rcmail_contact_photo',
'photouploadform' => 'rcmail_upload_photo_form',
'sourceselector' => 'rcmail_source_selector',
'filedroparea' => 'rcmail_photo_drop_area',
));
$OUTPUT->set_pagetitle($RCMAIL->gettext(($RCMAIL->action == 'add' ? 'addcontact' : 'editcontact')));
if ($RCMAIL->action == 'add' && $OUTPUT->template_exists('contactadd')) {
$OUTPUT->send('contactadd');
}
// this will be executed if no template for addcontact exists
$OUTPUT->send('contactedit');
function rcmail_get_edit_record()
{
global $RCMAIL, $CONTACTS;
// check if we have a valid result
if ($GLOBALS['EDIT_RECORD']) {
$record = $GLOBALS['EDIT_RECORD'];
}
else if ($RCMAIL->action != 'add'
&& !(($result = $CONTACTS->get_result()) && ($record = $result->first()))
) {
$RCMAIL->output->show_message('contactnotfound', 'error');
return false;
}
return $record;
}
function rcmail_contact_edithead($attrib)
{
global $RCMAIL;
// check if we have a valid result
$record = rcmail_get_edit_record();
$i_size = $attrib['size'] ?: 20;
$form = array(
'head' => array(
'name' => $RCMAIL->gettext('contactnameandorg'),
'content' => array(
'source' => array('id' => '_source', 'label' => $RCMAIL->gettext('addressbook')),
'prefix' => array('size' => $i_size),
'firstname' => array('size' => $i_size, 'visible' => true),
'middlename' => array('size' => $i_size),
'surname' => array('size' => $i_size, 'visible' => true),
'suffix' => array('size' => $i_size),
'name' => array('size' => 2*$i_size),
'nickname' => array('size' => 2*$i_size),
'organization' => array('size' => 2*$i_size),
'department' => array('size' => 2*$i_size),
'jobtitle' => array('size' => 2*$i_size),
)
)
);
list($form_start, $form_end) = get_form_tags($attrib);
unset($attrib['form'], $attrib['name'], $attrib['size']);
// return the address edit form
$out = rcmail_contact_form($form, $record, $attrib);
return $form_start . $out . $form_end;
}
function rcmail_contact_editform($attrib)
{
global $RCMAIL, $CONTACT_COLTYPES;
$record = rcmail_get_edit_record();
// copy (parsed) address template to client
if (preg_match_all('/\{([a-z0-9]+)\}([^{]*)/i', $RCMAIL->config->get('address_template', ''), $templ, PREG_SET_ORDER)) {
$RCMAIL->output->set_env('address_template', $templ);
}
$i_size = $attrib['size'] ?: 40;
$t_rows = $attrib['textarearows'] ?: 10;
$t_cols = $attrib['textareacols'] ?: 40;
$short_labels = rcube_utils::get_boolean($attrib['short-legend-labels']);
$form = array(
'contact' => array(
'name' => $RCMAIL->gettext('properties'),
'content' => array(
'email' => array('size' => $i_size, 'maxlength' => 254, 'visible' => true),
'phone' => array('size' => $i_size, 'visible' => true),
'address' => array('visible' => true),
'website' => array('size' => $i_size),
'im' => array('size' => $i_size),
),
),
'personal' => array(
'name' => $RCMAIL->gettext($short_labels ? 'personal' : 'personalinfo'),
'content' => array(
'gender' => array('visible' => true),
'maidenname' => array('size' => $i_size),
'birthday' => array('visible' => true),
'anniversary' => array(),
'manager' => array('size' => $i_size),
'assistant' => array('size' => $i_size),
'spouse' => array('size' => $i_size),
),
),
);
if (isset($CONTACT_COLTYPES['notes'])) {
$form['notes'] = array(
'name' => $RCMAIL->gettext('notes'),
'content' => array(
'notes' => array('size' => $t_cols, 'rows' => $t_rows, 'label' => false, 'visible' => true, 'limit' => 1),
),
'single' => true,
);
}
list($form_start, $form_end) = get_form_tags($attrib);
unset($attrib['form']);
// return the complete address edit form as table
$out = rcmail_contact_form($form, $record, $attrib);
return $form_start . $out . $form_end;
}
function rcmail_upload_photo_form($attrib)
{
global $RCMAIL;
$hidden = new html_hiddenfield(array('name' => '_cid', 'value' => $GLOBALS['cid']));
$attrib['prefix'] = $hidden->show();
$input_attr = array('name' => '_photo', 'accept' => 'image/*');
$RCMAIL->output->add_label('addphoto','replacephoto');
return $RCMAIL->upload_form($attrib, 'uploadform', 'upload-photo', $input_attr);
}
// similar function as in /steps/settings/edit_identity.inc
function get_form_tags($attrib)
{
global $CONTACTS, $EDIT_FORM, $RCMAIL, $SOURCE_ID;
$form_start = $form_end = '';
if (empty($EDIT_FORM)) {
$hiddenfields = new html_hiddenfield();
if ($RCMAIL->action == 'edit')
$hiddenfields->add(array('name' => '_source', 'value' => $SOURCE_ID));
$hiddenfields->add(array('name' => '_gid', 'value' => $CONTACTS->group_id));
$hiddenfields->add(array('name' => '_search', 'value' => rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC)));
if (($result = $CONTACTS->get_result()) && ($record = $result->first()))
$hiddenfields->add(array('name' => '_cid', 'value' => $record['ID']));
$form_start = $RCMAIL->output->request_form(array(
'name' => "form", 'method' => "post",
'task' => $RCMAIL->task, 'action' => 'save',
'request' => 'save.'.intval($record['ID']),
'noclose' => true) + $attrib, $hiddenfields->show());
$form_end = !strlen($attrib['form']) ? '</form>' : '';
$EDIT_FORM = $attrib['form'] ?: 'form';
$RCMAIL->output->add_gui_object('editform', $EDIT_FORM);
}
return array($form_start, $form_end);
}
function rcmail_source_selector($attrib)
{
global $RCMAIL, $SOURCE_ID;
$sources_list = $RCMAIL->get_address_sources(true, true);
if (count($sources_list) < 2) {
$source = $sources_list[$SOURCE_ID];
$hiddenfield = new html_hiddenfield(array('name' => '_source', 'value' => $SOURCE_ID));
return html::span($attrib, $source['name'] . $hiddenfield->show());
}
$attrib['name'] = '_source';
$attrib['is_escaped'] = true;
$attrib['onchange'] = rcmail_output::JS_OBJECT_NAME . ".command('save', 'reload', this.form)";
$select = new html_select($attrib);
foreach ($sources_list as $source)
$select->add($source['name'], $source['id']);
return $select->show($SOURCE_ID);
}
/**
* Register container as active area to drop photos onto
*/
function rcmail_photo_drop_area($attrib)
{
global $OUTPUT;
if ($attrib['id']) {
$OUTPUT->add_gui_object('filedrop', $attrib['id']);
$OUTPUT->set_env('filedrop', array('action' => 'upload-photo', 'fieldname' => '_photo', 'single' => 1, 'filter' => '^image/.+'));
}
}
diff --git a/program/steps/addressbook/export.inc b/program/steps/addressbook/export.inc
index 84c35f642..3959bfada 100644
--- a/program/steps/addressbook/export.inc
+++ b/program/steps/addressbook/export.inc
@@ -1,181 +1,179 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/addressbook/export.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2013, The Roundcube Dev Team |
- | Copyright (C) 2011-2013, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Export the selected address book as vCard file |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+-----------------------------------------------------------------------+
*/
$RCMAIL->request_security_check(rcube_utils::INPUT_GET);
// Use search result
if (!empty($_REQUEST['_search']) && isset($_SESSION['search'][$_REQUEST['_search']])) {
$sort_col = $RCMAIL->config->get('addressbook_sort_col', 'name');
$search = (array)$_SESSION['search'][$_REQUEST['_search']];
$records = array();
// Get records from all sources
foreach ($search as $s => $set) {
$source = $RCMAIL->get_address_book($s);
// reset page
$source->set_page(1);
$source->set_pagesize(99999);
$source->set_search_set($set);
// get records
$result = $source->list_records();
while ($record = $result->next()) {
// because vcard_map is per-source we need to create vcard here
prepare_for_export($record, $source);
$record['sourceid'] = $s;
$key = rcube_addressbook::compose_contact_key($record, $sort_col);
$records[$key] = $record;
}
unset($result);
}
// sort the records
ksort($records, SORT_LOCALE_STRING);
// create resultset object
$count = count($records);
$result = new rcube_result_set($count);
$result->records = array_values($records);
}
// selected contacts
else if (!empty($_REQUEST['_cid'])) {
$sort_col = $RCMAIL->config->get('addressbook_sort_col', 'name');
$records = array();
// Selected contact IDs (with multi-source support)
$cids = rcmail_get_cids();
foreach ($cids as $s => $ids) {
$source = $RCMAIL->get_address_book($s);
// reset page and page size (#6103)
$source->set_page(1);
$source->set_pagesize(count($ids));
$result = $source->search('ID', $ids, 1, true, true);
while ($record = $result->next()) {
// because vcard_map is per-source we need to create vcard here
prepare_for_export($record, $source);
$record['sourceid'] = $s;
$key = rcube_addressbook::compose_contact_key($record, $sort_col);
$records[$key] = $record;
}
}
ksort($records, SORT_LOCALE_STRING);
// create resultset object
$count = count($records);
$result = new rcube_result_set($count);
$result->records = array_values($records);
}
// selected directory/group
else {
$CONTACTS = rcmail_contact_source(null, true);
// get contacts for this user
$CONTACTS->set_page(1);
$CONTACTS->set_pagesize(99999);
$result = $CONTACTS->list_records(null, 0, true);
}
// Give plugins a possibility to implement other output formats or modify the result
$plugin = $RCMAIL->plugins->exec_hook('addressbook_export', array('result' => $result));
$result = $plugin['result'];
if ($plugin['abort']) {
exit;
}
// send downlaod headers
header('Content-Type: text/vcard; charset=' . RCUBE_CHARSET);
header('Content-Disposition: attachment; filename="contacts.vcf"');
while ($result && ($row = $result->next())) {
if ($CONTACTS) {
prepare_for_export($row, $CONTACTS);
}
// fix folding and end-of-line chars
$row['vcard'] = preg_replace('/\r|\n\s+/', '', $row['vcard']);
$row['vcard'] = preg_replace('/\n/', rcube_vcard::$eol, $row['vcard']);
echo rcube_vcard::rfc2425_fold($row['vcard']) . rcube_vcard::$eol;
}
exit;
/**
* Copy contact record properties into a vcard object
*/
function prepare_for_export(&$record, $source = null)
{
$groups = $source && $source->groups && $source->export_groups ? $source->get_record_groups($record['ID']) : null;
$fieldmap = $source ? $source->vcard_map : null;
if (empty($record['vcard'])) {
$vcard = new rcube_vcard($record['vcard'], RCUBE_CHARSET, false, $fieldmap);
$vcard->reset();
foreach ($record as $key => $values) {
list($field, $section) = explode(':', $key);
// avoid unwanted casting of DateTime objects to an array
// (same as in rcube_contacts::convert_save_data())
if (is_object($values) && is_a($values, 'DateTime')) {
$values = array($values);
}
foreach ((array) $values as $value) {
if (is_array($value) || is_a($value, 'DateTime') || @strlen($value)) {
$vcard->set($field, $value, strtoupper($section));
}
}
}
// append group names
if ($groups) {
$vcard->set('groups', join(',', $groups), null);
}
$record['vcard'] = $vcard->export();
}
// patch categories to alread existing vcard block
else if ($record['vcard']) {
$vcard = new rcube_vcard($record['vcard'], RCUBE_CHARSET, false, $fieldmap);
// unset CATEGORIES entry, it might be not up-to-date (#1490277)
$vcard->set('groups', null);
$record['vcard'] = $vcard->export();
if (!empty($groups)) {
$vgroups = 'CATEGORIES:' . rcube_vcard::vcard_quote($groups, ',');
$record['vcard'] = str_replace('END:VCARD', $vgroups . rcube_vcard::$eol . 'END:VCARD', $record['vcard']);
}
}
}
diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc
index bdc513937..3e4ce4d9f 100644
--- a/program/steps/addressbook/func.inc
+++ b/program/steps/addressbook/func.inc
@@ -1,1073 +1,1071 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/addressbook/func.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide addressbook functionality and GUI objects |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
$SEARCH_MODS_DEFAULT = array('name'=>1, 'firstname'=>1, 'surname'=>1, 'email'=>1, '*'=>1);
// general definition of contact coltypes
$CONTACT_COLTYPES = array(
'name' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $RCMAIL->gettext('name'), 'category' => 'main'),
'firstname' => array('type' => 'text', 'size' => 19, 'maxlength' => 50, 'limit' => 1, 'label' => $RCMAIL->gettext('firstname'), 'category' => 'main'),
'surname' => array('type' => 'text', 'size' => 19, 'maxlength' => 50, 'limit' => 1, 'label' => $RCMAIL->gettext('surname'), 'category' => 'main'),
'email' => array('type' => 'text', 'size' => 40, 'maxlength' => 254, 'label' => $RCMAIL->gettext('email'), 'subtypes' => array('home','work','other'), 'category' => 'main'),
'middlename' => array('type' => 'text', 'size' => 19, 'maxlength' => 50, 'limit' => 1, 'label' => $RCMAIL->gettext('middlename'), 'category' => 'main'),
'prefix' => array('type' => 'text', 'size' => 8, 'maxlength' => 20, 'limit' => 1, 'label' => $RCMAIL->gettext('nameprefix'), 'category' => 'main'),
'suffix' => array('type' => 'text', 'size' => 8, 'maxlength' => 20, 'limit' => 1, 'label' => $RCMAIL->gettext('namesuffix'), 'category' => 'main'),
'nickname' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $RCMAIL->gettext('nickname'), 'category' => 'main'),
'jobtitle' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $RCMAIL->gettext('jobtitle'), 'category' => 'main'),
'organization' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $RCMAIL->gettext('organization'), 'category' => 'main'),
'department' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $RCMAIL->gettext('department'), 'category' => 'main'),
'gender' => array('type' => 'select', 'limit' => 1, 'label' => $RCMAIL->gettext('gender'), 'options' => array('male' => $RCMAIL->gettext('male'), 'female' => $RCMAIL->gettext('female')), 'category' => 'personal'),
'maidenname' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $RCMAIL->gettext('maidenname'), 'category' => 'personal'),
'phone' => array('type' => 'text', 'size' => 40, 'maxlength' => 20, 'label' => $RCMAIL->gettext('phone'), 'subtypes' => array('home','home2','work','work2','mobile','main','homefax','workfax','car','pager','video','assistant','other'), 'category' => 'main'),
'address' => array('type' => 'composite', 'label' => $RCMAIL->gettext('address'), 'subtypes' => array('home','work','other'), 'childs' => array(
'street' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'label' => $RCMAIL->gettext('street'), 'category' => 'main'),
'locality' => array('type' => 'text', 'size' => 28, 'maxlength' => 50, 'label' => $RCMAIL->gettext('locality'), 'category' => 'main'),
'zipcode' => array('type' => 'text', 'size' => 8, 'maxlength' => 15, 'label' => $RCMAIL->gettext('zipcode'), 'category' => 'main'),
'region' => array('type' => 'text', 'size' => 12, 'maxlength' => 50, 'label' => $RCMAIL->gettext('region'), 'category' => 'main'),
'country' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'label' => $RCMAIL->gettext('country'), 'category' => 'main'),
), 'category' => 'main'),
'birthday' => array('type' => 'date', 'size' => 12, 'maxlength' => 16, 'label' => $RCMAIL->gettext('birthday'), 'limit' => 1, 'render_func' => 'rcmail_format_date_col', 'category' => 'personal'),
'anniversary' => array('type' => 'date', 'size' => 12, 'maxlength' => 16, 'label' => $RCMAIL->gettext('anniversary'), 'limit' => 1, 'render_func' => 'rcmail_format_date_col', 'category' => 'personal'),
'website' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'label' => $RCMAIL->gettext('website'), 'subtypes' => array('homepage','work','blog','profile','other'), 'category' => 'main'),
'im' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'label' => $RCMAIL->gettext('instantmessenger'), 'subtypes' => array('aim','icq','msn','yahoo','jabber','skype','other'), 'category' => 'main'),
'notes' => array('type' => 'textarea', 'size' => 40, 'rows' => 15, 'maxlength' => 500, 'label' => $RCMAIL->gettext('notes'), 'limit' => 1),
'photo' => array('type' => 'image', 'limit' => 1, 'category' => 'main'),
'assistant' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $RCMAIL->gettext('assistant'), 'category' => 'personal'),
'manager' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $RCMAIL->gettext('manager'), 'category' => 'personal'),
'spouse' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $RCMAIL->gettext('spouse'), 'category' => 'personal'),
// TODO: define fields for vcards like GEO, KEY
);
$PAGE_SIZE = $RCMAIL->config->get('addressbook_pagesize', $RCMAIL->config->get('pagesize', 50));
// Addressbook UI
if (!$RCMAIL->action && !$OUTPUT->ajax_call) {
// add list of address sources to client env
$js_list = $RCMAIL->get_address_sources();
// count all/writeable sources
$writeable = 0;
$count = 0;
foreach ($js_list as $sid => $s) {
$count++;
if (!$s['readonly']) {
$writeable++;
}
// unset hidden sources
if ($s['hidden']) {
unset($js_list[$sid]);
}
}
$search_mods = $RCMAIL->config->get('addressbook_search_mods', $SEARCH_MODS_DEFAULT);
$OUTPUT->set_env('search_mods', $search_mods);
$OUTPUT->set_env('address_sources', $js_list);
$OUTPUT->set_env('writable_source', $writeable);
$OUTPUT->set_env('contact_move_enabled', $writeable > 1);
$OUTPUT->set_env('contact_copy_enabled', $writeable > 1 || ($writeable == 1 && count($js_list) > 1));
$OUTPUT->set_pagetitle($RCMAIL->gettext('contacts'));
$_SESSION['addressbooks_count'] = $count;
$_SESSION['addressbooks_count_writeable'] = $writeable;
// select address book
$source = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
// use first directory by default
if (!strlen($source) || !isset($js_list[$source])) {
$source = $RCMAIL->config->get('default_addressbook');
if (!strlen($source) || !isset($js_list[$source])) {
$source = strval(key($js_list));
}
}
$CONTACTS = rcmail_contact_source($source, true);
}
// remove undo information...
if ($undo = $_SESSION['contact_undo']) {
// ...after timeout
$undo_time = $RCMAIL->config->get('undo_timeout', 0);
if ($undo['ts'] < time() - $undo_time)
$RCMAIL->session->remove('contact_undo');
}
// register UI objects
$OUTPUT->add_handlers(array(
'directorylist' => 'rcmail_directory_list',
'savedsearchlist' => 'rcmail_savedsearch_list',
'addresslist' => 'rcmail_contacts_list',
'addresslisttitle' => 'rcmail_contacts_list_title',
'recordscountdisplay' => 'rcmail_rowcount_display',
'searchform' => array($OUTPUT, 'search_form')
));
// register action aliases
$RCMAIL->register_action_map(array(
'add' => 'edit.inc',
'group-create' => 'groups.inc',
'group-rename' => 'groups.inc',
'group-delete' => 'groups.inc',
'group-addmembers' => 'groups.inc',
'group-delmembers' => 'groups.inc',
'search-create' => 'search.inc',
'search-delete' => 'search.inc',
));
// Disable qr-code if php-gd or Endroid's QrCode is not installed
if (!$OUTPUT->ajax_call) {
$OUTPUT->set_env('qrcode', function_exists('imagecreate') && class_exists('Endroid\QrCode\QrCode'));
$OUTPUT->add_label('qrcode');
}
// instantiate a contacts object according to the given source
function rcmail_contact_source($source=null, $init_env=false, $writable=false)
{
global $RCMAIL, $OUTPUT, $CONTACT_COLTYPES, $PAGE_SIZE;
if (!strlen($source)) {
$source = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
}
// Get object
$CONTACTS = $RCMAIL->get_address_book($source, $writable);
$CONTACTS->set_pagesize($PAGE_SIZE);
// set list properties and session vars
if (!empty($_GET['_page']))
$CONTACTS->set_page(($_SESSION['page'] = intval($_GET['_page'])));
else
$CONTACTS->set_page(isset($_SESSION['page']) ? $_SESSION['page'] : 1);
if ($group = rcube_utils::get_input_value('_gid', rcube_utils::INPUT_GP)) {
$CONTACTS->set_group($group);
}
if (!$init_env) {
return $CONTACTS;
}
$OUTPUT->set_env('readonly', $CONTACTS->readonly);
$OUTPUT->set_env('source', (string) $source);
$OUTPUT->set_env('group', $group);
// reduce/extend $CONTACT_COLTYPES with specification from the current $CONTACT object
if (is_array($CONTACTS->coltypes)) {
// remove cols not listed by the backend class
$contact_cols = $CONTACTS->coltypes[0] ? array_flip($CONTACTS->coltypes) : $CONTACTS->coltypes;
$CONTACT_COLTYPES = array_intersect_key($CONTACT_COLTYPES, $contact_cols);
// add associative coltypes definition
if (!$CONTACTS->coltypes[0]) {
foreach ($CONTACTS->coltypes as $col => $colprop) {
if (is_array($colprop['childs'])) {
foreach ($colprop['childs'] as $childcol => $childprop)
$colprop['childs'][$childcol] = array_merge((array)$CONTACT_COLTYPES[$col]['childs'][$childcol], $childprop);
}
$CONTACT_COLTYPES[$col] = $CONTACT_COLTYPES[$col] ? array_merge($CONTACT_COLTYPES[$col], $colprop) : $colprop;
}
}
}
$OUTPUT->set_env('photocol', is_array($CONTACT_COLTYPES['photo']));
return $CONTACTS;
}
function rcmail_set_sourcename($abook)
{
global $OUTPUT, $RCMAIL;
// get address book name (for display)
if ($abook && $_SESSION['addressbooks_count'] > 1) {
$name = $abook->get_name();
if (!$name) {
$name = $RCMAIL->gettext('personaladrbook');
}
$OUTPUT->set_env('sourcename', html_entity_decode($name, ENT_COMPAT, 'UTF-8'));
}
}
function rcmail_directory_list($attrib)
{
global $RCMAIL, $OUTPUT;
if (!$attrib['id'])
$attrib['id'] = 'rcmdirectorylist';
$out = '';
$jsdata = array();
$line_templ = html::tag('li', array(
'id' => 'rcmli%s', 'class' => '%s', 'noclose' => true),
html::a(array('href' => '%s',
'rel' => '%s',
'onclick' => "return ".rcmail_output::JS_OBJECT_NAME.".command('list','%s',this)"), '%s'));
$sources = (array) $OUTPUT->get_env('address_sources');
reset($sources);
// currently selected source
$current = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
foreach ($sources as $j => $source) {
$id = strval(strlen($source['id']) ? $source['id'] : $j);
$js_id = rcube::JQ($id);
// set class name(s)
$class_name = 'addressbook';
if ($current === $id)
$class_name .= ' selected';
if ($source['readonly'])
$class_name .= ' readonly';
if ($source['class_name'])
$class_name .= ' ' . $source['class_name'];
$name = $source['name'] ?: $id;
$out .= sprintf($line_templ,
rcube_utils::html_identifier($id, true),
$class_name,
rcube::Q($RCMAIL->url(array('_source' => $id))),
$source['id'],
$js_id, $name);
$groupdata = array('out' => $out, 'jsdata' => $jsdata, 'source' => $id);
if ($source['groups'])
$groupdata = rcmail_contact_groups($groupdata);
$jsdata = $groupdata['jsdata'];
$out = $groupdata['out'];
$out .= '</li>';
}
$OUTPUT->set_env('contactgroups', $jsdata);
$OUTPUT->set_env('collapsed_abooks', (string)$RCMAIL->config->get('collapsed_abooks',''));
$OUTPUT->add_gui_object('folderlist', $attrib['id']);
$OUTPUT->include_script('treelist.js');
// add some labels to client
$OUTPUT->add_label('deletegroupconfirm', 'groupdeleting', 'addingmember', 'removingmember',
'newgroup', 'grouprename', 'searchsave', 'namex', 'save', 'import', 'importcontacts',
'advsearch', 'search'
);
return html::tag('ul', $attrib, $out, html::$common_attrib);
}
function rcmail_savedsearch_list($attrib)
{
global $RCMAIL, $OUTPUT;
if (!$attrib['id'])
$attrib['id'] = 'rcmsavedsearchlist';
$out = '';
$line_templ = html::tag('li', array(
'id' => 'rcmli%s', 'class' => '%s'),
html::a(array('href' => '#', 'rel' => 'S%s',
'onclick' => "return ".rcmail_output::JS_OBJECT_NAME.".command('listsearch', '%s', this)"), '%s'));
// Saved searches
$sources = $RCMAIL->user->list_searches(rcube_user::SEARCH_ADDRESSBOOK);
foreach ($sources as $source) {
$id = $source['id'];
$js_id = rcube::JQ($id);
// set class name(s)
$classes = array('contactsearch');
if (!empty($source['class_name']))
$classes[] = $source['class_name'];
$out .= sprintf($line_templ,
rcube_utils::html_identifier('S'.$id, true),
join(' ', $classes),
$id,
$js_id, rcube::Q($source['name'] ?: $id)
);
}
$OUTPUT->add_gui_object('savedsearchlist', $attrib['id']);
return html::tag('ul', $attrib, $out, html::$common_attrib);
}
function rcmail_contact_groups($args)
{
global $RCMAIL;
$groups_html = '';
$groups = $RCMAIL->get_address_book($args['source'])->list_groups();
if (!empty($groups)) {
$line_templ = html::tag('li', array(
'id' => 'rcmli%s', 'class' => 'contactgroup'),
html::a(array('href' => '#',
'rel' => '%s:%s',
'onclick' => "return ".rcmail_output::JS_OBJECT_NAME.".command('listgroup',{'source':'%s','id':'%s'},this)"), '%s'));
// append collapse/expand toggle and open a new <ul>
$is_collapsed = strpos($RCMAIL->config->get('collapsed_abooks',''), '&'.rawurlencode($args['source']).'&') !== false;
$args['out'] .= html::div('treetoggle ' . ($is_collapsed ? 'collapsed' : 'expanded'), '&nbsp;');
foreach ($groups as $group) {
$groups_html .= sprintf($line_templ,
rcube_utils::html_identifier('G' . $args['source'] . $group['ID'], true),
$args['source'], $group['ID'],
$args['source'], $group['ID'], rcube::Q($group['name'])
);
$args['jsdata']['G'.$args['source'].$group['ID']] = array(
'source' => $args['source'], 'id' => $group['ID'],
'name' => $group['name'], 'type' => 'group');
}
}
$args['out'] .= html::tag('ul',
array('class' => 'groups', 'style' => ($is_collapsed || empty($groups) ? "display:none;" : null)),
$groups_html);
return $args;
}
// return the contacts list as HTML table
function rcmail_contacts_list($attrib)
{
global $RCMAIL, $CONTACTS, $OUTPUT;
// define list of cols to be displayed
$a_show_cols = array('name','action');
// add id to message list table if not specified
if (!strlen($attrib['id']))
$attrib['id'] = 'rcmAddressList';
// create XHTML table
$out = $RCMAIL->table_output($attrib, array(), $a_show_cols, $CONTACTS->primary_key);
// set client env
$OUTPUT->add_gui_object('contactslist', $attrib['id']);
$OUTPUT->set_env('current_page', (int)$CONTACTS->list_page);
$OUTPUT->include_script('list.js');
// add some labels to client
$OUTPUT->add_label('deletecontactconfirm', 'copyingcontact', 'movingcontact', 'contactdeleting');
return $out;
}
function rcmail_js_contacts_list($result, $prefix='')
{
global $OUTPUT, $RCMAIL;
if (empty($result) || $result->count == 0) {
return;
}
// define list of cols to be displayed
$a_show_cols = array('name','action');
while ($row = $result->next()) {
$emails = rcube_addressbook::get_col_values('email', $row, true);
$row['CID'] = $row['ID'];
$row['email'] = reset($emails);
$source_id = $OUTPUT->get_env('source');
$a_row_cols = array();
$classes = array($row['_type'] ?: 'person');
// build contact ID with source ID
if (isset($row['sourceid'])) {
$row['ID'] = $row['ID'].'-'.$row['sourceid'];
$source_id = $row['sourceid'];
}
// format each col
foreach ($a_show_cols as $col) {
$val = '';
switch ($col) {
case 'name':
$val = rcube::Q(rcube_addressbook::compose_list_name($row));
break;
case 'action':
if ($row['_type'] == 'group') {
$val = html::a(array(
'href' => '#list',
'rel' => $row['ID'],
'title' => $RCMAIL->gettext('listgroup'),
'onclick' => sprintf("return %s.command('pushgroup',{'source':'%s','id':'%s'},this,event)", rcmail_output::JS_OBJECT_NAME, $source_id, $row['CID']),
'class' => 'pushgroup',
'data-action-link' => true,
), '&raquo;');
}
else
$val = '';
break;
default:
$val = rcube::Q($row[$col]);
break;
}
$a_row_cols[$col] = $val;
}
if ($row['readonly'])
$classes[] = 'readonly';
$OUTPUT->command($prefix.'add_contact_row', $row['ID'], $a_row_cols, join(' ', $classes), array_intersect_key($row, array('ID'=>1,'readonly'=>1,'_type'=>1,'email'=>1,'name'=>1)));
}
}
function rcmail_contacts_list_title($attrib)
{
global $OUTPUT, $RCMAIL;
$attrib += array('label' => 'contacts', 'id' => 'rcmabooklisttitle', 'tag' => 'span');
unset($attrib['name']);
$OUTPUT->add_gui_object('addresslist_title', $attrib['id']);
$OUTPUT->add_label('contacts','uponelevel');
return html::tag($attrib['tag'], $attrib, $RCMAIL->gettext($attrib['label']), html::$common_attrib);
}
function rcmail_rowcount_display($attrib)
{
global $RCMAIL;
if (!$attrib['id'])
$attrib['id'] = 'rcmcountdisplay';
$RCMAIL->output->add_gui_object('countdisplay', $attrib['id']);
if ($attrib['label'])
$_SESSION['contactcountdisplay'] = $attrib['label'];
return html::span($attrib, $RCMAIL->gettext('loading'));
}
function rcmail_get_rowcount_text($result=null)
{
global $RCMAIL, $CONTACTS, $PAGE_SIZE;
// read nr of contacts
if (!$result) {
$result = $CONTACTS->get_result();
}
if ($result->count == 0)
$out = $RCMAIL->gettext('nocontactsfound');
else
$out = $RCMAIL->gettext(array(
'name' => $_SESSION['contactcountdisplay'] ?: 'contactsfromto',
'vars' => array(
'from' => $result->first + 1,
'to' => min($result->count, $result->first + $PAGE_SIZE),
'count' => $result->count)
));
return $out;
}
function rcmail_get_type_label($type)
{
global $RCMAIL;
$label = 'type'.$type;
if ($RCMAIL->text_exists($label, '*', $domain))
return $RCMAIL->gettext($label, $domain);
else if (preg_match('/\w+(\d+)$/', $label, $m)
&& ($label = preg_replace('/(\d+)$/', '', $label))
&& $RCMAIL->text_exists($label, '*', $domain))
return $RCMAIL->gettext($label, $domain) . ' ' . $m[1];
return ucfirst($type);
}
function rcmail_contact_form($form, $record, $attrib = null)
{
global $RCMAIL;
// group fields
$head_fields = array(
'source' => array('source'),
'names' => array('prefix','firstname','middlename','surname','suffix'),
'displayname' => array('name'),
'nickname' => array('nickname'),
'organization' => array('organization'),
'department' => array('department'),
'jobtitle' => array('jobtitle'),
);
// Allow plugins to modify contact form content
$plugin = $RCMAIL->plugins->exec_hook('contact_form', array(
'form' => $form, 'record' => $record, 'head_fields' => $head_fields));
$form = $plugin['form'];
$record = $plugin['record'];
$head_fields = $plugin['head_fields'];
$edit_mode = $RCMAIL->action != 'show' && $RCMAIL->action != 'print';
$compact = rcube_utils::get_boolean($attrib['compact-form']);
$use_labels = rcube_utils::get_boolean($attrib['use-labels']);
$with_source = rcube_utils::get_boolean($attrib['with-source']);
$out = '';
if ($attrib['deleteicon']) {
$del_button = html::img(array('src' => $RCMAIL->output->get_skin_file($attrib['deleteicon']), 'alt' => $RCMAIL->gettext('delete')));
}
else {
$del_button = html::span('inner', $RCMAIL->gettext('delete'));
}
unset($attrib['deleteicon']);
// get default coltypes
$coltypes = $GLOBALS['CONTACT_COLTYPES'];
$coltype_labels = array();
foreach ($coltypes as $col => $prop) {
if ($prop['subtypes']) {
$subtype_names = array_map('rcmail_get_type_label', $prop['subtypes']);
$select_subtype = new html_select(array('name' => '_subtype_'.$col.'[]', 'class' => 'contactselectsubtype custom-select', 'title' => $prop['label'] . ' ' . $RCMAIL->gettext('type')));
$select_subtype->add($subtype_names, $prop['subtypes']);
$coltypes[$col]['subtypes_select'] = $select_subtype->show();
}
if ($prop['childs']) {
foreach ($prop['childs'] as $childcol => $cp)
$coltype_labels[$childcol] = array('label' => $cp['label']);
}
}
foreach ($form as $section => $fieldset) {
// skip empty sections
if (empty($fieldset['content'])) {
continue;
}
$select_add = new html_select(array('class' => 'addfieldmenu', 'rel' => $section, 'data-compact' => $compact ? "true" : null));
$select_add->add($RCMAIL->gettext('addfield'), '');
// render head section with name fields (not a regular list of rows)
if ($section == 'head') {
$content = '';
// unset display name if it is composed from name parts
if ($record['name'] == rcube_addressbook::compose_display_name(array('name' => '') + (array)$record)) {
unset($record['name']);
}
foreach ($head_fields as $blockname => $colnames) {
$fields = '';
$block_attr = array('class' => $blockname . (count($colnames) == 1 ? ' row' : ''));
foreach ($colnames as $col) {
if ($col == 'source') {
if (!$with_source || !($source = $RCMAIL->output->get_env('sourcename'))) {
continue;
}
if (!$edit_mode) {
$record['source'] = $RCMAIL->gettext('addressbook') . ': ' . $source;
}
else if ($RCMAIL->action == 'add') {
$record['source'] = $source;
}
else {
continue;
}
}
// skip cols unknown to the backend
else if (!$coltypes[$col]) {
continue;
}
// skip cols not listed in the form definition
if (is_array($fieldset['content']) && !in_array($col, array_keys($fieldset['content']))) {
continue;
}
// only string values are expected here
if (is_array($record[$col])) {
$record[$col] = join(' ', $record[$col]);
}
if (!$edit_mode) {
if (!empty($record[$col])) {
$fields .= html::span('namefield ' . $col, rcube::Q($record[$col])) . ' ';
}
}
else {
$colprop = (array)$fieldset['content'][$col] + (array)$coltypes[$col];
$visible = true;
if (empty($colprop['id'])) {
$colprop['id'] = 'ff_' . $col;
}
if (empty($record[$col]) && !$colprop['visible']) {
$visible = false;
$colprop['style'] = $use_labels ? null : 'display:none';
$select_add->add($colprop['label'], $col);
}
if ($col == 'source') {
$input = rcmail_source_selector(array('id' => $colprop['id']));
}
else {
$input = rcube_output::get_edit_field($col, $record[$col], $colprop, $colprop['type']);
}
if ($use_labels) {
$_content = html::label($colprop['id'], rcube::Q($colprop['label'])) . html::div(null, $input);
if (count($colnames) > 1) {
$fields .= html::div(array('class' => 'row', 'style' => $visible ? null : 'display:none'), $_content);
}
else {
$fields .= $_content;
$block_attr['style'] = $visible ? null : 'display:none';
}
}
else {
$fields .= $input;
}
}
}
if ($fields) {
$content .= html::div($block_attr, $fields);
}
}
if ($edit_mode) {
$content .= html::p('addfield', $select_add->show(null));
}
$legend = !empty($fieldset['name']) ? html::tag('legend', null, rcube::Q($fieldset['name'])) : '';
$out .= html::tag('fieldset', $attrib, $legend . $content, html::$common_attrib) ."\n";
continue;
}
$content = '';
if (is_array($fieldset['content'])) {
foreach ($fieldset['content'] as $col => $colprop) {
// remove subtype part of col name
list($field, $subtype) = explode(':', $col);
if (!$subtype) $subtype = 'home';
$fullkey = $col.':'.$subtype;
// skip cols unknown to the backend
if (!$coltypes[$field] && empty($colprop['value'])) {
continue;
}
// merge colprop with global coltype configuration
if ($coltypes[$field]) {
$colprop += $coltypes[$field];
}
$label = isset($colprop['label']) ? $colprop['label'] : $RCMAIL->gettext($col);
// prepare subtype selector in edit mode
if ($edit_mode && is_array($colprop['subtypes'])) {
$subtype_names = array_map('rcmail_get_type_label', $colprop['subtypes']);
$select_subtype = new html_select(array('name' => '_subtype_'.$col.'[]', 'class' => 'contactselectsubtype', 'title' => $colprop['label'] . ' ' . $RCMAIL->gettext('type')));
$select_subtype->add($subtype_names, $colprop['subtypes']);
}
else {
$select_subtype = null;
}
if (!empty($colprop['value'])) {
$values = (array)$colprop['value'];
}
else {
// iterate over possible subtypes and collect values with their subtype
if (is_array($colprop['subtypes'])) {
$values = $subtypes = array();
foreach (rcube_addressbook::get_col_values($field, $record) as $st => $vals) {
foreach ((array)$vals as $value) {
$i = count($values);
$subtypes[$i] = $st;
$values[$i] = $value;
}
// TODO: add $st to $select_subtype if missing ?
}
}
else {
$values = $record[$fullkey] ?: $record[$field];
$subtypes = null;
}
}
// hack: create empty values array to force this field to be displayed
if (empty($values) && $colprop['visible']) {
$values = array('');
}
if (!is_array($values)) {
// $values can be an object, don't use (array)$values syntax
$values = !empty($values) ? array($values) : array();
}
$rows = '';
foreach ($values as $i => $val) {
if ($subtypes[$i]) {
$subtype = $subtypes[$i];
}
$colprop['id'] = 'ff_' . $col . intval($coltypes[$field]['count']);
$row_class = 'row';
// render composite field
if ($colprop['type'] == 'composite') {
$row_class .= ' composite';
$composite = array();
$template = $RCMAIL->config->get($col . '_template', '{'.join('} {', array_keys($colprop['childs'])).'}');
$j = 0;
foreach ($colprop['childs'] as $childcol => $cp) {
if (!empty($val) && is_array($val)) {
$childvalue = $val[$childcol] ?: $val[$j];
}
else {
$childvalue = '';
}
if ($edit_mode) {
if ($colprop['subtypes'] || $colprop['limit'] != 1) $cp['array'] = true;
$composite['{'.$childcol.'}'] = rcube_output::get_edit_field($childcol, $childvalue, $cp, $cp['type']) . ' ';
}
else {
$childval = $cp['render_func'] ? call_user_func($cp['render_func'], $childvalue, $childcol) : rcube::Q($childvalue);
$composite['{'.$childcol.'}'] = html::span('data ' . $childcol, $childval) . ' ';
}
$j++;
}
$coltypes[$field] += (array)$colprop;
$coltypes[$field]['count']++;
$val = preg_replace('/\{\w+\}/', '', strtr($template, $composite));
if ($compact) {
$val = html::div('content', str_replace('<br/>', '', $val));
}
}
else if ($edit_mode) {
// call callback to render/format value
if ($colprop['render_func']) {
$val = call_user_func($colprop['render_func'], $val, $col);
}
$coltypes[$field] = (array)$colprop + $coltypes[$field];
if ($colprop['subtypes'] || $colprop['limit'] != 1) {
$colprop['array'] = true;
}
// load jquery UI datepicker for date fields
if ($colprop['type'] == 'date') {
$colprop['class'] .= ($colprop['class'] ? ' ' : '') . 'datepicker';
if (!$colprop['render_func']) {
$val = rcmail_format_date_col($val);
}
}
$val = rcube_output::get_edit_field($col, $val, $colprop, $colprop['type']);
$coltypes[$field]['count']++;
}
else if ($colprop['render_func']) {
$val = call_user_func($colprop['render_func'], $val, $col);
}
else if (is_array($colprop['options']) && isset($colprop['options'][$val])) {
$val = $colprop['options'][$val];
}
else {
$val = rcube::Q($val);
}
// use subtype as label
if ($colprop['subtypes']) {
$label = rcmail_get_type_label($subtype);
}
$_del_btn = html::a(array('href' => '#del', 'class' => 'contactfieldbutton deletebutton', 'title' => $RCMAIL->gettext('delete'), 'rel' => $col), $del_button);
// add delete button/link
if (!$compact && $edit_mode && !($colprop['visible'] && $colprop['limit'] == 1)) {
$val .= $_del_btn;
}
// display row with label
if ($label) {
if ($RCMAIL->action == 'print') {
$_label = rcube::Q($colprop['label'] . ($label != $colprop['label'] ? ' (' . $label . ')' : ''));
if (!$compact) {
$_label = html::div('contactfieldlabel label', $_label);
}
}
else if ($select_subtype) {
$_label = $select_subtype->show($subtype);
if (!$compact) {
$_label = html::div('contactfieldlabel label', $_label);
}
}
else {
$_label = html::label(array('class' => 'contactfieldlabel label', 'for' => $colprop['id']), rcube::Q($label));
}
if (!$compact) {
$val = html::div('contactfieldcontent ' . $colprop['type'], $val);
}
else {
$val .= $_del_btn;
}
$rows .= html::div($row_class, $_label . $val);
}
// row without label
else {
$rows .= html::div($row_class, $compact ? $val : html::div('contactfield', $val));
}
}
// add option to the add-field menu
if (!$colprop['limit'] || $coltypes[$field]['count'] < $colprop['limit']) {
$select_add->add($colprop['label'], $col);
$select_add->_count++;
}
// wrap rows in fieldgroup container
if ($rows) {
$c_class = 'contactfieldgroup ' . ($colprop['subtypes'] ? 'contactfieldgroupmulti ' : '') . 'contactcontroller' . $col;
$with_label = $colprop['subtypes'] && $RCMAIL->action != 'print';
$content .= html::tag(
'fieldset',
array('class' => $c_class, 'style' => ($rows ? null : 'display:none')),
($with_label ? html::tag('legend', null, rcube::Q($colprop['label'])) : ' ') . $rows
);
}
}
if (!$content && (!$edit_mode || !$select_add->_count)) {
continue;
}
// also render add-field selector
if ($edit_mode) {
$content .= html::p('addfield', $select_add->show(null, array('style' => $select_add->_count ? null : 'display:none')));
}
$content = html::div(array('id' => 'contactsection' . $section), $content);
}
else {
$content = $fieldset['content'];
}
if ($content) {
$out .= html::tag('fieldset', array('class' => $attrib['fieldset-class']),
html::tag('legend', null, rcube::Q($fieldset['name'])) . $content) . "\n";
}
}
if ($edit_mode) {
$RCMAIL->output->set_env('coltypes', $coltypes + $coltype_labels);
$RCMAIL->output->set_env('delbutton', $del_button);
$RCMAIL->output->add_label('delete');
}
return $out;
}
function rcmail_contact_photo($attrib)
{
global $SOURCE_ID, $CONTACTS, $CONTACT_COLTYPES, $RCMAIL;
if ($result = $CONTACTS->get_result())
$record = $result->first();
$photo_img = $attrib['placeholder'] ? $RCMAIL->output->abs_url($attrib['placeholder'], true) : 'program/resources/blank.gif';
if ($record['_type'] == 'group' && $attrib['placeholdergroup'])
$photo_img = $RCMAIL->output->abs_url($attrib['placeholdergroup'], true);
$RCMAIL->output->set_env('photo_placeholder', $RCMAIL->output->asset_url($photo_img));
unset($attrib['placeholder']);
$plugin = $RCMAIL->plugins->exec_hook('contact_photo', array('record' => $record, 'data' => $record['photo']));
// check if we have photo data from contact form
if ($GLOBALS['EDIT_RECORD']) {
$rec = $GLOBALS['EDIT_RECORD'];
if ($rec['photo'] == '-del-') {
$record['photo'] = '';
}
else if ($_SESSION['contacts']['files'][$rec['photo']]) {
$record['photo'] = $file_id = $rec['photo'];
}
}
if ($plugin['url'])
$photo_img = $plugin['url'];
else if (preg_match('!^https?://!i', $record['photo']))
$photo_img = $record['photo'];
else if ($record['photo']) {
$url = array('_action' => 'photo', '_cid' => $record['ID'], '_source' => $SOURCE_ID);
if ($file_id) {
$url['_photo'] = $ff_value = $file_id;
}
$photo_img = $RCMAIL->url($url);
}
else {
$ff_value = '-del-'; // will disable delete-photo action
}
$content = html::div($attrib, html::img(array(
'src' => $photo_img,
'alt' => $RCMAIL->gettext('contactphoto'),
'onerror' => 'this.src = rcmail.env.photo_placeholder; this.onerror = null',
)));
if ($CONTACT_COLTYPES['photo'] && ($RCMAIL->action == 'edit' || $RCMAIL->action == 'add')) {
$RCMAIL->output->add_gui_object('contactphoto', $attrib['id']);
$hidden = new html_hiddenfield(array('name' => '_photo', 'id' => 'ff_photo', 'value' => $ff_value));
$content .= $hidden->show();
}
return $content;
}
function rcmail_format_date_col($val)
{
global $RCMAIL;
return $RCMAIL->format_date($val, $RCMAIL->config->get('date_format', 'Y-m-d'), false);
}
/**
* Updates saved search after data changed
*/
function rcmail_search_update($return = false)
{
global $RCMAIL;
if (($search_request = $_REQUEST['_search']) && isset($_SESSION['search'][$search_request])) {
$search = (array)$_SESSION['search'][$search_request];
$sort_col = $RCMAIL->config->get('addressbook_sort_col', 'name');
$afields = $return ? $RCMAIL->config->get('contactlist_fields') : array('name', 'email');
$records = array();
foreach ($search as $s => $set) {
$source = $RCMAIL->get_address_book($s);
// reset page
$source->set_page(1);
$source->set_pagesize(9999);
$source->set_search_set($set);
// get records
$result = $source->list_records($afields);
if (!$result->count) {
unset($search[$s]);
continue;
}
if ($return) {
while ($row = $result->next()) {
$row['sourceid'] = $s;
$key = rcube_addressbook::compose_contact_key($row, $sort_col);
$records[$key] = $row;
}
unset($result);
}
$search[$s] = $source->get_search_set();
}
$_SESSION['search'][$search_request] = $search;
return $records;
}
return false;
}
/**
* Returns contact ID(s) and source(s) from GET/POST data
*
* @return array List of contact IDs per-source
*/
function rcmail_get_cids($filter = null, $request_type = rcube_utils::INPUT_GPC)
{
// contact ID (or comma-separated list of IDs) is provided in two
// forms. If _source is an empty string then the ID is a string
// containing contact ID and source name in form: <ID>-<SOURCE>
$cid = rcube_utils::get_input_value('_cid', $request_type);
$source = (string) rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
if (is_array($cid)) {
return $cid;
}
if (!preg_match('/^[a-zA-Z0-9\+\/=_-]+(,[a-zA-Z0-9\+\/=_-]+)*$/', $cid)) {
return array();
}
$cid = explode(',', $cid);
$got_source = strlen($source);
$result = array();
// create per-source contact IDs array
foreach ($cid as $id) {
// extract source ID from contact ID (it's there in search mode)
// see #1488959 and #1488862 for reference
if (!$got_source) {
if ($sep = strrpos($id, '-')) {
$contact_id = substr($id, 0, $sep);
$source_id = (string) substr($id, $sep+1);
if (strlen($source_id)) {
$result[$source_id][] = $contact_id;
}
}
}
else {
if (substr($id, -($got_source+1)) === "-$source") {
$id = substr($id, 0, -($got_source+1));
}
$result[$source][] = $id;
}
}
return $filter !== null ? $result[$filter] : $result;
}
diff --git a/program/steps/addressbook/groups.inc b/program/steps/addressbook/groups.inc
index f4ddc5e1f..33b8627b9 100644
--- a/program/steps/addressbook/groups.inc
+++ b/program/steps/addressbook/groups.inc
@@ -1,154 +1,152 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/addressbook/groups.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2010-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Create/delete/rename contact groups and assign/remove contacts |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
$source = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
$CONTACTS = rcmail_contact_source($source);
if ($CONTACTS->readonly || !$CONTACTS->groups) {
$OUTPUT->show_message('sourceisreadonly', 'warning');
$OUTPUT->send();
}
if ($RCMAIL->action == 'group-addmembers') {
if (($gid = rcube_utils::get_input_value('_gid', rcube_utils::INPUT_POST)) && ($ids = rcmail_get_cids($source))) {
$plugin = $RCMAIL->plugins->exec_hook('group_addmembers', array(
'group_id' => $gid,
'ids' => $ids,
'source' => $source,
));
$CONTACTS->set_group($gid);
$num2add = count($plugin['ids']);
if (!$plugin['abort']) {
if (($maxnum = $RCMAIL->config->get('max_group_members', 0)) && ($CONTACTS->count()->count + $num2add > $maxnum)) {
$OUTPUT->show_message('maxgroupmembersreached', 'warning', array('max' => $maxnum));
$OUTPUT->send();
}
$result = $CONTACTS->add_to_group($gid, $plugin['ids']);
}
else {
$result = $plugin['result'];
}
if ($result)
$OUTPUT->show_message('contactaddedtogroup', 'confirmation');
else if ($plugin['abort'] || $CONTACTS->get_error())
$OUTPUT->show_message($plugin['message'] ?: 'errorsaving', 'error');
else
$OUTPUT->show_message($plugin['message'] ?: 'nogroupassignmentschanged');
}
}
else if ($RCMAIL->action == 'group-delmembers') {
if (($gid = rcube_utils::get_input_value('_gid', rcube_utils::INPUT_POST)) && ($ids = rcmail_get_cids($source))) {
$plugin = $RCMAIL->plugins->exec_hook('group_delmembers', array(
'group_id' => $gid,
'ids' => $ids,
'source' => $source,
));
if (!$plugin['abort'])
$result = $CONTACTS->remove_from_group($gid, $plugin['ids']);
else
$result = $plugin['result'];
if ($result) {
$OUTPUT->show_message('contactremovedfromgroup', 'confirmation');
$OUTPUT->command('remove_group_contacts',array('source' => $source, 'gid' => $gid));
}
else {
$OUTPUT->show_message($plugin['message'] ?: 'errorsaving', 'error');
}
}
}
else if ($RCMAIL->action == 'group-create') {
if ($name = trim(rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST, true))) {
$plugin = $RCMAIL->plugins->exec_hook('group_create', array(
'name' => $name,
'source' => $source,
));
if (!$plugin['abort'])
$created = $CONTACTS->create_group($plugin['name']);
else
$created = $plugin['result'];
}
if ($created && $OUTPUT->ajax_call) {
$created['name'] = rcube::Q($created['name']);
$OUTPUT->show_message('groupcreated', 'confirmation');
$OUTPUT->command('insert_contact_group', array('source' => $source) + $created);
}
else if (!$created) {
$OUTPUT->show_message($plugin['message'] ?: 'errorsaving', 'error');
}
}
else if ($RCMAIL->action == 'group-rename') {
if (($gid = rcube_utils::get_input_value('_gid', rcube_utils::INPUT_POST))
&& ($name = trim(rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST, true)))
) {
$plugin = $RCMAIL->plugins->exec_hook('group_rename', array(
'group_id' => $gid,
'name' => $name,
'source' => $source,
));
if (!$plugin['abort'])
$newname = $CONTACTS->rename_group($gid, $plugin['name'], $newgid);
else
$newname = $plugin['result'];
}
if ($newname && $OUTPUT->ajax_call) {
$OUTPUT->show_message('grouprenamed', 'confirmation');
$OUTPUT->command('update_contact_group', array(
'source' => $source, 'id' => $gid, 'name' => rcube::Q($newname), 'newid' => $newgid));
}
else if (!$newname) {
$OUTPUT->show_message($plugin['message'] ?: 'errorsaving', 'error');
}
}
else if ($RCMAIL->action == 'group-delete') {
if ($gid = rcube_utils::get_input_value('_gid', rcube_utils::INPUT_POST)) {
$plugin = $RCMAIL->plugins->exec_hook('group_delete', array(
'group_id' => $gid,
'source' => $source,
));
if (!$plugin['abort'])
$deleted = $CONTACTS->delete_group($gid);
else
$deleted = $plugin['result'];
}
if ($deleted) {
$OUTPUT->show_message('groupdeleted', 'confirmation');
$OUTPUT->command('remove_group_item', array('source' => $source, 'id' => $gid));
}
else {
$OUTPUT->show_message($plugin['message'] ?: 'errorsaving', 'error');
}
}
// send response
$OUTPUT->send();
diff --git a/program/steps/addressbook/import.inc b/program/steps/addressbook/import.inc
index f5e6d62b6..347cf7f69 100644
--- a/program/steps/addressbook/import.inc
+++ b/program/steps/addressbook/import.inc
@@ -1,333 +1,331 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/addressbook/import.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Import contacts from a vCard or CSV file |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+-----------------------------------------------------------------------+
*/
/** The import process **/
$importstep = 'rcmail_import_form';
if (is_array($_FILES['_file'])) {
$replace = (bool)rcube_utils::get_input_value('_replace', rcube_utils::INPUT_GPC);
$target = rcube_utils::get_input_value('_target', rcube_utils::INPUT_GPC);
$with_groups = intval(rcube_utils::get_input_value('_groups', rcube_utils::INPUT_GPC));
$vcards = array();
$upload_error = null;
$CONTACTS = $RCMAIL->get_address_book($target, true);
if (!$CONTACTS->groups) {
$with_groups = false;
}
if ($CONTACTS->readonly) {
$OUTPUT->show_message('addresswriterror', 'error');
}
else {
foreach ((array)$_FILES['_file']['tmp_name'] as $i => $filepath) {
// Process uploaded file if there is no error
$err = $_FILES['_file']['error'][$i];
if ($err) {
$upload_error = $err;
}
else {
$file_content = file_get_contents($filepath);
// let rcube_vcard do the hard work :-)
$vcard_o = new rcube_vcard();
$vcard_o->extend_fieldmap($CONTACTS->vcard_map);
$v_list = $vcard_o->import($file_content);
if (!empty($v_list)) {
$vcards = array_merge($vcards, $v_list);
continue;
}
// no vCards found, try CSV
$csv = new rcube_csv2vcard($_SESSION['language']);
$csv->import($file_content);
$v_list = $csv->export();
if (!empty($v_list)) {
$vcards = array_merge($vcards, $v_list);
}
}
}
}
// no vcards detected
if (!count($vcards)) {
if ($upload_error == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
$size = $RCMAIL->show_bytes(rcube_utils::max_upload_size());
$OUTPUT->show_message('filesizeerror', 'error', array('size' => $size));
}
else if ($upload_error) {
$OUTPUT->show_message('fileuploaderror', 'error');
}
else {
$OUTPUT->show_message('importformaterror', 'error');
}
$OUTPUT->command('parent.import_state_set', 'error');
}
else {
$IMPORT_STATS = new stdClass;
$IMPORT_STATS->names = array();
$IMPORT_STATS->skipped_names = array();
$IMPORT_STATS->count = count($vcards);
$IMPORT_STATS->inserted = $IMPORT_STATS->skipped = $IMPORT_STATS->invalid = $IMPORT_STATS->errors = 0;
if ($replace) {
$CONTACTS->delete_all($CONTACTS->groups && $with_groups < 2);
}
if ($with_groups) {
$import_groups = $CONTACTS->list_groups();
}
foreach ($vcards as $vcard) {
$a_record = $vcard->get_assoc();
// Generate contact's display name (must be before validation), the same we do in save.inc
if (empty($a_record['name'])) {
$a_record['name'] = rcube_addressbook::compose_display_name($a_record, true);
// Reset it if equals to email address (from compose_display_name())
if ($a_record['name'] == $a_record['email'][0]) {
$a_record['name'] = '';
}
}
// skip invalid (incomplete) entries
if (!$CONTACTS->validate($a_record, true)) {
$IMPORT_STATS->invalid++;
continue;
}
// We're using UTF8 internally
$email = $vcard->email[0];
$email = rcube_utils::idn_to_utf8($email);
if (!$replace) {
$existing = null;
// compare e-mail address
if ($email) {
$existing = $CONTACTS->search('email', $email, 1, false);
}
// compare display name if email not found
if ((!$existing || !$existing->count) && $vcard->displayname) {
$existing = $CONTACTS->search('name', $vcard->displayname, 1, false);
}
if ($existing && $existing->count) {
$IMPORT_STATS->skipped++;
$IMPORT_STATS->skipped_names[] = $vcard->displayname ?: $email;
continue;
}
}
$a_record['vcard'] = $vcard->export();
$plugin = $RCMAIL->plugins->exec_hook('contact_create',
array('record' => $a_record, 'source' => null));
$a_record = $plugin['record'];
// insert record and send response
if (!$plugin['abort'])
$success = $CONTACTS->insert($a_record);
else
$success = $plugin['result'];
if ($success) {
// assign groups for this contact (if enabled)
if ($with_groups && !empty($a_record['groups'])) {
foreach (explode(',', $a_record['groups'][0]) as $group_name) {
if ($group_id = rcmail_import_group_id($group_name, $CONTACTS, $with_groups == 1, $import_groups)) {
$CONTACTS->add_to_group($group_id, $success);
}
}
}
$IMPORT_STATS->inserted++;
$IMPORT_STATS->names[] = $a_record['name'] ?: $email;
}
else {
$IMPORT_STATS->errors++;
}
}
$importstep = 'rcmail_import_confirm';
$OUTPUT->command('parent.import_state_set', $IMPORT_STATS->inserted ? 'reload' : 'ok');
}
}
$OUTPUT->set_pagetitle($RCMAIL->gettext('importcontacts'));
$OUTPUT->add_handlers(array(
'importstep' => $importstep,
));
// render page
if ($OUTPUT->template_exists('contactimport')) {
$OUTPUT->send('contactimport');
}
else {
$OUTPUT->send('importcontacts'); // deprecated
}
/**
* Handler function to display the import/upload form
*/
function rcmail_import_form($attrib)
{
global $RCMAIL, $OUTPUT;
$target = rcube_utils::get_input_value('_target', rcube_utils::INPUT_GPC);
$attrib += array('id' => "rcmImportForm");
$writable_books = $RCMAIL->get_address_sources(true, true);
$max_filesize = $RCMAIL->upload_init();
$form = '';
$table = new html_table(array('cols' => 2));
$upload = new html_inputfield(array(
'type' => 'file',
'name' => '_file[]',
'id' => 'rcmimportfile',
'size' => 40,
'multiple' => 'multiple',
'class' => 'form-control-file',
));
$table->add('title', html::label('rcmimportfile', $RCMAIL->gettext('importfromfile')));
$table->add(null, $upload->show()
. html::div('hint', $RCMAIL->gettext(array('id' => 'importfile', 'name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize))))
);
// addressbook selector
if (count($writable_books) > 1) {
$select = new html_select(array('name' => '_target', 'id' => 'rcmimporttarget', 'is_escaped' => true));
foreach ($writable_books as $book) {
$select->add($book['name'], $book['id']);
}
$table->add('title', html::label('rcmimporttarget', $RCMAIL->gettext('importtarget')));
$table->add(null, $select->show($target));
}
else {
$abook = new html_hiddenfield(array('name' => '_target', 'value' => key($writable_books)));
$form .= $abook->show();
}
$form .= html::tag('input', array('type' => 'hidden', 'name' => '_unlock', 'value' => ''));
// selector for group import options
if (count($writable_books) >= 1 || $writable_books[0]->groups) {
$select = new html_select(array('name' => '_groups', 'id' => 'rcmimportgroups', 'is_escaped' => true));
$select->add($RCMAIL->gettext('none'), '0');
$select->add($RCMAIL->gettext('importgroupsall'), '1');
$select->add($RCMAIL->gettext('importgroupsexisting'), '2');
$table->add('title', html::label('rcmimportgroups', $RCMAIL->gettext('importgroups')));
$table->add(null, $select->show(rcube_utils::get_input_value('_groups', rcube_utils::INPUT_GPC)));
}
// checkbox to replace the entire address book
$check_replace = new html_checkbox(array('name' => '_replace', 'value' => 1, 'id' => 'rcmimportreplace'));
$table->add('title', html::label('rcmimportreplace', $RCMAIL->gettext('importreplace')));
$table->add(null, $check_replace->show(rcube_utils::get_input_value('_replace', rcube_utils::INPUT_GPC)));
$form .= $table->show(array('id' => null) + $attrib);
$OUTPUT->set_env('writable_source', !empty($writable_books));
$OUTPUT->add_label('selectimportfile','importwait');
$OUTPUT->add_gui_object('importform', $attrib['id']);
$out = html::p(null, rcube::Q($RCMAIL->gettext('importdesc'), 'show'))
. $OUTPUT->form_tag(array(
'action' => $RCMAIL->url('import'),
'method' => 'post',
'enctype' => 'multipart/form-data') + $attrib,
$form);
return $out;
}
/**
* Render the confirmation page for the import process
*/
function rcmail_import_confirm($attrib)
{
global $IMPORT_STATS, $RCMAIL;
$vars = get_object_vars($IMPORT_STATS);
$vars['names'] = $vars['skipped_names'] = '';
$content = html::p(null, $RCMAIL->gettext(array(
'name' => 'importconfirm',
'nr' => $IMPORT_STATS->inserted,
'vars' => $vars,
)) . ($IMPORT_STATS->names ? ':' : '.'));
if ($IMPORT_STATS->names) {
$content .= html::p('em', join(', ', array_map(array('rcube', 'Q'), $IMPORT_STATS->names)));
}
if ($IMPORT_STATS->skipped) {
$content .= html::p(null, $RCMAIL->gettext(array(
'name' => 'importconfirmskipped',
'nr' => $IMPORT_STATS->skipped,
'vars' => $vars,
)) . ':')
. html::p('em', join(', ', array_map(array('rcube', 'Q'), $IMPORT_STATS->skipped_names)));
}
return html::div($attrib, $content);
}
/**
* Returns the matching group id. If group doesn't exist, it'll be created if allowed.
*/
function rcmail_import_group_id($group_name, $CONTACTS, $create, &$import_groups)
{
$group_id = 0;
foreach ($import_groups as $group) {
if (strtolower($group['name']) == strtolower($group_name)) {
$group_id = $group['ID'];
break;
}
}
// create a new group
if (!$group_id && $create) {
$new_group = $CONTACTS->create_group($group_name);
if (!$new_group['ID'])
$new_group['ID'] = $new_group['id'];
$import_groups[] = $new_group;
$group_id = $new_group['ID'];
}
return $group_id;
}
diff --git a/program/steps/addressbook/list.inc b/program/steps/addressbook/list.inc
index 9841088ff..bf33b2483 100644
--- a/program/steps/addressbook/list.inc
+++ b/program/steps/addressbook/list.inc
@@ -1,75 +1,73 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/addressbook/list.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2012, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Send contacts list to client (as remote response) |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
if (!empty($_GET['_page']))
$page = intval($_GET['_page']);
else
$page = $_SESSION['page'] ?: 1;
$_SESSION['page'] = $page;
// Use search result
if (($records = rcmail_search_update(true)) !== false) {
// sort the records
ksort($records, SORT_LOCALE_STRING);
// create resultset object
$count = count($records);
$first = ($page-1) * $PAGE_SIZE;
$result = new rcube_result_set($count, $first);
// we need only records for current page
if ($PAGE_SIZE < $count) {
$records = array_slice($records, $first, $PAGE_SIZE);
}
$result->records = array_values($records);
}
// List selected directory
else {
$afields = $RCMAIL->config->get('contactlist_fields');
$CONTACTS = rcmail_contact_source(null, true);
// get contacts for this user
$result = $CONTACTS->list_records($afields);
if (!$result->count && $result->searchonly) {
$OUTPUT->show_message('contactsearchonly', 'notice');
$OUTPUT->command('command', 'advanced-search');
}
if ($CONTACTS->group_id) {
$group_data = array('ID' => $CONTACTS->group_id)
+ array_intersect_key((array)$CONTACTS->get_group($CONTACTS->group_id), array('name'=>1,'email'=>1));
}
}
$OUTPUT->command('set_group_prop', $group_data);
// update message count display
$OUTPUT->set_env('pagecount', ceil($result->count / $PAGE_SIZE));
$OUTPUT->command('set_rowcount', rcmail_get_rowcount_text($result));
// create javascript list
rcmail_js_contacts_list($result);
// send response
$OUTPUT->send();
diff --git a/program/steps/addressbook/mailto.inc b/program/steps/addressbook/mailto.inc
index 3cde674aa..81fa14d5d 100644
--- a/program/steps/addressbook/mailto.inc
+++ b/program/steps/addressbook/mailto.inc
@@ -1,76 +1,74 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/addressbook/mailto.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2007-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Compose a recipient list with all selected contacts |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
$cids = rcmail_get_cids();
$mailto = array();
$sources = array();
foreach ($cids as $source => $cid) {
$CONTACTS = $RCMAIL->get_address_book($source);
if ($CONTACTS->ready) {
$CONTACTS->set_page(1);
$CONTACTS->set_pagesize(count($cid) + 2); // +2 to skip counting query
$sources[] = $CONTACTS->search($CONTACTS->primary_key, $cid, 0, true, true, 'email');
}
}
if (!empty($_REQUEST['_gid']) && isset($_REQUEST['_source'])) {
$source = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GP);
$group_id = rcube_utils::get_input_value('_gid', rcube_utils::INPUT_GP);
$CONTACTS = $RCMAIL->get_address_book($source);
$group_data = $CONTACTS->get_group($group_id);
// group has an email address assigned: use that
if ($group_data['email']) {
$mailto[] = format_email_recipient($group_data['email'][0], $group_data['name']);
}
else if ($CONTACTS->ready) {
$CONTACTS->set_group($group_id);
$CONTACTS->set_page(1);
$CONTACTS->set_pagesize(200); // limit somehow
$sources[] = $CONTACTS->list_records();
}
}
foreach ($sources as $source) {
while (is_object($source) && ($rec = $source->iterate())) {
$emails = $CONTACTS->get_col_values('email', $rec, true);
if (!empty($emails)) {
$mailto[] = format_email_recipient($emails[0], $rec['name']);
}
}
}
if (!empty($mailto)) {
$mailto_str = join(', ', $mailto);
$mailto_id = substr(md5($mailto_str), 0, 16);
$_SESSION['mailto'][$mailto_id] = urlencode($mailto_str);
$OUTPUT->command('open_compose_step', array('_mailto' => $mailto_id));
}
else {
$OUTPUT->show_message('nocontactsfound', 'warning');
}
// send response
$OUTPUT->send();
diff --git a/program/steps/addressbook/move.inc b/program/steps/addressbook/move.inc
index 998d96cfd..0de7d31d0 100644
--- a/program/steps/addressbook/move.inc
+++ b/program/steps/addressbook/move.inc
@@ -1,214 +1,213 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/addressbook/move.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2007-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Move a contact record from one direcotry to another |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
// only process ajax requests
if (!$OUTPUT->ajax_call) {
return;
}
$cids = rcmail_get_cids();
$target = rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST);
$target_group = rcube_utils::get_input_value('_togid', rcube_utils::INPUT_POST);
$all = 0;
$deleted = 0;
$success = 0;
$errormsg = 'moveerror';
$maxnum = $RCMAIL->config->get('max_group_members', 0);
$page = $_SESSION['page'] ?: 1;
foreach ($cids as $source => $source_cids) {
// Something wrong, target not specified
if (!strlen($target)) {
break;
}
// It maight happen when moving records from search result
// Do nothing, go to next source
if ((string)$target === (string)$source) {
continue;
}
$CONTACTS = $RCMAIL->get_address_book($source);
$TARGET = $RCMAIL->get_address_book($target);
if (!$TARGET || !$TARGET->ready || $TARGET->readonly) {
break;
}
if (!$CONTACTS || !$CONTACTS->ready || $CONTACTS->readonly) {
continue;
}
$ids = array();
foreach ($source_cids as $idx => $cid) {
$a_record = $CONTACTS->get_record($cid, true);
// avoid moving groups
if ($a_record['_type'] == 'group') {
unset($source_cids[$idx]);
continue;
}
// Check if contact exists, if so, we'll need it's ID
// Note: Some addressbooks allows empty email address field
// @TODO: should we check all email addresses?
$email = $CONTACTS->get_col_values('email', $a_record, true);
if (!empty($email))
$result = $TARGET->search('email', $email[0], 1, true, true);
else if (!empty($a_record['name']))
$result = $TARGET->search('name', $a_record['name'], 1, true, true);
else
$result = new rcube_result_set();
// insert contact record
if (!$result->count) {
$plugin = $RCMAIL->plugins->exec_hook('contact_create', array(
'record' => $a_record, 'source' => $target, 'group' => $target_group));
if (!$plugin['abort']) {
if ($insert_id = $TARGET->insert($plugin['record'], false)) {
$ids[] = $insert_id;
$success++;
}
}
else if ($plugin['result']) {
$ids = array_merge($ids, $plugin['result']);
$success++;
}
}
else {
$record = $result->first();
$ids[] = $record['ID'];
$errormsg = empty($email) ? 'contactnameexists' : 'contactexists';
}
}
// remove source contacts
if ($success && !empty($source_cids)) {
$all += count($source_cids);
$plugin = $RCMAIL->plugins->exec_hook('contact_delete', array(
'id' => $source_cids, 'source' => $source));
$del_status = !$plugin['abort'] ? $CONTACTS->delete($source_cids) : $plugin['result'];
if ($del_status) {
$deleted += $del_status;
}
}
// assign to group
if ($target_group && $TARGET->groups && !empty($ids)) {
$plugin = $RCMAIL->plugins->exec_hook('group_addmembers', array(
'group_id' => $target_group, 'ids' => $ids, 'source' => $target));
if (!$plugin['abort']) {
$TARGET->reset();
$TARGET->set_group($target_group);
if ($maxnum && ($TARGET->count()->count + count($plugin['ids']) > $maxnum)) {
$OUTPUT->show_message('maxgroupmembersreached', 'warning', array('max' => $maxnum));
$OUTPUT->send();
}
if (($cnt = $TARGET->add_to_group($target_group, $plugin['ids'])) && $cnt > $success)
$success = $cnt;
}
else if ($plugin['result']) {
$success = $plugin['result'];
}
$errormsg = $plugin['message'] ?: 'moveerror';
}
}
if (!$deleted || $deleted != $all) {
$OUTPUT->command('list_contacts');
}
else {
// update saved search after data changed
if (($records = rcmail_search_update(true)) !== false) {
// create resultset object
$count = count($records);
$first = ($page-1) * $PAGE_SIZE;
$result = new rcube_result_set($count, $first);
$pages = ceil((count($records) + $delcnt) / $PAGE_SIZE);
// last page and it's empty, display previous one
if ($result->count && $result->count <= ($PAGE_SIZE * ($page - 1))) {
$OUTPUT->command('list_page', 'prev');
$rowcount = $RCMAIL->gettext('loading');
}
// get records from the next page to add to the list
else if ($pages > 1 && $page < $pages) {
// sort the records
ksort($records, SORT_LOCALE_STRING);
$first += $PAGE_SIZE;
// create resultset object
$res = new rcube_result_set($count, $first - $deleted);
if ($PAGE_SIZE < $count) {
$records = array_slice($records, $first - $deleted, $deleted);
}
$res->records = array_values($records);
$records = $res;
}
else {
unset($records);
}
}
else {
// count contacts for this user
$result = $CONTACTS->count();
$pages = ceil(($result->count + $deleted) / $PAGE_SIZE);
// last page and it's empty, display previous one
if ($result->count && $result->count <= ($PAGE_SIZE * ($page - 1))) {
$OUTPUT->command('list_page', 'prev');
$rowcount = $RCMAIL->gettext('loading');
}
// get records from the next page to add to the list
else if ($pages > 1 && $page < $pages) {
$CONTACTS->set_page($page);
$records = $CONTACTS->list_records(null, -$deleted);
}
}
// update message count display
$OUTPUT->set_env('pagecount', ceil($result->count / $PAGE_SIZE));
$OUTPUT->command('set_rowcount', $rowcount ?: rcmail_get_rowcount_text($result));
// add new rows from next page (if any)
if (!empty($records)) {
rcmail_js_contacts_list($records);
}
}
if (!$success)
$OUTPUT->show_message($errormsg, 'error');
else
$OUTPUT->show_message('movesuccess', 'confirmation', array('nr' => $success));
// send response
$OUTPUT->send();
diff --git a/program/steps/addressbook/photo.inc b/program/steps/addressbook/photo.inc
index 1d3d9506f..62b014ece 100644
--- a/program/steps/addressbook/photo.inc
+++ b/program/steps/addressbook/photo.inc
@@ -1,99 +1,97 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/addressbook/photo.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Show contact photo |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
// Get contact ID and source ID from request
$cids = rcmail_get_cids();
$source = key($cids);
$cid = $cids ? array_shift($cids[$source]) : null;
// read the referenced file
if (($file_id = rcube_utils::get_input_value('_photo', rcube_utils::INPUT_GPC)) && ($tempfile = $_SESSION['contacts']['files'][$file_id])) {
$tempfile = $RCMAIL->plugins->exec_hook('attachment_display', $tempfile);
if ($tempfile['status']) {
if ($tempfile['data'])
$data = $tempfile['data'];
else if ($tempfile['path'])
$data = file_get_contents($tempfile['path']);
}
}
else {
// by email, search for contact first
if ($email = rcube_utils::get_input_value('_email', rcube_utils::INPUT_GPC)) {
foreach ($RCMAIL->get_address_sources() as $s) {
$abook = $RCMAIL->get_address_book($s['id']);
$result = $abook->search(array('email'), $email, 1, true, true, 'photo');
while ($result && ($record = $result->iterate())) {
if ($record['photo'])
break 2;
}
}
}
// by contact id
if (!$record && $cid) {
// Initialize addressbook source
$CONTACTS = rcmail_contact_source($source, true);
$SOURCE_ID = $source;
// read contact record
$record = $CONTACTS->get_record($cid, true);
}
if ($record['photo']) {
$data = is_array($record['photo']) ? $record['photo'][0] : $record['photo'];
if (!preg_match('![^a-z0-9/=+-]!i', $data))
$data = base64_decode($data, true);
}
}
// let plugins do fancy things with contact photos
$plugin = $RCMAIL->plugins->exec_hook('contact_photo',
array('record' => $record, 'email' => $email, 'data' => $data));
// redirect to url provided by a plugin
if ($plugin['url']) {
$RCMAIL->output->redirect($plugin['url']);
}
$data = $plugin['data'];
// detect if photo data is an URL
if (strlen($data) < 1024 && filter_var($data, FILTER_VALIDATE_URL)) {
$RCMAIL->output->redirect($data);
}
// cache for one day if requested by email
if (!$cid && $email) {
$RCMAIL->output->future_expire_header(86400);
}
if ($data) {
header('Content-Type: ' . rcube_mime::image_content_type($data));
echo $data;
}
else if (!empty($_GET['_error'])) {
header('HTTP/1.0 404 Photo not found');
}
else {
header('Content-Type: image/gif');
echo base64_decode(rcmail_output::BLANK_GIF);
}
exit;
diff --git a/program/steps/addressbook/print.inc b/program/steps/addressbook/print.inc
index cb045b1fd..a14517b2e 100644
--- a/program/steps/addressbook/print.inc
+++ b/program/steps/addressbook/print.inc
@@ -1,138 +1,136 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/addressbook/print.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2015, The Roundcube Dev Team |
- | Copyright (C) 2011-2015, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Print contact details |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+-----------------------------------------------------------------------+
*/
// Get contact ID and source ID from request
$cids = rcmail_get_cids();
$source = key($cids);
$cid = $cids ? array_shift($cids[$source]) : null;
// Initialize addressbook source
$CONTACTS = rcmail_contact_source($source, true);
$SOURCE_ID = $source;
// read contact record
if ($cid && $CONTACTS) {
$record = $CONTACTS->get_record($cid, true);
}
$OUTPUT->add_handlers(array(
'contacthead' => 'rcmail_contact_head',
'contactdetails' => 'rcmail_contact_details',
'contactphoto' => 'rcmail_contact_photo',
));
$OUTPUT->send('contactprint');
function rcmail_contact_head($attrib)
{
global $CONTACTS, $RCMAIL;
// check if we have a valid result
if (!(($result = $CONTACTS->get_result()) && ($record = $result->first()))) {
$RCMAIL->output->show_message('contactnotfound', 'error');
return false;
}
$form = array(
'head' => array( // section 'head' is magic!
'name' => $RCMAIL->gettext('contactnameandorg'),
'content' => array(
'prefix' => array(),
'name' => array(),
'firstname' => array(),
'middlename' => array(),
'surname' => array(),
'suffix' => array(),
),
),
);
unset($attrib['name']);
return rcmail_contact_form($form, $record, $attrib);
}
function rcmail_contact_details($attrib)
{
global $CONTACTS, $RCMAIL, $CONTACT_COLTYPES;
// check if we have a valid result
if (!(($result = $CONTACTS->get_result()) && ($record = $result->first()))) {
return false;
}
$i_size = $attrib['size'] ?: 40;
$form = array(
'contact' => array(
'name' => $RCMAIL->gettext('properties'),
'content' => array(
'organization' => array(),
'department' => array(),
'jobtitle' => array(),
'email' => array(),
'phone' => array(),
'address' => array(),
'website' => array(),
'im' => array(),
'groups' => array(),
),
),
'personal' => array(
'name' => $RCMAIL->gettext('personalinfo'),
'content' => array(
'nickname' => array(),
'gender' => array(),
'maidenname' => array(),
'birthday' => array(),
'anniversary' => array(),
'manager' => array(),
'assistant' => array(),
'spouse' => array(),
),
),
);
if (isset($CONTACT_COLTYPES['notes'])) {
$form['notes'] = array(
'name' => $RCMAIL->gettext('notes'),
'content' => array(
'notes' => array('type' => 'textarea', 'label' => false),
),
);
}
if ($CONTACTS->groups) {
$groups = $CONTACTS->get_record_groups($record['ID']);
if (!empty($groups)) {
$form['contact']['content']['groups'] = array(
'value' => rcube::Q(implode(', ', $groups)),
'label' => $RCMAIL->gettext('groups')
);
}
}
return rcmail_contact_form($form, $record, $attrib);
}
diff --git a/program/steps/addressbook/qrcode.inc b/program/steps/addressbook/qrcode.inc
index 73f098956..26e7da118 100644
--- a/program/steps/addressbook/qrcode.inc
+++ b/program/steps/addressbook/qrcode.inc
@@ -1,75 +1,73 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/addressbook/qrcode.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2016, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Show contact data as QR code |
- | |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
// Get contact ID and source ID from request
$cids = rcmail_get_cids();
$source = key($cids);
$cid = $cids ? array_shift($cids[$source]) : null;
// read contact record
$abook = rcmail_contact_source($source, true);
$contact = $abook->get_record($cid, true);
// generate QR code image
if ($data = rcmail_contact_qrcode($contact)) {
header('Content-Type: image/png');
header('Content-Length: ' . strlen($data));
echo $data;
}
else {
header('HTTP/1.0 404 Contact not found');
}
exit;
function rcmail_contact_qrcode($contact)
{
$vcard = new rcube_vcard();
// QR code input is limited, use only common fields
$fields = array('firstname', 'surname', 'middlename', 'nickname', 'organization',
'prefix', 'suffix', 'phone', 'email', 'jobtitle');
foreach ($contact as $field => $value) {
list($field, $section) = explode(':', $field, 2);
if (in_array($field, $fields)) {
foreach ((array) $value as $v) {
$vcard->set($field, $v, $section);
}
}
}
$data = $vcard->export();
$qrCode = new Endroid\QrCode\QrCode();
$qrCode
->setText($data)
->setSize(300)
->setPadding(0)
->setErrorCorrection('high')
// ->setLabel('Scan the code')
// ->setLabelFontSize(16)
->setForegroundColor(array('r' => 0, 'g' => 0, 'b' => 0, 'a' => 0))
->setBackgroundColor(array('r' => 255, 'g' => 255, 'b' => 255, 'a' => 0));
return $qrCode->get('png');
}
diff --git a/program/steps/addressbook/save.inc b/program/steps/addressbook/save.inc
index 49759ff69..b91b5190e 100644
--- a/program/steps/addressbook/save.inc
+++ b/program/steps/addressbook/save.inc
@@ -1,265 +1,263 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/addressbook/save.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Save a contact entry or to add a new one |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
$CONTACTS = rcmail_contact_source(null, true, true);
$cid = rcube_utils::get_input_value('_cid', rcube_utils::INPUT_POST);
$return_action = empty($cid) ? 'add' : 'edit';
// Source changed, display the form again
if (!empty($_GET['_reload'])) {
$RCMAIL->overwrite_action($return_action);
return;
}
// cannot edit record
if ($CONTACTS->readonly) {
$OUTPUT->show_message('contactreadonly', 'error');
$RCMAIL->overwrite_action($return_action);
return;
}
// read POST values into hash array
$a_record = array();
foreach ($GLOBALS['CONTACT_COLTYPES'] as $col => $colprop) {
if ($colprop['composite']) {
continue;
}
$fname = '_'.$col;
// gather form data of composite fields
if ($colprop['childs']) {
$values = array();
foreach ($colprop['childs'] as $childcol => $cp) {
$vals = rcube_utils::get_input_value('_'.$childcol, rcube_utils::INPUT_POST, true);
foreach ((array)$vals as $i => $val) {
$values[$i][$childcol] = $val;
}
}
$subtypes = isset($_REQUEST['_subtype_' . $col]) ? (array)rcube_utils::get_input_value('_subtype_' . $col, rcube_utils::INPUT_POST) : array('');
foreach ($subtypes as $i => $subtype) {
$suffix = $subtype ? ':'.$subtype : '';
if ($values[$i]) {
$a_record[$col.$suffix][] = $values[$i];
}
}
}
// assign values and subtypes
else if (is_array($_POST[$fname])) {
$values = rcube_utils::get_input_value($fname, rcube_utils::INPUT_POST, true);
$subtypes = rcube_utils::get_input_value('_subtype_' . $col, rcube_utils::INPUT_POST);
foreach ($values as $i => $val) {
if ($col == 'email') {
// extract email from full address specification, e.g. "Name" <addr@domain.tld>
$addr = rcube_mime::decode_address_list($val, 1, false);
if (!empty($addr) && ($addr = array_pop($addr)) && $addr['mailto']) {
$val = $addr['mailto'];
}
}
$subtype = $subtypes[$i] ? ':'.$subtypes[$i] : '';
$a_record[$col.$subtype][] = $val;
}
}
else if (isset($_POST[$fname])) {
$a_record[$col] = rcube_utils::get_input_value($fname, rcube_utils::INPUT_POST, true);
// normalize the submitted date strings
if ($colprop['type'] == 'date') {
if ($a_record[$col] && ($dt = rcube_utils::anytodatetime($a_record[$col]))) {
$a_record[$col] = $dt->format('Y-m-d');
}
else {
unset($a_record[$col]);
}
}
}
}
// Generate contact's display name (must be before validation)
if (empty($a_record['name'])) {
$a_record['name'] = rcube_addressbook::compose_display_name($a_record, true);
// Reset it if equals to email address (from compose_display_name())
$email = rcube_addressbook::get_col_values('email', $a_record, true);
if ($a_record['name'] == $email[0]) {
$a_record['name'] = '';
}
}
// do input checks (delegated to $CONTACTS instance)
if (!$CONTACTS->validate($a_record)) {
$err = (array)$CONTACTS->get_error();
$OUTPUT->show_message($err['message'] ? rcube::Q($err['message']) : 'formincomplete', 'warning');
$GLOBALS['EDIT_RECORD'] = $a_record; // store submitted data to be used in edit form
$RCMAIL->overwrite_action($return_action);
return;
}
// get raw photo data if changed
if (isset($a_record['photo'])) {
if ($a_record['photo'] == '-del-') {
$a_record['photo'] = '';
}
else if ($tempfile = $_SESSION['contacts']['files'][$a_record['photo']]) {
$tempfile = $RCMAIL->plugins->exec_hook('attachment_get', $tempfile);
if ($tempfile['status'])
$a_record['photo'] = $tempfile['data'] ?: @file_get_contents($tempfile['path']);
}
else
unset($a_record['photo']);
// cleanup session data
$RCMAIL->plugins->exec_hook('attachments_cleanup', array('group' => 'contact'));
$RCMAIL->session->remove('contacts');
}
$source = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
// update an existing contact
if (!empty($cid)) {
$plugin = $RCMAIL->plugins->exec_hook('contact_update',
array('id' => $cid, 'record' => $a_record, 'source' => $source));
$a_record = $plugin['record'];
if (!$plugin['abort'])
$result = $CONTACTS->update($cid, $a_record);
else
$result = $plugin['result'];
if ($result) {
// show confirmation
$OUTPUT->show_message('successfullysaved', 'confirmation', null, false);
// in search mode, just reload the list (#1490015)
if ($_REQUEST['_search']) {
$OUTPUT->command('parent.command', 'list');
$OUTPUT->send('iframe');
}
// LDAP DN change
if (is_string($result) && strlen($result) > 1) {
$newcid = $result;
// change cid in POST for 'show' action
$_POST['_cid'] = $newcid;
}
// refresh contact data for list update and 'show' action
$CONTACT_RECORD = $CONTACTS->get_record($newcid ?: $cid, true);
// Plugins can decide to remove the contact on edit, e.g. automatic_addressbook
// Best we can do is to refresh the list (#5522)
if (empty($CONTACT_RECORD)) {
$OUTPUT->command('parent.command', 'list');
$OUTPUT->send('iframe');
}
// Update contacts list
$a_js_cols = array();
$record = $CONTACT_RECORD;
$record['email'] = reset($CONTACTS->get_col_values('email', $record, true));
$record['name'] = rcube_addressbook::compose_list_name($record);
foreach (array('name') as $col) {
$a_js_cols[] = rcube::Q((string)$record[$col]);
}
// performance: unset some big data items we don't need here
$record = array_intersect_key($record, array('ID' => 1,'email' => 1,'name' => 1));
$record['_type'] = 'person';
// update the changed col in list
$OUTPUT->command('parent.update_contact_row', $cid, $a_js_cols, $newcid, $source, $record);
$RCMAIL->overwrite_action('show');
}
else {
// show error message
$err = $CONTACTS->get_error();
$OUTPUT->show_message($plugin['message'] ?: ($err['message'] ?: 'errorsaving'), 'error', null, false);
$RCMAIL->overwrite_action('show');
}
}
// insert a new contact
else {
// Name of the addressbook already selected on the list
$orig_source = rcube_utils::get_input_value('_orig_source', rcube_utils::INPUT_GPC);
if (!strlen($source)) {
$source = $orig_source;
}
// show notice if existing contacts with same e-mail are found
foreach ($CONTACTS->get_col_values('email', $a_record, true) as $email) {
if ($email && ($res = $CONTACTS->search('email', $email, 1, false, true)) && $res->count) {
$OUTPUT->show_message('contactexists', 'notice', null, false);
break;
}
}
$plugin = $RCMAIL->plugins->exec_hook('contact_create', array(
'record' => $a_record, 'source' => $source));
$a_record = $plugin['record'];
// insert record and send response
if (!$plugin['abort'])
$insert_id = $CONTACTS->insert($a_record);
else
$insert_id = $plugin['result'];
if ($insert_id) {
$CONTACTS->reset();
// add new contact to the specified group
if ($CONTACTS->groups && $CONTACTS->group_id) {
$plugin = $RCMAIL->plugins->exec_hook('group_addmembers', array(
'group_id' => $CONTACTS->group_id, 'ids' => $insert_id, 'source' => $source));
if (!$plugin['abort']) {
if (($maxnum = $RCMAIL->config->get('max_group_members', 0)) && ($CONTACTS->count()->count + 1 > $maxnum)) {
// @FIXME: should we remove the contact?
$msgtext = $RCMAIL->gettext(array('name' => 'maxgroupmembersreached', 'vars' => array('max' => $maxnum)));
$OUTPUT->command('parent.display_message', $msgtext, 'warning');
}
else {
$CONTACTS->add_to_group($plugin['group_id'], $plugin['ids']);
}
}
}
// show confirmation
$OUTPUT->show_message('successfullysaved', 'confirmation', null, false);
$OUTPUT->command('parent.set_rowcount', $RCMAIL->gettext('loading'));
$OUTPUT->command('parent.list_contacts');
$OUTPUT->send('iframe');
}
else {
// show error message
$err = $CONTACTS->get_error();
$OUTPUT->show_message($plugin['message'] ?: ($err['message'] ?: 'errorsaving'), 'error', null, false);
$RCMAIL->overwrite_action('add');
}
}
diff --git a/program/steps/addressbook/search.inc b/program/steps/addressbook/search.inc
index b370a5b6b..a8147d6bf 100644
--- a/program/steps/addressbook/search.inc
+++ b/program/steps/addressbook/search.inc
@@ -1,350 +1,348 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/addressbook/search.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2011, The Roundcube Dev Team |
- | Copyright (C) 2011, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Search action (and form) for address book contacts |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+-----------------------------------------------------------------------+
*/
if ($RCMAIL->action == 'search-create') {
$id = rcube_utils::get_input_value('_search', rcube_utils::INPUT_POST);
$name = rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST, true);
if (($params = $_SESSION['search_params']) && $params['id'] == $id) {
$data = array(
'type' => rcube_user::SEARCH_ADDRESSBOOK,
'name' => $name,
'data' => array(
'fields' => $params['data'][0],
'search' => $params['data'][1],
),
);
$plugin = $RCMAIL->plugins->exec_hook('saved_search_create', array('data' => $data));
if (!$plugin['abort'])
$result = $RCMAIL->user->insert_search($plugin['data']);
else
$result = $plugin['result'];
}
if ($result) {
$OUTPUT->show_message('savedsearchcreated', 'confirmation');
$OUTPUT->command('insert_saved_search', rcube::Q($name), rcube::Q($result));
}
else
$OUTPUT->show_message($plugin['message'] ?: 'savedsearchcreateerror', 'error');
$OUTPUT->send();
}
if ($RCMAIL->action == 'search-delete') {
$id = rcube_utils::get_input_value('_sid', rcube_utils::INPUT_POST);
$plugin = $RCMAIL->plugins->exec_hook('saved_search_delete', array('id' => $id));
if (!$plugin['abort'])
$result = $RCMAIL->user->delete_search($id);
else
$result = $plugin['result'];
if ($result) {
$OUTPUT->show_message('savedsearchdeleted', 'confirmation');
$OUTPUT->command('remove_search_item', rcube::Q($id));
// contact list will be cleared, clear also page counter
$OUTPUT->command('set_rowcount', $RCMAIL->gettext('nocontactsfound'));
$OUTPUT->set_env('pagecount', 0);
}
else
$OUTPUT->show_message($plugin['message'] ?: 'savedsearchdeleteerror', 'error');
$OUTPUT->send();
}
if (!isset($_GET['_form'])) {
rcmail_contact_search();
}
$OUTPUT->add_handler('searchform', 'rcmail_contact_search_form');
$OUTPUT->send('contactsearch');
function rcmail_contact_search()
{
global $RCMAIL, $OUTPUT, $SEARCH_MODS_DEFAULT, $PAGE_SIZE;
$adv = isset($_POST['_adv']);
$sid = rcube_utils::get_input_value('_sid', rcube_utils::INPUT_GET);
// get search criteria from saved search
if ($sid && ($search = $RCMAIL->user->get_search($sid))) {
$fields = $search['data']['fields'];
$search = $search['data']['search'];
}
// get fields/values from advanced search form
else if ($adv) {
foreach (array_keys($_POST) as $key) {
$s = trim(rcube_utils::get_input_value($key, rcube_utils::INPUT_POST, true));
if (strlen($s) && preg_match('/^_search_([a-zA-Z0-9_-]+)$/', $key, $m)) {
$search[] = $s;
$fields[] = $m[1];
}
}
if (empty($fields)) {
// do nothing, show the form again
return;
}
}
// quick-search
else {
$search = trim(rcube_utils::get_input_value('_q', rcube_utils::INPUT_GET, true));
$fields = explode(',', rcube_utils::get_input_value('_headers', rcube_utils::INPUT_GET));
if (empty($fields)) {
$fields = array_keys($SEARCH_MODS_DEFAULT);
}
else {
$fields = array_filter($fields);
}
// update search_mods setting
$old_mods = $RCMAIL->config->get('addressbook_search_mods');
$search_mods = array_fill_keys($fields, 1);
if ($old_mods != $search_mods) {
$RCMAIL->user->save_prefs(array('addressbook_search_mods' => $search_mods));
}
if (in_array('*', $fields)) {
$fields = '*';
}
}
// Values matching mode
$mode = (int) $RCMAIL->config->get('addressbook_search_mode');
$mode |= rcube_addressbook::SEARCH_GROUPS;
// get sources list
$sources = $RCMAIL->get_address_sources();
$search_set = array();
$records = array();
$sort_col = $RCMAIL->config->get('addressbook_sort_col', 'name');
$afields = $RCMAIL->config->get('contactlist_fields');
foreach ($sources as $s) {
$source = $RCMAIL->get_address_book($s['id']);
// check if search fields are supported....
if (is_array($fields)) {
$cols = $source->coltypes[0] ? array_flip($source->coltypes) : $source->coltypes;
$supported = 0;
foreach ($fields as $f) {
if (array_key_exists($f, $cols)) {
$supported ++;
}
}
// in advanced search we require all fields (AND operator)
// in quick search we require at least one field (OR operator)
if (($adv && $supported < count($fields)) || (!$adv && !$supported)) {
continue;
}
}
// reset page
$source->set_page(1);
$source->set_pagesize(9999);
// get contacts count
$result = $source->search($fields, $search, $mode, false);
if (!$result->count) {
continue;
}
// get records
$result = $source->list_records($afields);
while ($row = $result->next()) {
$row['sourceid'] = $s['id'];
$key = rcube_addressbook::compose_contact_key($row, $sort_col);
$records[$key] = $row;
}
unset($result);
$search_set[$s['id']] = $source->get_search_set();
}
// sort the records
ksort($records, SORT_LOCALE_STRING);
// create resultset object
$count = count($records);
$result = new rcube_result_set($count);
// cut first-page records
if ($PAGE_SIZE < $count) {
$records = array_slice($records, 0, $PAGE_SIZE);
}
$result->records = array_values($records);
// search request ID
$search_request = md5('addr'
.(is_array($fields) ? implode($fields, ',') : $fields)
.(is_array($search) ? implode($search, ',') : $search));
// save search settings in session
$_SESSION['search'][$search_request] = $search_set;
$_SESSION['search_params'] = array('id' => $search_request, 'data' => array($fields, $search));
$_SESSION['page'] = 1;
if ($adv)
$OUTPUT->command('list_contacts_clear');
if ($result->count > 0) {
// create javascript list
rcmail_js_contacts_list($result);
$OUTPUT->show_message('contactsearchsuccessful', 'confirmation', array('nr' => $result->count));
}
else {
$OUTPUT->show_message('nocontactsfound', 'notice');
}
// update message count display
$OUTPUT->set_env('search_request', $search_request);
$OUTPUT->set_env('pagecount', ceil($result->count / $PAGE_SIZE));
$OUTPUT->command('set_rowcount', rcmail_get_rowcount_text($result));
// Re-set current source
$OUTPUT->set_env('search_id', $sid);
$OUTPUT->set_env('source', '');
$OUTPUT->set_env('group', '');
// Re-set list header
$OUTPUT->command('set_group_prop', null);
if (!$sid) {
// unselect currently selected directory/group
$OUTPUT->command('unselect_directory');
// enable "Save search" command
$OUTPUT->command('enable_command', 'search-create', true);
}
$OUTPUT->command('update_group_commands');
// send response
$OUTPUT->send();
}
function rcmail_contact_search_form($attrib)
{
global $RCMAIL, $CONTACT_COLTYPES;
$i_size = $attrib['size'] ?: 30;
$short_labels = rcube_utils::get_boolean($attrib['short-legend-labels']);
$form = array(
'main' => array(
'name' => $RCMAIL->gettext('properties'),
'content' => array(
),
),
'personal' => array(
'name' => $RCMAIL->gettext($short_labels ? 'personal' : 'personalinfo'),
'content' => array(
),
),
'other' => array(
'name' => $RCMAIL->gettext('other'),
'content' => array(
),
),
);
// get supported coltypes from all address sources
$sources = $RCMAIL->get_address_sources();
$coltypes = array();
foreach ($sources as $s) {
$CONTACTS = $RCMAIL->get_address_book($s['id']);
if (is_array($CONTACTS->coltypes)) {
$contact_cols = $CONTACTS->coltypes[0] ? array_flip($CONTACTS->coltypes) : $CONTACTS->coltypes;
$coltypes = array_merge($coltypes, $contact_cols);
}
}
// merge supported coltypes with $CONTACT_COLTYPES
foreach ($coltypes as $col => $colprop) {
$coltypes[$col] = $CONTACT_COLTYPES[$col] ? array_merge($CONTACT_COLTYPES[$col], (array)$colprop) : (array)$colprop;
}
// build form fields list
foreach ($coltypes as $col => $colprop) {
if ($colprop['type'] != 'image' && !$colprop['nosearch']) {
$ftype = $colprop['type'] == 'select' ? 'select' : 'text';
$label = isset($colprop['label']) ? $colprop['label'] : $RCMAIL->gettext($col);
$category = $colprop['category'] ?: 'other';
// load jquery UI datepicker for date fields
if ($colprop['type'] == 'date') {
$colprop['class'] .= ($colprop['class'] ? ' ' : '') . 'datepicker';
}
else if ($ftype == 'text') {
$colprop['size'] = $i_size;
}
$colprop['id'] = '_search_' . $col;
$content = html::div('row',
html::label(array('class' => 'contactfieldlabel label', 'for' => $colprop['id']), rcube::Q($label))
. html::div('contactfieldcontent', rcube_output::get_edit_field('search_'.$col, '', $colprop, $ftype)));
$form[$category]['content'][] = $content;
}
}
$hiddenfields = new html_hiddenfield();
$hiddenfields->add(array('name' => '_adv', 'value' => 1));
$out = $RCMAIL->output->request_form(array(
'name' => 'form',
'method' => 'post',
'task' => $RCMAIL->task,
'action' => 'search',
'noclose' => true,
) + $attrib, $hiddenfields->show());
$RCMAIL->output->add_gui_object('editform', $attrib['id']);
unset($attrib['name']);
unset($attrib['id']);
foreach ($form as $f) {
if (!empty($f['content'])) {
$content = html::div('contactfieldgroup', join("\n", $f['content']));
$out .= html::tag('fieldset', $attrib,
html::tag('legend', null, rcube::Q($f['name']))
. $content) . "\n";
}
}
return $out . '</form>';
}
diff --git a/program/steps/addressbook/show.inc b/program/steps/addressbook/show.inc
index e8e3712b6..ee5c58802 100644
--- a/program/steps/addressbook/show.inc
+++ b/program/steps/addressbook/show.inc
@@ -1,211 +1,209 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/addressbook/show.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Show contact details |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
// Get contact ID and source ID from request
$cids = rcmail_get_cids();
$source = key($cids);
$cid = $cids ? array_shift($cids[$source]) : null;
// Initialize addressbook source
$CONTACTS = rcmail_contact_source($source, true);
$SOURCE_ID = $source;
// read contact record (or get the one defined in 'save' action)
if ($cid && ($record = ($CONTACT_RECORD ?: $CONTACTS->get_record($cid, true)))) {
$OUTPUT->set_env('readonly', $CONTACTS->readonly || $record['readonly']);
$OUTPUT->set_env('cid', $record['ID']);
// remember current search request ID (if in search mode)
if ($search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GET)) {
$OUTPUT->set_env('search_request', $search);
}
}
// get address book name (for display)
rcmail_set_sourcename($CONTACTS);
$OUTPUT->add_handlers(array(
'contacthead' => 'rcmail_contact_head',
'contactdetails' => 'rcmail_contact_details',
'contactphoto' => 'rcmail_contact_photo',
));
$OUTPUT->send('contact');
function rcmail_contact_head($attrib)
{
global $CONTACTS, $RCMAIL;
// check if we have a valid result
if (!(($result = $CONTACTS->get_result()) && ($record = $result->first()))) {
$RCMAIL->output->show_message('contactnotfound', 'error');
return false;
}
$form = array(
'head' => array( // section 'head' is magic!
'name' => $RCMAIL->gettext('contactnameandorg'),
'content' => array(
'source' => array('type' => 'text'),
'prefix' => array('type' => 'text'),
'firstname' => array('type' => 'text'),
'middlename' => array('type' => 'text'),
'surname' => array('type' => 'text'),
'suffix' => array('type' => 'text'),
'name' => array('type' => 'text'),
'nickname' => array('type' => 'text'),
'organization' => array('type' => 'text'),
'department' => array('type' => 'text'),
'jobtitle' => array('type' => 'text'),
),
),
);
unset($attrib['name']);
return rcmail_contact_form($form, $record, $attrib);
}
function rcmail_contact_details($attrib)
{
global $CONTACTS, $RCMAIL, $CONTACT_COLTYPES;
// check if we have a valid result
if (!(($result = $CONTACTS->get_result()) && ($record = $result->first()))) {
return false;
}
$i_size = $attrib['size'] ?: 40;
$short_labels = rcube_utils::get_boolean($attrib['short-legend-labels']);
$form = array(
'contact' => array(
'name' => $RCMAIL->gettext('properties'),
'content' => array(
'email' => array('size' => $i_size, 'render_func' => 'rcmail_render_email_value'),
'phone' => array('size' => $i_size),
'address' => array(),
'website' => array('size' => $i_size, 'render_func' => 'rcmail_render_url_value'),
'im' => array('size' => $i_size),
),
),
'personal' => array(
'name' => $RCMAIL->gettext($short_labels ? 'personal' : 'personalinfo'),
'content' => array(
'gender' => array('size' => $i_size),
'maidenname' => array('size' => $i_size),
'birthday' => array('size' => $i_size),
'anniversary' => array('size' => $i_size),
'manager' => array('size' => $i_size),
'assistant' => array('size' => $i_size),
'spouse' => array('size' => $i_size),
),
),
);
if (isset($CONTACT_COLTYPES['notes'])) {
$form['notes'] = array(
'name' => $RCMAIL->gettext('notes'),
'content' => array(
'notes' => array('type' => 'textarea', 'label' => false),
),
);
}
if ($CONTACTS->groups) {
$form['groups'] = array(
'name' => $RCMAIL->gettext('groups'),
'content' => rcmail_contact_record_groups($record['ID']),
);
}
return rcmail_contact_form($form, $record, $attrib);
}
function rcmail_render_email_value($email)
{
global $RCMAIL;
return html::a(array(
'href' => 'mailto:' . $email,
'onclick' => sprintf("return %s.command('compose','%s',this)", rcmail_output::JS_OBJECT_NAME, rcube::JQ($email)),
'title' => $RCMAIL->gettext('composeto'),
'class' => 'email',
), rcube::Q($email));
}
function rcmail_render_url_value($url)
{
$prefix = preg_match('!^(http|ftp)s?://!', $url) ? '' : 'http://';
return html::a(array(
'href' => $prefix . $url,
'target' => '_blank',
'class' => 'url',
), rcube::Q($url));
}
function rcmail_contact_record_groups($contact_id)
{
global $RCMAIL, $CONTACTS, $GROUPS;
$GROUPS = $CONTACTS->list_groups();
if (empty($GROUPS)) {
return '';
}
$form_attrs = array(
'name' => 'form',
'method' => 'post',
'task' => $RCMAIL->task,
'action' => 'save',
'request' => 'save.' . intval($contact_id),
'noclose' => true,
);
$list = array();
$members = $CONTACTS->get_record_groups($contact_id);
$table = new html_table(array('tagname' => 'ul', 'cols' => 1, 'class' => 'proplist simplelist'));
$checkbox = new html_checkbox(array('name' => '_gid[]',
'class' => 'groupmember', 'disabled' => $CONTACTS->readonly));
foreach ($GROUPS as $group) {
$gid = $group['ID'];
$input = $checkbox->show($members[$gid] ? $gid : null, array('value' => $gid));
$table->add(null, html::label(null, $input . rcube::Q($group['name'])));
}
$hiddenfields = new html_hiddenfield(array('name' => '_source', 'value' => rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC)));
$hiddenfields->add(array('name' => '_cid', 'value' => $contact_id));
$form_start = $RCMAIL->output->request_form($form_attrs, $hiddenfields->show());
$form_end = '</form>';
$RCMAIL->output->add_gui_object('editform', 'form');
$RCMAIL->output->add_label('addingmember', 'removingmember');
return $form_start . html::tag('fieldset', 'contactfieldgroup contactgroups', $table->show()) . $form_end;
}
diff --git a/program/steps/addressbook/undo.inc b/program/steps/addressbook/undo.inc
index 973bfec99..0d1f21715 100644
--- a/program/steps/addressbook/undo.inc
+++ b/program/steps/addressbook/undo.inc
@@ -1,54 +1,52 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/addressbook/undo.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2011-2013, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Undelete contacts (CIDs) from last delete action |
- | |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+-----------------------------------------------------------------------+
*/
// process ajax requests only
if (!$OUTPUT->ajax_call) {
return;
}
$undo = $_SESSION['contact_undo'];
$delcnt = 0;
foreach ((array)$undo['data'] as $source => $cid) {
$CONTACTS = rcmail_contact_source($source);
$plugin = $RCMAIL->plugins->exec_hook('contact_undelete', array(
'id' => $cid, 'source' => $source));
$restored = !$plugin['abort'] ? $CONTACTS->undelete($cid) : $plugin['result'];
if (!$restored) {
$OUTPUT->show_message($plugin['message'] ?: 'contactrestoreerror', 'error');
$OUTPUT->command('list_contacts');
$OUTPUT->send();
}
else {
$delcnt += $restored;
}
}
$RCMAIL->session->remove('contact_undo');
$OUTPUT->show_message('contactrestored', 'confirmation');
$OUTPUT->command('list_contacts');
// send response
$OUTPUT->send();
diff --git a/program/steps/addressbook/upload_photo.inc b/program/steps/addressbook/upload_photo.inc
index 745c3f1a4..22bf3b5d7 100644
--- a/program/steps/addressbook/upload_photo.inc
+++ b/program/steps/addressbook/upload_photo.inc
@@ -1,91 +1,89 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/addressbook/upload_photo.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2011, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Handles contact photo uploads |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
// Supported image format types
// ImageMagick works with other non-image types (e.g.pdf) we don't want here
$IMAGE_TYPES = explode(',', 'jpeg,jpg,jp2,tiff,tif,bmp,eps,gif,png,png8,png24,png32,svg,ico');
// clear all stored output properties (like scripts and env vars)
$OUTPUT->reset();
if ($filepath = $_FILES['_photo']['tmp_name']) {
// check file type and resize image
$image = new rcube_image($_FILES['_photo']['tmp_name']);
$imageprop = $image->props();
if (in_array(strtolower($imageprop['type']), $IMAGE_TYPES)
&& $imageprop['width'] && $imageprop['height']
) {
$maxsize = intval($RCMAIL->config->get('contact_photo_size', 160));
$tmpfname = rcube_utils::temp_filename('imgconvert');
$save_hook = 'attachment_upload';
// scale image to a maximum size
if (($imageprop['width'] > $maxsize || $imageprop['height'] > $maxsize) && $image->resize($maxsize, $tmpfname)) {
$filepath = $tmpfname;
$save_hook = 'attachment_save';
}
// save uploaded file in storage backend
$attachment = $RCMAIL->plugins->exec_hook($save_hook, array(
'path' => $filepath,
'size' => $_FILES['_photo']['size'],
'name' => $_FILES['_photo']['name'],
'mimetype' => 'image/' . $imageprop['type'],
'group' => 'contact',
));
}
else {
$attachment['error'] = $RCMAIL->gettext('invalidimageformat');
}
if ($attachment['status'] && !$attachment['abort']) {
$file_id = $attachment['id'];
$_SESSION['contacts']['files'][$file_id] = $attachment;
$OUTPUT->command('replace_contact_photo', $file_id);
}
else { // upload failed
$err = $_FILES['_photo']['error'];
$size = $RCMAIL->show_bytes(rcube_utils::max_upload_size());
if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE)
$msg = $RCMAIL->gettext(array('name' => 'filesizeerror', 'vars' => array('size' => $size)));
else if ($attachment['error'])
$msg = $attachment['error'];
else
$msg = $RCMAIL->gettext('fileuploaderror');
$OUTPUT->command('display_message', $msg, 'error');
}
}
else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// if filesize exceeds post_max_size then $_FILES array is empty,
// show filesizeerror instead of fileuploaderror
if ($maxsize = ini_get('post_max_size'))
$msg = $RCMAIL->gettext(array('name' => 'filesizeerror', 'vars' => array('size' => $RCMAIL->show_bytes(parse_bytes($maxsize)))));
else
$msg = $RCMAIL->gettext('fileuploaderror');
$OUTPUT->command('display_message', $msg, 'error');
}
$OUTPUT->command('photo_upload_end');
$OUTPUT->send('iframe');
diff --git a/program/steps/mail/addcontact.inc b/program/steps/mail/addcontact.inc
index 1f54c6dc6..3193b4177 100644
--- a/program/steps/mail/addcontact.inc
+++ b/program/steps/mail/addcontact.inc
@@ -1,97 +1,95 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/addcontact.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Add the submitted contact to the users address book |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
// only process ajax requests
if (!$OUTPUT->ajax_call) {
return;
}
// Get default addressbook
$CONTACTS = $RCMAIL->get_address_book(-1, true);
if (!empty($_POST['_address']) && is_object($CONTACTS)) {
$address = rcube_utils::get_input_value('_address', rcube_utils::INPUT_POST, true);
$contact_arr = rcube_mime::decode_address_list($address, 1, false);
if (!empty($contact_arr[1]['mailto'])) {
$contact = array(
'email' => $contact_arr[1]['mailto'],
'name' => $contact_arr[1]['name'],
);
// Validity checks
if (empty($contact['email'])) {
$OUTPUT->show_message('errorsavingcontact', 'error');
$OUTPUT->send();
}
$email = rcube_utils::idn_to_ascii($contact['email']);
if (!rcube_utils::check_email($email, false)) {
$OUTPUT->show_message('emailformaterror', 'error', array('email' => $contact['email']));
$OUTPUT->send();
}
$contact['email'] = rcube_utils::idn_to_utf8($contact['email']);
$contact = $RCMAIL->plugins->exec_hook('contact_displayname', $contact);
if (empty($contact['firstname']) || empty($contact['surname'])) {
$contact['name'] = rcube_addressbook::compose_display_name($contact);
}
// validate contact record
if (!$CONTACTS->validate($contact, true)) {
$error = $CONTACTS->get_error();
// TODO: show dialog to complete record
// if ($error['type'] == rcube_addressbook::ERROR_VALIDATE) { }
$OUTPUT->show_message($error['message'] ?: 'errorsavingcontact', 'error');
$OUTPUT->send();
}
// check for existing contacts
$existing = $CONTACTS->search('email', $contact['email'], 1, false);
if ($done = $existing->count) {
$OUTPUT->show_message('contactexists', 'warning');
}
else {
$plugin = $RCMAIL->plugins->exec_hook('contact_create', array('record' => $contact, 'source' => null));
$contact = $plugin['record'];
$done = !$plugin['abort'] ? $CONTACTS->insert($contact) : $plugin['result'];
if ($done) {
$OUTPUT->show_message('addedsuccessfully', 'confirmation');
if (!empty($_POST['_reload'])) {
$OUTPUT->command('command', 'load-remote');
}
}
}
}
}
if (!$done) {
$OUTPUT->show_message($plugin['message'] ?: 'errorsavingcontact', 'error');
}
$OUTPUT->send();
diff --git a/program/steps/mail/attachments.inc b/program/steps/mail/attachments.inc
index e5a07217a..9a8049355 100644
--- a/program/steps/mail/attachments.inc
+++ b/program/steps/mail/attachments.inc
@@ -1,286 +1,284 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/attachments.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Upload, remove, display attachments in compose form |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
$COMPOSE_ID = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
$COMPOSE = null;
if ($COMPOSE_ID && $_SESSION['compose_data_' . $COMPOSE_ID]) {
$SESSION_KEY = 'compose_data_' . $COMPOSE_ID;
$COMPOSE =& $_SESSION[$SESSION_KEY];
}
if (!$COMPOSE) {
die("Invalid session var!");
}
$file_id = rcube_utils::get_input_value('_file', rcube_utils::INPUT_GPC);
$file_id = preg_replace('/^rcmfile/', '', $file_id) ?: 'unknown';
// remove an attachment
if ($RCMAIL->action == 'remove-attachment') {
if ($attachment = $COMPOSE['attachments'][$file_id]) {
$attachment = $RCMAIL->plugins->exec_hook('attachment_delete', $attachment);
}
if ($attachment['status']) {
if (is_array($COMPOSE['attachments'][$file_id])) {
$RCMAIL->session->remove($SESSION_KEY . '.attachments.' . $file_id);
$OUTPUT->command('remove_from_attachment_list', "rcmfile$file_id");
}
}
$OUTPUT->send();
exit;
}
// rename an attachment
if ($RCMAIL->action == 'rename-attachment') {
$filename = rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST);
$filename = trim($filename);
if (strlen($filename)
&& ($attachment = $COMPOSE['attachments'][$file_id])
&& is_array($attachment)
) {
$attachment['name'] = $filename;
$RCMAIL->session->remove($SESSION_KEY . '.attachments. ' . $file_id);
$RCMAIL->session->append($SESSION_KEY . '.attachments', $attachment['id'], $attachment);
$OUTPUT->command('rename_attachment_handler', "rcmfile$file_id", $filename);
}
$OUTPUT->send();
exit;
}
if ($RCMAIL->action == 'display-attachment') {
$RCMAIL->display_uploaded_file($COMPOSE['attachments'][$file_id]);
exit;
}
/***** attachment upload action *****/
// clear all stored output properties (like scripts and env vars)
$OUTPUT->reset();
$uploadid = rcube_utils::get_input_value('_uploadid', rcube_utils::INPUT_GPC);
$uri = rcube_utils::get_input_value('_uri', rcube_utils::INPUT_POST);
// handle dropping a reference to an attachment part of some message
if ($uri) {
$url = parse_url($uri);
parse_str($url['query'], $params);
if (strlen($params['_mbox']) && $params['_uid'] && $params['_part']) {
// @TODO: at some point we might support drag-n-drop between
// two different accounts on the same server, for now make sure
// this is the same server and the same user
list($host, $port) = explode(':', $_SERVER['HTTP_HOST']);
if ($host == $url['host'] && $port == $url['port']
&& $RCMAIL->get_user_name() == rawurldecode($url['user'])
) {
$message = new rcube_message($params['_uid'], $params['_mbox']);
if ($message && !empty($message->headers)) {
$attachment = rcmail_save_attachment($message, $params['_part'], $COMPOSE_ID);
}
}
}
$plugin = $RCMAIL->plugins->exec_hook('attachment_from_uri', array(
'attachment' => $attachment, 'uri' => $uri, 'compose_id' => $COMPOSE_ID));
if ($plugin['attachment']) {
rcmail_attachment_success($plugin['attachment'], $uploadid);
}
else {
$OUTPUT->command('display_message', $RCMAIL->gettext('filelinkerror'), 'error');
$OUTPUT->command('remove_from_attachment_list', $uploadid);
}
$OUTPUT->send();
return;
}
// handle file(s) upload
if (is_array($_FILES['_attachments']['tmp_name'])) {
$multiple = count($_FILES['_attachments']['tmp_name']) > 1;
$errors = array();
foreach ($_FILES['_attachments']['tmp_name'] as $i => $filepath) {
// Process uploaded attachment if there is no error
$err = $_FILES['_attachments']['error'][$i];
if (!$err) {
$filename = $_FILES['_attachments']['name'][$i];
$filesize = $_FILES['_attachments']['size'][$i];
$filetype = rcube_mime::file_content_type($filepath, $filename, $_FILES['_attachments']['type'][$i]);
if ($err = rcmail_check_message_size($filesize, $filetype)) {
if (!in_array($err, $errors)) {
$OUTPUT->command('display_message', $err, 'error');
$OUTPUT->command('remove_from_attachment_list', $uploadid);
$errors[] = $err;
}
continue;
}
$attachment = $RCMAIL->plugins->exec_hook('attachment_upload', array(
'path' => $filepath,
'name' => $filename,
'size' => $filesize,
'mimetype' => $filetype,
'group' => $COMPOSE_ID,
));
}
if (!$err && $attachment['status'] && !$attachment['abort']) {
// store new attachment in session
unset($attachment['status'], $attachment['abort']);
$RCMAIL->session->append($SESSION_KEY . '.attachments', $attachment['id'], $attachment);
rcmail_attachment_success($attachment, $uploadid);
}
else { // upload failed
if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
$size = $RCMAIL->show_bytes(rcube_utils::max_upload_size());
$msg = $RCMAIL->gettext(array('name' => 'filesizeerror', 'vars' => array('size' => $size)));
}
else if ($attachment['error']) {
$msg = $attachment['error'];
}
else {
$msg = $RCMAIL->gettext('fileuploaderror');
}
if ($attachment['error'] || $err != UPLOAD_ERR_NO_FILE) {
if (!in_array($msg, $errors)) {
$OUTPUT->command('display_message', $msg, 'error');
$OUTPUT->command('remove_from_attachment_list', $uploadid);
$errors[] = $msg;
}
}
}
}
}
else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// if filesize exceeds post_max_size then $_FILES array is empty,
// show filesizeerror instead of fileuploaderror
if ($maxsize = ini_get('post_max_size')) {
$msg = $RCMAIL->gettext(array(
'name' => 'filesizeerror',
'vars' => array('size' => $RCMAIL->show_bytes(parse_bytes($maxsize)))
));
}
else {
$msg = $RCMAIL->gettext('fileuploaderror');
}
$OUTPUT->command('display_message', $msg, 'error');
$OUTPUT->command('remove_from_attachment_list', $uploadid);
}
// send html page with JS calls as response
$OUTPUT->command('auto_save_start', false);
$OUTPUT->send('iframe');
function rcmail_attachment_success($attachment, $uploadid)
{
global $RCMAIL, $COMPOSE;
$id = $attachment['id'];
if (($icon = $COMPOSE['deleteicon']) && is_file($icon)) {
$button = html::img(array(
'src' => $icon,
'alt' => $RCMAIL->gettext('delete')
));
}
else if ($COMPOSE['textbuttons']) {
$button = rcube::Q($RCMAIL->gettext('delete'));
}
else {
$button = '';
}
$link_content = sprintf('<span class="attachment-name">%s</span><span class="attachment-size">(%s)</span>',
rcube::Q($attachment['name']), $RCMAIL->show_bytes($attachment['size']));
$content_link = html::a(array(
'href' => "#load",
'class' => 'filename',
'onclick' => sprintf("return %s.command('load-attachment','rcmfile%s', this, event)", rcmail_output::JS_OBJECT_NAME, $id),
), $link_content);
$delete_link = html::a(array(
'href' => "#delete",
'onclick' => sprintf("return %s.command('remove-attachment','rcmfile%s', this, event)", rcmail_output::JS_OBJECT_NAME, $id),
'title' => $RCMAIL->gettext('delete'),
'class' => 'delete',
'aria-label' => $RCMAIL->gettext('delete') . ' ' . $attachment['name'],
), $button);
$content = $COMPOSE['icon_pos'] == 'left' ? $delete_link.$content_link : $content_link.$delete_link;
$RCMAIL->output->command('add2attachment_list', "rcmfile$id", array(
'html' => $content,
'name' => $attachment['name'],
'mimetype' => $attachment['mimetype'],
'classname' => rcube_utils::file2class($attachment['mimetype'], $attachment['name']),
'complete' => true), $uploadid);
}
/**
* Checks if the attached file will fit in message size limit.
* Calculates size of all attachments and compares with the limit.
*
* @param int $filesize File size
* @param string $filetype File mimetype
*
* @return string Error message if the limit is exceeded
*/
function rcmail_check_message_size($filesize, $filetype)
{
global $RCMAIL, $COMPOSE;
$limit = parse_bytes($RCMAIL->config->get('max_message_size'));
$size = 10 * 1024; // size of message body
if (!$limit) {
return;
}
// add size of already attached files
foreach ((array) $COMPOSE['attachments'] as $att) {
// All attachments are base64-encoded except message/rfc822 (see sendmail.inc)
$multip = $att['mimetype'] == 'message/rfc822' ? 1 : 1.33;
$size += $att['size'] * $multip;
}
// add size of the new attachment
$multip = $filetype == 'message/rfc822' ? 1 : 1.33;
$size += $filesize * $multip;
if ($size > $limit) {
$limit = $RCMAIL->show_bytes($limit);
return $RCMAIL->gettext(array('name' => 'msgsizeerror', 'vars' => array('size' => $limit)));
}
}
diff --git a/program/steps/mail/autocomplete.inc b/program/steps/mail/autocomplete.inc
index 380b3b21d..0a0b47cc3 100644
--- a/program/steps/mail/autocomplete.inc
+++ b/program/steps/mail/autocomplete.inc
@@ -1,198 +1,197 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/autocomplete.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2013, Roundcube Dev Team |
- | Copyright (C) 2011-2013, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Perform a search on configured address books for the address |
| autocompletion of the message compose screen |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
if ($RCMAIL->action == 'group-expand') {
$abook = $RCMAIL->get_address_book(rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC));
if ($gid = rcube_utils::get_input_value('_gid', rcube_utils::INPUT_GET)) {
$abook->set_group($gid);
$abook->set_pagesize(9999); // TODO: limit number of group members by config?
$result = $abook->list_records($RCMAIL->config->get('contactlist_fields'));
$members = array();
while ($result && ($record = $result->iterate())) {
$emails = (array) $abook->get_col_values('email', $record, true);
if (!empty($emails) && ($email = array_shift($emails))) {
$members[] = format_email_recipient($email, rcube_addressbook::compose_list_name($record));
}
}
$OUTPUT->command('replace_group_recipients', $gid, join(', ', array_unique($members)));
}
$OUTPUT->send();
}
$MAXNUM = (int) $RCMAIL->config->get('autocomplete_max', 15);
$mode = (int) $RCMAIL->config->get('addressbook_search_mode');
$single = (bool) $RCMAIL->config->get('autocomplete_single');
$search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC, true);
$source = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
$reqid = rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC);
if (strlen($source)) {
$book_types = array($source);
}
else {
$book_types = (array) $RCMAIL->config->get('autocomplete_addressbooks', 'sql');
}
$contacts = array();
if (!empty($book_types) && strlen($search)) {
$sort_keys = array();
$books_num = count($book_types);
$search_lc = mb_strtolower($search);
$mode |= rcube_addressbook::SEARCH_GROUPS;
foreach ($book_types as $abook_id) {
$abook = $RCMAIL->get_address_book($abook_id);
$abook->set_pagesize($MAXNUM);
if ($result = $abook->search($RCMAIL->config->get('contactlist_fields'), $search, $mode, true, true, 'email')) {
while ($record = $result->iterate()) {
// Contact can have more than one e-mail address
$email_arr = (array)$abook->get_col_values('email', $record, true);
$email_cnt = count($email_arr);
$idx = 0;
foreach ($email_arr as $email) {
if (empty($email)) {
continue;
}
$name = rcube_addressbook::compose_list_name($record);
$contact = format_email_recipient($email, $name);
// skip entries that don't match
if ($email_cnt > 1 && strpos(mb_strtolower($contact), $search_lc) === false) {
continue;
}
$index = $contact;
// skip duplicates
if (empty($contacts[$index])) {
$contact = array(
'name' => $contact,
'type' => $record['_type'],
'id' => $record['ID'],
'source' => $abook_id,
);
if (($display = rcube_addressbook::compose_search_name($record, $email, $name)) && $display != $contact['name']) {
$contact['display'] = $display;
}
// groups with defined email address will not be expanded to its members' addresses
if ($record['_type'] == 'group') {
$contact['email'] = $email;
}
$contacts[$index] = $contact;
$sort_keys[$index] = sprintf('%s %03d', $contact['display'] ?: $name, $idx++);
if (count($contacts) >= $MAXNUM) {
break 2;
}
}
// skip redundant entries (show only first email address)
if ($single) {
break;
}
}
}
}
// also list matching contact groups
if ($abook->groups && count($contacts) < $MAXNUM) {
foreach ($abook->list_groups($search, $mode) as $group) {
$abook->reset();
$abook->set_group($group['ID']);
$group_prop = $abook->get_group($group['ID']);
// group (distribution list) with email address(es)
if ($group_prop['email']) {
$idx = 0;
foreach ((array)$group_prop['email'] as $email) {
$index = format_email_recipient($email, $group['name']);
if (empty($contacts[$index])) {
$sort_keys[$index] = sprintf('%s %03d', $group['name'] , $idx++);
$contacts[$index] = array(
'name' => $index,
'email' => $email,
'type' => 'group',
'id' => $group['ID'],
'source' => $abook_id,
);
if (count($contacts) >= $MAXNUM) {
break 2;
}
}
}
}
// show group with count
else if (($result = $abook->count()) && $result->count) {
if (empty($contacts[$group['name']])) {
$sort_keys[$group['name']] = $group['name'];
$contacts[$group['name']] = array(
'name' => $group['name'] . ' (' . intval($result->count) . ')',
'type' => 'group',
'id' => $group['ID'],
'source' => $abook_id,
);
if (count($contacts) >= $MAXNUM) {
break;
}
}
}
}
}
}
if (count($contacts)) {
// sort contacts index
asort($sort_keys, SORT_LOCALE_STRING);
// re-sort contacts according to index
foreach ($sort_keys as $idx => $val) {
$sort_keys[$idx] = $contacts[$idx];
}
$contacts = array_values($sort_keys);
}
}
// Allow autocomplete result optimization via plugin
$pluginResult = $RCMAIL->plugins->exec_hook('contacts_autocomplete_after', array(
'search' => $search,
'contacts' => $contacts, // Provide already-found contacts to plugin if they are required
));
$contacts = $pluginResult['contacts'];
$OUTPUT->command('ksearch_query_results', $contacts, $search, $reqid);
$OUTPUT->send();
diff --git a/program/steps/mail/bounce.inc b/program/steps/mail/bounce.inc
index bfdb6a5b2..fbe814449 100644
--- a/program/steps/mail/bounce.inc
+++ b/program/steps/mail/bounce.inc
@@ -1,90 +1,89 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/bounce.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2017, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Bounce/resend an email message |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
$msg_uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GP);
$msg_folder = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GP, true);
$MESSAGE = new rcube_message($msg_uid, $msg_folder);
if (!$MESSAGE->headers) {
$OUTPUT->show_message('messageopenerror', 'error');
$OUTPUT->send('iframe');
}
// Display Bounce form
if (empty($_POST)) {
if (!empty($MESSAGE->headers->charset)) {
$RCMAIL->storage->set_charset($MESSAGE->headers->charset);
}
// Initialize helper class to build the UI
$SENDMAIL = new rcmail_sendmail(
array('mode' => rcmail_sendmail::MODE_FORWARD),
array('message' => $MESSAGE)
);
$OUTPUT->set_env('mailbox', $msg_folder);
$OUTPUT->set_env('uid', $msg_uid);
$OUTPUT->send('bounce');
}
// Initialize helper class to send the message
$SENDMAIL = new rcmail_sendmail(array('mode' => rcmail_sendmail::MODE_FORWARD), array(
'sendmail' => true,
'error_handler' => function() use ($OUTPUT) {
call_user_func_array(array($OUTPUT, 'show_message'), func_get_args());
$OUTPUT->send('iframe');
}
));
// Handle the form input
$input_headers = $SENDMAIL->headers_input();
// Set Resent-* headers, these will be added on top of the bounced message
$headers = array_filter(array(
// 'Received' => $input_headers['Received'],
'Resent-From' => $input_headers['From'],
'Resent-To' => $input_headers['To'],
'Resent-Cc' => $input_headers['Cc'],
'Resent-Bcc' => $input_headers['Bcc'],
'Resent-Date' => $input_headers['Date'],
'Resent-Message-ID' => $input_headers['Message-ID'],
));
// Create the bounce message
$BOUNCE = new rcmail_resend_mail(array(
'bounce_message' => $MESSAGE,
'bounce_headers' => $headers,
));
// Send the bounce message
$SENDMAIL->deliver_message($BOUNCE);
// Save in Sent (if requested)
$saved = $SENDMAIL->save_message($BOUNCE);
if (!$saved && strlen($SENDMAIL->options['store_target'])) {
$RCMAIL->display_server_error('errorsaving');
}
$OUTPUT->show_message('messagesent', 'confirmation', null, false);
$OUTPUT->send('iframe');
diff --git a/program/steps/mail/check_recent.inc b/program/steps/mail/check_recent.inc
index dd6e488a4..2022cc3ae 100644
--- a/program/steps/mail/check_recent.inc
+++ b/program/steps/mail/check_recent.inc
@@ -1,162 +1,160 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/check_recent.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2014, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Check for recent messages, in all mailboxes |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
// If there's no folder or messages list, there's nothing to update
// This can happen on 'refresh' request
if (empty($_POST['_folderlist']) && empty($_POST['_list'])) {
return;
}
$trash = $RCMAIL->config->get('trash_mbox');
$current = $RCMAIL->storage->get_folder();
$check_all = $RCMAIL->action != 'refresh' || (bool)$RCMAIL->config->get('check_all_folders');
$page = $RCMAIL->storage->get_page();
$page_size = $RCMAIL->storage->get_pagesize();
$search_request = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC);
if ($search_request && $_SESSION['search_request'] != $search_request) {
$search_request = null;
}
// list of folders to check
if ($check_all) {
$a_mailboxes = $RCMAIL->storage->list_folders_subscribed('', '*', 'mail');
}
else if ($search_request && is_object($_SESSION['search'][1])) {
$a_mailboxes = (array) $_SESSION['search'][1]->get_parameters('MAILBOX');
}
else {
$a_mailboxes = (array) $current;
if ($current != 'INBOX') {
$a_mailboxes[] = 'INBOX';
}
}
// Control folders list from a plugin
$plugin = $RCMAIL->plugins->exec_hook('check_recent', array('folders' => $a_mailboxes, 'all' => $check_all));
$a_mailboxes = $plugin['folders'];
$RCMAIL->storage_fatal_error();
// check recent/unseen counts
foreach ($a_mailboxes as $mbox_name) {
$is_current = $mbox_name == $current || ($search_request && is_object($_SESSION['search'][1]) && in_array($mbox_name, (array)$_SESSION['search'][1]->get_parameters('MAILBOX')));
if ($is_current) {
// Synchronize mailbox cache, handle flag changes
$RCMAIL->storage->folder_sync($mbox_name);
}
// Get mailbox status
$status = $RCMAIL->storage->folder_status($mbox_name, $diff);
if ($is_current) {
$RCMAIL->storage_fatal_error();
}
if ($status & 1) {
// trigger plugin hook
$RCMAIL->plugins->exec_hook('new_messages',
array('mailbox' => $mbox_name, 'is_current' => $is_current, 'diff' => $diff));
}
rcmail_send_unread_count($mbox_name, true, null, (!$is_current && ($status & 1)) ? 'recent' : '');
if ($status && $is_current) {
// refresh saved search set
if ($search_request && isset($_SESSION['search'])) {
unset($search_request); // only do this once
$_SESSION['search'] = $RCMAIL->storage->refresh_search();
if ($_SESSION['search'][1]->multi) {
$mbox_name = '';
}
}
if (!empty($_POST['_quota'])) {
$OUTPUT->command('set_quota', $RCMAIL->quota_content(null, $mbox_name));
}
$OUTPUT->set_env('exists', $RCMAIL->storage->count($mbox_name, 'EXISTS', true));
// "No-list" mode, don't get messages
if (empty($_POST['_list'])) {
continue;
}
// get overall message count; allow caching because rcube_storage::folder_status()
// did a refresh but only in list mode
$list_mode = $RCMAIL->storage->get_threading() ? 'THREADS' : 'ALL';
$all_count = $RCMAIL->storage->count($mbox_name, $list_mode, $list_mode == 'THREADS', false);
// check current page if we're not on the first page
if ($all_count && $page > 1) {
$remaining = $all_count - $page_size * ($page - 1);
if ($remaining <= 0) {
$page -= 1;
$RCMAIL->storage->set_page($page);
$_SESSION['page'] = $page;
}
}
$OUTPUT->set_env('messagecount', $all_count);
$OUTPUT->set_env('pagecount', ceil($all_count/$page_size));
$OUTPUT->command('set_rowcount', rcmail_get_messagecount_text($all_count), $mbox_name);
$OUTPUT->set_env('current_page', $all_count ? $page : 1);
// remove old rows (and clear selection if new list is empty)
$OUTPUT->command('message_list.clear', $all_count ? false : true);
if ($all_count) {
$a_headers = $RCMAIL->storage->list_messages($mbox_name, null, rcmail_sort_column(), rcmail_sort_order());
// add message rows
rcmail_js_message_list($a_headers, false);
// remove messages that don't exists from list selection array
$OUTPUT->command('update_selection');
}
}
// handle flag updates
else if ($is_current && ($uids = rcube_utils::get_input_value('_uids', rcube_utils::INPUT_GPC)) && empty($search_request)) {
$data = $RCMAIL->storage->folder_data($mbox_name);
if (empty($_SESSION['list_mod_seq']) || $_SESSION['list_mod_seq'] != $data['HIGHESTMODSEQ']) {
$flags = $RCMAIL->storage->list_flags($mbox_name, explode(',', $uids), $_SESSION['list_mod_seq']);
foreach ($flags as $idx => $row) {
$flags[$idx] = array_change_key_case(array_map('intval', $row));
}
// remember last HIGHESTMODSEQ value (if supported)
if (!empty($data['HIGHESTMODSEQ'])) {
$_SESSION['list_mod_seq'] = $data['HIGHESTMODSEQ'];
}
$RCMAIL->output->set_env('recent_flags', $flags);
}
}
// set trash folder state
if ($mbox_name === $trash) {
$OUTPUT->command('set_trash_count', $RCMAIL->storage->count($mbox_name, 'EXISTS', true));
}
}
// trigger refresh hook
$RCMAIL->plugins->exec_hook('refresh', array());
$OUTPUT->send();
diff --git a/program/steps/mail/compose.inc b/program/steps/mail/compose.inc
index 76e4486af..0b0a7b2d6 100644
--- a/program/steps/mail/compose.inc
+++ b/program/steps/mail/compose.inc
@@ -1,1414 +1,1412 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/compose.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2017, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Compose a new mail message with all headers and attachments |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
$COMPOSE_ID = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GET);
$COMPOSE = null;
if ($COMPOSE_ID && $_SESSION['compose_data_'.$COMPOSE_ID]) {
$COMPOSE =& $_SESSION['compose_data_'.$COMPOSE_ID];
}
// give replicated session storage some time to synchronize
$retries = 0;
while ($COMPOSE_ID && !is_array($COMPOSE) && $RCMAIL->db->is_replicated() && $retries++ < 5) {
usleep(500000);
$RCMAIL->session->reload();
if ($_SESSION['compose_data_'.$COMPOSE_ID]) {
$COMPOSE =& $_SESSION['compose_data_'.$COMPOSE_ID];
}
}
// Nothing below is called during message composition, only at "new/forward/reply/draft" initialization or
// if a compose-ID is given (i.e. when the compose step is opened in a new window/tab).
if (!is_array($COMPOSE)) {
// Infinite redirect prevention in case of broken session (#1487028)
if ($COMPOSE_ID) {
// if we know the message with specified ID was already sent
// we can ignore the error and compose a new message (#1490009)
if ($COMPOSE_ID != $_SESSION['last_compose_session']) {
rcube::raise_error(array('code' => 450), false, true);
}
}
$COMPOSE_ID = uniqid(mt_rand());
$params = rcube_utils::request2param(rcube_utils::INPUT_GET, 'task|action', true);
$_SESSION['compose_data_'.$COMPOSE_ID] = array(
'id' => $COMPOSE_ID,
'param' => $params,
'mailbox' => $params['mbox'] ?: $RCMAIL->storage->get_folder(),
);
$COMPOSE =& $_SESSION['compose_data_'.$COMPOSE_ID];
rcmail_process_compose_params($COMPOSE);
// check if folder for saving sent messages exists and is subscribed (#1486802)
if ($sent_folder = $COMPOSE['param']['sent_mbox']) {
rcmail_sendmail::check_sent_folder($sent_folder, true);
}
// redirect to a unique URL with all parameters stored in session
$OUTPUT->redirect(array(
'_action' => 'compose',
'_id' => $COMPOSE['id'],
'_search' => $_REQUEST['_search'],
));
}
// add some labels to client
$OUTPUT->add_label('notuploadedwarning', 'savingmessage', 'siginserted', 'responseinserted',
'messagesaved', 'converting', 'editorwarning', 'discard',
'fileuploaderror', 'sendmessage', 'newresponse', 'responsename', 'responsetext', 'save',
'savingresponse', 'restoresavedcomposedata', 'restoremessage', 'delete', 'restore', 'ignore',
'selectimportfile', 'messageissent', 'loadingdata', 'nopubkeyfor', 'nopubkeyforsender',
'encryptnoattachments','encryptedsendialog','searchpubkeyservers', 'importpubkeys',
'encryptpubkeysfound', 'search', 'close', 'import', 'keyid', 'keylength', 'keyexpired',
'keyrevoked', 'keyimportsuccess', 'keyservererror', 'attaching', 'namex', 'attachmentrename'
);
$OUTPUT->set_pagetitle($RCMAIL->gettext('compose'));
$OUTPUT->set_env('compose_id', $COMPOSE['id']);
$OUTPUT->set_env('session_id', session_id());
$OUTPUT->set_env('mailbox', $RCMAIL->storage->get_folder());
$OUTPUT->set_env('top_posting', intval($RCMAIL->config->get('reply_mode')) > 0);
$OUTPUT->set_env('sig_below', $RCMAIL->config->get('sig_below'));
$OUTPUT->set_env('save_localstorage', (bool)$RCMAIL->config->get('compose_save_localstorage'));
$OUTPUT->set_env('is_sent', false);
$OUTPUT->set_env('mimetypes', rcmail_supported_mimetypes());
$drafts_mbox = $RCMAIL->config->get('drafts_mbox');
$config_show_sig = $RCMAIL->config->get('show_sig', 1);
// add config parameters to client script
if (strlen($drafts_mbox)) {
$OUTPUT->set_env('drafts_mailbox', $drafts_mbox);
$OUTPUT->set_env('draft_autosave', $RCMAIL->config->get('draft_autosave'));
}
// default font for HTML editor
$font = rcmail::font_defs($RCMAIL->config->get('default_font'));
if ($font && !is_array($font)) {
$OUTPUT->set_env('default_font', $font);
}
// default font size for HTML editor
if ($font_size = $RCMAIL->config->get('default_font_size')) {
$OUTPUT->set_env('default_font_size', $font_size);
}
// get reference message and set compose mode
if ($msg_uid = $COMPOSE['param']['draft_uid']) {
$compose_mode = rcmail_sendmail::MODE_DRAFT;
$OUTPUT->set_env('draft_id', $msg_uid);
$RCMAIL->storage->set_folder($drafts_mbox);
}
else if ($msg_uid = $COMPOSE['param']['reply_uid']) {
$compose_mode = rcmail_sendmail::MODE_REPLY;
}
else if ($msg_uid = $COMPOSE['param']['forward_uid']) {
$compose_mode = rcmail_sendmail::MODE_FORWARD;
$COMPOSE['forward_uid'] = $msg_uid;
$COMPOSE['as_attachment'] = !empty($COMPOSE['param']['attachment']);
}
else if ($msg_uid = $COMPOSE['param']['uid']) {
$compose_mode = rcmail_sendmail::MODE_EDIT;
}
if ($compose_mode) {
$COMPOSE['mode'] = $compose_mode;
$OUTPUT->set_env('compose_mode', $compose_mode);
}
if ($compose_mode == rcmail_sendmail::MODE_EDIT || $compose_mode == rcmail_sendmail::MODE_DRAFT) {
// don't add signature in draft/edit mode, we'll also not remove the old-one
// but only on page display, later we should be able to change identity/sig (#1489229)
if ($config_show_sig == 1 || $config_show_sig == 2) {
$OUTPUT->set_env('show_sig_later', true);
}
}
else if ($config_show_sig == 1)
$OUTPUT->set_env('show_sig', true);
else if ($config_show_sig == 2 && empty($compose_mode))
$OUTPUT->set_env('show_sig', true);
else if ($config_show_sig == 3 && ($compose_mode == rcmail_sendmail::MODE_REPLY || $compose_mode == rcmail_sendmail::MODE_FORWARD))
$OUTPUT->set_env('show_sig', true);
// set line length for body wrapping
$LINE_LENGTH = $RCMAIL->config->get('line_length', 72);
if (!empty($msg_uid) && empty($COMPOSE['as_attachment'])) {
$mbox_name = $RCMAIL->storage->get_folder();
// set format before rcube_message construction
// use the same format as for the message view
if (isset($_SESSION['msg_formats'][$mbox_name.':'.$msg_uid])) {
$RCMAIL->config->set('prefer_html', $_SESSION['msg_formats'][$mbox_name.':'.$msg_uid]);
}
else {
$prefer_html = $RCMAIL->config->get('prefer_html') || $RCMAIL->config->get('htmleditor')
|| $compose_mode == rcmail_sendmail::MODE_DRAFT || $compose_mode == rcmail_sendmail::MODE_EDIT;
$RCMAIL->config->set('prefer_html', $prefer_html);
}
$MESSAGE = new rcube_message($msg_uid);
// make sure message is marked as read
if ($MESSAGE->headers && $MESSAGE->context === null && empty($MESSAGE->headers->flags['SEEN'])) {
$RCMAIL->storage->set_flag($msg_uid, 'SEEN');
}
if (!empty($MESSAGE->headers->charset)) {
$RCMAIL->storage->set_charset($MESSAGE->headers->charset);
}
if (!$MESSAGE->headers) {
// error
}
else if ($compose_mode == rcmail_sendmail::MODE_FORWARD || $compose_mode == rcmail_sendmail::MODE_REPLY) {
if ($compose_mode == rcmail_sendmail::MODE_REPLY) {
$COMPOSE['reply_uid'] = $MESSAGE->context === null ? $msg_uid : null;
if (!empty($COMPOSE['param']['all'])) {
$MESSAGE->reply_all = $COMPOSE['param']['all'];
}
}
else {
$COMPOSE['forward_uid'] = $msg_uid;
}
$COMPOSE['reply_msgid'] = $MESSAGE->headers->messageID;
$COMPOSE['references'] = trim($MESSAGE->headers->references . " " . $MESSAGE->headers->messageID);
// Save the sent message in the same folder of the message being replied to
if ($RCMAIL->config->get('reply_same_folder') && ($sent_folder = $COMPOSE['mailbox'])
&& rcmail_sendmail::check_sent_folder($sent_folder, false)
) {
$COMPOSE['param']['sent_mbox'] = $sent_folder;
}
}
else if ($compose_mode == rcmail_sendmail::MODE_DRAFT || $compose_mode == rcmail_sendmail::MODE_EDIT) {
if ($compose_mode == rcmail_sendmail::MODE_DRAFT) {
if ($draft_info = $MESSAGE->headers->get('x-draft-info')) {
// get reply_uid/forward_uid to flag the original message when sending
$info = rcmail_sendmail::draftinfo_decode($draft_info);
if ($info['type'] == 'reply')
$COMPOSE['reply_uid'] = $info['uid'];
else if ($info['type'] == 'forward')
$COMPOSE['forward_uid'] = $info['uid'];
$COMPOSE['mailbox'] = $info['folder'];
// Save the sent message in the same folder of the message being replied to
if ($RCMAIL->config->get('reply_same_folder') && ($sent_folder = $info['folder'])
&& rcmail_sendmail::check_sent_folder($sent_folder, false)
) {
$COMPOSE['param']['sent_mbox'] = $sent_folder;
}
}
if (($msgid = $MESSAGE->headers->get('message-id')) && !preg_match('/^mid:[0-9]+$/', $msgid)) {
$COMPOSE['param']['message-id'] = $msgid;
}
// use message UID as draft_id
$OUTPUT->set_env('draft_id', $msg_uid);
}
if ($in_reply_to = $MESSAGE->headers->get('in-reply-to')) {
$COMPOSE['reply_msgid'] = '<' . $in_reply_to . '>';
}
$COMPOSE['references'] = $MESSAGE->headers->references;
}
}
else {
$MESSAGE = new stdClass();
// apply mailto: URL parameters
if (!empty($COMPOSE['param']['in-reply-to'])) {
$COMPOSE['reply_msgid'] = '<' . $COMPOSE['param']['in-reply-to'] . '>';
}
if (!empty($COMPOSE['param']['references'])) {
$COMPOSE['references'] = $COMPOSE['param']['references'];
}
}
if (!empty($COMPOSE['reply_msgid'])) {
$OUTPUT->set_env('reply_msgid', $COMPOSE['reply_msgid']);
}
// Initialize helper class to build the UI
$SENDMAIL = new rcmail_sendmail($COMPOSE, array('message' => $MESSAGE));
// process $MESSAGE body/attachments, set $MESSAGE_BODY/$HTML_MODE vars and some session data
$MESSAGE_BODY = rcmail_prepare_message_body();
// register UI objects (Note: some objects are registered by rcmail_sendmail above)
$OUTPUT->add_handlers(array(
'composebody' => 'rcmail_compose_body',
'composeattachmentlist' => 'rcmail_compose_attachment_list',
'composeattachmentform' => 'rcmail_compose_attachment_form',
'composeattachment' => 'rcmail_compose_attachment_field',
'filedroparea' => 'rcmail_compose_file_drop_area',
'editorselector' => 'rcmail_editor_selector',
'addressbooks' => 'rcmail_addressbook_list',
'addresslist' => 'rcmail_contacts_list',
'responseslist' => 'rcmail_compose_responses_list',
));
$OUTPUT->include_script('publickey.js');
rcmail_spellchecker_init();
$OUTPUT->send('compose');
/****** compose mode functions ********/
// process compose request parameters
function rcmail_process_compose_params(&$COMPOSE)
{
if ($COMPOSE['param']['to']) {
$mailto = explode('?', $COMPOSE['param']['to'], 2);
// #1486037: remove "mailto:" prefix
$COMPOSE['param']['to'] = preg_replace('/^mailto:/i', '', $mailto[0]);
// #1490346: decode the recipient address
// #1490510: use raw encoding for correct "+" character handling as specified in RFC6068
$COMPOSE['param']['to'] = rawurldecode($COMPOSE['param']['to']);
// Supported case-insensitive tokens in mailto URL
$url_tokens = array('to', 'cc', 'bcc', 'reply-to', 'in-reply-to', 'references', 'subject', 'body');
if (!empty($mailto[1])) {
parse_str($mailto[1], $query);
foreach ($query as $f => $val) {
if (($key = array_search(strtolower($f), $url_tokens)) !== false) {
$f = $url_tokens[$key];
}
// merge mailto: addresses with addresses from 'to' parameter
if ($f == 'to' && !empty($COMPOSE['param']['to'])) {
$to_addresses = rcube_mime::decode_address_list($COMPOSE['param']['to'], null, true, null, true);
$add_addresses = rcube_mime::decode_address_list($val, null, true);
foreach ($add_addresses as $addr) {
if (!in_array($addr['mailto'], $to_addresses)) {
$to_addresses[] = $addr['mailto'];
$COMPOSE['param']['to'] = (!empty($to_addresses) ? ', ' : '') . $addr['string'];
}
}
}
else {
$COMPOSE['param'][$f] = $val;
}
}
}
}
// resolve _forward_uid=* to an absolute list of messages from a search result
if ($COMPOSE['param']['forward_uid'] == '*' && is_object($_SESSION['search'][1])) {
$COMPOSE['param']['forward_uid'] = $_SESSION['search'][1]->get();
}
// clean HTML message body which can be submitted by URL
if (!empty($COMPOSE['param']['body'])) {
if ($COMPOSE['param']['html'] = strpos($COMPOSE['param']['body'], '<') !== false) {
$wash_params = array('safe' => false, 'inline_html' => true);
$COMPOSE['param']['body'] = rcmail_wash_html($COMPOSE['param']['body'], $wash_params, array());
$COMPOSE['param']['body'] = preg_replace('/<!--[^>\n]+>/', '', $COMPOSE['param']['body']);
$COMPOSE['param']['body'] = preg_replace('/<\/?body>/', '', $COMPOSE['param']['body']);
}
}
$RCMAIL = rcmail::get_instance();
// select folder where to save the sent message
$COMPOSE['param']['sent_mbox'] = $RCMAIL->config->get('sent_mbox');
// pipe compose parameters thru plugins
$plugin = $RCMAIL->plugins->exec_hook('message_compose', $COMPOSE);
$COMPOSE['param'] = array_merge($COMPOSE['param'], $plugin['param']);
// add attachments listed by message_compose hook
if (is_array($plugin['attachments'])) {
foreach ($plugin['attachments'] as $attach) {
// we have structured data
if (is_array($attach)) {
$attachment = $attach + array('group' => $COMPOSE_ID);
}
// only a file path is given
else {
$filename = basename($attach);
$attachment = array(
'group' => $COMPOSE_ID,
'name' => $filename,
'mimetype' => rcube_mime::file_content_type($attach, $filename),
'size' => filesize($attach),
'path' => $attach,
);
}
// save attachment if valid
if (($attachment['data'] && $attachment['name']) || ($attachment['path'] && file_exists($attachment['path']))) {
$attachment = rcmail::get_instance()->plugins->exec_hook('attachment_save', $attachment);
}
if ($attachment['status'] && !$attachment['abort']) {
unset($attachment['data'], $attachment['status'], $attachment['abort']);
$COMPOSE['attachments'][$attachment['id']] = $attachment;
}
}
}
}
function rcmail_compose_editor_mode()
{
global $RCMAIL, $COMPOSE;
static $useHtml;
if ($useHtml !== null) {
return $useHtml;
}
$html_editor = intval($RCMAIL->config->get('htmleditor'));
$compose_mode = $COMPOSE['mode'];
if (is_bool($COMPOSE['param']['html'])) {
$useHtml = $COMPOSE['param']['html'];
}
else if (isset($_POST['_is_html'])) {
$useHtml = !empty($_POST['_is_html']);
}
else if ($compose_mode == rcmail_sendmail::MODE_DRAFT || $compose_mode == rcmail_sendmail::MODE_EDIT) {
$useHtml = rcmail_message_is_html();
}
else if ($compose_mode == rcmail_sendmail::MODE_REPLY) {
$useHtml = $html_editor == 1 || ($html_editor >= 2 && rcmail_message_is_html());
}
else if ($compose_mode == rcmail_sendmail::MODE_FORWARD) {
$useHtml = $html_editor == 1 || $html_editor == 4
|| ($html_editor == 3 && rcmail_message_is_html());
}
else {
$useHtml = $html_editor == 1 || $html_editor == 4;
}
return $useHtml;
}
function rcmail_message_is_html()
{
global $RCMAIL, $MESSAGE;
return $RCMAIL->config->get('prefer_html') && ($MESSAGE instanceof rcube_message) && $MESSAGE->has_html_part(true);
}
function rcmail_spellchecker_init()
{
global $RCMAIL, $OUTPUT;
// Set language list
if ($RCMAIL->config->get('enable_spellcheck')) {
$spellchecker = new rcube_spellchecker();
$spellcheck_langs = $spellchecker->languages();
}
if (!empty($spellchecker) && empty($spellcheck_langs)) {
if ($err = $spellchecker->error()) {
rcube::raise_error(array('code' => 500,
'file' => __FILE__, 'line' => __LINE__,
'message' => "Spell check engine error: " . trim($err)),
true, false);
}
}
else if (!empty($spellchecker)) {
$dictionary = (bool) $RCMAIL->config->get('spellcheck_dictionary');
$lang = $_SESSION['language'];
// if not found in the list, try with two-letter code
if (!$spellcheck_langs[$lang]) {
$lang = strtolower(substr($lang, 0, 2));
}
if (!$spellcheck_langs[$lang]) {
$lang = 'en';
}
$editor_lang_set = array();
foreach ($spellcheck_langs as $key => $name) {
$editor_lang_set[] = ($key == $lang ? '+' : '') . rcube::JQ($name).'='.rcube::JQ($key);
}
// include GoogieSpell
$OUTPUT->include_script('googiespell.js');
$OUTPUT->add_script(sprintf(
"var googie = new GoogieSpell('%s/images/googiespell/','%s&lang=', %s);\n".
"googie.lang_chck_spell = \"%s\";\n".
"googie.lang_rsm_edt = \"%s\";\n".
"googie.lang_close = \"%s\";\n".
"googie.lang_revert = \"%s\";\n".
"googie.lang_no_error_found = \"%s\";\n".
"googie.lang_learn_word = \"%s\";\n".
"googie.setLanguages(%s);\n".
"googie.setCurrentLanguage('%s');\n".
"googie.setDecoration(false);\n".
"googie.decorateTextarea(rcmail.env.composebody);\n",
$RCMAIL->output->asset_url($RCMAIL->output->get_skin_path()),
$RCMAIL->url(array('_task' => 'utils', '_action' => 'spell', '_remote' => 1)),
!empty($dictionary) ? 'true' : 'false',
rcube::JQ(rcube::Q($RCMAIL->gettext('checkspelling'))),
rcube::JQ(rcube::Q($RCMAIL->gettext('resumeediting'))),
rcube::JQ(rcube::Q($RCMAIL->gettext('close'))),
rcube::JQ(rcube::Q($RCMAIL->gettext('revertto'))),
rcube::JQ(rcube::Q($RCMAIL->gettext('nospellerrors'))),
rcube::JQ(rcube::Q($RCMAIL->gettext('addtodict'))),
rcube_output::json_serialize($spellcheck_langs),
$lang
), 'foot');
$OUTPUT->add_label('checking');
$OUTPUT->set_env('spellcheck_langs', join(',', $editor_lang_set));
$OUTPUT->set_env('spell_langs', $spellcheck_langs);
$OUTPUT->set_env('spell_lang', $lang);
}
}
function rcmail_prepare_message_body()
{
global $RCMAIL, $MESSAGE, $COMPOSE, $HTML_MODE;
// use posted message body
if (!empty($_POST['_message'])) {
$body = rcube_utils::get_input_value('_message', rcube_utils::INPUT_POST, true);
$isHtml = (bool) rcube_utils::get_input_value('_is_html', rcube_utils::INPUT_POST);
}
else if ($COMPOSE['param']['body']) {
$body = $COMPOSE['param']['body'];
$isHtml = (bool) $COMPOSE['param']['html'];
}
// forward as attachment
else if ($COMPOSE['mode'] == rcmail_sendmail::MODE_FORWARD && $COMPOSE['as_attachment']) {
$isHtml = rcmail_compose_editor_mode();
$body = '';
rcmail_write_forward_attachments();
}
// reply/edit/draft/forward
else if ($COMPOSE['mode'] && ($COMPOSE['mode'] != rcmail_sendmail::MODE_REPLY || intval($RCMAIL->config->get('reply_mode')) != -1)) {
$isHtml = rcmail_compose_editor_mode();
$messages = array();
if (!empty($MESSAGE->parts)) {
// collect IDs of message/rfc822 parts
foreach ($MESSAGE->mime_parts() as $part) {
if ($part->mimetype == 'message/rfc822') {
$messages[] = $part->mime_id;
}
}
foreach ($MESSAGE->parts as $part) {
if ($part->realtype == 'multipart/encrypted') {
// find the encrypted message payload part
if ($pgp_mime_part = $MESSAGE->get_multipart_encrypted_part()) {
$RCMAIL->output->set_env('pgp_mime_message', array(
'_mbox' => $RCMAIL->storage->get_folder(),
'_uid' => $MESSAGE->uid,
'_part' => $pgp_mime_part->mime_id,
));
}
continue;
}
// skip no-content and attachment parts (#1488557)
if ($part->type != 'content' || !$part->size || $MESSAGE->is_attachment($part)) {
continue;
}
// skip all content parts inside the message/rfc822 part
foreach ($messages as $mimeid) {
if (strpos($part->mime_id, $mimeid . '.') === 0) {
continue 2;
}
}
if ($part_body = rcmail_compose_part_body($part, $isHtml)) {
$body .= ($body ? ($isHtml ? '<br/>' : "\n") : '') . $part_body;
}
}
}
else {
$body = rcmail_compose_part_body($MESSAGE, $isHtml);
}
// compose reply-body
if ($COMPOSE['mode'] == rcmail_sendmail::MODE_REPLY) {
$body = rcmail_create_reply_body($body, $isHtml);
if ($MESSAGE->pgp_mime) {
$RCMAIL->output->set_env('compose_reply_header', rcmail_get_reply_header($MESSAGE));
}
}
// forward message body inline
else if ($COMPOSE['mode'] == rcmail_sendmail::MODE_FORWARD) {
$body = rcmail_create_forward_body($body, $isHtml);
}
// load draft message body
else if ($COMPOSE['mode'] == rcmail_sendmail::MODE_DRAFT || $COMPOSE['mode'] == rcmail_sendmail::MODE_EDIT) {
$body = rcmail_create_draft_body($body, $isHtml);
}
}
else { // new message
$isHtml = rcmail_compose_editor_mode();
}
$plugin = $RCMAIL->plugins->exec_hook('message_compose_body',
array('body' => $body, 'html' => $isHtml, 'mode' => $COMPOSE['mode']));
$body = $plugin['body'];
unset($plugin);
// add blocked.gif attachment (#1486516)
$regexp = '# src="program/resources/blocked\.gif"#';
if ($isHtml && preg_match($regexp, $body)) {
$content = $RCMAIL->get_resource_content('blocked.gif');
if ($content && ($attachment = rcmail_save_image('blocked.gif', 'image/gif', $content))) {
$COMPOSE['attachments'][$attachment['id']] = $attachment;
$url = sprintf('%s&_id=%s&_action=display-attachment&_file=rcmfile%s',
$RCMAIL->comm_path, $COMPOSE['id'], $attachment['id']);
$body = preg_replace($regexp, ' src="' . $url . '"', $body);
}
}
$HTML_MODE = $isHtml;
return $body;
}
function rcmail_compose_part_body($part, $isHtml = false)
{
global $RCMAIL, $COMPOSE, $MESSAGE, $LINE_LENGTH;
// Check if we have enough memory to handle the message in it
// #1487424: we need up to 10x more memory than the body
if (!rcube_utils::mem_check($part->size * 10)) {
return '';
}
// fetch part if not available
$body = $MESSAGE->get_part_body($part->mime_id, true);
// message is cached but not exists (#1485443), or other error
if ($body === false) {
return '';
}
// register this part as pgp encrypted
if (strpos($body, '-----BEGIN PGP MESSAGE-----') !== false) {
$MESSAGE->pgp_mime = true;
$RCMAIL->output->set_env('pgp_mime_message', array(
'_mbox' => $RCMAIL->storage->get_folder(), '_uid' => $MESSAGE->uid, '_part' => $part->mime_id,
));
}
if ($isHtml) {
if ($part->ctype_secondary == 'html') {
}
else if ($part->ctype_secondary == 'enriched') {
$body = rcube_enriched::to_html($body);
}
else {
// try to remove the signature
if ($COMPOSE['mode'] != rcmail_sendmail::MODE_DRAFT && $COMPOSE['mode'] != rcmail_sendmail::MODE_EDIT) {
if ($RCMAIL->config->get('strip_existing_sig', true)) {
$body = rcmail_remove_signature($body);
}
}
// add HTML formatting
$body = rcmail_plain_body($body, $part->ctype_parameters['format'] == 'flowed', $part->ctype_parameters['delsp'] == 'yes');
}
}
else {
if ($part->ctype_secondary == 'enriched') {
$body = rcube_enriched::to_html($body);
$part->ctype_secondary = 'html';
}
if ($part->ctype_secondary == 'html') {
// use html part if it has been used for message (pre)viewing
// decrease line length for quoting
$len = $COMPOSE['mode'] == rcmail_sendmail::MODE_REPLY ? $LINE_LENGTH-2 : $LINE_LENGTH;
$body = $RCMAIL->html2text($body, array('width' => $len));
}
else {
if ($part->ctype_secondary == 'plain' && $part->ctype_parameters['format'] == 'flowed') {
$body = rcube_mime::unfold_flowed($body, null, $part->ctype_parameters['delsp'] == 'yes');
}
// try to remove the signature
if ($COMPOSE['mode'] != rcmail_sendmail::MODE_DRAFT && $COMPOSE['mode'] != rcmail_sendmail::MODE_EDIT) {
if ($RCMAIL->config->get('strip_existing_sig', true)) {
$body = rcmail_remove_signature($body);
}
}
}
}
return $body;
}
function rcmail_compose_body($attrib)
{
global $RCMAIL, $OUTPUT, $HTML_MODE, $MESSAGE_BODY, $SENDMAIL;
list($form_start, $form_end) = $SENDMAIL->form_tags($attrib);
unset($attrib['form']);
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmComposeBody';
}
// If desired, set this textarea to be editable by TinyMCE
$attrib['data-html-editor'] = true;
if ($HTML_MODE) {
$attrib['class'] = trim($attrib['class'] . ' mce_editor');
}
$attrib['name'] = '_message';
$textarea = new html_textarea($attrib);
$hidden = new html_hiddenfield();
$hidden->add(array('name' => '_draft_saveid', 'value' => $RCMAIL->output->get_env('draft_id')));
$hidden->add(array('name' => '_draft', 'value' => ''));
$hidden->add(array('name' => '_is_html', 'value' => $HTML_MODE ? "1" : "0"));
$hidden->add(array('name' => '_framed', 'value' => '1'));
$OUTPUT->set_env('composebody', $attrib['id']);
// include HTML editor
$RCMAIL->html_editor();
return ($form_start ? "$form_start\n" : '')
. "\n" . $hidden->show() . "\n" . $textarea->show($MESSAGE_BODY)
. ($form_end ? "\n$form_end\n" : '');
}
function rcmail_create_reply_body($body, $bodyIsHtml)
{
global $RCMAIL, $MESSAGE, $LINE_LENGTH;
$reply_mode = (int) $RCMAIL->config->get('reply_mode');
$reply_indent = $reply_mode != 2;
// In top-posting without quoting it's better to use multi-line header
if ($reply_mode == 2) {
$prefix = rcmail_get_forward_header($MESSAGE, $bodyIsHtml, false);
}
else {
$prefix = rcmail_get_reply_header($MESSAGE);
if ($bodyIsHtml) {
$prefix = '<p id="reply-intro">' . rcube::Q($prefix) . '</p>';
}
else {
$prefix .= "\n";
}
}
if (!$bodyIsHtml) {
// soft-wrap and quote message text
$body = rcmail_wrap_and_quote($body, $LINE_LENGTH, $reply_indent);
if ($reply_mode > 0) { // top-posting
$prefix = "\n\n\n" . $prefix;
$suffix = '';
}
else {
$suffix = "\n";
}
}
else {
// save inline images to files
$cid_map = rcmail_write_inline_attachments($MESSAGE);
// set is_safe flag (we need this for html body washing)
rcmail_check_safe($MESSAGE);
// clean up html tags
$body = rcmail_wash_html($body, array('safe' => $MESSAGE->is_safe), $cid_map);
$suffix = '';
if ($reply_indent) {
$prefix .= '<blockquote>';
$suffix .= '</blockquote>';
}
if ($reply_mode == 2) {
// top-posting, no indent
}
else if ($reply_mode > 0) {
// top-posting
$prefix = '<br>' . $prefix;
}
else {
$suffix .= '<p><br/></p>';
}
}
return $prefix . $body . $suffix;
}
function rcmail_get_reply_header($message)
{
global $RCMAIL;
$from = array_pop(rcube_mime::decode_address_list($message->get_header('from'), 1, false, $message->headers->charset));
return $RCMAIL->gettext(array(
'name' => 'mailreplyintro',
'vars' => array(
'date' => $RCMAIL->format_date($message->headers->date, $RCMAIL->config->get('date_long')),
'sender' => $from['name'] ?: rcube_utils::idn_to_utf8($from['mailto']),
)
));
}
function rcmail_create_forward_body($body, $bodyIsHtml)
{
global $RCMAIL, $MESSAGE, $COMPOSE;
// add attachments
if (!isset($COMPOSE['forward_attachments']) && is_array($MESSAGE->mime_parts)) {
$cid_map = rcmail_write_compose_attachments($MESSAGE, $bodyIsHtml);
}
if (!$bodyIsHtml) {
$body = trim($body, "\r\n");
}
else {
// set is_safe flag (we need this for html body washing)
rcmail_check_safe($MESSAGE);
// clean up html tags
$body = rcmail_wash_html($body, array('safe' => $MESSAGE->is_safe), $cid_map);
}
return rcmail_get_forward_header($MESSAGE, $bodyIsHtml) . $body;
}
function rcmail_get_forward_header($message, $bodyIsHtml = false, $extended = true)
{
global $RCMAIL;
$date = $RCMAIL->format_date($message->headers->date, $RCMAIL->config->get('date_long'));
if (!$bodyIsHtml) {
$prefix = "\n\n\n-------- " . $RCMAIL->gettext('originalmessage') . " --------\n";
$prefix .= $RCMAIL->gettext('subject') . ': ' . $message->subject . "\n";
$prefix .= $RCMAIL->gettext('date') . ': ' . $date . "\n";
$prefix .= $RCMAIL->gettext('from') . ': ' . $message->get_header('from') . "\n";
$prefix .= $RCMAIL->gettext('to') . ': ' . $message->get_header('to') . "\n";
if ($extended && ($cc = $message->headers->get('cc'))) {
$prefix .= $RCMAIL->gettext('cc') . ': ' . $cc . "\n";
}
if ($extended && ($replyto = $message->headers->get('reply-to')) && $replyto != $message->get_header('from')) {
$prefix .= $RCMAIL->gettext('replyto') . ': ' . $replyto . "\n";
}
$prefix .= "\n";
}
else {
$prefix = sprintf(
"<br /><p>-------- " . $RCMAIL->gettext('originalmessage') . " --------</p>" .
"<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tbody>" .
"<tr><th align=\"right\" nowrap=\"nowrap\" valign=\"baseline\">%s: </th><td>%s</td></tr>" .
"<tr><th align=\"right\" nowrap=\"nowrap\" valign=\"baseline\">%s: </th><td>%s</td></tr>" .
"<tr><th align=\"right\" nowrap=\"nowrap\" valign=\"baseline\">%s: </th><td>%s</td></tr>" .
"<tr><th align=\"right\" nowrap=\"nowrap\" valign=\"baseline\">%s: </th><td>%s</td></tr>",
$RCMAIL->gettext('subject'), rcube::Q($message->subject),
$RCMAIL->gettext('date'), rcube::Q($date),
$RCMAIL->gettext('from'), rcube::Q($message->get_header('from'), 'replace'),
$RCMAIL->gettext('to'), rcube::Q($message->get_header('to'), 'replace'));
if ($extended && ($cc = $message->headers->get('cc'))) {
$prefix .= sprintf("<tr><th align=\"right\" nowrap=\"nowrap\" valign=\"baseline\">%s: </th><td>%s</td></tr>",
$RCMAIL->gettext('cc'), rcube::Q($cc, 'replace'));
}
if ($extended && ($replyto = $message->headers->get('reply-to')) && $replyto != $message->get_header('from')) {
$prefix .= sprintf("<tr><th align=\"right\" nowrap=\"nowrap\" valign=\"baseline\">%s: </th><td>%s</td></tr>",
$RCMAIL->gettext('replyto'), rcube::Q($replyto, 'replace'));
}
$prefix .= "</tbody></table><br>";
}
return $prefix;
}
function rcmail_create_draft_body($body, $bodyIsHtml)
{
global $MESSAGE, $COMPOSE;
// add attachments
// count($MESSAGE->mime_parts) can be 1 - e.g. attachment, but no text!
if (empty($COMPOSE['forward_attachments'])
&& is_array($MESSAGE->mime_parts)
&& count($MESSAGE->mime_parts) > 0
) {
$cid_map = rcmail_write_compose_attachments($MESSAGE, $bodyIsHtml);
}
// clean up HTML tags - XSS prevention (#1489251)
if ($bodyIsHtml) {
$body = rcmail_wash_html($body, array('safe' => 1), $cid_map);
// cleanup
$body = preg_replace(array(
// remove comments (produced by washtml)
'/<!--[^>]+-->/',
// remove <body> tags
'/<body([^>]*)>/i',
'/<\/body>/i',
// convert TinyMCE's empty-line sequence (#1490463)
'/<p>\xC2\xA0<\/p>/',
),
array(
'',
'',
'',
'<p><br /></p>',
),
$body
);
// replace cid with href in inline images links
if (!empty($cid_map)) {
$body = str_replace(array_keys($cid_map), array_values($cid_map), $body);
}
}
return $body;
}
// Removes signature from the message body
function rcmail_remove_signature($body)
{
global $RCMAIL;
$body = str_replace("\r\n", "\n", $body);
$len = strlen($body);
$sig_max_lines = $RCMAIL->config->get('sig_max_lines', 15);
while (($sp = strrpos($body, "-- \n", $sp ? -$len+$sp-1 : 0)) !== false) {
if ($sp == 0 || $body[$sp-1] == "\n") {
// do not touch blocks with more that X lines
if (substr_count($body, "\n", $sp) < $sig_max_lines) {
$body = substr($body, 0, max(0, $sp-1));
}
break;
}
}
return $body;
}
function rcmail_write_compose_attachments(&$message, $bodyIsHtml)
{
global $RCMAIL, $COMPOSE;
$loaded_attachments = array();
foreach ((array)$COMPOSE['attachments'] as $attachment) {
$loaded_attachments[$attachment['name'] . $attachment['mimetype']] = $attachment;
}
$cid_map = array();
$messages = array();
if ($message->pgp_mime) {
return $cid_map;
}
foreach ((array) $message->mime_parts() as $pid => $part) {
if ($part->mimetype == 'message/rfc822') {
$messages[] = $part->mime_id;
}
if ($part->disposition == 'attachment' || ($part->disposition == 'inline' && $bodyIsHtml) || $part->filename) {
// skip parts that aren't valid attachments
if ($part->ctype_primary == 'multipart' || $part->mimetype == 'application/ms-tnef') {
continue;
}
// skip message attachments in reply mode
if ($part->ctype_primary == 'message' && $COMPOSE['mode'] == rcmail_sendmail::MODE_REPLY) {
continue;
}
// skip inline images when forwarding in text mode
if ($part->content_id && $part->disposition == 'inline' && !$bodyIsHtml && $COMPOSE['mode'] == rcmail_sendmail::MODE_FORWARD) {
continue;
}
// skip version.txt parts of multipart/encrypted messages
if ($message->pgp_mime && $part->mimetype == 'application/pgp-encrypted' && $part->filename == 'version.txt') {
continue;
}
// skip attachments included in message/rfc822 attachment (#1486487, #1490607)
foreach ($messages as $mimeid) {
if (strpos($part->mime_id, $mimeid . '.') === 0) {
continue 2;
}
}
if (($attachment = $loaded_attachments[rcmail_attachment_name($part) . $part->mimetype])
|| ($attachment = rcmail_save_attachment($message, $pid, $COMPOSE['id']))
) {
if ($bodyIsHtml && ($part->content_id || $part->content_location)) {
$url = sprintf('%s&_id=%s&_action=display-attachment&_file=rcmfile%s',
$RCMAIL->comm_path, $COMPOSE['id'], $attachment['id']);
if ($part->content_id)
$cid_map['cid:'.$part->content_id] = $url;
else
$cid_map[$part->content_location] = $url;
}
}
}
}
$COMPOSE['forward_attachments'] = true;
return $cid_map;
}
function rcmail_write_inline_attachments(&$message)
{
global $RCMAIL, $COMPOSE;
$cid_map = array();
$messages = array();
if ($message->pgp_mime) {
return $cid_map;
}
foreach ((array) $message->mime_parts() as $pid => $part) {
if ($part->mimetype == 'message/rfc822') {
$messages[] = $part->mime_id;
}
if (($part->content_id || $part->content_location) && $part->filename) {
// skip attachments included in message/rfc822 attachment (#1486487, #1490607)
foreach ($messages as $mimeid) {
if (strpos($part->mime_id, $mimeid . '.') === 0) {
continue 2;
}
}
if ($attachment = rcmail_save_attachment($message, $pid, $COMPOSE['id'])) {
$url = sprintf('%s&_id=%s&_action=display-attachment&_file=rcmfile%s',
$RCMAIL->comm_path, $COMPOSE['id'], $attachment['id']);
if ($part->content_id)
$cid_map['cid:'.$part->content_id] = $url;
else
$cid_map[$part->content_location] = $url;
}
}
}
return $cid_map;
}
// Creates attachment(s) from the forwarded message(s)
function rcmail_write_forward_attachments()
{
global $RCMAIL, $COMPOSE, $MESSAGE;
if ($MESSAGE->pgp_mime) {
return;
}
$storage = $RCMAIL->get_storage();
$names = array();
$refs = array();
$size_errors = 0;
$size_limit = parse_bytes($RCMAIL->config->get('max_message_size'));
$total_size = 10 * 1024; // size of message body, to start with
$loaded_attachments = array();
foreach ((array)$COMPOSE['attachments'] as $attachment) {
$loaded_attachments[$attachment['name'] . $attachment['mimetype']] = $attachment;
$total_size += $attachment['size'];
}
if ($COMPOSE['forward_uid'] == '*') {
$index = $storage->index(null, rcmail_sort_column(), rcmail_sort_order());
$COMPOSE['forward_uid'] = $index->get();
}
else if (!is_array($COMPOSE['forward_uid']) && strpos($COMPOSE['forward_uid'], ':')) {
$COMPOSE['forward_uid'] = rcube_imap_generic::uncompressMessageSet($COMPOSE['forward_uid']);
}
else if (is_string($COMPOSE['forward_uid'])) {
$COMPOSE['forward_uid'] = explode(',', $COMPOSE['forward_uid']);
}
foreach ((array)$COMPOSE['forward_uid'] as $uid) {
$message = new rcube_message($uid);
if (empty($message->headers)) {
continue;
}
if (!empty($message->headers->charset)) {
$storage->set_charset($message->headers->charset);
}
if (empty($MESSAGE->subject)) {
$MESSAGE->subject = $message->subject;
}
// generate (unique) attachment name
$name = strlen($message->subject) ? mb_substr($message->subject, 0, 64) : 'message_rfc822';
if (!empty($names[$name])) {
$names[$name]++;
$name .= '_' . $names[$name];
}
$names[$name] = 1;
$name .= '.eml';
if (!empty($loaded_attachments[$name . 'message/rfc822'])) {
continue;
}
if ($size_limit && $size_limit < $total_size + $message->headers->size) {
$size_errors++;
continue;
}
$total_size += $message->headers->size;
rcmail_save_attachment($message, null, $COMPOSE['id'], array('filename' => $name));
if ($message->headers->messageID) {
$refs[] = $message->headers->messageID;
}
}
// set In-Reply-To and References headers
if (count($refs) == 1) {
$COMPOSE['reply_msgid'] = $refs[0];
}
if (!empty($refs)) {
$COMPOSE['references'] = implode(' ', $refs);
}
if ($size_errors) {
$limit = $RCMAIL->show_bytes($size_limit);
$error = $RCMAIL->gettext(array('name' => 'msgsizeerrorfwd', 'vars' => array('num' => $size_errors, 'size' => $limit)));
$RCMAIL->output->add_script(sprintf("%s.display_message('%s', 'error');", rcmail_output::JS_OBJECT_NAME, rcube::JQ($error)), 'docready');
}
}
// Saves an image as attachment
function rcmail_save_image($path, $mimetype = '', $data = null)
{
global $COMPOSE;
// handle attachments in memory
if (empty($data)) {
$data = file_get_contents($path);
$is_file = true;
}
$name = rcmail_basename($path);
if (empty($mimetype)) {
if ($is_file) {
$mimetype = rcube_mime::file_content_type($path, $name);
}
else {
$mimetype = rcube_mime::file_content_type($data, $name, 'application/octet-stream', true);
}
}
$attachment = array(
'group' => $COMPOSE['id'],
'name' => $name,
'mimetype' => $mimetype,
'data' => $data,
'size' => strlen($data),
);
$attachment = rcmail::get_instance()->plugins->exec_hook('attachment_save', $attachment);
if ($attachment['status']) {
unset($attachment['data'], $attachment['status'], $attachment['content_id'], $attachment['abort']);
return $attachment;
}
return false;
}
// Unicode-safe basename()
function rcmail_basename($filename)
{
// basename() is not unicode safe and locale dependent
if (stristr(PHP_OS, 'win') || stristr(PHP_OS, 'netware')) {
return preg_replace('/^.*[\\\\\\/]/', '', $filename);
}
else {
return preg_replace('/^.*[\/]/', '', $filename);
}
}
/**
* Attachments list object for templates
*/
function rcmail_compose_attachment_list($attrib)
{
global $RCMAIL, $OUTPUT, $COMPOSE;
// add ID if not given
if (!$attrib['id'])
$attrib['id'] = 'rcmAttachmentList';
$out = "\n";
$jslist = array();
$button = '';
if ($attrib['icon_pos'] == 'left')
$COMPOSE['icon_pos'] = 'left';
if (is_array($COMPOSE['attachments'])) {
if ($attrib['deleteicon']) {
$button = html::img(array(
'src' => $RCMAIL->output->abs_url($attrib['deleteicon'], true),
'alt' => $RCMAIL->gettext('delete')
));
}
else if (rcube_utils::get_boolean($attrib['textbuttons'])) {
$button = rcube::Q($RCMAIL->gettext('delete'));
}
foreach ($COMPOSE['attachments'] as $id => $a_prop) {
if (empty($a_prop)) {
continue;
}
$link_content = sprintf('<span class="attachment-name" onmouseover="rcube_webmail.long_subject_title_ex(this)">%s</span> <span class="attachment-size">(%s)</span>',
rcube::Q($a_prop['name']), $RCMAIL->show_bytes($a_prop['size']));
$content_link = html::a(array(
'href' => "#load",
'class' => 'filename',
'onclick' => sprintf("return %s.command('load-attachment','rcmfile%s', this, event)", rcmail_output::JS_OBJECT_NAME, $id),
), $link_content);
$delete_link = html::a(array(
'href' => "#delete",
'title' => $RCMAIL->gettext('delete'),
'onclick' => sprintf("return %s.command('remove-attachment','rcmfile%s', this, event)", rcmail_output::JS_OBJECT_NAME, $id),
'class' => 'delete',
'tabindex' => $attrib['tabindex'] ?: '0',
'aria-label' => $RCMAIL->gettext('delete') . ' ' . $a_prop['name'],
), $button);
$out .= html::tag('li', array(
'id' => 'rcmfile'.$id,
'class' => rcube_utils::file2class($a_prop['mimetype'], $a_prop['name']),
),
$COMPOSE['icon_pos'] == 'left' ? $delete_link.$content_link : $content_link.$delete_link
);
$jslist['rcmfile'.$id] = array(
'name' => $a_prop['name'],
'complete' => true,
'mimetype' => $a_prop['mimetype']
);
}
}
if ($attrib['deleteicon'])
$COMPOSE['deleteicon'] = $RCMAIL->output->abs_url($attrib['deleteicon'], true);
else if (rcube_utils::get_boolean($attrib['textbuttons']))
$COMPOSE['textbuttons'] = true;
if ($attrib['cancelicon'])
$OUTPUT->set_env('cancelicon', $RCMAIL->output->abs_url($attrib['cancelicon'], true));
if ($attrib['loadingicon'])
$OUTPUT->set_env('loadingicon', $RCMAIL->output->abs_url($attrib['loadingicon'], true));
$OUTPUT->set_env('attachments', $jslist);
$OUTPUT->add_gui_object('attachmentlist', $attrib['id']);
// put tabindex value into data-tabindex attribute
if (isset($attrib['tabindex'])) {
$attrib['data-tabindex'] = $attrib['tabindex'];
unset($attrib['tabindex']);
}
return html::tag('ul', $attrib, $out, html::$common_attrib);
}
/**
* Attachment upload form object for templates
*/
function rcmail_compose_attachment_form($attrib)
{
global $RCMAIL;
return $RCMAIL->upload_form($attrib, 'uploadform', 'send-attachment', array('multiple' => true));
}
/**
* Register a certain container as active area to drop files onto
*/
function rcmail_compose_file_drop_area($attrib)
{
global $OUTPUT;
if ($attrib['id']) {
$OUTPUT->add_gui_object('filedrop', $attrib['id']);
$OUTPUT->set_env('filedrop', array('action' => 'upload', 'fieldname' => '_attachments'));
}
}
/**
* Editor mode selector object for templates
*/
function rcmail_editor_selector($attrib)
{
global $RCMAIL;
// determine whether HTML or plain text should be checked
$useHtml = rcmail_compose_editor_mode();
if (empty($attrib['editorid']))
$attrib['editorid'] = 'rcmComposeBody';
if (empty($attrib['name']))
$attrib['name'] = 'editorSelect';
$attrib['onchange'] = "return rcmail.command('toggle-editor', {id: '".$attrib['editorid']."', html: this.value == 'html'}, '', event)";
$select = new html_select($attrib);
$select->add(rcube::Q($RCMAIL->gettext('htmltoggle')), 'html');
$select->add(rcube::Q($RCMAIL->gettext('plaintoggle')), 'plain');
return $select->show($useHtml ? 'html' : 'plain');
}
/**
* Addressbooks list object for templates
*/
function rcmail_addressbook_list($attrib = array())
{
global $RCMAIL, $OUTPUT;
$attrib += array('id' => 'rcmdirectorylist');
$out = '';
$line_templ = html::tag('li', array(
'id' => 'rcmli%s', 'class' => '%s'),
html::a(array('href' => '#list',
'rel' => '%s',
'onclick' => "return ".rcmail_output::JS_OBJECT_NAME.".command('list-addresses','%s',this)"), '%s'));
foreach ($RCMAIL->get_address_sources(false, true) as $j => $source) {
$id = strval(strlen($source['id']) ? $source['id'] : $j);
$js_id = rcube::JQ($id);
// set class name(s)
$class_name = 'addressbook';
if ($source['class_name'])
$class_name .= ' ' . $source['class_name'];
$out .= sprintf($line_templ,
rcube_utils::html_identifier($id,true),
$class_name,
$source['id'],
$js_id, ($source['name'] ?: $id));
}
$OUTPUT->add_gui_object('addressbookslist', $attrib['id']);
return html::tag('ul', $attrib, $out, html::$common_attrib);
}
/**
* Contacts list object for templates
*/
function rcmail_contacts_list($attrib = array())
{
global $RCMAIL, $OUTPUT;
$attrib += array('id' => 'rcmAddressList');
// set client env
$OUTPUT->add_gui_object('contactslist', $attrib['id']);
$OUTPUT->set_env('pagecount', 0);
$OUTPUT->set_env('current_page', 0);
$OUTPUT->include_script('list.js');
return $RCMAIL->table_output($attrib, array(), array('name'), 'ID');
}
/**
* Responses list object for templates
*/
function rcmail_compose_responses_list($attrib)
{
global $RCMAIL, $OUTPUT;
$attrib += array('id' => 'rcmresponseslist', 'tagname' => 'ul', 'cols' => 1);
$jsenv = array();
$list = new html_table($attrib);
foreach ($RCMAIL->get_compose_responses(true) as $response) {
$key = $response['key'];
$item = html::a(array(
'href' => '#' . urlencode($response['name']),
'class' => rtrim('insertresponse ' . $attrib['itemclass']),
'unselectable' => 'on',
'tabindex' => '0',
'rel' => $key,
), rcube::Q($response['name']));
$jsenv[$key] = $response;
$list->add(array(), $item);
}
// set client env
$OUTPUT->set_env('textresponses', $jsenv);
$OUTPUT->add_gui_object('responseslist', $attrib['id']);
return $list->show();
}
diff --git a/program/steps/mail/copy.inc b/program/steps/mail/copy.inc
index d8212757a..a4521e611 100644
--- a/program/steps/mail/copy.inc
+++ b/program/steps/mail/copy.inc
@@ -1,62 +1,60 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/copy.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2014, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Copy the submitted messages to a specific mailbox |
- | |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
// only process ajax requests
if (!$OUTPUT->ajax_call) {
return;
}
// copy messages
if (!empty($_POST['_uid']) && strlen($_POST['_target_mbox'])) {
$target = rcube_utils::get_input_value('_target_mbox', rcube_utils::INPUT_POST, true);
$sources = array();
foreach (rcmail::get_uids(null, null, $multifolder, rcube_utils::INPUT_POST) as $mbox => $uids) {
if ($mbox === $target) {
$copied++;
}
else {
$copied += (int)$RCMAIL->storage->copy_message($uids, $target, $mbox);
$sources[] = $mbox;
}
}
if (!$copied) {
// send error message
$RCMAIL->display_server_error('errorcopying');
$OUTPUT->send();
exit;
}
else {
$OUTPUT->show_message('messagecopied', 'confirmation');
}
rcmail_send_unread_count($target, true);
$OUTPUT->command('set_quota', $RCMAIL->quota_content(null, $multifolder ? $sources[0] : 'INBOX'));
}
// unknown action or missing query param
else {
$OUTPUT->show_message('internalerror', 'error');
}
// send response
$OUTPUT->send();
diff --git a/program/steps/mail/folders.inc b/program/steps/mail/folders.inc
index 4c5be4f99..54fd894b7 100644
--- a/program/steps/mail/folders.inc
+++ b/program/steps/mail/folders.inc
@@ -1,87 +1,85 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/folders.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Implement folder operations line EXPUNGE and Clear |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
// only process ajax requests
if (!$OUTPUT->ajax_call) {
return;
}
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true);
// send EXPUNGE command
if ($RCMAIL->action == 'expunge') {
$success = $RCMAIL->storage->expunge_folder($mbox);
// reload message list if current mailbox
if ($success) {
$OUTPUT->show_message('folderexpunged', 'confirmation');
if (!empty($_REQUEST['_reload'])) {
$OUTPUT->command('set_quota', $RCMAIL->quota_content(null, $mbox));
$OUTPUT->command('message_list.clear');
$RCMAIL->action = 'list';
return;
}
}
else {
$RCMAIL->display_server_error();
}
}
// clear mailbox
else if ($RCMAIL->action == 'purge') {
$delimiter = $RCMAIL->storage->get_hierarchy_delimiter();
$trash_mbox = $RCMAIL->config->get('trash_mbox');
$junk_mbox = $RCMAIL->config->get('junk_mbox');
$trash_regexp = '/^' . preg_quote($trash_mbox . $delimiter, '/') . '/';
$junk_regexp = '/^' . preg_quote($junk_mbox . $delimiter, '/') . '/';
// we should only be purging trash and junk (or their subfolders)
if ($mbox == $trash_mbox || $mbox == $junk_mbox
|| preg_match($trash_regexp, $mbox) || preg_match($junk_regexp, $mbox)
) {
$success = $RCMAIL->storage->clear_folder($mbox);
if ($success) {
$OUTPUT->show_message('folderpurged', 'confirmation');
if (!empty($_REQUEST['_reload'])) {
$OUTPUT->set_env('messagecount', 0);
$OUTPUT->set_env('pagecount', 0);
$OUTPUT->set_env('exists', 0);
$OUTPUT->command('message_list.clear');
$OUTPUT->command('set_rowcount', rcmail_get_messagecount_text(), $mbox);
$OUTPUT->command('set_unread_count', $mbox, 0);
$OUTPUT->command('set_quota', $RCMAIL->quota_content(null, $mbox));
rcmail_set_unseen_count($mbox, 0);
// set trash folder state
if ($mbox === $trash_mbox) {
$OUTPUT->command('set_trash_count', 0);
}
}
}
else {
$RCMAIL->display_server_error();
}
}
}
$OUTPUT->send();
diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc
index ef5c27870..19aa8874b 100644
--- a/program/steps/mail/func.inc
+++ b/program/steps/mail/func.inc
@@ -1,1908 +1,1906 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/func.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2014, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide webmail functionality and GUI objects |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
// always instantiate storage object (but not connect to server yet)
$RCMAIL->storage_init();
// init environment - set current folder, page, list mode
rcmail_init_env();
// set message set for search result
if (!empty($_REQUEST['_search']) && isset($_SESSION['search'])
&& $_SESSION['search_request'] == $_REQUEST['_search']
) {
$RCMAIL->storage->set_search_set($_SESSION['search']);
$OUTPUT->set_env('search_request', $_REQUEST['_search']);
$OUTPUT->set_env('search_text', $_SESSION['last_text_search']);
}
// remove mbox part from _uid
if (($_uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GPC)) && !is_array($_uid) && preg_match('/^\d+-.+/', $_uid)) {
list($_uid, $mbox) = explode('-', $_uid, 2);
if (isset($_GET['_uid'])) $_GET['_uid'] = $_uid;
if (isset($_POST['_uid'])) $_POST['_uid'] = $_uid;
$_REQUEST['_uid'] = $_uid;
unset($_uid);
// override mbox
if (!empty($mbox)) {
$_GET['_mbox'] = $mbox;
$_POST['_mbox'] = $mbox;
$RCMAIL->storage->set_folder(($_SESSION['mbox'] = $mbox));
}
}
if (!empty($_SESSION['browser_caps']) && !$OUTPUT->ajax_call) {
$OUTPUT->set_env('browser_capabilities', $_SESSION['browser_caps']);
}
// set main env variables, labels and page title
if (empty($RCMAIL->action) || $RCMAIL->action == 'list') {
// connect to storage server and trigger error on failure
$RCMAIL->storage_connect();
$mbox_name = $RCMAIL->storage->get_folder();
if (empty($RCMAIL->action)) {
$OUTPUT->set_env('search_mods', rcmail_search_mods());
$scope = rcube_utils::get_input_value('_scope', rcube_utils::INPUT_GET) ?: $_SESSION['search_scope'];
if ($scope && preg_match('/^(all|sub)$/i', $scope)) {
$OUTPUT->set_env('search_scope', strtolower($scope));
}
rcmail_list_pagetitle();
}
$threading = (bool) $RCMAIL->storage->get_threading();
$delimiter = $RCMAIL->storage->get_hierarchy_delimiter();
// set current mailbox and some other vars in client environment
$OUTPUT->set_env('mailbox', $mbox_name);
$OUTPUT->set_env('pagesize', $RCMAIL->storage->get_pagesize());
$OUTPUT->set_env('current_page', max(1, $_SESSION['page']));
$OUTPUT->set_env('delimiter', $delimiter);
$OUTPUT->set_env('threading', $threading);
$OUTPUT->set_env('threads', $threading || $RCMAIL->storage->get_capability('THREAD'));
$OUTPUT->set_env('reply_all_mode', (int) $RCMAIL->config->get('reply_all_mode'));
$OUTPUT->set_env('layout', $RCMAIL->config->get('layout') ?: 'widescreen');
if ($RCMAIL->storage->get_capability('QUOTA')) {
$OUTPUT->set_env('quota', true);
}
// set special folders
foreach (array('drafts', 'trash', 'junk') as $mbox) {
if ($folder = $RCMAIL->config->get($mbox . '_mbox')) {
$OUTPUT->set_env($mbox . '_mailbox', $folder);
}
}
if (!empty($_GET['_uid'])) {
$OUTPUT->set_env('list_uid', $_GET['_uid']);
}
// set configuration
$RCMAIL->set_env_config(array('delete_junk', 'flag_for_deletion', 'read_when_deleted',
'skip_deleted', 'display_next', 'message_extwin', 'forward_attachment'));
if (!$OUTPUT->ajax_call) {
$OUTPUT->add_label('checkingmail', 'deletemessage', 'movemessagetotrash',
'movingmessage', 'copyingmessage', 'deletingmessage', 'markingmessage',
'copy', 'move', 'quota', 'replyall', 'replylist', 'stillsearching',
'flagged', 'unflagged', 'unread', 'deleted', 'replied', 'forwarded',
'priority', 'withattachment', 'fileuploaderror', 'mark', 'markallread',
'folders-cur', 'folders-sub', 'folders-all', 'cancel', 'bounce', 'bouncemsg',
'sendingmessage');
}
}
// register UI objects
$OUTPUT->add_handlers(array(
'mailboxlist' => array($RCMAIL, 'folder_list'),
'quotadisplay' => array($RCMAIL, 'quota_display'),
'messages' => 'rcmail_message_list',
'messagecountdisplay' => 'rcmail_messagecount_display',
'listmenulink' => 'rcmail_options_menu_link',
'mailboxname' => 'rcmail_mailbox_name_display',
'messageimportform' => 'rcmail_message_import_form',
'searchfilter' => 'rcmail_search_filter',
'searchinterval' => 'rcmail_search_interval',
'searchform' => array($OUTPUT, 'search_form'),
));
// register action aliases
$RCMAIL->register_action_map(array(
'refresh' => 'check_recent.inc',
'preview' => 'show.inc',
'print' => 'show.inc',
'move' => 'move_del.inc',
'delete' => 'move_del.inc',
'send' => 'sendmail.inc',
'expunge' => 'folders.inc',
'purge' => 'folders.inc',
'remove-attachment' => 'attachments.inc',
'rename-attachment' => 'attachments.inc',
'display-attachment' => 'attachments.inc',
'upload' => 'attachments.inc',
'group-expand' => 'autocomplete.inc',
));
/**
* Sets storage properties and session
*/
function rcmail_init_env()
{
global $RCMAIL;
$default_threading = $RCMAIL->config->get('default_list_mode', 'list') == 'threads';
$a_threading = $RCMAIL->config->get('message_threading', array());
$message_sort_col = $RCMAIL->config->get('message_sort_col');
$message_sort_order = $RCMAIL->config->get('message_sort_order');
// set imap properties and session vars
if (!strlen($mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GPC, true))) {
$mbox = strlen($_SESSION['mbox']) ? $_SESSION['mbox'] : 'INBOX';
}
// we handle 'page' argument on 'list' and 'getunread' to prevent from
// race condition and unintentional page overwrite in session
if ($RCMAIL->action == 'list' || $RCMAIL->action == 'getunread') {
if (!($page = intval($_GET['_page']))) {
$page = $_SESSION['page'] ?: 1;
}
$_SESSION['page'] = $page;
}
$RCMAIL->storage->set_folder($_SESSION['mbox'] = $mbox);
$RCMAIL->storage->set_page($_SESSION['page']);
// set default sort col/order to session
if (!isset($_SESSION['sort_col'])) {
$_SESSION['sort_col'] = $message_sort_col ?: '';
}
if (!isset($_SESSION['sort_order'])) {
$_SESSION['sort_order'] = strtoupper($message_sort_order) == 'ASC' ? 'ASC' : 'DESC';
}
// set threads mode
if (isset($_GET['_threads'])) {
if ($_GET['_threads']) {
// re-set current page number when listing mode changes
if (!$a_threading[$_SESSION['mbox']]) {
$RCMAIL->storage->set_page($_SESSION['page'] = 1);
}
$a_threading[$_SESSION['mbox']] = true;
}
else {
// re-set current page number when listing mode changes
if ($a_threading[$_SESSION['mbox']]) {
$RCMAIL->storage->set_page($_SESSION['page'] = 1);
}
$a_threading[$_SESSION['mbox']] = false;
}
$RCMAIL->user->save_prefs(array('message_threading' => $a_threading));
}
$threading = isset($a_threading[$_SESSION['mbox']]) ? $a_threading[$_SESSION['mbox']] : $default_threading;
$RCMAIL->storage->set_threading($threading);
}
/**
* Sets page title
*/
function rcmail_list_pagetitle()
{
global $RCMAIL;
if ($RCMAIL->output->get_env('search_request')) {
$pagetitle = $RCMAIL->gettext('searchresult');
}
else {
$mbox_name = $RCMAIL->output->get_env('mailbox') ?: $RCMAIL->storage->get_folder();
$delimiter = $RCMAIL->storage->get_hierarchy_delimiter();
$pagetitle = $RCMAIL->localize_foldername($mbox_name, true);
$pagetitle = str_replace($delimiter, " \xC2\xBB ", $pagetitle);
}
$RCMAIL->output->set_pagetitle($pagetitle);
}
/**
* Returns default search mods
*/
function rcmail_search_mods()
{
global $RCMAIL;
$mods = $RCMAIL->config->get('search_mods');
if (empty($mods)) {
$mods = array('*' => array('subject' => 1, 'from' => 1));
foreach (array('sent', 'drafts') as $mbox) {
if ($mbox = $RCMAIL->config->get($mbox . '_mbox')) {
$mods[$mbox] = array('subject' => 1, 'to' => 1);
}
}
}
return $mods;
}
/**
* Returns 'to' if current folder is configured Sent or Drafts
* or their subfolders, otherwise returns 'from'.
*
* @return string Column name
*/
function rcmail_message_list_smart_column_name()
{
global $RCMAIL;
$delim = $RCMAIL->storage->get_hierarchy_delimiter();
$mbox = $RCMAIL->output->get_env('mailbox') ?: $RCMAIL->storage->get_folder();
$sent_mbox = $RCMAIL->config->get('sent_mbox');
$drafts_mbox = $RCMAIL->config->get('drafts_mbox');
if ((strpos($mbox.$delim, $sent_mbox.$delim) === 0 || strpos($mbox.$delim, $drafts_mbox.$delim) === 0)
&& strtoupper($mbox) != 'INBOX'
) {
return 'to';
}
return 'from';
}
/**
* Returns configured messages list sorting column name
* The name is context-sensitive, which means if sorting is set to 'fromto'
* it will return 'from' or 'to' according to current folder type.
*
* @return string Column name
*/
function rcmail_sort_column()
{
global $RCMAIL;
if (isset($_SESSION['sort_col'])) {
$column = $_SESSION['sort_col'];
}
else {
$column = $RCMAIL->config->get('message_sort_col');
}
// get name of smart From/To column in folder context
if ($column == 'fromto') {
$column = rcmail_message_list_smart_column_name();
}
return $column;
}
/**
* Returns configured message list sorting order
*
* @return string Sorting order (ASC|DESC)
*/
function rcmail_sort_order()
{
global $RCMAIL;
if (isset($_SESSION['sort_order'])) {
return $_SESSION['sort_order'];
}
return $RCMAIL->config->get('message_sort_order');
}
/**
* return the message list as HTML table
*/
function rcmail_message_list($attrib)
{
global $RCMAIL, $OUTPUT;
// add some labels to client
$OUTPUT->add_label('from', 'to');
// add id to message list table if not specified
if (!strlen($attrib['id'])) {
$attrib['id'] = 'rcubemessagelist';
}
// define list of cols to be displayed based on parameter or config
if (empty($attrib['columns'])) {
$list_cols = $RCMAIL->config->get('list_cols');
$a_show_cols = !empty($list_cols) && is_array($list_cols) ? $list_cols : array('subject');
$OUTPUT->set_env('col_movable', !in_array('list_cols', (array)$RCMAIL->config->get('dont_override')));
}
else {
$a_show_cols = preg_split('/[\s,;]+/', str_replace(array("'", '"'), '', $attrib['columns']));
$attrib['columns'] = $a_show_cols;
}
// save some variables for use in ajax list
$_SESSION['list_attrib'] = $attrib;
// make sure 'threads' and 'subject' columns are present
if (!in_array('subject', $a_show_cols))
array_unshift($a_show_cols, 'subject');
if (!in_array('threads', $a_show_cols))
array_unshift($a_show_cols, 'threads');
$listcols = $a_show_cols;
// Widescreen layout uses hardcoded list of columns
if ($RCMAIL->config->get('layout', 'widescreen') == 'widescreen') {
$a_show_cols = array('threads', 'subject', 'fromto', 'date', 'flag', 'attachment');
$listcols = $a_show_cols;
array_shift($listcols);
}
// set client env
$OUTPUT->add_gui_object('messagelist', $attrib['id']);
$OUTPUT->set_env('autoexpand_threads', intval($RCMAIL->config->get('autoexpand_threads')));
$OUTPUT->set_env('sort_col', $_SESSION['sort_col']);
$OUTPUT->set_env('sort_order', $_SESSION['sort_order']);
$OUTPUT->set_env('messages', array());
$OUTPUT->set_env('listcols', $listcols);
$OUTPUT->include_script('list.js');
$table = new html_table($attrib);
if (!$attrib['noheader']) {
foreach (rcmail_message_list_head($attrib, $a_show_cols) as $cell)
$table->add_header(array('class' => $cell['className'], 'id' => $cell['id']), $cell['html']);
}
return $table->show();
}
/**
* return javascript commands to add rows to the message list
*/
function rcmail_js_message_list($a_headers, $insert_top=false, $a_show_cols=null)
{
global $RCMAIL, $OUTPUT;
if (empty($a_show_cols)) {
if (!empty($_SESSION['list_attrib']['columns']))
$a_show_cols = $_SESSION['list_attrib']['columns'];
else {
$list_cols = $RCMAIL->config->get('list_cols');
$a_show_cols = !empty($list_cols) && is_array($list_cols) ? $list_cols : array('subject');
}
}
else {
if (!is_array($a_show_cols)) {
$a_show_cols = preg_split('/[\s,;]+/', str_replace(array("'", '"'), '', $a_show_cols));
}
$head_replace = true;
}
$delimiter = $RCMAIL->storage->get_hierarchy_delimiter();
$search_set = $RCMAIL->storage->get_search_set();
$multifolder = $search_set && $search_set[1]->multi;
// add/remove 'folder' column to the list on multi-folder searches
if ($multifolder && !in_array('folder', $a_show_cols)) {
$a_show_cols[] = 'folder';
$head_replace = true;
}
else if (!$multifolder && ($found = array_search('folder', $a_show_cols)) !== false) {
unset($a_show_cols[$found]);
$head_replace = true;
}
$mbox = $RCMAIL->output->get_env('mailbox') ?: $RCMAIL->storage->get_folder();
// make sure 'threads' and 'subject' columns are present
if (!in_array('subject', $a_show_cols))
array_unshift($a_show_cols, 'subject');
if (!in_array('threads', $a_show_cols))
array_unshift($a_show_cols, 'threads');
// Make sure there are no duplicated columns (#1486999)
$a_show_cols = array_unique($a_show_cols);
$_SESSION['list_attrib']['columns'] = $a_show_cols;
// Widescreen layout uses hardcoded list of columns
if ($RCMAIL->config->get('layout', 'widescreen') == 'widescreen') {
$a_show_cols = array('threads', 'subject', 'fromto', 'date', 'flag', 'attachment');
}
// Plugins may set header's list_cols/list_flags and other rcube_message_header variables
// and list columns
$plugin = $RCMAIL->plugins->exec_hook('messages_list',
array('messages' => $a_headers, 'cols' => $a_show_cols));
$a_show_cols = $plugin['cols'];
$a_headers = $plugin['messages'];
if ($RCMAIL->config->get('layout', 'widescreen') == 'widescreen') {
if (!$RCMAIL->storage->get_threading()) {
if (($idx = array_search('threads', $a_show_cols)) !== false) {
unset($a_show_cols[$idx]);
}
}
}
$thead = $head_replace ? rcmail_message_list_head($_SESSION['list_attrib'], $a_show_cols) : NULL;
// get name of smart From/To column in folder context
if (array_search('fromto', $a_show_cols) !== false) {
$smart_col = rcmail_message_list_smart_column_name();
}
$OUTPUT->command('set_message_coltypes', array_values($a_show_cols), $thead, $smart_col);
if ($multifolder && $_SESSION['search_scope'] == 'all') {
$OUTPUT->command('select_folder', '');
}
$OUTPUT->set_env('multifolder_listing', $multifolder);
if (empty($a_headers)) {
return;
}
// remove 'threads', 'attachment', 'flag', 'status' columns, we don't need them here
foreach (array('threads', 'attachment', 'flag', 'status', 'priority') as $col) {
if (($key = array_search($col, $a_show_cols)) !== FALSE) {
unset($a_show_cols[$key]);
}
}
$sort_col = $_SESSION['sort_col'];
// loop through message headers
foreach ($a_headers as $header) {
if (empty($header) || !$header->size) {
continue;
}
// make message UIDs unique by appending the folder name
if ($multifolder) {
$header->uid .= '-'.$header->folder;
$header->flags['skip_mbox_check'] = true;
if ($header->parent_uid)
$header->parent_uid .= '-'.$header->folder;
}
$a_msg_cols = array();
$a_msg_flags = array();
// format each col; similar as in rcmail_message_list()
foreach ($a_show_cols as $col) {
$col_name = $col == 'fromto' ? $smart_col : $col;
if (in_array($col_name, array('from', 'to', 'cc', 'replyto'))) {
$cont = rcmail_address_string($header->$col_name, 3, false, null, $header->charset);
if (empty($cont)) $cont = '&nbsp;'; // for widescreen mode
}
else if ($col == 'subject') {
$cont = trim(rcube_mime::decode_header($header->$col, $header->charset));
if (!$cont) $cont = $RCMAIL->gettext('nosubject');
$cont = rcube::Q($cont);
}
else if ($col == 'size')
$cont = $RCMAIL->show_bytes($header->$col);
else if ($col == 'date')
$cont = $RCMAIL->format_date($sort_col == 'arrival' ? $header->internaldate : $header->date);
else if ($col == 'folder') {
if ($last_folder !== $header->folder) {
$last_folder = $header->folder;
$last_folder_name = $RCMAIL->localize_foldername($last_folder, true);
$last_folder_name = str_replace($delimiter, " \xC2\xBB ", $last_folder_name);
}
$cont = rcube::Q($last_folder_name);
}
else
$cont = rcube::Q($header->$col);
$a_msg_cols[$col] = $cont;
}
$a_msg_flags = array_change_key_case(array_map('intval', (array) $header->flags));
if ($header->depth)
$a_msg_flags['depth'] = $header->depth;
else if ($header->has_children)
$roots[] = $header->uid;
if ($header->parent_uid)
$a_msg_flags['parent_uid'] = $header->parent_uid;
if ($header->has_children)
$a_msg_flags['has_children'] = $header->has_children;
if ($header->unread_children)
$a_msg_flags['unread_children'] = $header->unread_children;
if ($header->flagged_children)
$a_msg_flags['flagged_children'] = $header->flagged_children;
if ($header->others['list-post'])
$a_msg_flags['ml'] = 1;
if ($header->priority)
$a_msg_flags['prio'] = (int) $header->priority;
$a_msg_flags['ctype'] = rcube::Q($header->ctype);
$a_msg_flags['mbox'] = $header->folder;
// merge with plugin result (Deprecated, use $header->flags)
if (!empty($header->list_flags) && is_array($header->list_flags))
$a_msg_flags = array_merge($a_msg_flags, $header->list_flags);
if (!empty($header->list_cols) && is_array($header->list_cols))
$a_msg_cols = array_merge($a_msg_cols, $header->list_cols);
$OUTPUT->command('add_message_row', $header->uid, $a_msg_cols, $a_msg_flags, $insert_top);
}
if ($RCMAIL->storage->get_threading()) {
$OUTPUT->command('init_threads', (array) $roots, $mbox);
}
}
/*
* Creates <THEAD> for message list table
*/
function rcmail_message_list_head($attrib, $a_show_cols)
{
global $RCMAIL;
// check to see if we have some settings for sorting
$sort_col = $_SESSION['sort_col'];
$sort_order = $_SESSION['sort_order'];
$dont_override = (array) $RCMAIL->config->get('dont_override');
$disabled_sort = in_array('message_sort_col', $dont_override);
$disabled_order = in_array('message_sort_order', $dont_override);
$RCMAIL->output->set_env('disabled_sort_col', $disabled_sort);
$RCMAIL->output->set_env('disabled_sort_order', $disabled_order);
// define sortable columns
if ($disabled_sort)
$a_sort_cols = $sort_col && !$disabled_order ? array($sort_col) : array();
else
$a_sort_cols = array('subject', 'date', 'from', 'to', 'fromto', 'size', 'cc');
if (!empty($attrib['optionsmenuicon'])) {
$list_menu = rcmail_options_menu_link();
}
$cells = $coltypes = array();
// get name of smart From/To column in folder context
if (array_search('fromto', $a_show_cols) !== false) {
$smart_col = rcmail_message_list_smart_column_name();
}
foreach ($a_show_cols as $col) {
$label = '';
$sortable = false;
$rel_col = $col == 'date' && $sort_col == 'arrival' ? 'arrival' : $col;
// get column name
switch ($col) {
case 'flag':
$col_name = html::span('flagged', $RCMAIL->gettext('flagged'));
break;
case 'attachment':
case 'priority':
$col_name = html::span($col, $RCMAIL->gettext($col));
break;
case 'status':
$col_name = html::span($col, $RCMAIL->gettext('readstatus'));
break;
case 'threads':
$col_name = (string) $list_menu;
break;
case 'fromto':
$label = $RCMAIL->gettext($smart_col);
$col_name = rcube::Q($label);
break;
default:
$label = $RCMAIL->gettext($col);
$col_name = rcube::Q($label);
}
// make sort links
if (in_array($col, $a_sort_cols)) {
$sortable = true;
$col_name = html::a(array(
'href' => "./#sort",
'class' => 'sortcol',
'rel' => $rel_col,
'title' => $RCMAIL->gettext('sortby')
), $col_name);
}
else if ($col_name[0] != '<') {
$col_name = '<span class="' . $col .'">' . $col_name . '</span>';
}
$sort_class = $rel_col == $sort_col && !$disabled_order ? " sorted$sort_order" : '';
$class_name = $col.$sort_class;
// put it all together
$cells[] = array('className' => $class_name, 'id' => "rcm$col", 'html' => $col_name);
$coltypes[$col] = array('className' => $class_name, 'id' => "rcm$col", 'label' => $label, 'sortable' => $sortable);
}
$RCMAIL->output->set_env('coltypes', $coltypes);
return $cells;
}
function rcmail_options_menu_link($attrib = array())
{
global $RCMAIL;
$onclick = 'return ' . rcmail_output::JS_OBJECT_NAME . ".command('menu-open', 'messagelistmenu', this, event)";
$inner = $title = $RCMAIL->gettext($attrib['label'] ?: 'listoptions');
if (is_string($attrib['optionsmenuicon']) && $attrib['optionsmenuicon'] != 'true') {
$inner = html::img(array('src' => $RCMAIL->output->abs_url($attrib['optionsmenuicon'], true), 'alt' => $title));
}
else if ($attrib['innerclass']) {
$inner = html::span($attrib['innerclass'], $inner);
}
return html::a(array(
'href' => '#list-options',
'onclick' => $onclick,
'class' => isset($attrib['class']) ? $attrib['class'] : 'listmenu',
'id' => 'listmenulink',
'title' => $title,
'tabindex' => '0',
), $inner);
}
function rcmail_messagecount_display($attrib)
{
global $RCMAIL;
if (!$attrib['id']) {
$attrib['id'] = 'rcmcountdisplay';
}
$RCMAIL->output->add_gui_object('countdisplay', $attrib['id']);
$content = $RCMAIL->action != 'show' ? rcmail_get_messagecount_text() : $RCMAIL->gettext('loading');
return html::span($attrib, $content);
}
function rcmail_get_messagecount_text($count = null, $page = null)
{
global $RCMAIL;
if ($page === null) {
$page = $RCMAIL->storage->get_page();
}
$page_size = $RCMAIL->storage->get_pagesize();
$start_msg = ($page-1) * $page_size + 1;
$max = $count;
if ($max === null && $RCMAIL->action) {
$max = $RCMAIL->storage->count(null, $RCMAIL->storage->get_threading() ? 'THREADS' : 'ALL');
}
if (!$max) {
$out = $RCMAIL->storage->get_search_set() ? $RCMAIL->gettext('nomessages') : $RCMAIL->gettext('mailboxempty');
}
else {
$out = $RCMAIL->gettext(array('name' => $RCMAIL->storage->get_threading() ? 'threadsfromto' : 'messagesfromto',
'vars' => array('from' => $start_msg,
'to' => min($max, $start_msg + $page_size - 1),
'count' => $max)));
}
return rcube::Q($out);
}
function rcmail_mailbox_name_display($attrib)
{
global $RCMAIL;
if (!$attrib['id']) {
$attrib['id'] = 'rcmmailboxname';
}
$RCMAIL->output->add_gui_object('mailboxname', $attrib['id']);
return html::span($attrib, rcmail_get_mailbox_name_text());
}
function rcmail_get_mailbox_name_text()
{
global $RCMAIL;
return $RCMAIL->localize_foldername($RCMAIL->output->get_env('mailbox') ?: $RCMAIL->storage->get_folder());
}
function rcmail_send_unread_count($mbox_name, $force=false, $count=null, $mark='')
{
global $RCMAIL;
$old_unseen = rcmail_get_unseen_count($mbox_name);
$unseen = $count;
if ($unseen === null) {
$unseen = $RCMAIL->storage->count($mbox_name, 'UNSEEN', $force);
}
if ($unseen !== $old_unseen || ($mbox_name == 'INBOX')) {
$RCMAIL->output->command('set_unread_count', $mbox_name, $unseen,
($mbox_name == 'INBOX'), $unseen && $mark ? $mark : '');
}
rcmail_set_unseen_count($mbox_name, $unseen);
return $unseen;
}
function rcmail_set_unseen_count($mbox_name, $count)
{
// @TODO: this data is doubled (session and cache tables) if caching is enabled
// Make sure we have an array here (#1487066)
if (!is_array($_SESSION['unseen_count'])) {
$_SESSION['unseen_count'] = array();
}
$_SESSION['unseen_count'][$mbox_name] = $count;
}
function rcmail_get_unseen_count($mbox_name)
{
if (is_array($_SESSION['unseen_count']) && array_key_exists($mbox_name, $_SESSION['unseen_count'])) {
return $_SESSION['unseen_count'][$mbox_name];
}
}
/**
* Sets message is_safe flag according to 'show_images' option value
*
* @param object rcube_message Message
*/
function rcmail_check_safe($message)
{
global $RCMAIL;
if (!$message->is_safe
&& ($show_images = $RCMAIL->config->get('show_images'))
&& $message->has_html_part()
) {
switch ($show_images) {
case 1: // known senders only
// get default addressbook, like in addcontact.inc
$CONTACTS = $RCMAIL->get_address_book(-1, true);
if ($CONTACTS && $message->sender['mailto']) {
$result = $CONTACTS->search('email', $message->sender['mailto'], 1, false);
if ($result->count) {
$message->set_safe(true);
}
}
$RCMAIL->plugins->exec_hook('message_check_safe', array('message' => $message));
break;
case 2: // always
$message->set_safe(true);
break;
}
}
return !empty($message->is_safe);
}
/**
* Cleans up the given message HTML Body (for displaying)
*
* @param string HTML
* @param array Display parameters
* @param array CID map replaces (inline images)
* @return string Clean HTML
*/
function rcmail_wash_html($html, $p, $cid_replaces = array())
{
global $REMOTE_OBJECTS;
$p += array('safe' => false, 'inline_html' => true);
// charset was converted to UTF-8 in rcube_storage::get_message_part(),
// change/add charset specification in HTML accordingly,
// washtml's DOMDocument methods cannot work without that
$meta = '<meta charset="'.RCUBE_CHARSET.'" />';
// remove old meta tag and add the new one, making sure
// that it is placed in the head (#1488093)
$html = preg_replace('/<meta[^>]+charset=[a-z0-9_"-]+[^>]*>/Ui', $meta, $html, -1, $rcount);
if (!$rcount) {
$html = preg_replace('/(<head[^>]*>)/Ui', '\\1'.$meta, $html, -1, $rcount);
}
if (!$rcount) {
// Note: HTML without <html> tag may still be a valid input (#6713)
if (($pos = stripos($html, '<html')) === false) {
$html = '<html><head>' . $meta . '</head>' . $html;
}
else {
$pos = strpos($html, '>', $pos);
$html = substr_replace($html, '<head>' . $meta . '</head>', $pos + 1, 0);
}
}
// clean HTML with washhtml by Frederic Motte
$wash_opts = array(
'show_washed' => false,
'allow_remote' => $p['safe'],
'blocked_src' => 'program/resources/blocked.gif',
'charset' => RCUBE_CHARSET,
'cid_map' => $cid_replaces,
'html_elements' => array('body'),
'css_prefix' => $p['css_prefix'],
'container_id' => $p['container_id'],
);
if (!$p['inline_html']) {
$wash_opts['html_elements'] = array('html','head','title','body','link');
}
if ($p['safe']) {
$wash_opts['html_attribs'] = array('rel','type');
}
// overwrite washer options with options from plugins
if (isset($p['html_elements'])) {
$wash_opts['html_elements'] = $p['html_elements'];
}
if (isset($p['html_attribs'])) {
$wash_opts['html_attribs'] = $p['html_attribs'];
}
// initialize HTML washer
$washer = new rcube_washtml($wash_opts);
if (!$p['skip_washer_form_callback']) {
$washer->add_callback('form', 'rcmail_washtml_callback');
}
// allow CSS styles, will be sanitized by rcmail_washtml_callback()
if (!$p['skip_washer_style_callback']) {
$washer->add_callback('style', 'rcmail_washtml_callback');
}
// modify HTML links to open a new window if clicked
if (!$p['skip_washer_link_callback']) {
$washer->add_callback('a', 'rcmail_washtml_link_callback');
$washer->add_callback('area', 'rcmail_washtml_link_callback');
$washer->add_callback('link', 'rcmail_washtml_link_callback');
}
// Remove non-UTF8 characters (#1487813)
$html = rcube_charset::clean($html);
$html = $washer->wash($html);
$REMOTE_OBJECTS = $washer->extlinks;
return $html;
}
/**
* Convert the given message part to proper HTML
* which can be displayed the message view
*
* @param string Message part body
* @param rcube_message_part Message part
* @param array Display parameters array
*
* @return string Formatted HTML string
*/
function rcmail_print_body($body, $part, $p = array())
{
global $RCMAIL;
// trigger plugin hook
$data = $RCMAIL->plugins->exec_hook('message_part_before',
array('type' => $part->ctype_secondary, 'body' => $body, 'id' => $part->mime_id)
+ $p + array('safe' => false, 'plain' => false, 'inline_html' => true));
// convert html to text/plain
if ($data['plain'] && ($data['type'] == 'html' || $data['type'] == 'enriched')) {
if ($data['type'] == 'enriched') {
$data['body'] = rcube_enriched::to_html($data['body']);
}
$body = $RCMAIL->html2text($data['body']);
$part->ctype_secondary = 'plain';
}
// text/html
else if ($data['type'] == 'html') {
$body = rcmail_wash_html($data['body'], $data, $part->replaces);
$part->ctype_secondary = $data['type'];
}
// text/enriched
else if ($data['type'] == 'enriched') {
$body = rcube_enriched::to_html($data['body']);
$body = rcmail_wash_html($body, $data, $part->replaces);
$part->ctype_secondary = 'html';
}
else {
// assert plaintext
$body = $data['body'];
$part->ctype_secondary = $data['type'] = 'plain';
}
// free some memory (hopefully)
unset($data['body']);
// plaintext postprocessing
if ($part->ctype_secondary == 'plain') {
$flowed = $part->ctype_parameters['format'] == 'flowed';
$delsp = $part->ctype_parameters['delsp'] == 'yes';
$body = rcmail_plain_body($body, $flowed, $delsp);
}
// allow post-processing of the message body
$data = $RCMAIL->plugins->exec_hook('message_part_after',
array('type' => $part->ctype_secondary, 'body' => $body, 'id' => $part->mime_id) + $data);
return $data['body'];
}
/**
* Handle links and citation marks in plain text message
*
* @param string Plain text string
* @param boolean Set to True if the source text is in format=flowed
*
* @return string Formatted HTML string
*/
function rcmail_plain_body($body, $flowed = false, $delsp = false)
{
$options = array('flowed' => $flowed, 'wrap' => !$flowed, 'replacer' => 'rcmail_string_replacer',
'delsp' => $delsp);
$text2html = new rcube_text2html($body, false, $options);
$body = $text2html->get_html();
return $body;
}
/**
* Callback function for washtml cleaning class
*/
function rcmail_washtml_callback($tagname, $attrib, $content, $washtml)
{
switch ($tagname) {
case 'form':
$out = html::div('form', $content);
break;
case 'style':
// Crazy big styles may freeze the browser (#1490539)
// remove content with more than 5k lines
if (substr_count($content, "\n") > 5000) {
$out = '';
break;
}
// decode all escaped entities and reduce to ascii strings
$decoded = rcube_utils::xss_entity_decode($content);
$stripped = preg_replace('/[^a-zA-Z\(:;]/', '', $decoded);
// now check for evil strings like expression, behavior or url()
if (!preg_match('/expression|behavior|javascript:|import[^a]/i', $stripped)) {
if (!$washtml->get_config('allow_remote') && preg_match('/url\((?!data:image)/', $stripped)) {
$washtml->extlinks = true;
}
else {
$out = html::tag('style', array('type' => 'text/css'), $decoded);
}
break;
}
default:
$out = '';
}
return $out;
}
function rcmail_part_image_type($part)
{
$mimetype = strtolower($part->mimetype);
// Skip TIFF/WEBP images if browser doesn't support this format
// ...until we can convert them to JPEG
$tiff_support = !empty($_SESSION['browser_caps']) && !empty($_SESSION['browser_caps']['tiff']);
$tiff_support = $tiff_support || rcube_image::is_convertable('image/tiff');
$webp_support = !empty($_SESSION['browser_caps']) && !empty($_SESSION['browser_caps']['webp']);
$webp_support = $webp_support || rcube_image::is_convertable('image/webp');
if ((!$tiff_support && $mimetype == 'image/tiff') || (!$webp_support && $mimetype == 'image/webp')) {
return;
}
// Content-Type: image/*...
if (strpos($mimetype, 'image/') === 0) {
return rcmail_fix_mimetype($mimetype);
}
// Many clients use application/octet-stream, we'll detect mimetype
// by checking filename extension
// Supported image filename extensions to image type map
$types = array(
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'bmp' => 'image/bmp',
);
if ($tiff_support) {
$types['tif'] = 'image/tiff';
$types['tiff'] = 'image/tiff';
}
if ($webp_support) {
$types['webp'] = 'image/webp';
}
if ($part->filename
&& $mimetype == 'application/octet-stream'
&& preg_match('/\.([^.]+)$/i', $part->filename, $m)
&& ($extension = strtolower($m[1]))
&& isset($types[$extension])
) {
return $types[$extension];
}
}
/**
* Modify a HTML message that it can be displayed inside a HTML page
*/
function rcmail_html4inline($body, $args)
{
$last_pos = 0;
$cont_id = $args['container_id'] . ($args['body_class'] ? ' div.' . $args['body_class'] : '');
// find STYLE tags
while (($pos = stripos($body, '<style', $last_pos)) && ($pos2 = stripos($body, '</style>', $pos))) {
$pos = strpos($body, '>', $pos) + 1;
$len = $pos2 - $pos;
// replace all css definitions with #container [def]
$styles = substr($body, $pos, $len);
$styles = rcube_utils::mod_css_styles($styles, $cont_id, $args['safe'], $args['css_prefix']);
$body = substr_replace($body, $styles, $pos, $len);
$last_pos = $pos2 + strlen($styles) - $len;
}
$body = preg_replace(array(
// add comments around html and other tags
'/(<!DOCTYPE[^>]*>)/i',
'/(<\?xml[^>]*>)/i',
'/(<\/?html[^>]*>)/i',
'/(<\/?head[^>]*>)/i',
'/(<title[^>]*>.*<\/title>)/Ui',
'/(<\/?meta[^>]*>)/i',
// quote <? of php and xml files that are specified as text/html
'/<\?/',
'/\?>/',
// replace <body> with <div>
'/<body([^>]*)>/i',
'/<\/body>/i',
),
array(
'<!--\\1-->',
'<!--\\1-->',
'<!--\\1-->',
'<!--\\1-->',
'<!--\\1-->',
'<!--\\1-->',
'&lt;?',
'?&gt;',
'<div class="' . $args['body_class'] . '"\\1>',
'</div>',
),
$body);
// Handle body attributes that doesn't play nicely with div elements
$regexp = '/<div class="' . preg_quote($args['body_class'], '/') . '"([^>]*)/';
if (preg_match($regexp, $body, $m)) {
$style = array();
$attrs = $m[0];
// Get bgcolor, we'll set it as background-color of the message container
if ($m[1] && preg_match('/bgcolor=["\']*([a-z0-9#]+)["\']*/i', $attrs, $mb)) {
$style['background-color'] = $mb[1];
$attrs = preg_replace('/bgcolor=["\']*[a-z0-9#]+["\']*/i', '', $attrs);
}
// Get background, we'll set it as background-image of the message container
if ($m[1] && preg_match('/background=["\']*([^"\'>\s]+)["\']*/', $attrs, $mb)) {
$style['background-image'] = 'url('.$mb[1].')';
$attrs = preg_replace('/background=["\']*([^"\'>\s]+)["\']*/', '', $attrs);
}
if (!empty($style)) {
$body = preg_replace($regexp, rtrim($attrs), $body, 1);
}
// handle body styles related to background image
if ($style['background-image']) {
// get body style
if (preg_match('/#'.preg_quote($cont_id, '/').'\s+\{([^}]+)}/i', $body, $m)) {
// get background related style
$regexp = '/(background-position|background-repeat)\s*:\s*([^;]+);/i';
if (preg_match_all($regexp, $m[1], $matches, PREG_SET_ORDER)) {
foreach ($matches as $m) {
$style[$m[1]] = $m[2];
}
}
}
}
if (!empty($style)) {
foreach ($style as $idx => $val) {
$style[$idx] = $idx . ': ' . $val;
}
$attributes['style'] = implode('; ', $style);
}
}
// make sure there's 'rcmBody' div, we need it for proper css modification
// its name is hardcoded in rcmail_message_body() also
else {
$body = '<div class="' . $args['body_class'] . '">' . $body . '</div>';
}
return $body;
}
/**
* Parse link (a, link, area) attributes and set correct target
*/
function rcmail_washtml_link_callback($tag, $attribs, $content, $washtml)
{
global $RCMAIL;
$attrib = html::parse_attrib_string($attribs);
// Remove non-printable characters in URL (#1487805)
if ($attrib['href']) {
$attrib['href'] = preg_replace('/[\x00-\x1F]/', '', $attrib['href']);
}
if ($tag == 'link' && preg_match('/^https?:\/\//i', $attrib['href'])) {
$tempurl = 'tmp-' . md5($attrib['href']) . '.css';
$_SESSION['modcssurls'][$tempurl] = $attrib['href'];
$attrib['href'] = $RCMAIL->url(array(
'task' => 'utils',
'action' => 'modcss',
'u' => $tempurl,
'c' => $washtml->get_config('container_id'),
'p' => $washtml->get_config('css_prefix'),
));
$end = ' />';
$content = null;
}
else if (preg_match('/^mailto:(.+)/i', $attrib['href'], $mailto)) {
list($mailto, $url) = explode('?', html_entity_decode($mailto[1], ENT_QUOTES, 'UTF-8'), 2);
// #6020: use raw encoding for correct "+" character handling as specified in RFC6068
$url = rawurldecode($url);
$mailto = rawurldecode($mailto);
$addresses = rcube_mime::decode_address_list($mailto, null, true);
$mailto = array();
// do sanity checks on recipients
foreach ($addresses as $idx => $addr) {
if (rcube_utils::check_email($addr['mailto'], false)) {
$addresses[$idx] = $addr['mailto'];
$mailto[] = $addr['string'];
}
else {
unset($addresses[$idx]);
}
}
if (!empty($addresses)) {
$attrib['href'] = 'mailto:' . implode(',', $addresses);
$attrib['onclick'] = sprintf(
"return %s.command('compose','%s',this)",
rcmail_output::JS_OBJECT_NAME,
rcube::JQ(implode(',', $mailto) . ($url ? "?$url" : '')));
}
else {
$attrib['href'] = '#NOP';
$attrib['onclick'] = '';
}
}
else if (empty($attrib['href']) && !isset($attrib['name'])) {
$attrib['href'] = './#NOP';
$attrib['onclick'] = 'return false';
}
else if (!empty($attrib['href']) && $attrib['href'][0] != '#') {
$attrib['target'] = '_blank';
}
// Better security by adding rel="noreferrer" (#1484686)
if (($tag == 'a' || $tag == 'area') && $attrib['href'] && $attrib['href'][0] != '#') {
$attrib['rel'] = 'noreferrer';
}
// allowed attributes for a|link|area tags
$allow = array('href','name','target','onclick','id','class','style','title',
'rel','type','media','alt','coords','nohref','hreflang','shape');
return html::tag($tag, $attrib, $content, $allow);
}
/**
* Decode address string and re-format it as HTML links
*/
function rcmail_address_string($input, $max=null, $linked=false, $addicon=null, $default_charset=null, $title=null)
{
global $RCMAIL, $PRINT_MODE;
$a_parts = rcube_mime::decode_address_list($input, null, true, $default_charset);
if (!count($a_parts)) {
return $input;
}
$c = count($a_parts);
$j = 0;
$out = '';
$allvalues = array();
$show_email = $RCMAIL->config->get('message_show_email');
if ($addicon && !isset($_SESSION['writeable_abook'])) {
$_SESSION['writeable_abook'] = $RCMAIL->get_address_sources(true) ? true : false;
}
foreach ($a_parts as $part) {
$j++;
$name = $part['name'];
$mailto = $part['mailto'];
$string = $part['string'];
$valid = rcube_utils::check_email($mailto, false);
// phishing email prevention (#1488981), e.g. "valid@email.addr <phishing@email.addr>"
if (!$show_email && $valid && $name && $name != $mailto && strpos($name, '@')) {
$name = '';
}
// IDNA ASCII to Unicode
if ($name == $mailto)
$name = rcube_utils::idn_to_utf8($name);
if ($string == $mailto)
$string = rcube_utils::idn_to_utf8($string);
$mailto = rcube_utils::idn_to_utf8($mailto);
if ($PRINT_MODE) {
$address = sprintf('%s &lt;%s&gt;', rcube::Q($name), rcube::Q($mailto));
}
else if ($valid) {
if ($linked) {
$attrs = array(
'href' => 'mailto:' . $mailto,
'class' => 'rcmContactAddress',
'onclick' => sprintf("return %s.command('compose','%s',this)",
rcmail_output::JS_OBJECT_NAME, rcube::JQ(format_email_recipient($mailto, $name))),
);
if ($show_email && $name && $mailto) {
$content = rcube::Q($name ? sprintf('%s <%s>', $name, $mailto) : $mailto);
}
else {
$content = rcube::Q($name ?: $mailto);
$attrs['title'] = $mailto;
}
$address = html::a($attrs, $content);
}
else {
$address = html::span(array('title' => $mailto, 'class' => "rcmContactAddress"),
rcube::Q($name ?: $mailto));
}
if ($addicon && $_SESSION['writeable_abook']) {
$label = $RCMAIL->gettext('addtoaddressbook');
$icon = html::img(array(
'src' => $RCMAIL->output->abs_url($addicon, true),
'alt' => $label,
'class' => 'noselect',
));
$address .= html::a(array(
'href' => "#add",
'title' => $label,
'class' => 'rcmaddcontact',
'onclick' => sprintf("return %s.command('add-contact','%s',this)",
rcmail_output::JS_OBJECT_NAME, rcube::JQ($string)),
),
$addicon == 'virtual' ? '' : $icon
);
}
}
else {
$address = $name ? rcube::Q($name) : '';
if ($mailto) {
$address = trim($address . ' ' . rcube::Q($name ? sprintf('<%s>', $mailto) : $mailto));
}
}
$address = html::span('adr', $address);
$allvalues[] = $address;
if (!$moreadrs) {
$out .= ($out ? ', ' : '') . $address;
}
if ($max && $j == $max && $c > $j) {
if ($linked) {
$moreadrs = $c - $j;
}
else {
$out .= '...';
break;
}
}
}
if ($moreadrs) {
$label = rcube::Q($RCMAIL->gettext(array('name' => 'andnmore', 'vars' => array('nr' => $moreadrs))));
if ($PRINT_MODE) {
$out .= ' ' . html::a(array(
'href' => '#more',
'class' => 'morelink',
'onclick' => '$(this).hide().next().show()',
), $label)
. html::span(array('style' => 'display:none'), join(', ', $allvalues));
}
else {
$out .= ' ' . html::a(array(
'href' => '#more',
'class' => 'morelink',
'onclick' => sprintf("return %s.show_popup_dialog('%s','%s')",
rcmail_output::JS_OBJECT_NAME,
rcube::JQ(join(', ', $allvalues)),
rcube::JQ($title))
), $label);
}
}
return $out;
}
/**
* Wrap text to a given number of characters per line
* but respect the mail quotation of replies messages (>).
* Finally add another quotation level by prepending the lines
* with >
*
* @param string Text to wrap
* @param int The line width
* @param bool Enable quote indentation
* @return string The wrapped text
*/
function rcmail_wrap_and_quote($text, $length = 72, $quote = true)
{
// Rebuild the message body with a maximum of $max chars, while keeping quoted message.
$max = max(75, $length + 8);
$lines = preg_split('/\r?\n/', trim($text));
$out = '';
foreach ($lines as $line) {
// don't wrap already quoted lines
if ($line[0] == '>') {
$line = rtrim($line);
if ($quote) {
$line = '>' . $line;
}
}
// wrap lines above the length limit, but skip these
// special lines with links list created by rcube_html2text
else if (mb_strlen($line) > $max && !preg_match('|^\[[0-9]+\] https?://\S+$|', $line)) {
$newline = '';
foreach (explode("\n", rcube_mime::wordwrap($line, $length - 2)) as $l) {
if ($quote) {
$newline .= strlen($l) ? "> $l\n" : ">\n";
}
else {
$newline .= "$l\n";
}
}
$line = rtrim($newline);
}
else if ($quote) {
$line = '> ' . $line;
}
// Append the line
$out .= $line . "\n";
}
return rtrim($out, "\n");
}
/**
* Send the MDN response
*
* @param mixed $message Original message object (rcube_message) or UID
* @param array $smtp_error SMTP error array (reference)
*
* @return boolean Send status
*/
function rcmail_send_mdn($message, &$smtp_error)
{
global $RCMAIL;
if (!is_object($message) || !is_a($message, 'rcube_message')) {
$message = new rcube_message($message);
}
if ($message->headers->mdn_to && empty($message->headers->flags['MDNSENT']) &&
($RCMAIL->storage->check_permflag('MDNSENT') || $RCMAIL->storage->check_permflag('*'))
) {
$identity = rcmail_sendmail::identity_select($message);
$sender = format_email_recipient($identity['email'], $identity['name']);
$recipient = array_shift(rcube_mime::decode_address_list(
$message->headers->mdn_to, 1, true, $message->headers->charset));
$mailto = $recipient['mailto'];
$compose = new Mail_mime("\r\n");
$compose->setParam('text_encoding', 'quoted-printable');
$compose->setParam('html_encoding', 'quoted-printable');
$compose->setParam('head_encoding', 'quoted-printable');
$compose->setParam('head_charset', RCUBE_CHARSET);
$compose->setParam('html_charset', RCUBE_CHARSET);
$compose->setParam('text_charset', RCUBE_CHARSET);
// compose headers array
$headers = array(
'Date' => $RCMAIL->user_date(),
'From' => $sender,
'To' => $message->headers->mdn_to,
'Subject' => $RCMAIL->gettext('receiptread') . ': ' . $message->subject,
'Message-ID' => $RCMAIL->gen_message_id($identity['email']),
'X-Sender' => $identity['email'],
'References' => trim($message->headers->references . ' ' . $message->headers->messageID),
'In-Reply-To' => $message->headers->messageID,
);
$report = "Final-Recipient: rfc822; {$identity['email']}\r\n"
. "Original-Message-ID: {$message->headers->messageID}\r\n"
. "Disposition: manual-action/MDN-sent-manually; displayed\r\n";
if ($message->headers->to) {
$report .= "Original-Recipient: {$message->headers->to}\r\n";
}
if ($agent = $RCMAIL->config->get('useragent')) {
$headers['User-Agent'] = $agent;
$report .= "Reporting-UA: $agent\r\n";
}
$to = rcube_mime::decode_mime_string($message->headers->to, $message->headers->charset);
$date = $RCMAIL->format_date($message->headers->date, $RCMAIL->config->get('date_long'));
$body = $RCMAIL->gettext("yourmessage") . "\r\n\r\n" .
"\t" . $RCMAIL->gettext("to") . ": {$to}\r\n" .
"\t" . $RCMAIL->gettext("subject") . ": {$message->subject}\r\n" .
"\t" . $RCMAIL->gettext("date") . ": {$date}\r\n" .
"\r\n" . $RCMAIL->gettext("receiptnote");
$compose->headers(array_filter($headers));
$compose->setContentType('multipart/report', array('report-type'=> 'disposition-notification'));
$compose->setTXTBody(rcube_mime::wordwrap($body, 75, "\r\n"));
$compose->addAttachment($report, 'message/disposition-notification', 'MDNPart2.txt', false, '7bit', 'inline');
// SMTP options
$options = array('mdn_use_from' => (bool) $RCMAIL->config->get('mdn_use_from'));
$sent = $RCMAIL->deliver_message($compose, $identity['email'], $mailto, $smtp_error, $body_file, $options, true);
if ($sent) {
$RCMAIL->storage->set_flag($message->uid, 'MDNSENT');
return true;
}
}
return false;
}
/**
* Detect recipient identity from specified message
* @deprecated Use rcmail_sendmail::identity_select()
*/
function rcmail_identity_select($MESSAGE, $identities = null, $compose_mode = 'reply')
{
return rcmail_sendmail::identity_select($MESSAGE, $identities, $compose_mode);
}
// Fixes some content-type names
function rcmail_fix_mimetype($name)
{
$map = array(
'image/x-ms-bmp' => 'image/bmp', // #1490282
);
$name = strtolower($name);
if ($alias = $map[$name]) {
$name = $alias;
}
// Some versions of Outlook create garbage Content-Type:
// application/pdf.A520491B_3BF7_494D_8855_7FAC2C6C0608
else if (preg_match('/^application\/pdf.+/', $name)) {
$name = 'application/pdf';
}
// treat image/pjpeg (image/pjpg, image/jpg) as image/jpeg (#1489097)
else if (preg_match('/^image\/p?jpe?g$/', $name)) {
$name = 'image/jpeg';
}
return $name;
}
// return attachment filename, handle empty filename case
function rcmail_attachment_name($attachment, $display = false)
{
global $RCMAIL;
$filename = (string) $attachment->filename;
$filename = str_replace(array("\r", "\n"), '', $filename);
if ($filename === '') {
if ($attachment->mimetype == 'text/html') {
$filename = $RCMAIL->gettext('htmlmessage');
}
else {
$ext = (array) rcube_mime::get_mime_extensions($attachment->mimetype);
$ext = array_shift($ext);
$filename = $RCMAIL->gettext('messagepart') . ' ' . $attachment->mime_id;
if ($ext) {
$filename .= '.' . $ext;
}
}
}
// Display smart names for some known mimetypes
if ($display) {
if (preg_match('/application\/(pgp|pkcs7)-signature/i', $attachment->mimetype)) {
$filename = $RCMAIL->gettext('digitalsig');
}
}
return $filename;
}
function rcmail_search_filter($attrib)
{
global $RCMAIL;
if (!strlen($attrib['id'])) {
$attrib['id'] = 'rcmlistfilter';
}
if (!rcube_utils::get_boolean($attrib['noevent'])) {
$attrib['onchange'] = rcmail_output::JS_OBJECT_NAME.'.filter_mailbox(this.value)';
}
// Content-Type values of messages with attachments
// the same as in app.js:add_message_row()
$ctypes = array('application/', 'multipart/m', 'multipart/signed', 'multipart/report');
// Build search string of "with attachment" filter
$attachment = trim(str_repeat(' OR', count($ctypes)-1));
foreach ($ctypes as $type) {
$attachment .= ' HEADER Content-Type ' . rcube_imap_generic::escape($type);
}
$select = new html_select($attrib);
$select->add($RCMAIL->gettext('all'), 'ALL');
$select->add($RCMAIL->gettext('unread'), 'UNSEEN');
$select->add($RCMAIL->gettext('flagged'), 'FLAGGED');
$select->add($RCMAIL->gettext('unanswered'), 'UNANSWERED');
if (!$RCMAIL->config->get('skip_deleted')) {
$select->add($RCMAIL->gettext('deleted'), 'DELETED');
$select->add($RCMAIL->gettext('undeleted'), 'UNDELETED');
}
$select->add($RCMAIL->gettext('withattachment'), $attachment);
$select->add($RCMAIL->gettext('priority').': '.$RCMAIL->gettext('highest'), 'HEADER X-PRIORITY 1');
$select->add($RCMAIL->gettext('priority').': '.$RCMAIL->gettext('high'), 'HEADER X-PRIORITY 2');
$select->add($RCMAIL->gettext('priority').': '.$RCMAIL->gettext('normal'), 'NOT HEADER X-PRIORITY 1 NOT HEADER X-PRIORITY 2 NOT HEADER X-PRIORITY 4 NOT HEADER X-PRIORITY 5');
$select->add($RCMAIL->gettext('priority').': '.$RCMAIL->gettext('low'), 'HEADER X-PRIORITY 4');
$select->add($RCMAIL->gettext('priority').': '.$RCMAIL->gettext('lowest'), 'HEADER X-PRIORITY 5');
$RCMAIL->output->add_gui_object('search_filter', $attrib['id']);
$selected = rcube_utils::get_input_value('_filter', rcube_utils::INPUT_GET);
if (!$selected && $_REQUEST['_search']) {
$selected = $_SESSION['search_filter'];
}
return $select->show($selected ?: 'ALL');
}
function rcmail_search_interval($attrib)
{
global $RCMAIL;
if (!strlen($attrib['id'])) {
$attrib['id'] = 'rcmsearchinterval';
}
$select = new html_select($attrib);
$select->add('', '');
foreach (array('1W', '1M', '1Y', '-1W', '-1M', '-1Y') as $value) {
$select->add($RCMAIL->gettext('searchinterval' . $value), $value);
}
$RCMAIL->output->add_gui_object('search_interval', $attrib['id']);
return $select->show($_REQUEST['_search'] ? $_SESSION['search_interval'] : '');
}
function rcmail_message_error()
{
global $RCMAIL;
// ... display message error page
if ($RCMAIL->output->template_exists('messageerror')) {
// Set env variables for messageerror.html template
if ($RCMAIL->action == 'show') {
$mbox_name = $RCMAIL->storage->get_folder();
$RCMAIL->output->set_env('mailbox', $mbox_name);
$RCMAIL->output->set_env('uid', null);
}
$RCMAIL->output->show_message('messageopenerror', 'error');
$RCMAIL->output->send('messageerror');
}
else {
$RCMAIL->raise_error(array('code' => 410), false, true);
}
}
function rcmail_message_import_form($attrib = array())
{
global $RCMAIL;
$RCMAIL->output->add_label('selectimportfile', 'importwait', 'importmessages', 'import');
$description = $RCMAIL->gettext('mailimportdesc');
$input_attr = array(
'multiple' => true,
'name' => '_file[]',
'accept' => '.eml, .mbox, .msg, message/rfc822, text/*',
);
if (class_exists('ZipArchive', false)) {
$input_attr['accept'] .= '.zip, application/zip, application/x-zip';
$description .= ' ' . $RCMAIL->gettext('mailimportzip');
}
$attrib['prefix'] = html::tag('input', array('type' => 'hidden', 'name' => '_unlock', 'value' => ''))
. html::tag('input', array('type' => 'hidden', 'name' => '_framed', 'value' => '1'))
. html::p(null, $description);
return $RCMAIL->upload_form($attrib, 'importform', 'import-messages', $input_attr);
}
/**
* Add groups from the given address source to the address book widget
*/
function rcmail_compose_contact_groups($abook, $source_id, $search = null, $search_mode = 0)
{
global $RCMAIL, $OUTPUT;
$jsresult = array();
foreach ($abook->list_groups($search, $search_mode) as $group) {
$abook->reset();
$abook->set_group($group['ID']);
// group (distribution list) with email address(es)
if ($group['email']) {
foreach ((array)$group['email'] as $email) {
$row_id = 'G'.$group['ID'];
$jsresult[$row_id] = format_email_recipient($email, $group['name']);
$OUTPUT->command('add_contact_row', $row_id, array(
'contactgroup' => html::span(array('title' => $email), rcube::Q($group['name']))), 'group');
}
}
// make virtual groups clickable to list their members
else if ($group['virtual']) {
$row_id = 'G'.$group['ID'];
$OUTPUT->command('add_contact_row', $row_id, array(
'contactgroup' => html::a(array(
'href' => '#list',
'rel' => $group['ID'],
'title' => $RCMAIL->gettext('listgroup'),
'onclick' => sprintf("return %s.command('pushgroup',{'source':'%s','id':'%s'},this,event)",
rcmail_output::JS_OBJECT_NAME, $source_id, $group['ID']),
), rcube::Q($group['name']) . '&nbsp;' . html::span('action', '&raquo;'))),
'group',
array('ID' => $group['ID'], 'name' => $group['name'], 'virtual' => true));
}
// show group with count
else if (($result = $abook->count()) && $result->count) {
$row_id = 'E'.$group['ID'];
$jsresult[$row_id] = $group['name'];
$OUTPUT->command('add_contact_row', $row_id, array(
'contactgroup' => rcube::Q($group['name'] . ' (' . intval($result->count) . ')')), 'group');
}
}
$abook->reset();
$abook->set_group(0);
return $jsresult;
}
function rcmail_save_attachment($message, $pid, $compose_id, $params = array())
{
global $COMPOSE;
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
if ($pid) {
// attachment requested
$part = $message->mime_parts[$pid];
$size = $part->size;
$mimetype = $part->ctype_primary . '/' . $part->ctype_secondary;
$filename = $params['filename'] ?: rcmail_attachment_name($part);
}
else if (is_object($message)) {
// the whole message requested
$size = $message->size;
$mimetype = 'message/rfc822';
$filename = $params['filename'] ?: 'message_rfc822.eml';
}
else if (is_string($message)) {
// the whole message requested
$size = strlen($message);
$data = $message;
$mimetype = $params['mimetype'];
$filename = $params['filename'];
}
if (!isset($data)) {
// don't load too big attachments into memory
if (!rcube_utils::mem_check($size)) {
$path = rcube_utils::temp_filename('attmnt');
if ($fp = fopen($path, 'w')) {
if ($pid) {
// part body
$message->get_part_body($pid, false, 0, $fp);
}
else {
// complete message
$storage->get_raw_body($message->uid, $fp);
}
fclose($fp);
}
else {
return false;
}
}
else if ($pid) {
// part body
$data = $message->get_part_body($pid);
}
else {
// complete message
$data = $storage->get_raw_body($message->uid);
}
}
$attachment = array(
'group' => $compose_id,
'name' => $filename,
'mimetype' => $mimetype,
'content_id' => $part ? $part->content_id : null,
'data' => $data,
'path' => $path,
'size' => $path ? filesize($path) : strlen($data),
'charset' => $part ? $part->charset : $params['charset'],
);
$attachment = $rcmail->plugins->exec_hook('attachment_save', $attachment);
if ($attachment['status']) {
unset($attachment['data'], $attachment['status'], $attachment['content_id'], $attachment['abort']);
// rcube_session::append() replaces current session data with the old values
// (in rcube_session::reload()). This is a problem in 'compose' action, because before
// the first append() use we set some important data in the session.
// It also overwrites attachments list. Fixing reload() is not so simple if possible
// as we don't really know what has been added and what removed in meantime.
// So, for now we'll do not use append() on 'compose' action (#1490608).
if ($rcmail->action == 'compose') {
$COMPOSE['attachments'][$attachment['id']] = $attachment;
}
else {
$rcmail->session->append('compose_data_' . $compose_id . '.attachments', $attachment['id'], $attachment);
}
return $attachment;
}
else if ($path) {
@unlink($path);
}
return false;
}
// Return mimetypes supported by the browser
function rcmail_supported_mimetypes()
{
$rcmail = rcube::get_instance();
// mimetypes supported by the browser (default settings)
$mimetypes = (array) $rcmail->config->get('client_mimetypes');
// Remove unsupported types, which makes that attachment which cannot be
// displayed in a browser will be downloaded directly without displaying an overlay page
if (empty($_SESSION['browser_caps']['pdf']) && ($key = array_search('application/pdf', $mimetypes)) !== false) {
unset($mimetypes[$key]);
}
if (empty($_SESSION['browser_caps']['flash']) && ($key = array_search('application/x-shockwave-flash', $mimetypes)) !== false) {
unset($mimetypes[$key]);
}
foreach (array('tiff', 'webp') as $type) {
if (empty($_SESSION['browser_caps'][$type]) && ($key = array_search('image/' . $type, $mimetypes)) !== false) {
// can we convert it to jpeg?
if (!rcube_image::is_convertable('image/' . $type)) {
unset($mimetypes[$key]);
}
}
}
// @TODO: support mail preview for compose attachments
if ($rcmail->action != 'compose' && !in_array('message/rfc822', $mimetypes)) {
$mimetypes[] = 'message/rfc822';
}
return array_values($mimetypes);
}
diff --git a/program/steps/mail/get.inc b/program/steps/mail/get.inc
index c757dcb52..3a22d978c 100644
--- a/program/steps/mail/get.inc
+++ b/program/steps/mail/get.inc
@@ -1,713 +1,711 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/get.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2016, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Delivering a specific uploaded file or mail message attachment |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
// This resets X-Frame-Options for framed output (#6688)
$OUTPUT->page_headers();
// show loading page
if (!empty($_GET['_preload'])) {
unset($_GET['_preload']);
unset($_GET['_safe']);
$url = $RCMAIL->url($_GET + array('_mimewarning' => 1, '_embed' => 1));
$message = $RCMAIL->gettext('loadingdata');
header('Content-Type: text/html; charset=' . RCUBE_CHARSET);
print "<html>\n<head>\n"
. '<meta http-equiv="refresh" content="0; url='.rcube::Q($url).'">' . "\n"
. '<meta http-equiv="content-type" content="text/html; charset='.RCUBE_CHARSET.'">' . "\n"
. "</head>\n<body>\n$message\n</body>\n</html>";
exit;
}
$attachment = new rcmail_attachment_handler;
$mimetype = $attachment->mimetype;
$filename = $attachment->filename;
// show part page
if (!empty($_GET['_frame'])) {
$OUTPUT->set_pagetitle($filename);
// register UI objects
$OUTPUT->add_handlers(array(
'messagepartframe' => 'rcmail_message_part_frame',
'messagepartcontrols' => 'rcmail_message_part_controls',
));
$part_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_GET);
$uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GET);
// message/rfc822 preview (Note: handle also multipart/ parts, they can
// come from Enigma, which replaces message/rfc822 with real mimetype)
if ($part_id && ($mimetype == 'message/rfc822' || strpos($mimetype, 'multipart/') === 0)) {
$uid = preg_replace('/\.[0-9.]+/', '', $uid);
$uid .= '.' . $part_id;
$OUTPUT->set_env('is_message', true);
}
$OUTPUT->set_env('mailbox', $RCMAIL->storage->get_folder());
$OUTPUT->set_env('uid', $uid);
$OUTPUT->set_env('part', $part_id);
$OUTPUT->set_env('filename', $filename);
$OUTPUT->set_env('mimetype', $mimetype);
$OUTPUT->send('messagepart');
exit;
}
// render thumbnail of an image attachment
if (!empty($_GET['_thumb']) && $attachment->is_valid()) {
$thumbnail_size = $RCMAIL->config->get('image_thumbnail_size', 240);
$file_ident = $attachment->ident;
$thumb_name = 'thumb' . md5($file_ident . ':' . $RCMAIL->user->ID . ':' . $thumbnail_size);
$cache_file = rcube_utils::temp_filename($thumb_name, false, false);
// render thumbnail image if not done yet
if (!is_file($cache_file) && $attachment->body_to_file($orig_name = $cache_file . '.tmp')) {
$image = new rcube_image($orig_name);
if ($imgtype = $image->resize($thumbnail_size, $cache_file, true)) {
$mimetype = 'image/' . $imgtype;
}
else {
// Resize failed, we need to check the file mimetype
// So, we do not exit here, but goto generic file body handler below
$_GET['_thumb'] = 0;
$_REQUEST['_embed'] = 1;
}
}
if (!empty($_GET['_thumb'])) {
if (is_file($cache_file)) {
$RCMAIL->output->future_expire_header(3600);
header('Content-Type: ' . $mimetype);
header('Content-Length: ' . filesize($cache_file));
readfile($cache_file);
}
exit;
}
}
// Handle attachment body (display or download)
if (empty($_GET['_thumb']) && $attachment->is_valid()) {
// require CSRF protected url for downloads
if (!empty($_GET['_download'])) {
$RCMAIL->request_security_check(rcube_utils::INPUT_GET);
}
$extensions = rcube_mime::get_mime_extensions($mimetype);
// compare file mimetype with the stated content-type headers and file extension to avoid malicious operations
if (!empty($_REQUEST['_embed']) && empty($_REQUEST['_nocheck'])) {
$file_extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
// 1. compare filename suffix with expected suffix derived from mimetype
$valid = $file_extension && in_array($file_extension, (array)$extensions) || empty($extensions) || !empty($_REQUEST['_mimeclass']);
// 2. detect the real mimetype of the attachment part and compare it with the stated mimetype and filename extension
if ($valid || !$file_extension || $mimetype == 'application/octet-stream' || stripos($mimetype, 'text/') === 0) {
$tmp_body = $attachment->body(2048);
// detect message part mimetype
$real_mimetype = rcube_mime::file_content_type($tmp_body, $filename, $mimetype, true, true);
list($real_ctype_primary, $real_ctype_secondary) = explode('/', $real_mimetype);
// accept text/plain with any extension
if ($real_mimetype == 'text/plain' && $real_mimetype == $mimetype) {
$valid_extension = true;
}
// ignore differences in text/* mimetypes. Filetype detection isn't very reliable here
else if ($real_ctype_primary == 'text' && strpos($mimetype, $real_ctype_primary) === 0) {
$real_mimetype = $mimetype;
$valid_extension = true;
}
// ignore filename extension if mimeclass matches (#1489029)
else if (!empty($_REQUEST['_mimeclass']) && $real_ctype_primary == $_REQUEST['_mimeclass']) {
$valid_extension = true;
}
else {
// get valid file extensions
$extensions = rcube_mime::get_mime_extensions($real_mimetype);
$valid_extension = !$file_extension || empty($extensions) || in_array($file_extension, (array)$extensions);
}
// fix mimetype for images wrongly declared as octet-stream
if ($mimetype == 'application/octet-stream' && strpos($real_mimetype, 'image/') === 0 && $valid_extension) {
$mimetype = $real_mimetype;
}
// fix mimetype for images with wrong mimetype
else if (strpos($real_mimetype, 'image/') === 0 && strpos($mimetype, 'image/') === 0) {
$mimetype = $real_mimetype;
}
// "fix" real mimetype the same way the original is before comparison
$real_mimetype = rcmail_fix_mimetype($real_mimetype);
$valid = $real_mimetype == $mimetype && $valid_extension;
}
else {
$real_mimetype = $mimetype;
}
// show warning if validity checks failed
if (!$valid) {
// send blocked.gif for expected images
if (empty($_REQUEST['_mimewarning']) && strpos($mimetype, 'image/') === 0) {
// Do not cache. Failure might be the result of a misconfiguration, thus real content should be returned once fixed.
$content = $RCMAIL->get_resource_content('blocked.gif');
$OUTPUT->nocacheing_headers();
header("Content-Type: image/gif");
header("Content-Transfer-Encoding: binary");
header("Content-Length: " . strlen($content));
echo $content;
}
// html warning with a button to load the file anyway
else {
$OUTPUT = new rcmail_html_page();
$OUTPUT->register_inline_warning(
$RCMAIL->gettext(array(
'name' => 'attachmentvalidationerror',
'vars' => array(
'expected' => $mimetype . ($file_extension ? " (.$file_extension)" : ''),
'detected' => $real_mimetype . ($extensions[0] ? " (.$extensions[0])" : ''),
)
)
),
$RCMAIL->gettext('showanyway'),
$RCMAIL->url(array_merge($_GET, array('_nocheck' => 1)))
);
$OUTPUT->write();
}
exit;
}
}
// TIFF/WEBP to JPEG conversion, if needed
foreach (array('tiff', 'webp') as $type) {
$img_support = !empty($_SESSION['browser_caps']) && !empty($_SESSION['browser_caps'][$type]);
if (!empty($_REQUEST['_embed']) && !$img_support
&& $attachment->image_type() == 'image/' . $type
&& rcube_image::is_convertable('image/' . $type)
) {
$convert2jpeg = true;
$mimetype = 'image/jpeg';
break;
}
}
// deliver part content
if ($mimetype == 'text/html' && empty($_GET['_download'])) {
$OUTPUT = new rcmail_html_page();
// Check if we have enough memory to handle the message in it
// #1487424: we need up to 10x more memory than the body
if (!rcube_utils::mem_check($attachment->size * 10)) {
$OUTPUT->register_inline_warning(
$RCMAIL->gettext('messagetoobig'),
$RCMAIL->gettext('download'),
$RCMAIL->url(array_merge($_GET, array('_download' => 1)))
);
}
else {
// render HTML body
$out = $attachment->html();
// insert remote objects warning into HTML body
if ($REMOTE_OBJECTS) {
$OUTPUT->register_inline_warning(
$RCMAIL->gettext('blockedresources'),
$RCMAIL->gettext('allow'),
$RCMAIL->url(array_merge($_GET, array('_safe' => 1)))
);
}
}
$OUTPUT->write($out);
exit;
}
// add filename extension if missing
if (!pathinfo($filename, PATHINFO_EXTENSION) && ($extensions = rcube_mime::get_mime_extensions($mimetype))) {
$filename .= '.' . $extensions[0];
}
$OUTPUT->download_headers($filename, array(
'type' => $mimetype,
'type_charset' => $attachment->charset,
'disposition' => !empty($_GET['_download']) ? 'attachment' : 'inline',
));
// handle tiff to jpeg conversion
if (!empty($convert2jpeg)) {
$file_path = rcube_utils::temp_filename('attmnt');
// convert image to jpeg and send it to the browser
if ($attachment->body_to_file($file_path)) {
$image = new rcube_image($file_path);
if ($image->convert(rcube_image::TYPE_JPG, $file_path)) {
header("Content-Length: " . filesize($file_path));
readfile($file_path);
}
}
}
else {
$attachment->output($mimetype);
}
exit;
}
// if we arrive here, the requested part was not found
header('HTTP/1.1 404 Not Found');
exit;
/**
* Attachment properties table
*/
function rcmail_message_part_controls($attrib)
{
global $attachment, $RCMAIL;
if (!$attachment->is_valid()) {
return '';
}
$table = new html_table(array('cols' => 2));
$table->add('title', rcube::Q($RCMAIL->gettext('namex')).':');
$table->add('header', rcube::Q($attachment->filename));
$table->add('title', rcube::Q($RCMAIL->gettext('type')).':');
$table->add('header', rcube::Q($attachment->mimetype));
$table->add('title', rcube::Q($RCMAIL->gettext('size')).':');
$table->add('header', rcube::Q($attachment->size()));
return $table->show($attrib);
}
/**
* Attachment preview frame
*/
function rcmail_message_part_frame($attrib)
{
global $RCMAIL;
if ($RCMAIL->output->get_env('is_message')) {
$url = array(
'task' => 'mail',
'action' => 'preview',
'uid' => $RCMAIL->output->get_env('uid'),
'mbox' => $RCMAIL->output->get_env('mailbox'),
);
}
else {
$mimetype = $RCMAIL->output->get_env('mimetype');
$url = $_GET;
$url[strpos($mimetype, 'text/') === 0 ? '_embed' : '_preload'] = 1;
unset($url['_frame']);
}
$url['_framed'] = 1; // For proper X-Frame-Options:deny handling
$attrib['src'] = $RCMAIL->url($url);
$RCMAIL->output->add_gui_object('messagepartframe', $attrib['id']);
return html::iframe($attrib);
}
/**
* Wrapper class with unified access to attachment properties and body
*
* Unified for message parts as well as uploaded attachments
*/
class rcmail_attachment_handler
{
public $filename;
public $size;
public $mimetype;
public $ident;
public $charset = RCUBE_CHARSET;
private $message;
private $part;
private $upload;
private $body;
private $body_file;
private $download = false;
/**
* Class constructor.
* Reads request parameters and initializes attachment/part props.
*/
public function __construct()
{
ob_end_clean();
$part_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_GET);
$file_id = rcube_utils::get_input_value('_file', rcube_utils::INPUT_GET);
$compose_id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GET);
$uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GET);
$rcube = rcube::get_instance();
$this->download = !empty($_GET['_download']);
// similar code as in program/steps/mail/show.inc
if (!empty($uid)) {
$rcube->config->set('prefer_html', true);
$this->message = new rcube_message($uid, null, intval($_GET['_safe']));
if ($this->part = $this->message->mime_parts[$part_id]) {
$this->filename = rcmail_attachment_name($this->part);
$this->mimetype = $this->part->mimetype;
$this->size = $this->part->size;
$this->ident = $this->message->headers->messageID . ':' . $this->part->mime_id . ':' . $this->size . ':' . $this->mimetype;
$this->charset = $this->part->charset ?: RCUBE_CHARSET;
if (empty($_GET['_frame'])) {
// allow post-processing of the attachment body
$plugin = $rcube->plugins->exec_hook('message_part_get', array(
'uid' => $uid,
'id' => $this->part->mime_id,
'mimetype' => $this->mimetype,
'part' => $this->part,
'download' => $this->download,
));
if ($plugin['abort']) {
exit;
}
// overwrite modified vars from plugin
$this->mimetype = $plugin['mimetype'];
if ($plugin['body']) {
$this->body = $plugin['body'];
$this->size = strlen($this->body);
}
}
}
}
else if ($file_id && $compose_id) {
$file_id = preg_replace('/^rcmfile/', '', $file_id);
if (($compose = $_SESSION['compose_data_' . $compose_id])
&& ($this->upload = $compose['attachments'][$file_id])
) {
$this->filename = $this->upload['name'];
$this->mimetype = $this->upload['mimetype'];
$this->size = $this->upload['size'];
$this->ident = sprintf('%s:%s%s', $compose_id, $file_id, $this->size);
$this->charset = $this->upload['charset'] ?: RCUBE_CHARSET;
}
}
if (empty($this->part) && empty($this->upload)) {
header('HTTP/1.1 404 Not Found');
exit;
}
// check connection status
self::check_storage_status();
$this->mimetype = rcmail_fix_mimetype($this->mimetype);
}
/**
* Remove temp files, etc.
*/
public function __destruct()
{
if ($this->body_file) {
@unlink($this->body_file);
}
}
/**
* Check if the object is a message part not uploaded file
*
* @return bool True if the object is a meesage part
*/
public function is_message_part()
{
return !empty($this->message);
}
/**
* Object/request status
*
* @return bool Status
*/
public function is_valid()
{
return !empty($this->part) || !empty($this->upload);
}
/**
* Return attachment/part mimetype if this is an image
* of supported type.
*
* @return string Image mimetype
*/
public function image_type()
{
$part = (object) array(
'filename' => $this->filename,
'mimetype' => $this->mimetype,
);
return rcmail_part_image_type($part);
}
/**
* Formatted attachment/part size (with units)
*
* @return string Attachment/part size (with units)
*/
public function size()
{
$part = $this->part ?: ((object) array('size' => $this->size, 'exact_size' => true));
return rcube::get_instance()->message_part_size($part);
}
/**
* Returns, prints or saves the attachment/part body
*/
public function body($size = null, $fp = null)
{
// we may have the body in memory or file already
if ($this->body !== null) {
if ($fp == -1) {
echo $size ? substr($this->body, 0, $size) : $this->body;
}
else if ($fp) {
$result = fwrite($fp, $size ? substr($this->body, $size) : $this->body) !== false;
}
else {
$result = $size ? substr($this->body, 0, $size) : $this->body;
}
}
else if ($this->body_file) {
if ($size) {
$result = file_get_contents($this->body_file, false, null, 0, $size);
}
else {
$result = file_get_contents($this->body_file);
}
if ($fp == -1) {
echo $result;
}
else if ($fp) {
$result = fwrite($fp, $result) !== false;
}
}
else if ($this->message) {
$result = $this->message->get_part_body($this->part->mime_id, false, 0, $fp);
// check connection status
if (!$fp && $this->size && empty($result)) {
self::check_storage_status();
}
}
else if ($this->upload) {
// This hook retrieves the attachment contents from the file storage backend
$attachment = rcube::get_instance()->plugins->exec_hook('attachment_get', $this->upload);
if ($fp && $fp != -1) {
if ($attachment['data']) {
$result = fwrite($fp, $size ? substr($attachment['data'], 0, $size) : $attachment['data']) !== false;
}
else if ($attachment['path']) {
if ($fh = fopen($attachment['path'], 'rb')) {
$result = stream_copy_to_stream($fh, $fp, $size ? $size : -1);
}
}
}
else {
$data = $attachment['data'];
if (!$data && $attachment['path']) {
$data = file_get_contents($attachment['path']);
}
if ($fp == -1) {
echo $size ? substr($data, 0, $size) : $data;
}
else {
$result = $size ? substr($data, 0, $size) : $data;
}
}
}
return $result;
}
/**
* Save the body to a file
*
* @param string $filename File name with path
*
* @return bool True on success, False on failure
*/
public function body_to_file($filename)
{
if ($filename && $this->size && ($fp = fopen($filename, 'w'))) {
$this->body(0, $fp);
$this->body_file = $filename;
fclose($fp);
@chmod(filename, 0600);
return true;
}
return false;
}
/**
* Output attachment body with content filtering
*/
public function output($mimetype)
{
if (!$this->size) {
return false;
}
$secure = stripos($mimetype, 'image/') === false || $this->download;
// Remove <script> in SVG images
if (!$secure && stripos($mimetype, 'image/svg') === 0) {
if (!$this->body) {
$this->body = $this->body();
if (empty($this->body)) {
return false;
}
}
echo self::svg_filter($this->body);
return true;
}
if ($this->body !== null && !$this->download) {
header("Content-Length: " . strlen($this->body));
echo $this->body;
return true;
}
// Don't be tempted to set Content-Length to $part->d_parameters['size'] (#1490482)
// RFC2183 says "The size parameter indicates an approximate size"
return $this->body(0, -1);
}
/**
* Returns formatted HTML if the attachment is HTML
*/
public function html()
{
list($type, $subtype) = explode($this->mimetype, '/');
$part = (object) array(
'charset' => $this->charset,
'ctype_secondary' => $subtype,
);
// get part body if not available
// fix formatting and charset
$body = rcube_message::format_part_body($this->body(), $part);
// show images?
$is_safe = $this->is_safe();
return rcmail_wash_html($body, array('safe' => $is_safe, 'inline_html' => false));
}
/**
* Remove <script> in SVG images
*/
public static function svg_filter($body)
{
// clean SVG with washtml
$wash_opts = array(
'show_washed' => false,
'allow_remote' => false,
'charset' => RCUBE_CHARSET,
'html_elements' => array('title'),
// 'blocked_src' => 'program/resources/blocked.gif',
);
// initialize HTML washer
$washer = new rcube_washtml($wash_opts);
// allow CSS styles, will be sanitized by rcmail_washtml_callback()
$washer->add_callback('style', 'rcmail_washtml_callback');
return $washer->wash($body);
}
/**
* Handles nicely storage connection errors
*/
public static function check_storage_status()
{
$error = rcmail::get_instance()->storage->get_error_code();
// Check if we have a connection error
if ($error == rcube_imap_generic::ERROR_BAD) {
ob_end_clean();
// Get action is often executed simultaneously.
// Some servers have MAXPERIP or other limits.
// To workaround this we'll wait for some time
// and try again (once).
// Note: Random sleep interval is used to minimize concurency
// in getting message parts
if (!isset($_GET['_redirected'])) {
usleep(rand(10,30)*100000); // 1-3 sec.
header('Location: ' . $_SERVER['REQUEST_URI'] . '&_redirected=1');
}
else {
rcube::raise_error(array(
'code' => 500, 'file' => __FILE__, 'line' => __LINE__,
'message' => 'Unable to get/display message part. IMAP connection error'),
true, true);
}
// Don't kill session, just quit (#1486995)
exit;
}
}
public function is_safe()
{
if ($this->message) {
return rcmail_check_safe($this->message);
}
return !empty($_GET['_safe']);
}
}
diff --git a/program/steps/mail/getunread.inc b/program/steps/mail/getunread.inc
index a686e8b6e..f7909db10 100644
--- a/program/steps/mail/getunread.inc
+++ b/program/steps/mail/getunread.inc
@@ -1,56 +1,54 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/getunread.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Check all mailboxes for unread messages and update GUI |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
$a_folders = $RCMAIL->storage->list_folders_subscribed('', '*', 'mail');
if (!empty($a_folders)) {
$current = $RCMAIL->storage->get_folder();
$inbox = ($current == 'INBOX');
$trash = $RCMAIL->config->get('trash_mbox');
$check_all = (bool)$RCMAIL->config->get('check_all_folders');
foreach ($a_folders as $mbox) {
$unseen_old = rcmail_get_unseen_count($mbox);
if (!$check_all && $unseen_old !== null && $mbox != $current) {
$unseen = $unseen_old;
}
else {
$unseen = $RCMAIL->storage->count($mbox, 'UNSEEN', $unseen_old === null);
}
// call it always for current folder, so it can update counter
// after possible message status change when opening a message
// not in preview frame
if ($unseen || $unseen_old === null || $mbox == $current) {
$OUTPUT->command('set_unread_count', $mbox, $unseen, $inbox && $mbox_row == 'INBOX');
}
rcmail_set_unseen_count($mbox, $unseen);
// set trash folder state
if ($mbox === $trash) {
$OUTPUT->command('set_trash_count', $RCMAIL->storage->count($mbox, 'EXISTS'));
}
}
}
$OUTPUT->send();
diff --git a/program/steps/mail/headers.inc b/program/steps/mail/headers.inc
index 5274586ae..ff139bedc 100644
--- a/program/steps/mail/headers.inc
+++ b/program/steps/mail/headers.inc
@@ -1,71 +1,69 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/headers.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2016, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Fetch message headers in raw format for display |
- | |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
$uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GP);
$inline = $OUTPUT instanceof rcmail_output_html;
if ($uid) {
if ($pos = strpos($uid, '.')) {
$message = new rcube_message($uid);
$source = $message->get_part_body(substr($uid, $pos + 1));
$source = substr($source, 0, strpos($source, "\r\n\r\n"));
}
else {
$source = $RCMAIL->storage->get_raw_headers($uid);
}
if ($source !== false) {
$source = trim(rcube_charset::clean($source));
$source = htmlspecialchars($source, ENT_COMPAT | ENT_HTML401, RCUBE_CHARSET);
$source = preg_replace(
array(
'/\n[\t\s]+/',
'/^([a-z0-9_:-]+)/im',
'/\r?\n/'
),
array(
"\n&nbsp;&nbsp;&nbsp;&nbsp;",
'<font class="bold">\1</font>',
'<br />'
), $source);
$OUTPUT->add_handlers(array('dialogcontent' => 'rcmail_headers_output'));
if ($inline) {
$OUTPUT->set_env('dialog_class', 'text-nowrap');
}
else {
$OUTPUT->command('set_headers', $source);
}
}
else if (!$inline) {
$RCMAIL->output->show_message('messageopenerror', 'error');
}
$OUTPUT->send($inline ? 'dialog' : null);
}
function rcmail_headers_output()
{
global $source;
return $source;
}
diff --git a/program/steps/mail/import.inc b/program/steps/mail/import.inc
index 98a62063b..995c12f75 100644
--- a/program/steps/mail/import.inc
+++ b/program/steps/mail/import.inc
@@ -1,196 +1,194 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/import.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2014, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Save the uploaded file(s) as messages to the current IMAP folder |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
// clear all stored output properties (like scripts and env vars)
$OUTPUT->reset();
if (is_array($_FILES['_file'])) {
$imported = 0;
$folder = $RCMAIL->storage->get_folder();
foreach ((array)$_FILES['_file']['tmp_name'] as $i => $filepath) {
// Process uploaded file if there is no error
$err = $_FILES['_file']['error'][$i];
if (!$err) {
// check file content type first
$ctype = rcube_mime::file_content_type($filepath, $_FILES['_file']['name'][$i], $_FILES['_file']['type'][$i]);
list($mtype_primary, $mtype_secondary) = explode('/', $ctype);
if (in_array($ctype, array('application/zip', 'application/x-zip'))) {
$filepath = rcmail_zip_extract($filepath);
if (empty($filepath)) {
continue;
}
}
else if (!in_array($mtype_primary, array('text', 'message'))) {
continue;
}
foreach ((array) $filepath as $file) {
// read the first few lines to detect header-like structure
$fp = fopen($file, 'r');
do {
$line = fgets($fp);
}
while ($line !== false && trim($line) == '');
if (!preg_match('/^From .+/', $line) && !preg_match('/^[a-z-_]+:\s+.+/i', $line)) {
continue;
}
$message = $lastline = '';
fseek($fp, 0);
while (($line = fgets($fp)) !== false) {
// importing mbox file, split by From - lines
if ($lastline === '' && strncmp($line, 'From ', 5) === 0 && strlen($line) > 5) {
if (!empty($message)) {
$imported += (int) rcmail_save_message($folder, $message);
}
$message = $line;
$lastline = '';
continue;
}
$message .= $line;
$lastline = rtrim($line);
}
if (!empty($message)) {
$imported += (int) rcmail_save_message($folder, $message);
}
// remove temp files extracted from zip
if (is_array($filepath)) {
unlink($file);
}
}
}
else if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
$size = $RCMAIL->show_bytes(rcube_utils::max_upload_size());
$msg = $RCMAIL->gettext(array('name' => 'filesizeerror', 'vars' => array('size' => $size)));
$OUTPUT->command('display_message', $msg, 'error');
}
else if ($err) {
$OUTPUT->show_message('fileuploaderror', 'error');
}
}
if ($imported) {
$OUTPUT->show_message($RCMAIL->gettext(array('name' => 'importmessagesuccess', 'nr' => $imported, 'vars' => array('nr' => $imported))), 'confirmation');
$OUTPUT->command('command', 'list');
}
else {
$OUTPUT->show_message('importmessageerror', 'error');
}
}
else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// if filesize exceeds post_max_size then $_FILES array is empty,
// show filesizeerror instead of fileuploaderror
if ($maxsize = ini_get('post_max_size'))
$msg = $RCMAIL->gettext(array('name' => 'filesizeerror', 'vars' => array('size' => $RCMAIL->show_bytes(parse_bytes($maxsize)))));
else
$msg = $RCMAIL->gettext('fileuploaderror');
$OUTPUT->command('display_message', $msg, 'error');
}
// send html page with JS calls as response
$OUTPUT->send('iframe');
function rcmail_zip_extract($path)
{
if (!class_exists('ZipArchive', false)) {
return;
}
$rcmail = rcmail::get_instance();
$zip = new ZipArchive;
$files = array();
if ($zip->open($path)) {
for ($i = 0; $i < $zip->numFiles; $i++) {
$entry = $zip->getNameIndex($i);
$tmpfname = rcube_utils::temp_filename('zipimport');
if (copy("zip://$path#$entry", $tmpfname)) {
$ctype = rcube_mime::file_content_type($tmpfname, $entry);
list($mtype_primary, $mtype_secondary) = explode('/', $ctype);
if (in_array($mtype_primary, array('text', 'message'))) {
$files[] = $tmpfname;
}
else {
unlink($tmpfname);
}
}
}
$zip->close();
}
return $files;
}
function rcmail_save_message($folder, &$message)
{
if (strncmp($message, 'From ', 5) === 0) {
// Extract the mbox from_line
$pos = strpos($message, "\n");
$from = substr($message, 0, $pos);
$message = substr($message, $pos + 1);
// Read the received date, support only known date formats
// RFC4155: "Sat Jan 3 01:05:34 1996"
$mboxdate_rx = '/^([a-z]{3} [a-z]{3} [0-9 ][0-9] [0-9]{2}:[0-9]{2}:[0-9]{2} [0-9]{4})/i';
// Roundcube/Zipdownload: "12-Dec-2016 10:56:33 +0100"
$imapdate_rx = '/^([0-9]{1,2}-[a-z]{3}-[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2} [0-9+-]{5})/i';
if (($pos = strpos($from, ' ', 6)) && ($dt_str = substr($from, $pos + 1))
&& (preg_match($mboxdate_rx, $dt_str, $m) || preg_match($imapdate_rx, $dt_str, $m))
) {
try {
$date = new DateTime($m[0], new DateTimeZone('UTC'));
}
catch (Exception $e) {
// ignore
}
}
}
// unquote ">From " lines in message body
$message = preg_replace('/\n>([>]*)From /', "\n\\1From ", $message);
$message = rtrim($message);
$rcmail = rcmail::get_instance();
if ($rcmail->storage->save_message($folder, $message, '', false, array(), $date)) {
return true;
}
rcube::raise_error("Failed to import message to $folder", true, false);
return false;
}
diff --git a/program/steps/mail/list.inc b/program/steps/mail/list.inc
index 215d09e73..a40c782e7 100644
--- a/program/steps/mail/list.inc
+++ b/program/steps/mail/list.inc
@@ -1,147 +1,145 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/list.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2014, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Send message list to client (as remote response) |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
if (!$OUTPUT->ajax_call) {
return;
}
$save_arr = array();
$dont_override = (array) $RCMAIL->config->get('dont_override');
// is there a sort type for this request?
if ($sort = rcube_utils::get_input_value('_sort', rcube_utils::INPUT_GET)) {
// yes, so set the sort vars
list($sort_col, $sort_order) = explode('_', $sort);
// set session vars for sort (so next page and task switch know how to sort)
if (!in_array('message_sort_col', $dont_override)) {
$_SESSION['sort_col'] = $save_arr['message_sort_col'] = $sort_col;
}
if (!in_array('message_sort_order', $dont_override)) {
$_SESSION['sort_order'] = $save_arr['message_sort_order'] = $sort_order;
}
}
// register layout change
if ($layout = rcube_utils::get_input_value('_layout', rcube_utils::INPUT_GET)) {
$OUTPUT->set_env('layout', $layout);
$save_arr['layout'] = $layout;
// force header replace on layout change
$cols = $_SESSION['list_attrib']['columns'];
}
// is there a set of columns for this request?
else if ($cols = rcube_utils::get_input_value('_cols', rcube_utils::INPUT_GET)) {
$_SESSION['list_attrib']['columns'] = explode(',', $cols);
if (!in_array('list_cols', $dont_override)) {
$save_arr['list_cols'] = explode(',', $cols);
}
}
if (!empty($save_arr)) {
$RCMAIL->user->save_prefs($save_arr);
}
$mbox_name = $RCMAIL->storage->get_folder();
$threading = (bool) $RCMAIL->storage->get_threading();
// Synchronize mailbox cache, handle flag changes
$RCMAIL->storage->folder_sync($mbox_name);
// fetch message headers
if ($count = $RCMAIL->storage->count($mbox_name, $threading ? 'THREADS' : 'ALL', !empty($_REQUEST['_refresh']))) {
$a_headers = $RCMAIL->storage->list_messages($mbox_name, NULL, rcmail_sort_column(), rcmail_sort_order());
}
// update search set (possible change of threading mode)
if (!empty($_REQUEST['_search']) && isset($_SESSION['search'])
&& $_SESSION['search_request'] == $_REQUEST['_search']
) {
$search_request = $_REQUEST['_search'];
$_SESSION['search'] = $RCMAIL->storage->get_search_set();
}
// remove old search data
else if (empty($_REQUEST['_search']) && isset($_SESSION['search'])) {
$RCMAIL->session->remove('search');
}
rcmail_list_pagetitle();
// update mailboxlist
if (empty($search_request)) {
rcmail_send_unread_count($mbox_name, !empty($_REQUEST['_refresh']), empty($a_headers) ? 0 : null);
}
// update message count display
$pages = ceil($count/$RCMAIL->storage->get_pagesize());
$page = $count ? $RCMAIL->storage->get_page() : 1;
$exists = $RCMAIL->storage->count($mbox_name, 'EXISTS', true);
$OUTPUT->set_env('messagecount', $count);
$OUTPUT->set_env('pagecount', $pages);
$OUTPUT->set_env('threading', $threading);
$OUTPUT->set_env('current_page', $page);
$OUTPUT->set_env('exists', $exists);
$OUTPUT->command('set_rowcount', rcmail_get_messagecount_text($count), $mbox_name);
// remove old message rows if commanded by the client
if (!empty($_REQUEST['_clear'])) {
$OUTPUT->command('clear_message_list');
}
// add message rows
rcmail_js_message_list($a_headers, false, $cols);
if (isset($a_headers) && count($a_headers)) {
if ($search_request) {
$OUTPUT->show_message('searchsuccessful', 'confirmation', array('nr' => $count));
}
// remember last HIGHESTMODSEQ value (if supported)
// we need it for flag updates in check-recent
$data = $RCMAIL->storage->folder_data($mbox_name);
if (!empty($data['HIGHESTMODSEQ'])) {
$_SESSION['list_mod_seq'] = $data['HIGHESTMODSEQ'];
}
}
else {
// handle IMAP errors (e.g. #1486905)
if ($err_code = $RCMAIL->storage->get_error_code()) {
$RCMAIL->display_server_error();
}
else if ($search_request) {
$OUTPUT->show_message('searchnomatch', 'notice');
}
else {
$OUTPUT->show_message('nomessagesfound', 'notice');
}
}
// set trash folder state
if ($mbox_name === $RCMAIL->config->get('trash_mbox')) {
$OUTPUT->command('set_trash_count', $exists);
}
if ($page == 1) {
$OUTPUT->command('set_quota', $RCMAIL->quota_content(null, $multifolder ? 'INBOX' : $mbox_name));
}
// send response
$OUTPUT->send();
diff --git a/program/steps/mail/list_contacts.inc b/program/steps/mail/list_contacts.inc
index 7f13b6d62..313f1a5d3 100644
--- a/program/steps/mail/list_contacts.inc
+++ b/program/steps/mail/list_contacts.inc
@@ -1,127 +1,125 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/list_contacts.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2012-2014, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Send contacts list to client (as remote response) |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
$afields = $RCMAIL->config->get('contactlist_fields');
$addr_sort_col = $RCMAIL->config->get('addressbook_sort_col', 'name');
$page_size = $RCMAIL->config->get('addressbook_pagesize', $RCMAIL->config->get('pagesize', 50));
$list_page = max(1, intval($_GET['_page']));
$jsresult = array();
// Use search result
if (!empty($_REQUEST['_search']) && isset($_SESSION['search'][$_REQUEST['_search']])) {
$search = (array)$_SESSION['search'][$_REQUEST['_search']];
$sparam = $_SESSION['search_params']['id'] == $_REQUEST['_search'] ? $_SESSION['search_params']['data'] : array();
// get records from all sources
foreach ($search as $s => $set) {
$CONTACTS = $RCMAIL->get_address_book($s);
// list matching groups of this source (on page one)
if ($sparam[1] && $CONTACTS->groups && $list_page == 1) {
$jsresult += rcmail_compose_contact_groups($CONTACTS, $s, $sparam[1], (int)$RCMAIL->config->get('addressbook_search_mode'));
}
// reset page
$CONTACTS->set_page(1);
$CONTACTS->set_pagesize(9999);
$CONTACTS->set_search_set($set);
// get records
$result = $CONTACTS->list_records($afields);
while ($row = $result->next()) {
$row['sourceid'] = $s;
$key = rcube_addressbook::compose_contact_key($row, $addr_sort_col);
$records[$key] = $row;
}
unset($result);
}
// sort the records
ksort($records, SORT_LOCALE_STRING);
// create resultset object
$count = count($records);
$first = ($list_page-1) * $page_size;
$result = new rcube_result_set($count, $first);
// we need only records for current page
if ($page_size < $count) {
$records = array_slice($records, $first, $page_size);
}
$result->records = array_values($records);
}
// list contacts from selected source
else {
$source = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
$CONTACTS = $RCMAIL->get_address_book($source);
if ($CONTACTS && $CONTACTS->ready) {
// set list properties
$CONTACTS->set_pagesize($page_size);
$CONTACTS->set_page($list_page);
if ($group_id = rcube_utils::get_input_value('_gid', rcube_utils::INPUT_GET)) {
$CONTACTS->set_group($group_id);
}
// list groups of this source (on page one)
else if ($CONTACTS->groups && $CONTACTS->list_page == 1) {
$jsresult = rcmail_compose_contact_groups($CONTACTS, $source);
}
// get contacts for this user
$result = $CONTACTS->list_records($afields);
}
}
if (!empty($result) && !$result->count && $result->searchonly) {
$OUTPUT->show_message('contactsearchonly', 'notice');
}
else if (!empty($result) && $result->count > 0) {
// create javascript list
while ($row = $result->next()) {
$name = rcube_addressbook::compose_list_name($row);
// add record for every email address of the contact
$emails = $CONTACTS->get_col_values('email', $row, true);
foreach ($emails as $i => $email) {
$source = $row['sourceid'] ?: $source;
$row_id = $source.'-'.$row['ID'].'-'.$i;
$jsresult[$row_id] = format_email_recipient($email, $name);
$classname = $row['_type'] == 'group' ? 'group' : 'person';
$keyname = $row['_type'] == 'group' ? 'contactgroup' : 'contact';
$OUTPUT->command('add_contact_row', $row_id, array(
$keyname => html::a(array('title' => $email), rcube::Q($name ?: $email) .
($name && count($emails) > 1 ? '&nbsp;' . html::span('email', rcube::Q($email)) : '')
)), $classname);
}
}
}
// update env
$OUTPUT->set_env('contactdata', $jsresult);
$OUTPUT->set_env('pagecount', ceil($result->count / $page_size));
$OUTPUT->command('set_page_buttons');
// send response
$OUTPUT->send();
diff --git a/program/steps/mail/mark.inc b/program/steps/mail/mark.inc
index eb5e416f4..4bbb65892 100644
--- a/program/steps/mail/mark.inc
+++ b/program/steps/mail/mark.inc
@@ -1,177 +1,175 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/mark.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2014, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Mark the submitted messages with the specified flag |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
// only process ajax requests
if (!$OUTPUT->ajax_call) {
return;
}
$threading = (bool) $RCMAIL->storage->get_threading();
$skip_deleted = (bool) $RCMAIL->config->get('skip_deleted');
$read_deleted = (bool) $RCMAIL->config->get('read_when_deleted');
$a_flags_map = array(
'undelete' => 'UNDELETED',
'delete' => 'DELETED',
'read' => 'SEEN',
'unread' => 'UNSEEN',
'flagged' => 'FLAGGED',
'unflagged' => 'UNFLAGGED',
);
$_uids = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
$flag = rcube_utils::get_input_value('_flag', rcube_utils::INPUT_POST);
$folders = rcube_utils::get_input_value('_folders', rcube_utils::INPUT_POST);
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
if ($_uids && $flag) {
$flag = $a_flags_map[$flag] ?: strtoupper($flag);
if ($flag == 'DELETED' && $skip_deleted && $_POST['_from'] != 'show') {
// count messages before changing anything
$old_count = $RCMAIL->storage->count(NULL, $threading ? 'THREADS' : 'ALL');
$old_pages = ceil($old_count / $RCMAIL->storage->get_pagesize());
}
if ($folders == 'all') {
$mboxes = $RCMAIL->storage->list_folders_subscribed('', '*', 'mail');
$input = array_combine($mboxes, array_fill(0, count($mboxes), '*'));
}
else if ($folders == 'sub') {
$delim = $RCMAIL->storage->get_hierarchy_delimiter();
$mboxes = $RCMAIL->storage->list_folders_subscribed($mbox . $delim, '*', 'mail');
array_unshift($mboxes, $mbox);
$input = array_combine($mboxes, array_fill(0, count($mboxes), '*'));
}
else if ($folders == 'cur') {
$input = array($mbox => '*');
}
else {
$input = rcmail::get_uids(null, null, $dummy, rcube_utils::INPUT_POST);
}
foreach ($input as $mbox => $uids) {
$marked += (int)$RCMAIL->storage->set_flag($uids, $flag, $mbox);
$count += is_array($uids) ? count($uids) : 1;
}
if (!$marked) {
// send error message
if ($_POST['_from'] != 'show') {
$OUTPUT->command('list_mailbox');
}
$RCMAIL->display_server_error('errormarking');
$OUTPUT->send();
exit;
}
else if (empty($_POST['_quiet'])) {
$OUTPUT->show_message('messagemarked', 'confirmation');
}
if ($flag == 'DELETED' && $read_deleted && !empty($_POST['_ruid'])) {
if ($ruids = rcube_utils::get_input_value('_ruid', rcube_utils::INPUT_POST)) {
foreach (rcmail::get_uids($ruids) as $mbox => $uids) {
$read += (int)$RCMAIL->storage->set_flag($uids, 'SEEN', $mbox);
}
}
if ($read && !$skip_deleted) {
$OUTPUT->command('flag_deleted_as_read', $ruids);
}
}
if ($flag == 'SEEN' || $flag == 'UNSEEN' || ($flag == 'DELETED' && !$skip_deleted)) {
foreach ($input as $mbox => $uids) {
rcmail_send_unread_count($mbox);
}
$OUTPUT->set_env('last_flag', $flag);
}
else if ($flag == 'DELETED' && $skip_deleted) {
if ($_POST['_from'] == 'show') {
if ($next = rcube_utils::get_input_value('_next_uid', rcube_utils::INPUT_GPC))
$OUTPUT->command('show_message', $next);
else
$OUTPUT->command('command', 'list');
}
else {
$search_request = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC);
// refresh saved search set after moving some messages
if ($search_request && $RCMAIL->storage->get_search_set()) {
$_SESSION['search'] = $RCMAIL->storage->refresh_search();
}
$msg_count = $RCMAIL->storage->count(NULL, $threading ? 'THREADS' : 'ALL');
$page_size = $RCMAIL->storage->get_pagesize();
$page = $RCMAIL->storage->get_page();
$pages = ceil($msg_count / $page_size);
$nextpage_count = $old_count - $page_size * $page;
$remaining = $msg_count - $page_size * ($page - 1);
// jump back one page (user removed the whole last page)
if ($page > 1 && $remaining == 0) {
$page -= 1;
$RCMAIL->storage->set_page($page);
$_SESSION['page'] = $page;
$jump_back = true;
}
// update message count display
$OUTPUT->set_env('messagecount', $msg_count);
$OUTPUT->set_env('current_page', $page);
$OUTPUT->set_env('pagecount', $pages);
// update mailboxlist
$mbox = $RCMAIL->storage->get_folder();
$unseen_count = $msg_count ? $RCMAIL->storage->count($mbox, 'UNSEEN') : 0;
$old_unseen = rcmail_get_unseen_count($mbox);
if ($old_unseen != $unseen_count) {
$OUTPUT->command('set_unread_count', $mbox, $unseen_count, ($mbox == 'INBOX'));
rcmail_set_unseen_count($mbox, $unseen_count);
}
$OUTPUT->command('set_rowcount', rcmail_get_messagecount_text($msg_count), $mbox);
if ($threading) {
$count = rcube_utils::get_input_value('_count', rcube_utils::INPUT_POST);
}
// add new rows from next page (if any)
if ($old_count && $_uids != '*' && ($jump_back || $nextpage_count > 0)) {
// #5862: Don't add more rows than it was on the next page
$count = $jump_back ? null : min($nextpage_count, $count);
$a_headers = $RCMAIL->storage->list_messages($mbox, null,
rcmail_sort_column(), rcmail_sort_order(), $count);
rcmail_js_message_list($a_headers, false);
}
}
}
}
else {
$OUTPUT->show_message('internalerror', 'error');
}
$OUTPUT->send();
diff --git a/program/steps/mail/move_del.inc b/program/steps/mail/move_del.inc
index 9d3ac8180..2c372992e 100644
--- a/program/steps/mail/move_del.inc
+++ b/program/steps/mail/move_del.inc
@@ -1,186 +1,184 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/move_del.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2014, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Move the submitted messages to a specific mailbox or delete them |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
// only process ajax requests
if (!$OUTPUT->ajax_call) {
return;
}
// count messages before changing anything
$threading = (bool) $RCMAIL->storage->get_threading();
$trash = $RCMAIL->config->get('trash_mbox');
$sources = array();
if ($_POST['_from'] != 'show') {
$old_count = $RCMAIL->storage->count(NULL, $threading ? 'THREADS' : 'ALL');
$old_pages = ceil($old_count / $RCMAIL->storage->get_pagesize());
}
// move messages
if ($RCMAIL->action == 'move' && !empty($_POST['_uid']) && strlen($_POST['_target_mbox'])) {
$target = rcube_utils::get_input_value('_target_mbox', rcube_utils::INPUT_POST, true);
$success = true;
foreach (rcmail::get_uids(null, null, $multifolder, rcube_utils::INPUT_POST) as $mbox => $uids) {
if ($mbox === $target) {
$count += is_array($uids) ? count($uids) : 1;
}
else if ($RCMAIL->storage->move_message($uids, $target, $mbox)) {
$count += is_array($uids) ? count($uids) : 1;
$sources[] = $mbox;
}
else {
$success = false;
}
}
if (!$success) {
// send error message
if ($_POST['_from'] != 'show') {
$OUTPUT->command('list_mailbox');
}
$RCMAIL->display_server_error('errormoving', null, $target == $trash ? 'delete' : '');
$OUTPUT->send();
}
else {
$OUTPUT->show_message($target == $trash ? 'messagemovedtotrash' : 'messagemoved', 'confirmation');
}
if (!empty($_POST['_refresh'])) {
// FIXME: send updated message rows instead of reloading the entire list
$OUTPUT->command('refresh_list');
}
else {
$addrows = true;
}
}
// delete messages
else if ($RCMAIL->action == 'delete' && !empty($_POST['_uid'])) {
foreach (rcmail::get_uids(null, null, $multifolder, rcube_utils::INPUT_POST) as $mbox => $uids) {
$del += (int)$RCMAIL->storage->delete_message($uids, $mbox);
$count += is_array($uids) ? count($uids) : 1;
$sources[] = $mbox;
}
if (!$del) {
// send error message
if ($_POST['_from'] != 'show') {
$OUTPUT->command('list_mailbox');
}
$RCMAIL->display_server_error('errordeleting');
$OUTPUT->send();
}
else {
$OUTPUT->show_message('messagedeleted', 'confirmation');
}
$addrows = true;
}
// unknown action or missing query param
else {
$OUTPUT->show_message('internalerror', 'error');
$OUTPUT->send();
}
$search_request = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC);
// refresh saved search set after moving some messages
if ($search_request && $RCMAIL->storage->get_search_set()) {
$_SESSION['search'] = $RCMAIL->storage->refresh_search();
}
if ($_POST['_from'] == 'show') {
if ($next = rcube_utils::get_input_value('_next_uid', rcube_utils::INPUT_GPC)) {
$OUTPUT->command('show_message', $next);
}
else {
$OUTPUT->command('command', 'list');
}
$OUTPUT->send();
}
$mbox = $RCMAIL->storage->get_folder();
$msg_count = $RCMAIL->storage->count(NULL, $threading ? 'THREADS' : 'ALL');
$exists = $RCMAIL->storage->count($mbox, 'EXISTS', true);
$page_size = $RCMAIL->storage->get_pagesize();
$page = $RCMAIL->storage->get_page();
$pages = ceil($msg_count / $page_size);
$nextpage_count = $old_count - $page_size * $page;
$remaining = $msg_count - $page_size * ($page - 1);
// jump back one page (user removed the whole last page)
if ($page > 1 && $remaining == 0) {
$page -= 1;
$RCMAIL->storage->set_page($page);
$_SESSION['page'] = $page;
$jump_back = true;
}
// update message count display
$OUTPUT->set_env('messagecount', $msg_count);
$OUTPUT->set_env('current_page', $page);
$OUTPUT->set_env('pagecount', $pages);
$OUTPUT->set_env('exists', $exists);
// update mailboxlist
$unseen_count = $msg_count ? $RCMAIL->storage->count($mbox, 'UNSEEN') : 0;
$old_unseen = rcmail_get_unseen_count($mbox);
if ($old_unseen != $unseen_count) {
$OUTPUT->command('set_unread_count', $mbox, $unseen_count, ($mbox == 'INBOX'));
rcmail_set_unseen_count($mbox, $unseen_count);
}
if ($RCMAIL->action == 'move' && strlen($target)) {
rcmail_send_unread_count($target, true);
}
$OUTPUT->command('set_quota', $RCMAIL->quota_content(null, $multifolder ? $sources[0] : 'INBOX'));
$OUTPUT->command('set_rowcount', rcmail_get_messagecount_text($msg_count), $mbox);
if ($threading) {
$count = rcube_utils::get_input_value('_count', rcube_utils::INPUT_POST);
}
// add new rows from next page (if any)
if ($addrows && $count && $uids != '*' && ($jump_back || $nextpage_count > 0)) {
// #5862: Don't add more rows than it was on the next page
$count = $jump_back ? null : min($nextpage_count, $count);
$a_headers = $RCMAIL->storage->list_messages($mbox, NULL,
rcmail_sort_column(), rcmail_sort_order(), $count);
rcmail_js_message_list($a_headers, false);
}
// set trash folder state
if ($mbox === $trash) {
$OUTPUT->command('set_trash_count', $exists);
}
else if ($target !== null && $target === $trash) {
$OUTPUT->command('set_trash_count', $RCMAIL->storage->count($trash, 'EXISTS', true));
}
// send response
$OUTPUT->send();
diff --git a/program/steps/mail/pagenav.inc b/program/steps/mail/pagenav.inc
index 653803acb..203b136f8 100644
--- a/program/steps/mail/pagenav.inc
+++ b/program/steps/mail/pagenav.inc
@@ -1,66 +1,64 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/pagenav.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2016, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Updates message page navigation controls |
- | |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
$uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GET);
$index = $RCMAIL->storage->index(null, rcmail_sort_column(), rcmail_sort_order());
$cnt = $index->count_messages();
if ($cnt && ($pos = $index->exists($uid, true)) !== false) {
$prev = $pos ? $index->get_element($pos-1) : 0;
$first = $pos ? $index->get_element('FIRST') : 0;
$next = $pos < $cnt-1 ? $index->get_element($pos+1) : 0;
$last = $pos < $cnt-1 ? $index->get_element('LAST') : 0;
}
else {
// error, this will at least disable page navigation
$OUTPUT->command('set_rowcount', '');
$OUTPUT->send();
}
// Set UIDs and activate navigation buttons
if ($prev) {
$OUTPUT->set_env('prev_uid', $prev);
$OUTPUT->command('enable_command', 'previousmessage', 'firstmessage', true);
}
if ($next) {
$OUTPUT->set_env('next_uid', $next);
$OUTPUT->command('enable_command', 'nextmessage', 'lastmessage', true);
}
if ($first) {
$OUTPUT->set_env('first_uid', $first);
}
if ($last) {
$OUTPUT->set_env('last_uid', $last);
}
// Don't need a real messages count value
$OUTPUT->set_env('messagecount', 1);
// Set rowcount text
$OUTPUT->command('set_rowcount', $RCMAIL->gettext(array(
'name' => 'messagenrof',
'vars' => array('nr' => $pos+1, 'count' => $cnt)
)));
$OUTPUT->send();
diff --git a/program/steps/mail/search.inc b/program/steps/mail/search.inc
index b83f2d527..f60528835 100644
--- a/program/steps/mail/search.inc
+++ b/program/steps/mail/search.inc
@@ -1,247 +1,246 @@
<?php
/**
+-----------------------------------------------------------------------+
- | steps/mail/search.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Mail messages search action |
+-----------------------------------------------------------------------+
| Author: Benjamin Smith <defitro@gmail.com> |
- | Thomas Bruederli <roundcube@gmail.com> |
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
$REMOTE_REQUEST = TRUE;
@set_time_limit(170); // extend default max_execution_time to ~3 minutes
// reset list_page and old search results
$RCMAIL->storage->set_page(1);
$RCMAIL->storage->set_search_set(NULL);
$_SESSION['page'] = 1;
// using encodeURI with javascript "should" give us
// a correctly encoded query string
$imap_charset = RCUBE_CHARSET;
// get search string
$str = rcube_utils::get_input_value('_q', rcube_utils::INPUT_GET, true);
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GET, true);
$filter = rcube_utils::get_input_value('_filter', rcube_utils::INPUT_GET);
$headers = rcube_utils::get_input_value('_headers', rcube_utils::INPUT_GET);
$scope = rcube_utils::get_input_value('_scope', rcube_utils::INPUT_GET);
$interval = rcube_utils::get_input_value('_interval', rcube_utils::INPUT_GET);
$continue = rcube_utils::get_input_value('_continue', rcube_utils::INPUT_GET);
$subject = array();
$filter = trim($filter);
$search_request = md5($mbox.$scope.$interval.$filter.$str);
// add list filter string
$search_str = $filter && $filter != 'ALL' ? $filter : '';
// Check the search string for type of search
if (preg_match("/^from:.*/i", $str)) {
list(,$srch) = explode(":", $str);
$subject['from'] = "HEADER FROM";
}
else if (preg_match("/^to:.*/i", $str)) {
list(,$srch) = explode(":", $str);
$subject['to'] = "HEADER TO";
}
else if (preg_match("/^cc:.*/i", $str)) {
list(,$srch) = explode(":", $str);
$subject['cc'] = "HEADER CC";
}
else if (preg_match("/^bcc:.*/i", $str)) {
list(,$srch) = explode(":", $str);
$subject['bcc'] = "HEADER BCC";
}
else if (preg_match("/^subject:.*/i", $str)) {
list(,$srch) = explode(":", $str);
$subject['subject'] = "HEADER SUBJECT";
}
else if (preg_match("/^body:.*/i", $str)) {
list(,$srch) = explode(":", $str);
$subject['body'] = "BODY";
}
else if (strlen(trim($str))) {
if ($headers) {
foreach (explode(',', $headers) as $header) {
if ($header == 'text') {
// #1488208: get rid of other headers when searching by "TEXT"
$subject = array('text' => 'TEXT');
break;
}
else {
$subject[$header] = ($header != 'body' ? 'HEADER ' : '') . strtoupper($header);
}
}
// save search modifiers for the current folder to user prefs
$mkey = $scope == 'all' ? '*' : $mbox;
$search_mods = rcmail_search_mods();
$search_mods[$mkey] = array_fill_keys(array_keys($subject), 1);
$RCMAIL->user->save_prefs(array('search_mods' => $search_mods));
}
else {
// search in subject by default
$subject['subject'] = 'HEADER SUBJECT';
}
}
$search = isset($srch) ? trim($srch) : trim($str);
if ($search_interval = rcmail_search_interval_criteria($interval)) {
$search_str .= ' ' . $search_interval;
}
if (!empty($subject)) {
$search_str .= str_repeat(' OR', count($subject)-1);
foreach ($subject as $sub) {
$search_str .= ' ' . $sub . ' ' . rcube_imap_generic::escape($search);
}
}
$search_str = trim($search_str);
$sort_column = rcmail_sort_column();
// set message set for already stored (but incomplete) search request
if (!empty($continue) && isset($_SESSION['search']) && $_SESSION['search_request'] == $continue) {
$RCMAIL->storage->set_search_set($_SESSION['search']);
$search_str = $_SESSION['search'][0];
}
// execute IMAP search
if ($search_str) {
// search all, current or subfolders folders
if ($scope == 'all') {
$mboxes = $RCMAIL->storage->list_folders_subscribed('', '*', 'mail', null, true);
natcasesort($mboxes); // we want natural alphabetic sorting of folders in the result set
}
else if ($scope == 'sub') {
$delim = $RCMAIL->storage->get_hierarchy_delimiter();
$mboxes = $RCMAIL->storage->list_folders_subscribed($mbox . $delim, '*', 'mail');
array_unshift($mboxes, $mbox);
}
if ($scope != 'all') {
// Remember current folder, it can change in meantime (plugins)
// but we need it to e.g. recognize Sent folder to handle From/To column later
$RCMAIL->output->set_env('mailbox', $mbox);
}
$result = $RCMAIL->storage->search($mboxes, $search_str, $imap_charset, $sort_column);
}
// save search results in session
if (!is_array($_SESSION['search'])) {
$_SESSION['search'] = array();
}
if ($search_str) {
$_SESSION['search'] = $RCMAIL->storage->get_search_set();
$_SESSION['last_text_search'] = $str;
}
$_SESSION['search_request'] = $search_request;
$_SESSION['search_scope'] = $scope;
$_SESSION['search_interval'] = $interval;
$_SESSION['search_filter'] = $filter;
// Get the headers
if (!$result->incomplete) {
$result_h = $RCMAIL->storage->list_messages($mbox, 1, $sort_column, rcmail_sort_order());
}
// Make sure we got the headers
if (!empty($result_h)) {
$count = $RCMAIL->storage->count($mbox, $RCMAIL->storage->get_threading() ? 'THREADS' : 'ALL');
rcmail_js_message_list($result_h, false);
if ($search_str) {
$OUTPUT->show_message('searchsuccessful', 'confirmation', array('nr' => $RCMAIL->storage->count(NULL, 'ALL')));
}
// remember last HIGHESTMODSEQ value (if supported)
// we need it for flag updates in check-recent
if ($mbox !== null) {
$data = $RCMAIL->storage->folder_data($mbox);
if (!empty($data['HIGHESTMODSEQ'])) {
$_SESSION['list_mod_seq'] = $data['HIGHESTMODSEQ'];
}
}
}
// handle IMAP errors (e.g. #1486905)
else if ($err_code = $RCMAIL->storage->get_error_code()) {
$count = 0;
$RCMAIL->display_server_error();
}
// advice the client to re-send the (cross-folder) search request
else if ($result->incomplete) {
$count = 0; // keep UI locked
$OUTPUT->command('continue_search', $search_request);
}
else {
$count = 0;
$OUTPUT->show_message('searchnomatch', 'notice');
$OUTPUT->set_env('multifolder_listing', (bool)$result->multi);
if ($result->multi && $scope == 'all') {
$OUTPUT->command('select_folder', '');
}
}
// update message count display
$OUTPUT->set_env('search_request', $search_str ? $search_request : '');
$OUTPUT->set_env('search_filter', $_SESSION['search_filter']);
$OUTPUT->set_env('messagecount', $count);
$OUTPUT->set_env('pagecount', ceil($count/$RCMAIL->storage->get_pagesize()));
$OUTPUT->set_env('exists', $mbox === null ? 0 : $RCMAIL->storage->count($mbox, 'EXISTS'));
$OUTPUT->command('set_rowcount', rcmail_get_messagecount_text($count, 1), $mbox);
rcmail_list_pagetitle();
// update unseen messages count
if (empty($search_str)) {
rcmail_send_unread_count($mbox, false, empty($result_h) ? 0 : null);
}
if (!$result->incomplete) {
$OUTPUT->command('set_quota', $RCMAIL->quota_content(null, $result->multi ? 'INBOX' : $mbox));
}
$OUTPUT->send();
// Creates BEFORE/SINCE search criteria from the specified interval
// Interval can be: 1W, 1M, 1Y, -1W, -1M, -1Y
function rcmail_search_interval_criteria($interval)
{
if (empty($interval)) {
return;
}
if ($interval[0] == '-') {
$search = 'BEFORE';
$interval = substr($interval, 1);
}
else {
$search = 'SINCE';
}
$date = new DateTime('now');
$interval = new DateInterval('P' . $interval);
$date->sub($interval);
return $search . ' ' . $date->format('j-M-Y');
}
diff --git a/program/steps/mail/search_contacts.inc b/program/steps/mail/search_contacts.inc
index 597a0ea49..77bbf57ee 100644
--- a/program/steps/mail/search_contacts.inc
+++ b/program/steps/mail/search_contacts.inc
@@ -1,121 +1,119 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/search_contacts.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2013-2014, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Search contacts from the address book widget |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
$search = rcube_utils::get_input_value('_q', rcube_utils::INPUT_GPC, true);
$sources = $RCMAIL->get_address_sources();
$search_mode = (int) $RCMAIL->config->get('addressbook_search_mode');
$addr_sort_col = $RCMAIL->config->get('addressbook_sort_col', 'name');
$afields = $RCMAIL->config->get('contactlist_fields');
$page_size = $RCMAIL->config->get('addressbook_pagesize', $RCMAIL->config->get('pagesize', 50));
$records = array();
$search_set = array();
$jsresult = array();
$search_mode |= rcube_addressbook::SEARCH_GROUPS;
foreach ($sources as $s) {
$source = $RCMAIL->get_address_book($s['id']);
$source->set_page(1);
$source->set_pagesize(9999);
// list matching groups of this source
if ($source->groups) {
$jsresult += rcmail_compose_contact_groups($source, $s['id'], $search, $search_mode);
}
// get contacts count
$result = $source->search($afields, $search, $search_mode, true, true, 'email');
if (!$result->count) {
continue;
}
while ($row = $result->next()) {
$row['sourceid'] = $s['id'];
$key = rcube_addressbook::compose_contact_key($row, $addr_sort_col);
$records[$key] = $row;
}
$search_set[$s['id']] = $source->get_search_set();
unset($result);
}
$group_count = count($jsresult);
// sort the records
ksort($records, SORT_LOCALE_STRING);
// create resultset object
$count = count($records);
$result = new rcube_result_set($count);
// select the requested page
if ($page_size < $count) {
$records = array_slice($records, $result->first, $page_size);
}
$result->records = array_values($records);
if (!empty($result) && $result->count > 0) {
// create javascript list
while ($row = $result->next()) {
$name = rcube_addressbook::compose_list_name($row);
$classname = $row['_type'] == 'group' ? 'group' : 'person';
$keyname = $row['_type'] == 'group' ? 'contactgroup' : 'contact';
// add record for every email address of the contact
// (same as in list_contacts.inc)
$emails = $source->get_col_values('email', $row, true);
foreach ($emails as $i => $email) {
$row_id = $row['sourceid'].'-'.$row['ID'].'-'.$i;
$jsresult[$row_id] = format_email_recipient($email, $name);
$title = rcube_addressbook::compose_search_name($row, $email, $name);
$OUTPUT->command('add_contact_row', $row_id, array(
$keyname => html::a(array('title' => $title), rcube::Q($name ?: $email) .
($name && count($emails) > 1 ? '&nbsp;' . html::span('email', rcube::Q($email)) : '')
)), $classname);
}
}
// search request ID
$search_request = md5('composeaddr' . $search);
// save search settings in session
$_SESSION['search'][$search_request] = $search_set;
$_SESSION['search_params'] = array('id' => $search_request, 'data' => array($afields, $search));
$OUTPUT->show_message('contactsearchsuccessful', 'confirmation', array('nr' => $result->count));
$OUTPUT->set_env('search_request', $search_request);
$OUTPUT->set_env('source', '');
$OUTPUT->command('unselect_directory');
}
else if (!$group_count) {
$OUTPUT->show_message('nocontactsfound', 'notice');
}
// update env
$OUTPUT->set_env('contactdata', $jsresult);
$OUTPUT->set_env('pagecount', ceil($result->count / $page_size));
$OUTPUT->command('set_page_buttons');
// send response
$OUTPUT->send();
diff --git a/program/steps/mail/sendmail.inc b/program/steps/mail/sendmail.inc
index f85b8fcb6..94b450253 100644
--- a/program/steps/mail/sendmail.inc
+++ b/program/steps/mail/sendmail.inc
@@ -1,360 +1,359 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/sendmail.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2017, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Compose a new mail message and send it or store as draft |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
// remove all scripts and act as called in frame
$OUTPUT->reset();
$OUTPUT->framed = true;
$COMPOSE_ID = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
$COMPOSE =& $_SESSION['compose_data_'.$COMPOSE_ID];
// Sanity checks
if (!isset($COMPOSE['id'])) {
rcube::raise_error(array('code' => 500, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Invalid compose ID"), true, false);
$OUTPUT->show_message('internalerror', 'error');
$OUTPUT->send('iframe');
}
$saveonly = !empty($_GET['_saveonly']);
$savedraft = !empty($_POST['_draft']) && !$saveonly;
$SENDMAIL = new rcmail_sendmail($COMPOSE, array(
'sendmail' => true,
'saveonly' => $saveonly,
'savedraft' => $savedraft,
'error_handler' => function() use ($OUTPUT) {
call_user_func_array(array($OUTPUT, 'show_message'), func_get_args());
$OUTPUT->send('iframe');
}
));
// Collect input for message headers
$headers = $SENDMAIL->headers_input();
$COMPOSE['param']['message-id'] = $headers['Message-ID'];
$message_id = $headers['Message-ID'];
$message_charset = $SENDMAIL->options['charset'];
$message_body = rcube_utils::get_input_value('_message', rcube_utils::INPUT_POST, true, $message_charset);
$isHtml = (bool) rcube_utils::get_input_value('_is_html', rcube_utils::INPUT_POST);
// Reset message body and attachments in Mailvelope mode
if (isset($_POST['_pgpmime'])) {
$pgp_mime = rcube_utils::get_input_value('_pgpmime', rcube_utils::INPUT_POST);
$isHtml = false;
$message_body = '';
// clear unencrypted attachments
foreach ((array) $COMPOSE['attachments'] as $attach) {
$RCMAIL->plugins->exec_hook('attachment_delete', $attach);
}
$COMPOSE['attachments'] = array();
}
if ($isHtml) {
$bstyle = array();
if ($font_size = $RCMAIL->config->get('default_font_size')) {
$bstyle[] = 'font-size: ' . $font_size;
}
if ($font_family = $RCMAIL->config->get('default_font')) {
$bstyle[] = 'font-family: ' . rcmail::font_defs($font_family);
}
// append doctype and html/body wrappers
$bstyle = !empty($bstyle) ? (" style='" . implode($bstyle, '; ') . "'") : '';
$message_body = '<html><head>'
. '<meta http-equiv="Content-Type" content="text/html; charset='
. ($message_charset ?: RCUBE_CHARSET) . '" /></head>'
. "<body" . $bstyle . ">\r\n" . $message_body;
}
if (!$savedraft) {
if ($isHtml) {
$b_style = 'padding: 0 0.4em; border-left: #1010ff 2px solid; margin: 0';
$pre_style = 'margin: 0; padding: 0; font-family: monospace';
$message_body = preg_replace(
array(
// remove empty signature div
'/<div id="_rc_sig">(&nbsp;)?<\/div>[\s\r\n]*$/',
// replace signature's div ID (#6073)
'/ id="_rc_sig"/',
// add inline css for blockquotes and container
'/<blockquote>/',
'/<div class="pre">/',
// convert TinyMCE's new-line sequences (#1490463)
'/<p>&nbsp;<\/p>/',
),
array(
'',
' id="signature"',
'<blockquote type="cite" style="'.$b_style.'">',
'<div class="pre" style="'.$pre_style.'">',
'<p><br /></p>',
),
$message_body);
rcube_utils::preg_error(array(
'line' => __LINE__,
'file' => __FILE__,
'message' => "Could not format HTML!"
), true);
}
// Check spelling before send
if ($RCMAIL->config->get('spellcheck_before_send')
&& $RCMAIL->config->get('enable_spellcheck')
&& empty($COMPOSE['spell_checked'])
&& !empty($message_body)
) {
$message_body = str_replace("\r\n", "\n", $message_body);
$spellchecker = new rcube_spellchecker(rcube_utils::get_input_value('_lang', rcube_utils::INPUT_GPC));
$spell_result = $spellchecker->check($message_body, $isHtml);
$COMPOSE['spell_checked'] = true;
if (!$spell_result) {
if ($isHtml) {
$result['words'] = $spellchecker->get();
$result['dictionary'] = (bool) $RCMAIL->config->get('spellcheck_dictionary');
}
else {
$result = $spellchecker->get_xml();
}
$OUTPUT->show_message('mispellingsfound', 'error');
$OUTPUT->command('spellcheck_resume', $result);
$OUTPUT->send('iframe');
}
}
// generic footer for all messages
if ($footer = $SENDMAIL->generic_message_footer($isHtml)) {
$message_body .= "\r\n" . $footer;
}
}
if ($isHtml) {
$message_body .= "\r\n</body></html>\r\n";
}
// sort attachments to make sure the order is the same as in the UI (#1488423)
if ($files = rcube_utils::get_input_value('_attachments', rcube_utils::INPUT_POST)) {
$files = explode(',', $files);
$files = array_flip($files);
foreach ($files as $idx => $val) {
$files[$idx] = $COMPOSE['attachments'][$idx];
unset($COMPOSE['attachments'][$idx]);
}
$COMPOSE['attachments'] = array_merge(array_filter($files), $COMPOSE['attachments']);
}
// Since we can handle big messages with disk usage, we need more time to work
@set_time_limit(360);
// create PEAR::Mail_mime instance, set headers, body and params
$MAIL_MIME = $SENDMAIL->create_message($headers, $message_body, $isHtml, $COMPOSE['attachments']);
// add stored attachments, if any
if (is_array($COMPOSE['attachments'])) {
foreach ($COMPOSE['attachments'] as $id => $attachment) {
// This hook retrieves the attachment contents from the file storage backend
$attachment = $RCMAIL->plugins->exec_hook('attachment_get', $attachment);
$is_inline = false;
if ($isHtml) {
$dispurl = '/[\'"]\S+display-attachment\S+file=rcmfile' . preg_quote($attachment['id']) . '[\'"]/';
$message_body = $MAIL_MIME->getHTMLBody();
$is_inline = preg_match($dispurl, $message_body);
}
// inline image
if ($is_inline) {
// Mail_Mime does not support many inline attachments with the same name (#1489406)
// we'll generate cid: urls here to workaround this
$cid = preg_replace('/[^0-9a-zA-Z]/', '', uniqid(time(), true));
if (preg_match('#(@[0-9a-zA-Z\-\.]+)#', $SENDMAIL->options['from'], $matches)) {
$cid .= $matches[1];
}
else {
$cid .= '@localhost';
}
$message_body = preg_replace($dispurl, '"cid:' . $cid . '"', $message_body);
rcube_utils::preg_error(array(
'line' => __LINE__,
'file' => __FILE__,
'message' => "Could not replace an image reference!"
), true);
$MAIL_MIME->setHTMLBody($message_body);
if ($attachment['data'])
$MAIL_MIME->addHTMLImage($attachment['data'], $attachment['mimetype'], $attachment['name'], false, $cid);
else
$MAIL_MIME->addHTMLImage($attachment['path'], $attachment['mimetype'], $attachment['name'], true, $cid);
}
else {
$ctype = str_replace('image/pjpeg', 'image/jpeg', $attachment['mimetype']); // #1484914
$file = $attachment['data'] ?: $attachment['path'];
$folding = (int) $RCMAIL->config->get('mime_param_folding');
$MAIL_MIME->addAttachment($file,
$ctype,
$attachment['name'],
$attachment['data'] ? false : true,
$ctype == 'message/rfc822' ? '8bit' : 'base64',
'attachment',
$attachment['charset'],
'', '',
$folding ? 'quoted-printable' : NULL,
$folding == 2 ? 'quoted-printable' : NULL,
'', RCUBE_CHARSET
);
}
}
}
// compose PGP/Mime message
if ($pgp_mime) {
$MAIL_MIME->addAttachment(new Mail_mimePart('Version: 1', array(
'content_type' => 'application/pgp-encrypted',
'description' => 'PGP/MIME version identification',
)));
$MAIL_MIME->addAttachment(new Mail_mimePart($pgp_mime, array(
'content_type' => 'application/octet-stream',
'filename' => 'encrypted.asc',
'disposition' => 'inline',
)));
$MAIL_MIME->setContentType('multipart/encrypted', array('protocol' => 'application/pgp-encrypted'));
$MAIL_MIME->setParam('preamble', 'This is an OpenPGP/MIME encrypted message (RFC 2440 and 3156)');
}
// This hook allows to modify the message before send or save action
$plugin = $RCMAIL->plugins->exec_hook('message_ready', array('message' => $MAIL_MIME));
$MAIL_MIME = $plugin['message'];
// Deliver the message over SMTP
if (!$savedraft && !$saveonly) {
$sent = $SENDMAIL->deliver_message($MAIL_MIME);
}
// Save the message in Drafts/Sent
$saved = $SENDMAIL->save_message($MAIL_MIME);
// raise error if saving failed
if (!$saved && $savedraft) {
$RCMAIL->display_server_error('errorsaving');
// start the auto-save timer again
$OUTPUT->command('auto_save_start');
$OUTPUT->send('iframe');
}
$store_target = $SENDMAIL->options['store_target'];
$store_folder = $SENDMAIL->options['store_folder'];
// delete previous saved draft
$drafts_mbox = $RCMAIL->config->get('drafts_mbox');
$old_id = rcube_utils::get_input_value('_draft_saveid', rcube_utils::INPUT_POST);
if ($old_id && ($sent || $saved)) {
$deleted = $RCMAIL->storage->delete_message($old_id, $drafts_mbox);
// raise error if deletion of old draft failed
if (!$deleted) {
rcube::raise_error(array('code' => 800, 'type' => 'imap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not delete message from $drafts_mbox"), true, false);
}
}
if ($savedraft) {
// remember new draft-uid ($saved could be an UID or true/false here)
if ($saved && is_bool($saved)) {
$index = $RCMAIL->storage->search_once($drafts_mbox, 'HEADER Message-ID ' . $message_id);
$saved = @max($index->get());
}
if ($saved) {
$plugin = $RCMAIL->plugins->exec_hook('message_draftsaved',
array('msgid' => $message_id, 'uid' => $saved, 'folder' => $store_target));
// display success
$OUTPUT->show_message($plugin['message'] ?: 'messagesaved', 'confirmation');
// update "_draft_saveid" and the "cmp_hash" to prevent "Unsaved changes" warning
$COMPOSE['param']['draft_uid'] = $plugin['uid'];
$OUTPUT->command('set_draft_id', $plugin['uid']);
$OUTPUT->command('compose_field_hash', true);
}
// start the auto-save timer again
$OUTPUT->command('auto_save_start');
}
else {
// Collect folders which could contain the composed message,
// we'll refresh the list if currently opened folder is one of them (#1490238)
$folders = array();
if (!$saveonly) {
if (in_array($COMPOSE['mode'], array('reply', 'forward', 'draft'))) {
$folders[] = $COMPOSE['mailbox'];
}
if (!empty($COMPOSE['param']['draft_uid']) && $drafts_mbox) {
$folders[] = $drafts_mbox;
}
}
if ($store_folder && !$saved) {
$params = $saveonly ? null : array('prefix' => true);
$RCMAIL->display_server_error('errorsavingsent', null, null, $params);
if ($saveonly) {
$OUTPUT->send('iframe');
}
$save_error = true;
}
else {
$RCMAIL->plugins->exec_hook('attachments_cleanup', array('group' => $COMPOSE_ID));
$RCMAIL->session->remove('compose_data_' . $COMPOSE_ID);
$_SESSION['last_compose_session'] = $COMPOSE_ID;
$OUTPUT->command('remove_compose_data', $COMPOSE_ID);
if ($store_folder) {
$folders[] = $store_target;
}
}
$msg = $RCMAIL->gettext($saveonly ? 'successfullysaved' : 'messagesent');
$OUTPUT->command('sent_successfully', 'confirmation', $msg, $folders, $save_error);
}
$OUTPUT->send('iframe');
diff --git a/program/steps/mail/sendmdn.inc b/program/steps/mail/sendmdn.inc
index fee21d235..2ef481967 100644
--- a/program/steps/mail/sendmdn.inc
+++ b/program/steps/mail/sendmdn.inc
@@ -1,43 +1,41 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/sendmdn.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2008-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Send a message disposition notification for a specific mail |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
// only process ajax requests
if (!$OUTPUT->ajax_call) {
return;
}
if (!empty($_POST['_uid'])) {
$sent = rcmail_send_mdn(rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST), $smtp_error);
}
// show either confirm or error message
if ($sent) {
$OUTPUT->set_env('mdn_request', false);
$OUTPUT->show_message('receiptsent', 'confirmation');
}
else if ($smtp_error) {
$OUTPUT->show_message($smtp_error['label'], 'error', $smtp_error['vars']);
}
else {
$OUTPUT->show_message('errorsendingreceipt', 'error');
}
$OUTPUT->send();
diff --git a/program/steps/mail/show.inc b/program/steps/mail/show.inc
index f9f63057d..c9cf12deb 100644
--- a/program/steps/mail/show.inc
+++ b/program/steps/mail/show.inc
@@ -1,800 +1,798 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/show.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Display a mail message similar as a usual mail application does |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
$PRINT_MODE = $RCMAIL->action == 'print';
// Read browser capabilities and store them in session
if ($caps = rcube_utils::get_input_value('_caps', rcube_utils::INPUT_GET)) {
$browser_caps = array();
foreach (explode(',', $caps) as $cap) {
$cap = explode('=', $cap);
$browser_caps[$cap[0]] = $cap[1];
}
$_SESSION['browser_caps'] = $browser_caps;
}
$msg_id = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GET);
$uid = preg_replace('/\.[0-9.]+$/', '', $msg_id);
$mbox_name = $RCMAIL->storage->get_folder();
// similar code as in program/steps/mail/get.inc
if ($uid) {
// set message format (need to be done before rcube_message construction)
if (!empty($_GET['_format'])) {
$prefer_html = $_GET['_format'] == 'html';
$RCMAIL->config->set('prefer_html', $prefer_html);
$_SESSION['msg_formats'][$mbox_name.':'.$uid] = $prefer_html;
}
else if (isset($_SESSION['msg_formats'][$mbox_name.':'.$uid])) {
$RCMAIL->config->set('prefer_html', $_SESSION['msg_formats'][$mbox_name.':'.$uid]);
}
$MESSAGE = new rcube_message($msg_id, $mbox_name, intval($_GET['_safe']));
// if message not found (wrong UID)...
if (empty($MESSAGE->headers)) {
rcmail_message_error($uid);
}
// show images?
rcmail_check_safe($MESSAGE);
// set message charset as default
if (!empty($MESSAGE->headers->charset)) {
$RCMAIL->storage->set_charset($MESSAGE->headers->charset);
}
if (!isset($_SESSION['writeable_abook'])) {
$_SESSION['writeable_abook'] = $RCMAIL->get_address_sources(true) ? true : false;
}
$OUTPUT->set_pagetitle(abbreviate_string($MESSAGE->subject, 128, '...', true));
// set environment
$OUTPUT->set_env('uid', $msg_id);
$OUTPUT->set_env('safemode', $MESSAGE->is_safe);
$OUTPUT->set_env('message_context', $MESSAGE->context);
$OUTPUT->set_env('message_flags', array_keys(array_change_key_case((array) $MESSAGE->headers->flags)));
$OUTPUT->set_env('sender', $MESSAGE->sender['string']);
$OUTPUT->set_env('mailbox', $mbox_name);
$OUTPUT->set_env('username', $RCMAIL->get_user_name());
$OUTPUT->set_env('permaurl', $RCMAIL->url(array('_action' => 'show', '_uid' => $msg_id, '_mbox' => $mbox_name)));
$OUTPUT->set_env('has_writeable_addressbook', $_SESSION['writeable_abook']);
$OUTPUT->set_env('delimiter', $RCMAIL->storage->get_hierarchy_delimiter());
$OUTPUT->set_env('mimetypes', rcmail_supported_mimetypes());
if ($MESSAGE->headers->get('list-post', false)) {
$OUTPUT->set_env('list_post', true);
}
// set configuration
$RCMAIL->set_env_config(array('delete_junk', 'flag_for_deletion', 'read_when_deleted',
'skip_deleted', 'display_next', 'forward_attachment'));
// set special folders
foreach (array('drafts', 'trash', 'junk') as $mbox) {
if ($folder = $RCMAIL->config->get($mbox . '_mbox')) {
$OUTPUT->set_env($mbox . '_mailbox', $folder);
}
}
if ($MESSAGE->has_html_part()) {
$prefer_html = $RCMAIL->config->get('prefer_html');
$OUTPUT->set_env('optional_format', $prefer_html ? 'text' : 'html');
}
if (!$OUTPUT->ajax_call) {
$OUTPUT->add_label('checkingmail', 'deletemessage', 'movemessagetotrash',
'movingmessage', 'deletingmessage', 'markingmessage', 'replyall', 'replylist',
'bounce', 'bouncemsg', 'sendingmessage');
}
// check for unset disposition notification
if ($MESSAGE->headers->mdn_to
&& $MESSAGE->context === null
&& empty($MESSAGE->headers->flags['MDNSENT'])
&& empty($MESSAGE->headers->flags['SEEN'])
&& ($RCMAIL->storage->check_permflag('MDNSENT') || $RCMAIL->storage->check_permflag('*'))
&& $mbox_name != $RCMAIL->config->get('drafts_mbox')
&& $mbox_name != $RCMAIL->config->get('sent_mbox')
) {
$mdn_cfg = intval($RCMAIL->config->get('mdn_requests'));
if ($mdn_cfg == 1 || (($mdn_cfg == 3 || $mdn_cfg == 4) && rcmail_contact_exists($MESSAGE->sender['mailto']))) {
// Send MDN
if (rcmail_send_mdn($MESSAGE, $smtp_error))
$OUTPUT->show_message('receiptsent', 'confirmation');
else if ($smtp_error)
$OUTPUT->show_message($smtp_error['label'], 'error', $smtp_error['vars']);
else
$OUTPUT->show_message('errorsendingreceipt', 'error');
}
else if ($mdn_cfg != 2 && $mdn_cfg != 4) {
// Ask user
$OUTPUT->add_label('mdnrequest');
$OUTPUT->set_env('mdn_request', true);
}
}
if (empty($MESSAGE->headers->flags['SEEN']) && $MESSAGE->context === null) {
$v = intval($RCMAIL->config->get('mail_read_time'));
if ($v > 0) {
$OUTPUT->set_env('mail_read_time', $v);
}
else if ($v == 0) {
$RCMAIL->output->command('set_unread_message', $MESSAGE->uid, $mbox_name);
$RCMAIL->plugins->exec_hook('message_read', array(
'uid' => $MESSAGE->uid,
'mailbox' => $mbox_name,
'message' => $MESSAGE,
));
$set_seen_flag = true;
}
}
}
$OUTPUT->add_handlers(array(
'mailboxname' => 'rcmail_mailbox_name_display',
'messageattachments' => 'rcmail_message_attachments',
'messageobjects' => 'rcmail_message_objects',
'messagesummary' => 'rcmail_message_summary',
'messageheaders' => 'rcmail_message_headers',
'messagefullheaders' => 'rcmail_message_full_headers',
'messagebody' => 'rcmail_message_body',
'contactphoto' => 'rcmail_message_contactphoto',
));
if ($RCMAIL->action == 'print' && $OUTPUT->template_exists('messageprint'))
$OUTPUT->send('messageprint', false);
else if ($RCMAIL->action == 'preview' && $OUTPUT->template_exists('messagepreview'))
$OUTPUT->send('messagepreview', false);
else
$OUTPUT->send('message', false);
// mark message as read
if (!empty($set_seen_flag)) {
if ($RCMAIL->storage->set_flag($MESSAGE->uid, 'SEEN', $mbox_name)) {
if ($count = rcmail_get_unseen_count($mbox_name)) {
rcmail_set_unseen_count($mbox_name, $count - 1);
}
}
}
exit;
function rcmail_message_attachments($attrib)
{
global $PRINT_MODE, $MESSAGE, $RCMAIL;
$out = $ol = '';
$attachments = array();
if (count($MESSAGE->attachments)) {
foreach ($MESSAGE->attachments as $attach_prop) {
$filename = rcmail_attachment_name($attach_prop, true);
$filesize = $RCMAIL->message_part_size($attach_prop);
$mimetype = rcmail_fix_mimetype($attach_prop->mimetype);
$class = rcube_utils::file2class($mimetype, $filename);
$id = 'attach' . $attach_prop->mime_id;
if ($attrib['maxlength'] && mb_strlen($filename) > $attrib['maxlength']) {
$title = $filename;
$filename = abbreviate_string($filename, $attrib['maxlength']);
}
else {
$title = '';
}
$item = html::span('attachment-name', rcube::Q($filename))
. html::span('attachment-size', '(' . rcube::Q($filesize) . ')');
if (!$PRINT_MODE) {
$item = html::a(array(
'href' => $MESSAGE->get_part_url($attach_prop->mime_id, false),
'onclick' => sprintf('return %s.command(\'load-attachment\',\'%s\',this)',
rcmail_output::JS_OBJECT_NAME, $attach_prop->mime_id),
'onmouseover' => $title ? '' : 'rcube_webmail.long_subject_title_ex(this, 0)',
'title' => $title,
'class' => 'filename',
), $item);
$attachments[$attach_prop->mime_id] = $mimetype;
}
$ol .= html::tag('li', array('class' => $class, 'id' => $id), $item);
}
$out = html::tag('ul', $attrib, $ol, html::$common_attrib);
$RCMAIL->output->set_env('attachments', $attachments);
$RCMAIL->output->add_gui_object('attachments', $attrib['id']);
}
return $out;
}
function rcmail_remote_objects_msg()
{
global $MESSAGE, $RCMAIL;
$attrib['id'] = 'remote-objects-message';
$attrib['class'] = 'notice';
$attrib['style'] = 'display: none';
$msg = html::span(null, rcube::Q($RCMAIL->gettext('blockedresources'))) . '&nbsp;'
. html::a(array(
'href' => "#loadremote",
'onclick' => rcmail_output::JS_OBJECT_NAME.".command('load-remote')"
),
rcube::Q($RCMAIL->gettext('allow')));
// add link to save sender in addressbook and reload message
if ($MESSAGE->sender['mailto'] && $RCMAIL->config->get('show_images') == 1) {
$msg .= ' ' . html::a(array(
'href' => "#loadremotealways",
'onclick' => rcmail_output::JS_OBJECT_NAME.".command('load-remote', true)",
'style' => "white-space:nowrap"
),
rcube::Q($RCMAIL->gettext(array('name' => 'alwaysallow', 'vars' => array('sender' => $MESSAGE->sender['mailto'])))));
}
$RCMAIL->output->add_gui_object('remoteobjectsmsg', $attrib['id']);
return html::div($attrib, $msg);
}
function rcmail_message_buttons()
{
global $RCMAIL, $MESSAGE;
$delim = $RCMAIL->storage->get_hierarchy_delimiter();
$dbox = $RCMAIL->config->get('drafts_mbox');
// the message is not a draft
if ($MESSAGE->context
|| ($MESSAGE->folder != $dbox && strpos($MESSAGE->folder, $dbox.$delim) !== 0)
) {
return '';
}
$attrib['id'] = 'message-buttons';
$attrib['class'] = 'information notice';
$msg = html::span(null, rcube::Q($RCMAIL->gettext('isdraft'))) . '&nbsp;'
. html::a(array(
'href' => "#edit",
'onclick' => rcmail_output::JS_OBJECT_NAME.".command('edit')"
),
rcube::Q($RCMAIL->gettext('edit')));
return html::div($attrib, $msg);
}
function rcmail_message_objects($attrib)
{
global $RCMAIL, $MESSAGE;
if (!$attrib['id'])
$attrib['id'] = 'message-objects';
$content = array(
rcmail_message_buttons(),
rcmail_remote_objects_msg(),
);
$plugin = $RCMAIL->plugins->exec_hook('message_objects',
array('content' => $content, 'message' => $MESSAGE));
$content = implode("\n", $plugin['content']);
return html::div($attrib, $content);
}
function rcmail_contact_exists($email)
{
global $RCMAIL;
if ($email) {
// @TODO: search in all address books?
$CONTACTS = $RCMAIL->get_address_book(-1, true);
if (is_object($CONTACTS)) {
$existing = $CONTACTS->search('email', $email, 1, false);
if ($existing->count) {
return true;
}
}
}
return false;
}
function rcmail_message_contactphoto($attrib)
{
global $RCMAIL, $MESSAGE;
$placeholder = $attrib['placeholder'] ? $RCMAIL->output->abs_url($attrib['placeholder'], true) : null;
$placeholder = $RCMAIL->output->asset_url($placeholder ?: 'program/resources/blank.gif');
if ($MESSAGE->sender) {
$photo_img = $RCMAIL->url(array(
'_task' => 'addressbook',
'_action' => 'photo',
'_email' => $MESSAGE->sender['mailto'],
'_error' => strpos($placeholder, 'blank.gif') === false ? 1 : null,
));
$attrib['onerror'] = "this.src = '$placeholder'; this.onerror = null";
}
else {
$photo_img = $placeholder;
}
return html::img(array('src' => $photo_img, 'alt' => $RCMAIL->gettext('contactphoto')) + $attrib);
}
/**
* Returns table with message headers
*/
function rcmail_message_headers($attrib, $headers=null)
{
global $MESSAGE, $PRINT_MODE, $RCMAIL;
static $sa_attrib;
// keep header table attrib
if (is_array($attrib) && !$sa_attrib && !$attrib['valueof']) {
$sa_attrib = $attrib;
}
else if (!is_array($attrib) && is_array($sa_attrib)) {
$attrib = $sa_attrib;
}
if (!isset($MESSAGE)) {
return false;
}
// get associative array of headers object
if (!$headers) {
$headers_obj = $MESSAGE->headers;
$headers = get_object_vars($MESSAGE->headers);
}
else if (is_object($headers)) {
$headers_obj = $headers;
$headers = get_object_vars($headers_obj);
}
else {
$headers_obj = rcube_message_header::from_array($headers);
}
// show these headers
$standard_headers = array('subject', 'from', 'sender', 'to', 'cc', 'bcc', 'replyto',
'mail-reply-to', 'mail-followup-to', 'date', 'priority');
$exclude_headers = $attrib['exclude'] ? explode(',', $attrib['exclude']) : array();
$output_headers = array();
foreach ($standard_headers as $hkey) {
if ($headers[$hkey]) {
$value = $headers[$hkey];
}
else if ($headers['others'][$hkey]) {
$value = $headers['others'][$hkey];
}
else if (!$attrib['valueof']) {
continue;
}
if (in_array($hkey, $exclude_headers)) {
continue;
}
$ishtml = false;
$header_title = $RCMAIL->gettext(preg_replace('/(^mail-|-)/', '', $hkey));
if ($hkey == 'date') {
$header_value = $RCMAIL->format_date($value,
$PRINT_MODE ? $RCMAIL->config->get('date_long', 'x') : null);
}
else if ($hkey == 'priority') {
if ($value) {
$header_value = html::span('prio' . $value, rcube::Q(rcmail_localized_priority($value)));
$ishtml = true;
}
else {
continue;
}
}
else if ($hkey == 'replyto') {
if ($headers['replyto'] != $headers['from']) {
$header_value = rcmail_address_string($value, $attrib['max'], true,
$attrib['addicon'], $headers['charset'], $header_title);
$ishtml = true;
}
else {
continue;
}
}
else if ($hkey == 'mail-reply-to') {
if ($headers['mail-replyto'] != $headers['replyto']
&& $headers['replyto'] != $headers['from']
) {
$header_value = rcmail_address_string($value, $attrib['max'], true,
$attrib['addicon'], $headers['charset'], $header_title);
$ishtml = true;
}
else {
continue;
}
}
else if ($hkey == 'sender') {
if ($headers['sender'] != $headers['from']) {
$header_value = rcmail_address_string($value, $attrib['max'], true,
$attrib['addicon'], $headers['charset'], $header_title);
$ishtml = true;
}
else {
continue;
}
}
else if ($hkey == 'mail-followup-to') {
$header_value = rcmail_address_string($value, $attrib['max'], true,
$attrib['addicon'], $headers['charset'], $header_title);
$ishtml = true;
}
else if (in_array($hkey, array('from', 'to', 'cc', 'bcc'))) {
$header_value = rcmail_address_string($value, $attrib['max'], true,
$attrib['addicon'], $headers['charset'], $header_title);
$ishtml = true;
}
else if ($hkey == 'subject' && empty($value)) {
$header_value = $RCMAIL->gettext('nosubject');
}
else {
$value = is_array($value) ? implode(' ', $value) : $value;
$header_value = trim(rcube_mime::decode_header($value, $headers['charset']));
}
$output_headers[$hkey] = array(
'title' => $header_title,
'value' => $header_value,
'raw' => $value,
'html' => $ishtml,
);
}
$plugin = $RCMAIL->plugins->exec_hook('message_headers_output', array(
'output' => $output_headers,
'headers' => $headers_obj,
'exclude' => $exclude_headers, // readonly
'folder' => $MESSAGE->folder, // readonly
'uid' => $MESSAGE->uid, // readonly
));
// single header value is requested
if (!empty($attrib['valueof'])) {
$row = $plugin['output'][$attrib['valueof']];
return $row['html'] ? $row['value'] : rcube::Q($row['value']);
}
// compose html table
$table = new html_table(array('cols' => 2));
foreach ($plugin['output'] as $hkey => $row) {
$val = $row['html'] ? $row['value'] : rcube::Q($row['value']);
$table->add(array('class' => 'header-title'), rcube::Q($row['title']));
$table->add(array('class' => 'header '.$hkey), $val);
}
return $table->show($attrib);
}
/**
* Returns element with "From|To <sender|recipient> on <date>"
*/
function rcmail_message_summary($attrib)
{
global $MESSAGE, $RCMAIL;
if (!isset($MESSAGE) || empty($MESSAGE->headers)) {
return;
}
$header = $MESSAGE->context ? 'from' : rcmail_message_list_smart_column_name();
$label = 'shortheader' . $header;
$date = $RCMAIL->format_date($MESSAGE->headers->date, $RCMAIL->config->get('date_long', 'x'));
$user = $MESSAGE->headers->$header;
if (!$user && $header == 'to') {
$user = $MESSAGE->headers->cc;
}
if (!$user && $header == 'to') {
$user = $MESSAGE->headers->bcc;
}
$vars[$header] = rcmail_address_string($user, 1, true, $attrib['addicon'], $MESSAGE->headers->charset);
$vars['date'] = html::span('text-nowrap', $date);
if (empty($user)) {
$label = 'shortheaderdate';
}
$out = html::span(null, $RCMAIL->gettext(array('name' => $label, 'vars' => $vars)));
return html::div($attrib, $out);
}
/**
* Convert Priority header value into a localized string
*/
function rcmail_localized_priority($value)
{
global $RCMAIL;
$labels_map = array(
'1' => 'highest',
'2' => 'high',
'3' => 'normal',
'4' => 'low',
'5' => 'lowest',
);
if ($value && $labels_map[$value]) {
return $RCMAIL->gettext($labels_map[$value]);
}
return '';
}
/**
* Returns block to show full message headers
*/
function rcmail_message_full_headers($attrib)
{
global $OUTPUT, $RCMAIL;
$html = html::div(array('id' => "all-headers", 'class' => "all", 'style' => 'display:none'),
html::div(array('id' => 'headers-source'), ''));
$html .= html::div(array(
'class' => "more-headers show-headers",
'onclick' => "return ".rcmail_output::JS_OBJECT_NAME.".command('show-headers','',this)",
'title' => $RCMAIL->gettext('togglefullheaders')
), '');
$OUTPUT->add_gui_object('all_headers_row', 'all-headers');
$OUTPUT->add_gui_object('all_headers_box', 'headers-source');
return html::div($attrib, $html);
}
/**
* Handler for the 'messagebody' GUI object
*
* @param array Named parameters
* @return string HTML content showing the message body
*/
function rcmail_message_body($attrib)
{
global $OUTPUT, $MESSAGE, $RCMAIL, $REMOTE_OBJECTS;
if (!is_array($MESSAGE->parts) && empty($MESSAGE->body)) {
return '';
}
if (!$attrib['id'])
$attrib['id'] = 'rcmailMsgBody';
$safe_mode = $MESSAGE->is_safe || intval($_GET['_safe']);
$out = '';
$part_no = 0;
$header_attrib = array();
foreach ($attrib as $attr => $value) {
if (preg_match('/^headertable([a-z]+)$/i', $attr, $regs)) {
$header_attrib[$regs[1]] = $value;
}
}
if (!empty($MESSAGE->parts)) {
foreach ($MESSAGE->parts as $part) {
if ($part->type == 'headers') {
$out .= html::div('message-partheaders', rcmail_message_headers(count($header_attrib) ? $header_attrib : null, $part->headers));
}
else if ($part->type == 'content') {
// unsupported (e.g. encrypted)
if ($part->realtype) {
if ($part->realtype == 'multipart/encrypted' || $part->realtype == 'application/pkcs7-mime') {
if (!empty($_SESSION['browser_caps']['pgpmime']) && ($pgp_mime_part = $MESSAGE->get_multipart_encrypted_part())) {
$out .= html::span('part-notice', $RCMAIL->gettext('externalmessagedecryption'));
$OUTPUT->set_env('pgp_mime_part', $pgp_mime_part->mime_id);
$OUTPUT->set_env('pgp_mime_container', '#' . $attrib['id']);
$OUTPUT->add_label('loadingdata');
}
if (!$MESSAGE->encrypted_part) {
$out .= html::span('part-notice', $RCMAIL->gettext('encryptedmessage'));
}
}
continue;
}
else if (!$part->size) {
continue;
}
// Check if we have enough memory to handle the message in it
// #1487424: we need up to 10x more memory than the body
else if (!rcube_utils::mem_check($part->size * 10)) {
$out .= rcmail_part_too_big_message($MESSAGE, $part->mime_id);
continue;
}
// fetch part body
$body = $MESSAGE->get_part_body($part->mime_id, true);
// message is cached but not exists (#1485443), or other error
if ($body === false) {
rcmail_message_error($MESSAGE->uid);
}
$plugin = $RCMAIL->plugins->exec_hook('message_body_prefix',
array('part' => $part, 'prefix' => ''));
// Set attributes of the part container
$container_class = $part->ctype_secondary == 'html' ? 'message-htmlpart' : 'message-part';
$container_id = $container_class . (++$part_no);
$container_attrib = array('class' => $container_class, 'id' => $container_id);
$body_args = array(
'safe' => $safe_mode,
'plain' => !$RCMAIL->config->get('prefer_html'),
'css_prefix' => 'v' . $part_no,
'body_class' => 'rcmBody',
'container_id' => $container_id,
'container_attrib' => $container_attrib,
);
// Parse the part content for display
$body = rcmail_print_body($body, $part, $body_args);
// check if the message body is PGP encrypted
if (strpos($body, '-----BEGIN PGP MESSAGE-----') !== false) {
$OUTPUT->set_env('is_pgp_content', '#' . $container_id);
}
if ($part->ctype_secondary == 'html') {
$body = rcmail_html4inline($body, $body_args);
}
$out .= html::div($container_attrib, $plugin['prefix'] . $body);
}
}
}
else {
// Check if we have enough memory to handle the message in it
// #1487424: we need up to 10x more memory than the body
if (!rcube_utils::mem_check(strlen($MESSAGE->body) * 10)) {
$out .= rcmail_part_too_big_message($MESSAGE, 0);
}
else {
$plugin = $RCMAIL->plugins->exec_hook('message_body_prefix',
array('part' => $MESSAGE, 'prefix' => ''));
$out .= html::div('message-part',
$plugin['prefix'] . rcmail_plain_body($MESSAGE->body));
}
}
// list images after mail body
if ($RCMAIL->config->get('inline_images', true) && !empty($MESSAGE->attachments)) {
$thumbnail_size = $RCMAIL->config->get('image_thumbnail_size', 240);
$client_mimetypes = (array)$RCMAIL->config->get('client_mimetypes');
$show_label = rcube::Q($RCMAIL->gettext('showattachment'));
$download_label = rcube::Q($RCMAIL->gettext('download'));
foreach ($MESSAGE->attachments as $attach_prop) {
// skip inline images
if ($attach_prop->content_id && $attach_prop->disposition == 'inline') {
continue;
}
// Content-Type: image/*...
if ($mimetype = rcmail_part_image_type($attach_prop)) {
// display thumbnails
if ($thumbnail_size) {
$supported = in_array($mimetype, $client_mimetypes);
$show_link_attr = array(
'href' => $MESSAGE->get_part_url($attach_prop->mime_id, false),
'onclick' => sprintf(
'return %s.command(\'load-attachment\',\'%s\',this)',
rcmail_output::JS_OBJECT_NAME,
$attach_prop->mime_id
)
);
$download_link_attr = array(
'href' => $show_link_attr['href'] . '&_download=1',
);
$show_link = html::a($show_link_attr + array('class' => 'open'), $show_label);
$download_link = html::a($download_link_attr + array('class' => 'download'), $download_label);
$out .= html::p(array('class' => 'image-attachment', 'style' => $supported ? '' : 'display:none'),
html::a($show_link_attr + array('class' => 'image-link', 'style' => sprintf('width:%dpx', $thumbnail_size)),
html::img(array(
'class' => 'image-thumbnail',
'src' => $MESSAGE->get_part_url($attach_prop->mime_id, 'image') . '&_thumb=1',
'title' => $attach_prop->filename,
'alt' => $attach_prop->filename,
'style' => sprintf('max-width:%dpx; max-height:%dpx', $thumbnail_size, $thumbnail_size),
'onload' => $supported ? '' : '$(this).parents(\'p.image-attachment\').show()',
))
) .
html::span('image-filename', rcube::Q($attach_prop->filename)) .
html::span('image-filesize', rcube::Q($RCMAIL->message_part_size($attach_prop))) .
html::span('attachment-links', ($supported ? $show_link . '&nbsp;' : '') . $download_link) .
html::br(array('style' => 'clear:both'))
);
}
else {
$out .= html::tag('fieldset', 'image-attachment',
html::tag('legend', 'image-filename', rcube::Q($attach_prop->filename)) .
html::p(array('align' => 'center'),
html::img(array(
'src' => $MESSAGE->get_part_url($attach_prop->mime_id, 'image'),
'title' => $attach_prop->filename,
'alt' => $attach_prop->filename,
)))
);
}
}
}
}
// tell client that there are blocked remote objects
if ($REMOTE_OBJECTS && !$safe_mode) {
$OUTPUT->set_env('blockedobjects', true);
}
return html::div($attrib, $out);
}
/**
* Returns a HTML notice element for too big message parts
*/
function rcmail_part_too_big_message($MESSAGE, $part_id)
{
global $RCMAIL;
$token = $RCMAIL->get_request_token();
$url = $RCMAIL->url(array(
'task' => 'mail',
'action' => 'get',
'download' => 1,
'uid' => $MESSAGE->uid,
'part' => $part_id,
'mbox' => $MESSAGE->folder,
'token' => $token,
));
return html::span('part-notice', $RCMAIL->gettext('messagetoobig') . '&nbsp;' . html::a($url, $RCMAIL->gettext('download')));
}
diff --git a/program/steps/mail/viewsource.inc b/program/steps/mail/viewsource.inc
index 0a6f1f0d6..532ea073b 100644
--- a/program/steps/mail/viewsource.inc
+++ b/program/steps/mail/viewsource.inc
@@ -1,85 +1,83 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/mail/viewsource.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2016, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Display a mail message similar as a usual mail application does |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
if (!empty($_GET['_save'])) {
$RCMAIL->request_security_check(rcube_utils::INPUT_GET);
}
ob_end_clean();
// similar code as in program/steps/mail/get.inc
if ($uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GET)) {
if ($pos = strpos($uid, '.')) {
$message = new rcube_message($uid);
$headers = $message->headers;
$part_id = substr($uid, $pos + 1);
}
else {
$headers = $RCMAIL->storage->get_message_headers($uid);
}
$charset = $headers->charset ?: $RCMAIL->config->get('default_charset');
if (!empty($_GET['_save'])) {
$subject = rcube_mime::decode_header($headers->subject, $headers->charset);
$filename = rcmail_filename_from_subject(mb_substr($subject, 0, 128));
$filename = ($filename ?: $uid) . '.eml';
$RCMAIL->output->download_headers($filename, array(
'length' => $headers->size,
'type' => 'text/plain',
'type_charset' => $charset,
));
}
else {
header("Content-Type: text/plain; charset={$charset}");
}
if (isset($message)) {
$message->get_part_body($part_id, empty($_GET['_save']), 0, -1);
}
else {
$RCMAIL->storage->print_raw_body($uid, empty($_GET['_save']));
}
}
else {
rcube::raise_error(array(
'code' => 500,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "Message UID $uid not found"
),
true, true);
}
exit;
/**
* Helper function to convert message subject into filename
*/
function rcmail_filename_from_subject($str)
{
$str = preg_replace('/[:\t\n\r\0\x0B\/]+\s*/', ' ', $str);
return trim($str, " \t\n\r\0\x0B./_");
}
diff --git a/program/steps/settings/about.inc b/program/steps/settings/about.inc
index fe23cc434..322fabb8d 100644
--- a/program/steps/settings/about.inc
+++ b/program/steps/settings/about.inc
@@ -1,111 +1,109 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/settings/about.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
- | Copyright (C) 2011-2013, Kolab Systems AG |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
+ | Copyright (C) Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Display license information about program and enabled plugins |
- | |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
define('RC_COPYRIGHT', 'Copyright &copy; 2005-2019, The Roundcube Dev Team');
define('RC_LICENSE', 'This program is free software; you can redistribute it and/or modify it under the terms of the '
. '<a href="http://www.gnu.org/licenses/gpl.html" target="_blank">GNU General Public License</a> '
. 'as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.'
. '<br/>Some <a href="https://roundcube.net/license" target="_blank">exceptions</a> for skins &amp; plugins apply.');
$OUTPUT->set_pagetitle($RCMAIL->gettext('about'));
$OUTPUT->add_handlers(array(
'supportlink' => 'rcmail_supportlink',
'pluginlist' => 'rcmail_plugins_list',
'copyright' => function() { return RC_COPYRIGHT; },
'license' => function() { return RC_LICENSE; },
));
$OUTPUT->send('about');
function rcmail_supportlink($attrib)
{
global $RCMAIL;
if ($url = $RCMAIL->config->get('support_url')) {
$label = $attrib['label'] ?: 'support';
$attrib['href'] = $url;
return html::a($attrib, $RCMAIL->gettext($label));
}
}
function rcmail_plugins_list($attrib)
{
global $RCMAIL;
if (!$attrib['id']) {
$attrib['id'] = 'rcmpluginlist';
}
$plugins = array_filter($RCMAIL->plugins->active_plugins);
$plugin_info = array();
foreach ($plugins as $name) {
if ($info = $RCMAIL->plugins->get_info($name)) {
$plugin_info[$name] = $info;
}
}
// load info from required plugins, too
foreach ($plugin_info as $name => $info) {
if (is_array($info['require']) && !empty($info['require'])) {
foreach ($info['require'] as $req_name) {
if (!isset($plugin_info[$req_name]) && ($req_info = $RCMAIL->plugins->get_info($req_name))) {
$plugin_info[$req_name] = $req_info;
}
}
}
}
if (empty($plugin_info)) {
return '';
}
ksort($plugin_info, SORT_LOCALE_STRING);
$table = new html_table($attrib);
// add table header
$table->add_header('name', $RCMAIL->gettext('plugin'));
$table->add_header('version', $RCMAIL->gettext('version'));
$table->add_header('license', $RCMAIL->gettext('license'));
$table->add_header('source', $RCMAIL->gettext('source'));
foreach ($plugin_info as $name => $data) {
$uri = $data['src_uri'] ?: $data['uri'];
if ($uri && stripos($uri, 'http') !== 0) {
$uri = 'http://' . $uri;
}
$table->add_row();
$table->add('name', rcube::Q($data['name'] ?: $name));
$table->add('version', rcube::Q($data['version']));
$table->add('license', $data['license_uri'] ? html::a(array('target' => '_blank', 'href' => rcube::Q($data['license_uri'])),
rcube::Q($data['license'])) : $data['license']);
$table->add('source', $uri ? html::a(array('target' => '_blank', 'href' => rcube::Q($uri)),
rcube::Q($RCMAIL->gettext('download'))) : '');
}
return $table->show();
}
diff --git a/program/steps/settings/edit_folder.inc b/program/steps/settings/edit_folder.inc
index ba55772ca..619985741 100644
--- a/program/steps/settings/edit_folder.inc
+++ b/program/steps/settings/edit_folder.inc
@@ -1,346 +1,344 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/settings/edit_folder.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide functionality to create/edit a folder |
- | |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
// register UI objects
$OUTPUT->add_handlers(array(
'folderdetails' => 'rcmail_folder_form',
));
$OUTPUT->add_label('nonamewarning');
$OUTPUT->send('folderedit');
// WARNING: folder names in UI are encoded with RCUBE_CHARSET
function rcmail_folder_form($attrib)
{
global $RCMAIL;
$storage = $RCMAIL->get_storage();
// edited folder name (empty in create-folder mode)
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GPC, true);
// predefined path for new folder
$parent = rcube_utils::get_input_value('_path', rcube_utils::INPUT_GPC, true);
$threading_supported = $storage->get_capability('THREAD');
$dual_use_supported = $storage->get_capability(rcube_storage::DUAL_USE_FOLDERS);
$delimiter = $storage->get_hierarchy_delimiter();
// Get mailbox parameters
if (strlen($mbox)) {
$options = rcmail_folder_options($mbox);
$namespace = $storage->get_namespace();
$path = explode($delimiter, $mbox);
$folder = array_pop($path);
$path = implode($delimiter, $path);
$folder = rcube_charset::convert($folder, 'UTF7-IMAP');
$hidden_fields = array('name' => '_mbox', 'value' => $mbox);
}
else {
$options = array();
$path = $parent;
// allow creating subfolders of INBOX folder
if ($path == 'INBOX') {
$path = $storage->mod_folder($path, 'in');
}
}
// remove personal namespace prefix
if (strlen($path)) {
$path_id = $path;
$path = $storage->mod_folder($path.$delimiter);
if ($path[strlen($path)-1] == $delimiter) {
$path = substr($path, 0, -1);
}
}
$form = array();
// General tab
$form['props'] = array(
'name' => $RCMAIL->gettext('properties'),
);
// Location (name)
if ($options['protected']) {
$foldername = str_replace($delimiter, ' &raquo; ', rcube::Q($RCMAIL->localize_foldername($mbox, false, true)));
}
else if ($options['norename']) {
$foldername = rcube::Q($folder);
}
else {
if (isset($_POST['_name'])) {
$folder = trim(rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST, true));
}
$foldername = new html_inputfield(array('name' => '_name', 'id' => '_name', 'size' => 30));
$foldername = '<span class="input-group">' . $foldername->show($folder);
if ($options['special'] && ($sname = $RCMAIL->localize_foldername($mbox, false, true)) != $folder) {
$foldername .= ' <span class="input-group-append"><span class="input-group-text">(' . rcube::Q($sname) .')</span></span>';
}
$foldername .= '</span>';
}
$form['props']['fieldsets']['location'] = array(
'name' => $RCMAIL->gettext('location'),
'content' => array(
'name' => array(
'label' => $RCMAIL->gettext('foldername'),
'value' => $foldername,
),
),
);
if (!empty($options) && ($options['norename'] || $options['protected'])) {
// prevent user from moving folder
$hidden_path = new html_hiddenfield(array('name' => '_parent', 'value' => $path));
$form['props']['fieldsets']['location']['content']['name']['value'] .= $hidden_path->show();
}
else {
$selected = isset($_POST['_parent']) ? $_POST['_parent'] : $path_id;
$exceptions = array($mbox);
// Exclude 'prefix' namespace from parent folders list (#1488349)
// If INBOX. namespace exists, folders created as INBOX subfolders
// will be listed at the same level - selecting INBOX as a parent does nothing
if ($prefix = $storage->get_namespace('prefix')) {
$exceptions[] = substr($prefix, 0, -1);
}
$select = $RCMAIL->folder_selector(array(
'id' => '_parent',
'name' => '_parent',
'noselection' => '---',
'maxlength' => 150,
'unsubscribed' => true,
'skip_noinferiors' => true,
'exceptions' => $exceptions,
'additional' => strlen($selected) ? array($selected) : null,
));
$form['props']['fieldsets']['location']['content']['parent'] = array(
'label' => $RCMAIL->gettext('parentfolder'),
'value' => $select->show($selected),
);
}
// Settings
$form['props']['fieldsets']['settings'] = array(
'name' => $RCMAIL->gettext('settings'),
);
// For servers that do not support both sub-folders and messages in a folder
if (!$dual_use_supported) {
if (!strlen($mbox)) {
$select = new html_select(array('name' => '_type', 'id' => '_type'));
$select->add($RCMAIL->gettext('dualusemail'), 'mail');
$select->add($RCMAIL->gettext('dualusefolder'), 'folder');
$value = rcube_utils::get_input_value('_type', rcube_utils::INPUT_POST);
$value = $select->show($value ?: 'mail');
}
else {
$value = $options['noselect'] ? 'folder' : 'mail';
$value = $RCMAIL->gettext('dualuse' . $value);
}
$form['props']['fieldsets']['settings']['content']['type'] = array(
'label' => $RCMAIL->gettext('dualuselabel'),
'value' => $value,
);
}
// Settings: threading
if ($threading_supported && ($mbox == 'INBOX' || (!$options['noselect'] && !$options['is_root']))) {
$select = new html_select(array('name' => '_viewmode', 'id' => '_viewmode'));
$select->add($RCMAIL->gettext('list'), 0);
$select->add($RCMAIL->gettext('threads'), 1);
if (isset($_POST['_viewmode'])) {
$value = (int) $_POST['_viewmode'];
}
else if (strlen($mbox)) {
$a_threaded = $RCMAIL->config->get('message_threading', array());
$default_mode = $RCMAIL->config->get('default_list_mode', 'list');
$value = (int) (isset($a_threaded[$mbox]) ? $a_threaded[$mbox] : $default_mode == 'threads');
}
$form['props']['fieldsets']['settings']['content']['viewmode'] = array(
'label' => $RCMAIL->gettext('listmode'),
'value' => $select->show($value),
);
}
/*
// Settings: sorting column
$select = new html_select(array('name' => '_sortcol', 'id' => '_sortcol'));
$select->add($RCMAIL->gettext('nonesort'), '');
$select->add($RCMAIL->gettext('arrival'), 'arrival');
$select->add($RCMAIL->gettext('sentdate'), 'date');
$select->add($RCMAIL->gettext('subject'), 'subject');
$select->add($RCMAIL->gettext('fromto'), 'from');
$select->add($RCMAIL->gettext('replyto'), 'replyto');
$select->add($RCMAIL->gettext('cc'), 'cc');
$select->add($RCMAIL->gettext('size'), 'size');
$value = isset($_POST['_sortcol']) ? $_POST['_sortcol'] : '';
$form['props']['fieldsets']['settings']['content']['sortcol'] = array(
'label' => $RCMAIL->gettext('listsorting'),
'value' => $select->show($value),
);
// Settings: sorting order
$select = new html_select(array('name' => '_sortord', 'id' => '_sortord'));
$select->add($RCMAIL->gettext('asc'), 'ASC');
$select->add($RCMAIL->gettext('desc'), 'DESC');
$value = isset($_POST['_sortord']) ? $_POST['_sortord'] : '';
$form['props']['fieldsets']['settings']['content']['sortord'] = array(
'label' => $RCMAIL->gettext('listorder'),
'value' => $select->show(),
);
*/
// Information (count, size) - Edit mode
if (strlen($mbox)) {
// Number of messages
$form['props']['fieldsets']['info'] = array(
'name' => $RCMAIL->gettext('info'),
'content' => array()
);
if ((!$options['noselect'] && !$options['is_root']) || $mbox == 'INBOX') {
$msgcount = $storage->count($mbox, 'ALL', true, false);
// Size
if ($msgcount) {
// create link with folder-size command
$onclick = sprintf("return %s.command('folder-size', '%s', this)",
rcmail_output::JS_OBJECT_NAME, rcube::JQ($mbox));
$size = html::a(array('href' => '#', 'onclick' => $onclick,
'id' => 'folder-size'), $RCMAIL->gettext('getfoldersize'));
}
else {
// no messages -> zero size
$size = 0;
}
$form['props']['fieldsets']['info']['content']['count'] = array(
'label' => $RCMAIL->gettext('messagecount'),
'value' => (int) $msgcount
);
$form['props']['fieldsets']['info']['content']['size'] = array(
'label' => $RCMAIL->gettext('size'),
'value' => $size,
);
}
// show folder type only if we have non-private namespaces
if (!empty($namespace['shared']) || !empty($namespace['others'])) {
$form['props']['fieldsets']['info']['content']['foldertype'] = array(
'label' => $RCMAIL->gettext('foldertype'),
'value' => $RCMAIL->gettext($options['namespace'] . 'folder'));
}
}
// Allow plugins to modify folder form content
$plugin = $RCMAIL->plugins->exec_hook('folder_form',
array('form' => $form, 'options' => $options,
'name' => $mbox, 'parent_name' => $parent));
$form = $plugin['form'];
// Set form tags and hidden fields
list($form_start, $form_end) = get_form_tags($attrib, 'save-folder', null, $hidden_fields);
unset($attrib['form'], $attrib['id']);
// return the complete edit form as table
$out = "$form_start\n";
// Create form output
foreach ($form as $idx => $tab) {
if (!empty($tab['fieldsets']) && is_array($tab['fieldsets'])) {
$content = '';
foreach ($tab['fieldsets'] as $fieldset) {
$subcontent = rcmail_get_form_part($fieldset, $attrib);
if ($subcontent) {
$subcontent = html::tag('legend', null, rcube::Q($fieldset['name'])) . $subcontent;
$content .= html::tag('fieldset', null, $subcontent) ."\n";
}
}
}
else {
$content = rcmail_get_form_part($tab, $attrib);
}
if ($idx != 'props') {
$out .= html::tag('fieldset', null, html::tag('legend', null, rcube::Q($tab['name'])) . $content) ."\n";
}
else {
$out .= $content ."\n";
}
}
$out .= "\n$form_end";
$RCMAIL->output->set_env('messagecount', (int) $msgcount);
$RCMAIL->output->set_env('folder', $mbox);
if ($mbox !== null && empty($_POST)) {
$RCMAIL->output->command('parent.set_quota', $RCMAIL->quota_content(null, $mbox));
}
return $out;
}
function rcmail_get_form_part($form, $attrib = array())
{
global $RCMAIL;
$content = '';
if (is_array($form['content']) && !empty($form['content'])) {
$table = new html_table(array('cols' => 2));
foreach ($form['content'] as $col => $colprop) {
$colprop['id'] = '_'.$col;
$label = $colprop['label'] ?: $RCMAIL->gettext($col);
$table->add('title', html::label($colprop['id'], rcube::Q($label)));
$table->add(null, $colprop['value']);
}
$content = $table->show($attrib);
}
else {
$content = $form['content'];
}
return $content;
}
diff --git a/program/steps/settings/edit_identity.inc b/program/steps/settings/edit_identity.inc
index 71b7c491b..133c50ffb 100644
--- a/program/steps/settings/edit_identity.inc
+++ b/program/steps/settings/edit_identity.inc
@@ -1,199 +1,197 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/settings/edit_identity.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Show edit form for a identity record or to add a new one |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
define('IDENTITIES_LEVEL', intval($RCMAIL->config->get('identities_level', 0)));
// edit-identity
if (($_GET['_iid'] || $_POST['_iid']) && $RCMAIL->action=='edit-identity') {
$id = rcube_utils::get_input_value('_iid', rcube_utils::INPUT_GPC);
$IDENTITY_RECORD = $RCMAIL->user->get_identity($id);
if (is_array($IDENTITY_RECORD)) {
$OUTPUT->set_env('iid', $IDENTITY_RECORD['identity_id']);
}
else {
$OUTPUT->show_message('dberror', 'error');
// go to identities page
$RCMAIL->overwrite_action('identities');
return;
}
}
// add-identity
else {
if (IDENTITIES_LEVEL > 1) {
$OUTPUT->show_message('opnotpermitted', 'error');
// go to identities page
$RCMAIL->overwrite_action('identities');
return;
}
else if (IDENTITIES_LEVEL == 1) {
$IDENTITY_RECORD['email'] = $RCMAIL->get_user_email();
}
}
$OUTPUT->include_script('list.js');
$OUTPUT->add_handler('identityform', 'rcube_identity_form');
$OUTPUT->set_env('identities_level', IDENTITIES_LEVEL);
$OUTPUT->add_label('deleteidentityconfirm', 'generate',
'encryptioncreatekey', 'openmailvelopesettings', 'encryptionprivkeysinmailvelope',
'encryptionnoprivkeysinmailvelope', 'keypaircreatesuccess');
$OUTPUT->set_pagetitle($RCMAIL->gettext(($RCMAIL->action == 'add-identity' ? 'addidentity' : 'editidentity')));
if ($RCMAIL->action == 'add-identity' && $OUTPUT->template_exists('identityadd')) {
$OUTPUT->send('identityadd');
}
$OUTPUT->send('identityedit');
function rcube_identity_form($attrib)
{
global $IDENTITY_RECORD, $RCMAIL, $OUTPUT;
// Add HTML editor script(s)
$RCMAIL->html_editor('identity');
// add some labels to client
$OUTPUT->add_label('noemailwarning', 'converting', 'editorwarning');
$i_size = $attrib['size'] ?: 40;
$t_rows = $attrib['textarearows'] ?: 6;
$t_cols = $attrib['textareacols'] ?: 40;
// list of available cols
$form = array(
'addressing' => array(
'name' => $RCMAIL->gettext('settings'),
'content' => array(
'name' => array('type' => 'text', 'size' => $i_size),
'email' => array('type' => 'text', 'size' => $i_size),
'organization' => array('type' => 'text', 'size' => $i_size),
'reply-to' => array('type' => 'text', 'size' => $i_size),
'bcc' => array('type' => 'text', 'size' => $i_size),
'standard' => array('type' => 'checkbox', 'label' => $RCMAIL->gettext('setdefault')),
)),
'signature' => array(
'name' => $RCMAIL->gettext('signature'),
'content' => array(
'signature' => array('type' => 'textarea', 'size' => $t_cols, 'rows' => $t_rows,
'spellcheck' => true, 'data-html-editor' => true),
'html_signature' => array('type' => 'checkbox',
'label' => $RCMAIL->gettext('htmlsignature'),
'onclick' => 'return rcmail.command(\'toggle-editor\', {id: \'rcmfd_signature\', html: this.checked}, \'\', event)'),
)),
'encryption' => array(
'name' => $RCMAIL->gettext('identityencryption'),
'attrs' => array('class' => 'identity-encryption', 'style' => 'display:none'),
'content' => html::div('identity-encryption-block', '')
)
);
// Enable TinyMCE editor
if ($IDENTITY_RECORD['html_signature']) {
$form['signature']['content']['signature']['class'] = 'mce_editor';
$form['signature']['content']['signature']['is_escaped'] = true;
// Correctly handle HTML entities in HTML editor (#1488483)
$IDENTITY_RECORD['signature'] = htmlspecialchars($IDENTITY_RECORD['signature'], ENT_NOQUOTES, RCUBE_CHARSET);
}
// hide "default" checkbox if only one identity is allowed
if (IDENTITIES_LEVEL > 1) {
unset($form['addressing']['content']['standard']);
}
// disable some field according to access level
if (IDENTITIES_LEVEL == 1 || IDENTITIES_LEVEL == 3) {
$form['addressing']['content']['email']['disabled'] = true;
$form['addressing']['content']['email']['class'] = 'disabled';
}
if (IDENTITIES_LEVEL == 4) {
foreach ($form['addressing']['content'] as $formfield => $value){
$form['addressing']['content'][$formfield]['disabled'] = true;
$form['addressing']['content'][$formfield]['class'] = 'disabled';
}
}
$IDENTITY_RECORD['email'] = rcube_utils::idn_to_utf8($IDENTITY_RECORD['email']);
// Allow plugins to modify identity form content
$plugin = $RCMAIL->plugins->exec_hook('identity_form', array(
'form' => $form, 'record' => $IDENTITY_RECORD));
$form = $plugin['form'];
$IDENTITY_RECORD = $plugin['record'];
// Set form tags and hidden fields
list($form_start, $form_end) = get_form_tags($attrib, 'save-identity',
intval($IDENTITY_RECORD['identity_id']),
array('name' => '_iid', 'value' => $IDENTITY_RECORD['identity_id']));
unset($plugin);
unset($attrib['form'], $attrib['id']);
// return the complete edit form as table
$out = "$form_start\n";
foreach ($form as $fieldset) {
if (empty($fieldset['content'])) {
continue;
}
$content = '';
if (is_array($fieldset['content'])) {
$table = new html_table(array('cols' => 2));
foreach ($fieldset['content'] as $col => $colprop) {
$colprop['id'] = 'rcmfd_'.$col;
$label = $colprop['label'] ?: $RCMAIL->gettext(str_replace('-', '', $col));
$value = $colprop['value'] ?: rcube_output::get_edit_field($col, $IDENTITY_RECORD[$col], $colprop, $colprop['type']);
$table->add('title', html::label($colprop['id'], rcube::Q($label)));
$table->add(null, $value);
}
$content = $table->show($attrib);
}
else {
$content = $fieldset['content'];
}
$content = html::tag('legend', null, rcube::Q($fieldset['name'])) . $content;
$out .= html::tag('fieldset', $fieldset['attrs'], $content) . "\n";
}
$out .= $form_end;
// add image upload form
$max_filesize = $RCMAIL->upload_init($RCMAIL->config->get('identity_image_size', 64) * 1024);
$upload_form_id = 'identityImageUpload';
$out .= '<form id="' . $upload_form_id . '" style="display: none">'
. html::div('hint', $RCMAIL->gettext(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize))))
. '</form>';
$RCMAIL->output->add_gui_object('uploadform', $upload_form_id);
return $out;
}
diff --git a/program/steps/settings/edit_prefs.inc b/program/steps/settings/edit_prefs.inc
index 87d3133bd..1719df550 100644
--- a/program/steps/settings/edit_prefs.inc
+++ b/program/steps/settings/edit_prefs.inc
@@ -1,87 +1,85 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/settings/edit_prefs.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide functionality for user's settings & preferences |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
if (!$OUTPUT->ajax_call) {
$OUTPUT->set_pagetitle($RCMAIL->gettext('preferences'));
}
$CURR_SECTION = rcube_utils::get_input_value('_section', rcube_utils::INPUT_GPC);
list($SECTIONS,) = rcmail_user_prefs($CURR_SECTION);
// register UI objects
$OUTPUT->add_handlers(array(
'userprefs' => 'rcmail_user_prefs_form',
'sectionname' => 'rcmail_prefs_section_name',
));
$OUTPUT->send('settingsedit');
function rcmail_user_prefs_form($attrib)
{
global $RCMAIL, $CURR_SECTION, $SECTIONS;
// add some labels to client
$RCMAIL->output->add_label('nopagesizewarning', 'nosupporterror');
unset($attrib['form']);
list($form_start, $form_end) = get_form_tags($attrib, 'save-prefs', null,
array('name' => '_section', 'value' => $CURR_SECTION));
$out = $form_start;
if(!empty($SECTIONS[$CURR_SECTION]['header'])) {
$out .= html::div(array('id' => 'preferences-header', 'class' =>'boxcontent'), $SECTIONS[$CURR_SECTION]['header']);
}
foreach ($SECTIONS[$CURR_SECTION]['blocks'] as $class => $block) {
if (!empty($block['options'])) {
$table = new html_table(array('cols' => 2));
foreach ($block['options'] as $option) {
if (isset($option['title'])) {
$table->add('title', $option['title']);
$table->add(null, $option['content']);
}
else {
$table->add(array('colspan' => 2), $option['content']);
}
}
$out .= html::tag('fieldset', $class, html::tag('legend', null, $block['name']) . $table->show($attrib));
}
else if (!empty($block['content'])) {
$out .= html::tag('fieldset', null, html::tag('legend', null, $block['name']) . $block['content']);
}
}
return $out . $form_end;
}
function rcmail_prefs_section_name()
{
global $SECTIONS, $CURR_SECTION;
return $SECTIONS[$CURR_SECTION]['section'];
}
diff --git a/program/steps/settings/edit_response.inc b/program/steps/settings/edit_response.inc
index 7f3ac5080..71d1f2ed0 100644
--- a/program/steps/settings/edit_response.inc
+++ b/program/steps/settings/edit_response.inc
@@ -1,107 +1,105 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/settings/edit_response.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Show edit form for a canned response record or to add a new one |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
$responses = $RCMAIL->get_compose_responses();
// edit-response
if (($key = rcube_utils::get_input_value('_key', rcube_utils::INPUT_GPC))) {
foreach ($responses as $i => $response) {
if ($response['key'] == $key) {
$RESPONSE_RECORD = $response;
$RESPONSE_RECORD['index'] = $i;
break;
}
}
}
// save response
if ($RCMAIL->action == 'save-response' && isset($_POST['_name']) && !$RESPONSE_RECORD['static']) {
$name = trim(rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST));
$text = trim(rcube_utils::get_input_value('_text', rcube_utils::INPUT_POST, true));
if (!empty($name) && !empty($text)) {
$dupes = 0;
foreach ($responses as $i => $resp) {
if ($RESPONSE_RECORD && $RESPONSE_RECORD['index'] === $i)
continue;
if (strcasecmp($name, preg_replace('/\s\(\d+\)$/', '', $resp['name'])) == 0)
$dupes++;
}
if ($dupes) { // require a unique name
$name .= ' (' . ++$dupes . ')';
}
$response = array('name' => $name, 'text' => $text, 'format' => 'text', 'key' => substr(md5($name), 0, 16));
if ($RESPONSE_RECORD && $responses[$RESPONSE_RECORD['index']]) {
$responses[$RESPONSE_RECORD['index']] = $response;
}
else {
$responses[] = $response;
}
$responses = array_filter($responses, function($item){ return empty($item['static']); });
if ($RCMAIL->user->save_prefs(array('compose_responses' => array_values($responses)))) {
$RCMAIL->output->show_message('successfullysaved', 'confirmation');
$RCMAIL->output->command('parent.update_response_row', $response, $key);
$RCMAIL->overwrite_action('edit-response');
$RESPONSE_RECORD = $response;
}
}
else {
$RCMAIL->output->show_message('formincomplete', 'error');
}
}
$OUTPUT->set_env('readonly', !empty($RESPONSE_RECORD['static']));
$OUTPUT->add_handler('responseform', 'rcube_response_form');
$OUTPUT->set_pagetitle($RCMAIL->gettext($RCMAIL->action == 'add-response' ? 'addresponse' : 'editresponse'));
$OUTPUT->send('responseedit');
function rcube_response_form($attrib)
{
global $RCMAIL, $RESPONSE_RECORD;
// Set form tags and hidden fields
$disabled = !empty($RESPONSE_RECORD['static']);
$key = $RESPONSE_RECORD['key'];
list($form_start, $form_end) = get_form_tags($attrib, 'save-response', $key, array('name' => '_key', 'value' => $key));
unset($attrib['form'], $attrib['id']);
// return the complete edit form as table
$out = "$form_start\n";
$table = new html_table(array('cols' => 2));
$table->add('title', html::label('ffname', rcube::Q($RCMAIL->gettext('responsename'))));
$table->add(null, rcube_output::get_edit_field('name', $RESPONSE_RECORD['name'],
array('id' => 'ffname', 'size' => $attrib['size'], 'disabled' => $disabled), 'text'));
$table->add('title', html::label('fftext', rcube::Q($RCMAIL->gettext('responsetext'))));
$table->add(null, rcube_output::get_edit_field('text', $RESPONSE_RECORD['text'],
array('id' => 'fftext', 'size' => $attrib['textareacols'], 'rows' => $attrib['textarearows'], 'disabled' => $disabled), 'textarea'));
$out .= $table->show($attrib);
$out .= $form_end;
return $out;
}
diff --git a/program/steps/settings/folders.inc b/program/steps/settings/folders.inc
index 8902fa28c..dd0198241 100644
--- a/program/steps/settings/folders.inc
+++ b/program/steps/settings/folders.inc
@@ -1,511 +1,509 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/settings/folders.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide functionality of folders management |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
// init IMAP connection
$STORAGE = $RCMAIL->get_storage();
// subscribe mailbox
if ($RCMAIL->action == 'subscribe') {
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true);
if (strlen($mbox)) {
$result = $STORAGE->subscribe(array($mbox));
// Handle virtual (non-existing) folders
if (!$result && $STORAGE->get_error_code() == -1 &&
$STORAGE->get_response_code() == rcube_storage::TRYCREATE
) {
$result = $STORAGE->create_folder($mbox, true);
if ($result) {
// @TODO: remove 'virtual' class of folder's row
}
}
if ($result) {
// Handle subscription of protected folder (#1487656)
if ($RCMAIL->config->get('protect_default_folders')
&& $STORAGE->is_special_folder($mbox)
) {
$OUTPUT->command('disable_subscription', $mbox);
}
$OUTPUT->show_message('foldersubscribed', 'confirmation');
}
else {
$RCMAIL->display_server_error('errorsaving');
$OUTPUT->command('reset_subscription', $mbox, false);
}
}
}
// unsubscribe mailbox
else if ($RCMAIL->action == 'unsubscribe') {
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true);
if (strlen($mbox)) {
$result = $STORAGE->unsubscribe(array($mbox));
if ($result) {
$OUTPUT->show_message('folderunsubscribed', 'confirmation');
}
else {
$RCMAIL->display_server_error('errorsaving');
$OUTPUT->command('reset_subscription', $mbox, true);
}
}
}
// delete an existing mailbox
else if ($RCMAIL->action == 'delete-folder') {
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true);
if (strlen($mbox)) {
$plugin = $RCMAIL->plugins->exec_hook('folder_delete', array('name' => $mbox));
if (!$plugin['abort']) {
$deleted = $STORAGE->delete_folder($plugin['name']);
}
else {
$deleted = $plugin['result'];
}
// #1488692: update session
if ($deleted && $_SESSION['mbox'] === $mbox) {
$RCMAIL->session->remove('mbox');
}
}
if ($OUTPUT->ajax_call && $deleted) {
// Remove folder and subfolders rows
$OUTPUT->command('remove_folder_row', $mbox);
$OUTPUT->show_message('folderdeleted', 'confirmation');
// Clear content frame
$OUTPUT->command('subscription_select');
$OUTPUT->command('set_quota', $RCMAIL->quota_content());
}
else if (!$deleted) {
$RCMAIL->display_server_error('errorsaving');
}
}
// rename an existing mailbox
else if ($RCMAIL->action == 'rename-folder') {
$name = trim(rcube_utils::get_input_value('_folder_newname', rcube_utils::INPUT_POST, true));
$oldname = rcube_utils::get_input_value('_folder_oldname', rcube_utils::INPUT_POST, true);
if (strlen($name) && strlen($oldname)) {
$rename = rcmail_rename_folder($oldname, $name);
}
if ($rename && $OUTPUT->ajax_call) {
rcmail_update_folder_row($name, $oldname);
}
else if (!$rename) {
$RCMAIL->display_server_error('errorsaving');
}
}
// clear mailbox
else if ($RCMAIL->action == 'purge') {
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true);
$delimiter = $STORAGE->get_hierarchy_delimiter();
$trash_mbox = $RCMAIL->config->get('trash_mbox');
$trash_regexp = '/^' . preg_quote($trash . $delimiter, '/') . '/';
// we should only be purging trash (or their subfolders)
if (!strlen($trash_mbox) || $mbox === $trash_mbox
|| preg_match($trash_regexp, $mbox)
) {
$success = $STORAGE->delete_message('*', $mbox);
$delete = true;
}
// move to Trash
else {
$success = $STORAGE->move_message('1:*', $trash_mbox, $mbox);
$delete = false;
}
if ($success) {
$OUTPUT->set_env('messagecount', 0);
if ($delete) {
$OUTPUT->show_message('folderpurged', 'confirmation');
$OUTPUT->command('set_quota', $RCMAIL->quota_content(null, $mbox));
}
else {
$OUTPUT->show_message('messagemoved', 'confirmation');
}
$_SESSION['unseen_count'][$mbox] = 0;
$OUTPUT->command('show_folder', $mbox, null, true);
}
else {
$RCMAIL->display_server_error('errorsaving');
}
}
// get mailbox size
else if ($RCMAIL->action == 'folder-size') {
$name = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true);
$size = $STORAGE->folder_size($name);
// @TODO: check quota and show percentage usage of specified mailbox?
if ($size !== false) {
$OUTPUT->command('folder_size_update', $RCMAIL->show_bytes($size));
}
else {
$RCMAIL->display_server_error();
}
}
if ($OUTPUT->ajax_call) {
$OUTPUT->send();
}
$OUTPUT->set_pagetitle($RCMAIL->gettext('folders'));
$OUTPUT->set_env('prefix_ns', $STORAGE->get_namespace('prefix'));
$OUTPUT->set_env('quota', (bool) $STORAGE->get_capability('QUOTA'));
$OUTPUT->include_script('treelist.js');
// add some labels to client
$OUTPUT->add_label('deletefolderconfirm', 'purgefolderconfirm', 'movefolderconfirm',
'folderdeleting', 'foldermoving', 'foldersubscribing', 'folderunsubscribing',
'move', 'quota');
// register UI objects
$OUTPUT->add_handlers(array(
'foldersubscription' => 'rcmail_folder_subscriptions',
'folderfilter' => 'rcmail_folder_filter',
'quotadisplay' => array($RCMAIL, 'quota_display'),
'searchform' => array($OUTPUT, 'search_form'),
));
$OUTPUT->send('folders');
// build table with all folders listed by server
function rcmail_folder_subscriptions($attrib)
{
global $RCMAIL, $OUTPUT;
if (!$attrib['id']) {
$attrib['id'] = 'rcmSubscriptionlist';
}
$STORAGE = $RCMAIL->get_storage();
// get folders from server
$STORAGE->clear_cache('mailboxes', true);
$a_unsubscribed = $STORAGE->list_folders();
$a_subscribed = $STORAGE->list_folders_subscribed('', '*', null, null, true); // unsorted
$delimiter = $STORAGE->get_hierarchy_delimiter();
$namespace = $STORAGE->get_namespace();
$special_folders = array_flip(array_merge(array('inbox' => 'INBOX'), $STORAGE->get_special_folders()));
$protect_default = $RCMAIL->config->get('protect_default_folders');
$seen = array();
$list_folders = array();
// pre-process folders list
foreach ($a_unsubscribed as $i => $folder) {
$folder_id = $folder;
$folder = $STORAGE->mod_folder($folder);
$foldersplit = explode($delimiter, $folder);
$name = rcube_charset::convert(array_pop($foldersplit), 'UTF7-IMAP');
$parent_folder = join($delimiter, $foldersplit);
$level = count($foldersplit);
// add any necessary "virtual" parent folders
if ($parent_folder && !isset($seen[$parent_folder])) {
for ($i=1; $i<=$level; $i++) {
$ancestor_folder = join($delimiter, array_slice($foldersplit, 0, $i));
if ($ancestor_folder && !$seen[$ancestor_folder]++) {
$ancestor_name = rcube_charset::convert($foldersplit[$i-1], 'UTF7-IMAP');
$list_folders[] = array(
'id' => $ancestor_folder,
'name' => $ancestor_name,
'level' => $i-1,
'virtual' => true,
);
}
}
}
// Handle properly INBOX.INBOX situation
if (isset($seen[$folder])) {
continue;
}
$seen[$folder]++;
$list_folders[] = array(
'id' => $folder_id,
'name' => $name,
'level' => $level,
);
}
unset($seen);
$checkbox_subscribe = new html_checkbox(array(
'name' => '_subscribed[]',
'title' => $RCMAIL->gettext('changesubscription'),
'onclick' => rcmail_output::JS_OBJECT_NAME.".command(this.checked?'subscribe':'unsubscribe',this.value)",
));
$js_folders = array();
$folders = array();
$collapsed = (string) $RCMAIL->config->get('collapsed_folders');
// create list of available folders
foreach ($list_folders as $i => $folder) {
$sub_key = array_search($folder['id'], $a_subscribed);
$subscribed = $sub_key !== false;
$special = $folder['id'] == 'INBOX' || isset($special_folders[$folder['id']]);
$protected = $folder['id'] == 'INBOX' || ($protect_default && $special);
$noselect = false;
$classes = array();
$folder_utf8 = rcube_charset::convert($folder['id'], 'UTF7-IMAP');
$display_folder = rcube::Q($special ? $RCMAIL->localize_foldername($folder['id'], false, true) : $folder['name']);
if ($folder['virtual']) {
$classes[] = 'virtual';
}
// Check \Noselect flag (of existing folder)
if (!$protected && in_array($folder['id'], $a_unsubscribed)) {
$attrs = $STORAGE->folder_attributes($folder['id']);
$noselect = in_array_nocase('\\Noselect', $attrs);
}
$disabled = (($protected && $subscribed) || $noselect);
// Below we will disable subscription option for "virtual" folders
// according to namespaces, but only if they aren't already subscribed.
// User should be able to unsubscribe from the folder
// even if it doesn't exists or is not accessible (OTRS:1000059)
if (!$subscribed && !$disabled && !empty($namespace) && $folder['virtual']) {
// check if the folder is a namespace prefix, then disable subscription option on it
if (!$disabled && $folder['level'] == 0) {
$fname = $folder['id'] . $delimiter;
foreach ($namespace as $ns) {
if (is_array($ns)) {
foreach ($ns as $item) {
if ($item[0] === $fname) {
$disabled = true;
break 2;
}
}
}
}
}
// check if the folder is an other users virtual-root folder, then disable subscription option on it
if (!$disabled && $folder['level'] == 1 && !empty($namespace['other'])) {
$parts = explode($delimiter, $folder['id']);
$fname = $parts[0] . $delimiter;
foreach ($namespace['other'] as $item) {
if ($item[0] === $fname) {
$disabled = true;
break;
}
}
}
// check if the folder is shared, then disable subscription option on it (if not subscribed already)
if (!$disabled) {
$tmp_ns = array_merge((array)$namespace['other'], (array)$namespace['shared']);
foreach ($tmp_ns as $item) {
if (strlen($item[0]) && strpos($folder['id'], $item[0]) === 0) {
$disabled = true;
break;
}
}
}
}
$is_collapsed = strpos($collapsed, '&'.rawurlencode($folder['id']).'&') !== false;
$folder_id = rcube_utils::html_identifier($folder['id'], true);
if ($folder_class = $RCMAIL->folder_classname($folder['id'])) {
$classes[] = $folder_class;
}
$folders[$folder['id']] = array(
'idx' => $folder_id,
'folder_imap' => $folder['id'],
'folder' => $folder_utf8,
'display' => $display_folder,
'protected' => $protected || $folder['virtual'],
'class' => join(' ', $classes),
'subscribed' => $subscribed,
'level' => $folder['level'],
'collapsed' => $is_collapsed,
'content' => html::a(array('href' => '#'), $display_folder)
. $checkbox_subscribe->show(($subscribed ? $folder['id'] : ''),
array('value' => $folder['id'], 'disabled' => $disabled ? 'disabled' : ''))
);
}
$plugin = $RCMAIL->plugins->exec_hook('folders_list', array('list' => $folders));
// add drop-target representing 'root'
$root = array(
'idx' => rcube_utils::html_identifier('*', true),
'folder_imap' => '*',
'folder' => '',
'display' => '',
'protected' => true,
'class' => 'root',
'content' => '<span>&nbsp;</span>',
);
$folders = array();
$plugin['list'] = array_values($plugin['list']);
array_unshift($plugin['list'], $root);
for ($i = 0, $length = count($plugin['list']); $i<$length; $i++) {
$folders[] = rcmail_folder_tree_element($plugin['list'], $i, $js_folders);
}
$OUTPUT->add_gui_object('subscriptionlist', $attrib['id']);
$OUTPUT->set_env('subscriptionrows', $js_folders);
$OUTPUT->set_env('defaultfolders', array_keys($special_folders));
$OUTPUT->set_env('collapsed_folders', $collapsed);
$OUTPUT->set_env('delimiter', $delimiter);
return html::tag('ul', $attrib, implode('', $folders), html::$common_attrib);
}
function rcmail_folder_tree_element($folders, &$key, &$js_folders)
{
$data = $folders[$key];
$idx = 'rcmli' . $data['idx'];
$js_folders[$data['folder_imap']] = array($data['folder'], $data['display'], $data['protected']);
$content = $data['content'];
$attribs = array(
'id' => $idx,
'class' => trim($data['class'] . ' mailbox')
);
$children = array();
while ($folders[$key+1] && $folders[$key+1]['level'] > $data['level']) {
$key++;
$children[] = rcmail_folder_tree_element($folders, $key, $js_folders);
}
if (!empty($children)) {
$content .= html::div('treetoggle ' . ($data['collapsed'] ? 'collapsed' : 'expanded'), '&nbsp;')
. html::tag('ul', array('style' => ($data['collapsed'] ? "display:none" : null)),
implode("\n", $children));
}
return html::tag('li', $attribs, $content);
}
function rcmail_folder_filter($attrib)
{
global $RCMAIL;
$storage = $RCMAIL->get_storage();
$namespace = $storage->get_namespace();
if (empty($namespace['personal']) && empty($namespace['shared']) && empty($namespace['other'])) {
return '';
}
if (!$attrib['id']) {
$attrib['id'] = 'rcmfolderfilter';
}
if (!rcube_utils::get_boolean($attrib['noevent'])) {
$attrib['onchange'] = rcmail_output::JS_OBJECT_NAME . '.folder_filter(this.value)';
}
$roots = array();
$select = new html_select($attrib);
$select->add($RCMAIL->gettext('all'), '---');
foreach (array_keys($namespace) as $type) {
foreach ((array)$namespace[$type] as $ns) {
$root = rtrim($ns[0], $ns[1]);
$label = $RCMAIL->gettext('namespace.' . $type);
if (count($namespace[$type]) > 1) {
$label .= ' (' . rcube_charset::convert($root, 'UTF7-IMAP', RCUBE_CHARSET) . ')';
}
$select->add($label, $root);
if (strlen($root)) {
$roots[] = $root;
}
}
}
$RCMAIL->output->add_gui_object('foldersfilter', $attrib['id']);
$RCMAIL->output->set_env('ns_roots', $roots);
return $select->show();
}
function rcmail_rename_folder($oldname, $newname)
{
global $RCMAIL;
$storage = $RCMAIL->get_storage();
$delimiter = $storage->get_hierarchy_delimiter();
$plugin = $RCMAIL->plugins->exec_hook('folder_rename', array(
'oldname' => $oldname, 'newname' => $newname));
if (!$plugin['abort']) {
$renamed = $storage->rename_folder($oldname, $newname);
}
else {
$renamed = $plugin['result'];
}
// update per-folder options for modified folder and its subfolders
if ($renamed) {
$a_threaded = (array) $RCMAIL->config->get('message_threading', array());
$oldprefix = '/^' . preg_quote($oldname . $delimiter, '/') . '/';
foreach ($a_threaded as $key => $val) {
if ($key == $oldname) {
unset($a_threaded[$key]);
$a_threaded[$newname] = $val;
}
else if (preg_match($oldprefix, $key)) {
unset($a_threaded[$key]);
$a_threaded[preg_replace($oldprefix, $newname.$delimiter, $key)] = $val;
}
}
$RCMAIL->user->save_prefs(array('message_threading' => $a_threaded));
// #1488692: update session
if ($_SESSION['mbox'] === $oldname) {
$_SESSION['mbox'] = $newname;
}
return true;
}
return false;
}
diff --git a/program/steps/settings/func.inc b/program/steps/settings/func.inc
index 0e5199e32..144d60c51 100644
--- a/program/steps/settings/func.inc
+++ b/program/steps/settings/func.inc
@@ -1,1502 +1,1500 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/settings/func.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide functionality for user's settings & preferences |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
if (!$OUTPUT->ajax_call) {
$OUTPUT->set_pagetitle($RCMAIL->gettext('preferences'));
}
// register UI objects
$OUTPUT->add_handlers(array(
'settingstabs' => 'rcmail_settings_tabs',
'sectionslist' => 'rcmail_sections_list',
'identitieslist' => 'rcmail_identities_list',
));
// register action aliases
$RCMAIL->register_action_map(array(
'folders' => 'folders.inc',
'rename-folder' => 'folders.inc',
'delete-folder' => 'folders.inc',
'subscribe' => 'folders.inc',
'unsubscribe' => 'folders.inc',
'purge' => 'folders.inc',
'folder-size' => 'folders.inc',
'add-folder' => 'edit_folder.inc',
'add-identity' => 'edit_identity.inc',
'add-response' => 'edit_response.inc',
'save-response' => 'edit_response.inc',
'delete-response' => 'responses.inc',
'delete-identity' => 'identities.inc',
'upload-display' => 'upload.inc',
));
function rcmail_sections_list($attrib)
{
global $RCMAIL;
// add id to message list table if not specified
if (!strlen($attrib['id'])) {
$attrib['id'] = 'rcmsectionslist';
}
list($list, $cols) = rcmail_user_prefs();
// create XHTML table
$out = $RCMAIL->table_output($attrib, $list, $cols, 'id');
// set client env
$RCMAIL->output->add_gui_object('sectionslist', $attrib['id']);
$RCMAIL->output->include_script('list.js');
return $out;
}
function rcmail_identities_list($attrib)
{
global $OUTPUT, $RCMAIL;
// add id to message list table if not specified
if (!strlen($attrib['id'])) {
$attrib['id'] = 'rcmIdentitiesList';
}
// get identities list and define 'mail' column
$list = $RCMAIL->user->list_emails();
foreach ($list as $idx => $row) {
$list[$idx]['mail'] = trim($row['name'] . ' <' . rcube_utils::idn_to_utf8($row['email']) . '>');
}
// get all identites from DB and define list of cols to be displayed
$plugin = $RCMAIL->plugins->exec_hook('identities_list', array(
'list' => $list,
'cols' => array('mail')
));
// @TODO: use <UL> instead of <TABLE> for identities list
// create XHTML table
$out = $RCMAIL->table_output($attrib, $plugin['list'], $plugin['cols'], 'identity_id');
// set client env
$OUTPUT->add_gui_object('identitieslist', $attrib['id']);
return $out;
}
// similar function as in /steps/addressbook/edit.inc
function get_form_tags($attrib, $action, $id = null, $hidden = null)
{
global $EDIT_FORM, $RCMAIL;
$form_start = $form_end = '';
if (empty($EDIT_FORM)) {
$request_key = $action . (isset($id) ? '.'.$id : '');
$form_start = $RCMAIL->output->request_form(array(
'name' => 'form',
'method' => 'post',
'task' => $RCMAIL->task,
'action' => $action,
'request' => $request_key,
'noclose' => true
) + $attrib);
if (is_array($hidden)) {
$hiddenfields = new html_hiddenfield($hidden);
$form_start .= $hiddenfields->show();
}
$form_end = !strlen($attrib['form']) ? '</form>' : '';
$EDIT_FORM = !empty($attrib['form']) ? $attrib['form'] : 'form';
$RCMAIL->output->add_gui_object('editform', $EDIT_FORM);
}
return array($form_start, $form_end);
}
function rcmail_user_prefs($current = null)
{
global $RCMAIL;
$sections['general'] = array('id' => 'general', 'section' => $RCMAIL->gettext('uisettings'));
$sections['mailbox'] = array('id' => 'mailbox', 'section' => $RCMAIL->gettext('mailboxview'));
$sections['mailview'] = array('id' => 'mailview','section' => $RCMAIL->gettext('messagesdisplaying'));
$sections['compose'] = array('id' => 'compose', 'section' => $RCMAIL->gettext('messagescomposition'));
$sections['addressbook'] = array('id' => 'addressbook','section' => $RCMAIL->gettext('contacts'));
$sections['folders'] = array('id' => 'folders', 'section' => $RCMAIL->gettext('specialfolders'));
$sections['server'] = array('id' => 'server', 'section' => $RCMAIL->gettext('serversettings'));
// hook + define list cols
$plugin = $RCMAIL->plugins->exec_hook('preferences_sections_list',
array('list' => $sections, 'cols' => array('section')));
$sections = $plugin['list'];
$config = $RCMAIL->config->all();
$no_override = array_flip((array)$RCMAIL->config->get('dont_override'));
foreach ($sections as $idx => $sect) {
$sections[$idx]['class'] = $sect['class'] ?: $idx;
if ($current && $sect['id'] != $current) {
continue;
}
$blocks = array();
switch ($sect['id']) {
// general
case 'general':
$blocks = array(
'main' => array('name' => rcube::Q($RCMAIL->gettext('mainoptions'))),
'skin' => array('name' => rcube::Q($RCMAIL->gettext('skin'))),
'browser' => array('name' => rcube::Q($RCMAIL->gettext('browseroptions'))),
'advanced'=> array('name' => rcube::Q($RCMAIL->gettext('advancedoptions'))),
);
// language selection
if (!isset($no_override['language'])) {
if (!$current) {
continue 2;
}
$a_lang = $RCMAIL->list_languages();
asort($a_lang);
$field_id = 'rcmfd_lang';
$select = new html_select(array('name' => '_language', 'id' => $field_id));
$select->add(array_values($a_lang), array_keys($a_lang));
$blocks['main']['options']['language'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('language'))),
'content' => $select->show($RCMAIL->user->language),
);
}
// timezone selection
if (!isset($no_override['timezone'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_timezone';
$select = new html_select(array('name' => '_timezone', 'id' => $field_id));
$select->add($RCMAIL->gettext('autodetect'), 'auto');
$zones = array();
foreach (DateTimeZone::listIdentifiers() as $i => $tzs) {
if ($data = rcmail_timezone_standard_time_data($tzs)) {
$zones[$data['key']] = array($tzs, $data['offset']);
}
}
ksort($zones);
foreach ($zones as $zone) {
list($tzs, $offset) = $zone;
$select->add('(GMT ' . $offset . ') ' . rcmail_timezone_label($tzs), $tzs);
}
$blocks['main']['options']['timezone'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('timezone'))),
'content' => $select->show((string)$config['timezone']),
);
}
// date/time formatting
if (!isset($no_override['time_format'])) {
if (!$current) {
continue 2;
}
$reftime = mktime(7,30,0);
$defaults = array('G:i', 'H:i', 'g:i a', 'h:i A');
$formats = (array)$RCMAIL->config->get('time_formats', $defaults);
$field_id = 'rcmfd_time_format';
$select = new html_select(array('name' => '_time_format', 'id' => $field_id));
foreach ($formats as $choice) {
$select->add(date($choice, $reftime), $choice);
}
$blocks['main']['options']['time_format'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('timeformat'))),
'content' => $select->show($RCMAIL->config->get('time_format')),
);
}
if (!isset($no_override['date_format'])) {
if (!$current) {
continue 2;
}
$refdate = mktime(12,30,0,7,24);
$defaults = array('Y-m-d','d-m-Y','Y/m/d','m/d/Y','d/m/Y','d.m.Y','j.n.Y');
$formats = (array)$RCMAIL->config->get('date_formats', $defaults);
$field_id = 'rcmfd_date_format';
$select = new html_select(array('name' => '_date_format', 'id' => $field_id));
foreach ($formats as $choice) {
$select->add(date($choice, $refdate), $choice);
}
$blocks['main']['options']['date_format'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('dateformat'))),
'content' => $select->show($config['date_format']),
);
}
// Show checkbox for toggling 'pretty dates'
if (!isset($no_override['prettydate'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_prettydate';
$input = new html_checkbox(array('name' => '_pretty_date', 'id' => $field_id, 'value' => 1));
$blocks['main']['options']['prettydate'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('prettydate'))),
'content' => $input->show($config['prettydate']?1:0),
);
}
if (!isset($no_override['refresh_interval'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_refresh_interval';
$select = new html_select(array('name' => '_refresh_interval', 'id' => $field_id));
$select->add($RCMAIL->gettext('never'), 0);
foreach (array(1, 3, 5, 10, 15, 30, 60) as $min) {
if (!$config['min_refresh_interval'] || $config['min_refresh_interval'] <= $min * 60) {
$label = $RCMAIL->gettext(array('name' => 'everynminutes', 'vars' => array('n' => $min)));
$select->add($label, $min);
}
}
$blocks['main']['options']['refresh_interval'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('refreshinterval'))),
'content' => $select->show($config['refresh_interval']/60),
);
}
// show drop-down for available skins
if (!isset($no_override['skin'])) {
if (!$current) {
continue 2;
}
$skins = rcmail_get_skins();
if (count($skins) > 1) {
$field_id = 'rcmfd_skin';
$input = new html_radiobutton(array('name'=>'_skin'));
foreach ($skins as $skin) {
$skinname = ucfirst($skin);
$author_link = $license_link = '';
$meta = @json_decode(@file_get_contents(INSTALL_PATH . "skins/$skin/meta.json"), true);
if (is_array($meta) && $meta['name']) {
$skinname = $meta['name'];
$author_link = $meta['url'] ? html::a(array('href' => $meta['url'], 'target' => '_blank'), rcube::Q($meta['author'])) : rcube::Q($meta['author']);
$license_link = $meta['license-url'] ? html::a(array('href' => $meta['license-url'], 'target' => '_blank', 'tabindex' => '-1'), rcube::Q($meta['license'])) : rcube::Q($meta['license']);
}
$img = html::img(array(
'src' => $RCMAIL->output->asset_url("skins/$skin/thumbnail.png"),
'class' => 'skinthumbnail',
'alt' => $skin,
'width' => 64,
'height' => 64,
'onerror' => "this.src = rcmail.assets_path('program/resources/blank.gif'); this.onerror = null",
));
$skinnames[] = mb_strtolower($skinname);
$blocks['skin']['options'][$skin]['content'] = html::label(array('class' => 'skinselection'),
html::span('skinitem', $input->show($config['skin'], array('value' => $skin, 'id' => $field_id.$skin))) .
html::span('skinitem', $img) .
html::span('skinitem', html::span('skinname', rcube::Q($skinname)) . html::br() .
html::span('skinauthor', $author_link ? 'by ' . $author_link : '') . html::br() .
html::span('skinlicense', $license_link ? $RCMAIL->gettext('license').':&nbsp;' . $license_link : ''))
);
}
array_multisort($blocks['skin']['options'], SORT_ASC, SORT_STRING, $skinnames);
}
}
// standard_windows option decides if new windows should be
// opened as popups or standard windows (which can be handled by browsers as tabs)
if (!isset($no_override['standard_windows'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_standard_windows';
$checkbox = new html_checkbox(array('name' => '_standard_windows', 'id' => $field_id, 'value' => 1));
$blocks['browser']['options']['standard_windows'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('standardwindows'))),
'content' => $checkbox->show($config['standard_windows']?1:0),
);
}
if ($current) {
$product_name = $RCMAIL->config->get('product_name', 'Roundcube Webmail');
$RCMAIL->output->add_script(sprintf("%s.check_protocol_handler('%s', '#mailtoprotohandler');",
rcmail_output::JS_OBJECT_NAME, rcube::JQ($product_name)), 'docready');
}
$blocks['browser']['options']['mailtoprotohandler'] = array(
'content' => html::a(array(
'href' => '#',
'id' => 'mailtoprotohandler'
),
rcube::Q($RCMAIL->gettext('mailtoprotohandler'))) .
html::span('mailtoprotohandler-status', ''),
);
break;
// Mailbox view (mail screen)
case 'mailbox':
$blocks = array(
'main' => array('name' => rcube::Q($RCMAIL->gettext('mainoptions'))),
'new_message' => array('name' => rcube::Q($RCMAIL->gettext('newmessage'))),
'advanced' => array('name' => rcube::Q($RCMAIL->gettext('advancedoptions'))),
);
if (!isset($no_override['layout'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_layout';
$select = new html_select(array('name' => '_layout', 'id' => $field_id));
$select->add($RCMAIL->gettext('layoutwidescreendesc'), 'widescreen');
$select->add($RCMAIL->gettext('layoutdesktopdesc'), 'desktop');
$select->add($RCMAIL->gettext('layoutlistdesc'), 'list');
$blocks['main']['options']['layout'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('layout'))),
'content' => $select->show($config['layout'] ?: 'widescreen'),
);
}
// show config parameter for auto marking the previewed message as read
if (!isset($no_override['mail_read_time'])) {
if (!$current) {
continue 2;
}
// apply default if config option is not set at all
$config['mail_read_time'] = intval($RCMAIL->config->get('mail_read_time'));
$field_id = 'rcmfd_mail_read_time';
$select = new html_select(array('name' => '_mail_read_time', 'id' => $field_id));
$select->add($RCMAIL->gettext('never'), -1);
$select->add($RCMAIL->gettext('immediately'), 0);
foreach (array(5, 10, 20, 30) as $sec) {
$label = $RCMAIL->gettext(array('name' => 'afternseconds', 'vars' => array('n' => $sec)));
$select->add($label, $sec);
}
$blocks['main']['options']['mail_read_time'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('automarkread'))),
'content' => $select->show($config['mail_read_time']),
);
}
if (!isset($no_override['mdn_requests'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_mdn_requests';
$select = new html_select(array('name' => '_mdn_requests', 'id' => $field_id));
$select->add($RCMAIL->gettext('askuser'), 0);
$select->add($RCMAIL->gettext('autosend'), 1);
$select->add($RCMAIL->gettext('autosendknown'), 3);
$select->add($RCMAIL->gettext('autosendknownignore'), 4);
$select->add($RCMAIL->gettext('ignorerequest'), 2);
$blocks['main']['options']['mdn_requests'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('mdnrequests'))),
'content' => $select->show($config['mdn_requests']),
);
}
if (!isset($no_override['autoexpand_threads'])) {
if (!$current) {
continue 2;
}
$storage = $RCMAIL->get_storage();
$supported = $storage->get_capability('THREAD');
if ($supported) {
$field_id = 'rcmfd_autoexpand_threads';
$select = new html_select(array('name' => '_autoexpand_threads', 'id' => $field_id));
$select->add($RCMAIL->gettext('never'), 0);
$select->add($RCMAIL->gettext('do_expand'), 1);
$select->add($RCMAIL->gettext('expand_only_unread'), 2);
$blocks['main']['options']['autoexpand_threads'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('autoexpand_threads'))),
'content' => $select->show($config['autoexpand_threads']),
);
}
}
// show page size selection
if (!isset($no_override['mail_pagesize'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_mail_pagesize';
$input = new html_inputfield(array('name' => '_mail_pagesize', 'id' => $field_id, 'size' => 5));
$size = intval($config['mail_pagesize'] ?: $config['pagesize']);
$blocks['main']['options']['pagesize'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('pagesize'))),
'content' => $input->show($size ?: 50),
);
}
if (!isset($no_override['check_all_folders'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_check_all_folders';
$input = new html_checkbox(array('name' => '_check_all_folders', 'id' => $field_id, 'value' => 1));
$blocks['new_message']['options']['check_all_folders'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('checkallfolders'))),
'content' => $input->show($config['check_all_folders']?1:0),
);
}
break;
// Message viewing
case 'mailview':
$blocks = array(
'main' => array('name' => rcube::Q($RCMAIL->gettext('mainoptions'))),
'advanced' => array('name' => rcube::Q($RCMAIL->gettext('advancedoptions'))),
);
// show checkbox to open message view in new window
if (!isset($no_override['message_extwin'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_message_extwin';
$input = new html_checkbox(array('name' => '_message_extwin', 'id' => $field_id, 'value' => 1));
$blocks['main']['options']['message_extwin'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('showinextwin'))),
'content' => $input->show($config['message_extwin']?1:0),
);
}
// show checkbox to show email instead of name
if (!isset($no_override['message_show_email'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_message_show_email';
$input = new html_checkbox(array('name' => '_message_show_email', 'id' => $field_id, 'value' => 1));
$blocks['main']['options']['message_show_email'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('showemail'))),
'content' => $input->show($config['message_show_email']?1:0),
);
}
// show checkbox for HTML/plaintext messages
if (!isset($no_override['prefer_html'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_htmlmsg';
$input = new html_checkbox(array('name' => '_prefer_html', 'id' => $field_id, 'value' => 1,
'onchange' => "$('#rcmfd_show_images').prop('disabled', !this.checked).val(0)"));
$blocks['main']['options']['prefer_html'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('preferhtml'))),
'content' => $input->show($config['prefer_html']?1:0),
);
}
if (!isset($no_override['default_charset'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_default_charset';
$blocks['advanced']['options']['default_charset'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('defaultcharset'))),
'content' => $RCMAIL->output->charset_selector(array(
'id' => $field_id, 'name' => '_default_charset', 'selected' => $config['default_charset']
)));
}
if (!isset($no_override['show_images'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_show_images';
$input = new html_select(array('name' => '_show_images', 'id' => $field_id,
'disabled' => !$config['prefer_html']));
$input->add($RCMAIL->gettext('never'), 0);
$input->add($RCMAIL->gettext('fromknownsenders'), 1);
$input->add($RCMAIL->gettext('always'), 2);
$blocks['main']['options']['show_images'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('allowremoteresources'))),
'content' => $input->show($config['prefer_html'] ? $config['show_images'] : 0),
);
}
if (!isset($no_override['inline_images'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_inline_images';
$input = new html_checkbox(array('name' => '_inline_images', 'id' => $field_id, 'value' => 1));
$blocks['main']['options']['inline_images'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('showinlineimages'))),
'content' => $input->show($config['inline_images']?1:0),
);
}
// "display after delete" checkbox
if (!isset($no_override['display_next'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_displaynext';
$input = new html_checkbox(array('name' => '_display_next', 'id' => $field_id, 'value' => 1));
$blocks['main']['options']['display_next'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('displaynext'))),
'content' => $input->show($config['display_next']?1:0),
);
}
break;
// Mail composition
case 'compose':
$blocks = array(
'main' => array('name' => rcube::Q($RCMAIL->gettext('mainoptions'))),
'sig' => array('name' => rcube::Q($RCMAIL->gettext('signatureoptions'))),
'spellcheck' => array('name' => rcube::Q($RCMAIL->gettext('spellcheckoptions'))),
'advanced' => array('name' => rcube::Q($RCMAIL->gettext('advancedoptions'))),
);
// show checkbox to compose messages in a new window
if (!isset($no_override['compose_extwin'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfdcompose_extwin';
$input = new html_checkbox(array('name' => '_compose_extwin', 'id' => $field_id, 'value' => 1));
$blocks['main']['options']['compose_extwin'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('composeextwin'))),
'content' => $input->show($config['compose_extwin']?1:0),
);
}
if (!isset($no_override['htmleditor'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_htmleditor';
$select = new html_select(array('name' => '_htmleditor', 'id' => $field_id));
$select->add($RCMAIL->gettext('never'), 0);
$select->add($RCMAIL->gettext('htmlonreply'), 2);
$select->add($RCMAIL->gettext('htmlonreplyandforward'), 3);
$select->add($RCMAIL->gettext('always'), 1);
$select->add($RCMAIL->gettext('alwaysbutplain'), 4);
$blocks['main']['options']['htmleditor'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('htmleditor'))),
'content' => $select->show(intval($config['htmleditor'])),
);
}
if (!isset($no_override['draft_autosave'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_autosave';
$select = new html_select(array('name' => '_draft_autosave', 'id' => $field_id, 'disabled' => empty($config['drafts_mbox'])));
$select->add($RCMAIL->gettext('never'), 0);
foreach (array(1, 3, 5, 10) as $i => $min) {
$label = $RCMAIL->gettext(array('name' => 'everynminutes', 'vars' => array('n' => $min)));
$select->add($label, $min*60);
}
$blocks['main']['options']['draft_autosave'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('autosavedraft'))),
'content' => $select->show($config['draft_autosave']),
);
}
if (!isset($no_override['mime_param_folding'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_param_folding';
$select = new html_select(array('name' => '_mime_param_folding', 'id' => $field_id));
$select->add($RCMAIL->gettext('2231folding'), 0);
$select->add($RCMAIL->gettext('miscfolding'), 1);
$select->add($RCMAIL->gettext('2047folding'), 2);
$blocks['advanced']['options']['mime_param_folding'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('mimeparamfolding'))),
'content' => $select->show($config['mime_param_folding']),
);
}
if (!isset($no_override['force_7bit'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_force_7bit';
$input = new html_checkbox(array('name' => '_force_7bit', 'id' => $field_id, 'value' => 1));
$blocks['advanced']['options']['force_7bit'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('force7bit'))),
'content' => $input->show($config['force_7bit']?1:0),
);
}
if (!isset($no_override['mdn_default'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_mdn_default';
$input = new html_checkbox(array('name' => '_mdn_default', 'id' => $field_id, 'value' => 1));
$blocks['main']['options']['mdn_default'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('reqmdn'))),
'content' => $input->show($config['mdn_default']?1:0),
);
}
if (!isset($no_override['dsn_default'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_dsn_default';
$input = new html_checkbox(array('name' => '_dsn_default', 'id' => $field_id, 'value' => 1));
$blocks['main']['options']['dsn_default'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('reqdsn'))),
'content' => $input->show($config['dsn_default']?1:0),
);
}
if (!isset($no_override['reply_same_folder'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_reply_same_folder';
$input = new html_checkbox(array('name' => '_reply_same_folder', 'id' => $field_id, 'value' => 1));
$blocks['main']['options']['reply_same_folder'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('replysamefolder'))),
'content' => $input->show($config['reply_same_folder']?1:0),
);
}
if (!isset($no_override['reply_mode'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_reply_mode';
$select = new html_select(array('name' => '_reply_mode', 'id' => $field_id));
$select->add($RCMAIL->gettext('replyempty'), -1);
$select->add($RCMAIL->gettext('replybottomposting'), 0);
$select->add($RCMAIL->gettext('replytopposting'), 1);
$select->add($RCMAIL->gettext('replytoppostingnoindent'), 2);
$blocks['main']['options']['reply_mode'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('whenreplying'))),
'content' => $select->show(intval($config['reply_mode'])),
);
}
if (!isset($no_override['spellcheck_before_send']) && $config['enable_spellcheck']) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_spellcheck_before_send';
$input = new html_checkbox(array('name' => '_spellcheck_before_send', 'id' => $field_id, 'value' => 1));
$blocks['spellcheck']['options']['spellcheck_before_send'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('spellcheckbeforesend'))),
'content' => $input->show($config['spellcheck_before_send']?1:0),
);
}
if ($config['enable_spellcheck']) {
if (!$current) {
continue 2;
}
foreach (array('syms', 'nums', 'caps') as $key) {
$key = 'spellcheck_ignore_'.$key;
if (!isset($no_override[$key])) {
$input = new html_checkbox(array('name' => '_'.$key, 'id' => 'rcmfd_'.$key, 'value' => 1));
$blocks['spellcheck']['options'][$key] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext(str_replace('_', '', $key)))),
'content' => $input->show($config[$key]?1:0),
);
}
}
}
if (!isset($no_override['show_sig'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_show_sig';
$select = new html_select(array('name' => '_show_sig', 'id' => $field_id));
$select->add($RCMAIL->gettext('never'), 0);
$select->add($RCMAIL->gettext('always'), 1);
$select->add($RCMAIL->gettext('newmessageonly'), 2);
$select->add($RCMAIL->gettext('replyandforwardonly'), 3);
$blocks['sig']['options']['show_sig'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('autoaddsignature'))),
'content' => $select->show($RCMAIL->config->get('show_sig', 1)),
);
}
if (!isset($no_override['sig_below'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_sig_below';
$input = new html_checkbox(array('name' => '_sig_below', 'id' => $field_id, 'value' => 1));
$blocks['sig']['options']['sig_below'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('sigbelow'))),
'content' => $input->show($RCMAIL->config->get('sig_below') ? 1 : 0),
);
}
if (!isset($no_override['strip_existing_sig'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_strip_existing_sig';
$input = new html_checkbox(array('name' => '_strip_existing_sig', 'id' => $field_id, 'value' => 1));
$blocks['sig']['options']['strip_existing_sig'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('replyremovesignature'))),
'content' => $input->show($config['strip_existing_sig']?1:0),
);
}
if (!isset($no_override['sig_separator'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_sig_separator';
$input = new html_checkbox(array('name' => '_sig_separator', 'id' => $field_id, 'value' => 1));
$blocks['sig']['options']['sig_separator'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('sigseparator'))),
'content' => $input->show($RCMAIL->config->get('sig_separator') ? 1 : 0),
);
}
if (!isset($no_override['forward_attachment'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_forward_attachment';
$select = new html_select(array('name' => '_forward_attachment', 'id' => $field_id));
$select->add($RCMAIL->gettext('inline'), 0);
$select->add($RCMAIL->gettext('asattachment'), 1);
$blocks['main']['options']['forward_attachment'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('forwardmode'))),
'content' => $select->show(intval($config['forward_attachment'])),
);
}
if (!isset($no_override['default_font']) || !isset($no_override['default_font_size'])) {
if (!$current) {
continue 2;
}
// Default font size
$field_id = 'rcmfd_default_font_size';
$select_default_font_size = new html_select(array('name' => '_default_font_size', 'id' => $field_id));
$fontsizes = array('', '8pt', '9pt', '10pt', '11pt', '12pt', '14pt', '18pt', '24pt', '36pt');
foreach ($fontsizes as $size) {
$select_default_font_size->add($size, $size);
}
// Default font
$field_id = 'rcmfd_default_font';
$select_default_font = new html_select(array('name' => '_default_font', 'id' => $field_id));
$select_default_font->add('', '');
$fonts = rcmail::font_defs();
foreach (array_keys($fonts) as $fname) {
$select_default_font->add($fname, $fname);
}
$blocks['main']['options']['default_font'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('defaultfont'))),
'content' => html::div('input-group', $select_default_font->show($RCMAIL->config->get('default_font', 1)) .
$select_default_font_size->show($RCMAIL->config->get('default_font_size', 1)))
);
}
if (!isset($no_override['reply_all_mode'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_reply_all_mode';
$select = new html_select(array('name' => '_reply_all_mode', 'id' => $field_id));
$select->add($RCMAIL->gettext('replyalldefault'), 0);
$select->add($RCMAIL->gettext('replyalllist'), 1);
$blocks['main']['options']['reply_all_mode'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('replyallmode'))),
'content' => $select->show(intval($config['reply_all_mode'])),
);
}
if (!isset($no_override['compose_save_localstorage'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_compose_save_localstorage';
$input = new html_checkbox(array('name' => '_compose_save_localstorage', 'id' => $field_id, 'value' => 1));
$blocks['advanced']['options']['compose_save_localstorage'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('savelocalstorage'))),
'content' => $input->show($config['compose_save_localstorage']?1:0),
);
}
break;
// Addressbook config
case 'addressbook':
$blocks = array(
'main' => array('name' => rcube::Q($RCMAIL->gettext('mainoptions'))),
'advanced' => array('name' => rcube::Q($RCMAIL->gettext('advancedoptions'))),
);
if (!isset($no_override['default_addressbook'])
&& (!$current || ($books = $RCMAIL->get_address_sources(true, true)))
) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_default_addressbook';
$select = new html_select(array('name' => '_default_addressbook', 'id' => $field_id));
foreach ($books as $book) {
$select->add(html_entity_decode($book['name'], ENT_COMPAT, 'UTF-8'), $book['id']);
}
$blocks['main']['options']['default_addressbook'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('defaultabook'))),
'content' => $select->show($config['default_addressbook']),
);
}
// show addressbook listing mode selection
if (!isset($no_override['addressbook_name_listing'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_addressbook_name_listing';
$select = new html_select(array('name' => '_addressbook_name_listing', 'id' => $field_id));
$select->add($RCMAIL->gettext('name'), 0);
$select->add($RCMAIL->gettext('firstname') . ' ' . $RCMAIL->gettext('surname'), 1);
$select->add($RCMAIL->gettext('surname') . ' ' . $RCMAIL->gettext('firstname'), 2);
$select->add($RCMAIL->gettext('surname') . ', ' . $RCMAIL->gettext('firstname'), 3);
$blocks['main']['options']['list_name_listing'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('listnamedisplay'))),
'content' => $select->show($config['addressbook_name_listing']),
);
}
// show addressbook sort column
if (!isset($no_override['addressbook_sort_col'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_addressbook_sort_col';
$select = new html_select(array('name' => '_addressbook_sort_col', 'id' => $field_id));
$select->add($RCMAIL->gettext('name'), 'name');
$select->add($RCMAIL->gettext('firstname'), 'firstname');
$select->add($RCMAIL->gettext('surname'), 'surname');
$blocks['main']['options']['sort_col'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('listsorting'))),
'content' => $select->show($config['addressbook_sort_col']),
);
}
// show addressbook page size selection
if (!isset($no_override['addressbook_pagesize'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_addressbook_pagesize';
$input = new html_inputfield(array('name' => '_addressbook_pagesize', 'id' => $field_id, 'size' => 5));
$size = intval($config['addressbook_pagesize'] ?: $config['pagesize']);
$blocks['main']['options']['pagesize'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('pagesize'))),
'content' => $input->show($size ?: 50),
);
}
if (!isset($no_override['autocomplete_single'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_autocomplete_single';
$checkbox = new html_checkbox(array('name' => '_autocomplete_single', 'id' => $field_id, 'value' => 1));
$blocks['main']['options']['autocomplete_single'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('autocompletesingle'))),
'content' => $checkbox->show($config['autocomplete_single']?1:0),
);
}
break;
// Special IMAP folders
case 'folders':
$blocks = array(
'main' => array('name' => rcube::Q($RCMAIL->gettext('mainoptions'))),
'advanced' => array('name' => rcube::Q($RCMAIL->gettext('advancedoptions'))),
);
if (!isset($no_override['show_real_foldernames'])) {
if (!$current) {
continue 2;
}
$field_id = 'show_real_foldernames';
$input = new html_checkbox(array('name' => '_show_real_foldernames', 'id' => $field_id, 'value' => 1));
$blocks['main']['options']['show_real_foldernames'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('show_real_foldernames'))),
'content' => $input->show($config['show_real_foldernames']?1:0),
);
}
// Configure special folders
$set = array('drafts_mbox', 'sent_mbox', 'junk_mbox', 'trash_mbox');
if ($current && count(array_intersect($no_override, $set)) < 4) {
$select = $RCMAIL->folder_selector(array(
'noselection' => '---',
'realnames' => true,
'maxlength' => 30,
'folder_filter' => 'mail',
'folder_rights' => 'w',
));
// #1486114, #1488279, #1489219
$onchange = "if ($(this).val() == 'INBOX') $(this).val('')";
}
if (!isset($no_override['drafts_mbox'])) {
if (!$current) {
continue 2;
}
$attrs = array('id' => '_drafts_mbox', 'name' => '_drafts_mbox', 'onchange' => $onchange);
$blocks['main']['options']['drafts_mbox'] = array(
'title' => html::label($attrs['id'], rcube::Q($RCMAIL->gettext('drafts'))),
'content' => $select->show($config['drafts_mbox'], $attrs),
);
}
if (!isset($no_override['sent_mbox'])) {
if (!$current) {
continue 2;
}
$attrs = array('id' => '_sent_mbox', 'name' => '_sent_mbox', 'onchange' => '');
$blocks['main']['options']['sent_mbox'] = array(
'title' => html::label($attrs['id'], rcube::Q($RCMAIL->gettext('sent'))),
'content' => $select->show($config['sent_mbox'], $attrs),
);
}
if (!isset($no_override['junk_mbox'])) {
if (!$current) {
continue 2;
}
$attrs = array('id' => '_junk_mbox', 'name' => '_junk_mbox', 'onchange' => $onchange);
$blocks['main']['options']['junk_mbox'] = array(
'title' => html::label($attrs['id'], rcube::Q($RCMAIL->gettext('junk'))),
'content' => $select->show($config['junk_mbox'], $attrs),
);
}
if (!isset($no_override['trash_mbox'])) {
if (!$current) {
continue 2;
}
$attrs = array('id' => '_trash_mbox', 'name' => '_trash_mbox', 'onchange' => $onchange);
$blocks['main']['options']['trash_mbox'] = array(
'title' => html::label($attrs['id'], rcube::Q($RCMAIL->gettext('trash'))),
'content' => $select->show($config['trash_mbox'], $attrs),
);
}
break;
// Server settings
case 'server':
$blocks = array(
'main' => array('name' => rcube::Q($RCMAIL->gettext('mainoptions'))),
'maintenance' => array('name' => rcube::Q($RCMAIL->gettext('maintenance'))),
'advanced' => array('name' => rcube::Q($RCMAIL->gettext('advancedoptions'))),
);
if (!isset($no_override['read_when_deleted'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_read_deleted';
$input = new html_checkbox(array('name' => '_read_when_deleted', 'id' => $field_id, 'value' => 1));
$blocks['main']['options']['read_when_deleted'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('readwhendeleted'))),
'content' => $input->show($config['read_when_deleted']?1:0),
);
}
if (!isset($no_override['flag_for_deletion'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_flag_for_deletion';
$input = new html_checkbox(array('name' => '_flag_for_deletion', 'id' => $field_id, 'value' => 1));
$blocks['main']['options']['flag_for_deletion'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('flagfordeletion'))),
'content' => $input->show($config['flag_for_deletion']?1:0),
);
}
// don't show deleted messages
if (!isset($no_override['skip_deleted'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_skip_deleted';
$input = new html_checkbox(array('name' => '_skip_deleted', 'id' => $field_id, 'value' => 1));
$blocks['main']['options']['skip_deleted'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('skipdeleted'))),
'content' => $input->show($config['skip_deleted']?1:0),
);
}
if (!isset($no_override['delete_always'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_delete_always';
$input = new html_checkbox(array('name' => '_delete_always', 'id' => $field_id, 'value' => 1));
$blocks['main']['options']['delete_always'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('deletealways'))),
'content' => $input->show($config['delete_always']?1:0),
);
}
if (!isset($no_override['delete_junk'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_delete_junk';
$input = new html_checkbox(array('name' => '_delete_junk', 'id' => $field_id, 'value' => 1));
$blocks['main']['options']['delete_junk'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('deletejunk'))),
'content' => $input->show($config['delete_junk']?1:0),
);
}
// Trash purging on logout
if (!isset($no_override['logout_purge'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_logout_purge';
$input = new html_checkbox(array('name' => '_logout_purge', 'id' => $field_id, 'value' => 1));
$blocks['maintenance']['options']['logout_purge'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('logoutclear'))),
'content' => $input->show($config['logout_purge']?1:0),
);
}
// INBOX compacting on logout
if (!isset($no_override['logout_expunge'])) {
if (!$current) {
continue 2;
}
$field_id = 'rcmfd_logout_expunge';
$input = new html_checkbox(array('name' => '_logout_expunge', 'id' => $field_id, 'value' => 1));
$blocks['maintenance']['options']['logout_expunge'] = array(
'title' => html::label($field_id, rcube::Q($RCMAIL->gettext('logoutcompact'))),
'content' => $input->show($config['logout_expunge']?1:0),
);
}
}
$found = false;
$data = $RCMAIL->plugins->exec_hook('preferences_list',
array('section' => $sect['id'], 'blocks' => $blocks, 'current' => $current));
$advanced_prefs = (array) $RCMAIL->config->get('advanced_prefs');
// create output
foreach ($data['blocks'] as $key => $block) {
if (!empty($block['content']) || !empty($block['options'])) {
$found = true;
}
// move some options to the 'advanced' block as configured by admin
if ($key != 'advanced') {
foreach ($advanced_prefs as $opt) {
if ($block['options'][$opt]) {
$data['blocks']['advanced']['options'][$opt] = $block['options'][$opt];
unset($data['blocks'][$key]['options'][$opt]);
}
}
}
}
// move 'advanced' block to the end of the list
if (!empty($data['blocks']['advanced'])) {
$adv = $data['blocks']['advanced'];
unset($data['blocks']['advanced']);
$data['blocks']['advanced'] = $adv;
}
if (!$found)
unset($sections[$idx]);
else
$sections[$idx]['blocks'] = $data['blocks'];
// allow plugins to add a header to each section
$data = $RCMAIL->plugins->exec_hook('preferences_section_header',
array('section' => $sect['id'], 'header' => '', 'current' => $current));
if (!empty($data['header'])) {
$sections[$idx]['header'] = $data['header'];
}
}
return array($sections, $plugin['cols']);
}
function rcmail_get_skins()
{
global $RCMAIL;
$path = RCUBE_INSTALL_PATH . 'skins';
$skins = array();
$dir = opendir($path);
$limit = (array) $RCMAIL->config->get('skins_allowed');
if (!$dir) {
return false;
}
while (($file = readdir($dir)) !== false) {
$filename = $path . '/' . $file;
if ($file[0] != '.' && (empty($limit) || in_array($file, $limit) && is_dir($filename) && is_readable($filename))) {
$skins[] = $file;
}
}
closedir($dir);
return $skins;
}
function rcmail_folder_options($mailbox)
{
global $RCMAIL;
$options = $RCMAIL->get_storage()->folder_info($mailbox);
$options['protected'] = $options['is_root']
|| strtoupper($mailbox) === 'INBOX'
|| ($options['special'] && $RCMAIL->config->get('protect_default_folders'));
return $options;
}
/**
* Updates (or creates) folder row in the subscriptions table
*
* @param string $name Folder name
* @param string $oldname Old folder name (for update)
* @param bool $subscribe Checks subscription checkbox
* @param string $class CSS class name for folder row
*/
function rcmail_update_folder_row($name, $oldname=null, $subscribe=false, $class_name=null)
{
global $RCMAIL;
$storage = $RCMAIL->get_storage();
$delimiter = $storage->get_hierarchy_delimiter();
$options = rcmail_folder_options($name);
$name_utf8 = rcube_charset::convert($name, 'UTF7-IMAP');
$foldersplit = explode($delimiter, $storage->mod_folder($name));
$level = count($foldersplit) - 1;
$display_name = $options['protected'] ? $RCMAIL->localize_foldername($name) : rcube_charset::convert($foldersplit[$level], 'UTF7-IMAP');
$class_name = trim($class_name . ' mailbox');
if ($oldname === null) {
$RCMAIL->output->command('add_folder_row', $name, $name_utf8, $display_name,
$options['protected'] || $options['noselect'], $subscribe, $class_name);
}
else {
$RCMAIL->output->command('replace_folder_row', $oldname, $name, $name_utf8, $display_name,
$options['protected'] || $options['noselect'], $class_name);
}
}
/**
* Render the list of settings sections (AKA tabs)
*/
function rcmail_settings_tabs($attrib)
{
global $RCMAIL, $OUTPUT;
// add default attributes
$attrib += array('tagname' => 'span', 'idprefix' => 'settingstab', 'selclass' => 'selected');
$default_actions = array(
array('action' => 'preferences', 'type' => 'link', 'label' => 'preferences', 'title' => 'editpreferences'),
array('action' => 'folders', 'type' => 'link', 'label' => 'folders', 'title' => 'managefolders'),
array('action' => 'identities', 'type' => 'link', 'label' => 'identities', 'title' => 'manageidentities'),
array('action' => 'responses', 'type' => 'link', 'label' => 'responses', 'title' => 'manageresponses'),
);
$disabled_actions = (array) $RCMAIL->config->get('disabled_actions');
// get all identites from DB and define list of cols to be displayed
$plugin = $RCMAIL->plugins->exec_hook('settings_actions', array(
'actions' => $default_actions,
'attrib' => $attrib,
));
$selected = $RCMAIL->action ?: 'preferences';
$attrib = $plugin['attrib'];
$tagname = $attrib['tagname'];
$tabs = array();
foreach ($plugin['actions'] as $action) {
if (!$action['command'] && $action['action']) {
$action['prop'] = $action['action'];
$action['command'] = 'show';
}
else if ($action['command'] != 'show') {
// Backwards compatibility, show command added in 1.4
$action['prop'] = $action['command'];
$action['command'] = 'show';
}
$cmd = $action['prop'] ?: $action['action'];
$id = $action['id'] ?: $cmd;
if (in_array('settings.' . $cmd, $disabled_actions)) {
continue;
}
if (!$action['href']) {
$action['href'] = $RCMAIL->url(array('_action' => $cmd));
}
$button = $OUTPUT->button($action + array('type' => 'link'));
$attr = $attrib;
if (!empty($id)) {
$attr['id'] = preg_replace('/[^a-z0-9]/i', '', $attrib['idprefix'] . $id);
}
$classnames = array($attrib['class']);
if (!empty($action['class'])) {
$classnames[] = $action['class'];
}
else if (!empty($cmd)) {
$classnames[] = $cmd;
}
if ($cmd == $selected) {
$classnames[] = $attrib['selclass'];
}
$attr['class'] = join(' ', $classnames);
$tabs[] = html::tag($tagname, $attr, $button, html::$common_attrib);
}
return join('', $tabs);
}
/**
* Localize timezone identifiers
*/
function rcmail_timezone_label($tz)
{
static $labels;
if ($labels === null) {
$labels = array();
$lang = $_SESSION['language'];
if ($lang && $lang != 'en_US') {
if (file_exists(RCUBE_LOCALIZATION_DIR . "$lang/timezones.inc")) {
include RCUBE_LOCALIZATION_DIR . "$lang/timezones.inc";
}
}
}
if (empty($labels)) {
return str_replace('_', ' ', $tz);
}
$tokens = explode('/', $tz);
$key = 'tz';
foreach ($tokens as $i => $token) {
$idx = strtolower($token);
$token = str_replace('_', ' ', $token);
$key .= ":$idx";
$tokens[$i] = $labels[$key] ?: $token;
}
return implode('/', $tokens);
}
/**
* Returns timezone offset in standard time
*/
function rcmail_timezone_standard_time_data($tzname)
{
try {
$tz = new DateTimeZone($tzname);
$date = new DateTime(null, $tz);
$count = 12;
// Move back for a month (up to 12 times) until non-DST date is found
while ($count > 0 && $date->format('I')) {
$date->sub(new DateInterval('P1M'));
$count--;
}
$offset = $date->format('Z') + 45000;
$sortkey = sprintf('%06d.%s', $offset, $tzname);
return array(
'key' => $sortkey,
'offset' => $date->format('P'),
);
}
catch (Exception $e) {}
}
diff --git a/program/steps/settings/identities.inc b/program/steps/settings/identities.inc
index f33c8dda8..847b87d9d 100644
--- a/program/steps/settings/identities.inc
+++ b/program/steps/settings/identities.inc
@@ -1,52 +1,50 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/settings/identities.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Manage identities of a user account |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
if ($RCMAIL->action == 'delete-identity' && $OUTPUT->ajax_call) {
$iid = rcube_utils::get_input_value('_iid', rcube_utils::INPUT_POST);
if ($iid && preg_match('/^[0-9]+(,[0-9]+)*$/', $iid)) {
$plugin = $RCMAIL->plugins->exec_hook('identity_delete', array('id' => $iid));
$deleted = !$plugin['abort'] ? $RCMAIL->user->delete_identity($iid) : $plugin['result'];
if ($deleted > 0 && $deleted !== false) {
$OUTPUT->show_message('deletedsuccessfully', 'confirmation', null, false);
$OUTPUT->command('remove_identity', $iid);
}
else {
$msg = $plugin['message'] ?: ($deleted < 0 ? 'nodeletelastidentity' : 'errorsaving');
$OUTPUT->show_message($msg, 'error', null, false);
}
}
$OUTPUT->send();
}
define('IDENTITIES_LEVEL', intval($RCMAIL->config->get('identities_level', 0)));
$OUTPUT->set_pagetitle($RCMAIL->gettext('identities'));
$OUTPUT->include_script('list.js');
$OUTPUT->set_env('identities_level', IDENTITIES_LEVEL);
$OUTPUT->add_label('deleteidentityconfirm');
$OUTPUT->send('identities');
diff --git a/program/steps/settings/responses.inc b/program/steps/settings/responses.inc
index 41335f73c..52f59b308 100644
--- a/program/steps/settings/responses.inc
+++ b/program/steps/settings/responses.inc
@@ -1,116 +1,114 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/settings/responses.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Manage and save canned response texts |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
if (!empty($_POST['_insert'])) {
$name = trim(rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST));
$text = trim(rcube_utils::get_input_value('_text', rcube_utils::INPUT_POST, true));
if (!empty($name) && !empty($text)) {
$dupes = 0;
$responses = $RCMAIL->get_compose_responses(false, true);
foreach ($responses as $resp) {
if (strcasecmp($name, preg_replace('/\s\(\d+\)$/', '', $resp['name'])) == 0)
$dupes++;
}
if ($dupes) { // require a unique name
$name .= ' (' . ++$dupes . ')';
}
$response = array('name' => $name, 'text' => $text, 'format' => 'text', 'key' => substr(md5($name), 0, 16));
$responses[] = $response;
if ($RCMAIL->user->save_prefs(array('compose_responses' => $responses))) {
$RCMAIL->output->command('add_response_item', $response);
$RCMAIL->output->command('display_message', $RCMAIL->gettext('successfullysaved'), 'confirmation');
}
else {
$RCMAIL->output->command('display_message', $RCMAIL->gettext('errorsaving'), 'error');
}
}
// send response
$RCMAIL->output->send();
}
if ($RCMAIL->action == 'delete-response' && $RCMAIL->output->ajax_call) {
if ($key = rcube_utils::get_input_value('_key', rcube_utils::INPUT_POST)) {
$responses = $RCMAIL->get_compose_responses(false, true);
foreach ($responses as $i => $response) {
if (empty($response['key']))
$response['key'] = substr(md5($response['name']), 0, 16);
if ($response['key'] == $key) {
unset($responses[$i]);
$deleted = $RCMAIL->user->save_prefs(array('compose_responses' => $responses));
break;
}
}
}
if ($deleted) {
$RCMAIL->output->command('display_message', $RCMAIL->gettext('deletedsuccessfully'), 'confirmation');
$RCMAIL->output->command('remove_response', $key);
}
$RCMAIL->output->send();
}
$OUTPUT->set_pagetitle($RCMAIL->gettext('responses'));
$OUTPUT->include_script('list.js');
$OUTPUT->add_handlers(array(
'responseslist' => 'rcmail_responses_list',
));
$OUTPUT->add_label('deleteresponseconfirm');
$OUTPUT->send('responses');
/**
*
*/
function rcmail_responses_list($attrib)
{
global $RCMAIL, $OUTPUT;
$attrib += array('id' => 'rcmresponseslist', 'tagname' => 'table');
$plugin = $RCMAIL->plugins->exec_hook('responses_list', array(
'list' => $RCMAIL->get_compose_responses(true),
'cols' => array('name')
));
$out = $RCMAIL->table_output($attrib, $plugin['list'], $plugin['cols'], 'key');
$readonly_responses = array();
foreach ($plugin['list'] as $item) {
if (!empty($item['static'])) {
$readonly_responses[] = $item['key'];
}
}
// set client env
$OUTPUT->add_gui_object('responseslist', $attrib['id']);
$OUTPUT->set_env('readonly_responses', $readonly_responses);
return $out;
}
diff --git a/program/steps/settings/save_folder.inc b/program/steps/settings/save_folder.inc
index 3cde709e8..1b48b1258 100644
--- a/program/steps/settings/save_folder.inc
+++ b/program/steps/settings/save_folder.inc
@@ -1,207 +1,205 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/settings/save_folder.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide functionality to create/edit a folder |
- | |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
// WARNING: folder names in UI are encoded with RCUBE_CHARSET
// init IMAP connection
$STORAGE = $RCMAIL->get_storage();
$name = trim(rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST, true));
$path = rcube_utils::get_input_value('_parent', rcube_utils::INPUT_POST, true);
$old_imap = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true);
$type = rcube_utils::get_input_value('_type', rcube_utils::INPUT_POST);
$name_imap = rcube_charset::convert($name, RCUBE_CHARSET, 'UTF7-IMAP');
// $path is in UTF7-IMAP already
$delimiter = $STORAGE->get_hierarchy_delimiter();
$options = strlen($old_imap) ? rcmail_folder_options($old_imap) : array();
// Folder name checks
if ($options['protected'] || $options['norename']) {
}
else if (!strlen($name)) {
$error = $RCMAIL->gettext('namecannotbeempty');
}
else if (mb_strlen($name) > 128) {
$error = $RCMAIL->gettext('nametoolong');
}
else if ($name[0] == '.' && $RCMAIL->config->get('imap_skip_hidden_folders')) {
$error = $RCMAIL->gettext('namedotforbidden');
}
else if (!$STORAGE->folder_validate($name, $char)) {
$error = $RCMAIL->gettext('forbiddencharacter') . " ($char)";
}
if ($error) {
$OUTPUT->command('display_message', $error, 'error');
}
else {
if ($options['protected'] || $options['norename']) {
$name_imap = $old_imap;
}
else if (strlen($path)) {
$name_imap = $path . $delimiter . $name_imap;
}
else {
$name_imap = $STORAGE->mod_folder($name_imap, 'in');
}
}
$dual_use_supported = $STORAGE->get_capability(rcube_storage::DUAL_USE_FOLDERS);
$acl_supported = $STORAGE->get_capability('ACL');
// Check access rights to the parent folder
if (!$error && $acl_supported && strlen($path) && (!strlen($old_imap) || $old_imap != $name_imap)) {
$parent_opts = $STORAGE->folder_info($path);
if ($parent_opts['namespace'] != 'personal'
&& (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts['rights'])))
) {
$error = $RCMAIL->gettext('parentnotwritable');
}
}
if ($error) {
$OUTPUT->command('display_message', $error, 'error');
}
else {
$folder['name'] = $name_imap;
$folder['oldname'] = $old_imap;
$folder['class'] = '';
$folder['options'] = $options;
$folder['settings'] = array(
// List view mode: 0-list, 1-threads
'view_mode' => (int) rcube_utils::get_input_value('_viewmode', rcube_utils::INPUT_POST),
'sort_column' => rcube_utils::get_input_value('_sortcol', rcube_utils::INPUT_POST),
'sort_order' => rcube_utils::get_input_value('_sortord', rcube_utils::INPUT_POST),
);
}
// create a new mailbox
if (!$error && !strlen($old_imap)) {
$folder['subscribe'] = true;
// Server does not support both sub-folders and messages in a folder
// For folders that are supposed to contain other folders we will:
// - disable subscribtion
// - add a separator at the end to make them \NoSelect
if (!$dual_use_supported && $type == 'folder') {
$folder['subscribe'] = false;
$folder['noselect'] = true;
}
$plugin = $RCMAIL->plugins->exec_hook('folder_create', array('record' => $folder));
$folder = $plugin['record'];
if (!$plugin['abort']) {
$created = $STORAGE->create_folder($folder['name'], $folder['subscribe'], null, $folder['noselect']);
}
else {
$created = $plugin['result'];
}
if ($created) {
// Save folder settings
if (isset($_POST['_viewmode'])) {
$a_threaded = (array) $RCMAIL->config->get('message_threading', array());
$a_threaded[$folder['name']] = (bool) $_POST['_viewmode'];
$RCMAIL->user->save_prefs(array('message_threading' => $a_threaded));
}
rcmail_update_folder_row($folder['name'], null, $folder['subscribe'], $folder['class']);
$OUTPUT->show_message('foldercreated', 'confirmation');
// reset folder preview frame
$OUTPUT->command('subscription_select');
$OUTPUT->send('iframe');
}
else {
// show error message
$OUTPUT->show_message($plugin['message'] ?: 'errorsaving', 'error', null, false);
}
}
// update a mailbox
else if (!$error) {
$plugin = $RCMAIL->plugins->exec_hook('folder_update', array('record' => $folder));
$folder = $plugin['record'];
$rename = ($folder['oldname'] != $folder['name']);
if (!$plugin['abort']) {
if ($rename) {
$updated = $STORAGE->rename_folder($folder['oldname'], $folder['name']);
}
else {
$updated = true;
}
}
else {
$updated = $plugin['result'];
}
if ($updated) {
// Update folder settings,
if (isset($_POST['_viewmode'])) {
$a_threaded = (array) $RCMAIL->config->get('message_threading', array());
// In case of name change update names of childrens in settings
if ($rename) {
$oldprefix = '/^' . preg_quote($folder['oldname'] . $delimiter, '/') . '/';
foreach ($a_threaded as $key => $val) {
if ($key == $folder['oldname']) {
unset($a_threaded[$key]);
}
else if (preg_match($oldprefix, $key)) {
unset($a_threaded[$key]);
$a_threaded[preg_replace($oldprefix, $folder['name'].$delimiter, $key)] = $val;
}
}
}
$a_threaded[$folder['name']] = (bool) $_POST['_viewmode'];
$RCMAIL->user->save_prefs(array('message_threading' => $a_threaded));
}
$OUTPUT->show_message('folderupdated', 'confirmation');
$OUTPUT->set_env('folder', $folder['name']);
if ($rename) {
// #1488692: update session
if ($_SESSION['mbox'] === $folder['oldname']) {
$_SESSION['mbox'] = $folder['name'];
}
rcmail_update_folder_row($folder['name'], $folder['oldname'], $folder['subscribe'], $folder['class']);
$OUTPUT->send('iframe');
}
else if (!empty($folder['class'])) {
rcmail_update_folder_row($folder['name'], $folder['oldname'], $folder['subscribe'], $folder['class']);
}
}
else {
// show error message
$OUTPUT->show_message($plugin['message'] ?: 'errorsaving', 'error', null, false);
}
}
$RCMAIL->overwrite_action('edit-folder');
diff --git a/program/steps/settings/save_identity.inc b/program/steps/settings/save_identity.inc
index 312930b12..052941ed7 100644
--- a/program/steps/settings/save_identity.inc
+++ b/program/steps/settings/save_identity.inc
@@ -1,266 +1,264 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/settings/save_identity.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Save an identity record or to add a new one |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
define('IDENTITIES_LEVEL', intval($RCMAIL->config->get('identities_level', 0)));
$a_save_cols = array('name', 'email', 'organization', 'reply-to', 'bcc', 'standard', 'signature', 'html_signature');
$a_boolean_cols = array('standard', 'html_signature');
$updated = $default_id = false;
// check input
if (empty($_POST['_email']) && (IDENTITIES_LEVEL == 0 || IDENTITIES_LEVEL == 2)) {
$OUTPUT->show_message('noemailwarning', 'warning');
$RCMAIL->overwrite_action('edit-identity');
return;
}
$save_data = array();
foreach ($a_save_cols as $col) {
$fname = '_'.$col;
if (isset($_POST[$fname])) {
$save_data[$col] = rcube_utils::get_input_value($fname, rcube_utils::INPUT_POST, true);
}
}
// set "off" values for checkboxes that were not checked, and therefore
// not included in the POST body.
foreach ($a_boolean_cols as $col) {
$fname = '_' . $col;
if (!isset($_POST[$fname])) {
$save_data[$col] = 0;
}
}
// make the identity a "default" if only one identity is allowed
if (IDENTITIES_LEVEL > 1) {
$save_data['standard'] = 1;
}
// unset email address if user has no rights to change it
if (IDENTITIES_LEVEL == 1 || IDENTITIES_LEVEL == 3) {
unset($save_data['email']);
}
// unset all fields except signature
else if (IDENTITIES_LEVEL == 4) {
foreach ($save_data as $idx => $value) {
if ($idx != 'signature' && $idx != 'html_signature') {
unset($save_data[$idx]);
}
}
}
// Validate e-mail addresses
$email_checks = array(rcube_utils::idn_to_ascii($save_data['email']));
foreach (array('reply-to', 'bcc') as $item) {
foreach (rcube_mime::decode_address_list($save_data[$item], null, false) as $rcpt) {
$email_checks[] = rcube_utils::idn_to_ascii($rcpt['mailto']);
}
}
foreach ($email_checks as $email) {
if ($email && !rcube_utils::check_email($email)) {
// show error message
$OUTPUT->show_message('emailformaterror', 'error', array('email' => rcube_utils::idn_to_utf8($email)), false);
$RCMAIL->overwrite_action('edit-identity');
return;
}
}
if (!empty($save_data['signature']) && !empty($save_data['html_signature'])) {
// replace uploaded images with data URIs
$save_data['signature'] = rcmail_attach_images($save_data['signature']);
// XSS protection in HTML signature (#1489251)
$save_data['signature'] = rcmail_wash_html($save_data['signature']);
// clear POST data of signature, we want to use safe content
// when the form is displayed again
unset($_POST['_signature']);
}
// update an existing identity
if ($_POST['_iid']) {
$iid = rcube_utils::get_input_value('_iid', rcube_utils::INPUT_POST);
if (in_array(IDENTITIES_LEVEL, array(1,3,4))) {
// merge with old identity data, fixes #1488834
$identity = $RCMAIL->user->get_identity($iid);
$save_data = array_merge($identity, $save_data);
unset($save_data['changed'], $save_data['del'], $save_data['user_id'], $save_data['identity_id']);
}
$plugin = $RCMAIL->plugins->exec_hook('identity_update', array('id' => $iid, 'record' => $save_data));
$save_data = $plugin['record'];
if ($save_data['email']) {
$save_data['email'] = rcube_utils::idn_to_ascii($save_data['email']);
}
if (!$plugin['abort'])
$updated = $RCMAIL->user->update_identity($iid, $save_data);
else
$updated = $plugin['result'];
if ($updated) {
$OUTPUT->show_message('successfullysaved', 'confirmation');
if (!empty($save_data['standard'])) {
$default_id = $iid;
}
if ($_POST['_framed']) {
// update the changed col in list
$name = $save_data['name'] . ' <' . rcube_utils::idn_to_utf8($save_data['email']) .'>';
$OUTPUT->command('parent.update_identity_row', $iid, rcube::Q(trim($name)));
}
}
else {
// show error message
$OUTPUT->show_message($plugin['message'] ?: 'errorsaving', 'error', null, false);
$RCMAIL->overwrite_action('edit-identity');
return;
}
}
// insert a new identity record
else if (IDENTITIES_LEVEL < 2) {
if (IDENTITIES_LEVEL == 1) {
$save_data['email'] = $RCMAIL->get_user_email();
}
$plugin = $RCMAIL->plugins->exec_hook('identity_create', array('record' => $save_data));
$save_data = $plugin['record'];
if ($save_data['email']) {
$save_data['email'] = rcube_utils::idn_to_ascii($save_data['email']);
}
if (!$plugin['abort'])
$insert_id = $save_data['email'] ? $RCMAIL->user->insert_identity($save_data) : null;
else
$insert_id = $plugin['result'];
if ($insert_id) {
$RCMAIL->plugins->exec_hook('identity_create_after', array('id' => $insert_id, 'record' => $save_data));
$OUTPUT->show_message('successfullysaved', 'confirmation', null, false);
$_GET['_iid'] = $insert_id;
if (!empty($save_data['standard'])) {
$default_id = $insert_id;
}
if ($_POST['_framed']) {
// add a new row to the list
$name = $save_data['name'] . ' <' . rcube_utils::idn_to_utf8($save_data['email']) .'>';
$OUTPUT->command('parent.update_identity_row', $insert_id, rcube::Q(trim($name)), true);
}
}
else {
// show error message
$OUTPUT->show_message($plugin['message'] ?: 'errorsaving', 'error', null, false);
$RCMAIL->overwrite_action('edit-identity');
return;
}
}
else {
$OUTPUT->show_message('opnotpermitted', 'error');
}
// mark all other identities as 'not-default'
if ($default_id) {
$RCMAIL->user->set_default($default_id);
}
// go to next step
if (!empty($_REQUEST['_framed'])) {
$RCMAIL->overwrite_action('edit-identity');
}
else {
$RCMAIL->overwrite_action('identities');
}
/**
* Attach uploaded images into signature as data URIs
*/
function rcmail_attach_images($html)
{
global $RCMAIL;
$offset = 0;
$regexp = '/\s(poster|src)\s*=\s*[\'"]*\S+upload-display\S+file=rcmfile(\w+)[\s\'"]*/';
while (preg_match($regexp, $html, $matches, 0, $offset)) {
$file_id = $matches[2];
$data_uri = ' ';
if ($file_id && ($file = $_SESSION['identity']['files'][$file_id])) {
$file = $RCMAIL->plugins->exec_hook('attachment_get', $file);
$data_uri .= 'src="data:' . $file['mimetype'] . ';base64,';
$data_uri .= base64_encode($file['data'] ?: file_get_contents($file['path']));
$data_uri .= '" ';
}
$html = str_replace($matches[0], $data_uri, $html);
$offset += strlen($data_uri) - strlen($matches[0]) + 1;
}
return $html;
}
/**
* Sanity checks/cleanups on HTML body of signature
*/
function rcmail_wash_html($html)
{
// Add header with charset spec., washtml cannot work without that
$html = '<html><head>'
. '<meta http-equiv="Content-Type" content="text/html; charset='.RCUBE_CHARSET.'" />'
. '</head><body>' . $html . '</body></html>';
// clean HTML with washhtml by Frederic Motte
$wash_opts = array(
'show_washed' => false,
'allow_remote' => 1,
'charset' => RCUBE_CHARSET,
'html_elements' => array('body', 'link'),
'html_attribs' => array('rel', 'type'),
);
// initialize HTML washer
$washer = new rcube_washtml($wash_opts);
//$washer->add_callback('form', 'rcmail_washtml_callback');
//$washer->add_callback('style', 'rcmail_washtml_callback');
// Remove non-UTF8 characters (#1487813)
$html = rcube_charset::clean($html);
$html = $washer->wash($html);
// remove unwanted comments and tags (produced by washtml)
$html = preg_replace(array('/<!--[^>]+-->/', '/<\/?body>/'), '', $html);
return $html;
}
diff --git a/program/steps/settings/save_prefs.inc b/program/steps/settings/save_prefs.inc
index a6e1691d5..1fbe9d7ff 100644
--- a/program/steps/settings/save_prefs.inc
+++ b/program/steps/settings/save_prefs.inc
@@ -1,249 +1,247 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/settings/save_prefs.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2017, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Save user preferences to DB and to the current session |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
$CURR_SECTION = rcube_utils::get_input_value('_section', rcube_utils::INPUT_POST);
$a_user_prefs = array();
$dont_override = (array) $RCMAIL->config->get('dont_override');
// set options for specified section
switch ($CURR_SECTION) {
case 'general':
$a_user_prefs = array(
'language' => rcmail_prefs_input('language', '/^[a-zA-Z_-]+$/'),
'timezone' => rcmail_prefs_input('timezone', '/^[a-zA-Z_\/-]+$/'),
'date_format' => rcmail_prefs_input('date_format', '/^[a-zA-Z_.\/ -]+$/'),
'time_format' => rcmail_prefs_input('time_format', '/^[a-zA-Z0-9: ]+$/'),
'prettydate' => isset($_POST['_pretty_date']),
'refresh_interval' => intval($_POST['_refresh_interval']) * 60,
'standard_windows' => isset($_POST['_standard_windows']),
'skin' => rcmail_prefs_input('skin', '/^[a-zA-Z0-9_.-]+$/'),
);
// compose derived date/time format strings
if ((isset($_POST['_date_format']) || isset($_POST['_time_format'])) && $a_user_prefs['date_format'] && $a_user_prefs['time_format']) {
$a_user_prefs['date_short'] = 'D ' . $a_user_prefs['time_format'];
$a_user_prefs['date_long'] = $a_user_prefs['date_format'] . ' ' . $a_user_prefs['time_format'];
}
break;
case 'mailbox':
$a_user_prefs = array(
'layout' => rcmail_prefs_input('layout', '/^[a-z]+$/'),
'mail_read_time' => intval($_POST['_mail_read_time']),
'autoexpand_threads' => intval($_POST['_autoexpand_threads']),
'mdn_requests' => intval($_POST['_mdn_requests']),
'check_all_folders' => isset($_POST['_check_all_folders']),
'mail_pagesize' => max(2, intval($_POST['_mail_pagesize'])),
);
break;
case 'mailview':
$a_user_prefs = array(
'message_extwin' => intval($_POST['_message_extwin']),
'message_show_email' => isset($_POST['_message_show_email']),
'prefer_html' => isset($_POST['_prefer_html']),
'inline_images' => isset($_POST['_inline_images']),
'show_images' => intval($_POST['_show_images']),
'display_next' => isset($_POST['_display_next']),
'default_charset' => rcmail_prefs_input('default_charset', '/^[a-zA-Z0-9-]+$/'),
);
break;
case 'compose':
$a_user_prefs = array(
'compose_extwin' => intval($_POST['_compose_extwin']),
'htmleditor' => intval($_POST['_htmleditor']),
'draft_autosave' => intval($_POST['_draft_autosave']),
'mime_param_folding' => intval($_POST['_mime_param_folding']),
'force_7bit' => isset($_POST['_force_7bit']),
'mdn_default' => isset($_POST['_mdn_default']),
'dsn_default' => isset($_POST['_dsn_default']),
'reply_same_folder' => isset($_POST['_reply_same_folder']),
'spellcheck_before_send' => isset($_POST['_spellcheck_before_send']),
'spellcheck_ignore_syms' => isset($_POST['_spellcheck_ignore_syms']),
'spellcheck_ignore_nums' => isset($_POST['_spellcheck_ignore_nums']),
'spellcheck_ignore_caps' => isset($_POST['_spellcheck_ignore_caps']),
'show_sig' => intval($_POST['_show_sig']),
'reply_mode' => intval($_POST['_reply_mode']),
'sig_below' => isset($_POST['_sig_below']),
'strip_existing_sig' => isset($_POST['_strip_existing_sig']),
'sig_separator' => isset($_POST['_sig_separator']),
'default_font' => rcmail_prefs_input('default_font', '/^[a-zA-Z ]+$/'),
'default_font_size' => rcmail_prefs_input('default_font_size', '/^[0-9]+pt$/'),
'reply_all_mode' => intval($_POST['_reply_all_mode']),
'forward_attachment' => !empty($_POST['_forward_attachment']),
'compose_save_localstorage' => intval($_POST['_compose_save_localstorage']),
);
break;
case 'addressbook':
$a_user_prefs = array(
'default_addressbook' => rcube_utils::get_input_value('_default_addressbook', rcube_utils::INPUT_POST, true),
'autocomplete_single' => isset($_POST['_autocomplete_single']),
'addressbook_sort_col' => rcmail_prefs_input('addressbook_sort_col', '/^[a-z_]+$/'),
'addressbook_name_listing' => intval($_POST['_addressbook_name_listing']),
'addressbook_pagesize' => max(2, intval($_POST['_addressbook_pagesize'])),
);
break;
case 'server':
$a_user_prefs = array(
'read_when_deleted' => isset($_POST['_read_when_deleted']),
'skip_deleted' => isset($_POST['_skip_deleted']),
'flag_for_deletion' => isset($_POST['_flag_for_deletion']),
'delete_always' => isset($_POST['_delete_always']),
'delete_junk' => isset($_POST['_delete_junk']),
'logout_purge' => isset($_POST['_logout_purge']),
'logout_expunge' => isset($_POST['_logout_expunge']),
);
break;
case 'folders':
$a_user_prefs = array(
'show_real_foldernames' => isset($_POST['_show_real_foldernames']),
// stop using SPECIAL-USE (#4782)
'lock_special_folders' => !in_array('lock_special_folders', $dont_override),
);
foreach (rcube_storage::$folder_types as $type) {
$a_user_prefs[$type . '_mbox'] = rcube_utils::get_input_value('_' . $type . '_mbox', rcube_utils::INPUT_POST, true);
};
break;
}
$plugin = rcmail::get_instance()->plugins->exec_hook('preferences_save',
array('prefs' => $a_user_prefs, 'section' => $CURR_SECTION));
$a_user_prefs = $plugin['prefs'];
// don't override these parameters
foreach ($dont_override as $p) {
$a_user_prefs[$p] = $RCMAIL->config->get($p);
}
// verify some options
switch ($CURR_SECTION) {
case 'general':
// switch UI language
if (isset($_POST['_language']) && $a_user_prefs['language'] != $_SESSION['language']) {
$RCMAIL->load_language($a_user_prefs['language']);
$OUTPUT->command('reload', 500);
}
// switch skin (if valid, otherwise unset the pref and fall back to default)
if (!$OUTPUT->check_skin($a_user_prefs['skin'])) {
unset($a_user_prefs['skin']);
}
else if ($RCMAIL->config->get('skin') != $a_user_prefs['skin']) {
$OUTPUT->command('reload', 500);
}
$a_user_prefs['timezone'] = (string) $a_user_prefs['timezone'];
$min_refresh_interval = (int) $RCMAIL->config->get('min_refresh_interval');
if (!empty($a_user_prefs['refresh_interval']) && $min_refresh_interval) {
if ($a_user_prefs['refresh_interval'] < $min_refresh_interval) {
$a_user_prefs['refresh_interval'] = $min_refresh_interval;
}
}
break;
case 'mailbox':
// force min size
if ($a_user_prefs['mail_pagesize'] < 1) {
$a_user_prefs['mail_pagesize'] = 10;
}
$max_pagesize = (int) $RCMAIL->config->get('max_pagesize');
if ($max_pagesize && ($a_user_prefs['mail_pagesize'] > $max_pagesize)) {
$a_user_prefs['mail_pagesize'] = $max_pagesize;
}
break;
case 'addressbook':
// force min size
if ($a_user_prefs['addressbook_pagesize'] < 1) {
$a_user_prefs['addressbook_pagesize'] = 10;
}
$max_pagesize = (int) $RCMAIL->config->get('max_pagesize');
if ($max_pagesize && ($a_user_prefs['addressbook_pagesize'] > $max_pagesize)) {
$a_user_prefs['addressbook_pagesize'] = $max_pagesize;
}
break;
case 'folders':
$storage = $RCMAIL->get_storage();
$specials = array();
foreach (rcube_storage::$folder_types as $type) {
$specials[$type] = $a_user_prefs[$type . '_mbox'];
}
$storage->set_special_folders($specials);
break;
}
// Save preferences
if (!$plugin['abort'])
$saved = $RCMAIL->user->save_prefs($a_user_prefs);
else
$saved = $plugin['result'];
if ($saved)
$OUTPUT->show_message('successfullysaved', 'confirmation');
else
$OUTPUT->show_message($plugin['message'] ?: 'errorsaving', 'error');
// display the form again
$RCMAIL->overwrite_action('edit-prefs');
// Get option value from POST and validate with a regex
function rcmail_prefs_input($name, $regex)
{
global $RCMAIL;
$value = rcube_utils::get_input_value('_' . $name, rcube_utils::INPUT_POST);
if (!is_string($value)) {
$value = null;
}
if ($value !== null && strlen($value) && !preg_match($regex, $value)) {
$value = $RCMAIL->config->get($name);
}
return $value;
}
diff --git a/program/steps/settings/upload.inc b/program/steps/settings/upload.inc
index 01ee433e3..586323934 100644
--- a/program/steps/settings/upload.inc
+++ b/program/steps/settings/upload.inc
@@ -1,144 +1,142 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/settings/upload.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2014, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Handles image uploads |
- | |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
$from = rcube_utils::get_input_value('_from', rcube_utils::INPUT_GET);
$type = preg_replace('/(add|edit)-/', '', $from);
// Plugins in Settings may use this file for some uploads (#5694)
// Make sure it does not contain a dot, which is a special character
// when using rcube_session::append() below
$type = str_replace('.', '-', $type);
if ($RCMAIL->action == 'upload-display') {
$id = 'undefined';
if (preg_match('/^rcmfile(\w+)$/', $_GET['_file'], $regs)) {
$id = $regs[1];
}
$RCMAIL->display_uploaded_file($_SESSION[$type]['files'][$id]);
exit;
}
// Supported image format types
$IMAGE_TYPES = explode(',', 'jpeg,jpg,jp2,tiff,tif,bmp,eps,gif,png,png8,png24,png32,svg,ico');
// clear all stored output properties (like scripts and env vars)
$OUTPUT->reset();
$max_size = $RCMAIL->config->get($type . '_image_size', 64) * 1024;
$post_size = $RCMAIL->show_bytes(rcube_utils::max_upload_size());
$uploadid = rcube_utils::get_input_value('_uploadid', rcube_utils::INPUT_GET);
if (is_array($_FILES['_file']['tmp_name'])) {
$multiple = count($_FILES['_file']['tmp_name']) > 1;
foreach ($_FILES['_file']['tmp_name'] as $i => $filepath) {
// Process uploaded attachment if there is no error
$err = $_FILES['_file']['error'][$i];
if (!$err) {
if ($max_size < $_FILES['_file']['size'][$i]) {
$err = 'size_error';
}
// check image file type
else {
$image = new rcube_image($filepath);
$imageprop = $image->props();
if (!in_array(strtolower($imageprop['type']), $IMAGE_TYPES)) {
$err = 'type_error';
}
}
}
// save uploaded image in storage backend
if (!$err) {
$attachment = $RCMAIL->plugins->exec_hook('attachment_upload', array(
'path' => $filepath,
'size' => $_FILES['_file']['size'][$i],
'name' => $_FILES['_file']['name'][$i],
'mimetype' => 'image/' . $imageprop['type'],
'group' => $type,
));
}
if (!$err && $attachment['status'] && !$attachment['abort']) {
$id = $attachment['id'];
// store new file in session
unset($attachment['status'], $attachment['abort']);
$RCMAIL->session->append($type . '.files', $id, $attachment);
$content = rcube::Q($attachment['name']);
$OUTPUT->command('add2attachment_list', "rcmfile$id", array(
'html' => $content,
'name' => $attachment['name'],
'mimetype' => $attachment['mimetype'],
'classname' => rcube_utils::file2class($attachment['mimetype'], $attachment['name']),
'complete' => true
),
$uploadid
);
}
else {
if ($err == 'type_error') {
$msg = $RCMAIL->gettext('invalidimageformat');
}
else if ($err == 'size_error') {
$msg = $RCMAIL->gettext(array('name' => 'filesizeerror', 'vars' => array('size' => $max_size)));
}
else if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
$msg = $RCMAIL->gettext(array('name' => 'filesizeerror', 'vars' => array('size' => $post_size)));
}
else if ($attachment['error']) {
$msg = $attachment['error'];
}
else {
$msg = $RCMAIL->gettext('fileuploaderror');
}
$OUTPUT->command('display_message', $msg, 'error');
}
}
}
else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// if filesize exceeds post_max_size then $_FILES array is empty,
// show filesizeerror instead of fileuploaderror
if ($maxsize = ini_get('post_max_size')) {
$msg = $RCMAIL->gettext(array(
'name' => 'filesizeerror',
'vars' => array('size' => $RCMAIL->show_bytes(parse_bytes($maxsize)))
));
}
else {
$msg = $RCMAIL->gettext('fileuploaderror');
}
$OUTPUT->command('display_message', $msg, 'error');
$OUTPUT->command('remove_from_attachment_list', $uploadid);
}
$OUTPUT->send('iframe');
diff --git a/program/steps/utils/error.inc b/program/steps/utils/error.inc
index af075173b..4dc4a495d 100644
--- a/program/steps/utils/error.inc
+++ b/program/steps/utils/error.inc
@@ -1,158 +1,156 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/utils/error.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2015, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Display error message page |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
$rcmail = rcmail::get_instance();
// authorization error
if ($ERROR_CODE == 401) {
$__error_title = mb_strtoupper($rcmail->gettext('errauthorizationfailed'));
$__error_text = nl2br($rcmail->gettext('errunauthorizedexplain') . "\n" .
$rcmail->gettext('errcontactserveradmin'));
}
// forbidden due to request check
else if ($ERROR_CODE == 403) {
if ($_SERVER['REQUEST_METHOD'] == 'GET' && $rcmail->request_status == rcube::REQUEST_ERROR_URL) {
$url = $rcmail->url($_GET, true, false, true);
$add = html::a($url, $rcmail->gettext('clicktoresumesession'));
}
else {
$add = $rcmail->gettext('errcontactserveradmin');
}
$__error_title = mb_strtoupper($rcmail->gettext('errrequestcheckfailed'));
$__error_text = nl2br($rcmail->gettext('errcsrfprotectionexplain')) . '<p>' . $add . '</p>';
}
// failed request (wrong step in URL)
else if ($ERROR_CODE == 404) {
$request_url = htmlentities($_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);
$__error_title = mb_strtoupper($rcmail->gettext('errnotfound'));
$__error_text = nl2br($rcmail->gettext('errnotfoundexplain') . "\n" .
$rcmail->gettext('errcontactserveradmin'));
$__error_text .= '<p><i>' . $rcmail->gettext('errfailedrequest') . ": $request_url</i></p>";
}
// browser is not compatible with this application
else if ($ERROR_CODE == 409) {
$user_agent = htmlentities($_SERVER['HTTP_USER_AGENT']);
$__error_title = 'Your browser does not suit the requirements for this application';
$__error_text = "Required features: <i>JavaScript enabled</i> and <i>XMLHTTPRequest support</i>."
. "<p><i>Your configuration:</i><br>$user_agent</p>";
}
// Gone, e.g. message cached but not in the storage
else if ($ERROR_CODE == 410) {
$__error_title = 'INTERNAL ERROR';
$__error_text = $rcmail->gettext('messageopenerror');
}
// invalid compose ID
else if ($ERROR_CODE == 450 && $_SERVER['REQUEST_METHOD'] == 'GET' && $rcmail->action == 'compose') {
$url = $rcmail->url('compose');
$__error_title = mb_strtoupper($rcmail->gettext('errcomposesession'));
$__error_text = nl2br($rcmail->gettext('errcomposesessionexplain'))
. '<p>' . html::a($url, $rcmail->gettext('clicktocompose')) . '</p>';
}
// database connection error
else if ($ERROR_CODE == 601) {
$__error_title = "CONFIGURATION ERROR";
$__error_text = nl2br($ERROR_MESSAGE) . "<br />Please read the INSTALL instructions!";
}
// database connection error
else if ($ERROR_CODE == 603) {
$__error_title = "DATABASE ERROR: CONNECTION FAILED!";
$__error_text = "Unable to connect to the database!<br />Please contact your server-administrator.";
}
// system error
else {
$__error_title = "SERVICE CURRENTLY NOT AVAILABLE!";
$__error_text = sprintf('Error No. [%s]', $ERROR_CODE);
}
// inform plugins
if ($rcmail && $rcmail->plugins) {
$plugin = $rcmail->plugins->exec_hook('error_page', array(
'code' => $ERROR_CODE,
'title' => $__error_title,
'text' => $__error_text,
));
if (!empty($plugin['title'])) {
$__error_title = $plugin['title'];
}
if (!empty($plugin['text'])) {
$__error_text = $plugin['text'];
}
}
$HTTP_ERR_CODE = $ERROR_CODE && $ERROR_CODE < 600 ? $ERROR_CODE : 500;
// Ajax request
if ($rcmail->output && $rcmail->output->type == 'js') {
header("HTTP/1.0 $HTTP_ERR_CODE $__error_title");
die;
}
// compose page content
$__page_content = <<<EOF
<div class="boxerror">
<h3 class="error-title">$__error_title</h3>
<div class="error-text">$__error_text</div>
</div>
EOF;
if ($rcmail->output && $rcmail->output->template_exists('error')) {
$rcmail->output->reset();
$rcmail->output->set_env('error_task', 'error' . (empty($rcmail->user) || empty($rcmail->user->ID) ? '-login' : ''));
$rcmail->output->set_env('server_error', $ERROR_CODE);
$rcmail->output->set_env('comm_path', $rcmail->comm_path);
$rcmail->output->send('error');
}
$__skin = $rcmail->config->get('skin', 'default');
$__productname = $rcmail->config->get('product_name', 'Roundcube Webmail');
// print system error page
print <<<EOF
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"><head>
<title>$__productname :: ERROR</title>
<link rel="stylesheet" type="text/css" href="skins/$__skin/common.css" />
</head>
<body>
<table border="0" cellsapcing="0" cellpadding="0" width="100%" height="80%"><tr><td align="center">
$__page_content
</td></tr></table>
</body>
</html>
EOF;
exit;
diff --git a/program/steps/utils/html2text.inc b/program/steps/utils/html2text.inc
index 20869da12..06c0045c1 100644
--- a/program/steps/utils/html2text.inc
+++ b/program/steps/utils/html2text.inc
@@ -1,31 +1,29 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/utils/html2text.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2015, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Convert HTML message to plain text |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
$html = stream_get_contents(fopen('php://input', 'r'));
$params['links'] = (bool) rcube_utils::get_input_value('_do_links', rcube_utils::INPUT_GET);
$params['width'] = (int) rcube_utils::get_input_value('_width', rcube_utils::INPUT_GET);
$text = $RCMAIL->html2text($html, $params);
header('Content-Type: text/plain; charset=' . RCUBE_CHARSET);
print $text;
exit;
diff --git a/program/steps/utils/killcache.inc b/program/steps/utils/killcache.inc
index ca0c687d1..881d0c5db 100644
--- a/program/steps/utils/killcache.inc
+++ b/program/steps/utils/killcache.inc
@@ -1,55 +1,53 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/utils/killcache.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2010, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Delete rows from cache tables |
- | |
+-----------------------------------------------------------------------+
| Author: Dennis P. Nikolaenko <dennis@nikolaenko.ru> |
+-----------------------------------------------------------------------+
*/
// don't allow public access if not in devel_mode
if (!$RCMAIL->config->get('devel_mode')) {
header("HTTP/1.0 401 Access denied");
die("Access denied!");
}
// @TODO: transaction here (if supported by DB) would be a good thing
$res = $RCMAIL->db->query("DELETE FROM " . $RCMAIL->db->table_name('cache', true));
if ($err = $RCMAIL->db->is_error($res)) {
exit($err);
}
$res = $RCMAIL->db->query("DELETE FROM " . $RCMAIL->db->table_name('cache_shared', true));
if ($err = $RCMAIL->db->is_error($res)) {
exit($err);
}
$res = $RCMAIL->db->query("DELETE FROM " . $RCMAIL->db->table_name('cache_messages', true));
if ($err = $RCMAIL->db->is_error($res)) {
exit($err);
}
$res = $RCMAIL->db->query("DELETE FROM " . $RCMAIL->db->table_name('cache_index', true));
if ($err = $RCMAIL->db->is_error($res)) {
exit($err);
}
$res = $RCMAIL->db->query("DELETE FROM " . $RCMAIL->db->table_name('cache_thread', true));
if ($err = $RCMAIL->db->is_error($res)) {
exit($err);
}
echo "Cache cleared\n";
exit;
diff --git a/program/steps/utils/modcss.inc b/program/steps/utils/modcss.inc
index 19fefe05e..f25cded3a 100644
--- a/program/steps/utils/modcss.inc
+++ b/program/steps/utils/modcss.inc
@@ -1,85 +1,83 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/utils/modcss.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2007-2014, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Modify CSS source from a URL |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
$url = preg_replace('![^a-z0-9.-]!i', '', $_GET['_u']);
if ($url === null || !($realurl = $_SESSION['modcssurls'][$url])) {
header('HTTP/1.1 403 Forbidden');
exit("Unauthorized request");
}
// don't allow any other connections than http(s)
if (!preg_match('~^(https?)://~i', $realurl, $matches)) {
header('HTTP/1.1 403 Forbidden');
exit("Invalid URL");
}
if (ini_get('allow_url_fopen')) {
$scheme = strtolower($matches[1]);
$options = array(
$scheme => array(
'method' => 'GET',
'timeout' => 15,
)
);
$context = stream_context_create($options);
$source = @file_get_contents($realurl, false, $context);
// php.net/manual/en/reserved.variables.httpresponseheader.php
$headers = implode("\n", (array) $http_response_header);
}
else if (function_exists('curl_init')) {
$curl = curl_init($realurl);
curl_setopt($curl, CURLOPT_TIMEOUT, 15);
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 15);
curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_setopt($curl, CURLOPT_ENCODING, '');
curl_setopt($curl, CURLOPT_HEADER, true);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$data = curl_exec($curl);
if ($data !== false) {
list($headers, $source) = explode("\r\n\r\n", $data, 2);
}
else {
$headers = false;
$source = false;
}
}
else {
header('HTTP/1.1 403 Forbidden');
exit("HTTP connections disabled");
}
$ctype_regexp = '~Content-Type:\s+text/(css|plain)~i';
$container_id = preg_replace('/[^a-z0-9]/i', '', $_GET['_c']);
$css_prefix = preg_replace('/[^a-z0-9]/i', '', $_GET['_p']);
if ($source !== false && preg_match($ctype_regexp, $headers)) {
header('Content-Type: text/css');
echo rcube_utils::mod_css_styles($source, $container, false, $css_prefix);
exit;
}
header('HTTP/1.0 404 Not Found');
exit("Invalid response returned by server");
diff --git a/program/steps/utils/save_pref.inc b/program/steps/utils/save_pref.inc
index 165d86c58..7fa4562d6 100644
--- a/program/steps/utils/save_pref.inc
+++ b/program/steps/utils/save_pref.inc
@@ -1,68 +1,66 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/utils/save_pref.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2013, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Save preferences setting in database |
- | |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
$name = rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST);
$value = rcube_utils::get_input_value('_value', rcube_utils::INPUT_POST);
$sessname = rcube_utils::get_input_value('_session', rcube_utils::INPUT_POST);
// Whitelisted preferences and session variables, others
// can be added by plugins
$whitelist = array(
'list_cols',
'collapsed_folders',
'collapsed_abooks',
);
$whitelist_sess = array(
'list_attrib/columns',
);
$whitelist = array_merge($whitelist, $RCMAIL->plugins->allowed_prefs);
$whitelist_sess = array_merge($whitelist_sess, $RCMAIL->plugins->allowed_session_prefs);
if (!in_array($name, $whitelist) || ($sessname && !in_array($sessname, $whitelist_sess))) {
rcube::raise_error(array('code' => 500, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => sprintf("Hack attempt detected (user: %s)", $RCMAIL->get_user_name())),
true, false);
$OUTPUT->reset();
$OUTPUT->send();
}
// save preference value
$RCMAIL->user->save_prefs(array($name => $value));
// update also session if requested
if ($sessname) {
// Support multidimensional arrays...
$vars = explode('/', $sessname);
// ... up to 3 levels
if (count($vars) == 1)
$_SESSION[$vars[0]] = $value;
else if (count($vars) == 2)
$_SESSION[$vars[0]][$vars[1]] = $value;
else if (count($vars) == 3)
$_SESSION[$vars[0]][$vars[1]][$vars[2]] = $value;
}
$OUTPUT->reset();
$OUTPUT->send();
diff --git a/program/steps/utils/spell.inc b/program/steps/utils/spell.inc
index bc1448e37..698a7ab73 100644
--- a/program/steps/utils/spell.inc
+++ b/program/steps/utils/spell.inc
@@ -1,64 +1,62 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/utils/spell.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2011, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Invoke the configured or default spell checking engine. |
- | |
+-----------------------------------------------------------------------+
| Author: Kris Steinhoff <steinhof@umich.edu> |
+-----------------------------------------------------------------------+
*/
// read input
$lang = rcube_utils::get_input_value('lang', rcube_utils::INPUT_GET);
$data = file_get_contents('php://input');
$learn_word = strpos($data, '<learnword>');
// Get data string
$left = strpos($data, '<text>');
$right = strrpos($data, '</text>');
$data = substr($data, $left+6, $right-($left+6));
$data = html_entity_decode($data, ENT_QUOTES, RCUBE_CHARSET);
$spellchecker = new rcube_spellchecker($lang);
if ($learn_word) {
$spellchecker->add_word($data);
$result = '<?xml version="1.0" encoding="'.RCUBE_CHARSET.'"?><learnwordresult></learnwordresult>';
}
else if (empty($data)) {
$result = '<?xml version="1.0" encoding="'.RCUBE_CHARSET.'"?><spellresult charschecked="0"></spellresult>';
}
else {
$spellchecker->check($data);
$result = $spellchecker->get_xml();
}
if ($err = $spellchecker->error()) {
rcube::raise_error(array('code' => 500, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Spell check engine error: " . trim($err)),
true, false);
header("HTTP/1.0 500 Internal Server Error");
exit;
}
// set response length
header("Content-Length: " . strlen($result));
// Don't use server's default Content-Type charset (#1486406)
header("Content-Type: text/xml; charset=" . RCUBE_CHARSET);
print $result;
exit;
diff --git a/program/steps/utils/spell_html.inc b/program/steps/utils/spell_html.inc
index 9381956b0..d85e39cee 100644
--- a/program/steps/utils/spell_html.inc
+++ b/program/steps/utils/spell_html.inc
@@ -1,57 +1,55 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/utils/spell_html.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2011, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Spellchecker for TinyMCE |
- | |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
$method = rcube_utils::get_input_value('method', rcube_utils::INPUT_POST);
$lang = rcube_utils::get_input_value('lang', rcube_utils::INPUT_POST);
$result = array();
$spellchecker = new rcube_spellchecker($lang);
if ($method == 'addToDictionary') {
$data = rcube_utils::get_input_value('word', rcube_utils::INPUT_POST);
$spellchecker->add_word($data);
$result['result'] = true;
}
else {
$data = rcube_utils::get_input_value('text', rcube_utils::INPUT_POST, true);
$data = html_entity_decode($data, ENT_QUOTES, RCUBE_CHARSET);
if ($data && !$spellchecker->check($data)) {
$result['words'] = $spellchecker->get();
$result['dictionary'] = (bool) $RCMAIL->config->get('spellcheck_dictionary');
}
}
if ($error = $spellchecker->error()) {
rcube::raise_error(array('code' => 500, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => sprintf("Spell check engine error: " . $error)),
true, false);
echo json_encode(array('error' => $error));
exit;
}
// send output
header("Content-Type: application/json; charset=".RCUBE_CHARSET);
echo json_encode($result);
exit;
diff --git a/program/steps/utils/text2html.inc b/program/steps/utils/text2html.inc
index 6b964b776..04bd573c7 100644
--- a/program/steps/utils/text2html.inc
+++ b/program/steps/utils/text2html.inc
@@ -1,28 +1,26 @@
<?php
/**
+-----------------------------------------------------------------------+
- | program/steps/utils/text2html.inc |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2014, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Convert plain text to HTML |
- | |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
$text = stream_get_contents(fopen('php://input', 'r'));
$converter = new rcube_text2html($text, false, array('wrap' => true));
header('Content-Type: text/html; charset=' . RCUBE_CHARSET);
print $converter->get_html();
exit;
diff --git a/public_html/index.php b/public_html/index.php
index fda239c57..8c7cdd0a3 100644
--- a/public_html/index.php
+++ b/public_html/index.php
@@ -1,26 +1,26 @@
<?php
/*
+-----------------------------------------------------------------------+
| Roundcube Webmail IMAP Client |
| Version 1.4-git |
| |
- | Copyright (C) 2005-2017, The Roundcube Dev Team |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| This is the public entry point for all HTTP requests to the |
| Roundcue webmail application. |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/');
// include index.php from application root directory
include INSTALL_PATH . 'index.php';
diff --git a/skins/classic/functions.js b/skins/classic/functions.js
index 0e7c02095..99dd4aa25 100644
--- a/skins/classic/functions.js
+++ b/skins/classic/functions.js
@@ -1,1124 +1,1124 @@
/**
* Roundcube functions for default skin interface
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
- * Copyright (c) 2006-2014, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*/
/**
* Settings
*/
function rcube_init_settings_tabs()
{
var el, cl, container = $('#tabsbar'),
last_tab = $('span', container).last(),
tab = '#settingstabpreferences',
action = window.rcmail && rcmail.env.action ? rcmail.env.action : null;
// move About tab to the end
if (last_tab && last_tab.attr('id') != 'settingstababout' && (el = $('#settingstababout'))) {
cl = el.clone(true);
el.remove();
last_tab.after(cl);
}
// get selected tab
if (action)
tab = '#settingstab' + (action.indexOf('identity')>0 ? 'identities' : action.replace(/\./g, ''));
$(tab).addClass('tablink-selected');
$('a', tab).removeAttr('onclick').click(function() { return false; });
}
// Fieldsets-to-tabs converter
// Warning: don't place "caller" <script> inside page element (id)
function rcube_init_tabs(id, current)
{
var content = $('#'+id),
fs = content.children('fieldset');
if (!fs.length)
return;
current = current ? current : 0;
// create tabs container (if not exists)
var tabs = content.find('.tabsbar');
if (!tabs.length)
tabs = $('<div>').addClass('tabsbar').appendTo(content);
// convert fildsets into tabs
fs.each(function(idx) {
var tab, a, elm = $(this), legend = elm.children('legend');
// skip invisible or already initialized fieldsets
if (!elm.is(':visible') || elm.hasClass('tabbed'))
return;
// create a tab
a = $('<a>').text(legend.text()).attr('href', '#');
tab = $('<span>').attr({'id': 'tab'+idx, 'class': 'tablink'})
.click(function() { rcube_show_tab(id, idx); return false })
// remove legend
legend.remove();
// style fieldset
elm.addClass('tabbed');
// style selected tab
if (idx == current)
tab.addClass('tablink-selected');
// add the tab to container
tab.append(a).appendTo(tabs);
});
// hide not selected tabs
fs.each(function(idx) { if (idx != current) $(this).hide(); });
}
function rcube_show_tab(id, index)
{
var fs = $('#'+id).children('fieldset');
fs.each(function(idx) {
// Show/hide fieldset (tab content)
$(this)[index==idx ? 'show' : 'hide']();
// Select/unselect tab
$('#tab'+idx).toggleClass('tablink-selected', idx==index);
});
}
/**
* Mail UI
*/
function rcube_mail_ui()
{
this.popups = {
markmenu: {id:'markmessagemenu'},
replyallmenu: {id:'replyallmenu'},
forwardmenu: {id:'forwardmenu', editable:1},
searchmenu: {id:'searchmenu', editable:1},
messagemenu: {id:'messagemenu'},
attachmentmenu: {id:'attachmentmenu'},
dragmenu: {id:'dragmenu', sticky:1},
groupmenu: {id:'groupoptionsmenu', above:1},
mailboxmenu: {id:'mailboxoptionsmenu', above:1},
composemenu: {id:'composeoptionsmenu', editable:1, overlap:1},
spellmenu: {id:'spellmenu'},
responsesmenu: {id:'responsesmenu'},
// toggle: #1486823, #1486930
uploadmenu: {id:'attachment-form', editable:1, above:1, toggle:!bw.ie&&!bw.linux },
uploadform: {id:'upload-form', editable:1, toggle:!bw.ie&&!bw.linux }
};
var obj;
for (var k in this.popups) {
obj = $('#'+this.popups[k].id)
if (obj.length)
this.popups[k].obj = obj;
else {
delete this.popups[k];
}
}
}
rcube_mail_ui.prototype = {
show_popup: function(popup, show, config)
{
var obj;
// auto-register menu object
if (!this.popups[popup] && (obj = $('#'+popup)) && obj.length)
this.popups[popup] = $.extend(config, {id: popup, obj: obj});
if (typeof this[popup] == 'function')
return this[popup](show);
else
return this.show_popupmenu(popup, show);
},
show_popupmenu: function(popup, show)
{
var obj = this.popups[popup].obj,
above = this.popups[popup].above,
ref = $(this.popups[popup].link ? this.popups[popup].link : rcube_find_object(popup+'link'));
if (typeof show == 'undefined')
show = obj.is(':visible') ? false : true;
else if (this.popups[popup].toggle && show && this.popups[popup].obj.is(':visible') )
show = false;
if (show && ref.length) {
var parent = ref.parent(),
win = $(window),
pos = parent.hasClass('dropbutton') ? parent.offset() : ref.offset();
if (!above && pos.top + ref.height() + obj.height() > win.height())
above = true;
if (pos.left + obj.width() > win.width())
pos.left = win.width() - obj.width() - 30;
obj.css({ left:pos.left, top:(pos.top + (above ? -obj.height() : ref.height())) });
}
obj[show?'show':'hide']();
},
dragmenu: function(show)
{
this.popups.dragmenu.obj[show?'show':'hide']();
},
forwardmenu: function(show)
{
$("input[name='forwardtype'][value="+(rcmail.env.forward_attachment ? 1 : 0)+"]", this.popups.forwardmenu.obj)
.prop('checked', true);
this.show_popupmenu('forwardmenu', show);
},
uploadmenu: function(show)
{
if (typeof show == 'object') // called as event handler
show = false;
// clear upload form
if (!show) {
try { $('#attachment-form form')[0].reset(); }
catch(e){} // ignore errors
}
if (rcmail.mailvelope_editor)
return;
this.show_popupmenu('uploadmenu', show);
if (!document.all && this.popups.uploadmenu.obj.is(':visible'))
$('#attachment-form input[type=file]').click();
},
searchmenu: function(show)
{
var obj = this.popups.searchmenu.obj,
ref = rcube_find_object('searchmenulink');
if (typeof show == 'undefined')
show = obj.is(':visible') ? false : true;
if (show && ref) {
var pos = $(ref).offset();
obj.css({left:pos.left, top:(pos.top + ref.offsetHeight + 2)});
if (rcmail.env.search_mods) {
var n, all,
list = $('input:checkbox[name="s_mods[]"]', obj),
mbox = rcmail.env.mailbox,
mods = rcmail.env.search_mods,
scope = rcmail.env.search_scope || 'base';
if (rcmail.env.task == 'mail') {
mods = mods[mbox] ? mods[mbox] : mods['*'];
all = 'text';
$('input:radio[name="s_scope"]').prop('checked', false).filter('#s_scope_'+scope).prop('checked', true);
}
else {
all = '*';
}
if (mods[all])
list.map(function() {
this.checked = true;
this.disabled = this.value != all;
});
else {
list.prop('disabled', false).prop('checked', false);
for (n in mods)
$('#s_mod_' + n).prop('checked', true);
}
}
}
obj[show?'show':'hide']();
},
set_searchmod: function(elem)
{
var all, m, task = rcmail.env.task,
mods = rcmail.env.search_mods,
mbox = rcmail.env.mailbox,
scope = $('input[name="s_scope"]:checked').val();
if (scope == 'all')
mbox = '*';
if (!mods)
mods = {};
if (task == 'mail') {
if (!mods[mbox])
mods[mbox] = rcube_clone_object(mods['*']);
m = mods[mbox];
all = 'text';
}
else { //addressbook
m = mods;
all = '*';
}
if (!elem.checked)
delete(m[elem.value]);
else
m[elem.value] = 1;
// mark all fields
if (elem.value == all) {
$('input:checkbox[name="s_mods[]"]').map(function() {
if (this == elem)
return;
this.checked = true;
if (elem.checked) {
this.disabled = true;
delete m[this.value];
}
else {
this.disabled = false;
m[this.value] = 1;
}
});
}
rcmail.set_searchmods(m);
},
show_listmenu: function(p)
{
var self = this, buttons = {}, $dialog = $('#listmenu');
// close the dialog
if ($dialog.is(':visible')) {
$dialog.dialog('close', p.originalEvent);
return;
}
// set form values
$('input[name="sort_col"][value="'+rcmail.env.sort_col+'"]').prop('checked', true);
$('input[name="sort_ord"][value="DESC"]').prop('checked', rcmail.env.sort_order == 'DESC');
$('input[name="sort_ord"][value="ASC"]').prop('checked', rcmail.env.sort_order != 'DESC');
$('input[name="view"][value="thread"]').prop('checked', rcmail.env.threading ? true : false);
$('input[name="view"][value="list"]').prop('checked', rcmail.env.threading ? false : true);
// set checkboxes
$('input[name="list_col[]"]').each(function() {
$(this).prop('checked', $.inArray(this.value, rcmail.env.listcols) != -1);
});
$.each(['widescreen', 'desktop', 'list'], function() {
$('input[name="layout"][value="' + this + '"]').prop('checked', rcmail.env.layout == this);
});
$('#listoptions-columns', $dialog)[rcmail.env.layout == 'widescreen' ? 'hide' : 'show']();
buttons[rcmail.gettext('save')] = function(e) {
$dialog.dialog('close', e);
self.save_listmenu();
};
$dialog.dialog({
modal: true,
resizable: false,
closeOnEscape: true,
title: null,
open: function(e) {
var maxheight = 0;
$('#listmenu fieldset').each(function() {
var height = $(this).height();
if (height > maxheight) {
maxheight = height;
}
}).css("min-height", maxheight+"px").height(maxheight);
setTimeout(function() { $dialog.find('a, input:not(:disabled)').not('[aria-disabled=true]').first().focus(); }, 100);
},
close: function(e) {
$dialog.dialog('destroy').hide();
if (e.originalEvent && rcube_event.is_keyboard(e.originalEvent))
$('#listmenulink').focus();
},
buttons: buttons,
minWidth: 500,
width: $dialog.width()+20
}).show();
},
save_listmenu: function()
{
var sort = $('input[name="sort_col"]:checked').val(),
ord = $('input[name="sort_ord"]:checked').val(),
thread = $('input[name="view"]:checked').val(),
layout = $('input[name="layout"]:checked').val(),
cols = $('input[name="list_col[]"]:checked')
.map(function(){ return this.value; }).get();
rcmail.set_list_options(cols, sort, ord, thread == 'thread' ? 1 : 0, layout);
},
spellmenu: function(show)
{
var link, li,
lang = rcmail.spellcheck_lang(),
menu = this.popups.spellmenu.obj,
ul = $('ul', menu);
if (!ul.length) {
ul = $('<ul>');
for (i in rcmail.env.spell_langs) {
li = $('<li>');
link = $('<a href="#"></a>').text(rcmail.env.spell_langs[i])
.addClass('active').data('lang', i)
.click(function() {
rcmail.spellcheck_lang_set($(this).data('lang'));
});
link.appendTo(li);
li.appendTo(ul);
}
ul.appendTo(menu);
}
// select current language
$('li', ul).each(function() {
var el = $('a', this);
if (el.data('lang') == lang)
el.addClass('selected');
else if (el.hasClass('selected'))
el.removeClass('selected');
});
this.show_popupmenu('spellmenu', show);
},
show_attachmentmenu: function(elem, event)
{
var id = elem.parentNode.id.replace(/^attach/, '');
$.each(['open', 'download', 'rename'], function() {
var action = this;
$('#attachmenu' + action).off('click').attr('onclick', '').click(function(e) {
return rcmail.command(action + '-attachment', id, this);
});
});
this.popups.attachmentmenu.link = elem;
rcmail.command('menu-open', {menu: 'attachmentmenu', id: id}, elem, event);
},
menu_open: function(p)
{
if (p && p.name == 'messagelistmenu')
this.show_listmenu();
},
body_mouseup: function(e)
{
var target = e.target; ref = this;
$.each(this.popups, function(i, popup) {
if (popup.obj.is(':visible') && target != rcube_find_object(i + 'link')
&& !popup.toggle
&& target != popup.obj.get(0) // check if scroll bar was clicked (#1489832)
&& (!popup.editable || !ref.target_overlaps(target, popup.id))
&& (!popup.sticky || !rcube_mouse_is_over(e, rcube_find_object(popup.id)))
&& !$(target).is('.folder-selector-link') && !$(target).children('.folder-selector-link').length
) {
window.setTimeout('rcmail_ui.show_popup("'+i+'",false);', 50);
}
});
},
target_overlaps: function (target, elementid)
{
var element = rcube_find_object(elementid);
while (target.parentNode) {
if (target.parentNode == element)
return true;
target = target.parentNode;
}
return false;
},
body_keydown: function(e)
{
if (e.keyCode == 27) {
for (var k in this.popups) {
if (this.popups[k].obj.is(':visible'))
this.show_popup(k, false);
}
}
},
// Mail view layout initialization and change handler
set_layout: function(p)
{
var layout = p ? p.new_layout : rcmail.env.layout,
top = $('#mailcontframe'),
bottom = $('#mailpreviewframe');
if (p)
$('#mailrightcontainer').removeClass().addClass(layout);
if (!this.mailviewsplitv) {
this.mailviewsplitv = new rcube_splitter({id:'mailviewsplitterv', p1: 'mailleftcontainer', p2: 'mailrightcontainer',
orientation: 'v', relative: true, start: 165, callback: rcube_render_mailboxlist });
this.mailviewsplitv.init();
}
$('#mailviewsplitter')[layout == 'desktop' ? 'show' : 'hide']();
$('#mailviewsplitter2')[layout == 'widescreen' ? 'show' : 'hide']();
$('#mailpreviewframe')[layout != 'list' ? 'show' : 'hide']();
rcmail.env.contentframe = layout == 'list' ? null : 'messagecontframe';
if (layout == 'widescreen') {
$('#countcontrols').detach().appendTo($('#messagelistheader'));
top.css({height: 'auto', width: 400});
bottom.css({top: 0, left: 410, height: 'auto'}).show();
if (!this.mailviewsplit2) {
this.mailviewsplit2 = new rcube_splitter({id:'mailviewsplitter2', p1: 'mailcontframe', p2: 'mailpreviewframe',
orientation: 'v', relative: true, start: 405});
this.mailviewsplit2.init();
}
else
this.mailviewsplit2.resize();
}
else if (layout == 'desktop') {
top.css({height: 200, width: '100%'});
bottom.css({left: 0, top: 210, height: 'auto'}).show();
if (!this.mailviewsplit) {
this.mailviewsplit = new rcube_splitter({id:'mailviewsplitter', p1: 'mailcontframe', p2: 'mailpreviewframe',
orientation: 'h', relative: true, start: 205});
this.mailviewsplit.init();
}
else
this.mailviewsplit.resize();
}
else { // layout == 'list'
top.css({height: 'auto', width: '100%'});
bottom.hide();
}
if (p && p.old_layout == 'widescreen') {
$('#countcontrols').detach().appendTo($('#messagelistfooter'));
}
},
/* Message composing */
init_compose_form: function()
{
var f, v, field, fields = ['cc', 'bcc', 'replyto', 'followupto'],
div = document.getElementById('compose-div'),
headers_div = document.getElementById('compose-headers-div');
// Show input elements with non-empty value
for (f=0; f<fields.length; f++) {
v = fields[f]; field = $('#_'+v);
if (field.length) {
field.on('change', {v:v}, function(e) { if (this.value) rcmail_ui.show_header_form(e.data.v); });
if (field.val() != '')
rcmail_ui.show_header_form(v);
}
}
// prevent from form data loss when pressing ESC key in IE
if (bw.ie) {
var form = rcube_find_object('form');
form.onkeydown = function (e) {
if (rcube_event.get_keycode(e) == 27)
rcube_event.cancel(e);
};
}
$(window).resize(function() {
rcmail_ui.resize_compose_body();
});
$('#compose-container').resize(function() {
rcmail_ui.resize_compose_body();
});
div.style.top = (parseInt(headers_div.offsetHeight, 10) + 3) + 'px';
$(window).resize();
// fixes contacts-table position when there's more than one addressbook
$('#contacts-table').css('top', $('#directorylist').height() + 24 + 'px');
// contacts search submit
$('#quicksearchbox').keydown(function(e) {
if (rcube_event.get_keycode(e) == 13)
rcmail.command('search');
});
},
resize_compose_body: function()
{
var div = $('#compose-div .boxlistcontent'),
w = div.width() - 6,
h = div.height() - 2,
x = bw.ie || bw.opera ? 4 : 0;
$('#compose-body_ifr').width(w + 6).height(h - 1 - $('div.mce-toolbar').height());
$('#compose-body').width(w-x).height(h);
$('#googie_edit_layer').width(w).height(h);
},
resize_compose_body_ev: function()
{
window.setTimeout(function(){rcmail_ui.resize_compose_body();}, 100);
},
show_header_form: function(id)
{
var row, s,
link = document.getElementById(id + '-link');
if ((s = this.next_sibling(link)))
s.style.display = 'none';
else if ((s = this.prev_sibling(link)))
s.style.display = 'none';
link.style.display = 'none';
if ((row = document.getElementById('compose-' + id))) {
var div = document.getElementById('compose-div'),
headers_div = document.getElementById('compose-headers-div');
$(row).show();
div.style.top = (parseInt(headers_div.offsetHeight, 10) + 3) + 'px';
this.resize_compose_body();
}
return false;
},
hide_header_form: function(id)
{
var row, ns,
link = document.getElementById(id + '-link'),
parent = link.parentNode,
links = parent.getElementsByTagName('a');
link.style.display = '';
for (var i=0; i<links.length; i++)
if (links[i].style.display != 'none')
for (var j=i+1; j<links.length; j++)
if (links[j].style.display != 'none')
if ((ns = this.next_sibling(links[i]))) {
ns.style.display = '';
break;
}
document.getElementById('_' + id).value = '';
if ((row = document.getElementById('compose-' + id))) {
var div = document.getElementById('compose-div'),
headers_div = document.getElementById('compose-headers-div');
row.style.display = 'none';
div.style.top = (parseInt(headers_div.offsetHeight, 10) + 1) + 'px';
this.resize_compose_body();
}
return false;
},
next_sibling: function(elm)
{
var ns = elm.nextSibling;
while (ns && ns.nodeType == 3)
ns = ns.nextSibling;
return ns;
},
prev_sibling: function(elm)
{
var ps = elm.previousSibling;
while (ps && ps.nodeType == 3)
ps = ps.previousSibling;
return ps;
},
enable_command: function(p)
{
if (p.command == 'reply-list' && rcmail.env.reply_all_mode == 1) {
var label = rcmail.gettext(p.status ? 'replylist' : 'replyall');
$('a.button.replyAll').attr('title', label);
}
else if (p.command == 'compose-encrypted') {
// show the toolbar button for Mailvelope
$('#messagetoolbar a.encrypt').parent().show();
}
else if (p.command == 'compose-encrypted-signed') {
// enable selector for encrypt and sign
$('#encryptionmenulink').show();
}
},
folder_search_init: function(container)
{
// animation to unfold list search box
$('.boxtitle a.search', container).click(function(e) {
var title = $('.boxtitle', container),
box = $('.listsearchbox', container),
dir = box.is(':visible') ? -1 : 1,
height = 24 + ($('select', box).length ? 24 : 0);
box.slideToggle({
duration: 160,
progress: function(animation, progress) {
if (dir < 0) progress = 1 - progress;
$('.boxlistcontent', container).css('top', (title.outerHeight() + height * progress) + 'px');
},
complete: function() {
box.toggleClass('expanded');
if (box.is(':visible')) {
box.find('input[type=text]').focus();
}
else {
$('a.reset', box).click();
}
// TODO: save state in cookie
}
});
return false;
});
}
};
/**
* Roundcube generic layer (floating box) class
*
* @constructor
*/
function rcube_layer(id, attributes)
{
this.name = id;
// create a new layer in the current document
this.create = function(arg)
{
var l = (arg.x) ? arg.x : 0,
t = (arg.y) ? arg.y : 0,
w = arg.width,
h = arg.height,
z = arg.zindex,
vis = arg.vis,
parent = arg.parent,
obj = document.createElement('DIV');
obj.id = this.name;
obj.style.position = 'absolute';
obj.style.visibility = (vis) ? (vis==2) ? 'inherit' : 'visible' : 'hidden';
obj.style.left = l+'px';
obj.style.top = t+'px';
if (w)
obj.style.width = w.toString().match(/\%$/) ? w : w+'px';
if (h)
obj.style.height = h.toString().match(/\%$/) ? h : h+'px';
if (z)
obj.style.zIndex = z;
if (parent)
parent.appendChild(obj);
else
document.body.appendChild(obj);
this.elm = obj;
};
// create new layer
if (attributes != null) {
this.create(attributes);
this.name = this.elm.id;
}
else // just refer to the object
this.elm = document.getElementById(id);
if (!this.elm)
return false;
// ********* layer object properties *********
this.css = this.elm.style;
this.event = this.elm;
this.width = this.elm.offsetWidth;
this.height = this.elm.offsetHeight;
this.x = parseInt(this.elm.offsetLeft);
this.y = parseInt(this.elm.offsetTop);
this.visible = (this.css.visibility=='visible' || this.css.visibility=='show' || this.css.visibility=='inherit') ? true : false;
// ********* layer object methods *********
// move the layer to a specific position
this.move = function(x, y)
{
this.x = x;
this.y = y;
this.css.left = Math.round(this.x)+'px';
this.css.top = Math.round(this.y)+'px';
};
// change the layers width and height
this.resize = function(w,h)
{
this.css.width = w+'px';
this.css.height = h+'px';
this.width = w;
this.height = h;
};
// show or hide the layer
this.show = function(a)
{
if(a == 1) {
this.css.visibility = 'visible';
this.visible = true;
}
else if(a == 2) {
this.css.visibility = 'inherit';
this.visible = true;
}
else {
this.css.visibility = 'hidden';
this.visible = false;
}
};
// write new content into a Layer
this.write = function(cont)
{
this.elm.innerHTML = cont;
};
};
/**
* Scroller
*
* @deprecated Use treelist widget
*/
function rcmail_scroller(list, top, bottom)
{
var ref = this;
this.list = $(list);
this.top = $(top);
this.bottom = $(bottom);
this.step_size = 6;
this.step_time = 20;
this.delay = 500;
this.top
.mouseenter(function() { ref.ts = window.setTimeout(function() { ref.scroll('down'); }, ref.delay); })
.mouseout(function() { if (ref.ts) window.clearTimeout(ref.ts); });
this.bottom
.mouseenter(function() { ref.ts = window.setTimeout(function() { ref.scroll('up'); }, ref.delay); })
.mouseout(function() { if (ref.ts) window.clearTimeout(ref.ts); });
this.scroll = function(dir)
{
var ref = this, size = this.step_size;
if (!rcmail.drag_active)
return;
if (dir == 'down')
size *= -1;
this.list.get(0).scrollTop += size;
this.ts = window.setTimeout(function() { ref.scroll(dir); }, this.step_time);
};
};
// Abbreviate mailbox names to fit width of the container
function rcube_render_mailboxlist()
{
var list = $('#mailboxlist > li > a, #mailboxlist ul:visible > li > a');
// it's too slow with really big number of folders
if (list.length > 100)
return;
list.each(function() {
var elem = $(this),
text = elem.data('text');
if (!text) {
text = elem.text().replace(/\s+\([0-9]+\)$/, '');
elem.data('text', text);
}
if (text.length < 6)
return;
var abbrev = fit_string_to_size(text, elem, elem.width() - elem.children('span.unreadcount').width() - 16);
if (abbrev != text)
elem.attr('title', text);
elem.contents().filter(function(){ return (this.nodeType == 3); }).get(0).data = abbrev;
});
};
// inspired by https://gist.github.com/24261/7fdb113f1e26111bd78c0c6fe515f6c0bf418af5
function fit_string_to_size(str, elem, len)
{
var w, span, $span, result = str, ellip = '...';
if (!rcmail.env.tmp_span) {
// it should be appended to elem to use the same css style
// but for performance reasons we'll append it to body (once)
span = $('<b>').css({visibility: 'hidden', padding: '0px',
'font-family': elem.css('font-family'),
'font-size': elem.css('font-size')})
.appendTo($('body', document)).get(0);
rcmail.env.tmp_span = span;
}
else {
span = rcmail.env.tmp_span;
}
$span = $(span);
$span.text(result);
// on first run, check if string fits into the length already.
w = span.offsetWidth;
if (w > len) {
var cut = Math.max(1, Math.floor(str.length * ((w - len) / w) / 2)),
mid = Math.floor(str.length / 2),
offLeft = mid,
offRight = mid;
while (true) {
offLeft = mid - cut;
offRight = mid + cut;
$span.text(str.substring(0,offLeft) + ellip + str.substring(offRight));
// break loop if string fits size
if (offLeft < 3 || span.offsetWidth)
break;
cut++;
}
// build resulting string
result = str.substring(0,offLeft) + ellip + str.substring(offRight);
}
return result;
};
function update_quota(data)
{
percent_indicator(rcmail.gui_objects.quotadisplay, data);
if (data.table) {
var menu = $('#quotamenu');
if (!menu.length)
menu = $('<div id="quotamenu" class="popupmenu">').appendTo($('body'));
menu.html(data.table);
$('#quotaimg').css('cursor', 'pointer').off('click').on('click', function(e) {
return rcmail.command('menu-open', 'quotamenu', e.target, e);
});
}
};
// percent (quota) indicator
function percent_indicator(obj, data)
{
if (!data || !obj)
return false;
var limit_high = 80,
limit_mid = 55,
width = data.width ? data.width : rcmail.env.indicator_width ? rcmail.env.indicator_width : 100,
height = data.height ? data.height : rcmail.env.indicator_height ? rcmail.env.indicator_height : 14,
quota = data.percent ? Math.abs(parseInt(data.percent)) : 0,
quota_width = parseInt(quota / 100 * width),
pos = $(obj).position();
// workarounds for Opera and Webkit bugs
pos.top = Math.max(0, pos.top);
pos.left = Math.max(0, pos.left);
rcmail.env.indicator_width = width;
rcmail.env.indicator_height = height;
// overlimit
if (quota_width > width) {
quota_width = width;
quota = 100;
}
if (data.title)
data.title = rcmail.get_label('quota') + ': ' + data.title;
// main div
var main = $('<div>');
main.css({position: 'absolute', top: pos.top, left: pos.left,
width: width + 'px', height: height + 'px', zIndex: 100, lineHeight: height + 'px'})
.attr('title', data.title).addClass('quota_text').html(quota + '%');
// used bar
var bar1 = $('<div>');
bar1.css({position: 'absolute', top: pos.top + 1, left: pos.left + 1,
width: quota_width + 'px', height: height + 'px', zIndex: 99});
// background
var bar2 = $('<div>');
bar2.css({position: 'absolute', top: pos.top + 1, left: pos.left + 1,
width: width + 'px', height: height + 'px', zIndex: 98})
.addClass('quota_bg');
if (quota >= limit_high) {
main.addClass(' quota_text_high');
bar1.addClass('quota_high');
}
else if(quota >= limit_mid) {
main.addClass(' quota_text_mid');
bar1.addClass('quota_mid');
}
else {
main.addClass(' quota_text_low');
bar1.addClass('quota_low');
}
// replace quota image
$(obj).html('').append(bar1).append(bar2).append(main);
// update #quotaimg title
$('#quotaimg').attr('title', data.title);
};
function attachment_menu_append(item)
{
$(item).append(
$('<a class="drop"></a>').on('click keypress', function(e) {
if (e.type != 'keypress' || e.which == 13) {
rcmail_ui.show_attachmentmenu(this, e);
return false;
}
})
);
};
// Optional parameters used by TinyMCE
var rcmail_editor_settings = {};
var rcmail_ui;
function rcube_init_mail_ui()
{
rcmail_ui = new rcube_mail_ui();
$(document.body).mouseup(function(e) { rcmail_ui.body_mouseup(e); })
.mousedown(function(e) { rcmail_ui.body_keydown(e); });
rcmail.addEventListener('init', function() {
if (rcmail.env.quota_content)
update_quota(rcmail.env.quota_content);
rcmail.addEventListener('setquota', update_quota);
rcube_webmail.set_iframe_events({mouseup: function(e) { return rcmail_ui.body_mouseup(e); }});
if (rcmail.env.task == 'mail') {
rcmail.addEventListener('enable-command', 'enable_command', rcmail_ui)
.addEventListener('menu-open', 'menu_open', rcmail_ui)
.addEventListener('aftersend-attachment', 'uploadmenu', rcmail_ui)
.addEventListener('aftertoggle-editor', 'resize_compose_body_ev', rcmail_ui)
.addEventListener('afterbounce', function(){ rcmail_ui.show_popup('forwardmenu', false); })
.gui_object('dragmenu', 'dragmenu');
if (rcmail.gui_objects.mailboxlist) {
rcmail.treelist.addEventListener('expand', rcube_render_mailboxlist);
rcmail.addEventListener('responseaftermark', rcube_render_mailboxlist)
.addEventListener('responseaftergetunread', rcube_render_mailboxlist)
.addEventListener('responseaftercheck-recent', rcube_render_mailboxlist)
.addEventListener('responseafterrefresh', rcube_render_mailboxlist)
.addEventListener('afterimport-messages', function(){ rcmail_ui.show_popup('uploadform', false); });
}
rcmail.init_pagejumper('#pagejumper');
// fix message list header on window resize (#1490213)
if (bw.ie && rcmail.message_list)
$(window).resize(function() {
setTimeout(function() { rcmail.message_list.resize(); }, 10);
});
if (rcmail.env.action == 'list' || !rcmail.env.action) {
rcmail.addEventListener('layout-change', 'set_layout', rcmail_ui);
rcmail_ui.set_layout();
}
else if (rcmail.env.action == 'compose') {
rcmail_ui.init_compose_form();
rcmail.addEventListener('compose-encrypted', function(e) {
$("a.button.encrypt")[(e.active ? 'addClass' : 'removeClass')]('selected');
$("select[name='editorSelector']").prop('disabled', e.active);
$('a.button.attach, a.button.responses, a.button.attach, #uploadmenulink')[(e.active ? 'addClass' : 'removeClass')]('buttonPas disabled');
$('#responseslist a.insertresponse')[(e.active ? 'removeClass' : 'addClass')]('active');
});
rcmail.addEventListener('fileappended', function(e) {
if (e.attachment.complete)
attachment_menu_append(e.item);
});
// add menu link for each attachment
$('#attachmentslist > li').each(function() {
attachment_menu_append(this);
});
}
else if (rcmail.env.action == 'show' || rcmail.env.action == 'preview') {
// add menu link for each attachment
$('#attachment-list > li[id^="attach"]').each(function() {
attachment_menu_append(this);
});
$(window).resize(function() {
if (!$('#attachment-list > li[id^="attach"]').length)
$('#attachment-list').hide();
var mvlpe = $('#messagebody.mailvelope');
if (mvlpe.length) {
var content = $('#messageframe'),
h = (content.length ? content.height() + content.offset().top - 25 : $(this).height()) - mvlpe.offset().top - 20;
mvlpe.height(h);
}
});
}
}
else if (rcmail.env.task == 'addressbook') {
rcmail.addEventListener('afterupload-photo', function(){ rcmail_ui.show_popup('uploadform', false); })
.gui_object('dragmenu', 'dragmenu');
}
else if (rcmail.env.task == 'settings') {
if (rcmail.env.action == 'folders') {
rcmail_ui.folder_search_init($('#folder-manager'));
}
$('#mainscreen > #prefs-title').detach().prependTo($('#mainscreen > .box'));
}
});
}
diff --git a/skins/classic/splitter.js b/skins/classic/splitter.js
index 460c35a21..848a53722 100644
--- a/skins/classic/splitter.js
+++ b/skins/classic/splitter.js
@@ -1,228 +1,228 @@
/**
* Roundcube splitter GUI class
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
- * Copyright (c) 2006-2014, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*
* @constructor
*/
function rcube_splitter(attrib)
{
this.p1id = attrib.p1;
this.p2id = attrib.p2;
this.id = attrib.id ? attrib.id : this.p1id + '_' + this.p2id + '_splitter';
this.orientation = attrib.orientation;
this.horizontal = (this.orientation == 'horizontal' || this.orientation == 'h');
this.pos = attrib.start ? attrib.start * 1 : 0;
this.relative = attrib.relative ? true : false;
this.drag_active = false;
this.callback = attrib.callback;
var me = this;
this.init = function()
{
this.p1 = document.getElementById(this.p1id);
this.p2 = document.getElementById(this.p2id);
// create and position the handle for this splitter
this.p1pos = this.relative ? $(this.p1).position() : $(this.p1).offset();
this.p2pos = this.relative ? $(this.p2).position() : $(this.p2).offset();
if (this.horizontal) {
var top = this.p1pos.top + this.p1.offsetHeight;
this.layer = new rcube_layer(this.id, {x: 0, y: top, height: 10,
width: '100%', vis: 1, parent: this.p1.parentNode});
}
else {
var left = this.p1pos.left + this.p1.offsetWidth;
this.layer = new rcube_layer(this.id, {x: left, y: 0, width: 10,
height: '100%', vis: 1, parent: this.p1.parentNode});
}
this.elm = this.layer.elm;
this.elm.className = 'splitter '+(this.horizontal ? 'splitter-h' : 'splitter-v');
this.elm.unselectable = 'on';
// add the mouse event listeners
$(this.elm).mousedown(onDragStart);
// Update splitter position and elements with on window resize
$(window).resize(function(e) { if (e.target === window) me.resize(); });
if (bw.ie)
$(window).resize(onResize);
// read saved position from cookie
var cookie = rcmail.get_cookie(this.id);
if (cookie && !isNaN(cookie)) {
this.pos = parseFloat(cookie);
this.resize();
}
else if (this.pos) {
this.resize();
this.set_cookie();
}
};
/**
* Set size and position of all DOM objects
* according to the saved splitter position
*/
this.resize = function()
{
if (this.horizontal) {
var lh = this.layer.height;
this.p1.style.height = Math.floor(this.pos - this.p1pos.top - lh / 2) + 'px';
this.p2.style.top = Math.ceil(this.pos + lh / 2) + 'px';
this.layer.move(this.layer.x, Math.round(this.pos - lh / 2 + 1));
if (bw.ie) {
var new_height = parseInt(this.p2.parentNode.offsetHeight, 10) - parseInt(this.p2.style.top, 10);
this.p2.style.height = (new_height > 0 ? new_height : 0) + 'px';
}
}
else {
var max_width = $(window).width() - $(this.p1).offset().left - 150,
pos = Math.min(this.pos, max_width);
this.p1.style.width = Math.floor(pos - this.p1pos.left - this.layer.width / 2) + 'px';
this.p2.style.left = Math.ceil(pos + this.layer.width / 2) + 'px';
this.layer.move(Math.round(pos - this.layer.width / 2 + 1), this.layer.y);
if (bw.ie) {
var new_width = parseInt(this.p2.parentNode.offsetWidth, 10) - parseInt(this.p2.style.left, 10) ;
this.p2.style.width = (new_width > 0 ? new_width : 0) + 'px';
}
}
$(this.p2).resize();
$(this.p1).resize();
};
/**
* Handler for mousedown events
*/
function onDragStart(e)
{
me.drag_active = true;
// disable text selection while dragging the splitter
if (bw.konq || bw.chrome || bw.safari)
document.body.style.webkitUserSelect = 'none';
me.p1pos = me.relative ? $(me.p1).position() : $(me.p1).offset();
me.p2pos = me.relative ? $(me.p2).position() : $(me.p2).offset();
// start listening to mousemove events
$(document).on('mousemove.' + me.id, onDrag).on('mouseup.' + me.id, onDragStop);
// enable dragging above iframes
$('iframe').each(function() {
$('<div class="iframe-splitter-fix"></div>')
.css({background: '#fff',
width: this.offsetWidth+'px', height: this.offsetHeight+'px',
position: 'absolute', opacity: '0.001', zIndex: 1000
})
.css($(this).offset())
.appendTo('body');
});
};
/**
* Handler for mousemove events
*/
function onDrag(e)
{
if (!me.drag_active)
return false;
// with timing events dragging action is more responsive
window.clearTimeout(me.ts);
me.ts = window.setTimeout(function() { onDragAction(e); }, 1);
return false;
};
function onDragAction(e)
{
var pos = rcube_event.get_mouse_pos(e);
if (me.relative) {
var parent = $(me.p1.parentNode).offset();
pos.x -= parent.left;
pos.y -= parent.top;
}
if (me.horizontal) {
if (((pos.y - me.layer.height * 1.5) > me.p1pos.top) && ((pos.y + me.layer.height * 1.5) < (me.p2pos.top + me.p2.offsetHeight))) {
me.pos = pos.y;
me.resize();
}
}
else if (((pos.x - me.layer.width * 1.5) > me.p1pos.left) && ((pos.x + me.layer.width * 1.5) < (me.p2pos.left + me.p2.offsetWidth))) {
me.pos = pos.x;
me.resize();
}
me.p1pos = me.relative ? $(me.p1).position() : $(me.p1).offset();
me.p2pos = me.relative ? $(me.p2).position() : $(me.p2).offset();
};
/**
* Handler for mouseup events
*/
function onDragStop(e)
{
me.drag_active = false;
// resume the ability to highlight text
if (bw.konq || bw.chrome || bw.safari)
document.body.style.webkitUserSelect = 'auto';
// cancel the listening for drag events
$(document).off('.' + me.id);
// remove temp divs
$('div.iframe-splitter-fix').remove();
me.set_cookie();
if (typeof me.callback == 'function')
me.callback(me);
return bw.safari ? true : rcube_event.cancel(e);
};
/**
* Handler for window resize events
*/
function onResize(e)
{
if (me.horizontal) {
var new_height = parseInt(me.p2.parentNode.offsetHeight, 10) - parseInt(me.p2.style.top, 10);
me.p2.style.height = (new_height > 0 ? new_height : 0) +'px';
}
else {
var new_width = parseInt(me.p2.parentNode.offsetWidth, 10) - parseInt(me.p2.style.left, 10);
me.p2.style.width = (new_width > 0 ? new_width : 0) + 'px';
}
};
/**
* Saves splitter position in cookie
*/
this.set_cookie = function()
{
var exp = new Date();
exp.setYear(exp.getFullYear() + 1);
rcmail.set_cookie(this.id, this.pos, exp);
};
} // end class rcube_splitter
diff --git a/skins/elastic/styles/colors.less b/skins/elastic/styles/colors.less
index 90dcb7d2f..496af5d67 100644
--- a/skins/elastic/styles/colors.less
+++ b/skins/elastic/styles/colors.less
@@ -1,208 +1,208 @@
/**
- * Roundcube webmail styles for the Elastic skin
+ * Roundcube Webmail styles for the Elastic skin
*
- * Copyright (c) 2017-2018, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
@color-main: #37beff;
@color-main-dark: darken(@color-main, 35%);
@color-black: #161b1d;
@color-font: lighten(@color-black, 10%);
@color-link: #00acff;
@color-link-hover: darken(@color-link, 10%);
@color-border: #ddd;
@color-error: #ff5552;
@color-success: #41b849;
@color-warning: #ffd452;
@color-link-secondary: lighten(@color-font, 60%);
@color-black-shade-text: tint(@color-black, 40%);
@color-black-shade-border: lighten(@color-black, 75%);
@color-black-shade-bg: lighten(@color-black, 85%);
// Layout elements
@color-layout-border: @color-black-shade-border;
@color-layout-header: @color-font;
@color-layout-sidebar-background: #fff;
@color-layout-list-background: #fff;
@color-layout-content-background: #fff;
@color-layout-header-background: #f4f4f4;
@color-layout-footer-background: #fff;
@color-layout-mobile-header-background: @color-layout-header-background;
@color-layout-mobile-footer-background: @color-layout-header-background;
// Task menu
@color-taskmenu-background: #2f3a3f;
@color-taskmenu-button: #fff;
@color-taskmenu-button-selected: @color-taskmenu-button;
@color-taskmenu-button-action: @color-main;
@color-taskmenu-button-special: @color-taskmenu-button;
@color-taskmenu-button-selected-background: lighten(@color-taskmenu-background, 10%);
@color-taskmenu-button-action-background: transparent;
@color-taskmenu-button-special-background: @color-taskmenu-background;
@color-taskmenu-button-hover: #fff;
@color-taskmenu-button-selected-hover: #fff;
@color-taskmenu-button-action-hover: @color-main;
@color-taskmenu-button-special-hover: lighten(@color-taskmenu-button-special, 10%);
@color-taskmenu-button-background-hover: lighten(@color-taskmenu-background, 10%);
@color-taskmenu-button-action-background-hover: @color-taskmenu-button-background-hover;
@color-taskmenu-button-special-background-hover:lighten(@color-taskmenu-button-special-background, 10%);
@color-taskmenu-button-logout-hover: @color-error;
// Toolbar
@color-toolbar-button: @color-font;
@color-toolbar-button-background-hover: darken(@color-layout-header-background, 3%);
@color-searchbar-icon-active: green;
@color-searchbar-background: #fbfbfb;
// Toolbar menu
@color-toolbarmenu-hover: #fff;
@color-toolbarmenu-hover-background: @color-main;
// Listings
@color-list: @color-font;
@color-list-selected: @color-font;
@color-list-selected-background: tint(@color-main, 90%);
@color-list-flagged: @color-error;
@color-list-deleted: lighten(@color-black, 50%);
@color-list-secondary: @color-black-shade-text;
@color-list-droptarget-background: #ffffcc;
@color-list-focus-indicator: lighten(@color-main, 20%);
@color-list-border: @color-black-shade-bg;
@color-list-badge: #fff;
@color-list-badge-background: @color-main;
@color-list-recent: blue;
@color-list-recent-badge: #fff;
@color-list-recent-badge-background: @color-list-recent;
@color-list-pagenav: @color-black-shade-text;
@color-list-icon: fadeout(@color-list-secondary, 25%);
@color-list-unread-status: @color-warning;
// Drag-n-drop layer
@color-drag-layer: #fff;
@color-drag-layer-background: @color-taskmenu-background;
@color-drag-layer-shadow: @color-black-shade-bg;
// Messages
@color-message: @color-font;
@color-message-border: rgba(0, 0, 0, 0.15);
@color-message-background: fadeout(@color-main, 95%);
@color-message-link: @color-main;
@color-message-link-font-weight: normal;
@color-message-information: @color-main;
@color-message-success: @color-success;
@color-message-warning: @color-warning;
@color-message-error: @color-error;
@color-message-loading: tint(@color-font, 30%);
@color-message-error-box: @color-message;
@color-message-information-box: @color-message;
@color-message-success-box: @color-message;
@color-message-warning-box: @color-message;
@color-message-error-box-background: fadeout(@color-message-error, 85%);
@color-message-information-box-background: fadeout(@color-message-information, 85%);
@color-message-success-box-background: fadeout(@color-message-success, 85%);
@color-message-warning-box-background: fadeout(#ffff66, 75%);
// Popovers (menus)
@color-popover-header: @color-black-shade-text;
@color-popover-header-background: transparent;
@color-popover-shadow: @color-black-shade-bg;
@color-popover-separator: @color-black-shade-text;
@color-popover-separator-background: @color-black-shade-bg;
@color-popover-mobile-header: #fff;
@color-popover-mobile-header-background: @color-main-dark;
// Dialogs
@color-dialog-overlay-background: fade(@color-font, 50%);
@color-dialog-header: @color-layout-header;
@color-dialog-header-border: @color-border;
@color-spinner-circle: @color-black-shade-bg;
@color-spinner-item: @color-black-shade-text;
// Forms
@color-input: @color-font;
@color-input-border: #ced4da; // from Bootstrap's .form-control
@color-input-border-focus: @color-main;
@color-input-border-focus-shadow: fadeout(@color-main, 75);
@color-input-border-invalid: @color-error;
@color-input-border-invalid-shadow: fadeout(@color-error, 75);
@color-input-addon-background: @color-black-shade-bg;
@color-recipient-input-border: @color-input-border;
@color-recipient-input-background: @color-black-shade-bg;
@color-input-placeholder: #bbb;
@color-checkbox: @color-main;
@color-checkbox-checked: @color-main;
@color-checkbox-checked-disabled: lighten(@color-main, 15%);
@color-checkbox-focus: @color-input-border-focus;
@color-checkbox-focus-shadow: @color-input-border-focus-shadow;
@color-form-hint: @color-black-shade-text;
@color-image-upload-background: @color-black-shade-bg;
@color-btn-secondary: #fff;
@color-btn-secondary-background: lighten(@color-black, 50%);
@color-btn-primary: #fff;
@color-btn-primary-background: @color-main;
@color-btn-danger: #fff;
@color-btn-danger-background: @color-error;
@color-quota-background: #fff;
@color-quota-text: @color-black-shade-text;
@color-quota-value: @color-main;
@color-quota-value-warning: @color-error;
@color-blockquote-background: fadeout(@color-black-shade-bg, 50%);
@color-blockquote-0: darken(@color-main, 30%);
@color-blockquote-1: darken(@color-success, 25%);
@color-blockquote-2: darken(@color-error, 20%);
@color-blockquote-0-border: @color-blockquote-0;
@color-blockquote-1-border: @color-blockquote-1;
@color-blockquote-2-border: @color-blockquote-2;
@color-mail-signature: @color-black-shade-text;
@color-mail-headers: @color-black-shade-text;
@color-spellcheck-link: @color-error;
@color-table-border: @color-layout-border;
@color-table-selected: @color-list-selected;
@color-table-selected-background: @color-list-selected-background;
// Datepicker
@color-datepicker-border: @color-layout-border;
@color-datepicker-font: @color-font;
@color-datepicker-highlight: !default;
@color-datepicker-highlight-background: lighten(@color-main, 30%);
@color-datepicker-active: #fff;
@color-datepicker-active-background: @color-main;
// Image tools
@color-image-tools: #fff;
@color-image-tools-background: fadeout(@color-main, 60%);
@color-image-tools-hover: fadeout(@color-main, 50%);
diff --git a/skins/elastic/styles/embed.less b/skins/elastic/styles/embed.less
index 58a2fff02..27a04ee54 100644
--- a/skins/elastic/styles/embed.less
+++ b/skins/elastic/styles/embed.less
@@ -1,95 +1,95 @@
/**
- * Roundcube webmail styles for the Elastic skin
+ * Roundcube Webmail styles for the Elastic skin
*
- * Copyright (c) 2017-2018, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
/* Style for embedded pages and TinyMCE editor content page */
@import "global";
.mce-content-body {
margin: 4px;
color: @color-input;
div.pre {
font-family: monospace;
}
blockquote {
border-left: #1010ff 2px solid;
margin: 0;
padding: 0 0.4em;
}
}
.rcmail-inline-message {
.font-family;
font-size: @page-font-size;
padding: .5em;
margin: 0 0 .5em 0;
opacity: .95;
color: @color-message;
background-color: @color-message-background;
display: flex;
align-items: center;
&:before {
.font-icon-class;
font-size: 1.5em;
line-height: 1;
width: 1em;
margin-right: .3em;
content: @fa-var-exclamation-triangle;
color: @color-message-warning;
}
span {
line-height: 1.5;
}
a {
color: @color-link;
}
a:hover {
color: @color-link-hover;
}
button {
vertical-align: middle;
white-space: nowrap;
padding: .375em .75em;
margin-left: .5em;
font-size: 1em;
line-height: 1.5;
border-radius: .25em;
border: 1px solid transparent;
color: @color-btn-primary;
background: @color-btn-primary-background;
&:focus {
box-shadow: 0 0 0 .2rem fade(@color-btn-primary-background, 50%);
}
&:hover {
background: darken(@color-btn-primary-background, 8%);
border-color: darken(@color-btn-primary-background, 10%);
}
&:not([disabled]):not(.disabled):active {
background: darken(@color-btn-primary-background, 11%);
border-color: darken(@color-btn-primary-background, 13%);
box-shadow: 0 0 0 .2rem fade(@color-btn-primary-background, 53%);
}
}
}
.rcmail-inline-buttons {
margin: 0;
}
diff --git a/skins/elastic/styles/global.less b/skins/elastic/styles/global.less
index e4cfdc0d9..9e02b3ec3 100644
--- a/skins/elastic/styles/global.less
+++ b/skins/elastic/styles/global.less
@@ -1,97 +1,97 @@
/**
- * Roundcube webmail styles for the Elastic skin
+ * Roundcube Webmail styles for the Elastic skin
*
- * Copyright (c) 2017-2018, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
@import (reference) "variables";
@import (reference) "mixins";
/*** Fonts ***/
@font-face {
font-family: 'Icons';
font-style: normal;
font-weight: 900;
src: url("../fonts/fa-solid-900.woff2") format('woff2'),
url("../fonts/fa-solid-900.woff") format('woff');
}
@font-face {
font-family: 'Icons';
font-style: normal;
font-weight: 400;
src: url("../fonts/fa-regular-400.woff2") format('woff2'),
url("../fonts/fa-regular-400.woff") format('woff');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'),
url('../fonts/roboto-v18-greek-ext_cyrillic-ext_cyrillic_greek_latin-ext_latin-regular.woff2') format('woff2'), // Chrome 26+, Opera 23+, Firefox 39+
url('../fonts/roboto-v18-greek-ext_cyrillic-ext_cyrillic_greek_latin-ext_latin-regular.woff') format('woff'); // Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+
}
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 400;
src: local('Roboto Italic'), local('Roboto-Italic'),
url('../fonts/roboto-v18-greek-ext_cyrillic-ext_cyrillic_greek_latin-ext_latin-italic.woff2') format('woff2'), // Chrome 26+, Opera 23+, Firefox 39+
url('../fonts/roboto-v18-greek-ext_cyrillic-ext_cyrillic_greek_latin-ext_latin-italic.woff') format('woff'); // Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'),
url('../fonts/roboto-v18-greek-ext_cyrillic-ext_cyrillic_greek_latin-ext_latin-700.woff2') format('woff2'), // Chrome 26+, Opera 23+, Firefox 39+
url('../fonts/roboto-v18-greek-ext_cyrillic-ext_cyrillic_greek_latin-ext_latin-700.woff') format('woff'); // Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+
}
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 700;
src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'),
url('../fonts/roboto-v18-greek-ext_cyrillic-ext_cyrillic_greek_latin-ext_latin-700italic.woff2') format('woff2'), // Chrome 26+, Opera 23+, Firefox 39+
url('../fonts/roboto-v18-greek-ext_cyrillic-ext_cyrillic_greek_latin-ext_latin-700italic.woff') format('woff'); // Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+
}
/* Reset some Bootstrap style */
body, button, input, optgroup, select, textarea, .popover {
.font-family;
}
button, input, select, textarea {
line-height: initial;
}
input {
vertical-align: middle;
}
a {
color: @color-link;
&:hover {
color: @color-link-hover;
}
&.disabled {
pointer-events: none;
}
&.disabled:not(.btn) {
opacity: .5;
}
}
diff --git a/skins/elastic/styles/layout.less b/skins/elastic/styles/layout.less
index 53458364d..bda4f2907 100644
--- a/skins/elastic/styles/layout.less
+++ b/skins/elastic/styles/layout.less
@@ -1,310 +1,310 @@
/**
- * Roundcube webmail styles for the Elastic skin
+ * Roundcube Webmail styles for the Elastic skin
*
- * Copyright (c) 2017-2018, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
/*** Responsive design - Layout ***/
/*
- Large screen (width > 1200px)
-----------------------------------------------------------------------------------------------------
| menu | sidebar | list | content |
-----------------------------------------------------------------------------------------------------
- Normal screen (1200px => width => 768px) - typical: 768x1024 (iPad Mini/Air)
-------------------------------------------------------------------
|menu| sidebar/list | content |
-------------------------------------------------------------------
- Small (480px < width < 768px)
------------------------------------------
|menu| sidebar/list/content |
------------------------------------------
- Phone (width <= 480px) - typical: 320x480 (iPhone 5), 375x667 (iPhone 6-7), 360x564 (Galaxy S6)
------------------------
| sidebar/list/content |
------------------------
*/
html {
height: 100%;
font-size: @page-font-size;
}
body {
min-width: @page-min-width;
height: 100%;
color: @color-font;
overflow: hidden;
}
body > #layout {
overflow: hidden;
display: flex;
height: 100%;
width: 100%;
& > div {
&.sidebar,
&.list {
display: flex;
flex-direction: column;
max-width: 30%;
border-right: 1px solid @color-layout-border;
}
&.content {
display: flex;
flex: 6;
flex-direction: column;
min-width: 50%;
background-color: @color-layout-content-background;
& > .formcontent, // e.g. Help plugin
& > .content {
height: 100%;
width: 100%;
overflow: auto;
flex: 1;
}
.iframe-wrapper {
width: 100%;
height: 100%;
flex: 1;
iframe {
width: 100%;
height: 100%;
border: 0;
}
}
}
&.sidebar {
display: flex;
min-width: 220px;
background-color: @color-layout-sidebar-background;
flex: 2;
}
&.list {
display: flex;
flex: 3;
min-width: 300px;
background-color: @color-layout-list-background;
}
& > .scroller {
flex: 1;
position: relative; // for .listing-info positioning
}
& > .content.only > .scroller {
overflow: auto;
}
& > .header,
& > .footer {
background-color: @color-layout-header-background;
font-size: @layout-header-font-size;
font-weight: bold;
line-height: @layout-header-height;
height: @layout-header-height;
min-height: @layout-header-height;
padding: 0 .25em;
margin: 0;
position: relative; // for absolute positioning of searchbar
overflow: hidden;
white-space: nowrap;
display: flex;
justify-content: center;
}
& > .header {
border-bottom: 1px solid @color-layout-border;
color: @color-layout-header;
.header-title {
.overflow-ellipsis;
flex: 1;
text-align: center;
margin: 0 -20rem;
}
a.toolbar-list-button,
a.toolbar-menu-button {
order: 99; // always the last button
}
}
& > .footer {
border-top: 1px solid @color-layout-border;
background-color: @color-layout-footer-background;
height: @layout-footer-height;
min-height: @layout-footer-height;
line-height: @layout-footer-height;
&.small {
height: @layout-footer-small-height;
min-height: @layout-footer-small-height;
line-height: @layout-footer-small-height;
}
&:empty {
display: none;
}
}
}
}
html.iframe {
body {
overflow: auto;
#layout > .content {
height: 100%;
}
}
}
@media screen and (max-width: @screen-width-large) {
body > #layout > div.sidebar,
body > #layout > div.list {
min-width: 260px;
flex: 3;
}
body > #layout > div.list > .header > a.button {
padding: 0 .25rem;
margin: 0 .25rem;
}
}
@media screen and (max-width: @screen-width-medium) {
}
@media screen and (max-width: @screen-width-small) {
body > #layout > div.sidebar,
body > #layout > div.list {
max-width: none;
border: 0;
}
body > #layout > div > .header {
background-color: @color-layout-mobile-header-background;
a.button {
// make the button active area smaller on touch devices
margin: 0 .3rem !important;
padding: 0 !important;
&:before {
font-size: 1.75rem;
height: @layout-touch-header-height;
margin: 0;
}
&.filter:before {
font-size: 1.6rem; // this icon is too big in FA5
}
.inner {
display: none;
}
}
}
body > #layout > div > .footer {
background-color: @color-layout-mobile-footer-background;
}
a.toolbar-list-button {
display: none;
}
}
@media screen and (max-width: @screen-width-xs) {
}
@media screen and (max-width: @screen-width-mini) {
body > #layout > div.sidebar,
body > #layout > div.list {
min-width: @page-min-width;
}
}
@media screen and (min-width: (@screen-width-xs + 1px)) {
a.menu-button {
display: none;
}
body > #layout > .menu {
// FIXME: we set background color here not in taskmenu.less, because
// otherwise background is partially white on Android/iOS
background-color: @color-taskmenu-background;
width: @layout-menu-width-sm;
}
}
@media screen and (min-width: (@screen-width-small + 1px)) {
.floating-action-buttons,
body > #layout > .content > .header > .header-title,
body > #layout > div > .header > .buttons,
a.toolbar-menu-button {
display: none;
}
}
@media screen and (min-width: (@screen-width-medium + 1px)) {
body > #layout > .menu {
width: @layout-menu-width;
}
}
@media screen and (min-width: (@screen-width-large + 1px)) {
body > #layout > .list > .header > .header-title:not(.all-sizes),
a.toolbar-list-button,
a.back-list-button,
a.back-sidebar-button {
display: none;
}
}
html.layout-phone {
.hidden-phone {
display: none !important;
}
}
html.layout-phone,
html.layout-small {
.hidden-small {
display: none !important;
}
}
html.layout-small {
.hidden-lbs {
display: none !important;
}
}
html.layout-large,
html.layout-normal {
.hidden-lbs,
.hidden-big {
display: none !important;
}
}
html.layout-large {
.hidden-large {
display: none !important;
}
}
diff --git a/skins/elastic/styles/mixins.less b/skins/elastic/styles/mixins.less
index 0a124dd64..b4f4eed28 100644
--- a/skins/elastic/styles/mixins.less
+++ b/skins/elastic/styles/mixins.less
@@ -1,62 +1,62 @@
/**
- * Roundcube webmail styles for the Elastic skin
+ * Roundcube Webmail styles for the Elastic skin
*
- * Copyright (c) 2017-2018, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
/*** Font-icons ***/
.font-icon-class {
font-size: 1.25em;
// FIXME: with inline-block we have some problems with icon alignment
// display: inline-block;
display: block;
float: left;
margin: 0 .25rem 0 0;
width: 1.18em;
height: 1em;
font-family: 'Icons';
font-style: normal;
font-weight: 900;
text-decoration: inherit;
text-align: center;
speak: none;
font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
}
.animated-icon-class {
// spinner-border is defined in Bootstrap
-webkit-animation: spinner-border 1.5s infinite linear;
animation: spinner-border 1.5s infinite linear;
}
.font-icon-solid(@icon) {
content: @icon;
font-weight: 900;
}
.font-icon-regular(@icon) {
content: @icon;
font-weight: 400;
}
.overflow-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
}
.font-family {
font-family: Roboto, sans-serif;
}
.style-input-focus {
border-color: @color-input-border-focus;
box-shadow: 0 0 0 .2rem @color-input-border-focus-shadow;
}
\ No newline at end of file
diff --git a/skins/elastic/styles/print.less b/skins/elastic/styles/print.less
index 7a819a27a..3701db3e3 100644
--- a/skins/elastic/styles/print.less
+++ b/skins/elastic/styles/print.less
@@ -1,79 +1,79 @@
/**
- * Roundcube webmail styles for the Elastic skin
+ * Roundcube Webmail styles for the Elastic skin
*
- * Copyright (c) 2017-2018, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
/*** Additional styles for printouts ***/
body {
overflow: auto;
height: auto;
}
#print-layout {
margin: 1rem;
// Overwrites for mail message printouts
.image-attachment {
.image-link {
margin-bottom: .5rem;
}
.attachment-links {
display: none;
}
}
.header-content {
.message-partheaders {
padding: 0 !important;
border: 0;
font-size: 1rem;
color: inherit;
}
}
#message-header {
margin-bottom: .5rem;
}
.attachment-size {
padding-left: .1rem;
}
// Overwrites for contact printouts
.formcontent {
padding: 0;
legend {
margin-top: .5rem;
}
.row .form-control-plaintext {
padding: .1rem;
}
.contactfield {
padding: .2rem 0;
}
}
.propform.groupped .row.input-group .input-group-text {
padding: 0;
min-width: 12rem;
background: #fff;
border: 0;
}
.contact-header {
margin-bottom: 0;
}
}
diff --git a/skins/elastic/styles/styles.less b/skins/elastic/styles/styles.less
index 0d199fc8e..9064a362d 100644
--- a/skins/elastic/styles/styles.less
+++ b/skins/elastic/styles/styles.less
@@ -1,140 +1,140 @@
/**
- * Roundcube webmail styles for the Elastic skin
+ * Roundcube Webmail styles for the Elastic skin
*
- * Copyright (c) 2017-2018, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
@import "global";
@import "layout";
@import "widgets/common";
@import "widgets/buttons";
@import "widgets/jqueryui";
@import "widgets/dialogs";
@import "widgets/taskmenu";
@import "widgets/messages";
@import "widgets/toolbar";
@import "widgets/lists";
@import "widgets/forms";
@import "widgets/editor";
@import "widgets/mail";
/*** Login form ***/
.task-login #layout > .content {
text-align: center;
width: 100%;
display: block;
}
.task-login #layout .content #logo {
display: inline-block;
position: relative;
top: 16vh;
max-height: 100px;
}
#login-form {
margin: 0 auto;
top: 20vh;
width: 95%;
max-width: 320px;
position: relative;
// Fixes table width in IE11
table, tbody {
display: block;
}
// Fixes input width in IE11
.row {
margin-right: 0;
margin-left: 0;
}
}
#rcmloginsubmit {
&:before {
display: none !important;
}
}
#login-footer {
flex: 1;
color: @color-black-shade-text;
& > div {
margin-top: 1rem;
padding: 1rem;
background: @color-black-shade-bg;
border-radius: .3rem;
}
}
#login-addon {
position: absolute;
bottom: 0;
max-height: 30%;
margin: 1rem !important;
width: auto !important;
overflow: auto;
@media screen and (min-width: (@screen-width-small + 1px)) {
max-width: @screen-width-small;
margin: auto !important;
bottom: 1rem;
left: 0;
right: 0;
}
}
body.task-error-login #layout {
& > .menu,
& > .content > .header {
display: none;
}
}
/*** Addressbook UI ***/
#contactpic {
min-width: @layout-contact-icon-width;
min-height: @layout-contact-icon-width;
border-radius: .5rem;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
img {
max-width: @layout-contact-icon-width;
max-height: @layout-contact-icon-height;
}
}
#contacthead {
.names {
margin-bottom: .5rem;
span.namefield {
font-size: 1.5rem;
font-weight: bold;
line-height: 1.2;
}
}
&.readonly {
.source.row {
color: @color-form-hint;
font-size: 90%;
margin-bottom: .25rem;
}
}
}
@import "_styles";
diff --git a/skins/elastic/styles/variables.less b/skins/elastic/styles/variables.less
index d8ff0f82b..12beedb51 100644
--- a/skins/elastic/styles/variables.less
+++ b/skins/elastic/styles/variables.less
@@ -1,52 +1,52 @@
/**
- * Roundcube webmail styles for the Elastic skin
+ * Roundcube Webmail styles for the Elastic skin
*
- * Copyright (c) 2017-2018, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
@import (reference) "fontawesome";
@import (reference) "colors";
@import (reference) "_variables";
@screen-width-large: 1200px;
@screen-width-medium: 1024px;
@screen-width-small: 768px;
@screen-width-xs: 480px;
@screen-width-mini: 320px;
@screen-width-touch: @screen-width-medium;
@screen-width-bs-phone: 576px;
@page-font-size: 14px;
@page-min-width: 240px;
@layout-menu-width: 5.6rem;
@layout-menu-width-sm: @layout-menu-width * 0.6;
@layout-header-height: 4.2rem;
@layout-footer-height: @layout-header-height;
@layout-footer-small-height: 2.5rem;
@layout-header-font-size: 1rem;
@layout-searchbar-height: 2.6rem;
@layout-touch-header-height: @layout-header-height;
@layout-touch-header-font-size: 1.2rem;
@layout-touch-menu-record-height: 3.4rem;
@layout-touch-menu-record-font-size: 1.2rem;
@layout-touch-icon-width: 2.2em;
@layout-mobile-menu-width: (@screen-width-mini * .85);
@layout-contact-icon-width: 112px;
@layout-contact-icon-height: 135px;
@listing-line-height: 2.5rem;
@listing-touch-line-height: 3.4rem;
@listing-treetoggle-width: 1.5em;
// Additional icons
@icon-resize-corner: data-uri("image/svg+xml;charset=utf-8", "../images/corner-handle.svg"); // size: 512x512
@icon-file-drop: data-uri("image/svg+xml;charset=utf-8", "../images/download.svg");
diff --git a/skins/elastic/styles/widgets/buttons.less b/skins/elastic/styles/widgets/buttons.less
index 386f74a44..92230ac51 100644
--- a/skins/elastic/styles/widgets/buttons.less
+++ b/skins/elastic/styles/widgets/buttons.less
@@ -1,320 +1,320 @@
/**
- * Roundcube webmail styles for the Elastic skin
+ * Roundcube Webmail styles for the Elastic skin
*
- * Copyright (c) 2017-2018, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
/*** Buttons ***/
a.rcmaddcontact {
display: none;
}
.button.disabled {
opacity: .5;
}
a.button {
text-decoration: none;
&.active {
cursor: pointer;
}
}
/* font-icons */
a.button.icon,
button.btn {
&:before {
&:extend(.font-icon-class);
}
&.sidebar-menu:before,
&.toolbar-menu-button:before,
&.toolbar-list-button:before {
content: @fa-var-ellipsis-v;
width: 1em;
}
&.menu-button:before {
content: @fa-var-bars;
}
&.back-sidebar-button:before,
&.back-content-button:before,
&.back-list-button:before {
content: @fa-var-chevron-left;
}
&.refresh:before {
content: @fa-var-sync;
}
&.generate:before,
&.yes:before,
&.submit:before,
&.continue:before,
&.save:before {
content: @fa-var-check;
}
&.create:before {
content: @fa-var-plus-square;
}
&.edit:before {
content: @fa-var-pencil-alt;
}
&.qrcode:before {
content: @fa-var-qrcode;
}
&.search:before {
content: @fa-var-search;
}
&.filter:before {
content: @fa-var-filter;
font-size: 1.2em; // this icon is too-big in FA5
}
&.import:before {
content: @fa-var-upload;
}
&.export:before {
content: @fa-var-download;
}
&.discard:before,
&.delete:before {
.font-icon-regular(@fa-var-trash-alt);
}
&.next:before {
content: @fa-var-arrow-right;
}
&.restore:before {
content: @fa-var-undo;
}
&.send:before,
&.bounce:before {
content: @fa-var-paper-plane;
}
&.attach:before {
content: @fa-var-paperclip;
}
&.attach.vcard:before {
content: @fa-var-user;
}
&.no:before,
&.close:before,
&.cancel:before {
content: @fa-var-times;
}
&.back:before {
content: @fa-var-chevron-left;
}
&.remove:before {
content: @fa-var-times;
}
&.unlock:before {
content: @fa-var-unlock;
}
&.help:before {
.font-icon-regular(@fa-var-life-ring);
}
&.folders:before {
content: @fa-var-folder-open;
}
&.tools:before,
&.settings:before {
content: @fa-var-wrench;
}
&.properties:before {
content: @fa-var-info-circle;
}
&.select:before {
.font-icon-regular(@fa-var-check-circle);
}
&.insert.recipient:before {
content: @fa-var-user-plus;
}
&.encrypt:before {
content: @fa-var-lock;
}
&.sign:before {
content: @fa-var-signature;
}
&.sso:before {
content: @fa-var-sign-in-alt;
}
}
a.btn,
button.btn {
&:before {
display: inline !important;
float: none !important;
vertical-align: middle;
margin-right: .4rem !important; // !important needed for a.btn
}
}
a.button.icon {
&.dropdown:before {
content: @fa-var-caret-down;
font-size: 1em;
}
& > span.inner {
display: none;
}
}
html.touch {
.btn:focus {
box-shadow: none;
}
}
@floating-action-button-size: 4rem;
.floating-action-buttons {
position: absolute;
right: 0;
bottom: 0;
.footer:not(:empty) + & {
bottom: @layout-footer-small-height;
}
a.button {
display: block;
float: left;
width: @floating-action-button-size;
height: @floating-action-button-size;
border-radius: 50%;
background: @color-main;
color: white;
opacity: .95;
box-shadow: 0 0 5px 5px lighten(@color-main, 35%);
margin: 0 1rem 1rem 0;
&:before {
&:extend(.font-icon-class);
content: @fa-var-plus;
width: @floating-action-button-size;
height: @floating-action-button-size;
line-height: @floating-action-button-size;
}
.inner {
display: none;
}
}
}
/*** Bootstrap button style overrides ***/
.btn {
&:focus {
box-shadow: 0 0 0 .2rem fade(@color-btn-primary-background, 30%);
}
}
.btn-secondary {
color: @color-btn-secondary;
background: @color-btn-secondary-background;
border-color: @color-btn-secondary-background;
&:focus {
box-shadow: 0 0 0 .2rem fade(@color-btn-secondary-background, 50%);
}
&:hover {
background: darken(@color-btn-secondary-background, 5%);
border-color: darken(@color-btn-secondary-background, 7%);
}
&.disabled,
&:disabled {
background: lighten(@color-btn-secondary-background, 20%);
border-color: lighten(@color-btn-secondary-background, 20%);
opacity: 1;
}
&:not(:disabled):not(.disabled) {
&:active,
&.active {
background: darken(@color-btn-secondary-background, 10%);
border-color: darken(@color-btn-secondary-background, 12%);
&:focus {
box-shadow: 0 0 0 .2rem fade(@color-btn-secondary-background, 53%);
}
}
}
}
.btn-primary {
color: @color-btn-primary;
background: @color-btn-primary-background;
border-color: @color-btn-primary-background;
&:focus {
box-shadow: 0 0 0 .2rem fade(@color-btn-primary-background, 50%);
}
&:hover {
background: darken(@color-btn-primary-background, 5%);
border-color: darken(@color-btn-primary-background, 7%);
}
&.disabled,
&:disabled {
background: lighten(@color-btn-primary-background, 20%);
border-color: lighten(@color-btn-primary-background, 20%);
opacity: 1;
}
&:not(:disabled):not(.disabled) {
&:active,
&.active {
background: darken(@color-btn-primary-background, 10%);
border-color: darken(@color-btn-primary-background, 12%);
&:focus {
box-shadow: 0 0 0 .2rem fade(@color-btn-primary-background, 53%);
}
}
}
}
.btn-danger {
color: @color-btn-danger;
background: @color-btn-danger-background;
border-color: @color-btn-danger-background;
&:focus {
box-shadow: 0 0 0 .2rem fade(@color-btn-danger-background, 50%);
}
&:hover {
background: darken(@color-btn-danger-background, 5%);
border-color: darken(@color-btn-danger-background, 7%);
}
&.disabled,
&:disabled {
background: lighten(@color-btn-danger-background, 20%);
border-color: lighten(@color-btn-danger-background, 20%);
opacity: 1;
}
&:not(:disabled):not(.disabled) {
&:active,
&.active {
background: darken(@color-btn-danger-background, 10%);
border-color: darken(@color-btn-danger-background, 12%);
&:focus {
box-shadow: 0 0 0 .2rem fade(@color-btn-danger-background, 53%);
}
}
}
}
diff --git a/skins/elastic/styles/widgets/common.less b/skins/elastic/styles/widgets/common.less
index 8b2fc53b7..9f8d85b16 100644
--- a/skins/elastic/styles/widgets/common.less
+++ b/skins/elastic/styles/widgets/common.less
@@ -1,532 +1,532 @@
/**
- * Roundcube webmail styles for the Elastic skin
+ * Roundcube Webmail styles for the Elastic skin
*
- * Copyright (c) 2017-2018, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
/*** Common UI elements ***/
.hidden,
.voice {
display: none !important;
}
font.bold {
font-weight: bold;
}
#rcmdraglayer {
min-width: 260px;
width: 260px;
background-color: @color-drag-layer-background;
color: @color-drag-layer;
box-shadow: 3px 3px 5px @color-drag-layer-shadow;
border-radius: .3rem;
z-index: 250;
opacity: .92;
padding: .5rem;
white-space: nowrap;
div {
line-height: 1.6em;
.overflow-ellipsis;
}
}
.frame-content {
padding: 1rem;
h2 {
font-weight: bold;
font-size: 1.5em;
}
h3 {
font-weight: bold;
font-size: 1.25em;
}
}
.listbox {
.scroller {
width: 100%;
overflow-x: hidden;
overflow-y: auto;
}
.navlist {
height: 0;
flex: initial !important;
.listing {
tr:last-child td,
li:last-child {
border-bottom: 0;
}
}
}
.popup & {
height: 100%;
display: flex;
flex-direction: column;
.scroller {
flex: 1;
}
.footer {
border-top: 1px solid @color-layout-border;
background-color: @color-searchbar-background;
}
}
}
.contact-header {
display: flex;
margin-bottom: 1rem;
.contact-photo {
min-width: @layout-contact-icon-width;
}
.contact-head {
margin-left: 1rem;
margin-top: 0 !important;
legend {
display: none;
}
}
}
@image-attachment-size: 250px;
// this is when image thumbnails are enabled
p.image-attachment {
position: relative;
border: 1px solid @color-border;
border-radius: .3rem;
background-color: @color-message-background;
float: left;
margin: .5rem;
min-width: 47%;
min-height: @image-attachment-size;
overflow: hidden;
// use flexbox to center the image
display: flex;
justify-content: center;
@media screen and (max-width: @screen-width-xs) {
float: none;
margin: .5rem 0 .5rem 0;
}
.image-link {
align-self: center;
text-align: center;
margin: 1.6rem .5rem;
}
span {
color: @color-form-hint;
padding: 0 .5rem;
font-size: 90%;
white-space: nowrap;
position: absolute;
line-height: 1.5rem;
}
.image-filename {
.overflow-ellipsis;
left: 0;
top: 0;
right: 0;
padding-right: 4rem;
}
.image-filesize {
right: 0;
top: 0;
}
.attachment-links {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
text-align: center;
a {
text-decoration: none;
display: inline-block;
padding: 0 .5rem;
line-height: 1.5rem;
}
a:before {
&:extend(.font-icon-class);
display: inline-block;
}
a.open:before {
content: @fa-var-external-link-square-alt;
}
a.download:before {
content: @fa-var-download;
}
}
}
// this is when image thumbnails are disabled
fieldset.image-attachment {
margin-top: .5rem;
legend {
color: @color-form-hint;
font-size: .9rem;
border-top: 1px solid lighten(@color-mail-headers, 50%);
margin: 0;
}
img {
max-width: 100%;
}
}
#folder-selector {
overflow-y: auto;
}
.noselect {
user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.iframe-loader {
width: 100%;
position: absolute;
top: 0;
bottom: 0;
background-color: rgba(255, 255, 255, .95);
display: flex;
align-items: center;
justify-content: center;
z-index: 3;
.spinner-border {
width: 7rem;
height: 7rem;
color: @color-spinner-circle;
border: 1rem solid;
border-color: currentColor @color-spinner-item currentColor currentColor;
}
}
.footer.toolbar + .iframe-loader {
top: @layout-header-height;
bottom: @layout-header-height;
}
// iOS: Fix scrolling of iframe, display scrollbars on scrollable elements
.ios-scroll {
padding: 0;
-webkit-overflow-scrolling: touch !important;
overflow: scroll !important;
&.iframe-wrapper {
margin-top: 1px; // FIXME: without this scrolling hides the wrapper neighbours' border
}
}
.webkit-scroller {
&::-webkit-scrollbar {
-webkit-appearance: none;
}
&::-webkit-scrollbar:vertical {
width: .6rem;
}
&::-webkit-scrollbar:horizontal {
height: .6rem;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, .4);
border-radius: .3rem;
border: 2px solid #fff;
}
}
.quota-widget {
width: 100%;
max-width: 15em;
padding: .5rem 1rem;
display: flex;
align-items: center;
color: @color-quota-text;
&:before {
&:extend(.font-icon-class);
content: @fa-var-hdd;
line-height: 1;
}
.count {
color: @color-quota-text;
font-size: 80%;
order: 2;
}
.bar {
flex: 1;
height: .5rem;
margin: 0 1rem;
background-color: @color-quota-background;
border: 1px solid @color-layout-border;
border-radius: .25rem;
overflow: hidden;
}
.value {
display: block;
background-color: @color-quota-value;
height: 1rem;
opacity: .75;
&.warning {
background-color: @color-quota-value-warning;
}
}
}
.quota-info {
width: 100%;
display: table !important;
td,th {
text-align: center;
white-space: nowrap;
}
th {
border-top: 0;
}
.root {
line-height: 1;
font-style: italic;
color: @color-popover-separator;
background-color: @color-popover-separator-background;
}
th:first-child,
.name {
text-align: left;
}
}
// Make Bootstrap tabs non-wrappable
.nav-tabs {
flex-wrap: nowrap;
.nav-item {
white-space: nowrap;
overflow: hidden;
}
.nav-link {
.overflow-ellipsis;
}
}
.props-table {
td.title {
width: 7em;
}
}
.table-widget {
display: flex;
flex-direction: column;
margin-bottom: .5rem;
border: 1px solid @color-table-border;
& > .content {
overflow-x: auto;
flex-grow: 1;
height: 18.5em;
table th {
border-top: 0;
}
}
& > .footer {
height: 3.5rem;
border-top: 1px solid @color-table-border;
text-align: left;
a.button {
padding: 0;
height: 3.5rem;
}
}
table {
margin: 0;
max-height: 18.5em;
}
// Options table is a table with first column for identifier/description
// and other columns for a state flag. E.g. ACL table
table.options-table {
td,th {
text-align: center;
vertical-align: middle;
&:first-child {
.overflow-ellipsis;
text-align: left;
}
}
tr:last-child td {
border-bottom: 1px solid @color-table-border;
}
tr.selected td {
background-color: @color-table-selected-background;
color: @color-table-selected;
outline: 0;
}
td:not(:first-child) span {
display: inline-block;
line-height: 1.25;
&:before {
&:extend(.font-icon-class);
}
}
td.enabled span:before {
content: @fa-var-check;
}
td.partial span:before {
opacity: .3;
content: @fa-var-check;
}
}
}
table.compact-table {
margin: 0;
width: 100%;
*:not(.invalid-feedback) {
font-size: inherit;
}
td {
padding: .25rem;
border: 0;
}
td:first-child {
padding-left: 0;
}
td:last-child {
padding-right: 0;
}
}
table.table {
.checkbox-cell {
width: 3rem;
white-space: nowrap;
overflow: hidden;
text-align: center;
padding: .5rem;
html.touch & {
padding: .5rem .3rem;
}
}
th.checkbox-cell {
padding: .75rem 0;
max-width: 1rem;
&:before {
&:extend(.font-icon-class);
cursor: pointer;
margin: 0 1rem;
line-height: 1;
}
&.subscription:before {
content: @fa-var-rss-square;
}
&.alarm:before {
.font-icon-regular(@fa-var-bell);
}
&.read:before {
content: @fa-var-eye;
}
&.write:before {
content: @fa-var-pencil-alt;
}
}
.buttons-cell {
width: 1%;
white-space: nowrap;
text-align: center;
a.button:before {
line-height: 1;
float: none;
display: inline-block;
}
@media screen and (min-width: @screen-width-xs) {
a.button .inner {
display: inline;
}
}
}
label {
margin: 0;
display: inline;
}
fieldset.tab-pane & thead th {
border: 0;
}
}
/* Bootstrap's .table style overwrites */
.table {
thead th {
border-width: 1px;
white-space: nowrap;
}
}
diff --git a/skins/elastic/styles/widgets/dialogs.less b/skins/elastic/styles/widgets/dialogs.less
index 93b9dceaf..c2269b27d 100644
--- a/skins/elastic/styles/widgets/dialogs.less
+++ b/skins/elastic/styles/widgets/dialogs.less
@@ -1,255 +1,255 @@
/**
- * Roundcube webmail styles for the Elastic skin
+ * Roundcube Webmail styles for the Elastic skin
*
- * Copyright (c) 2017-2018, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
/*** Dialogs and popovers ***/
.popupmenu {
display: none;
padding: 0;
min-width: 180px;
height: 100%;
li > a {
width: 100%;
}
&.propform {
overflow: hidden;
padding: .25rem; // so overflow do not truncate focus outline on inputs
}
&.simplelist {
min-width: 80px;
}
}
.popup.justified {
display: flex;
justify-content: space-around;
}
.popover-body {
padding: 0;
overflow-x: hidden;
& > .popupmenu {
display: block !important;
}
}
.popover {
box-shadow: 3px 3px 5px @color-popover-shadow;
border-color: @color-layout-border;
padding: 0;
.popover-header {
// On mobile don't display popup arrows and titles
display: none;
}
.listing {
li:first-child {
border-radius: .25rem .25rem 0 0;
}
li:last-child {
border-radius: 0 0 .25rem .25rem;
}
}
}
#rcmKSearchpane {
width: auto;
height: auto;
li {
padding-right: .5rem;
}
html.layout-small &,
html.layout-phone & {
bottom: auto;
border: 1px solid @color-input-border;
}
html.layout-phone & {
max-width: 100% !important;
}
}
html.layout-small,
html.layout-phone {
.popover:not(.select-menu) {
margin: 0 !important;
padding: 0;
right: 0;
left: initial !important;
bottom: 0;
top: 0;
width: @layout-mobile-menu-width;
transform: none !important;
border-radius: 0;
border: 0;
display: flex;
flex-direction: column;
box-shadow: none;
div.arrow {
display: none;
}
.listing li:last-child {
border-bottom: 1px solid @color-list-border;
}
}
.popover.menu {
left: 0 !important;
}
.popover-overlay {
z-index: 1000;
background-color: @color-dialog-overlay-background;
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
.popover-header {
display: block;
border-radius: 0;
border: 0;
padding: 0 .5em;
height: @layout-touch-header-height;
min-height: @layout-touch-header-height; // for when it's a flex item
line-height: @layout-touch-header-height;
font-size: @layout-touch-header-font-size;
color: @color-popover-mobile-header;
background-color: @color-popover-mobile-header-background;
&:before {
display: none; // hide the Bootstrap's popover arrow element
}
a {
display: inline-block;
width: 100%;
}
}
.popover-body > * {
max-height: 100% !important;
}
}
html.touch .popover {
.listing {
li a {
line-height: @layout-touch-menu-record-height;
font-size: @layout-touch-menu-record-font-size;
padding: 0 .5em;
&:before {
float: left; // overwrite icon float to have unified element height
}
}
}
}
.select-menu {
max-width: initial;
margin: 0;
.listing li a {
padding-left: .25rem;
outline: 0; // for Android browser
}
.popover-header {
border-radius: .25rem .25rem 0 0 !important;
}
}
/** PGP Key search/import dialog **/
.pgpkeyimport {
div.key {
position: relative;
padding: .5rem 0;
&.revoked,
&.disabled {
color: @color-list-secondary;
}
label {
display: inline-block;
margin-right: 0.5em;
margin-bottom: 0;
&:after {
content: ":";
}
&.keyid {
display: none;
}
}
label + a,
label + span {
line-height: 2.6rem;
margin-right: 1em;
white-space: nowrap;
text-decoration: none;
}
label.keyid + a {
font-weight: bold;
&:before {
&:extend(.font-icon-class);
content: @fa-var-key;
}
}
}
ul.uids {
margin: 0;
padding: 0;
}
li.uid {
border: 0;
padding: .25rem 0 0 1.5em;
line-height: 1.5rem !important;
list-style-type: none;
&:before {
&:extend(.font-icon-class);
content: @fa-var-user;
opacity: 0.25;
font-size: 1em;
line-height: 1.25;
}
}
button.importkey {
position: absolute;
top: .5rem;
right: 0;
}
button:disabled {
display: none;
}
}
diff --git a/skins/elastic/styles/widgets/editor.less b/skins/elastic/styles/widgets/editor.less
index c4e918217..3bec3b4be 100644
--- a/skins/elastic/styles/widgets/editor.less
+++ b/skins/elastic/styles/widgets/editor.less
@@ -1,942 +1,942 @@
/**
- * Roundcube webmail styles for the Elastic skin
+ * Roundcube Webmail styles for the Elastic skin
*
- * Copyright (c) 2017-2018, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
/*** Text Editor widget (and TinyMCE editor) ***/
.mce-tinymce {
&.mce-container.mce-panel {
border-radius: .25rem;
border-color: @color-input-border;
overflow: hidden;
}
.mce-btn,
.mce-panel {
background-color: @color-input-addon-background;
}
.mce-panel {
border-color: @color-input-border;
}
}
.mce-top-part::before,
.mce-tinymce,
.mce-window {
box-shadow: none !important;
}
.mce-btn {
&.mce-active {
background: @color-btn-secondary-background !important;
}
}
.mce-window {
&.mce-container {
border: 0;
& :not(.mce-ico) {
.font-family;
}
}
.mce-reset {
background: #fff;
}
.mce-container-body {
&.mce-abs-layout {
overflow: unset;
}
.mce-abs-end {
display: none;
}
}
.mce-window-head {
height: @layout-header-height;
border-bottom: 1px solid @color-dialog-header-border;
padding: 0;
.mce-title {
line-height: @layout-header-height;
font-size: 1.25rem;
padding: 0 3rem 0 1rem;
color: @color-dialog-header;
}
.mce-close {
border: 0;
color: @color-dialog-header;
background: transparent;
right: 0;
top: 0;
position: absolute;
height: (@layout-header-height - .7rem);
width: 1.25em;
margin: .25rem;
padding: .1rem .75rem;
cursor: pointer;
&:before {
&:extend(.font-icon-class);
content: @fa-var-times;
margin: 0;
}
i {
display: none;
}
}
}
.mce-foot {
border: 0;
height: @layout-header-height !important;
position: relative;
@media screen and (max-width: @screen-width-xs) {
border-top: 1px solid @color-dialog-header-border;
}
.mce-container-body {
height: 100% !important;
display: flex;
align-items: center;
justify-content: flex-end; // just 'end' does not work in Chrome
.mce-btn {
position: initial;
margin-right: .5rem;
line-height: 1;
width: auto !important;
height: auto !important;
&:last-child {
margin-right: 1rem;
}
.mce-txt {
line-height: 1.5;
vertical-align: unset;
}
button:before {
&:extend(.font-icon-class);
display: inline;
float: none;
vertical-align: middle;
margin-right: .4rem;
}
}
.mce-abs-end {
position: initial;
width: 1rem;
order: 9;
}
}
.mce-btn {
.btn-secondary;
border-radius: .3rem;
border-color: transparent;
&:focus {
border-color: transparent !important;
color: @color-btn-secondary;
background: @color-btn-secondary-background;
}
&.mce-primary {
.btn-primary;
}
button:hover,
button {
color: @color-btn-secondary;
padding: .5rem .75rem;
}
}
.mce-btn:last-child button:before {
content: @fa-var-times;
}
.mce-btn.mce-primary button:before {
content: @fa-var-check;
}
.mce-search-foot {
div:nth-of-type(2) button:before {
content: @fa-var-search;
}
div:nth-of-type(3) button:before,
div:nth-of-type(4) button:before {
content: @fa-var-pencil-alt;
}
div:nth-of-type(6) button:before {
content: @fa-var-chevron-left;
}
div:nth-of-type(7) button:before {
content: @fa-var-chevron-right;
}
div:nth-of-type(7) button:after {
&:extend(.font-icon-class);
display: inline;
float: none;
margin: 0 0 0 .2rem;
content: @fa-var-chevron-right;
}
@media screen and (min-width: (@screen-width-xs + 1px)) {
div:nth-of-type(6) {
margin-left: .5rem;
}
div:nth-of-type(7) button {
&:before {
display: none;
}
}
}
}
}
// Form elements, let's keep'em in .mce-window to make overwriting simpler
.mce-formitem {
min-width: 450px;
position: unset !important;
& > .mce-container-body {
margin-bottom: .5rem;
& > * {
width: 75% !important;
position: unset !important;
}
& > label {
max-width: 25%;
min-width: 25%;
line-height: 2.5 !important;
}
}
.mce-widget {
border-radius: .25rem;
}
}
.mce-form {
padding: 1rem;
box-sizing: border-box;
.mce-form {
padding: 0;
position: unset !important;
width: 100% !important;
& > .mce-container-body {
flex-wrap: wrap;
height: auto !important;
}
.mce-formitem {
min-width: 100%;
width: 100% !important;
}
}
.mce-container {
height: auto !important;
.mce-container-body {
display: flex;
height: auto !important;
& > input:not([size="5"]) {
position: relative;
left: 0 !important;
flex: 1;
}
}
}
& > .mce-container-body {
box-sizing: border-box;
width: 100% !important;
}
.mce-form-split {
.mce-formitem {
min-width: auto;
& > .mce-container-body {
width: 100% !important;
}
}
}
label {
position: unset;
line-height: 2.5 !important;
height: auto !important;
}
}
.mce-colorpicker {
& + .mce-form {
width: 150px !important;
padding: 0;
.mce-formitem {
min-width: unset;
& + :not(.mce-formitem) {
height: 50px !important;
}
}
}
}
.mce-textbox {
padding: .375rem .75rem !important;
line-height: 1.5;
color: @color-font;
&:not(textarea) {
height: auto !important;
}
&:focus {
color: @color-font;
border-color: @color-input-border-focus;
box-shadow: 0 0 0 .2rem @color-input-border-focus-shadow;
}
&[size="5"] {
width: auto !important;
}
&.mce-multiline {
line-height: 1.25;
width: 100% !important;
position: unset;
box-sizing: border-box;
display: block;
}
}
.mce-listbox {
button {
line-height: 1.5;
padding: .375rem .75rem;
}
&:focus {
border-color: @color-input-border-focus !important;
box-shadow: 0 0 0 .2rem @color-input-border-focus-shadow !important;
}
}
.mce-checkbox {
line-height: 2.5;
i.mce-i-checkbox {
border: 0;
width: auto;
color: @color-checkbox;
text-indent: 0;
&:before {
&:extend(.font-icon-class);
margin: 0;
content: @fa-var-toggle-off;
}
}
&.mce-checked i.mce-i-checkbox:before {
content: @fa-var-toggle-on;
}
&:focus {
i.mce-i-checkbox {
border: 0;
}
}
}
.mce-combobox {
display: flex;
input {
border-radius: .3rem 0 0 .3rem;
flex: 1;
&:focus {
z-index: 1;
}
}
&.mce-combobox-fake {
input {
border-radius: .3rem;
}
}
button {
padding: .4rem .6rem;
}
.mce-btn {
border-radius: 0 .3rem .3rem 0;
background: @color-input-addon-background;
&:focus {
background-color: @color-input-border-focus-shadow;
border-color: #c5c5c5;
}
}
}
.mce-tabs {
padding-top: 1rem;
margin: 0 1rem;
border-color: @color-layout-border;
.mce-tab {
border-radius: .25rem .25rem 0 0;
padding: .5rem 1rem;
height: auto !important;
border: 1px solid transparent;
color: @color-link;
background: transparent;
margin-bottom: -1px;
&.mce-active {
border: 1px solid @color-layout-border;
border-bottom-color: #fff;
color: @color-font !important;
}
&:not(.mce-active):hover {
border: 1px solid @color-list-border;
border-bottom-color: transparent;
border-bottom: 0;
}
&:focus {
outline: unset !important;
}
}
}
.mce-label {
text-shadow: none;
color: @color-font;
}
}
// Menus and popovers, e.g. color selector, emoticons selector, font selector
.mce-menu,
.mce-floatpanel.mce-popover {
box-shadow: 3px 3px 5px @color-popover-shadow !important;
border-color: @color-layout-border !important;
border-radius: .3rem;
}
.mce-menu {
.mce-menu-item.mce-active {
color: @color-toolbarmenu-hover;
background-color: @color-toolbarmenu-hover-background;
}
.popover-header {
display: none !important;
}
}
div.mce-menubtn.mce-opened {
z-index: 65530 !important; // BUG: https://github.com/tinymce/tinymce/issues/4542
}
#mce-modal-block.mce-in {
background-color: @color-dialog-overlay-background;
opacity: 1 !important;
}
@media screen and (max-width: @screen-width-xs) {
.mce-window {
width: 100% !important;
height: 100% !important;
left: 0 !important;
top: 0 !important;
border-width: 0 !important;
& > .mce-reset {
display: flex;
flex-direction: column;
height: 100%;
}
.mce-window-body {
flex: 1;
overflow-y: auto !important;
}
& > .mce-reset > div,
.mce-container-body {
width: 100% !important;
}
.mce-window-head {
background-color: @color-layout-mobile-header-background;
.mce-title {
font-size: 1rem;
text-align: center;
padding: 0 1rem;
}
.mce-close {
display: none;
}
}
.mce-foot {
background-color: @color-layout-mobile-footer-background;
.mce-container-body {
justify-content: space-evenly;
.mce-btn {
position: initial;
height: 100% !important;
margin: 0;
background: transparent;
border-width: 0;
&:focus {
box-shadow: none;
}
&:hover {
background: transparent;
}
&:last-child {
margin: 0;
}
button {
color: @color-font;
padding: .45rem;
font-size: .9rem;
&:before {
display: block;
float: none;
width: 100%;
margin: 0;
line-height: 1.75;
height: 1.75rem;
}
}
}
.mce-abs-end {
display: none;
}
}
.mce-search-foot {
div:nth-of-type(7) button:after {
display: none;
}
}
}
.mce-form,
.mce-form + .mce-container, // for Embed tab in Media dialog
.mce-formitem,
.mce-combobox,
.mce-panel:not(.mce-popover) {
width: 100% !important;
}
.mce-formitem {
min-width: unset;
}
.mce-form {
& > .mce-container-body {
display: flex;
flex-direction: column;
left: 0;
right: 0;
box-sizing: border-box;
}
.mce-container-body {
height: auto !important;
flex-direction: column;
& > label {
position: unset !important;
width: 100% !important;
max-width: 100%;
}
& > label + * {
position: unset !important;
width: auto !important;
}
& > .mce-checkbox {
position: absolute;
left: 0 !important;
top: 3rem !important;
}
}
}
// FIXME: How to fix the input width in less hacky way?
.mce-combobox input {
max-width: ~"calc(100% - 4rem)";
}
.mce-combobox-fake input {
max-width: ~"calc(100% - 1.7rem)";
}
}
.mce-menu {
width: @layout-mobile-menu-width !important;
right: 0;
top: 0 !important;
left: auto !important;
height: 100% !important;
max-height: unset !important;
padding: 0 !important;
margin: 0 !important;
border-radius: 0;
border: 0 !important;
.popover-header {
display: block !important;
a {
font-size: 1.2rem;
line-height: @layout-touch-header-height;
&:before {
content: @fa-var-times;
}
}
}
.mce-container-body {
width: 100% !important;
}
.mce-menu-item {
height: @layout-touch-menu-record-height;
line-height: @layout-touch-menu-record-height;
padding: 0 .5rem;
border-left: 0;
border-bottom: 1px solid @color-list-border;
margin: 0;
.mce-ico {
font-size: 150%;
padding: 0 .7rem 0 .25rem;
margin-top: -.2rem;
}
.mce-text {
font-size: 1.2rem;
.font-family;
line-height: @layout-touch-menu-record-height;
color: @color-font;
}
.mce-caret {
display: none;
}
&.mce-menu-item-preview {
&.mce-active {
border-left: none;
position: relative;
&:after {
.font-icon-class; // :extend() does not work here
content: @fa-var-check;
position: absolute;
right: .5rem;
top: 0;
color: @color-font;
}
}
}
&.mce-menu-item-expand {
position: relative;
&:after {
.font-icon-class; // :extend() does not work here
content: @fa-var-angle-right;
position: absolute;
right: .5rem;
top: 0;
color: @color-font;
}
}
}
}
.mce-menu-item-sep,
.mce-menu-shortcut {
display: none !important;
}
.mce-charmap-dialog {
position: unset !important;
+ .mce-container {
display: none;
}
}
.mce-charmap {
display: block;
tbody {
display: block;
}
tr {
display: flex;
flex-wrap: wrap;
}
td {
flex: 1;
height: auto !important;
min-width: 17%;
padding: 0 !important;
border: 0 !important;
border-bottom: 1px solid @color-list-border !important;
div {
font-size: 3rem;
line-height: 2;
}
}
}
}
html.touch .mce-grid td {
padding: .5rem;
}
/*** Media file selector for TinyMCE ***/
.image-selector {
margin: 1rem 1rem 1rem 1rem !important;
padding: 1rem .5rem 10rem .5rem !important;
&.droptarget {
border: .2rem dashed @color-table-border;
&:after {
margin-top: 2rem;
}
}
button {
.btn-secondary;
padding: .5rem .75rem;
line-height: 1.5;
position: relative;
&:before {
line-height: 1;
}
}
form {
position: absolute;
top: 0;
}
.attachmentslist {
margin-left: 0;
overflow-x: hidden;
overflow-y: auto;
height: 19.1em;
li {
padding: .25rem;
cursor: pointer;
&:before {
display: none;
}
&:hover,
&:focus {
background: @color-list-selected-background;
}
span.name {
flex: 1;
margin: auto;
padding-left: 1rem;
.overflow-ellipsis;
}
span.img {
height: 80px;
width: 80px;
display: flex;
border: 1px solid @color-list-border;
background: white;
border-radius: .75rem;
overflow: hidden;
}
img {
margin: auto;
}
}
html.layout-phone & {
height: auto;
}
}
}
/*** HTML editor widget ***/
.html-editor {
position: relative;
display: flex;
margin-bottom: .25rem;
& > .nav {
border-color: transparent;
z-index: 1;
position: absolute;
right: 1rem;
a.active {
border-color: @color-input-border !important;
&.mode-html {
background-color: @color-input-addon-background;
border-bottom-color: @color-input-addon-background !important;
}
&.mode-plain {
border-bottom-color: #fff !important;
}
}
a:hover,
a:focus {
border-bottom-color: transparent;
outline: 0;
}
}
& > iframe, // e.g. mailvelope frame
& > .googie_edit_layer,
& > .mce-tinymce,
& > textarea {
margin-top: 2.55rem;
font-family: monospace;
width: 100% !important;
}
& > iframe { // e.g. mailvelope frame
border-radius: .3rem;
border: 1px solid @color-input-border;
min-height: 30em;
}
#composebody_ifr {
min-height: 30em;
}
& > .mce-tinymce.focused {
border-color: @color-input-border-focus;
box-shadow: 0 0 0 .2rem @color-input-border-focus-shadow !important;
}
}
/*** GoogieSpell widget ***/
.googie_window {
width: 16rem;
}
.googie_edit_layer {
font-family: monospace;
// from Bootstrap's textarea
padding: .5rem .75rem;
border: 1px solid @color-input-border;
border-radius: .3rem;
line-height: 1.25;
}
.googie_link {
color: @color-spellcheck-link;
text-decoration: underline;
cursor: pointer;
}
.googie_list {
td {
min-width: 8rem;
width: auto;
&.googie_list_onhover {
color: @color-toolbarmenu-hover;
background-color: @color-toolbarmenu-hover-background;
}
.googie_list_revert:before {
&:extend(.font-icon-class);
content: @fa-var-plus-square;
}
.googie_add_to_dict:before {
&:extend(.font-icon-class);
content: @fa-var-plus-square;
}
}
input {
display: inline-block;
margin: .5rem .5rem .5rem 0 !important;
padding: .5rem .75rem !important;
}
}
diff --git a/skins/elastic/styles/widgets/forms.less b/skins/elastic/styles/widgets/forms.less
index c379be75c..73b8b5f74 100644
--- a/skins/elastic/styles/widgets/forms.less
+++ b/skins/elastic/styles/widgets/forms.less
@@ -1,1365 +1,1365 @@
/**
- * Roundcube webmail styles for the Elastic skin
+ * Roundcube Webmail styles for the Elastic skin
*
- * Copyright (c) 2017-2018, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
/*** Common form elements ***/
#uploadform {
display: none;
}
form.smart-upload,
input.smart-upload {
visibility: hidden;
width: 1px;
height: 1px;
opacity: 0;
}
.propform {
// TODO: do we need this?
&:not(.popupmenu) {
width: 100%;
}
// This is the way we can have multiple checkboxes in a single form field
.form-check td:not(.title) > label {
display: block;
margin: 0;
line-height: 2rem;
label {
margin-right: .5rem;
}
}
td.datetime {
display: flex;
input:first-child {
margin-right: .5rem;
}
input:last-child {
width: 75%;
}
}
td.rowbuttons {
width: 1%;
white-space: nowrap;
vertical-align: top;
span {
display: none;
}
a {
padding: 0;
line-height: 2.3rem;
height: 2.3rem;
font-size: 1rem;
&:before {
&:extend(.font-icon-class);
content: @fa-var-plus;
vertical-align: unset;
@media screen and (min-width: (@screen-width-bs-phone + 1px)) {
margin: 0 !important;
}
}
&.delete:before {
content: @fa-var-trash-alt;
}
&.advanced:before {
content: @fa-var-cog;
}
&:not(:last-child) {
margin-right: .25rem;
}
}
}
td.rowactions {
width: 1%;
vertical-align: top;
.form-control {
width: auto;
}
}
td.rowtargets {
vertical-align: top;
.composite {
input, textarea, select, .multi-input, .input-group {
margin-bottom: .5rem;
}
.input-group {
input, textarea, select, .multi-input {
margin-bottom: 0;
}
}
br {
display: block;
}
}
.input-group {
margin-bottom: .25rem;
*:first-child.input-group-prepend {
text-align: left;
min-width: 7.5em;
& > * {
width: 100%;
}
}
}
& > .advanced {
margin-top: .25rem;
}
}
td.title {
padding-top: 0;
padding-bottom: 0;
}
td > .flexbox {
display: flex;
& > .multi-input {
width: 100%;
margin-left: .25rem;
}
}
.rulerow {
margin-bottom: .5rem;
}
&.groupped {
&.readonly {
legend {
margin: 0;
}
.row.input-group {
margin-bottom: 0 !important;
}
label {
min-width: 7rem;
// Overwrite Bootstrap .input-group-* style to make the label transparent
background-color: transparent;
border: 0;
border-radius: 0;
}
}
.row.input-group {
margin-bottom: .5rem;
flex-wrap: nowrap;
& > *:first-child {
.overflow-ellipsis;
min-width: 8rem;
&:not(select) {
padding: 0;
}
@media screen and (max-width: @screen-width-xs) {
min-width: 6rem;
width: 6rem;
flex-grow: unset;
}
label {
width: 100%;
}
}
& > *:nth-child(2) {
flex-grow: 30;
}
&:last-child {
margin-bottom: 1rem;
}
select {
text-align: left;
}
&.composite select {
height: auto;
}
.content {
padding: 0;
display: flex;
flex-wrap: wrap;
border-radius: 0;
input {
border-radius: 0;
border-color: transparent;
}
.ff_street {
width: 100%;
}
.ff_locality {
width: 75%;
}
.ff_zipcode {
width: 25%;
}
.ff_country, .ff_region {
width: 50%;
}
}
}
.form-control-plaintext {
flex-grow: 1;
border: 0;
}
}
.addfield {
margin: 0;
select {
width: 8rem;
margin-top: .5rem;
}
}
.form-text {
font-size: 90%;
color: @color-form-hint;
}
// Some dialogs may use simple one-row forms like this
&.row.form-group {
margin-left: 0;
margin-right: 0;
label, div {
padding-left: 0;
padding-right: 0;
}
}
// Some forms may use multiple elements that are not part of .input-group
// add proper spacing
select + select,
select + .input-group {
padding-top: .5rem;
}
&.text-only {
margin-bottom: .25rem;
tr {
margin: 0;
}
label {
padding-bottom: 0 !important;
}
@media screen and (max-width: @screen-width-bs-phone) {
tr {
display: table-row;
}
td {
width: auto;
&:first-child {
width: 33%;
}
}
:not(tr).form-group.row {
.col-form-label {
width: 33%;
}
& > :last-child {
width: 67%;
}
}
}
}
}
@media screen and (max-width: @screen-width-bs-phone) {
.propform {
table.compact-table {
.rowactions > select,
.flexbox > select {
width: 100%;
}
tr {
display: flex;
flex-direction: column;
td {
width: 100%;
padding: .25rem 0 0 0;
&.rowbuttons {
text-align: right;
a {
margin-right: .5rem;
& > span {
display: inline;
}
}
}
}
}
}
}
}
.propform,
.formcontent {
fieldset:not(.tab-pane):nth-of-type(n+2) {
margin-top: 1em;
}
legend {
font-weight: bold;
font-size: 1.2em;
}
label {
-webkit-hyphens: auto;
-ms-hyphens: auto;
hyphens: auto;
overflow: hidden;
margin-bottom: 0;
}
}
fieldset.advanced {
> legend {
width: auto;
cursor: pointer;
&:after {
&:extend(.font-icon-class);
float: right;
margin: 0 0 0 .25rem;
line-height: inherit;
font-size: inherit;
content: @fa-var-angle-up;
}
&.closed:after {
content: @fa-var-angle-down;
}
}
}
@media screen and (max-width: @screen-width-bs-phone) {
.formcontent .text-only {
.form-group:not(tr) {
margin-bottom: .25rem;
.col-form-label {
width: 33%;
& + span {
width: 67%;
}
}
}
}
html.iframe .formcontent > .propform {
padding: .25rem;
}
}
// Forms fixes for IE and Edge
html.ms .propform {
.row:not(.form-check) > td {
display: flex;
flex-wrap: wrap; // puts "hint" element below the input
}
@media screen and (min-width: @screen-width-bs-phone) {
.row.form-check > td {
display: flex;
}
}
}
.formcontainer {
display: flex;
flex-direction: column;
justify-content:flex-start;
overflow-y: hidden !important;
.formcontent {
overflow-x: hidden;
overflow-y: auto;
}
.formbuttons {
padding: 0.5rem 1rem;
button {
margin-right: .5rem;
}
}
// Prevent button truncation on tablets
html.iframe.ipad &,
html.iframe.webkit.tablet & {
.formbuttons {
min-height: 4rem;
}
}
// We don't need buttons element on small devices
html.layout-small &,
html.layout-phone & {
.formbuttons {
display: none;
}
}
}
.formcontent {
&:not(.popupmenu) {
padding: 1rem;
}
.row {
margin-right: 0; // without these the form is too wide causing horizontal scrollbar appearence
margin-left: 0;
// Note: We never use odd numbers here
.col, .col-2, .col-4, .col-6, .col-8, .col-10, .col-12,
.col-sm, .col-sm-2, .col-sm-4, .col-sm-6, .col-sm-8, .col-sm-10, .col-sm-12 {
// overwrite Bootstrap's redundant padding
padding: 0;
}
.form-control-plaintext,
label.col-form-label {
padding: .375rem .375rem .375rem 0;
}
.form-control-plaintext {
padding-bottom: 0;
border: 0;
}
@media screen and (max-width: @screen-width-bs-phone) {
&.form-group {
& > td label {
padding-bottom: 0;
}
}
}
}
.row.form-check {
padding: 0; // without these e.g. inputs in compose screen are not aligned properly
display: flex; // https://github.com/twbs/bootstrap/issues/22348
flex-wrap: nowrap;
// alignment fixes needed because we do not stick precissely to Bootstrap's form structure
@media screen and (max-width: @screen-width-bs-phone) {
.col-6 {
max-width: 100%;
flex: auto;
}
& > *:last-child {
width: 1%;
min-width: 2.6rem; // for .custom-switch
}
&.with-link > *:last-child {
min-width: 8rem;
}
}
.form-check-input {
margin: .5rem 0; // fixes checkbox alignment with other inputs in a form
}
.custom-switch + a {
line-height: 2;
vertical-align: bottom;
}
td > label {
padding-bottom: 0;
}
}
.nav-tabs {
margin-bottom: 1rem;
&:empty {
display: none;
}
}
.hint {
font-style: italic;
color: @color-form-hint;
}
// RAW (CodeMirror) editor
&.raweditor {
height: 100%;
form {
height: 100%;
}
textarea {
font-family: monospace;
height: 100%;
}
.CodeMirror {
border: 1px solid @color-input-border;
border-radius: .3rem;
height: 100%;
color: @color-font;
}
.CodeMirror-focused {
border-color: @color-input-border-focus;
box-shadow: 0 0 0 .2rem @color-input-border-focus-shadow;
}
.CodeMirror-linebackground.line-error {
background-color: @color-error;
opacity: 0.4;
}
.errorGutter {
width: .8em;
}
}
}
@media screen and (max-width: @screen-width-mini) {
.formcontent {
.col-form-label {
flex: auto;
max-width: 100%;
}
.col-6, .col-8, .col-10 {
flex: auto;
max-width: 100%;
}
}
}
/* Some common icons for "iconized inputs" */
.input-group .icon {
text-decoration: none;
padding: .375rem .5rem;
&.input-group-text {
min-width: 2.4rem;
}
&:before {
&:extend(.font-icon-class);
margin: 0 !important;
line-height: 1;
font-size: 1.1em;
}
&.user:before {
content: @fa-var-user;
}
&.pass:before {
content: @fa-var-lock;
}
&.host:before {
content: @fa-var-home;
}
&.language.before {
content: @fa-var-globe;
}
&.cancel:before {
content: @fa-var-times;
}
&.delete:before {
content: @fa-var-trash-alt;
}
&.edit:before {
content: @fa-var-pencil-alt;
}
&.add:before {
content: @fa-var-plus;
}
&.add.recipient:before {
content: @fa-var-users;
}
&.search:before {
content: @fa-var-search;
}
&.filter:before {
content: @fa-var-filter;
}
&.key:before {
content: @fa-var-key;
}
.inner {
display: none;
}
}
.input-group a {
&:focus {
background-color: @color-input-border-focus-shadow;
outline: 0;
}
}
@proplist-record-height: 2rem;
.proplist {
margin-bottom: 0;
padding: 0;
li {
list-style-type: none;
line-height: @proplist-record-height;
margin-bottom: .25rem;
display: flex;
align-items: center;
&:last-child {
margin-bottom: 0;
}
input[type=radio] {
margin-right: .5em;
&:disabled + label {
opacity: .5;
}
}
label:not(.input-group-text) {
margin: 0;
line-height: @proplist-record-height;
}
select {
width: auto;
display: inline;
}
}
}
.checklist {
& > div {
line-height: 2rem;
display: block; // overwrite .custom-switch
}
.custom-control-label {
&:before,
&:after {
margin: -.15rem 0 0 0;
}
}
& > div + br {
display: none;
}
}
/*** Forms in popups ***/
.popup form.propform {
padding: .5rem;
overflow-x: hidden;
}
.popupmenu.form {
&.nolist {
padding: 0 .5rem;
}
ul {
list-style-type: none;
padding: 0;
margin: 0;
}
li:not(.separator) {
padding: 0 1rem;
label {
margin: 0;
line-height: @listing-line-height;
input {
margin-right: .5rem;
}
}
}
input {
vertical-align: middle;
}
select {
margin: .5rem 0;
}
.buttons {
text-align: center;
padding: .5rem;
}
}
/*** Smart list (multiple input) field ***/
.multi-input {
& > .content {
max-height: 11.65em;
overflow: hidden;
overflow-y: auto;
border-radius: .25rem;
border: 1px solid @color-input-border;
&.focused {
.style-input-focus;
}
// TODO: style button focus
}
a.icon {
&.reset:before {
&:extend(.font-icon-class);
content: @fa-var-trash-alt;
}
}
input.form-control {
padding-left: .75rem;
height: auto; // fixes input height
}
input,
input:focus,
.input-group-text {
border-radius: 0;
border: 0;
border-bottom: 1px solid @color-input-border;
box-shadow: none;
}
.input-group-text {
border-left: 1px solid @color-input-border;
}
.input-group-append {
margin-left: 0;
}
.input-group {
margin: 0 !important;
flex-wrap: nowrap; // Bootstrap makes them wrappable (imho)
&:last-child * {
border-bottom: 0;
}
}
& + .btn {
margin-top: .5rem;
}
&.is-invalid {
& > .content {
border: 1px solid @color-input-border-invalid;
&.focused {
border-color: @color-input-border-invalid;
box-shadow: 0 0 0 .2rem @color-input-border-invalid-shadow;
}
}
& > .invalid-feedback {
display: inline-block;
}
}
}
/*** Files upload widget with list of files, upload form and drop area ***/
.file-upload {
padding: 1rem 1rem 12rem;
margin: 0 1rem .25rem 1rem;
border-radius: .5rem;
border: .2rem dashed @color-table-border;
fieldset & {
margin: 0;
}
.upload-form {
text-align: center;
padding-bottom: 1em;
a.btn,
button {
margin-bottom: .25rem;
}
}
.hint {
margin-bottom: .5rem;
color: @color-form-hint;
text-align: center;
}
.attachmentslist {
li {
position: relative;
display: flex;
padding-right: 1.5em;
a.filename {
flex: 1;
}
a.delete,
a.cancelupload {
position: absolute;
right: 0;
width: auto; // fix button width if the widget is in a .popupmenu
&:before {
margin: 0;
}
}
.inner {
display: none;
}
}
}
&.droptarget {
padding-bottom: .5rem !important;
&:after {
content: @icon-file-drop;
width: 10rem;
margin: 5rem auto 0 auto;
display: block;
}
&.active {
border-color: darken(@color-toolbar-button-background-hover, 20%);
}
&.hover {
border-color: darken(@color-toolbar-button-background-hover, 20%);
background-color: @color-toolbar-button-background-hover;
}
}
}
/*** Smart recipient input field ***/
@recipient-input-margin-fix: .25rem;
.recipient-input {
display: flex;
flex-wrap: wrap;
padding: 0 .75rem @recipient-input-margin-fix .75rem;
list-style-type: none;
cursor: text;
height: auto; // reset .form-control height
&.focus {
.style-input-focus;
}
.recipient {
display: flex;
position: relative;
max-width: 50%;
border: 1px solid @color-recipient-input-border;
background-color: @color-recipient-input-background;
border-radius: .25em;
padding: 0 .25em;
margin-top: @recipient-input-margin-fix;
margin-right: .2em;
white-space: nowrap;
cursor: default;
@media screen and (max-width: 450px) {
width: 100%;
max-width: 100%;
}
}
.name {
.overflow-ellipsis;
flex-grow: 1;
display: inline-block;
line-height: 1.1;
padding: .25rem;
vertical-align: middle;
}
.email {
text-indent: -5000rem;
display: inline-block;
width: 0;
}
.quotes {
position: absolute;
width: 0;
opacity: 0;
}
a.button.icon {
font-size: .8em;
cursor: pointer;
padding: 0 0 0 .25em;
&:before {
float: none;
display: inline-block;
width: 1em;
margin: 0;
line-height: 1.5;
}
}
li {
max-width: 100%;
}
input {
width: 40px;
max-width: 100%;
background: transparent !important;
border: 0 !important;
margin-top: @recipient-input-margin-fix;
outline: 0;
line-height: 1.5;
&::-ms-clear {
display: none; // removes clear icon in IE11
}
}
}
/*** Tagedit widget (from jqueryui plugin) ***/
.tagedit-list {
display: flex;
flex-wrap: wrap;
padding: 0 .75rem @recipient-input-margin-fix .75rem;
margin: 0;
list-style-type: none;
min-height: 2.3rem;
& + .placeholder {
display: none;
}
&[tabindex="-1"] {
.style-input-focus;
}
li.tagedit-listelement-new {
margin-top: @recipient-input-margin-fix;
input {
width: 15px;
background: transparent !important;
border: 0;
outline: 0;
margin: 0;
padding: 0;
line-height: 1.5;
&.tagedit-input-disabled {
visibility: hidden;
}
}
}
li.tagedit-listelement-old {
max-width: 50%;
border: 1px solid @color-recipient-input-border;
background-color: @color-recipient-input-background;
border-radius: .25em;
margin-top: @recipient-input-margin-fix;
margin-right: .2em;
white-space: nowrap;
a /* TODO: .tagedit-close, .tagedit-break, .tagedit-delete, .tagedit-save */ {
font-size: .8em;
cursor: pointer;
display: inline-block;
width: 1.1em;
overflow: hidden;
vertical-align: middle;
margin-right: .2rem;
&:before {
&:extend(.font-icon-class);
content: @fa-var-times;
width: 1em;
line-height: 1.2;
}
}
span {
.overflow-ellipsis;
flex-grow: 1;
display: inline-block;
line-height: 1.4;
padding: 0 .25rem;
vertical-align: middle;
}
}
li.tagedit-listelement-focus {
// TODO
}
}
/*** Skin selection widget ***/
.skinselection {
white-space: nowrap;
display: table-row;
& > span {
display: table-cell;
vertical-align: middle;
padding: .1em .5em;
white-space: normal;
&:last-child {
padding-right: 0;
}
}
.skinitem input {
width: auto;
}
.skinname {
font-weight: bold;
}
.skinlicense,
.skinlicense a {
font-style: italic;
text-decoration: none;
}
.skinlicense a:hover {
text-decoration: underline;
}
.skinlicense,
.skinauthor {
font-size: 90%;
}
.skinthumbnail {
width: 64px;
height: 64px;
border: 1px solid @color-input-border;
background: #fff;
border-radius: 4px;
}
}
/*** Percent input with jQuery-UI slider ***/
// Structure: <input><span.label><div.ui-slider>
.input-percent-slider {
display: flex;
align-items: center;
input {
max-width: 4em;
}
span.label {
line-height: 2.4;
padding: 0 .5rem 0 .25rem;
}
div.ui-slider {
flex: 1;
margin: 0 .5em;
}
}
/*** Image upload widget ***/
.image-upload {
position: relative;
overflow: hidden;
cursor: pointer;
background-color: @color-image-upload-background;
a.button {
display: none;
position: absolute;
left: 0;
top: 0;
background-color: rgba(255, 255, 255, .85);
border-radius: 5px;
width: 2.5em;
padding: .5em;
margin: .5em;
line-height: 1;
}
&.changed a.button {
display: inline;
}
}
.input-group-combo {
select:first-of-type {
&.alone {
border-radius: .25rem !important;
}
&:not(.alone) {
flex: unset;
width: auto;
}
}
.input-group {
padding: 0 !important;
flex: 2;
}
select + select,
.input-group :first-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
/*** General browser hacks ***/
// Remove outline on selects in Firefox
@-moz-document url-prefix() {
select:-moz-focusring {
color: transparent !important;
text-shadow: 0 0 0 @color-font !important;
}
}
/*** Bootstrap style overrides and improvements ***/
.form-control {
color: @color-font;
&:focus {
color: @color-font;
border-color: @color-input-border-focus;
box-shadow: 0 0 0 .2rem @color-input-border-focus-shadow;
}
// FIXME: This fixes style of required inputs in Firefox/Edge, but
// makes inputs on login form red-bordered, commented for now
// &:invalid,
&.is-invalid {
border-color: @color-input-border-invalid;
box-shadow: none;
&:focus {
border-color: @color-input-border-invalid;
box-shadow: 0 0 0 .2rem @color-input-border-invalid-shadow;
}
}
&::placeholder {
color: @color-input-placeholder;
}
}
.invalid-feedback {
color: @color-error;
font-size: 90%;
}
.form-group {
margin-bottom: .5rem;
}
.input-group-text {
color: @color-input;
background-color: @color-input-addon-background;
input:focus {
z-index: 1;
border-color: @color-input-border-focus !important;
}
}
.custom-switch {
padding-left: 0;
display: inline-block;
.custom-control-input {
left: 0;
&:focus ~ .custom-control-label:before {
box-shadow: 0 0 0 .2rem @color-checkbox-focus-shadow;
}
&:focus:not(:checked) ~ .custom-control-label::before {
border-color: @color-checkbox-focus;
}
&:checked ~ .custom-control-label::before {
border-color: @color-checkbox;
background-color: @color-checkbox;
}
&:checked:disabled ~ .custom-control-label::before {
border-color: @color-checkbox-checked-disabled;
background-color: @color-checkbox-checked-disabled;
}
}
// Make switches bigger, we use smaller font than Bootstrap's default
.custom-control-label {
padding-left: 2.5rem;
min-height: 2rem;
line-height: 2;
display: inline-block;
html.touch & {
padding-left: 3rem;
}
&:before,
&:after {
border-radius: .6rem;
margin: .15rem 0;
cursor: pointer;
html.touch & {
border-radius: .8rem;
margin: 0;
}
}
&:before {
left: 2px;
width: 2rem;
height: 1.2rem;
html.touch & {
top: .2rem;
width: 2.6rem;
height: 1.6rem;
}
}
&:after {
left: 4px;
width: ~"calc(1.2rem - 4px)";
height: ~"calc(1.2rem - 4px)";
html.touch & {
top: ~"calc(.2rem + 2px)";
height: ~"calc(1.6rem - 4px)";
width: ~"calc(1.6rem - 4px)";
}
}
}
.custom-control-input:checked ~ .custom-control-label::after {
transform: translateX(.8rem);
html.touch & {
transform: translateX(1rem);
}
}
}
.custom-file {
display: block;
.custom-file-label {
white-space: nowrap;
.overflow-ellipsis;
padding-right: 100px;
line-height: 1.5 !important;
}
& + .hint {
margin-top: .25rem;
}
}
.custom-file-input:focus ~ .custom-file-label {
border-color: @color-input-border-focus;
box-shadow: 0 0 0 .2rem @color-input-border-focus-shadow;
}
diff --git a/skins/elastic/styles/widgets/jqueryui.less b/skins/elastic/styles/widgets/jqueryui.less
index ac8d4b2ba..d1426f845 100644
--- a/skins/elastic/styles/widgets/jqueryui.less
+++ b/skins/elastic/styles/widgets/jqueryui.less
@@ -1,435 +1,435 @@
/**
- * Roundcube webmail styles for the Elastic skin
+ * Roundcube Webmail styles for the Elastic skin
*
- * Copyright (c) 2017-2018, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
/*** jQuery-UI widgets' style overrides ***/
.ui-widget-overlay {
background-color: @color-dialog-overlay-background;
opacity: 1 !important; // override jQuery-UI opacity, the color above is semi-transparent
&.datepicker {
z-index: 119; // above Bootstrap's form controls, below datepicker
}
}
.ui-widget {
border: 1px solid @color-datepicker-border;
box-shadow: 3px 3px 5px @color-popover-shadow;
border-radius: .3rem;
}
.ui-menu {
overflow-y: auto;
overflow-x: hidden;
max-height: 400px;
border-radius: .3rem;
z-index: 240;
position: absolute;
.ui-state-active {
border: 0 !important;
background-color: @color-toolbarmenu-hover-background !important;
}
.ui-menu-item {
white-space: nowrap;
cursor: default;
}
.ui-menu-item-wrapper {
margin: 0 !important;
}
}
.ui-dialog {
border-radius: 0;
box-shadow: none;
&.no-titlebar {
.ui-dialog-titlebar {
display: none;
}
}
.ui-dialog-titlebar {
height: @layout-header-height;
border-bottom: 1px solid @color-dialog-header-border;
button {
&:before {
margin: 0;
}
}
}
.ui-dialog-title {
line-height: @layout-header-height;
font-size: 1.25rem;
padding: 0 3rem 0 1rem;
color: @color-dialog-header;
}
.ui-dialog-titlebar-close {
border: 0;
color: @color-dialog-header;
background: transparent;
right: 0;
top: 0;
position: absolute;
padding: .25rem .5rem;
margin: ((@layout-header-height - 2rem) / 2) .5rem;
&:before {
&:extend(.font-icon-class);
content: @fa-var-times;
line-height: 1.5rem;
margin: 0 !important;
}
}
.ui-dialog-content {
// fixes resize issue e.g. in qr-code dialog
box-sizing: initial;
& > .popupmenu {
display: block !important;
}
}
.ui-dialog-buttonpane {
.ui-dialog-buttonset {
display: flex;
justify-content: flex-end;
a.btn-link,
button {
.overflow-ellipsis;
min-width: 5rem;
margin: .65rem .3rem;
&:last-child {
margin-right: 0;
}
}
a.btn-link {
padding-right: 0;
padding-left: 0;
text-decoration: none;
color: @color-font;
&:focus {
background-color: fade(@color-btn-primary-background, 50%);
}
&.options {
order: -1;
padding: .375rem .25rem;
margin-right: .3rem;
&:before {
// this icon is for mobile version
&:extend(.font-icon-class);
content: @fa-var-cog;
width: 100%;
height: 1.25em;
}
}
}
}
}
iframe,
.ui-dialog-content.iframe {
padding: 0;
width: 100% !important;
height: 100%;
border: 0;
overflow: hidden;
}
}
// Overwriting this icon generally prevents from loading bigger images sprite from jQuery-UI
.ui-widget-content .ui-icon.ui-resizable-se {
background: @icon-resize-corner;
background-size: cover;
}
/* FIXME: why do I need !important here? */
@media screen and (max-width: @screen-width-xs) {
.ui-dialog {
width: 100% !important;
height: 100% !important;
display: flex;
flex-direction: column;
border: 0;
.ui-resizable-handle,
.ui-dialog-titlebar-close {
display: none !important;
}
.ui-dialog-titlebar {
height: @layout-touch-header-height;
text-align: center;
background-color: @color-layout-mobile-header-background;
}
.ui-dialog-title {
line-height: @layout-touch-header-height;
font-size: @layout-header-font-size;
padding: 0 1rem;
}
.ui-dialog-content {
flex: 1;
&:not(.iframe) {
padding: 1rem;
}
}
.ui-dialog-buttonpane {
padding: 0 !important;
text-align: center !important;
border-top: 1px solid @color-dialog-header-border;
height: @layout-header-height !important;
background-color: @color-layout-mobile-footer-background;
.ui-dialog-buttonset {
justify-content: space-around;
button {
margin: 0 !important;
padding: .45rem;
border: 0 !important;
height: @layout-header-height;
box-shadow: none;
font-size: 90%;
line-height: 1.5;
&:before {
display: block !important;
float: none;
width: auto;
height: 1.75rem;
line-height: 1.75;
margin: 0 !important;
}
&:active {
box-shadow: none;
}
&.btn-primary,
&.btn-secondary {
color: @color-toolbar-button;
background: transparent;
}
&.btn-danger {
color: @color-btn-danger-background;
background: transparent;
}
&.disabled,
&:disabled {
opacity: .5;
}
&.cancel {
order: 100; // makes Cancel/close button the last one
}
}
a.btn-link {
color: @color-toolbar-button;
margin: 0;
padding: .45rem;
font-size: 90%;
&.options:before {
display: block !important;
height: 1.75rem;
line-height: 1.75;
margin: 0;
}
}
}
}
}
}
/* Slider widget */
.ui-slider {
box-shadow: none;
.ui-slider-range {
border-radius: .3rem;
background: lighten(@color-main, 30%);
}
.ui-slider-handle {
border-radius: .3rem;
&.ui-state-active {
background: @color-main;
border-color: @color-main-dark;
}
}
}
/* Datepicker widget */
.ui-datepicker {
// Always display datepicker centered, overwriting widgets position
margin: ~"calc(50vh - 10em) calc(50vw - 10em) !important";
top: 0 !important;
left: 0 !important;
box-shadow: none;
user-select: none;
&:not(.ui-datepicker-inline) {
z-index: 120 !important; // fixes datepicker over input-group and dialogs
}
.ui-datepicker-header,
.ui-datepicker-title {
line-height: 4rem;
height: 4rem;
padding: 0;
}
.ui-datepicker-header {
border-bottom: 1px solid @color-dialog-header-border;
a {
height: 4rem;
}
select {
display: inline-block;
}
}
.ui-icon {
background-image: none !important;
background-position: none !important;
}
.ui-datepicker-prev,
.ui-datepicker-next {
cursor: pointer;
width: auto;
&:before {
&:extend(.font-icon-class);
content: "\f053";
margin: 0 .25em;
height: auto;
width: 1em;
}
}
.ui-datepicker-prev:before {
content: "\f053";
}
.ui-datepicker-next:before {
content: "\f054";
}
td a {
padding: 0;
line-height: 1.8em;
border-radius: .3rem;
}
.ui-state-default,
&.ui-widget-content .ui-state-default {
border: 0;
background: transparent;
color: @color-datepicker-font;
}
.ui-datepicker-days-cell-over a,
.ui-datepicker-days-cell-over a.ui-state-default,
.ui-state-highlight,
&.ui-widget-content .ui-state-highlight {
background: @color-datepicker-highlight-background;
color: @color-datepicker-highlight;
}
a.ui-state-active {
background: @color-datepicker-active-background !important;
color: @color-datepicker-active !important;
font-weight: bold;
}
html.touch {
& {
td a {
font-size: 1.2em;
line-height: 2.2em;
}
}
}
}
// Fixes datepicker z-index issue on input-group inputs in dialogs
// With non-relative position the input's z-index is ignored
.input-group > .form-control.hasDatepicker {
position: initial;
}
.minicolors-panel {
border: 1px solid @color-datepicker-border;
box-shadow: 3px 3px 5px @color-popover-shadow;
border-radius: .3rem;
height: 152px;
padding: 1px;
}
.input-group {
.minicolors-input {
width: 100%;
// needed so minicolors panel is not out of screen
// when the input is on the right side, e.g. Calendar plugin settings
// This is obviously minicolors script issue
min-width: 130px;
border-left: 0;
border-right: 0;
}
}
@media screen and (max-width: @screen-width-mini) {
.ui-widget-content {
border-radius: 0;
}
.ui-menu {
border-radius: .3rem;
left: 15px !important;
right: 15px;
width: auto;
}
.ui-dialog {
.ui-dialog-content:not(.iframe) {
padding: .65rem;
}
}
.ui-autocomplete {
// TODO: e.g. time input autocompletion on mobile
}
}
diff --git a/skins/elastic/styles/widgets/lists.less b/skins/elastic/styles/widgets/lists.less
index 608dec252..3dcf2f696 100644
--- a/skins/elastic/styles/widgets/lists.less
+++ b/skins/elastic/styles/widgets/lists.less
@@ -1,996 +1,996 @@
/**
- * Roundcube webmail styles for the Elastic skin
+ * Roundcube Webmail styles for the Elastic skin
*
- * Copyright (c) 2017-2018, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
/*** List and treelist widgets ***/
.listing {
tbody td,
li {
border-bottom: 1px solid @color-list-border;
cursor: default;
font-weight: normal;
line-height: @listing-line-height;
}
tbody td,
li a {
padding: 0 .5rem;
white-space: nowrap;
vertical-align: middle;
color: @color-list;
}
tbody td {
.overflow-ellipsis;
outline: none;
a {
color: @color-list;
}
}
li a {
display: block;
text-decoration: none;
cursor: default;
width: 100%;
}
li.selected,
tr.selected td {
color: @color-list-selected;
background-color: @color-list-selected-background;
}
td.selection {
padding: 0 0 0 .5em;
width: 2em;
text-align: center;
& > input {
vertical-align: middle;
}
}
&:not(.withselection) td.selection {
display: none;
}
td.name {
.overflow-ellipsis;
}
td.action {
padding: 0 .5em;
width: 2em;
text-align: center;
&:empty {
width: 0;
}
a {
display: block;
overflow: hidden;
text-decoration: none;
&:before {
&:extend(.font-icon-class);
margin: 0;
font-size: 1rem;
}
}
a.pushgroup:before {
content: @fa-var-chevron-right;
}
}
li.droptarget > a,
tr.droptarget > td {
background-color: @color-list-droptarget-background;
}
li.disabled,
tr.disabled td {
color: @color-list-deleted;
}
li > a.virtual,
li.virtual > a {
opacity: .4;
}
span.secondary {
color: @color-list-secondary;
}
}
// Focus indicator
html:not(.touch) {
.listing {
li > a,
tbody tr > td:first-child,
&:not(.withselection) tbody tr > td.selection + td {
border-left: 2px solid transparent;
}
li > a:focus,
&.focus tbody tr.focused > td:first-child,
&.focus:not(.withselection) tbody tr.focused > td.selection + td {
border-left: 2px solid @color-list-focus-indicator;
outline: 0;
}
}
}
table.listing {
width: 100%;
table-layout: fixed;
// border-spacing/border-collapse here fix problem with our focus indicator
// when the table cells use overflow: hidden. I.e. we use border-spacing:0
// instead of Bootstrap's border-collapse:collapse. Is this cross-browser?
border-spacing: 0;
border-collapse: unset;
}
ul.listing {
margin: 0;
padding: 0;
& > ul {
padding: 0;
}
li {
.overflow-ellipsis;
white-space: nowrap;
position: relative;
list-style: none;
ul {
border-top: 1px solid @color-list-border;
padding-left: 1.5em;
li:last-child {
border-bottom: none;
}
}
.custom-switch {
position: absolute;
padding: 0;
top: 0;
right: 0;
height: @listing-line-height;
vertical-align: middle;
.custom-control-label {
&:before,
&:after {
margin-top: .4rem;
html.touch & {
margin-top: .75rem;
}
}
}
}
}
&.simplelist {
li {
padding: 0 .5rem;
}
}
}
.listing-info {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
text-align: center;
font-weight: bold;
color: @color-list-secondary;
}
html.touch {
.listing:not(.toolbar) li,
.listing tbody td {
line-height: @listing-touch-line-height;
font-size: 1.2rem;
}
li input[type=checkbox] {
height: @listing-touch-line-height;
}
td.selection {
padding: 0;
width: 3em;
}
}
@media screen and (max-width: @screen-width-large) {
.listing.selection-large-only {
li.selected {
color: @color-list;
background-color: transparent;
}
}
}
/* icons */
.listing.iconized li {
a:before {
&:extend(.font-icon-class);
height: 2em; // TODO: ?
margin-right: .5rem;
}
&.preferences > a:before {
content: @fa-var-sliders-h;
}
&.folders > a:before {
content: @fa-var-folder;
}
&.responses > a:before {
content: @fa-var-comment;
}
&.identities > a:before {
content: @fa-var-id-card;
}
&.password > a:before {
content: @fa-var-lock;
}
&.addressbook a:before {
.font-icon-regular(@fa-var-address-book);
}
&.contactgroup a:before {
.font-icon-solid(@fa-var-users);
}
&.contactsearch a:before {
content: @fa-var-search;
}
&.filter > a:before {
content: @fa-var-filter;
}
&.vacation > a:before {
.font-icon-regular(@fa-var-clock);
}
&.forward > a:before {
content: @fa-var-share-square;
}
&.enigma.keys > a:before {
content: @fa-var-key;
}
&.userinfo > a:before {
content: @fa-var-info-circle;
}
&.twofactorauth > a:before {
content: @fa-var-sign-in-alt;
}
a.help:before {
content: @fa-var-life-ring;
}
a.about:before {
.font-icon-regular(@fa-var-question-circle);
}
a.license:before {
content: @fa-var-shield-alt;
}
// autocomplete popup
& > i:before {
&:extend(.font-icon-class);
content: @fa-var-user;
margin-left: .5rem;
}
&.group > i:before {
content: @fa-var-users;
}
}
html.ie11 .listing.iconized li a:before {
font-size: 1.25rem;
}
.listing.iconized tr {
td:before {
&:extend(.font-icon-class);
margin-right: .5rem;
}
&.contact.person td.name:before {
content: @fa-var-user;
}
&.contact.group td.name:before {
content: @fa-var-users;
}
&.general > td.section:before {
content: @fa-var-desktop;
}
&.mailbox > td.section:before {
.font-icon-regular(@fa-var-envelope);
}
&.mailview > td.section:before {
content: @fa-var-inbox;
}
&.compose > td.section:before {
content: @fa-var-paper-plane;
}
&.addressbook > td.section:before {
content: @fa-var-users;
}
&.folders > td.section:before {
.font-icon-regular(@fa-var-folder);
}
&.server > td.section:before {
content: @fa-var-server;
}
&.enigma > td.section:before {
content: @fa-var-lock;
}
&.calendar > td.section:before {
content: @fa-var-calendar;
}
&.chat > td.section:before {
content: @fa-var-comments;
}
}
/* selecatable list: e.g. spellcheck language selection */
.listing.iconized.selectable li {
a:before {
&:extend(.font-icon-class);
content: "";
}
a.selected:before {
content: @fa-var-check;
}
}
.popupmenu .listing {
li > a {
border-left: 0;
}
li.selected {
color: @color-toolbarmenu-hover;
background-color: @color-toolbarmenu-hover-background;
}
td {
.overflow-ellipsis;
}
}
ul.treelist {
li {
div.treetoggle {
position: absolute;
top: 0;
left: 0;
width: @listing-treetoggle-width;
cursor: pointer;
background-color: transparent;
&:before {
&:extend(.font-icon-class);
content: @fa-var-angle-right;
margin-left: .25em;
font-size: 1em;
}
&.expanded:before {
content: @fa-var-angle-down;
}
}
& > a {
.overflow-ellipsis;
padding-left: @listing-treetoggle-width;
}
&.selected {
// reset .listing selection style
color: inherit;
background-color: transparent;
& > div > a, // this is used e.g. by kolab_addressbook
& > a {
color: @color-list-selected;
background-color: @color-list-selected-background;
}
}
ul {
padding: 0;
li {
padding-left: 0;
a { padding-left: (2 * @listing-treetoggle-width); }
div.treetoggle { left: @listing-treetoggle-width; }
li {
a { padding-left: (3 * @listing-treetoggle-width); }
div.treetoggle { left: (2 * @listing-treetoggle-width); }
li {
a { padding-left: (4 * @listing-treetoggle-width); }
div.treetoggle { left: (3 * @listing-treetoggle-width); }
li {
a { padding-left: (5 * @listing-treetoggle-width); }
div.treetoggle { left: (4 * @listing-treetoggle-width); }
li {
a { padding-left: (6 * @listing-treetoggle-width); }
div.treetoggle { left: (5 * @listing-treetoggle-width); }
}
}
}
}
}
}
}
&.notree {
div.treetoggle {
display: none;
}
li > a {
padding-left: .5em;
}
}
}
/*** Folders list widget ***/
.folderlist {
li {
&.mailbox {
&.unread {
& > a {
padding-right: 2.8em;
font-weight: bold;
}
}
&.recent {
color: @color-list-recent;
}
.unreadcount {
position: absolute;
top: 0;
right: 0;
min-width: 2em;
line-height: 1.4rem;
margin: (@listing-line-height - 1.4rem)/2;
padding: 0 .3em;
border-radius: .4em;
background: @color-list-badge-background;
color: @color-list-badge;
text-align: center;
font-weight: bold;
html.touch & {
line-height: 2rem;
margin: (@listing-touch-line-height - 2rem)/2;
}
}
&.recent > .unreadcount {
background: @color-list-recent-badge-background;
color: @color-list-recent-badge;
}
&.root {
display: none !important;
// FIXME: This element is confusing, I propose to not use it
}
}
a:before {
&:extend(.font-icon-class);
.font-icon-regular(@fa-var-folder);
margin-right: .5rem;
}
&.inbox > a:before {
.font-icon-solid(@fa-var-inbox);
}
&.trash a:before {
.font-icon-solid(@fa-var-trash-alt);
}
&.trash.empty > a:before {
.font-icon-regular(@fa-var-trash-alt);
}
&.drafts a:before {
.font-icon-solid(@fa-var-pencil-alt);
}
&.sent a:before {
.font-icon-solid(@fa-var-paper-plane);
}
&.junk a:before {
.font-icon-solid(@fa-var-fire-alt);
}
&.archive > a:before {
.font-icon-solid(@fa-var-archive);
}
}
// folder-selector fix for left padding
&.toolbarmenu a:before {
margin-left: .5em;
}
&.toolbarmenu {
li {
a:before {
margin-right: .25em;
}
}
}
}
/*** Messages list widget ***/
.messagelist > thead,
.messagelist .branch,
table.fixedcopy {
display: none;
}
.messagelist {
td {
border-left: 0;
width: 2em;
vertical-align: top;
font-size: 1rem !important;
}
td.subject {
width: 100%;
padding-right: 0;
display: flex;
flex-wrap: wrap;
a {
text-decoration: none;
cursor: default;
}
span {
line-height: 2em;
&.date {
font-size: 90%;
color: @color-list-secondary;
}
&.fromto {
.overflow-ellipsis;
flex: 1;
font-size: 90%;
color: @color-list-secondary;
padding-left: 1.5em;
padding-right: .5rem;
}
&.subject {
.overflow-ellipsis;
width: 100%;
}
}
}
td.threads {
padding: 0 0 0 .25rem;
width: 1.5em;
}
td.flags {
width: 2.5em;
& > span {
height: 1.7em;
line-height: 1.7em;
display: block;
&.flag {
cursor: pointer;
}
}
}
tr.flagged td,
tr.flagged td.subject span.subject a,
tr.flagged td.subject span.date,
tr.flagged td.subject span.fromto {
color: @color-list-flagged;
}
tr.deleted td,
tr.deleted td.subject span.subject a,
tr.deleted td.subject span.date,
tr.deleted td.subject span.fromto {
color: @color-list-deleted;
}
tr.unread td.subject span.subject {
font-weight: bold;
}
// thread parent message with unread children
tr.unroot td.subject a {
text-decoration: underline;
}
tr.thread td.threads div:before {
&:extend(.font-icon-class);
content: @fa-var-angle-right;
cursor: pointer;
width: 1em;
}
tr.thread.expanded td.threads div:before {
content: @fa-var-angle-down;
}
td.subject span.msgicon.status {
&:before {
&:extend(.font-icon-class);
content: @fa-var-circle;
cursor: pointer;
font-size: .4rem;
width: 1.1rem;
height: 2rem;
}
&.unread:before {
content: @fa-var-circle;
color: @color-list-unread-status;
font-size: .5rem;
}
&.unreadchildren:before {
.font-icon-regular(@fa-var-circle);
font-size: .5rem;
}
&.replied:before {
.font-icon-solid(@fa-var-reply);
font-size: 1rem;
}
&.forwarded:before {
.font-icon-solid(@fa-var-share);
font-size: 1rem;
}
&.replied.forwarded:before {
.font-icon-solid(@fa-var-reply); // TODO
font-size: 1rem;
}
tr.deleted &:before {
.font-icon-solid(@fa-var-ban);
font-size: 1rem;
}
}
span.attachment span {
&:extend(.font-icon-class);
color: @color-list-icon;
&:before {
margin: 0;
content: @fa-var-paperclip;
}
&.report:before {
.font-icon-regular(@fa-var-file-alt);
}
&.encrypted:before {
content: @fa-var-lock;
}
&.vcard:before {
.font-icon-regular(@fa-var-user); // vcard_attachments plugin
}
}
span.flagged:before {
&:extend(.font-icon-class);
content: @fa-var-flag;
}
tr:hover span.unflagged:before {
&:extend(.font-icon-class);
.font-icon-regular(@fa-var-flag);
}
}
// On touch devices hide flag icon, but do it in a way
// that saves as much room as possible, keeping the attachment icon
html.layout-phone,
html.touch {
.messagelist {
tr {
position: relative;
}
td.flags {
top: .25rem;
right: 0;
bottom: 0;
.flag {
visibility: hidden;
}
}
td.subject {
padding-right: .5em;
.subject {
padding-right: 1.5rem;
}
}
}
}
/* Contacts list */
.contactlist {
.contact.readonly td {
font-style: italic;
}
td.action {
// TODO
a {
// TODO
}
}
// for contacts list in mail compose
td.contact:before {
&:extend(.font-icon-class);
content: @fa-var-user;
}
// for contacts list in mail compose
td.contactgroup:before {
&:extend(.font-icon-class);
content: @fa-var-users;
}
span.email {
display: inline;
color: @color-list-secondary;
font-style: italic;
margin-left: .5em;
}
li {
a:before {
&:extend(.font-icon-class);
margin-right: .5rem;
}
a.addressbook::before {
.font-icon-regular(@fa-var-address-book);
}
a.contactgroup::before {
.font-icon-solid(@fa-var-users);
}
}
}
/* Attachments list */
@attachmentslist-item-height: 2rem;
.attachmentslist {
padding: 0;
margin: 0;
li {
list-style: none;
display: inline-flex;
white-space: nowrap;
line-height: @attachmentslist-item-height;
max-width: 100%;
&:before {
&:extend(.font-icon-class);
.font-icon-regular(@fa-var-file);
height: @attachmentslist-item-height;
}
&.txt:before,
&.text:before {
.font-icon-regular(@fa-var-file-alt);
}
&.pdf:before {
.font-icon-regular(@fa-var-file-pdf);
}
&.odt:before,
&.doc:before,
&.docx:before,
&.msword:before {
.font-icon-regular(@fa-var-file-word);
}
&.ods:before,
&.xls:before,
&.xlsx:before,
&.msexcel:before {
.font-icon-regular(@fa-var-file-excel);
}
&.rar:before,
&.zip:before,
&.gz:before {
.font-icon-regular(@fa-var-file-archive);
}
&.image:before,
&.jpg:before,
&.jpeg:before,
&.png:before {
.font-icon-regular(@fa-var-file-image);
}
&.mp3:before,
&.audio:before {
.font-icon-regular(@fa-var-file-audio);
}
&.m4p:before,
&.video:before {
.font-icon-regular(@fa-var-file-video);
}
&.ics:before,
&.calendar:before {
// TODO
}
&.vcard:before {
.font-icon-regular(@fa-var-address-card);
}
&.html:before {
.font-icon-regular(@fa-var-file-code);
}
&.eml:before,
&.rfc822:before {
// TODO
}
&.odp:before,
&.otp:before,
&.ppt:before,
&.pptx:before,
&.ppsx:before,
&.vnd.mspowerpoint:before {
.font-icon-regular(@fa-var-file-powerpoint);
}
&.sig:before,
&.pgp-signature:before,
&.pkcs7-signature:before {
// TODO
}
&.application.asc:before {
// TODO
}
&.application.pgp-keys:before {
// TODO
}
a {
text-decoration: none;
line-height: @attachmentslist-item-height;
height: @attachmentslist-item-height;
}
a.cancelupload:before,
a.delete:before {
&:extend(.font-icon-class);
content: @fa-var-trash-alt;
line-height: @attachmentslist-item-height;
height: @attachmentslist-item-height;
}
&.uploading:before {
.animated-icon-class;
.font-icon-solid(@fa-var-circle-notch);
}
a.filename {
display: flex;
overflow: hidden;
}
.attachment-name {
.overflow-ellipsis;
color: @color-font;
}
.attachment-size {
color: @color-list-secondary;
padding: 0 .25em;
}
}
}
.keylist {
padding: 0;
list-style: none;
li {
line-height: 2;
&:before {
&:extend(.font-icon-class);
content: @fa-var-key;
line-height: 1.5;
}
}
}
#identities-table {
td.mail:before {
&:extend(.font-icon-class);
content: @fa-var-id-card;
}
}
#responses-table {
td.name:before {
&:extend(.font-icon-class);
content: @fa-var-comment;
}
}
#filterslist {
td.name:before {
&:extend(.font-icon-class);
content: @fa-var-filter;
}
}
#filtersetslist {
td.name:before {
&:extend(.font-icon-class);
content: @fa-var-file-alt;
}
}
#subscription-table {
li.mailbox a {
padding-right: 2.5rem;
}
}
diff --git a/skins/elastic/styles/widgets/mail.less b/skins/elastic/styles/widgets/mail.less
index de7749edd..353901476 100644
--- a/skins/elastic/styles/widgets/mail.less
+++ b/skins/elastic/styles/widgets/mail.less
@@ -1,319 +1,319 @@
/**
- * Roundcube webmail styles for the Elastic skin
+ * Roundcube Webmail styles for the Elastic skin
*
- * Copyright (c) 2017-2018, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
/*** Mail message body elements ***/
@mail-header-photo-height: 4rem;
#message-header {
margin-bottom: 1rem;
.subject {
font-size: 1.5rem;
font-weight: bold;
body.status-flagged &:before {
&:extend(.font-icon-class);
display: inline;
float: none;
content: @fa-var-flag;
font-size: 1em;
color: @color-error;
}
a.extwin {
&:before {
&:extend(.font-icon-class);
float: none;
display: inline-block;
font-size: 75%;
line-height: 1.5;
margin: 0;
content: @fa-var-external-link-square-alt;
}
}
span.inner {
display: none;
}
}
.short-header {
display: flex;
img.contactphoto {
margin: 0 1rem 0 0;
border-radius: 50%;
width: @mail-header-photo-height;
height: @mail-header-photo-height;
}
div.header-content {
min-height: @mail-header-photo-height;
flex: 1;
}
div.header-subject {
line-height: @mail-header-photo-height/2;
& > span {
line-height: 1.5;
display: inline-block;
vertical-align: middle;
}
}
div.header-links {
a {
font-size: 90%;
margin-right: .5rem;
text-decoration: none;
white-space: nowrap;
display: inline-block;
&:before {
&:extend(.font-icon-class);
height: 1.5rem;
line-height: 1.3;
}
&.envelope:before {
content: @fa-var-envelope;
}
&.html:before {
content: @fa-var-image;
}
&.plain:before {
content: @fa-var-align-justify;
}
&.zipdownload:before {
content: @fa-var-download;
}
}
}
.message-partheaders {
margin: 0 !important;
padding: .25rem 0 !important;
}
}
a.extwin,
a.headers {
text-decoration: none;
}
}
#message-content {
.attachmentslist:not(:empty) {
margin-bottom: 1rem;
}
}
#message-objects + .ui.alert {
float: left;
}
#messagebody {
&.mailvelope {
margin: 0;
iframe {
min-height: 75vh;
}
}
}
.message-part,
.message-htmlpart {
padding-top: .5rem;
&:not(:first-child) {
border-top: 1px solid lighten(@color-mail-headers, 50%);
margin-top: .5rem;
}
div.rcmBody {
// Remove margins that can be set by the mail message styles
margin: 0 !important;
}
blockquote {
.overflow-ellipsis;
color: @color-blockquote-0;
border-left: 2px solid @color-blockquote-0-border;
border-right: 2px solid @color-blockquote-0-border;
background-color: @color-blockquote-background;
margin: 2px 0;
padding: 0 .4em;
blockquote {
color: @color-blockquote-1;
border-left: 2px solid @color-blockquote-1-border;
border-right: 2px solid @color-blockquote-1-border;
blockquote {
color: @color-blockquote-2;
border-left: 2px solid @color-blockquote-2-border;
border-right: 2px solid @color-blockquote-2-border;
}
}
span.blockquote-link {
top: 0;
cursor: pointer;
right: .5rem;
min-width: 4rem;
padding: .2rem .25rem .2rem .5rem;
font-size: 90%;
text-align: center;
color: @color-black-shade-text;
background: @color-black-shade-bg;
border: 1px solid @color-black-shade-border;
border-radius: .3rem;
line-height: 1;
.font-family; // don't inherit monospace font
&:after {
&:extend(.font-icon-class);
content: @fa-var-angle-down;
display: inline-block;
float: none;
margin: 0;
font-size: 90%;
}
&.collapsed:after {
content: @fa-var-angle-up;
}
}
&.blockquote-header {
text-overflow: ellipsis !important;
padding-right: 5rem !important;
}
}
}
.message-part {
span.sig {
color: @color-mail-signature;
}
div.pre {
font-family: monospace;
}
}
.message-partheaders {
padding: .5rem 0;
margin: .5rem 0 0 0;
font-size: 90%;
border-top: 1px solid @color-list-border;
border-bottom: 1px solid @color-list-border;
color: @color-mail-headers;
.header-title {
.overflow-ellipsis;
white-space: nowrap;
max-width: 7em;
font-weight: bold;
padding-right: 1rem;
vertical-align: top;
}
.subject {
font-weight: bold;
}
& + .message-part,
& + .message-htmlpart {
border-top: 0;
margin: 0;
}
}
.image-tools {
position: absolute;
top: 5rem;
left: 0;
height: @layout-header-height;
overflow: hidden;
transform: translateX(-87%);
transition: transform 0.3s ease-in-out;
background-color: @color-image-tools-background;
border-radius: 0 .3rem .3rem 0;
.toolbar {
float: left;
height: @layout-header-height;
a.button:before {
width: auto;
height: 1.75rem;
display: block;
float: none;
}
}
a.button.icon.tools {
padding: 0 .25rem;
display: inline-block;
height: @layout-header-height;
span.inner {
display: none;
}
&:before {
line-height: @layout-header-height;
margin: 0;
}
}
&.open {
transform: translateX(0);
a.button.icon.tools:before {
content: @fa-var-chevron-left;
}
}
a {
color: @color-image-tools;
&:focus,
&:hover {
background-color: @color-image-tools-hover !important;
outline: 0;
}
}
}
#compose-attachments {
margin: .5rem .5rem 0 .5rem;
}
#composestatusbar {
position: absolute;
height: 2.5rem;
padding-top: .25rem;
opacity: .3;
@media screen and (min-width: (@screen-width-small + 1px)) {
display: none;
}
}
diff --git a/skins/elastic/styles/widgets/messages.less b/skins/elastic/styles/widgets/messages.less
index 6f28ed321..1b2f48f73 100644
--- a/skins/elastic/styles/widgets/messages.less
+++ b/skins/elastic/styles/widgets/messages.less
@@ -1,230 +1,230 @@
/**
- * Roundcube webmail styles for the Elastic skin
+ * Roundcube Webmail styles for the Elastic skin
*
- * Copyright (c) 2017-2018, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
/*** UI Messages ***/
// .boxwarning/.boxerror/.boxinformation classes are converted to .ui.alert in bootstrap_init()
.ui.alert {
margin: 0;
opacity: .95;
float: left;
width: 100%;
padding: .75em;
color: @color-message;
border: 1px solid @color-message-border;
background-color: @color-message-background;
display: flex;
align-items: center;
@media screen and (max-width: @screen-width-xs) {
border: 0;
border-radius: 0;
min-height: 4.2rem;
}
@media screen and (min-width: (@screen-width-xs + 1px)) {
&:not(:last-child) {
margin-bottom: .2rem;
}
}
span {
margin: auto 0;
}
& > i.icon {
line-height: 1;
color: lighten(@color-black, 25%);
margin: auto 0;
}
& > i.icon:before {
&:extend(.font-icon-class);
content: @fa-var-info-circle;
margin-right: .6rem;
}
.btn {
margin-left: .5rem;
}
&.loading {
color: @color-message-loading;
& > i.icon:before {
content: @fa-var-circle-notch;
.animated-icon-class;
width: 1em;
}
}
&.alert-success > i.icon:before {
content: @fa-var-check-circle;
color: @color-message-success;
}
&.alert-warning > i.icon:before {
content: @fa-var-exclamation-triangle;
color: @color-message-warning;
}
&.alert-danger > i.icon:before {
content: @fa-var-exclamation-circle;
color: @color-message-error;
}
&.vcardattachment > i.icon:before {
content: @fa-var-address-card; // vcard_attachments plugin
}
&.enigmaattachment > i.icon:before {
content: @fa-var-key; // enigma plugin
}
&.signed > i.icon:before,
&.encrypted > i.icon:before {
content: @fa-var-lock; // enigma plugin
}
&.chat > i.icon:before {
content: @fa-var-comment;
}
// This works with following structure: <i> <span> [button].
// <span> here is a one-line text, and button can be anything but <span>.
&.aligned-buttons {
display: flex;
span {
flex: 1;
}
}
a:not(.btn) {
color: @color-message-link;
font-weight: @color-message-link-font-weight;
}
h3 {
font-weight: bold;
font-size: 1.2rem;
}
p {
margin: 1rem 0;
}
&.boxerror,
&.boxconfirmation,
&.boxinformation,
&.boxwarning {
float: none;
border-radius: 0;
border: none;
padding: .5em;
margin-top: 0; // this can be a <p> element, reset default margin
i.icon {
font-size: 1.5em !important;
}
&:not(:last-child) {
margin-bottom: .2rem;
}
}
&.boxerror {
background-color: @color-message-error-box-background;
& when not(@color-message-error-box = @color-message) { color: @color-message-error-box; }
}
&.boxinformation {
background-color: @color-message-information-box-background;
& when not(@color-message-information-box = @color-message) { color: @color-message-information-box; }
}
&.boxconfirmation {
background-color: @color-message-success-box-background;
& when not(@color-message-error-box = @color-message) { color: @color-message-error-box; }
}
&.boxwarning {
background-color: @color-message-warning-box-background;
& when not(@color-message-warning-box = @color-message) { color: @color-message-warning-box; }
}
& + table {
margin-top: 1em;
}
}
#messagestack {
position: absolute;
bottom: .5em;
right: .7em;
z-index: 105; // needs to be above .ui-widget-overlay
width: 320px;
height: auto;
max-height: 85%;
@media screen and (max-width: @screen-width-xs) {
left: 0;
right: 0;
bottom: 0;
width: auto;
}
div {
border-color: transparent;
background-color: @color-message;
color: #fff;
&.voice {
position: absolute;
top: -1000px;
}
i.icon {
font-size: 1.5em !important;
}
& > i.icon:before {
color: #fff;
}
}
.loading {
background-color: @color-message-loading;
}
.alert-info.information {
background-color: @color-message-information;
}
.alert-success {
background-color: @color-message-success;
}
.alert-warning {
background-color: @color-message-warning;
}
.alert-danger {
background-color: @color-message-error;
}
a {
color: inherit !important;
text-decoration: underline;
cursor: pointer;
}
}
diff --git a/skins/elastic/styles/widgets/taskmenu.less b/skins/elastic/styles/widgets/taskmenu.less
index b44df3007..60cea07ef 100644
--- a/skins/elastic/styles/widgets/taskmenu.less
+++ b/skins/elastic/styles/widgets/taskmenu.less
@@ -1,221 +1,221 @@
/**
- * Roundcube webmail styles for the Elastic skin
+ * Roundcube Webmail styles for the Elastic skin
*
- * Copyright (c) 2017-2018, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
/*** Taskmenu ***/
#taskmenu {
height: 100%;
a {
.overflow-ellipsis;
text-decoration: none;
display: block;
&:before {
&:extend(.font-icon-class);
}
&.mail:before {
content: @fa-var-envelope;
}
&.addressbook:before {
content: @fa-var-users;
}
&.settings:before {
content: @fa-var-sliders-h;
}
&.help:before {
content: @fa-var-life-ring;
}
&.logout:before {
content: @fa-var-power-off;
}
&.about:before {
content: @fa-var-question;
}
&.refresh:before {
content: @fa-var-sync;
}
&.compose:before {
content: @fa-var-edit;
}
&.calendar:before {
content: @fa-var-calendar-alt;
}
&.tasklist:before {
content: @fa-var-tasks;
}
&.files:before {
content: @fa-var-folder;
}
&.notes:before {
content: @fa-var-sticky-note;
}
&.chat:before {
content: @fa-var-comments;
}
}
@media screen and (max-width: @screen-width-xs) {
z-index: 30001; // because autocompletion popup uses z-index:30000
overflow-x: hidden;
a {
width: 100%;
padding: 0 .5em;
text-align: left;
line-height: @layout-touch-menu-record-height;
height: @layout-touch-menu-record-height;
border-bottom: 1px solid @color-list-border;
color: @color-list;
font-size: 1.2rem;
&:before {
margin-right: .5rem;
}
}
}
@media screen and (min-width: (@screen-width-xs + 1px)) {
a {
text-align: center;
color: @color-taskmenu-button;
padding: .45rem 0;
width: @layout-menu-width-sm;
font-size: 1.2rem;
margin-bottom: 1px;
&:before {
display: block;
height: 2.1rem;
line-height: 1.5;
width: @layout-menu-width-sm;
margin: 0;
}
&.selected {
color: @color-taskmenu-button-selected;
background: @color-taskmenu-button-selected-background;
}
&:hover {
color: @color-taskmenu-button-hover;
background: @color-taskmenu-button-background-hover;
}
}
.special-buttons {
position: absolute;
bottom: 0;
left: 0;
background-color: @color-taskmenu-background;
}
.action-buttons {
a {
color: @color-taskmenu-button-action;
background: @color-taskmenu-button-action-background;
&:hover {
color: @color-taskmenu-button-action-hover;
background: @color-taskmenu-button-action-background-hover;
}
&.selected {
background: @color-taskmenu-button-selected-background;
}
}
}
span.inner {
display: none;
}
a.logout {
color: @color-taskmenu-button-logout-hover !important;
}
}
@media screen and (min-width: (@screen-width-medium + 1px)) {
a {
width: @layout-menu-width;
height: (@layout-header-height - .05rem);
font-size: 1rem;
&:before {
width: @layout-menu-width;
height: 1.75rem;
line-height: 1.5;
float: none; // fixed overflowing text in Edge
}
}
span.inner {
display: inline;
font-size: 85%;
padding: 0 .1em;
}
}
}
.menu {
.popover-header {
height: @layout-header-height;
line-height: @layout-header-height;
border: 0;
border-radius: 0;
text-align: center;
img {
max-height: @layout-header-height;
max-width: @layout-menu-width;
padding: .25rem;
@media screen and (min-width: (@screen-width-xs + 1px)) and (max-width: @screen-width-medium) {
max-width: @layout-menu-width * 0.45;
}
}
@media screen and (min-width: (@screen-width-xs + 1px)) {
padding: 0 !important;
background-color: @color-taskmenu-background !important;
a {
display: none !important;
}
}
html.layout-phone & {
display: flex !important;
align-items: center;
justify-content: center;
padding: 0 .5rem;
img {
max-width: @layout-mobile-menu-width - 50px;
}
a {
width: auto;
flex: 1;
&:before {
height: @layout-touch-header-height;
float: right;
}
.inner {
display: none;
}
}
}
}
}
diff --git a/skins/elastic/styles/widgets/toolbar.less b/skins/elastic/styles/widgets/toolbar.less
index b9d77d523..d3ce691f8 100644
--- a/skins/elastic/styles/widgets/toolbar.less
+++ b/skins/elastic/styles/widgets/toolbar.less
@@ -1,801 +1,801 @@
/**
- * Roundcube webmail styles for the Elastic skin
+ * Roundcube Webmail styles for the Elastic skin
*
- * Copyright (c) 2017-2018, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
/*** Toolbar widget ***/
.toolbar {
margin: 0;
font-size: 1rem;
text-align: center;
&.listing a,
a {
text-decoration: none;
cursor: pointer !important; // TODO: re-consider .listing class use on toolbar
color: @color-toolbar-button;
}
a.button {
display: inline-block;
border: 0 !important;
height: @layout-header-height;
min-width: 3.5rem;
max-width: 6rem;
width: auto; // reset width defined for links in .listing
padding: .45rem;
text-align: center;
text-overflow: ellipsis;
overflow-x: hidden;
line-height: 1.5;
&:hover,
&:focus {
outline: 0;
}
&.selected {
color: @color-success;
}
&:before {
&:extend(.font-icon-class);
float: none;
margin: 0;
line-height: 1.75;
}
}
&:not(.popupmenu) span.inner {
font-size: 90%;
font-weight: normal;
}
.dropbutton {
a.button.dropdown {
font-size: 75%;
overflow: hidden; // for IE11
span.inner {
display: none;
}
}
a.button:first-child {
padding-right: 0;
}
}
& > .spacer {
width: 1em;
}
&.pagenav {
display: flex;
justify-content: space-between;
align-items: center;
a.button {
flex-grow: 1;
min-width: 2rem !important;
height: @layout-footer-small-height;
&:before {
line-height: 1.25;
}
}
.pagenav-text {
.overflow-ellipsis;
color: @color-list-pagenav;
flex-grow: 4;
font-size: 80%;
white-space: nowrap;
}
input {
width: 3rem;
max-width: 5rem;
font-size: 90%;
text-align: center;
max-height: 1.6rem;
html.layout-phone & {
display: none;
}
}
span.inner {
display: none;
}
&.pagenav-list {
cursor: pointer;
background-color: @color-searchbar-background;
border-bottom: 1px solid @color-list-border;
a.button {
flex-grow: unset;
}
.pagenav-text {
text-align: left;
font-size: 100%;
}
& + .navlist {
background-color: #fbfbfb;
}
&.expanded + .navlist {
border-bottom: 1px solid @color-layout-border;
}
}
}
&.footer {
a.button:before {
height: 1.75rem;
float: none;
display: block;
width: auto;
}
}
&.footer.small {
a.button:before {
height: auto; // for Chrome
}
}
&.content-frame-navigation.hide-nav-buttons {
a.next,
a.prev {
display: none;
}
}
.listselectors {
max-width: 100%;
display: flex;
justify-content: space-around;
}
}
.header {
a.button {
color: @color-toolbar-button;
}
.buttons {
display: block;
button {
display: block;
float: left;
cursor: pointer;
color: @color-toolbar-button;
background-color: transparent;
border: 0;
padding: 0;
height: @layout-touch-header-height;
line-height: @layout-touch-header-height;
width: 2.5em;
&:before {
font-size: 1.75rem;
}
}
a.button {
display: inline-block;
&:before {
&:extend(.font-icon-class);
float: none;
}
&.reply:before {
content: @fa-var-reply;
}
&.send:before {
content: @fa-var-paper-plane;
}
}
}
}
.popupmenu.toolbar.listing {
a.button {
max-width: 100%;
width: 100%;
text-align: left;
line-height: @layout-touch-menu-record-height;
height: @layout-touch-menu-record-height;
&:before {
display: inline-block;
line-height: inherit;
margin-right: .5rem;
}
}
.dropbutton {
display: flex;
a.button:first-child {
.overflow-ellipsis;
flex: 1;
}
a.button.dropdown {
font-size: 100%;
cursor: pointer;
width: auto;
&:before {
content: @fa-var-angle-right;
margin-left: .5em;
margin-right: 0;
}
span.inner {
display: none;
}
}
}
li.spacer {
display: none;
}
li:last-child {
border: 0;
}
}
.toolbarmenu li {
&.separator {
// TODO: all separator properties
line-height: 1.5rem !important;
font-size: .75rem !important;
padding: 0 .5rem;
color: @color-popover-separator;
background-color: @color-popover-separator-background;
label {
margin: 0; // Unsets Bootstrap label margin, bug?
}
}
a {
opacity: .5;
&.active {
opacity: 1;
}
}
&:last-child {
border-bottom: none;
}
a:before {
&:extend(.font-icon-class);
}
}
.toolbarmenu.listing li {
&:hover {
a.active {
color: @color-toolbarmenu-hover;
background-color: @color-toolbarmenu-hover-background;
}
}
}
.toolbar a.button,
.toolbarmenu li a {
&.actions:before {
content: @fa-var-cog;
}
&.addressbook:before {
content: @fa-var-user;
}
&.archive:before {
content: @fa-var-archive;
}
&.assigngroup:before {
content: @fa-var-user-plus;
}
&.attach:before,
&.vcard:before {
content: @fa-var-paperclip;
}
&.next:before {
content: @fa-var-arrow-right;
}
&.prev:before,
&.back:before {
content: @fa-var-arrow-left;
}
&.check:before {
content: "";
}
&.check.selected:before {
content: @fa-var-check;
}
&.closewin:before {
content: @fa-var-window-close;
}
&.collapse:before {
content: @fa-var-angle-down;
}
&.compose:before {
content: @fa-var-edit;
}
&.copy:before {
content: @fa-var-copy;
}
&.create:before {
content: @fa-var-plus-square;
}
&.delete:before {
content: @fa-var-trash-alt;
}
&.download:before,
&.download.eml:before,
&.download.maildir:before,
&.download.mbox:before {
content: @fa-var-download;
}
&.dropdown:before {
content: @fa-var-caret-down;
}
&.edit:before {
content: @fa-var-pencil-alt;
}
&.encrypt:before,
&.enigma:before {
content: @fa-var-lock;
}
&.encrypt.sign:before {
content: @fa-var-lock; // TODO
}
&.expand:before {
content: @fa-var-angle-right;
}
&.expand.all:before,
&.expand.none:before,
&.expand.unread:before {
content: @fa-var-comments;
}
&.export:before,
&.export.all:before,
&.export.selection:before {
content: @fa-var-download;
}
&.expunge:before {
content: @fa-var-compress-arrows-alt;
}
&.extwin:before {
content: @fa-var-external-link-square-alt;
}
&.filterlink:before {
content: @fa-var-filter;
}
&.firstpage:before {
content: @fa-var-angle-double-left;
}
&.nextpage:before {
content: @fa-var-angle-right;
}
&.prevpage:before {
content: @fa-var-angle-left;
}
&.lastpage:before {
content: @fa-var-angle-double-right;
}
&.flag:before,
&.select.flagged:before {
.font-icon-solid(@fa-var-flag);
}
&.unflag:before {
.font-icon-regular(@fa-var-flag);
}
&.folders:before {
content: @fa-var-folder;
}
&.forward:before,
&.forward.attachment:before,
&.forward.bounce:before,
&.forward.inline:before {
content: @fa-var-share;
}
&.import:before,
&.upload:before {
content: @fa-var-upload;
}
&.insertresponse:before {
content: @fa-var-comment;
}
&.junk:before {
content: @fa-var-fire-alt;
}
&.notjunk:before {
content: @fa-var-inbox;
}
&.markmessage:before {
content: @fa-var-tag;
}
&.more:before {
content: @fa-var-ellipsis-h;
}
&.move:before {
content: @fa-var-folder-open;
}
&.print:before {
content: @fa-var-print;
}
&.properties:before {
content: @fa-var-file;
}
&.purge:before {
content: @fa-var-eraser;
}
&.qrcode:before {
content: @fa-var-qrcode;
}
&.read:before {
.font-icon-regular(@fa-var-envelope-open);
}
&.unread:before,
&.select.unread:before {
.font-icon-solid(@fa-var-envelope);
}
&.recipient:before {
.font-icon-regular(@fa-var-envelope);
}
&.refresh:before {
content: @fa-var-sync;
}
&.remove:before {
content: @fa-var-eraser;
}
&.removegroup:before {
content: @fa-var-user-times;
}
&.rename:before {
content: @fa-var-pencil-alt;
}
&.reply:before {
content: @fa-var-reply;
}
&.reply-all:before,
&.reply.all:before,
&.reply.list:before {
content: @fa-var-reply-all;
}
&.responses:before {
content: @fa-var-comment;
}
&.rotate:before {
content: @fa-var-redo-alt;
}
&.save:before {
.font-icon-regular(@fa-var-save);
}
&.search:before {
content: @fa-var-search;
}
&.search.delete:before {
content: @fa-var-trash-alt;
}
&.select:before {
.font-icon-regular(@fa-var-check-circle);
}
&.select.all:before {
content: @fa-var-check-square;
}
&.select.invert:before {
content: @fa-var-square;
}
&.select.none:before {
.font-icon-solid(@fa-var-times);
}
&.select.page:before {
.font-icon-solid(@fa-var-bars);
}
&.selection:before {
content: @fa-var-mouse-pointer;
}
&.send:before {
content: @fa-var-paper-plane;
}
&.settings:before {
content: @fa-var-sliders-h;
}
&.showurl:before {
content: @fa-var-link;
}
&.signature:before {
content: @fa-var-signature;
}
&.source:before {
content: @fa-var-file-code;
}
&.spellcheck:before {
content: @fa-var-magic; // TODO
}
&.status:before {
.font-icon-regular(@fa-var-lightbulb);
}
&.submit:before {
content: @fa-var-check;
}
&.threads:before {
content: @fa-var-comments;
}
&.zoomin:before {
content: @fa-var-search-plus;
}
&.zoomout:before {
content: @fa-var-search-minus;
}
}
html.touch {
.toolbarmenu.listing td,
.toolbarmenu.listing li,
#layout > :not(.content) > .header a.button {
font-size: 1.2rem;
}
.toolbarmenu.listing li {
}
.toolbarmenu li {
}
}
html.ie11 .toolbar .dropbutton a.dropdown:before {
font-size: 80%;
}
@media screen and (min-width: (@screen-width-small + 1px)) {
.toolbar {
.header > & {
display: flex; // for Safari on desktop
}
a.button {
&:not(.disabled):focus,
&:not(.disabled):hover {
background-color: @color-toolbar-button-background-hover;
}
}
&.listing li {
display: inline-block;
border: 0;
a.button:before {
height: 1.75rem;
float: none;
display: block;
width: auto;
margin: 0;
}
}
.dropbutton {
height: @layout-header-height;
display: inline-block;
&:not(.disabled):hover {
background-color: @color-toolbar-button-background-hover;
}
a.button.dropdown {
min-width: 1.1rem;
padding: 0 .3rem;
&:hover {
background-color: darken(@color-toolbar-button-background-hover, 5%);
}
&:before {
height: @layout-header-height;
line-height: 4.2;
}
}
}
}
.toolbar.content-frame-navigation {
display: none !important;
}
.header a.button.icon {
&:not(.disabled):focus,
&:not(.disabled):hover {
background-color: @color-toolbar-button-background-hover;
outline: 0;
}
&:before {
margin: 0;
}
}
}
@media screen and (max-width: @screen-width-small) {
body > #layout > div > .toolbar.footer {
justify-content: space-around;
& > * {
flex-grow: 1;
}
.buttons {
display: flex;
justify-content: space-around;
}
.listselectors > * {
flex-grow: 1;
}
}
.toolbar.listing a {
color: @color-font;
}
}
a.button.icon.toolbar-button {
order: 8;
@media screen and (min-width: (@screen-width-large + 1px)) {
height: @layout-header-height;
line-height: 1.5;
padding: .45rem;
&:before {
float: none;
height: 1.75rem;
line-height: 1.75;
width: auto;
}
span.inner {
display: inline;
font-weight: normal;
font-size: 90%;
}
}
}
/*** Searchbar and searchoptions widgets ***/
.searchbar {
height: @layout-searchbar-height;
min-height: @layout-searchbar-height; // because of Flexbox
line-height: @layout-searchbar-height;
background-color: @color-searchbar-background;
border-bottom: 1px solid @color-list-border;
display: flex;
align-items: center;
white-space: nowrap;
overflow: hidden;
position: relative;
padding-right: .5rem;
form {
flex: 1;
display: flex;
&:before {
&:extend(.font-icon-class);
content: @fa-var-search;
height: @layout-searchbar-height;
color: @color-list-pagenav;
margin: 0 0 0 .75rem;
}
}
input {
width: 100%;
border: 0;
background: transparent;
padding: .5rem;
line-height: normal; // fixes placeholder misalignment in IE11
outline: 0; // removes focus outline in Chrome
}
a.button {
height: @layout-searchbar-height;
min-width: auto;
color: @color-toolbar-button;
&:before {
&:extend(.font-icon-class);
}
&.options {
&:before {
content: @fa-var-angle-down;
line-height: 1.25;
width: 1em;
}
}
&.reset {
display: none;
&:before {
content: @fa-var-times;
font-size: 1em;
}
}
&.search {
display: none;
}
}
span.inner {
display: none;
}
&.active {
a.button.reset {
display: inline;
}
}
&.open a.button.options:before {
content: @fa-var-angle-up;
}
}
.searchoptions {
button.search {
width: 100%;
}
ul.proplist {
& + div {
margin-top: 1rem;
}
}
.input-group {
&:not(:last-child) {
margin-bottom: .5rem;
}
.input-group-prepend {
width: 30%;
}
label {
width: 100%;
}
}
.formbuttons {
// this is needed because we hide .formbuttons on small devices
// we don't want it for search options form
display: block !important;
}
}
diff --git a/skins/elastic/ui.js b/skins/elastic/ui.js
index c643588a8..2e93a333a 100644
--- a/skins/elastic/ui.js
+++ b/skins/elastic/ui.js
@@ -1,3973 +1,3973 @@
/**
* Roundcube webmail functions for the Elastic skin
*
- * Copyright (c) 2017-2018, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*
* @license magnet:?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt CC0-1.0
*/
"use strict";
function rcube_elastic_ui()
{
var ref = this,
mode = 'normal', // one of: large, normal, small, phone
touch = false,
ios = false,
popups_close_lock,
is_framed = rcmail.is_framed(),
env = {
config: {
standard_windows: rcmail.env.standard_windows,
message_extwin: rcmail.env.message_extwin,
compose_extwin: rcmail.env.compose_extwin,
help_open_extwin: rcmail.env.help_open_extwin
},
checkboxes: 0,
small_screen_config: {
standard_windows: true,
message_extwin: false,
compose_extwin: false,
help_open_extwin: false
}
},
menus = {},
content_buttons = [],
frame_buttons = [],
layout = {
menu: $('#layout > .menu'),
sidebar: $('#layout > .sidebar'),
list: $('#layout > .list'),
content: $('#layout > .content'),
},
buttons = {
menu: $('a.menu-button'),
back_sidebar: $('a.back-sidebar-button'),
back_list: $('a.back-list-button'),
back_content: $('a.back-content-button'),
};
// Public methods
this.register_content_buttons = register_content_buttons;
this.menu_hide = menu_hide;
this.menu_toggle = menu_toggle;
this.menu_destroy = menu_destroy;
this.popup_init = popup_init;
this.about_dialog = about_dialog;
this.headers_dialog = headers_dialog;
this.import_dialog = import_dialog;
this.headers_show = headers_show;
this.spellmenu = spellmenu;
this.searchmenu = searchmenu;
this.headersmenu = headersmenu;
this.header_reset = header_reset;
this.compose_status = compose_status;
this.attachmentmenu = attachmentmenu;
this.mailtomenu = mailtomenu;
this.recipient_selector = recipient_selector;
this.show_list = show_list;
this.show_sidebar = show_sidebar;
this.smart_field_init = smart_field_init;
this.smart_field_reset = smart_field_reset;
this.form_errors = form_errors;
this.switch_nav_list = switch_nav_list;
this.searchbar_init = searchbar_init;
this.pretty_checkbox = pretty_checkbox;
this.pretty_select = pretty_select;
this.datepicker_init = datepicker_init;
this.bootstrap_style = bootstrap_style;
// Detect screen size/mode
screen_mode();
// Initialize layout
layout_init();
// Convert some elements to Bootstrap style
bootstrap_style();
// Initialize responsive toolbars (have to be before popups init)
toolbar_init();
// Initialize content frame and list handlers
content_frame_init();
// Initialize menu dropdowns
dropdowns_init();
// Setup various UI elements
setup();
// Update layout after initialization
resize();
/**
* Setup procedure
*/
function setup()
{
var title, form, content_buttons = [];
// Intercept jQuery-UI dialogs...
$.ui && $.widget('ui.dialog', $.ui.dialog, {
open: function() {
// .. to unify min width for iframe'd dialogs
if ($(this.element).is('.iframe')) {
this.options.width = Math.max(576, this.options.width);
}
this._super();
// ... to re-style them on dialog open
dialog_open(this);
return this;
},
close: function() {
this._super();
// ... to close custom select dropdowns on dialog close
$('.select-menu:visible').remove();
return this;
}
});
// menu/sidebar/list button
buttons.menu.on('click', function() { app_menu(true); return false; });
buttons.back_sidebar.on('click', function() { show_sidebar(); return false; });
buttons.back_list.on('click', function() { show_list(); return false; });
buttons.back_content.on('click', function() { show_content(true); return false; });
// Initialize search forms
$('.searchbar').each(function() { searchbar_init(this); });
// Set content frame title in parent window (exclude ext-windows and dialog frames)
if (is_framed && !rcmail.env.extwin && !parent.$('.ui-dialog:visible').length) {
if (title = $('h1.voice').first().text()) {
parent.$('#layout > .content > .header > .header-title:not(.constant)').text(title);
}
}
else if (!is_framed) {
title = $('.boxtitle', layout.content).first().detach().text();
if (!title) {
title = $('h1.voice').first().text();
}
if (title) {
$('.header > .header-title', layout.content).text(title);
}
}
// Add content frame toolbar in the footer, for content buttons and navigation
if (!is_framed && layout.content.length && !$(layout.content).is('.no-navbar')
&& !$(layout.content).children('.frame-content').length
) {
env.frame_nav = $('<div class="footer toolbar content-frame-navigation hide-nav-buttons">')
.append($('<a class="button prev">')
.append($('<span class="inner"></span>').text(rcmail.gettext('previous'))))
.append($('<span class="buttons">'))
.append($('<a class="button next">')
.append($('<span class="inner"></span>').text(rcmail.gettext('next'))))
.appendTo(layout.content);
}
// Move some buttons to the frame footer toolbar
$('a[data-content-button]').each(function() {
content_buttons.push(create_cloned_button($(this)));
});
// Move form buttons from the content frame into the frame footer (on parent window)
$('.formbuttons').filter(function() { return !$(this).parent('.searchoptions').length; }).children().each(function() {
var target = $(this);
// skip non-content buttons
if (!is_framed && !target.parents('.content').length) {
return;
}
if (target.is('.cancel')) {
target.addClass('hidden');
return;
}
content_buttons.push(create_cloned_button(target));
});
(is_framed ? parent.UI : ref).register_content_buttons(content_buttons);
// Mail compose features
if (form = rcmail.gui_objects.messageform) {
form = $('form[name="' + form + '"]');
// Show input elements with non-empty value
// These event handlers need to be registered before rcmail 'init' event
$('#_cc, #_bcc, #_replyto, #_followupto', $('.compose-headers')).each(function() {
$(this).on('change', function() {
$('#compose' + $(this).attr('id'))[this.value ? 'removeClass' : 'addClass']('hidden');
});
});
// We put compose options outside of the main form
// Because IE/Edge (<16) does not support 'form' attribute we'll copy
// inputs into the main form as hidden fields
// TODO: Consider doing this for IE/Edge only, just set the 'form' attribute on others
$('#compose-options').find('textarea,input,select').each(function() {
var hidden = $('<input>')
.attr({type: 'hidden', name: $(this).attr('name')})
.appendTo(form);
$(this).attr('tabindex', 2)
.on('change', function() {
hidden.val(this.type != 'checkbox' || this.checked ? $(this).val() : '');
})
.change();
});
}
// Use smart recipient inputs
// This have to be after mail compose feature above
$('[data-recipient-input]').each(function() { recipient_input(this); });
// Image upload widget
$('.image-upload').each(function() { image_upload_input(this); });
// Add HTML/Plain tabs (switch) on top of textarea with TinyMCE editor
$('textarea[data-html-editor]').each(function() { html_editor_init(this); });
$('#dragmessage-menu,#dragcontact-menu').each(function() {
rcmail.gui_object('dragmenu', this.id);
});
// Taskmenu items added by plugins do not use elastic classes (e.g help plugin)
// it's for larry skin compat. We'll assign 'button', 'selected' and icon-specific class.
$('#taskmenu > a').each(function() {
if (/button-([a-z]+)/.test(this.className)) {
var data, name = RegExp.$1,
button = find_button(this.id);
if (button && (data = button.data)) {
if (data.sel) {
data.sel += ' button ' + name;
data.sel = data.sel.replace('button-selected', 'selected');
}
if (data.act) {
data.act += ' button ' + name;
}
rcmail.buttons[button.command][button.index] = data;
rcmail.init_button(button.command, data);
}
$(this).addClass('button ' + name);
$('.button-inner', this).addClass('inner');
}
$(this).on('mouseover', function() { rcube_webmail.long_subject_title(this, 0, $('span.inner', this)); });
});
// Some plugins use 'listbubtton' class, we'll replace it with 'button'
$('.listbutton').each(function() {
var button = find_button(this.id);
$(this).addClass('button').removeClass('listbutton');
if (button.data.sel) {
button.data.sel = button.data.sel.replace('listbutton', 'button');
}
if (button.data.act) {
button.data.act = button.data.act.replace('listbutton', 'button');
}
rcmail.buttons[button.command][button.index] = button.data;
rcmail.init_button(button.command, button.data);
});
// buttons that should be hidden on small screen devices
$('[data-hidden]').each(function() {
var m, v = $(this).data('hidden'),
parent = $(this).parent('li'),
re = /(large|big|small|phone|lbs)/g;
while (m = re.exec(v)) {
$(parent.length ? parent : this).addClass('hidden-' + m[1]);
}
});
// Modify normal checkboxes on lists so they are different
// than those used for row selection, i.e. use icons
$('[data-list]').each(function() {
$('input[type=checkbox]', this).each(function() { pretty_checkbox(this); });
});
// Assign .formcontainer class to the iframe body, when it
// contains .formcontent and .formbuttons.
if (is_framed) {
$('.formcontent').each(function() {
if ($(this).next('.formbuttons').length) {
$(this).parent().addClass('formcontainer');
}
});
}
// move "Download all attachments" button into a better location
$('#attachment-list + a.zipdownload').appendTo('.header-links');
if (ios = $('html').is('.ipad,.iphone')) {
$('.iframe-wrapper, .scroller').addClass('ios-scroll');
}
if ($('html').filter('.ipad,.iphone,.webkit.mobile,.webkit.tablet').addClass('webkit-scroller').length) {
$(layout.menu).addClass('webkit-scroller');
}
// Set .notree class on treelist widget update
$('.treelist').each(function() {
var list = this, callback = function() {
$(list)[$('.treetoggle', list).length > 0 ? 'removeClass' : 'addClass']('notree');
};
if (window.MutationObserver) {
(new MutationObserver(callback)).observe(list, {childList: true, subtree: true});
}
callback();
// Add title with full folder name on hover
// TODO: This should be done in another way, so if an entry is
// added after page load it also works there.
$('li.mailbox > a').on('mouseover', function() { rcube_webmail.long_subject_title_ex(this); });
});
// Store default logo path if not already set
if (!$('#logo').data('src-default')) {
$('#logo').data('src-default', $('#logo').attr('src'));
}
};
/**
* Moves form buttons into the content frame actions toolbar (for mobile)
*/
function register_content_buttons(buttons)
{
// we need these buttons really only in phone mode
if (/*mode == 'phone' && */ env.frame_nav && buttons && buttons.length) {
var toolbar = env.frame_nav.children('.buttons');
content_buttons = [];
$.each(buttons, function() {
if (this.data('target')) {
content_buttons.push(this.data('target'));
}
});
toolbar.html('').append(buttons);
}
};
/**
* Registers cloned button
*/
function register_cloned_button(old_id, new_id, active_class)
{
var button = find_button(old_id);
if (button) {
rcmail.register_button(button.command, new_id, button.data.type, active_class, button.data.sel);
}
};
/**
* Create a button clone for use in toolbar
*/
function create_cloned_button(target, menu_button, add_class, always_active)
{
var popup, click = true,
button = $('<a>'),
target_id = target.attr('id') || new Date().getTime(),
button_id = target_id + '-clone',
btn_class = target[0].className + (add_class ? ' ' + add_class : '');
if (!menu_button) {
btn_class = $.trim(btn_class.replace('btn-primary', 'primary').replace(/(btn[a-z-]*|button|disabled)/g, ''))
btn_class += ' button' + (!always_active ? ' disabled' : '');
}
else if (popup = target.data('popup')) {
button.data({popup: popup, 'toggle-button': target.data('toggle-button')});
popup_init(button[0]);
click = false;
rcmail.register_menu_button(button[0], popup);
}
button.attr({id: button_id, href: '#', 'class': btn_class})
.append($('<span class="inner">').text(target.text()));
if (click) {
button.on('click', function(e) { target.click(); });
}
if (is_framed && !menu_button) {
button.data('target', target);
frame_buttons.push($.extend({button_id: button_id}, find_button(target[0].id)));
}
else {
// Register the button to get active state updates
register_cloned_button(target_id, button_id, btn_class.replace(' disabled', ''));
}
return button;
};
/**
* Finds an rcmail button
*/
function find_button(id)
{
var i, button, command;
for (command in rcmail.buttons) {
for (i = 0; i < rcmail.buttons[command].length; i++) {
button = rcmail.buttons[command][i];
if (button.id == id) {
return {
command: command,
index: i,
data: button
};
}
}
}
};
/**
* Setup environment
*/
function layout_init()
{
// Select current layout element
env.last_selected = $('#layout > div.selected')[0];
if (!env.last_selected && layout.content.length) {
$.each(['sidebar', 'list', 'content'], function() {
if (layout[this].length) {
env.last_selected = layout[this][0];
layout[this].addClass('selected');
return false;
}
});
}
// Register resize handler
$(window).on('resize', function() {
clearTimeout(env.resize_timeout);
env.resize_timeout = setTimeout(function() { resize(); }, 25);
});
// Enable rcmail.open_window intercepting
env.open_window = rcmail.open_window;
rcmail.open_window = window_open;
rcmail
.addEventListener('message', message_displayed)
.addEventListener('menu-open', menu_toggle)
.addEventListener('menu-close', menu_toggle)
.addEventListener('editor-init', tinymce_init)
.addEventListener('autocomplete_create', rcmail_popup_init)
.addEventListener('googiespell_create', rcmail_popup_init)
.addEventListener('setquota', update_quota)
.addEventListener('enable-command', enable_command_handler)
.addEventListener('init', init);
// Add styling for TinyMCE editor popups
// We need to use MutationObserver, as TinyMCE does not provide any events for this
if (window.MutationObserver && window.tinymce) {
var callback = function(list) {
$.each(list, function() {
$.each(this.addedNodes, function() {
tinymce_style(this);
});
});
};
(new MutationObserver(callback)).observe(document.body, {childList: true});
}
};
/**
* rcmail 'init' event handler
*/
function init()
{
// Additional functionality on list widgets
$('[data-list]').filter('ul,table').each(function() {
var button,
table = $(this),
list = table.data('list');
if (rcmail[list] && rcmail[list].multiselect) {
var repl, button,
parent = table.parents('.sidebar,.list,.content').last(),
header = parent.find('.header'),
toolbar = header.find('ul');
if (!toolbar.length) {
toolbar = header;
}
else if (button = toolbar.find('a.button.select').data('toggle-button')) {
button = $('#' + button);
}
// Enable checkbox selection on list widgets
rcmail[list].enable_checkbox_selection();
// Add Select button to the list navigation bar
if (!button) {
button = $('<a>').attr({'class': 'button select disabled', role: 'button', title: rcmail.gettext('select')})
.on('click', function() { if ($(this).is('.active')) table.toggleClass('withselection'); })
.append($('<span class="inner">').text(rcmail.gettext('select')));
if (toolbar.is('.toolbar') || toolbar.is('.toolbarmenu')) {
button.prependTo(toolbar).wrap('<li role="menuitem">');
// Add a button to the content toolbar menu too
if (layout.content) {
var button2 = create_cloned_button(button, true, 'hidden-big hidden-large');
$('<li role="menuitem">').append(button2).appendTo('#toolbar-menu');
button = button.add(button2);
}
}
else {
if (repl = table.data('list-select-replace')) {
$(repl).replaceWith(button);
}
else {
button.appendTo(toolbar).addClass('icon');
if (!parent.is('.sidebar')) {
button.addClass('toolbar-button');
}
}
}
}
// Update Select button state on list update
rcmail.addEventListener('listupdate', function(prop) {
if (prop.list && prop.list == rcmail[list]) {
if (prop.rowcount) {
button.addClass('active').removeClass('disabled').attr('tabindex', 0);
}
else {
button.removeClass('active').addClass('disabled').attr('tabindex', -1);
}
}
});
}
// https://github.com/roundcube/elastic/issues/45
// Draggable blocks scrolling on touch devices, we'll disable it there
if (touch && rcmail[list]) {
if (typeof rcmail[list].draggable == 'function') {
rcmail[list].draggable('destroy');
}
else if (typeof rcmail[list].draggable == 'boolean') {
rcmail[list].draggable = false;
}
}
});
// Display "List is empty..." on the list
if (window.MutationObserver) {
$('[data-label-msg]').filter('ul,table').each(function() {
var fn, observer, callback,
info = $('<div class="listing-info hidden">').insertAfter(this),
table = $(this),
fn = function() {
var ext, command,
msg = table.data('label-msg'),
list = table.is('ul') ? table : table.children('tbody');
if (!rcmail.env.search_request && !rcmail.env.qsearch
&& msg && !list.children(':visible').length
) {
ext = table.data('label-ext');
command = table.data('create-command');
if (ext && (!command || rcmail.commands[command])) {
msg += ' ' + ext;
}
info.text(msg).removeClass('hidden');
return;
}
info.addClass('hidden');
};
callback = function() {
// wait until the UI stops loading and the list is visible
if (rcmail.busy || !table.is(':visible')) {
return setTimeout(callback, 250);
}
clearTimeout(env.list_timer);
env.list_timer = setTimeout(fn, 50);
};
// show/hide the message when something changes on the list
observer = new MutationObserver(callback);
observer.observe(table[0], {childList: true, subtree: true, attributes: true, attributeFilter: ['style']});
// initialize the message
callback();
});
}
// Create floating action button(s)
if ((layout.list.length || layout.content.length) && is_mobile()) {
var fabuttons = [];
$('[data-fab]').each(function() {
var button = $(this),
task = button.data('fab-task') || '*',
action = button.data('fab-action') || '*';
if ((task == '*' || task == rcmail.task)
&& (action == '*' || action == rcmail.env.action || (action == 'none' && !rcmail.env.action))
) {
fabuttons.push(create_cloned_button(button, false, false, true));
}
});
if (fabuttons.length) {
$('<div class="floating-action-buttons">').append(fabuttons)
.appendTo(layout.list.length ? layout.list : layout.content);
}
}
// Add menu link for each attachment
if (rcmail.env.action != 'print') {
$('#attachment-list > li').each(function() {
attachmentmenu_append(this);
});
}
var phone_confirmation = function(label) {
if (mode == 'phone') {
rcmail.display_message(rcmail.gettext(label), 'confirmation');
}
};
rcmail.addEventListener('fileappended', function(e) {
if (e.attachment.complete) {
attachmentmenu_append(e.item);
if (e.attachment.mimetype == 'text/vcard' && rcmail.commands['attach-vcard']) {
phone_confirmation('vcard_attachments.vcardattached');
}
}
})
.addEventListener('managesieve.insertrow', function(o) { bootstrap_style(o.obj); })
.addEventListener('add-recipient', function() { phone_confirmation('recipientsadded'); });
rcmail.init_pagejumper('.pagenav > input');
if (rcmail.task == 'mail') {
if (rcmail.env.action == 'compose') {
// In compose window we do not provide "Back' button, instead
// we modify the Mail button in the task menu to act like it (i.e. calls 'list' command)
if (!rcmail.env.extwin) {
$('a.button.mail', layout.menu).attr('onclick', "return rcmail.command('list','',this,event)");
}
rcmail.addEventListener('compose-encrypted', function(e) {
$("a.mode-html, button.attach").prop('disabled', e.active);
$('a.button.attach, a.button.responses')[e.active ? 'addClass' : 'removeClass']('disabled');
});
$('.sidebar > .footer:not(.pagenav) > a.button').click(function() {
if ($(this).is('.disabled')) {
rcmail.display_message(rcmail.gettext('nocontactselected'), 'warning');
}
});
// Update compose status bar on attachments list update
if (window.MutationObserver) {
var observer, list = $('#attachment-list'),
status_callback = function() { compose_status('attach', list.children().length > 0); };
observer = new MutationObserver(status_callback);
observer.observe(list[0], {childList: true});
status_callback();
}
}
// Append contact menu to all mailto: links
if (rcmail.env.action == 'preview' || rcmail.env.action == 'show') {
$('a').filter('[href^="mailto:"]').each(function() {
mailtomenu_append(this);
});
}
}
else if (rcmail.task == 'settings') {
rcmail.addEventListener('identity-encryption-show', function(p) {
bootstrap_style(p.container);
});
rcmail.addEventListener('identity-encryption-update', function(p) {
bootstrap_style(p.container);
});
}
rcmail.set_env({
thread_padding: '1.5rem',
// increase popup windows, so they do not switch to tablet mode
popup_width_small: 1025,
popup_width: 1200
});
// Update layout after initialization (again)
// In devel mode we have to wait until all styles are applied by less
if (rcmail.env.devel_mode && window.less) {
less.pageLoadFinished.then(function() {
resize();
});
}
else {
resize();
}
// Add date format placeholder to datepicker inputs
var func, format = rcmail.env.date_format_localized;
if (format) {
func = function(input) {
$(input).filter('.datepicker').attr('placeholder', format);
// also make selects pretty
$(input).parent().find('select').each(function() { pretty_select(this); });
};
$('input.datepicker').each(function() { func(this); });
rcmail.addEventListener('insert-edit-field', func);
}
};
/**
* Apply bootstrap classes to html elements
*/
function bootstrap_style(context)
{
if (!context) {
context = document;
}
// Buttons
$('input.button,button', context).not('.btn').addClass('btn').not('.btn-primary,.primary,.mainaction').addClass('btn-secondary');
$('input.button.mainaction,button.primary,button.mainaction', context).addClass('btn-primary');
$('button.btn.delete,button.btn.discard', context).addClass('btn-danger');
$.each(['warning', 'error', 'information', 'confirmation'], function() {
var type = this;
$('.box' + type + ':not(.ui.alert)', context).each(function() {
alert_style(this, type, true);
});
});
// Convert structure of single dialogs (one input or just an image),
// e.g. group create, attachment rename where we use <label>Label<input></label>
if (context != document && $('.popup', context).children().length == 1) {
var content = $('.popup', context).children().first();
if (content.is('img')) {
$('.popup', context).addClass('justified');
}
else if (content.is('label')) {
var input = content.find('input').detach(),
label = content.detach(),
id = input.attr('id');
if (!id) {
input.attr('id', id = 'dialog-input-elastic');
}
$('.popup', context).addClass('formcontent').append(
$('<div class="form-group row">')
.append(label.attr('for', id).addClass('col-sm-2 col-form-label'))
.append($('<div class="col-sm-10">').append(input))
);
input.focus();
}
}
// Forms
var supported_controls = 'input:not(.button,.no-bs,[type=button],[type=radio],[type=checkbox]),textarea';
$(supported_controls, $('.propform', context)).addClass('form-control');
$('[type=checkbox]', $('.propform', context)).addClass('form-check-input');
// Note: On selects we add form-control to get consistent focus
// and to not have to create separate rules for selects and inputs
$('select', context).addClass('form-control custom-select');
if (context != document) {
$(supported_controls, context).addClass('form-control');
}
$('table.propform', context).each(function() {
var text_rows = 0, form_rows = 0;
$(this).find('> tbody > tr, > tr').each(function() {
var first, last, row = $(this),
row_classes = ['form-group', 'row'],
cells = row.children('td');
if (cells.length == 2) {
first = cells.first();
last = cells.last();
$('label', first).addClass('col-form-label');
first.addClass('col-sm-4');
last.addClass('col-sm-8');
if (last.find('[type=checkbox]').length == 1 && !last.find('.proplist').length) {
row_classes.push('form-check');
if (last.find('a').length) {
row_classes.push('with-link');
}
form_rows++;
}
else if (!last.find('input:not([type=hidden]),textarea,radio,select').length) {
last.addClass('form-control-plaintext');
text_rows++;
}
else {
form_rows++;
}
// style some multi-input fields
if (last.children('.datepicker') && last.children('input').length == 2) {
last.addClass('datetime');
}
}
else if (cells.length == 1) {
cells.css('width', '100%');
}
row.addClass(row_classes.join(' '));
});
if (text_rows > form_rows) {
$(this).addClass('text-only');
}
});
// Special input + anything entry
$('td.input-group', context).each(function() {
$(this).children().slice(1).addClass('input-group-append');
});
// Other forms, e.g. Contact advanced search
$('fieldset.propform:not(.groupped) div.row', context).each(function() {
var has_input = $('input:not([type=hidden]),select,textarea', this).length > 0;
if (has_input) {
$(supported_controls, this).addClass('form-control');
}
$(this).children().last().addClass('col-sm-8' + (!has_input ? ' form-control-plaintext' : ''));
$(this).children().first().addClass('col-sm-4 col-form-label');
$(this).addClass('form-group');
});
// Contact info/edit form
$('fieldset.propform.groupped fieldset', context).each(function() {
$('.row', this).each(function() {
var label, first,
has_input = $('input,select,textarea', this).length > 0,
items = $(this).children();
if (has_input) {
$(supported_controls, this).addClass('form-control');
}
if (items.length < 2) {
return;
}
first = items.first();
if (first.is('select')) {
first.addClass('input-group-prepend');
}
else {
first.wrap('<span class="input-group-prepend">').addClass('input-group-text');
}
if (!has_input) {
items.last().addClass('form-control-plaintext');
}
$('.content', this).addClass('input-group-prepend input-group-append input-group-text');
$('a.deletebutton', this).addClass('input-group-text icon delete').wrap('<span class="input-group-append">');
$(this).addClass('input-group');
});
});
// Advanced options form
$('fieldset.advanced', context).each(function() {
var table = $(this).children('.propform').first();
table.wrap($('<div>').addClass('collapse'));
$(this).children('legend').first().addClass('closed').on('click', function() {
table.parent().collapse('toggle');
$(this).toggleClass('closed');
});
});
// Other forms, e.g. Insert response
$('.propform > .prop.block:not(.row)', context).each(function() {
$(this).addClass('form-group row').each(function() {
$('label', this).addClass('col-form-label').wrap($('<div class="col-sm-4">'));
$('input,select,textarea', this).wrap($('<div class="col-sm-8">'));
$(supported_controls, this).addClass('form-control');
});
});
$('td.rowbuttons > a', context).addClass('btn');
// Testing Bootstrap Tabs on contact info/edit page
// Tabs do not scale nicely on very small screen, so can be used
// only with small number of tabs with short text labels
$('form.tabbed,div.tabbed', context).each(function(idx, item) {
var tabs = [], nav = $('<ul>').attr({'class': 'nav nav-tabs', role: 'tablist'});
$(this).addClass('tab-content').children('fieldset').each(function(i, fieldset) {
var tab, id = fieldset.id || ('tab' + idx + '-' + i),
tab_class = $(fieldset).data('navlink-class');
$(fieldset).addClass('tab-pane').attr({id: id, role: 'tabpanel'});
tab = $('<li>').addClass('nav-item').append(
$('<a>').addClass('nav-link' + (tab_class ? ' ' + tab_class : ''))
.attr({role: 'tab', 'href': '#' + id})
.text($('legend', fieldset).first().text())
.click(function(e) {
$(this).tab('show');
// Because we return false we have to close popups
popups_close(e);
// Returning false here prevents from strange scrolling issue
// when the form is in an iframe, e.g. contact edit form
return false;
})
);
$('legend', fieldset).first().hide();
tabs.push(tab);
});
// create the navigation bar
nav.append(tabs).insertBefore(item);
// activate the first tab
$('a.nav-link', nav).first().click();
});
$('input[type=file]:not(.custom-file-input)', context).each(function() {
var label_text = rcmail.gettext('choosefile' + (this.multiple ? 's' : '')),
label = $('<label>').attr({'class': 'custom-file-label',
'data-browse': rcmail.gettext('browse')}).text(label_text);
$(this).addClass('custom-file-input').wrap('<div class="custom-file">');
$(this).on('change', function() {
var text = label_text;
if (this.files.length) {
text = this.files[0].name;
if (this.files.length > 1) {
text += ', ...';
}
}
// Note: We don't use label variable to allow cloning of the input
$(this).next().text(text);
})
.parent().append(label);
});
// Make tables pretier
$('table:not(.table,.compact-table,.propform,.listing,.ui-datepicker-calendar)', context)
.filter(function() {
// exclude direct propform children and external content
return !$(this).parent().is('.propform')
&& !$(this).parents('.message-htmlpart,.message-partheaders,.boxinformation,.raw-tables').length;
})
.each(function() {
// TODO: Consider implementing automatic setting of table-responsive on window resize
var table = $(this).addClass('table');
table.parent().addClass('table-responsive-sm');
table.find('thead').addClass('thead-default');
});
// The same for some other checkboxes
// We do this here, not in setup() because we want to cover dialogs
$('input.pretty-checkbox, .propform input[type=checkbox], .form-check input, .popupmenu.form input[type=checkbox], .toolbarmenu input[type=checkbox]', context)
.each(function() { pretty_checkbox(this); });
// Also when we add action-row of the form, e.g. Managesieve plugin adds them after the page is ready
if ($(context).is('.actionrow')) {
$('input[type=checkbox]', context).each(function() { pretty_checkbox(this); });
}
// Input-group combo is an element with a select field on the left
// and input(s) on right, and where the whole right side can be hidden
// depending on the select position. This code fixes border radius on select
$('.input-group-combo > select', context).first().on('change', function() {
var select = $(this),
fn = function() {
select[select.next().is(':visible') ? 'removeClass' : 'addClass']('alone');
};
setTimeout(fn, 50);
setTimeout(fn, 2000); // for devel mode
}).trigger('change');
// Make message-objects alerts pretty (the same as UI alerts)
$('#message-objects', context).children(':not(.ui.alert)').add('.part-notice').each(function() {
// message objects with notice class are really warnings
var cl = $(this).removeClass('notice part-notice').attr('class').split(/\s/)[0] || 'warning';
alert_style(this, cl);
$(this).addClass('box' + cl);
$('a', this).addClass('btn btn-primary btn-sm');
});
// Form validation errors (managesieve plugin)
$('.error', context).addClass('is-invalid');
// Make logon form prettier
if (rcmail.env.task == 'login' && context == document) {
$('#rcmloginsubmit').addClass('btn-lg text-uppercase w-100');
$('#login-form table tr').each(function() {
var input = $('input,select', this),
label = $('label', this),
icon_name = input.data('icon'),
icon = $('<i>').attr('class', 'input-group-text icon ' + input.attr('name').replace('_', ''));
if (icon_name) {
icon.addClass(icon_name);
}
$(this).addClass('form-group row');
label.parent().css('display', 'none');
input.addClass(input.is('select') ? 'custom-select' : 'form-control')
.attr('placeholder', label.text())
.before($('<span class="input-group-prepend">').append(icon))
.parent().addClass('input-group input-group-lg');
});
}
$('select:not([multiple])', context).each(function() { pretty_select(this); });
};
/**
* Detects if the element is TinyMCE dialog/menu
* and adds Elastic styling to it
*/
function tinymce_style(elem)
{
// TinyMCE dialog widnows
if ($(elem).is('.mce-window')) {
var body = $(elem).find('.mce-window-body'),
foot = $(elem).find('.mce-foot > .mce-container-body');
// Apply basic forms style
if (body.length) {
bootstrap_style(body[0]);
}
body.find('button').filter(function() { return $(this).parent('.mce-btn').length > 0; }).removeClass('btn btn-secondary');
// Fix icons in Find and Replace dialog footer
if (foot.children('.mce-widget').length === 5) {
foot.addClass('mce-search-foot');
}
// Apply some form structure fixes and helper classes
$(elem).find('.mce-charmap').parent().parent().addClass('mce-charmap-dialog');
$(elem).find('.mce-combobox').each(function() {
if (!$(this).children('.mce-btn').length) {
$(this).addClass('mce-combobox-fake');
}
});
$(elem).find('.mce-form > .mce-container-body').each(function() {
if ($(this).children('.mce-formitem').length > 4) {
$(this).addClass('mce-form-split');
}
});
$(elem).find('.mce-form').next(':not(.mce-formitem)').addClass('mce-form');
// Fix dialog height (e.g. Table properties dialog)
if (!is_mobile()) {
var offset, max_height = 0, height = body.height();
$(elem).find('.mce-form').each(function() {
max_height = Math.max(max_height, $(this).height());
});
if (height < max_height) {
max_height += (body.find('.mce-tabs').height() || 0) + 25;
body.height(max_height);
$(elem).height($(elem).height() + (max_height - height));
$(elem).css('top', ($(window).height() - $(elem).height())/2 + 'px');
}
}
}
// TinyMCE menus on mobile
else if ($(elem).is('.mce-menu')) {
$(elem).prepend(
$('<h3 class="popover-header">').append(
$('<a class="button icon "' + 'cancel' + '">')
.text(rcmail.gettext('close'))
.on('click', function() { $(document.body).click(); })));
if (window.MutationObserver) {
var callback = function() {
if (mode != 'phone') {
return;
}
if (!$('.mce-menu:visible').length) {
$('div.mce-overlay').click();
}
else if (!$('div.mce-overlay').length) {
$('<div>').attr('class', 'popover-overlay mce-overlay')
.appendTo('body')
.click(function() { $(this).remove(); });
}
};
(new MutationObserver(callback)).observe(elem, {attributes: true});
}
}
};
/**
* Initializes popup menus
*/
function dropdowns_init()
{
$('[data-popup]').each(function() { popup_init(this); });
$(document).on('click', popups_close);
rcube_webmail.set_iframe_events({mousedown: popups_close, touchstart: popups_close});
};
/**
* Init content frame
*/
function content_frame_init()
{
var last_selected = env.last_selected,
title_reset = function(title) {
if (typeof title !== 'string' || !title.length) {
title = $('h1.voice').text() || $('title').text() || '';
}
$('.header > .header-title', layout.content).text(title);
};
// display or reset the content frame
var common_content_handler = function(e, href, show, title)
{
if (is_mobile()) {
content_frame_navigation(href, e);
}
if (show && !layout.content.is(':visible')) {
env.last_selected = layout.content[0];
screen_resize();
if (title) {
title_reset(title);
}
}
else if (!show) {
if (env.last_selected != last_selected && !env.content_lock) {
env.last_selected = last_selected;
screen_resize();
}
title_reset();
}
env.content_lock = false;
};
var common_list_handler = function(e) {
if (mode != 'large' && !env.content_lock && e.force) {
show_list();
}
env.content_lock = false;
// display current folder name in list header
if (e.title) {
$('.header > .header-title', layout.list).text(e.title);
}
};
var list_handler = function(e) {
var args = {};
if (rcmail.env.task == 'addressbook' || rcmail.env.task == 'mail') {
args.force = true;
}
// display current folder name in list header
if (rcmail.env.task == 'mail' && !rcmail.env.action) {
var name = $.type(e) == 'string' ? e : rcmail.env.mailbox,
folder = rcmail.env.mailboxes[name];
args.title = folder ? folder.name : '';
}
common_list_handler(args);
};
// when loading content-frame in small-screen mode display it
layout.content.find('iframe').on('load', function(e) {
var href = '', show = true;
// Reset the scroll position of the iframe-wrapper
$(this).parent('.iframe-wrapper').scrollTop(0);
try {
href = e.target.contentWindow.location.href;
show = !href.endsWith(rcmail.env.blankpage);
// Reset title back to the default
$(e.target.contentWindow).on('unload', title_reset);
}
catch(e) { /* ignore */ }
common_content_handler(e, href, show);
});
rcmail
.addEventListener('afterlist', list_handler)
.addEventListener('afterlistgroup', list_handler)
.addEventListener('afterlistsearch', list_handler)
// plugins
.addEventListener('show-list', function(e) {
e.force = true;
common_list_handler(e);
})
.addEventListener('show-content', function(e) {
if (!$(e.obj).is('iframe')) {
$(e.scrollElement || e.obj).scrollTop(0);
if (is_mobile()) {
iframe_loader(e.obj);
}
}
common_content_handler(e.event || new Event, '_action=' + (e.mode || 'edit'), true, e.title);
});
};
/**
* Content frame navigation
*/
function content_frame_navigation(href, event)
{
// Don't display navigation for create/add action frames
if (href.match(/_action=(create|add)/) || href.match(/_nav=hide/)) {
$(env.frame_nav).addClass('hide-nav-buttons');
return;
}
var node, uid, list, _list = $('[data-list]', layout.list).data('list');
if (!_list || !(list = rcmail[_list])) {
// hide navbar if there are no visible buttons, e.g. Help plugin UI
if ($(env.frame_nav).is('.hide-nav-buttons') && !$('.buttons', env.frame_nav).children().length) {
$(env.frame_nav).addClass('hidden');
}
return;
}
$(env.frame_nav).removeClass('hide-nav-buttons hidden');
// expand collapsed row so we do not skip the whole thread
// TODO: Unified interface for list and treelist widgets
if (uid = list.get_single_selection()) {
if (list.rows && list.rows[uid] && !list.rows[uid].expanded) {
list.expand_row(event, uid);
}
else if (list.get_node && (node = list.get_node(uid)) && node.collapsed) {
list.expand(uid);
}
}
var prev, next,
frame = $('#' + rcmail.env.contentframe),
next_button = $('a.button.next', env.frame_nav).off('click').addClass('disabled'),
prev_button = $('a.button.prev', env.frame_nav).off('click').addClass('disabled');
if ((next = list.get_next()) || rcmail.env.current_page < rcmail.env.pagecount) {
next_button.removeClass('disabled').on('click', function() {
env.content_lock = true;
iframe_loader(frame);
if (next) {
list.select(next);
}
else {
rcmail.env.list_uid = 'FIRST';
rcmail.command('nextpage');
}
});
}
if (((prev = list.get_prev()) && (prev != '*' || _list != 'subscription_list')) || rcmail.env.current_page > 1) {
prev_button.removeClass('disabled').on('click', function() {
env.content_lock = true;
iframe_loader(frame);
if (prev) {
list.select(prev);
}
else {
rcmail.env.list_uid = 'LAST';
rcmail.command('previouspage');
}
});
}
};
/**
* Handler for editor-init event
*/
function tinymce_init(o)
{
// Enable autoresize plugin
o.config.plugins += ' autoresize';
if (is_touch()) {
// Make the toolbar icons bigger
o.config.toolbar_items_size = null;
// Use minimalistic toolbar
o.config.toolbar = 'undo redo | insert | styleselect';
if (o.config.plugins.match(/emoticons/)) {
o.config.toolbar += ' emoticons';
}
}
if (rcmail.task == 'mail' && rcmail.env.action == 'compose') {
var form = $('#compose-content > form'),
keypress = function(e) {
if (e.key == 'Tab' && e.shiftKey) {
$('#compose-content > form').scrollTop(0);
}
};
// Shift+Tab on mail compose editor scrolls the page to the top
o.config.setup_callback = function(ed) {
ed.on('keypress', keypress);
};
$('#composebody').on('keypress', keypress);
// Keep the editor toolbar on top of the screen on scroll
form.on('scroll', function() {
var container = $('.mce-container-body', form),
toolbar = $('.mce-top-part', container),
editor_offset = container.offset(),
header_top = form.offset().top;
if (editor_offset && (editor_offset.top - header_top < 0)) {
toolbar.css({position: 'fixed', top: header_top + 'px', width: container.width() + 'px'});
}
else {
toolbar.css({position: 'relative', top: 0, width: 'auto'})
}
});
$(window).resize(function() { form.trigger('scroll'); });
}
};
function datepicker_init(datepicker)
{
// Datepicker widget improvements: overlay element, styling updates on calendar element update
// The widget does not provide any event system, so we use MutationObserver
if (window.MutationObserver) {
$(datepicker).not('[data-observed]').each(function() {
var overlay, hidden = true,
win = is_framed ? parent : window,
callback = function(data) {
$.each(data, function(i, v) {
// add/remove overlay on widget show/hide
if (v.type == 'attributes') {
var is_hidden = $(v.target).attr('aria-hidden') == 'true';
if (is_hidden != hidden) {
if (!is_hidden) {
overlay = $('<div>').attr('class', 'ui-widget-overlay datepicker')
.appendTo(win.document.body)
.click(function(e) {
$(this).remove();
if (is_framed) {
$.datepicker._hideDatepicker();
}
});
}
else if (overlay) {
overlay.remove();
}
hidden = is_hidden;
}
}
else if (v.addedNodes.length) {
// apply styles when widget content changed
win.UI.bootstrap_style(v.target);
// Month/Year change handlers do not work from parent, fix it
if (is_framed) {
win.$('select.ui-datepicker-month', v.target).on('change', function() {
$.datepicker._selectMonthYear($.datepicker._lastInput, this, "M");
});
win.$('select.ui-datepicker-year', v.target).on('change', function() {
$.datepicker._selectMonthYear($.datepicker._lastInput, this, "Y");
});
}
}
});
};
$(this).attr('data-observed', '1');
if (is_framed) {
// move the datepicker to parent window
$(this).detach().appendTo(parent.document.body);
// create fake element, so the valid one is not removed by datepicker code
$('<div id="ui-datepicker-div" class="hidden">').appendTo(document.body);
}
(new MutationObserver(callback)).observe(this, {childList: true, subtree: false, attributes: true, attributeFilter: ['aria-hidden']});
});
}
};
/**
* Handler for some Roundcube core popups
*/
function rcmail_popup_init(o)
{
// Add some common styling to the autocomplete/googiespell popups
$('table,ul', o.obj).addClass('toolbarmenu listing iconized');
$(o.obj).addClass('popupmenu popover');
bootstrap_style(o.obj);
// for googiespell list
$('input', o.obj).addClass('form-control');
// Modify the googiespell menu on mobile
if (is_mobile() && $(o.obj).is('.googie_window')) {
// Set popup Close title
var title = rcmail.gettext('close'),
class_name = 'button icon cancel',
close_link = $('<a>').attr('class', class_name).text(title)
.click(function(e) {
e.stopPropagation();
$('.popover-overlay').remove();
$(o.obj).hide();
});
$('<h3 class="popover-header">').append(close_link).prependTo(o.obj);
// add overlay element for phone layout
if (!$('.popover-overlay').length) {
$('<div>').attr('class', 'popover-overlay')
.appendTo('body')
.click(function() { $(this).remove(); });
}
$('table,button', o.obj).click(function(e) {
if (!$(e.target).is('input')) {
$('.popover-overlay').remove();
}
});
}
};
/**
* Handler for 'enable-command' event
*/
function enable_command_handler(args)
{
if (is_framed) {
$.each(frame_buttons, function(i, button) {
if (args.command == button.command) {
parent.$('#' + button.button_id)[args.status ? 'removeClass' : 'addClass']('disabled');
}
});
}
if (rcmail.task == 'mail') {
switch (args.command) {
case 'reply-list':
if (rcmail.env.reply_all_mode == 1) {
var label = rcmail.gettext(args.status ? 'replylist' : 'replyall');
$('a.button.reply-all').attr('title', label).find('.inner').text(label);
}
break;
case 'compose-encrypted':
// show the toolbar button for Mailvelope
if (args.status) {
$('a.button.encrypt:not(.icon)').parent().show();
}
break;
case 'compose-encrypted-signed':
// enable selector for encrypt and sign
$('#encryption-menu-button').show();
break;
}
}
};
/**
* screen mode
*/
function screen_mode()
{
var size, width = $(window).width();
if (width <= 480)
size = 'phone';
else if (width > 1200)
size = 'large';
else if (width > 768)
size = 'normal';
else
size = 'small';
touch = width <= 1024;
mode = size;
};
/**
* Window resize handler
* Does layout reflows e.g. on screen orientation change
*/
function resize()
{
var mobile;
screen_mode();
screen_resize();
screen_resize_html();
// disable ext-windows and other features
if (mobile = is_mobile()) {
rcmail.set_env(env.small_screen_config);
rcmail.enable_command('extwin', false);
}
else {
rcmail.set_env(env.config);
rcmail.enable_command('extwin', true);
}
// Hide content frame buttons on small devices (with frame toolbar in parent window)
$.each(content_buttons, function() { $(this)[mobile ? 'hide' : 'show'](); });
};
function screen_resize()
{
if (is_framed && !layout.sidebar.length && !layout.list.length) {
return;
}
switch (mode) {
case 'phone': screen_resize_phone(); break;
case 'small': screen_resize_small(); break;
case 'normal': screen_resize_normal(); break;
case 'large': screen_resize_large(); break;
}
screen_resize_logo(mode);
screen_resize_headers();
// On iOS and Android the content frame height is never correct, fix it
if (bw.webkit) {
$('.iframe-wrapper').each(function() {
var h = $(this).height();
if (h) {
$(this).children('iframe').height(h);
}
});
}
};
/**
* Assigns layout-* and touch-mode class to the 'html' element
*
* If we're inside an iframe that is small we have to
* check if the parent window is also small (mobile).
* We use that e.g. to still display desktop-like popovers in dialogs
*/
function screen_resize_html()
{
var meta = layout_metadata(),
html = $(document.documentElement);
if (html[0].className.match(/layout-([a-z]+)/)) {
if (RegExp.$1 != meta.mode) {
html.removeClass('layout-' + RegExp.$1)
.addClass('layout-' + meta.mode);
}
}
else {
html.addClass('layout-' + meta.mode);
}
if (meta.touch && !html.is('.touch')) {
html.addClass('touch');
}
else if (!meta.touch && html.is('.touch')) {
html.removeClass('touch');
}
};
function screen_resize_logo(mode)
{
if (mode == 'phone' && $('#logo').data('src-small')) {
$('#logo').attr('src', $('#logo').data('src-small'));
}
else {
$('#logo').attr('src', $('#logo').data('src-default'));
}
}
/**
* Sets left and right margin to the header title element to make it
* properly centered depending on the number of buttons on both sides
*/
function screen_resize_headers()
{
$('#layout > div > .header').each(function() {
var title, right = 0, left = 0, padding = 0,
sizes = {left: 0, right: 0};
$(this).children(':visible').each(function() {
if (!title && $(this).is('.header-title')) {
title = $(this);
return;
}
sizes[title ? 'right' : 'left'] += this.offsetWidth;
});
if (padding + sizes.right >= sizes.left) {
right = 0;
left = sizes.right + padding - sizes.left;
}
else {
left = 0;
right = sizes.left - (padding + sizes.right);
}
$(title).css({
'margin-right': right + 'px',
'margin-left': left + 'px',
'padding-right': padding + 'px'
});
});
};
function screen_resize_phone()
{
screen_resize_small_all();
app_menu(false);
};
function screen_resize_small()
{
screen_resize_small_all();
app_menu(true);
};
function screen_resize_normal()
{
var show;
if (layout.list.length) {
show = layout.list.is(env.last_selected) || (!layout.sidebar.is(env.last_selected) && !layout.sidebar.is('.layout-sticky'));
layout.list[show ? 'removeClass' : 'addClass']('hidden');
}
if (layout.sidebar.length) {
show = !layout.list.length || layout.sidebar.is(env.last_selected) || layout.sidebar.is('.layout-sticky');
layout.sidebar[show ? 'removeClass' : 'addClass']('hidden');
}
layout.content.removeClass('hidden');
app_menu(true);
screen_resize_small_none();
if (layout.list) {
$('.header > ul.toolbar', layout.list).addClass('popupmenu toolbarmenu').removeClass('toolbar');
}
};
function screen_resize_large()
{
$.each(layout, function(name, item) { item.removeClass('hidden'); });
screen_resize_small_none();
if (layout.list) {
$('.header > ul.toolbarmenu.popupmenu', layout.list).removeClass('popupmenu toolbarmenu').addClass('toolbar');
}
};
function screen_resize_small_all()
{
var show, got_content = false;
if (layout.content.length) {
show = got_content = layout.content.is(env.last_selected);
layout.content[show ? 'removeClass' : 'addClass']('hidden');
$('.header > ul.toolbar', layout.content).addClass('popupmenu');
}
if (layout.list.length) {
show = !got_content && layout.list.is(env.last_selected);
layout.list[show ? 'removeClass' : 'addClass']('hidden');
$('.header > ul.toolbar', layout.list).addClass('popupmenu');
}
if (layout.sidebar.length) {
show = !got_content && (layout.sidebar.is(env.last_selected) || !layout.list.length);
layout.sidebar[show ? 'removeClass' : 'addClass']('hidden');
}
if (got_content) {
buttons.back_list.show();
}
};
function screen_resize_small_none()
{
buttons.back_list.filter(function() { return $(this).parents('.sidebar').length == 0; }).hide();
$('ul.toolbar.popupmenu').removeClass('popupmenu');
};
function show_content(unsticky)
{
// show sidebar and hide list
layout.list.addClass('hidden');
layout.sidebar.addClass('hidden');
layout.content.removeClass('hidden');
if (unsticky) {
layout.sidebar.removeClass('layout-sticky');
}
screen_resize_headers();
env.last_selected = layout.content[0];
};
function show_sidebar(sticky)
{
// show sidebar and hide list
layout.list.addClass('hidden');
layout.sidebar.removeClass('hidden');
if (sticky) {
layout.sidebar.addClass('layout-sticky');
}
if (mode == 'small' || mode == 'phone') {
layout.content.addClass('hidden');
}
screen_resize_headers();
env.last_selected = layout.sidebar[0];
};
function show_list(scroll)
{
if (!layout.list.length && !layout.sidebar.length) {
history.back();
}
else {
// show list and hide sidebar and content
layout.sidebar.addClass('hidden').removeClass('layout-sticky');
layout.list.removeClass('hidden');
if (mode == 'small' || mode == 'phone') {
hide_content();
}
if (scroll) {
layout.list.children('.scroller').scrollTop(0);
}
env.last_selected = layout.list[0];
}
screen_resize_headers();
};
function hide_content()
{
// show sidebar or list, hide content frame
env.last_selected = layout.list[0] || layout.sidebar[0];
screen_resize();
// reset content frame, so we can load it again
rcmail.show_contentframe(false);
// now we have to unselect selected row on the list
$('[data-list]', layout.list).each(function() {
var list = $(this).data('list');
if (rcmail[list]) {
if (rcmail[list].clear_selection) {
rcmail[list].clear_selection(); // list widget
}
else if (rcmail[list].select) {
rcmail[list].select(); // treelist widget
}
}
});
};
// show menu widget
function app_menu(show)
{
if (show) {
if (mode == 'phone') {
$('<div id="menu-overlay" class="popover-overlay">')
.on('click', function() { app_menu(false); })
.appendTo('body');
if (!env.menu_initialized) {
env.menu_initialized = true;
$('a', layout.menu).on('click', function(e) { if (mode == 'phone') app_menu(); });
}
layout.menu.addClass('popover');
}
layout.menu.removeClass('hidden');
}
else {
$('#menu-overlay').remove();
layout.menu.addClass('hidden').removeClass('popover');
}
};
/**
* Triggered when a UI message is displayed
*/
function message_displayed(p)
{
if (p.type == 'loading' && $('.iframe-loader:visible').length) {
// hide original message object, we don't need two "loaders"
rcmail.hide_message(p.object);
return;
}
alert_style(p.object, p.type, true);
$(p.object).attr('role', 'alert');
};
/**
* Applies some styling and icon to an alert object
*/
function alert_style(object, type, wrap)
{
var tmp, classes = 'ui alert',
addicon = !$(object).is('.noicon'),
map = {
information: 'alert-info',
notice: 'alert-info',
confirmation: 'alert-success',
warning: 'alert-warning',
error: 'alert-danger',
loading: 'alert-info loading',
uploading: 'alert-info loading',
vcardattachment: 'alert-info' // vcard_attachments plugin
};
// we need the content to be non-text node for best alignment
if (wrap && addicon && !$(object).is('.aligned-buttons')) {
$(object).html($('<span>').html($(object).html()));
}
// Type can be e.g. 'notice chat'
type = type.split(' ')[0];
if (tmp = map[type]) {
classes += ' ' + tmp;
if (addicon) {
$('<i>').attr('class', 'icon').prependTo(object);
}
}
$(object).addClass(classes);
};
/**
* Set UI dialogs size/style depending on screen size
*/
function dialog_open(dialog)
{
var me = $(dialog.uiDialog),
width = me.width(),
height = me.height(),
maxWidth = $(window).width(),
maxHeight = $(window).height();
if (maxWidth <= 480) {
me.css({width: '100%', height: '100%'});
}
else {
if (height > maxHeight) {
me.css('height', '100%');
}
if (width > maxWidth) {
me.css('width', '100%');
}
}
// Close all popovers
$(document).click();
// Display loader when the dialog has an iframe
iframe_loader($('div.popup > iframe', me));
// TODO: style buttons/forms
bootstrap_style(dialog.uiDialog);
};
/**
* Initializes searchbar widget
*/
function searchbar_init(bar)
{
var options_button = $('a.button.options', bar),
input = $('input:not([type=hidden])', bar),
placeholder = input.attr('placeholder'),
form = $('form', bar),
is_search_pending = function() {
if (input.val()) {
return true;
}
if (rcmail.task == 'mail' && $('#s_interval').val()) {
return true;
}
if (rcmail.gui_objects.search_filter && $(rcmail.gui_objects.search_filter).val() != 'ALL') {
return true;
}
if (rcmail.gui_objects.foldersfilter && $(rcmail.gui_objects.foldersfilter).val() != '---') {
return true;
}
},
close_func = function() {
if ($(bar).is('.open')) {
options_button.click();
}
},
update_func = function() {
$(bar)[is_search_pending() ? 'addClass' : 'removeClass']('active');
};
options_button.on('click', function(e) {
var id = $(this).data('target'),
options = $('#' + id),
open = options.is(':visible');
if (options.length) {
if (!open) {
if (ref[id]) {
ref[id](options.get(0), this, e);
}
else if (typeof window[id] == 'function') {
window[id](options.get(0), this, e);
}
}
options.next()[open ? 'show' : 'hide']();
options.toggleClass('hidden');
$('.floating-action-buttons').toggleClass('hidden');
$(bar).toggleClass('open');
$('button.search', options).off('click.search').on('click.search', function() {
options_button.trigger('click');
update_func();
});
}
});
input.on('input change', update_func)
.on('focus blur', function(e) { input.attr('placeholder', e.type == 'blur' ? placeholder : ''); });
// Search reset action
$('a.reset', bar).on('click', function(e) {
// for treelist widget's search setting val and keyup.treelist is needed
// in normal search form reset-search command will do the trick
input.val('').change().trigger('keyup.treelist', {keyCode: 27});
if ($(bar).is('.open')) {
options_button.click();
}
// Reset filter
if (rcmail.gui_objects.search_filter) {
$(rcmail.gui_objects.search_filter).val('ALL');
}
if (rcmail.gui_objects.foldersfilter) {
$(rcmail.gui_objects.foldersfilter).val('---').change();
rcmail.folder_filter('---');
}
update_func();
});
rcmail.addEventListener('init', update_func)
.addEventListener('responsebeforesearch', update_func)
// close options form on list/search request
.addEventListener('beforelist', close_func)
.addEventListener('beforesearch', close_func);
};
/**
* Converts toolbar menu into popup-menu for small screens
*/
function toolbar_init()
{
if (env.got_smart_toolbar) {
return;
}
env.got_smart_toolbar = true;
var list_mark, items = [],
list_items = [],
meta = layout_metadata(),
button_func = function(button, items, cloned) {
var item = $('<li role="menuitem">'),
button = cloned ? create_cloned_button($(button), true, 'hidden-big hidden-large') : $(button).detach();
// Remove empty text nodes that break alignment of text of the menu item
button.contents().filter(function() { if (this.nodeType == 3 && !$.trim(this.nodeValue).length) $(this).remove(); });
if (button.is('.spacer')) {
item.addClass('spacer');
}
else {
item.append(button);
}
items.push(item);
};
// convert content toolbar to a popup list
if (layout.content) {
$('.header > .toolbar', layout.content).each(function() {
var toolbar = $(this);
toolbar.children().each(function() { button_func(this, items); });
toolbar.remove();
});
}
// convert list toolbar to a popup list
if (layout.list) {
$('.header > .toolbar', layout.list).each(function() {
var toolbar = $(this);
list_mark = toolbar.next();
toolbar.children().each(function() {
if (meta.mode != 'large') {
// TODO: Would be better to set this automatically on submenu display
// i.e. in show/shown event (see popup_init()), if possible
$(this).data('popup-pos', 'right');
}
// add items to the content menu too
button_func(this, items, true);
button_func(this, list_items);
});
toolbar.remove();
});
}
// special elements to clone and add to the toolbar (mobile only)
$('ul[data-menu="toolbar-small"] > li > a').each(function() {
var button = $(this).clone();
button.attr('id', this.id + '_clone');
// TODO: rcmail.register_button()
items.push($('<li role="menuitem">').addClass('hidden-big').append(button));
});
// append the new list toolbar and menu button
if (list_items.length) {
var container = layout.list.children('.header'),
menu_attrs = {'class': 'toolbar popupmenu listing iconized', id: 'toolbar-list-menu'},
menu_button = $('<a class="button icon toolbar-list-button" href="#list-menu">')
.attr({'data-popup': 'toolbar-list-menu'}),
// TODO: copy original toolbar attributes (class, role, aria-*)
toolbar = $('<ul>').attr(menu_attrs).data('popup-parent', container).append(list_items);
if (list_mark.length) {
toolbar.insertBefore(list_mark);
}
else {
container.append(toolbar);
}
container.append(menu_button);
}
// append the new toolbar and menu button
if (items.length) {
var container = layout.content.children('.header'),
menu_attrs = {'class': 'toolbar popupmenu listing iconized', id: 'toolbar-menu'},
menu_button = $('<a class="button icon toolbar-menu-button" href="#menu">')
.attr({'data-popup': 'toolbar-menu'});
container
// TODO: copy original toolbar attributes (class, role, aria-*)
.append($('<ul>').attr(menu_attrs).data('popup-parent', container).append(items))
.append(menu_button);
if (layout.list.length) {
// bind toolbar menu with the menu button in the list header
$('a.toolbar-menu-button', layout.list).click(function(e) {
e.stopPropagation();
menu_button.click();
});
}
}
};
/**
* Initialize a popup for specified button element
*/
function popup_init(item, win)
{
// On mobile we display the menu from the frame in the parent window
if (is_framed && is_mobile()) {
return parent.UI.popup_init(item, win || window);
}
if (!win) win = window;
var level,
popup_id = $(item).data('popup'),
popup = $(win.$('#' + popup_id).get(0)), // a "hack" to support elements in frames
popup_orig = popup,
title = $(item).attr('title'),
content_element = function() {
// On mobile we display a menu from the frame in the parent window
// To make menu actions working we have to clone the menu
// and pass click events to it...
if (win != window) {
popup = popup_orig.clone(true, true);
popup.attr('id', popup_id + '-clone')
.appendTo(document.body)
.find('li > a').attr('onclick', '').off('click').on('click', function(e) {
if (!$(this).is('.disabled')) {
$(item).popover('hide');
win.$('#' + $(this).attr('id')).click();
}
return false;
});
}
return popup.get(0);
};
$(item).attr({
'aria-haspopup': 'true',
'aria-expanded': 'false',
'aria-owns': popup_id,
})
.popover({
content: content_element,
trigger: $(item).data('popup-trigger') || 'click',
placement: $(item).data('popup-pos') || 'bottom',
animation: true,
boundary: 'window', // fix for https://github.com/twbs/bootstrap/issues/25428
html: true
})
.on('show.bs.popover', function(event) {
var init_func = popup.data('popup-init');
if (popup_id && menus[popup_id]) {
menus[popup_id].transitioning = true;
}
if (init_func && ref[init_func]) {
ref[init_func](popup.get(0), item, event);
}
else if (init_func && win[init_func]) {
win[init_func](popup.get(0), item, event);
}
level = $('div.popover:visible').length + 1;
popup.removeClass('hidden').attr('aria-hidden', false)
// Stop propagation on menu items that have popups
// to make a click on them not hide their parent menu(s)
.find('[aria-haspopup="true"]')
.data('level', level + 1)
.off('click.popup')
.on('click.popup', function(e) { e.stopPropagation(); });
if (!is_mobile()) {
// Set popup height so it is less than the window height
popup.css('max-height', Math.min(36 * 15 - 1, $(window).height() - 30));
}
})
.on('shown.bs.popover', function(event) {
var mobile = is_mobile(),
popover = $('#' + $(item).attr('aria-describedby'));
level = $(item).data('level') || 1;
// Set popup Back/Close title
if (mobile) {
var label = level > 1 ? 'back' : 'close',
title = rcmail.gettext(label),
class_name = 'button icon ' + (label == 'back' ? 'back' : 'cancel');
$('.popover-header', popover).empty()
.append($('<a>').attr('class', class_name).text(title)
.on('click', function(e) {
$(item).popover('hide');
if (level > 1) {
e.stopPropagation();
}
})
.on('mousedown', function(e) {
// stop propagation to i.e. do not close jQuery-UI dialogs below
e.stopPropagation();
})
);
}
// Hide other menus on the same level
$.each(menus, function(id, prop) {
if ($(prop.target).data('level') == level && id != popup_id) {
menu_hide(id);
}
});
// On keyboard event focus the first (active) entry and enable keyboard navigation
if ($(item).data('event') == 'key') {
popover.off('keydown.popup').on('keydown.popup', 'a.active', function(e) {
var entry, node, mode = 'next';
switch (e.which) {
case 27: // ESC
case 9: // TAB
$(item).popover('toggle').focus();
return false;
case 13: // ENTER
case 32: // SPACE
$(this).trigger('click').data('event', 'key');
return false; // for IE
case 38: // ARROW-UP
case 63232:
mode = 'previous';
case 40: // ARROW-DOWN
case 63233:
entry = e.target.parentNode;
while (entry = entry[mode + 'Sibling']) {
if (node = $(entry).children('.active')[0]) {
node.focus();
break;
}
}
return false; // prevents from scrolling the whole page
}
});
popover.find('a.active').first().focus();
}
if (popup_id && menus[popup_id]) {
menus[popup_id].transitioning = false;
}
// add overlay element for phone layout
if (mobile && !$('.popover-overlay').length) {
$('<div>').attr('class', 'popover-overlay')
.appendTo('body')
.click(function() { $(this).remove(); });
}
$('.popover-body', popover).addClass('webkit-scroller');
})
.on('hide.bs.popover', function() {
if (level == 1) {
$('.popover-overlay').remove();
}
if (popup_id && menus[popup_id] && popup.is(':visible')) {
menus[popup_id].transitioning = true;
}
})
.on('hidden.bs.popover', function() {
if (/-clone$/.test(popup.attr('id'))) {
popup.remove();
}
else {
popup.attr('aria-hidden', true)
// Some menus aren't being hidden, force that
.addClass('hidden')
// Bootstrap will detach the popup element from
// the DOM (https://github.com/twbs/bootstrap/issues/20219)
// making our menus to not update buttons state.
// Work around this by attaching it back to the DOM tree.
popup.appendTo(popup.data('popup-parent') || document.body);
}
// close orphaned popovers, for some reason there are sometimes such dummy elements left
$('.popover-body:empty').each(function() { $(this).parent().remove(); });
if (popup_id && menus[popup_id]) {
delete menus[popup_id];
}
})
// Because Bootstrap does not provide originalEvent in show/shown events
// we have to handle that by our own using click and keydown handlers
.on('click', function() {
$(this).data('event', 'mouse');
})
.on('keydown', function(e) {
if (e.originalEvent) {
switch (e.originalEvent.which) {
case 13:
case 32:
// Open the popup on ENTER or SPACE
e.preventDefault();
$(this).data('event', 'key').popover('toggle');
break;
case 27:
// Close the popup on ESC key
$(this).popover('hide');
break;
}
}
});
// re-add title attribute removed by bootstrap popover
if (title) {
$(item).attr('title', title);
}
popup.attr('aria-hidden', 'true').data('button', item);
// stop propagation to e.g. do not hide the popup when
// clicking inside on form elements
if (popup.data('editable')) {
popup.on('click mousedown', function(e) { e.stopPropagation(); });
}
};
/**
* Closes all popups (for use as event handler)
*/
function popups_close(e)
{
// Ignore some of propagated click events (see pretty_select())
if (popups_close_lock && popups_close_lock > (new Date().getTime() - 250)) {
return;
}
$('.popover.show').each(function() {
var popup = $('.popover-body', this),
button = popup.children().first().data('button');
if (button && e.target != button && !$(button).find(e.target).length && typeof button !== 'string') {
$(button).popover('hide');
}
if (!button) {
$(this).remove();
}
});
};
/**
* Handler for menu-open and menu-close events
*/
function menu_toggle(p)
{
if (!p || !p.name || (p.props && p.props.skinable === false)) {
return;
}
if (is_framed && is_mobile()) {
if (!p.win) {
p.win = window;
}
return parent.UI.menu_toggle(p);
}
if (p.name == 'messagelistmenu') {
menu_messagelist(p);
}
else if (p.event == 'menu-open') {
var fn, pos,
content = $('ul', p.obj).first(),
target = p.props && p.props.link ? p.props.link : p.originalEvent.target;
if ($(target).is('span')) {
target = $(target).parents('a,li')[0];
}
if (p.name.match(/^drag/)) {
// create a fake element to position drag menu on the cursor position
pos = rcube_event.get_mouse_pos(p.originalEvent);
target = $('<a>').css({
position: 'absolute',
left: pos.x,
top: pos.y,
height: '1px',
width: '1px',
visibility: 'hidden'
})
.appendTo(document.body).get(0);
}
pos = $(target).data('popup-pos') || 'right';
if (p.name == 'folder-selector') {
content.addClass('listing folderlist');
}
else if (p.name == 'addressbook-selector' || p.name == 'contactgroup-selector') {
content.addClass('listing contactlist');
}
else if (content.hasClass('toolbarmenu')) {
content.addClass('listing');
}
if (p.name == 'pagejump-selector') {
content.addClass('simplelist');
p.obj.addClass('simplelist');
pos = 'top';
}
// There can be only one menu of the same type
if (menus[p.name]) {
menu_hide(p.name, p.originalEvent);
}
// Popover menus use animation. Sometimes the same menu is
// immediately hidden and shown (e.g. folder-selector for copy and move action)
// we have to wait until the previous menu hides before we can open it again
fn = function() {
if (menus[p.name] && menus[p.name].transitioning) {
return setTimeout(fn, 50);
}
if (!$(target).data('popup')) {
$(target).data({
popup: p.name,
'popup-pos': pos,
'popup-trigger': 'manual'
});
popup_init(target, p.win);
}
menus[p.name] = {target: target};
$(target).popover('show');
}
fn();
}
else {
menu_hide(p.name, p.originalEvent);
}
// Stop propagation so multi-level menus work properly
p.originalEvent.stopPropagation();
};
/**
* Close menu by name
*/
function menu_hide(name, event)
{
var target = menu_target(name);
if (name.match(/^drag/)) {
$(target).popover('dispose').remove();
}
else {
$(target).popover('hide');
// In phone mode close all menus when forwardmenu is requested to be closed
// FIXME: This is a hack, we need some generic solution.
if (name == 'forwardmenu') {
popups_close(event);
}
}
};
/**
* Destroys menu by name
*
* This is required when you replace the menu content element
*/
function menu_destroy(name)
{
$('[aria-owns=' + name + ']').popover('dispose').data('popup', null);
};
/**
* Get menu target by name
*/
function menu_target(name)
{
var target;
if (menus[name]) {
target = menus[name].target;
}
else {
target = $('#' + name).data('button');
if (!target) {
// catch cases as 'forwardmenu' where menu suffix has no hyphen
// or try with -menu suffix if it's not in the menu name already
if (name.match(/(?!-)menu$/)) {
name = name.substr(0, name.length - 4);
}
target = $('#' + name + '-menu').data('button');
}
}
return target;
};
/**
* Messages list options dialog
*/
function menu_messagelist(p)
{
var content = $('#listoptions-menu'),
width = content.width() + 25,
dialog = content.clone(true);
// set form values
$('select[name="sort_col"]', dialog).val(rcmail.env.sort_col || '');
$('select[name="sort_ord"]', dialog).val(rcmail.env.sort_order || 'ASC');
$('select[name="mode"]', dialog).val(rcmail.env.threading ? 'threads' : 'list');
// Fix id/for attributes
$('select', dialog).each(function() { this.id = this.id + '-clone'; });
$('label', dialog).each(function() { $(this).attr('for', $(this).attr('for') + '-clone'); });
var save_func = function(e) {
if (rcube_event.is_keyboard(e.originalEvent)) {
$('#listmenulink').focus();
}
var col = $('select[name="sort_col"]', dialog).val(),
ord = $('select[name="sort_ord"]', dialog).val(),
mode = $('select[name="mode"]', dialog).val();
rcmail.set_list_options([], col, ord, mode == 'threads' ? 1 : 0);
return true;
};
dialog = rcmail.simple_dialog(dialog, rcmail.gettext('listoptionstitle'), save_func, {
closeOnEscape: true,
minWidth: 400
});
};
/**
* About dialog
*/
function about_dialog(elem)
{
var support_url, support_func, support_button = false,
dialog = $('<iframe>').attr({id: 'aboutframe', src: rcmail.url('settings/about', {_framed: 1})}),
support_link = $('#supportlink');
if (support_link.length && (support_url = support_link.attr('href'))) {
support_button = support_link.text();
support_func = function(e) { support_url.indexOf('mailto:') < 0 ? window.open(support_url) : location.href = support_url; };
}
rcmail.simple_dialog(dialog, $(elem).text(), support_func, {
button: support_button,
button_class: 'help',
cancel_button: 'close',
height: 400
});
};
/**
* Show/hide more mail headers (envelope)
*/
function headers_show(button)
{
var headers = $(button).parent().prev();
headers[headers.is('.hidden') ? 'removeClass' : 'addClass']('hidden');
};
/**
* Mail headers dialog
*/
function headers_dialog()
{
var props = {_uid: rcmail.env.uid, _mbox: rcmail.env.mailbox, _framed: 1},
dialog = $('<iframe>').attr({id: 'headersframe', src: rcmail.url('headers', props)});
rcmail.simple_dialog(dialog, rcmail.gettext('arialabelmessageheaders'), null, {
cancel_button: 'close',
height: 400
});
};
/**
* Mail import dialog
*/
function import_dialog()
{
if (!rcmail.commands['import-messages']) {
return;
}
var content = $('#uploadform'),
dialog = content.clone(true);
var save_func = function(e) {
return rcmail.command('import-messages', $(dialog.find('form')[0]));
};
rcmail.simple_dialog(dialog, rcmail.gettext('importmessages'), save_func, {
button: 'import',
closeOnEscape: true,
minWidth: 400
});
};
/**
* Search options menu popup
*/
function searchmenu(obj)
{
var n, all,
list = $('input[name="s_mods[]"]', obj),
scope_select = $('#s_scope', obj),
mbox = rcmail.env.mailbox,
mods = rcmail.env.search_mods,
scope = rcmail.env.search_scope || 'base';
if (!$(obj).data('initialized')) {
$(obj).data('initialized', true);
if (list.length) {
list.on('change', function() { set_searchmod(obj, this); });
rcmail.addEventListener('beforesearch', function() { set_searchmod(obj); });
}
}
if (rcmail.env.search_mods) {
if (rcmail.env.task == 'mail') {
if (scope == 'all') {
mbox = '*';
}
mods = mods[mbox] ? mods[mbox] : mods['*'];
all = 'text';
scope_select.val(scope);
}
else {
all = '*';
}
if (mods[all]) {
list.map(function() {
this.checked = true;
this.disabled = this.value != all;
});
}
else {
list.prop('disabled', false).prop('checked', false);
for (n in mods) {
list.filter('[value="' + n + '"]').prop('checked', true);
}
}
}
};
function set_searchmod(menu, elem)
{
var all, m, task = rcmail.env.task,
mods = rcmail.env.search_mods,
mbox = rcmail.env.mailbox,
scope = $('#s_scope', menu).val(),
interval = $('#s_interval', menu).val();
if (scope == 'all') {
mbox = '*';
}
if (!mods) {
mods = {};
}
if (task == 'mail') {
if (!mods[mbox]) {
mods[mbox] = rcube_clone_object(mods['*']);
}
m = mods[mbox];
all = 'text';
rcmail.env.search_scope = scope;
rcmail.env.search_interval = interval;
}
else { //addressbook
m = mods;
all = '*';
}
if (!elem) {
return;
}
if (!elem.checked) {
delete(m[elem.value]);
}
else {
m[elem.value] = 1;
}
// mark all fields
if (elem.value == all) {
$('input[name="s_mods[]"]', menu).map(function() {
if (this == elem) {
return;
}
this.checked = true;
if (elem.checked) {
this.disabled = true;
delete m[this.value];
}
else {
this.disabled = false;
m[this.value] = 1;
}
});
}
rcmail.set_searchmods(m);
};
/**
* Spellcheck languages list
*/
function spellmenu(obj)
{
var i, link, li, list = [],
lang = rcmail.spellcheck_lang(),
ul = $('ul', obj);
if (!ul.length) {
ul = $('<ul class="toolbarmenu selectable listing iconized" role="menu">');
for (i in rcmail.env.spell_langs) {
li = $('<li role="menuitem">');
link = $('<a href="#'+ i +'" tabindex="0"></a>')
.text(rcmail.env.spell_langs[i])
.addClass('active').data('lang', i)
.on('click keypress', function(e) {
if (e.type != 'keypress' || rcube_event.get_keycode(e) == 13) {
rcmail.spellcheck_lang_set($(this).data('lang'));
rcmail.hide_menu('spell-menu', e);
return false;
}
});
link.appendTo(li);
list.push(li);
}
ul.append(list).appendTo(obj);
}
// select current language
$('li', ul).each(function() {
var el = $('a', this);
if (el.data('lang') == lang) {
el.addClass('selected').attr('aria-selected', 'true');
}
else if (el.hasClass('selected')) {
el.removeClass('selected').removeAttr('aria-selected');
}
});
};
/**
* Add/remove item to/from compose options status bar
*/
function compose_status(id, status)
{
var bar = $('#composestatusbar'), ico = bar.find('a.button.icon.' + id);
if (!status) {
ico.remove();
}
else if (!ico.length) {
$('<a>').attr('class', 'button icon ' + id)
.on('click', function() { show_sidebar(); })
.appendTo(bar);
}
};
/**
* Attachment menu
*/
function attachmentmenu(obj, button, event)
{
var id = $(button).parent().attr('id').replace(/^attach/, '');
$.each(['open', 'download', 'rename'], function() {
var action = this;
$('#attachmenu' + action, obj).off('click').attr('onclick', '').click(function(e) {
rcmail.command(action + '-attachment', id, this, e.originalEvent);
});
});
// call menu-open so core can set state of menu commands
return rcmail.command('menu-open', {menu: 'attachmentmenu', id: id}, obj, event);
};
/**
* Appends drop-icon to attachments list item (to invoke attachment menu)
*/
function attachmentmenu_append(item)
{
item = $(item);
if (!item.is('.no-menu') && !item.children('.drop').length) {
var label = rcmail.gettext('options');
var button = $('<a>')
.attr({
href: '#',
tabindex: 0,
title: label,
'class': 'button icon dropdown skip-content'
})
.on('click', function(e) {
return attachmentmenu($('#attachmentmenu'), button, e);
})
.append($('<span>').attr('class', 'inner').text(label))
.appendTo(item);
}
};
/**
* Mailto menu
*/
function mailtomenu(obj, button, event)
{
var mailto = $(button).attr('href').replace(/^mailto:/, '');
if (mailto.indexOf('@') < 0) {
return true; // let the browser handle this
}
if (rcmail.env.has_writeable_addressbook) {
$('.addressbook', obj).addClass('active')
.off('click').on('click', function(e) {
var i, contact = mailto,
txt = $(button).filter('.rcmContactAddress').text();
contact = contact.split('?')[0].split(',')[0].replace(/(^<|>$)/g, '');
if (txt) {
txt = txt.replace('<' + contact + '>', '');
contact = '"' + $.trim(txt) + '" <' + contact + '>';
}
rcmail.command('add-contact', contact, this, e.originalEvent);
});
}
$('.compose', obj).off('click').on('click', function(e) {
rcmail.command('compose', mailto, this, e.originalEvent);
});
return rcmail.command('menu-open', {menu: 'mailto-menu', link: button}, button, event);
};
/**
* Appends popup menu to mailto links
*/
function mailtomenu_append(item)
{
$(item).attr('onclick', '').on('click', function(e) {
return mailtomenu($('#mailto-menu'), item, e);
});
};
/**
* Headers menu in mail compose
*/
function headersmenu(obj, button, event)
{
$('li > a', obj).each(function() {
var link = $(this), target = '#compose_' + link.data('target');
link[$(target).is(':visible') ? 'removeClass' : 'addClass']('active')
.off().on('click', function() {
$(target).removeClass('hidden').find('.recipient-input input').focus();
link.removeClass('active');
rcmail.set_menu_buttons();
});
});
};
/**
* Reset/hide compose message recipient input
*/
function header_reset(id)
{
$('#' + id).val('').change()
// jump to the next input
.closest('.form-group').nextAll(':not(.hidden)').first().find('input').focus();
$('a[data-target=' + id.replace(/^_/, '') + ']').addClass('active');
rcmail.set_menu_buttons();
};
/**
* Recipient (contact) selector
*/
function recipient_selector(field, opts)
{
if (!opts) opts = {};
var title = rcmail.gettext(opts.title || 'insertcontact'),
dialog = $('#recipient-dialog'),
parent = dialog.parent(),
close_func = function() {
if (dialog.is(':visible')) {
rcmail.env.recipient_dialog.dialog('close');
}
},
insert_func = function() {
if (opts.action) {
opts.action();
close_func();
return;
}
rcmail.command('add-recipient');
};
if (!rcmail.env.recipient_selector_initialized) {
rcmail.addEventListener('add-recipient', close_func);
rcmail.env.recipient_selector_initialized = true;
}
if (field) {
rcmail.env.focused_field = '#_' + field;
}
rcmail.contact_list.clear_selection();
rcmail.contact_list.multiselect = 'multiselect' in opts ? opts.multiselect : true;
rcmail.env.recipient_dialog = rcmail.simple_dialog(dialog, title, insert_func, {
button: rcmail.gettext(opts.button || 'insert'),
button_class: opts.button_class || 'insert recipient',
height: 600,
classes: {
'ui-dialog-content': 'p-0' // remove padding on dialog content
},
open: function() {
// Don't want focus in the search field, we focus first contacts source record instead
$('#directorylist a').first().focus();
},
close: function() {
dialog.appendTo(parent);
$(this).remove();
$(opts.focus || rcmail.env.focused_field).focus();
}
});
};
/**
* Create/Update quota widget (setquota event handler)
*/
function update_quota(p)
{
var element = $('#quotadisplay'),
bar = element.find('.bar'),
value = p.total ? p.percent : 0;
if (!bar.length) {
bar = $('<span class="bar"><span class="value"></span></span>').appendTo(element);
}
if (value > 0 && value < 10) {
value = 10; // smaller values look not so nice
}
bar.find('.value').css('width', value + '%')[value >= 90 ? 'addClass' : 'removeClass']('warning');
// set title and reset tooltip's data (needed in case of empty title)
element.attr({'data-original-title': '', title: element.find('.count').attr('title')});
if (p.table) {
element.css('cursor', 'pointer').data('popup-pos', 'top')
.off('click').on('click', function(e) {
rcmail.simple_dialog(p.table, 'quota', null, {cancel_button: 'close'});
});
}
else {
element.tooltip('dispose').tooltip({trigger: is_mobile() ? 'click' : 'hover'});
}
};
/**
* Replaces recipient input with content-editable element that uses "recipient boxes"
*/
function recipient_input(obj)
{
var list, input, ac_props, update_lock,
input_len_update = function() {
input.css('width', Math.max(40, input.val().length * 15 + 25));
},
apply_func = function() {
// update the original input
$(obj).val(list.text() + input.val());
},
focus_func = function() {
list.addClass('focus');
},
insert_recipient = function(name, email, replace) {
var recipient = $('<li class="recipient">'),
name_element = $('<span class="name">').html(recipient_input_name(name || email))
.on('dblclick', function(e) { recipient_input_edit_dialog(e, insert_recipient); }),
email_element = $('<span class="email">'),
// TODO: should the 'close' link have tabindex?
link = $('<a>').attr({'class': 'button icon remove'})
.click(function() {
recipient.remove();
apply_func();
input.focus();
return false;
});
if (name) {
email = ' <' + email + '>';
}
email_element.text((name ? email : '') + ',');
recipient.attr('title', name ? (name + email) : null)
.append([name_element, email_element, link])
if (replace)
replace.replaceWith(recipient);
else
recipient.insertBefore(input.parent());
},
update_func = function(text) {
var result;
if (update_lock) {
return;
}
update_lock = true;
text = (text || input.val()).replace(/[,;\s]+$/, '');
result = recipient_input_parser(text);
$.each(result.recipients, function() {
insert_recipient(this.name, this.email);
});
// setTimeout() here is needed for proper input reset on paste event
// This is also the reason why we need parse_lock
setTimeout(function() {
input.val(result.text);
apply_func();
input_len_update();
update_lock = false;
}, 1);
return result.recipients.length > 0;
},
parse_func = function(e) {
if (e.type == 'blur') {
list.removeClass('focus');
}
// FIXME: This is a workaround for a bug where on a touch device
// selecting a recipient from autocomplete list do not work because
// of some events race condition (?)
if (this.value.indexOf('@') < 0) {
return;
}
// On paste the text is not yet in the input we have to use clipboard.
// Also because on paste new-line characters are replaced by spaces (#6460)
update_func(e.type == 'paste' ? (e.originalEvent.clipboardData || window.clipboardData).getData('text') : null);
},
keydown_func = function(e) {
// On Backspace remove the last recipient
if (e.keyCode == 8 && !input.val().length) {
list.children('li.recipient').first().remove();
apply_func();
return false;
}
// Here we add a recipient box when the separator (,;) or Enter was pressed
else if (e.key == ',' || e.key == ';' || (e.key == 'Enter' && !rcmail.ksearch_visible())) {
if (update_func()) {
return false;
}
}
input_len_update();
};
// Create the input element and "editable" area
input = $('<input>').attr({type: 'text', tabindex: $(obj).attr('tabindex')})
.on('paste change blur', parse_func)
.on('input', input_len_update) // only to fix input length after paste
.on('keydown', keydown_func)
.on('focus mousedown', focus_func);
list = $('<ul>').addClass('form-control recipient-input')
.append($('<li>').append(input))
.on('click', function() { input.focus(); });
// Hide the original input/textarea
// Note: we do not remove the original element, and we do not use
// display: none, because we want to handle onfocus event
// Note: tabindex:-1 to make Shift+TAB working on these widgets
$(obj).css({position: 'absolute', opacity: 0, left: '-5000px', width: '10px'})
.attr('tabindex', -1)
.after(list)
// some core code sometimes focuses or changes the original node
// in such cases we wan't to parse it's value and apply changes
// to the widget element
.on('focus', function(e) { input.focus(); })
.on('change', function(e) {
$('li.recipient', list).remove();
input.val(this.value).change();
})
// copy and parse the value already set
.change();
// this one line is here to fix border of Bootstrap's input-group,
// input-group should not contain any hidden elements
$(obj).detach().insertBefore(list.parent());
if (rcmail.env.autocomplete_threads > 0) {
ac_props = {
threads: rcmail.env.autocomplete_threads,
sources: rcmail.env.autocomplete_sources
};
}
// Init autocompletion
rcmail.init_address_input_events(input, ac_props);
};
/**
* Parses recipient address input and extracts recipients from it
*/
function recipient_input_parser(text)
{
// support new-line as a separator, for paste action (#6460)
text = $.trim(text.replace(/[,;\s]*[\r\n]+/g, ','));
var recipients = [],
address_rx_part = '(\\S+|("[^"]+"))@\\S+',
recipient_rx1 = new RegExp('(<' + address_rx_part + '>)'),
recipient_rx2 = new RegExp('(' + address_rx_part + ')'),
global_rx = /(?=\S)[^",;]*(?:"[^\\"]*(?:\\[,;\S][^\\"]*)*"[^",;]*)*/g,
matches = text.match(global_rx);
$.each(matches || [], function() {
if (this.length && (recipient_rx1.test(this) || recipient_rx2.test(this))) {
var email = RegExp.$1,
name = $.trim(this.replace(email, ''));
recipients.push({
name: name,
email: email.replace(/(^<|>$)/g, ''),
text: this
});
text = text.replace(this, '');
}
});
text = text.replace(/[,;]+/, ',').replace(/^[,;\s]+/, '');
return {recipients: recipients, text: text};
};
/**
* Generates HTML for a text adding <span class="hidden">
* for quote/backslash characters, so they are hidden from the user,
* but still in place to make copying simpler
*
* Note: Selection works in Chrome, but not in Firefox?
*/
function recipient_input_name(text)
{
var i, char, result = '', len = text.length;
if (text.charAt(0) != '"' && text.indexOf('"') > -1) {
text = '"' + text.replace('\\', '\\\\').replace('"', '\\"') + '"';
}
for (i=0; i<len; i++) {
char = text.charAt(i);
switch (char) {
case '"':
if (i > 0 && i < len - 1) {
result += '"';
break;
}
result += '<span class="quotes">' + char + '</span>';
break;
case '\\':
result += '<span class="quotes">' + char + '</span>';
if (text.charAt(i+1) == '\\') {
result += char;
i++;
}
break;
case '<':
result += '&lt;';
break;
case '>':
result += '&gt;';
break;
default:
result += char;
}
}
return result;
};
/**
* Displays dialog to edit a recipient entry
*/
function recipient_input_edit_dialog(e, callback)
{
var element = $(e.target).parents('.recipient'),
recipient = element.text().replace(/,+$/, ''),
input = $('<input>').attr({type: 'text', size: 50}).val(recipient),
content = $('<label>').text(rcmail.gettext('recipient')).append(input);
rcmail.simple_dialog(content, 'recipientedit', function() {
var result, value = input.val();
if (value) {
if (value != recipient) {
result = recipient_input_parser(value);
if (result.recipients.length != 1) {
return false;
}
callback(result.recipients[0].name, result.recipients[0].email, element);
}
return true;
}
});
};
/**
* Adds logic to the contact photo widget
*/
function image_upload_input(obj)
{
var reset_button = $('<a>')
.attr({'class': 'icon button delete', href: '#', })
.click(function(e) { rcmail.command('delete-photo', '', this, e); return false; });
$(obj).append(reset_button).click(function() { rcmail.upload_input('upload-form'); });
$('img', obj).on('load', function() {
// FIXME: will that work in IE?
var state = (this.currentSrc || this.src).indexOf(rcmail.env.photo_placeholder) != -1;
$(obj)[state ? 'removeClass' : 'addClass']('changed');
});
};
/**
* Displays loading... overlay for iframes
*/
function iframe_loader(frame)
{
frame = $(frame);
if (frame.length) {
var loader = $('<div class="iframe-loader">')
.append($('<div class="spinner spinner-border" role="status">')
.append($('<span class="sr-only">').text(rcmail.gettext('loading'))));
// custom 'loaded' event is expected to be triggered by plugins
// when using the loader not on an iframe
frame.on('load error loaded', function() {
// wait some time to make sure the iframe stopped loading
setTimeout(function() { loader.remove(); }, 500);
})
.parent().append(loader);
// fix scrolling in iOS
if (ios) {
frame.parent().addClass('ios-scroll');
}
}
};
/**
* Convert checkbox input into Bootstrap's custom switch
*/
function pretty_checkbox(checkbox)
{
var label, parent, id, checkbox = $(checkbox);
if (checkbox.is('.custom-control-input')) {
return;
}
if (!(id = checkbox.attr('id'))) {
id = 'icochk' + (++env.checkboxes);
checkbox.attr('id', id);
}
if (checkbox.parent().is('label')) {
label = checkbox.parent();
checkbox = checkbox.detach();
label.before(checkbox);
}
else {
label = $('<label>');
}
label.attr({'for': id, 'class': 'custom-control-label', title: checkbox.attr('title') || ''})
.on('click', function(e) { e.stopPropagation(); });
checkbox.addClass('form-check-input custom-control-input')
.wrap('<div class="custom-control custom-switch">')
.parent().append(label);
};
/**
* Make select dropdowns pretty
* TODO: searching, optgroup, [multiple], iPhone/iPad
*/
function pretty_select(select)
{
// iPhone is not supported yet (problem with browser dropdown on focus)
if (bw.iphone || bw.ipad) {
return;
}
select = $(select);
if (select.is('.pretty-select')) {
return;
}
var select_ident = 'select' + select.attr('id') + select.attr('name');
var is_menu_open = function() {
// Use proper window in cases when the select element intialized
// inside an iframe is then used in a dialog inside a parent's window
// For some reason we can't access data-button property in cross-window
// case, we use data-ident attribute instead
var win = select[0].ownerDocument.defaultView;
if (win.$('.select-menu .listing').data('ident') == select_ident) {
return true;
}
};
var close_func = function() {
var open = is_menu_open();
select.popover('dispose').focus();
return !open;
};
var open_func = function(e) {
var items = [],
dialog = select.closest('.ui-dialog')[0],
max_height = $(document.body).height() - 75,
max_width = $(document.body).width() - 20,
min_width = Math.min(select.outerWidth(), max_width),
value = select.val();
if (!is_mobile()) {
max_height *= 0.5;
}
// close other popups
popups_close(e);
$('option', select).each(function() {
var label = $(this).text(),
link = $('<a href="#">')
.data('value', this.value)
.addClass(this.disabled ? 'disabled' : 'active' + (this.value == value ? ' selected' : ''));
if (label.length) {
link.text(label);
}
else {
link.html('&nbsp;'); // link can't be empty
}
items.push($('<li>').append(link));
});
var list = $('<ul class="toolbarmenu listing selectable iconized">')
.attr('data-ident', select_ident)
.data('button', select[0])
.append(items)
.on('click', 'a.active', function() {
// first close the list, then update the select, the order is important
// for cases when the select might be removed in change event (datepicker)
var val = $(this).data('value'), ret = close_func();
select.val(val).change();
return ret;
})
.on('keydown', 'a.active', function(e) {
var item, node, mode = 'next';
switch (e.which) {
case 27: // ESC
case 9: // TAB
return close_func();
case 13: // ENTER
case 32: // SPACE
$(this).click();
return false; // for IE
case 38: // ARROW-UP
case 63232:
mode = 'previous';
case 40: // ARROW-DOWN
case 63233:
item = e.target.parentNode;
while (item = item[mode + 'Sibling']) {
if (node = $(item).children('.active')[0]) {
node.focus();
break;
}
}
return false; // prevents from scrolling the whole page
}
});
select.popover('dispose')
.popover({
// because of focus issues we can't always use body,
// if select is in a dialog, popover has to be a child of this dialog
container: dialog || document.body,
content: list[0],
placement: 'bottom',
trigger: 'manual',
boundary: 'viewport',
html: true,
offset: '0,2',
sanitize: false,
template: '<div class="popover select-menu" style="min-width: ' + min_width + 'px; max-width: ' + max_width + 'px">'
+ '<div class="popover-header"></div>'
+ '<div class="popover-body" style="max-height: ' + max_height + 'px"></div></div>'
})
.on('shown.bs.popover', function() {
select.focus(); // for Chrome
// Set popup Close title
list.parent().prev()
.empty()
.append($('<a class="button icon cancel">').text(rcmail.gettext('close'))
.on('click', function(e) {
e.stopPropagation();
return close_func();
})
);
// focus first active element on the list
if (rcube_event.is_keyboard(e)) {
list.find('a.active').first().focus();
}
// don't propagate mousedown event
list.on('mousedown', function(e) { e.stopPropagation(); });
})
.popover('show');
};
select.addClass('pretty-select custom-select form-control')
.on('mousedown keydown', function(e) {
select = $(e.target); // so it works after clone
// Do nothing on disabled select or on TAB key
if (select.prop('disabled')) {
return;
}
if (e.which == 9) {
close_func();
return true;
}
// Close popup on ESC key or on click if already open
if (e.which == 27 || (e.type == 'mousedown' && is_menu_open())) {
return close_func();
}
select.focus();
// prevent displaying browser-default select dropdown
select.prop('disabled', true);
setTimeout(function() { select.prop('disabled', false); }, 0);
e.stopPropagation();
// display options in our way (on SPACE, ENTER, ARROW-DOWN or mousedown)
if (e.type == 'mousedown' || e.which == 13 || e.which == 32 || e.which == 40 || e.which == 63233) {
open_func(e);
// Prevent from closing the menu by general popover closing handler (popups_close())
// We used to just stop propagation in onclick handler, but it didn't work
// in Chrome where onclick handler wasn't invoked on mobile (#6705)
popups_close_lock = new Date().getTime();
return false;
}
})
};
/**
* HTML editor textarea wrapper with nice looking tabs-like switch
*/
function html_editor_init(obj)
{
// Here we support two structures
// 1. <div><textarea></textarea><select name="editorSelector"></div>
// 2. <tr><td><td><td><textarea></textarea></td></tr>
// <tr><td><td><td><input type="checkbox"></td></tr>
var sw, is_table = false,
editor = $(obj),
parent = editor.parent(),
tabindex = editor.attr('tabindex'),
mode = function() {
if (is_table) {
return sw.is(':checked') ? 'html' : 'plain';
}
return sw.val();
},
tabs = $('<ul class="nav nav-tabs">')
.append($('<li class="nav-item">')
.append($('<a class="nav-link mode-html" href="#">')
.text(rcmail.gettext('htmltoggle'))))
.append($('<li class="nav-item">')
.append($('<a class="nav-link mode-plain" href="#">')
.text(rcmail.gettext('plaintoggle'))));
if (parent.is('td')) {
sw = $('input[type="checkbox"]', parent.parent().next());
is_table = true;
}
else {
sw = $('[name="editorSelector"]', obj.form);
}
// sanity check
if (sw.length != 1) {
return;
}
parent.addClass('html-editor');
editor.before(tabs);
$('a', tabs).attr('tabindex', tabindex)
.on('click', function(e) {
var id = editor.attr('id'), is_html = $(this).is('.mode-html');
e.preventDefault();
if (rcmail.command('toggle-editor', {id: id, html: is_html}, '', e.originalEvent)) {
$(this).tab('show').prop('tabindex', -1);
$('.mode-' + (is_html ? 'plain' : 'html'), tabs).prop('tabindex', tabindex);
if (is_table) {
sw.prop('checked', is_html);
}
}
})
.filter('.mode-' + mode()).tab('show').prop('tabindex', -1);
if (is_table) {
// Hide unwanted table cells
sw.parents('tr').first().hide();
parent.prev().hide();
// Modify the textarea cell to use 100% width
parent.addClass('col-sm-12');
}
// make the textarea autoresizeable
textarea_autoresize_init(editor);
};
/**
* Make the textarea autoresizeable depending on it's content length.
* The way there's no vertical scrollbar.
*/
function textarea_autoresize_init(textarea)
{
var resize = function(e) {
clearTimeout(env.textarea_timer);
env.textarea_timer = setTimeout(function() {
var area = $(e.target),
initial_height = area.data('initial-height'),
scroll_height = area[0].scrollHeight;
// do nothing when the area is hidden
if (!scroll_height) {
return;
}
if (!initial_height) {
area.data('initial-height', initial_height = scroll_height);
}
// strange effect in Chrome/Firefox when you delete a line in the textarea
// the scrollHeight is not decreased by the line height, but by 2px
// so jumps up many times in small steps, we'd rather use one big step
if (area.outerHeight() - scroll_height == 2) {
scroll_height -= 19; // 21px is the assumed line height
}
area.outerHeight(Math.max(initial_height, scroll_height));
}, 10);
};
$(textarea).css('overflow-y', 'hidden').on('input', resize).trigger('input');
// Make sure the height is up-to-date also in time intervals
setInterval(function() { $(textarea).trigger('input'); }, 1000);
};
// Inititalizes smart list input
function smart_field_init(field)
{
var tip, id = field.id + '_list',
area = $('<div class="multi-input"><div class="content"></div><div class="invalid-feedback"></div></div>'),
list = field.value ? field.value.split("\n") : [''];
if ($('#' + id).length) {
return;
}
// add input rows
$.each(list, function(i, v) {
smart_field_row_add($('.content', area), v, field.name, i, $(field).data('size'));
});
area.attr('id', id);
field = $(field);
if (field.attr('disabled')) {
area.hide();
}
// disable the original field anyway, we don't want it in POST
else {
field.prop('disabled', true);
}
if (field.data('hidden')) {
area.hide();
}
field.after(area);
if (field.hasClass('is-invalid')) {
area.addClass('is-invalid');
$('.invalid-feedback', area).text(field.data('error-msg'));
}
};
function smart_field_row_add(area, value, name, idx, size, after)
{
// build row element content
var input, elem = $('<div class="input-group">'
+ '<input type="text" class="form-control">'
+ '<span class="input-group-append"><a class="icon reset input-group-text" href="#"></a></span>'
+ '</div>'),
attrs = {value: value, name: name + '[]'};
if (size) {
attrs.size = size;
}
input = $('input', elem).attr(attrs)
.keydown(function(e) {
var input = $(this);
// element creation event (on Enter)
if (e.which == 13) {
var name = input.attr('name').replace(/\[\]$/, ''),
dt = (new Date()).getTime(),
elem = smart_field_row_add(area, '', name, dt, size, input.parent());
$('input', elem).focus();
}
// backspace or delete: remove input, focus previous one
else if ((e.which == 8 || e.which == 46) && input.val() == '') {
var parent = input.parent(),
siblings = area.children();
if (siblings.length > 1) {
if (parent.prev().length) {
parent.prev().children('input').focus();
}
else {
parent.next().children('input').focus();
}
parent.remove();
return false;
}
}
});
// element deletion event
$('a.reset', elem).click(function() {
var record = $(this.parentNode.parentNode);
if (area.children().length > 1) {
$('input', record.next().length ? record.next() : record.prev()).focus();
record.remove();
}
else {
$('input', record).val('').focus();
}
});
$(elem).find('input,a')
.on('focus', function() { area.addClass('focused'); })
.on('blur', function() { area.removeClass('focused'); });
if (after) {
after.after(elem);
}
else {
elem.appendTo(area);
}
return elem;
};
// Reset and fill the smart list input with new data
function smart_field_reset(field, data)
{
var id = field.id + '_list',
list = data.length ? data : [''],
area = $('#' + id).children('.content');
area.empty();
// add input rows
$.each(list, function(i, v) {
smart_field_row_add(area, v, field.name, i, $(field).data('size'));
});
};
/**
* Register form errors, mark fields as invalid, dsplay the error below the input
*/
function form_errors(tips)
{
$.each(tips, function() {
var input = $('#' + this[0]).addClass('is-invalid');
if (input.data('type') == 'list') {
input.data('error-msg', this[2]);
$('#' + this[0] + '_list > .invalid-feedback').text(this[2]);
return;
}
input.after($('<span class="invalid-feedback">').text(this[2]));
});
};
/**
* Show/hide the navigation list
*/
function switch_nav_list(obj)
{
var records, height, speed = 250,
button = $('a.button', obj),
navlist = $(obj).next();
if (!navlist.height()) {
records = $('tr,li', navlist).filter(function() { return this.style.display != 'none'; });
height = $(records[0]).height() || 50;
navlist.animate({height: (Math.min(5, records.length) * height + 1) + 'px'}, speed);
button.addClass('collapse').removeClass('expand');
$(obj).addClass('expanded');
}
else {
navlist.animate({height: '0'}, speed);
button.addClass('expand').removeClass('collapse');
$(obj).removeClass('expanded');
}
};
/**
* Wrapper for rcmail.open_window to intercept window opening
* and display a dialog with an iframe instead of a real window.
*/
function window_open(url)
{
// Use 4th argument to bypass the dialog-mode e.g. for external windows
if (!is_mobile() || arguments[3] === true) {
return env.open_window.apply(rcmail, arguments);
}
// _extwin=1, _framed=1 are required to display attachment preview
// layout properly and make mobile menus working
url = rcmail.add_url(url, '_framed', 1);
url = rcmail.add_url(url, '_extwin', 1);
var label, title = '',
props = {cancel_button: 'close', width: 768, height: 768},
frame = $('<iframe>').attr({id: 'windowframe', src: url});
if (/_action=([a-z_]+)/.test(url) && (label = rcmail.labels[RegExp.$1])) {
title = label;
}
if (/_frame=1/.test(url)) {
props.dialogClass = 'no-titlebar';
}
rcmail.simple_dialog(frame, title, null, props);
return true;
};
/**
* Get layout modes. In frame mode returns the parent layout modes.
*/
function layout_metadata()
{
if (is_framed) {
var doc = $(parent.document.documentElement);
return {
mode: doc[0].className.match(/layout-([a-z]+)/) ? RegExp.$1 : mode,
touch: doc.is('.touch'),
};
}
return {mode: mode, touch: touch};
};
/**
* Returns true if the layout is in 'small' or 'phone' mode
*/
function is_mobile()
{
var meta = layout_metadata();
return meta.mode == 'phone' || meta.mode == 'small';
};
/**
* Returns true if the layout is in 'touch' mode
*/
function is_touch()
{
var meta = layout_metadata();
return meta.touch;
};
}
if (window.rcmail) {
/**
* Elastic version of show_menu as we don't need e.g. menu positioning from core
* TODO: keyboard navigation in menus
*/
rcmail.show_menu = function(prop, show, event)
{
var name = typeof prop == 'object' ? prop.menu : prop,
obj = $('#' + name);
if (typeof prop == 'string') {
prop = {menu: name};
}
// just delegate the action to rcube_elastic_ui
return rcmail.triggerEvent(show === false ? 'menu-close' : 'menu-open', {name: name, obj: obj, props: prop, originalEvent: event});
}
/**
* Elastic version of hide_menu as we don't need e.g. menus stack handling
*/
rcmail.hide_menu = function(name, event)
{
// delegate to rcube_elastic_ui
return rcmail.triggerEvent('menu-close', {name: name, props: {menu: name}, originalEvent: event});
}
}
else {
// rcmail does not exists e.g. on the error template inside a frame
// we fake the engine a little
var rcmail = parent.rcmail,
rcube_webmail = parent.rcube_webmail,
bw = {};
}
var UI = new rcube_elastic_ui();
// Improve non-inline datepickers
if ($ && $.datepicker) {
var __newInst = $.datepicker._newInst;
$.extend($.datepicker, {
_newInst: function(target, inline) {
var inst = __newInst.call(this, target, inline);
if (!inst.inline) {
UI.datepicker_init(inst.dpDiv);
}
return inst;
}
});
}
diff --git a/skins/larry/addressbook.css b/skins/larry/addressbook.css
index 3331e5b6b..9e2d3223a 100644
--- a/skins/larry/addressbook.css
+++ b/skins/larry/addressbook.css
@@ -1,444 +1,444 @@
/**
* Roundcube webmail styles for the Address Book section
*
- * Copyright (c) 2012-2017, The Roundcube Dev Team
- * Screendesign by FLINT / B�ro f�r Gestaltung, bueroflint.com
+ * Copyright (c) The Roundcube Dev Team
+ * Screendesign by FLINT / Büro für Gestaltung, bueroflint.com
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
#addressview-left {
position: absolute;
top: 0;
left: 0;
width: 200px;
bottom: 0;
z-index: 2;
}
#addressview-right {
position: absolute;
top: 0;
left: 212px;
right: 0;
bottom: 0;
z-index: 3;
}
#addressbooktoolbar {
position: absolute;
top: -6px;
left: 0;
height: 40px;
white-space: nowrap;
z-index: 10;
}
#directorylistbox {
position: absolute;
top: 0;
left: 0;
width: 100%;
bottom: 0;
}
#addresslist {
position: absolute;
top: 0;
left: 0;
width: 260px;
bottom: 0;
}
#contacts-box {
position: absolute;
top: 0;
left: 272px;
right: 0;
bottom: 0;
}
#addressview-left #quicksearchbar input {
width: 156px;
}
#directorylist li a,
.treelist li.contactsearch a,
#contacts-table .contact td.name {
background-image: url(images/listicons.png);
background-position: -100px 0;
background-repeat: no-repeat;
overflow: hidden;
text-overflow: ellipsis;
}
#directorylist li.addressbook a {
background-position: 6px -766px;
}
#directorylist ul li.addressbook a {
background-position: 32px -766px;
}
#directorylist ul ul li.addressbook a {
background-position: 58px -766px;
}
#directorylist li.addressbook.selected > a {
background-position: 6px -791px;
}
#directorylist ul li.addressbook.selected > a {
background-position: 32px -791px;
}
#directorylist ul ul li.addressbook.selected > a {
background-position: 58px -791px;
}
#directorylist li.contactgroup a {
background-position: 6px -1554px;
}
#directorylist ul li.contactgroup a {
background-position: 32px -1554px;
}
#directorylist ul ul li.contactgroup a {
background-position: 58px -1554px;
}
#directorylist ul ul ul li.contactgroup a {
background-position: 84px -1554px;
}
#directorylist li.contactgroup.selected a {
background-position: 32px -1578px;
}
#directorylist ul ul li.contactgroup.selected a {
background-position: 58px -1578px;
}
#directorylist ul ul ul li.contactgroup.selected a {
background-position: 84px -1578px;
}
.treelist li.contactsearch a {
background-position: 6px -1651px;
}
.treelist li.contactsearch.selected a {
background-position: 6px -1675px;
}
#directorylist li.addressbook div.collapsed,
#directorylist li.addressbook div.expanded {
top: 15px;
}
#contacts-table .contact.readonly td {
font-style: italic;
}
#contacts-table td.name {
width: 95%;
}
#contacts-table td.action {
width: 24px;
padding: 4px;
}
#contacts-table td.action a {
display: block;
width: 16px;
height: 14px;
text-indent: -5000px;
overflow: hidden;
background: url(images/listicons.png) -2px -1180px no-repeat;
}
#contacts-table .contact td.name {
background-position: 4px -1601px;
}
#contacts-table .contact.selected td.name {
background-position: 4px -1625px;
font-weight: bold;
}
#contacts-table .group td.name {
background-position: 4px -1555px;
}
#contacts-table .group.selected td.name {
background-position: 4px -1578px;
font-weight: bold;
}
#contacts-table.focus .group.selected.focused td.name {
background-position: 4px -1578px;
}
#addresslist .boxtitle {
padding-right: 95px;
overflow: hidden;
text-overflow: ellipsis;
}
#addresslist .boxtitle a.poplink {
color: #004458;
font-size: 14px;
line-height: 12px;
text-decoration: none;
}
#contact-frame {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0px;
border: 0;
border-radius: 4px;
}
#headerbuttons {
position: absolute;
top: 48px;
right: 10px;
width: auto;
z-index: 10;
}
#sourcename {
color: #999;
font-size: 10px;
margin: -5px 0 8px 2px;
}
#contactphoto {
float: left;
margin: 0 18px 20px 0;
width: 112px;
border: 0;
padding: 0;
}
#contactpic {
width: 112px;
min-height: 112px;
background: white;
}
#contactpic img {
max-width: 112px;
visibility: inherit;
}
#contactpic.droptarget {
background-image: url(images/filedrop.png);
background-position: center;
background-repeat: no-repeat;
}
#contactpic.droptarget.hover {
background-color: #d9ecf4;
box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
}
#contactpic.droptarget.active img {
opacity: 0.15;
}
#contactpic.droptarget.hover img {
opacity: 0.05;
}
#contactphoto .formlinks a[aria-disabled='true'] {
visibility: hidden;
}
#contacthead {
border: 0;
margin: 0 16em 1em 0;
padding: 0;
line-height: 1.5em;
font-size: 12px;
}
#contacthead > legend {
display: none;
}
form #contacthead {
margin-right: 0;
}
#contacthead .names span.namefield,
#contacthead .names input {
font-size: 140%;
font-weight: bold;
}
#contacthead .displayname span.namefield {
font-size: 120%;
font-weight: bold;
}
#contacthead span.nickname:before,
#contacthead span.nickname:after {
content: '"';
}
#contacthead input {
margin-right: 6px;
margin-bottom: 0.2em;
}
#contacthead .names input,
#contacthead .addnames input {
width: 180px;
}
#contacthead input.ff_prefix,
#contacthead input.ff_suffix {
width: 90px;
}
.contactfieldgroup {
border: 0;
border-radius: 0;
background: #f7f7f7;
margin: 0 0 12px 0;
padding: 8px;
}
.contactfieldgroup legend {
display: block;
margin: 0 -8px;
width: 100%;
font-weight: bold;
padding: 8px 8px 6px 8px;
background: #e9e9e9;
border-bottom: 1px solid #dfdfdf;
border-radius: 0;
}
.contactfieldgroup .row {
position: relative;
margin: 0.2em 0;
}
.contactfieldgroup .contactfieldlabel {
position: absolute;
top: 0;
left: 2px;
width: 110px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #666;
line-height: 18px;
}
.contactfieldgroup .contactfieldlabel select {
width: 100%;
color: #666;
}
.contactfieldgroup .contactfieldcontent {
padding-left: 120px;
min-height: 1em;
line-height: 18px;
}
.contactfieldgroup .contactfield {
line-height: 1.3em;
}
.contactcontrolleraddress .contactfieldcontent input {
margin-bottom: 0.1em;
}
.contactfieldcontent.composite {
padding-bottom: 8px;
}
.contactfieldcontent .contactfieldbutton {
vertical-align: middle;
margin-left: 0.5em;
}
.contactfield .ff_notes {
width: 99%;
}
a.deletebutton {
position: relative;
left: 5px;
top: -3px;
display: inline-block;
width: 24px;
height: 18px;
text-decoration: none;
text-indent: -5000px;
background: url(images/buttons.png) -7px -377px no-repeat;
}
#import-box {
position: absolute;
bottom: 0px;
top: 34px;
left: 0;
right: 0;
overflow: auto;
padding: 10px;
}
#import-box p,
#import-box .propform {
max-width: 50em;
}
ul.toolbarmenu li span.qrcode {
background-position: 0 -2408px;
}
ul.toolbarmenu li span.assigngroup {
background-position: 0 -2358px;
}
ul.toolbarmenu li span.removegroup {
background-position: 0 -2384px;
}
#addressbook-selector li a,
#contactgroup-selector li a {
padding-left: 2px;
}
#addressbook-selector li a span,
#contactgroup-selector li a span {
background: url(images/listicons.png) 4px 20px no-repeat;
display: block;
height: 17px;
min-height: 14px;
padding: 4px 4px 1px 28px;
overflow: hidden;
max-width: 120px;
text-overflow: ellipsis;
}
#addressbook-selector li a.addressbook span {
background-position: 4px -2222px;
}
#addressbook-selector li a.contactgroup span,
#contactgroup-selector li a.contactgroup span {
background-position: 4px -2245px;
}
diff --git a/skins/larry/embed.css b/skins/larry/embed.css
index 53d02a94a..031e20160 100644
--- a/skins/larry/embed.css
+++ b/skins/larry/embed.css
@@ -1,36 +1,36 @@
/**
* Roundcube webmail "embedded" stylesheets
*
- * Copyright (c) 2012, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
.rcmail-inline-message {
font-family: sans-serif;
font-size: 11px;
font-weight: bold;
color: #996600;
border: 1px solid #ffdf0e;
background: url("images/messages.png") no-repeat scroll 5px -83px #fef893;
padding: 6px 12px 4px 30px;
margin-bottom: 0.8em;
}
.rcmail-inline-message em {
font-size: 90%;
}
.rcmail-inline-buttons {
margin-bottom: 0;
display: inline;
}
.rcmail-inline-buttons > button {
margin-left: 1em;
vertical-align: baseline;
line-height: 12px;
}
diff --git a/skins/larry/mail.css b/skins/larry/mail.css
index 1f75edec8..3673c868d 100644
--- a/skins/larry/mail.css
+++ b/skins/larry/mail.css
@@ -1,1564 +1,1564 @@
/**
* Roundcube webmail styles for the Email section
*
- * Copyright (c) 2012-2017, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
* Screendesign by FLINT / Büro für Gestaltung, bueroflint.com
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
#mailview-left {
position: absolute;
top: 0;
left: 0;
width: 200px;
bottom: 0;
z-index: 1; /* fixes scrolling in Edge (#5750) */
}
#mailview-right {
position: absolute;
top: 0;
left: 212px;
right: 0;
bottom: 0;
}
#mailview-right.fullwidth {
left: 0;
}
#mailview-top {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0px;
}
html.ie #mailview-top {
overflow: visible; /* fixes display issues of fixed list header in IE */
}
#mailview-bottom {
display: none;
position: absolute;
left: 0;
bottom: 0;
right: 0;
height: 0;
border-radius: 4px;
}
#composeview-right #mailview-bottom {
border-radius: 0 0 4px 4px;
}
#mailboxcontainer,
#messagelistcontainer {
position: absolute;
top: 0;
left: 0;
width: 100%;
bottom: 0;
outline: none;
}
#messagelistcontainer {
top: 0;
bottom: 30px;
overflow: auto;
}
/* Real browsers accept this (not IE) */
html>/**/body #messagelist {
overflow: auto;
overflow-x: hidden;
}
#messagelistfooter {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 22px;
padding: 5px 6px 3px;
border-top: 1px solid #ddd;
background: #eaeaea;
border-radius: 0 0 4px 4px;
white-space: nowrap;
}
#messagelistfooter.rightalign {
text-align: right;
}
#messagelistfooter #countcontrols {
display: inline-block;
}
#messagelistfooter #listcontrols,
#messagelistfooter #listselectors {
display: inline-block;
margin-right: 2em;
vertical-align: middle;
}
#messagelistfooter #listselectors .menuselector {
margin-top: -2px;
}
a.iconbutton.listmode {
width: 26px;
height: 20px;
background-position: 0 -477px;
}
a.iconbutton.threadmode {
width: 26px;
height: 20px;
background-position: 0 -497px;
}
a.iconbutton.listmode.selected {
background-position: -26px -477px;
}
a.iconbutton.threadmode.selected {
background-position: -26px -497px;
}
#mailboxlist > li:first-child {
border-top: 0;
}
.folderlist li.mailbox.unread > a {
padding-right: 36px;
}
.folderlist li.unread {
font-weight: bold;
}
.folderlist li.recent > a {
color: #017cb4;
}
.folderlist li.mailbox .unreadcount {
position: absolute;
top: 3px;
right: 6px;
min-width: 1.8em;
line-height: 15px;
padding: 2px 4px;
background: #6a939f;
border-radius: 9px;
color: #fff;
text-align: center;
font-weight: bold;
text-shadow: none;
}
.folderlist li.mailbox.selected > a .unreadcount {
background: #005d76;
}
.folderlist li.mailbox.recent > a .unreadcount {
background: #006ca4;
}
#searchfilter {
position: absolute;
right: 256px;
width: auto;
top: 2px;
}
#searchfilter select {
height: 26px;
}
#mailview-left select.mailboxlist {
position: relative;
top: 10px;
width: 100%;
}
#messagetoolbar {
position: absolute;
top: -6px;
left: 0;
height: 40px;
white-space: nowrap;
}
#messagetoolbar.fullwidth {
right: 0;
}
#messagesearchtools {
position: absolute;
right: 0;
top: 0;
}
#s_interval {
margin: 3px 8px;
}
/*** message list ***/
table.messagelist {
z-index: 1;
}
table.messagelist.fixedcopy {
z-index: 2;
}
.messagelist thead th:first-child {
border-radius: 4px 0 0 0; /* for Chrome */
}
.messagelist tr > .attachment,
.messagelist tr > .threads,
.messagelist tr > .status,
.messagelist tr > .flag,
.messagelist tr > .priority {
width: 20px;
padding: 2px 3px !important;
}
.messagelist tr > .threads {
width: 26px;
}
.messagelist tr > .threads + td,
.messagelist tr > .threads + th {
border-left: 0;
}
.messagelist tr > .size {
width: 60px;
text-align: right;
}
.messagelist thead tr th.size {
text-align: left;
}
.messagelist tr > .fromto,
.messagelist tr > .from,
.messagelist tr > .to,
.messagelist tr > .cc,
.messagelist tr > .replyto {
width: 200px;
}
.messagelist tr > .date {
width: 155px;
}
.messagelist tr > .folder {
width: 135px;
}
.messagelist tr > .hidden {
display: none;
}
.messagelist tr.message {
/* background-color: #fff; */
}
.messagelist tr.thread.expanded:not(.selected) td {
background-color: #ededed;
}
.messagelist tr.unread {
font-weight: bold;
/* background-color: #fff; */
}
.messagelist tr.flagged th,
.messagelist tr.flagged td,
.messagelist tr.flagged td a {
color: #f30;
}
.messagelist thead tr th.sortedASC a,
.messagelist thead tr th.sortedDESC a {
color: #004458;
text-decoration: underline;
background-image: url(images/listicons.png);
background-repeat: no-repeat;
background-position: right -912px;
}
.messagelist thead tr th.sortedASC a {
background-position: right -944px;
}
.messagelist td img {
vertical-align: middle;
display: inline-block;
}
.messagelist tbody td a {
color: #333;
text-decoration: none;
white-space: nowrap;
cursor: default;
}
.messagelist tbody tr td.flag,
.messagelist tbody tr td.status,
.messagelist tbody tr td.subject span.status {
cursor: pointer;
}
.messagelist tr > .flag span,
.messagelist tr > .status span,
.messagelist tr > .attachment span,
.messagelist tr > .priority span {
display: block;
width: 20px;
text-indent: -5000px;
overflow: hidden;
}
.messagelist tr td div.collapsed,
.messagelist tr td div.expanded,
.messagelist tr > .threads .listmenu,
.messagelist tr .attachment span.attachment,
.messagelist tr .attachment span.report,
.messagelist tr .attachment span.encrypted,
.messagelist tr > .priority span.priority,
.messagelist tr > .priority span.prio1,
.messagelist tr > .priority span.prio2,
.messagelist tr > .priority span.prio3,
.messagelist tr > .priority span.prio4,
.messagelist tr > .priority span.prio5,
.messagelist tr .flag span.flagged,
.messagelist tr .flag span.unflagged,
.messagelist tr .flag span.unflagged:hover,
.messagelist tr > .status span.status,
.messagelist tr > .status span.msgicon,
.messagelist tr > .status span.deleted,
.messagelist tr > .status span.unread,
.messagelist tr > .status span.unreadchildren,
.messagelist tr > .subject span.msgicon,
.messagelist tr > .subject span.deleted,
.messagelist tr > .subject span.unread,
.messagelist tr > .subject span.replied,
.messagelist tr > .subject span.forwarded,
.messagelist tr > .subject span.unreadchildren {
display: inline-block;
vertical-align: middle;
height: 18px;
width: 20px;
padding: 0;
background: url(images/listicons.png) -100px 0 no-repeat;
}
.messagelist tbody tr .attachment span.attachment {
background-position: 0 -996px;
}
.messagelist thead tr .attachment span.attachment {
background-position: -24px -996px;
}
.messagelist tbody tr .attachment span.report {
background-position: -24px -1116px;
}
.messagelist tbody tr .attachment span.encrypted {
background-position: 0 -2272px;
}
.messagelist thead tr th.priority span.priority {
background-position: -25px -1845px;
}
.messagelist tr td.priority span.prio5 {
background-position: -2px -1905px;
}
.messagelist tr td.priority span.prio4 {
background-position: -2px -1885px;
}
.messagelist tr td.priority span.prio2 {
background-position: -2px -1865px;
}
.messagelist tr td.priority span.prio1 {
background-position: -2px -1845px;
}
/* thread parent message with flagged children */
.messagelist tbody tr.flaggedroot .flag span {
background-position: -23px -1076px;
}
.messagelist tbody tr .flag span.flagged {
background-position: 0 -1036px;
}
.messagelist thead tr th.flag span.flagged {
background-position: -22px -1037px;
}
.messagelist tr:hover td.status span.msgicon {
background-position: -23px -1057px;
}
.messagelist tr:hover .flag span.unflagged {
background-position: -23px -1076px;
}
.messagelist tr td.subject span.msgicon,
.messagelist tr td.subject span.unreadchildren {
background-position: 0 -1056px;
margin: 0 1px 0 0;
width: 24px;
}
.messagelist tr td.subject span.replied {
background-position: 0 -1076px;
}
.messagelist tr td.subject span.forwarded {
background-position: 0 -1096px;
}
.messagelist tr td.subject span.replied.forwarded {
background-position: 0 -1116px;
}
.messagelist tr td.status span.msgicon,
.messagelist tr td.flag span.unflagged,
.messagelist tr td.status span.unreadchildren {
background-position: 0 1056px; /* no icon */
}
/*
.messagelist tr td.status span.msgicon:hover {
background-position: 0 -272px;
}
*/
.messagelist tr td.status span.deleted,
.messagelist tr:hover td.status span.deleted,
.messagelist tr td.subject span.deleted {
background-position: -21px -1096px;
}
.messagelist tr td.status span.status,
.messagelist tr td.status span.unread,
.messagelist tr td.subject span.unread,
.messagelist tr td.status span.unread:hover {
background-position: 0 -1017px !important;
}
.messagelist thead tr th.status span.status {
background-position: -23px -1017px;
}
.messagelist tr td div.collapsed {
background-position: 0 -1137px;
cursor: pointer;
}
.messagelist tr td div.expanded {
background-position: 0 -1157px;
cursor: pointer;
}
.messagelist tr th.threads .listmenu {
background-position: 4px -972px;
cursor: pointer;
width: 24px;
height: 21px;
overflow: hidden;
text-indent: -5000px;
margin: -3px -5px -2px -6px;
padding: 3px 5px 2px 6px;
}
.messagelist tr th.threads .listmenu:focus {
background-color: rgba(73,180,210,0.7);
outline: none;
}
.messagelist thead tr th.subject,
.messagelist tbody tr td.subject {
width: 99%;
white-space: nowrap;
}
.messagelist tbody tr td.subject a {
cursor: default;
vertical-align: middle; /* #1487091 */
}
/* thread parent message with unread children */
.messagelist tbody tr.unroot td.subject a {
text-decoration: underline;
}
/**** tree indicators ****/
.messagelist tbody tr td span.branch div {
display: inline-block;
}
.messagelist tbody tr td span.branch div.tree {
width: 15px;
}
#listoptions ul.proplist {
min-width: 16em;
}
/**** message view ****/
#mailpreviewframe {
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
bottom: 0px;
}
#messagecontframe {
border: 0;
border-radius: 4px;
}
#messagecontent {
position: absolute;
top: 110px;
left: 0;
width: 100%;
bottom: 1px;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
#messageheader,
#composeheaders {
position: relative;
padding: 3px 0;
background: #f9f9f9;
border-bottom: 1px solid #dfdfdf;
}
#mailview-right #messageheader {
border-radius: 4px 4px 0 0;
padding-left: 78px;
/* avoid headers eating up all the vertical space */
max-height: 50%;
overflow: auto;
}
h2.subject {
font-size: 15px;
margin: 0 15em 0 0;
padding: 4px 8px 2px 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#mailview-right #messageheader h2.subject {
margin-left: -56px;
}
h3.subject {
font-size: 14px;
margin: 0 15em 0 0;
padding: 8px 8px 4px 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.headers-table td {
color: #666;
padding: 1px 8px;
}
.headers-table td.header,
.ui-dialog-content.popup span.adr {
font-weight: bold;
}
.headers-table td.header-title {
white-space: nowrap;
}
.headers-table td.header a,
.ui-dialog-content.popup span.adr a {
color: #666;
text-decoration: none;
}
.headers-table td.header a:hover,
.ui-dialog-content.popup span.adr a:hover {
text-decoration: underline;
}
.headers-table td.subject {
color: #333;
font-weight: bold;
}
.headers-table td.header span,
.ui-dialog-content.popup span.adr {
white-space: nowrap;
}
.headers-table td.header a.morelink {
color: #0069a6;
white-space: nowrap;
font-weight: normal;
}
.rcmaddcontact {
position: relative;
top: 1px;
margin-left: 0.5em;
}
.rcmaddcontact imp {
width: 20px;
height: 13px;
}
#preview-allheaders {
display: none;
}
#preview-allheaders td.header-title,
#preview-shortheaders td.header-title {
padding-left: 0;
}
#preview-shortheaders td.header {
padding-right: 18px;
}
.moreheaderstoggle {
display: block;
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 18px;
padding: 0;
outline: none;
background: #e9e9e9;
border-right: 1px solid #dfdfdf;
border-radius: 3px 0 0 0; /* for Opera */
}
.moreheaderstoggle:focus {
background: #66bcd9;
border-right-color: #66bcd9;
}
.moreheaderstoggle .iconlink {
display: inline-block;
position: absolute;
top: 8px;
left: 0;
width: 18px;
height: 16px;
background: url(images/buttons.png) -27px -242px no-repeat;
}
.moreheaderstoggle.remove .iconlink {
top: auto;
bottom: 5px;
background-position: -5px -242px;
}
#full-headers {
position: relative;
}
div.more-headers {
position: absolute;
top: -12px;
right: 10px;
width: 12px;
height: 10px;
cursor: pointer;
background: url(images/buttons.png) center -1579px no-repeat;
}
div.hide-headers {
background-position: center -1590px;
}
#all-headers {
position: relative;
margin: 4px 10px;
padding: 0;
height: 180px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fdfdfd;
}
#headers-source {
display: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 3px 6px;
overflow: auto;
text-align: left;
color: #333;
}
#messageheader.previewheader #all-headers {
margin-left: 0;
}
#messageheader.previewheader {
position: relative;
height: auto;
min-height: 52px;
padding: 0 0 3px 72px;
}
#messageheader.previewheader h3.subject {
padding: 8px 8px 2px 0;
}
#messageheader.previewheader #contactphoto {
display: block;
position: absolute;
top: 11px;
left: 30px;
width: 32px;
height: 32px;
overflow: hidden;
background: url(images/contactpic_32px.png) center center no-repeat #fff;
border-radius: 3px;
}
#messageheader.previewheader #contactphoto img {
width: 32px;
height: auto;
border-radius: 3px;
}
#messageheader .message-headers {
min-height: 60px;
}
#messageheader #contactphoto {
display: block;
position: absolute;
top: 34px;
left: 30px;
width: 48px;
height: 48px;
overflow: hidden;
border-radius: 4px;
border: 1px solid #e6e6e6;
background: url(images/contactpic_48px.png) center center no-repeat #fff;
}
#messageheader #contactphoto img {
width: 48px;
height: auto;
border-radius: 4px;
}
#messageheader #countcontrols,
#messageheader #formatcontrols {
position: absolute;
top: 8px;
right: 8px;
text-align: right;
white-space: nowrap;
}
#messageheader #formatcontrols {
top: 38px;
right: 8px;
}
#messageheader .pagenav .countdisplay {
padding-right: 0.5em;
white-space: nowrap;
}
#messagecontent .leftcol,
#messagepreview .leftcol {
margin-right: 252px;
}
#messagecontent .rightcol,
#messagepreview .rightcol {
position: absolute;
right: 8px;
width: 230px;
min-height: 200px;
background: #f0f0f0;
padding: 8px;
border-radius: 4px;
z-index: 1;
}
#messagecontent .rightcol {
margin-top: 8px;
}
#messagebody {
position: relative;
margin: 8px;
}
#messagebody.mailvelope {
margin: 0;
}
#message-objects div,
#messagebody span.part-notice {
margin: 8px;
}
#message-objects div.notice {
display: block;
color: #960;
border: 1px solid #ffdf0e;
background-color: #fef893;
background-position: 5px -83px;
padding: 6px 12px 6px 30px;
white-space: normal;
}
#message-objects div a.button,
#messagebody span.part-notice a.button {
margin-left: 10px;
margin-top: -1px;
}
div.message-part,
div.message-htmlpart,
div.message-partheaders {
padding: 10px 2px;
border-top: 1px solid #ccc;
}
#messagebody div:first-child {
padding-top: 0;
border-top: 0;
}
div.message-part div.pre {
margin: 0;
padding: 0;
font-family: monospace;
font-size: 12px;
}
div.message-part span.sig {
color: #666;
}
div.message-part blockquote {
color: blue;
border-left: 2px solid blue;
border-right: 2px solid blue;
background-color: #F6F6F6;
margin: 2px 0;
padding: 0 0.4em;
overflow: hidden;
text-overflow: ellipsis;
}
div.message-part blockquote blockquote {
color: green;
border-left: 2px solid green;
border-right: 2px solid green;
}
div.message-part blockquote blockquote blockquote {
color: #900;
border-left: 2px solid #b00;
border-right: 2px solid #b00;
}
div.message-partheaders {
margin-top: 8px;
padding: 8px 0;
}
div.message-partheaders .headers-table {
width: 100%;
}
div.message-partheaders .headers-table td.header-title {
width: 1%;
padding-left: 0;
vertical-align: top;
}
div.message-partheaders .headers-table td.header {
width: 99%;
}
#messagebody > hr {
color: #fff;
background: #fff;
border: 0;
border-bottom: 2px solid #f0f0f0;
}
#messagebody fieldset.image-attachment {
border: 0;
border-top: 1px solid #ccc;
margin-top: 1em;
}
#messagebody fieldset.image-attachment p > img {
max-width: 80%;
}
#messagebody legend.image-filename {
color: #999;
font-size: 0.9em;
margin: 0 1em;
}
#messagebody p.image-attachment {
position: relative;
padding: 1em;
margin-bottom: 0;
border-top: 1px solid #ccc;
}
#messagebody p.image-attachment a.image-link {
float: left;
display: block;
margin-right: 2em;
min-width: 160px;
min-height: 60px;
text-align: center;
}
#messagebody p.image-attachment .image-filename {
display: block;
font-weight: bold;
line-height: 1.6em;
}
#messagebody p.image-attachment .image-filesize {
padding-right: 1em;
white-space: nowrap;
}
#messagebody p.image-attachment .attachment-links a {
margin-right: 0.6em;
}
#messagepartcontainer {
position: absolute;
top: 0;
left: 232px;
right: 0;
bottom: 0;
}
#messagepartframe {
border: 0;
width: 100%;
height: 100%;
}
#messagepartheader {
position: absolute;
top: 0;
left: 0;
width: 220px;
bottom: 0;
}
#messagepartheader table {
table-layout: fixed;
overflow: hidden;
}
#messagepartheader table td {
text-overflow: ellipsis;
overflow: hidden;
}
#messagepartheader table td.title {
width: 60px;
padding-right: 0;
}
.subject .flag-icon {
display: none;
}
body.status-flagged .flag-icon {
display: inline-block;
background: url(images/listicons.png) 0 -1038px no-repeat;
width: 20px;
height: 16px;
vertical-align: text-top;
}
/*** message composition ***/
#composeview-left {
position: absolute;
top: 0;
left: 0;
width: 200px;
bottom: 0;
}
#composeview-right {
position: absolute;
top: 0;
left: 212px;
right: 0;
bottom: 0;
}
#compose-contacts {
position: absolute;
top: 0;
left: 0;
width: 100%;
bottom: 0;
}
#compose-contacts .listsearchbox {
display: block;
}
#compose-contacts #directorylist {
border-bottom: 4px solid #c7e3ef;
}
#compose-contacts .scroller {
top: 65px;
}
#contacts-table {
table-layout: fixed;
}
#contacts-table td {
width: 100%;
}
#contacts-table td span {
display: block;
}
#contacts-table td span.email {
display: inline;
color: #69939e;
font-style: italic;
margin-left: 0.5em;
}
#compose-contacts li a,
#contacts-table td {
background-image: url(images/listicons.png);
background-position: -100px 0;
background-repeat: no-repeat;
overflow: hidden;
text-overflow: ellipsis;
}
#compose-contacts li a {
padding-left: 36px;
}
#contacts-table td.contactgroup a {
color: #376572;
text-decoration: none;
}
#contacts-table td.contactgroup a span {
display: inline-block;
font-size: 16px;
font-weight: bold;
line-height: 11px;
margin-left: 0.3em;
}
#contacts-table tr:first-child td {
border-top: 0;
}
#compose-contacts li.addressbook a {
background-position: 6px -766px;
}
#compose-contacts li.addressbook a:focus,
#compose-contacts li.addressbook.selected a {
background-position: 6px -791px;
}
#contacts-table td.contactgroup {
background-position: 6px -1553px;
}
#contacts-table tr.selected td.contactgroup {
background-position: 6px -1577px;
}
#contacts-table td.contact {
background-position: 6px -1601px;
}
#contacts-table tr.selected td.contact {
background-position: 6px -1625px;
}
#compose-content {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0px;
overflow: hidden;
}
#composeheaders {
border-radius: 4px 4px 0 0;
padding-left: 19px;
}
#composebuttons {
position: absolute;
top: 6px;
right: 6px;
width: auto;
white-space: nowrap;
z-index: 100;
}
#composebuttons a.button.extwin {
padding: 2px 3px;
}
.compose-headers {
width: 99%;
margin-bottom: 2px;
}
.compose-headers td {
padding: 2px 4px;
}
.compose-headers td.title {
width: 10%;
white-space: nowrap;
padding-left: 6px;
min-width: 60px;
line-height: 16px;
}
.compose-headers td.top {
vertical-align: top;
padding-top: 5px;
}
.compose-headers td.title label {
float: left;
}
.compose-headers td.title a.iconbutton {
float: right;
position: relative;
top: -2px;
width: 15px;
}
.compose-headers td.editfield {
width: 90%;
padding-left: 4px;
}
.compose-headers td.editfield a.iconlink {
margin-left: 0.5em;
}
.compose-headers td.formlinks {
padding: 0 4px;
}
.compose-headers td textarea,
.compose-headers td input {
width: 100%;
resize: none;
}
.compose-headers td.bounceopts {
padding-top: 20px;
}
#compose-cc, #compose-bcc, #compose-replyto, #compose-followupto {
display: none;
}
#composeoptions {
display: none;
padding: 0 0 0 8px;
white-space: normal;
}
.composeoption {
color: #666;
padding-right: 22px;
white-space: nowrap;
}
#composeoptions .composeoption {
display: inline-block;
padding: 4px 22px 4px 0;
}
#composeoptions .composeoption:last-child {
padding-right: 4px;
}
#composeview-bottom {
position: relative;
width: 100%;
height: 200px;
}
#composebodycontainer {
position: absolute;
top: 0;
left: 0;
right: 260px;
bottom: 0;
border-radius: 0 0 0 4px;
}
#composebodycontainer.buttons {
bottom: 42px;
}
#composebodycontainer.mailvelope {
right: 0;
z-index: 10;
}
#composebodycontainer.mailvelope > iframe[scrolling='no'] {
position: relative;
}
#composebody {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 99%;
border: 0;
border-radius: 0 0 0 4px;
padding: 4px;
resize: none;
font-family: monospace;
font-size: 9pt;
outline: none;
}
#composebody:active,
#composebody:focus {
box-shadow: inset 0 0 3px 2px rgba(71,135,177, 0.9);
}
#compose-attachments {
position: absolute;
right: 0;
top: 1px;
bottom: 0;
width: 240px;
background: #f0f0f0;
border-style: solid;
border-color: #f0f0f0 #f0f0f0 #f0f0f0 #ddd;
border-width: 1px;
padding: 8px;
overflow: auto;
}
#image-selector.droptarget,
#compose-attachments.droptarget {
background-image: url(images/filedrop.png);
background-position: center bottom;
background-repeat: no-repeat;
}
#compose-attachments.droptarget.hover,
#compose-attachments.droptarget.active {
border-color: #017db4;
box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
}
#compose-attachments.droptarget.hover {
background-color: #d9ecf4;
box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
}
#compose-attachments .attachment-size {
color: #888;
}
#compose-attachments .hint {
color: #666;
margin: 0 0 8px;
}
#composeview-bottom .formbuttons.floating {
position: absolute;
width: auto;
right: 260px;
z-index: 200;
padding-bottom: 8px;
}
#composebodycontainer .mce-tinymce {
border: 0 !important;
margin-top: 1px;
}
#composebodycontainer .mce-panel {
border-color: #ddd !important;
background: #f0f0f0;
}
#composebody_toolbargroup {
border-bottom: 1px solid #ddd;
}
#uploadform a.iconlink {
margin-left: 1em;
text-indent: -5000px;
}
#uploadform form div {
margin: 4px 0;
}
/**** Styles for widescreen (3-column) view ****/
.widescreen #mailview-top {
bottom: 0;
width: 400px;
height: auto;
}
.widescreen #mailview-bottom {
left: 412px;
top:0;
border: 1px solid #b2b8bf;
}
#messagelistheader,
.widescreen #messagelistfooter #countcontrols,
.widescreen .messagelist > thead,
.widescreen .messagelist .branch,
.widescreen table.fixedcopy {
display: none;
}
.widescreen #messagelistcontainer {
top: 34px;
overflow-x: hidden;
}
.widescreen #messagelistheader {
display: block;
position: absolute;
top: 0;
left: 0;
right: 0;
height: 34px;
padding: 6px 6px;
background: #efefef;
border-radius: 4px 4px 0 0;
white-space: nowrap;
border-bottom: 1px solid #dfdfdf;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.widescreen #messagelistheader .listmenu {
margin-right: 8px;
vertical-align: middle;
line-height: 24px;
width: 28px;
padding: 0;
text-indent: -5000px;
overflow: hidden;
display: inline-block;
background: url(images/listicons.png) 0 -2328px no-repeat;
}
.widescreen #messagelistheader .listmenu:focus {
background-color: rgba(128,128,128,0.55);
outline: none;
}
.widescreen #countcontrols {
line-height: 24px;
display: inline;
min-width: 0;
}
.widescreen #countcontrols span {
padding: 0;
}
.widescreen .pagenavbuttons {
position: absolute;
top: 4px;
right: 6px;
}
.widescreen .pagenavbuttons a.button {
background: none;
border: 0;
padding: 2px;
margin: 0;
height: 20px;
}
.widescreen .pagenavbuttons a.button.pressed {
background: inherit;
}
.widescreen a.listmenu:focus,
.widescreen .pagenav a.button:focus {
border-color: #017db6;
outline: none;
}
.widescreen .messagelist td {
border-left: 0;
vertical-align: top;
padding: 3px 2px !important;
}
.widescreen .messagelist td.subject {
width: 99%;
white-space: wrap;
position: relative; /* for span.date positioning in Firefox */
}
.widescreen .messagelist td.threads {
width: 20px;
vertical-align: bottom;
}
.widescreen .messagelist td.threads div {
padding-bottom: 1px;
}
.widescreen .messagelist td.flags {
width: 22px;
}
.widescreen .messagelist td.subject span {
line-height: 20px;
}
.widescreen .messagelist td.subject span.date {
right: 2px;
top: 3px;
position: absolute;
color: #666;
}
.widescreen .messagelist td.subject span.fromto {
padding-left: 22px;
display: block;
margin-right: 10em;
overflow: hidden;
text-overflow: ellipsis;
color: #666;
}
.widescreen .messagelist tr.flagged td.subject span.date,
.widescreen .messagelist tr.flagged td.subject span.fromto {
color: #ff5c33;
}
.widescreen .messagelist tr.deleted td.subject span.date,
.widescreen .messagelist tr.deleted td.subject span.fromto {
color: #ccc;
}
.widescreen .messagelist td.subject span.subject {
clear: both;
display: block;
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
}
.widescreen .messagelist td.flags span {
width: 20px;
height: 20px;
display: block;
margin-left: 1px;
}
.widescreen .messagelist td.flags span.flag {
cursor: pointer;
}
.widescreen .messagelist tr td.subject span.msgicon,
.widescreen .messagelist tr td.subject span.unreadchildren {
width: 20px;
height: 20px;
}
/* move attachments list to the bottom on narrow mail preview page */
@media screen and (max-width: 800px) {
#messagepreview .leftcol {
margin-right: 0;
}
#messagepreview .rightcol {
min-height: 0;
width: auto;
right: 0;
position: relative;
border-radius: 0;
border-bottom: 1px solid #dfdfdf;
background-color: #f9f9f9;
padding: 5px;
}
ul.attachmentslist,
ul.attachmentslist > li,
div.rightcol > div > a.button {
vertical-align: middle;
display: inline-block;
margin-top: 0;
}
}
diff --git a/skins/larry/print.css b/skins/larry/print.css
index bb2722a53..9ed746664 100644
--- a/skins/larry/print.css
+++ b/skins/larry/print.css
@@ -1,206 +1,206 @@
/**
* Roundcube webmail styles for message printing
*
- * Copyright (c) 2012, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
body {
font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
background-color: #fff;
color: #000;
margin: 2mm;
}
body, td, th, div, p {
font-size: 9pt;
color: #000;
}
h3 {
font-size: 18px;
color: #000;
}
a, a:active, a:visited {
color: #000;
}
#header {
float: right;
margin: 0 5mm 3mm 5mm;
}
table.headers-table {
table-layout: fixed;
margin-top: 14px;
}
table.headers-table tr td {
font-size: 9pt;
vertical-align: top;
}
table.headers-table td.header-title {
color: #666;
padding-right: 4mm;
white-space: nowrap;
}
table.headers-table tr td.subject {
font-weight: bold;
}
#attachment-list {
margin-top: 2mm;
padding-top: 2mm;
border-top: 1pt solid #ccc;
}
#attachment-list li {
font-size: 9pt;
}
#attachment-list li a {
text-decoration: none;
}
#attachment-list li a:hover {
text-decoration: underline;
}
#messagebody {
position: relative;
border-top: none;
}
div.message-part,
div.message-htmlpart {
padding: 2mm 1mm;
margin-top: 2mm;
margin-bottom: 5mm;
border-top: 1pt solid #ccc;
}
div.message-part a {
color: #00c;
}
div.message-part div.pre {
margin: 0;
padding: 0;
font-family: monospace;
font-size: 12px;
}
div.message-part blockquote {
color: blue;
border-left: 2px solid blue;
border-right: 2px solid blue;
background-color: #F6F6F6;
margin: 2px 0;
padding: 0 0.4em;
}
div.message-part blockquote blockquote {
color: green;
border-left: 2px solid green;
border-right: 2px solid green;
}
div.message-part blockquote blockquote blockquote {
color: #900;
border-left: 2px solid #b00;
border-right: 2px solid #b00;
}
p.image-attachment {
position: relative;
padding: 1em;
border-top: 1px solid #ccc;
}
p.image-attachment a.image-link {
float: left;
display: block;
margin-right: 2em;
min-width: 160px;
min-height: 60px;
text-align: center;
}
p.image-attachment .image-filename {
display: block;
line-height: 1.6em;
}
p.image-attachment .attachment-links {
display: none;
}
/* contact print */
#contact-details fieldset {
color: #666;
border: 1px solid #999;
margin-top: 5px;
}
#contact-details fieldset.contactfieldgroup {
border: 0;
padding: 0;
margin: 0;
}
#contact-details div.row {
padding: 2px 0;
}
#contact-details .contactfieldlabel {
display: inline-block;
vertical-align: top;
width: 150px;
overflow: hidden;
text-overflow: ellipsis;
}
#contact-details .contactfieldcontent {
display: inline-block;
vertical-align: top;
font-weight: bold;
}
#contact-details #contactphoto {
float: left;
margin: 5px 15px 5px 3px;
width: 112px;
border: 0;
padding: 0;
}
#contact-details #contactpic {
width: 112px;
background: white;
}
#contact-details #contactpic img {
max-width: 112px;
visibility: inherit;
}
#contact-details #contacthead {
border: 0;
margin: 0 16em 0 0;
padding: 0;
}
#contact-details #contacthead > legend {
display: none;
}
#contact-details #contacthead .names span.namefield {
font-size: 140%;
font-weight: bold;
}
diff --git a/skins/larry/settings.css b/skins/larry/settings.css
index 24faeccba..4a290fd12 100644
--- a/skins/larry/settings.css
+++ b/skins/larry/settings.css
@@ -1,386 +1,386 @@
/**
* Roundcube webmail styles for the Settings section
*
- * Copyright (c) 2017-2017, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
* Screendesign by FLINT / Büro für Gestaltung, bueroflint.com
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
#settings-sections {
position: absolute;
top: 0;
left: 0;
width: 200px;
bottom: 0;
}
#pluginbody,
#settings-right {
position: absolute;
top: 0;
left: 212px;
right: 0;
bottom: 0;
}
#sectionslist {
position: absolute;
top: 0;
left: 0;
width: 260px;
bottom: 0;
}
#preferences-box {
position: absolute;
top: 0;
left: 272px;
right: 0;
bottom: 0;
}
#preferences-frame {
border: 0;
border-radius: 4px;
}
#preferences-details fieldset.advanced legend {
position: relative;
display: block;
width: 100%;
cursor: pointer;
}
#preferences-details fieldset.advanced .propform {
display: none;
}
#preferences-details fieldset.advanced .advanced-toggle {
position: absolute;
display: block;
top: 0px;
right: 6px;
text-decoration: none;
color: #666;
font-size: 11px;
width: 20px;
height: 18px;
background: url('images/listicons.png') 0 -1157px no-repeat;
text-indent: -5000px;
overflow: hidden;
}
#preferences-details fieldset.advanced .collapsed .advanced-toggle {
background-position: -24px -1137px;
}
#preferences-details .identity-encryption-block {
background-color: #eee;
padding: 8px 10px;
}
#preferences-details .identity-encryption-block ul.keylist {
list-style: none;
margin: 8px 0 12px 0;
}
#preferences-details .identity-encryption-block ul.keylist {
display: block;
padding: 4px 10px 4px 0;
}
#sections-table tbody td,
#sections-table .listitem span,
#settings-sections .listitem a,
#settings-sections .tablink a {
padding-left: 36px;
background-image: url(images/listicons.png);
background-position: -100px 0;
background-repeat: no-repeat;
}
/* note: support span.tablink because this is used by plugins */
#settings-sections .listitem a,
#settings-sections .tablink a {
background-position: 6px -862px;
}
#settings-sections .selected a,
#settings-sections .tablink.selected a {
background-position: 6px -887px;
}
#settings-sections .preferences a {
background-position: 6px -431px;
}
#settings-sections .preferences.selected a {
background-position: 6px -455px;
}
#settings-sections .folders a {
background-position: 6px 2px;
}
#settings-sections .folders.selected a {
background-position: 6px -22px;
}
#sections-table #rcmrowfolders .section {
background-position: 4px 2px;
}
#sections-table #rcmrowfolders.selected .section {
background-position: 4px -22px;
}
#settings-sections .identities a {
background-position: 6px -478px;
}
#settings-sections .identities.selected a {
background-position: 6px -502px;
}
#settings-sections .filter a {
background-position: 6px -1746px;
}
#settings-sections .filter.selected a {
background-position: 6px -1770px;
}
#settings-sections .password a {
background-position: 6px -1795px;
}
#settings-sections .password.selected a {
background-position: 6px -1819px;
}
#settings-sections .responses a {
background-position: 6px -1972px;
}
#settings-sections .responses.selected a {
background-position: 6px -1996px;
}
#sections-table #rcmrowgeneral .section {
background-position: 4px -573px;
}
#sections-table #rcmrowgeneral.selected .section {
background-position: 4px -598px;
}
#sections-table #rcmrowmailbox .section {
background-position: 4px -621px;
}
#sections-table #rcmrowmailbox.selected .section {
background-position: 4px -646px;
}
#sections-table #rcmrowcompose .section {
background-position: 4px -670px;
}
#sections-table #rcmrowcompose.selected .section {
background-position: 4px -695px;
}
#sections-table #rcmrowmailview .section {
background-position: 4px -718px;
}
#sections-table #rcmrowmailview.selected .section {
background-position: 4px -742px;
}
#sections-table #rcmrowaddressbook .section {
background-position: 4px -766px;
}
#sections-table #rcmrowaddressbook.selected .section {
background-position: 4px -791px;
}
#sections-table #rcmrowserver .section {
background-position: 4px -814px;
}
#sections-table #rcmrowserver.selected .section {
background-position: 4px -838px;
}
#sections-table #rcmrowcalendar .section {
background-position: 4px -526px;
}
#sections-table #rcmrowcalendar.selected .section {
background-position: 4px -550px;
}
#folderslist .boxtitle a.iconbutton.search {
background-position: -2px -317px;
cursor: pointer;
position: absolute;
right: 8px;
top: 8px;
width: 16px;
}
#folderslist .listsearchbox + .scroller {
top: 34px;
}
.listsearchbox select {
width: 100%;
margin: 0 0 4px 0;
}
#folderslist,
#identitieslist,
#responseslist {
position: absolute;
top: 0;
left: 0;
width: 260px;
bottom: 0;
}
#identities-table,
#responses-table {
width: 100%;
table-layout: fixed;
}
#identities-table tbody td.mail {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
#identities-table tbody tr.readonly td {
font-style: italic;
}
#folder-details,
#identity-details,
#response-details {
position: absolute;
top: 0;
left: 272px;
right: 0;
bottom: 0;
}
#subscription-table li.root {
font-size: 5%;
line-height: 5px;
height: 5px;
padding: 2px;
}
#subscription-table li input {
position: absolute;
right: 8px;
top: 4px;
margin-right: 0;
}
#subscription-table li a {
padding-right: 28px;
}
.skinselection {
display: block;
}
.skinselection span {
display: inline-block;
vertical-align: middle;
padding-right: 1em;
}
.skinselection .skinname {
font-weight: bold;
}
.skinselection .skinlicense,
.skinselection .skinlicense a {
font-style: italic;
color: #666;
text-decoration: none;
}
.skinselection .skinlicense a:hover {
text-decoration: underline;
}
img.skinthumbnail {
width: 64px;
height: 64px;
border: 1px solid #ccc;
background: #fff;
border-radius: 4px;
}
#pluginlist td.version {
width: 5em;
}
.webkit #pluginlist td.version {
width: 6em;
}
#pluginlist td.license,
#pluginlist td.source {
width: 8em;
}
.webkit #pluginlist td.license,
.webkit #pluginlist td.source {
width: 9em;
}
#rcmfd_signature {
width: 99%;
min-width: 390px;
font-family: monospace;
}
#rcmfd_signature_toolbar1 td,
#rcmfd_signature_toolbar2 td {
width: auto;
}
.mailtoprotohandler-status {
padding-left: 1em;
font-style: italic;
}
#pluginlist {
table-layout: auto;
}
#pluginlist th.version {
width: 1%;
}
.readtext {
width: 45em;
padding: 12px;
font-size: 12px;
}
.readtext > h1,
.readtext > h2,
.readtext > h3 {
margin-top: 0;
}
diff --git a/skins/larry/styles.css b/skins/larry/styles.css
index 20ac1b33d..336be639c 100644
--- a/skins/larry/styles.css
+++ b/skins/larry/styles.css
@@ -1,3154 +1,3154 @@
/**
* Roundcube webmail styles for skin "Larry"
*
- * Copyright (c) 2012-2017, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
* Screendesign by FLINT / Büro für Gestaltung, bueroflint.com
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
body {
font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
font-size: 11px;
color: #333;
background: #cad2d9;
margin: 0;
}
body.noscroll {
/* also avoids bounce effect in Chrome and Safari */
overflow: hidden;
}
.iphone body.noscroll {
/* revert on iPhone (#1490551) */
overflow: auto;
}
a {
color: #0069a6;
}
a:visited {
color: #0186ba;
}
img {
border: 0;
}
.voice {
position: absolute;
border: 0;
clip: rect(0 0 0 0);
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
}
input,
textarea,
select,
button {
font-family: inherit;
font-size: inherit;
vertical-align: middle;
}
input[type="text"],
input[type="password"],
textarea {
margin: 0; /* Safari by default adds a margin */
padding: 4px;
border: 1px solid #b2b2b2;
border-radius: 4px;
}
input[type="text"]:focus,
input[type="password"]:focus,
button:focus,
input.button:focus,
textarea:focus {
border-color: #4787b1;
box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
outline: none;
}
input[type="text"]:required,
input[type="password"]:required {
border-color: #4787b1;
}
input.placeholder,
textarea.placeholder {
color: #aaa;
}
.bold {
font-weight: bold;
}
/* fixes vertical alignment of checkboxes and labels */
label input + span {
vertical-align: middle;
}
.noselect {
user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
li > .input-group {
display: inline;
}
/*** buttons ***/
button,
input.button {
display: inline-block;
margin: 0 2px;
padding: 4px 8px;
color: #525252;
border: 1px solid #c0c0c0;
border-radius: 4px;
background: #f7f7f7;
text-decoration: none;
outline: none;
}
.formbuttons button,
.formbuttons input.button {
color: #ddd;
font-size: 110%;
padding: 4px 12px;
border-color: #465864;
border-radius: 5px;
background: #666666;
box-shadow: 0 1px 1px 0 #ccc;
}
.formbuttons button:hover,
.formbuttons button:focus,
.formbuttons input.button:hover,
.formbuttons input.button:focus,
input.button.mainaction:hover,
input.button.mainaction:focus {
color: #f2f2f2;
border-color: #465864;
box-shadow: 0 0 5px 2px rgba(71,135,177, 0.6);
}
.formbuttons button:active,
.formbuttons input.button:active {
color: #fff;
background: #5f5f5f;
}
button.mainaction,
input.button.mainaction {
color: #ededed;
border-color: #1f262c;
background: #2c2f33;
}
button.mainaction:active,
input.button.mainaction:active {
color: #fff;
background: #515151;
background: -moz-linear-gradient(top, #2a2e31 0%, #505050 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#2a2e31), color-stop(100%,#505050));
background: -ms-linear-gradient(top, #2a2e31 0%, #505050 100%);
background: linear-gradient(to bottom, #2a2e31 0%, #505050 100%);
}
button[disabled],
button[disabled]:hover,
input.button[disabled],
input.button[disabled]:hover,
input.button.mainaction[disabled] {
color: #aaa !important;
}
button.mainaction,
input.mainaction {
font-weight: bold !important;
}
form.smart-upload,
input.smart-upload {
visibility: hidden;
width: 1px;
height: 1px;
opacity: 0;
}
/** link buttons **/
a.button,
.buttongroup {
display: inline-block;
margin: 0 2px;
padding: 2px 5px;
color: #525252;
border: 1px solid #c6c6c6;
border-radius: 4px;
background: #e6e6e6;
text-decoration: none;
}
.buttongroup {
padding: 0;
white-space: nowrap;
}
button:focus,
a.button:focus,
input.button:focus {
border-color: #017db6;
box-shadow: 0 0 2px 1px rgba(71,135,177, 0.6);
outline: none;
}
label.disabled,
button.disabled,
a.button.disabled {
color: #999;
}
a.button.disabled,
input.button.disabled,
input.button[disabled],
button.disabled,
button[disabled],
button.disabled:hover,
button[disabled]:hover,
a.button.disabled:hover,
input.button.disabled:hover,
input.button[disabled]:hover {
border-color: #c6c6c6;
}
.buttongroup a.button {
margin: 0;
border-width: 0 1px 0 0;
border-radius: 0;
background: none;
}
.buttongroup a.button.first,
.buttongroup a.button:first-child {
border-radius: 4px 0 0 4px;
border-left: 0;
}
.buttongroup a.button.last,
.buttongroup a.button:last-child {
border-radius: 0 4px 4px 0;
border-right: 0;
}
a.button.pressed,
a.button:active,
button:active,
input.button:active {
background: #f7f7f7;
}
.pagenav.dark a.button {
font-weight: bold;
border: 0;
background: transparent;
margin: 0;
}
.pagenav.dark a.button.pressed {
background: #d8d8d8;
}
.buttongroup a.button.selected,
.buttongroup a.button.selected:hover {
background: #8a8a8a;
border-right-color: #8a8a8a;
border-left-color: #555;
}
.buttongroup a.button:focus,
.buttongroup a.button.selected:focus {
background: #f2f2f2;
background: -moz-linear-gradient(top, #49b3d2 0, #66bcd9 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0,#49b3d2), color-stop(100%,#66bcd9));
background: -ms-linear-gradient(top, #49b3d2 0, #66bcd9 100%);
background: linear-gradient(to bottom, #49b3d2 0, #66bcd9 100%);
}
.pagenav a.button {
padding: 1px 3px;
height: 16px;
vertical-align: middle;
margin-bottom: 1px;
}
.pagenav .buttongroup a.button,
.pagenav .buttongroup a.button:hover {
padding: 1px 5px;
margin-bottom: 0;
}
a.button span.icon,
.pagenav a.button span.inner {
display: inline-block;
width: 16px;
height: 13px;
text-indent: 1000px;
overflow: hidden;
background: url(images/buttons.png) -6px -211px no-repeat;
}
a.button.prevpage span.icon,
.pagenav a.prevpage span.inner {
background-position: -7px -226px;
}
a.button.nextpage span.icon,
.pagenav a.nextpage span.inner {
background-position: -28px -226px;
}
a.button.lastpage span.icon,
.pagenav a.lastpage span.inner {
background-position: -28px -211px;
}
a.button.pageup span.icon,
.pagenav a.pageup span.inner {
background-position: -7px -241px;
}
a.button.pagedown span.icon,
.pagenav a.pagedown span.inner {
background-position: -29px -241px;
}
a.button.reply span.icon,
.pagenav a.reply span.inner {
background-position: -7px -256px;
}
a.button.forward span.icon,
.pagenav a.forward span.inner {
background-position: -29px -256px;
}
a.button.replyall span.icon,
.pagenav a.replyall span.inner {
background-position: -7px -271px;
}
a.button.extwin span.icon,
.pagenav a.extwin span.inner {
background-position: -29px -271px;
}
a.button.changeformat.html span.icon,
.pagenav a.changeformat.html span.inner {
background-position: -7px -1859px;
}
a.button.changeformat.html.selected span.icon,
.pagenav a.changeformat.html.selected span.inner {
background-position: -29px -1859px;
}
a.button.changeformat.text span.icon,
.pagenav a.changeformat.text span.inner {
background-position: -7px -1874px;
}
a.button.changeformat.text.selected span.icon,
.pagenav a.changeformat.text.selected span.inner {
background-position: -29px -1874px;
}
a.button.add span.icon {
background-position: -7px -2009px;
}
a.button.delete span.icon {
background-position: -29px -2009px;
}
.pagenav .countdisplay {
display: inline-block;
padding: 3px 1em 0 1em;
min-width: 16em;
}
.pagenavbuttons {
position: relative;
top: -2px;
}
.pagenav .pagejumper {
text-align: center;
padding: 3px 0;
cursor: default;
}
a.iconbutton {
display: inline-block;
width: 20px;
height: 18px;
text-decoration: none;
text-indent: -5000px;
background: url(images/buttons.png) -1000px 0 no-repeat;
}
a.iconbutton.disabled {
opacity: 0.4;
cursor: default;
}
a.iconbutton.searchicon,
a.iconbutton.searchoptions {
width: 24px;
background-position: -2px -317px;
}
a.iconbutton.searchicon {
width: 15px;
}
a.iconbutton.reset {
width: 24px;
background-position: -25px -317px;
}
a.iconbutton.remove,
a.iconbutton.cancel {
background-position: -7px -378px;
}
a.iconbutton.delete {
background-position: -7px -338px;
}
a.iconbutton.add {
background-position: -7px -358px;
}
a.iconbutton.remove {
background-position: -7px -379px;
}
a.iconbutton.cancel {
background-position: -7px -398px;
}
a.iconbutton.edit {
background-position: -7px -418px;
}
a.iconbutton.upload {
background-position: -6px -438px;
}
a.iconlink {
display: inline-block;
color: #888;
text-decoration: none;
white-space: nowrap;
padding: 2px 8px 2px 20px;
background: url(images/buttons.png) -1000px 0 no-repeat;
}
a.iconlink:hover {
text-decoration: underline;
}
a.iconlink.delete {
background-position: -7px -337px;
}
a.iconlink.add {
background-position: -7px -357px;
}
a.iconlink.remove {
background-position: -7px -378px;
}
a.iconlink.cancel {
background-position: -7px -397px;
}
a.iconlink.edit {
background-position: -7px -417px;
}
a.iconlink.upload {
background-position: -6px -437px;
}
/*** message bar ***/
#message div.loading,
#message div.uploading,
#message div.warning,
#message div.error,
#message div.notice,
#message div.confirmation,
#message-objects div.notice {
color: #555;
font-weight: bold;
padding: 6px 30px 6px 25px;
display: inline-block;
white-space: nowrap;
background: url(images/messages.png) 0 5px no-repeat;
cursor: default;
}
#message div.warning {
color: #960;
background-position: 0 -86px;
}
#message div.error {
color: #cf2734;
background-position: 0 -57px;
}
#message div.confirmation {
color: #093;
background-position: 0 -25px;
}
#message div.loading {
background: url(images/ajaxloader.gif) 2px 6px no-repeat;
}
#message div a,
#message div span {
padding-right: 0.5em;
text-decoration: none;
}
#message div a:hover {
text-decoration: underline;
cursor: pointer;
}
#message.statusbar {
display: none;
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 27px;
padding-left: 8px;
border-top: 1px solid #ddd;
border-radius: 0 0 4px 4px;
background: #eaeaea;
background: -moz-linear-gradient(top, #eaeaea 0%, #c8c8c8 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#eaeaea), color-stop(100%,#c8c8c8));
background: -ms-linear-gradient(top, #eaeaea 0%, #c8c8c8 100%);
background: linear-gradient(to bottom, #eaeaea 0%, #c8c8c8 100%);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#messagestack {
position: absolute;
bottom: 20px;
right: 12px;
z-index: 50000;
width: auto;
height: auto;
max-height: 85%;
overflow-y: auto;
padding: 2px;
}
#messagestack div {
display: block;
position: relative;
width: 280px;
height: auto;
min-height: 16px;
margin: 3px 2px 5px 2px;
padding: 8px 10px 7px 30px;
cursor: default;
font-size: 12px;
font-weight: bold;
border-radius: 4px;
border: 1px solid #444;
color: #ebebeb;
background: rgba(64,64,64,0.85);
background: -moz-linear-gradient(top, rgba(64,64,64,0.85) 0%, rgba(48,48,48,0.9) 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(64,64,64,0.85)), color-stop(100%,rgba(48,48,48,0.9)));
background: -webkit-linear-gradient(top, rgba(64,64,64,0.85) 0%, rgba(48,48,48,0.85) 100%);
background: -ms-linear-gradient(top, rgba(64,64,64,0.85) 0%, rgba(48,48,48,0.85) 100%);
background: linear-gradient(to bottom, rgba(64,64,64,0.85) 0%, rgba(48,48,48,0.85) 100%);
}
#messagestack div:after {
content: "";
position: absolute;
display: block;
top: 0;
left: 4px;
width: 20px;
height: 24px;
background: url(images/messages_dark.png) 0 7px no-repeat;
}
#messagestack div.error {
color: #ff615d;
}
#messagestack div.error:after {
background-position: 0 -55px;
}
#messagestack div.warning {
color: #f4bf0e;
}
#messagestack div.warning:after {
background-position: 0 -84px;
}
#messagestack div.confirmation {
color: #00e05a;
}
#messagestack div.confirmation:after {
background-position: 0 -25px;
}
#messagestack div.uploading,
#messagestack div.loading {
color: #ddd;
}
#messagestack div.uploading:after,
#messagestack div.loading:after {
top: 4px;
left: 6px;
background: url(images/ajaxloader_dark.gif) 0 4px no-repeat;
}
#messagestack div.voice {
position: absolute;
top: -1000px;
}
#messagestack div a {
color: #94c0da;
}
#messagestack div a:hover {
text-decoration: underline;
cursor: pointer;
}
.ui-dialog.error .ui-dialog-title,
.ui-dialog.warning .ui-dialog-title,
.ui-dialog.confirmation .ui-dialog-title {
padding-left: 25px;
background: url(images/messages.png) 0 5px no-repeat;
}
.ui-dialog.warning .ui-dialog-title {
color: #960;
background-position: 0 -91px;
}
.ui-dialog.error .ui-dialog-title {
color: #cf2734;
background-position: 0 -62px;
}
.ui-dialog.confirmation .ui-dialog-title {
color: #093;
background-position: 0 -32px;
}
.ui-autocomplete {
max-height: 160px;
overflow-x: hidden;
overflow-y: auto;
}
/*** basic page layout ***/
#header {
overflow-x: hidden; /* Chrome bug #1488851 */
}
#topline {
height: 18px;
background-color: #333333;
border-bottom: 1px solid #383838;
padding: 2px 0 2px 10px;
color: #aaa;
text-align: center;
}
#topnav {
position: relative;
height: 46px;
margin-bottom: 10px;
padding: 0 0 0 10px;
background: #1c1c1c;
}
#topline a,
#topnav a {
color: #eee;
text-decoration: none;
}
#topline a:hover {
text-decoration: underline;
}
#toplogo {
padding-top: 2px;
cursor: pointer;
border: none;
}
.topleft {
float: left;
}
.topright {
float: right;
}
.closelink {
display: inline-block;
padding: 2px 10px 2px 20px;
}
#topline span.username {
padding-right: 1em;
}
#topline .topleft a {
display: inline-block;
padding: 2px 0.8em 0 0;
color: #aaa;
}
#topline a.button-logout {
display: inline-block;
padding: 2px 10px 2px 20px;
background: url(images/buttons.png) -6px -193px no-repeat;
color: #fff;
}
#taskbar .button-logout {
display: none;
}
#taskbar a.button-logout span.button-inner {
background-position: -2px -1791px;
}
#taskbar a.button-logout:hover span.button-inner {
background-position: -2px -1829px;
}
/*** minimal version of the page header ***/
.minimal #topline {
position: fixed;
top: -18px;
background: #444;
z-index: 5000;
width: 100%;
height: 22px;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.minimal #topline:hover {
top: 0px;
opacity: 0.94;
-webkit-transition: top 0.3s ease-in-out;
-moz-transition: top 0.3s ease-in-out;
transition: top 0.3s ease-in-out;
}
.extwin #topline,
.extwin #topline:hover {
position: static;
top: 0px;
height: 18px;
width: auto;
-moz-box-sizing: content-box;
box-sizing: content-box;
opacity: 0.999;
}
.minimal #topline a.button-logout {
display: none;
}
.minimal #topline span.username {
display: inline-block;
padding-top: 2px;
}
.minimal #topnav {
position: relative;
top: 4px;
height: 42px;
}
.minimal #taskbar a {
position: relative;
padding: 10px 10px 0 6px;
height: 32px;
}
.minimal #taskbar .button-logout {
display: inline-block;
}
.minimal #taskbar .button-inner {
top: -4px;
padding: 0;
height: 24px !important;
width: 27px;
text-indent: -5000px;
}
#taskbar .tooltip {
display: none;
}
.minimal #taskbar .tooltip {
position: absolute;
top: -500px;
right: 2px;
display: inline-block;
padding: 2px 8px 3px 8px;
background: #444;
color: #eee;
font-weight: bold;
white-space: nowrap;
box-shadow: 0 1px 4px 0 #333;
z-index: 200;
white-space: nowrap;
}
.minimal #taskbar .tooltip:after {
content: "";
position: absolute;
top: -4px;
right: 15px;
border-style: solid;
border-width: 0 4px 4px;
border-color: #444 transparent;
/* reduce the damage in FF3.0 */
display: block;
width: 0;
z-index: 251;
}
.minimal #taskbar a:hover .tooltip {
display: block;
top: 39px;
}
/*** taskbar ***/
#taskbar {
position: relative;
padding-right: 18px;
}
#taskbar a {
display: inline-block;
height: 34px;
padding: 12px 10px 0 6px;
}
#taskbar a span.button-inner {
display: inline-block;
font-size: 110%;
font-weight: normal;
padding: 5px 0 0 34px;
height: 19px;
background: url(images/buttons.png) -1000px 0 no-repeat;
}
#taskbar a:focus {
color: #fff;
background-color: rgba(73,180,210,0.7);
outline: none;
}
#taskbar a.button-selected {
color: #20a6fb;
background-color: #2c2c2c;
}
#taskbar a.button-mail span.button-inner {
background-position: 0 2px;
}
#taskbar a.button-mail:hover span.button-inner,
#taskbar a.button-mail.button-selected span.button-inner {
background-position: 0 -22px;
}
#taskbar a.button-addressbook span.button-inner {
background-position: 0 -48px;
}
#taskbar a.button-addressbook:hover span.button-inner,
#taskbar a.button-addressbook.button-selected span.button-inner {
background-position: 0 -72px;
}
#taskbar a.button-settings span.button-inner {
background-position: 0 -96px;
}
#taskbar a.button-settings:hover span.button-inner,
#taskbar a.button-settings.button-selected span.button-inner {
background-position: 0 -120px;
}
#taskbar a.button-calendar span.button-inner {
background-position: 0 -144px;
}
#taskbar a.button-calendar:hover span.button-inner,
#taskbar a.button-calendar.button-selected span.button-inner {
background-position: 0 -168px;
}
#taskbar .minmodetoggle {
position: absolute;
top: 0;
right: 0;
display: block;
width: 19px;
height: 46px;
cursor: pointer;
background: url(images/buttons.png) -35px -1778px no-repeat;
}
.minimal #taskbar .minmodetoggle {
height: 42px;
background-position: -35px -1820px;
}
#mainscreen {
position: absolute;
top: 88px;
left: 10px;
right: 10px;
bottom: 20px;
}
#mainscreencontent {
position: absolute;
top: 42px;
left: 0;
right: 0;
bottom: 0;
}
#mainscreen.offset {
top: 132px;
}
#mainscreen .offset {
top: 42px;
}
.minimal #mainscreen {
top: 62px;
}
.minimal #mainscreen.offset {
top: 102px;
}
.extwin #mainscreen {
top: 40px;
}
.extwin #mainscreen.offset {
top: 86px;
}
.uibox {
border: 1px solid #b2b8bf;
border-radius: 4px;
overflow: hidden;
background: #fff;
}
.minwidth {
min-width: 1024px;
}
.scroller {
overflow: auto;
}
.watermark {
background-image: url(images/watermark.jpg);
background-position: center;
background-repeat: no-repeat;
}
/* fix scrolling within iframes in webkit browsers on touch devices */
@media screen and (-webkit-min-device-pixel-ratio:0) and (max-device-width:1024px) {
.iframebox {
overflow: auto;
-webkit-overflow-scrolling: touch;
}
}
/*** lists ***/
.listbox {
background: #d9ecf4;
overflow: hidden;
}
.listbox .scroller {
position: absolute;
top: 0;
left: 0;
width: 100%;
bottom: 0;
overflow-x: hidden;
overflow-y: auto;
}
.listbox .scroller.withfooter {
bottom: 42px;
}
.listbox .boxtitle + .scroller {
top: 34px;
}
.boxtitle,
.uibox .listing thead th,
.uibox .listing thead td {
font-size: 12px;
font-weight: bold;
padding: 7px 8px 6px 8px;
line-height: 20px;
margin: 0;
border-bottom: 1px solid #bbd3da;
white-space: nowrap;
}
.uibox .listing thead th,
.uibox .listing thead td {
padding-bottom: 8px;
height: auto;
}
.uibox .boxtitle,
.uibox .listing thead th,
.uibox .listing thead td {
background: #b0ccd7;
color: #004458;
}
.listbox .listitem,
.listbox .tablink,
.listing tbody td,
.listing li {
display: block;
border-bottom: 1px solid #bbd3da;
cursor: default;
font-weight: normal;
}
.listbox .listitem a,
.listbox .listitem span,
.listbox .tablink a,
.listing tbody td,
.listing li a {
display: block;
color: #376572;
text-decoration: none;
cursor: default;
padding: 5px 8px;
line-height: 17px;
height: 17px;
white-space: nowrap;
}
.listing tbody td {
display: table-cell;
min-height: 14px;
outline: none;
}
.listing tbody td a {
color: #376572;
text-decoration: none;
}
.webkit .listing tbody td {
height: 14px;
}
/* This padding-left minus the focused padding left should be half of the focused border-left */
.listing thead tr td:first-child,
.listing tbody tr td:first-child {
border-left: 2px solid transparent;
padding-left: 6px;
}
.listing.iconized thead tr td:first-child,
.listing.iconized tbody tr td:first-child {
padding-left: 34px;
}
/* because of border-collapse, we make the left border twice what we want it to be - half will be hidden to the left */
.listing.focus tbody tr.focused > td:first-child {
border-left: 2px solid #739da8;
}
.listbox .listitem.selected,
.listbox .tablink.selected,
.listbox .listitem.selected > a,
.listbox .tablink.selected > a,
.listing tbody tr.selected td,
.listing li.selected,
.listing li.selected > a {
color: #004458;
font-weight: bold;
background-color: #c7e3ef;
}
ul.listing {
display: block;
list-style: none;
margin: 0;
padding: 0;
}
ul.listing li {
background-color: #d9ecf4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
ul.listing li ul {
border-top: 1px solid #bbd3da;
}
ul.listing li.droptarget,
table.listing tr.droptarget td {
background-color: #e8e798;
}
.listbox table.listing {
background-color: #d9ecf4;
}
table.listing,
table.layout {
border: 0;
width: 100%;
border-spacing: 0;
}
table.layout td {
vertical-align: top;
}
ul.treelist li {
position: relative;
}
ul.treelist li ul {
margin: 0;
padding: 0;
}
ul.treelist li ul li:last-child {
border-bottom: 0;
}
ul.treelist li a {
display: block;
padding-left: 20px;
overflow: hidden;
text-overflow: ellipsis;
}
ul.treelist li a:focus,
ul.listing .listitem a:focus,
ul.listing .listitem span:focus,
ul.listing.focus .listitem.focused span {
color: #fff !important;
background-color: rgba(73,180,210,0.6);
outline: none;
}
ul.treelist ul li a {
padding-left: 38px;
}
ul.treelist ul ul li a {
padding-left: 54px;
}
ul.treelist.iconized li a {
padding-left: 36px;
}
ul.treelist.iconized ul li a {
padding-left: 62px;
}
ul.treelist.iconized ul ul li a {
padding-left: 88px;
}
ul.treelist.iconized ul ul ul li a {
padding-left: 114px;
}
ul.treelist li div.treetoggle {
position: absolute;
top: 7px;
left: 4px;
width: 13px;
height: 13px;
background: url(images/listicons.png) -3px -144px no-repeat;
cursor: pointer;
}
ul.treelist li ul li div.treetoggle {
left: 22px;
}
ul.treelist.iconized li div.treetoggle {
top: 13px;
left: 19px;
}
ul.treelist.iconized ul li div.treetoggle {
left: 45px;
}
ul.treelist.iconized ul ul li div.treetoggle {
left: 71px;
}
ul.treelist li div.treetoggle.expanded {
background-position: -3px -168px;
}
ul.treelist li.selected > div.collapsed {
background-position: -23px -144px;
}
ul.treelist li.selected > div.expanded {
background-position: -23px -168px;
}
.listbox .boxfooter {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 42px;
border-top: 1px solid #bbd3da;
background: #d9ecf4;
white-space: nowrap;
overflow: hidden;
}
.uibox .boxfooter {
border-radius: 0 0 4px 4px;
}
.boxfooter .listbutton {
display: inline-block;
text-decoration: none;
width: 48px;
border-right: 1px solid #fff;
background: #c7e3ef;
padding: 3px 0;
margin-top: 1px;
}
.boxfooter a.listbutton:focus {
color: #fff;
background-color: rgba(73,180,210,0.6);
outline: none;
}
.uibox .boxfooter .listbutton:first-child {
border-radius: 0 0 0 4px;
}
.boxfooter .listbutton .inner {
display: inline-block;
width: 48px;
height: 35px;
text-indent: -5000px;
background-image: url(images/buttons.png);
background-position: -1000px 0;
background-repeat: no-repeat;
}
.boxfooter .listbutton.add .inner {
background-position: 10px -1301px;
}
.boxfooter .listbutton.delete .inner {
background-position: 10px -1342px;
}
.boxfooter .listbutton.groupactions .inner {
background-position: 5px -1382px;
}
.boxfooter .listbutton.addto .inner {
background-position: 5px -1422px;
}
.boxfooter .listbutton.addcc .inner {
background-position: 5px -1462px;
}
.boxfooter .listbutton.addbcc {
width: 54px;
}
.boxfooter .listbutton.addbcc .inner {
width: 54px;
background-position: 2px -1502px;
}
.boxfooter .listbutton.removegroup .inner {
background-position: 5px -1540px;
}
.boxfooter .listbutton.disabled .inner {
opacity: 0.4;
}
.boxfooter .countdisplay {
display: inline-block;
position: relative;
top: 10px;
color: #69929e;
padding: 3px 6px;
}
.boxpagenav {
position: absolute;
top: 10px;
right: 6px;
width: auto;
}
.boxpagenav a.icon {
display: inline-block;
padding: 1px 3px;
height: 13px;
width: 14px;
text-indent: 1000px;
vertical-align: bottom;
overflow: hidden;
background: url(images/buttons.png) -4px -286px no-repeat;
}
.boxpagenav a.icon.prevpage {
background-position: -4px -301px;
}
.boxpagenav a.icon.nextpage {
background-position: -28px -301px;
}
.boxpagenav a.icon.lastpage {
background-position: -28px -286px;
}
.boxpagenav a.icon.disabled {
opacity: 0.4;
}
.centerbox {
width: 40em;
margin: 16px auto;
}
.errorbox {
width: 40em;
padding: 20px;
}
.errorbox h3 {
font-size: 16px;
margin-top: 0;
}
/*** Records table ***/
table.records-table {
display: table;
width: 100%;
table-layout: fixed;
border-spacing: 0;
border: 1px solid #bbd3da;
}
.boxlistcontent .records-table {
border: 0;
}
.records-table thead th,
.records-table thead td {
color: #69939e;
font-size: 11px;
font-weight: bold;
background: #d6eaf3;
border-left: 1px solid #bbd3da;
padding: 8px 7px;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
}
.records-table.sortheader thead th,
.records-table.sortheader thead td {
padding: 0;
}
.records-table thead th a,
.records-table thead td a,
.records-table thead th span,
.records-table thead td span {
display: block;
padding: 7px 7px;
color: #69939e;
text-decoration: none;
overflow: hidden;
text-overflow: ellipsis;
}
.records-table thead th a:focus,
.records-table thead td a:focus {
color: #fff;
background-color: rgba(73,180,210,0.7);
outline: none;
}
.records-table tbody td {
padding: 2px 7px;
border-bottom: 1px solid #ddd;
border-left: 1px dotted #bbd3da;
white-space: nowrap;
cursor: default;
overflow: hidden;
text-overflow: ellipsis;
background-color: #fff;
outline: none;
}
/* This padding-left minus the focused padding left should be half of the focused border-left */
.records-table thead tr th:first-child,
.records-table thead tr td:first-child,
.records-table tbody tr td:first-child {
border-left: 2px solid transparent;
padding-left: 4px;
}
/* because of border-collapse, we make the left border twice what we want it to be - half will be hidden to the left */
.records-table.focus tbody tr.focused > td:first-child {
border-left: 2px solid #49b3d2;
}
.records-table tr.selected td {
color: #fff !important;
background-color: #4db0d2;
}
.records-table.focus tr.selected td {
background-color: #017db6 !important;
}
.records-table tr.selected td a,
.records-table tr.selected td span {
color: #fff !important;
}
.records-table tr.deleted td,
.records-table tr.deleted td a {
color: #ccc !important;
}
/*** iFrames ***/
#aboutframe {
width: 97%;
height: 100%;
border: 0;
padding: 0;
}
body.iframe {
background: #fff;
margin: 38px 0 10px 0;
}
body.iframe.error {
background: #ededed;
}
body.iframe.floatingbuttons {
margin-bottom: 40px;
}
body.iframe.fullheight {
margin: 0;
}
.contentbox .boxtitle,
body.iframe .boxtitle {
color: #777;
background: #efefef;
border-bottom: 1px solid #d0d0d0;
}
body.iframe .boxtitle {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 100;
}
body.iframe .footerleft.floating,
#composeview-bottom .formbuttons.floating {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
z-index: 110;
background: #fff;
padding-top: 8px;
padding-bottom: 12px;
}
body.iframe .footerleft.floating:before,
#composeview-bottom .formbuttons.floating:before {
content: " ";
position: absolute;
top: -6px;
left: 0;
width: 100%;
height: 6px;
background: url(images/overflowshadow.png) top center no-repeat;
}
.boxcontent {
padding: 10px;
}
.boxcontent .boxwarning {
margin: 0 0 10px;
display: block;
color: #960;
border: 1px solid #ffdf0e;
background: url(images/messages.png) #fef893 5px -85px no-repeat;
padding: 6px 12px 6px 30px;
}
.contentbox .scroller {
position: absolute;
top: 34px;
left: 0;
right: 0;
bottom: 0px;
overflow: auto;
}
.iframebox {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0px;
}
.footerleft {
padding: 0 12px 4px 12px;
}
.propform fieldset {
margin-bottom: 20px;
border: 0;
padding: 0;
}
.propform fieldset legend {
display: block;
font-size: 14px;
font-weight: bold;
padding-bottom: 10px;
margin-bottom: 0;
}
.propform fieldset fieldset legend {
color: #666;
font-size: 12px;
}
.propform div.prop {
margin-bottom: 0.5em;
}
.propform div.prop.block label {
display: block;
margin-bottom: 0.3em;
}
.propform div.prop.block input,
.propform div.prop.block textarea {
width: 95%;
}
.propform a.disabled {
color: #999;
text-decoration: none;
cursor: default;
}
fieldset.floating {
float: left;
margin-right: 10px;
margin-bottom: 10px;
}
table.propform {
width: 100%;
border-spacing: 0;
border-collapse: collapse;
}
ul.proplist li,
table.propform td {
width: 80%;
padding: 4px 10px;
background: #eee;
border-bottom: 2px solid #fff;
}
table.propform td.title {
width: 20%;
color: #333;
padding-right: 20px;
white-space: nowrap;
}
table.propform .mceLayout td {
padding: 0;
border-bottom: 0;
}
ul.proplist {
list-style: none;
margin: 0;
padding: 0;
}
ul.proplist li {
width: auto;
}
ul.proplist.simplelist li {
border: 0;
background: transparent;
}
#pluginbody {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.formcontent input,
.formcontent textarea {
width: 95%;
}
.formcontent .hint {
font-style: italic;
margin-bottom: 1em;
}
/*** Login form ***/
#login-form {
position: relative;
width: 580px;
margin: 20ex auto 2ex auto;
}
#login-form .box-inner {
width: 430px;
background: #404040;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #404040), color-stop(100%, #2e2e2e));
background: -ms-linear-gradient(top, #404040 0%, #2e2e2e 100%);
background: linear-gradient(to bottom, #404040 0%, #2e2e2e 100%);
margin: 0 50px;
padding: 10px 24px 24px 24px;
border-radius: 6px;
}
#login-form .box-bottom {
margin-top: -3px;
padding-top: 10px;
}
#login-form .noscriptwarning {
margin: 0 auto;
width: 430px;
color: #cf2734;
font-size: 110%;
font-weight: bold;
}
#login-form td.input {
width: 80%;
padding: 8px;
}
#login-form input[type="text"],
#login-form input[type="password"] {
width: 100%;
border-color: #666;
}
#login-form button.button {
color: #444;
border-color: #f9f9f9;
background-color: #f9f9f9;
}
#login-form button.button:active {
color: #333;
background-color: #dcdcdc;
}
#login-form form table {
width: 98%;
}
#login-form td.title {
width: 20%;
white-space: nowrap;
color: #cecece;
text-align: right;
padding-right: 1em;
}
#login-form p.formbuttons {
margin-top: 2em;
text-align: center;
}
#login-form #logo {
margin-bottom: 20px;
border: none;
}
#login-form #message {
min-height: 40px;
padding: 5px 25px;
text-align: center;
font-size: 1.1em;
}
#login-form #message div {
display: inline-block;
padding-right: 0;
font-size: 12px;
}
#bottomline {
font-size: 90%;
text-align: center;
margin-top: 2em;
}
/*** quicksearch **/
.searchbox {
position: relative;
}
#quicksearchbar {
position: absolute;
right: 2px;
top: 2px;
width: 240px;
}
.searchbox input,
#quicksearchbar input {
width: 176px;
margin: 0;
padding: 3px 30px 3px 34px;
height: 18px;
background: #f1f1f1;
border-color: #ababab;
font-weight: bold;
font-size: 11px;
}
.searchbox .searchicon,
.searchbox #searchmenulink,
#quicksearchbar #searchmenulink {
position: absolute;
top: 5px;
left: 6px;
}
.searchbox #searchreset,
.searchbox .iconbutton.reset,
#quicksearchbar #searchreset {
position: absolute;
top: 4px;
right: 1px;
}
.listsearchbox {
padding: 4px;
background: #c7e3ef;
display: none;
}
.listsearchbox input {
width: 100%;
height: 26px;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
/*** toolbar ***/
.toolbar .spacer {
display: inline-block;
width: 24px;
height: 40px;
padding: 0;
}
.toolbar a.button {
text-align: center;
font-size: 10px;
color: #555;
min-width: 50px;
max-width: 70px;
height: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 28px 2px 0 2px;
background: url(images/buttons.png) -100px 0 no-repeat transparent;
border: 0;
border-radius: 0;
}
.dropbutton .dropbuttontip:focus,
.toolbar a.button:focus {
color: #fff;
background-color: rgba(30,150,192, 0.5);
border-radius: 3px;
}
.toolbar a.button.disabled {
opacity: 0.4;
}
.toolbar a.button.selected {
color: #1978a1;
}
.toolbar a.button.selected:focus {
color: #fff;
}
.toolbar a.button.hidden {
display: none;
}
.dropbutton {
display: inline-block;
position: relative;
}
.dropbutton .dropbuttontip {
display: block;
position: absolute;
right: 0;
top: 0;
height: 41px;
width: 18px;
overflow: hidden;
text-indent: -5000px;
background: url(images/buttons.png) 0 -1255px no-repeat;
cursor: pointer;
outline: none;
}
.dropbutton .dropbuttontip:focus,
.dropbutton .dropbuttontip:hover {
background-position: -26px -1255px;
}
.dropbutton a.button.disabled + .dropbuttontip {
opacity: 0.5;
}
.dropbutton a.button.disabled + .dropbuttontip:hover {
background-position: 0 -1255px;
}
.dropbutton a.button {
margin-left: 0;
padding-left: 0;
margin-right: 0;
padding-right: 0;
}
.toolbar a.button.back {
background-position: 0 -1216px;
}
.toolbar a.button.checkmail {
background-position: center -1176px;
}
.toolbar a.button.compose {
background-position: center -530px;
}
.toolbar a.button.reply {
background-position: center -570px;
}
.toolbar a.button.reply-all {
min-width: 64px;
background-position: 0 -610px;
}
.toolbar a.button.forward {
min-width: 64px;
background-position: 0 -650px;
}
.toolbar a.button.delete {
background-position: center -690px;
}
.toolbar a.button.archive {
background-position: center -730px;
}
.toolbar a.button.junk {
background-position: center -770px;
}
.toolbar a.button.print {
background-position: center -810px;
}
.toolbar a.button.markmessage {
background-position: center -1094px;
}
.toolbar a.button.move {
background-position: center -1971px;
}
.toolbar a.button.more {
background-position: center -850px;
}
.toolbar a.button.attach {
background-position: center -890px;
}
.toolbar a.button.spellcheck {
min-width: 64px;
background-position: 0 -930px;
}
.toolbar a.button.spellcheck.selected {
background-position: 0 -1620px;
color: #1978a1;
}
.toolbar a.button.insertsig {
background-position: center -1135px;
}
.toolbar a.button.search {
background-position: center -970px;
}
.toolbar a.button.import {
background-position: center -1012px;
}
.toolbar a.button.export {
min-width: 64px;
background-position: 0 -1054px;
}
.toolbar a.button.send {
background-position: center -1660px;
}
.toolbar a.button.savedraft {
background-position: center -1700px;
}
.toolbar a.button.close {
background-position: 0 -1745px;
}
.toolbar a.button.download {
background-position: center -1892px;
}
.toolbar a.button.responses {
background-position: center -1932px;
}
.toolbar a.button.encrypt {
min-width: 66px;
background-position: center -2025px;
}
.toolbar a.button.encrypt.selected {
background-position: center -2065px;
}
.toolbar a.button.rotate {
background-position: center -2148px;
}
.toolbar a.button.zoomin {
background-position: center -2190px;
}
.toolbar a.button.zoomout {
background-position: center -2230px;
}
a.menuselector {
display: inline-block;
border: 1px solid #ababab;
border-radius: 4px;
background: #f1f1f1;
text-decoration: none;
color: #333;
cursor: pointer;
white-space: nowrap;
}
a.menuselector .handle {
display: inline-block;
padding: 0 32px 0 6px;
height: 20px;
line-height: 19px;
background: url(images/selector.png) right center no-repeat;
border-radius: 4px;
}
a.menuselector:active {
background: #dddddd;
background: -moz-linear-gradient(top, #dddddd 0%, #f8f8f8 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#dddddd), color-stop(100%,#f8f8f8));
background: -ms-linear-gradient(top, #dddddd 0%, #f8f8f8 100%);
background: linear-gradient(to bottom, #dddddd 0%, #f8f8f8 100%);
text-decoration: none;
}
select.decorated {
position: relative;
z-index: 10;
opacity: 0;
height: 22px;
cursor: pointer;
-khtml-appearance: none;
-webkit-appearance: none;
border: 0;
}
html.opera select.decorated {
opacity: 1;
}
select.decorated option {
color: #fff;
background: #444;
border: 0;
border-top: 1px solid #5a5a5a;
border-bottom: 1px solid #333;
padding: 4px 6px;
outline: none;
cursor: default;
}
a.menuselector:focus,
a.menuselector.focus,
a.iconbutton:focus,
.pagenav a.button:focus {
border-color: #0883d0;
box-shadow: 0 0 4px 2px rgba(71,135,177, 0.8);
outline: none;
}
/*** quota indicator ***/
#quotadisplay {
left: 6px;
height: 18px;
font-size: 12px;
font-weight: bold;
padding-left: 30px;
background: url(images/quota.png) -100px 0 no-repeat;
}
#quotadisplay.p90,
#quotadisplay.p100 {
color: #e03221;
}
table.quota-info {
border-spacing: 0;
border-collapse: collapse;
table-layout: fixed;
margin: 5px;
}
table.quota-info td,
table.quota-info th {
color: white;
border: 1px solid lightgrey;
padding: 2px 3px;
text-align: center;
min-width: 80px;
}
table.quota-info td.name {
text-align: left;
}
table.quota-info td.root {
font-style: italic;
}
/*** popup menus ***/
.popupmenu,
#rcmKSearchpane {
display: none;
position: absolute;
top: 32px;
left: 90px;
width: auto;
max-height: 70%;
overflow: -moz-scrollbars-vertical;
overflow-y: auto;
background: #444;
z-index: 240;
border-radius: 4px;
box-shadow: 0 2px 6px 0 #333;
}
.popupmenu.dropdown {
border-radius: 0 0 4px 4px;
border-top: 0;
}
.popupmenu > .buttons {
border-top: 1px solid #5a5a5a;
height: 25px;
padding-top: 5px;
text-align: center;
}
ul.toolbarmenu,
ul.toolbarmenu ul,
#rcmKSearchpane ul {
margin: 0;
padding: 0;
list-style: none;
}
.googie_list td,
ul.toolbarmenu li,
#rcmKSearchpane ul li {
color: #fff;
white-space: nowrap;
min-width: 130px;
margin: 0;
border-top: 1px solid #5a5a5a;
}
.googie_list tr:first-child td,
ul.toolbarmenu > li:first-child,
select.decorated option:first-child {
border-top: 0;
}
.googie_list tr:last-child td,
ul.toolbarmenu > li:last-child,
select.decorated option:last-child {
border-bottom: 0;
}
.googie_list td span,
ul.toolbarmenu li a {
display: block;
color: #666;
text-decoration: none;
min-height: 14px;
padding: 6px 16px 6px 10px;
}
.googie_list td span {
padding: 3px 10px;
}
.googie_list td span,
ul.toolbarmenu li a.active {
color: #fff;
cursor: default;
}
.googie_list td.googie_list_onhover,
ul.toolbarmenu li a.active:hover,
ul.toolbarmenu li a.active:focus,
#rcmKSearchpane ul li.selected,
#pagejump-selector ul li.selected,
select.decorated option:hover,
select.decorated option[selected='selected'] {
background-color: #0883d0;
outline: none;
}
ul.toolbarmenu.iconized li a,
ul.toolbarmenu.selectable li a {
padding-left: 30px;
}
ul.toolbarmenu.selectable li a.selected {
background: url(images/messages.png) 4px -27px no-repeat;
}
ul.toolbarmenu li label {
display: block;
color: #fff;
padding: 4px 8px;
}
ul.toolbarmenu li.separator label {
color: #bbb;
font-style: italic;
padding: 0 8px;
line-height: 17px;
}
ul.toolbarmenu li input {
margin: 0;
}
ul.toolbarmenu li a.icon {
color: #eee;
padding: 2px 6px;
}
ul.toolbarmenu li span.icon,
#rcmKSearchpane ul li i.icon {
display: block;
min-height: 14px;
padding: 4px 4px 1px 24px;
height: 17px;
background-image: url(images/listicons.png);
background-position: -100px 0;
background-repeat: no-repeat;
opacity: 0.2;
}
ul.toolbarmenu li a.active span.icon {
opacity: 0.99;
}
ul.toolbarmenu li span.read {
background-position: 0 -1220px;
}
ul.toolbarmenu li span.unread {
background-position: 0 -1196px;
}
ul.toolbarmenu li span.flagged {
background-position: 0 -1244px;
}
ul.toolbarmenu li span.unflagged {
background-position: 0 -1268px;
}
ul.toolbarmenu li span.mail {
background-position: 0 -1293px;
}
ul.toolbarmenu li span.list {
background-position: 0 -1317px;
}
ul.toolbarmenu li span.invert {
background-position: 0 -1340px;
}
ul.toolbarmenu li span.cross {
background-position: 0 -1365px;
}
ul.toolbarmenu li span.print {
background-position: 0 -1436px;
}
ul.toolbarmenu li span.download {
background-position: 0 -1412px;
}
ul.toolbarmenu li span.rename {
background-position: 0 -2295px;
}
ul.toolbarmenu li span.edit {
background-position: 0 -1388px;
}
ul.toolbarmenu li span.viewsource {
background-position: 0 -1460px;
}
ul.toolbarmenu li span.extwin {
background-position: 0 -1484px;
}
ul.toolbarmenu li span.conversation {
background-position: 0 -1532px;
}
ul.toolbarmenu li span.move {
background-position: 0 -2126px;
}
ul.toolbarmenu li span.copy {
background-position: 0 -2150px;
}
#pagejump-selector {
max-height: 250px;
overflow-x: hidden;
}
#pagejump-selector ul li {
min-width: 45px;
padding: 4px 6px;
cursor: default;
}
#snippetslist {
max-width: 200px;
}
#snippetslist li a {
overflow: hidden;
text-overflow: ellipsis;
}
#rcmKSearchpane {
border-radius: 0 0 4px 4px;
border-top: 0;
}
#rcmKSearchpane ul li {
text-decoration: none;
min-height: 14px;
padding: 6px 10px 6px 28px;
border: 0;
cursor: default;
position: relative;
overflow: hidden;
text-overflow: ellipsis;
}
#rcmKSearchpane ul li i.icon {
opacity: 0.99;
position: absolute;
top: 4px;
left: 5px;
width: 18px;
height: 18px;
padding: 0;
background-position: -1px -2223px;
}
#rcmKSearchpane ul li.group i.icon {
background-position: -1px -2247px;
}
.popupdialog {
display: none;
padding: 10px;
}
.popupdialog .formbuttons {
margin: 20px 0 4px 0;
}
.ui-dialog .prompt input {
display: block;
margin: 8px 0;
}
.ui-dialog iframe {
width: 100%;
height: 100%;
border: 0;
}
.ui-dialog-content.iframe {
padding: 0 !important;
overflow: hidden !important;
}
.hint {
margin: 4px 0;
color: #999;
}
.splitter {
user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
position: absolute;
background: url(images/splitter.png) center no-repeat;
}
.splitter-h {
height: 10px;
width: 100%;
cursor: n-resize;
cursor: row-resize;
background-position: center 0;
}
.splitter-v {
width: 10px;
height: 100%;
cursor: e-resize;
cursor: col-resize;
background-position: 0 center;
}
#rcmdraglayer {
min-width: 260px;
width: auto !important;
width: 260px;
padding: 6px 8px;
background: #444;
border: 1px solid #555;
border-radius: 4px;
box-shadow: 0 2px 6px 0 #333;
z-index: 250;
color: #ccc;
white-space: nowrap;
opacity: 0.92;
}
#rcmdraglayer:after {
content: "";
position: absolute;
top: 6px;
left: -6px;
border-style: solid;
border-width: 6px 6px 6px 0;
border-color: transparent #444;
/* reduce the damage in FF3.0 */
display: block;
width: 0;
z-index: 251;
}
.draglayercopy:before {
position: absolute;
bottom: -6px;
left: -6px;
content: " ";
width: 16px;
height: 16px;
background: url(images/buttons.png) -7px -358px no-repeat;
z-index: 255;
}
.popup label > input {
margin-left: 10px;
}
/*** folder selector ***/
#folder-selector {
z-index: 1000;
}
#folder-selector li a span {
background: url("images/listicons.png") 4px -2021px no-repeat;
display: block;
height: 17px;
min-height: 14px;
padding: 4px 4px 1px 28px;
overflow: hidden;
max-width: 120px;
text-overflow: ellipsis;
}
#folder-selector li a.virtual span {
opacity: .2;
}
#folder-selector li.inbox span {
background-position: 4px -2049px;
}
#folder-selector li.drafts span {
background-position: 4px -1388px;
}
#folder-selector li.sent span {
background-position: 4px -2074px;
}
#folder-selector li.trash span {
background-position: 4px -1508px;
}
#folder-selector li.junk span {
background-position: 4px -2100px;
}
/*** folders list ***/
.folderlist li.mailbox a {
padding-left: 36px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
background-image: url(images/listicons.png);
background-repeat: no-repeat;
background-position: 6px 3px;
}
.folderlist li.mailbox.unread > a {
padding-right: 36px;
}
.folderlist li.mailbox > a:focus,
.folderlist li.mailbox.selected > a {
background-position: 6px -21px;
}
.folderlist li.mailbox.inbox > a {
background-position: 6px -189px;
}
.folderlist li.mailbox.inbox > a:focus,
.folderlist li.mailbox.inbox.selected > a {
background-position: 6px -213px;
}
.folderlist li.mailbox.drafts > a {
background-position: 6px -238px;
}
.folderlist li.mailbox.drafts > a:focus,
.folderlist li.mailbox.drafts.selected > a {
background-position: 6px -262px;
}
.folderlist li.mailbox.sent > a {
background-position: 6px -286px;
}
.folderlist li.mailbox.sent > a:focus,
.folderlist li.mailbox.sent.selected > a {
background-position: 6px -310px;
}
.folderlist li.mailbox.junk > a {
background-position: 6px -334px;
}
.folderlist li.mailbox.junk > a:focus,
.folderlist li.mailbox.junk.selected > a {
background-position: 6px -358px;
}
.folderlist li.mailbox.trash > a {
background-position: 6px -382px;
}
.folderlist li.mailbox.trash > a:focus,
.folderlist li.mailbox.trash.selected > a {
background-position: 6px -406px;
}
.folderlist li.mailbox.trash.empty > a {
background-position: 6px -1924px;
}
.folderlist li.mailbox.trash.empty > a:focus,
.folderlist li.mailbox.trash.empty.selected > a {
background-position: 6px -1948px;
}
.folderlist li.mailbox.archive > a {
background-position: 6px -1699px;
}
.folderlist li.mailbox.archive > a:focus,
.folderlist li.mailbox.archive.selected > a {
background-position: 6px -1723px;
}
.folderlist li.mailbox ul li.drafts > a {
background-position: 23px -238px;
}
.folderlist li.mailbox ul li.drafts > a:focus,
.folderlist li.mailbox ul li.drafts.selected > a {
background-position: 23px -262px;
}
.folderlist li.mailbox ul li.sent > a {
background-position: 23px -286px;
}
.folderlist li.mailbox ul li.sent > a:focus,
.folderlist li.mailbox ul li.sent.selected > a {
background-position: 23px -310px;
}
.folderlist li.mailbox ul li.junk > a {
background-position: 23px -334px;
}
.folderlist li.mailbox ul li.junk > a:focus,
.folderlist li.mailbox ul li.junk.selected > a {
background-position: 23px -358px;
}
.folderlist li.mailbox ul li.trash > a {
background-position: 23px -382px;
}
.folderlist li.mailbox ul li.trash > a:focus,
.folderlist li.mailbox ul li.trash.selected > a {
background-position: 23px -406px;
}
.folderlist li.mailbox ul li.trash.empty > a {
background-position: 23px -1924px;
}
.folderlist li.mailbox ul li.trash.empty > a:focus,
.folderlist li.mailbox ul li.trash.empty.selected > a {
background-position: 23px -1948px;
}
.folderlist li.mailbox ul li.archive > a {
background-position: 23px -1699px;
}
.folderlist li.mailbox ul li.archive > a:focus,
.folderlist li.mailbox ul li.archive.selected > a {
background-position: 23px -1723px;
}
.folderlist li.virtual > a {
color: #aaa;
}
.folderlist li.mailbox div.treetoggle {
top: 13px;
left: 19px;
}
.folderlist li.mailbox ul li:last-child {
border-bottom: 0;
}
/* nested mailboxes */
.folderlist li.mailbox ul {
list-style: none;
margin: 0;
padding: 0;
border-top: 1px solid #bbd3da;
}
.folderlist li.mailbox ul li a {
padding-left: 52px; /* 36 + 1 x 16 */
background-position: 22px -93px; /* 6 + 1 x 16 */
}
.folderlist li.mailbox ul li > a:focus,
.folderlist li.mailbox ul li.selected > a {
background-position: 22px -117px;
}
.folderlist li.mailbox ul li div.treetoggle {
left: 33px;
top: 14px;
}
.folderlist li.mailbox ul ul li.mailbox a {
padding-left: 68px; /* 2x */
background-position: 38px -93px;
}
.folderlist li.mailbox ul ul li > a:focus,
.folderlist li.mailbox ul ul li.selected > a {
background-position: 38px -117px;
}
.folderlist li.mailbox ul ul li div.treetoggle {
left: 48px;
}
.folderlist li.mailbox ul ul ul li.mailbox a {
padding-left: 84px; /* 3x */
background-position: 54px -93px;
}
.folderlist li.mailbox ul ul ul li > a:focus,
.folderlist li.mailbox ul ul ul li.selected > a {
background-position: 54px -117px;
}
.folderlist li.mailbox ul ul ul li div.treetoggle {
left: 64px;
}
.folderlist li.mailbox ul ul ul ul li.mailbox a {
padding-left: 100px; /* 4x */
background-position: 70px -93px;
}
.folderlist li.mailbox ul ul ul ul li > a:focus,
.folderlist li.mailbox ul ul ul ul li.selected > a {
background-position: 70px -117px;
}
.folderlist li.mailbox ul ul ul ul li div.treetoggle {
left: 80px;
}
/* indent folders on levels > 4 */
.folderlist li.mailbox ul ul ul ul ul li {
padding-left: 16px;
}
.folderlist li.mailbox ul ul ul ul ul li div.treetoggle {
left: 96px;
}
/*** attachment list ***/
.attachmentslist {
list-style: none;
margin: 0;
padding: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.attachmentslist li {
display: block;
position: relative;
background: url(images/filetypes.png) 0 0 no-repeat;
margin-bottom: 1px;
line-height: 24px;
}
.attachmentslist li.txt,
.attachmentslist li.text {
background-position: 0 -416px;
}
.attachmentslist li.pdf {
background-position: 0 -26px;
}
.attachmentslist li.doc,
.attachmentslist li.docx,
.attachmentslist li.msword {
background-position: 0 -52px;
}
.attachmentslist li.odt {
background-position: 0 -78px;
}
.attachmentslist li.xls,
.attachmentslist li.xlsx,
.attachmentslist li.msexcel {
background-position: 0 -104px;
}
.attachmentslist li.ods {
background-position: 0 -130px;
}
.attachmentslist li.zip,
.attachmentslist li.gz {
background-position: 0 -156px;
}
.attachmentslist li.rar {
background-position: 0 -182px;
}
.attachmentslist li.image {
background-position: 0 -208px;
}
.attachmentslist li.jpg,
.attachmentslist li.jpeg {
background-position: 0 -234px;
}
.attachmentslist li.png {
background-position: 0 -260px;
}
.attachmentslist li.m4p {
background-position: 0 -286px;
}
.attachmentslist li.mp3,
.attachmentslist li.audio {
background-position: 0 -312px;
}
.attachmentslist li.video {
background-position: 0 -338px;
}
.attachmentslist li.ics,
.attachmentslist li.calendar {
background-position: 0 -364px;
}
.attachmentslist li.vcard {
background-position: 0 -390px;
}
.attachmentslist li.sig,
.attachmentslist li.pgp-signature,
.attachmentslist li.pkcs7-signature {
background-position: 0 -442px;
}
.attachmentslist li.html {
background-position: 0 -468px;
}
.attachmentslist li.eml,
.attachmentslist li.rfc822 {
background-position: 0 -494px;
}
.attachmentslist li.ppt,
.attachmentslist li.pptx,
.attachmentslist li.ppsx,
.attachmentslist li.vnd.mspowerpoint {
background-position: 0 -520px;
}
.attachmentslist li.odp,
.attachmentslist li.otp {
background-position: 0 -546px;
}
.attachmentslist li.application.asc {
background-position: 0 -598px;
}
.attachmentslist li.application.pgp-keys {
background-position: 0 -572px;
}
.attachmentslist li a {
display: block;
color: #333;
font-weight: bold;
padding: 3px 15px 3px 30px;
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 20px;
outline: none;
}
.attachmentslist li a.drop {
background: url(images/buttons.png) no-repeat scroll center -1570px;
width: 14px;
height: 20px;
cursor: pointer;
position: absolute;
right: 0;
top: 0;
padding: 0;
overflow: hidden;
text-indent: -5000px;
outline: none;
}
#compose-attachments .attachmentslist li a.drop {
right: 24px;
}
.attachmentslist li a:focus,
.attachmentslist li a.drop:focus {
background-color: rgba(30,150,192, 0.5);
border-radius: 2px;
}
#compose-attachments ul li {
padding-right: 24px;
}
.attachmentslist li a:hover {
text-decoration: underline;
}
.attachmentslist li.uploading {
background: url(images/ajaxloader.gif) 4px 4px no-repeat;
padding-left: 30px;
}
.attachmentslist li a.delete,
.attachmentslist li a.cancelupload {
position: absolute;
top: 4px;
right: 0;
width: 20px;
height: 18px;
padding: 0;
text-decoration: none;
text-indent: -5000px;
background-image: url(images/buttons.png);
background-position: -6px -338px;
background-repeat: no-repeat;
}
.attachmentslist li a.cancelupload {
background-position: -6px -378px;
}
.attachmentslist li a.filename {
display: flex;
overflow: hidden;
}
.attachmentslist li .attachment-name {
overflow: hidden;
text-overflow: ellipsis;
}
.attachmentslist li .attachment-size {
padding: 0 .25em;
}
/*** fieldset tabs ***/
.tabbed.ui-tabs {
padding: 0;
border: 0 !important;
background: none;
}
.ui-dialog .tabbed.ui-tabs {
margin: -12px -8px 0 -8px;
}
.boxcontent.tabbed.ui-tabs {
padding: 10px;
}
.ui-tabs .tabsbar.ui-tabs-nav {
margin-bottom: 4px;
}
.ui-dialog-content .ui-tabs .tabsbar.ui-tabs-nav {
margin-bottom: 0;
}
.tabsbar .tablink:last-child {
background: none;
}
.tabsbar .tablink:last-child a {
border-right: 0;
}
.ui-tabs .ui-tabs-nav li.tablink a {
background: #fff;
}
.ui-tabs fieldset.ui-tabs-panel {
border: 0;
padding: 0;
margin-left: 0;
background: none;
}
.ui-dialog .propform .ui-tabs-panel {
display: block;
background: #efefef;
padding: 0.5em 1em;
}
#image-selector-form.droptarget {
background: url(images/filedrop.png) center bottom no-repeat;
}
/** Common TinyMCE fixes **/
.mce-btn:not(.mce-active) {
background: transparent !important;
}
.mce-btn:not(.mce-active):hover {
background: white !important;
}
.mce-btn-small .mce-ico {
display: inline; /* for old Firefox */
}
.mce-btn-small i {
line-height: 16px !important;
vertical-align: text-top !important;
}
_:not(), _:-moz-handler-blocked, .mozilla .mce-btn-small i {
line-height: 20px !important;
}
.mce-top-part::before {
box-shadow: none !important;
}
.mce-textbox {
border-radius: 0;
box-shadow: none;
}
button.mce-close,
.mce-btn button,
.mce-textbox:focus {
box-shadow: none;
outline: none;
}
/** PGP Key import dialog **/
.pgpkeyimport div.key {
position: relative;
margin-bottom: 2px;
padding: 1em;
background-color: #ebebeb;
}
.pgpkeyimport div.key.revoked,
.pgpkeyimport div.key.disabled {
color: #a0a0a0;
}
.pgpkeyimport div.key label {
display: inline-block;
margin-right: 0.5em;
}
.pgpkeyimport div.key label:after {
content: ":";
}
.pgpkeyimport div.key label + a,
.pgpkeyimport div.key label + span {
display: inline-block;
margin-right: 2em;
white-space: nowrap;
}
.pgpkeyimport div.key label + a {
font-weight: bold;
}
.pgpkeyimport ul.uids {
margin: 1em 0 0 0;
padding: 0;
}
.pgpkeyimport li.uid {
border: 0;
padding: 0.3em;
}
.pgpkeyimport div.key button.importkey {
position: absolute;
top: 0.8em;
right: 0.8em;
padding: 4px 6px;
}
.pgpkeyimport div.key button[disabled] {
display: none;
}
diff --git a/skins/larry/ui.js b/skins/larry/ui.js
index 917d66525..270aeb051 100644
--- a/skins/larry/ui.js
+++ b/skins/larry/ui.js
@@ -1,1515 +1,1515 @@
/**
* Roundcube functions for default skin interface
*
- * Copyright (c) 2013, The Roundcube Dev Team
+ * Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*
* @license magnet:?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt CC0-1.0
*/
function rcube_mail_ui()
{
var env = {};
var popups = {};
var popupconfig = {
forwardmenu: { editable:1 },
searchmenu: { editable:1, callback:searchmenu },
attachmentmenu: { },
listoptions: { editable:1 },
groupmenu: { above:1 },
mailboxmenu: { above:1 },
spellmenu: { callback: spellmenu },
'folder-selector': { iconized:1 }
};
var me = this;
var mailviewsplit;
var mailviewsplit2;
var compose_headers = {};
var prefs;
// export public methods
this.set = setenv;
this.init = init;
this.init_tabs = init_tabs;
this.show_about = show_about;
this.show_popup = show_popup;
this.toggle_popup = toggle_popup;
this.add_popup = add_popup;
this.import_dialog = import_dialog;
this.set_searchmod = set_searchmod;
this.set_searchscope = set_searchscope;
this.show_header_row = show_header_row;
this.hide_header_row = hide_header_row;
this.update_quota = update_quota;
this.get_pref = get_pref;
this.save_pref = save_pref;
this.folder_search_init = folder_search_init;
// set minimal mode on small screens (don't wait for document.ready)
if (window.$ && document.body) {
var minmode = get_pref('minimalmode');
if (parseInt(minmode) || (minmode === null && $(window).height() < 850)) {
$(document.body).addClass('minimal');
}
if (bw.tablet) {
$('#viewport').attr('content', "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0");
}
$(document).ready(function() { me.init(); });
}
/**
*
*/
function setenv(key, val)
{
env[key] = val;
}
/**
* Get preference stored in browser
*/
function get_pref(key)
{
if (!prefs) {
prefs = rcmail.local_storage_get_item('prefs.larry', {});
}
// fall-back to cookies
if (prefs[key] == null) {
var cookie = rcmail.get_cookie(key);
if (cookie != null) {
prefs[key] = cookie;
// copy value to local storage and remove cookie (if localStorage is supported)
if (rcmail.local_storage_set_item('prefs.larry', prefs)) {
rcmail.set_cookie(key, cookie, new Date()); // expire cookie
}
}
}
return prefs[key];
}
/**
* Saves preference value to browser storage
*/
function save_pref(key, val)
{
prefs[key] = val;
// write prefs to local storage (if supported)
if (!rcmail.local_storage_set_item('prefs.larry', prefs)) {
// store value in cookie
var exp = new Date();
exp.setYear(exp.getFullYear() + 1);
rcmail.set_cookie(key, val, exp);
}
}
/**
* Initialize UI
* Called on document.ready
*/
function init()
{
rcmail.addEventListener('message', message_displayed);
$.widget('ui.dialog', $.ui.dialog, {
open: function() {
this._super();
dialog_open(this);
return this;
}});
/*** prepare minmode functions ***/
$('#taskbar a').each(function(i,elem){
$(elem).append('<span class="tooltip">' + $('.button-inner', this).html() + '</span>')
});
$('#taskbar .minmodetoggle').click(function(e){
var ismin = $(document.body).toggleClass('minimal').hasClass('minimal');
save_pref('minimalmode', ismin?1:0);
$(window).resize();
});
/*** mail task ***/
if (rcmail.env.task == 'mail') {
rcmail.addEventListener('menu-open', menu_toggle)
.addEventListener('menu-close', menu_toggle)
.addEventListener('menu-save', save_listoptions)
.addEventListener('enable-command', enable_command)
.addEventListener('responseafterlist', function(e){ switch_view_mode(rcmail.env.threading ? 'thread' : 'list', true) })
.addEventListener('responseaftersearch', function(e){ switch_view_mode(rcmail.env.threading ? 'thread' : 'list', true) });
var dragmenu = $('#dragmessagemenu');
if (dragmenu.length) {
rcmail.gui_object('dragmenu', 'dragmessagemenu');
popups.dragmenu = dragmenu;
}
if (rcmail.env.action == 'show' || rcmail.env.action == 'preview') {
rcmail.addEventListener('aftershow-headers', function() { layout_messageview(); })
.addEventListener('afterhide-headers', function() { layout_messageview(); });
$('#previewheaderstoggle').click(function(e) {
toggle_preview_headers();
if (this.blur && !rcube_event.is_keyboard(e))
this.blur();
return false;
});
// add menu link for each attachment
$('#attachment-list > li').each(function() {
attachmentmenu_append(this);
});
if (get_pref('previewheaders') == '1') {
toggle_preview_headers();
}
if (rcmail.env.action == 'show') {
$('#messagecontent').focus();
}
}
else if (rcmail.env.action == 'compose') {
rcmail.addEventListener('fileappended', function(e) { if (e.attachment.complete) attachmentmenu_append(e.item); })
.addEventListener('aftertoggle-editor', function(e) {
window.setTimeout(function() { layout_composeview() }, 200);
if (e && e.mode)
$("select[name='editorSelector']").val(e.mode);
})
.addEventListener('compose-encrypted', function(e) {
$("select[name='editorSelector']").prop('disabled', e.active);
$('a.button.attach, a.button.responses')[(e.active?'addClass':'removeClass')]('disabled');
$('#responseslist a.insertresponse')[(e.active?'removeClass':'addClass')]('active');
});
init_compose_editfields();
$('#composeoptionstoggle').click(function(e){
var expanded = $('#composeoptions').toggle().is(':visible');
$('#composeoptionstoggle').toggleClass('remove').attr('aria-expanded', expanded ? 'true' : 'false');
layout_composeview();
save_pref('composeoptions', expanded ? '1' : '0');
if (!rcube_event.is_keyboard(e))
this.blur();
return false;
}).css('cursor', 'pointer');
if (get_pref('composeoptions') !== '0') {
$('#composeoptionstoggle').click();
}
// toggle compose options if opened in new window and they were visible before
var opener_rc = rcmail.opener();
if (opener_rc && opener_rc.env.action == 'compose' && $('#composeoptionstoggle', opener.document).hasClass('remove'))
$('#composeoptionstoggle').click();
new rcube_splitter({ id:'composesplitterv', p1:'#composeview-left', p2:'#composeview-right',
orientation:'v', relative:true, start:206, min:170, size:12, render:layout_composeview }).init();
// add menu link for each attachment
$('#attachment-list > li').each(function() {
attachmentmenu_append(this);
});
}
else if (rcmail.env.action == 'bounce') {
init_compose_editfields();
}
else if (rcmail.env.action == 'list' || !rcmail.env.action) {
mail_layout();
$('#maillistmode').addClass(rcmail.env.threading ? '' : 'selected').click(function(e) { switch_view_mode('list'); return false; });
$('#mailthreadmode').addClass(rcmail.env.threading ? 'selected' : '').click(function(e) { switch_view_mode('thread'); return false; });
rcmail.init_pagejumper('#pagejumper');
rcmail.addEventListener('setquota', update_quota)
.addEventListener('layout-change', mail_layout);
}
else if (rcmail.env.action == 'get') {
new rcube_splitter({ id:'mailpartsplitterv', p1:'#messagepartheader', p2:'#messagepartcontainer',
orientation:'v', relative:true, start:226, min:150, size:12}).init();
}
if ($('#mailview-left').length) {
new rcube_splitter({ id:'mailviewsplitterv', p1:'#mailview-left', p2:'#mailview-right',
orientation:'v', relative:true, start:206, min:150, size:12, callback:render_mailboxlist, render:resize_leftcol }).init();
}
}
/*** settings task ***/
else if (rcmail.env.task == 'settings') {
rcmail.addEventListener('init', function(){
var tab = '#settingstabpreferences';
if (rcmail.env.action)
tab = '#settingstab' + (rcmail.env.action.indexOf('identity')>0 ? 'identities' : rcmail.env.action.replace(/\./g, ''));
$(tab).addClass('selected')
.children().first().removeAttr('onclick').click(function() { return false; });
});
if (rcmail.env.action == 'folders') {
new rcube_splitter({ id:'folderviewsplitter', p1:'#folderslist', p2:'#folder-details',
orientation:'v', relative:true, start:266, min:180, size:12 }).init();
rcmail.addEventListener('setquota', update_quota);
folder_search_init($('#folderslist'));
}
else if (rcmail.env.action == 'identities') {
new rcube_splitter({ id:'identviewsplitter', p1:'#identitieslist', p2:'#identity-details',
orientation:'v', relative:true, start:266, min:180, size:12 }).init();
}
else if (rcmail.env.action == 'responses') {
new rcube_splitter({ id:'responseviewsplitter', p1:'#responseslist', p2:'#response-details',
orientation:'v', relative:true, start:266, min:180, size:12 }).init();
}
else if (rcmail.env.action == 'preferences' || !rcmail.env.action) {
new rcube_splitter({ id:'prefviewsplitter', p1:'#sectionslist', p2:'#preferences-box',
orientation:'v', relative:true, start:266, min:180, size:12 }).init();
}
else if (rcmail.env.action == 'edit-prefs') {
var legend = $('#preferences-details fieldset.advanced legend'),
toggle = $('<a href="#toggle"></a>')
.text(rcmail.gettext('toggleadvancedoptions'))
.attr('title', rcmail.gettext('toggleadvancedoptions'))
.addClass('advanced-toggle');
legend.click(function(e) {
toggle.html($(this).hasClass('collapsed') ? '&#9650;' : '&#9660;');
$(this).toggleClass('collapsed')
.closest('fieldset').children('.propform').toggle()
}).append(toggle).addClass('collapsed')
// this magically fixes incorrect position of toggle link created above in Firefox 3.6
if (bw.mz)
legend.parents('form').css('display', 'inline');
}
}
/*** addressbook task ***/
else if (rcmail.env.task == 'addressbook') {
rcmail.addEventListener('beforepushgroup', push_contactgroup)
.addEventListener('beforepopgroup', pop_contactgroup)
.addEventListener('menu-open', menu_toggle)
.addEventListener('menu-close', menu_toggle);
if (rcmail.env.action == '') {
new rcube_splitter({ id:'addressviewsplitterd', p1:'#addressview-left', p2:'#addressview-right',
orientation:'v', relative:true, start:206, min:150, size:12, render:resize_leftcol }).init();
new rcube_splitter({ id:'addressviewsplitter', p1:'#addresslist', p2:'#contacts-box',
orientation:'v', relative:true, start:266, min:260, size:12 }).init();
}
var dragmenu = $('#dragcontactmenu');
if (dragmenu.length) {
rcmail.gui_object('dragmenu', 'dragcontactmenu');
popups.dragmenu = dragmenu;
}
}
// turn a group of fieldsets into tabs
$('.tabbed').each(function(idx, elem){ init_tabs(elem); })
// decorate select elements
$('select.decorated').each(function(){
if (bw.opera) {
$(this).removeClass('decorated');
return;
}
var select = $(this),
parent = select.parent(),
height = Math.max(select.height(), 26) - 2,
width = select.width() - 22,
title = $('option', this).first().text();
if ($('option:selected', this).val() != '')
title = $('option:selected', this).text();
var overlay = $('<a class="menuselector" tabindex="-1"><span class="handle">' + title + '</span></a>')
.css('position', 'absolute')
.offset(select.position())
.insertAfter(select);
overlay.children().width(width).height(height).css('line-height', (height - 1) + 'px');
if (parent.css('position') != 'absolute')
parent.css('position', 'relative');
// re-set original select width to fix click action and options width in some browsers
select.width(overlay.width())
.on(bw.mz ? 'change keyup' : 'change', function() {
var val = $('option:selected', this).text();
$(this).next().children().text(val);
});
select
.on('focus', function(e){ overlay.addClass('focus'); })
.on('blur', function(e){ overlay.removeClass('focus'); });
});
// set min-width to show all toolbar buttons
var screen = $('body.minwidth');
if (screen.length) {
screen.css('min-width', $('.toolbar').width() + $('#quicksearchbar').width() + $('#searchfilter').width() + 30);
}
// don't use $(window).resize() due to some unwanted side-effects
window.onresize = resize;
resize();
}
/**
* Update UI on window resize
*/
function resize(e)
{
// resize in intervals to prevent lags and double onresize calls in Chrome (#1489005)
var interval = e ? 10 : 0;
if (rcmail.resize_timeout)
window.clearTimeout(rcmail.resize_timeout);
rcmail.resize_timeout = window.setTimeout(function() {
if (rcmail.env.task == 'mail') {
if (rcmail.env.action == 'show' || rcmail.env.action == 'preview')
layout_messageview();
else if (rcmail.env.action == 'compose')
layout_composeview();
}
// make iframe footer buttons float if scrolling is active
$('body.iframe .footerleft').each(function(){
var footer = $(this),
body = $(document.body),
floating = footer.hasClass('floating'),
overflow = body.outerHeight(true) > $(window).height();
if (overflow != floating) {
var action = overflow ? 'addClass' : 'removeClass';
footer[action]('floating');
body[action]('floatingbuttons');
}
});
}, interval);
}
/**
* Triggered when a new user message is displayed
*/
function message_displayed(p)
{
var siblings = $(p.object).siblings('div');
if (siblings.length)
$(p.object).insertBefore(siblings.first());
// show a popup dialog on errors
if (p.type == 'error' && rcmail.env.task != 'login') {
// hide original message object, we don't want both
rcmail.hide_message(p.object);
if (me.message_timer) {
window.clearTimeout(me.message_timer);
}
if (!me.messagedialog) {
me.messagedialog = $('<div>').addClass('popupdialog').hide();
}
var msg = p.message,
dialog_close = function() {
// check if dialog is still displayed, to prevent from js error
me.messagedialog.is(':visible') && me.messagedialog.dialog('destroy').hide();
};
if (me.messagedialog.is(':visible') && me.messagedialog.text() != msg)
msg = me.messagedialog.html() + '<p>' + p.message + '</p>';
me.messagedialog.html(msg)
.dialog({
resizable: false,
closeOnEscape: true,
dialogClass: p.type,
title: rcmail.gettext('errortitle'),
close: dialog_close,
hide: {effect: 'fadeOut'},
width: 420,
minHeight: 90
}).show();
me.messagedialog.closest('div[role=dialog]').attr('role', 'alertdialog');
if (p.timeout > 0)
me.message_timer = window.setTimeout(dialog_close, p.timeout);
}
}
// modify dialog position to fully fit the close button into the window
function dialog_open(dialog)
{
var me = $(dialog.uiDialog),
offset = me.offset(),
position = me.position(),
width = me.outerWidth(),
maxWidth = $(window).width(),
topOffset = offset.top - 12;
if (topOffset < 0)
me.css('top', position.top - topOffset);
if (offset.left + width + 12 > maxWidth)
me.css('left', position.left - 12);
}
// Mail view layout initialization and change handler
function mail_layout(p)
{
var layout = p ? p.new_layout : rcmail.env.layout,
top = $('#mailview-top'),
bottom = $('#mailview-bottom');
if (p)
$('#mainscreencontent').removeClass().addClass(layout);
$('#mailviewsplitter')[layout == 'desktop' ? 'show' : 'hide']();
$('#mailviewsplitter2')[layout == 'widescreen' ? 'show' : 'hide']();
$('#mailpreviewframe')[layout != 'list' ? 'show' : 'hide']();
rcmail.env.contentframe = layout == 'list' ? null : 'messagecontframe';
if (layout == 'widescreen') {
$('#countcontrols').detach().appendTo($('#messagelistheader'));
top.css({height: 'auto', width: 394});
bottom.css({top: 0, left: 406, height: 'auto'}).show();
if (!mailviewsplit2) {
mailviewsplit2 = new rcube_splitter({ id:'mailviewsplitter2', p1:'#mailview-top', p2:'#mailview-bottom',
orientation:'v', relative:true, start:416, min:400, size:12});
mailviewsplit2.init();
}
else
mailviewsplit2.resize();
}
else if (layout == 'desktop') {
top.css({height: 270, width: 'auto'});
bottom.css({left: 0, top: 284, height: 'auto'}).show();
if (!mailviewsplit) {
mailviewsplit = new rcube_splitter({ id:'mailviewsplitter', p1:'#mailview-top', p2:'#mailview-bottom',
orientation:'h', relative:true, start:276, min:150, size:12, offset:4 });
mailviewsplit.init();
}
else
mailviewsplit.resize();
}
else { // layout == 'list'
top.css({height: 'auto', width: 'auto'});
bottom.hide();
}
if (p && p.old_layout == 'widescreen') {
$('#countcontrols').detach().appendTo($('#messagelistfooter'));
}
}
/**
* Adjust UI objects of the mail view screen
*/
function layout_messageview()
{
$('#messagecontent').css('top', ($('#messageheader').outerHeight() + 1) + 'px');
$('#message-objects div a').addClass('button');
if (!$('#attachment-list li').length) {
$('div.rightcol').hide().attr('aria-hidden', 'true');
$('div.leftcol').css('margin-right', '0');
}
var mvlpe = $('#messagebody.mailvelope, #messagebody > .mailvelope');
if (mvlpe.length) {
var h = $('#messagecontent').length ?
$('#messagecontent').height() - 16 :
$(window).height() - mvlpe.offset().top - 2;
mvlpe.height(h);
}
}
function render_mailboxlist(splitter)
{
// TODO: implement smart shortening of long folder names
}
function resize_leftcol(splitter)
{
// STUB
}
function init_compose_editfields()
{
// Show input elements with non-empty value
var f, v, field, fields = ['cc', 'bcc', 'replyto', 'followupto'];
for (f=0; f < fields.length; f++) {
v = fields[f]; field = $('#_'+v);
if (field.length) {
field.on('change', {v: v}, function(e) { if (this.value) show_header_row(e.data.v, true); });
if (field.val() != '')
show_header_row(v, true);
}
}
// adjust hight when textarea starts to scroll
$("textarea[name='_to'], textarea[name='_cc'], textarea[name='_bcc']").change(function(e){ adjust_compose_editfields(this); }).change();
rcmail.addEventListener('autocomplete_insert', function(p){ adjust_compose_editfields(p.field); });
}
function adjust_compose_editfields(elem)
{
if (elem.nodeName == 'TEXTAREA') {
var $elem = $(elem), line_height = 14, // hard-coded because some browsers only provide the outer height in elem.clientHeight
content_height = elem.scrollHeight,
rows = elem.value.length > 80 && content_height > line_height*1.5 ? 2 : 1;
$elem.css('height', (line_height*rows) + 'px');
layout_composeview();
}
}
function layout_composeview()
{
var body = $('#composebody'),
form = $('#compose-content'),
bottom = $('#composeview-bottom'),
w, h, bh, ovflw, btns = 0,
minheight = 300;
if (!form.length)
return;
bh = form.height() - bottom.position().top;
ovflw = minheight - bh;
btns = ovflw > -100 ? 0 : 40;
bottom.height(Math.max(minheight, bh));
form.css('overflow', ovflw > 0 ? 'auto' : 'hidden');
w = body.parent().width() - 5;
h = body.parent().height() - 8;
body.width(w).height(h);
$('#composebodycontainer > div').width(w+8);
$('#composebody_ifr').height(h + 4 - $('div.mce-toolbar').height());
$('#googie_edit_layer').width(w).height(h);
// $('#composebodycontainer')[(btns ? 'addClass' : 'removeClass')]('buttons');
// $('#composeformbuttons')[(btns ? 'show' : 'hide')]();
var abooks = $('#directorylist');
if (abooks.length)
$('#compose-contacts .scroller').css('top', abooks.position().top + abooks.outerHeight());
}
function update_quota(p)
{
var element = $('#quotadisplay'), menu = $('#quotamenu'),
step = 24, step_count = 20,
y = p.total ? Math.ceil(p.percent / 100 * step_count) * step : 0;
// never show full-circle if quota is close to 100% but below.
if (p.total && y == step * step_count && p.percent < 100)
y -= step;
element.css('background-position', '0 -' + y + 'px');
element.attr('class', 'countdisplay p' + (Math.round(p.percent / 10) * 10));
if (p.table) {
if (!menu.length)
menu = $('<div id="quotamenu" class="popupmenu">').appendTo($('body'));
menu.html(p.table);
element.css('cursor', 'pointer').off('click').on('click', function(e) {
return rcmail.command('menu-open', 'quotamenu', e.target, e);
});
}
}
function folder_search_init(container)
{
// animation to unfold list search box
$('.boxtitle a.search', container).click(function(e) {
var title = $('.boxtitle', container),
box = $('.listsearchbox', container),
dir = box.is(':visible') ? -1 : 1,
height = 34 + ($('select', box).length ? 22 : 0);
box.slideToggle({
duration: 160,
progress: function(animation, progress) {
if (dir < 0) progress = 1 - progress;
$('.scroller', container).css('top', (title.outerHeight() + height * progress) + 'px');
},
complete: function() {
box.toggleClass('expanded');
if (box.is(':visible')) {
box.find('input[type=text]').focus();
height = 34 + ($('select', box).length ? $('select', box).outerHeight() + 4 : 0);
$('.scroller', container).css('top', (title.outerHeight() + height) + 'px');
}
else {
$('a.reset', box).click();
}
// TODO: save state in localStorage
}
});
return false;
});
}
function enable_command(p)
{
if (p.command == 'reply-list' && rcmail.env.reply_all_mode == 1) {
var label = rcmail.gettext(p.status ? 'replylist' : 'replyall');
if (rcmail.env.action == 'preview')
$('a.button.replyall').attr('title', label);
else
$('a.button.reply-all').text(label).attr('title', label);
}
else if (p.command == 'compose-encrypted') {
// show the toolbar button for Mailvelope
$('a.button.encrypt').parent().show();
}
else if (p.command == 'compose-encrypted-signed') {
// enable selector for encrypt and sign
$('#encryptionmenulink').show();
}
}
/**
* Register a popup menu
*/
function add_popup(popup, config)
{
var obj = popups[popup] = $('#'+popup);
obj.appendTo(document.body); // move it to top for proper absolute positioning
if (obj.length)
popupconfig[popup] = $.extend(popupconfig[popup] || {}, config || {});
}
/**
* Trigger for popup menus
*/
function toggle_popup(popup, e, config)
{
// auto-register menu object
if (config || !popupconfig[popup])
add_popup(popup, config);
return rcmail.command('menu-open', popup, e.target, e);
}
/**
* (Deprecated) trigger for popup menus
*/
function show_popup(popup, show, config)
{
// auto-register menu object
if (config || !popupconfig[popup])
add_popup(popup, config);
config = popupconfig[popup] || {};
var ref = $(config.link ? config.link : '#'+popup+'link'),
pos = ref.offset();
if (ref.has('.inner'))
ref = ref.children('.inner');
// fire command with simulated mouse click event
return rcmail.command('menu-open',
{ menu:popup, show:show },
ref.get(0),
$.Event('click', { target:ref.get(0), pageX:pos.left, pageY:pos.top, clientX:pos.left, clientY:pos.top }));
}
/**
* Switch between short and full headers display in message preview
*/
function toggle_preview_headers()
{
$('#preview-shortheaders').toggle();
var full = $('#preview-allheaders').toggle(),
button = $('a#previewheaderstoggle');
// add toggle button to full headers table
if (full.is(':visible'))
button.attr('href', '#hide').removeClass('add').addClass('remove').attr('aria-expanded', 'true');
else
button.attr('href', '#details').removeClass('remove').addClass('add').attr('aria-expanded', 'false');
save_pref('previewheaders', full.is(':visible') ? '1' : '0');
}
/**
*
*/
function switch_view_mode(mode, force)
{
if (force || !$('#mail'+mode+'mode').hasClass('disabled')) {
$('#maillistmode, #mailthreadmode').removeClass('selected').attr('tabindex', '0').attr('aria-disabled', 'false');
$('#mail'+mode+'mode').addClass('selected').attr('tabindex', '-1').attr('aria-disabled', 'true');
}
}
/**** popup menu callbacks ****/
/**
* Handler for menu-open and menu-close events
*/
function menu_toggle(p)
{
if (p && p.name == 'messagelistmenu') {
show_listoptions(p);
}
else if (p) {
// adjust menu position according to config
var config = popupconfig[p.name] || {},
ref = $(config.link || '#'+p.name+'link'),
visible = p.obj && p.obj.is(':visible'),
above = config.above;
// fix position according to config
if (p.obj && visible && ref.length) {
var parent = ref.parent(),
win = $(window), pos;
if (parent.hasClass('dropbutton'))
ref = parent;
if (config.above || ref.hasClass('dropbutton')) {
pos = ref.offset();
p.obj.css({ left:pos.left+'px', top:(pos.top + (config.above ? -p.obj.height() : ref.outerHeight()))+'px' });
}
}
// add the right classes
if (p.obj && config.iconized) {
p.obj.children('ul').addClass('iconized');
}
// apply some data-attributes from menu config
if (p.obj && config.editable)
p.obj.attr('data-editable', 'true');
// trigger callback function
if (typeof config.callback == 'function') {
config.callback(visible, p);
}
}
}
function searchmenu(show)
{
if (show && rcmail.env.search_mods) {
var n, all,
obj = popups['searchmenu'],
list = $('input:checkbox[name="s_mods[]"]', obj),
mbox = rcmail.env.mailbox,
mods = rcmail.env.search_mods,
scope = rcmail.env.search_scope || 'base';
if (rcmail.env.task == 'mail') {
if (scope == 'all')
mbox = '*';
mods = mods[mbox] ? mods[mbox] : mods['*'];
all = 'text';
$('input:radio[name="s_scope"]').prop('checked', false).filter('#s_scope_'+scope).prop('checked', true);
}
else {
all = '*';
}
if (mods[all])
list.map(function() {
this.checked = true;
this.disabled = this.value != all;
});
else {
list.prop('disabled', false).prop('checked', false);
for (n in mods)
$('#s_mod_' + n).prop('checked', true);
}
}
}
function attachmentmenu(elem, event)
{
var id = elem.parentNode.id.replace(/^attach/, '');
$.each(['open', 'download', 'rename'], function() {
var action = this;
$('#attachmenu' + action).off('click').attr('onclick', '').click(function(e) {
return rcmail.command(action + '-attachment', id, this);
});
});
popupconfig.attachmentmenu.link = elem;
rcmail.command('menu-open', {menu: 'attachmentmenu', id: id}, elem, event);
}
function spellmenu(show, p)
{
var k, link, li,
lang = rcmail.spellcheck_lang(),
ul = $('ul', p.obj);
if (!ul.length) {
ul = $('<ul class="toolbarmenu selectable" role="menu">');
for (k in rcmail.env.spell_langs) {
li = $('<li role="menuitem">');
link = $('<a href="#'+k+'" tabindex="0"></a>').text(rcmail.env.spell_langs[k])
.addClass('active').data('lang', k)
.on('click keypress', function(e) {
if (e.type != 'keypress' || rcube_event.get_keycode(e) == 13) {
rcmail.spellcheck_lang_set($(this).data('lang'));
rcmail.hide_menu('spellmenu', e);
return false;
}
});
link.appendTo(li);
li.appendTo(ul);
}
ul.appendTo(p.obj);
}
// select current language
$('li', ul).each(function() {
var el = $('a', this);
if (el.data('lang') == lang)
el.addClass('selected').attr('aria-selected', 'true');
else if (el.hasClass('selected'))
el.removeClass('selected').removeAttr('aria-selected');
});
}
// append drop-icon to attachments list item (to invoke attachment menu)
function attachmentmenu_append(item)
{
item = $(item);
if (!item.children('.drop').length)
var label = rcmail.gettext('options');
item.append($('<a>')
.attr({'class': 'drop skip-content', tabindex: 0, 'aria-haspopup': true, title: label})
.text(label)
.on('click keypress', function(e) {
if (e.type != 'keypress' || rcube_event.get_keycode(e) == 13) {
attachmentmenu(this, e);
return false;
}
}));
}
/**
*
*/
function show_listoptions(p)
{
var $dialog = $('#listoptions');
// close the dialog
if ($dialog.is(':visible')) {
$dialog.dialog('close', p.originalEvent);
return;
}
// set form values
$('input[name="sort_col"][value="'+rcmail.env.sort_col+'"]').prop('checked', true);
$('input[name="sort_ord"][value="DESC"]').prop('checked', rcmail.env.sort_order == 'DESC');
$('input[name="sort_ord"][value="ASC"]').prop('checked', rcmail.env.sort_order != 'DESC');
$.each(['widescreen', 'desktop', 'list'], function() {
$('input[name="layout"][value="' + this + '"]').prop('checked', rcmail.env.layout == this);
});
$('#listoptions-columns', $dialog)[rcmail.env.layout == 'widescreen' ? 'hide' : 'show']();
// set checkboxes
$('input[name="list_col[]"]').each(function() {
$(this).prop('checked', $.inArray(this.value, rcmail.env.listcols) != -1);
});
$dialog.dialog({
modal: true,
resizable: false,
closeOnEscape: true,
title: null,
open: function(e) {
setTimeout(function(){ $dialog.find('a, input:not(:disabled)').not('[aria-disabled=true]').first().focus(); }, 100);
},
close: function(e) {
$dialog.dialog('destroy').hide();
if (e.originalEvent && rcube_event.is_keyboard(e.originalEvent))
$('#listmenulink').focus();
},
minWidth: 500,
width: $dialog.width()+25
}).show();
}
/**
*
*/
function save_listoptions(p)
{
$('#listoptions').dialog('close');
if (rcube_event.is_keyboard(p.originalEvent))
$('#listmenulink').focus();
var sort = $('input[name="sort_col"]:checked').val(),
ord = $('input[name="sort_ord"]:checked').val(),
layout = $('input[name="layout"]:checked').val(),
cols = $('input[name="list_col[]"]:checked')
.map(function(){ return this.value; }).get();
rcmail.set_list_options(cols, sort, ord, rcmail.env.threading, layout);
}
/**
*
*/
function set_searchmod(elem)
{
var all, m, task = rcmail.env.task,
mods = rcmail.env.search_mods,
mbox = rcmail.env.mailbox,
scope = $('input[name="s_scope"]:checked').val();
if (scope == 'all')
mbox = '*';
if (!mods)
mods = {};
if (task == 'mail') {
if (!mods[mbox])
mods[mbox] = rcube_clone_object(mods['*']);
m = mods[mbox];
all = 'text';
}
else { //addressbook
m = mods;
all = '*';
}
if (!elem.checked)
delete(m[elem.value]);
else
m[elem.value] = 1;
// mark all fields
if (elem.value == all) {
$('input:checkbox[name="s_mods[]"]').map(function() {
if (this == elem)
return;
this.checked = true;
if (elem.checked) {
this.disabled = true;
delete m[this.value];
}
else {
this.disabled = false;
m[this.value] = 1;
}
});
}
rcmail.set_searchmods(m);
}
function set_searchscope(elem)
{
rcmail.set_searchscope(elem.value);
}
function push_contactgroup(p)
{
// lets the contacts list swipe to the left, nice!
var table = $('#contacts-table'),
scroller = table.parent().css('overflow', 'hidden');
table.clone()
.css({ position:'absolute', top:'0', left:'0', width:table.width()+'px', 'z-index':10 })
.appendTo(scroller)
.animate({ left: -(table.width()+5) + 'px' }, 300, 'swing', function(){
$(this).remove();
scroller.css('overflow', 'auto')
});
}
function pop_contactgroup(p)
{
// lets the contacts list swipe to the left, nice!
var table = $('#contacts-table'),
scroller = table.parent().css('overflow', 'hidden'),
clone = table.clone().appendTo(scroller);
table.css({ position:'absolute', top:'0', left:-(table.width()+5) + 'px', width:table.width()+'px', height:table.height()+'px', 'z-index':10 })
.animate({ left:'0' }, 300, 'linear', function(){
clone.remove();
$(this).css({ position:'relative', left:'0', width:'100%', height:'auto', 'z-index':1 });
scroller.css('overflow', 'auto')
});
}
/**
* Mail import dialog
*/
function import_dialog()
{
var content = $('#uploadform'),
dialog = content.clone().removeClass('popupdialog');
var save_func = function(e) {
return rcmail.command('import-messages', $(dialog.find('form')[0]));
};
rcmail.simple_dialog(dialog, rcmail.gettext('importmessages'), save_func, {
button: 'import',
closeOnEscape: true,
minWidth: 400
});
}
/**
*
*/
function show_header_row(which, updated)
{
var row = $('#compose-' + which);
if (row.is(':visible'))
return; // nothing to be done here
if (compose_headers[which] && !updated)
$('#_' + which).val(compose_headers[which]);
row.show();
$('#' + which + '-link').hide();
layout_composeview();
$('input,textarea', row).focus();
return false;
}
/**
*
*/
function hide_header_row(which)
{
// copy and clear field value
var field = $('#_' + which);
compose_headers[which] = field.val();
field.val('');
$('#compose-' + which).hide();
$('#' + which + '-link').show();
layout_composeview();
return false;
}
/**
* Fieldsets-to-tabs converter
*/
function init_tabs(elem, current)
{
var content = $(elem),
id = content.get(0).id,
fs = content.children('fieldset');
if (!fs.length)
return;
if (!id) {
id = 'rcmtabcontainer';
content.attr('id', id);
}
// create tabs container
var tabs = $('<ul>').addClass('tabsbar').prependTo(content);
// convert fildsets into tabs
fs.each(function(idx) {
var tab, a, elm = $(this),
legend = elm.children('legend'),
tid = id + '-t' + idx;
// create a tab
a = $('<a>').text(legend.text()).attr('href', '#' + tid);
tab = $('<li>').addClass('tablink');
// remove legend
legend.remove();
// link fieldset with tab item
elm.attr('id', tid);
// add the tab to container
tab.append(a).appendTo(tabs);
});
// use jquery UI tabs widget to do the interaction and styling
content.tabs({
active: current || 0,
heightStyle: 'content',
activate: function(e, ui) {resize(); }
});
}
/**
* Show about page as jquery UI dialog
*/
function show_about(elem)
{
var frame = $('<iframe>').attr({id: 'aboutframe', src: rcmail.url('settings/about'), frameborder: '0'});
h = Math.floor($(window).height() * 0.75),
buttons = {},
supportln = $('#supportlink');
if (supportln.length && (env.supporturl = supportln.attr('href')))
buttons[supportln.html()] = function(e){ env.supporturl.indexOf('mailto:') < 0 ? window.open(env.supporturl) : location.href = env.supporturl };
frame.dialog({
modal: true,
resizable: false,
closeOnEscape: true,
title: elem ? elem.title || elem.innerHTML : null,
close: function() {
frame.dialog('destroy').remove();
},
buttons: buttons,
width: 640,
height: h
}).width(640);
}
}
/**
* Roundcube Scroller class
*
* @deprecated Use treelist widget
*/
function rcube_scroller(list, top, bottom)
{
var ref = this;
this.list = $(list);
this.top = $(top);
this.bottom = $(bottom);
this.step_size = 6;
this.step_time = 20;
this.delay = 500;
this.top
.mouseenter(function() { if (rcmail.drag_active) ref.ts = window.setTimeout(function() { ref.scroll('down'); }, ref.delay); })
.mouseout(function() { if (ref.ts) window.clearTimeout(ref.ts); });
this.bottom
.mouseenter(function() { if (rcmail.drag_active) ref.ts = window.setTimeout(function() { ref.scroll('up'); }, ref.delay); })
.mouseout(function() { if (ref.ts) window.clearTimeout(ref.ts); });
this.scroll = function(dir)
{
var ref = this, size = this.step_size;
if (!rcmail.drag_active)
return;
if (dir == 'down')
size *= -1;
this.list.get(0).scrollTop += size;
this.ts = window.setTimeout(function() { ref.scroll(dir); }, this.step_time);
};
};
/**
* Roundcube UI splitter class
*
* @constructor
*/
function rcube_splitter(p)
{
this.p = p;
this.id = p.id;
this.horizontal = (p.orientation == 'horizontal' || p.orientation == 'h');
this.halfsize = (p.size !== undefined ? p.size : 10) / 2;
this.pos = p.start || 0;
this.min = p.min || 20;
this.offset = p.offset || 0;
this.relative = p.relative ? true : false;
this.drag_active = false;
this.render = p.render;
this.callback = p.callback;
var me = this;
rcube_splitter._instances[this.id] = me;
this.init = function()
{
this.p1 = $(this.p.p1);
this.p2 = $(this.p.p2);
this.parent = this.p1.parent();
// check if referenced elements exist, otherwise abort
if (!this.p1.length || !this.p2.length)
return;
// create and position the handle for this splitter
this.p1pos = this.relative ? this.p1.position() : this.p1.offset();
this.p2pos = this.relative ? this.p2.position() : this.p2.offset();
this.handle = $('<div>')
.attr('id', this.id)
.attr('unselectable', 'on')
.attr('role', 'presentation')
.addClass('splitter ' + (this.horizontal ? 'splitter-h' : 'splitter-v'))
.appendTo(this.parent)
.mousedown(onDragStart);
if (this.horizontal) {
var top = this.p1pos.top + this.p1.outerHeight();
this.handle.css({ left:'0px', top:top+'px' });
}
else {
var left = this.p1pos.left + this.p1.outerWidth();
this.handle.css({ left:left+'px', top:'0px' });
}
// listen to window resize on IE
if (bw.ie)
$(window).resize(onResize);
// read saved position from cookie
var cookie = this.get_cookie();
if (cookie && !isNaN(cookie)) {
this.pos = parseFloat(cookie);
this.resize();
}
else if (this.pos) {
this.resize();
this.set_cookie();
}
};
/**
* Set size and position of all DOM objects
* according to the saved splitter position
*/
this.resize = function()
{
if (this.horizontal) {
this.p1.css('height', Math.floor(this.pos - this.p1pos.top - Math.floor(this.halfsize)) + 'px');
this.p2.css('top', Math.ceil(this.pos + Math.ceil(this.halfsize) + 2) + 'px');
this.handle.css('top', Math.round(this.pos - this.halfsize + this.offset)+'px');
if (bw.ie) {
var new_height = parseInt(this.parent.outerHeight(), 10) - parseInt(this.p2.css('top'), 10);
this.p2.css('height', (new_height > 0 ? new_height : 0) + 'px');
}
}
else {
this.p1.css('width', Math.floor(this.pos - this.p1pos.left - Math.floor(this.halfsize)) + 'px');
this.p2.css('left', Math.ceil(this.pos + Math.ceil(this.halfsize)) + 'px');
this.handle.css('left', Math.round(this.pos - this.halfsize + this.offset + 3)+'px');
if (bw.ie) {
var new_width = parseInt(this.parent.outerWidth(), 10) - parseInt(this.p2.css('left'), 10) ;
this.p2.css('width', (new_width > 0 ? new_width : 0) + 'px');
}
}
this.p2.resize();
this.p1.resize();
// also resize iframe covers
if (this.drag_active) {
$('iframe').each(function(i, elem) {
var pos = $(this).offset();
$('#iframe-splitter-fix-'+i).css({ top: pos.top+'px', left: pos.left+'px', width:elem.offsetWidth+'px', height: elem.offsetHeight+'px' });
});
}
if (typeof this.render == 'function')
this.render(this);
};
/**
* Handler for mousedown events
*/
function onDragStart(e)
{
// disable text selection while dragging the splitter
if (bw.konq || bw.chrome || bw.safari)
document.body.style.webkitUserSelect = 'none';
me.p1pos = me.relative ? me.p1.position() : me.p1.offset();
me.p2pos = me.relative ? me.p2.position() : me.p2.offset();
me.drag_active = true;
// start listening to mousemove events
$(document).on('mousemove.' + this.id, onDrag).on('mouseup.' + this.id, onDragStop);
// hack messages list so it will propagate the mouseup event over the list
if (rcmail.message_list)
rcmail.message_list.drag_active = true;
// enable dragging above iframes
$('iframe').each(function(i, elem) {
$('<div>')
.attr('id', 'iframe-splitter-fix-'+i)
.addClass('iframe-splitter-fix')
.css({ background: '#fff',
width: elem.offsetWidth+'px', height: elem.offsetHeight+'px',
position: 'absolute', opacity: '0.001', zIndex: 1000
})
.css($(this).offset())
.appendTo('body');
});
};
/**
* Handler for mousemove events
*/
function onDrag(e)
{
if (!me.drag_active)
return false;
// with timing events dragging action is more responsive
window.clearTimeout(me.ts);
me.ts = window.setTimeout(function() { onDragAction(e); }, 1);
return false;
};
/**
* Dragging action (see onDrag())
*/
function onDragAction(e)
{
var pos = rcube_event.get_mouse_pos(e);
if (me.relative) {
var parent = me.parent.offset();
pos.x -= parent.left;
pos.y -= parent.top;
}
if (me.horizontal) {
if (((pos.y - me.halfsize) > me.p1pos.top) && ((pos.y + me.halfsize) < (me.p2pos.top + me.p2.outerHeight()))) {
me.pos = Math.max(me.min, pos.y - Math.max(0, me.offset));
if (me.pos > me.min)
me.pos = Math.min(me.pos, me.parent.height() - me.min);
me.resize();
}
}
else {
if (((pos.x - me.halfsize) > me.p1pos.left) && ((pos.x + me.halfsize) < (me.p2pos.left + me.p2.outerWidth()))) {
me.pos = Math.max(me.min, pos.x - Math.max(0, me.offset));
if (me.pos > me.min)
me.pos = Math.min(me.pos, me.parent.width() - me.min);
me.resize();
}
}
me.p1pos = me.relative ? me.p1.position() : me.p1.offset();
me.p2pos = me.relative ? me.p2.position() : me.p2.offset();
};
/**
* Handler for mouseup events
*/
function onDragStop(e)
{
// resume the ability to highlight text
if (bw.konq || bw.chrome || bw.safari)
document.body.style.webkitUserSelect = 'auto';
// cancel the listening for drag events
$(document).off('.' + me.id);
me.drag_active = false;
if (rcmail.message_list)
rcmail.message_list.drag_active = false;
// remove temp divs
$('div.iframe-splitter-fix').remove();
me.set_cookie();
if (typeof me.callback == 'function')
me.callback(me);
return bw.safari ? true : rcube_event.cancel(e);
};
/**
* Handler for window resize events
*/
function onResize(e)
{
if (me.horizontal) {
var new_height = parseInt(me.parent.outerHeight(), 10) - parseInt(me.p2[0].style.top, 10);
me.p2.css('height', (new_height > 0 ? new_height : 0) +'px');
}
else {
var new_width = parseInt(me.parent.outerWidth(), 10) - parseInt(me.p2[0].style.left, 10);
me.p2.css('width', (new_width > 0 ? new_width : 0) + 'px');
}
};
/**
* Get saved splitter position from cookie
*/
this.get_cookie = function()
{
return window.UI ? UI.get_pref(this.id) : null;
};
/**
* Saves splitter position in cookie
*/
this.set_cookie = function()
{
if (window.UI)
UI.save_pref(this.id, this.pos);
};
} // end class rcube_splitter
// static getter for splitter instances
rcube_splitter._instances = {};
rcube_splitter.get_instance = function(id)
{
return rcube_splitter._instances[id];
};
// @license-end
diff --git a/tests/Selenium/bootstrap.php b/tests/Selenium/bootstrap.php
index 9baf0c09c..47e53757f 100644
--- a/tests/Selenium/bootstrap.php
+++ b/tests/Selenium/bootstrap.php
@@ -1,349 +1,348 @@
<?php
/*
+-----------------------------------------------------------------------+
- | tests/Selenium/bootstrap.php |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2009-2014, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Environment initialization script for unit tests |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
if (php_sapi_name() != 'cli')
die("Not in shell mode (php-cli)");
if (!defined('INSTALL_PATH')) define('INSTALL_PATH', realpath(__DIR__ . '/../../') . '/' );
define('TESTS_DIR', realpath(__DIR__ . '/../') . '/');
if (@is_dir(TESTS_DIR . 'config')) {
define('RCUBE_CONFIG_DIR', TESTS_DIR . 'config');
}
require_once(INSTALL_PATH . 'program/include/iniset.php');
// Extend include path so some plugin test won't fail
$include_path = ini_get('include_path') . PATH_SEPARATOR . TESTS_DIR . '..';
if (set_include_path($include_path) === false) {
die("Fatal error: ini_set/set_include_path does not work.");
}
$rcmail = rcmail::get_instance(0, 'test');
define('TESTS_URL', $rcmail->config->get('tests_url'));
define('TESTS_BROWSER', $rcmail->config->get('tests_browser', 'firefox'));
define('TESTS_USER', $rcmail->config->get('tests_username'));
define('TESTS_PASS', $rcmail->config->get('tests_password'));
define('TESTS_SLEEP', $rcmail->config->get('tests_sleep', 5));
PHPUnit_Extensions_Selenium2TestCase::shareSession(true);
/**
* satisfy PHPUnit
*/
class bootstrap
{
static $imap_ready = null;
/**
* Wipe and re-initialize (mysql) database
*/
public static function init_db()
{
$rcmail = rcmail::get_instance();
$dsn = rcube_db::parse_dsn($rcmail->config->get('db_dsnw'));
if ($dsn['phptype'] == 'mysql' || $dsn['phptype'] == 'mysqli') {
// drop all existing tables first
$db = $rcmail->get_dbh();
$db->query("SET FOREIGN_KEY_CHECKS=0");
$sql_res = $db->query("SHOW TABLES");
while ($sql_arr = $db->fetch_array($sql_res)) {
$table = reset($sql_arr);
$db->query("DROP TABLE $table");
}
// init database with schema
system(sprintf('cat %s %s | mysql -h %s -u %s --password=%s %s',
realpath(INSTALL_PATH . '/SQL/mysql.initial.sql'),
realpath(TESTS_DIR . 'Selenium/data/mysql.sql'),
escapeshellarg($dsn['hostspec']),
escapeshellarg($dsn['username']),
escapeshellarg($dsn['password']),
escapeshellarg($dsn['database'])
));
}
else if ($dsn['phptype'] == 'sqlite') {
// delete database file -- will be re-initialized on first access
system(sprintf('rm -f %s', escapeshellarg($dsn['database'])));
}
}
/**
* Wipe the configured IMAP account and fill with test data
*/
public static function init_imap()
{
if (!TESTS_USER) {
return false;
}
else if (self::$imap_ready !== null) {
return self::$imap_ready;
}
self::connect_imap(TESTS_USER, TESTS_PASS);
self::purge_mailbox('INBOX');
self::ensure_mailbox('Archive', true);
return self::$imap_ready;
}
/**
* Authenticate to IMAP with the given credentials
*/
public static function connect_imap($username, $password, $host = null)
{
$rcmail = rcmail::get_instance();
$imap = $rcmail->get_storage();
if ($imap->is_connected()) {
$imap->close();
self::$imap_ready = false;
}
$imap_host = $host ?: $rcmail->config->get('default_host');
$a_host = parse_url($imap_host);
if ($a_host['host']) {
$imap_host = $a_host['host'];
$imap_ssl = isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'));
$imap_port = isset($a_host['port']) ? $a_host['port'] : ($imap_ssl ? 993 : 143);
}
else {
$imap_port = 143;
$imap_ssl = false;
}
if (!$imap->connect($imap_host, $username, $password, $imap_port, $imap_ssl)) {
die("IMAP error: unable to authenticate with user " . TESTS_USER);
}
self::$imap_ready = true;
}
/**
* Import the given file into IMAP
*/
public static function import_message($filename, $mailbox = 'INBOX')
{
if (!self::init_imap()) {
die(__METHOD__ . ': IMAP connection unavailable');
}
$imap = rcmail::get_instance()->get_storage();
$imap->save_message($mailbox, file_get_contents($filename));
}
/**
* Delete all messages from the given mailbox
*/
public static function purge_mailbox($mailbox)
{
if (!self::init_imap()) {
die(__METHOD__ . ': IMAP connection unavailable');
}
$imap = rcmail::get_instance()->get_storage();
$imap->delete_message('*', $mailbox);
}
/**
* Make sure the given mailbox exists in IMAP
*/
public static function ensure_mailbox($mailbox, $empty = false)
{
if (!self::init_imap()) {
die(__METHOD__ . ': IMAP connection unavailable');
}
$imap = rcmail::get_instance()->get_storage();
$folders = $imap->list_folders();
if (!in_array($mailbox, $folders)) {
$imap->create_folder($mailbox, true);
}
else if ($empty) {
$imap->delete_message('*', $mailbox);
}
}
}
// @TODO: make sure mailbox has some content (always the same) or is empty
// @TODO: plugins: enable all?
/**
* Base class for all tests in this directory
*/
class Selenium_Test extends PHPUnit_Extensions_Selenium2TestCase
{
protected $login_data = null;
protected function setUp()
{
$this->setBrowser(TESTS_BROWSER);
$this->login_data = array(TESTS_USER, TESTS_PASS);
// Set root to our index.html, for better performance
// See https://github.com/sebastianbergmann/phpunit-selenium/issues/217
$baseurl = preg_replace('!/index(-.+)?\.php^!', '', TESTS_URL);
$this->setBrowserUrl($baseurl . '/tests/Selenium');
}
protected function login($username = null, $password = null)
{
if (!empty($username)) {
$this->login_data = array($username, $password);
}
$this->go('mail', null, true);
}
protected function do_login()
{
$user_input = $this->byCssSelector('form input[name="_user"]');
$pass_input = $this->byCssSelector('form input[name="_pass"]');
$submit = $this->byCssSelector('form input[type="submit"]');
$user_input->value($this->login_data[0]);
$pass_input->value($this->login_data[1]);
// submit login form
$submit->click();
// wait after successful login
sleep(TESTS_SLEEP);
}
protected function go($task = 'mail', $action = null, $login = true)
{
$this->url(TESTS_URL . '?_task=' . $task);
// wait for interface load (initial ajax requests, etc.)
sleep(TESTS_SLEEP);
// check if we have a valid session
$env = $this->get_env();
if ($login && $env['task'] == 'login') {
$this->do_login();
}
if ($action) {
$this->click_button($action);
sleep(TESTS_SLEEP);
}
}
protected function get_env()
{
return $this->execute(array(
'script' => 'return window.rcmail ? rcmail.env : {};',
'args' => array(),
));
}
protected function get_buttons($action)
{
$buttons = $this->execute(array(
'script' => "return rcmail.buttons['$action'];",
'args' => array(),
));
if (is_array($buttons)) {
foreach ($buttons as $idx => $button) {
$buttons[$idx] = $button['id'];
}
}
return (array) $buttons;
}
protected function get_objects()
{
return $this->execute(array(
'script' => "var i,r = []; for (i in rcmail.gui_objects) r.push(i); return r;",
'args' => array(),
));
}
protected function click_button($action)
{
$buttons = $this->get_buttons($action);
$id = array_shift($buttons);
// this doesn't work for me
$this->byId($id)->click();
}
protected function ajaxResponse($action, $script = '', $button = false)
{
if (!$script && !$button) {
$script = "rcmail.command('$action')";
}
$script =
"if (!window.test_ajax_response) {
window.test_ajax_response_object = {};
function test_ajax_response(response)
{
if (response.response && response.response.action) {
window.test_ajax_response_object[response.response.action] = response.response;
}
}
rcmail.addEventListener('responsebefore', test_ajax_response);
}
window.test_ajax_response_object['$action'] = null;
$script;
";
// run request
$this->execute(array(
'script' => $script,
'args' => array(),
));
if ($button) {
$this->click_button($action);
}
// wait
sleep(TESTS_SLEEP);
// get response
$response = $this->execute(array(
'script' => "return window.test_ajax_response_object ? test_ajax_response_object['$action'] : {};",
'args' => array(),
));
return $response;
}
protected function getText($element)
{
return $element->text() ?: $element->attribute('textContent');
}
protected function assertHasClass($classname, $element)
{
$this->assertContains($classname, $element->attribute('class'));
}
}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 925cca66a..9e5a0e0e4 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -1,41 +1,40 @@
<?php
/*
+-----------------------------------------------------------------------+
- | tests/bootstrap.php |
- | |
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2009-2012, The Roundcube Dev Team |
+ | |
+ | Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Environment initialization script for unit tests |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
if (php_sapi_name() != 'cli')
die("Not in shell mode (php-cli)");
if (!defined('INSTALL_PATH')) define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/' );
define('TESTS_DIR', __DIR__ . '/');
if (@is_dir(TESTS_DIR . 'config')) {
define('RCUBE_CONFIG_DIR', TESTS_DIR . 'config');
}
require_once(INSTALL_PATH . 'program/include/iniset.php');
rcmail::get_instance(0, 'test')->config->set('devel_mode', false);
// Extend include path so some plugin test won't fail
$include_path = ini_get('include_path') . PATH_SEPARATOR . TESTS_DIR . '..';
if (set_include_path($include_path) === false) {
die("Fatal error: ini_set/set_include_path does not work.");
}

File Metadata

Mime Type
application/octet-stream
Expires
Thu, Oct 17, 5:15 AM (2 d)
Storage Engine
chunks
Storage Format
Chunks
Storage Handle
eDdpUb2MbUv8
Default Alt Text
(4 MB)

Event Timeline