diff options
32 files changed, 466 insertions, 68 deletions
@@ -1,6 +1,15 @@ CHANGELOG Roundcube Webmail =========================== +- List related text/html part as attachment in plain text mode (#1488677) +- Use IMAP BINARY (RFC3516) extension to fetch message/part bodies +- Fix folder creation under public namespace root (#1488665) +- Fix so "Edit as new" on draft creates a new message (#1488687) +- Fix invalid error message on deleting mail from read only folder (#1488694) +- Fix error where session wasn't updated after folder rename/delete (#1488692) +- Replace data URIs of images (pasted in HTML editor) with inline attachments (#1488502) +- 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) diff --git a/config/main.inc.php.dist b/config/main.inc.php.dist index 7e07341a9..a6661c323 100644 --- a/config/main.inc.php.dist +++ b/config/main.inc.php.dist @@ -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; diff --git a/installer/check.php b/installer/check.php index 52460bb0f..d6c9f5c40 100644 --- a/installer/check.php +++ b/installer/check.php @@ -35,12 +35,12 @@ $ini_checks = array( 'suhosin.session.encrypt' => 0, 'magic_quotes_runtime' => 0, 'magic_quotes_sybase' => 0, + 'date.timezone' => '-NOTEMPTY-', ); $optional_checks = array( // required for utils/modcss.inc, should we require this? 'allow_url_fopen' => 1, - 'date.timezone' => '-NOTEMPTY-', ); $source_urls = array( @@ -171,7 +171,15 @@ foreach ($ini_checks as $var => $val) { $status = ini_get($var); if ($val === '-NOTEMPTY-') { if (empty($status)) { - $RCI->fail($var, "cannot be empty and needs to be set"); + $RCI->fail($var, "empty value detected"); + } else if ($var == 'date.timezone') { + try { + $tz = new DateTimeZone($status); + $RCI->pass($var); + } + catch (Exception $e) { + $RCI->fail($var, "invalid value detected: $status"); + } } else { $RCI->pass($var); } diff --git a/plugins/password/config.inc.php.dist b/plugins/password/config.inc.php.dist index 37c79315d..8d7b433af 100644 --- a/plugins/password/config.inc.php.dist +++ b/plugins/password/config.inc.php.dist @@ -36,7 +36,8 @@ $rcmail_config['password_db_dsn'] = ''; // The query can contain the following macros that will be expanded as follows: // %p is replaced with the plaintext new password // %c is replaced with the crypt version of the new password, MD5 if available -// otherwise DES. +// otherwise DES. More hash function can be enabled using the password_crypt_hash +// configuration parameter. // %D is replaced with the dovecotpw-crypted version of the new password // %o is replaced with the password before the change // %n is replaced with the hashed version of the new password @@ -51,6 +52,13 @@ $rcmail_config['password_db_dsn'] = ''; // Default: "SELECT update_passwd(%c, %u)" $rcmail_config['password_query'] = 'SELECT update_passwd(%c, %u)'; +// By default the crypt() function which is used to create the '%c' +// parameter uses the md5 algorithm. To use different algorithms +// you can choose between: des, md5, blowfish, sha256, sha512. +// Before using other hash functions than des or md5 please make sure +// your operating system supports the other hash functions. +$rcmail_config['password_crypt_hash'] = 'md5'; + // By default domains in variables are using unicode. // Enable this option to use punycoded names $rcmail_config['password_idn_ascii'] = false; diff --git a/plugins/password/drivers/sql.php b/plugins/password/drivers/sql.php index 449e2df5b..8bdcabf83 100644 --- a/plugins/password/drivers/sql.php +++ b/plugins/password/drivers/sql.php @@ -40,13 +40,38 @@ class rcube_sql_password // crypted password if (strpos($sql, '%c') !== FALSE) { $salt = ''; - if (CRYPT_MD5) { - // Always use eight salt characters for MD5 (#1488136) - $len = 8; - } else if (CRYPT_STD_DES) { - $len = 2; - } else { - return PASSWORD_CRYPT_ERROR; + + if (!($crypt_hash = $rcmail->config->get('password_crypt_hash'))) + { + if (CRYPT_MD5) + $crypt_hash = 'md5'; + else if (CRYPT_STD_DES) + $crypt_hash = 'des'; + } + + switch ($crypt_hash) + { + case 'md5': + $len = 8; + $salt_hashindicator = '$1$'; + break; + case 'des': + $len = 2; + break; + case 'blowfish': + $len = 22; + $salt_hashindicator = '$2a$'; + break; + case 'sha256': + $len = 16; + $salt_hashindicator = '$5$'; + break; + case 'sha512': + $len = 16; + $salt_hashindicator = '$6$'; + break; + default: + return PASSWORD_CRYPT_ERROR; } //Restrict the character set used as salt (#1488136) @@ -55,7 +80,7 @@ class rcube_sql_password $salt .= $seedchars[rand(0, 63)]; } - $sql = str_replace('%c', $db->quote(crypt($passwd, CRYPT_MD5 ? '$1$'.$salt.'$' : $salt)), $sql); + $sql = str_replace('%c', $db->quote(crypt($passwd, $salt_hashindicator ? $salt_hashindicator .$salt.'$' : $salt)), $sql); } // dovecotpw diff --git a/program/include/rcmail.php b/program/include/rcmail.php index 02f38e647..5a9a1fa86 100644 --- a/program/include/rcmail.php +++ b/program/include/rcmail.php @@ -1774,10 +1774,7 @@ class rcmail extends rcube $err_code = $this->storage->get_error_code(); $res_code = $this->storage->get_response_code(); - if ($err_code < 0) { - $this->output->show_message('storageerror', 'error'); - } - else if ($res_code == rcube_storage::NOPERM) { + if ($res_code == rcube_storage::NOPERM) { $this->output->show_message('errornoperm', 'error'); } else if ($res_code == rcube_storage::READONLY) { @@ -1792,6 +1789,9 @@ class rcmail extends rcube $this->output->show_message('servererrormsg', 'error', array('msg' => $err_str)); } } + else if ($err_code < 0) { + $this->output->show_message('storageerror', 'error'); + } else if ($fallback) { $this->output->show_message($fallback, 'error', $fallback_args); } diff --git a/program/include/rcube_charset.php b/program/include/rcube_charset.php index 1740a6096..35c69729b 100644 --- a/program/include/rcube_charset.php +++ b/program/include/rcube_charset.php @@ -86,7 +86,7 @@ class rcube_charset * Sometimes charset string is malformed, there are also charset aliases * but we need strict names for charset conversion (specially utf8 class) * - * @param string Input charset name + * @param string $input Input charset name * * @return string The validated charset name */ @@ -176,9 +176,10 @@ class rcube_charset { static $iconv_options = null; static $mbstring_list = null; + static $mbstring_sch = null; static $conv = null; - $to = empty($to) ? strtoupper(RCMAIL_CHARSET) : self::parse_charset($to); + $to = empty($to) ? strtoupper(RCMAIL_CHARSET) : $to; $from = self::parse_charset($from); // It is a common case when UTF-16 charset is used with US-ASCII content (#1488654) @@ -221,6 +222,7 @@ class rcube_charset if ($mbstring_list === null) { if (extension_loaded('mbstring')) { + $mbstring_sch = mb_substitute_character(); $mbstring_list = mb_list_encodings(); $mbstring_list = array_map('strtoupper', $mbstring_list); } @@ -229,14 +231,25 @@ class rcube_charset // convert charset using mbstring module if ($mbstring_list !== null) { $aliases['WINDOWS-1257'] = 'ISO-8859-13'; + // it happens that mbstring supports ASCII but not US-ASCII + if (($from == 'US-ASCII' || $to == 'US-ASCII') && !in_array('US-ASCII', $mbstring_list)) { + $aliases['US-ASCII'] = 'ASCII'; + } $mb_from = $aliases[$from] ? $aliases[$from] : $from; $mb_to = $aliases[$to] ? $aliases[$to] : $to; // return if encoding found, string matches encoding and convert succeeded if (in_array($mb_from, $mbstring_list) && in_array($mb_to, $mbstring_list)) { - if (mb_check_encoding($str, $mb_from) && ($out = mb_convert_encoding($str, $mb_to, $mb_from))) { - return $out; + if (mb_check_encoding($str, $mb_from)) { + // Do the same as //IGNORE with iconv + mb_substitute_character('none'); + $out = mb_convert_encoding($str, $mb_to, $mb_from); + mb_substitute_character($mbstring_sch); + + if ($out !== false) { + return $out; + } } } } @@ -646,14 +659,14 @@ class rcube_charset return $failover; } - // FIXME: the order is important, because sometimes + // FIXME: the order is important, because sometimes // iso string is detected as euc-jp and etc. $enc = array( 'UTF-8', 'SJIS', 'BIG5', 'GB2312', 'ISO-8859-1', 'ISO-8859-2', 'ISO-8859-3', 'ISO-8859-4', 'ISO-8859-5', 'ISO-8859-6', 'ISO-8859-7', 'ISO-8859-8', 'ISO-8859-9', 'ISO-8859-10', 'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15', 'ISO-8859-16', - 'WINDOWS-1252', 'WINDOWS-1251', 'EUC-JP', 'EUC-TW', 'KOI8-R', + 'WINDOWS-1252', 'WINDOWS-1251', 'EUC-JP', 'EUC-TW', 'KOI8-R', 'ISO-2022-KR', 'ISO-2022-JP' ); diff --git a/program/include/rcube_imap.php b/program/include/rcube_imap.php index 66b5c4bd6..ebf31d578 100644 --- a/program/include/rcube_imap.php +++ b/program/include/rcube_imap.php @@ -1434,6 +1434,12 @@ class rcube_imap extends rcube_storage $criteria = 'UNDELETED '.$criteria; } + // unset CHARSET if criteria string is ASCII, this way + // SEARCH won't be re-sent after "unsupported charset" response + if ($charset && $charset != 'US-ASCII' && is_ascii($criteria)) { + $charset = 'US-ASCII'; + } + if ($this->threading) { $threads = $this->conn->thread($folder, $this->threading, $criteria, true, $charset); @@ -1465,7 +1471,7 @@ class rcube_imap extends rcube_storage } $messages = $this->conn->search($folder, - ($charset ? "CHARSET $charset " : '') . $criteria, true); + ($charset && $charset != 'US-ASCII' ? "CHARSET $charset " : '') . $criteria, true); // Error, try with US-ASCII (some servers may support only US-ASCII) if ($messages->is_error() && $charset && $charset != 'US-ASCII') { @@ -3291,11 +3297,8 @@ class rcube_imap extends rcube_storage } // Get folder rights (MYRIGHTS) - if ($acl && !$options['noselect']) { - // skip shared roots - if (!$options['is_root'] || $options['namespace'] == 'personal') { - $options['rights'] = (array)$this->my_rights($folder); - } + if ($acl && ($rights = $this->my_rights($folder))) { + $options['rights'] = $rights; } // Set 'norename' flag diff --git a/program/include/rcube_imap_generic.php b/program/include/rcube_imap_generic.php index c3cfabc3a..cce53aedc 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); @@ -2400,10 +2402,13 @@ class rcube_imap_generic $mode = 0; } + // Use BINARY extension when possible (and safe) + $binary = $mode && preg_match('/^[0-9.]+$/', $part) && $this->hasCapability('BINARY'); + $fetch_mode = $binary ? 'BINARY' : 'BODY'; + // format request - $reply_key = '* ' . $id; $key = $this->nextTag(); - $request = $key . ($is_uid ? ' UID' : '') . " FETCH $id (BODY.PEEK[$part])"; + $request = $key . ($is_uid ? ' UID' : '') . " FETCH $id ($fetch_mode.PEEK[$part])"; // send request if (!$this->putLine($request)) { @@ -2411,6 +2416,10 @@ class rcube_imap_generic return false; } + if ($binary) { + $mode = -1; + } + // receive reply line do { $line = rtrim($this->readLine(1024)); @@ -2455,13 +2464,13 @@ class rcube_imap_generic $prev = ''; while ($bytes > 0) { - $line = $this->readLine(4096); + $line = $this->readLine(8192); if ($line === NULL) { break; } - $len = strlen($line); + $len = strlen($line); if ($len > $bytes) { $line = substr($line, 0, $bytes); diff --git a/program/include/rcube_message.php b/program/include/rcube_message.php index 6af1d0133..fe2fcf354 100644 --- a/program/include/rcube_message.php +++ b/program/include/rcube_message.php @@ -494,8 +494,13 @@ class rcube_message } // list as attachment as well - if (!empty($mail_part->filename)) + if (!empty($mail_part->filename)) { + $this->attachments[] = $mail_part; + } + // list html part as attachment (here the part is most likely inside a multipart/related part) + else if ($this->parse_alternative && ($secondary_type == 'html' && !$this->opt['prefer_html'])) { $this->attachments[] = $mail_part; + } } // part message/* else if ($primary_type == 'message') { diff --git a/program/include/rcube_result_index.php b/program/include/rcube_result_index.php index cc1615d35..334ec8530 100644 --- a/program/include/rcube_result_index.php +++ b/program/include/rcube_result_index.php @@ -61,10 +61,14 @@ class rcube_result_index for ($i=0, $len=count($data); $i<$len; $i++) { $data_item = &$data[$i]; if (preg_match('/^ SORT/i', $data_item)) { + // valid response, initialize raw_data for is_error() + $this->raw_data = ''; $data_item = substr($data_item, 5); break; } else if (preg_match('/^ (E?SEARCH)/i', $data_item, $m)) { + // valid response, initialize raw_data for is_error() + $this->raw_data = ''; $data_item = substr($data_item, strlen($m[0])); if (strtoupper($m[1]) == 'ESEARCH') { diff --git a/program/include/rcube_result_thread.php b/program/include/rcube_result_thread.php index 214aec217..09fa46522 100644 --- a/program/include/rcube_result_thread.php +++ b/program/include/rcube_result_thread.php @@ -61,6 +61,8 @@ class rcube_result_thread // ...skip unilateral untagged server responses for ($i=0, $len=count($data); $i<$len; $i++) { if (preg_match('/^ THREAD/i', $data[$i])) { + // valid response, initialize raw_data for is_error() + $this->raw_data = ''; $data[$i] = substr($data[$i], 7); break; } diff --git a/program/include/rcube_shared.inc b/program/include/rcube_shared.inc index c15305c08..4577c6df5 100644 --- a/program/include/rcube_shared.inc +++ b/program/include/rcube_shared.inc @@ -255,6 +255,21 @@ function asciiwords($str, $css_id = false, $replace_with = '') /** + * Check if a string contains only ascii characters + * + * @param string $str String to check + * @param bool $control_chars Includes control characters + * + * @return bool + */ +function is_ascii($str, $control_chars = true) +{ + $regexp = $control_chars ? '/[^\x00-\x7F]/' : '/[^\x20-\x7E]/'; + return preg_match($regexp, $str) ? false : true; +} + + +/** * Remove single and double quotes from a given string * * @param string Input value diff --git a/program/include/rcube_utils.php b/program/include/rcube_utils.php index 23bf556e4..b278431a6 100644 --- a/program/include/rcube_utils.php +++ b/program/include/rcube_utils.php @@ -221,6 +221,10 @@ class rcube_utils static $js_rep_table = false; static $xml_rep_table = false; + if (!is_string($str)) { + $str = strval($str); + } + // encode for HTML output if ($enctype == 'html') { if (!$html_encode_arr) { diff --git a/program/include/rcube_vcard.php b/program/include/rcube_vcard.php index 2bfd474c6..49b312c5c 100644 --- a/program/include/rcube_vcard.php +++ b/program/include/rcube_vcard.php @@ -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 48de21764..2182a2b88 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -669,7 +669,7 @@ function rcube_webmail() this.load_identity(props, 'edit-identity'); else if (this.task == 'mail' && (cid = this.get_single_uid())) { url = { _mbox: this.env.mailbox }; - url[this.env.mailbox == this.env.drafts_mailbox ? '_draft_uid' : '_uid'] = cid; + url[this.env.mailbox == this.env.drafts_mailbox && props != 'new' ? '_draft_uid' : '_uid'] = cid; this.goto_url('compose', url, true); } break; diff --git a/program/steps/addressbook/edit.inc b/program/steps/addressbook/edit.inc index 0f1fd6697..90069a7eb 100644 --- a/program/steps/addressbook/edit.inc +++ b/program/steps/addressbook/edit.inc @@ -117,9 +117,6 @@ function rcmail_contact_editform($attrib) $record = rcmail_get_edit_record(); - // add some labels to client - $RCMAIL->output->add_label('noemailwarning', 'nonamewarning'); - // copy (parsed) address template to client if (preg_match_all('/\{([a-z0-9]+)\}([^{]*)/i', $RCMAIL->config->get('address_template', ''), $templ, PREG_SET_ORDER)) $RCMAIL->output->set_env('address_template', $templ); diff --git a/program/steps/addressbook/import.inc b/program/steps/addressbook/import.inc index 654a33602..15e04b82a 100644 --- a/program/steps/addressbook/import.inc +++ b/program/steps/addressbook/import.inc @@ -189,32 +189,36 @@ if (is_array($_FILES['_file'])) { $IMPORT_STATS->names = array(); $IMPORT_STATS->skipped_names = array(); $IMPORT_STATS->count = count($vcards); - $IMPORT_STATS->inserted = $IMPORT_STATS->skipped = $IMPORT_STATS->nomail = $IMPORT_STATS->errors = 0; + $IMPORT_STATS->inserted = $IMPORT_STATS->skipped = $IMPORT_STATS->invalid = $IMPORT_STATS->errors = 0; if ($replace) { $CONTACTS->delete_all(); } foreach ($vcards as $vcard) { - $email = $vcard->email[0]; $a_record = $vcard->get_assoc(); - // skip entries without an e-mail address or invalid - if (empty($email) || !$CONTACTS->validate($a_record, true)) { - $IMPORT_STATS->nomail++; + // skip invalid (incomplete) entries + if (!$CONTACTS->validate($a_record, true)) { + $IMPORT_STATS->invalid++; continue; } // We're using UTF8 internally + $email = $vcard->email[0]; $email = rcube_idn_to_utf8($email); - if (!$replace && $email) { + if (!$replace) { + $existing = null; // compare e-mail address - $existing = $CONTACTS->search('email', $email, 1, false); - if (!$existing->count && $vcard->displayname) { // compare display name + if ($email) { + $existing = $CONTACTS->search('email', $email, 1, false); + } + // compare display name if email not found + if ((!$existing || !$existing->count) && $vcard->displayname) { $existing = $CONTACTS->search('name', $vcard->displayname, 1, false); } - if ($existing->count) { + if ($existing && $existing->count) { $IMPORT_STATS->skipped++; $IMPORT_STATS->skipped_names[] = $vcard->displayname ? $vcard->displayname : $email; continue; diff --git a/program/steps/addressbook/save.inc b/program/steps/addressbook/save.inc index 3bfce3b4d..887e49827 100644 --- a/program/steps/addressbook/save.inc +++ b/program/steps/addressbook/save.inc @@ -161,7 +161,6 @@ else { $source = $orig_source; // show notice if existing contacts with same e-mail are found - $existing = false; foreach ($CONTACTS->get_col_values('email', $a_record, true) as $email) { if ($email && ($res = $CONTACTS->search('email', $email, 1, false, true)) && $res->count) { $OUTPUT->show_message('contactexists', 'notice', null, false); diff --git a/program/steps/mail/compose.inc b/program/steps/mail/compose.inc index 29e12675e..04efe7df5 100644 --- a/program/steps/mail/compose.inc +++ b/program/steps/mail/compose.inc @@ -1047,15 +1047,23 @@ function rcmail_remove_signature($body) function rcmail_write_compose_attachments(&$message, $bodyIsHtml) { - global $RCMAIL, $COMPOSE; + global $RCMAIL, $COMPOSE, $compose_mode; $cid_map = $messages = array(); foreach ((array)$message->mime_parts as $pid => $part) { - if (($part->ctype_primary != 'message' || !$bodyIsHtml) && $part->ctype_primary != 'multipart' && - ($part->disposition == 'attachment' || ($part->disposition == 'inline' && $bodyIsHtml) || $part->filename) - && $part->mimetype != 'application/ms-tnef' - ) { + if ($part->disposition == 'attachment' || ($part->disposition == 'inline' && $bodyIsHtml) || $part->filename) { + if ($part->ctype_primary == 'message' || $part->ctype_primary == 'multipart') { + continue; + } + if ($part->mimetype == 'application/ms-tnef') { + continue; + } + // skip inline images when forwarding in plain text + if ($part->content_id && !$bodyIsHtml && $compose_mode == RCUBE_COMPOSE_FORWARD) { + continue; + } + $skip = false; if ($part->mimetype == 'message/rfc822') { $messages[] = $part->mime_id; diff --git a/program/steps/mail/search.inc b/program/steps/mail/search.inc index 670680959..db5424b3b 100644 --- a/program/steps/mail/search.inc +++ b/program/steps/mail/search.inc @@ -100,7 +100,7 @@ $search = isset($srch) ? trim($srch) : trim($str); if (!empty($subject)) { $search_str .= str_repeat(' OR', count($subject)-1); foreach ($subject as $sub) - $search_str .= sprintf(" %s {%d}\r\n%s", $sub, strlen($search), $search); + $search_str .= ' ' . $sub . ' ' . rcube_imap_generic::escape($search); } $search_str = trim($search_str); diff --git a/program/steps/mail/sendmail.inc b/program/steps/mail/sendmail.inc index 577751742..5c2c6de20 100644 --- a/program/steps/mail/sendmail.inc +++ b/program/steps/mail/sendmail.inc @@ -93,9 +93,8 @@ function rcmail_get_identity($id) * to this: * * <img src="/path/on/server/.../tiny_mce/plugins/emotions/images/smiley-cool.gif" border="0" alt="Cool" title="Cool" /> - * ... */ -function rcmail_fix_emoticon_paths(&$mime_message) +function rcmail_fix_emoticon_paths($mime_message) { global $CONFIG; @@ -134,8 +133,53 @@ function rcmail_fix_emoticon_paths(&$mime_message) } $mime_message->setHTMLBody($body); +} + +/** + * Extract image attachments from HTML content (data URIs) + */ +function rcmail_extract_inline_images($mime_message, $from) +{ + $body = $mime_message->getHTMLBody(); + $offset = 0; + $list = array(); + $regexp = '# src=[\'"](data:(image/[a-z]+);base64,([a-z0-9+/=\r\n]+))([\'"])#i'; + + // get domain for the Content-ID, must be the same as in Mail_Mime::get() + if (preg_match('#@([0-9a-zA-Z\-\.]+)#', $from, $matches)) { + $domain = $matches[1]; + } else { + $domain = 'localhost'; + } + + if (preg_match_all($regexp, $body, $matches, PREG_OFFSET_CAPTURE)) { + foreach ($matches[1] as $idx => $m) { + $data = preg_replace('/\r\n/', '', $matches[3][$idx][0]); + $data = base64_decode($data); - return $body; + if (empty($data)) { + continue; + } + + $hash = md5($data) . '@' . $domain; + $mime_type = $matches[2][$idx][0]; + $name = $list[$hash]; + + // add the image to the MIME message + if (!$name) { + $ext = preg_replace('#^[^/]+/#', '', $mime_type); + $name = substr($hash, 0, 8) . '.' . $ext; + $list[$hash] = $name; + + $mime_message->addHTMLImage($data, $mime_type, $name, false, $hash); + } + + $body = substr_replace($body, $name, $m[1] + $offset, strlen($m[0])); + $offset += strlen($name) - strlen($m[0]); + } + } + + $mime_message->setHTMLBody($body); } /** @@ -522,7 +566,10 @@ if ($isHtml) { // look for "emoticon" images from TinyMCE and change their src paths to // be file paths on the server instead of URL paths. - $message_body = rcmail_fix_emoticon_paths($MAIL_MIME); + rcmail_fix_emoticon_paths($MAIL_MIME); + + // Extract image Data URIs into message attachments (#1488502) + rcmail_extract_inline_images($MAIL_MIME, $from); } else { $plugin = $RCMAIL->plugins->exec_hook('message_outgoing_body', diff --git a/program/steps/settings/folders.inc b/program/steps/settings/folders.inc index 6ca704998..3231ed644 100644 --- a/program/steps/settings/folders.inc +++ b/program/steps/settings/folders.inc @@ -85,6 +85,11 @@ else if ($RCMAIL->action == 'delete-folder') else { $deleted = $plugin['result']; } + + // #1488692: update session + if ($deleted && $_SESSION['mbox'] === $mbox) { + $RCMAIL->session->remove('mbox'); + } } if ($OUTPUT->ajax_call && $deleted) { @@ -393,15 +398,20 @@ function rcmail_rename_folder($oldname, $newname) foreach ($a_threaded as $key => $val) { if ($key == $oldname) { unset($a_threaded[$key]); - $a_threaded[$newname] = true; + $a_threaded[$newname] = true; } else if (preg_match($oldprefix, $key)) { unset($a_threaded[$key]); - $a_threaded[preg_replace($oldprefix, $newname.$delimiter, $key)] = true; + $a_threaded[preg_replace($oldprefix, $newname.$delimiter, $key)] = true; } } $RCMAIL->user->save_prefs(array('message_threading' => $a_threaded)); + // #1488692: update session + if ($_SESSION['mbox'] === $oldname) { + $_SESSION['mbox'] = $newname; + } + return true; } diff --git a/program/steps/settings/save_folder.inc b/program/steps/settings/save_folder.inc index 73cc5e4bf..877b0fbbe 100644 --- a/program/steps/settings/save_folder.inc +++ b/program/steps/settings/save_folder.inc @@ -1,11 +1,11 @@ <?php -/* +/** +-----------------------------------------------------------------------+ | program/steps/settings/save_folder.inc | | | | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2009, 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. | @@ -170,7 +170,7 @@ else if (!$error) { } else if (preg_match($oldprefix, $key)) { unset($a_threaded[$key]); - $a_threaded[preg_replace($oldprefix, $folder['name'].$delimiter, $key)] = true; + $a_threaded[preg_replace($oldprefix, $folder['name'].$delimiter, $key)] = true; } } } @@ -183,7 +183,12 @@ else if (!$error) { } $OUTPUT->show_message('folderupdated', 'confirmation'); + if ($rename) { + // #1488692: update session + if ($_SESSION['mbox'] === $folder['oldname']) { + $_SESSION['mbox'] = $folder['name']; + } rcmail_update_folder_row($folder['name'], $folder['oldname'], $folder['subscribe'], $folder['class']); $OUTPUT->send('iframe'); } diff --git a/skins/classic/includes/messagetoolbar.html b/skins/classic/includes/messagetoolbar.html index 3f4995b83..ecaf8f79b 100644 --- a/skins/classic/includes/messagetoolbar.html +++ b/skins/classic/includes/messagetoolbar.html @@ -45,7 +45,7 @@ <ul class="toolbarmenu"> <li><roundcube:button class="printlink" command="print" label="printmessage" classAct="printlink active" /></li> <li><roundcube:button class="downloadlink" command="download" label="emlsave" classAct="downloadlink active" /></li> - <li><roundcube:button class="editlink" command="edit" label="editasnew" classAct="editlink active" /></li> + <li><roundcube:button class="editlink" command="edit" prop="new" label="editasnew" classAct="editlink active" /></li> <li class="separator_below"><roundcube:button class="sourcelink" command="viewsource" label="viewsource" classAct="sourcelink active" /></li> <li><roundcube:button class="openlink" command="open" label="openinextwin" target="_blank" classAct="openlink active" /></li> <roundcube:container name="messagemenu" id="messagemenu" /> diff --git a/skins/larry/includes/mailtoolbar.html b/skins/larry/includes/mailtoolbar.html index f750e061a..60cebe01b 100644 --- a/skins/larry/includes/mailtoolbar.html +++ b/skins/larry/includes/mailtoolbar.html @@ -37,7 +37,7 @@ <ul class="toolbarmenu iconized"> <li><roundcube:button command="print" label="printmessage" class="icon" classAct="icon active" innerclass="icon print" /></li> <li><roundcube:button command="download" label="emlsave" class="icon" classAct="icon active" innerclass="icon download" /></li> - <li><roundcube:button command="edit" label="editasnew" class="icon" classAct="icon active" innerclass="icon edit" /></li> + <li><roundcube:button command="edit" prop="new" label="editasnew" class="icon" classAct="icon active" innerclass="icon edit" /></li> <li><roundcube:button command="viewsource" label="viewsource" class="icon" classAct="icon active" innerclass="icon viewsource" /></li> <li><roundcube:button command="open" label="openinextwin" target="_blank" class="icon" classAct="icon active" innerclass="icon extwin" /></li> <roundcube:container name="messagemenu" id="messagemenu" /> diff --git a/skins/larry/mail.css b/skins/larry/mail.css index 496cbbd15..04f56e134 100644 --- a/skins/larry/mail.css +++ b/skins/larry/mail.css @@ -837,6 +837,7 @@ h3.subject { #messageheader { position: relative; height: auto; + min-height: 52px; margin: 0 8px 0 0; padding: 0 0 0 72px; border-bottom: 2px solid #f0f0f0; diff --git a/skins/larry/ui.js b/skins/larry/ui.js index e3b5eefe4..c221b8681 100644 --- a/skins/larry/ui.js +++ b/skins/larry/ui.js @@ -144,7 +144,7 @@ function rcube_mail_ui() new rcube_splitter({ id:'identviewsplitter', p1:'#identitieslist', p2:'#identity-details', orientation:'v', relative:true, start:266, min:180, size:12 }).init(); } - else if (rcmail.env.action == 'preferences') { + else if (rcmail.env.action == 'preferences' || !rcmail.env.action) { new rcube_splitter({ id:'prefviewsplitter', p1:'#sectionslist', p2:'#preferences-box', orientation:'v', relative:true, start:266, min:180, size:12 }).init(); } diff --git a/tests/Framework/Charset.php b/tests/Framework/Charset.php index 9e3fad4d3..1fd1654dc 100644 --- a/tests/Framework/Charset.php +++ b/tests/Framework/Charset.php @@ -14,15 +14,149 @@ class Framework_Charset extends PHPUnit_Framework_TestCase function data_clean() { return array( - array('', '', 'Empty string'), + array('', ''), + array("\xC1", ''), ); } /** * @dataProvider data_clean */ - function test_clean($input, $output, $title) + function test_clean($input, $output) { - $this->assertEquals(rcube_charset::clean($input), $output, $title); + $this->assertEquals($output, rcube_charset::clean($input)); } + + /** + * Data for test_parse_charset() + */ + function data_parse_charset() + { + return array( + array('UTF8', 'UTF-8'), + array('WIN1250', 'WINDOWS-1250'), + ); + } + + /** + * @dataProvider data_parse_charset + */ + function test_parse_charset($input, $output) + { + $this->assertEquals($output, rcube_charset::parse_charset($input)); + } + + /** + * Data for test_convert() + */ + function data_convert() + { + return array( + array('ö', 'ö', 'UTF-8', 'UTF-8'), + array('ö', '', 'UTF-8', 'US-ASCII'), + array('aż', 'a', 'UTF-8', 'US-ASCII'), + array('&BCAEMARBBEEESwQ7BDoEOA-', 'Рассылки', 'UTF7-IMAP', 'UTF-8'), + array('Рассылки', '&BCAEMARBBEEESwQ7BDoEOA-', 'UTF-8', 'UTF7-IMAP'), + ); + } + + /** + * @dataProvider data_convert + */ + function test_convert($input, $output, $from, $to) + { + $this->assertEquals($output, rcube_charset::convert($input, $from, $to)); + } + + /** + * Data for test_utf7_to_utf8() + */ + function data_utf7_to_utf8() + { + return array( + array('+BCAEMARBBEEESwQ7BDoEOA-', 'Рассылки'), + ); + } + + /** + * @dataProvider data_utf7_to_utf8 + */ + function test_utf7_to_utf8($input, $output) + { + $this->assertEquals($output, rcube_charset::utf7_to_utf8($input)); + } + + /** + * Data for test_utf7imap_to_utf8() + */ + function data_utf7imap_to_utf8() + { + return array( + array('&BCAEMARBBEEESwQ7BDoEOA-', 'Рассылки'), + ); + } + + /** + * @dataProvider data_utf7imap_to_utf8 + */ + function test_utf7imap_to_utf8($input, $output) + { + $this->assertEquals($output, rcube_charset::utf7imap_to_utf8($input)); + } + + /** + * Data for test_utf8_to_utf7imap() + */ + function data_utf8_to_utf7imap() + { + return array( + array('Рассылки', '&BCAEMARBBEEESwQ7BDoEOA-'), + ); + } + + /** + * @dataProvider data_utf8_to_utf7imap + */ + function test_utf8_to_utf7imap($input, $output) + { + $this->assertEquals($output, rcube_charset::utf8_to_utf7imap($input)); + } + + /** + * Data for test_utf16_to_utf8() + */ + function data_utf16_to_utf8() + { + return array( + array(base64_decode('BCAEMARBBEEESwQ7BDoEOA=='), 'Рассылки'), + ); + } + + /** + * @dataProvider data_utf16_to_utf8 + */ + function test_utf16_to_utf8($input, $output) + { + $this->assertEquals($output, rcube_charset::utf16_to_utf8($input)); + } + + /** + * Data for test_detect() + */ + function data_detect() + { + return array( + array('', '', 'UTF-8'), + array('a', 'UTF-8', 'UTF-8'), + ); + } + + /** + * @dataProvider data_detect + */ + function test_detect($input, $fallback, $output) + { + $this->assertEquals($output, rcube_charset::detect($input, $fallback)); + } + } diff --git a/tests/Framework/Shared.php b/tests/Framework/Shared.php index 99ef829da..0394cd025 100644 --- a/tests/Framework/Shared.php +++ b/tests/Framework/Shared.php @@ -201,4 +201,32 @@ class Framework_Shared extends PHPUnit_Framework_TestCase } + /** + * rcube_shared.inc: is_ascii() + */ + function test_is_ascii() + { + $result = is_ascii("0123456789"); + $this->assertTrue($result, "Valid ASCII (numbers)"); + + $result = is_ascii("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"); + $this->assertTrue($result, "Valid ASCII (letters)"); + + $result = is_ascii(" !\"#\$%&'()*+,-./:;<=>?@[\\^_`{|}~"); + $this->assertTrue($result, "Valid ASCII (special characters)"); + + $result = is_ascii("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F" + ."\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"); + $this->assertTrue($result, "Valid ASCII (control characters)"); + + $result = is_ascii("\n", false); + $this->assertFalse($result, "Valid ASCII (control characters)"); + + $result = is_ascii("ż"); + $this->assertFalse($result, "Invalid ASCII (UTF-8 character)"); + + $result = is_ascii("ż", false); + $this->assertFalse($result, "Invalid ASCII (UTF-8 character [2])"); + } + } diff --git a/tests/Framework/VCard.php b/tests/Framework/VCard.php index a830c2cbc..56ca9d721 100644 --- a/tests/Framework/VCard.php +++ b/tests/Framework/VCard.php @@ -49,6 +49,20 @@ class Framework_VCard extends PHPUnit_Framework_TestCase $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')); 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
|