diff options
50 files changed, 1012 insertions, 299 deletions
@@ -1,6 +1,18 @@ CHANGELOG Roundcube Webmail =========================== +- Display full attachment name using title attribute when name is too long to display (#1489320) +- Fix XSS issue in addressbook group name field [CVE-2013-5646] (#1489333) +- Fix attachment icon issue when rare font/language is used (#1489326) +- After message is sent refresh messages list of replied message folder (#1489249) +- Add option force specified domain in user login - username_domain_forced (#1489264) +- Fix expanded thread root message styling after refreshing messages list (#1489327) +- Fix issue where From address was removed from Cc and Bcc fields when editing a draft (#1489319) +- Add option to import Vcards with group assignments +- Save groups membership in Vcard export (#1488509) +- Workaround broken PHP function timezone_name_from_abbr (#1489261) +- Fix error_reporting directive check (#1489323) +- Make cached message size limit configurable - messages_cache_threshold (#1489317) - Log also failed logins to userlogins log - Add temp_dir_ttl configuration option (#1489304) - Allow setting INBOX as Sent folder (#1489219) @@ -62,6 +74,8 @@ CHANGELOG Roundcube Webmail RELEASE 0.9.4 ------------- +- Make identities matching case insensitive (#1485480) +- Fix issue where too big message data was stored in cache causing sql errors (#1489316) - Fix iframe scrollbars on webkit desktop browsers (#1489306) - Fix issue where legacy config was overriden by default config (#1489288) - Fix newmail_notifier issue where favicon wasn't changed back to default (#1489313) @@ -84,8 +98,8 @@ RELEASE 0.9.3 - Fix base URL resolving on attribute values with no quotes (#1489275) - Fix wrong handling of links with '|' character (#1489276) - Fix colorspace issue on image conversion using ImageMagick (#1489270) -- Fix XSS vulnerability when editing a message "as new" or draft (#1489251) -- Fix XSS vulnerability when saving HTML signatures (#1489251) +- Fix XSS vulnerability when editing a message "as new" or draft [CVE-2013-5645] (#1489251) +- Fix XSS vulnerability when saving HTML signatures [CVE-2013-5645] (#1489251) - Fix rewrite rule in .htaccess (#1489240) - Fix detecting Turkish language in ISO-8859-9 encoding (#1489252) - Fix identity-selection using Return-Path headers (#1489241) @@ -305,7 +319,7 @@ RELEASE 0.8.5 - Fix #countcontrols issue in IE<=8 when text is very long (#1488890) - Fix unwanted horizontal scrollbar in message preview header (#1488866) - Add workaround for IE<=8 bug where Content-Disposition:inline was ignored (#1488844) -- Fix XSS vulnerability in vbscript: and data:text links handling (#1488850) +- Fix XSS vulnerability in vbscript: and data:text links handling [CVE-2012-6121] (#1488850) - Fix absolute positioning in HTML messages (#1488819) - Fix cache (in)validation after setting \Deleted flag - Fix keybord events on messages list in opera browser (#1488823) @@ -360,8 +374,8 @@ RELEASE 0.8.1 - Fix bug where domain name was converted to lower-case even with login_lc=false (#1488593) - Fix lower-casing email address on replies (#1488598) - Fix line separator in exported messages (#1488603) -- Fix XSS issue where plain signatures wasn't secured in HTML mode (#1488613) -- Fix XSS issue where href="javascript:" wasn't secured (#1488613) +- Fix XSS issue where plain signatures wasn't secured in HTML mode [CVE-2012-4668] (#1488613) +- Fix XSS issue where href="javascript:" wasn't secured [CVE-2012-3508] (#1488613) - Fix impossible to create message with empty plain text part (#1488610) - Fix stripped apostrophes when replying in plain text to HTML message (#1488606) - Fix inactive Save search option after advanced search (#1488607) @@ -396,7 +410,7 @@ RELEASE 0.8.0 - Fix removing contact photo using LDAP addressbook (#1488420) - Fix storing X-ANNIVERSARY date in vCard format (#1488527) - Update to Mail_Mime-1.8.5 (#1488521) -- Fix XSS vulnerability in message subject handling using Larry skin (#1488519) +- Fix XSS vulnerability in message subject handling using Larry skin [CVE-2012-3507] (#1488519) - Fix handling of links with various URI schemes e.g. "skype:" (#1488106) - Fix handling of links inside PRE elements on html to text conversion - Fix indexing of links on html to text conversion @@ -523,7 +537,7 @@ RELEASE 0.7 - Improved handling of some malformed values encoded with quoted-printable (#1488232) - Add possibility to do LDAP bind before searching for bind DN - Fix handling of empty <U> tags in HTML messages (#1488225) -- Add content filter for embedded attachments to protect from XSS on IE (#1487895) +- Add content filter for embedded attachments to protect from XSS on IE [CVE-2012-1253] (#1487895) - Use strpos() instead of strstr() when possible (#1488211) - Fix handling HTML entities when converting HTML to text (#1488212) - Fix fit_string_to_size() renders browser and ui unresponsive (#1488207) @@ -691,7 +705,7 @@ RELEASE 0.6-beta RELEASE 0.5.4 ------------- -- Fix XSS vulnerability in UI messages (#1488030) +- Fix XSS vulnerability in UI messages [CVE-2011-2937] (#1488030) RELEASE 0.5.3 ------------- @@ -741,8 +755,8 @@ RELEASE 0.5.1 - Security: add optional referer check to prevent CSRF in GET requests - Fix email_dns_check setting not used for identities/contacts (#1487740) - Fix ICANN example addresses doesn't validate (#1487742) -- Security: protect login form submission from CSRF -- Security: prevent from relaying malicious requests through modcss.inc +- Security: protect login form submission from CSRF [CVE-2011-1491] +- Security: prevent from relaying malicious requests through modcss.inc [CVE-2011-1492] - Fix handling of non-image attachments in multipart/related messages (#1487750) - Fix IDNA support when IDN/INTL modules are in use (#1487742) - Fix handling of invalid HTML comments in messages (#1487759) @@ -1185,7 +1199,7 @@ RELEASE 0.3-RC1 --------------- - Fix import of vCard entries with params (#1485453) - Fix HTML messages output with empty block elements (#1485974) -- Use request tokens to protect POST requests from CSRF +- Use request tokens to protect POST requests from CSRF [CVE-2009-4076, CVE-2009-4077] - Added hook when killing a session - Added hook to write_log function (#1485971) - Performance improvements by use UID commands (#1485690) @@ -1312,7 +1326,7 @@ RELEASE 0.2.1 - Fix large search results on server without SORT capability (#1485668) - Get rid of preg_replace() with eval modifier and create_function usage (#1485686) - Bring back <base> and <link> tags in HTML messages -- Fix XSS vulnerability through background attributes as reported by Julien Cayssol +- Fix XSS vulnerability through background attributes [CVE-2009-0413] - Fix problems with backslash as IMAP hierarchy delimiter (#1484467) - Secure vcard export by getting rid of preg's 'e' modifier use (#1485689) - Fix authentication when submitting form with existing session (#1485679) diff --git a/bin/updatedb.sh b/bin/updatedb.sh index b4ed8b7ba..1f5e18434 100755 --- a/bin/updatedb.sh +++ b/bin/updatedb.sh @@ -72,13 +72,20 @@ if (!$version && $opts['version']) { '0.2-alpha' => 2008040500, '0.2-beta' => 2008060900, '0.2-stable' => 2008092100, + '0.2.1' => 2008092100, + '0.2.2' => 2008092100, '0.3-stable' => 2008092100, '0.3.1' => 2009090400, '0.4-beta' => 2009103100, + '0.4' => 2010042300, + '0.4.1' => 2010042300, '0.4.2' => 2010042300, '0.5-beta' => 2010100600, '0.5' => 2010100600, '0.5.1' => 2010100600, + '0.5.2' => 2010100600, + '0.5.3' => 2010100600, + '0.5.4' => 2010100600, '0.6-beta' => 2011011200, '0.6' => 2011011200, '0.7-beta' => 2011092800, diff --git a/config/defaults.inc.php b/config/defaults.inc.php index 268bf009a..97c8f3b25 100644 --- a/config/defaults.inc.php +++ b/config/defaults.inc.php @@ -164,6 +164,11 @@ $config['imap_cache_ttl'] = '10d'; // Lifetime of messages cache. Possible units: s, m, h, d, w $config['messages_cache_ttl'] = '10d'; +// Maximum cached message size in kilobytes. +// Note: On MySQL this should be less than (max_allowed_packet - 30%) +$config['messages_cache_threshold'] = 50; + + // ---------------------------------- // SMTP // ---------------------------------- @@ -240,6 +245,8 @@ $config['support_url'] = ''; // replace Roundcube logo with this image // specify an URL relative to the document root of this Roundcube installation +// an array can be used to specify different logos for specific template files, '*' for default logo +// for example array("*" => "/images/roundcube_logo.png", "messageprint" => "/images/roundcube_logo_print.png") $config['skin_logo'] = null; // automatically create a new Roundcube user when log-in the first time. @@ -341,6 +348,10 @@ $config['des_key'] = 'rcmail-!24ByteDESkey*Str'; // For example %n = mail.domain.tld, %t = domain.tld $config['username_domain'] = ''; +// Force domain configured in username_domain to be used for login. +// Any domain in username will be replaced by username_domain. +$config['username_domain_forced'] = false; + // This domain will be used to form e-mail addresses of new users // Specify an array with 'host' => 'domain' values to support multiple hosts // Supported replacement variables: diff --git a/plugins/managesieve/Changelog b/plugins/managesieve/Changelog index 60b2f1831..e660ee1ee 100644 --- a/plugins/managesieve/Changelog +++ b/plugins/managesieve/Changelog @@ -1,3 +1,5 @@ +* version 7.0 [2013-09-09] +----------------------------------------------------------- - Add vacation-seconds extension support (RFC 6131) - Several script parser code improvements - Support string list arguments in filter form (#1489018) @@ -5,6 +7,7 @@ - Split plugin file into two files - Fix handling of &, <, > characters in scripts/filter names (#1489208) - Support 'keep' action (#1489226) +- Add common headers to header selector (#1489271) * version 6.2 [2013-02-17] ----------------------------------------------------------- diff --git a/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php b/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php index bbbfa9d91..e4efef5b3 100644 --- a/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php +++ b/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php @@ -195,7 +195,7 @@ class rcube_sieve_engine } else { $this->exts = $this->sieve->get_extensions(); - $this->script = $this->sieve->script->as_array(); + $this->init_script(); $this->rc->output->set_env('currentset', $this->sieve->current); $_SESSION['managesieve_current'] = $this->sieve->current; } @@ -742,13 +742,22 @@ class rcube_sieve_engine $cust_header = (is_array($headers) && count($headers) == 1) ? $headers[0] : $headers; } + $header = $header == '...' ? $cust_header : $header; + + if (is_array($header)) { + foreach ($header as $h_index => $val) { + if (isset($this->headers[$val])) { + $header[$h_index] = $this->headers[$val]; + } + } + } + if ($type == 'exists') { $this->form['tests'][$i]['test'] = 'exists'; - $this->form['tests'][$i]['arg'] = $header == '...' ? $cust_header : $header; + $this->form['tests'][$i]['arg'] = $header; } else { - $test = 'header'; - $header = $header == '...' ? $cust_header : $header; + $test = 'header'; if ($mod == 'address' || $mod == 'envelope') { $found = false; @@ -1258,27 +1267,33 @@ class rcube_sieve_engine $select_header = new html_select(array('name' => "_header[]", 'id' => 'header'.$id, 'onchange' => 'rule_header_select(' .$id .')')); - foreach ($this->headers as $name => $val) - $select_header->add(rcube::Q($this->plugin->gettext($name)), Q($val)); - $select_header->add(rcube::Q($this->plugin->gettext('...')), '...'); + foreach ($this->headers as $index => $header) { + $header = $this->rc->text_exists($index) ? $this->plugin->gettext($index) : $header; + $select_header->add($header, $index); + } + $select_header->add($this->plugin->gettext('...'), '...'); if (in_array('body', $this->exts)) - $select_header->add(rcube::Q($this->plugin->gettext('body')), 'body'); - $select_header->add(rcube::Q($this->plugin->gettext('size')), 'size'); + $select_header->add($this->plugin->gettext('body'), 'body'); + $select_header->add($this->plugin->gettext('size'), 'size'); if (in_array('date', $this->exts)) { - $select_header->add(rcube::Q($this->plugin->gettext('datetest')), 'date'); - $select_header->add(rcube::Q($this->plugin->gettext('currdate')), 'currentdate'); + $select_header->add($this->plugin->gettext('datetest'), 'date'); + $select_header->add($this->plugin->gettext('currdate'), 'currentdate'); } if (isset($rule['test'])) { if (in_array($rule['test'], array('header', 'address', 'envelope')) - && !is_array($rule['arg1']) && in_array($rule['arg1'], $this->headers) + && !is_array($rule['arg1']) + && ($header = strtolower($rule['arg1'])) + && isset($this->headers[$header]) ) { - $test = $rule['arg1']; + $test = $header; } else if ($rule['test'] == 'exists' - && !is_array($rule['arg']) && in_array($rule['arg'], $this->headers) + && !is_array($rule['arg']) + && ($header = strtolower($rule['arg'])) + && isset($this->headers[$header]) ) { - $test = $rule['arg']; + $test = $header; } else if (in_array($rule['test'], array('size', 'body', 'date', 'currentdate'))) { $test = $rule['test']; @@ -2120,4 +2135,37 @@ class rcube_sieve_engine return $result; } + + /** + * Initializes internal script data + */ + private function init_script() + { + $this->script = $this->sieve->script->as_array(); + + if (!$this->script) { + return; + } + + $headers = array(); + + // find common headers used in script, will be added to the list + // of available (predefined) headers (#1489271) + foreach ($this->script as $rule) { + foreach ((array) $rule['tests'] as $test) { + if ($test['test'] == 'header') { + foreach ((array) $test['arg1'] as $header) { + $lc_header = strtolower($header); + if (!isset($this->headers[$lc_header]) && !isset($headers[$lc_header])) { + $headers[$lc_header] = $header; + } + } + } + } + } + + ksort($headers); + + $this->headers += $headers; + } } diff --git a/plugins/managesieve/package.xml b/plugins/managesieve/package.xml index 9c02957a5..6ae53c250 100644 --- a/plugins/managesieve/package.xml +++ b/plugins/managesieve/package.xml @@ -17,10 +17,10 @@ <email>alec@alec.pl</email> <active>yes</active> </lead> - <date>2013-02-17</date> + <date>2013-09-09</date> <version> - <release>6.2</release> - <api>6.0</api> + <release>7.0</release> + <api>7.0</api> </version> <stability> <release>stable</release> @@ -38,6 +38,10 @@ <tasks:replace from="@name@" to="name" type="package-info"/> <tasks:replace from="@package_version@" to="version" type="package-info"/> </file> + <file name="lib/Roundcube/rcube_sieve.php" role="php"></file> + <file name="lib/Roundcube/rcube_sieve_engine.php" role="php"></file> + <file name="lib/Roundcube/rcube_sieve_script.php" role="php"></file> + <file name="lib/Net/Sieve.php" role="php"></file> <file name="localization/be_BE.inc" role="data"></file> <file name="localization/bg_BG.inc" role="data"></file> <file name="localization/bs_BA.inc" role="data"></file> @@ -106,10 +110,6 @@ <file name="skins/larry/images/down_small.gif" role="data"></file> <file name="skins/larry/images/erase.png" role="data"></file> <file name="skins/larry/images/up_small.gif" role="data"></file> - <file name="lib/Roundcube/rcube_sieve.php" role="php"></file> - <file name="lib/Roundcube/rcube_sieve_engine.php" role="php"></file> - <file name="lib/Roundcube/rcube_sieve_script.php" role="php"></file> - <file name="lib/Net/Sieve.php" role="php"></file> <file name="config.inc.php.dist" role="data"></file> </dir> <!-- / --> diff --git a/plugins/newmail_notifier/config.inc.php.dist b/plugins/newmail_notifier/config.inc.php.dist index cdb563c40..1a7c0d74f 100644 --- a/plugins/newmail_notifier/config.inc.php.dist +++ b/plugins/newmail_notifier/config.inc.php.dist @@ -9,4 +9,7 @@ $config['newmail_notifier_sound'] = false; // Enables desktop notification $config['newmail_notifier_desktop'] = false; +// Desktop notification close timeout in seconds +$config['newmail_notifier_desktop_timeout'] = 10; + ?> diff --git a/plugins/newmail_notifier/localization/en_US.inc b/plugins/newmail_notifier/localization/en_US.inc index 7c1c5cf3f..1c4054615 100644 --- a/plugins/newmail_notifier/localization/en_US.inc +++ b/plugins/newmail_notifier/localization/en_US.inc @@ -25,5 +25,6 @@ $labels['body'] = 'You\'ve received a new message.'; $labels['testbody'] = 'This is a test notification.'; $labels['desktopdisabled'] = 'Desktop notifications are disabled in your browser.'; $labels['desktopunsupported'] = 'Your browser does not support desktop notifications.'; +$labels['desktoptimeout'] = 'Close desktop notification'; ?> diff --git a/plugins/newmail_notifier/newmail_notifier.js b/plugins/newmail_notifier/newmail_notifier.js index b00f33d10..846bc94c3 100644 --- a/plugins/newmail_notifier/newmail_notifier.js +++ b/plugins/newmail_notifier/newmail_notifier.js @@ -90,13 +90,11 @@ function newmail_notifier_sound() // - Require Chrome or Firefox latest version (22+) / 21.0 or older with a plugin function newmail_notifier_desktop(body) { + var timeout = rcmail.env.newmail_notifier_timeout || 10; -/** - * Fix: As of 17 June 2013, Chrome/Chromium does not implement Notification.permission correctly that - * it gives 'undefined' until an object has been created: - * https://code.google.com/p/chromium/issues/detail?id=163226 - * - */ + // As of 17 June 2013, Chrome/Chromium does not implement Notification.permission correctly that + // it gives 'undefined' until an object has been created: + // https://code.google.com/p/chromium/issues/detail?id=163226 try { if (Notification.permission == 'granted' || Notification.permission == undefined) { var popup = new Notification(rcmail.gettext('title', 'newmail_notifier'), { @@ -109,7 +107,7 @@ function newmail_notifier_desktop(body) popup.onclick = function() { this.close(); } - setTimeout(function() { popup.close(); }, 10000); // close after 10 seconds + setTimeout(function() { popup.close(); }, timeout * 1000); if (popup.permission == 'granted') return true; } } @@ -125,7 +123,7 @@ function newmail_notifier_desktop(body) this.cancel(); } popup.show(); - setTimeout(function() { popup.cancel(); }, 10000); // close after 10 seconds + setTimeout(function() { popup.cancel(); }, timeout * 1000); rcmail.newmail_popup = popup; return true; } diff --git a/plugins/newmail_notifier/newmail_notifier.php b/plugins/newmail_notifier/newmail_notifier.php index ca1c2ff67..20c542f58 100644 --- a/plugins/newmail_notifier/newmail_notifier.php +++ b/plugins/newmail_notifier/newmail_notifier.php @@ -123,6 +123,23 @@ class newmail_notifier extends rcube_plugin } } + $type = 'desktop_timeout'; + $key = 'newmail_notifier_' . $type; + if (!in_array($key, $dont_override)) { + $field_id = '_' . $key; + $select = new html_select(array('name' => $field_id, 'id' => $field_id)); + + foreach (array(5, 10, 15, 30, 45, 60) as $sec) { + $label = $this->rc->gettext(array('name' => 'afternseconds', 'vars' => array('n' => $sec))); + $select->add($label, $sec); + } + + $args['blocks']['new_message']['options'][$key] = array( + 'title' => html::label($field_id, rcube::Q($this->gettext('desktoptimeout'))), + 'content' => $select->show((int) $this->rc->config->get($key)) + ); + } + return $args; } @@ -148,6 +165,13 @@ class newmail_notifier extends rcube_plugin } } + $option = 'newmail_notifier_desktop_timeout'; + if (!in_array($option, $dont_override)) { + if ($value = (int) rcube_utils::get_input_value('_' . $option, rcube_utils::INPUT_POST)) { + $args['prefs'][$option] = $value; + } + } + return $args; } @@ -180,6 +204,7 @@ class newmail_notifier extends rcube_plugin if ($unseen->count()) { $this->notified = true; + $this->rc->output->set_env('newmail_notifier_timeout', $this->rc->config->get('newmail_notifier_desktop_timeout')); $this->rc->output->command('plugin.newmail_notifier', array( 'basic' => $this->opt['basic'], diff --git a/plugins/newmail_notifier/package.xml b/plugins/newmail_notifier/package.xml index b8ef34933..e46c9bc92 100644 --- a/plugins/newmail_notifier/package.xml +++ b/plugins/newmail_notifier/package.xml @@ -19,9 +19,9 @@ <email>alec@alec.pl</email> <active>yes</active> </lead> - <date>2013-03-16</date> + <date>2013-09-12</date> <version> - <release>0.5</release> + <release>0.6</release> <api>0.5</api> </version> <stability> diff --git a/plugins/password/config.inc.php.dist b/plugins/password/config.inc.php.dist index 82f6617e5..bfea52918 100644 --- a/plugins/password/config.inc.php.dist +++ b/plugins/password/config.inc.php.dist @@ -320,8 +320,7 @@ $config['hmailserver_server'] = array( // 5: domain-username // 6: username_domain // 7: domain_username -// 8: username@domain; mbox.username -$config['password_virtualmin_format'] = 8; +$config['password_virtualmin_format'] = 0; // pw_usermod Driver options diff --git a/plugins/password/drivers/virtualmin.php b/plugins/password/drivers/virtualmin.php index 2c7aee617..2d2f73f97 100644 --- a/plugins/password/drivers/virtualmin.php +++ b/plugins/password/drivers/virtualmin.php @@ -48,10 +48,6 @@ class rcube_virtualmin_password $pieces = explode("_", $username); $domain = $pieces[0]; break; - case 8: // domain taken from alias, username left as it was - $email = $rcmail->user->data['alias']; - $domain = substr(strrchr($email, "@"), 1); - break; default: // username@domain $domain = substr(strrchr($username, "@"), 1); } diff --git a/program/include/rcmail.php b/program/include/rcmail.php index 02287d312..0483f0e18 100644 --- a/program/include/rcmail.php +++ b/program/include/rcmail.php @@ -481,15 +481,22 @@ class rcmail extends rcube $port = $config['default_port']; } - /* Modify username with domain if required - Inspired by Marco <P0L0_notspam_binware.org> - */ - // Check if we need to add domain - if (!empty($config['username_domain']) && strpos($username, '@') === false) { - if (is_array($config['username_domain']) && isset($config['username_domain'][$host])) - $username .= '@'.rcube_utils::parse_host($config['username_domain'][$host], $host); - else if (is_string($config['username_domain'])) - $username .= '@'.rcube_utils::parse_host($config['username_domain'], $host); + // Check if we need to add/force domain to username + if (!empty($config['username_domain'])) { + $domain = is_array($config['username_domain']) ? $config['username_domain'][$host] : $config['username_domain']; + + if ($domain = rcube_utils::parse_host((string)$domain, $host)) { + $pos = strpos($username, '@'); + + // force configured domains + if (!empty($config['username_domain_forced']) && $pos !== false) { + $username = substr($username, 0, $pos) . '@' . $domain; + } + // just add domain if not specified + else if ($pos === false) { + $username .= '@' . $domain; + } + } } if (!isset($config['login_lc'])) { diff --git a/program/include/rcmail_output_html.php b/program/include/rcmail_output_html.php index a2ec29ca3..6db559358 100644 --- a/program/include/rcmail_output_html.php +++ b/program/include/rcmail_output_html.php @@ -924,8 +924,21 @@ class rcmail_output_html extends rcmail_output } else if ($object == 'logo') { $attrib += array('alt' => $this->xml_command(array('', 'object', 'name="productname"'))); - if ($logo = $this->config->get('skin_logo')) - $attrib['src'] = $logo; + + 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') { diff --git a/program/include/rcmail_output_json.php b/program/include/rcmail_output_json.php index def6ee42c..d0e1eec64 100644 --- a/program/include/rcmail_output_json.php +++ b/program/include/rcmail_output_json.php @@ -227,6 +227,13 @@ class rcmail_output_json extends rcmail_output if (!empty($this->callbacks)) $response['callbacks'] = $this->callbacks; + // trigger generic hook where plugins can put additional content to the response + $hook = $this->app->plugins->exec_hook("render_response", array('response' => $response)); + + // save some memory + $response = $hook['response']; + unset($hook['response']); + echo self::json_serialize($response); } diff --git a/program/js/app.js b/program/js/app.js index dedad37d2..337a12156 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -1784,7 +1784,6 @@ function rcube_webmail() + (!flags.seen ? ' unread' : '') + (flags.deleted ? ' deleted' : '') + (flags.flagged ? ' flagged' : '') - + (flags.unread_children && flags.seen && !this.env.autoexpand_threads ? ' unroot' : '') + (message.selected ? ' selected' : ''), row = { cols:[], style:{}, id:'rcmrow'+uid }; @@ -1834,6 +1833,9 @@ function rcube_webmail() expando = '<div id="rcmexpando' + uid + '" class="' + (message.expanded ? 'expanded' : 'collapsed') + '"> </div>'; row_class += ' thread' + (message.expanded? ' expanded' : ''); } + + if (flags.unread_children && flags.seen && !message.expanded) + row_class += ' unroot'; } tree += '<span id="msgicn'+uid+'" class="'+css_class+'"> </span>'; @@ -1879,7 +1881,7 @@ function rcube_webmail() html = expando; else if (c == 'subject') { if (bw.ie) { - col.onmouseover = function() { rcube_webmail.long_subject_title_ie(this, message.depth+1); }; + col.onmouseover = function() { rcube_webmail.long_subject_title_ex(this, message.depth+1); }; if (bw.ie8) tree = '<span></span>' + tree; // #1487821 } @@ -3425,7 +3427,8 @@ function rcube_webmail() message = input_message.val(), is_html = ($("input[name='_is_html']").val() == '1'), sig = this.env.identity, - delim = this.env.recipients_delimiter, + delim = this.env.recipients_separator, + rx_delim = RegExp.escape(delim), headers = ['replyto', 'bcc']; // update reply-to/bcc fields with addresses defined in identities @@ -3442,16 +3445,18 @@ function rcube_webmail() } // cleanup - rx = new RegExp(RegExp.escape(delim) + '\\s*' + RegExp(delim), 'g'); - input_val = input_val.replace(rx, delim) - rx = new RegExp('^\\s*' + RegExp.escape(delim) + '\\s*$'); - input_val = input_val.replace(rx, '') + rx = new RegExp(rx_delim + '\\s*' + rx_delim, 'g'); + input_val = input_val.replace(rx, delim); + rx = new RegExp('^[\\s' + rx_delim + ']+'); + input_val = input_val.replace(rx, ''); // add new address(es) - if (new_val) { - rx = new RegExp(RegExp.escape(delim) + '\\s*$'); - if (input_val && !rx.test(input_val)) - input_val += delim + ' '; + 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 + ' '; } @@ -3637,7 +3642,12 @@ function rcube_webmail() 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>').attr('id', name).addClass(att.classname).html(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, 0); }); // replace indicator's li if (upload_id && (indicator = document.getElementById(upload_id))) { @@ -3783,7 +3793,7 @@ function rcube_webmail() this.env.search_id = null; }; - this.sent_successfully = function(type, msg, target) + this.sent_successfully = function(type, msg, folders) { this.display_message(msg, type); @@ -3792,9 +3802,11 @@ function rcube_webmail() this.lock_form(this.gui_objects.messageform); if (rc) { rc.display_message(msg, type); - // refresh the folder where sent message was saved - if (target && rc.env.task == 'mail' && rc.env.action == '' && rc.env.mailbox == target) - rc.command('checkmail'); + // 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', rc.env.mailbox); + } } setTimeout(function(){ window.close() }, 1000); } @@ -4341,7 +4353,7 @@ function rcube_webmail() boxtitle.append(' » '); } - boxtitle.append($('<span>'+prop.name+'</span>')); + boxtitle.append($('<span>').text(prop.name)); } this.triggerEvent('groupupdate', prop); @@ -6982,11 +6994,11 @@ rcube_webmail.long_subject_title = function(elem, indent) if (!elem.title) { var $elem = $(elem); if ($elem.width() + indent * 15 > $elem.parent().width()) - elem.title = $elem.html(); + elem.title = $elem.text(); } }; -rcube_webmail.long_subject_title_ie = function(elem, indent) +rcube_webmail.long_subject_title_ex = function(elem, indent) { if (!elem.title) { var $elem = $(elem), diff --git a/program/lib/Roundcube/bootstrap.php b/program/lib/Roundcube/bootstrap.php index 5d5a22387..6e5143382 100644 --- a/program/lib/Roundcube/bootstrap.php +++ b/program/lib/Roundcube/bootstrap.php @@ -26,25 +26,25 @@ */ $config = array( - 'error_reporting' => E_ALL &~ (E_NOTICE | E_STRICT), + 'error_reporting' => E_ALL & ~E_NOTICE & ~E_STRICT, // Some users are not using Installer, so we'll check some // critical PHP settings here. Only these, which doesn't provide // an error/warning in the logs later. See (#1486307). 'mbstring.func_overload' => 0, - 'magic_quotes_runtime' => 0, - 'magic_quotes_sybase' => 0, // #1488506 + 'magic_quotes_runtime' => false, + 'magic_quotes_sybase' => false, // #1488506 ); // check these additional ini settings if not called via CLI if (php_sapi_name() != 'cli') { $config += array( - 'suhosin.session.encrypt' => 0, - 'file_uploads' => 1, + 'suhosin.session.encrypt' => false, + 'file_uploads' => true, ); } foreach ($config as $optname => $optval) { - $ini_optval = filter_var(ini_get($optname), FILTER_VALIDATE_BOOLEAN); + $ini_optval = filter_var(ini_get($optname), is_bool($optval) ? FILTER_VALIDATE_BOOLEAN : FILTER_VALIDATE_INT); if ($optval != $ini_optval && @ini_set($optname, $optval) === false) { $error = "ERROR: Wrong '$optname' option value and it wasn't possible to set it to required value ($optval).\n" . "Check your PHP configuration (including php_admin_flag)."; diff --git a/program/lib/Roundcube/rcube_addressbook.php b/program/lib/Roundcube/rcube_addressbook.php index 4ed139c45..9301211ff 100644 --- a/program/lib/Roundcube/rcube_addressbook.php +++ b/program/lib/Roundcube/rcube_addressbook.php @@ -3,7 +3,7 @@ /* +-----------------------------------------------------------------------+ | This file is part of the Roundcube Webmail client | - | Copyright (C) 2006-2012, The Roundcube Dev Team | + | 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. | @@ -35,6 +35,7 @@ abstract class rcube_addressbook /** public properties (mandatory) */ public $primary_key; public $groups = false; + public $export_groups = true; public $readonly = true; public $searchonly = false; public $undelete = false; @@ -423,7 +424,7 @@ abstract class rcube_addressbook * @param boolean True to return one array with all values, False for hash array with values grouped by type * @return array List of column values */ - function get_col_values($col, $data, $flat = false) + public static function get_col_values($col, $data, $flat = false) { $out = array(); foreach ((array)$data as $c => $values) { @@ -476,7 +477,8 @@ abstract class rcube_addressbook $fn = trim(join(' ', array_filter(array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix'])))); // use email address part for name - $email = is_array($contact['email']) ? $contact['email'][0] : $contact['email']; + $email = self::get_col_values('email', $contact, true); + $email = $email[0]; if ($email && (empty($fn) || $fn == $email)) { // return full email @@ -523,9 +525,9 @@ abstract class rcube_addressbook $fn = $contact['name']; // fallback to email address - $email = is_array($contact['email']) ? $contact['email'][0] : $contact['email']; - if (empty($fn) && $email) - return $email; + if (empty($fn) && ($email = self::get_col_values('email', $contact, true)) && !empty($email)) { + return $email[0]; + } return $fn; } @@ -538,8 +540,8 @@ abstract class rcube_addressbook $key = $contact[$sort_col] . ':' . $contact['sourceid']; // add email to a key to not skip contacts with the same name (#1488375) - if (!empty($contact['email'])) { - $key .= ':' . implode(':', (array)$contact['email']); + if (($email = self::get_col_values('email', $contact, true)) && !empty($email)) { + $key .= ':' . implode(':', (array)$email); } return $key; @@ -561,9 +563,9 @@ abstract class rcube_addressbook // use only strict comparison (mode = 1) // @TODO: partial search, e.g. match only day and month if (in_array($colname, $this->date_cols)) { - return (($value = rcube_utils::strtotime($value)) - && ($search = rcube_utils::strtotime($search)) - && date('Ymd', $value) == date('Ymd', $search)); + return (($value = rcube_utils::anytodatetime($value)) + && ($search = rcube_utils::anytodatetime($search)) + && $value->format('Ymd') == $search->format('Ymd')); } // composite field, e.g. address diff --git a/program/lib/Roundcube/rcube_config.php b/program/lib/Roundcube/rcube_config.php index 90e1394cf..a3741758f 100644 --- a/program/lib/Roundcube/rcube_config.php +++ b/program/lib/Roundcube/rcube_config.php @@ -3,7 +3,7 @@ /* +-----------------------------------------------------------------------+ | This file is part of the Roundcube Webmail client | - | Copyright (C) 2008-2012, The Roundcube Dev Team | + | Copyright (C) 2008-2013, The Roundcube Dev Team | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | @@ -27,7 +27,7 @@ class rcube_config const DEFAULT_SKIN = 'larry'; private $env = ''; - private $basedir = 'config/'; + private $paths = array(); private $prop = array(); private $errors = array(); private $userprefs = array(); @@ -58,7 +58,32 @@ class rcube_config public function __construct($env = '') { $this->env = $env; - $this->basedir = RCUBE_CONFIG_DIR; + + if ($paths = getenv('RCUBE_CONFIG_PATH')) { + $this->paths = explode(PATH_SEPARATOR, $paths); + // make all paths absolute + foreach ($this->paths as $i => $path) { + if (!$this->_is_absolute($path)) { + if ($realpath = realpath(RCUBE_INSTALL_PATH . $path)) { + $this->paths[$i] = unslashify($realpath) . '/'; + } + else { + unset($this->paths[$i]); + } + } + else { + $this->paths[$i] = unslashify($path) . '/'; + } + } + } + + if (defined('RCUBE_CONFIG_DIR') && !in_array(RCUBE_CONFIG_DIR, $this->paths)) { + $this->paths[] = RCUBE_CONFIG_DIR; + } + + if (empty($this->paths)) { + $this->paths[] = RCUBE_INSTALL_PATH . 'config/'; + } $this->load(); @@ -138,17 +163,6 @@ class rcube_config // enable display_errors in 'show' level, but not for ajax requests ini_set('display_errors', intval(empty($_REQUEST['_remote']) && ($this->prop['debug_level'] & 4))); - // set timezone auto settings values - if ($this->prop['timezone'] == 'auto') { - $this->prop['_timezone_value'] = $this->client_timezone(); - } - else if (is_numeric($this->prop['timezone']) && ($tz = timezone_name_from_abbr("", $this->prop['timezone'] * 3600, 0))) { - $this->prop['timezone'] = $tz; - } - else if (empty($this->prop['timezone'])) { - $this->prop['timezone'] = 'UTC'; - } - // remove deprecated properties unset($this->prop['dst_active']); @@ -186,47 +200,73 @@ class rcube_config */ public function load_from_file($file) { - $fpath = $this->resolve_path($file); - if ($fpath && is_file($fpath) && is_readable($fpath)) { - // use output buffering, we don't need any output here - ob_start(); - include($fpath); - ob_end_clean(); - - if (is_array($config)) { - $this->merge($config); - return true; - } - // deprecated name of config variable - else if (is_array($rcmail_config)) { - $this->merge($rcmail_config); - return true; + $success = false; + + foreach ($this->resolve_paths($file) as $fpath) { + if ($fpath && is_file($fpath) && is_readable($fpath)) { + // use output buffering, we don't need any output here + ob_start(); + include($fpath); + ob_end_clean(); + + if (is_array($config)) { + $this->merge($config); + $success = true; + } + // deprecated name of config variable + if (is_array($rcmail_config)) { + $this->merge($rcmail_config); + $success = true; + } } } - return false; + return $success; } /** - * Helper method to resolve the absolute path to the given config file. + * Helper method to resolve absolute paths to the given config file. * This also takes the 'env' property into account. + * + * @param string Filename or absolute file path + * @param boolean Return -$env file path if exists + * @return array List of candidates in config dir path(s) */ - public function resolve_path($file, $use_env = true) + public function resolve_paths($file, $use_env = true) { - if (strpos($file, '/') === false) { - $file = realpath($this->basedir . '/' . $file); - } + $files = array(); + $abs_path = $this->_is_absolute($file); - // check if <file>-env.ini exists - if ($file && $use_env && !empty($this->env)) { - $envfile = preg_replace('/\.(inc.php)$/', '-' . $this->env . '.\\1', $file); - if (is_file($envfile)) - return $envfile; + foreach ($this->paths as $basepath) { + $realpath = $abs_path ? $file : realpath($basepath . '/' . $file); + + // check if <file>-env.ini exists + if ($realpath && $use_env && !empty($this->env)) { + $envfile = preg_replace('/\.(inc.php)$/', '-' . $this->env . '.\\1', $realpath); + if (is_file($envfile)) + $realpath = $envfile; + } + + if ($realpath) { + $files[] = $realpath; + + // no need to continue the loop if an absolute file path is given + if ($abs_path) { + break; + } + } } - return $file; + return $files; } + /** + * Determine whether the given file path is absolute or relative + */ + private function _is_absolute($path) + { + return $path[0] == DIRECTORY_SEPARATOR || preg_match('!^[a-z]:[\\\\/]!i', $path); + } /** * Getter for a specific config parameter @@ -246,8 +286,10 @@ class rcube_config $rcube = rcube::get_instance(); - if ($name == 'timezone' && isset($this->prop['_timezone_value'])) { - $result = $this->prop['_timezone_value']; + if ($name == 'timezone') { + if (empty($result) || $result == 'auto') { + $result = $this->client_timezone(); + } } else if ($name == 'client_mimetypes') { if ($result == null && $def == null) @@ -305,11 +347,6 @@ class rcube_config } } - // convert user's timezone into the new format - if (is_numeric($prefs['timezone']) && ($tz = timezone_name_from_abbr('', $prefs['timezone'] * 3600, 0))) { - $prefs['timezone'] = $tz; - } - // larry is the new default skin :-) if ($prefs['skin'] == 'default') { $prefs['skin'] = self::DEFAULT_SKIN; @@ -317,13 +354,6 @@ class rcube_config $this->userprefs = $prefs; $this->prop = array_merge($this->prop, $prefs); - - // override timezone settings with client values - if ($this->prop['timezone'] == 'auto') { - $this->prop['_timezone_value'] = isset($_SESSION['timezone']) ? $this->client_timezone() : $this->prop['_timezone_value']; - } - else if (isset($this->prop['_timezone_value'])) - unset($this->prop['_timezone_value']); } @@ -464,13 +494,12 @@ class rcube_config */ private function client_timezone() { - if (isset($_SESSION['timezone']) && is_numeric($_SESSION['timezone']) - && ($ctz = timezone_name_from_abbr("", $_SESSION['timezone'] * 3600, 0))) { - return $ctz; - } - else if (!empty($_SESSION['timezone'])) { + // @TODO: remove this legacy timezone handling in the future + $props = $this->fix_legacy_props(array('timezone' => $_SESSION['timezone'])); + + if (!empty($props['timezone'])) { try { - $tz = timezone_open($_SESSION['timezone']); + $tz = new DateTimeZone($props['timezone']); return $tz->getName(); } catch (Exception $e) { /* gracefully ignore */ } @@ -498,6 +527,77 @@ class rcube_config } } + // convert deprecated numeric timezone value + if (isset($props['timezone']) && is_numeric($props['timezone'])) { + if ($tz = self::timezone_name_from_abbr($props['timezone'])) { + $props['timezone'] = $tz; + } + else { + unset($props['timezone']); + } + } + return $props; } + + /** + * timezone_name_from_abbr() replacement. Converts timezone offset + * into timezone name abbreviation. + * + * @param float $offset Timezone offset (in hours) + * + * @return string Timezone abbreviation + */ + static public function timezone_name_from_abbr($offset) + { + // List of timezones here is not complete - https://bugs.php.net/bug.php?id=44780 + if ($tz = timezone_name_from_abbr('', $offset * 3600, 0)) { + return $tz; + } + + // try with more complete list (#1489261) + $timezones = array( + '-660' => "Pacific/Apia", + '-600' => "Pacific/Honolulu", + '-570' => "Pacific/Marquesas", + '-540' => "America/Anchorage", + '-480' => "America/Los_Angeles", + '-420' => "America/Denver", + '-360' => "America/Chicago", + '-300' => "America/New_York", + '-270' => "America/Caracas", + '-240' => "America/Halifax", + '-210' => "Canada/Newfoundland", + '-180' => "America/Sao_Paulo", + '-60' => "Atlantic/Azores", + '0' => "Europe/London", + '60' => "Europe/Paris", + '120' => "Europe/Helsinki", + '180' => "Europe/Moscow", + '210' => "Asia/Tehran", + '240' => "Asia/Dubai", + '300' => "Asia/Karachi", + '270' => "Asia/Kabul", + '300' => "Asia/Karachi", + '330' => "Asia/Kolkata", + '345' => "Asia/Katmandu", + '360' => "Asia/Yekaterinburg", + '390' => "Asia/Rangoon", + '420' => "Asia/Krasnoyarsk", + '480' => "Asia/Shanghai", + '525' => "Australia/Eucla", + '540' => "Asia/Tokyo", + '570' => "Australia/Adelaide", + '600' => "Australia/Melbourne", + '630' => "Australia/Lord_Howe", + '660' => "Asia/Vladivostok", + '690' => "Pacific/Norfolk", + '720' => "Pacific/Auckland", + '765' => "Pacific/Chatham", + '780' => "Pacific/Enderbury", + '840' => "Pacific/Kiritimati", + ); + + return $timezones[(string) intval($offset * 60)]; + } } diff --git a/program/lib/Roundcube/rcube_contacts.php b/program/lib/Roundcube/rcube_contacts.php index 3919cdc6e..6d01368a1 100644 --- a/program/lib/Roundcube/rcube_contacts.php +++ b/program/lib/Roundcube/rcube_contacts.php @@ -718,6 +718,10 @@ class rcube_contacts extends rcube_addressbook foreach ($save_data as $key => $values) { list($field, $section) = explode(':', $key); $fulltext = in_array($field, $this->fulltext_cols); + // avoid casting DateTime objects to array + if (is_object($values) && is_a($values, 'DateTime')) { + $values = array(0 => $values); + } foreach ((array)$values as $value) { if (isset($value)) $vcard->set($field, $value, $section); diff --git a/program/lib/Roundcube/rcube_csv2vcard.php b/program/lib/Roundcube/rcube_csv2vcard.php index fb8d8f103..00e6d4e20 100644 --- a/program/lib/Roundcube/rcube_csv2vcard.php +++ b/program/lib/Roundcube/rcube_csv2vcard.php @@ -145,6 +145,7 @@ class rcube_csv2vcard 'work_mobile' => 'phone:work,cell', 'work_title' => 'jobtitle', 'work_zip' => 'zipcode:work', + 'group' => 'groups', ); /** @@ -268,6 +269,7 @@ class rcube_csv2vcard 'work_mobile' => "Work Mobile", 'work_title' => "Work Title", 'work_zip' => "Work Zip", + 'groups' => "Group", ); protected $local_label_map = array(); diff --git a/program/lib/Roundcube/rcube_db.php b/program/lib/Roundcube/rcube_db.php index 852070073..e66226ff5 100644 --- a/program/lib/Roundcube/rcube_db.php +++ b/program/lib/Roundcube/rcube_db.php @@ -386,17 +386,7 @@ class rcube_db $result = $this->dbh->query($query); if ($result === false) { - $error = $this->dbh->errorInfo(); - - if (empty($this->options['ignore_key_errors']) || $error[0] != '23000') { - $this->db_error = true; - $this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]); - - rcube::raise_error(array('code' => 500, 'type' => 'db', - 'line' => __LINE__, 'file' => __FILE__, - 'message' => $this->db_error_msg . " (SQL Query: $query)" - ), true, false); - } + $result = $this->handle_error($query); } $this->last_result = $result; @@ -405,6 +395,30 @@ class rcube_db } /** + * Helper method to handle DB errors. + * This by default logs the error but could be overriden by a driver implementation + * + * @param string Query that triggered the error + * @return mixed Result to be stored and returned + */ + protected function handle_error($query) + { + $error = $this->dbh->errorInfo(); + + if (empty($this->options['ignore_key_errors']) || $error[0] != '23000') { + $this->db_error = true; + $this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]); + + rcube::raise_error(array('code' => 500, 'type' => 'db', + 'line' => __LINE__, 'file' => __FILE__, + 'message' => $this->db_error_msg . " (SQL Query: $query)" + ), true, false); + } + + return false; + } + + /** * Get number of affected rows for the last query * * @param mixed $result Optional query handle diff --git a/program/lib/Roundcube/rcube_db_mysql.php b/program/lib/Roundcube/rcube_db_mysql.php index 6fa5ad768..24f9ce1bd 100644 --- a/program/lib/Roundcube/rcube_db_mysql.php +++ b/program/lib/Roundcube/rcube_db_mysql.php @@ -179,4 +179,29 @@ class rcube_db_mysql extends rcube_db return isset($this->variables[$varname]) ? $this->variables[$varname] : $default; } + /** + * Handle DB errors, re-issue the query on deadlock errors from InnoDB row-level locking + * + * @param string Query that triggered the error + * @return mixed Result to be stored and returned + */ + protected function handle_error($query) + { + $error = $this->dbh->errorInfo(); + + // retry after "Deadlock found when trying to get lock" errors + $retries = 2; + while ($error[1] == 1213 && $retries >= 0) { + usleep(50000); // wait 50 ms + $result = $this->dbh->query($query); + if ($result !== false) { + return $result; + } + $error = $this->dbh->errorInfo(); + $retries--; + } + + return parent::handle_error($query); + } + } diff --git a/program/lib/Roundcube/rcube_imap.php b/program/lib/Roundcube/rcube_imap.php index 689a6266d..aa074233f 100644 --- a/program/lib/Roundcube/rcube_imap.php +++ b/program/lib/Roundcube/rcube_imap.php @@ -3785,9 +3785,10 @@ class rcube_imap extends rcube_storage if ($this->messages_caching && !$this->mcache) { $rcube = rcube::get_instance(); if (($dbh = $rcube->get_dbh()) && ($userid = $rcube->get_user_id())) { - $ttl = $rcube->config->get('messages_cache_ttl', '10d'); + $ttl = $rcube->config->get('messages_cache_ttl', '10d'); + $threshold = $rcube->config->get('messages_cache_threshold', 50); $this->mcache = new rcube_imap_cache( - $dbh, $this, $userid, $this->options['skip_deleted'], $ttl); + $dbh, $this, $userid, $this->options['skip_deleted'], $ttl, $threshold); } } diff --git a/program/lib/Roundcube/rcube_imap_cache.php b/program/lib/Roundcube/rcube_imap_cache.php index 061ac546d..d72bfe0ab 100644 --- a/program/lib/Roundcube/rcube_imap_cache.php +++ b/program/lib/Roundcube/rcube_imap_cache.php @@ -56,6 +56,13 @@ class rcube_imap_cache private $ttl; /** + * Maximum cached message size + * + * @var int + */ + private $threshold; + + /** * Internal (in-memory) cache * * @var array @@ -96,9 +103,9 @@ class rcube_imap_cache * @param int $userid User identifier * @param bool $skip_deleted skip_deleted flag * @param string $ttl Expiration time of memcache/apc items - * + * @param int $threshold Maximum cached message size */ - function __construct($db, $imap, $userid, $skip_deleted, $ttl=0) + function __construct($db, $imap, $userid, $skip_deleted, $ttl=0, $threshold=0) { // convert ttl string to seconds $ttl = get_offset_sec($ttl); @@ -109,6 +116,7 @@ class rcube_imap_cache $this->userid = $userid; $this->skip_deleted = $skip_deleted; $this->ttl = $ttl; + $this->threshold = $threshold; } @@ -1155,13 +1163,13 @@ class rcube_imap_cache // Save current message from internal cache if ($message = $this->icache['__message']) { // clean up some object's data - $object = $this->message_object_prepare($message['object']); + $this->message_object_prepare($message['object']); // calculate current md5 sum - $md5sum = md5(serialize($object)); + $md5sum = md5(serialize($message['object'])); if ($message['md5sum'] != $md5sum) { - $this->add_message($message['mailbox'], $object, !$message['exists']); + $this->add_message($message['mailbox'], $message['object'], !$message['exists']); } $this->icache['__message']['md5sum'] = $md5sum; @@ -1171,12 +1179,19 @@ class rcube_imap_cache /** * Prepares message object to be stored in database. + * + * @param rcube_message_header|rcube_message_part */ - private function message_object_prepare($msg) + private function message_object_prepare(&$msg, &$size = 0) { - // Remove body too big (>25kB) - if ($msg->body && strlen($msg->body) > 25 * 1024) { - unset($msg->body); + // Remove body too big + if ($msg->body && ($length = strlen($msg->body))) { + $size += $length; + + if ($size > $this->threshold * 1024) { + $size -= $length; + unset($msg->body); + } } // Fix mimetype which might be broken by some code when message is displayed @@ -1186,13 +1201,19 @@ class rcube_imap_cache list($msg->ctype_primary, $msg->ctype_secondary) = explode('/', $msg->mimetype); } + unset($msg->replaces); + if (is_array($msg->structure->parts)) { - foreach ($msg->structure->parts as $idx => $part) { - $msg->structure->parts[$idx] = $this->message_object_prepare($part); + foreach ($msg->structure->parts as $part) { + $this->message_object_prepare($part, $size); } } - return $msg; + if (is_array($msg->parts)) { + foreach ($msg->parts as $part) { + $this->message_object_prepare($part, $size); + } + } } diff --git a/program/lib/Roundcube/rcube_ldap.php b/program/lib/Roundcube/rcube_ldap.php index cb7fa8466..78573789b 100644 --- a/program/lib/Roundcube/rcube_ldap.php +++ b/program/lib/Roundcube/rcube_ldap.php @@ -34,6 +34,7 @@ class rcube_ldap extends rcube_addressbook public $ready = false; public $group_id = 0; public $coltypes = array(); + public $export_groups = false; // private properties protected $ldap; diff --git a/program/lib/Roundcube/rcube_mime.php b/program/lib/Roundcube/rcube_mime.php index 572540f47..96a8eac61 100644 --- a/program/lib/Roundcube/rcube_mime.php +++ b/program/lib/Roundcube/rcube_mime.php @@ -708,12 +708,20 @@ class rcube_mime */ public static function file_content_type($path, $name, $failover = 'application/octet-stream', $is_stream = false, $skip_suffix = false) { + static $mime_ext = array(); + $mime_type = null; - $mime_magic = rcube::get_instance()->config->get('mime_magic'); - $mime_ext = $skip_suffix ? null : @include(RCUBE_CONFIG_DIR . '/mimetypes.php'); + $config = rcube::get_instance()->config; + $mime_magic = $config->get('mime_magic'); + + if (!$skip_suffix && empty($mime_ext)) { + foreach ($config->resolve_paths('mimetypes.php') as $fpath) { + $mime_ext = array_merge($mime_ext, (array) @include($fpath)); + } + } // use file name suffix with hard-coded mime-type map - if (is_array($mime_ext) && $name) { + if (!$skip_suffix && is_array($mime_ext) && $name) { if ($suffix = substr($name, strrpos($name, '.')+1)) { $mime_type = $mime_ext[strtolower($suffix)]; } @@ -818,7 +826,9 @@ class rcube_mime // fallback to some well-known types most important for daily emails if (empty($mime_types)) { - $mime_extensions = (array) @include(RCUBE_CONFIG_DIR . '/mimetypes.php'); + foreach (rcube::get_instance()->config->resolve_paths('mimetypes.php') as $fpath) { + $mime_extensions = array_merge($mime_extensions, (array) @include($fpath)); + } foreach ($mime_extensions as $ext => $mime) { $mime_types[$mime][] = $ext; diff --git a/program/lib/Roundcube/rcube_utils.php b/program/lib/Roundcube/rcube_utils.php index 2540f779d..1d76ae508 100644 --- a/program/lib/Roundcube/rcube_utils.php +++ b/program/lib/Roundcube/rcube_utils.php @@ -787,6 +787,44 @@ class rcube_utils return (int) $ts; } + /** + * Date parsing function that turns the given value into a DateTime object + * + * @param string $date Date string + * + * @return object DateTime instance or false on failure + */ + public static function anytodatetime($date) + { + if (is_object($date) && is_a($date, 'DateTime')) { + return $date; + } + + $dt = false; + $date = trim($date); + + // try to parse string with DateTime first + if (!empty($date)) { + try { + $dt = new DateTime($date); + } + catch (Exception $e) { + // ignore + } + } + + // try our advanced strtotime() method + if (!$dt && ($timestamp = self::strtotime($date))) { + try { + $dt = new DateTime("@".$timestamp); + } + catch (Exception $e) { + // ignore + } + } + + return $dt; + } /* * Idn_to_ascii wrapper. diff --git a/program/lib/Roundcube/rcube_vcard.php b/program/lib/Roundcube/rcube_vcard.php index a71305c4b..d54dc56ad 100644 --- a/program/lib/Roundcube/rcube_vcard.php +++ b/program/lib/Roundcube/rcube_vcard.php @@ -47,6 +47,7 @@ class rcube_vcard 'manager' => 'X-MANAGER', 'spouse' => 'X-SPOUSE', 'edit' => 'X-AB-EDIT', + 'groups' => 'CATEGORIES', ); private $typemap = array( 'IPHONE' => 'mobile', @@ -357,8 +358,8 @@ class rcube_vcard case 'birthday': case 'anniversary': - if (($val = rcube_utils::strtotime($value)) && ($fn = self::$fieldmap[$field])) { - $this->raw[$fn][] = array(0 => date('Y-m-d', $val), 'value' => array('date')); + if (($val = rcube_utils::anytodatetime($value)) && ($fn = self::$fieldmap[$field])) { + $this->raw[$fn][] = array(0 => $val->format('Y-m-d'), 'value' => array('date')); } break; @@ -756,7 +757,7 @@ class rcube_vcard * * @return string Joined and quoted string */ - private static function vcard_quote($s, $sep = ';') + public static function vcard_quote($s, $sep = ';') { if (is_array($s)) { foreach($s as $part) { @@ -765,7 +766,7 @@ class rcube_vcard return(implode($sep, (array)$r)); } - return strtr($s, array('\\' => '\\\\', "\r" => '', "\n" => '\n', ',' => '\,', ';' => '\;')); + return strtr($s, array('\\' => '\\\\', "\r" => '', "\n" => '\n', $sep => '\\'.$sep)); } /** diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc index 1865bcb3d..840c9358c 100644 --- a/program/localization/en_US/labels.inc +++ b/program/localization/en_US/labels.inc @@ -367,8 +367,11 @@ $labels['searchdelete'] = 'Delete search'; $labels['import'] = 'Import'; $labels['importcontacts'] = 'Import contacts'; $labels['importfromfile'] = 'Import from file:'; -$labels['importtarget'] = 'Add new contacts to address book:'; +$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'; diff --git a/program/steps/addressbook/export.inc b/program/steps/addressbook/export.inc index 761f26b75..1e988feab 100644 --- a/program/steps/addressbook/export.inc +++ b/program/steps/addressbook/export.inc @@ -5,7 +5,7 @@ | program/steps/addressbook/export.inc | | | | This file is part of the Roundcube Webmail client | - | Copyright (C) 2008-2011, The Roundcube Dev Team | + | Copyright (C) 2008-2013, The Roundcube Dev Team | | Copyright (C) 2011, Kolab Systems AG | | | | Licensed under the GNU General Public License version 3 or | @@ -21,6 +21,46 @@ +-----------------------------------------------------------------------+ */ + +/** + * Copy contact record properties into a vcard object + */ +function prepare_for_export(&$record, $source = null) +{ + $groups = $source && $source->groups && $source->export_groups ? $source->get_record_groups($record['ID']) : null; + + if (empty($record['vcard'])) { + $vcard = new rcube_vcard(); + if ($source) { + $vcard->extend_fieldmap($source->vcard_map); + } + $vcard->load($record['vcard']); + $vcard->reset(); + + foreach ($record as $key => $values) { + list($field, $section) = explode(':', $key); + foreach ((array)$values as $value) { + if (is_array($value) || @strlen($value)) { + $vcard->set($field, $value, strtoupper($section)); + } + } + } + + // append group names + if ($groups) { + $vcard->set('groups', join(',', $groups), null); + } + + $record['vcard'] = $vcard->export(true); + } + // patch categories to alread existing vcard block + else if ($record['vcard'] && !empty($groups) && !strpos($record['vcard'], 'CATEGORIES:')) { + $vgroups = 'CATEGORIES:' . rcube_vcard::vcard_quote(join(',', $groups)); + $record['vcard'] = str_replace('END:VCARD', $vgroups . rcube_vcard::$eol . 'END:VCARD', $record['vcard']); + } +} + + // Use search result if (!empty($_REQUEST['_search']) && isset($_SESSION['search'][$_REQUEST['_search']])) { @@ -42,23 +82,7 @@ if (!empty($_REQUEST['_search']) && isset($_SESSION['search'][$_REQUEST['_search while ($record = $result->next()) { // because vcard_map is per-source we need to create vcard here - if (empty($record['vcard']) || empty($record['name'])) { - $vcard = new rcube_vcard(); - $vcard->extend_fieldmap($source->vcard_map); - $vcard->load($record['vcard']); - $vcard->reset(); - - foreach ($record as $key => $values) { - list($field, $section) = explode(':', $key); - foreach ((array)$values as $value) { - if (is_array($value) || @strlen($value)) { - $vcard->set($field, $value, strtoupper($section)); - } - } - } - - $record['vcard'] = $vcard->export(true); - } + prepare_for_export($record, $source); $record['sourceid'] = $s; $key = rcube_addressbook::compose_contact_key($record, $sort_col); @@ -90,23 +114,7 @@ else if (!empty($_REQUEST['_cid'])) { while ($record = $result->next()) { // because vcard_map is per-source we need to create vcard here - if (empty($record['vcard']) || empty($record['name'])) { - $vcard = new rcube_vcard(); - $vcard->extend_fieldmap($source->vcard_map); - $vcard->load($record['vcard']); - $vcard->reset(); - - foreach ($record as $key => $values) { - list($field, $section) = explode(':', $key); - foreach ((array)$values as $value) { - if (is_array($value) || @strlen($value)) { - $vcard->set($field, $value, strtoupper($section)); - } - } - } - - $record['vcard'] = $vcard->export(true); - } + prepare_for_export($record, $source); $record['sourceid'] = $s; $key = rcube_addressbook::compose_contact_key($record, $sort_col); @@ -136,30 +144,12 @@ header('Content-Type: text/x-vcard; charset='.RCMAIL_CHARSET); header('Content-Disposition: attachment; filename="contacts.vcf"'); while ($result && ($row = $result->next())) { - // we already have a vcard record - if ($row['vcard'] && $row['name']) { - // fix folding and end-of-line chars - $row['vcard'] = preg_replace('/\r|\n\s+/', '', $row['vcard']); - $row['vcard'] = preg_replace('/\n/', rcube_vcard::$eol, $row['vcard']); - echo rcube_vcard::rfc2425_fold($row['vcard']) . rcube_vcard::$eol; - } - // copy values into vcard object - else { - $vcard = new rcube_vcard(); - $vcard->extend_fieldmap($CONTACTS->vcard_map); - $vcard->load($row['vcard']); - $vcard->reset(); + prepare_for_export($row, $CONTACTS); - foreach ($row as $key => $values) { - list($field, $section) = explode(':', $key); - foreach ((array)$values as $value) { - if (is_array($value) || @strlen($value)) - $vcard->set($field, $value, strtoupper($section)); - } - } - - echo $vcard->export(true) . rcube_vcard::$eol; - } + // fix folding and end-of-line chars + $row['vcard'] = preg_replace('/\r|\n\s+/', '', $row['vcard']); + $row['vcard'] = preg_replace('/\n/', rcube_vcard::$eol, $row['vcard']); + echo rcube_vcard::rfc2425_fold($row['vcard']) . rcube_vcard::$eol; } exit; diff --git a/program/steps/addressbook/import.inc b/program/steps/addressbook/import.inc index 915aac884..60f5d7b61 100644 --- a/program/steps/addressbook/import.inc +++ b/program/steps/addressbook/import.inc @@ -40,6 +40,7 @@ function rcmail_import_form($attrib) 'multiple' => 'multiple', )); $form = html::p(null, html::label('rcmimportfile', rcube_label('importfromfile')) . $upload->show()); + $table = new html_table(array('cols' => 2)); // addressbook selector if (count($writable_books) > 1) { @@ -48,17 +49,31 @@ function rcmail_import_form($attrib) foreach ($writable_books as $book) $select->add($book['name'], $book['id']); - $form .= html::p(null, html::label('rcmimporttarget', rcube_label('importtarget')) - . $select->show($target)); + $table->add('title', html::label('rcmimporttarget', rcube_label('importtarget'))); + $table->add(null, $select->show($target)); } else { $abook = new html_hiddenfield(array('name' => '_target', 'value' => key($writable_books))); $form .= $abook->show(); } + // selector for group import options + if (count($writable_books) >= 1 || $writable_books[0]->groups) { + $select = new html_select(array('name' => '_groups', 'id' => 'rcmimportgroups', 'is_escaped' => true)); + $select->add(rcube_label('none'), '0'); + $select->add(rcube_label('importgroupsall'), '1'); + $select->add(rcube_label('importgroupsexisting'), '2'); + + $table->add('title', html::label('rcmimportgroups', rcube_label('importgroups'))); + $table->add(null, $select->show(get_input_value('_groups', RCUBE_INPUT_GPC))); + } + + // checkbox to replace the entire address book $check_replace = new html_checkbox(array('name' => '_replace', 'value' => 1, 'id' => 'rcmimportreplace')); - $form .= html::p(null, $check_replace->show(get_input_value('_replace', RCUBE_INPUT_GPC)) . - html::label('rcmimportreplace', rcube_label('importreplace'))); + $table->add('title', html::label('rcmimportreplace', rcube_label('importreplace'))); + $table->add(null, $check_replace->show(get_input_value('_replace', RCUBE_INPUT_GPC))); + + $form .= $table->show(array('id' => null) + $attrib); $OUTPUT->set_env('writable_source', !empty($writable_books)); $OUTPUT->add_label('selectimportfile','importwait'); @@ -134,19 +149,50 @@ function rcmail_import_buttons($attrib) } +/** + * Returns the matching group id. If group doesn't exist, it'll be created if allowed. + */ +function rcmail_import_group_id($group_name, $CONTACTS, $create, &$import_groups) +{ + $group_id = 0; + foreach ($import_groups as $key => $group) { + if (strtolower($group['name']) == strtolower($group_name)) { + $group_id = $group['ID']; + break; + } + } + + // create a new group + if (!$group_id && $create) { + $new_group = $CONTACTS->create_group($group_name); + if (!$new_group['ID']) + $new_group['ID'] = $new_group['id']; + $import_groups[] = $new_group; + $group_id = $new_group['ID']; + } + + return $group_id; +} + + /** The import process **/ $importstep = 'rcmail_import_form'; if (is_array($_FILES['_file'])) { - $replace = (bool)get_input_value('_replace', RCUBE_INPUT_GPC); - $target = get_input_value('_target', RCUBE_INPUT_GPC); + $replace = (bool)get_input_value('_replace', RCUBE_INPUT_GPC); + $target = get_input_value('_target', RCUBE_INPUT_GPC); + $with_groups = intval(get_input_value('_groups', RCUBE_INPUT_GPC)); $vcards = array(); $upload_error = null; $CONTACTS = $RCMAIL->get_address_book($target, true); + if (!$CONTACTS->groups) { + $with_groups = false; + } + if ($CONTACTS->readonly) { $OUTPUT->show_message('addresswriterror', 'error'); } @@ -206,6 +252,10 @@ if (is_array($_FILES['_file'])) { $CONTACTS->delete_all(); } + if ($with_groups) { + $import_groups = $CONTACTS->list_groups(); + } + foreach ($vcards as $vcard) { $a_record = $vcard->get_assoc(); @@ -258,6 +308,15 @@ if (is_array($_FILES['_file'])) { $success = $plugin['result']; if ($success) { + // assign groups for this contact (if enabled) + if ($with_groups && !empty($a_record['groups'])) { + foreach (explode(',', $a_record['groups'][0]) as $group_name) { + if ($group_id = rcmail_import_group_id($group_name, $CONTACTS, $with_groups == 1, $import_groups)) { + $CONTACTS->add_to_group($group_id, $success); + } + } + } + $IMPORT_STATS->inserted++; $IMPORT_STATS->names[] = $a_record['name'] ? $a_record['name'] : $email; } diff --git a/program/steps/addressbook/save.inc b/program/steps/addressbook/save.inc index e7e5efc63..2adc53bcf 100644 --- a/program/steps/addressbook/save.inc +++ b/program/steps/addressbook/save.inc @@ -59,15 +59,34 @@ foreach ($GLOBALS['CONTACT_COLTYPES'] as $col => $colprop) { } // assign values and subtypes else if (is_array($_POST[$fname])) { - $values = get_input_value($fname, RCUBE_INPUT_POST, true); + $values = get_input_value($fname, RCUBE_INPUT_POST, true); $subtypes = get_input_value('_subtype_' . $col, RCUBE_INPUT_POST); + foreach ($values as $i => $val) { + if ($col == 'email') { + // extract email from full address specification, e.g. "Name" <addr@domain.tld> + $addr = rcube_mime::decode_address_list($val, 1, false); + if (!empty($addr) && ($addr = array_pop($addr)) && $addr['mailto']) { + $val = $addr['mailto']; + } + } + $subtype = $subtypes[$i] ? ':'.$subtypes[$i] : ''; $a_record[$col.$subtype][] = $val; } } else if (isset($_POST[$fname])) { $a_record[$col] = get_input_value($fname, RCUBE_INPUT_POST, true); + + // normalize the submitted date strings + if ($colprop['type'] == 'date') { + if ($timestamp = rcube_utils::strtotime($a_record[$col])) { + $a_record[$col] = date('Y-m-d', $timestamp); + } + else { + unset($a_record[$col]); + } + } } } @@ -75,8 +94,10 @@ foreach ($GLOBALS['CONTACT_COLTYPES'] as $col => $colprop) { if (empty($a_record['name'])) { $a_record['name'] = rcube_addressbook::compose_display_name($a_record, true); // Reset it if equals to email address (from compose_display_name()) - if ($a_record['name'] == $a_record['email'][0]) + $email = rcube_addressbook::get_col_values('email', $a_record, true); + if ($a_record['name'] == $email[0]) { $a_record['name'] = ''; + } } // do input checks (delegated to $CONTACTS instance) diff --git a/program/steps/mail/attachments.inc b/program/steps/mail/attachments.inc index f83f6892e..85aa9542b 100644 --- a/program/steps/mail/attachments.inc +++ b/program/steps/mail/attachments.inc @@ -118,9 +118,12 @@ if (is_array($_FILES['_attachments']['tmp_name'])) { 'alt' => rcube_label('delete') )); } - else { + else if ($COMPOSE['textbuttons']) { $button = Q(rcube_label('delete')); } + else { + $button = ''; + } $content = html::a(array( 'href' => "#delete", diff --git a/program/steps/mail/compose.inc b/program/steps/mail/compose.inc index b56f5b5b4..30c9f79fb 100644 --- a/program/steps/mail/compose.inc +++ b/program/steps/mail/compose.inc @@ -168,6 +168,8 @@ else if ($msg_uid = $COMPOSE['param']['forward_uid']) { else if ($msg_uid = $COMPOSE['param']['uid']) { $compose_mode = RCUBE_COMPOSE_EDIT; } + +$COMPOSE['mode'] = $compose_mode; $OUTPUT->set_env('compose_mode', $compose_mode); $config_show_sig = $RCMAIL->config->get('show_sig', 1); @@ -380,7 +382,12 @@ foreach ($parts as $header) { $mailto = format_email(rcube_idn_to_utf8($addr_part['mailto'])); if (!in_array($mailto, $a_recipients) - && ($header == 'to' || empty($MESSAGE->compose['from_email']) || $mailto != $MESSAGE->compose['from_email']) + && ( + $header == 'to' + || $compose_mode != RCUBE_COMPOSE_REPLY + || empty($MESSAGE->compose['from_email']) + || $mailto != $MESSAGE->compose['from_email'] + ) ) { if ($addr_part['name'] && $addr_part['mailto'] != $addr_part['name']) $string = format_email_recipient($mailto, $addr_part['name']); @@ -1366,8 +1373,9 @@ function rcmail_compose_attachment_list($attrib) if (!$attrib['id']) $attrib['id'] = 'rcmAttachmentList'; - $out = "\n"; + $out = "\n"; $jslist = array(); + $button = ''; if (is_array($COMPOSE['attachments'])) { if ($attrib['deleteicon']) { @@ -1376,27 +1384,38 @@ function rcmail_compose_attachment_list($attrib) 'alt' => rcube_label('delete') )); } - else + else if (rcube_utils::get_boolean($attrib['textbuttons'])) { $button = Q(rcube_label('delete')); + } foreach ($COMPOSE['attachments'] as $id => $a_prop) { if (empty($a_prop)) continue; - $out .= html::tag('li', array('id' => 'rcmfile'.$id, 'class' => rcmail_filetype2classname($a_prop['mimetype'], $a_prop['name'])), + $out .= html::tag('li', + array( + 'id' => 'rcmfile'.$id, + 'class' => rcmail_filetype2classname($a_prop['mimetype'], $a_prop['name']), + 'onmouseover' => "rcube_webmail.long_subject_title_ex(this, 0)", + ), html::a(array( 'href' => "#delete", 'title' => rcube_label('delete'), 'onclick' => sprintf("return %s.command('remove-attachment','rcmfile%s', this)", JS_OBJECT_NAME, $id), - 'class' => 'delete'), - $button) . Q($a_prop['name'])); + 'class' => 'delete' + ), + $button + ) . Q($a_prop['name']) + ); - $jslist['rcmfile'.$id] = array('name' => $a_prop['name'], 'complete' => true, 'mimetype' => $a_prop['mimetype']); + $jslist['rcmfile'.$id] = array('name' => $a_prop['name'], 'complete' => true, 'mimetype' => $a_prop['mimetype']); } } if ($attrib['deleteicon']) $COMPOSE['deleteicon'] = $CONFIG['skin_path'] . $attrib['deleteicon']; + else if (rcube_utils::get_boolean($attrib['textbuttons'])) + $COMPOSE['textbuttons'] = true; if ($attrib['cancelicon']) $OUTPUT->set_env('cancelicon', $CONFIG['skin_path'] . $attrib['cancelicon']); if ($attrib['loadingicon']) diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc index 018a31b84..a7d9ca240 100644 --- a/program/steps/mail/func.inc +++ b/program/steps/mail/func.inc @@ -1730,7 +1730,7 @@ function rcmail_identity_select($MESSAGE, $identities = null, $compose_mode = 'r $a_to = rcube_mime::decode_address_list($MESSAGE->headers->to, null, true, $MESSAGE->headers->charset); foreach ($a_to as $addr) { if (!empty($addr['mailto'])) { - $a_recipients[] = format_email($addr['mailto']); + $a_recipients[] = strtolower($addr['mailto']); $a_names[] = $addr['name']; } } @@ -1739,7 +1739,7 @@ function rcmail_identity_select($MESSAGE, $identities = null, $compose_mode = 'r $a_cc = rcube_mime::decode_address_list($MESSAGE->headers->cc, null, true, $MESSAGE->headers->charset); foreach ($a_cc as $addr) { if (!empty($addr['mailto'])) { - $a_recipients[] = format_email($addr['mailto']); + $a_recipients[] = strtolower($addr['mailto']); $a_names[] = $addr['name']; } } @@ -1765,7 +1765,7 @@ function rcmail_identity_select($MESSAGE, $identities = null, $compose_mode = 'r break; } // use replied message recipients - else if (($found = array_search($ident['email_ascii'], $a_recipients)) !== false) { + else if (($found = array_search(strtolower($ident['email_ascii']), $a_recipients)) !== false) { if ($found_idx === null) { $found_idx = $idx; } diff --git a/program/steps/mail/sendmail.inc b/program/steps/mail/sendmail.inc index 436b4eea7..8fe149611 100644 --- a/program/steps/mail/sendmail.inc +++ b/program/steps/mail/sendmail.inc @@ -825,15 +825,24 @@ if ($savedraft) { // start the auto-save timer again $OUTPUT->command('auto_save_start'); - - $OUTPUT->send('iframe'); } else { + $folders = array(); + + if ($COMPOSE['mode'] == 'reply' || $COMPOSE['mode'] == 'forward') + $folders[] = $COMPOSE['mailbox']; + rcmail_compose_cleanup($COMPOSE_ID); if ($store_folder && !$saved) - $OUTPUT->command('sent_successfully', 'error', rcube_label('errorsavingsent')); - else - $OUTPUT->command('sent_successfully', 'confirmation', rcube_label('messagesent'), $store_target); - $OUTPUT->send('iframe'); + $OUTPUT->command('sent_successfully', 'error', rcube_label('errorsavingsent'), $folders); + else { + if ($store_folder) { + $folders[] = $store_target; + } + + $OUTPUT->command('sent_successfully', 'confirmation', rcube_label('messagesent'), $folders); + } } + +$OUTPUT->send('iframe'); diff --git a/program/steps/mail/show.inc b/program/steps/mail/show.inc index 59f4d55e1..9d85f9c8f 100644 --- a/program/steps/mail/show.inc +++ b/program/steps/mail/show.inc @@ -175,9 +175,9 @@ function rcmail_message_attachments($attrib) $ol .= html::tag('li', null, Q(sprintf("%s (%s)", $filename, $size))); } else { - if (mb_strlen($filename) > 50) { + if ($attrib['maxlength'] && mb_strlen($filename) > $attrib['maxlength']) { $title = $filename; - $filename = abbreviate_string($filename, 50); + $filename = abbreviate_string($filename, $attrib['maxlength']); } else { $title = ''; @@ -190,6 +190,7 @@ function rcmail_message_attachments($attrib) 'href' => $MESSAGE->get_part_url($attach_prop->mime_id, false), 'onclick' => sprintf('return %s.command(\'load-attachment\',\'%s\',this)', JS_OBJECT_NAME, $attach_prop->mime_id), + 'onmouseover' => $title ? '' : 'rcube_webmail.long_subject_title_ex(this, 0)', 'title' => Q($title), ), Q($filename)); $ol .= html::tag('li', array('class' => $class, 'id' => $id), $link); diff --git a/program/steps/settings/edit_prefs.inc b/program/steps/settings/edit_prefs.inc index 468e4994d..adf6b1623 100644 --- a/program/steps/settings/edit_prefs.inc +++ b/program/steps/settings/edit_prefs.inc @@ -40,24 +40,21 @@ function rcmail_user_prefs_form($attrib) $out = $form_start; - foreach ($SECTIONS[$CURR_SECTION]['blocks'] as $block) { + foreach ($SECTIONS[$CURR_SECTION]['blocks'] as $class => $block) { if (!empty($block['options'])) { $table = new html_table(array('cols' => 2)); foreach ($block['options'] as $option) { - if ($option['advanced']) - $table->set_row_attribs('advanced'); - if (isset($option['title'])) { $table->add('title', $option['title']); - $table->add(null, $option['content']); + $table->add(null, $option['content']); } else { $table->add(array('colspan' => 2), $option['content']); } } - $out .= html::tag('fieldset', null, html::tag('legend', null, $block['name']) . $table->show($attrib)); + $out .= html::tag('fieldset', $class, html::tag('legend', null, $block['name']) . $table->show($attrib)); } else if (!empty($block['content'])) { $out .= html::tag('fieldset', null, html::tag('legend', null, $block['name']) . $block['content']); diff --git a/program/steps/settings/func.inc b/program/steps/settings/func.inc index 57c52a01e..b492c9644 100644 --- a/program/steps/settings/func.inc +++ b/program/steps/settings/func.inc @@ -158,6 +158,7 @@ function rcmail_user_prefs($current = null) 'main' => array('name' => Q(rcube_label('mainoptions'))), 'skin' => array('name' => Q(rcube_label('skin'))), 'browser' => array('name' => Q(rcube_label('browseroptions'))), + 'advanced'=> array('name' => Q(rcube_label('advancedoptions'))), ); // language selection @@ -367,6 +368,7 @@ function rcmail_user_prefs($current = null) $blocks = array( 'main' => array('name' => Q(rcube_label('mainoptions'))), 'new_message' => array('name' => Q(rcube_label('newmessage'))), + 'advanced' => array('name' => Q(rcube_label('advancedoptions'))), ); // show config parameter for preview pane @@ -488,6 +490,7 @@ function rcmail_user_prefs($current = null) case 'mailview': $blocks = array( 'main' => array('name' => Q(rcube_label('mainoptions'))), + 'advanced' => array('name' => Q(rcube_label('advancedoptions'))), ); // show checkbox to open message view in new window @@ -543,7 +546,7 @@ function rcmail_user_prefs($current = null) $field_id = 'rcmfd_default_charset'; - $blocks['main']['options']['default_charset'] = array( + $blocks['advanced']['options']['default_charset'] = array( 'title' => html::label($field_id, Q(rcube_label('defaultcharset'))), 'content' => $RCMAIL->output->charset_selector(array( 'id' => $field_id, 'name' => '_default_charset', 'selected' => $config['default_charset'] @@ -605,6 +608,7 @@ function rcmail_user_prefs($current = null) 'main' => array('name' => Q(rcube_label('mainoptions'))), 'sig' => array('name' => Q(rcube_label('signatureoptions'))), 'spellcheck' => array('name' => Q(rcube_label('spellcheckoptions'))), + 'advanced' => array('name' => Q(rcube_label('advancedoptions'))), ); // show checkbox to compose messages in a new window @@ -673,8 +677,7 @@ function rcmail_user_prefs($current = null) $select->add(rcube_label('miscfolding'), 1); $select->add(rcube_label('2047folding'), 2); - $blocks['main']['options']['mime_param_folding'] = array( - 'advanced' => true, + $blocks['advanced']['options']['mime_param_folding'] = array( 'title' => html::label($field_id, Q(rcube_label('mimeparamfolding'))), 'content' => $select->show($config['mime_param_folding']), ); @@ -688,8 +691,7 @@ function rcmail_user_prefs($current = null) $field_id = 'rcmfd_force_7bit'; $input = new html_checkbox(array('name' => '_force_7bit', 'id' => $field_id, 'value' => 1)); - $blocks['main']['options']['force_7bit'] = array( - 'advanced' => true, + $blocks['advanced']['options']['force_7bit'] = array( 'title' => html::label($field_id, Q(rcube_label('force7bit'))), 'content' => $input->show($config['force_7bit']?1:0), ); @@ -871,7 +873,8 @@ function rcmail_user_prefs($current = null) // Addressbook config case 'addressbook': $blocks = array( - 'main' => array('name' => Q(rcube_label('mainoptions'))), + 'main' => array('name' => Q(rcube_label('mainoptions'))), + 'advanced' => array('name' => Q(rcube_label('advancedoptions'))), ); if (!isset($no_override['default_addressbook']) @@ -967,7 +970,8 @@ function rcmail_user_prefs($current = null) // Special IMAP folders case 'folders': $blocks = array( - 'main' => array('name' => Q(rcube_label('mainoptions'))), + 'main' => array('name' => Q(rcube_label('mainoptions'))), + 'advanced' => array('name' => Q(rcube_label('advancedoptions'))), ); if (!isset($no_override['show_real_foldernames'])) { @@ -1048,6 +1052,7 @@ function rcmail_user_prefs($current = null) $blocks = array( 'main' => array('name' => Q(rcube_label('mainoptions'))), 'maintenance' => array('name' => Q(rcube_label('maintenance'))), + 'advanced' => array('name' => Q(rcube_label('advancedoptions'))), ); if (!isset($no_override['read_when_deleted'])) { diff --git a/skins/classic/mail.css b/skins/classic/mail.css index b8cc9f351..43367749c 100644 --- a/skins/classic/mail.css +++ b/skins/classic/mail.css @@ -1592,10 +1592,9 @@ input.from_address #compose-attachments ul li { height: 18px; + line-height: 16px; font-size: 11px; - padding-left: 2px; - padding-top: 2px; - padding-right: 4px; + padding: 2px 2px 1px 2px; border-bottom: 1px solid #EBEBEB; white-space: nowrap; overflow: hidden; @@ -1608,8 +1607,10 @@ input.from_address text-indent: -5000px; width: 17px; height: 16px; + padding-bottom: 2px; display: inline-block; text-decoration: none; + vertical-align: middle; } #compose-attachments li img diff --git a/skins/classic/templates/message.html b/skins/classic/templates/message.html index 757c0a635..bd4fbf277 100644 --- a/skins/classic/templates/message.html +++ b/skins/classic/templates/message.html @@ -49,7 +49,7 @@ </div> <roundcube:object name="messageHeaders" class="headers-table" cellspacing="0" cellpadding="2" addicon="/images/icons/silhouette.png" summary="Message headers" /> <roundcube:object name="messageFullHeaders" id="full-headers" /> -<roundcube:object name="messageAttachments" id="attachment-list" /> +<roundcube:object name="messageAttachments" id="attachment-list" maxlength="50" /> <roundcube:object name="messageObjects" id="message-objects" /> <roundcube:object name="messageBody" id="messagebody" /> </div> diff --git a/skins/classic/templates/messagepreview.html b/skins/classic/templates/messagepreview.html index b42a06342..82414c420 100644 --- a/skins/classic/templates/messagepreview.html +++ b/skins/classic/templates/messagepreview.html @@ -20,7 +20,7 @@ </div> <roundcube:object name="messageHeaders" class="headers-table" cellspacing="0" cellpadding="2" addicon="/images/icons/silhouette.png" summary="Message headers" /> <roundcube:object name="messageFullHeaders" id="full-headers" /> -<roundcube:object name="messageAttachments" id="attachment-list" /> +<roundcube:object name="messageAttachments" id="attachment-list" maxlength="50" /> </div> <roundcube:object name="messageObjects" id="message-objects" /> diff --git a/skins/larry/addressbook.css b/skins/larry/addressbook.css index 6bf9426c4..39d0cce21 100644 --- a/skins/larry/addressbook.css +++ b/skins/larry/addressbook.css @@ -387,3 +387,8 @@ a.deletebutton { overflow: auto; padding: 10px; } + +#import-box p, +#import-box .propform { + max-width: 50em; +} diff --git a/skins/larry/settings.css b/skins/larry/settings.css index 59037ac76..6afa48c40 100644 --- a/skins/larry/settings.css +++ b/skins/larry/settings.css @@ -48,6 +48,26 @@ border-radius: 4px 4px 0 0; } +#preferences-details fieldset.advanced legend { + position: relative; + display: block; + width: 100%; + cursor: pointer; +} + +#preferences-details fieldset.advanced .propform { + display: none; +} + +#preferences-details fieldset.advanced .advanced-toggle { + position: absolute; + top: 2px; + right: 6px; + text-decoration: none; + color: #666; + font-size: 11px; +} + #sections-table tbody td.section, #settings-sections span.listitem a, #settings-sections span.tablink a { diff --git a/skins/larry/styles.css b/skins/larry/styles.css index d542768b7..4b238c163 100644 --- a/skins/larry/styles.css +++ b/skins/larry/styles.css @@ -2292,12 +2292,13 @@ ul.toolbarmenu li span.conversation { display: block; color: #333; font-weight: bold; - padding: 8px 15px 3px 30px; + 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; } .attachmentslist li a.drop { @@ -2326,9 +2327,9 @@ ul.toolbarmenu li span.conversation { .attachmentslist li a.delete, .attachmentslist li a.cancelupload { position: absolute; - top: 6px; + top: 4px; right: 0; - width: 24px; + width: 20px; height: 18px; padding: 0; text-decoration: none; diff --git a/skins/larry/templates/importcontacts.html b/skins/larry/templates/importcontacts.html index d3d0f2b93..69b138b9a 100644 --- a/skins/larry/templates/importcontacts.html +++ b/skins/larry/templates/importcontacts.html @@ -18,7 +18,7 @@ <h2 class="boxtitle"><roundcube:label name="importcontacts" /></h2> <div id="import-box" class="boxcontent"> -<roundcube:object name="importstep" /> +<roundcube:object name="importstep" class="propform" /> <br/> <p class="formbuttons"> <roundcube:object name="importnav" class="button" /> diff --git a/skins/larry/ui.js b/skins/larry/ui.js index ae14d81b2..d558f16a2 100644 --- a/skins/larry/ui.js +++ b/skins/larry/ui.js @@ -195,6 +195,19 @@ function rcube_mail_ui() new rcube_splitter({ id:'prefviewsplitter', p1:'#sectionslist', p2:'#preferences-box', orientation:'v', relative:true, start:266, min:180, size:12 }).init(); } + else if (rcmail.env.action == 'edit-prefs') { + $('<a href="#toggle">▼</a>') + .addClass('advanced-toggle') + .appendTo('#preferences-details fieldset.advanced legend'); + + $('#preferences-details fieldset.advanced legend').click(function(e){ + var collapsed = $(this).hasClass('collapsed'), + toggle = $('.advanced-toggle', this).html(collapsed ? '▲' : '▼'); + $(this) + .toggleClass('collapsed') + .closest('fieldset').children('.propform').toggle() + }).addClass('collapsed') + } } /*** addressbook task ***/ else if (rcmail.env.task == 'addressbook') { diff --git a/tests/Framework/Browser.php b/tests/Framework/Browser.php index c3860d8a3..832d4bf14 100644 --- a/tests/Framework/Browser.php +++ b/tests/Framework/Browser.php @@ -17,4 +17,207 @@ class Framework_Browser extends PHPUnit_Framework_TestCase $this->assertInstanceOf('rcube_browser', $object, "Class constructor"); } + + /** + * @dataProvider browsers + */ + function test_browser($useragent, $opera, $chrome, $ie, $ns, $ns4, $khtml, $safari, $mz) + { + + $object = $this->getBrowser($useragent); + + $this->assertEquals($opera, $object->opera, 'Check for Opera failed'); + $this->assertEquals($chrome, $object->chrome, 'Check for Chrome failed'); + $this->assertEquals($ie, $object->ie, 'Check for IE failed'); + $this->assertEquals($ns, $object->ns, 'Check for NS failed'); + $this->assertEquals($ns4, $object->ns4, 'Check for NS4 failed'); + $this->assertEquals($khtml, $object->khtml, 'Check for khtml failed'); + $this->assertEquals($safari, $object->safari, 'Check for Safari failed'); + $this->assertEquals($mz, $object->mz, 'Check for MZ failed'); + } + + /** + * @dataProvider os + */ + function test_os($useragent, $windows, $linux, $unix, $mac) + { + $object = $this->getBrowser($useragent); + + $this->assertEquals($windows, $object->win, 'Check Result of Windows'); + $this->assertEquals($linux, $object->linux, 'Check Result of Linux'); + $this->assertEquals($mac, $object->mac, 'Check Result of Mac'); + $this->assertEquals($unix, $object->unix, 'Check Result of Unix'); + + } + + /** + * @dataProvider versions + */ + function test_version($useragent, $version) + { + $object = $this->getBrowser($useragent); + $this->assertEquals($version, $object->ver); + } + + /** + * @dataProvider dom + */ + function test_dom($useragent, $dom) + { + $object = $this->getBrowser($useragent); + $this->assertEquals($dom, $object->dom); + + } + + /** + * @dataProvider pngalpha + */ + function test_pngalpha($useragent, $pngalpha) + { + $object = $this->getBrowser($useragent); + $this->assertEquals($pngalpha, $object->pngalpha); + } + + /** + * @dataProvider imgdata + */ + function test_imgdata($useragent, $imgdata) + { + $object = $this->getBrowser($useragent); + $this->assertEquals($imgdata, $object->imgdata); + } + + function versions() + { + return $this->extractDataSet(array('version')); + } + + function pngalpha() + { + return $this->extractDataSet(array('canPNGALPHA')); + } + + function imgdata() + { + return $this->extractDataSet(array('canIMGDATA')); + } + + private function extractDataSet($keys) + { + $keys = array_merge(array('useragent'), $keys); + + $browser = $this->useragents(); + + $extracted = array(); + + foreach ($browser as $label => $data) { + foreach($keys as $key) { + $extracted[$data['useragent']][] = $data[$key]; + } + + } + + return $extracted; + } + + function lang() + { + return $this->extractDataSet(array('lang')); + } + + function dom() + { + return $this->extractDataSet(array('hasDOM')); + } + + function browsers() + { + return $this->extractDataSet(array('isOpera','isChrome','isIE','isNS','isNS4','isKHTML','isSafari','isMZ')); + } + + function useragents() + { + return array( + 'WIN: Mozilla Firefox ' => array( + 'useragent' => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.1) Gecko/20060111 Firefox/1.5.0.1', + 'version' => '1.8', //Version + 'isWin' => true, //isWindows + 'isLinux' => false, + 'isMac' => false, //isMac + 'isUnix' => false, //isUnix + 'isOpera' => false, //isOpera + 'isChrome' => false, //isChrome + 'isIE' => false, //isIE + 'isNS' => false, //isNS + 'isNS4' => false, //isNS4 + 'isKHTML' => false, //isKHTML + 'isSafari' => false, //isSafari + 'isMZ' => true, //isMZ + 'lang' => 'en-US', //lang + 'hasDOM' => true, //hasDOM + 'canPNGALPHA' => true, //canPNGALPHA + 'canIMGDATA' => true, //canIMGDATA + ), + 'LINUX: Bon Echo ' => array( + 'useragent' => 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.1) Gecko/20070222 BonEcho/2.0.0.1', + 'version' => '1.8', //Version + 'isWin' => false, //isWindows + 'isLinux' => true, + 'isMac' => false, //isMac + 'isUnix' => false, //isUnix + 'isOpera' => false, //isOpera + 'isChrome' => false, //isChrome + 'isIE' => false, //isIE + 'isNS' => false, //isNS + 'isNS4' => false, //isNS4 + 'isKHTML' => false, //isKHTML + 'isSafari' => false, //isSafari + 'isMZ' => true, //isMZ + 'lang' => 'en-US', //lang + 'hasDOM' => true, //hasDOM + 'canPNGALPHA' => true, //canPNGALPHA + 'canIMGDATA' => true, //canIMGDATA + ), + + 'Chrome Mac' => array( + 'useragent' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.461.0 Safari/534.3', + 'version' => '5', //Version + 'isWin' => false, //isWindows + 'isLinux' => false, + 'isMac' => true, //isMac + 'isUnix' => false, //isUnix + 'isOpera' => false, //isOpera + 'isChrome' => true, //isChrome + 'isIE' => false, //isIE + 'isNS' => false, //isNS + 'isNS4' => false, //isNS4 + 'isKHTML' => true, //isKHTML + 'isSafari' => false, //isSafari + 'isMZ' => false, //isMZ + 'lang' => 'en-US', //lang + 'hasDOM' => false, //hasDOM + 'canPNGALPHA' => false, //canPNGALPHA + 'canIMGDATA' => true, //canIMGDATA + ), + ); + } + + function os() + { + return $this->extractDataSet(array('isWin','isLinux','isUnix','isMac')); + } + + /** + * @param string $useragent + * @return rcube_browser + */ + private function getBrowser($useragent) + { + /** @var $object rcube_browser */ + $_SERVER['HTTP_USER_AGENT'] = $useragent; + + $object = new rcube_browser(); + + return $object; + } } |