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 | 522 +++++++++++++++++++++++++++++++++++----------------------- 1 files changed, 315 insertions(+), 207 deletions(-) diff --git a/program/lib/Roundcube/rcube_imap_generic.php b/program/lib/Roundcube/rcube_imap_generic.php index 8532cf8..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,6 +73,9 @@ const COMMAND_NORESPONSE = 1; const COMMAND_CAPABILITY = 2; const COMMAND_LASTLINE = 4; + const COMMAND_ANONYMIZED = 8; + + const DEBUG_LINE_LENGTH = 4098; // 4KB + 2B for \r\n /** * Object constructor @@ -84,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" : '')); @@ -112,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; @@ -134,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; @@ -149,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; @@ -515,7 +533,7 @@ $reply = base64_encode($user . ' ' . $hash); // send result - $this->putLine($reply); + $this->putLine($reply, true, true); } else { // RFC2831: DIGEST-MD5 @@ -533,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] == '+') { @@ -573,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"); @@ -584,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); } @@ -615,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)) { @@ -702,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 = ''; @@ -746,7 +757,7 @@ } if ($this->prefs['timeout'] <= 0) { - $this->prefs['timeout'] = ini_get('default_socket_timeout'); + $this->prefs['timeout'] = max(0, intval(ini_get('default_socket_timeout'))); } // Connect @@ -794,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 @@ -889,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']); + } } /** @@ -1077,7 +1116,7 @@ } if (!$this->data['READ-WRITE']) { - $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'EXPUNGE'); + $this->setError(self::ERROR_READONLY, "Mailbox is read-only"); return false; } @@ -1152,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); } @@ -1254,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. @@ -1270,6 +1318,7 @@ } $args = array(); + $rets = array(); if (!empty($select_opts) && $this->getCapability('LIST-EXTENDED')) { $select_opts = (array) $select_opts; @@ -1280,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); @@ -1327,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; @@ -1531,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); } @@ -1555,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; @@ -1595,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'; @@ -1652,7 +1714,6 @@ } if (!empty($criteria)) { - $modseq = stripos($criteria, 'MODSEQ') !== false; $params .= ($params ? ' ' : '') . $criteria; } else { @@ -1791,7 +1852,6 @@ if ($skip_deleted && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) { $flags = explode(' ', strtoupper($matches[1])); if (in_array('\\DELETED', $flags)) { - $deleted[$id] = $id; continue; } } @@ -1936,7 +1996,7 @@ } if (!$this->data['READ-WRITE']) { - $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE'); + $this->setError(self::ERROR_READONLY, "Mailbox is read-only"); return false; } @@ -1997,7 +2057,7 @@ } if (!$this->data['READ-WRITE']) { - $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE'); + $this->setError(self::ERROR_READONLY, "Mailbox is read-only"); return false; } @@ -2010,31 +2070,32 @@ unset($this->data['STATUS:'.$to]); unset($this->data['STATUS:'.$from]); - $r = $this->execute('UID MOVE', array( + $result = $this->execute('UID MOVE', array( $this->compressMessageSet($messages), $this->escape($to)), self::COMMAND_NORESPONSE); + + return ($result == self::ERROR_OK); } + // use COPY + STORE +FLAGS.SILENT \Deleted + EXPUNGE - else { - $r = $this->copy($messages, $from, $to); + $result = $this->copy($messages, $from, $to); - if ($r) { - // Clear internal status cache - unset($this->data['STATUS:'.$from]); + if ($result) { + // Clear internal status cache + unset($this->data['STATUS:'.$from]); - $r = $this->flag($from, $messages, 'DELETED'); + $result = $this->flag($from, $messages, 'DELETED'); - if ($messages == '*') { - // CLOSE+SELECT should be faster than EXPUNGE - $this->close(); - } - else { - $this->expunge($from, $messages); - } + if ($messages == '*') { + // CLOSE+SELECT should be faster than EXPUNGE + $this->close(); + } + else { + $this->expunge($from, $messages); } } - return $r; + return $result; } /** @@ -2157,21 +2218,25 @@ 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; } } // create array with header field:data if (!empty($headers)) { $headers = explode("\n", trim($headers)); - foreach ($headers as $hid => $resln) { + foreach ($headers as $resln) { if (ord($resln[0]) <= 32) { $lines[$ln] .= (empty($lines[$ln]) ? '' : "\n") . trim($resln); } else { @@ -2179,7 +2244,7 @@ } } - while (list($lines_key, $str) = each($lines)) { + foreach ($lines as $str) { list($field, $string) = explode(':', $str, 2); $field = strtolower($field); @@ -2264,24 +2329,53 @@ return $result; } - function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = false, $add = '') + /** + * Returns message(s) data (flags, headers, etc.) + * + * @param string $mailbox Mailbox name + * @param mixed $message_set Message(s) sequence identifier(s) or UID(s) + * @param bool $is_uid True if $message_set contains UIDs + * @param bool $bodystr Enable to add BODYSTRUCTURE data to the result + * @param array $add_headers List of additional headers + * + * @return bool|array List of rcube_message_header elements, False on error + */ + function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = false, $add_headers = array()) { $query_items = array('UID', 'RFC822.SIZE', 'FLAGS', 'INTERNALDATE'); - if ($bodystr) + $headers = array('DATE', 'FROM', 'TO', 'SUBJECT', 'CONTENT-TYPE', 'CC', 'REPLY-TO', + 'LIST-POST', 'DISPOSITION-NOTIFICATION-TO', 'X-PRIORITY'); + + if (!empty($add_headers)) { + $add_headers = array_map('strtoupper', $add_headers); + $headers = array_unique(array_merge($headers, $add_headers)); + } + + if ($bodystr) { $query_items[] = 'BODYSTRUCTURE'; - $query_items[] = 'BODY.PEEK[HEADER.FIELDS (' - . 'DATE FROM TO SUBJECT CONTENT-TYPE CC REPLY-TO LIST-POST DISPOSITION-NOTIFICATION-TO X-PRIORITY' - . ($add ? ' ' . trim($add) : '') - . ')]'; + } + + $query_items[] = 'BODY.PEEK[HEADER.FIELDS (' . implode(' ', $headers) . ')]'; $result = $this->fetch($mailbox, $message_set, $is_uid, $query_items); return $result; } - function fetchHeader($mailbox, $id, $uidfetch=false, $bodystr=false, $add='') + /** + * Returns message data (flags, headers, etc.) + * + * @param string $mailbox Mailbox name + * @param int $id Message sequence identifier or UID + * @param bool $is_uid True if $id is an UID + * @param bool $bodystr Enable to add BODYSTRUCTURE data to the result + * @param array $add_headers List of additional headers + * + * @return bool|rcube_message_header Message data, False on error + */ + function fetchHeader($mailbox, $id, $is_uid = false, $bodystr = false, $add_headers = array()) { - $a = $this->fetchHeaders($mailbox, $id, $uidfetch, $bodystr, $add); + $a = $this->fetchHeaders($mailbox, $id, $is_uid, $bodystr, $add_headers); if (is_array($a)) { return array_shift($a); } @@ -2445,6 +2539,7 @@ $key = $this->nextTag(); $request = $key . ($is_uid ? ' UID' : '') . " FETCH $id ($fetch_mode.PEEK[$part]$partial)"; $result = false; + $found = false; // send request if (!$this->putLine($request)) { @@ -2453,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; } @@ -2464,18 +2559,26 @@ break; } - if (!preg_match('/^\* ([0-9]+) FETCH (.*)$/', $line, $m)) { + // skip irrelevant untagged responses (we have a result already) + if ($found || !preg_match('/^\* ([0-9]+) FETCH (.*)$/', $line, $m)) { continue; } $line = $m[2]; - $last = substr($line, -1); // handle one line response - if ($line[0] == '(' && $last == ')') { + if ($line[0] == '(' && substr($line, -1) == ')') { // tokenize content inside brackets - $tokens = $this->tokenizeResponse(preg_replace('/(^\(|\$)/', '', $line)); - $result = count($tokens) == 1 ? $tokens[0] : false; + // 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) { + if (preg_match('/^(BODY|BINARY)/i', $tokens[$i])) { + $result = $tokens[$i+1]; + $found = true; + break; + } + } if ($result !== false) { if ($mode == 1) { @@ -2493,8 +2596,13 @@ else if (preg_match('/\{([0-9]+)\}$/', $line, $m)) { $bytes = (int) $m[1]; $prev = ''; + $found = true; - while ($bytes > 0) { + // empty body + if (!$bytes) { + $result = ''; + } + else while ($bytes > 0) { $line = $this->readLine(8192); if ($line === NULL) { @@ -2576,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 */ @@ -2594,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; } @@ -2625,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; } @@ -2664,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); } /** @@ -2969,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); @@ -3118,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)); @@ -3374,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, ''); } @@ -3466,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])); } @@ -3501,7 +3582,7 @@ if (is_array($element)) { reset($element); - while (list($key, $value) = each($element)) { + foreach ($element as $value) { $string .= ' ' . self::r_implode($value); } } @@ -3637,8 +3718,20 @@ */ static function strToTime($date) { - // support non-standard "GMTXXXX" literal - $date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date); + // Clean malformed data + $date = preg_replace( + array( + '/GMT\s*([+-][0-9]+)/', // support non-standard "GMTXXXX" literal + '/[^a-z0-9\x20\x09:+-]/i', // remove any invalid characters + '/\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*/i', // remove weekday names + ), + array( + '\\1', + '', + '', + ), $date); + + $date = trim($date); // if date parsing fails, we have a date in non-rfc format // remove token from the end and try again @@ -3663,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) { @@ -3708,9 +3809,10 @@ /** * Set the value of the debugging flag. * - * @param boolean $debug New value for the debugging flag. + * @param boolean $debug New value for the debugging flag. + * @param callback $handler Logging handler function * - * @since 0.5-stable + * @since 0.5-stable */ function setDebug($debug, $handler = null) { @@ -3721,12 +3823,18 @@ /** * Write the given debug text to the current debug output handler. * - * @param string $message Debug mesage text. + * @param string $message Debug mesage text. * - * @since 0.5-stable + * @since 0.5-stable */ private function debug($message) { + if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) { + $diff = $len - self::DEBUG_LINE_LENGTH; + $message = substr($message, 0, self::DEBUG_LINE_LENGTH) + . "... [truncated $diff bytes]"; + } + if ($this->resourceid) { $message = sprintf('[%s] %s', $this->resourceid, $message); } -- Gitblit v1.9.1