diff options
58 files changed, 1164 insertions, 400 deletions
| @@ -1,12 +1,21 @@  CHANGELOG Roundcube Webmail  =========================== -- Fix bugs when invoking contact creation form when read-only addressbook is selected (#1489296) -- Fix identity selection on reply (#1489291) -- Fix so additional headers are added to all messages sent (#1489284) -- Fix display issue after moving folder in Folder Manager (#1489293) -- Fix handling of non-default date formats (#1489294) -- Fix unquoted path in PREG expression on Windows (#1489290) +- 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)  - Fix replacement variables in user-specific base_dn in some LDAP requests (#1489279)  - Fix image scaling issues when image has only one dimension smaller than the limit (#1489274)  - Fix issue where uploaded photo was lost when contact form did not validate (#1489274) @@ -63,6 +72,23 @@ CHANGELOG Roundcube Webmail  - Fix export of selected contacts from search result (#1488905)  - Feature to export only selected contacts from addressbook (by Phil Weir) +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) +- Fix setting of Junk and NonJunk flags by markasjunk plugin (#1489285) +- Fix lack of Reply-To address in header of forwarded message body (#1489298) +- Fix bugs when invoking contact creation form when read-only addressbook is selected (#1489296) +- Fix identity selection on reply (#1489291) +- Fix so additional headers are added to all messages sent (#1489284) +- Fix display issue after moving folder in Folder Manager (#1489293) +- Fix handling of non-default date formats (#1489294) +- Fix unquoted path in PREG expression on Windows (#1489290) +- Fix wrong close tag in /template/mail.html (#1489295) +  RELEASE 0.9.3  -------------  - Fix setting refresh_interval to "Never" in Preferences (#1489286) @@ -72,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) @@ -293,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) @@ -348,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) @@ -384,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 @@ -511,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) @@ -679,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  ------------- @@ -729,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) @@ -1173,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) @@ -1300,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 30ed70bd2..95e024ccc 100644 --- a/config/defaults.inc.php +++ b/config/defaults.inc.php @@ -63,7 +63,7 @@ $config['syslog_facility'] = LOG_USER;  // Log sent messages to <log_dir>/sendmail or to syslog  $config['smtp_log'] = true; -// Log successful logins to <log_dir>/userlogins or to syslog +// Log successful/failed logins to <log_dir>/userlogins or to syslog  $config['log_logins'] = false;  // Log session authentication errors to <log_dir>/session or to syslog @@ -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  // ---------------------------------- @@ -259,6 +264,10 @@ $config['log_dir'] = 'logs/';  // use this folder to store temp files (must be writeable for apache user)  $config['temp_dir'] = 'temp/'; +// expire files in temp_dir after 48 hours +// possible units: s, m, h, d, w +$config['temp_dir_ttl'] = '48h'; +  // enforce connections over https  // with this option enabled, all non-secure connections will be redirected.  // set the port for the ssl connection as value of this option if it differs from the default 443 @@ -339,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: @@ -138,7 +138,7 @@ if ($RCMAIL->task == 'login' && $RCMAIL->action == 'login') {    }    else {      if (!$auth['valid']) { -      $error_code  = RCMAIL::ERROR_INVALID_REQUEST; +      $error_code = RCMAIL::ERROR_INVALID_REQUEST;      }      else {        $error_code = $auth['error'] ? $auth['error'] : $RCMAIL->login_error(); @@ -153,6 +153,9 @@ if ($RCMAIL->task == 'login' && $RCMAIL->action == 'login') {      $error_message = $error_labels[$error_code] ? $error_labels[$error_code] : 'loginfailed'; +    // log failed login +    $RCMAIL->log_login($auth['user'], true, $error_code); +      $OUTPUT->show_message($error_message, 'warning');      $RCMAIL->plugins->exec_hook('login_failed', array(        'code' => $error_code, 'host' => $auth['host'], 'user' => $auth['user'])); 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/markasjunk/markasjunk.php b/plugins/markasjunk/markasjunk.php index 76b14c140..4448b506c 100644 --- a/plugins/markasjunk/markasjunk.php +++ b/plugins/markasjunk/markasjunk.php @@ -19,6 +19,7 @@ class markasjunk extends rcube_plugin      $rcmail = rcmail::get_instance();      $this->register_action('plugin.markasjunk', array($this, 'request_action')); +    $this->add_hook('storage_init', array($this, 'storage_init'));      if ($rcmail->action == '' || $rcmail->action == 'show') {        $skin_path = $this->local_skin_path(); @@ -38,24 +39,36 @@ class markasjunk extends rcube_plugin      }    } +  function storage_init($args) +  { +    $flags = array( +      'JUNK'    => 'Junk', +      'NONJUNK' => 'NonJunk', +    ); + +    // register message flags +    $args['message_flags'] = array_merge((array)$args['message_flags'], $flags); + +    return $args; +  } +    function request_action()    {      $this->add_texts('localization'); -    $GLOBALS['IMAP_FLAGS']['JUNK'] = 'Junk'; -    $GLOBALS['IMAP_FLAGS']['NONJUNK'] = 'NonJunk'; -          $uids = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);      $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST); -     -    $rcmail = rcmail::get_instance(); -    $rcmail->storage->unset_flag($uids, 'NONJUNK'); -    $rcmail->storage->set_flag($uids, 'JUNK'); -     + +    $rcmail  = rcmail::get_instance(); +    $storage = $rcmail->get_storage(); + +    $storage->unset_flag($uids, 'NONJUNK'); +    $storage->set_flag($uids, 'JUNK'); +      if (($junk_mbox = $rcmail->config->get('junk_mbox')) && $mbox != $junk_mbox) {        $rcmail->output->command('move_messages', $junk_mbox);      } -     +      $rcmail->output->command('display_message', $this->gettext('reportedasjunk'), 'confirmation');      $rcmail->output->send();    } diff --git a/plugins/markasjunk/package.xml b/plugins/markasjunk/package.xml index a5b4bf92c..95597481a 100644 --- a/plugins/markasjunk/package.xml +++ b/plugins/markasjunk/package.xml @@ -13,11 +13,10 @@  		<email>roundcube@gmail.com</email>  		<active>yes</active>  	</lead> -	<date>2010-03-29</date> -	<time>13:20:00</time> +	<date>2013-08-29</date>  	<version> -		<release>1.1</release> -		<api>1.1</api> +		<release>1.2</release> +		<api>1.2</api>  	</version>  	<stability>  		<release>stable</release> 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 c398424b6..846bc94c3 100644 --- a/plugins/newmail_notifier/newmail_notifier.js +++ b/plugins/newmail_notifier/newmail_notifier.js @@ -30,9 +30,9 @@ function newmail_notifier_run(prop)  function newmail_notifier_stop(prop)  {      // revert original favicon -    if (rcmail.env.favicon_href && (!prop || prop.action != 'check-recent')) { +    if (rcmail.env.favicon_href && rcmail.env.favicon_changed && (!prop || prop.action != 'check-recent')) {          $('<link rel="shortcut icon" href="'+rcmail.env.favicon_href+'"/>').replaceAll('link[rel="shortcut icon"]'); -        rcmail.env.favicon_href = null; +        rcmail.env.favicon_changed = 0;      }      // Remove IE icon overlay if we're pinned to Taskbar @@ -54,7 +54,10 @@ function newmail_notifier_basic()      var link = $('<link rel="shortcut icon" href="plugins/newmail_notifier/favicon.ico"/>'),          oldlink = $('link[rel="shortcut icon"]', w.document); -    rcmail.env.favicon_href = oldlink.attr('href'); +    if (!rcmail.env.favicon_href) +        rcmail.env.favicon_href = oldlink.attr('href'); + +    rcmail.env.favicon_changed = 1;      link.replaceAll(oldlink);      // Add IE icon overlay if we're pinned to Taskbar @@ -87,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'), { @@ -106,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;          }      } @@ -122,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 62f6b6c46..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'])) { @@ -954,22 +961,32 @@ class rcmail extends rcube      /**       * Write login data (name, ID, IP address) to the 'userlogins' log file.       */ -    public function log_login() +    public function log_login($user = null, $failed_login = false, $error_code = 0)      {          if (!$this->config->get('log_logins')) {              return;          } -        $user_name = $this->get_user_name(); -        $user_id   = $this->get_user_id(); +        // 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; +            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());          } -        self::write_log('userlogins', -            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);      } 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.php b/program/lib/Roundcube/rcube.php index af9c069cf..d9c3dd8b9 100644 --- a/program/lib/Roundcube/rcube.php +++ b/program/lib/Roundcube/rcube.php @@ -498,7 +498,14 @@ class rcube      public function gc_temp()      {          $tmp = unslashify($this->config->get('temp_dir')); -        $expire = time() - 172800;  // expire in 48 hours + +        // expire in 48 hours by default +        $temp_dir_ttl = $this->config->get('temp_dir_ttl', '48h'); +        $temp_dir_ttl = get_offset_sec($temp_dir_ttl); +        if ($temp_dir_ttl < 6*3600) +            $temp_dir_ttl = 6*3600;   // 6 hours sensible lower bound. + +        $expire = time() - $temp_dir_ttl;          if ($tmp && ($dir = opendir($tmp))) {              while (($fname = readdir($dir)) !== false) { 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 62567a0e0..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(); +        $success = false; -            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; +        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); + +        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; -        // 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; +                // 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) @@ -282,8 +324,8 @@ class rcube_config       */      public function merge($prefs)      { +        $prefs = $this->fix_legacy_props($prefs);          $this->prop = array_merge($this->prop, $prefs, $this->userprefs); -        $this->fix_legacy_props();      } @@ -295,6 +337,8 @@ class rcube_config       */      public function set_user_prefs($prefs)      { +        $prefs = $this->fix_legacy_props($prefs); +          // Honor the dont_override setting for any existing user preferences          $dont_override = $this->get('dont_override');          if (is_array($dont_override) && !empty($dont_override)) { @@ -303,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; @@ -315,22 +354,13 @@ class rcube_config          $this->userprefs = $prefs;          $this->prop      = array_merge($this->prop, $prefs); - -        $this->fix_legacy_props(); - -        // 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']);      }      /**       * Getter for all config options       * -     * @return array  Hash array containg all config properties +     * @return array  Hash array containing all config properties       */      public function all()      { @@ -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 */ } @@ -482,16 +511,93 @@ class rcube_config      /**       * Convert legacy options into new ones +     * +     * @param array $props Hash array with config props +     * +     * @return array Converted config props       */ -    private function fix_legacy_props() +    private function fix_legacy_props($props)      {          foreach ($this->legacy_props as $new => $old) { -            if (isset($this->prop[$old])) { -                if (!isset($this->prop[$new])) { -                    $this->prop[$new] = $this->prop[$old]; +            if (isset($props[$old])) { +                if (!isset($props[$new])) { +                    $props[$new] = $props[$old];                  } -                unset($this->prop[$old]); +                unset($props[$old]); +            } +        } + +        // convert deprecated numeric timezone value +        if (isset($props['timezone']) && is_numeric($props['timezone'])) { +            if ($tz = self::timezone_name_from_abbr($props['timezone'])) { +                $props['timezone'] = $tz; +            } +            else { +                unset($props['timezone']);              }          } + +        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 c5346c8aa..aa074233f 100644 --- a/program/lib/Roundcube/rcube_imap.php +++ b/program/lib/Roundcube/rcube_imap.php @@ -70,7 +70,7 @@ class rcube_imap extends rcube_storage      protected $search_sort_field = '';      protected $search_threads = false;      protected $search_sorted = false; -    protected $options = array('auth_method' => 'check'); +    protected $options = array('auth_type' => 'check');      protected $caching = false;      protected $messages_caching = false;      protected $threading = false; @@ -391,10 +391,10 @@ class rcube_imap extends rcube_storage      public function check_permflag($flag)      {          $flag       = strtoupper($flag); -        $imap_flag  = $this->conn->flags[$flag];          $perm_flags = $this->get_permflags($this->folder); +        $imap_flag  = $this->conn->flags[$flag]; -        return in_array_nocase($imap_flag, $perm_flags); +        return $imap_flag && !empty($perm_flags) && in_array_nocase($imap_flag, $perm_flags);      } @@ -410,17 +410,7 @@ class rcube_imap extends rcube_storage          if (!strlen($folder)) {              return array();          } -/* -        Checking PERMANENTFLAGS is rather rare, so we disable caching of it -        Re-think when we'll use it for more than only MDNSENT flag -        $cache_key = 'mailboxes.permanentflags.' . $folder; -        $permflags = $this->get_cache($cache_key); - -        if ($permflags !== null) { -            return explode(' ', $permflags); -        } -*/          if (!$this->check_connection()) {              return array();          } @@ -435,10 +425,7 @@ class rcube_imap extends rcube_storage          if (!is_array($permflags)) {              $permflags = array();          } -/* -        // Store permflags as string to limit cached object size -        $this->update_cache($cache_key, implode(' ', $permflags)); -*/ +          return $permflags;      } @@ -3798,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_imap_generic.php b/program/lib/Roundcube/rcube_imap_generic.php index e1193749b..bce4cd4e2 100644 --- a/program/lib/Roundcube/rcube_imap_generic.php +++ b/program/lib/Roundcube/rcube_imap_generic.php @@ -704,22 +704,11 @@ class rcube_imap_generic       */      function connect($host, $user, $password, $options=null)      { -        // set options -        if (is_array($options)) { -            $this->prefs = $options; -        } -        // set auth method -        if (!empty($this->prefs['auth_type'])) { -            $auth_method = strtoupper($this->prefs['auth_type']); -        } else { -            $auth_method = 'CHECK'; -        } +        // configure +        $this->set_prefs($options); -        if (!empty($this->prefs['disabled_caps'])) { -            $this->prefs['disabled_caps'] = array_map('strtoupper', (array)$this->prefs['disabled_caps']); -        } - -        $result = false; +        $auth_method = $this->prefs['auth_type']; +        $result      = false;          // initialize connection          $this->error    = ''; @@ -896,6 +885,36 @@ class rcube_imap_generic      }      /** +     * Initializes environment +     */ +    protected function set_prefs($prefs) +    { +        // set preferences +        if (is_array($prefs)) { +            $this->prefs = $prefs; +        } + +        // set auth method +        if (!empty($this->prefs['auth_type'])) { +            $this->prefs['auth_type'] = strtoupper($this->prefs['auth_type']); +        } +        else { +            $this->prefs['auth_type'] = 'CHECK'; +        } + +        // disabled capabilities +        if (!empty($this->prefs['disabled_caps'])) { +            $this->prefs['disabled_caps'] = array_map('strtoupper', (array)$this->prefs['disabled_caps']); +        } + +        // additional message flags +        if (!empty($this->prefs['message_flags'])) { +            $this->flags = array_merge($this->flags, $this->prefs['message_flags']); +            unset($this->prefs['message_flags']); +        } +    } + +    /**       * Checks connection status       *       * @return bool True if connection is active and user is logged in, False otherwise. 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_storage.php b/program/lib/Roundcube/rcube_storage.php index de8334551..e697b2c73 100644 --- a/program/lib/Roundcube/rcube_storage.php +++ b/program/lib/Roundcube/rcube_storage.php @@ -39,7 +39,7 @@ abstract class rcube_storage      protected $default_charset = 'ISO-8859-1';      protected $default_folders = array('INBOX');      protected $search_set; -    protected $options = array('auth_method' => 'check'); +    protected $options = array('auth_type' => 'check');      protected $page_size = 10;      protected $threading = false; 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 9dadfe4ad..39dca3b03 100644 --- a/program/steps/mail/compose.inc +++ b/program/steps/mail/compose.inc @@ -165,6 +165,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); @@ -377,7 +379,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']); @@ -947,10 +954,10 @@ function rcmail_create_forward_body($body, $bodyIsHtml)      $prefix .= rcube_label('from')    . ': ' . $MESSAGE->get_header('from') . "\n";      $prefix .= rcube_label('to')      . ': ' . $MESSAGE->get_header('to') . "\n"; -    if ($MESSAGE->headers->cc) -      $prefix .= rcube_label('cc') . ': ' . $MESSAGE->get_header('cc') . "\n"; -    if ($MESSAGE->headers->replyto && $MESSAGE->headers->replyto != $MESSAGE->headers->from) -      $prefix .= rcube_label('replyto') . ': ' . $MESSAGE->get_header('replyto') . "\n"; +    if ($cc = $MESSAGE->headers->get('cc')) +      $prefix .= rcube_label('cc') . ': ' . $cc . "\n"; +    if (($replyto = $MESSAGE->headers->get('reply-to')) && $replyto != $MESSAGE->get_header('from')) +      $prefix .= rcube_label('replyto') . ': ' . $replyto . "\n";      $prefix .= "\n";      $body = trim($body, "\r\n"); @@ -973,15 +980,13 @@ function rcmail_create_forward_body($body, $bodyIsHtml)        rcube_label('from'), Q($MESSAGE->get_header('from'), 'replace'),        rcube_label('to'), Q($MESSAGE->get_header('to'), 'replace')); -    if ($MESSAGE->headers->cc) +    if ($cc = $MESSAGE->headers->get('cc'))        $prefix .= sprintf("<tr><th align=\"right\" nowrap=\"nowrap\" valign=\"baseline\">%s: </th><td>%s</td></tr>", -        rcube_label('cc'), -        Q($MESSAGE->get_header('cc'), 'replace')); +        rcube_label('cc'), Q($cc, 'replace')); -    if ($MESSAGE->headers->replyto && $MESSAGE->headers->replyto != $MESSAGE->headers->from) +    if (($replyto = $MESSAGE->headers->get('reply-to')) && $replyto != $MESSAGE->get_header('from'))        $prefix .= sprintf("<tr><th align=\"right\" nowrap=\"nowrap\" valign=\"baseline\">%s: </th><td>%s</td></tr>", -        rcube_label('replyto'), -        Q($MESSAGE->get_header('replyto'), 'replace')); +        rcube_label('replyto'), Q($replyto, 'replace'));      $prefix .= "</tbody></table><br>";    } @@ -1365,8 +1370,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']) { @@ -1375,27 +1381,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']) @@ -1422,7 +1439,7 @@ function rcmail_compose_attachment_form($attrib)    $out = html::div($attrib,      $OUTPUT->form_tag(array('id' => $attrib['id'].'Frm', 'name' => 'uploadform', 'method' => 'post', 'enctype' => 'multipart/form-data'), -      html::div(null, rcmail_compose_attachment_field(array('size' => $attrib['attachmentfieldsize']))) . +      html::div(null, rcmail_compose_attachment_field()) .        html::div('hint', rcube_label(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize)))) .        (get_boolean($attrib['buttons']) ? html::div('buttons',          $button->show(rcube_label('close'), array('class' => 'button', 'onclick' => "$('#$attrib[id]').hide()")) . ' ' . @@ -1436,7 +1453,7 @@ function rcmail_compose_attachment_form($attrib)  } -function rcmail_compose_attachment_field($attrib) +function rcmail_compose_attachment_field($attrib = array())  {    $attrib['type'] = 'file';    $attrib['name'] = '_attachments[]'; diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc index e14d25ee3..a7d9ca240 100644 --- a/program/steps/mail/func.inc +++ b/program/steps/mail/func.inc @@ -177,7 +177,9 @@ function rcmail_message_list_smart_column_name()    $sent_mbox   = $RCMAIL->config->get('sent_mbox');    $drafts_mbox = $RCMAIL->config->get('drafts_mbox'); -  if (strpos($mbox.$delim, $sent_mbox.$delim) === 0 || strpos($mbox.$delim, $drafts_mbox.$delim) === 0) { +  if ((strpos($mbox.$delim, $sent_mbox.$delim) === 0 || strpos($mbox.$delim, $drafts_mbox.$delim) === 0) +    && strtoupper($mbox) != 'INBOX' +  ) {      return 'to';    } @@ -1728,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'];              }          } @@ -1737,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'];                  }              } @@ -1763,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;              } @@ -1924,7 +1926,6 @@ function rcmail_message_import_form($attrib = array())    $fileinput = new html_inputfield(array(        'type' => 'file',        'name' => '_file[]', -      'size' => $attrib['attachmentfieldsize'],        'multiple' => 'multiple',        'accept' => ".eml, .mbox, message/rfc822, text/*",    )); diff --git a/program/steps/mail/sendmail.inc b/program/steps/mail/sendmail.inc index 1a92844c0..779fb87cd 100644 --- a/program/steps/mail/sendmail.inc +++ b/program/steps/mail/sendmail.inc @@ -823,15 +823,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_folder.inc b/program/steps/settings/edit_folder.inc index fdb38e602..f19e2177b 100644 --- a/program/steps/settings/edit_folder.inc +++ b/program/steps/settings/edit_folder.inc @@ -264,9 +264,12 @@ function rcmail_folder_form($attrib)              $content = rcmail_get_form_part($tab, $attrib);          } -        if ($content) { +        if ($content && sizeof($form) > 1) {              $out .= html::tag('fieldset', null, html::tag('legend', null, Q($tab['name'])) . $content) ."\n";          } +        else { +            $out .= $content ."\n"; +        }      }      $out .= "\n$form_end"; 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 f6ea79ec6..ecd35e94b 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),                  ); @@ -866,7 +868,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']) @@ -962,7 +965,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'])) { @@ -987,11 +991,12 @@ function rcmail_user_prefs($current = null)                      'maxlength'     => 30,                      'folder_filter' => 'mail',                      'folder_rights' => 'w', -                    // #1486114, #1488279 -                    'onchange'      => "if ($(this).val() == 'INBOX') $(this).val('')",                  ));              } +            // #1486114, #1488279, #1489219 +            $onchange = "if ($(this).val() == 'INBOX') $(this).val('')"; +              if (!isset($no_override['drafts_mbox'])) {                  if (!$current) {                      continue 2; @@ -999,7 +1004,7 @@ function rcmail_user_prefs($current = null)                  $blocks['main']['options']['drafts_mbox'] = array(                      'title'   => Q(rcube_label('drafts')), -                    'content' => $select->show($config['drafts_mbox'], array('name' => "_drafts_mbox")), +                    'content' => $select->show($config['drafts_mbox'], array('name' => "_drafts_mbox", 'onchange' => $onchange)),                  );              } @@ -1010,7 +1015,7 @@ function rcmail_user_prefs($current = null)                  $blocks['main']['options']['sent_mbox'] = array(                      'title'   => Q(rcube_label('sent')), -                    'content' => $select->show($config['sent_mbox'], array('name' => "_sent_mbox")), +                    'content' => $select->show($config['sent_mbox'], array('name' => "_sent_mbox", 'onchange' => '')),                  );              } @@ -1021,7 +1026,7 @@ function rcmail_user_prefs($current = null)                  $blocks['main']['options']['junk_mbox'] = array(                      'title'   => Q(rcube_label('junk')), -                    'content' => $select->show($config['junk_mbox'], array('name' => "_junk_mbox")), +                    'content' => $select->show($config['junk_mbox'], array('name' => "_junk_mbox", 'onchange' => $onchange)),                  );              } @@ -1032,7 +1037,7 @@ function rcmail_user_prefs($current = null)                  $blocks['main']['options']['trash_mbox'] = array(                      'title'   => Q(rcube_label('trash')), -                    'content' => $select->show($config['trash_mbox'], array('name' => "_trash_mbox")), +                    'content' => $select->show($config['trash_mbox'], array('name' => "_trash_mbox", 'onchange' => $onchange)),                  );              }          break; @@ -1042,6 +1047,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 09c7a60ed..4b238c163 100644 --- a/skins/larry/styles.css +++ b/skins/larry/styles.css @@ -930,7 +930,7 @@ a.iconlink.upload {  }  /* fix scrolling within iframes in webkit browsers on touch devices */ -@media screen and (-webkit-min-device-pixel-ratio:0) { +@media screen and (-webkit-min-device-pixel-ratio:0) and (max-device-width:1024px) {  	.iframebox {  		overflow: auto;  		-webkit-overflow-scrolling: touch; @@ -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/compose.html b/skins/larry/templates/compose.html index 09eafe73b..806939a42 100644 --- a/skins/larry/templates/compose.html +++ b/skins/larry/templates/compose.html @@ -85,25 +85,25 @@  	</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' />" />x</a> +			<a href="#cc" onclick="return UI.hide_header_row('cc');" class="iconbutton cancel" title="<roundcube:label name='delete' />">x</a>  		</td>  		<td class="editfield"><roundcube:object name="composeHeaders" part="cc" form="form" id="_cc" cols="70" rows="1" tabindex="3" /></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' />" />x</a> +			<a href="#bcc" onclick="return UI.hide_header_row('bcc');" class="iconbutton cancel" title="<roundcube:label name='delete' />">x</a>  		</td> -		<td colspan="2" class="editfield"><roundcube:object name="composeHeaders" part="bcc" form="form" id="_bcc" cols="70" rows="1" tabindex="4" /></td> +		<td class="editfield"><roundcube:object name="composeHeaders" part="bcc" form="form" id="_bcc" cols="70" rows="1" tabindex="4" /></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' />" />x</a> +			<a href="#replyto" onclick="return UI.hide_header_row('replyto');" class="iconbutton cancel" title="<roundcube:label name='delete' />">x</a>  		</td>  		<td class="editfield"><roundcube:object name="composeHeaders" part="replyto" form="form" id="_replyto" size="70" tabindex="5" /></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' />" />x</a> +			<a href="#followupto" onclick="return UI.hide_header_row('followupto');" class="iconbutton cancel" title="<roundcube:label name='delete' />">x</a>  		</td>  		<td class="editfield"><roundcube:object name="composeHeaders" part="followupto" form="form" id="_followupto" size="70" tabindex="7" /></td>  	</tr><tr> @@ -185,7 +185,7 @@  </div><!-- end mainscreen -->  <div id="upload-dialog" class="propform popupdialog" title="<roundcube:label name='addattachment' />"> -	<roundcube:object name="composeAttachmentForm" id="uploadform" attachmentFieldSize="40" buttons="no" /> +	<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()" /> 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/templates/mail.html b/skins/larry/templates/mail.html index 5f465d767..f2c52c820 100644 --- a/skins/larry/templates/mail.html +++ b/skins/larry/templates/mail.html @@ -228,7 +228,7 @@  </div>  <div id="upload-dialog" class="propform popupdialog" title="<roundcube:label name='importmessages' />"> -	<roundcube:object name="messageimportform" id="uploadform" attachmentFieldSize="40" buttons="no" /> +	<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()" /> 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; +    }  } | 
