From ab0b51a1fef87bcc643c3aaf2e635c811b28ccd8 Mon Sep 17 00:00:00 2001 From: alecpl <alec@alec.pl> Date: Tue, 15 Feb 2011 06:10:59 -0500 Subject: [PATCH] - Use only one from IMAP authentication methods to prevent login delays (1487784) --- program/include/rcube_imap_generic.php | 310 ++++++++++++++++++++++++++++++++++++--------------- 1 files changed, 218 insertions(+), 92 deletions(-) diff --git a/program/include/rcube_imap_generic.php b/program/include/rcube_imap_generic.php index 512e7e4..cc590e0 100644 --- a/program/include/rcube_imap_generic.php +++ b/program/include/rcube_imap_generic.php @@ -5,7 +5,7 @@ | program/include/rcube_imap_generic.php | | | | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2010, Roundcube Dev. - Switzerland | + | Copyright (C) 2005-2010, The Roundcube Dev Team | | Licensed under the GNU GPL | | | | PURPOSE: | @@ -109,6 +109,8 @@ private $prefs; private $cmd_tag; private $cmd_num = 0; + private $_debug = false; + private $_debug_handler = false; const ERROR_OK = 0; const ERROR_NO = -1; @@ -142,8 +144,8 @@ if (!$this->fp) return false; - if (!empty($this->prefs['debug_mode'])) { - write_log('imap', 'C: '. rtrim($string)); + if ($this->_debug) { + $this->debug('C: '. rtrim($string)); } $res = fwrite($this->fp, $string . ($endln ? "\r\n" : '')); @@ -231,8 +233,8 @@ $this->fp = null; break; } - if (!empty($this->prefs['debug_mode'])) { - write_log('imap', 'S: '. rtrim($buffer)); + if ($this->_debug) { + $this->debug('S: '. rtrim($buffer)); } $line .= $buffer; } while ($buffer[strlen($buffer)-1] != "\n"); @@ -268,8 +270,8 @@ while ($len < $bytes && !feof($this->fp)) { $d = fread($this->fp, $bytes-$len); - if (!empty($this->prefs['debug_mode'])) { - write_log('imap', 'S: '. $d); + if ($this->_debug) { + $this->debug('S: '. $d); } $data .= $d; $data_len = strlen($data); @@ -369,10 +371,43 @@ return false; } - function getCapability($name) + private function hasCapability($name) { + if (empty($this->capability) || $name == '') { + return false; + } + if (in_array($name, $this->capability)) { return true; + } + else if (strpos($name, '=')) { + return false; + } + + $result = array(); + foreach ($this->capability as $cap) { + $entry = explode('=', $cap); + if ($entry[0] == $name) { + $result[] = $entry[1]; + } + } + + return !empty($result) ? $result : false; + } + + /** + * Capabilities checker + * + * @param string $name Capability name + * + * @return mixed Capability values array for key=value pairs, true/false for others + */ + function getCapability($name) + { + $result = $this->hasCapability($name); + + if (!empty($result)) { + return $result; } else if ($this->capability_readed) { return false; @@ -388,11 +423,7 @@ $this->capability_readed = true; - if (in_array($name, $this->capability)) { - return true; - } - - return false; + return $this->hasCapability($name); } function clearCapability() @@ -686,8 +717,8 @@ $line = trim(fgets($this->fp, 8192)); - if ($this->prefs['debug_mode'] && $line) { - write_log('imap', 'S: '. $line); + if ($this->_debug && $line) { + $this->debug('S: '. $line); } // Connected to wrong port or connection error? @@ -728,23 +759,35 @@ } } + // Send ID info + if (!empty($this->prefs['ident']) && $this->getCapability('ID')) { + $this->id($this->prefs['ident']); + } + $auth_methods = array(); $result = null; // check for supported auth methods if ($auth_method == 'CHECK') { - if ($this->getCapability('AUTH=DIGEST-MD5')) { - $auth_methods[] = 'DIGEST-MD5'; - } - if ($this->getCapability('AUTH=CRAM-MD5') || $this->getCapability('AUTH=CRAM_MD5')) { - $auth_methods[] = 'CRAM-MD5'; - } - if ($this->getCapability('AUTH=PLAIN')) { - $auth_methods[] = 'PLAIN'; + if ($auth_caps = $this->getCapability('AUTH')) { + $auth_methods = $auth_caps; } // RFC 2595 (LOGINDISABLED) LOGIN disabled when connection is not secure - if (!$this->getCapability('LOGINDISABLED')) { + $login_disabled = $this->getCapability('LOGINDISABLED'); + if (($key = array_search('LOGIN', $auth_methods)) !== false) { + if ($login_disabled) { + unset($auth_methods[$key]); + } + } + 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 { @@ -755,30 +798,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) { - case 'DIGEST-MD5': + switch ($auth_method) { + case '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 @@ -1124,6 +1165,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); @@ -1461,7 +1540,7 @@ // INTERNALDATE "16-Nov-2008 21:08:46 +0100" BODYSTRUCTURE (...) // BODY[HEADER.FIELDS ... - if (preg_match('/^\* [0-9]+ FETCH \((.*) BODY/s', $line, $matches)) { + if (preg_match('/^\* [0-9]+ FETCH \((.*) BODY/sU', $line, $matches)) { $str = $matches[1]; // swap parents with quotes, then explode @@ -1498,7 +1577,7 @@ // BODYSTRUCTURE if ($bodystr) { - while (!preg_match('/ BODYSTRUCTURE (.*) BODY\[HEADER.FIELDS/s', $line, $m)) { + while (!preg_match('/ BODYSTRUCTURE (.*) BODY\[HEADER.FIELDS/sU', $line, $m)) { $line2 = $this->readLine(1024); $line .= $this->multLine($line2, true); } @@ -1598,7 +1677,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]; } @@ -2604,13 +2683,13 @@ */ function getACL($mailbox) { - list($code, $response) = $this->execute('GETACL', $this->escape($mailbox)); + list($code, $response) = $this->execute('GETACL', array($this->escape($mailbox))); if ($code == self::ERROR_OK && preg_match('/^\* ACL /i', $response)) { // Parse server response (remove "* ACL ") $response = substr($response, 6); $ret = $this->tokenizeResponse($response); - $mbox = array_unshift($ret); + $mbox = array_shift($ret); $size = count($ret); // Create user-rights hash array @@ -2677,7 +2756,7 @@ */ function myRights($mailbox) { - list($code, $response) = $this->execute('MYRIGHTS', array($this->escape(mailbox))); + list($code, $response) = $this->execute('MYRIGHTS', array($this->escape($mailbox))); if ($code == self::ERROR_OK && preg_match('/^\* MYRIGHTS /i', $response)) { // Parse server response (remove "* MYRIGHTS ") @@ -2804,37 +2883,46 @@ list($code, $response) = $this->execute('GETMETADATA', array( $this->escape($mailbox), $optlist)); - if ($code == self::ERROR_OK && preg_match('/^\* METADATA /i', $response)) { - // Parse server response (remove "* METADATA ") - $response = substr($response, 11); - $ret_mbox = $this->tokenizeResponse($response, 1); - $data = $this->tokenizeResponse($response); + if ($code == self::ERROR_OK) { + $result = array(); + $data = $this->tokenizeResponse($response); // The METADATA response can contain multiple entries in a single // response or multiple responses for each entry or group of entries if (!empty($data) && ($size = count($data))) { for ($i=0; $i<$size; $i++) { - if (is_array($data[$i])) { + if (isset($mbox) && is_array($data[$i])) { $size_sub = count($data[$i]); for ($x=0; $x<$size_sub; $x++) { - $data[$data[$i][$x]] = $data[$i][++$x]; + $result[$mbox][$data[$i][$x]] = $data[$i][++$x]; } unset($data[$i]); } - else if ($data[$i] == '*' && $data[$i+1] == 'METADATA') { - unset($data[$i]); // "*" - unset($data[++$i]); // "METADATA" - unset($data[++$i]); // Mailbox + else if ($data[$i] == '*') { + if ($data[$i+1] == 'METADATA') { + $mbox = $data[$i+2]; + unset($data[$i]); // "*" + unset($data[++$i]); // "METADATA" + unset($data[++$i]); // Mailbox + } + // get rid of other untagged responses + else { + unset($mbox); + unset($data[$i]); + } } - else { - $data[$data[$i]] = $data[++$i]; + else if (isset($mbox)) { + $result[$mbox][$data[$i]] = $data[++$i]; unset($data[$i]); unset($data[$i-1]); + } + else { + unset($data[$i]); } } } - return $data; + return $result; } return NULL; @@ -2870,8 +2958,9 @@ $value = sprintf("{%d}\r\n%s", strlen($value), $value); } + // ANNOTATEMORE drafts before version 08 require quoted parameters $entries[] = sprintf('%s (%s %s)', - $this->escape($name), $this->escape($attr), $value); + $this->escape($name, true), $this->escape($attr, true), $value); } $entries = implode(' ', $entries); @@ -2921,8 +3010,9 @@ $entries = array($entries); } // create entries string + // ANNOTATEMORE drafts before version 08 require quoted parameters foreach ($entries as $idx => $name) { - $entries[$idx] = $this->escape($name); + $entries[$idx] = $this->escape($name, true); } $entries = '(' . implode(' ', $entries) . ')'; @@ -2931,50 +3021,65 @@ } // create entries string foreach ($attribs as $idx => $name) { - $attribs[$idx] = $this->escape($name); + $attribs[$idx] = $this->escape($name, true); } $attribs = '(' . implode(' ', $attribs) . ')'; list($code, $response) = $this->execute('GETANNOTATION', array( $this->escape($mailbox), $entries, $attribs)); - if ($code == self::ERROR_OK && preg_match('/^\* ANNOTATION /i', $response)) { - // Parse server response (remove "* ANNOTATION ") - $response = substr($response, 13); - $ret_mbox = $this->tokenizeResponse($response, 1); - $data = $this->tokenizeResponse($response); - $res = array(); + if ($code == self::ERROR_OK) { + $result = array(); + $data = $this->tokenizeResponse($response); // Here we returns only data compatible with METADATA result format if (!empty($data) && ($size = count($data))) { for ($i=0; $i<$size; $i++) { - $entry = $data[$i++]; - if (is_array($entry)) { + $entry = $data[$i]; + if (isset($mbox) && is_array($entry)) { $attribs = $entry; $entry = $last_entry; } - else - $attribs = $data[$i++]; + else if ($entry == '*') { + if ($data[$i+1] == 'ANNOTATION') { + $mbox = $data[$i+2]; + unset($data[$i]); // "*" + unset($data[++$i]); // "ANNOTATION" + unset($data[++$i]); // Mailbox + } + // get rid of other untagged responses + else { + unset($mbox); + unset($data[$i]); + } + continue; + } + else if (isset($mbox)) { + $attribs = $data[++$i]; + } + else { + unset($data[$i]); + continue; + } if (!empty($attribs)) { for ($x=0, $len=count($attribs); $x<$len;) { $attr = $attribs[$x++]; $value = $attribs[$x++]; if ($attr == 'value.priv') { - $res['/private' . $entry] = $value; + $result[$mbox]['/private' . $entry] = $value; } else if ($attr == 'value.shared') { - $res['/shared' . $entry] = $value; + $result[$mbox]['/shared' . $entry] = $value; } } } $last_entry = $entry; - unset($data[$i-1]); - unset($data[$i-2]); + unset($data[$i]); } } - return $res; + return $result; } return NULL; @@ -3164,21 +3269,7 @@ */ private function strToTime($date) { - // support non-standard "GMTXXXX" literal - $date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date); - // if date parsing fails, we have a date in non-rfc format. - // remove token from the end and try again - while ((($ts = @strtotime($date))===false) || ($ts < 0)) { - $d = explode(' ', $date); - array_pop($d); - if (!$d) { - break; - } - $date = implode(' ', $d); - } - - $ts = (int) $ts; - + $ts = (int) rcube_strtotime($date); return $ts < 0 ? 0 : $ts; } @@ -3211,12 +3302,13 @@ /** * Escapes a string when it contains special characters (RFC3501) * - * @param string $string IMAP string + * @param string $string IMAP string + * @param boolean $force_quotes Forces string quoting * * @return string Escaped string * @todo String literals, lists */ - static function escape($string) + static function escape($string, $force_quotes=false) { if ($string === null) { return 'NIL'; @@ -3224,8 +3316,11 @@ else if ($string === '') { return '""'; } - else if (preg_match('/([\x00-\x20\x28-\x29\x7B\x25\x2A\x22\x5C\x5D\x7F]+)/', $string)) { - // string: special chars: SP, CTL, (, ), {, %, *, ", \, ] + // 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\x5B\x5C\x5D\x7F]+)/', $string) + ) { return '"' . strtr($string, array('"'=>'\\"', '\\' => '\\\\')) . '"'; } @@ -3238,4 +3333,35 @@ return strtr($string, array('\\"'=>'"', '\\\\' => '\\')); } + /** + * Set the value of the debugging flag. + * + * @param boolean $debug New value for the debugging flag. + * + * @access public + * @since 0.5-stable + */ + function setDebug($debug, $handler = null) + { + $this->_debug = $debug; + $this->_debug_handler = $handler; + } + + /** + * Write the given debug text to the current debug output handler. + * + * @param string $message Debug mesage text. + * + * @access private + * @since 0.5-stable + */ + private function debug($message) + { + if ($this->_debug_handler) { + call_user_func_array($this->_debug_handler, array(&$this, $message)); + } else { + echo "DEBUG: $message\n"; + } + } + } -- Gitblit v1.9.1