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.php | 479 +++++++++++++++++++++++++++++++++++++++++------------------ 1 files changed, 331 insertions(+), 148 deletions(-) diff --git a/program/include/rcube_imap.php b/program/include/rcube_imap.php index 212e886..f5a9368 100644 --- a/program/include/rcube_imap.php +++ b/program/include/rcube_imap.php @@ -5,7 +5,7 @@ | program/include/rcube_imap.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: | @@ -100,6 +100,16 @@ 'RETURN-PATH', ); + const UNKNOWN = 0; + const NOPERM = 1; + const READONLY = 2; + const TRYCREATE = 3; + const INUSE = 4; + const OVERQUOTA = 5; + const ALREADYEXISTS = 6; + const NONEXISTENT = 7; + const CONTACTADMIN = 8; + /** * Object constructor @@ -138,6 +148,18 @@ $this->options['port'] = $port; + if ($this->options['debug']) { + $this->conn->setDebug(true, array($this, 'debug_handler')); + + $this->options['ident'] = array( + 'name' => 'Roundcube Webmail', + 'version' => RCMAIL_VERSION, + 'php' => PHP_VERSION, + 'os' => PHP_OS, + 'command' => $_SERVER['REQUEST_URI'], + ); + } + $attempt = 0; do { $data = rcmail::get_instance()->plugins->exec_hook('imap_connect', @@ -156,21 +178,20 @@ $this->ssl = $use_ssl; if ($this->conn->connected()) { - // print trace messages - if ($this->conn->message && ($this->debug_level & 8)) { - console($this->conn->message); - } // get namespace and delimiter $this->set_env(); - return true; } // write error log else if ($this->conn->error) { - if ($pass && $user) + if ($pass && $user) { + $message = sprintf("Login failed for %s from %s. %s", + $user, rcmail_remote_ip(), $this->conn->error); + raise_error(array('code' => 403, 'type' => 'imap', 'file' => __FILE__, 'line' => __LINE__, - 'message' => $this->conn->error), true, false); + 'message' => $message), true, false); + } } return false; @@ -185,7 +206,7 @@ */ function close() { - $this->conn->close(); + $this->conn->closeConnection(); $this->write_cache(); } @@ -198,11 +219,11 @@ */ function reconnect() { - $this->close(); - $this->connect($this->host, $this->user, $this->pass, $this->port, $this->ssl); + $this->conn->closeConnection(); + $connected = $this->connect($this->host, $this->user, $this->pass, $this->port, $this->ssl); // issue SELECT command to restore connection status - if ($this->mailbox) + if ($connected && strlen($this->mailbox)) $this->conn->select($this->mailbox); } @@ -214,7 +235,7 @@ */ function get_error_code() { - return ($this->conn) ? $this->conn->errornum : 0; + return $this->conn->errornum; } @@ -225,7 +246,48 @@ */ function get_error_str() { - return ($this->conn) ? $this->conn->error : ''; + return $this->conn->error; + } + + + /** + * Returns code of last command response + * + * @return int Response code + */ + function get_response_code() + { + switch ($this->conn->resultcode) { + case 'NOPERM': + return self::NOPERM; + case 'READ-ONLY': + return self::READONLY; + case 'TRYCREATE': + return self::TRYCREATE; + case 'INUSE': + return self::INUSE; + case 'OVERQUOTA': + return self::OVERQUOTA; + case 'ALREADYEXISTS': + return self::ALREADYEXISTS; + case 'NONEXISTENT': + return self::NONEXISTENT; + case 'CONTACTADMIN': + return self::CONTACTADMIN; + default: + return self::UNKNOWN; + } + } + + + /** + * Returns last command response + * + * @return string Response + */ + function get_response_str() + { + return $this->conn->result; } @@ -300,9 +362,9 @@ * @param string $mailbox Mailbox/Folder name * @access public */ - function select_mailbox($mailbox) + function select_mailbox($mailbox=null) { - $mailbox = $this->mod_mailbox($mailbox); + $mailbox = strlen($mailbox) ? $this->mod_mailbox($mailbox) : $this->mailbox; $selected = $this->conn->select($mailbox); @@ -417,12 +479,12 @@ { $this->threading = false; - if ($enable) { - if ($this->get_capability('THREAD=REFS')) + if ($enable && ($caps = $this->get_capability('THREAD'))) { + if (in_array('REFS', $caps)) $this->threading = 'REFS'; - else if ($this->get_capability('THREAD=REFERENCES')) + else if (in_array('REFERENCES', $caps)) $this->threading = 'REFERENCES'; - else if ($this->get_capability('THREAD=ORDEREDSUBJECT')) + else if (in_array('ORDEREDSUBJECT', $caps)) $this->threading = 'ORDEREDSUBJECT'; } @@ -493,56 +555,56 @@ $imap_shared = $config->get('imap_ns_shared'); $imap_delimiter = $config->get('imap_delimiter'); - if ($imap_delimiter) { - $this->delimiter = $imap_delimiter; - } - - if (!$this->conn) + if (!$this->conn->connected()) return; $ns = $this->conn->getNamespace(); - // NAMESPACE supported + // Set namespaces (NAMESPACE supported) if (is_array($ns)) { $this->namespace = $ns; - - if (empty($this->delimiter)) - $this->delimiter = $ns['personal'][0][1]; - if (empty($this->delimiter)) - $this->delimiter = $this->conn->getHierarchyDelimiter(); - if (empty($this->delimiter)) - $this->delimiter = '/'; } - // not supported, get namespace from config - else if ($imap_personal !== null || $imap_shared !== null || $imap_other !== null) { - if (empty($this->delimiter)) - $this->delimiter = $this->conn->getHierarchyDelimiter(); - if (empty($this->delimiter)) - $this->delimiter = '/'; - + else { $this->namespace = array( 'personal' => NULL, 'other' => NULL, 'shared' => NULL, ); + } - if ($imap_personal !== null) { - foreach ((array)$imap_personal as $dir) { - $this->namespace['personal'][] = array($dir, $this->delimiter); + if ($imap_delimiter) { + $this->delimiter = $imap_delimiter; + } + if (empty($this->delimiter)) { + $this->delimiter = $this->namespace['personal'][0][1]; + } + if (empty($this->delimiter)) { + $this->delimiter = $this->conn->getHierarchyDelimiter(); + } + if (empty($this->delimiter)) { + $this->delimiter = '/'; + } + + // Overwrite namespaces + if ($imap_personal !== null) { + $this->namespace['personal'] = NULL; + foreach ((array)$imap_personal as $dir) { + $this->namespace['personal'][] = array($dir, $this->delimiter); + } + } + if ($imap_other !== null) { + $this->namespace['other'] = NULL; + foreach ((array)$imap_other as $dir) { + if ($dir) { + $this->namespace['other'][] = array($dir, $this->delimiter); } } - if ($imap_other !== null) { - foreach ((array)$imap_other as $dir) { - if ($dir) { - $this->namespace['other'][] = array($dir, $this->delimiter); - } - } - } - if ($imap_shared !== null) { - foreach ((array)$imap_shared as $dir) { - if ($dir) { - $this->namespace['shared'][] = array($dir, $this->delimiter); - } + } + if ($imap_shared !== null) { + $this->namespace['shared'] = NULL; + foreach ((array)$imap_shared as $dir) { + if ($dir) { + $this->namespace['shared'][] = array($dir, $this->delimiter); } } } @@ -617,7 +679,7 @@ } // RECENT count is fetched a bit different else if ($mode == 'RECENT') { - $count = $this->conn->checkForRecent($mailbox); + $count = $this->conn->countRecent($mailbox); } // use SEARCH for message counting else if ($this->skip_deleted) { @@ -2000,7 +2062,7 @@ return false; } - $struct = &$this->_structure_part($structure); + $struct = &$this->_structure_part($structure, 0, '', $headers); $struct->headers = get_object_vars($headers); // don't trust given content-type @@ -2132,6 +2194,11 @@ $struct->charset = $struct->ctype_parameters['charset']; } + // #1487700: workaround for lack of charset in malformed structure + if (empty($struct->charset) && !empty($mime_headers) && $mime_headers->charset) { + $struct->charset = $mime_headers->charset; + } + // read content encoding if (!empty($part[5]) && $part[5]!='NIL') { $struct->encoding = strtolower($part[5]); @@ -2180,7 +2247,11 @@ $mime_headers = $this->conn->fetchPartHeader( $this->mailbox, $this->_msg_id, false, $struct->mime_id); } - $struct->headers = $this->_parse_headers($mime_headers) + $struct->headers; + + if (is_string($mime_headers)) + $struct->headers = $this->_parse_headers($mime_headers) + $struct->headers; + else if (is_object($mime_headers)) + $struct->headers = get_object_vars($mime_headers) + $struct->headers; // get real content-type of message/rfc822 if ($struct->mimetype == 'message/rfc822') { @@ -2376,20 +2447,21 @@ // TODO: Add caching for message parts - if (!$part) $part = 'TEXT'; + if (!$part) { + $part = 'TEXT'; + } $body = $this->conn->handlePartBody($this->mailbox, $uid, true, $part, $o_part->encoding, $print, $fp); - if ($fp || $print) + if ($fp || $print) { return true; + } - // convert charset (if text or message part) - if ($body && ($o_part->ctype_primary == 'text' || $o_part->ctype_primary == 'message')) { - // assume default if no charset specified - if (empty($o_part->charset) || strtolower($o_part->charset) == 'us-ascii') - $o_part->charset = $this->default_charset; - + // convert charset (if text or message part) and part's charset is specified + if ($body && $o_part->charset + && preg_match('/^(text|message)$/', $o_part->ctype_primary) + ) { $body = rcube_charset_convert($body, $o_part->charset); } @@ -2552,6 +2624,9 @@ $tbox = $to_mbox; $to_mbox = $this->mod_mailbox($to_mbox); $from_mbox = strlen($from_mbox) ? $this->mod_mailbox($from_mbox) : $this->mailbox; + + if ($to_mbox === $from_mbox) + return false; list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox); @@ -2771,7 +2846,23 @@ else $a_uids = NULL; - $result = $this->conn->expunge($mailbox, $a_uids); + // force mailbox selection and check if mailbox is writeable + // to prevent a situation when CLOSE is executed on closed + // or EXPUNGE on read-only mailbox + $result = $this->conn->select($mailbox); + if (!$result) { + return false; + } + if (!$this->conn->data['READ-WRITE']) { + $this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Mailbox is read-only"); + return false; + } + + // CLOSE(+SELECT) should be faster than EXPUNGE + if (empty($a_uids) || $a_uids == '1:*') + $result = $this->conn->close(); + else + $result = $this->conn->expunge($mailbox, $a_uids); if ($result && $clear_cache) { $this->clear_message_cache($mailbox.'.msg'); @@ -3009,6 +3100,26 @@ /** + * Get mailbox size (size of all messages in a mailbox) + * + * @param string $name Mailbox name + * @return int Mailbox size in bytes, False on error + */ + function get_mailbox_size($name) + { + $name = $this->mod_mailbox($name); + + // @TODO: could we try to use QUOTA here? + $result = $this->conn->fetchHeaderIndex($name, '1:*', 'SIZE', false); + + if (is_array($result)) + $result = array_sum($result); + + return $result; + } + + + /** * Subscribe to a specific mailbox(es) * * @param array $a_mboxes Mailbox name(s) @@ -3043,130 +3154,120 @@ /** * Create a new mailbox on the server and register it in local cache * - * @param string $name New mailbox name (as utf-7 string) + * @param string $name New mailbox name * @param boolean $subscribe True if the new mailbox should be subscribed - * @param string Name of the created mailbox, false on error + * @param boolean True on success */ function create_mailbox($name, $subscribe=false) { - $result = false; - - // reduce mailbox name to 100 chars - $name = substr($name, 0, 100); + $result = false; $abs_name = $this->mod_mailbox($name); - $result = $this->conn->createFolder($abs_name); + $result = $this->conn->createFolder($abs_name); // try to subscribe it if ($result && $subscribe) $this->subscribe($name); - return $result ? $name : false; + return $result; } /** * Set a new name to an existing mailbox * - * @param string $mbox_name Mailbox to rename (as utf-7 string) - * @param string $new_name New mailbox name (as utf-7 string) - * @return string Name of the renames mailbox, False on error + * @param string $mbox_name Mailbox to rename + * @param string $new_name New mailbox name + * + * @return boolean True on success */ function rename_mailbox($mbox_name, $new_name) { $result = false; - // encode mailbox name and reduce it to 100 chars - $name = substr($new_name, 0, 100); - // make absolute path - $mailbox = $this->mod_mailbox($mbox_name); - $abs_name = $this->mod_mailbox($name); + $mailbox = $this->mod_mailbox($mbox_name); + $abs_name = $this->mod_mailbox($new_name); + $delm = $this->get_hierarchy_delimiter(); - // check if mailbox is subscribed - $a_subscribed = $this->_list_mailboxes(); - $subscribed = in_array($mailbox, $a_subscribed); - - // unsubscribe folder - if ($subscribed) - $this->conn->unsubscribe($mailbox); + // get list of subscribed folders + if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false)) { + $a_subscribed = $this->_list_mailboxes('', $mbox_name . $delm . '*'); + $subscribed = $this->mailbox_exists($mbox_name, true); + } + else { + $a_subscribed = $this->_list_mailboxes(); + $subscribed = in_array($mailbox, $a_subscribed); + } if (strlen($abs_name)) $result = $this->conn->renameFolder($mailbox, $abs_name); if ($result) { - $delm = $this->get_hierarchy_delimiter(); + // unsubscribe the old folder, subscribe the new one + if ($subscribed) { + $this->conn->unsubscribe($mailbox); + $this->conn->subscribe($abs_name); + } // check if mailbox children are subscribed - foreach ($a_subscribed as $c_subscribed) + foreach ($a_subscribed as $c_subscribed) { if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_subscribed)) { $this->conn->unsubscribe($c_subscribed); $this->conn->subscribe(preg_replace('/^'.preg_quote($mailbox, '/').'/', $abs_name, $c_subscribed)); } + } // clear cache $this->clear_message_cache($mailbox.'.msg'); $this->clear_cache('mailboxes'); } - // try to subscribe it - if ($result && $subscribed) - $this->conn->subscribe($abs_name); - - return $result ? $name : false; + return $result; } /** - * Remove mailboxes from server + * Remove mailbox from server * - * @param string|array $mbox_name sMailbox name(s) string/array + * @param string $mbox_name Mailbox name + * * @return boolean True on success */ function delete_mailbox($mbox_name) { - $deleted = false; + $result = false; + $mailbox = $this->mod_mailbox($mbox_name); + $delm = $this->get_hierarchy_delimiter(); - if (is_array($mbox_name)) - $a_mboxes = $mbox_name; - else if (is_string($mbox_name) && strlen($mbox_name)) - $a_mboxes = explode(',', $mbox_name); + // get list of folders + if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false)) + $sub_mboxes = $this->list_unsubscribed('', $mailbox . $delm . '*'); + else + $sub_mboxes = $this->list_unsubscribed(); - if (is_array($a_mboxes)) { - $delimiter = $this->get_hierarchy_delimiter(); - - foreach ($a_mboxes as $mbox_name) { - $mailbox = $this->mod_mailbox($mbox_name); - $sub_mboxes = $this->conn->listMailboxes('', $mbox_name . $delimiter . '*'); + // send delete command to server + $result = $this->conn->deleteFolder($mailbox); - // unsubscribe mailbox before deleting - $this->conn->unsubscribe($mailbox); + if ($result) { + // unsubscribe mailbox + $this->conn->unsubscribe($mailbox); - // send delete command to server - $result = $this->conn->deleteFolder($mailbox); - if ($result) { - $deleted = true; - $this->clear_message_cache($mailbox.'.msg'); - } - - foreach ($sub_mboxes as $c_mbox) { - if ($c_mbox != 'INBOX') { - $this->conn->unsubscribe($c_mbox); - $result = $this->conn->deleteFolder($c_mbox); - if ($result) { - $deleted = true; - $this->clear_message_cache($c_mbox.'.msg'); - } + foreach ($sub_mboxes as $c_mbox) { + if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_mbox)) { + $this->conn->unsubscribe($c_mbox); + if ($this->conn->deleteFolder($c_mbox)) { + $this->clear_message_cache($c_mbox.'.msg'); } } } + + // clear mailbox-related cache + $this->clear_message_cache($mailbox.'.msg'); + $this->clear_cache('mailboxes'); } - // clear mailboxlist cache - if ($deleted) - $this->clear_cache('mailboxes'); - - return $deleted; + return $result; } @@ -3208,7 +3309,7 @@ } else { $a_folders = $this->conn->listMailboxes('', $mbox); - } + } if (is_array($a_folders) && in_array($mbox, $a_folders)) { $this->icache[$key][] = $mbox; @@ -3235,32 +3336,26 @@ // If folder contains namespace prefix, don't modify it if (is_array($this->namespace['shared'])) { foreach ($this->namespace['shared'] as $ns) { - foreach ((array)$ns as $root) { - if ($root[0] && strpos($mbox_name, $root[0]) === 0) { - return $mbox_name; - } + if ($ns[0] && strpos($mbox_name, $ns[0]) === 0) { + return $mbox_name; } } } if (is_array($this->namespace['other'])) { foreach ($this->namespace['other'] as $ns) { - foreach ((array)$ns as $root) { - if ($root[0] && strpos($mbox_name, $root[0]) === 0) { - return $mbox_name; - } + if ($ns[0] && strpos($mbox_name, $ns[0]) === 0) { + return $mbox_name; } } } if (is_array($this->namespace['personal'])) { foreach ($this->namespace['personal'] as $ns) { - foreach ((array)$ns as $root) { - if ($root[0] && strpos($mbox_name, $root[0]) === 0) { - return $mbox_name; - } + if ($ns[0] && strpos($mbox_name, $ns[0]) === 0) { + return $mbox_name; } } // Add prefix if first personal namespace is non-empty - if ($this->namespace['personal'][0][0]) { + if ($mbox_name != 'INBOX' && $this->namespace['personal'][0][0]) { return $this->namespace['personal'][0][0].$mbox_name; } } @@ -4614,10 +4709,13 @@ private function _parse_address_list($str, $decode=true) { // remove any newlines and carriage returns before - $a = rcube_explode_quoted_string('[,;]', preg_replace( "/[\r\n]/", " ", $str)); + $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str); + + // extract list items, remove comments + $str = self::explode_header_string(',;', $str, true); $result = array(); - foreach ($a as $key => $val) { + foreach ($str as $key => $val) { $name = ''; $address = ''; $val = trim($val); @@ -4657,6 +4755,91 @@ return $result; } + + /** + * Explodes header (e.g. address-list) string into array of strings + * using specified separator characters with proper handling + * of quoted-strings and comments (RFC2822) + * + * @param string $separator String containing separator characters + * @param string $str Header string + * @param bool $remove_comments Enable to remove comments + * + * @return array Header items + */ + static function explode_header_string($separator, $str, $remove_comments=false) + { + $length = strlen($str); + $result = array(); + $quoted = false; + $comment = 0; + $out = ''; + + for ($i=0; $i<$length; $i++) { + // we're inside a quoted string + if ($quoted) { + if ($str[$i] == '"') { + $quoted = false; + } + else if ($str[$i] == '\\') { + if ($comment <= 0) { + $out .= '\\'; + } + $i++; + } + } + // we're inside a comment string + else if ($comment > 0) { + if ($str[$i] == ')') { + $comment--; + } + else if ($str[$i] == '(') { + $comment++; + } + else if ($str[$i] == '\\') { + $i++; + } + continue; + } + // separator, add to result array + else if (strpos($separator, $str[$i]) !== false) { + if ($out) { + $result[] = $out; + } + $out = ''; + continue; + } + // start of quoted string + else if ($str[$i] == '"') { + $quoted = true; + } + // start of comment + else if ($remove_comments && $str[$i] == '(') { + $comment++; + } + + if ($comment <= 0) { + $out .= $str[$i]; + } + } + + if ($out && $comment <= 0) { + $result[] = $out; + } + + return $result; + } + + + /** + * This is our own debug handler for the IMAP connection + * @access public + */ + public function debug_handler(&$imap, $message) + { + write_log('imap', $message); + } + } // end class rcube_imap -- Gitblit v1.9.1