From 3e398182213cb66057ff40b605296092bb9fe83d Mon Sep 17 00:00:00 2001
From: alecpl <alec@alec.pl>
Date: Wed, 09 Mar 2011 05:30:15 -0500
Subject: [PATCH] - Add code for prevention from IMAP connection hangs when server closes socket unexpectedly

---
 program/include/rcube_imap_generic.php |  138 ++++++++++++++++++++++++++++++++++------------
 1 files changed, 102 insertions(+), 36 deletions(-)

diff --git a/program/include/rcube_imap_generic.php b/program/include/rcube_imap_generic.php
index e8b1fd4..f21b6b0 100644
--- a/program/include/rcube_imap_generic.php
+++ b/program/include/rcube_imap_generic.php
@@ -213,31 +213,26 @@
     {
         $line = '';
 
-        if (!$this->fp) {
-            return NULL;
-        }
-
         if (!$size) {
             $size = 1024;
         }
 
         do {
-            if (feof($this->fp)) {
+            if ($this->eof()) {
                 return $line ? $line : NULL;
             }
 
             $buffer = fgets($this->fp, $size);
 
             if ($buffer === false) {
-                @fclose($this->fp);
-                $this->fp = null;
+                $this->closeSocket();
                 break;
             }
             if ($this->_debug) {
                 $this->debug('S: '. rtrim($buffer));
             }
             $line .= $buffer;
-        } while ($buffer[strlen($buffer)-1] != "\n");
+        } while (substr($buffer, -1) != "\n");
 
         return $line;
     }
@@ -267,7 +262,7 @@
     {
         $data = '';
         $len  = 0;
-        while ($len < $bytes && !feof($this->fp))
+        while ($len < $bytes && !$this->eof())
         {
             $d = fread($this->fp, $bytes-$len);
             if ($this->_debug) {
@@ -312,8 +307,7 @@
             } else if ($res == 'BAD') {
                 $this->errornum = self::ERROR_BAD;
             } else if ($res == 'BYE') {
-                @fclose($this->fp);
-                $this->fp = null;
+                $this->closeSocket();
                 $this->errornum = self::ERROR_BYE;
             }
 
@@ -339,6 +333,32 @@
         return self::ERROR_UNKNOWN;
     }
 
+    private function eof()
+    {
+        if (!is_resource($this->fp)) {
+            return true;
+        }
+
+        // If a connection opened by fsockopen() wasn't closed
+        // by the server, feof() will hang.
+        $start = microtime(true);
+
+        if (feof($this->fp) || 
+            ($this->prefs['timeout'] && (microtime(true) - $start > $this->prefs['timeout']))
+        ) {
+            $this->closeSocket();
+            return true;
+        }
+
+        return false;
+    }
+
+    private function closeSocket()
+    {
+        @fclose($this->fp);
+        $this->fp = null;
+    }
+
     function setError($code, $msg='')
     {
         $this->errornum = $code;
@@ -360,8 +380,7 @@
         }
         if ($error && preg_match('/^\* (BYE|BAD) /i', $string, $m)) {
             if (strtoupper($m[1]) == 'BYE') {
-                @fclose($this->fp);
-                $this->fp = null;
+                $this->closeSocket();
             }
             return true;
         }
@@ -701,11 +720,12 @@
             $host = $this->prefs['ssl_mode'] . '://' . $host;
         }
 
+        if ($this->prefs['timeout'] <= 0) {
+            $this->prefs['timeout'] = ini_get('default_socket_timeout');
+        }
+
         // Connect
-        if ($this->prefs['timeout'] > 0)
-            $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']);
-        else
-            $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr);
+        $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']);
 
         if (!$this->fp) {
             $this->setError(self::ERROR_BAD, sprintf("Could not connect to %s:%d: %s", $host, $this->prefs['port'], $errstr));
@@ -759,6 +779,11 @@
             }
         }
 
+        // Send ID info
+        if (!empty($this->prefs['ident']) && $this->getCapability('ID')) {
+            $this->id($this->prefs['ident']);
+        }
+
         $auth_methods = array();
         $result       = null;
 
@@ -777,6 +802,13 @@
             else if (!$login_disabled) {
                 $auth_methods[] = 'LOGIN';
             }
+
+            // Use best (for security) supported authentication method
+            foreach (array('DIGEST-MD5', 'CRAM-MD5', 'CRAM_MD5', 'PLAIN', 'LOGIN') as $auth_method) {
+                if (in_array($auth_method, $auth_methods)) {
+                    break;
+                }
+            }
         }
         else {
             // Prevent from sending credentials in plain text when connection is not secure
@@ -786,32 +818,28 @@
                 return false;
             }
             // replace AUTH with CRAM-MD5 for backward compat.
