From f8911c2a7f9d41e2197d0c3e1aa49aea62e320fa Mon Sep 17 00:00:00 2001
From: Francis Russell <francis@unchartedbackwaters.co.uk>
Date: Thu, 14 Jan 2016 06:47:49 -0500
Subject: [PATCH] Enable use of TLSv1.1 and TLSv1.2 for IMAP.

---
 program/lib/Roundcube/rcube_imap_generic.php |  295 ++++++++++++++++++++++++++++++++++------------------------
 1 files changed, 174 insertions(+), 121 deletions(-)

diff --git a/program/lib/Roundcube/rcube_imap_generic.php b/program/lib/Roundcube/rcube_imap_generic.php
index 0325064..86a1fcf 100644
--- a/program/lib/Roundcube/rcube_imap_generic.php
+++ b/program/lib/Roundcube/rcube_imap_generic.php
@@ -1,6 +1,6 @@
 <?php
 
-/**
+/*
  +-----------------------------------------------------------------------+
  | This file is part of the Roundcube Webmail client                     |
  | Copyright (C) 2005-2012, The Roundcube Dev Team                       |
@@ -47,8 +47,6 @@
         'MDNSENT'  => '$MDNSent',
         '*'        => '\\*',
     );
-
-    public static $mupdate;
 
     protected $fp;
     protected $host;
@@ -912,7 +910,13 @@
                 return false;
             }
 
-            if (!stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
+            // There is no flag to enable all TLS methods. Net_SMTP
+            // handles enabling TLS similarly.
+            $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT
+                | @STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT
+                | @STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
+
+            if (!stream_socket_enable_crypto($this->fp, true, $crypto_method)) {
                 $this->setError(self::ERROR_BAD, "Unable to negotiate TLS");
                 $this->closeConnection();
                 return false;
@@ -1108,7 +1112,8 @@
             // folder name with spaces. Let's try to handle this situation
             if (!is_array($items) && ($pos = strpos($response, '(')) !== false) {
                 $response = substr($response, $pos);
-                $items = $this->tokenizeResponse($response, 1);
+                $items    = $this->tokenizeResponse($response, 1);
+
                 if (!is_array($items)) {
                     return $result;
                 }
@@ -1146,7 +1151,7 @@
         }
 
         // Clear internal status cache
-        unset($this->data['STATUS:'.$mailbox]);
+        $this->clear_status_cache($mailbox);
 
         if (!empty($messages) && $messages != '*' && $this->hasCapability('UIDPLUS')) {
             $messages = self::compressMessageSet($messages);
@@ -1296,8 +1301,8 @@
      * @param array  $return_opts (see self::_listMailboxes)
      * @param array  $select_opts (see self::_listMailboxes)
      *
-     * @return array List of mailboxes or hash of options if $return_opts argument
-     *               is non-empty.
+     * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
+     *                    is requested, False on error.
      */
     function listMailboxes($ref, $mailbox, $return_opts=array(), $select_opts=array())
     {
@@ -1311,8 +1316,8 @@
      * @param string $mailbox     Mailbox name
      * @param array  $return_opts (see self::_listMailboxes)
      *
-     * @return array List of mailboxes or hash of options if $return_opts argument
-     *               is non-empty.
+     * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
+     *                    is requested, False on error.
      */
     function listSubscribed($ref, $mailbox, $return_opts=array())
     {
@@ -1332,8 +1337,8 @@
      *                            Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE,
      *                                      SPECIAL-USE (RFC6154)
      *
-     * @return array List of mailboxes or hash of options if $status_ops argument
-     *               is non-empty.
+     * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
+     *                    is requested, False on error.
      */
     protected function _listMailboxes($ref, $mailbox, $subscribed=false,
         $return_opts=array(), $select_opts=array())
@@ -1355,7 +1360,9 @@
         $args[] = $this->escape($mailbox);
 
         if (!empty($return_opts) && $this->getCapability('LIST-EXTENDED')) {
-            $rets = array_intersect($return_opts, array('SUBSCRIBED', 'CHILDREN'));
+            $ext_opts    = array('SUBSCRIBED', 'CHILDREN');
+            $rets        = array_intersect($return_opts, $ext_opts);
+            $return_opts = array_diff($return_opts, $rets);
         }
 
         if (!empty($return_opts) && $this->getCapability('LIST-STATUS')) {
@@ -1459,13 +1466,9 @@
      *
      * @return int Number of messages, False on error
      */
-    function countMessages($mailbox, $refresh = false)
+    function countMessages($mailbox)
     {
-        if ($refresh) {
-            $this->selected = null;
-        }
-
-        if ($this->selected === $mailbox) {
+        if ($this->selected === $mailbox && isset($this->data['EXISTS'])) {
             return $this->data['EXISTS'];
         }
 
@@ -1493,14 +1496,20 @@
      */
     function countRecent($mailbox)
     {
-        if (!strlen($mailbox)) {
-            $mailbox = 'INBOX';
+        if ($this->selected === $mailbox && isset($this->data['RECENT'])) {
+            return $this->data['RECENT'];
         }
 
-        $this->select($mailbox);
+        // Check internal cache
+        $cache = $this->data['STATUS:'.$mailbox];
+        if (!empty($cache) && isset($cache['RECENT'])) {
+            return (int) $cache['RECENT'];
+        }
 
-        if ($this->selected === $mailbox) {
-            return $this->data['RECENT'];
+        // Try STATUS (should be faster than SELECT)
+        $counts = $this->status($mailbox, array('RECENT'));
+        if (is_array($counts)) {
+            return (int) $counts['RECENT'];
         }
 
         return false;
@@ -1702,7 +1711,6 @@
         $encoding  = $encoding ? trim($encoding) : 'US-ASCII';
         $algorithm = $algorithm ? trim($algorithm) : 'REFERENCES';
         $criteria  = $criteria ? 'ALL '.trim($criteria) : 'ALL';
-        $data      = '';
 
         list($code, $response) = $this->execute($return_uid ? 'UID THREAD' : 'THREAD',
             array($algorithm, $encoding, $criteria));
@@ -1916,8 +1924,8 @@
                         $result[$id] = '';
                     }
                 } else if ($mode == 2) {
-                    if (preg_match('/(UID|RFC822\.SIZE) ([0-9]+)/', $line, $matches)) {
-                        $result[$id] = trim($matches[2]);
+                    if (preg_match('/' . $index_field . ' ([0-9]+)/', $line, $matches)) {
+                        $result[$id] = trim($matches[1]);
                     } else {
                         $result[$id] = 0;
                     }
@@ -2026,10 +2034,6 @@
      */
     protected function modFlag($mailbox, $messages, $flag, $mod = '+')
     {
-        if ($mod != '+' && $mod != '-') {
-            $mod = '+';
-        }
-
         if (!$this->select($mailbox)) {
             return false;
         }
@@ -2039,12 +2043,31 @@
             return false;
         }
 
+        if ($this->flags[strtoupper($flag)]) {
+            $flag = $this->flags[strtoupper($flag)];
+        }
+
+        if (!$flag) {
+            return false;
+        }
+
+        // if PERMANENTFLAGS is not specified all flags are allowed
+        if (!empty($this->data['PERMANENTFLAGS'])
+            && !in_array($flag, (array) $this->data['PERMANENTFLAGS'])
+            && !in_array('\\*', (array) $this->data['PERMANENTFLAGS'])
+        ) {
+            return false;
+        }
+
         // Clear internal status cache
         if ($flag == 'SEEN') {
             unset($this->data['STATUS:'.$mailbox]['UNSEEN']);
         }
 
-        $flag   = $this->flags[strtoupper($flag)];
+        if ($mod != '+' && $mod != '-') {
+            $mod = '+';
+        }
+
         $result = $this->execute('UID STORE', array(
             $this->compressMessageSet($messages), $mod . 'FLAGS.SILENT', "($flag)"),
             self::COMMAND_NORESPONSE);
@@ -2107,7 +2130,7 @@
 
             // Clear internal status cache
             unset($this->data['STATUS:'.$to]);
-            unset($this->data['STATUS:'.$from]);
+            $this->clear_status_cache($from);
 
             $result = $this->execute('UID MOVE', array(
                 $this->compressMessageSet($messages), $this->escape($to)),
@@ -2421,7 +2444,16 @@
         return false;
     }
 
-    function sortHeaders($a, $field, $flag)
+    /**
+     * Sort messages by specified header field
+     *
+     * @param array  $messages Array of rcube_message_header objects
+     * @param string $field    Name of the property to sort by
+     * @param string $flag     Sorting order (ASC|DESC)
+     *
+     * @return array Sorted input array
+     */
+    public static function sortHeaders($messages, $field, $flag)
     {
         if (empty($field)) {
             $field = 'uid';
@@ -2430,57 +2462,65 @@
             $field = strtolower($field);
         }
 
-        if ($field == 'date' || $field == 'internaldate') {
-            $field = 'timestamp';
-        }
-
         if (empty($flag)) {
             $flag = 'ASC';
-        } else {
+        }
+        else {
             $flag = strtoupper($flag);
         }
 
-        $c = count($a);
-        if ($c > 0) {
-            // Strategy:
-            // First, we'll create an "index" array.
-            // Then, we'll use sort() on that array,
-            // and use that to sort the main array.
+        // Strategy: First, we'll create an "index" array.
+        // Then, we'll use sort() on that array, and use that to sort the main array.
 
-            // create "index" array
-            $index = array();
-            reset($a);
-            while (list($key, $val) = each($a)) {
-                if ($field == 'timestamp') {
-                    $data = $this->strToTime($val->date);
-                    if (!$data) {
-                        $data = $val->timestamp;
-                    }
-                } else {
-                    $data = $val->$field;
-                    if (is_string($data)) {
-                        $data = str_replace('"', '', $data);
-                        if ($field == 'subject') {
-                            $data = preg_replace('/^(Re: \s*|Fwd:\s*|Fw:\s*)+/i', '', $data);
-                        }
-                        $data = strtoupper($data);
-                    }
+        $index  = array();
+        $result = array();
+
+        reset($messages);
+
+        while (list($key, $headers) = each($messages)) {
+            $value = null;
+
+            switch ($field) {
+            case 'arrival':
+                $field = 'internaldate';
+            case 'date':
+            case 'internaldate':
+            case 'timestamp':
+                $value = self::strToTime($headers->$field);
+                if (!$value && $field != 'timestamp') {
+                    $value = $headers->timestamp;
                 }
-                $index[$key] = $data;
+
+                break;
+
+            default:
+                // @TODO: decode header value, convert to UTF-8
+                $value = $headers->$field;
+                if (is_string($value)) {
+                    $value = str_replace('"', '', $value);
+                    if ($field == 'subject') {
+                        $value = preg_replace('/^(Re:\s*|Fwd:\s*|Fw:\s*)+/i', '', $value);
+                    }
+
+                    $data = strtoupper($value);
+                }
             }
 
+            $index[$key] = $value;
+        }
+
+        if (!empty($index)) {
             // sort index
             if ($flag == 'ASC') {
                 asort($index);
-            } else {
+            }
+            else {
                 arsort($index);
             }
 
             // form new array based on index
-            $result = array();
-            reset($index);
             while (list($key, $val) = each($index)) {
-                $result[$key] = $a[$key];
+                $result[$key] = $messages[$key];
             }
         }
 
@@ -2552,50 +2592,61 @@
             return false;
         }
 
-        switch ($encoding) {
-        case 'base64':
-            $mode = 1;
-            break;
-        case 'quoted-printable':
-            $mode = 2;
-            break;
-        case 'x-uuencode':
-        case 'x-uue':
-        case 'uue':
-        case 'uuencode':
-            $mode = 3;
-            break;
-        default:
-            $mode = 0;
-        }
-
-        // Use BINARY extension when possible (and safe)
-        $binary     = $mode && preg_match('/^[0-9.]+$/', $part) && $this->hasCapability('BINARY');
-        $fetch_mode = $binary ? 'BINARY' : 'BODY';
-        $partial    = $max_bytes ? sprintf('<0.%d>', $max_bytes) : '';
-
-        // format request
-        $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)) {
-            $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
-            return false;
-        }
-
-        if ($binary) {
-            // WARNING: Use $formatted argument with care, this may break binary data stream
-            $mode = -1;
-        }
+        $binary    = true;
 
         do {
+            if (!$initiated) {
+                switch ($encoding) {
+                case 'base64':
+                    $mode = 1;
+                    break;
+                case 'quoted-printable':
+                    $mode = 2;
+                    break;
+                case 'x-uuencode':
+                case 'x-uue':
+                case 'uue':
+                case 'uuencode':
+                    $mode = 3;
+                    break;
+                default:
+                    $mode = 0;
+                }
+
+                // Use BINARY extension when possible (and safe)
+                $binary     = $binary && $mode && preg_match('/^[0-9.]+$/', $part) && $this->hasCapability('BINARY');
+                $fetch_mode = $binary ? 'BINARY' : 'BODY';
+                $partial    = $max_bytes ? sprintf('<0.%d>', $max_bytes) : '';
+
+                // format request
+                $key       = $this->nextTag();
+                $request   = $key . ($is_uid ? ' UID' : '') . " FETCH $id ($fetch_mode.PEEK[$part]$partial)";
+                $result    = false;
+                $found     = false;
+                $initiated = true;
+
+                // send request
+                if (!$this->putLine($request)) {
+                    $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
+                    return false;
+                }
+
+                if ($binary) {
+                    // WARNING: Use $formatted argument with care, this may break binary data stream
+                    $mode = -1;
+                }
+            }
+
             $line = trim($this->readLine(1024));
 
             if (!$line) {
                 break;
+            }
+
+            // handle UNKNOWN-CTE response - RFC 3516, try again with standard BODY request
+            if ($binary && !$found && preg_match('/^' . $key . ' NO \[UNKNOWN-CTE\]/i', $line)) {
+                $binary = $initiated = false;
+                continue;
             }
 
             // skip irrelevant untagged responses (we have a result already)
@@ -2658,7 +2709,7 @@
 
                     // BASE64
                     if ($mode == 1) {
-                        $line = rtrim($line, "\t\r\n\0\x0B");
+                        $line = preg_replace('|[^a-zA-Z0-9+=/]|', '', $line);
                         // create chunks with proper length for base64 decoding
                         $line = $prev.$line;
                         $length = strlen($line);
@@ -2703,7 +2754,7 @@
                     }
                 }
             }
-        } while (!$this->startsWith($line, $key, true));
+        } while (!$this->startsWith($line, $key, true) || !$initiated);
 
         if ($result !== false) {
             if ($file) {
@@ -3188,9 +3239,9 @@
                 for ($i=0; $i<$size; $i++) {
                     if (isset($mbox) && is_array($data[$i])) {
                         $size_sub = count($data[$i]);
-                        for ($x=0; $x<$size_sub; $x++) {
+                        for ($x=0; $x<$size_sub; $x+=2) {
                             if ($data[$i][$x+1] !== null)
-                                $result[$mbox][$data[$i][$x]] = $data[$i][++$x];
+                                $result[$mbox][$data[$i][$x]] = $data[$i][$x+1];
                         }
                         unset($data[$i]);
                     }
@@ -3208,8 +3259,8 @@
                         }
                     }
                     else if (isset($mbox)) {
-                        if ($data[$i+1] !== null)
-                            $result[$mbox][$data[$i]] = $data[++$i];
+                        if ($data[++$i] !== null)
+                            $result[$mbox][$data[$i-1]] = $data[$i];
                         unset($data[$i]);
                         unset($data[$i-1]);
                     }
@@ -3243,11 +3294,6 @@
         }
 
         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));
@@ -3740,6 +3786,17 @@
     }
 
     /**
+     * Clear internal status cache
+     */
+    protected function clear_status_cache($mailbox)
+    {
+        unset($this->data['STATUS:' . $mailbox]);
+        unset($this->data['EXISTS']);
+        unset($this->data['RECENT']);
+        unset($this->data['UNSEEN']);
+    }
+
+    /**
      * Converts flags array into string for inclusion in IMAP command
      *
      * @param array $flags Flags (see self::flags)
@@ -3810,10 +3867,6 @@
 
         if (!isset($this->prefs['literal+']) && in_array('LITERAL+', $this->capability)) {
             $this->prefs['literal+'] = true;
-        }
-
-        if (preg_match('/(\[| )MUPDATE=.*/', $str)) {
-            self::$mupdate = true;
         }
 
         if ($trusted) {

--
Gitblit v1.9.1