From 8b6eff6e69c2842cefbfe5ca384732c3ccd1305e Mon Sep 17 00:00:00 2001 From: alecpl Date: Mon, 18 Oct 2010 12:55:07 +0000 Subject: [PATCH] - Add ACL extension support into IMAP classes (RFC4314) - Add ANNOTATEMORE extension support into IMAP classes (draft-daboo-imap-annotatemore) - Add METADATA extension support into IMAP classes (RFC5464) --- CHANGELOG | 3 + program/include/rcube_imap.php | 245 ++++++++++ program/include/rcube_imap_generic.php | 609 ++++++++++++++++++++++++- 3 files changed, 856 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 98dbfa2dc..37f82e19d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -32,6 +32,9 @@ CHANGELOG Roundcube Webmail - Display IMAP errors for LIST/THREAD/SEARCH commands (#1486905) - Add LITERAL+ (IMAP4 non-synchronizing literals) support (RFC2088) - Add separate column for message status icon (#1486665) +- Add ACL extension support into IMAP classes (RFC4314) +- Add ANNOTATEMORE extension support into IMAP classes (draft-daboo-imap-annotatemore) +- Add METADATA extension support into IMAP classes (RFC5464) RELEASE 0.4.2 ------------- diff --git a/program/include/rcube_imap.php b/program/include/rcube_imap.php index 9d027c87c..b78639229 100644 --- a/program/include/rcube_imap.php +++ b/program/include/rcube_imap.php @@ -3125,6 +3125,251 @@ class rcube_imap } + /* ----------------------------------------- + * ACL and METADATA/ANNOTATEMORE methods + * ----------------------------------------*/ + + /** + * Changes the ACL on the specified mailbox (SETACL) + * + * @param string $mailbox Mailbox name + * @param string $user User name + * @param string $acl ACL string + * + * @return boolean True on success, False on failure + * + * @access public + * @since 0.5-beta + */ + function set_acl($mailbox, $user, $acl) + { + $mailbox = $this->mod_mailbox($mailbox); + + if ($this->get_capability('ACL')) + return $this->conn->setACL($mailbox, $user, $acl); + + return false; + } + + + /** + * Removes any pair for the + * specified user from the ACL for the specified + * mailbox (DELETEACL) + * + * @param string $mailbox Mailbox name + * @param string $user User name + * + * @return boolean True on success, False on failure + * + * @access public + * @since 0.5-beta + */ + function delete_acl($mailbox, $user) + { + $mailbox = $this->mod_mailbox($mailbox); + + if ($this->get_capability('ACL')) + return $this->conn->deleteACL($mailbox, $user); + + return false; + } + + + /** + * Returns the access control list for mailbox (GETACL) + * + * @param string $mailbox Mailbox name + * + * @return array User-rights array on success, NULL on error + * @access public + * @since 0.5-beta + */ + function get_acl($mailbox) + { + $mailbox = $this->mod_mailbox($mailbox); + + if ($this->get_capability('ACL')) + return $this->conn->getACL($mailbox); + + return NULL; + } + + + /** + * Returns information about what rights can be granted to the + * user (identifier) in the ACL for the mailbox (LISTRIGHTS) + * + * @param string $mailbox Mailbox name + * @param string $user User name + * + * @return array List of user rights + * @access public + * @since 0.5-beta + */ + function list_rights($mailbox, $user) + { + $mailbox = $this->mod_mailbox($mailbox); + + if ($this->get_capability('ACL')) + return $this->conn->listRights($mailbox, $user); + + return NULL; + } + + + /** + * Returns the set of rights that the current user has to + * mailbox (MYRIGHTS) + * + * @param string $mailbox Mailbox name + * + * @return array MYRIGHTS response on success, NULL on error + * @access public + * @since 0.5-beta + */ + function my_rights($mailbox) + { + $mailbox = $this->mod_mailbox($mailbox); + + if ($this->get_capability('ACL')) + return $this->conn->myRights($mailbox); + + return NULL; + } + + + /** + * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION) + * + * @param string $mailbox Mailbox name (empty for server metadata) + * @param array $entries Entry-value array (use NULL value as NIL) + * + * @return boolean True on success, False on failure + * @access public + * @since 0.5-beta + */ + function set_metadata($mailbox, $entries) + { + if ($mailbox) + $mailbox = $this->mod_mailbox($mailbox); + + if ($this->get_capability('METADATA') || + empty($mailbox) && $this->get_capability('METADATA-SERVER') + ) { + return $this->conn->setMetadata($mailbox, $entries); + } + else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) { + foreach ($entries as $entry => $value) { + list($ent, $attr) = $this->md2annotate($entry); + $entries[$entry] = array($ent, $attr, $value); + } + return $this->conn->setAnnotation($mailbox, $entries); + } + + return false; + } + + + /** + * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION) + * + * @param string $mailbox Mailbox name (empty for server metadata) + * @param array $entries Entry names array + * + * @return boolean True on success, False on failure + * + * @access public + * @since 0.5-beta + */ + function delete_metadata($mailbox, $entries) + { + if ($mailbox) + $mailbox = $this->mod_mailbox($mailbox); + + if ($this->get_capability('METADATA') || + empty($mailbox) && $this->get_capability('METADATA-SERVER') + ) { + return $this->conn->deleteMetadata($mailbox, $entries); + } + else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) { + foreach ($entries as $idx => $entry) { + list($ent, $attr) = $this->md2annotate($entry); + $entries[$idx] = array($ent, $attr, NULL); + } + return $this->conn->setAnnotation($mailbox, $entries); + } + + return false; + } + + + /** + * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION) + * + * @param string $mailbox Mailbox name (empty for server metadata) + * @param array $entries Entries + * @param array $options Command options (with MAXSIZE and DEPTH keys) + * + * @return array Metadata entry-value hash array on success, NULL on error + * + * @access public + * @since 0.5-beta + */ + function get_metadata($mailbox, $entries, $options=array()) + { + if ($mailbox) + $mailbox = $this->mod_mailbox($mailbox); + + if ($this->get_capability('METADATA') || + empty($mailbox) && $this->get_capability('METADATA-SERVER') + ) { + return $this->conn->getMetadata($mailbox, $entries, $options); + } + else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) { + $queries = array(); + $res = array(); + + // Convert entry names + foreach ($entries as $entry) { + list($ent, $attr) = $this->md2annotate($entry); + $queries[$attr][] = $ent; + } + + // @TODO: Honor MAXSIZE and DEPTH options + foreach ($queries as $attrib => $entry) + if ($result = $this->conn->getAnnotation($mailbox, $entry, $attrib)) + $res = array_merge($res, $result); + + return $res; + } + + return NULL; + } + + + /** + * Converts the METADATA extension entry name into the correct + * entry-attrib names for older ANNOTATEMORE version. + * + * @param string Entry name + * + * @return array Entry-attribute list, NULL if not supported (?) + */ + private function md2annotate($name) + { + if (substr($entry, 0, 7) == '/shared') { + return array(substr($entry, 7), 'value.shared'); + } + else if (substr($entry, 0, 8) == '/private') { + return array(substr($entry, 8), 'value.priv'); + } + + // @TODO: log error + return NULL; + } + + /* -------------------------------- * internal caching methods * --------------------------------*/ diff --git a/program/include/rcube_imap_generic.php b/program/include/rcube_imap_generic.php index 80f25b928..e952b2038 100644 --- a/program/include/rcube_imap_generic.php +++ b/program/include/rcube_imap_generic.php @@ -2181,6 +2181,607 @@ class rcube_imap_generic return $result; } + /** + * Send the SETACL command (RFC4314) + * + * @param string $mailbox Mailbox name + * @param string $user User name + * @param mixed $acl ACL string or array + * + * @return boolean True on success, False on failure + * + * @access public + * @since 0.5-beta + */ + function setACL($mailbox, $user, $acl) + { + if (is_array($acl)) { + $acl = implode('', $acl); + } + + $key = 'acl1'; + $command = sprintf("%s SETACL \"%s\" \"%s\" %s", + $key, $this->escape($mailbox), $this->escape($user), strtolower($acl)); + + if (!$this->putLine($command)) { + $this->set_error(self::ERROR_COMMAND, "Unable to send command: $command"); + return false; + } + + $line = $this->readReply(); + return ($this->parseResult($line, 'SETACL: ') == self::ERROR_OK); + } + + /** + * Send the DELETEACL command (RFC4314) + * + * @param string $mailbox Mailbox name + * @param string $user User name + * + * @return boolean True on success, False on failure + * + * @access public + * @since 0.5-beta + */ + function deleteACL($mailbox, $user) + { + $key = 'acl2'; + $command = sprintf("%s DELETEACL \"%s\" \"%s\"", + $key, $this->escape($mailbox), $this->escape($user)); + + if (!$this->putLine($command)) { + $this->set_error(self::ERROR_COMMAND, "Unable to send command: $command"); + return false; + } + + $line = $this->readReply(); + return ($this->parseResult($line, 'DELETEACL: ') == self::ERROR_OK); + } + + /** + * Send the GETACL command (RFC4314) + * + * @param string $mailbox Mailbox name + * + * @return array User-rights array on success, NULL on error + * @access public + * @since 0.5-beta + */ + function getACL($mailbox) + { + $key = 'acl3'; + $command = sprintf("%s GETACL \"%s\"", $key, $this->escape($mailbox)); + + if (!$this->putLine($command)) { + $this->set_error(self::ERROR_COMMAND, "Unable to send command: $command"); + return NULL; + } + + $response = ''; + + do { + $line = $this->readLine(4096); + $response .= $line; + } while (!$this->startsWith($line, $key, true, true)); + + if ($this->parseResult($line, 'GETACL: ') == self::ERROR_OK) { + // Parse server response (remove "* ACL " and "\r\nacl3 OK...") + $response = substr($response, 6, -(strlen($line)+2)); + $ret = $this->tokenizeResponse($response); + $mbox = array_unshift($ret); + $size = count($ret); + + // Create user-rights hash array + // @TODO: consider implementing fixACL() method according to RFC4314.2.1.1 + // so we could return only standard rights defined in RFC4314, + // excluding 'c' and 'd' defined in RFC2086. + if ($size % 2 == 0) { + for ($i=0; $i<$size; $i++) { + $ret[$ret[$i]] = str_split($ret[++$i]); + unset($ret[$i-1]); + unset($ret[$i]); + } + return $ret; + } + + $this->set_error(self::ERROR_COMMAND, "Incomplete ACL response"); + return NULL; + } + + return NULL; + } + + /** + * Send the LISTRIGHTS command (RFC4314) + * + * @param string $mailbox Mailbox name + * @param string $user User name + * + * @return array List of user rights + * @access public + * @since 0.5-beta + */ + function listRights($mailbox, $user) + { + $key = 'acl4'; + $command = sprintf("%s LISTRIGHTS \"%s\" \"%s\"", + $key, $this->escape($mailbox), $this->escape($user)); + + if (!$this->putLine($command)) { + $this->set_error(self::ERROR_COMMAND, "Unable to send command: $command"); + return NULL; + } + + $response = ''; + + do { + $line = $this->readLine(4096); + $response .= $line; + } while (!$this->startsWith($line, $key, true, true)); + + if ($this->parseResult($line, 'LISTRIGHTS: ') == self::ERROR_OK) { + // Parse server response (remove "* LISTRIGHTS " and "\r\nacl3 OK...") + $response = substr($response, 13, -(strlen($line)+2)); + + $ret_mbox = $this->tokenizeResponse($response, 1); + $ret_user = $this->tokenizeResponse($response, 1); + $granted = $this->tokenizeResponse($response, 1); + $optional = trim($response); + + return array( + 'granted' => str_split($granted), + 'optional' => explode(' ', $optional), + ); + } + + return NULL; + } + + /** + * Send the MYRIGHTS command (RFC4314) + * + * @param string $mailbox Mailbox name + * + * @return array MYRIGHTS response on success, NULL on error + * @access public + * @since 0.5-beta + */ + function myRights($mailbox) + { + $key = 'acl5'; + $command = sprintf("%s MYRIGHTS \"%s\"", $key, $this->escape(mailbox)); + + if (!$this->putLine($command)) { + $this->set_error(self::ERROR_COMMAND, "Unable to send command: $command"); + return NULL; + } + + $response = ''; + + do { + $line = $this->readLine(1024); + $response .= $line; + } while (!$this->startsWith($line, $key, true, true)); + + if ($this->parseResult($line, 'MYRIGHTS: ') == self::ERROR_OK) { + // Parse server response (remove "* MYRIGHTS " and "\r\nacl5 OK...") + $response = substr($response, 11, -(strlen($line)+2)); + + $ret_mbox = $this->tokenizeResponse($response, 1); + $rights = $this->tokenizeResponse($response, 1); + + return str_split($rights); + } + + return NULL; + } + + /** + * Send the SETMETADATA command (RFC5464) + * + * @param string $mailbox Mailbox name + * @param array $entries Entry-value array (use NULL value as NIL) + * + * @return boolean True on success, False on failure + * @access public + * @since 0.5-beta + */ + function setMetadata($mailbox, $entries) + { + if (!is_array($entries) || empty($entries)) { + $this->set_error(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command"); + return false; + } + + foreach ($entries as $name => $value) { + if ($value === null) + $value = 'NIL'; + else + $value = sprintf("{%d}\r\n%s", strlen($value), $value); + + $entries[$name] = '"' . $this->escape($name) . '" ' . $value; + } + + $entries = implode(' ', $entries); + $key = 'md1'; + $command = sprintf("%s SETMETADATA \"%s\" (%s)", + $key, $this->escape($mailbox), $entries); + + if (!$this->putLineC($command)) { + $this->set_error(self::ERROR_COMMAND, "Unable to send command: $command"); + return false; + } + + $line = $this->readReply(); + return ($this->parseResult($line, 'SETMETADATA: ') == self::ERROR_OK); + } + + /** + * Send the SETMETADATA command with NIL values (RFC5464) + * + * @param string $mailbox Mailbox name + * @param array $entries Entry names array + * + * @return boolean True on success, False on failure + * + * @access public + * @since 0.5-beta + */ + function deleteMetadata($mailbox, $entries) + { + if (!is_array($entries) && !empty($entries)) + $entries = explode(' ', $entries); + + if (empty($entries)) { + $this->set_error(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command"); + return false; + } + + foreach ($entries as $entry) + $data[$entry] = NULL; + + return $this->setMetadata($mailbox, $data); + } + + /** + * Send the GETMETADATA command (RFC5464) + * + * @param string $mailbox Mailbox name + * @param array $entries Entries + * @param array $options Command options (with MAXSIZE and DEPTH keys) + * + * @return array GETMETADATA result on success, NULL on error + * + * @access public + * @since 0.5-beta + */ + function getMetadata($mailbox, $entries, $options=array()) + { + if (!is_array($entries)) { + $entries = array($entries); + } + + // create entries string + foreach ($entries as $idx => $name) { + $entries[$idx] = '"' . $this->escape($name) . '"'; + } + + $optlist = ''; + $entlist = '(' . implode(' ', $entries) . ')'; + + // create options string + if (is_array($options)) { + $options = array_change_key_case($options, CASE_UPPER); + $opts = array(); + + if (!empty($options['MAXSIZE'])) + $opts[] = 'MAXSIZE '.intval($options['MAXSIZE']); + if (!empty($options['DEPTH'])) + $opts[] = 'DEPTH '.intval($options['DEPTH']); + + if ($opts) + $optlist = '(' . implode(' ', $opts) . ')'; + } + + $optlist .= ($optlist ? ' ' : '') . $entlist; + + $key = 'md2'; + $command = sprintf("%s GETMETADATA \"%s\" %s", + $key, $this->escape($mailbox), $optlist); + + if (!$this->putLine($command)) { + $this->set_error(self::ERROR_COMMAND, "Unable to send command: $command"); + return NULL; + } + + $response = ''; + + do { + $line = $this->readLine(4096); + $response .= $line; + } while (!$this->startsWith($line, $key, true, true)); + + if ($this->parseResult($line, 'GETMETADATA: ') == self::ERROR_OK) { + // Parse server response (remove "* METADATA " and "\r\nmd2 OK...") + $response = substr($response, 11, -(strlen($line)+2)); + $ret_mbox = $this->tokenizeResponse($response, 1); + $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])) { + $size_sub = count($data[$i]); + for ($x=0; $x<$size_sub; $x++) { + $data[$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 { + $data[$data[$i]] = $data[++$i]; + unset($data[$i]); + unset($data[$i-1]); + } + } + } + + return $data; + } + + return NULL; + } + + /** + * Send the SETANNOTATION command (draft-daboo-imap-annotatemore) + * + * @param string $mailbox Mailbox name + * @param array $data Data array where each item is an array with + * three elements: entry name, attribute name, value + * + * @return boolean True on success, False on failure + * @access public + * @since 0.5-beta + */ + function setAnnotation($mailbox, $data) + { + if (!is_array($data) || empty($data)) { + $this->set_error(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command"); + return false; + } + + foreach ($data as $entry) { + $name = $entry[0]; + $attr = $entry[1]; + $value = $entry[2]; + + if ($value === null) + $value = 'NIL'; + else + $value = sprintf("{%d}\r\n%s", strlen($value), $value); + + $entries[] = sprintf('"%s" ("%s" %s)', + $this->escape($name), $this->escape($attr), $value); + } + + $entries = implode(' ', $entries); + $key = 'an1'; + $command = sprintf("%s SETANNOTATION \"%s\" %s", + $key, $this->escape($mailbox), $entries); + + if (!$this->putLineC($command)) { + $this->set_error(self::ERROR_COMMAND, "Unable to send command: $command"); + return false; + } + + $line = $this->readReply(); + return ($this->parseResult($line, 'SETANNOTATION: ') == self::ERROR_OK); + } + + /** + * Send the SETANNOTATION command with NIL values (draft-daboo-imap-annotatemore) + * + * @param string $mailbox Mailbox name + * @param array $data Data array where each item is an array with + * two elements: entry name and attribute name + * + * @return boolean True on success, False on failure + * + * @access public + * @since 0.5-beta + */ + function deleteAnnotation($mailbox, $data) + { + if (!is_array($data) || empty($data)) { + $this->set_error(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command"); + return false; + } + + return $this->setAnnotation($mailbox, $data); + } + + /** + * Send the GETANNOTATION command (draft-daboo-imap-annotatemore) + * + * @param string $mailbox Mailbox name + * @param array $entries Entries names + * @param array $attribs Attribs names + * + * @return array Annotations result on success, NULL on error + * + * @access public + * @since 0.5-beta + */ + function getAnnotation($mailbox, $entries, $attribs) + { + if (!is_array($entries)) { + $entries = array($entries); + } + // create entries string + foreach ($entries as $idx => $name) { + $entries[$idx] = '"' . $this->escape($name) . '"'; + } + $entries = '(' . implode(' ', $entries) . ')'; + + if (!is_array($attribs)) { + $attribs = array($attribs); + } + // create entries string + foreach ($attribs as $idx => $name) { + $attribs[$idx] = '"' . $this->escape($name) . '"'; + } + $attribs = '(' . implode(' ', $attribs) . ')'; + + $key = 'an2'; + $command = sprintf("%s GETANNOTATION \"%s\" %s %s", + $key, $this->escape($mailbox), $entries, $attribs); + + if (!$this->putLine($command)) { + $this->set_error(self::ERROR_COMMAND, "Unable to send command: $command"); + return NULL; + } + + $response = ''; + + do { + $line = $this->readLine(4096); + $response .= $line; + } while (!$this->startsWith($line, $key, true, true)); + + if ($this->parseResult($line, 'GETANNOTATION: ') == self::ERROR_OK) { + // Parse server response (remove "* ANNOTATION " and "\r\nan2 OK...") + $response = substr($response, 13, -(strlen($line)+2)); + $ret_mbox = $this->tokenizeResponse($response, 1); + $data = $this->tokenizeResponse($response); + $res = array(); + + // 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)) { + $attribs = $entry; + $entry = $last_entry; + } + else + $attribs = $data[$i++]; + + 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; + else if ($attr == 'value.shared') + $res['/shared' . $entry] = $value; + } + } + $last_entry = $entry; + unset($data[$i-1]); + unset($data[$i-2]); + } + } + + return $res; + } + + return NULL; + } + + /** + * Splits IMAP response into string tokens + * + * @param string &$str The IMAP's server response + * @param int $num Number of tokens to return + * + * @return mixed Tokens array or string if $num=1 + * @access public + * @since 0.5-beta + */ + static function tokenizeResponse(&$str, $num=0) + { + $result = array(); + + while (!$num || count($result) < $num) { + // remove spaces from the beginning of the string + $str = ltrim($str); + + switch ($str[0]) { + + // String literal + case '{': + if (($epos = strpos($str, "}\r\n", 1)) == false) { + // error + } + if (!is_numeric(($bytes = substr($str, 1, $epos - 1)))) { + // error + } + $result[] = substr($str, $epos + 3, $bytes); + // Advance the string + $str = substr($str, $epos + 3 + $bytes); + break; + + // Quoted string + case '"': + $len = strlen($str); + + for ($pos=1; $pos<$len; $pos++) { + if ($str[$pos] == '"') { + break; + } + if ($str[$pos] == "\\") { + if ($str[$pos + 1] == '"' || $str[$pos + 1] == "\\") { + $pos++; + } + } + } + if ($str[$pos] != '"') { + // error + } + // we need to strip slashes for a quoted string + $result[] = stripslashes(substr($str, 1, $pos - 1)); + $str = substr($str, $pos + 1); + break; + + // Parenthesized list + case '(': + $str = substr($str, 1); + $result[] = self::tokenizeResponse($str); + break; + case ')': + $str = substr($str, 1); + return $result; + break; + + // String atom, number, NIL, *, % + default: + // empty or one character + if ($str === '') { + break 2; + } + if (strlen($str) < 2) { + $result[] = $str; + $str = ''; + break; + } + + // excluded chars: SP, CTL, (, ), {, ", ], % + if (preg_match('/^([\x21\x23\x24\x26\x27\x2A-\x5C\x5E-\x7A\x7C-\x7E]+)/', $str, $m)) { + $result[] = $m[1] == 'NIL' ? NULL : $m[1]; + $str = substr($str, strlen($m[1])); + } + break; + } + } + + return $num == 1 ? $result[0] : $result; + } + private function _xor($string, $string2) { $result = ''; @@ -2191,6 +2792,13 @@ class rcube_imap_generic return $result; } + /** + * Converts datetime string into unix timestamp + * + * @param string $date Date string + * + * @return int Unix timestamp + */ private function strToTime($date) { // support non-standard "GMTXXXX" literal @@ -2278,4 +2886,3 @@ class rcube_imap_generic } } -