From 3412e50b54e3daac8745234e21ab6e72be0ed165 Mon Sep 17 00:00:00 2001
From: Thomas Bruederli <thomas@roundcube.net>
Date: Wed, 04 Jun 2014 11:20:33 -0400
Subject: [PATCH] Fix attachment menu structure and aria-attributes
---
program/lib/Roundcube/rcube_imap_generic.php | 385 +++++++++++++++++++++++++++++++-----------------------
1 files changed, 218 insertions(+), 167 deletions(-)
diff --git a/program/lib/Roundcube/rcube_imap_generic.php b/program/lib/Roundcube/rcube_imap_generic.php
index 1d2a9be..f45694d 100644
--- a/program/lib/Roundcube/rcube_imap_generic.php
+++ b/program/lib/Roundcube/rcube_imap_generic.php
@@ -48,6 +48,8 @@
'*' => '\\*',
);
+ public static $mupdate;
+
private $fp;
private $host;
private $logged = false;
@@ -71,8 +73,9 @@
const COMMAND_NORESPONSE = 1;
const COMMAND_CAPABILITY = 2;
const COMMAND_LASTLINE = 4;
+ const COMMAND_ANONYMIZED = 8;
- const DEBUG_LINE_LENGTH = 4096;
+ const DEBUG_LINE_LENGTH = 4098; // 4KB + 2B for \r\n
/**
* Object constructor
@@ -86,16 +89,28 @@
*
* @param string $string Command string
* @param bool $endln True if CRLF need to be added at the end of command
+ * @param bool $anonymized Don't write the given data to log but a placeholder
*
* @param int Number of bytes sent, False on error
*/
- function putLine($string, $endln=true)
+ function putLine($string, $endln=true, $anonymized=false)
{
if (!$this->fp)
return false;
if ($this->_debug) {
- $this->debug('C: '. rtrim($string));
+ // anonymize the sent command for logging
+ $cut = $endln ? 2 : 0;
+ if ($anonymized && preg_match('/^(A\d+ (?:[A-Z]+ )+)(.+)/', $string, $m)) {
+ $log = $m[1] . sprintf('****** [%d]', strlen($m[2]) - $cut);
+ }
+ else if ($anonymized) {
+ $log = sprintf('****** [%d]', strlen($string) - $cut);
+ }
+ else {
+ $log = rtrim($string);
+ }
+ $this->debug('C: ' . $log);
}
$res = fwrite($this->fp, $string . ($endln ? "\r\n" : ''));
@@ -114,10 +129,11 @@
*
* @param string $string Command string
* @param bool $endln True if CRLF need to be added at the end of command
+ * @param bool $anonymized Don't write the given data to log but a placeholder
*
* @return int|bool Number of bytes sent, False on error
*/
- function putLineC($string, $endln=true)
+ function putLineC($string, $endln=true, $anonymized=false)
{
if (!$this->fp) {
return false;
@@ -136,7 +152,7 @@
$parts[$i+1] = sprintf("{%d+}\r\n", $matches[1]);
}
- $bytes = $this->putLine($parts[$i].$parts[$i+1], false);
+ $bytes = $this->putLine($parts[$i].$parts[$i+1], false, $anonymized);
if ($bytes === false)
return false;
$res += $bytes;
@@ -151,7 +167,7 @@
$i++;
}
else {
- $bytes = $this->putLine($parts[$i], false);
+ $bytes = $this->putLine($parts[$i], false, $anonymized);
if ($bytes === false)
return false;
$res += $bytes;
@@ -517,7 +533,7 @@
$reply = base64_encode($user . ' ' . $hash);
// send result
- $this->putLine($reply);
+ $this->putLine($reply, true, true);
}
else {
// RFC2831: DIGEST-MD5
@@ -535,7 +551,7 @@
base64_decode($challenge), $this->host, 'imap', $user));
// send result
- $this->putLine($reply);
+ $this->putLine($reply, true, true);
$line = trim($this->readReply());
if ($line[0] == '+') {
@@ -575,7 +591,7 @@
// RFC 4959 (SASL-IR): save one round trip
if ($this->getCapability('SASL-IR')) {
list($result, $line) = $this->execute("AUTHENTICATE PLAIN", array($reply),
- self::COMMAND_LASTLINE | self::COMMAND_CAPABILITY);
+ self::COMMAND_LASTLINE | self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED);
}
else {
$this->putLine($this->nextTag() . " AUTHENTICATE PLAIN");
@@ -586,7 +602,7 @@
}
// send result, get reply and process it
- $this->putLine($reply);
+ $this->putLine($reply, true, true);
$line = $this->readReply();
$result = $this->parseResult($line);
}
@@ -617,7 +633,7 @@
function login($user, $password)
{
list($code, $response) = $this->execute('LOGIN', array(
- $this->escape($user), $this->escape($password)), self::COMMAND_CAPABILITY);
+ $this->escape($user), $this->escape($password)), self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED);
// re-set capabilities list if untagged CAPABILITY response provided
if (preg_match('/\* CAPABILITY (.+)/i', $response, $matches)) {
@@ -704,18 +720,11 @@
*/
function connect($host, $user, $password, $options=null)
{
- // set options
- if (is_array($options)) {
- $this->prefs = $options;
- }
- // set auth method
- if (!empty($this->prefs['auth_type'])) {
- $auth_method = strtoupper($this->prefs['auth_type']);
- } else {
- $auth_method = 'CHECK';
- }
+ // configure
+ $this->set_prefs($options);
- $result = false;
+ $auth_method = $this->prefs['auth_type'];
+ $result = false;
// initialize connection
$this->error = '';
@@ -796,23 +805,21 @@
// TLS connection
if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) {
- if (version_compare(PHP_VERSION, '5.1.0', '>=')) {
- $res = $this->execute('STARTTLS');
+ $res = $this->execute('STARTTLS');
- if ($res[0] != self::ERROR_OK) {
- $this->closeConnection();
- return false;
- }
-
- if (!stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
- $this->setError(self::ERROR_BAD, "Unable to negotiate TLS");
- $this->closeConnection();
- return false;
- }
-
- // Now we're secure, capabilities need to be reread
- $this->clearCapability();
+ if ($res[0] != self::ERROR_OK) {
+ $this->closeConnection();
+ return false;
}
+
+ if (!stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
+ $this->setError(self::ERROR_BAD, "Unable to negotiate TLS");
+ $this->closeConnection();
+ return false;
+ }
+
+ // Now we're secure, capabilities need to be reread
+ $this->clearCapability();
}
// Send ID info
@@ -891,6 +898,36 @@
$this->closeConnection();
return false;
+ }
+
+ /**
+ * Initializes environment
+ */
+ protected function set_prefs($prefs)
+ {
+ // set preferences
+ if (is_array($prefs)) {
+ $this->prefs = $prefs;
+ }
+
+ // set auth method
+ if (!empty($this->prefs['auth_type'])) {
+ $this->prefs['auth_type'] = strtoupper($this->prefs['auth_type']);
+ }
+ else {
+ $this->prefs['auth_type'] = 'CHECK';
+ }
+
+ // disabled capabilities
+ if (!empty($this->prefs['disabled_caps'])) {
+ $this->prefs['disabled_caps'] = array_map('strtoupper', (array)$this->prefs['disabled_caps']);
+ }
+
+ // additional message flags
+ if (!empty($this->prefs['message_flags'])) {
+ $this->flags = array_merge($this->flags, $this->prefs['message_flags']);
+ unset($this->prefs['message_flags']);
+ }
}
/**
@@ -1154,13 +1191,20 @@
* Folder creation (CREATE)
*
* @param string $mailbox Mailbox name
+ * @param array $types Optional folder types (RFC 6154)
*
* @return bool True on success, False on error
*/
- function createFolder($mailbox)
+ function createFolder($mailbox, $types = null)
{
- $result = $this->execute('CREATE', array($this->escape($mailbox)),
- self::COMMAND_NORESPONSE);
+ $args = array($this->escape($mailbox));
+
+ // RFC 6154: CREATE-SPECIAL-USE
+ if (!empty($types) && $this->getCapability('CREATE-SPECIAL-USE')) {
+ $args[] = '(USE (' . implode(' ', $types) . '))';
+ }
+
+ $result = $this->execute('CREATE', $args, self::COMMAND_NORESPONSE);
return ($result == self::ERROR_OK);
}
@@ -1256,10 +1300,12 @@
* @param string $ref Reference name
* @param string $mailbox Mailbox name
* @param bool $subscribed Enables returning subscribed mailboxes only
- * @param array $status_opts List of STATUS options (RFC5819: LIST-STATUS)
- * Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN
+ * @param array $status_opts List of STATUS options
+ * (RFC5819: LIST-STATUS: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN)
+ * or RETURN options (RFC5258: LIST_EXTENDED: SUBSCRIBED, CHILDREN)
* @param array $select_opts List of selection options (RFC5258: LIST-EXTENDED)
- * Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE
+ * Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE,
+ * SPECIAL-USE (RFC6154)
*
* @return array List of mailboxes or hash of options if $status_ops argument
* is non-empty.
@@ -1272,6 +1318,7 @@
}
$args = array();
+ $rets = array();
if (!empty($select_opts) && $this->getCapability('LIST-EXTENDED')) {
$select_opts = (array) $select_opts;
@@ -1282,11 +1329,21 @@
$args[] = $this->escape($ref);
$args[] = $this->escape($mailbox);
- if (!empty($status_opts) && $this->getCapability('LIST-STATUS')) {
- $status_opts = (array) $status_opts;
- $lstatus = true;
+ if (!empty($status_opts) && $this->getCapability('LIST-EXTENDED')) {
+ $rets = array_intersect($status_opts, array('SUBSCRIBED', 'CHILDREN'));
+ }
- $args[] = 'RETURN (STATUS (' . implode(' ', $status_opts) . '))';
+ if (!empty($status_opts) && $this->getCapability('LIST-STATUS')) {
+ $status_opts = array_intersect($status_opts, array('MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN'));
+
+ if (!empty($status_opts)) {
+ $lstatus = true;
+ $rets[] = 'STATUS (' . implode(' ', $status_opts) . ')';
+ }
+ }
+
+ if (!empty($rets)) {
+ $args[] = 'RETURN (' . implode(' ', $rets) . ')';
}
list($code, $response) = $this->execute($subscribed ? 'LSUB' : 'LIST', $args);
@@ -1329,9 +1386,8 @@
$folders[$mailbox] = array();
}
- // store LSUB options only if not empty, this way
- // we can detect a situation when LIST doesn't return specified folder
- if (!empty($opts) || $cmd == 'LIST') {
+ // store folder options
+ if ($cmd == 'LIST') {
// Add to options array
if (empty($this->data['LIST'][$mailbox]))
$this->data['LIST'][$mailbox] = $opts;
@@ -1533,23 +1589,23 @@
*
* @param string $mailbox Mailbox name
* @param string $field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
- * @param string $add Searching criteria
+ * @param string $criteria Searching criteria
* @param bool $return_uid Enables UID SORT usage
* @param string $encoding Character set
*
* @return rcube_result_index Response data
*/
- function sort($mailbox, $field, $add='', $return_uid=false, $encoding = 'US-ASCII')
+ function sort($mailbox, $field = 'ARRIVAL', $criteria = '', $return_uid = false, $encoding = 'US-ASCII')
{
- $field = strtoupper($field);
+ $old_sel = $this->selected;
+ $supported = array('ARRIVAL', 'CC', 'DATE', 'FROM', 'SIZE', 'SUBJECT', 'TO');
+ $field = strtoupper($field);
+
if ($field == 'INTERNALDATE') {
$field = 'ARRIVAL';
}
- $fields = array('ARRIVAL' => 1,'CC' => 1,'DATE' => 1,
- 'FROM' => 1, 'SIZE' => 1, 'SUBJECT' => 1, 'TO' => 1);
-
- if (!$fields[$field]) {
+ if (!in_array($field, $supported)) {
return new rcube_result_index($mailbox);
}
@@ -1557,17 +1613,21 @@
return new rcube_result_index($mailbox);
}
+ // return empty result when folder is empty and we're just after SELECT
+ if ($old_sel != $mailbox && !$this->data['EXISTS']) {
+ return new rcube_result_index($mailbox, '* SORT');
+ }
+
// RFC 5957: SORT=DISPLAY
if (($field == 'FROM' || $field == 'TO') && $this->getCapability('SORT=DISPLAY')) {
$field = 'DISPLAY' . $field;
}
- // message IDs
- if (!empty($add))
- $add = $this->compressMessageSet($add);
+ $encoding = $encoding ? trim($encoding) : 'US-ASCII';
+ $criteria = $criteria ? 'ALL ' . trim($criteria) : 'ALL';
list($code, $response) = $this->execute($return_uid ? 'UID SORT' : 'SORT',
- array("($field)", $encoding, 'ALL' . (!empty($add) ? ' '.$add : '')));
+ array("($field)", $encoding, $criteria));
if ($code != self::ERROR_OK) {
$response = null;
@@ -1597,7 +1657,7 @@
// return empty result when folder is empty and we're just after SELECT
if ($old_sel != $mailbox && !$this->data['EXISTS']) {
- return new rcube_result_thread($mailbox);
+ return new rcube_result_thread($mailbox, '* THREAD');
}
$encoding = $encoding ? trim($encoding) : 'US-ASCII';
@@ -2158,14 +2218,18 @@
else if ($name == 'RFC822') {
$result[$id]->body = $value;
}
- else if ($name == 'BODY') {
- $body = $this->tokenizeResponse($line, 1);
- if ($value[0] == 'HEADER.FIELDS')
- $headers = $body;
- else if (!empty($value))
- $result[$id]->bodypart[$value[0]] = $body;
+ else if (stripos($name, 'BODY[') === 0) {
+ $name = str_replace(']', '', substr($name, 5));
+
+ if ($name == 'HEADER.FIELDS') {
+ // skip ']' after headers list
+ $this->tokenizeResponse($line, 1);
+ $headers = $this->tokenizeResponse($line, 1);
+ }
+ else if (strlen($name))
+ $result[$id]->bodypart[$name] = $value;
else
- $result[$id]->body = $body;
+ $result[$id]->body = $value;
}
}
@@ -2484,7 +2548,7 @@
}
if ($binary) {
- // WARNING: Use $formatting argument with care, this may break binary data stream
+ // WARNING: Use $formatted argument with care, this may break binary data stream
$mode = -1;
}
@@ -2505,6 +2569,7 @@
// handle one line response
if ($line[0] == '(' && substr($line, -1) == ')') {
// tokenize content inside brackets
+ // the content can be e.g.: (UID 9844 BODY[2.4] NIL)
$tokens = $this->tokenizeResponse(preg_replace('/(^\(|\)$)/', '', $line));
for ($i=0; $i<count($tokens); $i+=2) {
@@ -2533,7 +2598,11 @@
$prev = '';
$found = true;
- while ($bytes > 0) {
+ // empty body
+ if (!$bytes) {
+ $result = '';
+ }
+ else while ($bytes > 0) {
$line = $this->readLine(8192);
if ($line === NULL) {
@@ -2615,11 +2684,11 @@
/**
* Handler for IMAP APPEND command
*
- * @param string $mailbox Mailbox name
- * @param string $message Message content
- * @param array $flags Message flags
- * @param string $date Message internal date
- * @param bool $binary Enable BINARY append (RFC3516)
+ * @param string $mailbox Mailbox name
+ * @param string|array $message The message source string or array (of strings and file pointers)
+ * @param array $flags Message flags
+ * @param string $date Message internal date
+ * @param bool $binary Enable BINARY append (RFC3516)
*
* @return string|bool On success APPENDUID response (if available) or True, False on failure
*/
@@ -2633,13 +2702,28 @@
$binary = $binary && $this->getCapability('BINARY');
$literal_plus = !$binary && $this->prefs['literal+'];
+ $len = 0;
+ $msg = is_array($message) ? $message : array(&$message);
+ $chunk_size = 512000;
- if (!$binary) {
- $message = str_replace("\r", '', $message);
- $message = str_replace("\n", "\r\n", $message);
+ for ($i=0, $cnt=count($msg); $i<$cnt; $i++) {
+ if (is_resource($msg[$i])) {
+ $stat = fstat($msg[$i]);
+ if ($stat === false) {
+ return false;
+ }
+ $len += $stat['size'];
+ }
+ else {
+ if (!$binary) {
+ $msg[$i] = str_replace("\r", '', $msg[$i]);
+ $msg[$i] = str_replace("\n", "\r\n", $msg[$i]);
+ }
+
+ $len += strlen($msg[$i]);
+ }
}
- $len = strlen($message);
if (!$len) {
return false;
}
@@ -2664,7 +2748,32 @@
}
}
- if (!$this->putLine($message)) {
+ foreach ($msg as $msg_part) {
+ // file pointer
+ if (is_resource($msg_part)) {
+ rewind($msg_part);
+ while (!feof($msg_part) && $this->fp) {
+ $buffer = fread($msg_part, $chunk_size);
+ $this->putLine($buffer, false);
+ }
+ fclose($msg_part);
+ }
+ // string
+ else {
+ $size = strlen($msg_part);
+
+ // Break up the data by sending one chunk (up to 512k) at a time.
+ // This approach reduces our peak memory usage
+ for ($offset = 0; $offset < $size; $offset += $chunk_size) {
+ $chunk = substr($msg_part, $offset, $chunk_size);
+ if (!$this->putLine($chunk, false)) {
+ return false;
+ }
+ }
+ }
+ }
+
+ if (!$this->putLine('')) { // \r\n
return false;
}
@@ -2703,94 +2812,23 @@
*/
function appendFromFile($mailbox, $path, $headers=null, $flags = array(), $date = null, $binary = false)
{
- unset($this->data['APPENDUID']);
-
- if ($mailbox === null || $mailbox === '') {
- return false;
- }
-
// open message file
- $in_fp = false;
if (file_exists(realpath($path))) {
- $in_fp = fopen($path, 'r');
+ $fp = fopen($path, 'r');
}
- if (!$in_fp) {
+ if (!$fp) {
$this->setError(self::ERROR_UNKNOWN, "Couldn't open $path for reading");
return false;
}
- $body_separator = "\r\n\r\n";
- $len = filesize($path);
-
- if (!$len) {
- return false;
- }
-
+ $message = array();
if ($headers) {
- $headers = preg_replace('/[\r\n]+$/', '', $headers);
- $len += strlen($headers) + strlen($body_separator);
+ $message[] = trim($headers, "\r\n") . "\r\n\r\n";
}
+ $message[] = $fp;
- $binary = $binary && $this->getCapability('BINARY');
- $literal_plus = !$binary && $this->prefs['literal+'];
-
- // build APPEND command
- $key = $this->nextTag();
- $request = "$key APPEND " . $this->escape($mailbox) . ' (' . $this->flagsToStr($flags) . ')';
- if (!empty($date)) {
- $request .= ' ' . $this->escape($date);
- }
- $request .= ' ' . ($binary ? '~' : '') . '{' . $len . ($literal_plus ? '+' : '') . '}';
-
- // send APPEND command
- if ($this->putLine($request)) {
- // Don't wait when LITERAL+ is supported
- if (!$literal_plus) {
- $line = $this->readReply();
-
- if ($line[0] != '+') {
- $this->parseResult($line, 'APPEND: ');
- return false;
- }
- }
-
- // send headers with body separator
- if ($headers) {
- $this->putLine($headers . $body_separator, false);
- }
-
- // send file
- while (!feof($in_fp) && $this->fp) {
- $buffer = fgets($in_fp, 4096);
- $this->putLine($buffer, false);
- }
- fclose($in_fp);
-
- if (!$this->putLine('')) { // \r\n
- return false;
- }
-
- // read response
- do {
- $line = $this->readLine();
- } while (!$this->startsWith($line, $key, true, true));
-
- // Clear internal status cache
- unset($this->data['STATUS:'.$mailbox]);
-
- if ($this->parseResult($line, 'APPEND: ') != self::ERROR_OK)
- return false;
- else if (!empty($this->data['APPENDUID']))
- return $this->data['APPENDUID'];
- else
- return true;
- }
- else {
- $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
- }
-
- return false;
+ return $this->append($mailbox, $message, $flags, $date, $binary);
}
/**
@@ -3008,7 +3046,7 @@
}
foreach ($entries as $name => $value) {
- $entries[$name] = $this->escape($name) . ' ' . $this->escape($value);
+ $entries[$name] = $this->escape($name) . ' ' . $this->escape($value, true);
}
$entries = implode(' ', $entries);
@@ -3157,6 +3195,11 @@
}
foreach ($data as $entry) {
+ // Workaround cyrus-murder bug, the entry[2] string needs to be escaped
+ if (self::$mupdate) {
+ $entry[2] = addcslashes($entry[2], '\\"');
+ }
+
// ANNOTATEMORE drafts before version 08 require quoted parameters
$entries[] = sprintf('%s (%s %s)', $this->escape($entry[0], true),
$this->escape($entry[1], true), $this->escape($entry[2], true));
@@ -3413,7 +3456,7 @@
}
// Send command
- if (!$this->putLineC($query)) {
+ if (!$this->putLineC($query, true, ($options & self::COMMAND_ANONYMIZED))) {
$this->setError(self::ERROR_COMMAND, "Unable to send command: $query");
return $noresp ? self::ERROR_COMMAND : array(self::ERROR_COMMAND, '');
}
@@ -3505,25 +3548,24 @@
// Parenthesized list
case '(':
- case '[':
$str = substr($str, 1);
$result[] = self::tokenizeResponse($str);
break;
case ')':
- case ']':
$str = substr($str, 1);
return $result;
break;
- // String atom, number, NIL, *, %
+ // String atom, number, astring, NIL, *, %
default:
// empty string
if ($str === '' || $str === null) {
break 2;
}
- // excluded chars: SP, CTL, ), [, ]
- if (preg_match('/^([^\x00-\x20\x29\x5B\x5D\x7F]+)/', $str, $m)) {
+ // excluded chars: SP, CTL, ), DEL
+ // we do not exclude [ and ] (#1489223)
+ if (preg_match('/^([^\x00-\x20\x29\x7F]+)/', $str, $m)) {
$result[] = $m[1] == 'NIL' ? NULL : $m[1];
$str = substr($str, strlen($m[1]));
}
@@ -3714,8 +3756,16 @@
$this->capability = explode(' ', strtoupper($str));
+ if (!empty($this->prefs['disabled_caps'])) {
+ $this->capability = array_diff($this->capability, $this->prefs['disabled_caps']);
+ }
+
if (!isset($this->prefs['literal+']) && in_array('LITERAL+', $this->capability)) {
$this->prefs['literal+'] = true;
+ }
+
+ if (preg_match('/(\[| )MUPDATE=.*/', $str)) {
+ self::$mupdate = true;
}
if ($trusted) {
@@ -3780,8 +3830,9 @@
private function debug($message)
{
if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) {
- $message = substr_replace($message, "\n-----[debug cut]-----\n",
- self::DEBUG_LINE_LENGTH/2 - 11, $len - self::DEBUG_LINE_LENGTH - 22);
+ $diff = $len - self::DEBUG_LINE_LENGTH;
+ $message = substr($message, 0, self::DEBUG_LINE_LENGTH)
+ . "... [truncated $diff bytes]";
}
if ($this->resourceid) {
--
Gitblit v1.9.1