Page MenuHomePhorge

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/program/include/rcmail.php b/program/include/rcmail.php
index cc70739cf..8e66e85e7 100644
--- a/program/include/rcmail.php
+++ b/program/include/rcmail.php
@@ -1,2115 +1,2115 @@
<?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 |
| |
| 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;
/**
* This implements the 'singleton' design pattern
*
* @param string Environment name to run (e.g. live, dev, test)
*
* @return rcmail The one and only instance
*/
static function get_instance($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;
}
// 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)
// 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
if (!empty($_REQUEST['_remote']))
$GLOBALS['OUTPUT'] = $this->json_init();
else
$GLOBALS['OUTPUT'] = $this->load_gui(!empty($_REQUEST['_framed']));
// load plugins
$this->plugins->init($this, $this->task);
$this->plugins->load_plugins((array)$this->config->get('plugins', array()),
array('filesystem_attachments', 'jqueryui'));
}
/**
* Setter for application task
*
* @param string Task to set
*/
public function set_task($task)
{
$task = asciiwords($task, true);
if ($this->user && $this->user->ID)
$task = !$task ? 'mail' : $task;
else if (php_sapi_name() == 'cli')
$task = 'cli';
else
$task = 'login';
$this->task = $task;
$this->comm_path = $this->url(array('task' => $this->task));
if ($this->output) {
$this->output->set_env('task', $this->task);
}
}
/**
* Setter for system user object
*
* @param rcube_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
if (version_compare(PHP_VERSION, '5.5.0', '<') && in_array($lang, array('tr_TR', 'ku', 'az_AZ'))) {
setlocale(LC_CTYPE, 'en_US.utf8', 'en_US.UTF-8');
}
}
/**
* Return instance of the internal address book class
*
* @param string Address book identifier (-1 for default addressbook)
* @param boolean 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 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 True if the address book needs to be writeable
* @param boolean 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 True to sort the list alphabetically
* @param boolean 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-' . 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 ? strtolower($response['name']) : $response['key'];
$responses[$k] = $response;
}
// sort list by name
if ($sorted) {
ksort($responses, SORT_LOCALE_STRING);
}
return array_values($responses);
}
/**
* 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 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());
}
// add some basic labels to client
$this->output->add_label('loading', 'servererror', 'connerror', 'requesttimedout', 'refreshing');
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;
}
// restore skin selection after logout
if ($_SESSION['temp'] && !empty($_SESSION['skin'])) {
$this->config->set('skin', $_SESSION['skin']);
}
}
/**
* Perfom 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 Mail storage (IMAP) user name
* @param string Mail storage (IMAP) password
* @param string Mail storage (IMAP) host
* @param bool Enables cookie check
*
* @return boolean True on success, False on failure
*/
function login($username, $pass, $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;
}
$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);
if (!$host) {
$host = $default_host;
}
// Validate that selected host is in the list of configured hosts
if (is_array($default_host)) {
$allowed = false;
foreach ($default_host as $key => $host_allowed) {
if (!is_numeric($key)) {
$host_allowed = $key;
}
if ($host == $host_allowed) {
$allowed = true;
break;
}
}
if (!$allowed) {
$host = null;
}
}
else if (!empty($default_host) && $host != rcube_utils::parse_host($default_host)) {
$host = null;
}
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);
$username = rcube_utils::idn_to_ascii($username);
// user already registered -> overwrite username
if ($user = rcube_user::query($username, $host)) {
$username = $user->data['username'];
}
$storage = $this->get_storage();
// try to log in
if (!$storage->connect($host, $username, $pass, $port, $ssl)) {
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($pass);
$_SESSION['login_time'] = time();
if (isset($_REQUEST['_timezone']) && $_REQUEST['_timezone'] != '_default_') {
$_SESSION['timezone'] = rcube_utils::get_input_value('_timezone', rcube_utils::INPUT_GPC);
}
// 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)) {
list($key, $val) = each($default_host);
$host = is_numeric($key) ? $val : $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, 'skin' => $this->config->get('skin'));
$this->user->reset();
}
/**
* Do server side actions on logout
*/
public function logout_actions()
{
$config = $this->config->all();
$storage = $this->get_storage();
if ($config['logout_purge'] && !empty($config['trash_mbox'])) {
$storage->clear_folder($config['trash_mbox']);
}
if ($config['logout_expunge']) {
$storage->expunge_folder('INBOX');
}
// Try to save unsaved user preferences
if (!empty($_SESSION['preferences'])) {
$this->user->save_prefs(unserialize($_SESSION['preferences']));
}
}
/**
* Generate a unique token to be used in a form request
*
* @return string The request token
*/
public function get_request_token()
{
$sess_id = $_COOKIE[ini_get('session.name')];
if (!$sess_id) {
$sess_id = session_id();
}
$plugin = $this->plugins->exec_hook('request_token', array(
'value' => md5('RT' . $this->get_user_id() . $this->config->get('des_key') . $sess_id)));
return $plugin['value'];
}
/**
* Check if the current request contains a valid token
*
* @param int Request method
*
* @return boolean True if request token is valid false if not
*/
public function check_request($mode = rcube_utils::INPUT_POST)
{
$token = rcube_utils::get_input_value('_token', $mode);
$sess_id = $_COOKIE[ini_get('session.name')];
return !empty($sess_id) && $token == $this->get_request_token();
}
/**
* Build a valid URL to this instance of Roundcube
*
* @param mixed Either a string with the action or url parameters as key-value pairs
*
* @return string Valid application URL
*/
public function url($p)
{
if (!is_array($p)) {
if (strpos($p, 'http') === 0) {
return $p;
}
$p = array('_action' => @func_get_arg(0));
}
$task = $p['_task'] ? $p['_task'] : ($p['task'] ? $p['task'] : $this->task);
$p['_task'] = $task;
unset($p['task']);
$url = './' . $this->filename;
$delm = '?';
foreach (array_reverse($p) as $key => $val) {
if ($val !== '' && $val !== null) {
$par = $key[0] == '_' ? $key : '_'.$key;
$url .= $delm.urlencode($par).'='.urlencode($val);
$delm = '&';
}
}
return $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')) {
// 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 = $this->show_bytes(memory_get_usage());
if (function_exists('memory_get_peak_usage'))
$mem .= '/'.$this->show_bytes(memory_get_peak_usage());
$log = $this->task . ($this->action ? '/'.$this->action : '') . ($mem ? " [$mem]" : '');
if (defined('RCMAIL_START'))
self::print_timer(RCMAIL_START, $log);
else
self::console($log);
}
}
/**
* 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;
$prefs = $this->config->all();
if (!empty($prefs['namespace_fixed']))
return;
// 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 = $prefs[$opt]) {
if ($value != 'INBOX' && !preg_match($regexp, $value)) {
$prefs[$opt] = $prefix.$value;
}
}
}
if (!empty($prefs['search_mods'])) {
$folders = array();
foreach ($prefs['search_mods'] as $idx => $value) {
if ($idx != 'INBOX' && $idx != '*' && !preg_match($regexp, $idx)) {
$idx = $prefix.$idx;
}
$folders[$idx] = $value;
}
$prefs['search_mods'] = $folders;
}
if (!empty($prefs['message_threading'])) {
$folders = array();
foreach ($prefs['message_threading'] as $idx => $value) {
if ($idx != 'INBOX' && !preg_match($regexp, $idx)) {
$idx = $prefix.$idx;
}
$folders[$prefix.$idx] = $value;
}
$prefs['message_threading'] = $folders;
}
if (!empty($prefs['collapsed_folders'])) {
$folders = explode('&&', $prefs['collapsed_folders']);
$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 New action value
*/
public function overwrite_action($action)
{
$this->action = $action;
$this->output->set_env('action', $action);
}
/**
* Set environment variables for specified config options
*/
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) {
$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 Named table attributes
* @param mixed Table row data. Either a two-dimensional array or a valid SQL result set
* @param array List of cols to show
* @param string Name of the identifier col
*
* @return string HTML table code
*/
public function table_output($attrib, $table_data, $a_show_cols, $id_col)
{
$table = new html_table($attrib);
// add table header
if (!$attrib['noheader']) {
foreach ($a_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 ($a_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'] : '';
$rowid = 'rcmrow' . rcube_utils::html_identifier($row_data[$id_col]);
$table->add_row(array('id' => $rowid, 'class' => $class));
// format each col
foreach ($a_show_cols as $col) {
$table->add($col, $this->Q(is_array($row_data[$col]) ? $row_data[$col][0] : $row_data[$col]));
}
}
}
return $table->show($attrib);
}
/**
* Convert the given date to a human readable form
* This uses the date formatting properties from config
*
* @param mixed Date representation (string, timestamp or DateTime object)
* @param string Date format to use
* @param bool Enables date convertion 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)');
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
// add some labels to client
$rcmail->output->add_label('purgefolderconfirm', 'deletemessagesconfirm');
$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
$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) {
$rcmail->build_folder_tree($a_mailboxes, $folder, $delimiter);
}
}
// allow plugins to alter the folder tree or to localize folder names
$hook = $rcmail->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($rcmail->gettext($attrib['noselection'])), '');
}
$rcmail->render_folder_tree_select($a_mailboxes, $mbox_name, $attrib['maxlength'], $select, $attrib['realnames']);
$out = $select->show($attrib['default']);
}
else {
$js_mailboxlist = array();
$tree = $rcmail->render_folder_tree_html($a_mailboxes, $mbox_name, $js_mailboxlist, $attrib);
if ($type != 'js') {
$out = html::tag('ul', $attrib, $tree, html::$common_attrib);
$rcmail->output->include_script('treelist.js');
$rcmail->output->add_gui_object('mailboxlist', $attrib['id']);
$rcmail->output->set_env('unreadwrap', $attrib['unreadwrap']);
$rcmail->output->set_env('collapsed_folders', (string)$rcmail->config->get('collapsed_folders'));
}
$rcmail->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
$rcmail->output->set_env('mailboxes_list', array_keys($js_mailboxlist));
}
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())
{
$p += array('maxlength' => 100, 'realnames' => false, '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)", rcmail_output::JS_OBJECT_NAME, $js_name),
+ '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('\\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
*
* @return string Localized folder name in UTF-8 encoding
*/
public function localize_foldername($name, $with_path = false)
{
$realnames = $this->config->get('show_real_foldernames');
if (!$realnames && ($folder_class = $this->folder_classname($name))) {
return $this->gettext($folder_class);
}
// try to localize path of the folder
if ($with_path && !$realnames) {
$storage = $this->get_storage();
$delimiter = $storage->get_hierarchy_delimiter();
$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));
return $this->gettext($folder_class) . $delimiter . rcube_charset::convert($name, 'UTF7-IMAP');
}
}
}
}
return rcube_charset::convert($name, 'UTF7-IMAP');
}
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);
}
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;');
}
public function quota_content($attrib = null)
{
$quota = $this->storage->get_quota();
$quota = $this->plugins->exec_hook('quota', $quota);
$quota_result = (array) $quota;
$quota_result['type'] = isset($_SESSION['quota_display']) ? $_SESSION['quota_display'] : '';
if ($quota['total'] > 0) {
if (!isset($quota['percent'])) {
$quota_result['percent'] = min(100, round(($quota['used']/max(1,$quota['total']))*100));
}
$title = 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'];
}
}
else {
$unlimited = $this->config->get('quota_zero_as_unlimited');
$quota_result['title'] = $this->gettext($unlimited ? 'unlimited' : 'unknown');
$quota_result['percent'] = 0;
}
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
*/
public function display_server_error($fallback = null, $fallback_args = null, $suffix = '')
{
$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 = 'errorroverquota';
}
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" or "quotum would exceed"
else if (stripos($err_str, 'quot') !== false && stripos($err_str, 'exceed') !== false) {
$error = 'erroroverquota';
}
else {
$error = 'servererrormsg';
$args = array('msg' => $err_str);
}
}
else if ($err_code < 0) {
$error = 'storageerror';
}
else if ($fallback) {
$error = $fallback;
$args = $fallback_args;
}
if ($error) {
if ($suffix && $this->text_exists($error . $suffix)) {
$error .= $suffix;
}
$this->output->show_message($error, 'error', $args);
}
}
/**
* Output HTML editor scripts
*
* @param string $mode Editor mode
*/
public function html_editor($mode = '')
{
$hook = $this->plugins->exec_hook('html_editor', array('mode' => $mode));
if ($hook['abort']) {
return;
}
$lang = strtolower($_SESSION['language']);
// TinyMCE uses two-letter lang codes, with exception of Chinese
if (strpos($lang, 'zh_') === 0) {
$lang = str_replace('_', '-', $lang);
}
else {
$lang = substr($lang, 0, 2);
}
if (!file_exists(INSTALL_PATH . 'program/js/tiny_mce/langs/'.$lang.'.js')) {
$lang = 'en';
}
$script = json_encode(array(
'mode' => $mode,
'lang' => $lang,
'skin_path' => $this->output->get_skin_path(),
'spellcheck' => intval($this->config->get('enable_spellcheck')),
'spelldict' => intval($this->config->get('spellcheck_dictionary'))
));
$this->output->include_script('tiny_mce/tiny_mce.js');
$this->output->include_script('editor.js');
$this->output->add_script("rcmail_editor_init($script)", 'docready');
}
/**
* Replaces TinyMCE's emoticon images with plain-text representation
*
* @param string $html HTML content
*
* @return string HTML content
*/
public static function replace_emoticons($html)
{
$emoticons = array(
'8-)' => 'smiley-cool',
':-#' => 'smiley-foot-in-mouth',
':-*' => 'smiley-kiss',
':-X' => 'smiley-sealed',
':-P' => 'smiley-tongue-out',
':-@' => 'smiley-yell',
":'(" => 'smiley-cry',
':-(' => 'smiley-frown',
':-D' => 'smiley-laughing',
':-)' => 'smiley-smile',
':-S' => 'smiley-undecided',
':-$' => 'smiley-embarassed',
'O:-)' => 'smiley-innocent',
':-|' => 'smiley-money-mouth',
':-O' => 'smiley-surprised',
';-)' => 'smiley-wink',
);
foreach ($emoticons as $idx => $file) {
// <img title="Cry" src="http://.../program/js/tiny_mce/plugins/emotions/img/smiley-cry.gif" border="0" alt="Cry" />
$search[] = '/<img title="[a-z ]+" src="https?:\/\/[a-z0-9_.\/-]+\/tiny_mce\/plugins\/emotions\/img\/'.$file.'.gif"[^>]+\/>/i';
$replace[] = $idx;
}
return preg_replace($search, $replace, $html);
}
/**
* File upload progress handler.
*/
public function upload_progress()
{
$prefix = ini_get('apc.rfc1867_prefix');
$params = array(
'action' => $this->action,
'name' => rcube_utils::get_input_value('_progress', rcube_utils::INPUT_GET),
);
if (function_exists('apc_fetch')) {
$status = apc_fetch($prefix . $params['name']);
if (!empty($status)) {
$status['percent'] = round($status['current']/$status['total']*100);
$params = array_merge($status, $params);
}
}
if (isset($params['percent']))
$params['text'] = $this->gettext(array('name' => 'uploadprogress', 'vars' => array(
'percent' => $params['percent'] . '%',
'current' => $this->show_bytes($params['current']),
'total' => $this->show_bytes($params['total'])
)));
$this->output->command('upload_progress_update', $params);
$this->output->send();
}
/**
* Initializes file uploading interface.
*/
public function upload_init()
{
// Enable upload progress bar
$rfc1867 = filter_var(ini_get('apc.rfc1867'), FILTER_VALIDATE_BOOLEAN);
if ($rfc1867 && ($seconds = $this->config->get('upload_progress'))) {
if ($field_name = ini_get('apc.rfc1867_name')) {
$this->output->set_env('upload_progress_name', $field_name);
$this->output->set_env('upload_progress_time', (int) $seconds);
}
}
// 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;
}
$this->output->set_env('max_filesize', $max_filesize);
$max_filesize = $this->show_bytes($max_filesize);
$this->output->set_env('filesizeerror', $this->gettext(array(
'name' => 'filesizeerror', 'vars' => array('size' => $max_filesize))));
return $max_filesize;
}
/**
* 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 Number of bytes
*
* @return string Byte string
*/
public function show_bytes($bytes)
{
if ($bytes >= 1073741824) {
$gb = $bytes/1073741824;
$str = sprintf($gb>=10 ? "%d " : "%.1f ", $gb) . $this->gettext('GB');
}
else if ($bytes >= 1048576) {
$mb = $bytes/1048576;
$str = sprintf($mb>=10 ? "%d " : "%.1f ", $mb) . $this->gettext('MB');
}
else if ($bytes >= 1024) {
$str = sprintf("%d ", round($bytes/1024)) . $this->gettext('KB');
}
else {
$str = sprintf('%d ', $bytes) . $this->gettext('B');
}
return $str;
}
/**
* Returns real size (calculated) of the message part
*
* @param rcube_message_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 ($part->encoding == 'base64') {
$size = $size / 1.33;
}
$size = '~' . $this->show_bytes($size);
}
return $size;
}
/**
* Returns message UID(s) and IMAP folder(s) from GET/POST data
*
* @param string UID value to decode
* @param string Default mailbox value (if not encoded in UIDs)
* @return array List of message UIDs per folder
*/
public static function get_uids($uids = null, $mbox = 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', RCUBE_INPUT_GPC);
$_mbox = $mbox ?: (string)rcube_utils::get_input_value('_mbox', RCUBE_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) {
// 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;
if ($uid == '*')
$result[$mbox] = $uid;
else
$result[$mbox][] = $uid;
}
}
return $result;
}
/************************************************************************
********* Deprecated methods (to be removed) *********
***********************************************************************/
public static function setcookie($name, $value, $exp = 0)
{
rcube_utils::setcookie($name, $value, $exp);
}
public function imap_connect()
{
return $this->storage_connect();
}
public function imap_init()
{
return $this->storage_init();
}
/**
* 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_output_html.php b/program/include/rcmail_output_html.php
index 1fa0376fb..e8b5c9828 100644
--- a/program/include/rcmail_output_html.php
+++ b/program/include/rcmail_output_html.php
@@ -1,1859 +1,1859 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcmail_output_html.php |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2006-2013, 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 = null;
protected $js_env = array();
protected $js_labels = array();
protected $js_commands = array();
protected $skin_paths = array();
protected $template_name;
protected $scripts_path = '';
protected $script_files = array();
protected $css_files = array();
protected $scripts = array();
protected $default_template = "<html>\n<head><title></title></head>\n<body></body>\n</html>";
protected $header = '';
protected $footer = '';
protected $body = '';
protected $base_path = '';
protected $devel_mode = false;
// deprecated names of templates used before 0.5
protected $deprecated_templates = array(
'contact' => 'showcontact',
'contactadd' => 'addcontact',
'contactedit' => 'editcontact',
'identityedit' => 'editidentity',
'messageprint' => 'printmessage',
);
/**
* Constructor
*
* @todo Replace $this->config with the real rcube_config object
*/
public function __construct($task = null, $framed = false)
{
parent::__construct();
$this->devel_mode = $this->config->get('devel_mode');
//$this->framed = $framed;
$this->set_env('task', $task);
$this->set_env('x_frame_options', $this->config->get('x_frame_options', 'sameorigin'));
$this->set_env('standard_windows', (bool) $this->config->get('standard_windows'));
// 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));
// load the correct skin (in case user-defined)
$skin = $this->config->get('skin');
$this->set_skin($skin);
$this->set_env('skin', $skin);
if (!empty($_REQUEST['_extwin']))
$this->set_env('extwin', 1);
if ($this->framed || !empty($_REQUEST['_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
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 environment variable
*
* @param string Property name
* @param mixed Property value
* @param boolean True if this property should be added to client environment
*/
public function set_env($name, $value, $addtojs = true)
{
$this->env[$name] = $value;
if ($addtojs || isset($this->js_env[$name])) {
$this->js_env[$name] = $value;
}
}
/**
* 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;
}
/**
* Set skin
*/
public function set_skin($skin)
{
$valid = false;
$path = RCUBE_INSTALL_PATH . 'skins/';
if (!empty($skin) && is_dir($path . $skin) && is_readable($path . $skin)) {
$skin_path = 'skins/' . $skin;
$valid = true;
}
else {
$skin_path = $this->config->get('skin_path');
if (!$skin_path) {
$skin_path = 'skins/' . rcube_config::DEFAULT_SKIN;
}
$valid = !$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);
return $valid;
}
/**
* 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 dependecies
$meta = @file_get_contents(RCUBE_INSTALL_PATH . $skin_path . '/meta.json');
$meta = @json_decode($meta, true);
if ($meta['extends']) {
$path = RCUBE_INSTALL_PATH . 'skins/';
if (is_dir($path . $meta['extends']) && is_readable($path . $meta['extends'])) {
$this->load_skin('skins/' . $meta['extends']);
}
}
}
/**
* Check if a specific template exists
*
* @param string 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 name/path to resolve (starting with /)
* @param string Reference to the base path of the matching skin
* @param string 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);
foreach ($skin_paths as $skin_path) {
$path = realpath($skin_path . $file);
if (is_file($path)) {
return $skin_path . $file;
}
}
return false;
}
/**
* Register a GUI object to the client script
*
* @param string Object name
* @param string Object ID
* @return void
*/
public function add_gui_object($obj, $id)
{
$this->add_script(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('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;
$env = $all ? null : array_intersect_key($this->env, array('extwin'=>1, 'framed'=>1));
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->script_files = array();
$this->scripts = array();
$this->header = '';
$this->footer = '';
$this->body = '';
// load defaults
if (!$all) {
$this->__construct();
}
}
/**
* 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
*/
public function redirect($p = array(), $delay = 1)
{
if ($this->env['extwin'])
$p['extwin'] = 1;
$location = $this->app->url($p);
header('Location: ' . $location);
exit;
}
/**
* Send the request output to the client.
* This will either parse a skin tempalte or send an AJAX response
*
* @param string Template name
* @param boolean True if script should terminate (default)
*/
public function send($templ = null, $exit = true)
{
if ($templ != 'iframe') {
// prevent from endless loops
if ($exit != 'recur' && $this->app->plugins->is_processing('render_page')) {
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 = '')
{
// unlock interface after iframe load
$unlock = preg_replace('/[^a-z0-9]/i', '', $_REQUEST['_unlock']);
if ($this->framed) {
array_unshift($this->js_commands, array('iframe_loaded', $unlock));
}
else if ($unlock) {
array_unshift($this->js_commands, array('hide_message', $unlock));
}
if (!empty($this->script_files))
$this->set_env('request_token', $this->app->get_request_token());
// write all env variables to client
if ($commands = $this->get_js_commands()) {
$js = $this->framed ? "if (window.parent) {\n" : '';
$js .= $commands . ($this->framed ? ' }' : '');
$this->add_script($js, 'head_top');
}
// send clickjacking protection headers
$iframe = $this->framed || !empty($_REQUEST['_framed']);
if (!headers_sent() && ($xframe = $this->app->config->get('x_frame_options', 'sameorigin')))
header('X-Frame-Options: ' . ($iframe && $xframe == 'deny' ? 'sameorigin' : $xframe));
// call super method
$this->_write($template, $this->config->get('skin_path'));
}
/**
* Parse a specific skin template and deliver to stdout (or return)
*
* @param string Template name
* @param boolean Exit script
* @param boolean 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;
$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
$plugin_skin_paths = array();
foreach ($this->skin_paths as $skin_path) {
$plugin_skin_paths[] = $this->app->plugins->url . $plugin . '/' . $skin_path;
}
// add fallback to default skin
if (is_dir($this->app->plugins->dir . $plugin . '/skins/default')) {
$skin_dir = $plugin . '/skins/default';
$plugin_skin_paths[] = $this->app->plugins->url . $skin_dir;
}
// add 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) {
$path = "$skin_path/templates/$name.html";
// fallback to deprecated template names
if (!is_readable($path) && $this->deprecated_templates[$realname]) {
$path = "$skin_path/templates/" . $this->deprecated_templates[$realname] . ".html";
if (is_readable($path)) {
rcube::raise_error(array(
'code' => 502, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Using deprecated template '" . $this->deprecated_templates[$realname]
. "' in $skin_path/templates. Please rename to '$realname'"),
true, false);
}
}
if (is_readable($path)) {
$this->config->set('skin_path', $skin_path);
$this->base_path = preg_replace('!plugins/\w+/!', '', $skin_path); // set base_path to core skin directory (not plugin's skin)
$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' => 501,
'type' => 'php',
'line' => __LINE__,
'file' => __FILE__,
'message' => 'Error loading template for '.$realname
), true, $write);
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));
// save some memory
$output = $hook['content'];
unset($hook['content']);
// make sure all <form> tags have a valid request token
$output = preg_replace_callback('/<form\s+([^>]+)>/Ui', array($this, 'alter_form_tag'), $output);
$this->footer = preg_replace_callback('/<form\s+([^>]+)>/Ui', array($this, 'alter_form_tag'), $this->footer);
if (!$write) {
return $output;
}
$this->write(trim($output));
if ($exit) {
exit;
}
}
/**
* Return executable javascript code for all registered commands
*
* @return string $out
*/
protected function get_js_commands()
{
$out = '';
if (!$this->framed && !empty($this->js_env)) {
$out .= self::JS_OBJECT_NAME . '.set_env('.self::json_serialize($this->js_env).");\n";
}
if (!empty($this->js_labels)) {
$this->command('add_label', $this->js_labels);
}
foreach ($this->js_commands as $i => $args) {
$method = array_shift($args);
foreach ($args as $i => $arg) {
$args[$i] = self::json_serialize($arg);
}
$parent = $this->framed || preg_match('/^parent\./', $method);
$out .= sprintf(
"%s.%s(%s);\n",
($parent ? 'if(window.parent && parent.'.self::JS_OBJECT_NAME.') parent.' : '') . self::JS_OBJECT_NAME,
preg_replace('/^parent\./', '', $method),
implode(',', $args)
);
}
return $out;
}
/**
* Make URLs starting with a slash point to skin directory
*
* @param string Input string
* @param boolean True if URL should be resolved using the current skin path stack
* @return string
*/
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;
}
else
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;
}
/***** 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 funtion 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 timestamp to .js and .css filename
*/
protected function fix_paths($output)
{
return preg_replace_callback(
'!(src|href|background)=(["\']?)([a-z0-9/_.-]+)(["\'\s>])!i',
array($this, 'file_callback'), $output);
}
/**
* Callback function for preg_replace_callback in write()
*
* @return string Parsed string
*/
protected function file_callback($matches)
{
$file = $matches[3];
$file = preg_replace('!^/this/!', '/', $file);
// correct absolute paths
if ($file[0] == '/') {
$file = $this->base_path . $file;
}
// add file modification timestamp
if (preg_match('/\.(js|css)$/', $file, $m)) {
$file = $this->file_mod($file);
}
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($minified_file)) {
return $minified_file . '?s=' . $fs;
}
}
if ($fs = @filemtime($file)) {
$file .= '?s=' . $fs;
}
return $file;
}
/**
* Public wrapper to dipp into template parsing.
*
* @param string $input
* @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);
return $input;
}
/**
* Parse for conditional tags
*
* @param string $input
* @return string
*/
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];
$result.= ($submatches[1] != 'endif' ? preg_replace('/.*<roundcube:endif\s+[^>]+>\n?/Uis', '', $submatches[3], 1) : $submatches[3]);
}
else {
$result = "<roundcube:$submatches[1] $submatches[2]>" . $submatches[3];
}
return $matches[0] . $this->parse_conditions($result);
}
rcube::raise_error(array(
'code' => 500,
'type' => 'php',
'line' => __LINE__,
'file' => __FILE__,
'message' => "Unable to parse conditional tag " . $matches[2]
), true, false);
}
return $input;
}
/**
* Determines if a given condition is met
*
* @todo Extend this to allow real conditions, not just "set"
* @param string Condition statement
* @return boolean True if condition is met, False if not
*/
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 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']",
"\$app->config->get('\\1',rcube_utils::get_boolean('\\3'))",
"\$env['\\1']",
"rcube_utils::get_input_value('\\1', rcube_utils::INPUT_GPC)",
"\$_COOKIE['\\1']",
"\$browser->{'\\1'}",
$this->template_name,
),
$expression
);
$fn = create_function('$app,$browser,$env', "return ($expression);");
if (!$fn) {
rcube::raise_error(array(
'code' => 505,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "Expression parse error on: ($expression)"), true, false);
return null;
}
return $fn($this->app, $this->browser, $this->env);
}
/**
* Search for special tags in input and replace them
* with the appropriate content
*
* @param string Input string to parse
* @return string Altered input string
* @todo Use DOM-parser to traverse template HTML
* @todo Maybe a cache.
*/
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 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 (!empty($attrib['title']) && $this->app->text_exists($attrib['title'])) {
+ if ($command != 'button' && !empty($attrib['title']) && $this->app->text_exists($attrib['title'])) {
$attrib['title'] = $this->app->gettext($attrib['title']);
}
- if (!empty($attrib['summary']) && $this->app->text_exists($attrib['summary'])) {
+ 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']) {
// @FIXME: 'noshow' is useless, remove?
if ($attrib['noshow']) {
return '';
}
$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' : '');
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;
// include a file
case 'include':
$old_base_path = $this->base_path;
if (!empty($attrib['skin_path'])) $attrib['skinpath'] = $attrib['skin_path'];
if ($path = $this->get_skin_file($attrib['file'], $skin_path, $attrib['skinpath'])) {
$this->base_path = preg_replace('!plugins/\w+/!', '', $skin_path); // set base_path to core skin directory (not plugin's skin)
$path = realpath($path);
}
if (is_readable($path)) {
if ($this->config->get('skin_include_php')) {
$incl = $this->include_php($path);
}
else {
$incl = file_get_contents($path);
}
$incl = $this->parse_conditions($incl);
$incl = $this->parse_xml($incl);
$incl = $this->fix_paths($incl);
$this->base_path = $old_base_path;
return $incl;
}
break;
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 = '';
// we are calling a class/method
if (($handler = $this->object_handlers[$object]) && is_array($handler)) {
if ((is_object($handler[0]) && method_exists($handler[0], $handler[1])) ||
(is_string($handler[0]) && class_exists($handler[0])))
$content = call_user_func($handler, $attrib);
}
// execute object handler function
else if (function_exists($handler)) {
$content = call_user_func($handler, $attrib);
}
else if ($object == 'doctype') {
$content = html::doctype($attrib['value']);
}
else if ($object == 'logo') {
$attrib += array('alt' => $this->xml_command(array('', 'object', 'name="productname"')));
if ($logo = $this->config->get('skin_logo')) {
if (is_array($logo)) {
if ($template_logo = $logo[$this->template_name]) {
$attrib['src'] = $template_logo;
}
elseif ($template_logo = $logo['*']) {
$attrib['src'] = $template_logo;
}
}
else {
$attrib['src'] = $logo;
}
}
$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);
}
// exec plugin hooks for this template object
$hook = $this->app->plugins->exec_hook("template_object_$object", $attrib + array('content' => $content));
return $hook['content'];
// return code for a specified eval expression
case 'exp':
return html::quote($this->eval_expression($attrib['expression']));
// return variable
case 'var':
$var = explode(':', $attrib['name']);
$name = $var[1];
$value = '';
switch ($var[0]) {
case 'env':
$value = $this->env[$name];
break;
case 'config':
$value = $this->config->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]);
break;
case 'browser':
$value = $this->browser->{$name};
break;
}
if (is_array($value)) {
$value = implode(', ', $value);
}
return html::quote($value);
case 'form':
return $this->form_tag($attrib);
}
return '';
}
/**
* Include a specific file and return it's contents
*
* @param string 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;
}
/**
* Create and register a button
*
* @param array Named button attributes
* @return string HTML button
* @todo Remove all inline JS calls and use jQuery instead.
* @todo Remove all sprintf()'s - they are pretty, but also slow.
*/
public function button($attrib)
{
static $s_button_count = 100;
// these commands can be called directly via url
$a_static_commands = array('compose', 'list', 'preferences', 'folders', 'identities');
if (!($attrib['command'] || $attrib['name'] || $attrib['href'])) {
return '';
}
// try to find out the button type
if ($attrib['type']) {
$attrib['type'] = strtolower($attrib['type']);
}
else {
$attrib['type'] = ($attrib['image'] || $attrib['imagepas'] || $attrib['imageact']) ? 'image' : 'link';
}
$command = $attrib['command'];
if ($attrib['task'])
$command = $attrib['task'] . '.' . $command;
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']) {
$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'));
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'));
}
// generate html code for button
if ($btn_content) {
$attrib_str = html::attrib_string($attrib, array_merge($link_attrib, array('data-*')));
$out = sprintf('<a%s>%s</a>', $attrib_str, $btn_content);
}
if ($attrib['wrapper']) {
$out = html::tag($attrib['wrapper'], null, $out);
}
return $out;
}
/**
* Link an external script file
*
* @param string File URL
* @param string Target position [head|foot]
*/
public function include_script($file, $position='head')
{
if (!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 JS code snippet
* @param string Target position [head|head_top|foot]
*/
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 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 HTML template
* @param string Base for absolute paths
*/
public function _write($templ = '', $base_path = '')
{
$output = trim($templ);
if (empty($output)) {
$output = $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);
}
}
// 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";
}
// definition of the code to be placed in the document header and footer
if (is_array($this->script_files['head'])) {
foreach ($this->script_files['head'] as $file) {
$page_header .= html::script($file);
}
}
$head_script = $this->scripts['head_top'] . $this->scripts['head'];
if (!empty($head_script)) {
$page_header .= html::script(array(), $head_script);
}
if (!empty($this->header)) {
$page_header .= $this->header;
}
// put docready commands into page footer
if (!empty($this->scripts['docready'])) {
$this->add_script('$(document).ready(function(){ ' . $this->scripts['docready'] . "\n});", 'foot');
}
if (is_array($this->script_files['foot'])) {
foreach ($this->script_files['foot'] as $file) {
$page_footer .= html::script($file);
}
}
if (!empty($this->footer)) {
$page_footer .= $this->footer . "\n";
}
if (!empty($this->scripts['foot'])) {
$page_footer .= html::script(array(), $this->scripts['foot']);
}
// 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>'))) {
$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) {
$css .= html::tag('link', array('rel' => 'stylesheet',
'type' => 'text/css', 'href' => $file, 'nl' => true));
}
$output = substr_replace($output, $css, $pos, 0);
}
$output = $this->parse_with_globals($this->fix_paths($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) : 'program/resources/blank.gif';
// register as 'contentframe' object
if ($is_contentframe || $attrib['contentframe']) {
$this->set_env('contentframe', $attrib['contentframe'] ? $attrib['contentframe'] : $attrib['name']);
$this->set_env('blankpage', $attrib['src']);
}
return html::iframe($attrib);
}
/* ************* common functions delivering gui objects ************** */
/**
* Create a form tag with the necessary hidden fields
*
* @param array Named tag parameters
* @return string HTML code for the form
*/
public function form_tag($attrib, $content = null)
{
if ($this->framed || !empty($_REQUEST['_framed'])) {
$hiddenfield = new html_hiddenfield(array('name' => '_framed', 'value' => '1'));
$hidden = $hiddenfield->show();
}
if ($this->env['extwin']) {
$hiddenfield = new html_hiddenfield(array('name' => '_extwin', '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 Named tag parameters including 'action' and 'task' values which will be put into hidden fields
* @param string Form content
* @return string HTML code for the form
*/
public function request_form($attrib, $content = '')
{
$hidden = new html_hiddenfield();
if ($attrib['task']) {
$hidden->add(array('name' => '_task', 'value' => $attrib['task']));
}
if ($attrib['action']) {
$hidden->add(array('name' => '_action', 'value' => $attrib['action']));
}
unset($attrib['task'], $attrib['request']);
$attrib['action'] = './';
// we already have a <form> tag
if ($attrib['form']) {
if ($this->framed || !empty($_REQUEST['_framed']))
$hidden->add(array('name' => '_framed', 'value' => '1'));
return $hidden->show() . $content;
}
else
return $this->form_tag($attrib, $hidden->show() . $content);
}
/**
* GUI object 'username'
* Showing IMAP username of the current session
*
* @param array Named tag parameters (currently not used)
* @return string HTML code for the gui object
*/
public function current_username($attrib)
{
static $username;
// alread fetched
if (!empty($username)) {
return $username;
}
// 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 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';
// 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);
}
$form_name = !empty($attrib['form']) ? $attrib['form'] : 'form';
$this->add_gui_object('loginform', $form_name);
// create HTML table with two cols
$table = new html_table(array('cols' => 2));
$table->add('title', html::label('rcmloginuser', 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'])) {
$submit = new html_inputfield(array('type' => 'submit', 'id' => 'rcmloginsubmit',
'class' => 'button mainaction', 'value' => $this->app->gettext('login')));
$out .= html::p('formbuttons', $submit->show());
}
// 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 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);
if (empty($images) || $this->app->task == 'logout')
return;
$this->add_script('var images = ' . self::json_serialize($images) .';
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 Named parameters
* @return string HTML code for the gui object
*/
protected function search_form($attrib)
{
// add some labels to client
$this->add_label('searching');
$attrib['name'] = '_q';
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmqsearchbox';
}
if ($attrib['type'] == 'search' && !$this->browser->khtml) {
unset($attrib['type'], $attrib['results']);
}
$input_q = new html_inputfield($attrib);
$out = $input_q->show();
$this->add_gui_object('qsearchbox', $attrib['id']);
// add form tag around text field
if (empty($attrib['form'])) {
$out = $this->form_tag(array(
'name' => "rcmqsearchform",
'onsubmit' => self::JS_OBJECT_NAME . ".command('search'); return false",
'style' => "display:inline"),
$out);
}
return $out;
}
/**
* Builder for GUI object 'message'
*
* @param array Named tag parameters
* @return string HTML code for the gui object
*/
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 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 (!empty($_POST['_charset'])) {
$set = $_POST['_charset'];
}
else if (!empty($attrib['selected'])) {
$set = $attrib['selected'];
}
else {
$set = $this->get_charset();
}
$set = strtoupper($set);
if (!isset($charsets[$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;
}
}
diff --git a/program/js/app.js b/program/js/app.js
index bf163c614..e7f9d02b6 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -1,8219 +1,8240 @@
/**
* 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-2014, The Roundcube Dev Team
* Copyright (C) 2011-2014, 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 = new Array();
// webmail client settings
this.dblclick_time = 500;
this.message_time = 5000;
this.identifier_expr = new RegExp('[^0-9a-z\-_]', 'gi');
// environment defaults
this.env = {
request_timeout: 180, // seconds
draft_autosave: 0, // seconds
comm_path: './',
blankpage: 'program/resources/blank.gif',
recipients_separator: ',',
recipients_delimiter: ', ',
popup_width: 1150,
popup_width_small: 900
};
// 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).bind('beforeunload', function() { rcmail.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)
init_button(command, button_prop);
};
// 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
if (this.env.server_error != 409 && (!bw.dom || !bw.xmlhttp_test() || (bw.mz && bw.vendver < 1.9) || (bw.ie && bw.vendver < 7))) {
this.goto_url('error', '_code=0x199');
return;
}
// 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]);
// clickjacking protection
if (this.env.x_frame_options) {
try {
// bust frame if not allowed
if (this.env.x_frame_options == 'deny' && top.location.href != self.location.href)
top.location.href = self.location.href;
else if (top.location.hostname != self.location.hostname)
throw 1;
} catch (e) {
// possible clickjacking attack: disable all form elements
$('form').each(function(){ ref.lock_form(this, true); });
this.display_message("Blocked: possible clickjacking attack!", 'error');
return;
}
}
// 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.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('click', function(o) { ref.msglist_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); })
.addEventListener('listupdate', function(o) { ref.triggerEvent('listupdate', 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.gui_objects.messagelist.parentNode.onclick = function(e){ return ref.click_on_list(e || window.event); };
this.enable_command('toggle_status', 'toggle_flag', 'sort', true);
this.enable_command('set-listmode', this.env.threads && !this.is_multifolder_listing());
// load messages
this.command('list');
$(this.gui_objects.qsearchbox).val(this.env.search_text).focusin(function() { rcmail.message_list.blur(); });
}
this.set_button_titles();
this.env.message_commands = ['show', 'reply', 'reply-all', 'reply-list',
'move', 'copy', 'delete', 'open', 'mark', 'edit', 'viewsource',
'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.blockedobjects) {
if (this.gui_objects.remoteobjectsmsg)
this.gui_objects.remoteobjectsmsg.style.display = 'block';
this.enable_command('load-images', 'always-load', 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);
}
}
else if (this.env.action == 'compose') {
this.env.address_group_stack = [];
this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel',
'toggle-editor', 'list-adresses', 'pushgroup', 'search', 'reset-search', 'extwin',
'insert-response', 'save-response', 'menu-open', 'menu-close'];
if (this.env.drafts_mailbox)
this.env.compose_commands.push('savedraft')
this.enable_command(this.env.compose_commands, 'identities', 'responses', true);
// add more commands (not enabled)
$.merge(this.env.compose_commands, ['add-recipient', 'firstpage', 'previouspage', 'nextpage', 'lastpage']);
if (this.env.spellcheck) {
this.env.spellcheck.spelling_state_observer = function(s) { ref.spellcheck_state(); };
this.env.compose_commands.push('spellcheck')
this.enable_command('spellcheck', true);
}
// 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); })
.bind('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
for (var i=0; this.buttons['save-response'] && i < this.buttons['save-response'].length; i++) {
$('#'+this.buttons['save-response'][i].id).mousedown(function(e){ return rcube_event.cancel(e); })
}
}
// init message compose form
this.init_messageform();
}
else if (this.env.action == 'get')
this.enable_command('download', 'print', true);
// show printing dialog
else if (this.env.action == 'print' && this.env.uid) {
if (bw.safari)
setTimeout('window.print()', 10);
else
window.print();
}
// 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');
}
// 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('to'); })
- .addEventListener('keypress', function(o) { if (o.key_pressed == o.ENTER_KEY) ref.compose_add_recipient('to'); })
+ .addEventListener('keypress', function(o) {
+ if (o.key_pressed == o.ENTER_KEY) {
+ if (!ref.compose_add_recipient('to')) {
+ // 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();
}
if (this.gui_objects.addressbookslist) {
this.gui_objects.folderlist = this.gui_objects.addressbookslist;
this.enable_command('list-adresses', 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);
}
// 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();
if (this.env.cid)
this.contact_list.highlight_row(this.env.cid);
this.gui_objects.contactslist.parentNode.onmousedown = function(e){ return ref.click_on_list(e); };
$(this.gui_objects.qsearchbox).focusin(function() { ref.contact_list.blur(); });
this.update_group_commands();
this.command('list');
}
this.set_page_buttons();
if (this.env.cid) {
this.enable_command('show', 'edit', 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();
}
break;
case 'settings':
this.enable_command('preferences', 'identities', 'responses', 'save', 'folders', 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);
}
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:false});
this.identity_list
.addEventListener('select', function(o) { ref.identity_select(o); })
.init()
.focus();
if (this.env.iid)
this.identity_list.highlight_row(this.env.iid);
}
else if (this.gui_objects.sectionslist) {
this.sections_list = new rcube_list_widget(this.gui_objects.sectionslist, {multiselect:false, draggable:false, keyboard:false});
this.sections_list
.addEventListener('select', function(o) { 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:false});
this.responses_list
.addEventListener('select', function(list) {
var win, id = list.get_single_selection();
ref.enable_command('delete', !!id && $.inArray(id, ref.env.readonly_responses) < 0);
if (id && (win = ref.get_frame_window(ref.env.contentframe))) {
ref.set_busy(true);
ref.location_href({ _action:'edit-response', _key:id, _framed:1 }, win);
}
})
.init()
.focus();
}
break;
case 'login':
var input_user = $('#rcmloginuser');
input_user.bind('keyup', function(e){ return rcmail.login_user_keyup(e); });
if (input_user.val() == '')
input_user.focus();
else
$('#rcmloginpwd').focus();
// detect client timezone
if (window.jstz) {
var timezone = jstz.determine();
if (timezone.name())
$('#rcmlogintz').val(timezone.name());
}
else {
$('#rcmlogintz').val(new Date().getStdTimezoneOffset() / -60);
}
// display 'loading' message on form submit, lock submit button
$('form').submit(function () {
$('input[type=submit]', this).prop('disabled', true);
rcmail.clear_messages();
rcmail.display_message('', 'loading');
});
this.enable_command('login', true);
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();
// unset contentframe variable if preview_pane is enabled
if (this.env.contentframe && !$('#' + this.env.contentframe).is(':visible'))
this.env.contentframe = null;
// 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(this.pending_message[0], this.pending_message[1], this.pending_message[2]);
// map implicit containers
if (this.gui_objects.folderlist) {
this.gui_containers.foldertray = $(this.gui_objects.folderlist);
// init treelist widget
if (window.rcube_treelist_widget) {
this.treelist = new rcube_treelist_widget(this.gui_objects.folderlist, {
id_prefix: 'rcmli',
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('select', function(node) { ref.triggerEvent('selectfolder', { folder:node.id, prefix:'rcmli' }) });
}
}
// activate html5 file drop feature (if browser supports it and if configured)
if (this.gui_objects.filedrop && this.env.filedrop && ((window.XMLHttpRequest && XMLHttpRequest.prototype && XMLHttpRequest.prototype.sendAsBinary) || window.FormData)) {
$(document.body).bind('dragover dragleave drop', function(e){ return ref.document_drag_hover(e, e.type == 'dragover'); });
$(this.gui_objects.filedrop).addClass('droptarget')
.bind('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)
.bind('mouseup', body_mouseup)
.bind('keydown', function(e){ return ref.doc_keypress(e); });
$('iframe').load(function(e) {
try { $(this.contentDocument || this.contentWindow).on('mouseup', body_mouseup); }
catch (e) {/* catch possible "Permission denied" error in IE */ }
})
.contents().on('mouseup', body_mouseup);
// trigger init event hook
this.triggerEvent('init', { task:this.task, action:this.env.action });
// execute all foreign onload scripts
// @deprecated
for (var i in this.onloads) {
if (typeof this.onloads[i] === 'string')
eval(this.onloads[i]);
else if (typeof this.onloads[i] === 'function')
this.onloads[i]();
}
// start keep-alive and refresh intervals
this.start_refresh();
this.start_keepalive();
};
this.log = function(msg)
{
if (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)
+ if (obj && obj.blur && !(event || rcube_event.is_keyboard(event)))
obj.blur();
// do nothing if interface is locked by other command (with exception for searching reset)
if (this.busy && !(command == 'reset-search' && this.last_command == 'search'))
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' && $.inArray(command, this.env.compose_commands) < 0 && !this.env.server_error) {
if (this.cmp_hash != this.compose_field_hash() && !confirm(this.get_label('notsentwarning')))
return false;
// remove copy from local storage if compose screen is left intentionally
this.remove_compose_data(this.env.compose_id);
}
this.last_command = command;
// process external commands
if (typeof this.command_handlers[command] === 'function') {
ret = this.command_handlers[command](props, obj);
return ret !== undefined ? ret : (obj ? false : true);
}
else if (typeof this.command_handlers[command] === 'string') {
ret = window[this.command_handlers[command]](props, obj);
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) {
case 'login':
if (this.gui_objects.loginform)
this.gui_objects.loginform.submit();
break;
// 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();
$("input[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.display_message(this.get_label('windowopenerror'), 'error');
}
}
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];
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', {_mbox: this.get_message_mailbox(uid), _uid: uid});
return true;
}
break;
case 'close':
if (this.env.extwin)
window.close();
break;
case 'list':
// re-send search query for the selected folder
if (props && props != '' && this.env.search_request && this.gui_objects.qsearchbox.value) {
var oldmbox = this.env.search_scope == 'all' ? '*' : this.env.mailbox;
this.env.search_mods[props] = this.env.search_mods[oldmbox]; // copy search mods from active search
this.env.mailbox = props;
this.env.search_scope = 'sub';
this.qsearch(this.gui_objects.qsearchbox.value);
this.select_folder(this.env.mailbox, '', true);
break;
}
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');
}
break;
case 'add':
if (this.task == 'addressbook')
this.load_contact(0, 'add');
else if (this.task == 'settings' && this.env.action == 'responses') {
var frame;
if ((frame = this.get_frame_window(this.env.contentframe))) {
this.set_busy(true);
this.location_href({ _action:'add-response', _framed:1 }, frame);
}
}
else if (this.task == 'settings') {
this.identity_list.clear_selection();
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 == 'settings' && props)
this.load_identity(props, 'edit-identity');
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) {
// adv. search
if (this.env.action == 'search') {
}
// user prefs
else if ((input = $("input[name='_pagesize']", form)) && input.length && isNaN(parseInt(input.val()))) {
alert(this.get_label('nopagesizewarning'));
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 = $("input[name='_email']", form)) && input.length && !rcube_check_email(input.val())
) {
alert(this.get_label('noemailwarning'));
input.focus();
break;
}
// clear empty input fields
$('input.placeholder').each(function(){ if (this.value == this._placeholder) this.value = ''; });
}
// 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);
break;
case 'copy':
if (this.task == 'mail')
this.copy_messages(props, event);
else if (this.task == 'addressbook')
this.copy_contacts(props);
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 'always-load':
if (this.env.uid && this.env.sender) {
this.add_contact(this.env.sender);
setTimeout(function(){ ref.command('load-images'); }, 300);
break;
}
case 'load-images':
if (this.env.uid)
this.show_message(this.env.uid, true, this.env.action=='preview');
break;
case 'load-attachment':
case 'open-attachment':
case 'download-attachment':
var qstring = '_mbox='+urlencode(this.env.mailbox)+'&_uid='+this.env.uid+'&_part='+props,
mimetype = this.env.attachments[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) {
if (this.open_window(this.env.comm_path+'&_action=get&'+qstring+'&_frame=1'))
break;
}
this.goto_url('get', qstring+'&_download=1', false);
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
if (props == 'invert')
this.message_list.invert_selection();
else
this.message_list.select_all(props == 'page' ? '' : props);
this.dummy_select = null;
break;
case 'select-none':
this.select_all_mode = false;
this.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;
if (props)
url._to = props;
// also send search request so we can go back to search result after message is sent
if (this.env.search_request)
url._search = this.env.search_request;
}
// 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);
else if (this.env.group)
this.http_post('mailto', { _gid: this.env.group, _source: this.env.source }, true);
break;
}
}
else if (props)
url._to = props;
this.open_compose_step(url);
break;
case 'spellcheck':
if (this.spellcheck_state()) {
this.stop_spellchecking();
}
else {
if (window.tinyMCE && tinyMCE.get(this.env.composebody)) {
tinyMCE.execCommand('mceSpellCheck', true);
}
else if (this.env.spellcheck && this.env.spellcheck.spellCheck) {
this.env.spellcheck.spellCheck();
}
}
this.spellcheck_state();
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.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)
alert(this.get_label('selectimportfile'));
aborted = true;
}
break;
case 'insert-sig':
this.change_identity($("[name='_from']")[0], true);
break;
case 'list-adresses':
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)};
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.env.action == 'get') {
this.gui_objects.messagepartframe.contentWindow.print();
}
else if (uid = this.get_single_uid()) {
ref.printwin = this.open_window(this.env.comm_path+'&_action=print&_uid='+uid+'&_mbox='+urlencode(this.get_message_mailbox(uid))+(this.env.safemode ? '&_safe=1' : ''), true, true);
if (this.printwin) {
if (this.env.action != 'show')
this.mark_message('read', uid);
}
}
break;
case 'viewsource':
if (uid = this.get_single_uid())
this.open_window(this.env.comm_path+'&_action=viewsource&_uid='+uid+'&_mbox='+urlencode(this.env.mailbox), true, true);
break;
case 'download':
if (this.env.action == 'get') {
location.href = location.href.replace(/_frame=/, '_download=');
}
else if (uid = this.get_single_uid()) {
this.goto_url('viewsource', { _uid: uid, _mbox: this.get_message_mailbox(uid), _save: 1 });
}
break;
// quicksearch
case 'search':
if (!props && this.gui_objects.qsearchbox)
props = this.gui_objects.qsearchbox.value;
if (props) {
this.qsearch(props);
break;
}
// reset quicksearch
case 'reset-search':
var n, s = this.env.search_request || this.env.qsearch;
this.reset_qsearch();
this.select_all_mode = false;
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 to stack
this.env.address_group_stack.push(props.id);
if (obj && event)
rcube_event.cancel(event);
case 'listgroup':
this.reset_qsearch();
this.list_contacts(props.source, props.id);
break;
case 'popgroup':
if (this.env.address_group_stack.length > 1) {
this.env.address_group_stack.pop();
this.reset_qsearch();
this.list_contacts(props.source, this.env.address_group_stack[this.env.address_group_stack.length-1]);
}
break;
case 'import-messages':
var form = props || this.gui_objects.importform,
importlock = this.set_busy(true, 'importwait');
$('input[name="_unlock"]', form).val(importlock);
if (!(flag = this.upload_file(form, 'import'))) {
this.set_busy(false, null, importlock);
if (flag !== false)
alert(this.get_label('selectimportfile'));
aborted = true;
}
break;
case 'import':
if (this.env.action == 'import' && this.gui_objects.importform) {
var file = document.getElementById('rcmimportfile');
if (file && !file.value) {
alert(this.get_label('selectimportfile'));
aborted = true;
break;
}
this.gui_objects.importform.submit();
this.set_busy(true, 'importwait');
this.lock_form(this.gui_objects.importform, true);
}
else
this.goto_url('import', (this.env.source ? '_target='+urlencode(this.env.source)+'&' : ''));
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 });
}
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(',') });
}
break;
case 'upload-photo':
this.upload_contact_photo(props || this.gui_objects.uploadform);
break;
case 'delete-photo':
this.replace_contact_photo('-del-');
break;
// user settings commands
case 'preferences':
case 'identities':
case 'responses':
case 'folders':
this.goto_url('settings/' + command);
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);
}
break;
}
if (!aborted && this.triggerEvent('after'+command, props) === false)
ret = false;
this.triggerEvent('actionafter', { props:props, action:command, aborted:aborted });
return ret === false ? false : obj ? false : 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.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;
//document.body.style.cursor = a ? 'wait' : 'default';
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)
{
if (this.task===task && task!='mail')
return;
var url = this.get_task_url(task);
if (task == 'mail')
url += '&_mbox=INBOX';
else if (task == 'logout')
this.clear_compose_data();
this.redirect(url);
};
this.get_task_url = function(task, url)
{
if (!url)
url = this.env.comm_path;
return url.replace(/_task=[a-z0-9_-]+/i, '_task='+task);
};
this.reload = function(delay)
{
if (this.is_framed())
parent.rcmail.reload(delay);
else if (delay)
setTimeout(function(){ rcmail.reload(); }, delay);
else if (window.location)
location.href = this.env.comm_path + (this.env.action ? '&_action='+this.env.action : '');
};
// Add variable to GET string, replace old value if exists
this.add_url = function(url, name, value)
{
value = urlencode(value);
if (/(\?.*)$/.test(url)) {
var 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);
}
return url + '?' + name + '=' + value;
};
this.is_framed = function()
{
return (this.env.framed && parent.rcmail && parent.rcmail != this && parent.rcmail.command);
};
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);
if (this.preview_read_timer)
clearTimeout(this.preview_read_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;
if (this.drag_active && model && this.env.last_folder_target) {
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';
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 (this.env.mailbox && this.env.mailbox.startsWith(name + this.env.delimiter) && !node.virtual)
this.command('list', name);
}
else {
var reg = new RegExp('&'+urlencode(node.id)+'&');
this.env[prefname] = this.env[prefname].replace(reg, '');
}
if (!this.drag_active) {
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;
list = this.message_list || this.contact_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
window.setTimeout(function(e){
var obj, skip, config, id, i;
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)
&& 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);
};
// 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, mod = dir < 0 ? 'prevAll' : 'nextAll', limit = dir < 0 ? 'last' : 'first';
+ var obj, item, mod = dir < 0 ? 'prevAll' : 'nextAll', limit = dir < 0 ? 'last' : 'first';
if (ref.focused_menu && (obj = $('#'+ref.focused_menu))) {
- return obj.find(':focus').closest('li')[mod](':has(:not([aria-disabled=true]))').find('a,input')[limit]().focus().length;
+ 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(mod = keyCode == 38 || keyCode == 63232 ? -1 : 1);
break;
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.click_on_list = function(e)
{
if (this.gui_objects.qsearchbox)
this.gui_objects.qsearchbox.blur();
if (this.message_list)
this.message_list.focus(e);
else if (this.contact_list)
this.contact_list.focus(e);
return true;
};
this.msglist_select = function(list)
{
if (this.preview_timer)
clearTimeout(this.preview_timer);
if (this.preview_read_timer)
clearTimeout(this.preview_read_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.selection.length > 0);
// reset all-pages-selection
if (selected || (list.selection.length && list.selection.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)
this.preview_timer = setTimeout(function() { ref.msglist_get_preview(); }, this.dblclick_time);
else if (this.env.contentframe)
this.show_contentframe(false);
};
// This allow as to re-select selected message and display it in preview frame
this.msglist_click = function(list)
{
if (list.multi_selecting || !this.env.contentframe)
return;
if (list.get_single_selection())
return;
var win = this.get_frame_window(this.env.contentframe);
if (win && win.location.href.indexOf(this.env.blankpage) >= 0) {
if (this.preview_timer)
clearTimeout(this.preview_timer);
if (this.preview_read_timer)
clearTimeout(this.preview_read_timer);
this.preview_timer = setTimeout(function() { ref.msglist_get_preview(); }, this.dblclick_time);
}
};
this.msglist_dbl_click = function(list)
{
if (this.preview_timer)
clearTimeout(this.preview_timer);
if (this.preview_read_timer)
clearTimeout(this.preview_read_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 'settings':
return id != this.env.mailbox ? 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'));
}
// 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
window.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('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;
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?flags.unread_children:0,
parent_uid: flags.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],
row_class = 'message'
+ (!flags.seen ? ' unread' : '')
+ (flags.deleted ? ' deleted' : '')
+ (flags.flagged ? ' flagged' : '')
+ (message.selected ? ' selected' : ''),
row = { cols:[], style:{}, id:'rcmrow'+this.html_identifier(uid,true), 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('replied') + ' ';
}
// 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' + row.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';
}
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
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') {
if (bw.ie)
col.events.mouseover = function() { rcube_webmail.long_subject_title_ex(this); };
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);
}
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);
}
};
this.set_list_sorting = function(sort_col, sort_order)
{
// set table header class
$('#rcm'+this.env.sort_col).removeClass('sorted'+(this.env.sort_order.toUpperCase()));
if (sort_col)
$('#rcm'+sort_col).addClass('sorted'+sort_order);
this.env.sort_col = sort_col;
this.env.sort_order = sort_order;
};
this.set_list_options = function(cols, sort_col, sort_order, threads)
{
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 (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,
action = preview ? 'preview': 'show',
url = '&_action='+action+'&_uid='+id+'&_mbox='+urlencode(this.get_message_mailbox(id));
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;
// add browser capabilities, so we can properly handle attachments
url += '&_caps='+urlencode(this.browser_capabilities());
if (this.env.extwin)
url += '&_extwin=1';
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(this.env.comm_path+url, true);
else
this.location_href(this.env.comm_path+url, target, true);
// mark as read and change mbox unread counter
if (preview && this.message_list && this.message_list.rows[id] && this.message_list.rows[id].unread && this.env.preview_pane_mark_read >= 0) {
this.preview_read_timer = setTimeout(function() {
ref.set_message(id, 'unread', false);
if (ref.env.unread_counts[ref.env.mailbox]) {
ref.env.unread_counts[ref.env.mailbox] -= 1;
ref.set_unread_count(ref.env.mailbox, ref.env.unread_counts[ref.env.mailbox], ref.env.mailbox == 'INBOX');
}
if (ref.env.preview_pane_mark_read > 0)
ref.http_post('mark', {_uid: id, _flag: 'read', _quiet: 1});
}, this.env.preview_pane_mark_read * 1000);
}
}
};
this.show_contentframe = function(show)
{
var frame, win, name = this.env.contentframe;
if (name && (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)
{
var 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', this.search_params(false, filter), lock);
};
// 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;
// also send search request to get the right messages
if (this.env.search_request)
url._search = this.env.search_request;
// set page=1 if changeing to another mailbox
if (this.env.mailbox != mbox) {
page = 1;
this.env.current_page = page;
this.select_all_mode = false;
}
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;
}
// load message list to target frame/window
if (mbox) {
this.set_busy(true, 'loading');
url._mbox = mbox;
if (page)
url._page = page;
this.location_href(url, target);
}
};
this.clear_message_list = function()
{
this.env.messages = {};
- this.last_selected = 0;
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._mbox = mbox;
if (page)
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 selected = this.message_list.selection,
rows = this.message_list.rows,
i, selection = [];
for (i in selected)
if (rows[selected[i]])
selection.push(selected[i]);
this.message_list.selection = selection;
}
// expand all threads with unread children
this.expand_unread = function()
{
var r, tbody = this.gui_objects.messagelist.tBodies[0],
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 mark
row.expanded = !row.expanded;
this.set_unread_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 ? p.unread_children + 1 : 1;
}
else {
return;
}
this.set_message_icon(root);
this.set_unread_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)
return 0;
var r, parent, count = 0,
rows = this.message_list.rows,
row = rows[uid],
depth = rows[uid].depth,
roots = [];
if (!row.depth) // root message: decrease roots count
count--;
else if (row.unread) {
// update unread_children for thread root
parent = this.message_list.find_root(uid);
rows[parent].unread_children--;
this.set_unread_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'))
.bind('mousedown', {uid:r.uid, p:this},
function(e) { return e.data.p.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 for roots
for (var i=0; i<roots.length; i++)
this.set_unread_children(roots[i].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');
row.unread = status;
}
else if(flag == 'deleted')
row.deleted = status;
else if (flag == 'replied')
row.replied = status;
else if (flag == 'forwarded')
row.forwarded = status;
else if (flag == 'flagged')
row.flagged = 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);
var rowobj = $(row.obj);
if (row.unread && !rowobj.hasClass('unread'))
rowobj.addClass('unread');
else if (!row.unread && rowobj.hasClass('unread'))
rowobj.removeClass('unread');
if (row.deleted && !rowobj.hasClass('deleted'))
rowobj.addClass('deleted');
else if (!row.deleted && rowobj.hasClass('deleted'))
rowobj.removeClass('deleted');
if (row.flagged && !rowobj.hasClass('flagged'))
rowobj.addClass('flagged');
else if (!row.flagged && rowobj.hasClass('flagged'))
rowobj.removeClass('flagged');
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;
if (!row.unread && row.unread_children && !row.expanded)
$(row.obj).addClass('unroot');
else
$(row.obj).removeClass('unroot');
};
// copy selected messages to the specified mailbox
this.copy_messages = function(mbox, event)
{
if (mbox && typeof mbox === 'object')
mbox = mbox.id;
else if (!mbox)
return this.folder_selector(event, function(folder) { ref.command('copy', folder); });
// exit if current or no mailbox specified
if (!mbox || mbox == this.env.mailbox)
return;
var post_data = this.selection_post_data({_target_mbox: mbox});
// exit if selection is empty
if (!post_data._uid)
return;
// send request to server
this.http_post('copy', post_data, this.display_message(this.get_label('copyingmessage'), 'loading'));
};
// move selected messages to the specified mailbox
this.move_messages = function(mbox, event)
{
if (mbox && typeof mbox === 'object')
mbox = mbox.id;
else if (!mbox)
return this.folder_selector(event, function(folder) { ref.command('move', folder); });
// 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});
// 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)) {
if (confirm(this.get_label('deletemessagesconfirm')))
this.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
// @private
this._with_selected_messages = function(action, post_data, lock)
{
var count = 0, msg,
remove = (action == 'delete' || !this.is_multifolder_listing());
// update the list (remove rows, clear selection)
if (this.message_list) {
var n, id, root, roots = [],
selection = this.message_list.get_selection();
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(this.get_label(msg), 'loading');
}
// send request to server
this.http_post(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 = {};
data._mbox = this.env.mailbox;
if (!data._uid) {
var uids = this.env.uid ? [this.env.uid] : this.message_list.get_selection();
data._uid = this.uids_to_list(uids);
}
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(this.get_label('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,
post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: flag}),
lock = this.display_message(this.get_label('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));
this.http_post('mark', post_data, lock);
};
// mark all message rows as deleted/undeleted
this.toggle_delete_status = function(a_uids)
{
var len = a_uids.length,
i, uid, all_deleted = true,
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(this.get_label('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(this.get_label('markingmessage'), 'loading'),
rows = this.message_list ? this.message_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);
this.message_list.remove_row(uid, (this.env.display_next && i == this.message_list.selection.length-1));
}
else
this.set_message(uid, 'deleted', true);
}
}
// make sure there are no selected rows
if (this.env.skip_deleted && this.message_list) {
if (!this.env.display_next)
this.message_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();
}
// ??
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 icn_src, uid, i, len,
rows = this.message_list ? this.message_list.rows : {};
if (typeof uids == 'string')
uids = String(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 ? '*' : (uids.length <= 1 ? 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);
};
/*********************************************************/
/********* 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)
{
var lock, post_data = {_mbox: mbox};
if (!confirm(this.get_label('purgefolderconfirm')))
return false;
// lock interface if it's the active mailbox
if (mbox == this.env.mailbox) {
lock = this.set_busy(true, 'loading');
post_data._reload = 1;
}
// send request to server
this.http_post('purge', post_data, lock);
};
// 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)
));
};
/*********************************************************/
/********* 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 input_from = $("[name='_from']"),
input_to = $("[name='_to']"),
input_subject = $("input[name='_subject']"),
input_message = $("[name='_message']").get(0),
html_mode = $("input[name='_is_html']").val() == '1',
ac_fields = ['cc', 'bcc', 'replyto', 'followupto'],
ac_props, 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;
}
// 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 (var i in ac_fields) {
this.init_address_input_events($("[name='_"+ac_fields[i]+"']"), ac_props);
}
if (!html_mode) {
this.set_caret_pos(input_message, this.env.top_posting ? 0 : $(input_message).val().length);
// add signature according to selected identity
// if we have HTML editor, signature is added in callback
if (input_from.prop('type') == 'select-one') {
this.change_identity(input_from[0]);
}
}
// check for locally stored compose data
if (window.localStorage) {
var index = this.local_storage_get_item('compose.index', []);
for (var key, i = 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'),
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'),
click: function(){
ref.remove_compose_data(key);
$(this).dialog('close');
}
},
{
text: this.get_label('ignore'),
click: function(){
$(this).dialog('close');
}
}]
);
break;
}
}
}
if (input_to.val() == '')
input_to.focus();
else if (input_subject.val() == '')
input_subject.focus();
else if (input_message)
input_message.focus();
this.env.compose_focus_elem = document.activeElement;
// get summary of all field values
this.compose_field_hash(true);
// start the auto-save timer
this.auto_save_start();
};
this.init_address_input_events = function(obj, props)
{
this.env.recipients_delimiter = this.env.recipients_separator + ' ';
obj.keydown(function(e) { return ref.ksearch_keydown(e, this, props); })
.attr('autocomplete', 'off')
.attr('aria-autocomplete', 'list')
.attr('aria-expanded', 'false');
};
this.submit_messageform = function(draft)
{
var form = this.gui_objects.messageform;
if (!form)
return;
// all checks passed, send message
var msgid = this.set_busy(true, draft ? 'savingmessage' : 'sendingmessage'),
lang = this.spellcheck_lang(),
files = [];
// send files list
$('li', this.gui_objects.attachmentlist).each(function() { files.push(this.id.replace(/^rcmfile/, '')); });
$('input[name="_attachments"]', form).val(files.join());
form.target = 'savetarget';
form._draft.value = draft ? '1' : '';
form.action = this.add_url(form.action, '_unlock', msgid);
form.action = this.add_url(form.action, '_lang', lang);
// register timer to notify about connection timeout
this.submit_timer = setTimeout(function(){
ref.set_busy(false, null, msgid);
ref.display_message(ref.get_label('requesttimedout'), 'error');
}, this.env.request_timeout * 1000);
form.submit();
};
this.compose_recipient_select = function(list)
{
var id, n, recipients = 0;
for (n=0; n < list.selection.length; n++) {
id = list.selection[n];
if (this.env.contactdata[id])
recipients++;
}
this.enable_command('add-recipient', recipients);
};
this.compose_add_recipient = function(field)
{
var recipients = [], input = $('#_'+field), delim = this.env.recipients_delimiter;
if (this.contact_list && this.contact_list.selection.length) {
for (var id, n=0; n < this.contact_list.selection.length; n++) {
id = this.contact_list.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(), rx = new RegExp(RegExp.escape(delim) + '\\s*$');
if (oldval && !rx.test(oldval))
oldval += delim + ' ';
input.val(oldval + recipients.join(delim + ' ') + delim + ' ');
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)
{
// check input fields
var ed, input_to = $("[name='_to']"),
input_cc = $("[name='_cc']"),
input_bcc = $("[name='_bcc']"),
input_from = $("[name='_from']"),
input_subject = $("[name='_subject']"),
input_message = $("[name='_message']");
// check sender (if have no identities)
if (input_from.prop('type') == 'text' && !rcube_check_email(input_from.val(), true)) {
alert(this.get_label('nosenderwarning'));
input_from.focus();
return false;
}
// check for empty recipient
var recipients = input_to.val() ? input_to.val() : (input_cc.val() ? input_cc.val() : input_bcc.val());
if (!rcube_check_email(recipients.replace(/^\s+/, '').replace(/[\s,;]+$/, ''), true)) {
alert(this.get_label('norecipientwarning'));
input_to.focus();
return false;
}
// check if all files has been uploaded
for (var key in this.env.attachments) {
if (typeof this.env.attachments[key] === 'object' && !this.env.attachments[key].complete) {
alert(this.get_label('notuploadedwarning'));
return false;
}
}
// display localized warning for missing subject
if (input_subject.val() == '') {
var myprompt = $('<div class="prompt">').html('<div class="message">' + this.get_label('nosubjectwarning') + '</div>').appendTo(document.body);
var prompt_value = $('<input>').attr('type', 'text').attr('size', 30).appendTo(myprompt).val(this.get_label('nosubject'));
var buttons = {};
buttons[this.get_label('cancel')] = function(){
input_subject.focus();
$(this).dialog('close');
};
buttons[this.get_label('sendmessage')] = function(){
input_subject.val(prompt_value.val());
$(this).dialog('close');
ref.command(cmd, { nocheck:true }); // repeat command which triggered this
};
myprompt.dialog({
modal: true,
resizable: false,
buttons: buttons,
close: function(event, ui) { $(this).remove() }
});
prompt_value.select();
return false;
}
// Apply spellcheck changes if spell checker is active
this.stop_spellchecking();
if (window.tinyMCE)
ed = tinyMCE.get(this.env.composebody);
// check for empty body
if (!ed && input_message.val() == '' && !confirm(this.get_label('nobodywarning'))) {
input_message.focus();
return false;
}
else if (ed) {
if (!ed.getContent() && !confirm(this.get_label('nobodywarning'))) {
ed.focus();
return false;
}
// move body from html editor to textarea (just to be sure, #1485860)
tinyMCE.triggerSave();
}
return true;
};
this.toggle_editor = function(props)
{
this.stop_spellchecking();
if (props.mode == 'html') {
this.plain2html($('#'+props.id).val(), props.id);
tinyMCE.execCommand('mceAddControl', false, props.id);
if (this.env.default_font)
setTimeout(function() {
$(tinyMCE.get(props.id).getBody()).css('font-family', rcmail.env.default_font);
}, 500);
}
else {
var thisMCE = tinyMCE.get(props.id), existingHtml;
if (existingHtml = thisMCE.getContent()) {
if (!confirm(this.get_label('editorwarning'))) {
return false;
}
this.html2plain(existingHtml, props.id);
}
tinyMCE.execCommand('mceRemoveControl', false, props.id);
}
return true;
};
this.insert_response = function(key)
{
var insert = this.env.textresponses[key] ? this.env.textresponses[key].text : null;
if (!insert)
return false;
// insert into tinyMCE editor
if ($("input[name='_is_html']").val() == '1') {
var editor = tinyMCE.get(this.env.composebody);
editor.getWin().focus(); // correct focus in IE & Chrome
editor.selection.setContent(this.quote_html(insert).replace(/\r?\n/g, '<br/>'), { format:'text' });
}
// replace selection in compose textarea
else {
var textarea = rcube_find_object(this.env.composebody),
selection = $(textarea).is(':focus') ? this.get_input_selection(textarea) : { start:0, end:0 },
inp_value = textarea.value;
pre = inp_value.substring(0, selection.start),
end = inp_value.substring(selection.end, inp_value.length);
// insert response text
textarea.value = pre + insert + end;
// set caret after inserted text
this.set_caret_pos(textarea, selection.start + insert.length);
textarea.focus();
}
};
/**
* Open the dialog to save a new canned response
*/
this.save_response = function()
{
var sigstart, text = '', strip = false;
// get selected text from tinyMCE editor
if ($("input[name='_is_html']").val() == '1') {
var editor = tinyMCE.get(this.env.composebody);
editor.getWin().focus(); // correct focus in IE & Chrome
text = editor.selection.getContent({ format:'text' });
if (!text) {
text = editor.getContent({ format:'text' });
strip = true;
}
}
// get selected text from compose textarea
else {
var textarea = rcube_find_object(this.env.composebody), sigstart;
if (textarea && $(textarea).is(':focus')) {
text = this.get_input_selection(textarea).text;
}
if (!text && textarea) {
text = textarea.value;
strip = true;
}
}
// strip off signature
if (strip) {
sigstart = text.indexOf('-- \n');
if (sigstart > 0) {
text = text.substring(0, sigstart);
}
}
// show dialog to enter a name and to modify the text to be saved
var buttons = {},
html = '<form class="propform">' +
'<div class="prop block"><label>' + this.get_label('responsename') + '</label>' +
'<input type="text" name="name" id="ffresponsename" size="40" /></div>' +
'<div class="prop block"><label>' + this.get_label('responsetext') + '</label>' +
'<textarea name="text" id="ffresponsetext" cols="40" rows="8"></textarea></div>' +
'</form>';
buttons[this.gettext('save')] = function(e) {
var name = $('#ffresponsename').val(),
text = $('#ffresponsetext').val();
if (!text) {
$('#ffresponsetext').select();
return false;
}
if (!name)
name = text.substring(0,40);
var lock = ref.display_message(ref.get_label('savingresponse'), 'loading');
ref.http_post('settings/responses', { _insert:1, _name:name, _text:text }, lock);
$(this).dialog('close');
};
buttons[this.gettext('cancel')] = function() {
$(this).dialog('close');
};
this.show_popup_dialog(html, this.gettext('savenewresponse'), buttons);
$('#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);
})
.bind('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 && confirm(this.get_label('deleteresponseconfirm'))) {
this.http_post('settings/delete-response', { _key: key }, false);
return true;
}
return false;
};
this.stop_spellchecking = function()
{
var ed;
if (window.tinyMCE && (ed = tinyMCE.get(this.env.composebody))) {
if (ed.plugins && ed.plugins.spellchecker && ed.plugins.spellchecker.active)
ed.execCommand('mceSpellCheck');
}
else if (ed = this.env.spellcheck) {
if (ed.state && ed.state != 'ready' && ed.state != 'no_error_found')
$(ed.spell_span).trigger('click');
}
this.spellcheck_state();
};
this.spellcheck_state = function()
{
var ed, active;
if (window.tinyMCE && (ed = tinyMCE.get(this.env.composebody)) && ed.plugins && ed.plugins.spellchecker)
active = ed.plugins.spellchecker.active;
else if ((ed = this.env.spellcheck) && ed.state)
active = ed.state != 'ready' && ed.state != 'no_error_found';
if (rcmail.buttons.spellcheck)
$('#'+rcmail.buttons.spellcheck[0].id)[active ? 'addClass' : 'removeClass']('selected');
return active;
};
// get selected language
this.spellcheck_lang = function()
{
var ed;
if (window.tinyMCE && (ed = tinyMCE.get(this.env.composebody)) && ed.plugins && ed.plugins.spellchecker)
return ed.plugins.spellchecker.selectedLang;
else if (this.env.spellcheck)
return GOOGIE_CUR_LANG;
};
this.spellcheck_lang_set = function(lang)
{
var ed;
if (window.tinyMCE && (ed = tinyMCE.get(this.env.composebody)) && ed.plugins)
ed.plugins.spellchecker.selectedLang = lang;
else if (this.env.spellcheck)
this.env.spellcheck.setCurrentLanguage(lang);
};
// resume spellchecking, highlight provided mispellings without new ajax request
this.spellcheck_resume = function(ishtml, data)
{
if (ishtml) {
var ed = tinyMCE.get(this.env.composebody);
sp = ed.plugins.spellchecker;
sp.active = 1;
sp._markWords(data);
ed.nodeChanged();
}
else {
var sp = this.env.spellcheck;
sp.prepare(false, true);
sp.processData(data);
}
this.spellcheck_state();
}
this.set_draft_id = function(id)
{
var rc;
if (id && id != this.env.draft_id) {
if (rc = this.opener()) {
// refresh the drafts folder in opener window
if (rc.env.task == 'mail' && rc.env.action == '' && rc.env.mailbox == this.env.drafts_mailbox)
rc.command('checkmail');
}
this.env.draft_id = id;
$("input[name='_draft_saveid']").val(id);
// reset history of hidden iframe used for saving draft (#1489643)
// but don't do this on timer-triggered draft-autosaving (#1489789)
if (window.frames['savetarget'] && window.frames['savetarget'].history && !this.draft_autosave_submit) {
window.frames['savetarget'].history.back();
}
this.draft_autosave_submit = false;
}
// always remove local copy upon saving as draft
this.remove_compose_data(this.env.compose_id);
};
this.auto_save_start = function()
{
if (this.env.draft_autosave)
this.draft_autosave_submit = false;
this.save_timer = setTimeout(function(){
ref.draft_autosave_submit = true; // set auto-saved flag (#1489789)
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) {
// track typing activity and only save on changes
this.compose_type_activity = this.compose_type_activity_last = 0;
$(document).bind('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);
}
// Unlock interface now that saving is complete
this.busy = false;
};
this.compose_field_hash = function(save)
{
// check input fields
var ed, i, 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 + ':';
if (window.tinyMCE && (ed = tinyMCE.get(this.env.composebody)))
str += ed.getContent();
else
str += $("[name='_message']").val();
if (this.env.attachments)
for (var upload_id in this.env.attachments)
str += upload_id;
if (save)
this.cmp_hash = str;
return str;
};
// store the contents of the compose form to localstorage
this.save_compose_form_local = function()
{
var formdata = { session:this.env.session_id, changed:new Date().getTime() },
ed, empty = true;
// get fresh content from editor
if (window.tinyMCE && (ed = tinyMCE.get(this.env.composebody))) {
tinyMCE.triggerSave();
}
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 (window.localStorage && !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') {
if (!html_mode) {
tinyMCE.execCommand('mceAddControl', false, this.env.composebody);
this.triggerEvent('aftertoggle-editor', { mode:'html' });
}
}
else if (html_mode) {
tinyMCE.execCommand('mceRemoveControl', false, this.env.composebody);
this.triggerEvent('aftertoggle-editor', { mode:'plain' });
}
}
};
// remove stored compose data from localStorage
this.remove_compose_data = function(key)
{
if (window.localStorage) {
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()
{
if (window.localStorage) {
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_sig)
{
if (!obj || !obj.options)
return false;
if (!show_sig)
show_sig = this.env.show_sig;
// 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;
}
var i, rx, cursor_pos, p = -1,
id = obj.options[obj.selectedIndex].value,
input_message = $("[name='_message']"),
message = input_message.val(),
is_html = ($("input[name='_is_html']").val() == '1'),
sig = this.env.identity,
delim = this.env.recipients_separator,
rx_delim = RegExp.escape(delim),
headers = ['replyto', 'bcc'];
// update reply-to/bcc fields with addresses defined in identities
for (i in headers) {
var key = headers[i],
old_val = sig && this.env.identities[sig] ? this.env.identities[sig][key] : '',
new_val = id && this.env.identities[id] ? this.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
rx = new RegExp(rx_delim + '\\s*' + rx_delim, 'g');
input_val = String(input_val).replace(rx, delim);
rx = new RegExp('^[\\s' + rx_delim + ']+');
input_val = input_val.replace(rx, '');
// add new address(es)
if (new_val && input_val.indexOf(new_val) == -1 && input_val.indexOf(new_val.replace(/"/g, '')) == -1) {
if (input_val) {
rx = new RegExp('[' + rx_delim + '\\s]+$')
input_val = input_val.replace(rx, '') + delim + ' ';
}
input_val += new_val + delim + ' ';
}
if (old_val || new_val)
input.val(input_val).change();
}
// enable manual signature insert
if (this.env.signatures && this.env.signatures[id]) {
this.enable_command('insert-sig', true);
this.env.compose_commands.push('insert-sig');
}
else
this.enable_command('insert-sig', false);
if (!is_html) {
// remove the 'old' signature
if (show_sig && sig && this.env.signatures && this.env.signatures[sig]) {
sig = this.env.signatures[sig].text;
sig = sig.replace(/\r\n/g, '\n');
p = this.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 && this.env.signatures && this.env.signatures[id]) {
sig = this.env.signatures[id].text;
sig = sig.replace(/\r\n/g, '\n');
if (this.env.top_posting) {
if (p >= 0) { // in place of removed signature
message = message.substring(0, p) + sig + message.substring(p, message.length);
cursor_pos = p - 1;
}
else if (!message) { // empty message
cursor_pos = 0;
message = '\n\n' + sig;
}
else if (pos = this.get_caret_pos(input_message.get(0))) { // at cursor position
message = message.substring(0, pos) + '\n' + sig + '\n\n' + message.substring(pos, message.length);
cursor_pos = pos;
}
else { // on top
cursor_pos = 0;
message = '\n\n' + sig + '\n\n' + message.replace(/^[\r\n]+/, '');
}
}
else {
message = message.replace(/[\r\n]+$/, '');
cursor_pos = !this.env.top_posting && message.length ? message.length+1 : 0;
message += '\n\n' + sig;
}
}
else
cursor_pos = this.env.top_posting ? 0 : message.length;
input_message.val(message);
// move cursor before the signature
this.set_caret_pos(input_message.get(0), cursor_pos);
}
else if (show_sig && this.env.signatures) { // html
var editor = tinyMCE.get(this.env.composebody),
sigElem = editor.dom.get('_rc_sig');
// Append the signature as a div within the body
if (!sigElem) {
var body = editor.getBody(),
doc = editor.getDoc();
sigElem = doc.createElement('div');
sigElem.setAttribute('id', '_rc_sig');
if (this.env.top_posting) {
// if no existing sig and top posting then insert at caret pos
editor.getWin().focus(); // correct focus in IE & Chrome
var node = editor.selection.getNode();
if (node.nodeName == 'BODY') {
// no real focus, insert at start
body.insertBefore(sigElem, body.firstChild);
body.insertBefore(doc.createElement('br'), body.firstChild);
}
else {
body.insertBefore(sigElem, node.nextSibling);
body.insertBefore(doc.createElement('br'), node.nextSibling);
}
}
else {
if (bw.ie) // add empty line before signature on IE
body.appendChild(doc.createElement('br'));
body.appendChild(sigElem);
}
}
if (this.env.signatures[id])
sigElem.innerHTML = this.env.signatures[id].html;
}
this.env.identity = id;
this.triggerEvent('change_identity');
return true;
};
// upload (attachment) file
this.upload_file = function(form, action)
{
if (!form)
return;
// count files and size on capable browser
var size = 0, numfiles = 0;
$('input[type=file]', form).each(function(i, field) {
var files = field.files ? field.files.length : (field.value ? 1 : 0);
// check file size
if (field.files) {
for (var i=0; i < files; i++)
size += field.files[i].size;
}
numfiles += files;
});
// create hidden iframe and post upload form
if (numfiles) {
if (this.env.max_filesize && this.env.filesizeerror && size > this.env.max_filesize) {
this.display_message(this.env.filesizeerror, 'error');
return false;
}
var frame_name = this.async_upload_form(form, action || 'upload', function(e) {
var d, content = '';
try {
if (this.contentDocument) {
d = this.contentDocument;
} else if (this.contentWindow) {
d = this.contentWindow.document;
}
content = d.childNodes[0].innerHTML;
} catch (err) {}
if (!content.match(/add2attachment/) && (!bw.opera || (rcmail.env.uploadframe && rcmail.env.uploadframe == e.data.ts))) {
if (!content.match(/display_message/))
rcmail.display_message(rcmail.get_label('fileuploaderror'), 'error');
rcmail.remove_from_attachment_list(e.data.ts);
}
// Opera hack: handle double onload
if (bw.opera)
rcmail.env.uploadframe = e.data.ts;
});
// display upload indicator and cancel button
var content = '<span>' + this.get_label('uploading' + (numfiles > 1 ? 'many' : '')) + '</span>',
ts = frame_name.replace(/^rcmupload/, '');
this.add2attachment_list(ts, { name:'', html:content, classname:'uploading', frame:frame_name, complete:false });
// upload progress support
if (this.env.upload_progress_time) {
this.upload_progress_start('upload', ts);
}
// set reference to the form object
this.gui_objects.attachmentform = form;
return true;
}
};
// add file name to attachment list
// called from upload page
this.add2attachment_list = function(name, att, upload_id)
{
if (!this.gui_objects.attachmentlist)
return false;
if (!att.complete && ref.env.loadingicon)
att.html = '<img src="'+ref.env.loadingicon+'" alt="" class="uploading" />' + att.html;
if (!att.complete && att.frame)
att.html = '<a title="'+this.get_label('cancel')+'" onclick="return rcmail.cancel_attachment_upload(\''+name+'\', \''+att.frame+'\');" href="#cancelupload" class="cancelupload">'
+ (this.env.cancelicon ? '<img src="'+this.env.cancelicon+'" alt="" />' : this.get_label('cancel')) + '</a>' + att.html;
var indicator, li = $('<li>');
li.attr('id', name)
.addClass(att.classname)
.html(att.html)
.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);
}
if (upload_id && this.env.attachments[upload_id])
delete this.env.attachments[upload_id];
this.env.attachments[name] = att;
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 true;
};
this.cancel_attachment_upload = function(name, frame_name)
{
if (!name || !frame_name)
return false;
this.remove_from_attachment_list(name);
$("iframe[name='"+frame_name+"']").remove();
return false;
};
this.upload_progress_start = function(action, name)
{
setTimeout(function() { rcmail.http_request(action, {_progress: name}); },
this.env.upload_progress_time * 1000);
};
this.upload_progress_update = function(param)
{
var elem = $('#'+param.name + '> span');
if (!elem.length || !param.text)
return;
elem.text(param.text);
if (!param.done)
this.upload_progress_start(param.action, param.name);
};
// send remote request to add a new contact
this.add_contact = function(value)
{
if (value)
this.http_post('addcontact', {_address: value});
return true;
};
// send remote request to search mail or contacts
this.qsearch = function(value)
{
if (value != '') {
var r, lock = this.set_busy(true, 'searching'),
url = this.search_params(value);
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;
var action = this.env.action == 'compose' && this.contact_list ? 'search-contacts' : 'search';
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 = ref.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, smods)
{
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 (filter)
url._filter = filter;
if (search) {
url._q = search;
if (!smods && mods && this.message_list)
smods = mods[mbox] || mods['*'];
if (smods) {
for (n in smods)
mods_arr.push(n);
url._headers = mods_arr.join(',');
}
}
if (scope)
url._scope = scope;
if (mbox && scope != 'all')
url._mbox = mbox;
return url;
};
// reset quick-search form
this.reset_qsearch = function()
{
if (this.gui_objects.qsearchbox)
this.gui_objects.qsearchbox.value = '';
if (this.env.qsearch)
this.abort_request(this.env.qsearch);
this.env.qsearch = null;
this.env.search_request = null;
this.env.search_id = null;
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_searchmods = function(mods)
{
var mbox = rcmail.env.mailbox,
scope = this.env.search_scope || 'base';
if (scope == 'all')
mbox = '*';
if (!this.env.search_mods)
this.env.search_mods = {};
this.env.search_mods[mbox] = mods;
};
this.is_multifolder_listing = function()
{
return typeof this.env.multifolder_listing != 'undefined' ? this.env.multifolder_listing :
(this.env.search_request && (this.env.search_scope || 'base') != 'base');
}
this.sent_successfully = function(type, msg, folders)
{
this.display_message(msg, type);
if (this.env.extwin) {
var rc = this.opener();
this.lock_form(this.gui_objects.messageform);
if (rc) {
rc.display_message(msg, type);
// refresh the folder where sent message was saved or replied message comes from
if (folders && rc.env.task == 'mail' && rc.env.action == '' && $.inArray(rc.env.mailbox, folders) >= 0) {
// @TODO: try with 'checkmail' here when #1485186 is fixed. See also #1489249.
rc.command('list');
}
}
setTimeout(function(){ window.close() }, 1000);
}
else {
// before redirect we need to wait some time for Chrome (#1486177)
setTimeout(function(){ ref.list_mailbox(); }, 500);
}
};
/*********************************************************/
/********* 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 highlight,
key = rcube_event.get_keycode(e),
mod = rcube_event.get_modifier(e);
switch (key) {
case 38: // arrow up
case 40: // arrow down
if (!this.ksearch_visible())
return;
var dir = key==38 ? 1 : 0;
highlight = document.getElementById('rcmkSearchItem' + this.ksearch_selected);
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 (mod == 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();
return 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').removeAttr('aria-selected', 'true');
this.ksearch_selected = node._rcm_id;
$(this.ksearch_input).attr('aria-activedecendant', 'rcmkSearchItem' + this.ksearch_selected);
}
};
this.insert_recipient = function(id)
{
if (id === null || !this.env.contacts[id] || !this.ksearch_input)
return;
// get cursor pos
var inp_value = this.ksearch_input.value,
cpos = this.get_caret_pos(this.ksearch_input),
p = inp_value.lastIndexOf(this.ksearch_value, cpos),
trigger = false,
insert = '',
// replace search string with full address
pre = inp_value.substring(0, p),
end = inp_value.substring(p+this.ksearch_value.length, inp_value.length);
this.ksearch_destroy();
// insert all members of a group
if (typeof this.env.contacts[id] === 'object' && this.env.contacts[id].type == 'group') {
insert += this.env.contacts[id].name + this.env.recipients_delimiter;
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 + this.env.recipients_delimiter;
trigger = true;
}
else if (typeof this.env.contacts[id] === 'string') {
insert = this.env.contacts[id] + this.env.recipients_delimiter;
trigger = true;
}
this.ksearch_input.value = pre + insert + end;
// set caret to insert pos
cpos = p+insert.length;
if (this.ksearch_input.setSelectionRange)
this.ksearch_input.setSelectionRange(cpos, cpos);
if (trigger) {
this.triggerEvent('autocomplete_insert', { field:this.ksearch_input, insert:insert, data:this.env.contacts[id] });
this.compose_type_activity++;
}
};
this.replace_group_recipients = function(id, recipients)
{
if (this.group2expand[id]) {
this.group2expand[id].input.value = this.group2expand[id].input.value.replace(this.group2expand[id].name, recipients);
this.triggerEvent('autocomplete_insert', { field:this.group2expand[id].input, insert:recipients });
this.group2expand[id] = null;
this.compose_type_activity++;
}
};
// address search processor
this.ksearch_get_results = function(props)
{
var inp_value = this.ksearch_input ? this.ksearch_input.value : null;
if (inp_value === null)
return;
if (this.ksearch_pane && this.ksearch_pane.is(":visible"))
this.ksearch_pane.hide();
// get string from current cursor pos to last comma
var cpos = this.get_caret_pos(this.ksearch_input),
p = inp_value.lastIndexOf(this.env.recipients_separator, cpos-1),
q = inp_value.substring(p+1, cpos),
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;
// ...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(this.get_label('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,
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').attr('role', 'listbox')
.css({ position:'absolute', 'z-index':30000 }).append(ul).appendTo(document.body);
this.ksearch_pane.__ul = ul[0];
// register (delegate) event handlers
ul.on('mouseover', 'li', function(e){ ref.ksearch_select(e.target); })
.on('onmouseup', 'li', function(e){ ref.ksearch_click(e.target); })
}
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 = [];
// move the results pane right under the input box
var pos = $(this.ksearch_input).offset();
this.ksearch_pane.css({ left:pos.left+'px', top:(pos.top + this.ksearch_input.offsetHeight)+'px', 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].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(this.quote_html(text.replace(new RegExp('('+RegExp.escape(value)+')', 'ig'), '##$1%%')).replace(/##([^%]+)%%/g, '<b>$1</b>'))
.addClass(type || '')
.appendTo(ul)
.get(0)._rcm_id = id;
maxlen -= 1;
}
}
if (ul.childNodes.length) {
this.ksearch_pane.show();
// select the first
if (!this.env.contacts.length) {
this.ksearch_select($('li:first', ul).get(0));
}
// set the right aria-* attributes to the input field
$(this.ksearch_input)
.attr('aria-haspopup', 'true')
.attr('aria-expanded', 'true')
.attr('aria-owns', 'rcmKSearchpane')
}
if (len)
this.env.contacts = this.env.contacts.concat(results);
if (this.ksearch_data.id == reqid)
this.ksearch_data.num--;
};
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-activedecendant')
.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 n, id, sid, contact, writable = false,
source = this.env.source ? this.env.address_sources[this.env.source] : null;
// we don't have dblclick handler here, so use 200 instead of this.dblclick_time
if (id = list.get_single_selection())
this.preview_timer = setTimeout(function(){ ref.load_contact(id, 'show'); }, 200);
else if (this.env.contentframe)
this.show_contentframe(false);
if (list.selection.length) {
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);
}
for (n in list.selection) {
contact = list.data[list.selection[n]];
if (!source) {
sid = String(list.selection[n]).replace(/^[^-]+-/, '');
if (sid && this.env.address_sources[sid]) {
writable = writable || (!this.env.address_sources[sid].readonly && !contact.readonly);
this.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 a group is currently selected, and there is at least one contact selected
// thend we can enable the group-remove-selected command
this.enable_command('group-remove-selected', this.env.group && list.selection.length > 0 && writable);
this.enable_command('compose', this.env.group || list.selection.length > 0);
this.enable_command('export-selected', 'copy', list.selection.length > 0);
this.enable_command('edit', id && writable);
this.enable_command('delete', 'move', list.selection.length > 0 && writable);
return false;
};
this.list_contacts = function(src, group, page)
{
var win, folder, url = {},
target = window;
if (!src)
src = this.env.source;
if (page && this.current_page == page && src == this.env.source && group == this.env.group)
return false;
if (src != this.env.source) {
page = this.env.current_page = 1;
this.reset_qsearch();
}
else if (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
var index = $.inArray(this.env.group, this.env.address_group_stack);
if (index < 0)
this.env.address_group_stack = [];
else
this.env.address_group_stack = this.env.address_group_stack.slice(0,index);
// make sure the current group is on top of the stack
if (this.env.group) {
this.env.address_group_stack.push(this.env.group);
// mark the first group on the stack as selected in the directory list
folder = 'G'+src+this.env.address_group_stack[0];
}
else if (this.gui_objects.addresslist_title) {
$(this.gui_objects.addresslist_title).html(this.get_label('contacts'));
}
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);
};
this.list_contacts_clear = function()
{
this.contact_list.data = {};
this.contact_list.clear(true);
this.show_contentframe(false);
this.enable_command('delete', 'move', 'copy', false);
this.enable_command('compose', this.env.group ? true : 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) {
$('<a href="#list">...</a>')
.addClass('poplink')
.appendTo(boxtitle)
.click(function(e){ return ref.command('popgroup','',this); });
boxtitle.append('&nbsp;&raquo;&nbsp;');
}
boxtitle.append($('<span>').text(prop.name));
}
this.triggerEvent('groupupdate', prop);
};
// 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('compose', rec && rec.email);
this.enable_command('export-selected', 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;
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)
{
what = what == 'add' ? 'add' : 'del';
var label = this.get_label(what == 'add' ? 'addingmember' : 'removingmember'),
lock = this.display_message(label, '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)
{
var n, dest = to.type == 'group' ? to.source : to.id,
source = this.env.source,
group = this.env.group ? this.env.group : '',
cid = 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(this.get_label('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(this.get_label('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)
{
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});
}
// target is an addressbook
else if (to.id != source)
this._with_selected_contacts('move', {_to: to.id});
};
// delete contact(s)
this.delete_contacts = function()
{
var undelete = this.env.source && this.env.address_sources[this.env.source].undelete;
if (!undelete && !confirm(this.get_label('deletecontactconfirm')))
return;
return this._with_selected_contacts('delete');
};
this._with_selected_contacts = function(action, post_data)
{
var selection = this.contact_list ? this.contact_list.get_selection() : [];
// exit if no mailbox 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(this.get_label(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 c, row, 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(e) {
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: '-100:+10',
showOtherMonths: true,
selectOtherMonths: true,
onSelect: function(dateText) { $(this).focus().val(dateText) }
});
$('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; });
};
this.group_create = function()
{
this.add_input_row('contactgroup');
};
this.group_rename = function()
{
if (!this.env.group || !this.gui_objects.folderlist)
return;
if (!this.name_input) {
this.enable_command('list', 'listgroup', false);
this.name_input = $('<input>').attr('type', 'text').val(this.env.contactgroups['G'+this.env.source+this.env.group].name);
this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); });
this.env.group_renaming = true;
var link, li = this.get_folder_li('G'+this.env.source+this.env.group,'',true);
if (li && (link = li.firstChild)) {
$(link).hide().before(this.name_input);
}
}
this.name_input.select().focus();
};
this.group_delete = function()
{
if (this.env.group && confirm(this.get_label('deletegroupconfirm'))) {
var lock = this.set_busy(true, 'groupdeleting');
this.http_post('group-delete', {_source: this.env.source, _gid: this.env.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)) {
this.triggerEvent('group_delete', { source:prop.source, id:prop.id });
delete this.env.contactfolders[key];
delete this.env.contactgroups[key];
}
this.list_contacts(prop.source, 0);
};
// @TODO: maybe it would be better to use popup instead of inserting input to the list?
this.add_input_row = function(type)
{
if (!this.gui_objects.folderlist)
return;
if (!this.name_input) {
this.name_input = $('<input>').attr('type', 'text').data('tt', type);
this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); });
this.name_input_li = $('<li>').addClass(type).append(this.name_input);
var ul, li;
// find list (UL) element
if (type == 'contactsearch')
ul = this.gui_objects.folderlist;
else
ul = $('ul.groups', this.get_folder_li(this.env.source,'',true));
// append to the list
li = $('li:last', ul);
if (li.length)
this.name_input_li.insertAfter(li);
else {
this.name_input_li.appendTo(ul);
ul.show(); // make sure the list is visible
}
}
this.name_input.select().focus();
};
//remove selected contacts from current active group
this.group_remove_selected = function()
{
ref.http_post('group-delmembers', {_cid: this.contact_list.selection,
_source: this.env.source, _gid: this.env.group});
};
//callback after deleting contact(s) from current group
this.remove_group_contacts = function(props)
{
if('undefined' != typeof this.env.group && (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));
}
}
}
// handler for keyboard events on the input field
this.add_input_keydown = function(e)
{
var key = rcube_event.get_keycode(e),
input = $(e.target), itype = input.data('tt');
// enter
if (key == 13) {
var newname = input.val();
if (newname) {
var lock = this.set_busy(true, 'loading');
if (itype == 'contactsearch')
this.http_post('search-create', {_search: this.env.search_request, _name: newname}, lock);
else if (this.env.group_renaming)
this.http_post('group-rename', {_source: this.env.source, _gid: this.env.group, _name: newname}, lock);
else
this.http_post('group-create', {_source: this.env.source, _name: newname}, lock);
}
return false;
}
// escape
else if (key == 27)
this.reset_add_input();
return true;
};
this.reset_add_input = function()
{
if (this.name_input) {
var li = this.name_input.parent();
if (this.env.group_renaming) {
li.children().last().show();
this.env.group_renaming = false;
}
else if ($('li', li.parent()).length == 1)
li.parent().hide();
this.name_input.remove();
if (this.name_input_li)
this.name_input_li.remove();
this.name_input = this.name_input_li = null;
}
this.enable_command('list', 'listgroup', true);
};
// callback for creating a new contact group
this.insert_contact_group = function(prop)
{
this.reset_add_input();
prop.type = 'group';
var key = 'G'+prop.source+prop.id,
link = $('<a>').attr('href', '#')
.attr('rel', prop.source+':'+prop.id)
.click(function() { return rcmail.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, true);
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)
{
this.reset_add_input();
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 rcmail.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;
}
// update list node and re-sort it
this.treelist.update(key, newnode, true);
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;
this.enable_command('group-create', (source && source.groups && !source.readonly));
this.enable_command('group-rename', 'group-delete', (source && source.groups && this.env.group && !source.readonly));
};
this.init_edit_field = function(col, elem)
{
var label = this.env.coltypes[col].label;
if (!elem)
elem = $('.ff_' + col);
if (label)
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) {
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.length && appendcontainer.get(0).nodeName == 'FIELDSET') {
var input, colprop = this.env.coltypes[col],
row = $('<div>').addClass('row'),
cell = $('<div>').addClass('contactfieldcontent data'),
label = $('<div>').addClass('contactfieldlabel label');
if (colprop.subtypes_select)
label.html(colprop.subtypes_select);
else
label.html(colprop.label);
var name_suffix = colprop.limit != 1 ? '[]' : '';
if (colprop.type == 'text' || colprop.type == 'date') {
input = $('<input>')
.addClass('ff_'+col)
.attr({type: 'text', name: '_'+col+name_suffix, size: colprop.size})
.appendTo(cell);
this.init_edit_field(col, input);
if (colprop.type == 'date' && $.datepicker)
input.datepicker();
}
else if (colprop.type == 'textarea') {
input = $('<textarea>')
.addClass('ff_'+col)
.attr({ name: '_'+col+name_suffix, cols:colprop.size, rows:colprop.rows })
.appendTo(cell);
this.init_edit_field(col, input);
}
else if (colprop.type == 'composite') {
var childcol, cp, first, templ, cols = [], suffices = [];
// read template for composite field order
if ((templ = this.env[col+'_template'])) {
for (var j=0; j < templ.length; j++) {
cols.push(templ[j][1]);
suffices.push(templ[j][2]);
}
}
else { // list fields according to appearance in colprop
for (childcol in colprop.childs)
cols.push(childcol);
}
for (var i=0; i < cols.length; i++) {
childcol = cols[i];
cp = colprop.childs[childcol];
input = $('<input>')
.addClass('ff_'+childcol)
.attr({ type: 'text', name: '_'+childcol+name_suffix, size: cp.size })
.appendTo(cell);
cell.append(suffices[i] || " ");
this.init_edit_field(childcol, input);
if (!first) first = input;
}
input = first; // set focus to the first of this composite fields
}
else if (colprop.type == 'select') {
input = $('<select>')
.addClass('ff_'+col)
.attr('name', '_'+col+name_suffix)
.appendTo(cell);
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')
.attr({title: this.get_label('delete'), rel: col})
.html(this.env.delbutton)
.click(function(){ ref.delete_edit_field(this); return false })
.appendTo(cell);
row.append(label).append(cell).appendTo(appendcontainer.show());
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.delete_edit_field = function(elem)
{
var col = $(elem).attr('rel'),
colprop = this.env.coltypes[col],
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)
$(elem).parent().children('input').val('').blur();
else {
$(elem).parents('div.row').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) {
rcmail.set_busy(false, null, rcmail.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 win, url = {_form: 1, _action: 'search'}, target = window;
if (win = this.get_frame_window(this.env.contentframe)) {
url._framed = 1;
target = win;
this.contact_list.clear_selection();
}
this.location_href(url, target, true);
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)
{
this.reset_add_input();
var key = 'S'+id,
link = $('<a>').attr('href', '#')
.attr('rel', id)
.click(function() { return rcmail.command('listsearch', id, this); })
.html(name),
prop = { name:name, id:id };
this.treelist.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 an input for saved search name
this.search_create = function()
{
this.add_input_row('contactsearch');
};
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.treelist.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 folder, lock = this.set_busy(true, 'searching');
if (this.contact_list) {
this.list_contacts_clear();
}
this.reset_qsearch();
this.select_folder('S'+id, '', true);
// reset vars
this.env.current_page = 1;
this.http_request('search', {_sid: id}, lock);
};
/*********************************************************/
/********* user settings methods *********/
/*********************************************************/
// preferences section select and load options frame
this.section_select = function(list)
{
var win, id = list.get_single_selection(), target = window,
url = {_action: 'edit-prefs', _section: id};
if (id) {
if (win = this.get_frame_window(this.env.contentframe)) {
url._framed = 1;
target = win;
}
this.location_href(url, target, true);
}
return true;
};
this.identity_select = function(list)
{
var id;
if (id = list.get_single_selection()) {
this.enable_command('delete', list.rowcount > 1 && this.env.identities_level < 2);
this.load_identity(id, 'edit-identity');
}
};
// load identity record
this.load_identity = function(id, action)
{
if (action == 'edit-identity' && (!id || id == this.env.iid))
return false;
var win, target = window,
url = {_action: action, _iid: id};
if (win = this.get_frame_window(this.env.contentframe)) {
url._framed = 1;
target = win;
}
if (id || action == 'add-identity') {
this.location_href(url, target, true);
}
return 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 (confirm(this.get_label('deleteidentityconfirm')))
this.goto_url('delete-identity', { _iid: id, _token: this.env.request_token }, true);
return 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);
if (this.env.contentframe && (frame = this.get_frame_window(this.env.contentframe))) {
frame.location.href = this.env.blankpage;
}
}
};
/*********************************************************/
/********* 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_list_widget(this.gui_objects.subscriptionlist,
{multiselect:false, draggable:true, keyboard:false, toggleselect:true});
this.subscription_list
.addEventListener('select', function(o){ ref.subscription_select(o); })
.addEventListener('dragstart', function(o){ ref.drag_active = true; })
.addEventListener('dragend', function(o){ ref.subscription_move_folder(o); })
.addEventListener('initrow', function (row) {
row.obj.onmouseover = function() { ref.focus_subscription(row.id); };
row.obj.onmouseout = function() { ref.unfocus_subscription(row.id); };
})
.init();
$('#mailboxroot')
.mouseover(function(){ ref.focus_subscription(this.id); })
.mouseout(function(){ ref.unfocus_subscription(this.id); })
};
this.focus_subscription = function(id)
{
var row, folder;
if (this.drag_active && this.env.mailbox && (row = document.getElementById(id)))
if (this.env.subscriptionrows[id] &&
(folder = this.env.subscriptionrows[id][0]) !== null
) {
if (this.check_droptarget(folder) &&
!this.env.subscriptionrows[this.get_folder_row_id(this.env.mailbox)][2] &&
folder != this.env.mailbox.replace(this.last_sub_rx, '') &&
!folder.startsWith(this.env.mailbox + this.env.delimiter)
) {
this.env.dstfolder = folder;
$(row).addClass('droptarget');
}
}
};
this.unfocus_subscription = function(id)
{
var row = $('#'+id);
this.env.dstfolder = null;
if (this.env.subscriptionrows[id] && row.length)
row.removeClass('droptarget');
else
$(this.subscription_list.frame).removeClass('droptarget');
};
this.subscription_select = function(list)
{
var id, folder;
if (list && (id = list.get_single_selection()) &&
(folder = this.env.subscriptionrows['rcmrow'+id])
) {
this.env.mailbox = folder[0];
this.show_folder(folder[0]);
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(list)
{
if (this.env.mailbox && this.env.dstfolder !== null &&
this.env.dstfolder != this.env.mailbox &&
this.env.dstfolder != this.env.mailbox.replace(this.last_sub_rx, '')
) {
var path = this.env.mailbox.split(this.env.delimiter),
basename = path.pop(),
newname = this.env.dstfolder === '' ? basename : this.env.dstfolder + this.env.delimiter + basename;
if (newname != this.env.mailbox) {
this.http_post('rename-folder', {_folder_oldname: this.env.mailbox, _folder_newname: newname}, this.set_busy(true, 'foldermoving'));
this.subscription_list.draglayer.hide();
}
}
this.drag_active = false;
this.unfocus_subscription(this.get_folder_row_id(this.env.dstfolder));
};
// 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)
{
var id = this.get_folder_row_id(name ? name : this.env.mailbox),
folder = this.env.subscriptionrows[id][0];
if (folder && confirm(this.get_label('deletefolderconfirm'))) {
var lock = this.set_busy(true, 'folderdeleting');
this.http_post('delete-folder', {_mbox: folder}, lock);
}
};
// Add folder row to the table and initialize it
this.add_folder_row = function (name, display_name, is_protected, subscribed, skip_init, class_name)
{
if (!this.gui_objects.subscriptionlist)
return false;
var row, n, i, tmp, tmp_name, rowid, folders = [], list = [], slist = [],
tbody = this.gui_objects.subscriptionlist.tBodies[0],
refrow = $('tr', tbody).get(1),
id = 'rcmrow'+((new Date).getTime());
if (!refrow) {
// Refresh page if we don't have a table row to clone
this.goto_url('folders');
return false;
}
// clone a table row if there are existing rows
row = $(refrow).clone(true);
// set ID, reset css class
row.attr({id: id, 'class': class_name});
// set folder name
row.find('td:first').html(display_name);
// update subscription checkbox
$('input[name="_subscribed[]"]', row).val(name)
.prop({checked: subscribed ? true : false, disabled: is_protected ? true : false});
// add to folder/row-ID map
this.env.subscriptionrows[id] = [name, display_name, false];
// sort folders (to find a place where to insert the row)
// replace delimiter with \0 character to fix sorting
// issue where 'Abc Abc' would be placed before 'Abc/def'
var replace_from = RegExp(RegExp.escape(this.env.delimiter), 'g'),
replace_to = String.fromCharCode(0);
$.each(this.env.subscriptionrows, function(k,v) {
if (v.length < 4) {
var n = v[0];
n = n.replace(replace_from, replace_to);
v.push(n);
}
folders.push(v);
});
folders.sort(function(a, b) {
var len = a.length - 1; n1 = a[len], n2 = b[len];
return n1 < n2 ? -1 : 1;
});
for (n in folders) {
// protected folder
if (folders[n][2]) {
tmp_name = folders[n][0] + this.env.delimiter;
// prefix namespace cannot have subfolders (#1488349)
if (tmp_name == this.env.prefix_ns)
continue;
slist.push(folders[n][0]);
tmp = tmp_name;
}
// protected folder's child
else if (tmp && folders[n][0].startsWith(tmp))
slist.push(folders[n][0]);
// other
else {
list.push(folders[n][0]);
tmp = null;
}
}
// check if subfolder of a protected folder
for (n=0; n<slist.length; n++) {
if (name.startsWith(slist[n] + this.env.delimiter))
rowid = this.get_folder_row_id(slist[n]);
}
// find folder position after sorting
for (n=0; !rowid && n<list.length; n++) {
if (n && list[n] == name)
rowid = this.get_folder_row_id(list[n-1]);
}
// add row to the table
if (rowid)
$('#'+rowid).after(row);
else
row.appendTo(tbody);
// update list widget
this.subscription_list.clear_selection();
if (!skip_init)
this.init_subscription_list();
row = row.get(0);
if (row.scrollIntoView)
row.scrollIntoView();
return row;
};
// replace an existing table row with a new folder line (with subfolders)
this.replace_folder_row = function(oldfolder, newfolder, display_name, is_protected, class_name)
{
if (!this.gui_objects.subscriptionlist) {
if (this.is_framed)
return parent.rcmail.replace_folder_row(oldfolder, newfolder, display_name, is_protected, class_name);
return false;
}
var i, n, len, name, dispname, oldrow, tmprow, row, level,
tbody = this.gui_objects.subscriptionlist.tBodies[0],
folders = this.env.subscriptionrows,
id = this.get_folder_row_id(oldfolder),
prefix_len = oldfolder.length,
subscribed = $('input[name="_subscribed[]"]', $('#'+id)).prop('checked'),
// find subfolders of renamed folder
list = this.get_subfolders(oldfolder);
// no renaming, only update class_name
if (oldfolder == newfolder) {
$('#'+id).attr('class', class_name || '');
this.subscription_list.focus();
return;
}
// replace an existing table row
this._remove_folder_row(id);
row = $(this.add_folder_row(newfolder, display_name, is_protected, subscribed, true, class_name));
// detect tree depth change
if (len = list.length) {
level = (oldfolder.split(this.env.delimiter)).length - (newfolder.split(this.env.delimiter)).length;
}
// move subfolders to the new branch
for (n=0; n<len; n++) {
id = list[n];
name = this.env.subscriptionrows[id][0];
dispname = this.env.subscriptionrows[id][1];
oldrow = $('#'+id);
tmprow = oldrow.clone(true);
oldrow.remove();
row.after(tmprow);
row = tmprow;
// update folder index
name = newfolder + name.slice(prefix_len);
$('input[name="_subscribed[]"]', row).val(name);
this.env.subscriptionrows[id][0] = name;
// update the name if level is changed
if (level != 0) {
if (level > 0) {
for (i=level; i>0; i--)
dispname = dispname.replace(/^&nbsp;&nbsp;&nbsp;&nbsp;/, '');
}
else {
for (i=level; i<0; i++)
dispname = '&nbsp;&nbsp;&nbsp;&nbsp;' + dispname;
}
row.find('td:first').html(dispname);
this.env.subscriptionrows[id][1] = dispname;
}
}
// update list widget
this.init_subscription_list();
};
// remove the table row of a specific mailbox from the table
this.remove_folder_row = function(folder, subs)
{
var n, len, list = [], id = this.get_folder_row_id(folder);
// get subfolders if any
if (subs)
list = this.get_subfolders(folder);
// remove old row
this._remove_folder_row(id);
// remove subfolders
for (n=0, len=list.length; n<len; n++)
this._remove_folder_row(list[n]);
};
this._remove_folder_row = function(id)
{
this.subscription_list.remove_row(id.replace(/^rcmrow/, ''));
$('#'+id).remove();
delete this.env.subscriptionrows[id];
}
this.get_subfolders = function(folder)
{
var name, list = [],
prefix = folder + this.env.delimiter,
row = $('#'+this.get_folder_row_id(folder)).get(0);
while (row = row.nextSibling) {
if (row.id) {
name = this.env.subscriptionrows[row.id][0];
if (name && name.startsWith(prefix)) {
list.push(row.id);
}
else
break;
}
}
return list;
}
this.subscribe = function(folder)
{
if (folder) {
var lock = this.display_message(this.get_label('foldersubscribing'), 'loading');
this.http_post('subscribe', {_mbox: folder}, lock);
}
};
this.unsubscribe = function(folder)
{
if (folder) {
var lock = this.display_message(this.get_label('folderunsubscribing'), 'loading');
this.http_post('unsubscribe', {_mbox: folder}, lock);
}
};
// helper method to find a specific mailbox row ID
this.get_folder_row_id = function(folder)
{
var id, folders = this.env.subscriptionrows;
for (id in folders)
if (folders[id] && folders[id][0] == folder)
break;
return id;
};
// when user select a folder in manager
this.show_folder = function(folder, path, force)
{
var win, target = window,
url = '&_action=edit-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 id = this.get_folder_row_id(folder);
if (id)
$('input[name="_subscribed[]"]', $('#'+id)).prop('disabled', true);
};
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);
};
/*********************************************************/
/********* GUI functionality *********/
/*********************************************************/
var 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 rcmail.button_sel(this._command, this._id); };
elm.onmouseup = function(e){ return rcmail.button_out(this._command, this._id); };
if (preload)
new Image().src = prop.sel;
}
if (prop.over) {
elm.onmouseover = function(e){ return rcmail.button_over(this._command, this._id); };
elm.onmouseout = function(e){ return rcmail.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++) {
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);
// set image according to button state
if (button.type == 'image' && button[state]) {
button.status = state;
obj.src = button[state];
}
// set class name according to button state
else if (button[state] !== undefined) {
button.status = state;
obj.className = button[state];
}
// disable/enable input buttons
if (button.type == 'input') {
button.status = state;
obj.disabled = state == 'pas';
}
else if (button.type == 'uibutton') {
button.status = state;
$(obj).button('option', '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)
{
// 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];
return 1;
}
type = type ? type : 'notice';
var key = this.html_identifier(msg),
date = new Date(),
id = type + date.getTime();
if (!timeout)
timeout = this.message_time * (type == 'error' || type == 'warning' ? 2 : 1);
if (type == 'loading') {
key = 'loading';
timeout = this.env.request_timeout * 1000;
if (!msg)
msg = this.get_label('loading');
}
// The same message is already displayed
if (this.messages[key]) {
// replace label
if (this.messages[key].obj)
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).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 {
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;
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 = {};
};
// open a jquery UI dialog with the given content
this.show_popup_dialog = function(html, title, buttons, options)
{
// forward call to parent window
if (this.is_framed()) {
return parent.rcmail.show_popup_dialog(html, title, buttons, options);
}
var popup = $('<div class="popup">')
.html(html)
.dialog($.extend({
title: title,
buttons: buttons,
modal: true,
resizable: true,
width: 500,
close: function(event, ui) { $(this).remove() }
}, options || {}));
// resize and center popup
var win = $(window), w = win.width(), h = win.height(),
width = popup.width(), height = popup.height();
popup.dialog('option', {
height: Math.min(h - 40, height + 75 + (buttons ? 50 : 0)),
width: Math.min(w - 20, width + 36)
});
return popup;
};
// 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);
};
// mark a mailbox as selected and set environment variable
this.select_folder = function(name, prefix, encode)
{
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;
// 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('td');
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).html(content.percent+'%').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);
};
// 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_row && 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() { rcmail.command('hide-headers', '', elem); };
// fetch headers only once
if (!this.gui_objects.all_headers_box.innerHTML) {
this.http_post('headers', {_uid: this.env.uid, _mbox: this.env.mailbox},
this.display_message(this.get_label('loading'), '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() { rcmail.command('show-headers', '', elem); };
};
// create folder selector popup, position and display it
this.folder_selector = function(event, callback)
{
var container = this.folder_selector_element;
if (!container) {
var rows = [],
delim = this.env.delimiter,
ul = $('<ul class="toolbarmenu">'),
link = document.createElement('a');
container = $('<div id="folder-selector" class="popupmenu"></div>');
link.href = '#';
link.className = 'icon';
// loop over sorted folders list
$.each(this.env.mailboxes_list, function() {
var n = 0, s = 0,
folder = ref.env.mailboxes[this],
id = folder.id,
a = $(link.cloneNode(false)),
row = $('<li>');
if (folder.virtual)
a.addClass('virtual').attr('aria-disabled', 'true').attr('tabindex', '-1');
else
a.addClass('active').data('id', folder.id);
if (folder['class'])
a.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));
row.append(a);
rows.push(row);
});
ul.append(rows).appendTo(container);
// temporarily show element to calculate its size
container.css({left: '-1000px', top: '-1000px'})
.appendTo($('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'));
return false;
});
/*
// hide selector on click out of selector element
var fn = function(e) { if (e.target != container.get(0)) container.hide(); };
$(document.body).on('mouseup', fn);
$('iframe').contents().on('mouseup', fn)
.load(function(e) { try { $(this).contents().on('mouseup', fn); } catch(e) {}; });
*/
this.folder_selector_element = container;
}
container.data('callback', callback);
// position menu on the screen
this.show_menu('folder-selector', true, event);
};
/***********************************************/
/********* 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;
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)
this.hide_menu(this.menu_stack[i]);
}
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'});
};
/********************************************************/
/********* html to text conversion functions *********/
/********************************************************/
this.html2plain = function(htmlText, id)
{
var url = '?_task=utils&_action=html2text',
lock = this.set_busy(true, 'converting');
this.log('HTTP POST: ' + url);
$.ajax({ type: 'POST', url: url, data: htmlText, 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); $('#'+id).val(data); ref.log(data); }
});
};
this.plain2html = function(plain, id)
{
var lock = this.set_busy(true, 'converting');
plain = plain.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
$('#'+id).val(plain ? '<pre>'+plain+'</pre>' : '');
this.set_busy(false, null, lock);
};
/********************************************************/
/********* 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 base = this.env.comm_path, k, param = {};
// overwrite task name
if (action && action.match(/([a-z0-9_-]+)\/([a-z0-9-_.]+)/)) {
query._action = RegExp.$2;
base = base.replace(/\_task=[a-z0-9_-]+/, '_task='+RegExp.$1);
}
// remove undefined values
for (k in query) {
if (query[k] !== undefined && query[k] !== null)
param[k] = query[k];
}
return base + (base.indexOf('?') > -1 ? '&' : '?') + $.param(param) + querystring;
};
this.redirect = function(url, lock)
{
if (lock || lock === null)
this.set_busy(true);
if (this.is_framed()) {
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)
{
this.redirect(this.url(action, query), 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)
window.history.replaceState({}, document.title, rcmail.url('', query));
};
// send a http request to the server
this.http_request = function(action, query, lock)
{
var url = this.url(action, query);
// trigger plugin hook
var result = this.triggerEvent('request'+action, query);
if (result !== undefined) {
// abort if one the handlers returned false
if (result === false)
return false;
else
url = this.url(action, result);
}
url += '&_remote=1';
// send request
this.log('HTTP GET: ' + url);
// reset keep-alive interval
this.start_keepalive();
return $.ajax({
type: 'GET', url: url, data: { _unlock:(lock?lock:0) }, 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 POST request to the server
this.http_post = function(action, postdata, lock)
{
var url = this.url(action);
if (postdata && typeof postdata === 'object') {
postdata._remote = 1;
postdata._unlock = (lock ? lock : 0);
}
else
postdata += (postdata ? '&' : '') + '_remote=1' + (lock ? '&_unlock='+lock : '');
// trigger plugin hook
var result = this.triggerEvent('request'+action, postdata);
if (result !== undefined) {
// abort if one of the handlers returned false
if (result === false)
return false;
else
postdata = result;
}
// send request
this.log('HTTP POST: ' + url);
// reset keep-alive interval
this.start_keepalive();
return $.ajax({
type: 'POST', url: url, data: postdata, dataType: 'json',
success: function(data){ ref.http_response(data); },
error: function(o, status, err) { ref.http_error(o, status, err, lock, action); }
});
};
// 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);
this.triggerEvent('responsebefore', {response: response});
this.triggerEvent('responsebefore'+response.action, {response: response});
// set env vars
if (response.env)
this.set_env(response.env);
// we have labels to add
if (typeof response.texts === 'object') {
for (var name in response.texts)
if (typeof response.texts[name] === 'string')
this.add_label(name, response.texts[name]);
}
// if we get javascript code from server -> execute it
if (response.exec) {
this.log(response.exec);
eval(response.exec);
}
// execute callback functions of plugins
if (response.callbacks && response.callbacks.length) {
for (var 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 '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('compose', (uid && this.contact_list.rows[uid]));
this.enable_command('delete', 'edit', writable);
this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0));
this.enable_command('export-selected', 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', { 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', { 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();
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);
this.enable_command('set-listmode', this.env.threads && !is_multifolder);
if ((response.action == 'list' || response.action == 'search') && this.message_list) {
- this.message_list.focus();
+ if (this.message_list.rowcount > 0)
+ this.message_list.focus();
this.msglist_select(this.message_list);
this.triggerEvent('listupdate', { folder:this.env.mailbox, rowcount:this.message_list.rowcount });
}
}
else if (this.task == 'addressbook') {
this.enable_command('export', (this.contact_list && this.contact_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();
- this.contact_list.focus();
+ if (this.contact_list.rowcount > 0)
+ this.contact_list.focus();
this.triggerEvent('listupdate', { folder:this.env.source, rowcount:this.contact_list.rowcount });
}
}
break;
+
+ case 'list-contacts':
+ case 'search-contacts':
+ if (this.contact_list && this.contact_list.rowcount > 0)
+ this.contact_list.focus();
+ 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(this.get_label('requesttimedout'), 'error');
else if (request.status == 0 && status != 'abort')
this.display_message(this.get_label('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();
}
else if (redirect_url) {
window.setTimeout(function(){ ref.redirect(redirect_url, true); }, 2000);
}
};
// callback when an iframe finished loading
this.iframe_loaded = function(unlock)
{
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(this.get_label('loading'), '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 postdata, query;
// replace %s in post data
if (prop.postdata) {
postdata = {};
for (var 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 (var 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)
{
var frame, ts = new Date().getTime(),
frame_name = 'rcmupload'+ts;
// upload progress support
if (this.env.upload_progress_name) {
var fname = this.env.upload_progress_name,
field = $('input[name='+fname+']', form);
if (!field.length) {
field = $('<input>').attr({type: 'hidden', name: fname});
field.prependTo(form);
}
field.val(ts);
}
// have to do it this way for IE
// otherwise the form will be posted to a new window
if (document.all) {
document.body.insertAdjacentHTML('BeforeEnd', '<iframe name="'+frame_name+'"'
+ ' src="program/resources/blank.gif" style="width:0;height:0;visibility:hidden;"></iframe>');
frame = $('iframe[name="'+frame_name+'"]');
}
// for standards-compliant browsers
else {
frame = $('<iframe>').attr('name', frame_name)
.css({border: 'none', width: 0, height: 0, visibility: 'hidden'})
.appendTo(document.body);
}
// handle upload errors, parsing iframe content in onload
frame.bind('load', {ts:ts}, onload);
$(form).attr({
target: frame_name,
action: this.url(action, { _id:this.env.compose_id||'', _uploadid:ts }),
method: 'POST'})
.attr(form.encoding ? 'encoding' : 'enctype', 'multipart/form-data')
.submit();
return frame_name;
};
// html5 file-drop API
this.document_drag_hover = function(e, over)
{
e.preventDefault();
$(ref.gui_objects.filedrop)[(over?'addClass':'removeClass')]('active');
};
this.file_drag_hover = function(e, over)
{
e.preventDefault();
e.stopPropagation();
$(ref.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 files = e.target.files || e.dataTransfer.files,
formdata = window.FormData ? new FormData() : null,
fieldname = (this.env.filedrop.fieldname || '_file') + (this.env.filedrop.single ? '' : '[]'),
boundary = '------multipartformboundary' + (new Date).getTime(),
dashdash = '--', crlf = '\r\n',
multipart = dashdash + boundary + crlf;
if (!files || !files.length)
return;
// inline function to submit the files to the server
var submit_data = function() {
var multiple = files.length > 1,
ts = new Date().getTime(),
content = '<span>' + (multiple ? ref.get_label('uploadingmany') : files[0].name) + '</span>';
// add to attachments list
if (!ref.add2attachment_list(ts, { name:'', html:content, classname:'uploading', complete:false }))
ref.file_upload_id = ref.set_busy(true, 'uploading');
// complete multipart content and post request
multipart += dashdash + boundary + dashdash + crlf;
$.ajax({
type: 'POST',
dataType: 'json',
url: ref.url(ref.env.filedrop.action||'upload', { _id:ref.env.compose_id||ref.env.cid||'', _uploadid:ts, _remote:1 }),
contentType: formdata ? false : 'multipart/form-data; boundary=' + boundary,
processData: false,
timeout: 0, // disable default timeout set in ajaxSetup()
data: formdata || multipart,
headers: {'X-Roundcube-Request': ref.env.request_token},
xhr: function() { var xhr = jQuery.ajaxSettings.xhr(); if (!formdata && xhr.sendAsBinary) xhr.send = xhr.sendAsBinary; return xhr; },
success: function(data){ ref.http_response(data); },
error: function(o, status, err) { ref.http_error(o, status, err, null, 'attachment'); }
});
};
// get contents of all dropped files
var last = this.env.filedrop.single ? 0 : files.length - 1;
for (var j=0, i=0, f; j <= last && (f = files[i]); i++) {
if (!f.name) f.name = f.fileName;
if (!f.size) f.size = f.fileSize;
if (!f.type) f.type = 'application/octet-stream';
// file name contains non-ASCII characters, do UTF8-binary string conversion.
if (!formdata && /[^\x20-\x7E]/.test(f.name))
f.name_bin = unescape(encodeURIComponent(f.name));
// filter by file type if requested
if (this.env.filedrop.filter && !f.type.match(new RegExp(this.env.filedrop.filter))) {
// TODO: show message to user
continue;
}
// do it the easy way with FormData (FF 4+, Chrome 5+, Safari 5+)
if (formdata) {
formdata.append(fieldname, f);
if (j == last)
return submit_data();
}
// use FileReader supporetd by Firefox 3.6
else if (window.FileReader) {
var reader = new FileReader();
// closure to pass file properties to async callback function
reader.onload = (function(file, j) {
return function(e) {
multipart += 'Content-Disposition: form-data; name="' + fieldname + '"';
multipart += '; filename="' + (f.name_bin || file.name) + '"' + crlf;
multipart += 'Content-Length: ' + file.size + crlf;
multipart += 'Content-Type: ' + file.type + crlf + crlf;
multipart += reader.result + crlf;
multipart += dashdash + boundary + crlf;
if (j == last) // we're done, submit the data
return submit_data();
}
})(f,j);
reader.readAsBinaryString(f);
}
// Firefox 3
else if (f.getAsBinary) {
multipart += 'Content-Disposition: form-data; name="' + fieldname + '"';
multipart += '; filename="' + (f.name_bin || f.name) + '"' + crlf;
multipart += 'Content-Length: ' + f.size + crlf;
multipart += 'Content-Type: ' + f.type + crlf + crlf;
multipart += f.getAsBinary() + crlf;
multipart += dashdash + boundary +crlf;
if (j == last)
return submit_data();
}
j++;
}
};
// 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);
this._keepalive = setInterval(function(){ ref.keep_alive(); }, this.env.session_lifetime * 0.5 * 1000);
};
// 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()
{
// catch Error: Permission denied to access property rcmail
try {
if (window.opener && !opener.closed && opener.rcmail)
return opener.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()
{
return this.env.uid ? this.env.uid : (this.message_list ? this.message_list.get_single_selection() : null);
};
// same as above but for contacts
this.get_single_cid = function()
{
return this.env.cid ? this.env.cid : (this.contact_list ? this.contact_list.get_single_selection() : null);
};
// get the IMP mailbox of the message with the given UID
this.get_message_mailbox = function(uid)
{
var msg = this.env.messages ? this.env.messages[uid] : {};
return msg.mbox || this.env.mailbox;
}
// gets cursor position
this.get_caret_pos = function(obj)
{
if (obj.selectionEnd !== undefined)
return obj.selectionEnd;
if (document.selection && document.selection.createRange) {
var range = document.selection.createRange();
if (range.parentElement() != obj)
return 0;
var gm = range.duplicate();
if (obj.tagName == 'TEXTAREA')
gm.moveToElementText(obj);
else
gm.expand('textedit');
gm.setEndPoint('EndToStart', range);
var p = gm.text.length;
return p <= obj.value.length ? p : -1;
}
return obj.value.length;
};
// moves cursor to specified position
this.set_caret_pos = function(obj, pos)
{
if (obj.setSelectionRange)
obj.setSelectionRange(pos, pos);
else if (obj.createTextRange) {
var range = obj.createTextRange();
range.collapse(true);
range.moveEnd('character', pos);
range.moveStart('character', pos);
range.select();
}
};
// get selected text from an input field
// http://stackoverflow.com/questions/7186586/how-to-get-the-selected-text-in-textarea-using-jquery-in-internet-explorer-7
this.get_input_selection = function(obj)
{
var start = 0, end = 0,
normalizedValue, range,
textInputRange, len, endRange;
if (typeof obj.selectionStart == "number" && typeof obj.selectionEnd == "number") {
normalizedValue = obj.value;
start = obj.selectionStart;
end = obj.selectionEnd;
}
else {
range = document.selection.createRange();
if (range && range.parentElement() == obj) {
len = obj.value.length;
normalizedValue = obj.value; //.replace(/\r\n/g, "\n");
// create a working TextRange that lives only in the input
textInputRange = obj.createTextRange();
textInputRange.moveToBookmark(range.getBookmark());
// Check if the start and end of the selection are at the very end
// of the input, since moveStart/moveEnd doesn't return what we want
// in those cases
endRange = obj.createTextRange();
endRange.collapse(false);
if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
start = end = len;
}
else {
start = -textInputRange.moveStart("character", -len);
start += normalizedValue.slice(0, start).split("\n").length - 1;
if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
end = len;
}
else {
end = -textInputRange.moveEnd("character", -len);
end += normalizedValue.slice(0, end).split("\n").length - 1;
}
}
}
}
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;
var n, len, elm;
if (lock)
this.disabled_form_elements = [];
for (n=0, len=form.elements.length; n<len; n++) {
elm = form.elements[n];
if (elm.type == 'hidden')
continue;
// remember which elem was disabled before lock
if (lock && elm.disabled)
this.disabled_form_elements.push(elm);
// check this.disabled_form_elements before inArray() as a workaround for FF5 bug
// http://bugs.jquery.com/ticket/9873
else if (lock || (this.disabled_form_elements && $.inArray(elm, this.disabled_form_elements)<0))
elm.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(){ return false; });
}
else {
var status = null;
if (typeof nav.isProtocolHandlerRegistered == 'function') {
status = nav.isProtocolHandlerRegistered('mailto', this.mailto_handler_uri());
if (status)
$(elem).parent().find('.mailtoprotohandler-status').html(status);
}
else {
$(elem).click(function() { rcmail.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 = {};
if (this.env.browser_capabilities.pdf === undefined)
this.env.browser_capabilities.pdf = this.pdf_support_check();
if (this.env.browser_capabilities.flash === undefined)
this.env.browser_capabilities.flash = this.flash_support_check();
if (this.env.browser_capabilities.tif === undefined)
this.tif_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.tif_support_check = function()
{
var img = new Image();
img.onload = function() { rcmail.env.browser_capabilities.tif = 1; };
img.onerror = function() { rcmail.env.browser_capabilities.tif = 0; };
img.src = 'program/resources/blank.tif';
};
this.pdf_support_check = function()
{
var 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 (window.ActiveXObject) {
try {
if (axObj = new ActiveXObject("AcroPDF.PDF"))
return 1;
}
catch (e) {}
try {
if (axObj = 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;
}
return 0;
};
this.flash_support_check = function()
{
var plugin = navigator.mimeTypes ? navigator.mimeTypes["application/x-shockwave-flash"] : {};
if (plugin && plugin.enabledPlugin)
return 1;
if (window.ActiveXObject) {
try {
if (axObj = new ActiveXObject("ShockwaveFlash.ShockwaveFlash"))
return 1;
}
catch (e) {}
}
return 0;
};
// 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)
{
// TODO: add encryption
var item = localStorage.getItem(this.get_local_storage_prefix() + key);
return item !== null ? JSON.parse(item) : (deflt || null);
};
// wrapper for localStorage.setItem(key, data)
this.local_storage_set_item = function(key, data, encrypted)
{
// TODO: add encryption
return localStorage.setItem(this.get_local_storage_prefix() + key, JSON.stringify(data));
};
// wrapper for localStorage.removeItem(key)
this.local_storage_remove_item = function(key)
{
return localStorage.removeItem(this.get_local_storage_prefix() + key);
};
} // end object rcube_webmail
// some static methods
rcube_webmail.long_subject_title = function(elem, indent)
{
if (!elem.title) {
var $elem = $(elem);
if ($elem.width() + (indent || 0) * 15 > $elem.parent().width())
elem.title = $elem.text();
}
};
rcube_webmail.long_subject_title_ex = function(elem)
{
if (!elem.title) {
var $elem = $(elem),
txt = $.trim($elem.text()),
tmp = $('<span>').text(txt)
.css({'position': 'absolute', 'float': 'left', 'visibility': 'hidden',
'font-size': $elem.css('font-size'), 'font-weight': $elem.css('font-weight')})
.appendTo($('body')),
w = tmp.width();
tmp.remove();
if (w + $('span.branch', $elem).width() * 15 > $elem.width())
elem.title = txt;
}
};
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/list.js b/program/js/list.js
index 0bf5d568b..65d4a9260 100644
--- a/program/js/list.js
+++ b/program/js/list.js
@@ -1,1777 +1,1776 @@
/**
* 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
*
* 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.drag_active = false;
this.col_drag_active = false;
this.column_fixed = null;
this.last_selected = 0;
this.shift_start = 0;
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
// overwrite default paramaters
if (p && typeof p === 'object')
for (var n in p)
this[n] = p[n];
};
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.tbody) {
this.rows = {};
this.rowcount = 0;
var r, len, rows = this.tbody.childNodes, me = this;
for (r=0, len=rows.length; r<len; r++) {
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); });
}
}
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};
// set eventhandlers to table row
row.onmousedown = function(e){ return self.drag_row(e, this.uid); };
row.onmouseup = function(e){ return self.click_row(e, this.uid); };
if (bw.touch) {
row.addEventListener('touchstart', function(e) {
if (e.touches.length == 1) {
self.touchmoved = false;
self.drag_row(rcube_event.touchevent(e.touches[0]), this.uid)
}
}, false);
row.addEventListener('touchend', function(e) {
if (e.changedTouches.length == 1) {
if (!self.touchmoved && !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);
}
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('tr 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');
if (!bw.webkit)
me.fixed_header.css('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 tr 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('tr td').each(function(index) {
$(this).css('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 = 0;
if (sel)
this.clear_selection();
- else
- this.last_selected = 0;
// 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';
if (sel_next)
this.select_next();
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 domrow = document.createElement(this.row_tagname());
if (row.id) domrow.id = row.id;
if (row.className) domrow.className = row.className;
if (row.style) $.extend(domrow.style, row.style);
if (row.uid) $(domrow).data('uid', row.uid);
for (var e, domcell, col, i=0; row.cols && i < row.cols.length; i++) {
col = row.cols[i];
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 (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_row: function(id, cols, newid, select)
{
var row = this.rows[id];
if (!row) return false;
var domrow = row.obj;
for (var domcell, col, i=0; cols && i < cols.length; i++) {
this.get_cell(domrow, i).html(cols[i]);
}
if (newid) {
delete this.rows[id];
domrow.id = 'rcmrow' + newid;
this.init_row(domrow);
if (select)
this.selection[0] = newid;
}
},
/**
* 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_col).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
var me = this;
setTimeout(function(){
$(me.list).removeClass('focus').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_col).removeAttr('tabindex');
}
},
/**
* 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();
}
return false;
},
/**
* onmouseup-handler of message list row
*/
click_row: function(e, id)
{
// 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)));
},
/*
* 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 && row.uid)
return row.uid;
var uid;
if (row && (uid = $(row).data('uid')))
row.uid = uid;
else if (row && 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()
{
if (!this.rowcount)
return false;
var last_selected_row = this.rows[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()
{
if (!this.rowcount)
return false;
var last_selected_row = this.rows[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, len, uid, rows = this.tbody.childNodes;
for (i=0, len=rows.length-1; i<len; i++)
if (rows[i].id && (uid = this.get_row_uid(rows[i])))
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])))
return uid;
}
return null;
},
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);
},
/**
* 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(',');
if (!this.multiselect)
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;
}
// trigger event if selection changed
if (this.selection.join(',') != select_before)
this.triggerEvent('select');
if (this.last_selected != 0 && this.rows[this.last_selected]) {
$(this.rows[this.last_selected].obj).removeClass('focused')
.find(this.col_tagname()).eq(this.subject_col).removeAttr('tabindex');
}
// unselect if toggleselect is active and the same row was clicked again
if (this.toggleselect && this.last_selected == id) {
this.clear_selection();
id = null;
}
else {
$(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_col).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 last selected one.
* Either below or above.
*/
select_next: function()
{
var next_row = this.get_next_row(),
prev_row = this.get_prev_row(),
new_row = (next_row) ? next_row : prev_row;
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)
{
for (var n in this.selection)
if (this.selection[n] == id)
return 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 (num_select && !this.selection.length && !no_event) {
this.triggerEvent('select');
this.last_selected = 0;
}
},
/**
* Getter for the selection array
*/
get_selection: function(deep)
{
var res = $.merge([], this.selection);
// 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()
{
if (this.selection.length == 1)
return this.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');
}
}
else {
if (!this.in_selection(id)) { // select row
this.selection.push(id);
$(this.rows[id].obj).addClass('selected').attr('aria-selected', 'true');
if (!norecur && !this.rows[id].expanded)
this.highlight_children(id, true);
}
else { // unselect row
var p = $.inArray(id, this.selection),
a_pre = this.selection.slice(0, p),
a_post = this.selection.slice(p+1, this.selection.length);
this.selection = a_pre.concat(a_post);
$(this.rows[id].obj).removeClass('selected').removeAttr('aria-selected');
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)
{
var target = e.target || {};
if (this.focused != true || target.nodeName == 'INPUT' || target.nodeName == 'TEXTAREA' || target.nodeName == '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].obj;
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('...');
return false;
}
$('> ' + self.col_tagname(), self.rows[uid].obj).each(function(n, cell) {
if (self.subject_col < 0 || (self.subject_col >= 0 && self.subject_col == n)) {
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;
}
if (this.col_drag_active)
this.focus();
this.col_drag_active = false;
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.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);
}
}
this.triggerEvent('column_dragend', e);
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');
}
};
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;
diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc
index 0b5ff8c5a..874f19be2 100644
--- a/program/localization/en_US/labels.inc
+++ b/program/localization/en_US/labels.inc
@@ -1,601 +1,604 @@
<?php
/*
+-----------------------------------------------------------------------+
| localization/<lang>/labels.inc |
| |
| Localization file of the Roundcube Webmail client |
| Copyright (C) 2005-2013, 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['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['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['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['more'] = 'More';
$labels['back'] = 'Back';
$labels['options'] = 'Options';
$labels['first'] = 'First';
$labels['last'] = 'Last';
$labels['previous'] = 'Previous';
$labels['next'] = 'Next';
$labels['select'] = 'Select';
$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['folderactions'] = 'Folder actions...';
$labels['compact'] = 'Compact';
$labels['empty'] = 'Empty';
$labels['importmessages'] = 'Import messages';
$labels['quota'] = 'Disk usage';
$labels['unknown'] = 'unknown';
$labels['unlimited'] = 'unlimited';
$labels['quicksearch'] = 'Quick 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['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['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['responses'] = 'Responses';
$labels['insertresponse'] = 'Insert a response';
$labels['manageresponses'] = 'Manage responses';
$labels['savenewresponse'] = 'Save new response';
$labels['editresponses'] = 'Edit responses';
$labels['editresponse'] = 'Edit response';
$labels['responsename'] = 'Name';
$labels['responsetext'] = 'Response Text';
$labels['attach'] = 'Attach';
$labels['attachments'] = 'Attachments';
$labels['upload'] = 'Upload';
$labels['uploadprogress'] = '$percent ($current from $total)';
$labels['close'] = 'Close';
$labels['messageoptions'] = 'Message options...';
$labels['togglecomposeoptions'] = 'Toggle composition options';
$labels['low'] = 'Low';
$labels['lowest'] = 'Lowest';
$labels['normal'] = 'Normal';
$labels['high'] = 'High';
$labels['highest'] = 'Highest';
$labels['nosubject'] = '(no subject)';
$labels['showimages'] = 'Display images';
$labels['alwaysshow'] = 'Always show images from $sender';
$labels['isdraft'] = 'This is a draft message.';
$labels['andnmore'] = '$nr more...';
$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.';
// 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['searchfor'] = 'Search for "$q"';
$labels['advsearch'] = 'Advanced Search';
$labels['advanced'] = 'Advanced';
$labels['other'] = 'Other';
$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 new contact';
$labels['editcontact'] = 'Edit contact';
$labels['contacts'] = 'Contacts';
$labels['contactproperties'] = 'Contact properties';
$labels['personalinfo'] = 'Personal information';
$labels['edit'] = 'Edit';
$labels['cancel'] = 'Cancel';
$labels['save'] = 'Save';
$labels['delete'] = 'Delete';
$labels['rename'] = 'Rename';
$labels['addphoto'] = 'Add';
$labels['replacephoto'] = 'Replace';
$labels['uploadphoto'] = 'Upload photo';
$labels['newcontact'] = 'Create new contact card';
$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['newcontactgroup'] = 'Create new contact group';
$labels['grouprename'] = 'Rename group';
$labels['groupdelete'] = 'Delete group';
$labels['groupremoveselected'] = 'Remove selected contacts from group';
$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="http://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 for this account';
$labels['newidentity'] = 'New identity';
$labels['newitem'] = 'New item';
$labels['edititem'] = 'Edit item';
$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['ignore'] = 'ignore';
$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['fromknownsenders'] = 'from known senders';
$labels['always'] = 'always';
$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['advancedoptions'] = '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['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['previewpanemarkread'] = 'Mark previewed 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['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';
// 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['arialabelmessagessearchfilter'] = 'Email listing filter';
$labels['arialabelsearchform'] = 'Email message search form';
$labels['arialabelquicksearchbox'] = 'Email search input';
$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['arialabelcomposeoptions'] = 'Composition options';
$labels['arialabelresponsesmenu'] = 'Canned responses menu';
$labels['arialabelattachmentuploadform'] = 'Attachment upload form';
+$labels['arialabelattachmentpreview'] = 'Attachment preview';
+$labels['ariasummarycomposecontacts'] = 'List of contacts and groups to select as recipients';
?>
diff --git a/program/steps/mail/list_contacts.inc b/program/steps/mail/list_contacts.inc
index 0ee81135b..4f17beffd 100644
--- a/program/steps/mail/list_contacts.inc
+++ b/program/steps/mail/list_contacts.inc
@@ -1,127 +1,127 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/steps/mail/list_contacts.inc |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2012-2014, 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_GPC)) {
$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) {
$row_id = $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::span(array('title' => $email), rcube::Q($name ? $name : $email) .
+ $keyname => html::a(array('title' => $email), rcube::Q($name ? $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/search_contacts.inc b/program/steps/mail/search_contacts.inc
index d56581695..ccef32dd2 100644
--- a/program/steps/mail/search_contacts.inc
+++ b/program/steps/mail/search_contacts.inc
@@ -1,119 +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 |
| |
| 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 adress 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();
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;
}
// get records
$result = $source->list_records($afields);
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);
// 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['ID'].'-'.$i;
$jsresult[$row_id] = format_email_recipient($email, $name);
$OUTPUT->command('add_contact_row', $row_id, array(
- 'contact' => html::span(array('title' => $email), rcube::Q($name ? $name : $email) .
+ 'contact' => html::a(array('title' => $email), rcube::Q($name ? $name : $email) .
($name && count($emails) > 1 ? '&nbsp;' . html::span('email', rcube::Q($email)) : '')
)), 'person');
}
}
// 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->command('set_env', 'search_request', $search_request);
$OUTPUT->command('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/skins/classic/mail.css b/skins/classic/mail.css
index 0a4653a3a..3310ac54f 100644
--- a/skins/classic/mail.css
+++ b/skins/classic/mail.css
@@ -1,1880 +1,1881 @@
/***** Roundcube|Mail mail task styles *****/
#messagetoolbar
{
position: absolute;
top: 47px;
left: 205px;
right: 10px;
height: 35px;
min-width: 650px;
white-space: nowrap;
/* border: 1px solid #cccccc; */
}
.extwin #messagetoolbar
{
top: 5px;
left: 20px;
}
#messagetoolbar a,
#messagetoolbar select
{
display: block;
float: left;
padding-right: 10px;
}
#messagetoolbar a.button,
#messagetoolbar a.buttonPas {
display: block;
float: left;
width: 32px;
height: 32px;
padding: 0;
margin: 0 5px;
overflow: hidden;
background: url(images/mail_toolbar.png) 0 0 no-repeat transparent;
opacity: 0.99; /* this is needed to make buttons appear correctly in Chrome */
}
#messagetoolbar a.buttonPas {
opacity: 0.35;
}
#messagetoolbar a.button.selected {
background-color: #ddd;
margin-left: 4px;
margin-right: 4px;
margin-top: -1px;
border: 1px solid #ccc;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
}
#messagetoolbar a.checkmailSel {
background-position: 0 -32px;
}
#messagetoolbar a.back {
background-position: -32px 0;
}
#messagetoolbar a.backSel {
background-position: -32px -32px;
}
#messagetoolbar a.compose {
background-position: -64px 0;
}
#messagetoolbar a.composeSel {
background-position: -64px -32px;
}
#messagetoolbar a.reply {
background-position: -96px 0;
}
#messagetoolbar a.replySel {
background-position: -96px -32px;
}
#messagetoolbar a.replyAll {
background-position: -128px 0;
}
#messagetoolbar a.replyAllSel {
background-position: -128px -32px;
}
#messagetoolbar a.forward {
background-position: -160px 0;
}
#messagetoolbar a.forwardSel {
background-position: -160px -32px;
}
#messagetoolbar a.delete {
background-position: -192px 0;
}
#messagetoolbar a.deleteSel {
background-position: -192px -32px;
}
#messagetoolbar a.print {
background-position: -256px 0;
}
#messagetoolbar a.printSel {
background-position: -256px -32px;
}
#messagetoolbar a.markmessage {
background-position: -288px 0;
}
#messagetoolbar a.messagemenu {
background-position: -320px 0;
width: 34px;
}
#messagetoolbar a.spellcheck {
background-position: -418px 0;
}
#messagetoolbar a.spellcheckSel {
background-position: -418px -32px;
}
#messagetoolbar a.attach {
background-position: -386px 0;
}
#messagetoolbar a.attachSel {
background-position: -386px -32px;
}
#messagetoolbar a.insertsig {
background-position: -482px 0;
}
#messagetoolbar a.insertsigSel {
background-position: -482px -32px;
}
#messagetoolbar a.savedraft {
background-position: -354px 0;
}
#messagetoolbar a.savedraftSel {
background-position: -354px -32px;
}
#messagetoolbar a.send {
background-position: -450px 0;
}
#messagetoolbar a.sendSel {
background-position: -450px -32px;
}
#messagetoolbar a.move {
background-position: -580px 0;
}
#messagetoolbar a.moveSel {
background-position: -580px -32px;
}
#messagetoolbar a.download {
background-position: -480px 0;
}
#messagetoolbar a.downloadSel {
background-position: -480px -32px;
}
#messagetoolbar a.responses {
background-position: -548px 0;
}
#messagemenu li a.active:hover,
#attachmentmenu li a.active:hover,
#markmessagemenu li a.active:hover
{
color: #fff;
background-color: #c00;
}
#messagemenu li a,
#attachmentmenu li a
{
background: url(images/messageactions.png) no-repeat 7px 0;
background-position: 7px 20px;
}
#messagemenu li a.printlink
{
background-position: 7px 1px;
}
#messagemenu li a.downloadlink,
#attachmentmenu li a.downloadlink
{
background-position: 7px -17px;
}
#messagemenu li a.sourcelink
{
background-position: 7px -35px;
}
#messagemenu li a.openlink,
#attachmentmenu li a.openlink
{
background-position: 7px -52px;
}
#messagemenu li a.editlink
{
background-position: 6px -70px;
}
#messagemenu li a.movelink
{
background-position: 6px -161px;
}
#messagemenu li a.copylink
{
background-position: 6px -143px;
}
#markmessagemenu li a,
#compose-attachments li a
{
background: url(images/messageicons.png) no-repeat;
}
#markmessagemenu li a.readlink
{
background-position: 7px -51px;
}
#markmessagemenu li a.unreadlink
{
background-position: 7px -119px;
}
#markmessagemenu li a.flaggedlink
{
background-position: 7px -153px;
}
#markmessagemenu li a.unflaggedlink
{
background-position: 7px -136px;
}
#searchfilter
{
white-space: nowrap;
position: absolute;
right: 198px;
vertical-align: middle;
}
#searchfilter label
{
font-size: 11px;
}
#mailleftcontainer
{
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 160px;
}
#mailrightcontainer
{
position: absolute;
top: 0;
left: 170px;
bottom: 0;
right: 0;
min-width: 600px;
}
#mailrightcontent
{
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
#messagepartcontainer
{
position: absolute;
top: 0;
left: 170px;
right: 0;
bottom: 0;
}
#messagepartheader
{
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 160px;
border: 1px solid #999999;
background-color: #F9F9F9;
overflow: hidden;
}
#messagepartheader table
{
width: 100%;
table-layout: fixed;
}
#messagepartheader table td
{
text-overflow: ellipsis;
}
#messagepartheader table td.title
{
width: 60px;
}
#mailcontframe
{
position: absolute;
width: 100%;
top: 0;
bottom: 0;
border: 1px solid #999999;
background-color: #F9F9F9;
overflow: hidden;
}
#mailpreviewframe
{
position: absolute;
width: 100%;
top: 205px;
bottom: 0px;
border: 1px solid #999999;
background-color: #F9F9F9;
}
#messagecontframe
{
position: relative;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
width: 100%;
height: 100%;
min-height: 100%; /* Chrome 14 bug */
}
#messagepartframe
{
width: 100%;
height: 100%;
min-height: 100%; /* Chrome 14 bug */
border: 1px solid #999999;
background-color: #fff;
}
/** mailbox list styles */
#mailboxlist-container
{
position: absolute;
top: 0;
left: 0;
width: 100%;
bottom: 0;
border: 1px solid #999;
background-color: #F9F9F9;
}
#mailboxlist
{
position:relative;
height: auto;
margin: 0px;
padding: 0px;
list-style-image: none;
list-style-type: none;
overflow: hidden;
white-space: nowrap;
background-color: #FFF;
}
#mailboxlist li
{
display: block;
position: relative;
font-size: 11px;
border-bottom: 1px solid #EBEBEB;
}
#mailboxlist li ul li:last-child
{
border-bottom: 0 none;
}
#mailboxlist li.inbox a
{
background-position: 5px -18px;
}
#mailboxlist li.drafts a
{
background-position: 5px -37px;
}
#mailboxlist li.sent a
{
background-position: 5px -54px;
}
#mailboxlist li.junk a
{
background-position: 5px -73px;
}
#mailboxlist li.trash a
{
background-position: 5px -180px;
}
#mailboxlist li.trash.empty a
{
background-position: 5px -90px;
}
#mailboxlist li a
{
cursor: default;
display: block;
position: relative;
padding-left: 25px;
padding-top: 2px;
padding-bottom: 2px;
text-decoration: none;
height: 15px;
background: url(images/icons/folders.png) 5px 0 no-repeat;
}
#mailboxlist li.unread
{
font-weight: bold;
}
#mailboxlist li.virtual > a
{
color: #666;
}
#mailboxlist li.recent > a
{
color: #0066FF;
}
#mailboxlist li.selected,
#mailboxlist li.droptarget li.selected
{
background-color: #929292;
}
#mailboxlist li.selected > a,
#mailboxlist li.droptarget li.selected a
{
color: #FFF;
font-weight: bold;
}
#mailboxlist li.droptarget
{
background-color: #FFFFA6;
}
/* styles for nested folders */
#mailboxlist ul {
list-style: none;
padding: 0;
margin: 0;
border-top: 1px solid #EBEBEB;
background-color: #FFF;
font-weight: normal;
}
#mailboxlist li.mailbox ul li a {
padding-left: 40px; /* 24 + 1 x 16 */
background-position: 20px 0; /* 4 + 1 x 16 */
}
#mailboxlist li.mailbox ul li div.treetoggle {
left: 23px !important;
}
#mailboxlist li.mailbox ul ul li.mailbox a {
padding-left: 56px; /* 2x */
background-position: 36px 0;
}
#mailboxlist li.mailbox ul ul li div.treetoggle {
left: 39px !important;
}
#mailboxlist li.mailbox ul ul ul li.mailbox a {
padding-left: 72px; /* 3x */
background-position: 52px 0;
}
#mailboxlist li.mailbox ul ul ul li div.treetoggle {
left: 55px !important;
}
#mailboxlist li.mailbox ul ul ul ul li.mailbox a {
padding-left: 88px; /* 4x */
background-position: 68px 0;
}
#mailboxlist li.mailbox ul ul ul ul li div.treetoggle {
left: 71px !important;
}
/* indent folders on levels > 4 */
#mailboxlist li.mailbox ul ul ul ul ul li {
padding-left: 16px;
}
#mailboxlist li.mailbox ul ul ul ul ul li div.treetoggle {
left: 87px !important;
}
#mailboxlist li.mailbox ul li.drafts a
{
background-position: 21px -37px;
}
#mailboxlist li.mailbox ul li.sent a
{
background-position: 21px -54px;
}
#mailboxlist li.mailbox ul li.junk a
{
background-position: 21px -73px;
}
#mailboxlist li.mailbox ul li.trash a
{
background-position: 21px -180px;
}
#mailboxlist li.mailbox ul li.trash.empty a
{
background-position: 21px -90px;
}
#listcontrols
{
position: relative;
white-space: nowrap;
line-height: 22px;
padding: 0 4px;
width: auto;
min-width: 300px;
}
#listcontrols a,
#listcontrols span
{
display: block;
float: left;
font-size: 11px;
}
#listcontrols span input
{
vertical-align: middle;
}
#listcontrols a.button,
#listcontrols a.buttonPas
{
display: block;
float: left;
width: 15px;
height: 15px;
padding: 0;
margin-top: 4px;
margin-right: 2px;
overflow: hidden;
background: url(images/mail_footer.png) 0 0 no-repeat transparent;
opacity: 0.99; /* this is needed to make buttons appear correctly in Chrome */
}
#listcontrols a.buttonPas
{
opacity: 0.35;
}
#listcontrols a.all {
background-position: -30px 0;
}
#listcontrols a.allsel {
background-position: -30px -15px;
}
#listcontrols a.page {
background-position: -135px 0;
}
#listcontrols a.pagesel {
background-position: -135px -15px;
}
#listcontrols a.unread {
background-position: -45px 0;
}
#listcontrols a.unreadsel {
background-position: -45px -15px;
}
#listcontrols a.invert {
background-position: -60px 0;
}
#listcontrols a.invertsel {
background-position: -60px -15px;
}
#listcontrols a.none {
background-position: -75px 0;
}
#listcontrols a.nonesel {
background-position: -75px -15px;
}
#listcontrols a.expand-all {
background-position: -90px 0;
}
#listcontrols a.expand-allsel {
background-position: -90px -15px;
}
#listcontrols a.collapse-all {
background-position: -105px 0;
}
#listcontrols a.collapse-allsel {
background-position: -105px -15px;
}
#listcontrols a.expand-unread {
background-position: -120px 0;
}
#listcontrols a.expand-unreadsel {
background-position: -120px -15px;
}
#countcontrols
{
position: absolute;
top: 4px;
right: 4px;
white-space: nowrap;
font-size: 11px;
}
#countcontrols a.button,
#countcontrols a.buttonPas
{
float: right;
}
/** message list styles */
body.messagelist
{
margin: 0px;
background-color: #F9F9F9;
}
table.messagelist
{
width: 100%;
display: table;
table-layout: fixed;
border-collapse: collapse;
border-spacing: 0;
z-index: 1;
}
table.messagelist.fixedcopy
{
z-index: 2;
}
.messagelist thead tr td
{
height: 20px;
padding: 0 4px 0 2px;
vertical-align: middle;
border-bottom: 1px solid #999999;
color: #333333;
background: url(images/listheader.gif) top left repeat-x #CCC;
font-size: 11px;
font-weight: bold;
}
.messagelist thead tr td.sortedASC,
.messagelist thead tr td.sortedDESC
{
background-position: 0 -26px;
}
.messagelist thead tr td.sortedASC a
{
background: url(images/icons/sort.gif) right 0 no-repeat;
}
.messagelist thead tr td.sortedDESC a
{
background: url(images/icons/sort.gif) right -14px no-repeat;
}
.messagelist thead tr td a
{
display: block;
width: auto !important;
width: 100%;
color: #333333;
text-decoration: none;
}
.messagelist thead tr td.size.sortedASC a,
.messagelist thead tr td.size.sortedDESC a
{
padding-right: 18px;
}
.messagelist thead tr td.subject
{
padding-left: 18px;
width: 99%;
}
.messagelist tbody tr td
{
height: 20px;
padding: 0;
font-size: 11px;
overflow: hidden;
vertical-align: middle;
white-space: nowrap;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
border-bottom: 1px solid #EBEBEB;
cursor: default;
+ outline: none;
}
.messagelist tbody tr td a
{
color: #000;
text-decoration: none;
white-space: nowrap;
cursor: inherit;
}
.messagelist td img
{
vertical-align: middle;
display: inline-block;
}
.messagelist tbody tr td.flag,
.messagelist tbody tr td.status,
.messagelist tbody tr td.subject span.status
{
cursor: pointer;
}
.messagelist tr td.flag span,
.messagelist tr td.status span,
.messagelist tr td.attachment span,
.messagelist tr td.priority span
{
display: block;
width: 15px;
}
.messagelist tr td div.collapsed,
.messagelist tr td div.expanded,
.messagelist tr td.threads .listmenu,
.messagelist tr td.attachment span.attachment,
.messagelist tr td.attachment span.report,
.messagelist tr td.priority span.priority,
.messagelist tr td.priority span.prio1,
.messagelist tr td.priority span.prio2,
.messagelist tr td.priority span.prio3,
.messagelist tr td.priority span.prio4,
.messagelist tr td.priority span.prio5,
.messagelist tr td.flag span.flagged,
.messagelist tr td.flag span.unflagged,
.messagelist tr td.flag span.unflagged:hover,
.messagelist tr td.status span.status,
.messagelist tr td.status span.msgicon,
.messagelist tr td.status span.deleted,
.messagelist tr td.status span.unread,
.messagelist tr td.status span.unreadchildren,
.messagelist tr td.subject span.msgicon,
.messagelist tr td.subject span.deleted,
.messagelist tr td.subject span.unread,
.messagelist tr td.subject span.replied,
.messagelist tr td.subject span.forwarded,
.messagelist tr td.subject span.unreadchildren
{
display: inline-block;
vertical-align: middle;
height: 17px;
width: 15px;
background: url(images/messageicons.png) center no-repeat;
}
.messagelist tr td.attachment span.attachment
{
background-position: 0 -170px;
}
.messagelist tr td.attachment span.report
{
background-position: 0 -255px;
}
.messagelist tr td.priority span.priority
{
background-position: 0 -309px;
}
.messagelist tr td.priority span.prio5
{
background-position: 0 -358px;
}
.messagelist tr td.priority span.prio4
{
background-position: 0 -340px;
}
.messagelist tr td.priority span.prio3
{
background-position: 0 -324px;
}
.messagelist tr td.priority span.prio2
{
background-position: 0 -309px;
}
.messagelist tr td.priority span.prio1
{
background-position: 0 -290px;
}
.messagelist tr td.flag span.flagged
{
background-position: 0 -153px;
}
.messagelist tr td.flag span.unflagged:hover
{
background-position: 0 -136px;
}
.messagelist tr td.subject span.msgicon,
.messagelist tr td.subject span.unreadchildren
{
background-position: 0 -51px;
margin: 0 2px;
}
.messagelist tr td.subject span.replied
{
background-position: 0 -85px;
}
.messagelist tr td.subject span.forwarded
{
background-position: 0 -68px;
}
.messagelist tr td.subject span.replied.forwarded
{
background-position: 0 -102px;
}
.messagelist tr td.status span.msgicon,
.messagelist tr td.flag span.unflagged,
.messagelist tr td.status span.unreadchildren
{
background-position: 0 17px; /* no icon */
}
.messagelist tr td.status span.msgicon:hover
{
background-position: 0 -272px;
}
.messagelist tr td.status span.deleted,
.messagelist tr td.subject span.deleted
{
background-position: 0 -187px;
}
.messagelist tr td.status span.status,
.messagelist tr td.status span.unread,
.messagelist tr td.subject span.unread
{
background-position: 0 -119px;
}
.messagelist tr td div.collapsed
{
background-position: 0 -221px;
cursor: pointer;
}
.messagelist tr td div.expanded
{
background-position: 0 -204px;
cursor: pointer;
}
.messagelist tr td.threads .listmenu
{
background-position: 0 -238px;
cursor: pointer;
overflow: hidden;
text-indent: -5000px;
}
.messagelist tbody tr td.subject
{
width: 99%;
}
.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;
}
.messagelist tr td.attachment,
.messagelist tr td.threads,
.messagelist tr td.status,
.messagelist tr td.flag,
.messagelist tr td.priority
{
width: 17px;
padding: 0 0 0 2px;
}
.messagelist tr td.size
{
width: 60px;
text-align: right;
padding: 0 2px;
}
.messagelist tr td.fromto,
.messagelist tr td.from,
.messagelist tr td.to,
.messagelist tr td.cc,
.messagelist tr td.replyto
{
width: 180px;
padding: 0 2px;
}
.messagelist tr td.date
{
width: 135px;
padding: 0 2px;
}
.messagelist tr td.folder
{
width: 135px;
}
.messagelist tr td.hidden
{
display: none;
}
.messagelist tr.message
{
background-color: #FFF;
}
.messagelist tr.unread
{
font-weight: bold;
background-color: #FFFFFF;
}
.messagelist tr.flagged td,
.messagelist tr.flagged td a
{
color: #CC0000;
}
/* This padding-left minus the focused padding left should be half of the focused border-left */
.messagelist thead tr td:first-child,
.messagelist tbody tr td:first-child {
border-left: 0;
padding-left: 6px;
}
/* because of border-collapse, we make the left border twice what we want it to be - half will be hidden to the left */
.messagelist tbody tr.focused > td:first-child {
border-left: 4px solid #d4d4d4;
padding-left: 4px;
}
.messagelist tbody tr.selected.focused > td:first-child {
border-left: 2px solid #ccc;
padding-left: 5px;
}
.messagelist tr.selected td
{
color: #FFFFFF;
background-color: #929292;
}
.messagelist.focus tr.selected td
{
background-color: #CC3333;
}
.messagelist tr.selected td a
{
color: #FFFFFF;
}
.messagelist tr.deleted td,
.messagelist tr.deleted td a
{
color: #CCCCCC;
}
#listmenu
{
padding: 6px;
max-height: none;
}
#listmenu legend
{
color: #999999;
}
#listmenu fieldset
{
border: 1px solid #999999;
margin: 0 5px;
float: left;
}
#listmenu div
{
padding: 8px 0 3px 0;
text-align: center;
clear: both;
}
/***** tree indicators *****/
td span.branch div
{
float: left;
height: 16px;
}
td span.branch div.tree
{
height: 17px;
width: 15px;
background: url(images/tree.gif) 0px 0px no-repeat;
}
td span.branch div.l1
{
background-position: 0px 0px; /* L */
}
td span.branch div.l2
{
background-position: -30px 0px; /* | */
}
td span.branch div.l3
{
background-position: -15px 0px; /* |- */
}
/** message view styles */
#messageframe
{
position: absolute;
top: 0;
left: 180px;
right: 0;
bottom: 0;
border: 1px solid #999;
background-color: #FFF;
overflow: auto;
z-index: 1;
}
.extwin #messageframe
{
left: 0;
}
div.messageheaderbox
{
margin: -14px 8px 0px 8px;
border: 1px solid #ccc;
}
table.headers-table
{
width: 100%;
background-color: #EBEBEB;
}
#messagebody #full-headers,
#messagebody table.headers-table
{
width: auto;
margin: 6px 8px;
background-color: #F4F4F4;
}
#messagebody table.headers-table
{
margin: 16px 6px 6px 6px;
}
div.message-partheaders + div.message-part
{
border-top: 0;
padding-top: 4px;
}
table.headers-table tr td
{
font-size: 11px;
border-bottom:1px solid #FFFFFF;
}
table.headers-table tr td.header-title
{
width: 1%;
color: #666666;
font-weight: bold;
text-align: right;
white-space: nowrap;
padding: 0 4px 0 8px;
}
table.headers-table tr td.header
{
width: 99%;
}
table.headers-table tr td.subject
{
font-weight: bold;
}
table.headers-table tr td.header span
{
white-space: nowrap;
}
#attachment-list
{
margin: 0;
padding: 0 4px 0 8px;
min-height: 16px;
list-style-image: none;
list-style-type: none;
background: url(images/icons/attachment.png) 4px 2px no-repeat #DFDFDF;
}
#messageframe #attachment-list
{
border-bottom: 1px solid #ccc;
}
.messageheaderbox #attachment-list
{
border-top: 1px solid #ccc;
}
#attachment-list:after
{
content: ".";
display: block;
height: 0;
font-size: 0;
clear: both;
visibility: hidden;
}
#attachment-list li
{
float: left;
height: 18px;
font-size: 11px;
padding: 2px 0px 0px 15px;
white-space: nowrap;
}
#attachment-list li a
{
text-decoration: none;
}
#attachment-list li a:hover
{
text-decoration: underline;
}
#attachment-list li a.drop {
background: url(images/icons/down_small.gif) no-repeat center 6px;
width: 12px;
height: 7px;
cursor: pointer;
padding: 5px 0 0;
margin-left: 3px;
display: inline-block;
}
#messagebody
{
position:relative;
padding-bottom: 10px;
background-color: #FFFFFF;
}
div.message-part,
div.message-htmlpart
{
padding: 10px 8px;
border-top: 1px solid #ccc;
/* overflow: hidden; */
}
#messagebody div:first-child
{
border-top: 0;
}
div.message-part a,
div.message-htmlpart a
{
color: #0000CC;
}
div.message-part pre,
div.message-htmlpart pre,
div.message-part div.pre
{
margin: 0px;
padding: 0px;
font-family: monospace;
font-size: 12px;
white-space: -moz-pre-wrap !important;
white-space: pre-wrap !important;
white-space: pre;
}
div.message-part span.sig
{
color: #666666;
}
div.message-part blockquote
{
color: blue;
border-left: 2px solid blue;
border-right: 2px solid blue;
background-color: #F6F6F6;
margin: 2px 0px;
padding: 1px 8px 1px 10px;
}
div.message-part blockquote blockquote
{
color: green;
border-left: 2px solid green;
border-right: 2px solid green;
}
div.message-part blockquote blockquote blockquote
{
color: #990000;
border-left: 2px solid #bb0000;
border-right: 2px solid #bb0000;
}
body.iframe div.message-htmlpart
{
margin: 8px;
}
div.message-htmlpart div.rcmBody
{
margin: 8px;
}
#messagebody span.part-notice
{
display: block;
}
#message-objects div,
#messagebody span.part-notice
{
margin: 8px;
min-height: 20px;
padding: 10px 10px 6px 46px;
}
#message-objects div a,
#messagebody span.part-notice a
{
color: #666666;
padding-left: 10px;
}
#message-objects div a:hover,
#messagebody span.part-notice a:hover
{
color: #333333;
}
#messagebody fieldset.image-attachment {
border: 0;
border-top: 1px solid #ccc;
margin: 1em 1em 0 1em;
}
#messagebody fieldset.image-attachment p > img
{
max-width: 80%;
}
#messagebody legend.image-filename
{
color: #999;
font-size: 0.9em;
}
#messagebody p.image-attachment
{
margin: 0 1em;
padding: 1em;
border-top: 1px solid #ccc;
}
#messagebody p.image-attachment a.image-link
{
float: left;
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
{
font-size: 11px;
padding-right: 1em;
}
#messagebody p.image-attachment .attachment-links a
{
margin-right: 0.6em;
color: #cc0000;
font-size: 11px;
text-decoration: none;
}
#messagebody p.image-attachment .attachment-links a:hover
{
text-decoration: underline;
}
#messagelinks
{
position: absolute;
top: 8px;
right: 10px;
height: 16px;
text-align: right;
}
#messageframe #messagelinks
{
top: 2px;
right: 2px;
}
#compose-headers #openextwinlink
{
position: absolute;
height: 15px;
top: 4px;
right: 2px;
}
#full-headers
{
color: #666666;
text-align: center;
padding: 2px 6px;
border-bottom: 1px solid #ccc;
background-color: #EBEBEB;
}
.messageheaderbox #full-headers
{
border-bottom: 0;
}
div.more-headers
{
cursor: pointer;
height: 8px;
border-bottom: 0;
}
div.show-headers
{
background: url(images/icons/down_small.gif) no-repeat center;
}
div.hide-headers
{
background: url(images/icons/up_small.gif) no-repeat center;
}
#headers-source
{
margin: 2px 0;
padding: 0.5em;
height: 145px;
background: white;
overflow: auto;
font-size: 11px;
border: 1px solid #CCC;
display: none;
text-align: left;
color: #333;
}
/** message compose styles */
#compose-container
{
position: absolute;
top: 0;
left: 205px;
right: 0;
bottom: 0;
margin: 0;
}
#compose-div
{
position: absolute;
top: 85px;
right: 0;
left: 0;
bottom: 0;
margin: 0;
}
#compose-body-div
{
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 175px;
border: 1px solid #999;
}
#compose-div .boxlistcontent
{
bottom: 23px;
}
#compose-body
{
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: 0;
font-size: 9pt;
font-family: monospace;
resize: none;
border: none;
outline: none;
}
#compose-body_tbl,
#compose-body_tbl td
{
border: none;
}
#compose-body_tbl tr.mceFirst td.mceToolbar
{
border-bottom: 1px solid #ccc;
}
#compose-headers
{
width: 100%;
}
#compose-headers td.editfield
{
padding-right: 8px;
width: 95%;
}
#compose-headers td.top
{
vertical-align: top;
}
#compose-headers td.title,
#compose-subject td.title
{
width: 80px !important;
font-size: 11px;
font-weight: bold;
padding-right: 10px;
white-space: nowrap;
color: #666;
}
#compose-headers td textarea,
#compose-headers td input
{
resize: none;
width: 100%;
border: 1px solid #999;
}
#compose-headers td textarea
{
height: 32px;
}
input.from_address
{
width: 80% !important;
}
#compose-cc,
#compose-bcc,
#compose-replyto,
#compose-followupto
{
display: none;
}
#compose-editorfooter
{
position: absolute;
right: 5px;
bottom: 0;
text-align: right;
line-height: 20px;
}
#compose-editorfooter label
{
font-size: 11px;
font-weight: bold;
color: #666;
}
#compose-buttons
{
position: absolute;
left: 5px;
bottom: 1px;
width: auto;
}
#compose-contacts
{
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 195px;
border: 1px solid #999;
background-color: #F9F9F9;
}
#compose-attachments
{
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 1px solid #999;
background-color: #F9F9F9;
}
#compose-attachments.droptarget.hover
{
background-color: #F0F0EE;
box-shadow: 0 0 5px 0 #999;
-moz-box-shadow: 0 0 5px 0 #999;
-o-box-shadow: 0 0 5px 0 #999;
}
#compose-attachments ul
{
margin: 0px;
padding: 0px;
background-color: #FFF;
list-style-image: none;
list-style-type: none;
}
#compose-attachments ul li
{
height: 18px;
line-height: 16px;
font-size: 11px;
padding: 2px 2px 1px 2px;
border-bottom: 1px solid #EBEBEB;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
}
#compose-attachments li a
{
text-indent: -5000px;
width: 17px;
height: 16px;
padding-bottom: 2px;
display: inline-block;
text-decoration: none;
vertical-align: middle;
}
#compose-attachments li img
{
vertical-align: middle;
}
#compose-attachments li a.delete,
#compose-attachments li a.cancelupload
{
background-position: 0px -392px;
}
#compose-attachments li span
{
line-height: 18px;
vertical-align: middle;
}
#upload-form,
#attachment-form
{
padding: 6px;
}
#upload-form div,
#attachment-form div
{
padding: 2px;
}
#upload-form div.buttons,
#attachment-form div.buttons
{
margin-top: 4px;
}
#quota
{
position: absolute;
top: 3px;
right: 8px;
width: 100px;
}
#quotaimg
{
position: absolute;
top: 3px;
right: 6px;
z-index: 101;
}
/* addressbook in compose - copy from addressbook.css */
#directorylist
{
list-style: none;
margin: 0;
padding: 0;
background-color: #FFFFFF;
}
#directorylist li
{
display: block;
font-size: 11px;
background: url(images/icons/folders.png) 5px -108px no-repeat;
border-bottom: 1px solid #EBEBEB;
white-space: nowrap;
}
#directorylist li a
{
cursor: default;
display: block;
padding-left: 25px;
padding-top: 2px;
padding-bottom: 2px;
text-decoration: none;
white-space: nowrap;
height: 15px;
}
#directorylist li.selected
{
background-color: #929292;
border-bottom: 1px solid #898989;
}
#directorylist li.selected a
{
color: #FFF;
font-weight: bold;
}
#contacts-table
{
width: 100%;
table-layout: fixed;
}
#contacts-table tbody td
{
cursor: default;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
}
#contacts-table td span.email
{
display: inline;
color: #ccc;
font-style: italic;
margin-left: 0.5em;
}
#abookcountbar
{
margin-top: 4px;
margin-left: 4px;
position: absolute;
margin-right: 5px;
right: 0;
}
#abookactions
{
position: absolute;
text-underline: none;
}
#abookactions a
{
font-weight: bold;
line-height: 22px;
height: 22px;
width: auto;
margin: 0;
padding-left: 5px;
padding-right: 5px;
text-shadow: 1px 1px white;
background: url("images/icons/groupactions.png") no-repeat right -70px;
}
#abookactions a.disabled
{
color: #999;
}
#compose-contacts #quicksearchbar
{
top: 2px;
left: 7px;
}
#compose-contacts #directorylist
{
width: 100%;
top: 23px;
position: absolute;
border-top: 1px solid #eee;
}
#compose-contacts #contacts-table
{
top: 45px;
position: absolute;
}
diff --git a/skins/larry/styles.css b/skins/larry/styles.css
index d7ad74c01..add47324d 100644
--- a/skins/larry/styles.css
+++ b/skins/larry/styles.css
@@ -1,2815 +1,2821 @@
/**
* Roundcube webmail styles for skin "Larry"
*
* Copyright (c) 2012, 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: url(images/linen.jpg) repeat #d1d5d8;
margin: 0;
}
body.noscroll {
/* also avoids bounce effect in Chrome and Safari */
overflow: hidden;
}
a {
color: #0069a6;
}
a:visited {
color: #0186ba;
}
img {
border: 0;
}
.voice {
display: none;
}
input[type="text"],
input[type="password"],
textarea {
margin: 0; /* Safari by default adds a margin */
padding: 4px;
border: 1px solid #b2b2b2;
border-radius: 4px;
box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
-moz-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
-webkit-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
-o-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
}
input[type="text"]:focus,
input[type="password"]:focus,
input[type="text"]:required,
input[type="password"]:required,
input.button:focus,
textarea:focus {
border-color: #4787b1;
box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-moz-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-webkit-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-o-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
outline: none;
}
input.placeholder,
textarea.placeholder {
color: #aaa;
}
.bold {
font-weight: bold;
}
/* fixes vertical alignment of checkboxes and labels */
label input,
label span {
vertical-align: middle;
}
/*** buttons ***/
input.button {
display: inline-block;
margin: 0 2px;
padding: 2px 5px;
color: #525252;
text-shadow: 0px 1px 1px #fff;
border: 1px solid #c0c0c0;
border-radius: 4px;
background: #f7f7f7;
background: -moz-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f9f9f9), color-stop(100%,#e6e6e6));
background: -o-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
background: -ms-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
background: linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
-o-box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
-webkit-box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
-moz-box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
text-decoration: none;
outline: none;
}
.formbuttons input.button {
color: #ddd;
font-size: 110%;
text-shadow: 0px 1px 1px #333;
padding: 4px 12px;
border-color: #465864;
border-radius: 5px;
background: #7a7b7d;
background: -moz-linear-gradient(top, #7b7b7b 0%, #606060 100%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#7b7b7b), color-stop(100%,#606060)); /* Chrome,Safari4+ */
background: -o-linear-gradient(top, #7b7b7b 0%, #606060 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, #7b7b7b 0%, #606060 100%); /* IE10+ */
background: linear-gradient(top, #7b7b7b 0%, #606060 100%); /* W3C */
box-shadow: 0 1px 1px 0 #ccc, inset 0 1px 0 0 #888;
-o-box-shadow: 0 1px 1px 0 #ccc, inset 0 1px 0 0 #888;
-webkit-box-shadow: 0 1px 1px 0 #ccc, inset 0 1px 0 0 #888;
-moz-box-shadow: 0 1px 1px 0 #ccc, inset 0 1px 0 0 #888;
}
.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), inset 0 1px 0 0 #888;
-moz-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.6), inset 0 1px 0 0 #888;
-webkit-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.6), inset 0 1px 0 0 #888;
-o-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.6), inset 0 1px 0 0 #888;
}
.formbuttons input.button:active {
color: #fff;
background: -moz-linear-gradient(top, #5c5c5c 0%, #7b7b7b 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#5c5c5c), color-stop(100%,#7b7b7b));
background: -o-linear-gradient(top, #5c5c5c 0%, #7b7b7b 100%);
background: -ms-linear-gradient(top, #5c5c5c 0%, #7b7b7b 100%);
background: linear-gradient(top, #5c5c5c 0%, #7b7b7b 100%);
}
input.button.mainaction {
color: #ededed;
text-shadow: 0px 1px 1px #333;
border-color: #1f262c;
background: #505050;
background: -moz-linear-gradient(top, #505050 0%, #2a2e31 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#505050), color-stop(100%,#2a2e31));
background: -o-linear-gradient(top, #505050 0%, #2a2e31 100%);
background: -ms-linear-gradient(top, #505050 0%, #2a2e31 100%);
background: linear-gradient(top, #505050 0%, #2a2e31 100%);
box-shadow: inset 0 1px 0 0 #777;
-moz-box-shadow: inset 0 1px 0 0 #777;
-webkit-box-shadow: inset 0 1px 0 0 #777;
-o-box-shadow: inset 0 1px 0 0 #777;
}
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: -o-linear-gradient(top, #2a2e31 0%, #505050 100%);
background: -ms-linear-gradient(top, #2a2e31 0%, #505050 100%);
background: linear-gradient(top, #2a2e31 0%, #505050 100%);
}
input.button[disabled],
input.button[disabled]:hover,
input.button.mainaction[disabled] {
color: #aaa !important;
}
input.mainaction {
font-weight: bold;
}
/** link buttons **/
a.button,
.buttongroup {
display: inline-block;
margin: 0 2px;
padding: 2px 5px;
color: #525252;
text-shadow: 0px 1px 1px #fff;
border: 1px solid #c6c6c6;
border-radius: 4px;
background: #f7f7f7;
background: -moz-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f9f9f9), color-stop(100%,#e6e6e6));
background: -o-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
background: -ms-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
background: linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
-o-box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
-webkit-box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
-moz-box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
text-decoration: none;
}
.buttongroup {
padding: 0;
white-space: nowrap;
}
a.button:focus,
input.button:focus {
border-color: #4fadd5;
box-shadow: 0 0 2px 1px rgba(71,135,177, 0.6);
-moz-box-shadow: 0 0 2px 1px rgba(71,135,177, 0.6);
-webkit-box-shadow: 0 0 2px 1px rgba(71,135,177, 0.6);
-o-box-shadow: 0 0 2px 1px rgba(71,135,177, 0.6);
outline: none;
}
label.disabled,
a.button.disabled {
color: #999;
}
a.button.disabled,
input.button.disabled,
input.button[disabled],
a.button.disabled:hover,
input.button.disabled:hover,
input.button[disabled]:hover {
border-color: #c6c6c6;
box-shadow: 0 1px 1px 0 rgba(160, 160, 160, 0.4);
-o-box-shadow: 0 1px 1px 0 rgba(160, 160, 160, 0.4);
-webkit-box-shadow: 0 1px 1px 0 rgba(160, 160, 160, 0.4);
-moz-box-shadow: 0 1px 1px 0 rgba(160, 160, 160, 0.4);
}
a.button.disabled span.inner {
opacity: 0.4;
filter: alpha(opacity=40);
}
.buttongroup a.button {
margin: 0;
border-width: 0 1px 0 0;
border-radius: 0;
background: none;
box-shadow: none;
-o-box-shadow: none;
-webkit-box-shadow: none;
-moz-box-shadow: 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,
input.button:active {
background: #e6e6e6;
background: -moz-linear-gradient(top, #e6e6e6 0%, #f9f9f9 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#e6e6e6), color-stop(100%,#f9f9f9));
background: -o-linear-gradient(top, #e6e6e6 0%, #f9f9f9 100%);
background: -ms-linear-gradient(top, #e6e6e6 0%, #f9f9f9 100%);
background: linear-gradient(top, #e6e6e6 0%, #f9f9f9 100%);
}
.pagenav.dark a.button {
font-weight: bold;
border-color: #e6e6e6;
background: #d8d8d8;
background: -moz-linear-gradient(top, #d8d8d8 0%, #bababa 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#d8d8d8), color-stop(100%,#bababa));
background: -o-linear-gradient(top, #d8d8d8 0%, #bababa 100%);
background: -ms-linear-gradient(top, #d8d8d8 0%, #bababa 100%);
background: linear-gradient(top, #d8d8d8 0%, #bababa 100%);
box-shadow: 0 1px 1px 0 #999;
-o-box-shadow: 0 1px 1px 0 #999;
-webkit-box-shadow: 0 1px 1px 0 #999;
-moz-box-shadow: 0 1px 1px 0 #999;
}
.pagenav.dark a.button.pressed {
background: #bababa;
background: -moz-linear-gradient(top, #bababa 0%, #d8d8d8 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#bababa), color-stop(100%,#d8d8d8));
background: -o-linear-gradient(top, #bababa 0%, #d8d8d8 100%);
background: -ms-linear-gradient(top, #bababa 0%, #d8d8d8 100%);
background: linear-gradient(top, #bababa 0%, #d8d8d8 100%);
}
.buttongroup a.button.selected,
.buttongroup a.button.selected:hover {
background: #8a8a8a;
background: -moz-linear-gradient(top, #909090 0%, #858585 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#909090), color-stop(100%,#858585));
background: -o-linear-gradient(top, #909090 0%, #858585 100%);
background: -ms-linear-gradient(top, #909090 0%, #858585 100%);
background: linear-gradient(top, #909090 0%, #858585 100%);
-webkit-box-shadow: inset 0 1px 2px 0 #555;
-moz-box-shadow: inset 0 1px 2px 0 #555;
box-shadow: inset 0 1px 2px 0 #555;
border-right-color: #555;
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: -o-linear-gradient(top, #49b3d2 0, #66bcd9 100%);
background: -ms-linear-gradient(top, #49b3d2 0, #66bcd9 100%);
background: linear-gradient(top, #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;
text-shadow: 0px 1px 1px #fff;
min-width: 16em;
}
.pagenavbuttons {
position: relative;
top: -2px;
}
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;
filter: alpha(opacity=40);
cursor: default;
}
a.iconbutton.searchoptions {
width: 24px;
background-position: -2px -317px;
}
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.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 -55px;
}
#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: -o-linear-gradient(top, #eaeaea 0%, #c8c8c8 100%);
background: -ms-linear-gradient(top, #eaeaea 0%, #c8c8c8 100%);
background: linear-gradient(top, #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;
text-shadow: 0 1px 1px #000;
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: -o-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%);
-moz-box-shadow: 0 1px 4px 0 rgba(50,50,50,0.8), inset 0px 1px 0 0px #888;
-webkit-box-shadow: 0 1px 4px 0 rgba(50,50,50,0.8), inset 0px 1px 0 0px #888;
-o-box-shadow: 0 1px 4px 0 rgba(50,50,50,0.8), inset 0px 1px 0 0px #888;
box-shadow: 0 1px 4px 0 rgba(50,50,50,0.8), inset 0px 1px 0 0px #888;
}
#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.loading {
color: #ddd;
}
#messagestack div.loading:after {
top: 4px;
left: 6px;
background: url(images/ajaxloader_dark.gif) 0 4px no-repeat;
}
#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;
text-shadow: 0 1px 1px #fff;
}
.ui-dialog.warning .ui-dialog-title {
color: #960;
background-position: 0 -90px;
}
.ui-dialog.error .ui-dialog-title {
color: #cf2734;
background-position: 0 -60px;
}
.ui-dialog.confirmation .ui-dialog-title {
color: #093;
background-position: 0 -30px;
}
.ui-dialog.popupmessage .ui-dialog-titlebar {
padding: 8px 1em 4px 1em;
background: #e3e3e3;
background: -moz-linear-gradient(top, #e3e3e3 0%, #cfcfcf 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#e3e3e3), color-stop(100%,#cfcfcf));
background: -o-linear-gradient(top, #e3e3e3 0%, #cfcfcf 100%);
background: -ms-linear-gradient(top, #e3e3e3 0%, #cfcfcf 100%);
background: linear-gradient(top, #e3e3e3 0%, #cfcfcf 100%);
}
.ui-dialog.popupmessage .ui-widget-content {
font-size: 12px;
background: #eee;
background: -moz-linear-gradient(top, #eee 0%, #dcdcdc 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#eee), color-stop(100%,#dcdcdc));
background: -o-linear-gradient(top, #eee 0%, #dcdcdc 100%);
background: -ms-linear-gradient(top, #eee 0%, #dcdcdc 100%);
background: linear-gradient(top, #eee 0%, #dcdcdc 100%);
}
/*** basic page layout ***/
#header {
overflow-x: hidden; /* Chrome bug #1488851 */
}
#topline {
height: 18px;
background: url(images/linen_header.jpg) repeat #666;
border-bottom: 1px solid #4f4f4f;
padding: 2px 0 2px 10px;
color: #aaa;
text-align: center;
}
#topnav {
position: relative;
height: 46px;
margin-bottom: 10px;
padding: 0 0 0 10px;
background: #111;
background: -moz-linear-gradient(top, #404040 0%, #060606 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#404040), color-stop(100%,#060606));
background: -o-linear-gradient(top, #404040 0%, #060606 100%);
background: -ms-linear-gradient(top, #404040 0%, #060606 100%);
background: linear-gradient(top, #404040 0%, #060606 100%);
}
#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;
filter: alpha(opacity=94);
-webkit-transition: top 0.3s ease-in-out;
-moz-transition: top 0.3s ease-in-out;
-o-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;
background: -moz-linear-gradient(top, #444 0%, #333 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#444), color-stop(100%,#333));
background: -o-linear-gradient(top, #444 0%, #333 100%);
background: -ms-linear-gradient(top, #444 0%, #333 100%);
background: linear-gradient(top, #444 0%, #333 100%);
color: #eee;
font-weight: bold;
white-space: nowrap;
border: 1px solid #777;
box-shadow: 0 1px 5px 0 #333;
-moz-box-shadow: 0 1px 5px 0 #333;
-webkit-box-shadow: 0 1px 5px 0 #333;
-o-box-shadow: 0 1px 5px 0 #333;
z-index: 200;
white-space: nowrap;
text-shadow: 0px 1px 1px #000;
}
.minimal #taskbar .tooltip:after {
content: "";
position: absolute;
top: -4px;
right: 15px;
border-style: solid;
border-width: 0 4px 4px;
border-color: #888 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;
text-shadow: 0px 1px 1px black;
padding: 5px 0 0 34px;
height: 19px;
background: url(images/buttons.png) -1000px 0 no-repeat;
}
#taskbar a:focus {
color: #fff;
text-shadow: 0px 1px 1px #666;
background-color: rgba(73,180,210,0.7);
outline: none;
}
#taskbar a.button-selected {
color: #3cf;
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;
}
.minimal #mainscreen {
top: 62px;
}
.minimal #mainscreen.offset {
top: 102px;
}
.extwin #mainscreen {
top: 40px;
}
#mainscreen.offset {
top: 132px;
}
#mainscreen .offset {
top: 42px;
}
.uibox {
border: 1px solid #a3a3a3;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 0 2px #999;
-o-box-shadow: 0 0 2px #999;
-webkit-box-shadow: 0 0 2px #999;
-moz-box-shadow: 0 0 2px #999;
background: #fff;
}
.minwidth {
min-width: 1024px;
}
.scroller {
overflow: auto;
}
.readtext {
width: 42em;
padding: 12px;
font-size: 12px;
}
.readtext > h1,
.readtext > h2,
.readtext > h3 {
margin-top: 0;
}
.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 td {
font-size: 12px;
font-weight: bold;
padding: 10px 8px 3px 8px;
height: 20px; /* doesn't affect table-cells in FF */
margin: 0;
text-shadow: 0px 1px 1px #fff;
border-bottom: 1px solid #bbd3da;
white-space: nowrap;
}
.uibox .listing thead td {
padding-bottom: 8px;
height: auto;
}
.uibox .boxtitle,
.uibox .listing thead td {
background: #b0ccd7;
color: #004458;
border-radius: 4px 4px 0 0;
}
.listbox .listitem,
.listbox .tablink,
.listing tbody td,
.listing li {
display: block;
border-top: 1px solid #fff;
border-bottom: 1px solid #bbd3da;
cursor: default;
font-weight: normal;
}
.listbox .listitem a,
.listbox .tablink a,
.listing tbody td,
.listing li a {
display: block;
color: #376572;
text-shadow: 0px 1px 1px #fff;
text-decoration: none;
cursor: default;
padding: 6px 8px 2px 8px;
height: 17px; /* doesn't affect table-cells in FF */
white-space: nowrap;
}
.listing tbody td {
display: table-cell;
padding-bottom: 5px;
height: auto;
min-height: 14px;
outline: none;
}
+.listing tbody td a {
+ color: #376572;
+ text-shadow: 0px 1px 1px #fff;
+ 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: 0;
padding-left: 6px;
}
.listing.iconized thead tr td:first-child,
.listing.iconized tbody tr td:first-child {
padding-left: 36px;
}
/* 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 #b0ccd7;
padding-left: 4px;
}
.listing.iconized.focus tbody tr.focused > td:first-child {
padding-left: 34px;
}
.listing.focus tbody tr.selected.focused > td:first-child {
border-left-color: #9ec2d0;
}
.listing.inconized.focus tr.focused td:first-child {
padding-left: 34px;
}
.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 {
color: #fff !important;
background-color: rgba(73,180,210,0.6);
text-shadow: 0px 1px 1px #666;
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 li ul li a {
padding-left: 62px;
}
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 li ul li div.treetoggle {
left: 35px;
}
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 #ccdde4;
background: #d9ecf4;
-webkit-box-shadow: inset 0 1px 0 0 #fff;
-moz-box-shadow: inset 0 1px 0 0 #fff;
box-shadow: inset 0 1px 0 0 #fff;
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);
text-shadow: 0px 1px 1px #666;
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;
filter: alpha(opacity=40);
}
.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;
filter: alpha(opacity=40);
}
.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 td {
color: #69939e;
font-size: 11px;
font-weight: bold;
background: #d6eaf3;
background: -moz-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%);
background: -webkit-gradient(linear, left top, right top, color-stop(0,#e3f2f6), color-stop(8%,#d6eaf3), color-stop(100%,#d6eaf3));
background: -o-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%);
background: -ms-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px ,#d6eaf3 100%);
background: linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%);
border-left: 1px solid #bbd3da;
padding: 8px 7px;
overflow: hidden;
text-overflow: ellipsis;
}
.records-table.sortheader thead td {
padding: 0;
}
.records-table thead td a,
.records-table thead td span {
display: block;
padding: 7px 7px;
color: #69939e;
text-decoration: none;
overflow: hidden;
text-overflow: ellipsis;
}
.records-table thead td a:focus {
color: #fff;
background-color: rgba(73,180,210,0.7);
text-shadow: 0px 1px 1px #666;
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 td:first-child,
.records-table tbody tr td:first-child {
border-left: 0;
padding-left: 6px;
}
/* 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 #b0ccd7;
padding-left: 4px;
}
.records-table.focus tbody tr.selected.focused > td:first-child {
border-left-color: #49b3d2;
}
.records-table tr.selected td {
color: #fff !important;
background-color: #4db0d2 !important;
}
.records-table.focus tr.selected td {
background: #019bc6;
background: -moz-linear-gradient(top, #019bc6 0%, #017cb4 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#019bc6), color-stop(100%,#017cb4));
background: -o-linear-gradient(top, #019bc6 0%, #017cb4 100%);
background: -ms-linear-gradient(top, #019bc6 0%, #017cb4 100%);
background: linear-gradient(top, #019bc6 0%, #017cb4 100%);
}
.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: #eee;
background: -moz-linear-gradient(top, #eee 0%, #dfdfdf 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#eee), color-stop(100%,#dfdfdf));
background: -o-linear-gradient(top, #eee 0%, #dfdfdf 100%);
background: -ms-linear-gradient(top, #eee 0%, #dfdfdf 100%);
background: linear-gradient(top, #eee 0%, #dfdfdf 100%);
border-bottom: 1px solid #ccc;
}
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;
}
.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;
}
#pluginbody {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
/*** Login form ***/
#login-form {
position: relative;
width: 580px;
margin: 20ex auto 2ex auto;
}
#login-form .box-inner {
width: 430px;
background: url(images/linen_login.jpg) top left no-repeat #5c5c5c;
margin: 0 50px;
padding: 10px 24px 24px 24px;
border: 1px solid #333;
border-radius: 5px;
box-shadow: inset 0 0 1px #ccc;
-o-box-shadow: inset 0 0 1px #ccc;
-webkit-box-shadow: inset 0 0 1px #ccc;
-moz-box-shadow: inset 0 0 1px #ccc;
}
#login-form .box-bottom {
background: url(images/login_shadow.png) top center no-repeat;
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 input.button {
color: #444;
text-shadow: 0px 1px 1px #fff;
border-color: #f9f9f9;
background: #f9f9f9;
background: -moz-linear-gradient(top, #f9f9f9 0%, #e2e2e2 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f9f9f9), color-stop(100%,#e2e2e2));
background: -o-linear-gradient(top, #f9f9f9 0%, #e2e2e2 100%);
background: -ms-linear-gradient(top, #f9f9f9 0%, #e2e2e2 100%);
background: linear-gradient(top, #f9f9f9 0%, #e2e2e2 100%);
box-shadow: inset 0 1px 0 0 #fff;
-moz-box-shadow: inset 0 1px 0 0 #fff;
-webkit-box-shadow: inset 0 1px 0 0 #fff;
-o-box-shadow: inset 0 1px 0 0 #fff;
}
#login-form input.button:hover,
#login-form input.button:focus {
box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9), inset 0 1px 0 0 #fff;
-moz-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9), inset 0 1px 0 0 #fff;
-webkit-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9), inset 0 1px 0 0 #fff;
-o-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9), inset 0 1px 0 0 #fff;
}
#login-form input.button:active {
color: #333;
background: -moz-linear-gradient(top, #dcdcdc 0%, #f9f9f9 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#dcdcdc), color-stop(100%,#f9f9f9));
background: -o-linear-gradient(top, #dcdcdc 0%, #f9f9f9 100%);
background: -ms-linear-gradient(top, #dcdcdc 0%, #f9f9f9 100%);
background: linear-gradient(top, #dcdcdc 0%, #f9f9f9 100%);
}
#login-form form table {
width: 98%;
}
#login-form td.title {
width: 20%;
white-space: nowrap;
color: #cecece;
text-shadow: 0px 1px 1px black;
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 #searchmenulink,
#quicksearchbar #searchmenulink {
position: absolute;
top: 5px;
left: 6px;
}
.searchbox #searchreset,
#quicksearchbar #searchreset {
position: absolute;
top: 4px;
right: 1px;
}
/*** 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: 75px;
height: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 28px 2px 0 2px;
text-shadow: 0px 1px 1px #eee;
box-shadow: none;
-moz-box-shadow: none;
-webkit-box-shadow: none;
-o-box-shadow: none;
background: url(images/buttons.png) -100px 0 no-repeat transparent;
border: 0;
border-radius: 0;
}
.dropbutton .dropbuttontip:focus,
.toolbar a.button:focus {
color: #fff;
text-shadow: 0px 1px 1px #666;
background-color: rgba(30,150,192, 0.5);
border-radius: 3px;
}
.toolbar a.button.disabled {
opacity: 0.4;
filter: alpha(opacity=40);
}
.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;
filter: alpha(opacity=50);
}
.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: left -610px;
}
.toolbar a.button.forward {
min-width: 64px;
background-position: left -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: left -930px;
}
.toolbar a.button.spellcheck.selected {
background-position: left -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: 74px;
background-position: center -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;
}
a.menuselector {
display: inline-block;
border: 1px solid #ababab;
border-radius: 4px;
background: #f8f8f8;
background: -moz-linear-gradient(top, #f8f8f8 0%, #dddddd 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f8f8f8), color-stop(100%,#dddddd));
background: -o-linear-gradient(top, #f8f8f8 0%, #dddddd 100%);
background: -ms-linear-gradient(top, #f9f9f9 0%, #dddddd 100%);
background: linear-gradient(top, #f8f8f8 0%, #dddddd 100%);
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;
text-shadow: 0px 1px 1px #fff;
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: -o-linear-gradient(top, #dddddd 0%, #f8f8f8 100%);
background: -ms-linear-gradient(top, #dddddd 0%, #f8f8f8 100%);
background: linear-gradient(top, #dddddd 0%, #f8f8f8 100%);
text-decoration: none;
}
select.decorated {
position: relative;
z-index: 10;
opacity: 0;
height: 22px;
cursor: pointer;
filter: alpha(opacity=0);
-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;
text-shadow: 0px 1px 1px #333;
padding: 4px 6px;
outline: none;
cursor: default;
}
a.menuselector:focus,
a.menuselector.focus,
a.iconbutton:focus,
.pagenav a.button:focus {
border-color: #4fadd5;
-webkit-box-shadow: 0 0 4px 2px rgba(71,135,177, 0.8);
-moz-box-shadow: 0 0 4px 2px rgba(71,135,177, 0.8);
-o-box-shadow: 0 0 4px 2px rgba(71,135,177, 0.8);
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;
text-shadow: 0px 1px 1px #fff;
padding-left: 30px;
background: url(images/quota.png) -100px 0 no-repeat;
}
/*** 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;
border: 1px solid #999;
z-index: 240;
border-radius: 4px;
box-shadow: 0 2px 6px 0 #333;
-moz-box-shadow: 0 2px 6px 0 #333;
-webkit-box-shadow: 0 2px 6px 0 #333;
-o-box-shadow: 0 2px 6px 0 #333;
}
.popupmenu.dropdown {
border-radius: 0 0 4px 4px;
border-top: 0;
}
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;
border-bottom: 1px solid #333;
}
.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-shadow: 0px 1px 1px #333;
text-decoration: none;
min-height: 14px;
padding: 6px 10px 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,
select.decorated option:hover,
select.decorated option[selected='selected'] {
background-color: #00aad6;
background: -moz-linear-gradient(top, #00aad6 0%, #008fc9 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#00aad6), color-stop(100%,#008fc9));
background: -o-linear-gradient(top, #00aad6 0%, #008fc9 100%);
background: -ms-linear-gradient(top, #00aad6 0%, #008fc9 100%);
background: linear-gradient(top, #00aad6 0%, #008fc9 100%);
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;
text-shadow: 0px 1px 1px #333;
}
ul.toolbarmenu li.separator label {
color: #bbb;
font-style: italic;
}
ul.toolbarmenu li a.icon {
color: #eee;
padding: 2px 6px;
}
ul.toolbarmenu li span.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;
filter: alpha(opacity=20);
}
ul.toolbarmenu li a.active span.icon {
opacity: 0.99;
filter: alpha(opacity=100);
}
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.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;
}
#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-shadow: 0px 1px 1px #333;
text-decoration: none;
min-height: 14px;
padding: 6px 10px 6px 10px;
border: 0;
cursor: default;
}
.popupdialog {
display: none;
padding: 10px;
}
.popupdialog .formbuttons {
margin: 20px 0 4px 0;
}
.ui-dialog .prompt input {
display: block;
margin: 8px 0;
}
.hint {
margin: 4px 0;
color: #999;
text-shadow: 0px 1px 1px #fff;
}
.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;
-moz-box-shadow: 0 2px 6px 0 #333;
-webkit-box-shadow: 0 2px 6px 0 #333;
-o-box-shadow: 0 2px 6px 0 #333;
z-index: 250;
color: #ccc;
white-space: nowrap;
opacity: 0.92;
filter: alpha(opacity=92);
text-shadow: 0px 1px 1px #333;
}
#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;
}
/*** 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 {
opacity: .2;
}
#folder-selector li a.inbox span {
background-position: 4px -2049px;
}
#folder-selector li a.drafts span {
background-position: 4px -1388px;
}
#folder-selector li a.sent span {
background-position: 4px -2074px;
}
#folder-selector li a.trash span {
background-position: 4px -1508px;
}
#folder-selector li a.junk span {
background-position: 4px -2100px;
}
/*** 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;
}
.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.txt,
.attachmentslist li.text {
background-position: 0 -416px;
}
.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 a,
#compose-attachments ul li {
display: block;
color: #333;
font-weight: bold;
padding: 3px 15px 3px 30px;
text-shadow: 0px 1px 1px #fff;
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;
}
.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: 28px;
}
.attachmentslist li a:hover {
text-decoration: underline;
}
.attachmentslist li.uploading {
background: url(images/ajaxloader.gif) 2px 6px no-repeat;
}
.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: url(images/buttons.png) -7px -337px no-repeat;
}
.attachmentslist li a.cancelupload {
background-position: -7px -377px;
}
/*** fieldset tabs ***/
.tabsbar {
margin-bottom: 12px;
padding-top: 15px;
height: 27px;
white-space: nowrap;
}
.ui-dialog-content .tabsbar {
margin-bottom: 0;
}
.tabsbar .tablink {
padding: 15px 1px 15px 0;
background: #f8f8f8;
background: -moz-linear-gradient(top, #f8f8f8 0%, #d3d3d3 50%, #f8f8f8 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f8f8f8), color-stop(50%,#d3d3d3), color-stop(100%,#f8f8f8));
background: -webkit-linear-gradient(top, #f8f8f8 0%, #d3d3d3 50%, #f8f8f8 100%);
background: -o-linear-gradient(top, #f8f8f8 0%, #d3d3d3 50%, #f8f8f8 100%);
background: -ms-linear-gradient(top, #f8f8f8 0%, #d3d3d3 50%, #f8f8f8 100%);
background: linear-gradient(top, #f8f8f8 0%, #d3d3d3 50%, #f8f8f8 100%);
}
.tabsbar .tablink:last-child {
background: none;
}
.tabsbar .tablink:last-child a {
border-right: 0;
}
.tabsbar .tablink a {
padding: 15px;
color: #999;
font-size: 12px;
font-weight: bold;
text-decoration: none;
background: #fff;
border-right: 1px solid #fafafa;
}
.tabsbar .tablink.selected a {
color: #004458;
background: #f6f6f6;
background: -moz-linear-gradient(top, #fff 40%, #efefef 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(40%,#fff), color-stop(100%,#efefef));
background: -o-linear-gradient(top, #fff 40%, #efefef 100%);
background: -ms-linear-gradient(top, #fff 40%, #efefef 100%);
background: linear-gradient(top, #fff 40%, #efefef 100%);
}
fieldset.tab {
border: 0;
padding: 0;
margin-left: 0;
}
diff --git a/skins/larry/templates/compose.html b/skins/larry/templates/compose.html
index 87993ed70..c643b36f5 100644
--- a/skins/larry/templates/compose.html
+++ b/skins/larry/templates/compose.html
@@ -1,223 +1,223 @@
<roundcube:object name="doctype" value="html5" />
<html>
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
<roundcube:if condition="config:enable_spellcheck" />
<link rel="stylesheet" type="text/css" href="/googiespell.css" />
<roundcube:endif />
</head>
<roundcube:if condition="env:extwin" /><body class="extwin"><roundcube:else /><body><roundcube:endif />
<roundcube:include file="/includes/header.html" />
<div id="mainscreen">
<h1 class="voice"><roundcube:object name="pagetitle" /></h1>
<!-- toolbar -->
<h2 id="aria-label-toolbar" class="voice"><roundcube:label name="arialabeltoolbar" /></h2>
<div id="messagetoolbar" class="toolbar fullwidth" role="toolbar" aria-labelledby="aria-label-toolbar">
<roundcube:button command="list" type="link" class="button back disabled" classAct="button back" label="cancel" condition="!env:extwin" tabindex="2" />
<roundcube:button command="close" type="link" class="button close disabled" classAct="button close" label="cancel" condition="env:extwin" tabindex="2" />
<span class="spacer"></span>
<roundcube:button command="send" type="link" class="button send disabled" classAct="button send" label="send" title="sendmessage" tabindex="2" />
<roundcube:button command="savedraft" type="link" class="button savedraft disabled" classAct="button savedraft" label="save" title="savemessage" tabindex="2" />
<span class="spacer"></span>
<roundcube:if condition="config:enable_spellcheck" />
<span class="dropbutton">
<roundcube:button command="spellcheck" type="link" class="button spellcheck disabled" classAct="button spellcheck" classSel="button spellcheck pressed" label="spellcheck" title="checkspelling" tabindex="2" />
<a href="#languages" class="dropbuttontip" id="spellmenulink" onclick="UI.toggle_popup('spellmenu',event);return false" aria-haspopup="true" aria-expanded="false"tabindex="2">Select Spell Language</a>
</span>
<roundcube:endif />
<roundcube:button name="addattachment" type="link" class="button attach" label="attach" title="addattachment" onclick="UI.show_uploadform(event);return false" aria-haspopup="true" aria-expanded="false"tabindex="2" />
<roundcube:button command="insert-sig" type="link" class="button insertsig disabled" classAct="button insertsig" label="signature" title="insertsignature" tabindex="2" />
<a href="#responses" class="button responses" label="responses" title="<roundcube:label name='insertresponse' />" id="responsesmenulink" unselectable="on" onmousedown="return false" onclick="UI.toggle_popup('responsesmenu',event);return false" tabindex="2" aria-haspopup="true" aria-expanded="false"aria-owns="textresponsesmenu"><roundcube:label name="responses" /></a>
<roundcube:container name="toolbar" id="compose-toolbar" />
</div>
<div id="mainscreencontent">
<div id="composeview-left">
<!-- inline address book -->
<div id="compose-contacts" class="uibox listbox" role="region" aria-labelledby="aria-label-composecontacts">
<h2 id="aria-label-composecontacts" class="boxtitle"><roundcube:label name="contacts" /></h2>
<div id="composequicksearch" role="search" aria-labelledby="aria-label-composequicksearch">
<h3 id="aria-label-composequicksearch" class="voice"><roundcube:label name="arialabelcontactquicksearch" /></h3>
<div class="searchbox">
<label for="contactsearchbox" class="voice"><roundcube:label name="arialabelcontactsearchbox" /></label>
<roundcube:object name="searchform" id="contactsearchbox" />
<a id="searchmenulink" class="iconbutton searchoptions"> </a>
<roundcube:button command="reset-search" id="searchreset" class="iconbutton reset" title="resetsearch" content=" " />
</div>
</div>
- <roundcube:object name="addressbooks" id="directorylist" class="treelist listing" />
+ <roundcube:object name="addressbooks" id="directorylist" class="treelist listing" summary="ariasummarycomposecontacts" />
<div class="scroller withfooter" tabindex="-1">
<roundcube:object name="addresslist" id="contacts-table" class="listing iconized" noheader="true" />
</div>
<div class="boxfooter">
<roundcube:button command="add-recipient" prop="to" type="link" title="to" class="listbutton addto disabled" classAct="listbutton addto" innerClass="inner" content="To+" /><roundcube:button command="add-recipient" prop="cc" type="link" title="cc" class="listbutton addcc disabled" classAct="listbutton addcc" innerClass="inner" content="Cc+" /><roundcube:button command="add-recipient" prop="bcc" type="link" title="bcc" class="listbutton addbcc disabled" classAct="listbutton addbcc" innerClass="inner" content="Bcc+" />
</div>
<div class="boxpagenav">
<roundcube:button command="firstpage" type="link" class="icon firstpage disabled" classAct="icon firstpage" title="firstpage" label="first" />
<roundcube:button command="previouspage" type="link" class="icon prevpage disabled" classAct="icon prevpage" title="previouspage" label="previous" />
<roundcube:button command="nextpage" type="link" class="icon nextpage disabled" classAct="icon nextpage" title="nextpage" label="next" />
<roundcube:button command="lastpage" type="link" class="icon lastpage disabled" classAct="icon lastpage" title="lastpage" label="last" />
</div>
</div>
</div>
<div id="composeview-right" role="main">
<roundcube:form name="form" method="post" id="compose-content" class="uibox">
<!-- message headers -->
<div id="composeheaders" role="region" aria-labelledby="aria-label-composeheaders">
<h2 id="aria-label-composeheaders" class="voice"><roundcube:label name="arialabelmessageheaders" /></h2>
<a href="#options" id="composeoptionstoggle" class="moreheaderstoggle" title="<roundcube:label name='togglecomposeoptions' />" aria-exapnded="false"><span class="iconlink"></span></a>
<table class="headers-table compose-headers">
<tbody>
<tr>
<td class="title"><label for="_from"><roundcube:label name="from" /></label></td>
<td class="editfield">
<roundcube:object name="composeHeaders" part="from" form="form" id="_from" tabindex="1" />
<a href="#identities" onclick="return rcmail.command('identities')" class="iconlink edit" tabindex="0"><roundcube:label name="editidents" /></a>
</td>
</tr><tr>
<td class="title top"><label for="_to"><roundcube:label name="to" /></label></td>
<td class="editfield"><roundcube:object name="composeHeaders" part="to" form="form" id="_to" cols="70" rows="1" tabindex="1" aria-required="true" /></td>
</tr><tr id="compose-cc">
<td class="title top">
<label for="_cc"><roundcube:label name="cc" /></label>
- <a href="#cc" onclick="return UI.hide_header_row('cc');" class="iconbutton cancel" title="<roundcube:label name='delete' /> <roundcube:label name='cc' />" tabindex="3">x</a>
+ <a href="#cc" onclick="return UI.hide_header_row('cc');" class="iconbutton cancel" title="<roundcube:label name='delete' />" tabindex="3"><roundcube:label name="delete" /> <roundcube:label name="cc" /></a>
</td>
<td class="editfield"><roundcube:object name="composeHeaders" part="cc" form="form" id="_cc" cols="70" rows="1" tabindex="1" /></td>
</tr><tr id="compose-bcc">
<td class="title top">
<label for="_bcc"><roundcube:label name="bcc" /></label>
- <a href="#bcc" onclick="return UI.hide_header_row('bcc');" class="iconbutton cancel" title="<roundcube:label name='delete' /> <roundcube:label name='bcc' />" tabindex="3">x</a>
+ <a href="#bcc" onclick="return UI.hide_header_row('bcc');" class="iconbutton cancel" title="<roundcube:label name='delete' />" tabindex="3"><roundcube:label name="delete" /> <roundcube:label name="bcc" /></a>
</td>
<td class="editfield"><roundcube:object name="composeHeaders" part="bcc" form="form" id="_bcc" cols="70" rows="1" tabindex="1" /></td>
</tr><tr id="compose-replyto">
<td class="title top">
<label for="_replyto"><roundcube:label name="replyto" /></label>
- <a href="#replyto" onclick="return UI.hide_header_row('replyto');" class="iconbutton cancel" title="<roundcube:label name='delete' /> <roundcube:label name='replyto' />" tabindex="3">x</a>
+ <a href="#replyto" onclick="return UI.hide_header_row('replyto');" class="iconbutton cancel" title="<roundcube:label name='delete' />" tabindex="3"><roundcube:label name="delete" /> <roundcube:label name="replyto" /></a>
</td>
<td class="editfield"><roundcube:object name="composeHeaders" part="replyto" form="form" id="_replyto" size="70" tabindex="1" /></td>
</tr><tr id="compose-followupto">
<td class="title top">
<label for="_followupto"><roundcube:label name="followupto" /></label>
- <a href="#followupto" onclick="return UI.hide_header_row('followupto');" class="iconbutton cancel" title="<roundcube:label name='delete' /> <roundcube:label name='followupto' />" tabindex="3">x</a>
+ <a href="#followupto" onclick="return UI.hide_header_row('followupto');" class="iconbutton cancel" title="<roundcube:label name='delete' />" tabindex="3"><roundcube:label name="delete" /> <roundcube:label name="followupto" /></a>
</td>
<td class="editfield"><roundcube:object name="composeHeaders" part="followupto" form="form" id="_followupto" size="70" tabindex="1" /></td>
</tr><tr>
<td></td>
<td class="formlinks">
<a href="#cc" onclick="return UI.show_header_row('cc')" id="cc-link" class="iconlink add" tabindex="3"><roundcube:label name="addcc" /></a>
<a href="#bcc" onclick="return UI.show_header_row('bcc')" id="bcc-link" class="iconlink add" tabindex="3"><roundcube:label name="addbcc" /></a>
<a href="#reply-to" onclick="return UI.show_header_row('replyto')" id="replyto-link" class="iconlink add" tabindex="3"><roundcube:label name="addreplyto" /></a>
<a href="#followup-to" onclick="return UI.show_header_row('followupto')" id="followupto-link" class="iconlink add" tabindex="3"><roundcube:label name="addfollowupto" /></a>
</td>
</tr><tr>
<td class="title"><label for="compose-subject"><roundcube:label name="subject" /></label></td>
<td class="editfield"><roundcube:object name="composeSubject" id="compose-subject" form="form" tabindex="1" /></td>
</tr>
</tbody>
</table>
<div id="composebuttons" class="formbuttons">
<roundcube:button command="extwin" type="link" class="button extwin" classSel="button extwin pressed" innerClass="icon" title="openinextwin" label="openinextwin" condition="!env:extwin" />
</div>
<!-- (collapsable) message options -->
<div id="composeoptions" role="region" aria-labelledby="aria-label-composeoptions">
<h2 id="aria-label-composeoptions" class="voice"><roundcube:label name="arialabelcomposeoptions" /></h2>
<roundcube:if condition="!in_array('htmleditor', (array)config:dont_override)" />
<span class="composeoption">
<label><roundcube:label name="editortype" />
<roundcube:object name="editorSelector" editorid="composebody" tabindex="4" /></label>
</span>
<roundcube:endif />
<span class="composeoption">
<label for="rcmcomposepriority"><roundcube:label name="priority" />
<roundcube:object name="prioritySelector" form="form" id="rcmcomposepriority" tabindex="4" /></label>
</span>
<span class="composeoption">
<label><roundcube:object name="receiptCheckBox" form="form" id="rcmcomposereceipt" tabindex="4" /> <roundcube:label name="returnreceipt" /></label>
</span>
<roundcube:if condition="config:smtp_server != ''" />
<span class="composeoption">
<label><roundcube:object name="dsnCheckBox" form="form" id="rcmcomposedsn" tabindex="4" /> <roundcube:label name="dsn" /></label>
</span>
<roundcube:endif />
<roundcube:if condition="!config:no_save_sent_messages" />
<span class="composeoption">
<label><roundcube:label name="savesentmessagein" /> <roundcube:object name="storetarget" maxlength="30" style="max-width:12em" tabindex="4" /></label>
</span>
<roundcube:endif />
<roundcube:container name="composeoptions" id="composeoptions" />
</div>
</div>
<!-- message compose body -->
<div id="composeview-bottom">
<div id="composebodycontainer">
<label for="composebody" class="voice"><roundcube:label name="arialabelmessagebody" /></label>
<roundcube:object name="composeBody" id="composebody" form="form" cols="70" rows="20" tabindex="1" />
</div>
<div id="compose-attachments" class="rightcol" role="region" aria-labelledby="aria-label-composeattachments">
<h2 id="aria-label-composeattachments" class="voice"><roundcube:label name="attachments" /></h2>
<div style="text-align:center; margin-bottom:20px">
<roundcube:button name="addattachment" type="input" class="button" classSel="button pressed" label="addattachment" onclick="UI.show_uploadform(event);return false" tabindex="1" />
</div>
<roundcube:object name="composeAttachmentList" id="attachment-list" class="attachmentslist" />
<roundcube:object name="fileDropArea" id="compose-attachments" />
</div>
<!--
<div id="composeformbuttons" class="footerleft formbuttons floating">
<roundcube:button type="input" command="send" class="button mainaction" label="sendmessage" tabindex="11" />
<roundcube:button type="input" command="savedraft" class="button" label="savemessage" tabindex="12" />
<roundcube:button type="input" command="list" class="button" label="cancel" tabindex="13" />
</div>
-->
</div>
</form>
</div><!-- end mailview-right -->
</div><!-- end mainscreencontent -->
</div><!-- end mainscreen -->
<div id="upload-dialog" class="propform popupdialog" title="<roundcube:label name='addattachment' />" aria-hidden="true">
<h2 id="aria-label-uploaddialog" class="voice"><roundcube:label name="arialabelattachmentuploadform" /></h2>
<roundcube:object name="composeAttachmentForm" id="uploadform" buttons="no" />
<div class="formbuttons">
<roundcube:button command="send-attachment" type="input" class="button mainaction" label="upload" />
<roundcube:button name="close" type="input" class="button" label="cancel" onclick="UI.show_uploadform()" />
</div>
</div>
<div id="spellmenu" class="popupmenu" aria-hidden="true"></div>
<div id="responsesmenu" class="popupmenu" aria-hidden="true">
<h3 id="aria-label-responsesmenu" class="voice"><roundcube:label name="arialabelresponsesmenu" /></h3>
<ul class="toolbarmenu" id="textresponsesmenu" role="menu" aria-labelledby="aria-label-responsesmenu">
<li role="separator" class="separator" id=""><label><roundcube:label name="insertresponse" /></label></li>
<roundcube:object name="responseslist" id="responseslist" tagname="ul" itemclass="active" />
<li role="separator" class="separator"><label><roundcube:label name="manageresponses" /></label></li>
<li role="menuitem"><roundcube:button command="save-response" type="link" label="savenewresponse" classAct="active" unselectable="on" /></li>
<li role="menuitem"><roundcube:button command="responses" type="link" label="editresponses" classAct="active" /></li>
</ul>
</div>
<roundcube:include file="/includes/footer.html" />
</body>
</html>
diff --git a/skins/larry/templates/mail.html b/skins/larry/templates/mail.html
index 92b01e019..cf8ff2f7d 100644
--- a/skins/larry/templates/mail.html
+++ b/skins/larry/templates/mail.html
@@ -1,254 +1,255 @@
<roundcube:object name="doctype" value="html5" />
<html>
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
<style type="text/css">
<roundcube:if condition="config:preview_pane == true" />
#mailview-top { height: 276px; }
#mailview-bottom { top: 330px; height: auto; display: block; }
#mailpreviewframe { display: block; }
<roundcube:endif />
</style>
</head>
<body class="minwidth">
<roundcube:include file="/includes/header.html" />
<div id="mainscreen">
<h1 class="voice"><roundcube:label name="mail" /></h1>
<!-- toolbar -->
<h2 id="aria-label-toolbar" class="voice"><roundcube:label name="arialabeltoolbar" /></h2>
<div id="messagetoolbar" class="toolbar" role="toolbar" aria-labelledby="aria-label-toolbar">
<roundcube:button command="checkmail" type="link" class="button checkmail disabled" classAct="button checkmail" classSel="button checkmail pressed" label="refresh" title="checkmail" />
<roundcube:include file="/includes/mailtoolbar.html" />
</div>
<div id="messagesearchtools">
<!-- search filter -->
<div id="searchfilter">
<label for="messagessearchfilter" class="voice"><roundcube:label name="arialabelmessagessearchfilter" /></label>
<roundcube:object name="searchfilter" class="searchfilter decorated" id="messagessearchfilter" aria-controls="messagelist" />
</div>
<!-- search box -->
<div id="quicksearchbar" class="searchbox" role="search" aria-labelledby="aria-label-searchform">
<h2 id="aria-label-searchform" class="voice"><roundcube:label name="arialabelsearchform" /></h2>
<label for="quicksearchbox" class="voice"><roundcube:label name="arialabelquicksearchbox" /></label>
<roundcube:object name="searchform" id="quicksearchbox" />
<roundcube:button command="menu-open" prop="searchmenu" id="searchmenulink" class="iconbutton searchoptions" title="searchmod" label="options" aria-haspopup="true" aria-expanded="false"aria-owns="searchmenu-menu" />
<roundcube:button command="reset-search" id="searchreset" class="iconbutton reset" title="resetsearch" content="Reset" />
<div id="searchmenu" class="popupmenu" data-editable="true">
<h3 id="aria-label-searchmenu" class="voice"><roundcube:label name="searchmod" /></h3>
<ul class="toolbarmenu" id="searchmenu-menu" role="menu" aria-labelledby="aria-label-searchmenu">
<li role="menuitem"><label><input type="checkbox" name="s_mods[]" value="subject" id="s_mod_subject" onclick="UI.set_searchmod(this)" /> <span><roundcube:label name="subject" /></span></label></li>
<li role="menuitem"><label><input type="checkbox" name="s_mods[]" value="from" id="s_mod_from" onclick="UI.set_searchmod(this)" /> <span><roundcube:label name="from" /></span></label></li>
<li role="menuitem"><label><input type="checkbox" name="s_mods[]" value="to" id="s_mod_to" onclick="UI.set_searchmod(this)" /> <span><roundcube:label name="to" /></span></label></li>
<li role="menuitem"><label><input type="checkbox" name="s_mods[]" value="cc" id="s_mod_cc" onclick="UI.set_searchmod(this)" /> <span><roundcube:label name="cc" /></span></label></li>
<li role="menuitem"><label><input type="checkbox" name="s_mods[]" value="bcc" id="s_mod_bcc" onclick="UI.set_searchmod(this)" /> <span><roundcube:label name="bcc" /></span></label></li>
<li role="menuitem"><label><input type="checkbox" name="s_mods[]" value="body" id="s_mod_body" onclick="UI.set_searchmod(this)" /> <span><roundcube:label name="body" /></span></label></li>
<li role="menuitem"><label><input type="checkbox" name="s_mods[]" value="text" id="s_mod_text" onclick="UI.set_searchmod(this)" /> <span><roundcube:label name="msgtext" /></span></label></li>
<li role="separator" class="separator"><label><roundcube:label name="searchscope" /></label></li>
<li role="menuitem"><label><input type="radio" name="s_scope" value="base" id="s_scope_base" onclick="UI.set_searchscope(this)" /> <span><roundcube:label name="currentfolder" /></span></label></li>
<li role="menuitem"><label><input type="radio" name="s_scope" value="sub" id="s_scope_sub" onclick="UI.set_searchscope(this)" /> <span><roundcube:label name="subfolders" /></span></label></li>
<li role="menuitem"><label><input type="radio" name="s_scope" value="all" id="s_scope_all" onclick="UI.set_searchscope(this)" /> <span><roundcube:label name="allfolders" /></span></label></li>
</ul>
</div>
</div>
</div>
<div id="mainscreencontent">
<div id="mailview-left">
<!-- folders list -->
<div id="mailboxcontainer" class="uibox listbox" role="navigation" aria-labelledby="aria-label-folderlist">
<h2 id="aria-label-folderlist" class="voice"><roundcube:label name="arialabelfolderlist" /></h2>
<div id="folderlist-content" class="scroller withfooter">
<roundcube:object name="mailboxlist" id="mailboxlist" class="treelist listing" folder_filter="mail" unreadwrap="%s" />
</div>
<div id="folderlist-footer" class="boxfooter">
<roundcube:button name="mailboxmenulink" id="mailboxmenulink" type="link" title="folderactions" class="listbutton groupactions" onclick="UI.toggle_popup('mailboxmenu',event);return false" innerClass="inner" content="&#9881;" aria-haspopup="true" aria-expanded="false"aria-owns="mailboxmenu-menu" />
<roundcube:if condition="env:quota" />
+ <span class="voice"><roundcube:label name="quota"></span>
<roundcube:object name="quotaDisplay" id="quotadisplay" class="countdisplay" display="text" />
<roundcube:endif />
</div>
</div>
</div>
<div id="mailview-right" role="main">
<roundcube:if condition="config:preview_pane == true" />
<div id="mailview-top" class="uibox">
<roundcube:else />
<div id="mailview-top" class="uibox fullheight">
<roundcube:endif />
<!-- messagelist -->
<div id="messagelistcontainer" class="boxlistcontent">
<h2 id="aria-label-messagelist" class="voice"><roundcube:label name="arialabelmessagelist" /></h2>
<roundcube:object name="messages"
id="messagelist"
class="records-table messagelist sortheader fixedheader"
optionsmenuIcon="true"
aria-labelledby="aria-label-messagelist" />
</div>
<!-- list footer -->
<div id="messagelistfooter">
<div id="listcontrols">
<roundcube:button href="#list" command="set-listmode" prop="list" class="iconbutton listmode disabled" classAct="iconbutton listmode" id="maillistmode" title="list" content="List" />
<roundcube:button href="#threads" command="set-listmode" prop="threads" class="iconbutton threadmode disabled" classAct="iconbutton threadmode" id="mailthreadmode" title="threads" content="Threads" />
</div>
<div id="listselectors">
<a href="#select" id="listselectmenulink" class="menuselector" onclick="UI.toggle_popup('listselectmenu', event);return false" aria-haspopup="true" aria-expanded="false"aria-owns="listselectmenu-menu"><span class="handle"><roundcube:label name="select" /></span></a>
<roundcube:if condition="env:threads" />
&nbsp; <a href="#threads" id="threadselectmenulink" class="menuselector" onclick="UI.toggle_popup('threadselectmenu', event);return false" aria-haspopup="true" aria-expanded="false"aria-owns="threadselectmenu-menu"><span class="handle"><roundcube:label name="threads" /></span></a>
<roundcube:endif />
</div>
<div id="countcontrols" class="pagenav dark">
<roundcube:object name="messageCountDisplay" class="countdisplay" aria-live="polite" aria-relevant="text" />
<span class="pagenavbuttons">
<roundcube:button command="firstpage" type="link" class="button firstpage disabled" classAct="button firstpage" classSel="button firstpage pressed" innerClass="inner" title="firstpage" label="first" />
<roundcube:button command="previouspage" type="link" class="button prevpage disabled" classAct="button prevpage" classSel="button prevpage pressed" innerClass="inner" title="previouspage" label="previous" />
<roundcube:button command="nextpage" type="link" class="button nextpage disabled" classAct="button nextpage" classSel="button nextpage pressed" innerClass="inner" title="nextpage" label="next" />
<roundcube:button command="lastpage" type="link" class="button lastpage disabled" classAct="button lastpage" classSel="button lastpage pressed" innerClass="inner" title="lastpage" label="last" />
</span>
</div>
<roundcube:container name="listcontrols" id="listcontrols" />
<a href="#preview" id="mailpreviewtoggle" class="iconbutton" title="<roundcube:label name='previewpane' />" role="button" tabindex="0"><roundcube:label name="previewpane" /></a>
</div>
</div><!-- end mailview-top -->
<div id="mailview-bottom" class="uibox">
<div id="mailpreviewframe" class="iframebox" role="complementary" aria-labelledby="aria-label-mailpreviewframe">
<h2 id="aria-label-mailpreviewframe" class="voice"><roundcube:label name="arialabelmailpreviewframe" /></h2>
<roundcube:object name="messagecontentframe" id="messagecontframe" style="width:100%; height:100%" frameborder="0" src="/watermark.html" title="arialabelmailpreviewframe" />
</div>
</div><!-- end mailview-bottom -->
</div><!-- end mailview-right -->
</div><!-- end mainscreencontent -->
</div><!-- end mainscreen -->
<div id="dragmessagemenu" class="popupmenu" aria-hidden="true">
<ul class="toolbarmenu" role="menu">
<li role="menuitem"><roundcube:button command="move" onclick="return rcmail.drag_menu_action('move')" label="move" classAct="active" /></li>
<li role="menuitem"><roundcube:button command="copy" onclick="return rcmail.drag_menu_action('copy')" label="copy" classAct="active" /></li>
</ul>
</div>
<div id="mailboxmenu" class="popupmenu" aria-hidden="true">
<h3 id="aria-label-mailboxmenu" class="voice"><roundcube:label name="arialabelmailboxmenu" /></h3>
<ul id="mailboxmenu-menu" class="toolbarmenu" id="mailboxoptionsmenu" role="menu" aria-labelledby="aria-label-mailboxmenu">
<li role="menuitem"><roundcube:button command="expunge" type="link" label="compact" classAct="active" /></li>
<li role="menuitem"><roundcube:button command="purge" type="link" label="empty" classAct="active" /></li>
<li role="menuitem"><roundcube:button command="import-messages" name="messageimport" type="link" classAct="active" label="importmessages" onclick="if(rcmail.command_enabled('import-messages'))UI.show_uploadform();return false" /></li>
<li role="menuitem"><roundcube:button command="folders" task="settings" type="link" label="managefolders" classAct="active" /></li>
<roundcube:container name="mailboxoptions" id="mailboxoptionsmenu" />
</ul>
</div>
<div id="listselectmenu" class="popupmenu dropdown" aria-hidden="true">
<h3 id="aria-label-listselectmenu" class="voice"><roundcube:label name="arialabellistselectmenu" /></h3>
<ul id="listselectmenu-menu" class="toolbarmenu iconized" role="menu" aria-labelledby="aria-label-listselectmenu">
<li role="menuitem"><roundcube:button command="select-all" type="link" label="all" class="icon" classAct="icon active" innerclass="icon mail" /></li>
<li role="menuitem"><roundcube:button command="select-all" type="link" prop="page" label="currpage" class="icon" classAct="icon active" innerclass="icon list" /></li>
<li role="menuitem"><roundcube:button command="select-all" type="link" prop="unread" label="unread" class="icon" classAct="icon active" innerclass="icon unread" /></li>
<li role="menuitem"><roundcube:button command="select-all" type="link" prop="flagged" label="flagged" class="icon" classAct="icon active" innerclass="icon flagged" /></li>
<li role="menuitem"><roundcube:button command="select-all" type="link" prop="invert" label="invert" class="icon" classAct="icon active" innerclass="icon invert" /></li>
<li role="menuitem"><roundcube:button command="select-none" type="link" label="none" class="icon" classAct="icon active" innerclass="icon cross" /></li>
</ul>
</div>
<div id="threadselectmenu" class="popupmenu dropdown" aria-hidden="true">
<h3 id="aria-label-threadselectmenu" class="voice"><roundcube:label name="arialabelthreadselectmenu" /></h3>
<ul id="threadselectmenu-menu" class="toolbarmenu" role="menu" aria-labelledby="aria-label-threadselectmenu">
<li role="menuitem"><roundcube:button command="expand-all" type="link" label="expand-all" class="icon" classAct="icon active" innerclass="icon conversation" /></li>
<li role="menuitem"><roundcube:button command="expand-unread" type="link" label="expand-unread" class="icon" classAct="icon active" innerclass="icon conversation" /></li>
<li role="menuitem"><roundcube:button command="collapse-all" type="link" label="collapse-all" class="icon" classAct="icon active" innerclass="icon conversation" /></li>
</ul>
</div>
<div id="listoptions" class="propform popupdialog" role="dialog" aria-labelledby="aria-label-listoptions" aria-hidden="true">
<h2 id="aria-label-listoptions" class="voice"><roundcube:label name="arialabelmessagelistoptions" /></h2>
<roundcube:if condition="!in_array('list_cols', (array)config:dont_override)" />
<fieldset class="floating">
<legend><roundcube:label name="listcolumns" /></legend>
<ul class="proplist">
<li><label class="disabled"><input type="checkbox" name="list_col[]" value="threads" checked="checked" disabled="disabled" /> <span><roundcube:label name="threads" /></span></label></li>
<li><label class="disabled"><input type="checkbox" name="list_col[]" value="subject" checked="checked" disabled="disabled" /> <span><roundcube:label name="subject" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="fromto" /> <span><roundcube:label name="fromto" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="from" /> <span><roundcube:label name="from" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="to" /> <span><roundcube:label name="to" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="replyto" /> <span><roundcube:label name="replyto" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="cc" /> <span><roundcube:label name="cc" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="date" /> <span><roundcube:label name="date" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="size" /> <span><roundcube:label name="size" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="status" /> <span><roundcube:label name="readstatus" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="attachment" /> <span><roundcube:label name="attachment" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="flag" /> <span><roundcube:label name="flag" /></span></label></li>
<li><label><input type="checkbox" name="list_col[]" value="priority" /> <span><roundcube:label name="priority" /></span></label></li>
</ul>
</fieldset>
<roundcube:endif />
<roundcube:if condition="!in_array('message_sort_col', (array)config:dont_override)" />
<fieldset class="floating">
<legend><roundcube:label name="listsorting" /></legend>
<ul class="proplist">
<li><label><input type="radio" name="sort_col" value="" /> <span><roundcube:label name="nonesort" /></span></label></li>
<li><label><input type="radio" name="sort_col" value="arrival" /> <span><roundcube:label name="arrival" /></span></label></li>
<li><label><input type="radio" name="sort_col" value="date" /> <span><roundcube:label name="sentdate" /></span></label></li>
<li><label><input type="radio" name="sort_col" value="subject" /> <span><roundcube:label name="subject" /></span></label></li>
<li><label><input type="radio" name="sort_col" value="fromto" /> <span><roundcube:label name="fromto" /></span></label></li>
<li><label><input type="radio" name="sort_col" value="from" /> <span><roundcube:label name="from" /></span></label></li>
<li><label><input type="radio" name="sort_col" value="to" /> <span><roundcube:label name="to" /></span></label></li>
<li><label><input type="radio" name="sort_col" value="cc" /> <span><roundcube:label name="cc" /></span></label></li>
<li><label><input type="radio" name="sort_col" value="size" /> <span><roundcube:label name="size" /></span></label></li>
</ul>
</fieldset>
<roundcube:endif />
<roundcube:if condition="!in_array('message_sort_order', (array)config:dont_override)" />
<fieldset class="floating">
<legend><roundcube:label name="listorder" /></legend>
<ul class="proplist">
<li><label><input type="radio" name="sort_ord" value="ASC" /> <span><roundcube:label name="asc" /></span></label></li>
<li><label><input type="radio" name="sort_ord" value="DESC" /> <span><roundcube:label name="desc" /></span></label></li>
</ul>
</fieldset>
<roundcube:endif />
<br style="clear:both" />
<div class="formbuttons">
<roundcube:button command="menu-save" id="listmenusave" type="input" class="button mainaction" label="save" />
<roundcube:button command="menu-close" prop="messagelistmenu" id="listmenucancel" type="input" class="button" label="cancel" />
</div>
</div>
<div id="upload-dialog" class="propform popupdialog" title="<roundcube:label name='importmessages' />" aria-hidden="true">
<h2 id="aria-label-uploaddialog" class="voice"><roundcube:label name="arialabelmailimportdialog" /></h2>
<roundcube:object name="messageimportform" id="uploadform" buttons="no" />
<div class="formbuttons">
<roundcube:button command="import-messages" type="input" class="button mainaction" label="upload" />
<roundcube:button name="close" type="input" class="button" label="cancel" onclick="UI.show_uploadform()" />
</div>
</div>
<roundcube:include file="/includes/footer.html" />
</body>
</html>
diff --git a/skins/larry/templates/messagepart.html b/skins/larry/templates/messagepart.html
index 2df9c7b77..edf275f6e 100644
--- a/skins/larry/templates/messagepart.html
+++ b/skins/larry/templates/messagepart.html
@@ -1,44 +1,44 @@
<roundcube:object name="doctype" value="html5" />
<html>
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
</head>
<body class="extwin noscroll">
<roundcube:include file="/includes/header.html" />
<div id="mainscreen">
<h1 class="voice"><roundcube:label name="attachment" />: <roundcube:var name="env:filename" /></h1>
<h2 id="aria-label-toolbar" class="voice"><roundcube:label name="arialabeltoolbar" /></h2>
<div id="messagetoolbar" class="toolbar fullwidth" role="toolbar" aria-labelledby="aria-label-toolbar">
<roundcube:button command="download" type="link" class="button download disabled" classAct="button download" classSel="button download pressed" label="download" />
<roundcube:button command="print" type="link" class="button print disabled" classAct="button print" classSel="button print pressed" label="print" />
<roundcube:container name="toolbar" id="messagetoolbar" />
</div>
<div id="mainscreencontent">
<div id="messagepartheader" class="uibox listbox" role="contentinfo" aria-labelledby="aria-label-contentinfo">
<h2 class="boxtitle" id="aria-label-contentinfo"><roundcube:label name="properties" /></h2>
<div class="scroller">
<roundcube:object name="messagePartControls" class="listing" />
</div>
</div>
<div id="messagepartcontainer" class="uibox" role="main" aria-labelledby="aria-label-messagepart">
- <h2 id="aria-label-messagepart" class="voice">Attachment preview</h2>
+ <h2 id="aria-label-messagepart" class="voice"><roundcube:label name="arialabelattachmentpreview" /></h2>
<div class="iframebox">
- <roundcube:object name="messagePartFrame" id="messagepartframe" frameborder="0" title="Attachment preview" />
+ <roundcube:object name="messagePartFrame" id="messagepartframe" frameborder="0" title="arialabelattachmentpreview" />
</div>
</div>
</div>
</div>
<roundcube:include file="/includes/footer.html" />
</body>
</html>
diff --git a/skins/larry/templates/messagepreview.html b/skins/larry/templates/messagepreview.html
index 97efdf3e7..82ecd7a43 100644
--- a/skins/larry/templates/messagepreview.html
+++ b/skins/larry/templates/messagepreview.html
@@ -1,80 +1,80 @@
<roundcube:object name="doctype" value="html5" />
<html>
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
</head>
<body class="iframe fullheight">
<div id="messageheader" class="previewheader">
<!-- record navigation -->
<div id="countcontrols" role="toolbar" aria-labelledby="aria-label-messagetoolbar">
-<h2 id="aria-label-messagetoolbar" class="voice">Message actions</h2>
+<h2 id="aria-label-messagetoolbar" class="voice"><roundcube:label name="arialabelmessageactions" /></h2>
<roundcube:if condition="env:optional_format=='text'" />
<span class="buttongroup">
<roundcube:button command="change-format" prop="html" type="link" class="button first changeformat html selected" innerClass="icon" title="changeformathtml" content="HTML" /><roundcube:button command="change-format" prop="text" type="link" class="button last changeformat text" classSel="button changeformat text pressed" innerClass="icon" title="changeformattext" content="Text" />
</span>
&nbsp;
<roundcube:elseif condition="env:optional_format=='html'" />
<span class="buttongroup">
<roundcube:button command="change-format" prop="html" type="link" class="button first changeformat html" classSel="button changeformat html pressed" innerClass="icon" title="changeformathtml" content="HTML" /><roundcube:button command="change-format" prop="text" type="link" class="button last changeformat text selected" innerClass="icon" title="changeformattext" content="Text" />
</span>
&nbsp;
<roundcube:endif />
<roundcube:if condition="env:mailbox != config:drafts_mbox">
<roundcube:button command="reply" type="link" class="button reply" classSel="button reply pressed" innerClass="icon" title="replytomessage" label="replytomessage" />
<roundcube:button command="reply-all" type="link" class="button replyall" classSel="button replyall pressed" innerClass="icon" title="replytoallmessage" label="replytoallmessage" />
<roundcube:button command="forward" type="link" class="button forward" classSel="button forward pressed" innerClass="icon" title="forwardmessage" label="forwardmessage" />
&nbsp;
<roundcube:endif />
<roundcube:button command="extwin" type="link" class="button extwin" classSel="button extwin pressed" innerClass="icon" title="openinextwin" label="openinextwin" />
</div>
<h3 class="subject"><span class="voice"><roundcube:label name="subject" />: </span><roundcube:object name="messageHeaders" valueOf="subject" /></h3>
<a href="#details" id="previewheaderstoggle" class="moreheaderstoggle" aria-expanded="false"><span class="iconlink" title="<roundcube:label name='togglemoreheaders' />"></span></a>
<div id="contactphoto"><roundcube:object name="contactphoto" /></div>
<table class="headers-table" id="preview-shortheaders"><tbody><tr>
<roundcube:if condition="env:mailbox == config:drafts_mbox || env:mailbox == config:sent_mbox">
<td class="header-title"><roundcube:label name="to" /></td>
<td class="header from"><roundcube:object name="messageHeaders" valueOf="to" max="3" addicon="/images/addcontact.png" /></td>
<roundcube:else />
<td class="header-title"><roundcube:label name="from" /></td>
<td class="header from"><roundcube:object name="messageHeaders" valueOf="from" addicon="/images/addcontact.png" /></td>
<roundcube:endif />
<td class="header-title"><roundcube:label name="date" /></td>
<td class="header date"><span><roundcube:object name="messageHeaders" valueOf="date" /></span></td>
</tr></tbody></table>
<roundcube:object name="messageHeaders" id="preview-allheaders" class="headers-table" addicon="/images/addcontact.png" max="10" exclude="subject,replyto" />
<roundcube:object name="messageFullHeaders" id="full-headers" />
</div>
<div id="messagepreview" role="main">
<div class="rightcol" role="region" aria-labelledby="aria-label-messageattachments">
<h2 id="aria-label-messageattachments" class="voice"><roundcube:label name="attachments" /></h2>
<roundcube:object name="messageAttachments" id="attachment-list" class="attachmentslist" />
</div>
<div class="leftcol" role="region" aria-labelledby="aria-label-messagebody">
-<h2 id="aria-label-messagebody" class="voice">Message Body</h2>
+<h2 id="aria-label-messagebody" class="voice"><roundcube:label name="arialabelmessagebody" /></h2>
<roundcube:object name="messageObjects" id="message-objects" />
<roundcube:object name="messageBody" id="messagebody" headertableclass="message-partheaders headers-table" />
</div>
</div>
<div id="attachmentmenu" class="popupmenu">
<ul class="toolbarmenu">
<li><roundcube:button command="open-attachment" id="attachmenuopen" type="link" label="open" class="icon" classAct="icon active" innerclass="icon extwin" /></li>
<li><roundcube:button command="download-attachment" id="attachmenudownload" type="link" label="download" class="icon" classAct="icon active" innerclass="icon download" /></li>
<roundcube:container name="attachmentmenu" id="attachmentmenu" />
</ul>
</div>
<roundcube:include file="/includes/footer.html" />
</body>
</html>

File Metadata

Mime Type
text/x-diff
Expires
Sat, Mar 1, 12:23 AM (1 h, 15 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
165248
Default Alt Text
(610 KB)

Event Timeline