-            $auth_methods[] = $auth_method == 'AUTH' ? 'CRAM-MD5' : $auth_method;
+            if ($auth_method == 'AUTH') {
+                $auth_method = 'CRAM-MD5';
+            }
         }
 
         // pre-login capabilities can be not complete
         $this->capability_readed = false;
 
         // Authenticate
-        foreach ($auth_methods as $method) {
-            switch ($method) {
+        switch ($auth_method) {
             case 'CRAM_MD5':
-                $method = 'CRAM-MD5';
+                $auth_method = 'CRAM-MD5';
             case 'CRAM-MD5':
             case 'DIGEST-MD5':
             case 'PLAIN':
-                $result = $this->authenticate($user, $password, $method);
+                $result = $this->authenticate($user, $password, $auth_method);
                 break;
             case 'LOGIN':
                 $result = $this->login($user, $password);
                 break;
             default:
-                $this->setError(self::ERROR_BAD, "Configuration error. Unknown auth method: $method");
-            }
-
-            if (is_resource($result)) {
-                break;
-            }
+                $this->setError(self::ERROR_BAD, "Configuration error. Unknown auth method: $auth_method");
         }
 
         // Connected and authenticated
@@ -840,8 +868,7 @@
             $this->readReply();
         }
 
-        @fclose($this->fp);
-        $this->fp = false;
+        $this->closeSocket();
     }
 
     /**
@@ -1157,6 +1184,44 @@
         return false;
     }
 
+    /**
+     * Executes ID command (RFC2971)
+     *
+     * @param array $items Client identification information key/value hash
+     *
+     * @return array Server identification information key/value hash
+     * @access public
+     * @since 0.6
+     */
+    function id($items=array())
+    {
+        if (is_array($items) && !empty($items)) {
+            foreach ($items as $key => $value) {
+                $args[] = $this->escape($key);
+                $args[] = $this->escape($value);
+            }
+        }
+
+        list($code, $response) = $this->execute('ID', array(
+            !empty($args) ? '(' . implode(' ', (array) $args) . ')' : $this->escape(null)
+        ));
+
+
+        if ($code == self::ERROR_OK && preg_match('/\* ID /i', $response)) {
+            $response = substr($response, 5); // remove prefix "* ID "
+            $items    = $this->tokenizeResponse($response);
+            $result   = null;
+
+            for ($i=0, $len=count($items); $i<$len; $i += 2) {
+                $result[$items[$i]] = $items[$i+1];
+            }
+
+            return $result;
+        }
+
+        return false;
+    }
+
     function sort($mailbox, $field, $add='', $is_uid=FALSE, $encoding = 'US-ASCII')
     {
         $field = strtoupper($field);
@@ -1298,7 +1363,7 @@
                         $result[$id] = '';
                     }
                 } else if ($mode == 2) {
-                    if (preg_match('/\((UID|RFC822\.SIZE) ([0-9]+)/', $line, $matches)) {
+                    if (preg_match('/(UID|RFC822\.SIZE) ([0-9]+)/', $line, $matches)) {
                         $result[$id] = trim($matches[2]);
                     } else {
                         $result[$id] = 0;
@@ -1631,7 +1696,7 @@
                         break;
                         case 'content-type':
                             $ctype_parts = preg_split('/[; ]/', $string);
-                            $result[$id]->ctype = array_shift($ctype_parts);
+                            $result[$id]->ctype = strtolower(array_shift($ctype_parts));
                             if (preg_match('/charset\s*=\s*"?([a-z0-9\-\.\_]+)"?/i', $string, $regs)) {
                                 $result[$id]->charset = $regs[1];
                             }
@@ -3270,11 +3335,12 @@
         else if ($string === '') {
             return '""';
         }
+        // need quoted-string? find special chars: SP, CTL, (, ), {, %, *, ", \, ]
+        // plus [ character as a workaround for DBMail's bug (#1487766)
         else if ($force_quotes ||
-            preg_match('/([\x00-\x20\x28-\x29\x7B\x25\x2A\x22\x5C\x5D\x7F]+)/', $string)
+            preg_match('/([\x00-\x20\x28-\x29\x7B\x25\x2A\x22\x5B\x5C\x5D\x7F]+)/', $string)
         ) {
-            // string: special chars: SP, CTL, (, ), {, %, *, ", \, ]
-            return '"' . strtr($string, array('"'=>'\\"', '\\' => '\\\\')) . '"';
+            return '"' . addcslashes($string, '\\"') . '"';
         }
 
         // atom
@@ -3283,7 +3349,7 @@
 
     static function unEscape($string)
     {
-        return strtr($string, array('\\"'=>'"', '\\\\' => '\\'));
+        return stripslashes($string);
     }
 
     /**

--
Gitblit v1.9.1