diff options
134 files changed, 3029 insertions, 2091 deletions
| @@ -1,11 +1,28 @@  CHANGELOG Roundcube Webmail  =========================== -- Fix focus on the list when list row is clicked (#1488600) -- Added separate From and To columns apart from smart From/To column (#1486891) -- Fix fallback to Larry skin when configured skin isn't available (#1488591) -- Fix (workaround) delete operations with some versions of memcache (#1488592) -- Fix (disable) request validation for spell and spell_html actions +- Fix PLAIN authentication for some IMAP servers (#1488674) +- Fix encoding vCard file when contains PHOTO;ENCODING=b (#1488683) +- Fix focus issue in IE when selecting message row (#1488620) +- Remove (too big) min-width on mail screen +- Add full headers view in message preview window (#1488538) +- Fix message display page issues - unified with message preview (#1488590, #1488642) +- Fix displaying all headers when they contain malformed characters (#1488666) +- Fix decoding of HTML messages with UTF-16 charset specified (#1488654) +- Fix quota capability detection so it can be overwritten by a plugin (#1488655) +- Added template object 'frame' +- Fix identity selection on reply (#1488101) +- Add option to enable HTML editor on forwarding (#1488517) +- Add option to not include original message on reply, rename option top_posting to reply_mode (#1485149) +- Fix Larry's messages list filter in IE (#1488632) +- Fix more IE issues by disabling Compat. mode with X-UA-Compatible meta tag (#1488626) +- Fix setting locales under Solaris - use additional .UTF-8 suffix (#1488628) +- Fix email address validation for addresses with IP address in domain part +- Fix Larry skin issues in IE7 compat. mode (#1488618) +- Fix so subscribed non-existing/non-accessible shared folder can be unsubscribed +- Added session_path config option and unified cookies settings in javascript +- Added "Undeleted" option to messages list filter +- Rewritten test scripts for PHPUnit  - Add new DB abstraction layer based on PHP PDO, supporting SQLite3 (#1488332)  - Removed PEAR::MDB2 package  - Removed users.alias column, added option ('user_aliases') @@ -31,6 +48,24 @@ CHANGELOG Roundcube Webmail      Move global functions from main.inc and rcube_shared.inc into classes      Better classes separation +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 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) +- Fix Remove from group option is active for contact search result (#1488608) +- Disable autocapitalization in login form on iPad/iPhone (#1488609) +- Fix focus on the list when list row is clicked (#1488600) +- Added separate From and To columns apart from smart From/To column (#1486891) +- Fix fallback to Larry skin when configured skin isn't available (#1488591) +- Fix (workaround) delete operations with some versions of memcache (#1488592) +- Fix (disable) request validation for spell and spell_html actions +  RELEASE 0.8.0  -------------  - Don't show product version on login screen (can be enabled by config) diff --git a/config/main.inc.php.dist b/config/main.inc.php.dist index 58f9ca865..a6661c323 100644 --- a/config/main.inc.php.dist +++ b/config/main.inc.php.dist @@ -21,7 +21,7 @@ $rcmail_config = array();  // LOGGING/DEBUGGING  // ---------------------------------- -// system error reporting: 1 = log; 2 = report (not implemented yet), 4 = show, 8 = trace +// system error reporting, sum of: 1 = log; 4 = show, 8 = trace  $rcmail_config['debug_level'] = 1;  // log driver:  'syslog' or 'file'. @@ -78,7 +78,7 @@ $rcmail_config['default_host'] = '';  // TCP port used for IMAP connections  $rcmail_config['default_port'] = 143; -// IMAP AUTH type (DIGEST-MD5, CRAM-MD5, LOGIN, PLAIN or empty to use +// IMAP AUTH type (DIGEST-MD5, CRAM-MD5, LOGIN, PLAIN or null to use  // best server supported one)  $rcmail_config['imap_auth_type'] = null; @@ -224,11 +224,12 @@ $rcmail_config['use_https'] = false;  // 0 - disabled, 1 - username and host only, 2 - username, host, password  $rcmail_config['login_autocomplete'] = 0; -// If users authentication is not case sensitive this must be enabled. -// You can also use it to force conversion of logins to lower case. +// Forces conversion of logins to lower case. +// 0 - disabled, 1 - only domain part, 2 - domain and local part. +// If users authentication is not case-sensitive this must be enabled.  // After enabling it all user records need to be updated, e.g. with query:  // UPDATE users SET username = LOWER(username); -$rcmail_config['login_lc'] = false; +$rcmail_config['login_lc'] = 0;  // Includes should be interpreted as PHP files  $rcmail_config['skin_include_php'] = false; @@ -240,12 +241,15 @@ $rcmail_config['display_version'] = false;  // must be greater than 'keep_alive'/60  $rcmail_config['session_lifetime'] = 10; -// session domain: .example.org +// Session domain: .example.org  $rcmail_config['session_domain'] = ''; -// session name. Default: 'roundcube_sessid' +// Session name. Default: 'roundcube_sessid'  $rcmail_config['session_name'] = null; +// Session path. Defaults to PHP session.cookie_path setting. +$rcmail_config['session_path'] = null; +  // Backend to use for session storage. Can either be 'db' (default) or 'memcache'  // If set to memcache, a list of servers need to be specified in 'memcache_hosts'  // Make sure the Memcache extension (http://pecl.php.net/package/memcache) version >= 2.0.0 is installed @@ -721,7 +725,7 @@ $rcmail_config['prefer_html'] = true;  $rcmail_config['show_images'] = 0;  // compose html formatted messages by default -// 0 - never, 1 - always, 2 - on reply to HTML message only  +// 0 - never, 1 - always, 2 - on reply to HTML message, 3 - on forward or reply to HTML message  $rcmail_config['htmleditor'] = 0;  // show pretty dates as standard @@ -779,8 +783,11 @@ $rcmail_config['display_next'] = true;  // 2 - Expand only threads with unread messages   $rcmail_config['autoexpand_threads'] = 0; -// When replying place cursor above original message (top posting) -$rcmail_config['top_posting'] = false; +// When replying: +// -1 - don't cite the original message +// 0  - place cursor below the original message +// 1  - place cursor above original message (top posting) +$rcmail_config['reply_mode'] = 0;  // When replying strip original signature from message  $rcmail_config['strip_existing_sig'] = true; diff --git a/installer/rcube_install.php b/installer/rcube_install.php index bfb111f1d..5af871346 100644 --- a/installer/rcube_install.php +++ b/installer/rcube_install.php @@ -35,13 +35,14 @@ class rcube_install    var $obsolete_config = array('db_backend', 'double_auth');    var $replaced_config = array( -    'skin_path' => 'skin', -    'locale_string' => 'language', -    'multiple_identities' => 'identities_level', +    'skin_path'            => 'skin', +    'locale_string'        => 'language', +    'multiple_identities'  => 'identities_level',      'addrbook_show_images' => 'show_images', -    'imap_root' => 'imap_ns_personal', -    'pagesize' => 'mail_pagesize', +    'imap_root'            => 'imap_ns_personal', +    'pagesize'             => 'mail_pagesize',      'default_imap_folders' => 'default_folders', +    'top_posting'          => 'reply_mode',    );    // these config options are required for a working system diff --git a/plugins/acl/acl.php b/plugins/acl/acl.php index 144250495..1952dad49 100644 --- a/plugins/acl/acl.php +++ b/plugins/acl/acl.php @@ -233,8 +233,7 @@ class acl extends rcube_plugin          // Advanced rights          $attrib['id'] = 'advancedrights'; -        foreach ($supported as $val) { -            $id = "acl$val"; +        foreach ($supported as $idx => $val) {              $ul .= html::tag('li', null,                  $input->show('', array(                      'name' => "acl[$val]", 'value' => $val, 'id' => $id)) diff --git a/plugins/acl/skins/larry/acl.css b/plugins/acl/skins/larry/acl.css index 5e2448efd..e392a269e 100644 --- a/plugins/acl/skins/larry/acl.css +++ b/plugins/acl/skins/larry/acl.css @@ -123,3 +123,7 @@  {    margin-left: 0.5em;  } + +ul.toolbarmenu li span.delete { +  background-position: 0 -1509px; +} diff --git a/plugins/acl/skins/larry/templates/table.html b/plugins/acl/skins/larry/templates/table.html index 7f99f6ffe..3cf8292a4 100644 --- a/plugins/acl/skins/larry/templates/table.html +++ b/plugins/acl/skins/larry/templates/table.html @@ -3,14 +3,14 @@      <roundcube:object name="acltable" id="acltable" class="records-table" />  </div>  <div id="acllist-footer" class="boxfooter"> -    <roundcube:button command="acl-create" id="aclcreatelink" type="link" title="acl.newuser" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="aclmenulink" id="aclmenulink" type="link" title="acl.actions" class="listbutton groupactions"onclick="UI.show_popup('aclmenu');return false" innerClass="inner" content="⚙" /> +    <roundcube:button command="acl-create" id="aclcreatelink" type="link" title="acl.newuser" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="aclmenulink" id="aclmenulink" type="link" title="acl.actions" class="listbutton groupactions"onclick="UI.show_popup('aclmenu', undefined, {above:1});return false" innerClass="inner" content="⚙" />  </div>  </div>  <div id="aclmenu" class="popupmenu"> -    <ul class="toolbarmenu selectable"> -        <li><roundcube:button command="acl-edit" label="edit" classAct="active" /></li> -        <li><roundcube:button command="acl-delete" label="delete" classAct="active" /></li> +    <ul class="toolbarmenu selectable iconized"> +        <li><roundcube:button command="acl-edit" label="edit" class="icon" classAct="icon active" innerclass="icon edit" /></li> +        <li><roundcube:button command="acl-delete" label="delete" class="icon" classAct="icon active" innerclass="icon delete" /></li>          <roundcube:if condition="!in_array('acl_advanced_mode', (array)config:dont_override)" />              <li><roundcube:button name="acl-switch" id="acl-switch" label="acl.advanced" onclick="rcmail.command('acl-mode-switch')" class="active" /></li>          <roundcube:endif /> diff --git a/plugins/hide_blockquote/hide_blockquote.php b/plugins/hide_blockquote/hide_blockquote.php index ca0273a5d..7af163dcd 100644 --- a/plugins/hide_blockquote/hide_blockquote.php +++ b/plugins/hide_blockquote/hide_blockquote.php @@ -27,11 +27,7 @@ class hide_blockquote extends rcube_plugin              && ($limit = $rcmail->config->get('hide_blockquote_limit'))          ) {              // include styles -            $skin = $rcmail->config->get('skin'); -            if (!file_exists($this->home."/skins/$skin/style.css")) { -                $skin = 'default'; -            } -            $this->include_stylesheet("skins/$skin/style.css"); +            $this->include_stylesheet($this->local_skin_path() . "/style.css");              // Script and localization              $this->include_script('hide_blockquote.js'); diff --git a/plugins/hide_blockquote/skins/default/style.css b/plugins/hide_blockquote/skins/larry/style.css index 198172f92..198172f92 100644 --- a/plugins/hide_blockquote/skins/default/style.css +++ b/plugins/hide_blockquote/skins/larry/style.css diff --git a/plugins/managesieve/tests/Makefile b/plugins/managesieve/tests/Makefile deleted file mode 100644 index 072be2f2c..000000000 --- a/plugins/managesieve/tests/Makefile +++ /dev/null @@ -1,7 +0,0 @@ - -clean: -	rm -f *.log *.php *.diff *.exp *.out - - -test: -	pear run-tests *.phpt diff --git a/plugins/managesieve/tests/Parser.php b/plugins/managesieve/tests/Parser.php new file mode 100644 index 000000000..00915cc20 --- /dev/null +++ b/plugins/managesieve/tests/Parser.php @@ -0,0 +1,54 @@ +<?php + +class Parser extends PHPUnit_Framework_TestCase +{ + +    function setUp() +    { +        include_once dirname(__FILE__) . '/../lib/rcube_sieve_script.php'; +    } + +    /** +     * Sieve script parsing +     * +     * @dataProvider data_parser +     */ +    function test_parser($input, $output, $message) +    { +        $script = new rcube_sieve_script($input); +        $result = $script->as_text(); + +        $this->assertEquals(trim($result), trim($output), $message); +    } + +    /** +     * Data provider for test_parser() +     */ +    function data_parser() +    { +        $dir_path = realpath(dirname(__FILE__) . '/src'); +        $dir      = opendir($dir_path); +        $result   = array(); + +        while ($file = readdir($dir)) { +            if (preg_match('/^[a-z0-9_]+$/', $file)) { +                $input = file_get_contents($dir_path . '/' . $file); + +                if (file_exists($dir_path . '/' . $file . '.out')) { +                    $output = file_get_contents($dir_path . '/' . $file . '.out'); +                } +                else { +                    $output = $input; +                } + +                $result[] = array( +                    'input'   => $input, +                    'output'  => $output, +                    'message' => "Error in parsing '$file' file", +                ); +            } +        } + +        return $result; +    } +} diff --git a/plugins/managesieve/tests/Tokenizer.php b/plugins/managesieve/tests/Tokenizer.php new file mode 100644 index 000000000..8c0bced23 --- /dev/null +++ b/plugins/managesieve/tests/Tokenizer.php @@ -0,0 +1,33 @@ +<?php + +class Tokenizer extends PHPUnit_Framework_TestCase +{ + +    function setUp() +    { +        include_once dirname(__FILE__) . '/../lib/rcube_sieve_script.php'; +    } + +    function data_tokenizer() +    { +        return array( +            array(1, "text: #test\nThis is test ; message;\nMulti line\n.\n;\n", '"This is test ; message;\nMulti line"'), +            array(0, '["test1","test2"]', '[["test1","test2"]]'), +            array(1, '["test"]', '["test"]'), +            array(1, '"te\\"st"', '"te\\"st"'), +            array(0, 'test #comment', '["test"]'), +            array(0, "text:\ntest\n.\ntext:\ntest\n.\n", '["test","test"]'), +            array(1, '"\\a\\\\\\"a"', '"a\\\\\\"a"'), +        ); +    } + +    /** +     * @dataProvider data_tokenizer +     */ +    function test_tokenizer($num, $input, $output) +    { +        $res = json_encode(rcube_sieve_script::tokenize($input, $num)); + +        $this->assertEquals(trim($res), trim($output)); +    } +} diff --git a/plugins/managesieve/tests/parser.phpt b/plugins/managesieve/tests/parser.phpt deleted file mode 100644 index aec042187..000000000 --- a/plugins/managesieve/tests/parser.phpt +++ /dev/null @@ -1,120 +0,0 @@ ---TEST-- -Main test of script parser ---SKIPIF-- ---FILE-- -<?php -include '../lib/rcube_sieve_script.php'; - -$txt = ' -require ["fileinto","reject","envelope"]; -# rule:[spam] -if anyof (header :contains "X-DSPAM-Result" "Spam") -{ -	fileinto "Spam"; -	stop; -} -# rule:[test1] -if anyof (header :comparator "i;ascii-casemap" :contains ["From","To"] "test@domain.tld") -{ -	discard; -	stop; -} -# rule:[test2] -if anyof (not header :comparator "i;octet" :contains ["Subject"] "[test]", header :contains "Subject" "[test2]") -{ -	fileinto "test"; -	stop; -} -# rule:[comments] -if anyof (true) /* comment - * "comment" #comment */ { -    /* comment */ stop; -# comment -} -# rule:[reject] -if size :over 5000K { -	reject "Message over 5MB size limit. Please contact me before sending this."; -} -# rule:[false] -if false # size :over 5000K -{ -	stop; /* rule disabled */ -} -# rule:[true] -if true -{ -	stop; -} -fileinto "Test"; -# rule:[address test] -if address :all :is "From" "nagios@domain.tld" -{ -	fileinto "domain.tld"; -	stop; -} -# rule:[envelope test] -if envelope :domain :is "From" "domain.tld" -{ -	fileinto "domain.tld"; -	stop; -} -'; - -$s = new rcube_sieve_script($txt); -echo $s->as_text(); - -// ------------------------------------------------------------------------------- -?> ---EXPECT-- -require ["fileinto","reject","envelope"]; -# rule:[spam] -if header :contains "X-DSPAM-Result" "Spam" -{ -	fileinto "Spam"; -	stop; -} -# rule:[test1] -if header :contains ["From","To"] "test@domain.tld" -{ -	discard; -	stop; -} -# rule:[test2] -if anyof (not header :comparator "i;octet" :contains "Subject" "[test]", header :contains "Subject" "[test2]") -{ -	fileinto "test"; -	stop; -} -# rule:[comments] -if true -{ -	stop; -} -# rule:[reject] -if size :over 5000K -{ -	reject "Message over 5MB size limit. Please contact me before sending this."; -} -# rule:[false] -if false # size :over 5000K -{ -	stop; -} -# rule:[true] -if true -{ -	stop; -} -fileinto "Test"; -# rule:[address test] -if address :all :is "From" "nagios@domain.tld" -{ -	fileinto "domain.tld"; -	stop; -} -# rule:[envelope test] -if envelope :domain :is "From" "domain.tld" -{ -	fileinto "domain.tld"; -	stop; -} diff --git a/plugins/managesieve/tests/parser_body.phpt b/plugins/managesieve/tests/parser_body.phpt deleted file mode 100644 index 08ad54959..000000000 --- a/plugins/managesieve/tests/parser_body.phpt +++ /dev/null @@ -1,49 +0,0 @@ ---TEST-- -Test of Sieve body extension (RFC5173) ---SKIPIF-- ---FILE-- -<?php -include '../lib/rcube_sieve_script.php'; - -$txt = ' -require ["body","fileinto"]; -if body :raw :contains "MAKE MONEY FAST" -{ -	stop; -} -if body :content "text" :contains ["missile","coordinates"] -{ -	fileinto "secrets"; -} -if body :content "audio/mp3" :contains "" -{ -	fileinto "jukebox"; -} -if body :text :contains "project schedule" -{ -	fileinto "project/schedule"; -} -'; - -$s = new rcube_sieve_script($txt); -echo $s->as_text(); - -?> ---EXPECT-- -require ["body","fileinto"]; -if body :raw :contains "MAKE MONEY FAST" -{ -	stop; -} -if body :content "text" :contains ["missile","coordinates"] -{ -	fileinto "secrets"; -} -if body :content "audio/mp3" :contains "" -{ -	fileinto "jukebox"; -} -if body :text :contains "project schedule" -{ -	fileinto "project/schedule"; -} diff --git a/plugins/managesieve/tests/parser_imapflags.phpt b/plugins/managesieve/tests/parser_imapflags.phpt deleted file mode 100644 index a4bc465a3..000000000 --- a/plugins/managesieve/tests/parser_imapflags.phpt +++ /dev/null @@ -1,28 +0,0 @@ ---TEST-- -Test of Sieve vacation extension (RFC5232) ---SKIPIF-- ---FILE-- -<?php -include '../lib/rcube_sieve_script.php'; - -$txt = ' -require ["imapflags"]; -# rule:[imapflags] -if header :matches "Subject" "^Test$" { -    setflag "\\\\Seen"; -    addflag ["\\\\Answered","\\\\Deleted"]; -} -'; - -$s = new rcube_sieve_script($txt, array('imapflags')); -echo $s->as_text(); - -?> ---EXPECT-- -require ["imapflags"]; -# rule:[imapflags] -if header :matches "Subject" "^Test$" -{ -	setflag "\\Seen"; -	addflag ["\\Answered","\\Deleted"]; -} diff --git a/plugins/managesieve/tests/parser_include.phpt b/plugins/managesieve/tests/parser_include.phpt deleted file mode 100644 index addc0d449..000000000 --- a/plugins/managesieve/tests/parser_include.phpt +++ /dev/null @@ -1,30 +0,0 @@ ---TEST-- -Test of Sieve include extension ---SKIPIF-- ---FILE-- -<?php -include '../lib/rcube_sieve_script.php'; - -$txt = ' -require ["include"]; - -include "script.sieve"; -# rule:[two] -if true -{ -    include :optional "second.sieve"; -} -'; - -$s = new rcube_sieve_script($txt, array(), array('variables')); -echo $s->as_text(); - -?> ---EXPECT-- -require ["include"]; -include "script.sieve"; -# rule:[two] -if true -{ -	include :optional "second.sieve"; -} diff --git a/plugins/managesieve/tests/parser_kep14.phpt b/plugins/managesieve/tests/parser_kep14.phpt deleted file mode 100644 index dcdbd48a0..000000000 --- a/plugins/managesieve/tests/parser_kep14.phpt +++ /dev/null @@ -1,19 +0,0 @@ ---TEST-- -Test of Kolab's KEP:14 implementation ---SKIPIF-- ---FILE-- -<?php -include '../lib/rcube_sieve_script.php'; - -$txt = ' -# EDITOR Roundcube -# EDITOR_VERSION 123 -'; - -$s = new rcube_sieve_script($txt, array('body')); -echo $s->as_text(); - -?> ---EXPECT-- -# EDITOR Roundcube -# EDITOR_VERSION 123 diff --git a/plugins/managesieve/tests/parser_prefix.phpt b/plugins/managesieve/tests/parser_prefix.phpt deleted file mode 100644 index c87e9658f..000000000 --- a/plugins/managesieve/tests/parser_prefix.phpt +++ /dev/null @@ -1,25 +0,0 @@ ---TEST-- -Test of prefix comments handling ---SKIPIF-- ---FILE-- -<?php -include '../lib/rcube_sieve_script.php'; - -$txt = ' -# this is a comment -# and the second line - -require ["variables"]; -set "b" "c"; -'; - -$s = new rcube_sieve_script($txt, array(), array('variables')); -echo $s->as_text(); - -?> ---EXPECT-- -# this is a comment -# and the second line - -require ["variables"]; -set "b" "c"; diff --git a/plugins/managesieve/tests/parser_relational.phpt b/plugins/managesieve/tests/parser_relational.phpt deleted file mode 100644 index 6b6f29f4c..000000000 --- a/plugins/managesieve/tests/parser_relational.phpt +++ /dev/null @@ -1,25 +0,0 @@ ---TEST-- -Test of Sieve relational extension (RFC5231) ---SKIPIF-- ---FILE-- -<?php -include '../lib/rcube_sieve_script.php'; - -$txt = ' -require ["relational","comparator-i;ascii-numeric"]; -# rule:[redirect] -if header :value "ge" :comparator "i;ascii-numeric" -    ["X-Spam-score"] ["14"] {redirect "test@test.tld";} -'; - -$s = new rcube_sieve_script($txt); -echo $s->as_text(); - -?> ---EXPECT-- -require ["relational","comparator-i;ascii-numeric"]; -# rule:[redirect] -if header :value "ge" :comparator "i;ascii-numeric" "X-Spam-score" "14" -{ -	redirect "test@test.tld"; -} diff --git a/plugins/managesieve/tests/parser_vacation.phpt b/plugins/managesieve/tests/parser_vacation.phpt deleted file mode 100644 index a603ff6c1..000000000 --- a/plugins/managesieve/tests/parser_vacation.phpt +++ /dev/null @@ -1,39 +0,0 @@ ---TEST-- -Test of Sieve vacation extension (RFC5230) ---SKIPIF-- ---FILE-- -<?php -include '../lib/rcube_sieve_script.php'; - -$txt = ' -require ["vacation"]; -# rule:[test-vacation] -if anyof (header :contains "Subject" "vacation") -{ -	vacation :days 1 text: -# test -test test /* test */ -test -. -; -	stop; -} -'; - -$s = new rcube_sieve_script($txt); -echo $s->as_text(); - -?> ---EXPECT-- -require ["vacation"]; -# rule:[test-vacation] -if header :contains "Subject" "vacation" -{ -	vacation :days 1 text: -# test -test test /* test */ -test -. -; -	stop; -} diff --git a/plugins/managesieve/tests/parser_variables.phpt b/plugins/managesieve/tests/parser_variables.phpt deleted file mode 100644 index cf1f8fcad..000000000 --- a/plugins/managesieve/tests/parser_variables.phpt +++ /dev/null @@ -1,39 +0,0 @@ ---TEST-- -Test of Sieve variables extension ---SKIPIF-- ---FILE-- -<?php -include '../lib/rcube_sieve_script.php'; - -$txt = ' -require ["variables"]; -set "honorific" "Mr"; -set "vacation" text: -Dear ${HONORIFIC} ${last_name}, -I am out, please leave a message after the meep. -. -; -set :length "b" "${a}"; -set :lower "b" "${a}"; -set :upperfirst "b" "${a}"; -set :upperfirst :lower "b" "${a}"; -set :quotewildcard "b" "Rock*"; -'; - -$s = new rcube_sieve_script($txt, array(), array('variables')); -echo $s->as_text(); - -?> ---EXPECT-- -require ["variables"]; -set "honorific" "Mr"; -set "vacation" text: -Dear ${HONORIFIC} ${last_name}, -I am out, please leave a message after the meep. -. -; -set :length "b" "${a}"; -set :lower "b" "${a}"; -set :upperfirst "b" "${a}"; -set :upperfirst :lower "b" "${a}"; -set :quotewildcard "b" "Rock*"; diff --git a/plugins/managesieve/tests/parset_subaddress.phpt b/plugins/managesieve/tests/parset_subaddress.phpt deleted file mode 100644 index 6d4d03c6e..000000000 --- a/plugins/managesieve/tests/parset_subaddress.phpt +++ /dev/null @@ -1,38 +0,0 @@ ---TEST-- -Test of Sieve subaddress extension (RFC5233) ---SKIPIF-- ---FILE-- -<?php -include '../lib/rcube_sieve_script.php'; - -$txt = ' -require ["envelope","subaddress","fileinto"]; -if envelope :user "To" "postmaster" -{ -	fileinto "postmaster"; -	stop; -} -if envelope :detail :is "To" "mta-filters" -{ -	fileinto "mta-filters"; -	stop; -} -'; - -$s = new rcube_sieve_script($txt); -echo $s->as_text(); - -// ------------------------------------------------------------------------------- -?> ---EXPECT-- -require ["envelope","subaddress","fileinto"]; -if envelope :user "To" "postmaster" -{ -	fileinto "postmaster"; -	stop; -} -if envelope :detail :is "To" "mta-filters" -{ -	fileinto "mta-filters"; -	stop; -} diff --git a/plugins/managesieve/tests/src/parser b/plugins/managesieve/tests/src/parser new file mode 100644 index 000000000..9c4717be4 --- /dev/null +++ b/plugins/managesieve/tests/src/parser @@ -0,0 +1,52 @@ +require ["fileinto","reject","envelope"]; +# rule:[spam] +if anyof (header :contains "X-DSPAM-Result" "Spam") +{ +	fileinto "Spam"; +	stop; +} +# rule:[test1] +if anyof (header :comparator "i;ascii-casemap" :contains ["From","To"] "test@domain.tld") +{ +	discard; +	stop; +} +# rule:[test2] +if anyof (not header :comparator "i;octet" :contains ["Subject"] "[test]", header :contains "Subject" "[test2]") +{ +	fileinto "test"; +	stop; +} +# rule:[comments] +if anyof (true) /* comment + * "comment" #comment */ { +    /* comment */ stop; +# comment +} +# rule:[reject] +if size :over 5000K { +	reject "Message over 5MB size limit. Please contact me before sending this."; +} +# rule:[false] +if false # size :over 5000K +{ +	stop; /* rule disabled */ +} +# rule:[true] +if true +{ +	stop; +} +fileinto "Test"; +# rule:[address test] +if address :all :is "From" "nagios@domain.tld" +{ +	fileinto "domain.tld"; +	stop; +} +# rule:[envelope test] +if envelope :domain :is "From" "domain.tld" +{ +	fileinto "domain.tld"; +	stop; +} diff --git a/plugins/managesieve/tests/src/parser.out b/plugins/managesieve/tests/src/parser.out new file mode 100644 index 000000000..385c8890d --- /dev/null +++ b/plugins/managesieve/tests/src/parser.out @@ -0,0 +1,52 @@ +require ["fileinto","reject","envelope"]; +# rule:[spam] +if header :contains "X-DSPAM-Result" "Spam" +{ +	fileinto "Spam"; +	stop; +} +# rule:[test1] +if header :contains ["From","To"] "test@domain.tld" +{ +	discard; +	stop; +} +# rule:[test2] +if anyof (not header :comparator "i;octet" :contains "Subject" "[test]", header :contains "Subject" "[test2]") +{ +	fileinto "test"; +	stop; +} +# rule:[comments] +if true +{ +	stop; +} +# rule:[reject] +if size :over 5000K +{ +	reject "Message over 5MB size limit. Please contact me before sending this."; +} +# rule:[false] +if false # size :over 5000K +{ +	stop; +} +# rule:[true] +if true +{ +	stop; +} +fileinto "Test"; +# rule:[address test] +if address :all :is "From" "nagios@domain.tld" +{ +	fileinto "domain.tld"; +	stop; +} +# rule:[envelope test] +if envelope :domain :is "From" "domain.tld" +{ +	fileinto "domain.tld"; +	stop; +} diff --git a/plugins/managesieve/tests/src/parser_body b/plugins/managesieve/tests/src/parser_body new file mode 100644 index 000000000..bd142ed8c --- /dev/null +++ b/plugins/managesieve/tests/src/parser_body @@ -0,0 +1,17 @@ +require ["body","fileinto"]; +if body :raw :contains "MAKE MONEY FAST" +{ +	stop; +} +if body :content "text" :contains ["missile","coordinates"] +{ +	fileinto "secrets"; +} +if body :content "audio/mp3" :contains "" +{ +	fileinto "jukebox"; +} +if body :text :contains "project schedule" +{ +	fileinto "project/schedule"; +} diff --git a/plugins/managesieve/tests/src/parser_imapflags b/plugins/managesieve/tests/src/parser_imapflags new file mode 100644 index 000000000..e67bf7cfc --- /dev/null +++ b/plugins/managesieve/tests/src/parser_imapflags @@ -0,0 +1,7 @@ +require ["imap4flags"]; +# rule:[imapflags] +if header :matches "Subject" "^Test$" +{ +	setflag "\\Seen"; +	addflag ["\\Answered","\\Deleted"]; +} diff --git a/plugins/managesieve/tests/src/parser_include b/plugins/managesieve/tests/src/parser_include new file mode 100644 index 000000000..b5585a4ba --- /dev/null +++ b/plugins/managesieve/tests/src/parser_include @@ -0,0 +1,7 @@ +require ["include"]; +include "script.sieve"; +# rule:[two] +if true +{ +	include :optional "second.sieve"; +} diff --git a/plugins/managesieve/tests/src/parser_kep14 b/plugins/managesieve/tests/src/parser_kep14 new file mode 100644 index 000000000..1ded8d8d4 --- /dev/null +++ b/plugins/managesieve/tests/src/parser_kep14 @@ -0,0 +1,2 @@ +# EDITOR Roundcube +# EDITOR_VERSION 123 diff --git a/plugins/managesieve/tests/src/parser_kep14.out b/plugins/managesieve/tests/src/parser_kep14.out new file mode 100644 index 000000000..cb7faa7f8 --- /dev/null +++ b/plugins/managesieve/tests/src/parser_kep14.out @@ -0,0 +1,3 @@ +require ["variables"]; +set "EDITOR" "Roundcube"; +set "EDITOR_VERSION" "123"; diff --git a/plugins/managesieve/tests/src/parser_prefix b/plugins/managesieve/tests/src/parser_prefix new file mode 100644 index 000000000..9f6a33a1c --- /dev/null +++ b/plugins/managesieve/tests/src/parser_prefix @@ -0,0 +1,5 @@ +# this is a comment +# and the second line + +require ["variables"]; +set "b" "c"; diff --git a/plugins/managesieve/tests/src/parser_relational b/plugins/managesieve/tests/src/parser_relational new file mode 100644 index 000000000..0a92fde54 --- /dev/null +++ b/plugins/managesieve/tests/src/parser_relational @@ -0,0 +1,6 @@ +require ["relational","comparator-i;ascii-numeric"]; +# rule:[redirect] +if header :value "ge" :comparator "i;ascii-numeric" "X-Spam-score" "14" +{ +	redirect "test@test.tld"; +} diff --git a/plugins/managesieve/tests/src/parser_subaddress b/plugins/managesieve/tests/src/parser_subaddress new file mode 100644 index 000000000..f106b796e --- /dev/null +++ b/plugins/managesieve/tests/src/parser_subaddress @@ -0,0 +1,11 @@ +require ["envelope","subaddress","fileinto"]; +if envelope :user "To" "postmaster" +{ +	fileinto "postmaster"; +	stop; +} +if envelope :detail :is "To" "mta-filters" +{ +	fileinto "mta-filters"; +	stop; +} diff --git a/plugins/managesieve/tests/src/parser_vacation b/plugins/managesieve/tests/src/parser_vacation new file mode 100644 index 000000000..93026db45 --- /dev/null +++ b/plugins/managesieve/tests/src/parser_vacation @@ -0,0 +1,12 @@ +require ["vacation"]; +# rule:[test-vacation] +if header :contains "Subject" "vacation" +{ +	vacation :days 1 text: +# test +test test /* test */ +test +. +; +	stop; +} diff --git a/plugins/managesieve/tests/src/parser_variables b/plugins/managesieve/tests/src/parser_variables new file mode 100644 index 000000000..bd5941c02 --- /dev/null +++ b/plugins/managesieve/tests/src/parser_variables @@ -0,0 +1,12 @@ +require ["variables"]; +set "honorific" "Mr"; +set "vacation" text: +Dear ${HONORIFIC} ${last_name}, +I am out, please leave a message after the meep. +. +; +set :length "b" "${a}"; +set :lower "b" "${a}"; +set :upperfirst "b" "${a}"; +set :upperfirst :lower "b" "${a}"; +set :quotewildcard "b" "Rock*"; diff --git a/plugins/managesieve/tests/tokenize.phpt b/plugins/managesieve/tests/tokenize.phpt deleted file mode 100644 index f988653ee..000000000 --- a/plugins/managesieve/tests/tokenize.phpt +++ /dev/null @@ -1,66 +0,0 @@ ---TEST-- -Script parsing: tokenizer ---SKIPIF-- ---FILE-- -<?php -include '../lib/rcube_sieve_script.php'; - -$txt[1] = array(1, 'text: #test -This is test ; message; -Multi line -. -; -'); -$txt[2] = array(0, '["test1","test2"]'); -$txt[3] = array(1, '["test"]'); -$txt[4] = array(1, '"te\\"st"'); -$txt[5] = array(0, 'test #comment'); -$txt[6] = array(0, 'text: -test -. -text: -test -. -'); -$txt[7] = array(1, '"\\a\\\\\\"a"'); - -foreach ($txt as $idx => $t) { -    echo "[$idx]---------------\n";  -    var_dump(rcube_sieve_script::tokenize($t[1], $t[0])); -} -?> ---EXPECT-- -[1]--------------- -string(34) "This is test ; message; -Multi line" -[2]--------------- -array(1) { -  [0]=> -  array(2) { -    [0]=> -    string(5) "test1" -    [1]=> -    string(5) "test2" -  } -} -[3]--------------- -array(1) { -  [0]=> -  string(4) "test" -} -[4]--------------- -string(5) "te"st" -[5]--------------- -array(1) { -  [0]=> -  string(4) "test" -} -[6]--------------- -array(2) { -  [0]=> -  string(4) "test" -  [1]=> -  string(4) "test" -} -[7]--------------- -string(4) "a\"a" diff --git a/plugins/password/drivers/virtualmin.php b/plugins/password/drivers/virtualmin.php index b2547e07f..f9eca9633 100644 --- a/plugins/password/drivers/virtualmin.php +++ b/plugins/password/drivers/virtualmin.php @@ -48,6 +48,10 @@ 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/plugins/virtuser_query/virtuser_query.php b/plugins/virtuser_query/virtuser_query.php index 21a869c57..073b4e230 100644 --- a/plugins/virtuser_query/virtuser_query.php +++ b/plugins/virtuser_query/virtuser_query.php @@ -14,8 +14,11 @@   *   * $rcmail_config['virtuser_query'] = array('email' => '', 'user' => '', 'host' => '');   * + * The email query can return more than one record to create more identities. + * This requires identities_level option to be set to value less than 2. + *   * @version @package_version@ - * @author Aleksander Machniak + * @author Aleksander Machniak <alec@alec.pl>   * @author Steffen Vogel   */  class virtuser_query extends rcube_plugin diff --git a/program/include/clisetup.php b/program/include/clisetup.php index 039020bdc..a9af90a6f 100644 --- a/program/include/clisetup.php +++ b/program/include/clisetup.php @@ -33,33 +33,36 @@ require_once INSTALL_PATH . 'program/include/iniset.php';   */  function get_opt($aliases = array())  { -	$args = array(); -	for ($i=1; $i < count($_SERVER['argv']); $i++) { -		$arg = $_SERVER['argv'][$i]; -		$value = true; -		$key = null; +    $args = array(); -		if ($arg[0] == '-') { -			$key = preg_replace('/^-+/', '', $arg); -			$sp = strpos($arg, '='); -			if ($sp > 0) { -				$key = substr($key, 0, $sp - 2); -				$value = substr($arg, $sp+1); -			} -			else if (strlen($_SERVER['argv'][$i+1]) && $_SERVER['argv'][$i+1][0] != '-') { -				$value = $_SERVER['argv'][++$i]; -			} +    for ($i=1; $i < count($_SERVER['argv']); $i++) { +        $arg   = $_SERVER['argv'][$i]; +        $value = true; +        $key   = null; -			$args[$key] = is_string($value) ? preg_replace(array('/^["\']/', '/["\']$/'), '', $value) : $value; -		} -		else -			$args[] = $arg; +        if ($arg[0] == '-') { +            $key = preg_replace('/^-+/', '', $arg); +            $sp  = strpos($arg, '='); +            if ($sp > 0) { +                $key   = substr($key, 0, $sp - 2); +                $value = substr($arg, $sp+1); +            } +            else if (strlen($_SERVER['argv'][$i+1]) && $_SERVER['argv'][$i+1][0] != '-') { +                $value = $_SERVER['argv'][++$i]; +            } -		if ($alias = $aliases[$key]) -			$args[$alias] = $args[$key]; -	} +            $args[$key] = is_string($value) ? preg_replace(array('/^["\']/', '/["\']$/'), '', $value) : $value; +        } +        else { +            $args[] = $arg; +        } -	return $args; +        if ($alias = $aliases[$key]) { +            $args[$alias] = $args[$key]; +        } +    } + +    return $args;  } diff --git a/program/include/html.php b/program/include/html.php index b42da1d7f..c6507f813 100644 --- a/program/include/html.php +++ b/program/include/html.php @@ -154,7 +154,7 @@ class html              $attr = array('src' => $attr);          }          return self::tag('img', $attr + array('alt' => ''), null, array_merge(self::$common_attrib, -	        array('src','alt','width','height','border','usemap','onclick'))); +            array('src','alt','width','height','border','usemap','onclick')));      }      /** @@ -171,7 +171,7 @@ class html              $attr = array('href' => $attr);          }          return self::tag('a', $attr, $cont, array_merge(self::$common_attrib, -	    array('href','target','name','rel','onclick','onmouseover','onmouseout','onmousedown','onmouseup'))); +        array('href','target','name','rel','onclick','onmouseover','onmouseout','onmousedown','onmouseup')));      }      /** @@ -358,7 +358,7 @@ class html_inputfield extends html      protected $tagname = 'input';      protected $type = 'text';      protected $allowed = array( -        'type','name','value','size','tabindex', +        'type','name','value','size','tabindex','autocapitalize',          'autocomplete','checked','onchange','onclick','disabled','readonly',          'spellcheck','results','maxlength','src','multiple','placeholder',      ); @@ -532,7 +532,7 @@ class html_textarea extends html  {      protected $tagname = 'textarea';      protected $allowed = array('name','rows','cols','wrap','tabindex', -	'onchange','disabled','readonly','spellcheck'); +        'onchange','disabled','readonly','spellcheck');      /**       * Get HTML code for this object @@ -563,7 +563,7 @@ class html_textarea extends html          }          return self::tag($this->tagname, $this->attrib, $value, -	        array_merge(self::$common_attrib, $this->allowed)); +            array_merge(self::$common_attrib, $this->allowed));      }  } @@ -591,7 +591,7 @@ class html_select extends html      protected $tagname = 'select';      protected $options = array();      protected $allowed = array('name','size','tabindex','autocomplete', -	'multiple','onchange','disabled','rel'); +        'multiple','onchange','disabled','rel');      /**       * Add a new option to this drop-down @@ -655,7 +655,7 @@ class html_table extends html  {      protected $tagname = 'table';      protected $allowed = array('id','class','style','width','summary', -	    'cellpadding','cellspacing','border'); +        'cellpadding','cellspacing','border');      private $header = array();      private $rows = array(); @@ -705,8 +705,9 @@ class html_table extends html       */      public function add_header($attr, $cont)      { -        if (is_string($attr)) -    	    $attr = array('class' => $attr); +        if (is_string($attr)) { +            $attr = array('class' => $attr); +        }          $cell = new stdClass;          $cell->attrib = $attr; @@ -763,11 +764,13 @@ class html_table extends html       */      public function set_row_attribs($attr = array(), $index = null)      { -        if (is_string($attr)) -    	    $attr = array('class' => $attr); +        if (is_string($attr)) { +            $attr = array('class' => $attr); +        } -        if ($index === null) +        if ($index === null) {              $index = $this->rowindex; +        }          $this->rows[$index]->attrib = $attr;      } @@ -781,8 +784,9 @@ class html_table extends html       */      public function get_row_attribs($index = null)      { -        if ($index === null) +        if ($index === null) {              $index = $this->rowindex; +        }          return $this->rows[$index] ? $this->rows[$index]->attrib : null;      } diff --git a/program/include/rcmail.php b/program/include/rcmail.php index a6b0bcd57..02f38e647 100644 --- a/program/include/rcmail.php +++ b/program/include/rcmail.php @@ -157,14 +157,16 @@ class rcmail extends rcube        $this->config->set_user_prefs((array)$this->user->get_prefs());      } -    $_SESSION['language'] = $this->user->language = $this->language_prop($this->config->get('language', $_SESSION['language'])); +    $lang = $this->language_prop($this->config->get('language', $_SESSION['language'])); +    $_SESSION['language'] = $this->user->language = $lang;      // set localization -    setlocale(LC_ALL, $_SESSION['language'] . '.utf8', 'en_US.utf8'); +    setlocale(LC_ALL, $lang . '.utf8', $lang . '.UTF-8', 'en_US.utf8', 'en_US.UTF-8');      // workaround for http://bugs.php.net/bug.php?id=18556 -    if (in_array($_SESSION['language'], array('tr_TR', 'ku', 'az_AZ'))) -      setlocale(LC_CTYPE, 'en_US' . '.utf8'); +    if (in_array($lang, array('tr_TR', 'ku', 'az_AZ'))) { +      setlocale(LC_CTYPE, 'en_US.utf8', 'en_US.UTF-8'); +    }    } @@ -306,7 +308,7 @@ class rcmail extends rcube    /**     * Init output object for GUI and add common scripts. -   * This will instantiate a rcmail_template object and set +   * This will instantiate a rcube_output_html object and set     * environment vars according to the current session and configuration     *     * @param boolean True if this request is loaded in a (i)frame @@ -453,7 +455,14 @@ class rcmail extends rcube      // Convert username to lowercase. If storage backend      // is case-insensitive we need to store always the same username (#1487113)      if ($config['login_lc']) { -      $username = mb_strtolower($username); +      if ($config['login_lc'] == 2 || $config['login_lc'] === true) { +        $username = mb_strtolower($username); +      } +      else if (strpos($username, '@')) { +        // lowercase domain name +        list($local, $domain) = explode('@', $username); +        $username = $local . '@' . mb_strtolower($domain); +      }      }      // try to resolve email address from virtuser table @@ -463,17 +472,13 @@ class rcmail extends rcube      // Here we need IDNA ASCII      // Only rcube_contacts class is using domain names in Unicode -    $host = rcube_utils::idn_to_ascii($host); -    if (strpos($username, '@')) { -      // lowercase domain name -      list($local, $domain) = explode('@', $username); -      $username = $local . '@' . mb_strtolower($domain); -      $username = rcube_utils::idn_to_ascii($username); -    } +    $host     = rcube_utils::idn_to_ascii($host); +    $username = rcube_utils::idn_to_ascii($username);      // user already registered -> overwrite username -    if ($user = rcube_user::query($username, $host)) +    if ($user = rcube_user::query($username, $host)) {        $username = $user->data['username']; +    }      $storage = $this->get_storage(); @@ -1204,7 +1209,7 @@ class rcmail extends rcube          }          else {              if (!empty($date)) { -                $timestamp = rcube_strtotime($date); +                $timestamp = rcube_utils::strtotime($date);              }              if (empty($timestamp)) { diff --git a/program/include/rcube.php b/program/include/rcube.php index 494b5c3dd..0e40b3c6b 100644 --- a/program/include/rcube.php +++ b/program/include/rcube.php @@ -25,364 +25,372 @@   * Base class of the Roundcube Framework   * implemented as singleton   * - * @package Core + * @package    Framework + * @subpackage Core   */  class rcube  { -  const INIT_WITH_DB = 1; -  const INIT_WITH_PLUGINS = 2; +    const INIT_WITH_DB = 1; +    const INIT_WITH_PLUGINS = 2; + +    /** +     * Singleton instace of rcube +     * +     * @var rcmail +     */ +    static protected $instance; -  /** -   * Singleton instace of rcube -   * -   * @var rcmail -   */ -  static protected $instance; +    /** +     * Stores instance of rcube_config. +     * +     * @var rcube_config +     */ +    public $config; -  /** -   * Stores instance of rcube_config. -   * -   * @var rcube_config -   */ -  public $config; +    /** +     * Instace of database class. +     * +     * @var rcube_db +     */ +    public $db; -  /** -   * Instace of database class. -   * -   * @var rcube_pdo -   */ -  public $db; +    /** +     * Instace of Memcache class. +     * +     * @var Memcache +     */ +    public $memcache; -  /** -   * Instace of Memcache class. -   * -   * @var Memcache -   */ -  public $memcache; +   /** +     * Instace of rcube_session class. +     * +     * @var rcube_session +     */ +    public $session; -  /** -   * Instace of rcube_session class. -   * -   * @var rcube_session -   */ -  public $session; +    /** +     * Instance of rcube_smtp class. +     * +     * @var rcube_smtp +     */ +    public $smtp; -  /** -   * Instance of rcube_smtp class. -   * -   * @var rcube_smtp -   */ -  public $smtp; +    /** +     * Instance of rcube_storage class. +     * +     * @var rcube_storage +     */ +    public $storage; -  /** -   * Instance of rcube_storage class. -   * -   * @var rcube_storage -   */ -  public $storage; +    /** +     * Instance of rcube_output class. +     * +     * @var rcube_output +     */ +    public $output; -  /** -   * Instance of rcube_output class. -   * -   * @var rcube_output -   */ -  public $output; +    /** +     * Instance of rcube_plugin_api. +     * +     * @var rcube_plugin_api +     */ +    public $plugins; -  /** -   * Instance of rcube_plugin_api. -   * -   * @var rcube_plugin_api -   */ -  public $plugins; +    /* private/protected vars */ +    protected $texts; +    protected $caches = array(); +    protected $shutdown_functions = array(); +    protected $expunge_cache = false; -  /* private/protected vars */ -  protected $texts; -  protected $caches = array(); -  protected $shutdown_functions = array(); -  protected $expunge_cache = false; +    /** +     * This implements the 'singleton' design pattern +     * +     * @param integer Options to initialize with this instance. See rcube::INIT_WITH_* constants +     * +     * @return rcube The one and only instance +     */ +    static function get_instance($mode = 0) +    { +        if (!self::$instance) { +            self::$instance = new rcube(); +            self::$instance->init($mode); +        } -  /** -   * This implements the 'singleton' design pattern -   * -   * @param integer Options to initialize with this instance. See rcube::INIT_WITH_* constants -   * @return rcube The one and only instance -   */ -  static function get_instance($mode = 0) -  { -    if (!self::$instance) { -      self::$instance = new rcube(); -      self::$instance->init($mode); +        return self::$instance;      } -    return self::$instance; -  } +    /** +     * Private constructor +     */ +    protected function __construct() +    { +        // load configuration +        $this->config  = new rcube_config; +        $this->plugins = new rcube_dummy_plugin_api; -  /** -   * Private constructor -   */ -  protected function __construct() -  { -    // load configuration -    $this->config = new rcube_config(); -    $this->plugins = new rcube_dummy_plugin_api; +        register_shutdown_function(array($this, 'shutdown')); +    } -    register_shutdown_function(array($this, 'shutdown')); -  } +    /** +     * Initial startup function +     */ +    protected function init($mode = 0) +    { +        // initialize syslog +        if ($this->config->get('log_driver') == 'syslog') { +            $syslog_id       = $this->config->get('syslog_id', 'roundcube'); +            $syslog_facility = $this->config->get('syslog_facility', LOG_USER); +            openlog($syslog_id, LOG_ODELAY, $syslog_facility); +        } -  /** -   * Initial startup function -   */ -  protected function init($mode = 0) -  { -    // initialize syslog -    if ($this->config->get('log_driver') == 'syslog') { -      $syslog_id = $this->config->get('syslog_id', 'roundcube'); -      $syslog_facility = $this->config->get('syslog_facility', LOG_USER); -      openlog($syslog_id, LOG_ODELAY, $syslog_facility); -    } +        // connect to database +        if ($mode & self::INIT_WITH_DB) { +            $this->get_dbh(); +        } -    // connect to database -    if ($mode & self::INIT_WITH_DB) { -      $this->get_dbh(); +        // create plugin API and load plugins +        if ($mode & self::INIT_WITH_PLUGINS) { +            $this->plugins = rcube_plugin_api::get_instance(); +        }      } -    // create plugin API and load plugins -    if ($mode & self::INIT_WITH_PLUGINS) { -      $this->plugins = rcube_plugin_api::get_instance(); -    } -  } +    /** +     * Get the current database connection +     * +     * @return rcube_db Database object +     */ +    public function get_dbh() +    { +        if (!$this->db) { +            $config_all = $this->config->all(); +            $this->db = rcube_db::factory($config_all['db_dsnw'], $config_all['db_dsnr'], $config_all['db_persistent']); +            $this->db->set_debug((bool)$config_all['sql_debug']); +        } -  /** -   * Get the current database connection -   * -   * @return rcube_pdo  Database connection object -   */ -  public function get_dbh() -  { -    if (!$this->db) { -      $config_all = $this->config->all(); -      $this->db = rcube_db::factory($config_all['db_dsnw'], $config_all['db_dsnr'], $config_all['db_persistent']); -      $this->db->set_debug((bool)$config_all['sql_debug']); +        return $this->db;      } -    return $this->db; -  } +    /** +     * Get global handle for memcache access +     * +     * @return object Memcache +     */ +    public function get_memcache() +    { +        if (!isset($this->memcache)) { +            // no memcache support in PHP +            if (!class_exists('Memcache')) { +                $this->memcache = false; +                return false; +            } -  /** -   * Get global handle for memcache access -   * -   * @return object Memcache -   */ -  public function get_memcache() -  { -    if (!isset($this->memcache)) { -      // no memcache support in PHP -      if (!class_exists('Memcache')) { -        $this->memcache = false; -        return false; -      } +            $this->memcache     = new Memcache; +            $this->mc_available = 0; -      $this->memcache = new Memcache; -      $this->mc_available = 0; +            // add all configured hosts to pool +            $pconnect = $this->config->get('memcache_pconnect', true); +            foreach ($this->config->get('memcache_hosts', array()) as $host) { +                if (substr($host, 0, 7) != 'unix://') { +                    list($host, $port) = explode(':', $host); +                    if (!$port) $port = 11211; +                } +                else { +                    $port = 0; +                } -      // add all configured hosts to pool -      $pconnect = $this->config->get('memcache_pconnect', true); -      foreach ($this->config->get('memcache_hosts', array()) as $host) { -        if (substr($host, 0, 7) != 'unix://') { -          list($host, $port) = explode(':', $host); -          if (!$port) $port = 11211; -        } -        else { -          $port = 0; -        } +                $this->mc_available += intval($this->memcache->addServer( +                    $host, $port, $pconnect, 1, 1, 15, false, array($this, 'memcache_failure'))); +            } -        $this->mc_available += intval($this->memcache->addServer($host, $port, $pconnect, 1, 1, 15, false, array($this, 'memcache_failure'))); -      } +            // test connection and failover (will result in $this->mc_available == 0 on complete failure) +            $this->memcache->increment('__CONNECTIONTEST__', 1);  // NOP if key doesn't exist -      // test connection and failover (will result in $this->mc_available == 0 on complete failure) -      $this->memcache->increment('__CONNECTIONTEST__', 1);  // NOP if key doesn't exist +            if (!$this->mc_available) { +                $this->memcache = false; +            } +        } -      if (!$this->mc_available) -        $this->memcache = false; +        return $this->memcache;      } -    return $this->memcache; -  } - -  /** -   * Callback for memcache failure -   */ -  public function memcache_failure($host, $port) -  { -    static $seen = array(); +    /** +     * Callback for memcache failure +     */ +    public function memcache_failure($host, $port) +    { +        static $seen = array(); -    // only report once -    if (!$seen["$host:$port"]++) { -      $this->mc_available--; -      self::raise_error(array('code' => 604, 'type' => 'db', -        'line' => __LINE__, 'file' => __FILE__, -        'message' => "Memcache failure on host $host:$port"), -        true, false); +        // only report once +        if (!$seen["$host:$port"]++) { +            $this->mc_available--; +            self::raise_error(array( +                'code' => 604, 'type' => 'db', +                'line' => __LINE__, 'file' => __FILE__, +                'message' => "Memcache failure on host $host:$port"), +                true, false); +        }      } -  } -  /** -   * Initialize and get cache object -   * -   * @param string $name   Cache identifier -   * @param string $type   Cache type ('db', 'apc' or 'memcache') -   * @param string $ttl    Expiration time for cache items -   * @param bool   $packed Enables/disables data serialization -   * -   * @return rcube_cache Cache object -   */ -  public function get_cache($name, $type='db', $ttl=0, $packed=true) -  { -    if (!isset($this->caches[$name]) && ($userid = $this->get_user_id())) { -      $this->caches[$name] = new rcube_cache($type, $userid, $name, $ttl, $packed); +    /** +     * Initialize and get cache object +     * +     * @param string $name   Cache identifier +     * @param string $type   Cache type ('db', 'apc' or 'memcache') +     * @param string $ttl    Expiration time for cache items +     * @param bool   $packed Enables/disables data serialization +     * +     * @return rcube_cache Cache object +     */ +    public function get_cache($name, $type='db', $ttl=0, $packed=true) +    { +        if (!isset($this->caches[$name]) && ($userid = $this->get_user_id())) { +            $this->caches[$name] = new rcube_cache($type, $userid, $name, $ttl, $packed); +        } + +        return $this->caches[$name];      } -    return $this->caches[$name]; -  } +    /** +     * Create SMTP object and connect to server +     * +     * @param boolean True if connection should be established +     */ +    public function smtp_init($connect = false) +    { +        $this->smtp = new rcube_smtp(); -  /** -   * Create SMTP object and connect to server -   * -   * @param boolean True if connection should be established -   */ -  public function smtp_init($connect = false) -  { -    $this->smtp = new rcube_smtp(); +        if ($connect) { +            $this->smtp->connect(); +        } +    } -    if ($connect) -      $this->smtp->connect(); -  } +    /** +     * Initialize and get storage object +     * +     * @return rcube_storage Storage object +     */ +    public function get_storage() +    { +        // already initialized +        if (!is_object($this->storage)) { +            $this->storage_init(); +        } -  /** -   * Initialize and get storage object -   * -   * @return rcube_storage Storage object -   */ -  public function get_storage() -  { -    // already initialized -    if (!is_object($this->storage)) { -      $this->storage_init(); +        return $this->storage;      } -    return $this->storage; -  } +    /** +     * Initialize storage object +     */ +    public function storage_init() +    { +        // already initialized +        if (is_object($this->storage)) { +            return; +        } -  /** -   * Initialize storage object -   */ -  public function storage_init() -  { -    // already initialized -    if (is_object($this->storage)) { -      return; -    } - -    $driver = $this->config->get('storage_driver', 'imap'); -    $driver_class = "rcube_{$driver}"; +        $driver       = $this->config->get('storage_driver', 'imap'); +        $driver_class = "rcube_{$driver}"; -    if (!class_exists($driver_class)) { -      self::raise_error(array( -        'code' => 700, 'type' => 'php', -        'file' => __FILE__, 'line' => __LINE__, -        'message' => "Storage driver class ($driver) not found!"), -        true, true); -    } +        if (!class_exists($driver_class)) { +            self::raise_error(array( +                'code' => 700, 'type' => 'php', +                'file' => __FILE__, 'line' => __LINE__, +                'message' => "Storage driver class ($driver) not found!"), +                true, true); +        } -    // Initialize storage object -    $this->storage = new $driver_class; +        // Initialize storage object +        $this->storage = new $driver_class; -    // for backward compat. (deprecated, will be removed) -    $this->imap = $this->storage; +        // for backward compat. (deprecated, will be removed) +        $this->imap = $this->storage; -    // enable caching of mail data -    $storage_cache  = $this->config->get("{$driver}_cache"); -    $messages_cache = $this->config->get('messages_cache'); -    // for backward compatybility -    if ($storage_cache === null && $messages_cache === null && $this->config->get('enable_caching')) { -        $storage_cache  = 'db'; -        $messages_cache = true; -    } +        // enable caching of mail data +        $storage_cache  = $this->config->get("{$driver}_cache"); +        $messages_cache = $this->config->get('messages_cache'); +        // for backward compatybility +        if ($storage_cache === null && $messages_cache === null && $this->config->get('enable_caching')) { +            $storage_cache  = 'db'; +            $messages_cache = true; +        } -    if ($storage_cache) -        $this->storage->set_caching($storage_cache); -    if ($messages_cache) -        $this->storage->set_messages_caching(true); +        if ($storage_cache) { +            $this->storage->set_caching($storage_cache); +        } +        if ($messages_cache) { +            $this->storage->set_messages_caching(true); +        } -    // set pagesize from config -    $pagesize = $this->config->get('mail_pagesize'); -    if (!$pagesize) { -        $pagesize = $this->config->get('pagesize', 50); -    } -    $this->storage->set_pagesize($pagesize); +        // set pagesize from config +        $pagesize = $this->config->get('mail_pagesize'); +        if (!$pagesize) { +            $pagesize = $this->config->get('pagesize', 50); +        } +        $this->storage->set_pagesize($pagesize); -    // set class options -    $options = array( -      'auth_type'   => $this->config->get("{$driver}_auth_type", 'check'), -      'auth_cid'    => $this->config->get("{$driver}_auth_cid"), -      'auth_pw'     => $this->config->get("{$driver}_auth_pw"), -      'debug'       => (bool) $this->config->get("{$driver}_debug"), -      'force_caps'  => (bool) $this->config->get("{$driver}_force_caps"), -      'timeout'     => (int) $this->config->get("{$driver}_timeout"), -      'skip_deleted' => (bool) $this->config->get('skip_deleted'), -      'driver'      => $driver, -    ); +        // set class options +        $options = array( +            'auth_type'   => $this->config->get("{$driver}_auth_type", 'check'), +            'auth_cid'    => $this->config->get("{$driver}_auth_cid"), +            'auth_pw'     => $this->config->get("{$driver}_auth_pw"), +            'debug'       => (bool) $this->config->get("{$driver}_debug"), +            'force_caps'  => (bool) $this->config->get("{$driver}_force_caps"), +            'timeout'     => (int) $this->config->get("{$driver}_timeout"), +            'skip_deleted' => (bool) $this->config->get('skip_deleted'), +            'driver'      => $driver, +        ); -    if (!empty($_SESSION['storage_host'])) { -      $options['host']     = $_SESSION['storage_host']; -      $options['user']     = $_SESSION['username']; -      $options['port']     = $_SESSION['storage_port']; -      $options['ssl']      = $_SESSION['storage_ssl']; -      $options['password'] = $this->decrypt($_SESSION['password']); -      $_SESSION[$driver.'_host'] = $_SESSION['storage_host']; -    } +        if (!empty($_SESSION['storage_host'])) { +            $options['host']     = $_SESSION['storage_host']; +            $options['user']     = $_SESSION['username']; +            $options['port']     = $_SESSION['storage_port']; +            $options['ssl']      = $_SESSION['storage_ssl']; +            $options['password'] = $this->decrypt($_SESSION['password']); +            $_SESSION[$driver.'_host'] = $_SESSION['storage_host']; +        } -    $options = $this->plugins->exec_hook("storage_init", $options); +        $options = $this->plugins->exec_hook("storage_init", $options); -    // for backward compat. (deprecated, to be removed) -    $options = $this->plugins->exec_hook("imap_init", $options); +        // for backward compat. (deprecated, to be removed) +        $options = $this->plugins->exec_hook("imap_init", $options); -    $this->storage->set_options($options); -    $this->set_storage_prop(); -  } +        $this->storage->set_options($options); +        $this->set_storage_prop(); +    } -  /** -   * Set storage parameters. -   * This must be done AFTER connecting to the server! -   */ -  protected function set_storage_prop() -  { -    $storage = $this->get_storage(); +    /** +     * Set storage parameters. +     * This must be done AFTER connecting to the server! +     */ +    protected function set_storage_prop() +    { +        $storage = $this->get_storage(); -    $storage->set_charset($this->config->get('default_charset', RCMAIL_CHARSET)); +        $storage->set_charset($this->config->get('default_charset', RCMAIL_CHARSET)); -    if ($default_folders = $this->config->get('default_folders')) { -      $storage->set_default_folders($default_folders); -    } -    if (isset($_SESSION['mbox'])) { -      $storage->set_folder($_SESSION['mbox']); -    } -    if (isset($_SESSION['page'])) { -      $storage->set_page($_SESSION['page']); +        if ($default_folders = $this->config->get('default_folders')) { +            $storage->set_default_folders($default_folders); +        } +        if (isset($_SESSION['mbox'])) { +            $storage->set_folder($_SESSION['mbox']); +        } +        if (isset($_SESSION['page'])) { +            $storage->set_page($_SESSION['page']); +        }      } -  }      /** @@ -397,12 +405,17 @@ class rcube          $sess_name   = $this->config->get('session_name');          $sess_domain = $this->config->get('session_domain'); +        $sess_path   = $this->config->get('session_path');          $lifetime    = $this->config->get('session_lifetime', 0) * 60;          // set session domain          if ($sess_domain) {              ini_set('session.cookie_domain', $sess_domain);          } +        // set session path +        if ($sess_path) { +            ini_set('session.cookie_path', $sess_path); +        }          // set session garbage collecting time according to session_lifetime          if ($lifetime) {              ini_set('session.gc_maxlifetime', $lifetime * 2); @@ -492,433 +505,469 @@ class rcube      } -  /** -   * Get localized text in the desired language -   * -   * @param mixed   $attrib  Named parameters array or label name -   * @param string  $domain  Label domain (plugin) name -   * -   * @return string Localized text -   */ -  public function gettext($attrib, $domain=null) -  { -    // load localization files if not done yet -    if (empty($this->texts)) -      $this->load_language(); +    /** +     * Get localized text in the desired language +     * +     * @param mixed   $attrib  Named parameters array or label name +     * @param string  $domain  Label domain (plugin) name +     * +     * @return string Localized text +     */ +    public function gettext($attrib, $domain=null) +    { +        // load localization files if not done yet +        if (empty($this->texts)) { +            $this->load_language(); +        } -    // extract attributes -    if (is_string($attrib)) -      $attrib = array('name' => $attrib); +        // extract attributes +        if (is_string($attrib)) { +            $attrib = array('name' => $attrib); +        } -    $name = $attrib['name'] ? $attrib['name'] : ''; +        $name = $attrib['name'] ? $attrib['name'] : ''; -    // attrib contain text values: use them from now -    if (($setval = $attrib[strtolower($_SESSION['language'])]) || ($setval = $attrib['en_us'])) -        $this->texts[$name] = $setval; +        // attrib contain text values: use them from now +        if (($setval = $attrib[strtolower($_SESSION['language'])]) || ($setval = $attrib['en_us'])) { +            $this->texts[$name] = $setval; +        } -    // check for text with domain -    if ($domain && ($text = $this->texts[$domain.'.'.$name])) -      ; -    // text does not exist -    else if (!($text = $this->texts[$name])) { -      return "[$name]"; -    } +        // check for text with domain +        if ($domain && ($text = $this->texts[$domain.'.'.$name])) { +        } +        // text does not exist +        else if (!($text = $this->texts[$name])) { +            return "[$name]"; +        } -    // replace vars in text -    if (is_array($attrib['vars'])) { -      foreach ($attrib['vars'] as $var_key => $var_value) -        $text = str_replace($var_key[0]!='$' ? '$'.$var_key : $var_key, $var_value, $text); -    } +        // replace vars in text +        if (is_array($attrib['vars'])) { +            foreach ($attrib['vars'] as $var_key => $var_value) { +                $text = str_replace($var_key[0]!='$' ? '$'.$var_key : $var_key, $var_value, $text); +            } +        } -    // format output -    if (($attrib['uppercase'] && strtolower($attrib['uppercase']=='first')) || $attrib['ucfirst']) -      return ucfirst($text); -    else if ($attrib['uppercase']) -      return mb_strtoupper($text); -    else if ($attrib['lowercase']) -      return mb_strtolower($text); +        // format output +        if (($attrib['uppercase'] && strtolower($attrib['uppercase'] == 'first')) || $attrib['ucfirst']) { +            return ucfirst($text); +        } +        else if ($attrib['uppercase']) { +            return mb_strtoupper($text); +        } +        else if ($attrib['lowercase']) { +            return mb_strtolower($text); +        } -    return strtr($text, array('\n' => "\n")); -  } +        return strtr($text, array('\n' => "\n")); +    } -  /** -   * Check if the given text label exists -   * -   * @param string  $name       Label name -   * @param string  $domain     Label domain (plugin) name or '*' for all domains -   * @param string  $ref_domain Sets domain name if label is found -   * -   * @return boolean True if text exists (either in the current language or in en_US) -   */ -  public function text_exists($name, $domain = null, &$ref_domain = null) -  { -    // load localization files if not done yet -    if (empty($this->texts)) -      $this->load_language(); +    /** +     * Check if the given text label exists +     * +     * @param string  $name       Label name +     * @param string  $domain     Label domain (plugin) name or '*' for all domains +     * @param string  $ref_domain Sets domain name if label is found +     * +     * @return boolean True if text exists (either in the current language or in en_US) +     */ +    public function text_exists($name, $domain = null, &$ref_domain = null) +    { +        // load localization files if not done yet +        if (empty($this->texts)) { +            $this->load_language(); +        } -    if (isset($this->texts[$name])) { -        $ref_domain = ''; -        return true; -    } +        if (isset($this->texts[$name])) { +            $ref_domain = ''; +            return true; +        } -    // any of loaded domains (plugins) -    if ($domain == '*') { -      foreach ($this->plugins->loaded_plugins() as $domain) -        if (isset($this->texts[$domain.'.'.$name])) { -          $ref_domain = $domain; -          return true; +        // any of loaded domains (plugins) +        if ($domain == '*') { +            foreach ($this->plugins->loaded_plugins() as $domain) { +                if (isset($this->texts[$domain.'.'.$name])) { +                    $ref_domain = $domain; +                    return true; +                } +            }          } +        // specified domain +        else if ($domain) { +            $ref_domain = $domain; +            return isset($this->texts[$domain.'.'.$name]); +        } + +        return false;      } -    // specified domain -    else if ($domain) { -      $ref_domain = $domain; -      return isset($this->texts[$domain.'.'.$name]); -    } -    return false; -  } -  /** -   * Load a localization package -   * -   * @param string Language ID -   */ -  public function load_language($lang = null, $add = array()) -  { -    $lang = $this->language_prop(($lang ? $lang : $_SESSION['language'])); +    /** +     * Load a localization package +     * +     * @param string Language ID +     * @param array  Additional text labels/messages +     */ +    public function load_language($lang = null, $add = array()) +    { +        $lang = $this->language_prop(($lang ? $lang : $_SESSION['language'])); + +        // load localized texts +        if (empty($this->texts) || $lang != $_SESSION['language']) { +            $this->texts = array(); -    // load localized texts -    if (empty($this->texts) || $lang != $_SESSION['language']) { -      $this->texts = array(); +            // handle empty lines after closing PHP tag in localization files +            ob_start(); -      // handle empty lines after closing PHP tag in localization files -      ob_start(); +            // get english labels (these should be complete) +            @include(INSTALL_PATH . 'program/localization/en_US/labels.inc'); +            @include(INSTALL_PATH . 'program/localization/en_US/messages.inc'); -      // get english labels (these should be complete) -      @include(INSTALL_PATH . 'program/localization/en_US/labels.inc'); -      @include(INSTALL_PATH . 'program/localization/en_US/messages.inc'); +            if (is_array($labels)) +                $this->texts = $labels; +            if (is_array($messages)) +                $this->texts = array_merge($this->texts, $messages); -      if (is_array($labels)) -        $this->texts = $labels; -      if (is_array($messages)) -        $this->texts = array_merge($this->texts, $messages); +            // include user language files +            if ($lang != 'en' && $lang != 'en_US' && is_dir(INSTALL_PATH . 'program/localization/' . $lang)) { +                include_once(INSTALL_PATH . 'program/localization/' . $lang . '/labels.inc'); +                include_once(INSTALL_PATH . 'program/localization/' . $lang . '/messages.inc'); -      // include user language files -      if ($lang != 'en' && $lang != 'en_US' && is_dir(INSTALL_PATH . 'program/localization/' . $lang)) { -        include_once(INSTALL_PATH . 'program/localization/' . $lang . '/labels.inc'); -        include_once(INSTALL_PATH . 'program/localization/' . $lang . '/messages.inc'); +                if (is_array($labels)) +                    $this->texts = array_merge($this->texts, $labels); +                if (is_array($messages)) +                    $this->texts = array_merge($this->texts, $messages); +            } -        if (is_array($labels)) -          $this->texts = array_merge($this->texts, $labels); -        if (is_array($messages)) -          $this->texts = array_merge($this->texts, $messages); -      } +            ob_end_clean(); -      ob_end_clean(); +            $_SESSION['language'] = $lang; +        } -      $_SESSION['language'] = $lang; +        // append additional texts (from plugin) +        if (is_array($add) && !empty($add)) { +            $this->texts += $add; +        }      } -    // append additional texts (from plugin) -    if (is_array($add) && !empty($add)) -      $this->texts += $add; -  } +    /** +     * Check the given string and return a valid language code +     * +     * @param string Language code +     * +     * @return string Valid language code +     */ +    protected function language_prop($lang) +    { +        static $rcube_languages, $rcube_language_aliases; -  /** -   * Check the given string and return a valid language code -   * -   * @param string Language code -   * @return string Valid language code -   */ -  protected function language_prop($lang) -  { -    static $rcube_languages, $rcube_language_aliases; +        // user HTTP_ACCEPT_LANGUAGE if no language is specified +        if (empty($lang) || $lang == 'auto') { +            $accept_langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']); +            $lang         = str_replace('-', '_', $accept_langs[0]); +        } -    // user HTTP_ACCEPT_LANGUAGE if no language is specified -    if (empty($lang) || $lang == 'auto') { -       $accept_langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']); -       $lang = str_replace('-', '_', $accept_langs[0]); -     } +        if (empty($rcube_languages)) { +            @include(INSTALL_PATH . 'program/localization/index.inc'); +        } -    if (empty($rcube_languages)) { -      @include(INSTALL_PATH . 'program/localization/index.inc'); -    } +        // check if we have an alias for that language +        if (!isset($rcube_languages[$lang]) && isset($rcube_language_aliases[$lang])) { +            $lang = $rcube_language_aliases[$lang]; +        } +        // try the first two chars +        else if (!isset($rcube_languages[$lang])) { +            $short = substr($lang, 0, 2); -    // check if we have an alias for that language -    if (!isset($rcube_languages[$lang]) && isset($rcube_language_aliases[$lang])) { -      $lang = $rcube_language_aliases[$lang]; -    } -    // try the first two chars -    else if (!isset($rcube_languages[$lang])) { -      $short = substr($lang, 0, 2); +            // check if we have an alias for the short language code +            if (!isset($rcube_languages[$short]) && isset($rcube_language_aliases[$short])) { +                $lang = $rcube_language_aliases[$short]; +            } +            // expand 'nn' to 'nn_NN' +            else if (!isset($rcube_languages[$short])) { +                $lang = $short.'_'.strtoupper($short); +            } +        } -      // check if we have an alias for the short language code -      if (!isset($rcube_languages[$short]) && isset($rcube_language_aliases[$short])) { -        $lang = $rcube_language_aliases[$short]; -      } -      // expand 'nn' to 'nn_NN' -      else if (!isset($rcube_languages[$short])) { -        $lang = $short.'_'.strtoupper($short); -      } -    } +        if (!isset($rcube_languages[$lang]) || !is_dir(INSTALL_PATH . 'program/localization/' . $lang)) { +            $lang = 'en_US'; +        } -    if (!isset($rcube_languages[$lang]) || !is_dir(INSTALL_PATH . 'program/localization/' . $lang)) { -      $lang = 'en_US'; +        return $lang;      } -    return $lang; -  } +    /** +     * Read directory program/localization and return a list of available languages +     * +     * @return array List of available localizations +     */ +    public function list_languages() +    { +        static $sa_languages = array(); -  /** -   * Read directory program/localization and return a list of available languages -   * -   * @return array List of available localizations -   */ -  public function list_languages() -  { -    static $sa_languages = array(); - -    if (!sizeof($sa_languages)) { -      @include(INSTALL_PATH . 'program/localization/index.inc'); +        if (!sizeof($sa_languages)) { +            @include(INSTALL_PATH . 'program/localization/index.inc'); -      if ($dh = @opendir(INSTALL_PATH . 'program/localization')) { -        while (($name = readdir($dh)) !== false) { -          if ($name[0] == '.' || !is_dir(INSTALL_PATH . 'program/localization/' . $name)) -            continue; +            if ($dh = @opendir(INSTALL_PATH . 'program/localization')) { +                while (($name = readdir($dh)) !== false) { +                    if ($name[0] == '.' || !is_dir(INSTALL_PATH . 'program/localization/' . $name)) { +                        continue; +                    } -          if ($label = $rcube_languages[$name]) -            $sa_languages[$name] = $label; +                    if ($label = $rcube_languages[$name]) { +                        $sa_languages[$name] = $label; +                    } +                } +                closedir($dh); +            }          } -        closedir($dh); -      } + +        return $sa_languages;      } -    return $sa_languages; -  } +    /** +     * Encrypt using 3DES +     * +     * @param string $clear clear text input +     * @param string $key encryption key to retrieve from the configuration, defaults to 'des_key' +     * @param boolean $base64 whether or not to base64_encode() the result before returning +     * +     * @return string encrypted text +     */ +    public function encrypt($clear, $key = 'des_key', $base64 = true) +    { +        if (!$clear) { +            return ''; +        } -  /** -   * Encrypt using 3DES -   * -   * @param string $clear clear text input -   * @param string $key encryption key to retrieve from the configuration, defaults to 'des_key' -   * @param boolean $base64 whether or not to base64_encode() the result before returning -   * -   * @return string encrypted text -   */ -  public function encrypt($clear, $key = 'des_key', $base64 = true) -  { -    if (!$clear) -      return ''; +        /*- +         * Add a single canary byte to the end of the clear text, which +         * will help find out how much of padding will need to be removed +         * upon decryption; see http://php.net/mcrypt_generic#68082 +         */ +        $clear = pack("a*H2", $clear, "80"); -    /*- -     * Add a single canary byte to the end of the clear text, which -     * will help find out how much of padding will need to be removed -     * upon decryption; see http://php.net/mcrypt_generic#68082 -     */ -    $clear = pack("a*H2", $clear, "80"); +        if (function_exists('mcrypt_module_open') && +            ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, "")) +        ) { +            $iv = $this->create_iv(mcrypt_enc_get_iv_size($td)); +            mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv); +            $cipher = $iv . mcrypt_generic($td, $clear); +            mcrypt_generic_deinit($td); +            mcrypt_module_close($td); +        } +        else { +            @include_once 'des.inc'; -    if (function_exists('mcrypt_module_open') && -        ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, ""))) { -      $iv = $this->create_iv(mcrypt_enc_get_iv_size($td)); -      mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv); -      $cipher = $iv . mcrypt_generic($td, $clear); -      mcrypt_generic_deinit($td); -      mcrypt_module_close($td); -    } -    else { -      @include_once 'des.inc'; +            if (function_exists('des')) { +                $des_iv_size = 8; +                $iv = $this->create_iv($des_iv_size); +                $cipher = $iv . des($this->config->get_crypto_key($key), $clear, 1, 1, $iv); +            } +            else { +                self::raise_error(array( +                    'code' => 500, 'type' => 'php', +                    'file' => __FILE__, 'line' => __LINE__, +                    'message' => "Could not perform encryption; make sure Mcrypt is installed or lib/des.inc is available" +                    ), true, true); +            } +        } -      if (function_exists('des')) { -        $des_iv_size = 8; -        $iv = $this->create_iv($des_iv_size); -        $cipher = $iv . des($this->config->get_crypto_key($key), $clear, 1, 1, $iv); -      } -      else { -        self::raise_error(array( -          'code' => 500, 'type' => 'php', -          'file' => __FILE__, 'line' => __LINE__, -          'message' => "Could not perform encryption; make sure Mcrypt is installed or lib/des.inc is available" -        ), true, true); -      } +        return $base64 ? base64_encode($cipher) : $cipher;      } -    return $base64 ? base64_encode($cipher) : $cipher; -  } -  /** -   * Decrypt 3DES-encrypted string -   * -   * @param string $cipher encrypted text -   * @param string $key encryption key to retrieve from the configuration, defaults to 'des_key' -   * @param boolean $base64 whether or not input is base64-encoded -   * -   * @return string decrypted text -   */ -  public function decrypt($cipher, $key = 'des_key', $base64 = true) -  { -    if (!$cipher) -      return ''; +    /** +     * Decrypt 3DES-encrypted string +     * +     * @param string $cipher encrypted text +     * @param string $key encryption key to retrieve from the configuration, defaults to 'des_key' +     * @param boolean $base64 whether or not input is base64-encoded +     * +     * @return string decrypted text +     */ +    public function decrypt($cipher, $key = 'des_key', $base64 = true) +    { +        if (!$cipher) { +            return ''; +        } + +        $cipher = $base64 ? base64_decode($cipher) : $cipher; -    $cipher = $base64 ? base64_decode($cipher) : $cipher; +        if (function_exists('mcrypt_module_open') && +            ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, "")) +        ) { +            $iv_size = mcrypt_enc_get_iv_size($td); +            $iv = substr($cipher, 0, $iv_size); -    if (function_exists('mcrypt_module_open') && -        ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, ""))) { -      $iv_size = mcrypt_enc_get_iv_size($td); -      $iv = substr($cipher, 0, $iv_size); +            // session corruption? (#1485970) +            if (strlen($iv) < $iv_size) { +                return ''; +            } -      // session corruption? (#1485970) -      if (strlen($iv) < $iv_size) -        return ''; +            $cipher = substr($cipher, $iv_size); +            mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv); +            $clear = mdecrypt_generic($td, $cipher); +            mcrypt_generic_deinit($td); +            mcrypt_module_close($td); +        } +        else { +            @include_once 'des.inc'; -      $cipher = substr($cipher, $iv_size); -      mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv); -      $clear = mdecrypt_generic($td, $cipher); -      mcrypt_generic_deinit($td); -      mcrypt_module_close($td); -    } -    else { -      @include_once 'des.inc'; +            if (function_exists('des')) { +                $des_iv_size = 8; +                $iv = substr($cipher, 0, $des_iv_size); +                $cipher = substr($cipher, $des_iv_size); +                $clear = des($this->config->get_crypto_key($key), $cipher, 0, 1, $iv); +            } +            else { +                self::raise_error(array( +                    'code' => 500, 'type' => 'php', +                    'file' => __FILE__, 'line' => __LINE__, +                    'message' => "Could not perform decryption; make sure Mcrypt is installed or lib/des.inc is available" +                    ), true, true); +            } +        } + +        /*- +         * Trim PHP's padding and the canary byte; see note in +         * rcube::encrypt() and http://php.net/mcrypt_generic#68082 +         */ +        $clear = substr(rtrim($clear, "\0"), 0, -1); -      if (function_exists('des')) { -        $des_iv_size = 8; -        $iv = substr($cipher, 0, $des_iv_size); -        $cipher = substr($cipher, $des_iv_size); -        $clear = des($this->config->get_crypto_key($key), $cipher, 0, 1, $iv); -      } -      else { -        self::raise_error(array( -          'code' => 500, 'type' => 'php', -          'file' => __FILE__, 'line' => __LINE__, -          'message' => "Could not perform decryption; make sure Mcrypt is installed or lib/des.inc is available" -        ), true, true); -      } +        return $clear;      } -    /*- -     * Trim PHP's padding and the canary byte; see note in -     * rcube::encrypt() and http://php.net/mcrypt_generic#68082 + +    /** +     * Generates encryption initialization vector (IV) +     * +     * @param int Vector size +     * +     * @return string Vector string       */ -    $clear = substr(rtrim($clear, "\0"), 0, -1); +    private function create_iv($size) +    { +        // mcrypt_create_iv() can be slow when system lacks entrophy +        // we'll generate IV vector manually +        $iv = ''; +        for ($i = 0; $i < $size; $i++) { +            $iv .= chr(mt_rand(0, 255)); +        } -    return $clear; -  } +        return $iv; +    } -  /** -   * Generates encryption initialization vector (IV) -   * -   * @param int Vector size -   * @return string Vector string -   */ -  private function create_iv($size) -  { -    // mcrypt_create_iv() can be slow when system lacks entrophy -    // we'll generate IV vector manually -    $iv = ''; -    for ($i = 0; $i < $size; $i++) -        $iv .= chr(mt_rand(0, 255)); -    return $iv; -  } +    /** +     * Build a valid URL to this instance of Roundcube +     * +     * @param mixed Either a string with the action or url parameters as key-value pairs +     * @return string Valid application URL +     */ +    public function url($p) +    { +        // STUB: should be overloaded by the application +        return ''; +    } -  /** -   * Build a valid URL to this instance of Roundcube -   * -   * @param mixed Either a string with the action or url parameters as key-value pairs -   * @return string Valid application URL -   */ -  public function url($p) -  { -      // STUB: should be overloaded by the application -      return ''; -  } +    /** +     * Function to be executed in script shutdown +     * Registered with register_shutdown_function() +     */ +    public function shutdown() +    { +        foreach ($this->shutdown_functions as $function) { +            call_user_func($function); +        } -  /** -   * Function to be executed in script shutdown -   * Registered with register_shutdown_function() -   */ -  public function shutdown() -  { -    foreach ($this->shutdown_functions as $function) -      call_user_func($function); +        if (is_object($this->smtp)) { +            $this->smtp->disconnect(); +        } -    if (is_object($this->smtp)) -      $this->smtp->disconnect(); +        foreach ($this->caches as $cache) { +            if (is_object($cache)) { +                $cache->close(); +            } +        } -    foreach ($this->caches as $cache) { -        if (is_object($cache)) -            $cache->close(); +        if (is_object($this->storage)) { +            if ($this->expunge_cache) { +                $this->storage->expunge_cache(); +            } +            $this->storage->close(); +        }      } -    if (is_object($this->storage)) { -      if ($this->expunge_cache) -        $this->storage->expunge_cache(); -      $this->storage->close(); -    } -  } +    /** +     * Registers shutdown function to be executed on shutdown. +     * The functions will be executed before destroying any +     * objects like smtp, imap, session, etc. +     * +     * @param callback Function callback +     */ +    public function add_shutdown_function($function) +    { +        $this->shutdown_functions[] = $function; +    } -  /** -   * Registers shutdown function to be executed on shutdown. -   * The functions will be executed before destroying any -   * objects like smtp, imap, session, etc. -   * -   * @param callback Function callback -   */ -  public function add_shutdown_function($function) -  { -    $this->shutdown_functions[] = $function; -  } +    /** +     * Construct shell command, execute it and return output as string. +     * Keywords {keyword} are replaced with arguments +     * +     * @param $cmd Format string with {keywords} to be replaced +     * @param $values (zero, one or more arrays can be passed) +     * +     * @return output of command. shell errors not detectable +     */ +    public static function exec(/* $cmd, $values1 = array(), ... */) +    { +        $args   = func_get_args(); +        $cmd    = array_shift($args); +        $values = $replacements = array(); -  /** -   * Construct shell command, execute it and return output as string. -   * Keywords {keyword} are replaced with arguments -   * -   * @param $cmd Format string with {keywords} to be replaced -   * @param $values (zero, one or more arrays can be passed) -   * @return output of command. shell errors not detectable -   */ -  public static function exec(/* $cmd, $values1 = array(), ... */) -  { -    $args = func_get_args(); -    $cmd = array_shift($args); -    $values = $replacements = array(); +        // merge values into one array +        foreach ($args as $arg) { +            $values += (array)$arg; +        } -    // merge values into one array -    foreach ($args as $arg) -      $values += (array)$arg; +        preg_match_all('/({(-?)([a-z]\w*)})/', $cmd, $matches, PREG_SET_ORDER); +        foreach ($matches as $tags) { +            list(, $tag, $option, $key) = $tags; +            $parts = array(); -    preg_match_all('/({(-?)([a-z]\w*)})/', $cmd, $matches, PREG_SET_ORDER); -    foreach ($matches as $tags) { -      list(, $tag, $option, $key) = $tags; -      $parts = array(); +            if ($option) { +                foreach ((array)$values["-$key"] as $key => $value) { +                    if ($value === true || $value === false || $value === null) { +                        $parts[] = $value ? $key : ""; +                    } +                    else { +                        foreach ((array)$value as $val) { +                            $parts[] = "$key " . escapeshellarg($val); +                        } +                    } +                } +            } +            else { +                foreach ((array)$values[$key] as $value) { +                    $parts[] = escapeshellarg($value); +                } +            } -      if ($option) { -        foreach ((array)$values["-$key"] as $key => $value) { -          if ($value === true || $value === false || $value === null) -            $parts[] = $value ? $key : ""; -          else foreach ((array)$value as $val) -            $parts[] = "$key " . escapeshellarg($val); +            $replacements[$tag] = join(" ", $parts);          } -      } -      else { -        foreach ((array)$values[$key] as $value) -          $parts[] = escapeshellarg($value); -      } -      $replacements[$tag] = join(" ", $parts); -    } - -    // use strtr behaviour of going through source string once -    $cmd = strtr($cmd, $replacements); +        // use strtr behaviour of going through source string once +        $cmd = strtr($cmd, $replacements); -    return (string)shell_exec($cmd); -  } +        return (string)shell_exec($cmd); +    }      /** diff --git a/program/include/rcube_bc.inc b/program/include/rcube_bc.inc index 1932f86e2..1894873e6 100644 --- a/program/include/rcube_bc.inc +++ b/program/include/rcube_bc.inc @@ -38,11 +38,6 @@ function get_table_name($table)      return rcmail::get_instance()->db->table_name($table);  } -function get_sequence_name($sequence) -{ -    return rcmail::get_instance()->db->sequence_name($sequence); -} -  function rcube_label($p, $domain=null)  {      return rcmail::get_instance()->gettext($p, $domain); diff --git a/program/include/rcube_browser.php b/program/include/rcube_browser.php index 06033e036..7cfae709d 100644 --- a/program/include/rcube_browser.php +++ b/program/include/rcube_browser.php @@ -20,8 +20,6 @@  */  /** - * rcube_browser - *   * Provide details about the client's browser based on the User-Agent header   *   * @package Core diff --git a/program/include/rcube_cache.php b/program/include/rcube_cache.php index cdb1dd52f..4e60deaff 100644 --- a/program/include/rcube_cache.php +++ b/program/include/rcube_cache.php @@ -254,7 +254,7 @@ class rcube_cache              }              else if ($this->type == 'apc') {                  $data = apc_fetch($this->ckey($key)); -	        } +            }              if ($data) {                  $md5sum = md5($data); @@ -294,7 +294,7 @@ class rcube_cache                  }                  $this->cache[$key]      = $data; -	            $this->cache_sums[$key] = $md5sum; +                $this->cache_sums[$key] = $md5sum;              }              else {                  $this->cache[$key] = null; diff --git a/program/include/rcube_charset.php b/program/include/rcube_charset.php index 380d14978..1740a6096 100644 --- a/program/include/rcube_charset.php +++ b/program/include/rcube_charset.php @@ -181,6 +181,12 @@ class rcube_charset          $to   = empty($to) ? strtoupper(RCMAIL_CHARSET) : self::parse_charset($to);          $from = self::parse_charset($from); +        // It is a common case when UTF-16 charset is used with US-ASCII content (#1488654) +        // In that case we can just skip the conversion (use UTF-8) +        if ($from == 'UTF-16' && !preg_match('/[^\x00-\x7F]/', $str)) { +            $from = 'UTF-8'; +        } +          if ($from == $to || empty($str) || empty($from)) {              return $str;          } diff --git a/program/include/rcube_config.php b/program/include/rcube_config.php index 2fe0d9745..41acc80dd 100644 --- a/program/include/rcube_config.php +++ b/program/include/rcube_config.php @@ -42,6 +42,7 @@ class rcube_config          'default_folders'      => 'default_imap_folders',          'mail_pagesize'        => 'pagesize',          'addressbook_pagesize' => 'pagesize', +        'reply_mode'           => 'top_posting',      ); @@ -324,7 +325,7 @@ class rcube_config          if (strlen($key) != 24) {              rcube::raise_error(array(                  'code' => 500, 'type' => 'php', -	            'file' => __FILE__, 'line' => __LINE__, +                'file' => __FILE__, 'line' => __LINE__,                  'message' => "Configured crypto key '$key' is not exactly 24 bytes long"              ), true, true);          } @@ -348,7 +349,7 @@ class rcube_config              else                  rcube::raise_error(array(                      'code' => 500, 'type' => 'php', -	                'file' => __FILE__, 'line' => __LINE__, +                    'file' => __FILE__, 'line' => __LINE__,                      'message' => "Invalid mail_header_delimiter setting"                  ), true, false);          } diff --git a/program/include/rcube_db.php b/program/include/rcube_db.php index 042ca15e4..f97d70ab3 100644 --- a/program/include/rcube_db.php +++ b/program/include/rcube_db.php @@ -576,6 +576,10 @@ class rcube_db              return intval($input);          } +        if (is_null($input)) { +            return 'NULL'; +        } +          // create DB handle if not available          if (!$this->dbh) {              $this->db_connect('r'); diff --git a/program/include/rcube_imap.php b/program/include/rcube_imap.php index 4ab06cf60..66b5c4bd6 100644 --- a/program/include/rcube_imap.php +++ b/program/include/rcube_imap.php @@ -359,11 +359,11 @@ class rcube_imap extends rcube_storage          return array(              $this->search_string, -	        $this->search_set, -        	$this->search_charset, -        	$this->search_sort_field, -        	$this->search_sorted, -	    ); +            $this->search_set, +            $this->search_charset, +            $this->search_sort_field, +            $this->search_sorted, +        );      } @@ -2138,14 +2138,17 @@ class rcube_imap extends rcube_storage      /**       * Sends the whole message source to stdout +     * +     * @param int  $uid       Message UID +     * @param bool $formatted Enables line-ending formatting       */ -    public function print_raw_body($uid) +    public function print_raw_body($uid, $formatted = true)      {          if (!$this->check_connection()) {              return;          } -        $this->conn->handlePartBody($this->folder, $uid, true, NULL, NULL, true); +        $this->conn->handlePartBody($this->folder, $uid, true, null, null, true, null, $formatted);      } @@ -2219,6 +2222,10 @@ class rcube_imap extends rcube_storage              $folder = $this->folder;          } +        if (!$this->check_connection()) { +            return false; +        } +          // make sure folder exists          if ($this->folder_exists($folder)) {              if ($is_file) { @@ -3847,12 +3854,12 @@ class rcube_imap extends rcube_storage      protected function rsort($folder, $delimiter, &$list, &$out)      {          while (list($key, $name) = each($list)) { -	        if (strpos($name, $folder.$delimiter) === 0) { -	            // set the type of folder name variable (#1485527) -    	        $out[] = (string) $name; -	            unset($list[$key]); -	            $this->rsort($name, $delimiter, $list, $out); -	        } +            if (strpos($name, $folder.$delimiter) === 0) { +                // set the type of folder name variable (#1485527) +                $out[] = (string) $name; +                unset($list[$key]); +                $this->rsort($name, $delimiter, $list, $out); +            }          }          reset($list);      } diff --git a/program/include/rcube_imap_cache.php b/program/include/rcube_imap_cache.php index a061a1f6e..f36ace0eb 100644 --- a/program/include/rcube_imap_cache.php +++ b/program/include/rcube_imap_cache.php @@ -5,7 +5,7 @@   | program/include/rcube_imap_cache.php                                  |   |                                                                       |   | This file is part of the Roundcube Webmail client                     | - | Copyright (C) 2005-2011, The Roundcube Dev Team                       | + | Copyright (C) 2005-2012, The Roundcube Dev Team                       |   |                                                                       |   | Licensed under the GNU General Public License version 3 or            |   | any later version with exceptions for skins & plugins.                | @@ -350,11 +350,11 @@ class rcube_imap_cache      function get_message($mailbox, $uid, $update = true, $cache = true)      {          // Check internal cache -        if ($this->icache['message'] -            && $this->icache['message']['mailbox'] == $mailbox -            && $this->icache['message']['object']->uid == $uid +        if ($this->icache['__message'] +            && $this->icache['__message']['mailbox'] == $mailbox +            && $this->icache['__message']['object']->uid == $uid          ) { -            return $this->icache['message']['object']; +            return $this->icache['__message']['object'];          }          $sql_result = $this->db->query( @@ -386,7 +386,7 @@ class rcube_imap_cache              // Save current message from internal cache              $this->save_icache(); -            $this->icache['message'] = array( +            $this->icache['__message'] = array(                  'object'  => $message,                  'mailbox' => $mailbox,                  'exists'  => $found, @@ -459,20 +459,28 @@ class rcube_imap_cache       */      function change_flag($mailbox, $uids, $flag, $enabled = false)      { +        if (empty($uids)) { +            return; +        } +          $flag = strtoupper($flag);          $idx  = (int) array_search($flag, $this->flags); +        $uids = (array) $uids;          if (!$idx) {              return;          }          // Internal cache update -        if ($uids && count($uids) == 1 && ($uid = current($uids)) -            && ($message = $this->icache['message']) -            && $message['mailbox'] == $mailbox && $message['object']->uid == $uid +        if (($message = $this->icache['__message']) +            && $message['mailbox'] === $mailbox +            && in_array($message['object']->uid, $uids)          ) {              $message['object']->flags[$flag] = $enabled; -            return; + +            if (count($uids) == 1) { +                return; +            }          }          $this->db->query( @@ -481,7 +489,7 @@ class rcube_imap_cache              .", flags = flags ".($enabled ? "+ $idx" : "- $idx")              ." WHERE user_id = ?"                  ." AND mailbox = ?" -                .($uids !== null ? " AND uid IN (".$this->db->array2list((array)$uids, 'integer').")" : "") +                .($uids !== null ? " AND uid IN (".$this->db->array2list($uids, 'integer').")" : "")                  ." AND (flags & $idx) ".($enabled ? "= 0" : "= $idx"),              $this->userid, $mailbox);      } @@ -503,10 +511,11 @@ class rcube_imap_cache          }          else {              // Remove the message from internal cache -            if (!empty($uids) && !is_array($uids) && ($message = $this->icache['message']) -                && $message['mailbox'] == $mailbox && $message['object']->uid == $uids +            if (!empty($uids) && ($message = $this->icache['__message']) +                && $message['mailbox'] === $mailbox +                && in_array($message['object']->uid, (array)$uids)              ) { -                $this->icache['message'] = null; +                $this->icache['__message'] = null;              }              $this->db->query( @@ -608,13 +617,13 @@ class rcube_imap_cache          // get expiration timestamp          $ts = get_offset_time($ttl, -1); -        $this->db->query("DELETE FROM ".get_table_name('cache_messages') +        $this->db->query("DELETE FROM ".$this->db->table_name('cache_messages')                ." WHERE changed < " . $this->db->fromunixtime($ts)); -        $this->db->query("DELETE FROM ".get_table_name('cache_index') +        $this->db->query("DELETE FROM ".$this->db->table_name('cache_index')                ." WHERE changed < " . $this->db->fromunixtime($ts)); -        $this->db->query("DELETE FROM ".get_table_name('cache_thread') +        $this->db->query("DELETE FROM ".$this->db->table_name('cache_thread')                ." WHERE changed < " . $this->db->fromunixtime($ts));      } @@ -763,6 +772,11 @@ class rcube_imap_cache          $object    = $index['object'];          $is_thread = is_a($object, 'rcube_result_thread'); +        // sanity check +        if (empty($object)) { +            return false; +        } +          // Get mailbox data (UIDVALIDITY, counters, etc.) for status check          $mbox_data = $this->imap->folder_data($mailbox); @@ -1078,7 +1092,7 @@ class rcube_imap_cache      private function save_icache()      {          // Save current message from internal cache -        if ($message = $this->icache['message']) { +        if ($message = $this->icache['__message']) {              // clean up some object's data              $object = $this->message_object_prepare($message['object']); @@ -1089,7 +1103,7 @@ class rcube_imap_cache                  $this->add_message($message['mailbox'], $object, !$message['exists']);              } -            $this->icache['message']['md5sum'] = $md5sum; +            $this->icache['__message']['md5sum'] = $md5sum;          }      } diff --git a/program/include/rcube_imap_generic.php b/program/include/rcube_imap_generic.php index 8d956f2b9..25e6fc421 100644 --- a/program/include/rcube_imap_generic.php +++ b/program/include/rcube_imap_generic.php @@ -530,6 +530,7 @@ class rcube_imap_generic                  }                  else {                      $authc = $user; +                    $user  = '';                  }                  $auth_sasl = Auth_SASL::factory('digestmd5');                  $reply = base64_encode($auth_sasl->getResponse($authc, $pass, @@ -568,6 +569,7 @@ class rcube_imap_generic              }              else {                  $authc = $user; +                $user  = '';              }              $reply = base64_encode($user . chr(0) . $authc . chr(0) . $pass); @@ -2377,7 +2379,7 @@ class rcube_imap_generic          return $this->handlePartBody($mailbox, $id, $is_uid, $part);      } -    function handlePartBody($mailbox, $id, $is_uid=false, $part='', $encoding=NULL, $print=NULL, $file=NULL) +    function handlePartBody($mailbox, $id, $is_uid=false, $part='', $encoding=NULL, $print=NULL, $file=NULL, $formatted=true)      {          if (!$this->select($mailbox)) {              return false; @@ -2494,7 +2496,7 @@ class rcube_imap_generic                          continue;                      $line = convert_uudecode($line);                  // default -                } else { +                } else if ($formatted) {                      $line = rtrim($line, "\t\r\n\0\x0B") . "\n";                  } @@ -2538,7 +2540,7 @@ class rcube_imap_generic      {          unset($this->data['APPENDUID']); -        if (!$mailbox) { +        if ($mailbox === null || $mailbox === '') {              return false;          } @@ -2603,7 +2605,7 @@ class rcube_imap_generic      {          unset($this->data['APPENDUID']); -        if (!$mailbox) { +        if ($mailbox === null || $mailbox === '') {              return false;          } @@ -2612,6 +2614,7 @@ class rcube_imap_generic          if (file_exists(realpath($path))) {              $in_fp = fopen($path, 'r');          } +          if (!$in_fp) {              $this->setError(self::ERROR_UNKNOWN, "Couldn't open $path for reading");              return false; diff --git a/program/include/rcube_ldap.php b/program/include/rcube_ldap.php index 3a7fc1805..ad2ccddeb 100644 --- a/program/include/rcube_ldap.php +++ b/program/include/rcube_ldap.php @@ -139,6 +139,11 @@ class rcube_ldap extends rcube_addressbook                      unset($this->coltypes[$childcol]);  // remove address child col from global coltypes list                  }              } + +            // at least one address type must be specified +            if (empty($this->coltypes['address']['subtypes'])) { +                $this->coltypes['address']['subtypes'] = array('home'); +            }          }          else if ($this->coltypes['address']) {              $this->coltypes['address'] += array('type' => 'textarea', 'childs' => null, 'size' => 40); @@ -767,9 +772,9 @@ class rcube_ldap extends rcube_addressbook          }          // use VLV pseudo-search for autocompletion -        $rcmail = rcmail::get_instance(); +        $rcube = rcube::get_instance(); -        if ($this->prop['vlv_search'] && $this->conn && join(',', (array)$fields) == join(',', $rcmail->config->get('contactlist_fields'))) +        if ($this->prop['vlv_search'] && $this->conn && join(',', (array)$fields) == join(',', $rcube->config->get('contactlist_fields')))          {              // add general filter to query              if (!empty($this->prop['filter']) && empty($this->filter)) @@ -2027,12 +2032,12 @@ class rcube_ldap extends rcube_addressbook          # a0 = type context-specific/constructed with a length of 06 (6) bytes following          # 02 = type integer with 2 bytes following (offset): 01 01 (ie 1)          # 02 = type integer with 2 bytes following (contentCount):  01 00 -         +          # whith a search string present:          # 81 = type context-specific/constructed with a length of 04 (4) bytes following (the length will change here)          # 81 indicates a user string is present where as a a0 indicates just a offset search          # 81 = type context-specific/constructed with a length of 06 (6) bytes following -         +          # the following info was taken from the ISO/IEC 8825-1:2003 x.690 standard re: the          # encoding of integer values (note: these values are in          # two-complement form so since offset will never be negative bit 8 of the @@ -2042,7 +2047,7 @@ class rcube_ldap extends rcube_addressbook          # of the second (to the left of first octet) octet:          # a) shall not all be ones; and          # b) shall not all be zero -         +          if ($search)          {              $search = preg_replace('/[^-[:alpha:] ,.()0-9]+/', '', $search); @@ -2062,7 +2067,7 @@ class rcube_ldap extends rcube_addressbook              // now compute length over $str              $str = self::_ber_addseq($str, 'a0');          } -         +          // now tack on records per page          $str = "020100" . self::_ber_addseq(self::_ber_encode_int($rpp-1), '02') . $str; diff --git a/program/include/rcube_message.php b/program/include/rcube_message.php index 9d36acf38..6af1d0133 100644 --- a/program/include/rcube_message.php +++ b/program/include/rcube_message.php @@ -50,13 +50,14 @@ class rcube_message       */      private $mime;      private $opt = array(); -    private $inline_parts = array();      private $parse_alternative = false; -    public $uid = null; +    public $uid; +    public $folder;      public $headers;      public $parts = array();      public $mime_parts = array(); +    public $inline_parts = array();      public $attachments = array();      public $subject = '';      public $sender = null; @@ -68,17 +69,22 @@ class rcube_message       *       * Provide a uid, and parse message structure.       * -     * @param string $uid The message UID. +     * @param string $uid    The message UID. +     * @param string $folder Folder name       *       * @see self::$app, self::$storage, self::$opt, self::$parts       */ -    function __construct($uid) +    function __construct($uid, $folder = null)      {          $this->uid  = $uid;          $this->app  = rcube::get_instance();          $this->storage = $this->app->get_storage(); +        $this->folder  = strlen($folder) ? $folder : $this->storage->get_folder();          $this->storage->set_options(array('all_headers' => true)); +        // Set current folder +        $this->storage->set_folder($this->folder); +          $this->headers = $this->storage->get_message($uid);          if (!$this->headers) @@ -179,10 +185,12 @@ class rcube_message                  }                  return $fp ? true : $part->body;              } +              // get from IMAP +            $this->storage->set_folder($this->folder); +              return $this->storage->get_message_part($this->uid, $mime_id, $part, NULL, $fp, $skip_charset_conv); -        } else -            return null; +        }      } @@ -637,8 +645,10 @@ class rcube_message      function tnef_decode(&$part)      {          // @TODO: attachment may be huge, hadle it via file -        if (!isset($part->body)) +        if (!isset($part->body)) { +            $this->storage->set_folder($this->folder);              $part->body = $this->storage->get_message_part($this->uid, $part->mime_id, $part); +        }          $parts = array();          $tnef = new tnef_decoder; @@ -673,8 +683,10 @@ class rcube_message      function uu_decode(&$part)      {          // @TODO: messages may be huge, hadle body via file -        if (!isset($part->body)) +        if (!isset($part->body)) { +            $this->storage->set_folder($this->folder);              $part->body = $this->storage->get_message_part($this->uid, $part->mime_id, $part); +        }          $parts = array();          // FIXME: line length is max.65? diff --git a/program/include/rcube_mime.php b/program/include/rcube_mime.php index e1f736a78..d8e04a97c 100644 --- a/program/include/rcube_mime.php +++ b/program/include/rcube_mime.php @@ -541,10 +541,10 @@ class rcube_mime                      $prefix = $regs[0];                      $level = strlen($prefix);                      $line  = rtrim(substr($line, $level)); -                    $line  = $prefix . rc_wordwrap($line, $length - $level - 2, " \r\n$prefix "); +                    $line  = $prefix . self::wordwrap($line, $length - $level - 2, " \r\n$prefix ");                  }                  else if ($line) { -                    $line = rc_wordwrap(rtrim($line), $length - 2, " \r\n"); +                    $line = self::wordwrap(rtrim($line), $length - 2, " \r\n");                      // space-stuffing                      $line = preg_replace('/(^|\r\n)(From| |>)/', '\\1 \\2', $line);                  } diff --git a/program/include/rcube_output_html.php b/program/include/rcube_output_html.php index 30512d227..2743e7705 100644 --- a/program/include/rcube_output_html.php +++ b/program/include/rcube_output_html.php @@ -67,6 +67,11 @@ class rcube_output_html extends rcube_output          $this->set_env('task', $task);          $this->set_env('x_frame_options', $this->config->get('x_frame_options', 'sameorigin')); +        // add cookie info +        $this->set_env('cookie_domain', ini_get('session.cookie_domain')); +        $this->set_env('cookie_path', ini_get('session.cookie_path')); +        $this->set_env('cookie_secure', ini_get('session.cookie_secure')); +          // load the correct skin (in case user-defined)          $skin = $this->config->get('skin');          $this->set_skin($skin); @@ -395,7 +400,7 @@ class rcube_output_html extends rcube_output                  'line' => __LINE__,                  'file' => __FILE__,                  'message' => 'Error loading template for '.$realname -                ), true, true); +                ), true, $write);              return false;          } @@ -693,6 +698,11 @@ class rcube_output_html extends rcube_output                  }                  break; +            // frame +            case 'frame': +                return $this->frame($attrib); +                break; +              // show a label              case 'label':                  if ($attrib['name'] || $attrib['command']) { @@ -1270,6 +1280,30 @@ class rcube_output_html extends rcube_output      } +    /** +     * Returns iframe object, registers some related env variables +     * +     * @param array $attrib HTML attributes +     * +     * @return string IFRAME element +     */ +    public function frame($attrib) +    { +        if (!$attrib['id']) { +            $attrib['id'] = 'rcmframe'; +        } + +        if (!$attrib['name']) { +            $attrib['name'] = $attrib['id']; +        } + +        $this->set_env('contentframe', $attrib['id']); +        $this->set_env('blankpage', $attrib['src'] ? $this->abs_url($attrib['src']) : 'program/resources/blank.gif'); + +        return html::iframe($attrib); +    } + +      /*  ************* common functions delivering gui objects **************  */ @@ -1378,6 +1412,9 @@ class rcube_output_html extends rcube_output          if (empty($url) && !preg_match('/_(task|action)=logout/', $_SERVER['QUERY_STRING']))              $url = $_SERVER['QUERY_STRING']; +        // Disable autocapitalization on iPad/iPhone (#1488609) +        $attrib['autocapitalize'] = 'off'; +          // set atocomplete attribute          $user_attrib = $autocomplete > 0 ? array() : array('autocomplete' => 'off');          $host_attrib = $autocomplete > 0 ? array() : array('autocomplete' => 'off'); diff --git a/program/include/rcube_plugin.php b/program/include/rcube_plugin.php index b1ec32a8a..c1035733b 100644 --- a/program/include/rcube_plugin.php +++ b/program/include/rcube_plugin.php @@ -336,7 +336,7 @@ abstract class rcube_plugin    public function local_skin_path()    {      $rcmail = rcube::get_instance(); -    foreach (array($rcmail->config->get('skin'),'default') as $skin) { +    foreach (array($rcmail->config->get('skin'), 'larry') as $skin) {        $skin_path = 'skins/' . $skin;        if (is_dir(realpath(slashify($this->home) . $skin_path)))          break; diff --git a/program/include/rcube_plugin_api.php b/program/include/rcube_plugin_api.php index 8c1e125ce..9ef68cab9 100644 --- a/program/include/rcube_plugin_api.php +++ b/program/include/rcube_plugin_api.php @@ -32,12 +32,12 @@ if (!defined('RCMAIL_PLUGINS_DIR'))  class rcube_plugin_api  {    static private $instance; -   +    public $dir;    public $url = 'plugins/';    public $task = '';    public $output; -   +    public $handlers = array();    private $plugins = array();    private $tasks = array(); diff --git a/program/include/rcube_session.php b/program/include/rcube_session.php index b6a0ccf62..6192466cd 100644 --- a/program/include/rcube_session.php +++ b/program/include/rcube_session.php @@ -531,7 +531,7 @@ class rcube_session    public function set_keep_alive($keep_alive)    {      $this->keep_alive = $keep_alive; -     +      if ($this->lifetime < $keep_alive)          $this->set_lifetime($keep_alive + 30);    } @@ -551,7 +551,7 @@ class rcube_session    {      return $this->ip;    } -   +    /**     * Setter for cookie encryption secret     */ @@ -568,7 +568,8 @@ class rcube_session    {      $this->ip_check = $check;    } -   + +    /**     * Setter for the cookie name used for session cookie     */ @@ -605,7 +606,7 @@ class rcube_session            $result = true;          }        } -	} +    }      if (!$result)        $this->log("Session authentication failed for " . $this->key . "; invalid auth cookie sent; timeslot = " . date('Y-m-d H:i:s', $prev)); @@ -637,7 +638,7 @@ class rcube_session    }    /** -   *  +   * Writes debug information to the log     */    function log($line)    { diff --git a/program/include/rcube_shared.inc b/program/include/rcube_shared.inc index 85f278432..c15305c08 100644 --- a/program/include/rcube_shared.inc +++ b/program/include/rcube_shared.inc @@ -108,11 +108,11 @@ function slashify($str)  /** - * Remove slash at the end of the string + * Remove slashes at the end of the string   */  function unslashify($str)  { -  return preg_replace('/\/$/', '', $str); +  return preg_replace('/\/+$/', '', $str);  } @@ -231,7 +231,7 @@ function array_keys_recursive($array)  {      $keys = array(); -    if (!empty($array)) { +    if (!empty($array) && is_array($array)) {          foreach ($array as $key => $child) {              $keys[] = $key;              foreach (array_keys_recursive($child) as $val) { @@ -255,7 +255,7 @@ function asciiwords($str, $css_id = false, $replace_with = '')  /** - * Remove single and double quotes from given string + * Remove single and double quotes from a given string   *   * @param string Input value   * @@ -306,6 +306,29 @@ function format_email_recipient($email, $name = '')  /** + * Format e-mail address + * + * @param string $email E-mail address + * + * @return string Formatted e-mail address + */ +function format_email($email) +{ +    $email = trim($email); +    $parts = explode('@', $email); +    $count = count($parts); + +    if ($count > 1) { +        $parts[$count-1] = mb_strtolower($parts[$count-1]); + +        $email = implode('@', $parts); +    } + +    return $email; +} + + +/**   * mbstring replacement functions   */  if (!extension_loaded('mbstring')) @@ -399,7 +422,6 @@ function rcube_autoload($classname)  {      $filename = preg_replace(          array( -            '/MDB2_(.+)/',              '/Mail_(.+)/',              '/Net_(.+)/',              '/Auth_(.+)/', @@ -408,7 +430,6 @@ function rcube_autoload($classname)          ),          array(              'Mail/\\1', -            'Mail/\\1',              'Net/\\1',              'Auth/\\1',              'html', diff --git a/program/include/rcube_smtp.php b/program/include/rcube_smtp.php index e5748248c..b28be5206 100644 --- a/program/include/rcube_smtp.php +++ b/program/include/rcube_smtp.php @@ -423,7 +423,7 @@ class rcube_smtp          $lines[] = $key . ': ' . $value;        }      } -     +      return array($from, join(SMTP_MIME_CRLF, $lines) . SMTP_MIME_CRLF);    } diff --git a/program/include/rcube_storage.php b/program/include/rcube_storage.php index 1075b0f43..f83e24041 100644 --- a/program/include/rcube_storage.php +++ b/program/include/rcube_storage.php @@ -195,7 +195,7 @@ abstract class rcube_storage       */      public function set_folder($folder)      { -        if ($this->folder == $folder) { +        if ($this->folder === $folder) {              return;          } @@ -502,8 +502,11 @@ abstract class rcube_storage      /**       * Sends the whole message source to stdout +     * +     * @param int  $uid       Message UID +     * @param bool $formatted Enables line-ending formatting       */ -    abstract function print_raw_body($uid); +    abstract function print_raw_body($uid, $formatted = true);      /** diff --git a/program/include/rcube_user.php b/program/include/rcube_user.php index 644d24bd9..29eb0f26e 100644 --- a/program/include/rcube_user.php +++ b/program/include/rcube_user.php @@ -443,7 +443,7 @@ class rcube_user          }          $data = $rcube->plugins->exec_hook('user_create', -	        array('user'=>$user, 'user_name'=>$user_name, 'user_email'=>$user_email, 'host'=>$host)); +            array('user'=>$user, 'user_name'=>$user_name, 'user_email'=>$user_email, 'host'=>$host));          // plugin aborted this operation          if ($data['abort']) diff --git a/program/include/rcube_utils.php b/program/include/rcube_utils.php index 8762a2018..9bedf2108 100644 --- a/program/include/rcube_utils.php +++ b/program/include/rcube_utils.php @@ -84,17 +84,17 @@ class rcube_utils          // from PEAR::Validate          $regexp = '&^(?: -	        ("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+")| 			 	            #1 quoted name -	        ([-\w!\#\$%\&\'*+~/^`|{}=]+(?:\.[-\w!\#\$%\&\'*+~/^`|{}=]+)*)) 	#2 OR dot-atom (RFC5322) -	        $&xi'; +            ("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+")|                             #1 quoted name +            ([-\w!\#\$%\&\'*+~/^`|{}=]+(?:\.[-\w!\#\$%\&\'*+~/^`|{}=]+)*))  #2 OR dot-atom (RFC5322) +            $&xi';          if (!preg_match($regexp, $local_part)) {              return false;          } -        // Check domain part -        if (preg_match('/^\[*(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}\]*$/', $domain_part)) { -            return true; // IP address +        // Validate domain part +        if (preg_match('/^\[((IPv6:[0-9a-f:.]+)|([0-9.]+))\]$/i', $domain_part, $matches)) { +            return self::check_ip(preg_replace('/^IPv6:/i', '', $matches[1])); // valid IPv4 or IPv6 address          }          else {              // If not an IP address @@ -110,6 +110,11 @@ class rcube_utils                  }              } +            // last domain part +            if (preg_match('/[^a-zA-Z]/', array_pop($domain_array))) { +                return false; +            } +              $rcube = rcube::get_instance();              if (!$dns_check || !$rcube->config->get('email_dns_check')) { @@ -141,6 +146,52 @@ class rcube_utils          return false;      } + +    /** +     * Validates IPv4 or IPv6 address +     * +     * @param string $ip IP address in v4 or v6 format +     * +     * @return bool True if the address is valid +     */ +    public static function check_ip($ip) +    { +        // IPv6, but there's no build-in IPv6 support +        if (strpos($ip, ':') !== false && !defined('AF_INET6')) { +            $parts = explode(':', $domain_part); +            $count = count($parts); + +            if ($count > 8 || $count < 2) { +                return false; +            } + +            foreach ($parts as $idx => $part) { +                $length = strlen($part); +                if (!$length) { +                    // there can be only one :: +                    if ($found_empty) { +                        return false; +                    } +                    $found_empty = true; +                } +                // last part can be an IPv4 address +                else if ($idx == $count - 1) { +                    if (!preg_match('/^[0-9a-f]{1,4}$/i', $part)) { +                        return @inet_pton($part) !== false; +                    } +                } +                else if (!preg_match('/^[0-9a-f]{1,4}$/i', $part)) { +                    return false; +                } +            } + +            return true; +        } + +        return @inet_pton($ip) !== false; +    } + +      /**       * Check whether the HTTP referer matches the current request       * @@ -149,8 +200,8 @@ class rcube_utils      public static function check_referer()      {          $uri = parse_url($_SERVER['REQUEST_URI']); -        $referer = parse_url(rcube_request_header('Referer')); -        return $referer['host'] == rcube_request_header('Host') && $referer['path'] == $uri['path']; +        $referer = parse_url(self::request_header('Referer')); +        return $referer['host'] == self::request_header('Host') && $referer['path'] == $uri['path'];      } diff --git a/program/include/rcube_vcard.php b/program/include/rcube_vcard.php index 37cd3ab26..49b312c5c 100644 --- a/program/include/rcube_vcard.php +++ b/program/include/rcube_vcard.php @@ -313,7 +313,7 @@ class rcube_vcard        case 'birthday':        case 'anniversary': -        if (($val = rcube_strtotime($value)) && ($fn = self::$fieldmap[$field])) +        if (($val = rcube_utils::strtotime($value)) && ($fn = self::$fieldmap[$field]))            $this->raw[$fn][] = array(0 => date('Y-m-d', $val), 'value' => array('date'));          break; @@ -555,6 +555,7 @@ class rcube_vcard            if ((list($key, $value) = explode('=', $attr)) && $value) {              $value = trim($value);              if ($key == 'ENCODING') { +              $value = strtoupper($value);                // add next line(s) to value string if QP line end detected                if ($value == 'QUOTED-PRINTABLE') {                  while (preg_match('/=$/', $lines[$i])) diff --git a/program/js/app.js b/program/js/app.js index 214a5cb80..48de21764 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -976,7 +976,7 @@ function rcube_webmail()              // do reply-list, when list is detected and popup menu wasn't used               url._all = (!props && this.commands['reply-list'] ? 'list' : 'all');            else if (command == 'reply-list') -            url._all = list; +            url._all = 'list';            this.goto_url('compose', url, true);          } @@ -1447,29 +1447,21 @@ function rcube_webmail()    this.doc_mouse_up = function(e)    { -    var model, list, li, id; +    var model, list, id;      // ignore event if jquery UI dialog is open      if ($(rcube_event.get_target(e)).closest('.ui-dialog, .ui-widget-overlay').length)        return; -    if (list = this.message_list) { -      if (!rcube_mouse_is_over(e, list.list.parentNode)) -        list.blur(); -      else -        list.focus(); +    if (list = this.message_list)        model = this.env.mailboxes; -    } -    else if (list = this.contact_list) { -      if (!rcube_mouse_is_over(e, list.list.parentNode)) -        list.blur(); -      else -        list.focus(); +    else if (list = this.contact_list)        model = this.env.contactfolders; -    } -    else if (this.ksearch_value) { +    else if (this.ksearch_value)        this.ksearch_blur(); -    } + +    if (list && !rcube_mouse_is_over(e, list.list.parentNode)) +      list.blur();      // handle mouse release when dragging      if (this.drag_active && model && this.env.last_folder_target) { @@ -1546,14 +1538,17 @@ function rcube_webmail()      if (list.multi_selecting || !this.env.contentframe)        return; -    if (list.get_single_selection() && window.frames && window.frames[this.env.contentframe]) { -      if (window.frames[this.env.contentframe].location.href.indexOf(this.env.blankpage)>=0) { -        if (this.preview_timer) -          clearTimeout(this.preview_timer); -        if (this.preview_read_timer) -          clearTimeout(this.preview_read_timer); -        this.preview_timer = setTimeout(function(){ ref.msglist_get_preview(); }, 200); -      } +    if (list.get_single_selection()) +      return; + +    var win = this.get_frame_window(this.env.contentframe); + +    if (win && win.location.href.indexOf(this.env.blankpage)>=0) { +      if (this.preview_timer) +        clearTimeout(this.preview_timer); +      if (this.preview_read_timer) +        clearTimeout(this.preview_read_timer); +      this.preview_timer = setTimeout(function(){ ref.msglist_get_preview(); }, 200);      }    }; @@ -1918,12 +1913,12 @@ function rcube_webmail()      if (!id)        return; -    var target = window, +    var win, target = window,        action = preview ? 'preview': 'show',        url = '&_action='+action+'&_uid='+id+'&_mbox='+urlencode(this.env.mailbox); -    if (preview && this.env.contentframe && window.frames && window.frames[this.env.contentframe]) { -      target = window.frames[this.env.contentframe]; +    if (preview && (win = this.get_frame_window(this.env.contentframe))) { +      target = win;        url += '&_framed=1';      } @@ -1960,20 +1955,37 @@ function rcube_webmail()    this.show_contentframe = function(show)    { -    var frm, win; -    if (this.env.contentframe && (frm = $('#'+this.env.contentframe)) && frm.length) { -      if (!show && (win = window.frames[this.env.contentframe])) { +    var frame, win, name = this.env.contentframe; + +    if (name && (frame = this.get_frame_element(name))) { +      if (!show && (win = this.get_frame_window(name))) {          if (win.location && win.location.href.indexOf(this.env.blankpage)<0)            win.location.href = this.env.blankpage;        }        else if (!bw.safari && !bw.konq) -        frm[show ? 'show' : 'hide'](); -      } +        $(frame)[show ? 'show' : 'hide'](); +    }      if (!show && this.busy)        this.set_busy(false, null, this.env.frame_lock);    }; +  this.get_frame_element = function(id) +  { +    var frame; + +    if (id && (frame = document.getElementById(id))) +      return frame; +  }; + +  this.get_frame_window = function(id) +  { +    var frame = this.get_frame_element(id); + +    if (frame && frame.name && window.frames) +      return window.frames[frame.name]; +  }; +    this.lock_frame = function()    {      if (!this.env.frame_lock) @@ -2017,7 +2029,7 @@ function rcube_webmail()    // list messages of a specific mailbox    this.list_mailbox = function(mbox, page, sort, url)    { -    var target = window; +    var win, target = window;      if (typeof url != 'object')        url = {}; @@ -2056,8 +2068,8 @@ function rcube_webmail()        return;      } -    if (this.env.contentframe && window.frames && window.frames[this.env.contentframe]) { -      target = window.frames[this.env.contentframe]; +    if (win = this.get_frame_window(this.env.contentframe)) { +      target = win;        url._framed = 1;      } @@ -2652,34 +2664,37 @@ function rcube_webmail()    // set a specific flag to one or more messages    this.mark_message = function(flag, uid)    { -    var a_uids = [], r_uids = [], len, n, id, -      selection = this.message_list ? this.message_list.get_selection() : []; +    var a_uids = [], r_uids = [], len, n, id, selection, +      list = this.message_list;      if (uid)        a_uids[0] = uid;      else if (this.env.uid)        a_uids[0] = this.env.uid; -    else if (this.message_list) { +    else if (list) { +      selection = list.get_selection();        for (n=0, len=selection.length; n<len; n++) {            a_uids.push(selection[n]);        }      } -    if (!this.message_list) +    if (!list)        r_uids = a_uids; -    else +    else { +      list.focus();        for (n=0, len=a_uids.length; n<len; n++) {          id = a_uids[n]; -        if ((flag=='read' && this.message_list.rows[id].unread)  -            || (flag=='unread' && !this.message_list.rows[id].unread) -            || (flag=='delete' && !this.message_list.rows[id].deleted) -            || (flag=='undelete' && this.message_list.rows[id].deleted) -            || (flag=='flagged' && !this.message_list.rows[id].flagged) -            || (flag=='unflagged' && this.message_list.rows[id].flagged)) +        if ((flag=='read' && list.rows[id].unread)  +            || (flag=='unread' && !list.rows[id].unread) +            || (flag=='delete' && !list.rows[id].deleted) +            || (flag=='undelete' && list.rows[id].deleted) +            || (flag=='flagged' && !list.rows[id].flagged) +            || (flag=='unflagged' && list.rows[id].flagged))          {            r_uids.push(id);          }        } +    }      // nothing to do      if (!r_uids.length && !this.select_all_mode) @@ -3298,8 +3313,7 @@ function rcube_webmail()        input_message = $("[name='_message']"),        message = input_message.val(),        is_html = ($("input[name='_is_html']").val() == '1'), -      sig = this.env.identity, -      sig_separator = this.env.sig_above && (this.env.compose_mode == 'reply' || this.env.compose_mode == 'forward') ? '---' : '-- '; +      sig = this.env.identity;      // enable manual signature insert      if (this.env.signatures && this.env.signatures[id]) { @@ -3312,25 +3326,18 @@ function rcube_webmail()      if (!is_html) {        // remove the 'old' signature        if (show_sig && sig && this.env.signatures && this.env.signatures[sig]) { - -        sig = this.env.signatures[sig].is_html ? this.env.signatures[sig].plain_text : this.env.signatures[sig].text; +        sig = this.env.signatures[sig].text;          sig = sig.replace(/\r\n/g, '\n'); -        if (!sig.match(/^--[ -]\n/m)) -          sig = sig_separator + '\n' + sig; -          p = this.env.sig_above ? message.indexOf(sig) : message.lastIndexOf(sig);          if (p >= 0)            message = message.substring(0, p) + message.substring(p+sig.length, message.length);        }        // add the new signature string        if (show_sig && this.env.signatures && this.env.signatures[id]) { -        sig = this.env.signatures[id]['is_html'] ? this.env.signatures[id]['plain_text'] : this.env.signatures[id]['text']; +        sig = this.env.signatures[id].text;          sig = sig.replace(/\r\n/g, '\n'); -        if (!sig.match(/^--[ -]\n/m)) -          sig = sig_separator + '\n' + sig; -          if (this.env.sig_above) {            if (p >= 0) { // in place of removed signature              message = message.substring(0, p) + sig + message.substring(p, message.length); @@ -3394,21 +3401,8 @@ function rcube_webmail()          }        } -      if (this.env.signatures[id]) { -        if (this.env.signatures[id].is_html) { -          sig = this.env.signatures[id].text; -          if (!this.env.signatures[id].plain_text.match(/^--[ -]\r?\n/m)) -            sig = sig_separator + '<br />' + sig; -        } -        else { -          sig = this.env.signatures[id].text; -          if (!sig.match(/^--[ -]\r?\n/m)) -            sig = sig_separator + '\n' + sig; -          sig = '<pre>' + sig + '</pre>'; -        } - -        sigElem.innerHTML = sig; -      } +      if (this.env.signatures[id]) +        sigElem.innerHTML = this.env.signatures[id].html;      }      this.env.identity = id; @@ -4031,8 +4025,7 @@ function rcube_webmail()      // if a group is currently selected, and there is at least one contact selected      // thend we can enable the group-remove-selected command -    this.enable_command('group-remove-selected', typeof this.env.group != 'undefined' && list.selection.length > 0); - +    this.enable_command('group-remove-selected', this.env.group && list.selection.length > 0);      this.enable_command('compose', this.env.group || list.selection.length > 0);      this.enable_command('edit', id && writable);      this.enable_command('delete', list.selection.length && writable); @@ -4042,7 +4035,7 @@ function rcube_webmail()    this.list_contacts = function(src, group, page)    { -    var folder, url = {}, +    var win, folder, url = {},        target = window;      if (!src) @@ -4074,8 +4067,8 @@ function rcube_webmail()        return;      } -    if (this.env.contentframe && window.frames && window.frames[this.env.contentframe]) { -      target = window.frames[this.env.contentframe]; +    if (win = this.get_frame_window(this.env.contentframe)) { +      target = win;        url._framed = 1;      } @@ -4131,11 +4124,11 @@ function rcube_webmail()    // load contact record    this.load_contact = function(cid, action, framed)    { -    var url = {}, target = window; +    var win, url = {}, target = window; -    if (this.env.contentframe && window.frames && window.frames[this.env.contentframe]) { +    if (win = this.get_frame_window(this.env.contentframe)) {        url._framed = 1; -      target = window.frames[this.env.contentframe]; +      target = win;        this.show_contentframe(true);        // load dummy content @@ -4753,11 +4746,11 @@ function rcube_webmail()    // load advanced search page    this.advanced_search = function()    { -    var url = {_form: 1, _action: 'search'}, target = window; +    var win, url = {_form: 1, _action: 'search'}, target = window; -    if (this.env.contentframe && window.frames && window.frames[this.env.contentframe]) { +    if (win = this.get_frame_window(this.env.contentframe)) {        url._framed = 1; -      target = window.frames[this.env.contentframe]; +      target = win;        this.contact_list.clear_selection();      } @@ -4879,13 +4872,13 @@ function rcube_webmail()    // preferences section select and load options frame    this.section_select = function(list)    { -    var id = list.get_single_selection(), target = window, +    var win, id = list.get_single_selection(), target = window,        url = {_action: 'edit-prefs', _section: id};      if (id) { -      if (this.env.contentframe && window.frames && window.frames[this.env.contentframe]) { +      if (win = this.get_frame_window(this.env.contentframe)) {          url._framed = 1; -        target = window.frames[this.env.contentframe]; +        target = win;        }        this.location_href(url, target, true);      } @@ -4908,13 +4901,12 @@ function rcube_webmail()      if (action == 'edit-identity' && (!id || id == this.env.iid))        return false; -    var target = window, +    var win, target = window,        url = {_action: action, _iid: id}; -    if (this.env.contentframe && window.frames && window.frames[this.env.contentframe]) { +    if (win = this.get_frame_window(this.env.contentframe)) {        url._framed = 1; -      target = window.frames[this.env.contentframe]; -      document.getElementById(this.env.contentframe).style.visibility = 'inherit'; +      target = win;      }      if (action && (id || action == 'add-identity')) { @@ -5290,14 +5282,14 @@ function rcube_webmail()    // when user select a folder in manager    this.show_folder = function(folder, path, force)    { -    var target = window, +    var win, target = window,        url = '&_action=edit-folder&_mbox='+urlencode(folder);      if (path)        url += '&_path='+urlencode(path); -    if (this.env.contentframe && window.frames && window.frames[this.env.contentframe]) { -      target = window.frames[this.env.contentframe]; +    if (win = this.get_frame_window(this.env.contentframe)) { +      target = win;        url += '&_framed=1';      } @@ -6607,6 +6599,12 @@ function rcube_webmail()      return 0;    }; +  // Cookie setter +  this.set_cookie = function(name, value, expires) +  { +    setCookie(name, value, expires, this.env.cookie_path, this.env.cookie_domain, this.env.cookie_secure); +  } +  }  // end object rcube_webmail @@ -6637,6 +6635,8 @@ rcube_webmail.long_subject_title_ie = function(elem, indent)    }  }; +rcube_webmail.prototype.get_cookie = getCookie; +  // copy event engine prototype  rcube_webmail.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;  rcube_webmail.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener; diff --git a/program/js/common.js b/program/js/common.js index fdef3453e..f9e945c05 100644 --- a/program/js/common.js +++ b/program/js/common.js @@ -494,12 +494,15 @@ function rcube_check_email(input, inline)        atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+',        quoted_pair = '\\x5c[\\x00-\\x7f]',        quoted_string = '\\x22('+qtext+'|'+quoted_pair+')*\\x22', +      ipv4 = '\\[(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}\\]', +      ipv6 = '\\[IPv6:[0-9a-f:.]+\\]', +      ip_addr = '(' + ipv4 + ')|(' + ipv6 + ')',        // Use simplified domain matching, because we need to allow Unicode characters here        // So, e-mail address should be validated also on server side after idn_to_ascii() use        //domain_literal = '\\x5b('+dtext+'|'+quoted_pair+')*\\x5d',        //sub_domain = '('+atom+'|'+domain_literal+')',        // allow punycode/unicode top-level domain -      domain = '([^@\\x2e]+\\x2e)+([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,})', +      domain = '(('+ip_addr+')|(([^@\\x2e]+\\x2e)+([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,})))',        // ICANN e-mail test (http://idn.icann.org/E-mail_test)        icann_domains = [          '\\u0645\\u062b\\u0627\\u0644\\x2e\\u0625\\u062e\\u062a\\u0628\\u0627\\u0631', @@ -527,7 +530,6 @@ function rcube_check_email(input, inline)    return false;  }; -  // recursively copy an object  function rcube_clone_object(obj)  { @@ -635,6 +637,7 @@ function getCookie(name)    return unescape(dc.substring(begin + prefix.length, end));  }; +// deprecated aliases, to be removed, use rcmail.set_cookie/rcmail.get_cookie  roundcube_browser.prototype.set_cookie = setCookie;  roundcube_browser.prototype.get_cookie = getCookie; diff --git a/program/js/googiespell.js b/program/js/googiespell.js index 9f1b41bb2..478858bac 100644 --- a/program/js/googiespell.js +++ b/program/js/googiespell.js @@ -25,7 +25,7 @@ var GOOGIE_CUR_LANG,  function GoogieSpell(img_dir, server_url, has_dict)  {      var ref = this, -        cookie_value = getCookie('language'); +        cookie_value = rcmail.get_cookie('language');      GOOGIE_CUR_LANG = cookie_value != null ? cookie_value : GOOGIE_DEFAULT_LANG; @@ -150,7 +150,7 @@ this.setCurrentLanguage = function(lan_code)      //Set cookie      var now = new Date();      now.setTime(now.getTime() + 365 * 24 * 60 * 60 * 1000); -    setCookie('language', lan_code, now); +    rcmail.set_cookie('language', lan_code, now);  };  this.setForceWidthHeight = function(width, height) diff --git a/program/js/list.js b/program/js/list.js index e84124b7c..1457382a4 100644 --- a/program/js/list.js +++ b/program/js/list.js @@ -231,8 +231,8 @@ focus: function(e)      }    } -  // Un-focus already focused elements -  $(document.activeElement).blur(); +  // Un-focus already focused elements (#1487123, #1487316, #1488600, #1488620) +  $(':focus:not(body)').blur();    $('iframe').each(function() { this.blur(); });    if (e || (e = window.event)) diff --git a/program/lib/html2text.php b/program/lib/html2text.php index 9de2e961e..28c5ae059 100644 --- a/program/lib/html2text.php +++ b/program/lib/html2text.php @@ -89,7 +89,7 @@   *  out that extra spaces should be compressed--a problem addressed with   *  Marcus Bointon's fixes but that I had not yet incorporated.   * - *	Thanks to Daniel Schledermann (http://www.typoconsult.dk/) for + *  Thanks to Daniel Schledermann (http://www.typoconsult.dk/) for   *  suggesting a valuable fix with <a> tag handling.   *   *  Thanks to Wojciech Bajon (again!) for suggesting fixes and additions, @@ -200,7 +200,7 @@ class html2text      var $ent_search = array(          '/&(nbsp|#160);/i',                      // Non-breaking space          '/&(quot|rdquo|ldquo|#8220|#8221|#147|#148);/i', -		                                         // Double quotes +                                         // Double quotes          '/&(apos|rsquo|lsquo|#8216|#8217);/i',   // Single quotes          '/>/i',                               // Greater-than          '/</i',                               // Less-than @@ -437,11 +437,11 @@ class html2text      function set_base_url( $url = '' )      {          if ( empty($url) ) { -        	if ( !empty($_SERVER['HTTP_HOST']) ) { -	            $this->url = 'http://' . $_SERVER['HTTP_HOST']; -        	} else { -	            $this->url = ''; -	        } +            if ( !empty($_SERVER['HTTP_HOST']) ) { +                $this->url = 'http://' . $_SERVER['HTTP_HOST']; +            } else { +                $this->url = ''; +            }          } else {              // Strip any trailing slashes for consistency (relative              // URLs may already start with a slash like "/file.html") @@ -515,7 +515,7 @@ class html2text          $text = preg_replace($this->ent_search, $this->ent_replace, $text);          // Replace known html entities -        $text = html_entity_decode($text, ENT_COMPAT, 'UTF-8'); +        $text = html_entity_decode($text, ENT_QUOTES, 'UTF-8');          // Remove unknown/unhandled entities (this cannot be done in search-and-replace block)          $text = preg_replace('/&([a-zA-Z0-9]{2,6}|#[0-9]{2,4});/', '', $text); @@ -535,7 +535,7 @@ class html2text          // for PHP versions >= 4.0.2. Default width is 75          // If width is 0 or less, don't wrap the text.          if ( $this->width > 0 ) { -        	$text = wordwrap($text, $this->width); +            $text = wordwrap($text, $this->width);          }      } @@ -554,16 +554,16 @@ class html2text       */      function _build_link_list( $link, $display )      { -	    if (!$this->_do_links || empty($link)) { -	        return $display; -	    } +        if (!$this->_do_links || empty($link)) { +            return $display; +        }          // Ignored link types -	    if (preg_match('!^(javascript:|mailto:|#)!i', $link)) { -		    return $display; +        if (preg_match('!^(javascript:|mailto:|#)!i', $link)) { +            return $display;          } -	    if (preg_match('!^([a-z][a-z0-9.+-]+:)!i', $link)) { +        if (preg_match('!^([a-z][a-z0-9.+-]+:)!i', $link)) {              $url = $link;          }          else { diff --git a/program/lib/washtml.php b/program/lib/washtml.php index c12315fec..98ae5ed5a 100644 --- a/program/lib/washtml.php +++ b/program/lib/washtml.php @@ -214,8 +214,11 @@ class washtml        $key = strtolower($key);        $value = $node->getAttribute($key);        if (isset($this->_html_attribs[$key]) || -         ($key == 'href' && preg_match('!^([a-z][a-z0-9.+-]+:|//|#).+!i', $value))) +         ($key == 'href' && !preg_match('!^javascript!i', $value) +           && preg_match('!^([a-z][a-z0-9.+-]+:|//|#).+!i', $value)) +      ) {          $t .= ' ' . $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"'; +      }        else if ($key == 'style' && ($style = $this->wash_style($value))) {          $quot = strpos($style, '"') !== false ? "'" : '"';          $t .= ' style=' . $quot . $style . $quot; @@ -237,7 +240,8 @@ class washtml          else if (preg_match('/^data:.+/i', $value)) { // RFC2397            $t .= ' ' . $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"';          } -      } else +      } +      else          $washed .= ($washed?' ':'') . $key;      }      return $t . ($washed && $this->config['show_washed']?' x-washed="'.$washed.'"':''); diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc index 94bae1974..9882c19b5 100644 --- a/program/localization/en_US/labels.inc +++ b/program/localization/en_US/labels.inc @@ -1,7 +1,6 @@  <?php  /* -   +-----------------------------------------------------------------------+   | language/en_US/labels.inc                                             |   |                                                                       | @@ -15,9 +14,6 @@   +-----------------------------------------------------------------------+   | Author: Thomas Bruederli <roundcube@gmail.com>                        |   +-----------------------------------------------------------------------+ - - @version $Id$ -  */  $labels = array(); @@ -163,6 +159,7 @@ $labels['unread'] = 'Unread';  $labels['flagged'] = 'Flagged';  $labels['unanswered'] = 'Unanswered';  $labels['deleted'] = 'Deleted'; +$labels['undeleted'] = 'Not deleted';  $labels['invert'] = 'Invert';  $labels['filter'] = 'Filter';  $labels['list'] = 'List'; @@ -384,7 +381,8 @@ $labels['pagesize']  = 'Rows per page';  $labels['signature'] = 'Signature';  $labels['dstactive']  = 'Daylight saving time';  $labels['htmleditor'] = 'Compose HTML messages'; -$labels['htmlonreply'] = 'on reply to HTML message only'; +$labels['htmlonreply'] = 'on reply to HTML message'; +$labels['htmlonreplyandforward'] = 'on forward or reply to HTML message';  $labels['htmlsignature'] = 'HTML signature';  $labels['previewpane'] = 'Show preview pane';  $labels['skin'] = 'Interface skin'; @@ -432,8 +430,9 @@ $labels['maintenance'] = 'Maintenance';  $labels['newmessage'] = 'New Message';  $labels['signatureoptions'] = 'Signature Options';  $labels['whenreplying'] = 'When replying'; -$labels['replytopposting'] = 'start new message above original'; -$labels['replybottomposting'] = 'start new message below original'; +$labels['replyempty'] = 'do not quote the original message'; +$labels['replytopposting'] = 'start new message above the quote'; +$labels['replybottomposting'] = 'start new message below the quote';  $labels['replyremovesignature'] = 'When replying remove original signature from message';  $labels['autoaddsignature'] = 'Automatically add signature';  $labels['newmessageonly'] = 'new message only'; diff --git a/program/localization/pl_PL/labels.inc b/program/localization/pl_PL/labels.inc index 92da1f6ef..d5ffcaa61 100644 --- a/program/localization/pl_PL/labels.inc +++ b/program/localization/pl_PL/labels.inc @@ -134,6 +134,7 @@ $labels['unread'] = 'Nieprzeczytane';  $labels['flagged'] = 'Oznaczone';  $labels['unanswered'] = 'Bez odpowiedzi';  $labels['deleted'] = 'Usunięte'; +$labels['undeleted'] = 'Nieusunięte';  $labels['invert'] = 'Odwróć';  $labels['filter'] = 'Filtr';  $labels['list'] = 'Lista'; diff --git a/program/steps/addressbook/export.inc b/program/steps/addressbook/export.inc index 84a63aebc..850795c85 100644 --- a/program/steps/addressbook/export.inc +++ b/program/steps/addressbook/export.inc @@ -86,7 +86,7 @@ while ($result && ($row = $result->next())) {          foreach ($row as $key => $values) {              list($field, $section) = explode(':', $key);              foreach ((array)$values as $value) { -                if (is_array($value) || strlen($value)) +                if (is_array($value) || @strlen($value))                      $vcard->set($field, $value, strtoupper($section));              }          } diff --git a/program/steps/addressbook/search.inc b/program/steps/addressbook/search.inc index d31e54b1a..851325070 100644 --- a/program/steps/addressbook/search.inc +++ b/program/steps/addressbook/search.inc @@ -237,9 +237,12 @@ function rcmail_contact_search()      $OUTPUT->command('set_env', 'source', '');      $OUTPUT->command('set_env', 'group', ''); -    // unselect currently selected directory/group -    if (!$sid) +    if (!$sid) { +        // unselect currently selected directory/group          $OUTPUT->command('unselect_directory'); +        // enable "Save search" command +        $OUTPUT->command('enable_command', 'search-create', true); +    }      $OUTPUT->command('update_group_commands');      // send response diff --git a/program/steps/mail/compose.inc b/program/steps/mail/compose.inc index 71a1c0f21..29e12675e 100644 --- a/program/steps/mail/compose.inc +++ b/program/steps/mail/compose.inc @@ -139,7 +139,7 @@ if (!empty($CONFIG['drafts_mbox'])) {  // set current mailbox in client environment  $OUTPUT->set_env('mailbox', $RCMAIL->storage->get_folder());  $OUTPUT->set_env('sig_above', $RCMAIL->config->get('sig_above', false)); -$OUTPUT->set_env('top_posting', $RCMAIL->config->get('top_posting', false)); +$OUTPUT->set_env('top_posting', intval($RCMAIL->config->get('reply_mode')) > 0);  $OUTPUT->set_env('recipients_separator', trim($RCMAIL->config->get('recipients_separator', ',')));  // default font for HTML editor @@ -252,7 +252,8 @@ $MESSAGE->identities = $RCMAIL->user->list_identities();  if (count($MESSAGE->identities))  {    foreach ($MESSAGE->identities as $idx => $ident) { -    $email = mb_strtolower(rcube_idn_to_utf8($ident['email'])); +    $ident['email'] = format_email($ident['email']); +    $email = format_email(rcube_idn_to_utf8($ident['email']));      $MESSAGE->identities[$idx]['email_ascii'] = $ident['email'];      $MESSAGE->identities[$idx]['ident']       = format_email_recipient($ident['email'], $ident['name']); @@ -277,7 +278,7 @@ else if (count($MESSAGE->identities)) {      $a_to = rcube_mime::decode_address_list($MESSAGE->headers->to, null, true, $MESSAGE->headers->charset);      foreach ($a_to as $addr) {        if (!empty($addr['mailto'])) { -        $a_recipients[] = strtolower($addr['mailto']); +        $a_recipients[] = format_email($addr['mailto']);          $a_names[]      = $addr['name'];        }      } @@ -286,7 +287,7 @@ else if (count($MESSAGE->identities)) {        $a_cc = rcube_mime::decode_address_list($MESSAGE->headers->cc, null, true, $MESSAGE->headers->charset);        foreach ($a_cc as $addr) {          if (!empty($addr['mailto'])) { -          $a_recipients[] = strtolower($addr['mailto']); +          $a_recipients[] = format_email($addr['mailto']);            $a_names[]      = $addr['name'];          }        } @@ -294,16 +295,12 @@ else if (count($MESSAGE->identities)) {    }    $from_idx         = null; -  $default_identity = null; +  $found_idx        = null; +  $default_identity = 0; // default identity is always first on the list    $return_path      = $MESSAGE->headers->others['return-path'];    // Select identity    foreach ($MESSAGE->identities as $idx => $ident) { -    // save default identity ID -    if ($ident['standard']) { -      $default_identity = $idx; -    } -      // use From header      if (in_array($compose_mode, array(RCUBE_COMPOSE_DRAFT, RCUBE_COMPOSE_EDIT))) {        if ($MESSAGE->headers->from == $ident['ident']) { @@ -318,13 +315,22 @@ else if (count($MESSAGE->identities)) {      }      // use replied message recipients      else if (($found = array_search($ident['email_ascii'], $a_recipients)) !== false) { -      // match identity name, prefer default identity -      if ($from_idx === null || ($a_names[$found] && $ident['name'] && $a_names[$found] == $ident['name'])) { +      if ($found_idx === null) { +        $found_idx = $idx; +      } +      // match identity name +      if ($a_names[$found] && $ident['name'] && $a_names[$found] == $ident['name']) {          $from_idx = $idx; +        break;        }      }    } +  // If matching by name+address doesn't found any amtches, get first found address (identity) +  if ($from_idx === null) { +    $from_idx = $found_idx; +  } +    // Fallback using Return-Path    if ($from_idx === null && $return_path) {      foreach ($MESSAGE->identities as $idx => $ident) { @@ -335,12 +341,7 @@ else if (count($MESSAGE->identities)) {      }    } -  // Still no ID, use default/first identity -  if ($from_idx === null) { -    $from_idx = $default_identity !== null ? $default_identity : key(reset($MESSAGE->identities)); -  } - -  $ident   = $MESSAGE->identities[$from_idx]; +  $ident   = $MESSAGE->identities[$from_idx !== null ? $from_idx : $default_identity];    $from_id = $ident['identity_id'];    $MESSAGE->compose['from_email'] = $ident['email']; @@ -433,7 +434,7 @@ foreach ($parts as $header) {        if (empty($addr_part['mailto']))          continue; -      $mailto = mb_strtolower(rcube_idn_to_utf8($addr_part['mailto'])); +      $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']) @@ -529,7 +530,7 @@ function rcmail_compose_headers($attrib)  function rcmail_compose_header_from($attrib)  { -  global $MESSAGE, $OUTPUT; +  global $MESSAGE, $OUTPUT, $RCMAIL, $compose_mode;    // pass the following attributes to the form class    $field_attrib = array('name' => '_from'); @@ -540,6 +541,8 @@ function rcmail_compose_header_from($attrib)    if (count($MESSAGE->identities))    {      $a_signatures = array(); +    $separator    = $RCMAIL->config->get('sig_above') +      && ($compose_mode == RCUBE_COMPOSE_REPLY || $compose_mode == RCUBE_COMPOSE_FORWARD) ? '---' : '-- ';      $field_attrib['onchange'] = JS_OBJECT_NAME.".change_identity(this)";      $select_from = new html_select($field_attrib); @@ -553,13 +556,27 @@ function rcmail_compose_header_from($attrib)        // add signature to array        if (!empty($sql_arr['signature']) && empty($COMPOSE['param']['nosig']))        { -        $a_signatures[$identity_id]['text'] = $sql_arr['signature']; -        $a_signatures[$identity_id]['is_html'] = ($sql_arr['html_signature'] == 1) ? true : false; -        if ($a_signatures[$identity_id]['is_html']) -        { -            $h2t = new html2text($a_signatures[$identity_id]['text'], false, false); -            $a_signatures[$identity_id]['plain_text'] = trim($h2t->get_text()); +        $text = $html = $sql_arr['signature']; + +        if ($sql_arr['html_signature']) { +            $h2t  = new html2text($sql_arr['signature'], false, false); +            $text = trim($h2t->get_text());          } +        else { +            $html = htmlentities($html, ENT_NOQUOTES, RCMAIL_CHARSET); +        } + +        if (!preg_match('/^--[ -]\r?\n/m', $text)) { +            $text = $separator . "\n" . $text; +            $html = $separator . "<br>" . $html; +        } + +        if (!$sql_arr['html_signature']) { +            $html = "<pre>" . $html . "</pre>"; +        } + +        $a_signatures[$identity_id]['text'] = $text; +        $a_signatures[$identity_id]['html'] = $html;        }      } @@ -593,9 +610,12 @@ function rcmail_compose_editor_mode()      $useHtml = $MESSAGE->has_html_part(false);    }    else if ($compose_mode == RCUBE_COMPOSE_REPLY) { -    $useHtml = ($html_editor == 1 || ($html_editor == 2 && $MESSAGE->has_html_part(false))); +    $useHtml = ($html_editor == 1 || ($html_editor >= 2 && $MESSAGE->has_html_part(false))); +  } +  else if ($compose_mode == RCUBE_COMPOSE_FORWARD) { +    $useHtml = ($html_editor == 1 || ($html_editor == 3 && $MESSAGE->has_html_part(false)));    } -  else { // RCUBE_COMPOSE_FORWARD or NEW +  else {      $useHtml = ($html_editor == 1);    } @@ -624,7 +644,7 @@ function rcmail_prepare_message_body()        rcmail_write_forward_attachment($MESSAGE);    }    // reply/edit/draft/forward -  else if ($compose_mode) { +  else if ($compose_mode && ($compose_mode != RCUBE_COMPOSE_REPLY || $RCMAIL->config->get('reply_mode') != -1)) {      $isHtml = rcmail_compose_editor_mode();      if (!empty($MESSAGE->parts)) { @@ -889,8 +909,9 @@ function rcmail_create_reply_body($body, $bodyIsHtml)      $prefix .= "\n";      $suffix = ''; -    if ($RCMAIL->config->get('top_posting')) +    if (intval($RCMAIL->config->get('reply_mode')) > 0) { // top-posting        $prefix = "\n\n\n" . $prefix; +    }    }    else {      // save inline images to files @@ -904,7 +925,7 @@ function rcmail_create_reply_body($body, $bodyIsHtml)      $prefix = '<p>' . Q($prefix) . "</p>\n";      $prefix .= '<blockquote>'; -    if ($RCMAIL->config->get('top_posting')) { +    if (intval($RCMAIL->config->get('reply_mode')) > 0) { // top-posting        $prefix = '<br>' . $prefix;        $suffix = '</blockquote>';      } diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc index 3d65eacb1..8bf80a6ee 100644 --- a/program/steps/mail/func.inc +++ b/program/steps/mail/func.inc @@ -90,11 +90,13 @@ if (empty($RCMAIL->action) || $RCMAIL->action == 'list') {    // set current mailbox and some other vars in client environment    $OUTPUT->set_env('mailbox', $mbox_name);    $OUTPUT->set_env('pagesize', $RCMAIL->storage->get_pagesize()); -  $OUTPUT->set_env('quota', $RCMAIL->storage->get_capability('QUOTA'));    $OUTPUT->set_env('delimiter', $RCMAIL->storage->get_hierarchy_delimiter());    $OUTPUT->set_env('threading', $threading);    $OUTPUT->set_env('threads', $threading || $RCMAIL->storage->get_capability('THREAD'));    $OUTPUT->set_env('preview_pane_mark_read', $RCMAIL->config->get('preview_pane_mark_read', 0)); +  if ($RCMAIL->storage->get_capability('QUOTA')) { +    $OUTPUT->set_env('quota', true); +  }    if ($CONFIG['delete_junk'])      $OUTPUT->set_env('delete_junk', true); @@ -1053,12 +1055,17 @@ function rcmail_message_full_headers($attrib, $headers=NULL)    global $OUTPUT;    $html = html::div(array('id' => "all-headers", 'class' => "all", 'style' => 'display:none'), html::div(array('id' => 'headers-source'), '')); -  $html .= html::div(array('class' => "more-headers show-headers", 'onclick' => "return ".JS_OBJECT_NAME.".command('show-headers','',this)"), ''); + +  if (!get_boolean($attrib['no-switch'])) { +    $html .= html::div(array('class' => "more-headers show-headers", 'onclick' => "return ".JS_OBJECT_NAME.".command('show-headers','',this)"), ''); +  } + +  unset($attrib['no-switch']);    $OUTPUT->add_gui_object('all_headers_row', 'all-headers');    $OUTPUT->add_gui_object('all_headers_box', 'headers-source'); -  return html::div($attrib, $html); +  return count($attrib) > 1 ? html::div($attrib, $html) : $html;  } @@ -1712,8 +1719,10 @@ function rcmail_search_filter($attrib)    $select_filter->add(rcube_label('unread'), 'UNSEEN');    $select_filter->add(rcube_label('flagged'), 'FLAGGED');    $select_filter->add(rcube_label('unanswered'), 'UNANSWERED'); -  if (!$CONFIG['skip_deleted']) +  if (!$CONFIG['skip_deleted']) {      $select_filter->add(rcube_label('deleted'), 'DELETED'); +    $select_filter->add(rcube_label('undeleted'), 'UNDELETED'); +  }    $select_filter->add(rcube_label('priority').': '.rcube_label('highest'), 'HEADER X-PRIORITY 1');    $select_filter->add(rcube_label('priority').': '.rcube_label('high'), 'HEADER X-PRIORITY 2');    $select_filter->add(rcube_label('priority').': '.rcube_label('normal'), 'NOT HEADER X-PRIORITY 1 NOT HEADER X-PRIORITY 2 NOT HEADER X-PRIORITY 4 NOT HEADER X-PRIORITY 5'); diff --git a/program/steps/mail/headers.inc b/program/steps/mail/headers.inc index 4d6627393..cad113f68 100644 --- a/program/steps/mail/headers.inc +++ b/program/steps/mail/headers.inc @@ -24,7 +24,8 @@ if ($uid = get_input_value('_uid', RCUBE_INPUT_POST))      $source = $RCMAIL->storage->get_raw_headers($uid);      if ($source !== false) { -        $source = htmlspecialchars(trim($source)); +        $source = trim(rcube_charset::clean($source)); +        $source = htmlspecialchars($source);          $source = preg_replace(              array(                  '/\n[\t\s]+/', diff --git a/program/steps/mail/sendmail.inc b/program/steps/mail/sendmail.inc index 70f1af714..577751742 100644 --- a/program/steps/mail/sendmail.inc +++ b/program/steps/mail/sendmail.inc @@ -511,14 +511,9 @@ if ($isHtml) {    $h2t = new html2text($plugin['body'], false, true, 0);    $plainTextPart = rc_wordwrap($h2t->get_text(), $LINE_LENGTH, "\r\n");    $plainTextPart = wordwrap($plainTextPart, 998, "\r\n", true); -  if (!$plainTextPart) { -    // empty message body breaks attachment handling in drafts -    $plainTextPart = "\r\n"; -  } -  else { -    // make sure all line endings are CRLF (#1486712) -    $plainTextPart = preg_replace('/\r?\n/', "\r\n", $plainTextPart); -  } + +  // make sure all line endings are CRLF (#1486712) +  $plainTextPart = preg_replace('/\r?\n/', "\r\n", $plainTextPart);    $plugin = $RCMAIL->plugins->exec_hook('message_outgoing_body',      array('body' => $plainTextPart, 'type' => 'alternative', 'message' => $MAIL_MIME)); @@ -542,10 +537,6 @@ else {      $message_body = rc_wordwrap($message_body, $LINE_LENGTH, "\r\n");    $message_body = wordwrap($message_body, 998, "\r\n", true); -  if (!strlen($message_body)) {  -    // empty message body breaks attachment handling in drafts  -    $message_body = "\r\n";  -  }    $MAIL_MIME->setTXTBody($message_body, false, true);  } diff --git a/program/steps/mail/viewsource.inc b/program/steps/mail/viewsource.inc index 59ccb386e..c560d7d41 100644 --- a/program/steps/mail/viewsource.inc +++ b/program/steps/mail/viewsource.inc @@ -44,7 +44,7 @@ if ($uid = get_input_value('_uid', RCUBE_INPUT_GET))      header("Content-Disposition: attachment; filename=\"$filename\"");    } -  $RCMAIL->storage->print_raw_body($uid); +  $RCMAIL->storage->print_raw_body($uid, empty($_GET['_save']));  }  else  { diff --git a/program/steps/settings/folders.inc b/program/steps/settings/folders.inc index 2691a6e26..6ca704998 100644 --- a/program/steps/settings/folders.inc +++ b/program/steps/settings/folders.inc @@ -44,8 +44,8 @@ if ($RCMAIL->action == 'subscribe')          if ($result) {              // Handle subscription of protected folder (#1487656) -            if ($CONFIG['protect_default_folders'] == true -                && in_array($mbox, $CONFIG['default_folders']) +            if ($RCMAIL->config->get('protect_default_folders') +                && in_array($mbox, (array)$RCMAIL->config->get('default_folders'))              ) {                  $OUTPUT->command('disable_subscription', $mbox);              } @@ -321,8 +321,8 @@ function rcube_subscription_form($attrib)                  }              }          } -        // check if the folder is shared, then disable subscription option on it -        if (!$disabled && $folder['virtual'] && !empty($namespace)) { +        // check if the folder is shared, then disable subscription option on it (if not subscribed already) +        if (!$disabled && !$subscribed && $folder['virtual'] && !empty($namespace)) {              $tmp_ns = array_merge((array)$namespace['other'], (array)$namespace['shared']);              foreach ($tmp_ns as $item) {                  if (strpos($folder['id'], $item[0]) === 0) { @@ -411,8 +411,10 @@ function rcmail_rename_folder($oldname, $newname)  $OUTPUT->set_pagetitle(rcube_label('folders'));  $OUTPUT->include_script('list.js'); -$OUTPUT->set_env('quota', $STORAGE->get_capability('QUOTA'));  $OUTPUT->set_env('prefix_ns', $STORAGE->get_namespace('prefix')); +if ($STORAGE->get_capability('QUOTA')) { +    $OUTPUT->set_env('quota', true); +}  // add some labels to client  $OUTPUT->add_label('deletefolderconfirm', 'purgefolderconfirm', 'folderdeleting', diff --git a/program/steps/settings/func.inc b/program/steps/settings/func.inc index 3f5ef5390..59b4e3735 100644 --- a/program/steps/settings/func.inc +++ b/program/steps/settings/func.inc @@ -470,6 +470,7 @@ function rcmail_user_prefs($current=null)        $select_htmleditor->add(rcube_label('never'), 0);        $select_htmleditor->add(rcube_label('always'), 1);        $select_htmleditor->add(rcube_label('htmlonreply'), 2); +      $select_htmleditor->add(rcube_label('htmlonreplyandforward'), 3);        $blocks['main']['options']['htmleditor'] = array(          'title' => html::label($field_id, Q(rcube_label('htmleditor'))), @@ -544,16 +545,17 @@ function rcmail_user_prefs($current=null)        );      } -    if (!isset($no_override['top_posting'])) { -      $field_id = 'rcmfd_top_posting'; -      $select_replymode = new html_select(array('name' => '_top_posting', 'id' => $field_id, -        'onchange' => "\$('#rcmfd_sig_above').attr('disabled',this.selectedIndex==0)")); +    if (!isset($no_override['reply_mode'])) { +      $field_id = 'rcmfd_reply_mode'; +      $select_replymode = new html_select(array('name' => '_reply_mode', 'id' => $field_id, +        'onchange' => "\$('#rcmfd_sig_above').attr('disabled',this.selectedIndex<2)")); +      $select_replymode->add(rcube_label('replyempty'), -1);        $select_replymode->add(rcube_label('replybottomposting'), 0);        $select_replymode->add(rcube_label('replytopposting'), 1); -      $blocks['main']['options']['top_posting'] = array( +      $blocks['main']['options']['reply_mode'] = array(          'title' => html::label($field_id, Q(rcube_label('whenreplying'))), -        'content' => $select_replymode->show($config['top_posting']?1:0), +        'content' => $select_replymode->show(intval($config['reply_mode'])),        );      } @@ -597,7 +599,7 @@ function rcmail_user_prefs($current=null)      if (!isset($no_override['sig_above'])) {        $field_id = 'rcmfd_sig_above'; -      $select_sigabove = new html_select(array('name' => '_sig_above', 'id' => $field_id, 'disabled' => !$config['top_posting'])); +      $select_sigabove = new html_select(array('name' => '_sig_above', 'id' => $field_id, 'disabled' => $config['reply_mode'] < 1));        $select_sigabove->add(rcube_label('belowquote'), 0);        $select_sigabove->add(rcube_label('abovequote'), 1); diff --git a/program/steps/settings/save_folder.inc b/program/steps/settings/save_folder.inc index 09f76ac27..73cc5e4bf 100644 --- a/program/steps/settings/save_folder.inc +++ b/program/steps/settings/save_folder.inc @@ -80,7 +80,10 @@ if (!$error && strlen($path) && (!strlen($old_imap) || $old_imap != $name_imap))      }  } -if (!$error) { +if ($error) { +    $OUTPUT->command('display_message', $error, 'error'); +} +else {      $folder['name']     = $name_imap;      $folder['oldname']  = $old_imap;      $folder['class']    = ''; diff --git a/program/steps/settings/save_prefs.inc b/program/steps/settings/save_prefs.inc index 88fa5298a..dc149929e 100644 --- a/program/steps/settings/save_prefs.inc +++ b/program/steps/settings/save_prefs.inc @@ -82,9 +82,9 @@ switch ($CURR_SECTION)        'spellcheck_ignore_nums' => isset($_POST['_spellcheck_ignore_nums']) ? TRUE : FALSE,        'spellcheck_ignore_caps' => isset($_POST['_spellcheck_ignore_caps']) ? TRUE : FALSE,        'show_sig'           => isset($_POST['_show_sig']) ? intval($_POST['_show_sig']) : 1, -      'top_posting'        => !empty($_POST['_top_posting']), +      'reply_mode'         => isset($_POST['_reply_mode']) ? intval($_POST['_reply_mode']) : 0,        'strip_existing_sig' => isset($_POST['_strip_existing_sig']), -      'sig_above'          => !empty($_POST['_sig_above']) && !empty($_POST['_top_posting']), +      'sig_above'          => !empty($_POST['_sig_above']) && $_POST['_reply_mode'] < 1,        'default_font'       => get_input_value('_default_font', RCUBE_INPUT_POST),        'forward_attachment' => !empty($_POST['_forward_attachment']),      ); diff --git a/skins/classic/functions.js b/skins/classic/functions.js index 9b83b9044..73c43f60f 100644 --- a/skins/classic/functions.js +++ b/skins/classic/functions.js @@ -28,11 +28,6 @@ function rcube_init_settings_tabs()    $('a', tab).removeAttr('onclick').click(function() { return false; });  } -function rcube_show_advanced(visible) -{ -  $('tr.advanced').css('display', (visible ? (bw.ie ? 'block' : 'table-row') : 'none')); -} -  // Fieldsets-to-tabs converter  // Warning: don't place "caller" <script> inside page element (id)  function rcube_init_tabs(id, current) @@ -530,7 +525,7 @@ show_header_form: function(id)    if ((row = document.getElementById('compose-' + id))) {      var div = document.getElementById('compose-div'),        headers_div = document.getElementById('compose-headers-div'); -    row.style.display = (document.all && !window.opera) ? 'block' : 'table-row'; +    $(row).show();      div.style.top = (parseInt(headers_div.offsetHeight, 10) + 3) + 'px';      this.resize_compose_body();    } @@ -550,11 +545,11 @@ hide_header_form: function(id)    for (var i=0; i<links.length; i++)      if (links[i].style.display != 'none')        for (var j=i+1; j<links.length; j++) -	    if (links[j].style.display != 'none') +        if (links[j].style.display != 'none')            if ((ns = this.next_sibling(links[i]))) { -	        ns.style.display = ''; -	        break; -	      } +            ns.style.display = ''; +            break; +          }    document.getElementById('_' + id).value = ''; diff --git a/skins/classic/includes/links.html b/skins/classic/includes/links.html index 82673220a..6d8d03c52 100644 --- a/skins/classic/includes/links.html +++ b/skins/classic/includes/links.html @@ -1,3 +1,4 @@ +<meta http-equiv="X-UA-Compatible" content="IE=EDGE" />  <link rel="index" href="$__comm_path" />  <link rel="shortcut icon" href="/images/favicon.ico"/>  <link rel="stylesheet" type="text/css" href="/common.css" /> diff --git a/skins/classic/splitter.js b/skins/classic/splitter.js index 59ebb5151..3f1c97302 100644 --- a/skins/classic/splitter.js +++ b/skins/classic/splitter.js @@ -47,7 +47,7 @@ function rcube_splitter(attrib)        rcube_event.add_listener({element: window, event:'resize', object:this, method:'onResize'});      // read saved position from cookie -    var cookie = bw.get_cookie(this.id); +    var cookie = rcmail.get_cookie(this.id);      if (cookie && !isNaN(cookie)) {        this.pos = parseFloat(cookie);        this.resize(); @@ -197,7 +197,7 @@ function rcube_splitter(attrib)    {      var exp = new Date();      exp.setYear(exp.getFullYear() + 1); -    bw.set_cookie(this.id, this.pos, exp); +    rcmail.set_cookie(this.id, this.pos, exp);    };  } // end class rcube_splitter diff --git a/skins/classic/templates/compose.html b/skins/classic/templates/compose.html index caebf31a0..1e1403e18 100644 --- a/skins/classic/templates/compose.html +++ b/skins/classic/templates/compose.html @@ -79,7 +79,7 @@                  <a href="#bcc" onclick="return rcmail_ui.hide_header_form('bcc');"><img src="/images/icons/minus.gif" alt="" width="13" height="11" title="<roundcube:label name='delete' />" /></a>                  <label for="_bcc"><roundcube:label name="bcc" /></label>              </td> -            <td colspan="2" class="editfield"><roundcube:object name="composeHeaders" part="bcc" form="form" id="_bcc" cols="70" rows="2" tabindex="4" /></td> +            <td class="editfield"><roundcube:object name="composeHeaders" part="bcc" form="form" id="_bcc" cols="70" rows="2" tabindex="4" /></td>          </tr><tr id="compose-replyto">              <td class="title top">                  <a href="#replyto" onclick="return rcmail_ui.hide_header_form('replyto');"><img src="/images/icons/minus.gif" alt="" width="13" height="11" title="<roundcube:label name='delete' />" /></a> diff --git a/skins/classic/templates/message.html b/skins/classic/templates/message.html index 714540b78..c03376e4a 100644 --- a/skins/classic/templates/message.html +++ b/skins/classic/templates/message.html @@ -23,11 +23,9 @@  <div id="mailboxlist-container">  <div id="mailboxlist-title" class="boxtitle"><roundcube:label name="mailboxlist" /></div>  <div class="boxlistcontent"> -<roundcube:object name="mailboxlist" id="mailboxlist" maxlength="25" /> -</div> -<div class="boxfooter"> -  <roundcube:button name="mailboxmenulink" id="mailboxmenulink" type="link" title="folderactions" class="button groupactions" onclick="rcmail_ui.show_popup('mailboxmenu');return false" content=" " /> +    <roundcube:object name="mailboxlist" id="mailboxlist" maxlength="25" />  </div> +<div class="boxfooter"></div>  </div>  </div> @@ -57,14 +55,5 @@      rcmail.add_onload('mailviewsplitv.init()');  </script> -<div id="mailboxoptionsmenu" class="popupmenu"> -  <ul> -    <li><roundcube:button command="expunge" type="link" label="compact" classAct="active" /></li> -    <li class="separator_below"><roundcube:button command="purge" type="link" label="empty" classAct="active" /></li> -    <li><roundcube:button command="folders" task="settings" type="link" label="managefolders" classAct="active" /></li> -    <roundcube:container name="mailboxoptions" id="mailboxoptionsmenu" /> -  </ul> -</div> -  </body>  </html> diff --git a/skins/classic/templates/messageerror.html b/skins/classic/templates/messageerror.html index 9af45f432..918e3092a 100644 --- a/skins/classic/templates/messageerror.html +++ b/skins/classic/templates/messageerror.html @@ -42,11 +42,9 @@  <div id="mailboxlist-container">  <div class="boxtitle"><roundcube:label name="mailboxlist" /></div>  <div class="boxlistcontent"> -<roundcube:object name="mailboxlist" id="mailboxlist" folder_filter="mail" /> -</div> -<div class="boxfooter"> -  <roundcube:button name="mailboxmenulink" id="mailboxmenulink" type="link" title="folderactions" class="button groupactions" onclick="rcmail_ui.show_popup('mailboxmenu');return false" content=" " /> +    <roundcube:object name="mailboxlist" id="mailboxlist" folder_filter="mail" />  </div> +<div class="boxfooter"></div>  </div>  </div> @@ -63,15 +61,6 @@      rcmail.add_onload('mailviewsplitv.init()');  </script> -<div id="mailboxoptionsmenu" class="popupmenu"> -  <ul> -    <li><roundcube:button command="expunge" type="link" label="compact" classAct="active" /></li> -    <li class="separator_below"><roundcube:button command="purge" type="link" label="empty" classAct="active" /></li> -    <li><roundcube:button command="folders" task="settings" type="link" label="managefolders" classAct="active" /></li> -    <roundcube:container name="mailboxoptions" id="mailboxoptionsmenu" /> -  </ul> -</div> -  </body>  <roundcube:endif /> diff --git a/skins/larry/addressbook.css b/skins/larry/addressbook.css index fe087aece..74bc0d7d8 100644 --- a/skins/larry/addressbook.css +++ b/skins/larry/addressbook.css @@ -34,7 +34,6 @@  	position: absolute;  	top: -6px;  	left: 0; -	right: 260px;  	height: 40px;  	white-space: nowrap;  	z-index: 10; diff --git a/skins/larry/ie7hacks.css b/skins/larry/ie7hacks.css index 6161d03dd..935a504fe 100644 --- a/skins/larry/ie7hacks.css +++ b/skins/larry/ie7hacks.css @@ -7,10 +7,16 @@   * License. It is allowed to copy, distribute, transmit and to adapt the work   * by keeping credits to the original autors in the README file.   * See http://creativecommons.org/licenses/by-sa/3.0/ for details. - * - * $Id$   */ +/* #1488618 */ +#mainscreen { +  height: expression((parseInt(document.documentElement.clientHeight)-108)+'px'); +} +#mainscreen.offset { +  height: expression((parseInt(document.documentElement.clientHeight)-150)+'px'); +} +  input.button {  	display: inline;  	font-size: 90%; @@ -23,7 +29,7 @@ a.deletebutton,  .boxfooter .listbutton .inner,  .attachmentslist li a.delete,  .attachmentslist li a.cancelupload, -#messagepreviewheader .iconlink { +#messageheader .iconlink {  	/* workaround for text-indent which also offsets the background image */  	text-indent: 0;  	font-size: 0; @@ -39,7 +45,7 @@ a.deletebutton,  .pagenav a.button,  .pagenav a.button span.inner, -#messagepreviewheader .iconlink, +#messageheader .iconlink,  #uploadform a.iconlink {  	display: inline;  } @@ -61,7 +67,7 @@ a.deletebutton,  	text-align: left;  } -#messagepreviewheader .iconlink { +#messageheader .iconlink {  	color: #fff;  	height: 14px;  } diff --git a/skins/larry/iehacks.css b/skins/larry/iehacks.css index 288202111..bba93dc33 100644 --- a/skins/larry/iehacks.css +++ b/skins/larry/iehacks.css @@ -143,7 +143,7 @@ ul.toolbarmenu li a.active:hover,  	filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#005d76', endColorstr='#004558', GradientType=0);  } -#messageheader, #partheader, #composeheaders { +#partheader, #composeheaders {  	filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e9e9e9', GradientType=0);  } diff --git a/skins/larry/images/contactpic_32px.png b/skins/larry/images/contactpic_32px.pngBinary files differ index 276f1974f..25a81418d 100644 --- a/skins/larry/images/contactpic_32px.png +++ b/skins/larry/images/contactpic_32px.png diff --git a/skins/larry/images/contactpic_48px.png b/skins/larry/images/contactpic_48px.pngBinary files differ new file mode 100644 index 000000000..9cd3bceaf --- /dev/null +++ b/skins/larry/images/contactpic_48px.png diff --git a/skins/larry/includes/links.html b/skins/larry/includes/links.html index 0ddc2e160..8bd8012e1 100644 --- a/skins/larry/includes/links.html +++ b/skins/larry/includes/links.html @@ -1,3 +1,4 @@ +<meta http-equiv="X-UA-Compatible" content="IE=EDGE" />  <link rel="shortcut icon" href="/images/favicon.ico"/>  <link rel="stylesheet" type="text/css" href="/styles.css" />  <roundcube:if condition="in_array(env:task, array('mail','addressbook','settings'))" /> diff --git a/skins/larry/mail.css b/skins/larry/mail.css index 4fff24307..496cbbd15 100644 --- a/skins/larry/mail.css +++ b/skins/larry/mail.css @@ -38,10 +38,6 @@  	bottom: 28px;  } -#mailview-top.fullheight { -	border-radius: 4px 4px 0 0; -} -  #mailview-bottom {  	position: absolute;  	left: 0; @@ -50,6 +46,10 @@  	height: 26px;  } +#mailview-top.fullheight { +	border-radius: 4px 4px 0 0; +} +  #folderlist-header {  	width: 100%;  	height: 12px; @@ -341,7 +341,6 @@ a.iconbutton.threadmode.selected {  #messagetoolbar {  	position: absolute;  	top: -6px; -	right: 390px;  	left: 0;  	height: 40px;  	white-space: nowrap; @@ -362,7 +361,7 @@ a.iconbutton.threadmode.selected {  	position: absolute;  	right: 0;  	top: 0; -	width: 240px; +	width: 400px;  }  #mailpreviewtoggle { @@ -383,11 +382,7 @@ a.iconbutton.threadmode.selected {  /*** message list ***/  #messagelist thead td:first-child { -	border-radius: 4px 0 0 0; -} - -#messagelist thead td:last-child { -	border-radius: 0 4px 0 0; +	border-radius: 4px 0 0 0; /* for Chrome */  }  #messagelist tr td.attachment, @@ -680,15 +675,14 @@ a.iconbutton.threadmode.selected {  #messagecontent {  	position: absolute; -	top: 140px; +	top: 0;  	left: 0;  	width: 100%; -	bottom: 0; +	bottom: 28px;  	overflow: auto;  	border-radius: 4px 4px 0 0;  } -#messageheader,  #partheader,  #composeheaders {  	position: relative; @@ -712,7 +706,7 @@ h2.subject {  h3.subject {  	font-size: 14px; -	margin: 0 8em 0 0; +	margin: 0 13em 0 0;  	padding: 8px 8px 4px 8px;  	white-space: nowrap;  	overflow: hidden; @@ -787,6 +781,7 @@ h3.subject {  	background: -ms-linear-gradient(left, #fbfbfb 0, #e9e9e9 100%);  	background: linear-gradient(left, #fbfbfb 0, #e9e9e9 100%);  	border-right: 1px solid #dfdfdf; +	border-radius: 3px 0 0 0; /* for Opera */  }  #previewheaderstoggle .iconlink { @@ -801,28 +796,29 @@ h3.subject {  #previewheaderstoggle.remove .iconlink {  	top: auto; -	bottom: 5px; +	bottom: 15px;  	background-position: -5px -242px;  } -div.more-headers { -	cursor: pointer; -	height: 10px; -	background: url(images/buttons.png) center -1619px no-repeat; +#previewheaderstoggle .iconlink.allheaders { +	display: none;  } -div.hide-headers { -	background-position: center -1629px; +#previewheaderstoggle.remove .iconlink.allheaders { +	top: auto; +	bottom: 2px; +	display: inline-block; +	background-position: -27px -242px;  }  #all-headers {  	position: relative; -	margin: 0 10px; +	margin: 2px 0;  	padding: 0;  	height: 180px; -	border: 1px solid #bbb; +	background-color: #f0f0f0; +	overflow: hidden;  	border-radius: 4px; -	background: #fff;  }  #headers-source { @@ -832,25 +828,30 @@ div.hide-headers {  	left: 0;  	right: 0;  	bottom: 0; -	padding: 2px 5px; +	padding: 2px;  	overflow: auto;  	text-align: left; -	color: #333; +	color: #666;  } -#messagepreviewheader { +#messageheader {  	position: relative;  	height: auto;  	margin: 0 8px 0 0; -	padding: 0 0 6px 72px; +	padding: 0 0 0 72px;  	border-bottom: 2px solid #f0f0f0;  } -#messagepreviewheader h3.subject { +#messagecontent #messageheader { +	padding: 0 0 0 90px; +	min-height: 68px; +} + +#messageheader h3.subject {  	padding: 8px 8px 2px 0;  } -#messagepreviewheader #contactphoto { +#messageheader #contactphoto {  	display: block;  	position: absolute;  	top: 11px; @@ -862,52 +863,40 @@ div.hide-headers {  	border-radius: 3px;  } -#messagepreviewheader #contactphoto img { +#messageheader #contactphoto img {  	width: 32px;  	height: auto;  	border-radius: 3px;  } -#messageheader #contactphoto { -	display: block; -	position: absolute; -	top: 40px; -	right: 10px; +#messagecontent #messageheader #contactphoto { +	top: 11px; +	left: 31px;  	width: 48px;  	height: 48px; -	overflow: hidden; +	background: url(images/contactpic_48px.png) center center no-repeat #fff;  	border-radius: 4px;  } -#messageheader #contactphoto img { +#messagecontent #messageheader #contactphoto img {  	width: 48px;  	height: auto;  	border-radius: 4px;  } -#messagepreviewheader #countcontrols,  #messageheader #countcontrols {  	position: absolute;  	top: 8px; -	right: 8px; -	width: 20em; +	right: 0;  	text-align: right;  	white-space: nowrap;  } -#messageheader .pagenav .countdisplay { -	min-width: 0; -	padding-right: 0.5em; -	white-space: nowrap; -} - -#messagecontent .leftcol,  #messagepreview .leftcol {  	margin-right: 252px;  	overflow-x: auto;  } -#messagecontent .rightcol,  #messagepreview .rightcol {  	float: right;  /* @@ -921,6 +910,7 @@ div.hide-headers {  	min-height: 200px;  	background: #f0f0f0;  	padding: 8px; +	border-radius: 4px;  }  #messagebody { diff --git a/skins/larry/styles.css b/skins/larry/styles.css index 0a72c5048..f2d4888b1 100644 --- a/skins/larry/styles.css +++ b/skins/larry/styles.css @@ -647,6 +647,7 @@ a.iconlink.upload {  .uibox {  	border: 1px solid #a3a3a3;  	border-radius: 4px; +	overflow: hidden;  	box-shadow: 0 0 2px #999;  	-o-box-shadow: 0 0 2px #999;  	-webkit-box-shadow: 0 0 2px #999; @@ -660,7 +661,7 @@ a.iconlink.upload {  	left: 0;  	bottom: 0;  	width: 100%; -	min-width: 1150px; +	min-width: 1024px;  }  .scroller { @@ -698,7 +699,8 @@ a.iconlink.upload {  	left: 0;  	width: 100%;  	bottom: 0; -	overflow: auto; +	overflow-x: hidden; +	overflow-y: auto;  }  .listbox .scroller.withfooter { diff --git a/skins/larry/svggradient.php b/skins/larry/svggradient.php index c54bdec17..8db2c5f63 100644 --- a/skins/larry/svggradient.php +++ b/skins/larry/svggradient.php @@ -11,6 +11,8 @@   * See http://creativecommons.org/licenses/by-sa/3.0/ for details.   */ +ini_set('error_reporting', E_ALL &~ (E_NOTICE | E_STRICT)); +  header('Content-Type: image/svg+xml');  header("Expires: ".gmdate("D, d M Y H:i:s", time()+864000)." GMT");  header("Cache-Control: max-age=864000"); diff --git a/skins/larry/svggradients.css b/skins/larry/svggradients.css index 143fb375f..4f1dd8a05 100644 --- a/skins/larry/svggradients.css +++ b/skins/larry/svggradients.css @@ -133,7 +133,7 @@ ul.toolbarmenu li a.active:hover,  	background-image: url(svggradient.php?c=005d76;004558);  } -#messageheader, #partheader, #composeheaders { +#partheader, #composeheaders {  	background-image: url(svggradient.php?c=ffffff;e9e9e9);  } diff --git a/skins/larry/templates/compose.html b/skins/larry/templates/compose.html index ff0c833c6..d29c1bd2a 100644 --- a/skins/larry/templates/compose.html +++ b/skins/larry/templates/compose.html @@ -131,7 +131,7 @@  		</span>  		<roundcube:endif />  		<span class="composeoption"> -			<label><label for="rcmcomposepriority"><roundcube:label name="priority" /> +			<label for="rcmcomposepriority"><roundcube:label name="priority" />  				<roundcube:object name="prioritySelector" form="form" id="rcmcomposepriority" /></label>  		</span>  		<span class="composeoption"> diff --git a/skins/larry/templates/message.html b/skins/larry/templates/message.html index 1becd711a..89b7bd808 100644 --- a/skins/larry/templates/message.html +++ b/skins/larry/templates/message.html @@ -24,20 +24,38 @@  <!-- folders list -->  <div id="mailboxcontainer" class="uibox listbox"> -<div class="scroller"> -<roundcube:object name="mailboxlist" id="mailboxlist" class="listing" folder_filter="mail" unreadwrap="%s" /> -</div> +    <div class="scroller"> +        <roundcube:object name="mailboxlist" id="mailboxlist" class="listing" folder_filter="mail" unreadwrap="%s" /> +    </div>  </div> -</div> +</div><!-- end mailview-left --> + +<div id="mailview-right" class="uibox" style="top: 42px"> + +<div id="messagecontent"> -<div id="mailview-right"> +<div id="messageheader"> +<h3 class="subject"><roundcube:object name="messageHeaders" valueOf="subject" /></h3> + +<a href="#details" id="previewheaderstoggle"><span class="iconlink"></span><span id="headerstoggleall" class="iconlink allheaders"></span></a> + +<div id="contactphoto"><roundcube:object name="contactphoto" /></div> -<div id="mailview-top"> -<div id="messageheader" class="uibox"> -<h2 class="subject"><roundcube:object name="messageHeaders" valueOf="subject" /></h2> -<roundcube:object name="messageHeaders" class="headers-table" addicon="/images/addcontact.png" exclude="subject" /> -<roundcube:object name="messageFullHeaders" id="full-headers" /> +<table class="headers-table" id="preview-shortheaders"><tbody><tr> +<roundcube:if condition="env:mailbox == config:drafts_mbox || env:mailbox == config:sent_mbox"> +	<td class="header-title"><roundcube:label name="to" /></td> +	<td class="header from"><roundcube:object name="messageHeaders" valueOf="to" addicon="/images/addcontact.png" /></td> +<roundcube:else /> +	<td class="header-title"><roundcube:label name="from" /></td> +	<td class="header from"><roundcube:object name="messageHeaders" valueOf="from" addicon="/images/addcontact.png" /></td> +<roundcube:endif /> +	<td class="header-title"><roundcube:label name="date" /></td> +	<td class="header from"><roundcube:object name="messageHeaders" valueOf="date" /></td> +</tr></tbody></table> + +<roundcube:object name="messageHeaders" id="preview-allheaders" class="headers-table" addicon="/images/addcontact.png" exclude="subject,replyto" /> +<roundcube:object name="messageFullHeaders" no-switch="true" />  <!-- record navigation -->  <div id="countcontrols" class="pagenav"> @@ -46,24 +64,21 @@  	<roundcube:button command="nextmessage" type="link" class="button nextpage disabled" classAct="button nextpage" classSel="button nextpage pressed" innerClass="inner" title="nextmessage" content="&gt;" />  </div> -<div id="contactphoto"><roundcube:object name="contactphoto" /></div> -</div> +</div><!-- end messageheader --> -<div id="messagecontent" class="uibox"> -<div class="rightcol"> -<roundcube:object name="messageAttachments" id="attachment-list" class="attachmentslist" /> -</div> -<div class="leftcol"> -<roundcube:object name="messageObjects" id="message-objects" /> -<roundcube:object name="messageBody" id="messagebody" /> -</div> +<div id="messagepreview"> +    <div class="rightcol"> +        <roundcube:object name="messageAttachments" id="attachment-list" class="attachmentslist" /> +    </div> +    <div class="leftcol"> +        <roundcube:object name="messageObjects" id="message-objects" /> +        <roundcube:object name="messageBody" id="messagebody" /> +    </div>  </div> -</div><!-- end mailview-top --> +</div><!-- end messagecontent --> -<div id="mailview-bottom" class="uibox">  <roundcube:object name="message" id="message" class="statusbar" /> -</div>  </div><!-- end mailview-right --> diff --git a/skins/larry/templates/messageerror.html b/skins/larry/templates/messageerror.html index 70181f174..2f5243200 100644 --- a/skins/larry/templates/messageerror.html +++ b/skins/larry/templates/messageerror.html @@ -27,8 +27,6 @@  </div> -<div id="mailview-right"> -  <!-- toolbar -->  <div id="messagetoolbar" class="fullwidth">  	<div id="mailtoolbar" class="toolbar"> @@ -36,11 +34,11 @@  	</div>  </div> -<div id="mailview-top" class="uibox watermark"></div> +<div id="mailview-right" class="uibox" style="top: 42px"> -<div id="mailview-bottom" class="uibox"> -	<roundcube:object name="message" id="message" class="statusbar" /> -</div> +<div id="messagecontent" class="watermark"></div> + +<roundcube:object name="message" id="message" class="statusbar" />  </div><!-- end mailview-right --> diff --git a/skins/larry/templates/messagepreview.html b/skins/larry/templates/messagepreview.html index b53683ec1..74c414b0d 100644 --- a/skins/larry/templates/messagepreview.html +++ b/skins/larry/templates/messagepreview.html @@ -6,10 +6,10 @@  </head>  <body class="iframe fullheight"> -<div id="messagepreviewheader"> +<div id="messageheader">  <h3 class="subject"><roundcube:object name="messageHeaders" valueOf="subject" /></h3> -<a href="#details" id="previewheaderstoggle"><span class="iconlink"></span></a> +<a href="#details" id="previewheaderstoggle"><span class="iconlink"></span><span id="headerstoggleall" class="iconlink allheaders"></a>  <div id="contactphoto"><roundcube:object name="contactphoto" /></div>  <table class="headers-table" id="preview-shortheaders"><tbody><tr> @@ -25,6 +25,7 @@  </tr></tbody></table>  <roundcube:object name="messageHeaders" id="preview-allheaders" class="headers-table" addicon="/images/addcontact.png" exclude="subject,replyto" /> +<roundcube:object name="messageFullHeaders" no-switch="true" />  <!-- record navigation -->  <div id="countcontrols" class="pagenav"> diff --git a/skins/larry/ui.js b/skins/larry/ui.js index b6056b6ee..e3b5eefe4 100644 --- a/skins/larry/ui.js +++ b/skins/larry/ui.js @@ -74,9 +74,8 @@ function rcube_mail_ui()        if (rcmail.env.action == 'show' || rcmail.env.action == 'preview') {          layout_messageview(); -        rcmail.addEventListener('aftershow-headers', function() { layout_messageview(); }); -        rcmail.addEventListener('afterhide-headers', function() { layout_messageview(); }); -        $('#previewheaderstoggle').click(function(e){ toggle_preview_headers(this); return false }); +        $('#previewheaderstoggle').click(function(e){ toggle_preview_headers(this); return false; }); +        $('#headerstoggleall').click(function(e){ toggle_all_headers(this); return false; });        }        else if (rcmail.env.action == 'compose') {          rcmail.addEventListener('aftertoggle-editor', function(){ window.setTimeout(function(){ layout_composeview() }, 200); }); @@ -164,6 +163,12 @@ function rcube_mail_ui()        }      } +    // set min-width to show all toolbar buttons +    var screen = $('.minwidth'); +    if (screen.length) { +      screen.css('min-width', $('.toolbar').width() + $('#quicksearchbar').parent().width() + 20); +    } +      // turn a group of fieldsets into tabs      $('.tabbed').each(function(idx, elem){ init_tabs(elem); }) @@ -254,11 +259,11 @@ function rcube_mail_ui()     */    function resize()    { -    if (rcmail.env.task == 'mail' && (rcmail.env.action == 'show' || rcmail.env.action == 'preview')) { -      layout_messageview(); -    } -    if (rcmail.env.task == 'mail' && rcmail.env.action == 'compose') { -      layout_composeview(); +    if (rcmail.env.task == 'mail') { +      if (rcmail.env.action == 'show' || rcmail.env.action == 'preview') +        layout_messageview(); +      else if (rcmail.env.action == 'compose') +        layout_composeview();      }      // make iframe footer buttons float if scrolling is active @@ -267,13 +272,13 @@ function rcube_mail_ui()          body = $(document.body),          floating = footer.hasClass('floating'),          overflow = body.outerHeight(true) > $(window).height(); +        if (overflow != floating) {          var action = overflow ? 'addClass' : 'removeClass';          footer[action]('floating');          body[action]('floatingbuttons');        } -    }) - +    });    }    /** @@ -315,7 +320,6 @@ function rcube_mail_ui()     */    function layout_messageview()    { -    $('#messagecontent').css('top', ($('#messageheader').outerHeight() + 10) + 'px');      $('#message-objects div a').addClass('button');      if (!$('#attachment-list li').length) { @@ -461,7 +465,7 @@ function rcube_mail_ui()      var button = $(e.target),        frame = $('#mailpreviewframe'),        visible = !frame.is(':visible'), -      splitter = mailviewsplit.pos || parseInt(bw.get_cookie('mailviewsplitter') || 320), +      splitter = mailviewsplit.pos || parseInt(rcmail.get_cookie('mailviewsplitter') || 320),        topstyles, bottomstyles, uid;      frame.toggle(); @@ -508,13 +512,31 @@ function rcube_mail_ui()    {      $('#preview-shortheaders').toggle();      var full = $('#preview-allheaders').toggle(), -      button = $('a#previewheaderstoggle'); +      button = $('#previewheaderstoggle'); + +    if (!$('#headerstoggleall').length) +      $('#all-headers').toggle();      // add toggle button to full headers table -    if (full.is(':visible')) -      button.attr('href', '#hide').removeClass('add').addClass('remove') -    else -      button.attr('href', '#details').removeClass('remove').addClass('add') +    if (full.is(':visible')) { +      button.attr('href', '#hide').removeClass('add').addClass('remove'); +    } +    else { +      button.attr('href', '#details').removeClass('remove').addClass('add'); +    } +  } + + +  /** +   * Show/hide all message headers +   */ +  function toggle_all_headers(button) +  { +    rcmail.command('show-headers', '', button); +    $(button).remove(); +    $('#previewheaderstoggle span').css({bottom: '5px'}); + +    return false;    } @@ -847,6 +869,8 @@ function rcube_mail_ui()        // Select/unselect tab        $('#tab'+idx).toggleClass('selected', idx==index);      }); + +    resize();    }    /** @@ -974,7 +998,7 @@ function rcube_splitter(p)        $(window).resize(onResize);      // read saved position from cookie -    var cookie = bw.get_cookie(this.id); +    var cookie = rcmail.get_cookie(this.id);      if (cookie && !isNaN(cookie)) {        this.pos = parseFloat(cookie);        this.resize(); @@ -1135,7 +1159,7 @@ function rcube_splitter(p)    {      var exp = new Date();      exp.setYear(exp.getFullYear() + 1); -    bw.set_cookie(this.id, this.pos, exp); +    rcmail.set_cookie(this.id, this.pos, exp);    };  } // end class rcube_splitter diff --git a/tests/Framework/BaseReplacer.php b/tests/Framework/BaseReplacer.php new file mode 100644 index 000000000..e00b9e5eb --- /dev/null +++ b/tests/Framework/BaseReplacer.php @@ -0,0 +1,20 @@ +<?php + +/** + * Test class to test rcube_base_replacer class + * + * @package Tests + */ +class Framework_BaseReplacer extends PHPUnit_Framework_TestCase +{ + +    /** +     * Class constructor +     */ +    function test_class() +    { +        $object = new rcube_base_replacer('test'); + +        $this->assertInstanceOf('rcube_base_replacer', $object, "Class constructor"); +    } +} diff --git a/tests/Framework/Browser.php b/tests/Framework/Browser.php new file mode 100644 index 000000000..c3860d8a3 --- /dev/null +++ b/tests/Framework/Browser.php @@ -0,0 +1,20 @@ +<?php + +/** + * Test class to test rcube_browser class + * + * @package Tests + */ +class Framework_Browser extends PHPUnit_Framework_TestCase +{ + +    /** +     * Class constructor +     */ +    function test_class() +    { +        $object = new rcube_browser(); + +        $this->assertInstanceOf('rcube_browser', $object, "Class constructor"); +    } +} diff --git a/tests/Framework/Cache.php b/tests/Framework/Cache.php new file mode 100644 index 000000000..dc026a634 --- /dev/null +++ b/tests/Framework/Cache.php @@ -0,0 +1,20 @@ +<?php + +/** + * Test class to test rcube_cache class + * + * @package Tests + */ +class Framework_Cache extends PHPUnit_Framework_TestCase +{ + +    /** +     * Class constructor +     */ +    function test_class() +    { +        $object = new rcube_cache('db', 1); + +        $this->assertInstanceOf('rcube_cache', $object, "Class constructor"); +    } +} diff --git a/tests/Framework/Charset.php b/tests/Framework/Charset.php new file mode 100644 index 000000000..9e3fad4d3 --- /dev/null +++ b/tests/Framework/Charset.php @@ -0,0 +1,28 @@ +<?php + +/** + * Test class to test rcube_charset class + * + * @package Tests + */ +class Framework_Charset extends PHPUnit_Framework_TestCase +{ + +    /** +     * Data for test_clean() +     */ +    function data_clean() +    { +        return array( +            array('', '', 'Empty string'), +        ); +    } + +    /** +     * @dataProvider data_clean +     */ +    function test_clean($input, $output, $title) +    { +        $this->assertEquals(rcube_charset::clean($input), $output, $title); +    } +} diff --git a/tests/Framework/ContentFilter.php b/tests/Framework/ContentFilter.php new file mode 100644 index 000000000..9bee9368b --- /dev/null +++ b/tests/Framework/ContentFilter.php @@ -0,0 +1,20 @@ +<?php + +/** + * Test class to test rcube_content_filter class + * + * @package Tests + */ +class Framework_ContentFilter extends PHPUnit_Framework_TestCase +{ + +    /** +     * Class constructor +     */ +    function test_class() +    { +        $object = new rcube_content_filter(); + +        $this->assertInstanceOf('rcube_content_filter', $object, "Class constructor"); +    } +} diff --git a/tests/Framework/Html.php b/tests/Framework/Html.php new file mode 100644 index 000000000..8a27baca8 --- /dev/null +++ b/tests/Framework/Html.php @@ -0,0 +1,46 @@ +<?php + +/** + * Test class to test rcube_html class + * + * @package Tests + */ +class Framework_Html extends PHPUnit_Framework_TestCase +{ + +    /** +     * Class constructor +     */ +    function test_class() +    { +        $object = new html; + +        $this->assertInstanceOf('html', $object, "Class constructor"); +    } + +    /** +     * Data for test_quote() +     */ +    function data_quote() +    { +        return array( +            array('abc', 'abc'), +            array('?', '?'), +            array('"', '"'), +            array('<', '<'), +            array('>', '>'), +            array('&', '&'), +            array('&', '&amp;'), +            array('&', '&', true), +        ); +    } + +    /** +     * Test for quote() +     * @dataProvider data_quote +     */ +    function test_quote($str, $result, $validate = false) +    { +        $this->assertEquals(html::quote($str, $validate), $result); +    } +} diff --git a/tests/Framework/Image.php b/tests/Framework/Image.php new file mode 100644 index 000000000..31e852042 --- /dev/null +++ b/tests/Framework/Image.php @@ -0,0 +1,20 @@ +<?php + +/** + * Test class to test rcube_image class + * + * @package Tests + */ +class Framework_Image extends PHPUnit_Framework_TestCase +{ + +    /** +     * Class constructor +     */ +    function test_class() +    { +        $object = new rcube_image('test'); + +        $this->assertInstanceOf('rcube_image', $object, "Class constructor"); +    } +} diff --git a/tests/Framework/Imap.php b/tests/Framework/Imap.php new file mode 100644 index 000000000..3f52e07be --- /dev/null +++ b/tests/Framework/Imap.php @@ -0,0 +1,20 @@ +<?php + +/** + * Test class to test rcube_imap class + * + * @package Tests + */ +class Framework_Imap extends PHPUnit_Framework_TestCase +{ + +    /** +     * Class constructor +     */ +    function test_class() +    { +        $object = new rcube_imap; + +        $this->assertInstanceOf('rcube_imap', $object, "Class constructor"); +    } +} diff --git a/tests/Framework/ImapGeneric.php b/tests/Framework/ImapGeneric.php new file mode 100644 index 000000000..0b2cc3d53 --- /dev/null +++ b/tests/Framework/ImapGeneric.php @@ -0,0 +1,20 @@ +<?php + +/** + * Test class to test rcube_imap_generic class + * + * @package Tests + */ +class Framework_ImapGeneric extends PHPUnit_Framework_TestCase +{ + +    /** +     * Class constructor +     */ +    function test_class() +    { +        $object = new rcube_imap_generic; + +        $this->assertInstanceOf('rcube_imap_generic', $object, "Class constructor"); +    } +} diff --git a/tests/Framework/MessageHeader.php b/tests/Framework/MessageHeader.php new file mode 100644 index 000000000..e5bc1752f --- /dev/null +++ b/tests/Framework/MessageHeader.php @@ -0,0 +1,20 @@ +<?php + +/** + * Test class to test rcube_message_header class + * + * @package Tests + */ +class Framework_MessageHeader extends PHPUnit_Framework_TestCase +{ + +    /** +     * Class constructor +     */ +    function test_class() +    { +        $object = new rcube_message_header; + +        $this->assertInstanceOf('rcube_message_header', $object, "Class constructor"); +    } +} diff --git a/tests/Framework/MessagePart.php b/tests/Framework/MessagePart.php new file mode 100644 index 000000000..deb426024 --- /dev/null +++ b/tests/Framework/MessagePart.php @@ -0,0 +1,20 @@ +<?php + +/** + * Test class to test rcube_message_part class + * + * @package Tests + */ +class Framework_MessagePart extends PHPUnit_Framework_TestCase +{ + +    /** +     * Class constructor +     */ +    function test_class() +    { +        $object = new rcube_message_part; + +        $this->assertInstanceOf('rcube_message_part', $object, "Class constructor"); +    } +} diff --git a/tests/Framework/Mime.php b/tests/Framework/Mime.php new file mode 100644 index 000000000..dcd55992a --- /dev/null +++ b/tests/Framework/Mime.php @@ -0,0 +1,123 @@ +<?php + +/** + * Test class to test rcube_mime class + * + * @package Tests + */ +class Framework_Mime extends PHPUnit_Framework_TestCase +{ + +    /** +     * Test decoding of single e-mail address strings +     * Uses rcube_mime::decode_address_list() +     */ +    function test_decode_single_address() +    { +        $headers = array( +            0  => 'test@domain.tld', +            1  => '<test@domain.tld>', +            2  => 'Test <test@domain.tld>', +            3  => 'Test Test <test@domain.tld>', +            4  => 'Test Test<test@domain.tld>', +            5  => '"Test Test" <test@domain.tld>', +            6  => '"Test Test"<test@domain.tld>', +            7  => '"Test \\" Test" <test@domain.tld>', +            8  => '"Test<Test" <test@domain.tld>', +            9  => '=?ISO-8859-1?B?VGVzdAo=?= <test@domain.tld>', +            10 => '=?ISO-8859-1?B?VGVzdAo=?=<test@domain.tld>', // #1487068 +            // comments in address (#1487673) +            11 => 'Test (comment) <test@domain.tld>', +            12 => '"Test" (comment) <test@domain.tld>', +            13 => '"Test (comment)" (comment) <test@domain.tld>', +            14 => '(comment) <test@domain.tld>', +            15 => 'Test <test@(comment)domain.tld>', +            16 => 'Test Test ((comment)) <test@domain.tld>', +            17 => 'test@domain.tld (comment)', +            18 => '"Test,Test" <test@domain.tld>', +            // 1487939 +            19 => 'Test <"test test"@domain.tld>', +            20 => '<"test test"@domain.tld>', +            21 => '"test test"@domain.tld', +        ); + +        $results = array( +            0  => array(1, '', 'test@domain.tld'), +            1  => array(1, '', 'test@domain.tld'), +            2  => array(1, 'Test', 'test@domain.tld'), +            3  => array(1, 'Test Test', 'test@domain.tld'), +            4  => array(1, 'Test Test', 'test@domain.tld'), +            5  => array(1, 'Test Test', 'test@domain.tld'), +            6  => array(1, 'Test Test', 'test@domain.tld'), +            7  => array(1, 'Test " Test', 'test@domain.tld'), +            8  => array(1, 'Test<Test', 'test@domain.tld'), +            9  => array(1, 'Test', 'test@domain.tld'), +            10 => array(1, 'Test', 'test@domain.tld'), +            11 => array(1, 'Test', 'test@domain.tld'), +            12 => array(1, 'Test', 'test@domain.tld'), +            13 => array(1, 'Test (comment)', 'test@domain.tld'), +            14 => array(1, '', 'test@domain.tld'), +            15 => array(1, 'Test', 'test@domain.tld'), +            16 => array(1, 'Test Test', 'test@domain.tld'), +            17 => array(1, '', 'test@domain.tld'), +            18 => array(1, 'Test,Test', 'test@domain.tld'), +            19 => array(1, 'Test', '"test test"@domain.tld'), +            20 => array(1, '', '"test test"@domain.tld'), +            21 => array(1, '', '"test test"@domain.tld'), +        ); + +        foreach ($headers as $idx => $header) { +            $res = rcube_mime::decode_address_list($header); + +            $this->assertEquals($results[$idx][0], count($res), "Rows number in result for header: " . $header); +            $this->assertEquals($results[$idx][1], $res[1]['name'], "Name part decoding for header: " . $header); +            $this->assertEquals($results[$idx][2], $res[1]['mailto'], "Email part decoding for header: " . $header); +        } +    } + +    /** +     * Test decoding of header values +     * Uses rcube_mime::decode_mime_string() +     */ +    function test_header_decode_qp() +    { +        $test = array( +            // #1488232: invalid character "?" +            'quoted-printable (1)' => array( +                'in'  => '=?utf-8?Q?Certifica=C3=A7=C3=A3??=', +                'out' => 'Certifica=C3=A7=C3=A3?', +            ), +            'quoted-printable (2)' => array( +                'in'  => '=?utf-8?Q?Certifica=?= =?utf-8?Q?C3=A7=C3=A3?=', +                'out' => 'Certifica=C3=A7=C3=A3', +            ), +            'quoted-printable (3)' => array( +                'in'  => '=?utf-8?Q??= =?utf-8?Q??=', +                'out' => '', +            ), +            'quoted-printable (4)' => array( +                'in'  => '=?utf-8?Q??= a =?utf-8?Q??=', +                'out' => ' a ', +            ), +            'quoted-printable (5)' => array( +                'in'  => '=?utf-8?Q?a?= =?utf-8?Q?b?=', +                'out' => 'ab', +            ), +            'quoted-printable (6)' => array( +                'in'  => '=?utf-8?Q?   ?= =?utf-8?Q?a?=', +                'out' => '   a', +            ), +            'quoted-printable (7)' => array( +                'in'  => '=?utf-8?Q?___?= =?utf-8?Q?a?=', +                'out' => '   a', +            ), +        ); + +        foreach ($test as $idx => $item) { +            $res = rcube_mime::decode_mime_string($item['in'], 'UTF-8'); +            $res = quoted_printable_encode($res); + +            $this->assertEquals($item['out'], $res, "Header decoding for: " . $idx); +        } +    } +} diff --git a/tests/Framework/Rcube.php b/tests/Framework/Rcube.php new file mode 100644 index 000000000..637558dc9 --- /dev/null +++ b/tests/Framework/Rcube.php @@ -0,0 +1,20 @@ +<?php + +/** + * Test class to test rcube class + * + * @package Tests + */ +class Framework_Rcube extends PHPUnit_Framework_TestCase +{ + +    /** +     * Class constructor +     */ +    function test_class() +    { +        $object = rcube::get_instance(); + +        $this->assertInstanceOf('rcube', $object, "Class singleton"); +    } +} diff --git a/tests/Framework/ResultIndex.php b/tests/Framework/ResultIndex.php new file mode 100644 index 000000000..efbba6da7 --- /dev/null +++ b/tests/Framework/ResultIndex.php @@ -0,0 +1,20 @@ +<?php + +/** + * Test class to test rcube_result_index class + * + * @package Tests + */ +class Framework_ResultIndex extends PHPUnit_Framework_TestCase +{ + +    /** +     * Class constructor +     */ +    function test_class() +    { +        $object = new rcube_result_index; + +        $this->assertInstanceOf('rcube_result_index', $object, "Class constructor"); +    } +} diff --git a/tests/Framework/ResultSet.php b/tests/Framework/ResultSet.php new file mode 100644 index 000000000..2d04e53c0 --- /dev/null +++ b/tests/Framework/ResultSet.php @@ -0,0 +1,20 @@ +<?php + +/** + * Test class to test rcube_result_set class + * + * @package Tests + */ +class Framework_ResultSet extends PHPUnit_Framework_TestCase +{ + +    /** +     * Class constructor +     */ +    function test_class() +    { +        $object = new rcube_result_set; + +        $this->assertInstanceOf('rcube_result_set', $object, "Class constructor"); +    } +} diff --git a/tests/Framework/ResultThread.php b/tests/Framework/ResultThread.php new file mode 100644 index 000000000..f980845cc --- /dev/null +++ b/tests/Framework/ResultThread.php @@ -0,0 +1,20 @@ +<?php + +/** + * Test class to test rcube_result_thread class + * + * @package Tests + */ +class Framework_ResultThread extends PHPUnit_Framework_TestCase +{ + +    /** +     * Class constructor +     */ +    function test_class() +    { +        $object = new rcube_result_thread; + +        $this->assertInstanceOf('rcube_result_thread', $object, "Class constructor"); +    } +} diff --git a/tests/Framework/Shared.php b/tests/Framework/Shared.php new file mode 100644 index 000000000..99ef829da --- /dev/null +++ b/tests/Framework/Shared.php @@ -0,0 +1,204 @@ +<?php + +/** + * Test class to test rcube_shared functions + * + * @package Tests + */ +class Framework_Shared extends PHPUnit_Framework_TestCase +{ + +    /** +     * rcube_shared.inc: in_array_nocase() +     */ +    function test_in_array_nocase() +    { +        $haystack = array('Test'); +        $needle = 'test'; +        $result = in_array_nocase($needle, $haystack); + +        $this->assertTrue($result, $title); + +        $result = in_array_nocase($needle, null); + +        $this->assertFalse($result, $title); +    } + +    /** +     * rcube_shared.inc: get_boolean() +     */ +    function test_get_boolean() +    { +        $input = array( +            false, 'false', '0', 'no', 'off', 'nein', 'FALSE', '', null, +        ); + +        foreach ($input as $idx => $value) { +            $this->assertFalse(get_boolean($value), "Invalid result for $idx test item"); +        } + +        $input = array( +            true, 'true', '1', 1, 'yes', 'anything', 1000, +        ); + +        foreach ($input as $idx => $value) { +            $this->assertTrue(get_boolean($value), "Invalid result for $idx test item"); +        } +    } + +    /** +     * rcube_shared.inc: parse_bytes() +     */ +    function test_parse_bytes() +    { +        $data = array( +            '1'      => 1, +            '1024'   => 1024, +            '2k'     => 2 * 1024, +            '2 k'     => 2 * 1024, +            '2kb'    => 2 * 1024, +            '2kB'    => 2 * 1024, +            '2m'     => 2 * 1048576, +            '2 m'     => 2 * 1048576, +            '2mb'    => 2 * 1048576, +            '2mB'    => 2 * 1048576, +            '2g'     => 2 * 1024 * 1048576, +            '2 g'     => 2 * 1024 * 1048576, +            '2gb'    => 2 * 1024 * 1048576, +            '2gB'    => 2 * 1024 * 1048576, +        ); + +        foreach ($data as $value => $expected) { +            $result = parse_bytes($value); +            $this->assertEquals($expected, $result, "Invalid parse_bytes() result for $value"); +        } +    } + +    /** +     * rcube_shared.inc: slashify() +     */ +    function test_slashify() +    { +        $data = array( +            'test'    => 'test/', +            'test/'   => 'test/', +            ''        => '/', +            "\\"      => "\\/", +        ); + +        foreach ($data as $value => $expected) { +            $result = slashify($value); +            $this->assertEquals($expected, $result, "Invalid slashify() result for $value"); +        } + +    } + +    /** +     * rcube_shared.inc: unslashify() +     */ +    function test_unslashify() +    { +        $data = array( +            'test'      => 'test', +            'test/'     => 'test', +            '/'         => '', +            "\\/"       => "\\", +            'test/test' => 'test/test', +            'test//'    => 'test', +        ); + +        foreach ($data as $value => $expected) { +            $result = unslashify($value); +            $this->assertEquals($expected, $result, "Invalid unslashify() result for $value"); +        } + +    } + +    /** +     * rcube_shared.inc: get_offset_sec() +     */ +    function test_get_offset_sec() +    { +        $data = array( +            '1s'    => 1, +            '1m'    => 1 * 60, +            '1h'    => 1 * 60 * 60, +            '1d'    => 1 * 60 * 60 * 24, +            '1w'    => 1 * 60 * 60 * 24 * 7, +            '1y'    => (int) '1y', +            100     => 100, +            '100'   => 100, +        ); + +        foreach ($data as $value => $expected) { +            $result = get_offset_sec($value); +            $this->assertEquals($expected, $result, "Invalid get_offset_sec() result for $value"); +        } + +    } + +    /** +     * rcube_shared.inc: array_keys_recursive() +     */ +    function test_array_keys_recursive() +    { +        $input = array( +            'one' => array( +                'two' => array( +                    'three' => array(), +                    'four' => 'something', +                ), +            ), +            'five' => 'test', +        ); + +        $result     = array_keys_recursive($input); +        $input_str  = 'one,two,three,four,five'; +        $result_str = implode(',', $result); + +        $this->assertEquals($input_str, $result_str, "Invalid array_keys_recursive() result"); +    } + +    /** +     * rcube_shared.inc: format_email() +     */ +    function test_format_email() +    { +        $data = array( +            ''                 => '', +            'test'             => 'test', +            'test@test.tld'    => 'test@test.tld', +            'test@[127.0.0.1]' => 'test@[127.0.0.1]', +            'TEST@TEST.TLD'    => 'TEST@test.tld', +        ); + +        foreach ($data as $value => $expected) { +            $result = format_email($value); +            $this->assertEquals($expected, $result, "Invalid format_email() result for $value"); +        } + +    } + +    /** +     * rcube_shared.inc: format_email_recipient() +     */ +    function test_format_email_recipient() +    { +        $data = array( +            ''                          => array(''), +            'test'                      => array('test'), +            'test@test.tld'             => array('test@test.tld'), +            'test@[127.0.0.1]'          => array('test@[127.0.0.1]'), +            'TEST@TEST.TLD'             => array('TEST@TEST.TLD'), +            'TEST <test@test.tld>'      => array('test@test.tld', 'TEST'), +            '"TEST\"" <test@test.tld>'  => array('test@test.tld', 'TEST"'), +        ); + +        foreach ($data as $expected => $value) { +            $result = format_email_recipient($value[0], $value[1]); +            $this->assertEquals($expected, $result, "Invalid format_email_recipient()"); +        } + +    } + +} diff --git a/tests/Framework/Smtp.php b/tests/Framework/Smtp.php new file mode 100644 index 000000000..4bd78d097 --- /dev/null +++ b/tests/Framework/Smtp.php @@ -0,0 +1,20 @@ +<?php + +/** + * Test class to test rcube_smtp class + * + * @package Tests + */ +class Framework_Smtp extends PHPUnit_Framework_TestCase +{ + +    /** +     * Class constructor +     */ +    function test_class() +    { +        $object = new rcube_smtp; + +        $this->assertInstanceOf('rcube_smtp', $object, "Class constructor"); +    } +} diff --git a/tests/Framework/Spellchecker.php b/tests/Framework/Spellchecker.php new file mode 100644 index 000000000..9c3e92ffd --- /dev/null +++ b/tests/Framework/Spellchecker.php @@ -0,0 +1,20 @@ +<?php + +/** + * Test class to test rcube_spellchecker class + * + * @package Tests + */ +class Framework_Spellchecker extends PHPUnit_Framework_TestCase +{ + +    /** +     * Class constructor +     */ +    function test_class() +    { +        $object = new rcube_spellchecker; + +        $this->assertInstanceOf('rcube_spellchecker', $object, "Class constructor"); +    } +} diff --git a/tests/Framework/StringReplacer.php b/tests/Framework/StringReplacer.php new file mode 100644 index 000000000..11210c0da --- /dev/null +++ b/tests/Framework/StringReplacer.php @@ -0,0 +1,20 @@ +<?php + +/** + * Test class to test rcube_string_replacer class + * + * @package Tests + */ +class Framework_StringReplacer extends PHPUnit_Framework_TestCase +{ + +    /** +     * Class constructor +     */ +    function test_class() +    { +        $sr = new rcube_string_replacer; + +        $this->assertInstanceOf('rcube_string_replacer', $sr, "Class constructor"); +    } +} diff --git a/tests/Framework/User.php b/tests/Framework/User.php new file mode 100644 index 000000000..3b1983c58 --- /dev/null +++ b/tests/Framework/User.php @@ -0,0 +1,20 @@ +<?php + +/** + * Test class to test rcube_user class + * + * @package Tests + */ +class Framework_User extends PHPUnit_Framework_TestCase +{ + +    /** +     * Class constructor +     */ +    function test_class() +    { +        $user = new rcube_user; + +        $this->assertInstanceOf('rcube_user', $user, "Class constructor"); +    } +} diff --git a/tests/Framework/Utils.php b/tests/Framework/Utils.php new file mode 100644 index 000000000..e58835956 --- /dev/null +++ b/tests/Framework/Utils.php @@ -0,0 +1,196 @@ +<?php + +/** + * Test class to test rcube_utils class + * + * @package Tests + */ +class Framework_Utils extends PHPUnit_Framework_TestCase +{ + +    /** +     * Valid email addresses for test_valid_email() +     */ +    function data_valid_email() +    { +        return array( +            array('email@domain.com', 'Valid email'), +            array('firstname.lastname@domain.com', 'Email contains dot in the address field'), +            array('email@subdomain.domain.com', 'Email contains dot with subdomain'), +            array('firstname+lastname@domain.com', 'Plus sign is considered valid character'), +            array('email@[123.123.123.123]', 'Square bracket around IP address'), +            array('email@[IPv6:::1]', 'Square bracket around IPv6 address (1)'), +            array('email@[IPv6:::1.2.3.4]', 'Square bracket around IPv6 address (2)'), +            array('email@[IPv6:2001:2d12:c4fe:5afe::1]', 'Square bracket around IPv6 address (3)'), +            array('"email"@domain.com', 'Quotes around email is considered valid'), +            array('1234567890@domain.com', 'Digits in address are valid'), +            array('email@domain-one.com', 'Dash in domain name is valid'), +            array('_______@domain.com', 'Underscore in the address field is valid'), +            array('email@domain.name', '.name is valid Top Level Domain name'), +            array('email@domain.co.jp', 'Dot in Top Level Domain name also considered valid (use co.jp as example here)'), +            array('firstname-lastname@domain.com', 'Dash in address field is valid'), +        ); +    } + +    /** +     * Invalid email addresses for test_invalid_email() +     */ +    function data_invalid_email() +    { +        return array( +            array('plainaddress', 'Missing @ sign and domain'), +            array('#@%^%#$@#$@#.com', 'Garbage'), +            array('@domain.com', 'Missing username'), +            array('Joe Smith <email@domain.com>', 'Encoded html within email is invalid'), +            array('email.domain.com', 'Missing @'), +            array('email@domain@domain.com', 'Two @ sign'), +            array('.email@domain.com', 'Leading dot in address is not allowed'), +            array('email.@domain.com', 'Trailing dot in address is not allowed'), +            array('email..email@domain.com', 'Multiple dots'), +            array('あいうえお@domain.com', 'Unicode char as address'), +            array('email@domain.com (Joe Smith)', 'Text followed email is not allowed'), +            array('email@domain', 'Missing top level domain (.com/.net/.org/etc)'), +            array('email@-domain.com', 'Leading dash in front of domain is invalid'), +//            array('email@domain.web', '.web is not a valid top level domain'), +            array('email@123.123.123.123', 'IP address without brackets'), +            array('email@2001:2d12:c4fe:5afe::1', 'IPv6 address without brackets'), +            array('email@IPv6:2001:2d12:c4fe:5afe::1', 'IPv6 address without brackets (2)'), +            array('email@[111.222.333.44444]', 'Invalid IP format'), +            array('email@[111.222.255.257]', 'Invalid IP format (2)'), +            array('email@[.222.255.257]', 'Invalid IP format (3)'), +            array('email@[::1]', 'Invalid IPv6 format (1)'), +            array('email@[IPv6:2001:23x2:1]', 'Invalid IPv6 format (2)'), +            array('email@[IPv6:1111:2222:33333::4444:5555]', 'Invalid IPv6 format (3)'), +            array('email@[IPv6:1111::3333::4444:5555]', 'Invalid IPv6 format (4)'), +            array('email@domain..com', 'Multiple dot in the domain portion is invalid'), +        ); +    } + +    /** +     * @dataProvider data_valid_email +     */ +    function test_valid_email($email, $title) +    { +        $this->assertTrue(rcube_utils::check_email($email, false), $title); +    } + +    /** +     * @dataProvider data_invalid_email +     */ +    function test_invalid_email($email, $title) +    { +        $this->assertFalse(rcube_utils::check_email($email, false), $title); +    } + +    /** +     * Valid IP addresses for test_valid_ip() +     */ +    function data_valid_ip() +    { +        return array( +            array('0.0.0.0'), +            array('123.123.123.123'), +            array('::'), +            array('::1'), +            array('::1.2.3.4'), +            array('2001:2d12:c4fe:5afe::1'), +        ); +    } + +    /** +     * Valid IP addresses for test_invalid_ip() +     */ +    function data_invalid_ip() +    { +        return array( +            array(''), +            array(0), +            array('123.123.123.1234'), +            array('1.1.1.1.1'), +            array('::1.2.3.260'), +            array('::1.0'), +            array('2001::c4fe:5afe::1'), +        ); +    } + +    /** +     * @dataProvider data_valid_ip +     */ +    function test_valid_ip($ip) +    { +        $this->assertTrue(rcube_utils::check_ip($ip)); +    } + +    /** +     * @dataProvider data_invalid_ip +     */ +    function test_invalid_ip($ip) +    { +        $this->assertFalse(rcube_utils::check_ip($ip)); +    } + +    /** +     * Data for test_rep_specialchars_output() +     */ +    function data_rep_specialchars_output() +    { +        return array( +            array('', '', 'abc', 'abc'), +            array('', '', '?', '?'), +            array('', '', '"', '"'), +            array('', '', '<', '<'), +            array('', '', '>', '>'), +            array('', '', '&', '&'), +            array('', '', '&', '&amp;'), +            array('', '', '<a>', '<a>'), +            array('', 'remove', '<a>', ''), +        ); +    } + +    /** +     * Test for rep_specialchars_output +     * @dataProvider data_rep_specialchars_output +     */ +    function test_rep_specialchars_output($type, $mode, $str, $res) +    { +        $result = rcube_utils::rep_specialchars_output( +            $str, $type ? $type : 'html', $mode ? $mode : 'strict'); + +        $this->assertEquals($result, $res); +    } + +    /** +     * rcube_utils::mod_css_styles() +     */ +    function test_mod_css_styles() +    { +        $css = file_get_contents(TESTS_DIR . 'src/valid.css'); +        $mod = rcube_utils::mod_css_styles($css, 'rcmbody'); + +        $this->assertRegExp('/#rcmbody\s+\{/', $mod, "Replace body style definition"); +        $this->assertRegExp('/#rcmbody h1\s\{/', $mod, "Prefix tag styles (single)"); +        $this->assertRegExp('/#rcmbody h1, #rcmbody h2, #rcmbody h3, #rcmbody textarea\s+\{/', $mod, "Prefix tag styles (multiple)"); +        $this->assertRegExp('/#rcmbody \.noscript\s+\{/', $mod, "Prefix class styles"); +    } + +    /** +     * rcube_utils::mod_css_styles() +     */ +    function test_mod_css_styles_xss() +    { +        $mod = rcube_utils::mod_css_styles("body.main2cols { background-image: url('../images/leftcol.png'); }", 'rcmbody'); +        $this->assertEquals("/* evil! */", $mod, "No url() values allowed"); + +        $mod = rcube_utils::mod_css_styles("@import url('http://localhost/somestuff/css/master.css');", 'rcmbody'); +        $this->assertEquals("/* evil! */", $mod, "No import statements"); + +        $mod = rcube_utils::mod_css_styles("left:expression(document.body.offsetWidth-20)", 'rcmbody'); +        $this->assertEquals("/* evil! */", $mod, "No expression properties"); + +        $mod = rcube_utils::mod_css_styles("left:exp/*  */ression( alert('xss3') )", 'rcmbody'); +        $this->assertEquals("/* evil! */", $mod, "Don't allow encoding quirks"); + +        $mod = rcube_utils::mod_css_styles("background:\\0075\\0072\\006c( javascript:alert('xss') )", 'rcmbody'); +        $this->assertEquals("/* evil! */", $mod, "Don't allow encoding quirks (2)"); +    } +} diff --git a/tests/Framework/VCard.php b/tests/Framework/VCard.php new file mode 100644 index 000000000..56ca9d721 --- /dev/null +++ b/tests/Framework/VCard.php @@ -0,0 +1,73 @@ +<?php + +/** + * Unit tests for class rcube_vcard + * + * @package Tests + */ +class Framework_VCard extends PHPUnit_Framework_TestCase +{ + +    function _srcpath($fn) +    { +        return realpath(dirname(__FILE__) . '/../src/' . $fn); +    } + +    function test_parse_one() +    { +        $vcard = new rcube_vcard(file_get_contents($this->_srcpath('apple.vcf'))); + +        $this->assertTrue($vcard->business, "Identify as business record"); +        $this->assertEquals("Apple Computer AG", $vcard->displayname, "FN => displayname"); +        $this->assertEquals("", $vcard->firstname, "No person name set"); +    } + +    function test_parse_two() +    { +        $vcard = new rcube_vcard(file_get_contents($this->_srcpath('johndoe.vcf')), null); + +        $this->assertFalse($vcard->business, "Identify as private record"); +        $this->assertEquals("John Doë", $vcard->displayname, "Decode according to charset attribute"); +        $this->assertEquals("roundcube.net", $vcard->organization, "Test organization field"); +        $this->assertCount(2, $vcard->email, "List two e-mail addresses"); +        $this->assertEquals("roundcube@gmail.com", $vcard->email[0], "Use PREF e-mail as primary"); +    } + +    function test_import() +    { +        $input = file_get_contents($this->_srcpath('apple.vcf')); +        $input .= file_get_contents($this->_srcpath('johndoe.vcf')); + +        $vcards = rcube_vcard::import($input); + +        $this->assertCount(2, $vcards, "Detected 2 vcards"); +        $this->assertEquals("Apple Computer AG", $vcards[0]->displayname, "FN => displayname"); +        $this->assertEquals("John Doë", $vcards[1]->displayname, "Displayname with correct charset"); + +        // http://trac.roundcube.net/ticket/1485542 +        $vcards2 = rcube_vcard::import(file_get_contents($this->_srcpath('thebat.vcf'))); +        $this->assertEquals("Iksiñski", $vcards2[0]->surname, "Detect charset in encoded values"); +    } + +    function test_import_photo_encoding() +    { +        $input = file_get_contents($this->_srcpath('photo.vcf')); + +        $vcards = rcube_vcard::import($input); +        $vcard = $vcards[0]->get_assoc(); + +        $this->assertCount(1, $vcards, "Detected 1 vcard"); + +        // ENCODING=b case (#1488683) +        $this->assertEquals("/9j/4AAQSkZJRgABAQA", substr(base64_encode($vcard['photo']), 0, 19), "Photo decoding"); +        $this->assertEquals("Müller", $vcard['surname'], "Unicode characters"); +    } + +    function test_encodings() +    { +        $input = file_get_contents($this->_srcpath('utf-16_sample.vcf')); + +        $vcards = rcube_vcard::import($input); +        $this->assertEquals("Ǽgean ĽdaMonté", $vcards[0]->displayname, "Decoded from UTF-16"); +    } +} diff --git a/tests/html_to_text.php b/tests/HtmlToText.php index aabc1a800..b90c61adf 100644 --- a/tests/html_to_text.php +++ b/tests/HtmlToText.php @@ -5,18 +5,12 @@   *   * @package Tests   */ -class rcube_test_html2text extends UnitTestCase +class HtmlToText extends PHPUnit_Framework_TestCase  { -    function __construct() +    function data_html2text()      { -        $this->UnitTestCase("HTML-to-Text conversion tests"); - -    } - -    function test_html2text() -    { -        $data = array( +        return array(              0 => array(                  'title' => 'Test entry',                  'in'    => '', @@ -48,14 +42,18 @@ class rcube_test_html2text extends UnitTestCase                  'out'   => 'Ś',              ),          ); +    } +    /** +     * @dataProvider data_html2text +     */ +    function test_html2text($title, $in, $out) +    {          $ht = new html2text(null, false, false); -        foreach ($data as $idx => $item) { -            $ht->set_html($item['in']); -            $res = $ht->get_text(); -            $this->assertEqual($item['out'], $res, $item['title'] . "($idx)"); -        } -    } +        $ht->set_html($in); +        $res = $ht->get_text(); +        $this->assertEquals($out, $res, $title); +    }  } diff --git a/tests/MailFunc.php b/tests/MailFunc.php new file mode 100644 index 000000000..967277c2a --- /dev/null +++ b/tests/MailFunc.php @@ -0,0 +1,172 @@ +<?php + +/** + * Test class to test steps/mail/func.inc functions + * + * @package Tests + */ +class MailFunc extends PHPUnit_Framework_TestCase +{ + +    function setUp() +    { +        // simulate environment to successfully include func.inc +        $GLOBALS['RCMAIL'] = $RCMAIL = rcmail::get_instance(); +        $GLOBALS['OUTPUT'] = $OUTPUT = $RCMAIL->load_gui(); +        $RCMAIL->action = 'autocomplete'; +        $RCMAIL->storage_init(false); + +        require_once INSTALL_PATH . 'program/steps/mail/func.inc'; + +        $GLOBALS['EMAIL_ADDRESS_PATTERN'] = $EMAIL_ADDRESS_PATTERN; +    } + +    /** +     * Helper method to create a HTML message part object +     */ +    function get_html_part($body) +    { +        $part = new rcube_message_part; +        $part->ctype_primary = 'text'; +        $part->ctype_secondary = 'html'; +        $part->body = file_get_contents(TESTS_DIR . $body); +        $part->replaces = array(); +        return $part; +    } + + +    /** +     * Test sanitization of a "normal" html message +     */ +    function test_html() +    { +        $part = $this->get_html_part('src/htmlbody.txt'); +        $part->replaces = array('ex1.jpg' => 'part_1.2.jpg', 'ex2.jpg' => 'part_1.2.jpg'); + +        // render HTML in normal mode +        $html = rcmail_html4inline(rcmail_print_body($part, array('safe' => false)), 'foo'); + +        $this->assertRegExp('/src="'.$part->replaces['ex1.jpg'].'"/', $html, "Replace reference to inline image"); +        $this->assertRegExp('#background="./program/resources/blocked.gif"#', $html, "Replace external background image"); +        $this->assertNotRegExp('/ex3.jpg/', $html, "No references to external images"); +        $this->assertNotRegExp('/<meta [^>]+>/', $html, "No meta tags allowed"); +        //$this->assertNoPattern('/<style [^>]+>/', $html, "No style tags allowed"); +        $this->assertNotRegExp('/<form [^>]+>/', $html, "No form tags allowed"); +        $this->assertRegExp('/Subscription form/', $html, "Include <form> contents"); +        $this->assertRegExp('/<!-- link ignored -->/', $html, "No external links allowed"); +        $this->assertRegExp('/<a[^>]+ target="_blank">/', $html, "Set target to _blank"); +        $this->assertTrue($GLOBALS['REMOTE_OBJECTS'], "Remote object detected"); + +        // render HTML in safe mode +        $html2 = rcmail_html4inline(rcmail_print_body($part, array('safe' => true)), 'foo'); + +        $this->assertRegExp('/<style [^>]+>/', $html2, "Allow styles in safe mode"); +        $this->assertRegExp('#src="http://evilsite.net/mailings/ex3.jpg"#', $html2, "Allow external images in HTML (safe mode)"); +        $this->assertRegExp("#url\('?http://evilsite.net/newsletter/image/bg/bg-64.jpg'?\)#", $html2, "Allow external images in CSS (safe mode)"); +        $css = '<link rel="stylesheet" .+_u=tmp-[a-z0-9]+\.css.+_action=modcss'; +        $this->assertRegExp('#'.$css.'#Ui', $html2, "Filter (anonymized) external styleseehts with utils/modcss.inc"); +    } + +    /** +     * Test the elimination of some trivial XSS vulnerabilities +     */ +    function test_html_xss() +    { +        $part = $this->get_html_part('src/htmlxss.txt'); +        $washed = rcmail_print_body($part, array('safe' => true)); + +        $this->assertNotRegExp('/src="skins/', $washed, "Remove local references"); +        $this->assertNotRegExp('/\son[a-z]+/', $washed, "Remove on* attributes"); + +        $html = rcmail_html4inline($washed, 'foo'); +        $this->assertNotRegExp('/onclick="return rcmail.command(\'compose\',\'xss@somehost.net\',this)"/', $html, "Clean mailto links"); +        $this->assertNotRegExp('/alert/', $html, "Remove alerts"); +    } + +    /** +     * Test HTML sanitization to fix the CSS Expression Input Validation Vulnerability +     * reported at http://www.securityfocus.com/bid/26800/ +     */ +    function test_html_xss2() +    { +        $part = $this->get_html_part('src/BID-26800.txt'); +        $washed = rcmail_html4inline(rcmail_print_body($part, array('safe' => true)), 'dabody', '', $attr, true); + +        $this->assertNotRegExp('/alert|expression|javascript|xss/', $washed, "Remove evil style blocks"); +        $this->assertNotRegExp('/font-style:italic/', $washed, "Allow valid styles"); +    } + +    /** +     * Test washtml class on non-unicode characters (#1487813) +     */ +    function test_washtml_utf8() +    { +        $part = $this->get_html_part('src/invalidchars.html'); +        $washed = rcmail_print_body($part); + +        $this->assertRegExp('/<p>символ<\/p>/', $washed, "Remove non-unicode characters from HTML message body"); +    } + +    /** +     * Test links pattern replacements in plaintext messages +     */ +    function test_plaintext() +    { +        $part = new rcube_message_part; +        $part->ctype_primary = 'text'; +        $part->ctype_secondary = 'plain'; +        $part->body = quoted_printable_decode(file_get_contents(TESTS_DIR . 'src/plainbody.txt')); +        $html = rcmail_print_body($part, array('safe' => true)); + +        $this->assertRegExp('/<a href="mailto:nobody@roundcube.net" onclick="return rcmail.command\(\'compose\',\'nobody@roundcube.net\',this\)">nobody@roundcube.net<\/a>/', $html, "Mailto links with onclick"); +        $this->assertRegExp('#<a href="http://www.apple.com/legal/privacy" target="_blank">http://www.apple.com/legal/privacy</a>#', $html, "Links with target=_blank"); +        $this->assertRegExp('#\\[<a href="http://example.com/\\?tx\\[a\\]=5" target="_blank">http://example.com/\\?tx\\[a\\]=5</a>\\]#', $html, "Links with square brackets"); +    } + +    /** +     * Test mailto links in html messages +     */ +    function test_mailto() +    { +        $part = $this->get_html_part('src/mailto.txt'); + +        // render HTML in normal mode +        $html = rcmail_html4inline(rcmail_print_body($part, array('safe' => false)), 'foo'); + +        $mailto = '<a href="mailto:me@me.com?subject=this is the subject&body=this is the body"' +            .' onclick="return rcmail.command(\'compose\',\'me@me.com?subject=this is the subject&body=this is the body\',this)">e-mail</a>'; + +        $this->assertRegExp('|'.preg_quote($mailto, '|').'|', $html, "Extended mailto links"); +    } + +    /** +     * Test the elimination of HTML comments +     */ +    function test_html_comments() +    { +        $part = $this->get_html_part('src/htmlcom.txt'); +        $washed = rcmail_print_body($part, array('safe' => true)); + +        // #1487759 +        $this->assertRegExp('|<p>test1</p>|', $washed, "Buggy HTML comments"); +        // but conditional comments (<!--[if ...) should be removed +        $this->assertNotRegExp('|<p>test2</p>|', $washed, "Conditional HTML comments"); +    } + +    /** +     * Test URI base resolving in HTML messages +     */ +    function test_resolve_base() +    { +        $html = file_get_contents(TESTS_DIR . 'src/htmlbase.txt'); +        $html = rcmail_resolve_base($html); + +        $this->assertRegExp('|src="http://alec\.pl/dir/img1\.gif"|', $html, "URI base resolving [1]"); +        $this->assertRegExp('|src="http://alec\.pl/dir/img2\.gif"|', $html, "URI base resolving [2]"); +        $this->assertRegExp('|src="http://alec\.pl/img3\.gif"|', $html, "URI base resolving [3]"); + +        // base resolving exceptions +        $this->assertRegExp('|src="cid:theCID"|', $html, "URI base resolving exception [1]"); +        $this->assertRegExp('|src="http://other\.domain\.tld/img3\.gif"|', $html, "URI base resolving exception [2]"); +    } +} diff --git a/tests/runtests.sh b/tests/bootstrap.php index 9cfeb0a25..a9e25610c 100755..100644 --- a/tests/runtests.sh +++ b/tests/bootstrap.php @@ -1,54 +1,35 @@ -#!/usr/bin/env php  <?php  /*   +-----------------------------------------------------------------------+ - | tests/runtests.sh                                                     | + | tests/bootstrap.php                                                   |   |                                                                       |   | This file is part of the Roundcube Webmail client                     | - | Copyright (C) 2009, The Roundcube Dev Team                            | + | Copyright (C) 2009-2012, The Roundcube Dev Team                       |   |                                                                       |   | Licensed under the GNU General Public License version 3 or            |   | any later version with exceptions for skins & plugins.                |   | See the README file for a full license statement.                     |   |                                                                       |   | PURPOSE:                                                              | - |   Run-script for unit tests based on http://simpletest.org            | - |   All .php files in this folder will be treated as tests              | + |   Environment initialization script for unit tests                    |   +-----------------------------------------------------------------------+   | Author: Thomas Bruederli <roundcube@gmail.com>                        | + | Author: Aleksander Machniak <alec@alec.pl>                            |   +-----------------------------------------------------------------------+  */  if (php_sapi_name() != 'cli')    die("Not in shell mode (php-cli)"); -if (!defined('SIMPLETEST'))   define('SIMPLETEST', '/www/simpletest/');  if (!defined('INSTALL_PATH')) define('INSTALL_PATH', realpath(dirname(__FILE__) . '/..') . '/' );  define('TESTS_DIR', dirname(__FILE__) . '/'); -define('RCMAIL_CONFIG_DIR', TESTS_DIR . 'config'); -require_once(SIMPLETEST . 'unit_tester.php'); -require_once(SIMPLETEST . 'reporter.php'); -require_once(INSTALL_PATH . 'program/include/iniset.php'); - -if (count($_SERVER['argv']) > 1) { -  $testfiles = array(); -  for ($i=1; $i < count($_SERVER['argv']); $i++) -    $testfiles[] = realpath('./' . $_SERVER['argv'][$i]); +if (@is_dir(TESTS_DIR . 'config')) { +    define('RCMAIL_CONFIG_DIR', TESTS_DIR . 'config');  } -else { -  $testfiles = glob(TESTS_DIR . '*.php'); -} - -$test = new TestSuite('Roundcube unit tests'); -$reporter = new TextReporter(); -foreach ($testfiles as $fn) { -  $test->addTestFile($fn); -} - -$test->run($reporter); +require_once(INSTALL_PATH . 'program/include/iniset.php'); -?> +rcmail::get_instance()->config->set('devel_mode', false); diff --git a/tests/maildecode.php b/tests/maildecode.php deleted file mode 100644 index 4ac499360..000000000 --- a/tests/maildecode.php +++ /dev/null @@ -1,130 +0,0 @@ -<?php - -/** - * Test class to test messages decoding functions - * - * @package Tests - */ -class rcube_test_maildecode extends UnitTestCase -{ -  private $app; - -  function __construct() -  { -    $this->UnitTestCase('Mail headers decoding tests'); -  } - -  /** -   * Test decoding of single e-mail address strings -   * Uses rcube_mime::decode_address_list() -   */ -  function test_decode_single_address() -  { -    $headers = array( -        0  => 'test@domain.tld', -        1  => '<test@domain.tld>', -        2  => 'Test <test@domain.tld>', -        3  => 'Test Test <test@domain.tld>', -        4  => 'Test Test<test@domain.tld>', -        5  => '"Test Test" <test@domain.tld>', -        6  => '"Test Test"<test@domain.tld>', -        7  => '"Test \\" Test" <test@domain.tld>', -        8  => '"Test<Test" <test@domain.tld>', -        9  => '=?ISO-8859-1?B?VGVzdAo=?= <test@domain.tld>', -        10 => '=?ISO-8859-1?B?VGVzdAo=?=<test@domain.tld>', // #1487068 -        // comments in address (#1487673) -        11 => 'Test (comment) <test@domain.tld>', -        12 => '"Test" (comment) <test@domain.tld>', -        13 => '"Test (comment)" (comment) <test@domain.tld>', -        14 => '(comment) <test@domain.tld>', -        15 => 'Test <test@(comment)domain.tld>', -        16 => 'Test Test ((comment)) <test@domain.tld>', -        17 => 'test@domain.tld (comment)', -        18 => '"Test,Test" <test@domain.tld>', -        // 1487939 -        19 => 'Test <"test test"@domain.tld>', -        20 => '<"test test"@domain.tld>', -        21 => '"test test"@domain.tld', -    ); - -    $results = array( -        0  => array(1, '', 'test@domain.tld'), -        1  => array(1, '', 'test@domain.tld'), -        2  => array(1, 'Test', 'test@domain.tld'), -        3  => array(1, 'Test Test', 'test@domain.tld'), -        4  => array(1, 'Test Test', 'test@domain.tld'), -        5  => array(1, 'Test Test', 'test@domain.tld'), -        6  => array(1, 'Test Test', 'test@domain.tld'), -        7  => array(1, 'Test " Test', 'test@domain.tld'), -        8  => array(1, 'Test<Test', 'test@domain.tld'), -        9  => array(1, 'Test', 'test@domain.tld'), -        10 => array(1, 'Test', 'test@domain.tld'), -        11 => array(1, 'Test', 'test@domain.tld'), -        12 => array(1, 'Test', 'test@domain.tld'), -        13 => array(1, 'Test (comment)', 'test@domain.tld'), -        14 => array(1, '', 'test@domain.tld'), -        15 => array(1, 'Test', 'test@domain.tld'), -        16 => array(1, 'Test Test', 'test@domain.tld'), -        17 => array(1, '', 'test@domain.tld'), -        18 => array(1, 'Test,Test', 'test@domain.tld'), -        19 => array(1, 'Test', '"test test"@domain.tld'), -        20 => array(1, '', '"test test"@domain.tld'), -        21 => array(1, '', '"test test"@domain.tld'), -    ); - -    foreach ($headers as $idx => $header) { -      $res = rcube_mime::decode_address_list($header); - -      $this->assertEqual($results[$idx][0], count($res), "Rows number in result for header: " . $header); -      $this->assertEqual($results[$idx][1], $res[1]['name'], "Name part decoding for header: " . $header); -      $this->assertEqual($results[$idx][2], $res[1]['mailto'], "Email part decoding for header: " . $header); -    } -  } - -  /** -   * Test decoding of header values -   * Uses rcube_mime::decode_mime_string() -   */ -  function test_header_decode_qp() -  { -    $test = array( -      // #1488232: invalid character "?" -      'quoted-printable (1)' => array( -        'in'  => '=?utf-8?Q?Certifica=C3=A7=C3=A3??=', -        'out' => 'Certifica=C3=A7=C3=A3?', -      ), -      'quoted-printable (2)' => array( -        'in'  => '=?utf-8?Q?Certifica=?= =?utf-8?Q?C3=A7=C3=A3?=', -        'out' => 'Certifica=C3=A7=C3=A3', -      ), -      'quoted-printable (3)' => array( -        'in'  => '=?utf-8?Q??= =?utf-8?Q??=', -        'out' => '', -      ), -      'quoted-printable (4)' => array( -        'in'  => '=?utf-8?Q??= a =?utf-8?Q??=', -        'out' => ' a ', -      ), -      'quoted-printable (5)' => array( -        'in'  => '=?utf-8?Q?a?= =?utf-8?Q?b?=', -        'out' => 'ab', -      ), -      'quoted-printable (6)' => array( -        'in'  => '=?utf-8?Q?   ?= =?utf-8?Q?a?=', -        'out' => '   a', -      ), -      'quoted-printable (7)' => array( -        'in'  => '=?utf-8?Q?___?= =?utf-8?Q?a?=', -        'out' => '   a', -      ), -    ); - -    foreach ($test as $idx => $item) { -      $res = rcube_mime::decode_mime_string($item['in'], 'UTF-8'); -      $res = quoted_printable_encode($res); - -      $this->assertEqual($item['out'], $res, "Header decoding for: " . $idx); -    } - -  } -} diff --git a/tests/mailfunc.php b/tests/mailfunc.php deleted file mode 100644 index 493ce946e..000000000 --- a/tests/mailfunc.php +++ /dev/null @@ -1,173 +0,0 @@ -<?php - -/** - * Test class to test steps/mail/func.inc functions - * - * @package Tests - */ -class rcube_test_mailfunc extends UnitTestCase -{ - -  function __construct() -  { -    $this->UnitTestCase('Mail body rendering tests'); -     -    // simulate environment to successfully include func.inc -    $GLOBALS['RCMAIL'] = $RCMAIL = rcmail::get_instance(); -    $GLOBALS['OUTPUT'] = $OUTPUT = $RCMAIL->load_gui(); -    $RCMAIL->action = 'autocomplete'; -    $RCMAIL->storage_init(false); -     -    require_once INSTALL_PATH . 'program/steps/mail/func.inc'; -     -    $GLOBALS['EMAIL_ADDRESS_PATTERN'] = $EMAIL_ADDRESS_PATTERN; -  } - -  /** -   * Helper method to create a HTML message part object -   */ -  function get_html_part($body) -  { -    $part = new rcube_message_part; -    $part->ctype_primary = 'text'; -    $part->ctype_secondary = 'html'; -    $part->body = file_get_contents(TESTS_DIR . $body); -    $part->replaces = array(); -    return $part; -  } - -  /** -   * Test sanitization of a "normal" html message -   */ -  function test_html() -  { -    $part = $this->get_html_part('src/htmlbody.txt'); -    $part->replaces = array('ex1.jpg' => 'part_1.2.jpg', 'ex2.jpg' => 'part_1.2.jpg'); -     -    // render HTML in normal mode -    $html = rcmail_html4inline(rcmail_print_body($part, array('safe' => false)), 'foo'); - -    $this->assertPattern('/src="'.$part->replaces['ex1.jpg'].'"/', $html, "Replace reference to inline image"); -    $this->assertPattern('#background="./program/resources/blocked.gif"#', $html, "Replace external background image"); -    $this->assertNoPattern('/ex3.jpg/', $html, "No references to external images"); -    $this->assertNoPattern('/<meta [^>]+>/', $html, "No meta tags allowed"); -    //$this->assertNoPattern('/<style [^>]+>/', $html, "No style tags allowed"); -    $this->assertNoPattern('/<form [^>]+>/', $html, "No form tags allowed"); -    $this->assertPattern('/Subscription form/', $html, "Include <form> contents"); -    $this->assertPattern('/<!-- link ignored -->/', $html, "No external links allowed"); -    $this->assertPattern('/<a[^>]+ target="_blank">/', $html, "Set target to _blank"); -    $this->assertTrue($GLOBALS['REMOTE_OBJECTS'], "Remote object detected"); -     -    // render HTML in safe mode -    $html2 = rcmail_html4inline(rcmail_print_body($part, array('safe' => true)), 'foo'); -     -    $this->assertPattern('/<style [^>]+>/', $html2, "Allow styles in safe mode"); -    $this->assertPattern('#src="http://evilsite.net/mailings/ex3.jpg"#', $html2, "Allow external images in HTML (safe mode)"); -    $this->assertPattern("#url\('?http://evilsite.net/newsletter/image/bg/bg-64.jpg'?\)#", $html2, "Allow external images in CSS (safe mode)"); -    $css = '<link rel="stylesheet" .+_u=tmp-[a-z0-9]+\.css.+_action=modcss'; -    $this->assertPattern('#'.$css.'#Ui', $html2, "Filter (anonymized) external styleseehts with utils/modcss.inc"); -  } - -  /** -   * Test the elimination of some trivial XSS vulnerabilities -   */ -  function test_html_xss() -  { -    $part = $this->get_html_part('src/htmlxss.txt'); -    $washed = rcmail_print_body($part, array('safe' => true)); -     -    $this->assertNoPattern('/src="skins/', $washed, "Remove local references"); -    $this->assertNoPattern('/\son[a-z]+/', $washed, "Remove on* attributes"); -     -    $html = rcmail_html4inline($washed, 'foo'); -    $this->assertNoPattern('/onclick="return rcmail.command(\'compose\',\'xss@somehost.net\',this)"/', $html, "Clean mailto links"); -    $this->assertNoPattern('/alert/', $html, "Remove alerts"); -  } - -  /** -   * Test HTML sanitization to fix the CSS Expression Input Validation Vulnerability -   * reported at http://www.securityfocus.com/bid/26800/ -   */ -  function test_html_xss2() -  { -    $part = $this->get_html_part('src/BID-26800.txt'); -    $washed = rcmail_html4inline(rcmail_print_body($part, array('safe' => true)), 'dabody', '', $attr, true); - -    $this->assertNoPattern('/alert|expression|javascript|xss/', $washed, "Remove evil style blocks"); -    $this->assertNoPattern('/font-style:italic/', $washed, "Allow valid styles"); -  } - -  /** -   * Test washtml class on non-unicode characters (#1487813) -   */ -  function test_washtml_utf8() -  { -    $part = $this->get_html_part('src/invalidchars.html'); -    $washed = rcmail_print_body($part); - -    $this->assertPattern('/<p>символ<\/p>/', $washed, "Remove non-unicode characters from HTML message body"); -  } - -  /** -   * Test links pattern replacements in plaintext messages -   */ -  function test_plaintext() -  { -    $part = new rcube_message_part; -    $part->ctype_primary = 'text'; -    $part->ctype_secondary = 'plain'; -    $part->body = quoted_printable_decode(file_get_contents(TESTS_DIR . 'src/plainbody.txt')); -    $html = rcmail_print_body($part, array('safe' => true)); - -    $this->assertPattern('/<a href="mailto:nobody@roundcube.net" onclick="return rcmail.command\(\'compose\',\'nobody@roundcube.net\',this\)">nobody@roundcube.net<\/a>/', $html, "Mailto links with onclick"); -    $this->assertPattern('#<a href="http://www.apple.com/legal/privacy" target="_blank">http://www.apple.com/legal/privacy</a>#', $html, "Links with target=_blank"); -    $this->assertPattern('#\\[<a href="http://example.com/\\?tx\\[a\\]=5" target="_blank">http://example.com/\\?tx\\[a\\]=5</a>\\]#', $html, "Links with square brackets"); -  } - -  /** -   * Test mailto links in html messages -   */ -  function test_mailto() -  { -    $part = $this->get_html_part('src/mailto.txt'); - -    // render HTML in normal mode -    $html = rcmail_html4inline(rcmail_print_body($part, array('safe' => false)), 'foo'); - -    $mailto = '<a href="mailto:me@me.com?subject=this is the subject&body=this is the body"' -      .' onclick="return rcmail.command(\'compose\',\'me@me.com?subject=this is the subject&body=this is the body\',this)">e-mail</a>'; - -    $this->assertPattern('|'.preg_quote($mailto, '|').'|', $html, "Extended mailto links"); -  } - -  /** -   * Test the elimination of HTML comments -   */ -  function test_html_comments() -  { -    $part = $this->get_html_part('src/htmlcom.txt'); -    $washed = rcmail_print_body($part, array('safe' => true)); - -    // #1487759 -    $this->assertPattern('|<p>test1</p>|', $washed, "Buggy HTML comments"); -    // but conditional comments (<!--[if ...) should be removed -    $this->assertNoPattern('|<p>test2</p>|', $washed, "Conditional HTML comments"); -  } - -  /** -   * Test URI base resolving in HTML messages -   */ -  function test_resolve_base() -  { -    $html = file_get_contents(TESTS_DIR . 'src/htmlbase.txt'); -    $html = rcmail_resolve_base($html); - -    $this->assertPattern('|src="http://alec\.pl/dir/img1\.gif"|', $html, "URI base resolving [1]"); -    $this->assertPattern('|src="http://alec\.pl/dir/img2\.gif"|', $html, "URI base resolving [2]"); -    $this->assertPattern('|src="http://alec\.pl/img3\.gif"|', $html, "URI base resolving [3]"); - -    // base resolving exceptions -    $this->assertPattern('|src="cid:theCID"|', $html, "URI base resolving exception [1]"); -    $this->assertPattern('|src="http://other\.domain\.tld/img3\.gif"|', $html, "URI base resolving exception [2]"); -  } -} diff --git a/tests/modcss.php b/tests/modcss.php deleted file mode 100644 index 945cac318..000000000 --- a/tests/modcss.php +++ /dev/null @@ -1,45 +0,0 @@ -<?php - -/** - * Test class to test rcmail_mod_css_styles and XSS vulnerabilites - * - * @package Tests - */ -class rcube_test_modcss extends UnitTestCase -{ - -  function __construct() -  { -    $this->UnitTestCase('CSS modification and vulnerability tests'); -  } -   -  function test_modcss() -  { -    $css = file_get_contents(TESTS_DIR . 'src/valid.css'); -    $mod = rcmail_mod_css_styles($css, 'rcmbody'); - -    $this->assertPattern('/#rcmbody\s+\{/', $mod, "Replace body style definition"); -    $this->assertPattern('/#rcmbody h1\s\{/', $mod, "Prefix tag styles (single)"); -    $this->assertPattern('/#rcmbody h1, #rcmbody h2, #rcmbody h3, #rcmbody textarea\s+\{/', $mod, "Prefix tag styles (multiple)"); -    $this->assertPattern('/#rcmbody \.noscript\s+\{/', $mod, "Prefix class styles"); -  } -   -  function test_xss() -  { -    $mod = rcmail_mod_css_styles("body.main2cols { background-image: url('../images/leftcol.png'); }", 'rcmbody'); -    $this->assertEqual("/* evil! */", $mod, "No url() values allowed"); -     -    $mod = rcmail_mod_css_styles("@import url('http://localhost/somestuff/css/master.css');", 'rcmbody'); -    $this->assertEqual("/* evil! */", $mod, "No import statements"); -     -    $mod = rcmail_mod_css_styles("left:expression(document.body.offsetWidth-20)", 'rcmbody'); -    $this->assertEqual("/* evil! */", $mod, "No expression properties"); -     -    $mod = rcmail_mod_css_styles("left:exp/*  */ression( alert('xss3') )", 'rcmbody'); -    $this->assertEqual("/* evil! */", $mod, "Don't allow encoding quirks"); -     -    $mod = rcmail_mod_css_styles("background:\\0075\\0072\\006c( javascript:alert('xss') )", 'rcmbody'); -    $this->assertEqual("/* evil! */", $mod, "Don't allow encoding quirks (2)"); -  } -   -} diff --git a/tests/phpunit.xml b/tests/phpunit.xml new file mode 100644 index 000000000..8b3883223 --- /dev/null +++ b/tests/phpunit.xml @@ -0,0 +1,37 @@ +<phpunit backupGlobals="false" +    bootstrap="bootstrap.php" +    colors="true"> +    <testsuites> +        <testsuite name="All Tests"> +            <file>Framework/BaseReplacer.php</file> +            <file>Framework/Browser.php</file> +            <file>Framework/Cache.php</file> +            <file>Framework/Charset.php</file> +            <file>Framework/ContentFilter.php</file> +            <file>Framework/Html.php</file> +            <file>Framework/Imap.php</file> +            <file>Framework/ImapGeneric.php</file> +            <file>Framework/Image.php</file> +            <file>Framework/MessageHeader.php</file> +            <file>Framework/MessagePart.php</file> +            <file>Framework/Mime.php</file> +            <file>Framework/Rcube.php</file> +            <file>Framework/ResultIndex.php</file> +            <file>Framework/ResultSet.php</file> +            <file>Framework/ResultThread.php</file> +            <file>Framework/Shared.php</file> +            <file>Framework/Smtp.php</file> +            <file>Framework/Spellchecker.php</file> +            <file>Framework/StringReplacer.php</file> +            <file>Framework/User.php</file> +            <file>Framework/Utils.php</file> +            <file>Framework/VCard.php</file> +            <file>HtmlToText.php</file> +            <file>MailFunc.php</file> +        </testsuite> +        <testsuite name="managesieve"> +            <file>./../plugins/managesieve/tests/Parser.php</file> +            <file>./../plugins/managesieve/tests/Tokenizer.php</file> +        </testsuite> +    </testsuites> +</phpunit> diff --git a/tests/src/photo.vcf b/tests/src/photo.vcf new file mode 100644 index 000000000..c3a805009 --- /dev/null +++ b/tests/src/photo.vcf @@ -0,0 +1,45 @@ +BEGIN:VCARD
 +VERSION:3.0
 +N:Müller;Jörg;;;
 +FN:Apple Computer AG
 +ORG:Apple Computer AG;
 +PHOTO;ENCODING=b:
 +  /9j/4AAQSkZJRgABAQAAAQABAAD/7QAcUGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAD/2wBDAAEB +  AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB +  AQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB +  AQEBAQEBAQEBAQEBAQEBAQH/wAARCAAwADADAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAA +  AAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEI +  I0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlq +  c3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW +  19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL +  /8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLR +  ChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOE +  hYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn +  6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD+/igAoAKAPmH43ftT+CfgzqNt4bNjeeLvGV2IHXw7 +  pVxDbLZx3LBbdtU1GVLhbN7jIMFvHa3VzIpWRoY4mWQ9dDCTrrmuoQ/mavfvZXV7dW2jkr4ynQfL +  Zzn1inZL1lZ6+ST87H0lp1zLe6fY3k9s1nNd2dtczWjv5j2ss8KSyWzybU3tA7mJn2JuKk7Vzgcr +  Vm1e9m1fvrv8zqi+aKbVm0nbtdXt8i5SGFAHHeOfH3hH4b6DP4k8Z61a6JpUBCCW4LPNdTsCUtbK +  1iD3F5dSYO2C3jd8AuwVFZhdOnOrLlhFyf4Lzb6IzqVYUo81SSiundvslu3/AEz5i0n9u74Fanqo +  064m8UaNbvII49X1TRUGnnORvmFje3t5BGTjDtatwcyCPBrreArpX9xvqlJ3/FJP7zlWYUHKzVRL +  +ZxVvnaTa+4+QLvVPgJ4U+LutfFXx78QJfi3q954iuvEOieHvBmkyzaVbO1x5ulPruq6pLawTvp1 +  uLdIrK08xFmgXzl2oI67LV50o0qdP2K5VGUptKXnyxi29XfVtb6HDehGtKpUm6zcnNKCfK9brnlK +  3pZJ37pH3/8ACj9qb4T/ABd1FdD0TUr3SPETozwaJ4ht47C5vQgLONOnjnuLS8dVBYwJOt1tDMIN +  oLV51XCVqK5pJSj1cW3b1uk1vvqvM9Kji6VZ8qbjJ7KVlf0abTf4n0dXMdQUAfhf+2J8UNW8ffFz +  W9Cnlki0XwDqOp+GdNsFdhB9qsr6a31DUDHu2tcXbwojSEbhFCka4Uc+9hKUadGMl8VRKcn11V0v +  RJng4urKpWkntTlKCXTRtN+re58n11HKFAH6PfsGfBvQfEl3rHxR8Q2y38vhrUrfT/DNpIT5FvqY +  iF1carIgI8ye2R4Y7RXykbySTbS6xlfOx9aUVGlF2503N+W1vnrc9HAUYzlKrJX5GuVf3t7+dvu3 +  vc/WKvIPXCgD8FP2s/BF/wCC/jd4wlu0It/Fmp6h4usJf4JINZ1G7ndVbu0UpZZBztY446V9BhZq +  dCnZ/DFQfrFJHz+Kg4V6l/tSc15qTb/rzPmqug5woA++v2J/j74d+HN9rHgLxndrpmjeJr62vtJ1 +  mbP2Sw1dY/s0ltfOM+Rb30Yh8u5ZTHFPEFlZEk3Dgx2HlVUZw1lBNOPVp9vNa6X22137sFiI0ZSh +  N2jNpqXSLSe/k+/R+p+w1eMe0FAHxz+2P8DLr4r+CIPEPhy2Nx4y8FJdXVnaxrmfWdGlAk1DSosK +  WkuozEt5p8fHmTLNADuuQR24KuqU3GTtCpbXtLo32T2b9GcWNw7qwU4q84X06yi9Wl531XfXyPxK +  ME4nNsYZRciUwG3MbicTh/LMJix5glD/ACGPbv3/AC4zxXtniFvUNJ1XSmjXVNM1DTWmUvCuoWVz +  ZtKgxloxcRxmRRkZZcgZGTzSTT2afo7hqtz6w/ZB+BV78UPHtl4n1eykHgfwdewajfXE0bCDVtVt +  mE+n6PAzAJOBOkdxqAUssdsnlSDNwoPLi66pU3FP95NNJdk95P06d35XOvCUHWqJtfu4NOT7vdR8 +  7vfsvVH7gV4R7oUAFAHKReA/A8Gsy+IofBnhSHxBO7ST67F4d0iPWZnb7zy6mlmL2R2/iZ5yT3NX +  7Spbl9pPl/l55W+69iPZUubm9nDmvfm5I81+97XuX9a8MeGvEtsLLxF4e0PX7MMGFprWk2Gq2wZe +  jCC+t54tw7HZkdqUZzg7wlKL7xk4v700OUIT0lCMl2lFS/NMuaXpOlaHZQ6Zoumafo+nW4It9P0u +  yttPsoATkiG1tI4oIgTyQkagnmk5Sk7ybk+7bb+96jjGMVaMVFdopJfcjQpDP//Z
 +X-ABShowAs:COMPANY
 +X-ABUID:2E4CB084-4767-4C85-BBCA-805B1DCB1C8E\:ABPerson
 +END:VCARD
 diff --git a/tests/vcards.php b/tests/vcards.php deleted file mode 100644 index 22f7cdd33..000000000 --- a/tests/vcards.php +++ /dev/null @@ -1,65 +0,0 @@ -<?php - -/** - * Unit tests for class rcube_vcard - * - * @package Tests - */ -class rcube_test_vcards extends UnitTestCase -{ - -  function __construct() -  { -    $this->UnitTestCase('Vcard encoding/decoding tests'); -  } -   -  function _srcpath($fn) -  { -    return realpath(dirname(__FILE__) . '/src/' . $fn); -  } -   -  function test_parse_one() -  { -    $vcard = new rcube_vcard(file_get_contents($this->_srcpath('apple.vcf'))); -     -    $this->assertEqual(true, $vcard->business, "Identify as business record"); -    $this->assertEqual("Apple Computer AG", $vcard->displayname, "FN => displayname"); -    $this->assertEqual("", $vcard->firstname, "No person name set"); -  } - -  function test_parse_two() -  { -    $vcard = new rcube_vcard(file_get_contents($this->_srcpath('johndoe.vcf')), null); -     -    $this->assertEqual(false, $vcard->business, "Identify as private record"); -    $this->assertEqual("John Doë", $vcard->displayname, "Decode according to charset attribute"); -    $this->assertEqual("roundcube.net", $vcard->organization, "Test organization field"); -    $this->assertEqual(2, count($vcard->email), "List two e-mail addresses"); -    $this->assertEqual("roundcube@gmail.com", $vcard->email[0], "Use PREF e-mail as primary"); -  } -   -  function test_import() -  { -    $input = file_get_contents($this->_srcpath('apple.vcf')); -    $input .= file_get_contents($this->_srcpath('johndoe.vcf')); -     -    $vcards = rcube_vcard::import($input); - -    $this->assertEqual(2, count($vcards), "Detected 2 vcards"); -    $this->assertEqual("Apple Computer AG", $vcards[0]->displayname, "FN => displayname"); -    $this->assertEqual("John Doë", $vcards[1]->displayname, "Displayname with correct charset"); -     -    // http://trac.roundcube.net/ticket/1485542 -    $vcards2 = rcube_vcard::import(file_get_contents($this->_srcpath('thebat.vcf'))); -    $this->assertEqual("Iksiñski", $vcards2[0]->surname, "Detect charset in encoded values"); -  } -   -  function test_encodings() -  { -      $input = file_get_contents($this->_srcpath('utf-16_sample.vcf')); -       -      $vcards = rcube_vcard::import($input); -      $this->assertEqual("Ǽgean ĽdaMonté", $vcards[0]->displayname, "Decoded from UTF-16"); -  } -   -} | 
