Implemented IMAP METADATA (RFC5464) support for both the socket and cclient based...
authorGunnar Wrobel <p@rdus.de>
Mon, 7 Sep 2009 18:52:13 +0000 (20:52 +0200)
committerGunnar Wrobel <p@rdus.de>
Mon, 7 Sep 2009 18:57:28 +0000 (20:57 +0200)
The code should support both the final RFC version of the extension as well as a predecessor of the RFC called ANNOTATEMORE. Currently Cyrus Imapd implements ANNOTATEMORE. There is also a dovecot plugin. The IMAP METADATA extension is central to Kolab support.

Added a mock driver with the primary objective of supporting unit tests for the Kolab drivers.

framework/Imap_Client/lib/Horde/Imap/Client/Base.php
framework/Imap_Client/lib/Horde/Imap/Client/Cclient.php
framework/Imap_Client/lib/Horde/Imap/Client/Mock.php [new file with mode: 0644]
framework/Imap_Client/lib/Horde/Imap/Client/Socket.php
framework/Imap_Client/package.xml
framework/Imap_Client/test/Horde/Imap/test_client.php

index 9940350..f9fa863 100644 (file)
@@ -2440,6 +2440,160 @@ abstract class Horde_Imap_Client_Base
      */
     abstract protected function _getMyACLRights($mailbox);
 
+    /**
+     * Get metadata for a given mailbox. The server must support the
+     * IMAP METADATA extension (RFC 5464).
+     *
+     * @param string $mailbox A mailbox. Either in UTF7-IMAP or UTF-8.
+     * @param array  $entries The entries to fetch.
+     * @param array  $options Additional options:
+     * <pre>
+     * 'maxsize'      - (int) The maximal size the returned values may have.
+     *                  This option is only available if 'annotatemore' has
+     *                  not been set.
+     *                  DEFAULT: No maximal size.
+     * 'depth'        - (string) Either "0", "1" or "infinity". Returns only
+     *                  the given value ("0"), only values one level below
+     *                  the specified value ("1") or all entries below the
+     *                  specified value ("infinity").
+     *                  This option is only available if 'annotatemore' has
+     *                  not been set.
+     *                  DEFAULT: Unset which is equivalent to "0".
+     * 'nocapability' - (boolean) Do not check for the METADATA capability of
+     *                  the server ("true"). Otherwise the call will fail if
+     *                  the server does not announce the METADATA capability
+     *                  ("false").
+     *                  DEFAULT: false - Check the capability.
+     * 'annotatemore' - (boolean) Use the ANNOTATION command rather than the 
+     *                  METADATA command ("true"). This corresponds to
+     *                  an old version of the RFC 5464 that is available for
+     *                  some servers (cyrus, dovecot).
+     *                  DEFAULT: Use the newer METADATA command.
+     * </pre>
+     *
+     * @return array 
+     * @throws Horde_Imap_Client_Exception
+     */
+    public function getMetadata($mailbox, $entries, $options = array())
+    {
+        if (empty($options['nocapability'])) {
+            if (empty($options['annotatemore'])) {
+                $capability = 'METADATA';
+            } else {
+                $capability = 'ANNOTATEMORE';
+            }
+            if (!$this->queryCapability($capability)) {
+                throw new Horde_Imap_Client_Exception('Server does not support the METADATA extension.',
+                                                      Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT);
+            }
+        }
+
+        if (!is_array($entries)) {
+            $entries = array($entries);
+        }
+
+        $entries_utf7 = array();
+        foreach ($entries as $entry) {
+            $entries_utf7[] = Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($entry);
+        }
+
+        return $this->_getMetadata(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox),
+                                   $entries_utf7,
+                                   $options);
+    }
+
+    /**
+     * Get metadata for a given mailbox.
+     *
+     * @param string $mailbox A mailbox (UTF7-IMAP).
+     * @param array  $entries The entries to fetch.
+     * @param array  $options Additional options.
+     *
+     * @return array  An array with metadata names as the keys and
+     *                metadata values as the values.
+     * @throws Horde_Imap_Client_Exception
+     */
+    abstract protected function _getMetadata($mailbox, $entries, $options);
+
+    /**
+     * Set metadata for a given mailbox/identifier.
+     *
+     * @param string $mailbox A mailbox. Either in UTF7-IMAP or UTF-8.
+     * @param array  $data    A set of data values. The metadata values
+     *                        corresponding to the keys of the array will
+     *                        be set to the values in the array.
+     * @param array  $options Additional options:
+     * <pre>
+     * 'nocapability' - (boolean) Do not check for the METADATA capability of
+     *                  the server ("true"). Otherwise the call will fail if
+     *                  the server does not announce the METADATA capability
+     *                  ("false").
+     *                  DEFAULT: false - Check the capability.
+     * 'annotatemore' - (boolean) Use the ANNOTATION command rather than the 
+     *                  METADATA command ("true"). This corresponds to
+     *                  an older draft version of the RFC 5464 that has been
+     *                  implemented in some servers (cyrus, dovecot).
+     *                  http://ietfreport.isoc.org/idref/draft-daboo-imap-annotatemore/
+     *                  DEFAULT: Use the newer METADATA command.
+     * </pre>
+     *
+     *
+     * @throws Horde_Imap_Client_Exception
+     */
+    public function setMetadata($mailbox, $data, $options = array())
+    {
+        if (empty($options['nocapability'])) {
+            if (empty($options['annotatemore'])) {
+                $capability = 'METADATA';
+            } else {
+                $capability = 'ANNOTATEMORE';
+            }
+            if (!$this->queryCapability($capability)) {
+                throw new Horde_Imap_Client_Exception('Server does not support the METADATA extension.',
+                                                      Horde_Imap_Client_Exception::NOSUPPORTIMAPEXT);
+            }
+        }
+
+        return $this->_setMetadata(Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($mailbox), $data, $options);
+    }
+
+    /**
+     * Set metadata for a given mailbox/identifier.
+     *
+     * @param string $mailbox A mailbox (UTF7-IMAP).
+     * @param array  $data    A set of data values. The metadata values
+     *                        corresponding to the keys of the array will
+     *                        be set to the values in the array.
+     * @param array  $options Additional options.
+     *
+     * @throws Horde_Imap_Client_Exception
+     */
+    abstract protected function _setMetadata($mailbox, $data, $options);
+
+    /**
+     * Split a name for the METADATA extension into the correct syntax for the
+     * older ANNOTATEMORE version (it is a predecessor of RFC 5464.
+     *
+     * @param string $name A name for a metadata entry.
+     *
+     * @return array A list of two elements: The entry name and the value type.
+     *
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _getAnnotateMoreEntry($name)
+    {
+        if (substr($name, 0, 7) == '/shared') {
+            $entry = substr($name, 7);
+            $type  = 'value.shared';
+        } else if (substr($name, 0, 8) == '/private') {
+            $entry = substr($name, 8);
+            $type  = 'value.priv';
+        } else {
+            throw new Horde_Imap_Client_Exception('Invalid METADATA entry: ' . $name);
+        }
+        return array($entry, $type);
+    }
+
     /* Utility functions. */
 
     /**
index 2b5298f..92093ee 100644 (file)
@@ -1621,6 +1621,76 @@ class Horde_Imap_Client_Cclient extends Horde_Imap_Client_Base
         return $this->_getSocket()->getMyACLRights($mailbox);
     }
 
+    /**
+     * Get metadata for a given mailbox.
+     *
+     * @param string $mailbox A mailbox (UTF7-IMAP).
+     * @param array  $entries The entries to fetch.
+     * @param array  $options Additional options.
+     *
+     * @return array An array with identifiers as the keys and the
+     *               metadata as the values.
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _getMetadata($mailbox, $entries, $options)
+    {
+        if (!empty($options['annotatemore'])
+            && function_exists('imap_getannotation')) {
+            $result = array();
+            foreach ($entries as $md_entry) {
+                list($entry, $type) = $this->_getAnnotateMoreEntry($md_entry);
+                $old_error = error_reporting(0);
+                $res = imap_getannotation($this->_stream, $mailbox, $entry, $type);
+                error_reporting($old_error);
+                if (!$res) {
+                    throw new Horde_Imap_Client_Exception('Error when fetching METADATA: ' . imap_last_error());
+                }
+                foreach ($res as $key => $value) {
+                    switch ($type) {
+                    case 'value.priv':
+                        $result[$mailbox]['/private' . $entry] = $value;
+                        break;
+                    case 'value.shared':
+                        $result[$mailbox]['/shared' . $entry] = $value;
+                        break;
+                    }
+                }
+            }
+            return $result;
+        } else {
+            return $this->_getSocket()->getMetadata($mailbox, $entry, $options);
+        }
+    }
+
+    /**
+     * Set metadata for a given mailbox/identifier.
+     *
+     * @param string $mailbox A mailbox (UTF7-IMAP).
+     * @param array  $data    A set of data values. The metadata values
+     *                        corresponding to the keys of the array will
+     *                        be set to the values in the array.
+     * @param array  $options Additional options.
+     *
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _setMetadata($mailbox, $data, $options)
+    {
+        if (!empty($options['annotatemore'])
+            && function_exists('imap_setannotation')) {
+            foreach ($data as $key => $value) {
+                list($entry, $type) = $this->_getAnnotateMoreEntry($key);
+                $old_error = error_reporting(0);
+                $res = imap_setannotation($this->_stream, $mailbox, $entry, $type, $value);
+                error_reporting($old_error);
+                if (!$res) {
+                    throw new Horde_Imap_Client_Exception('Error when setting METADATA: ' . imap_last_error());
+                }
+            }
+        } else {
+            return $this->_getSocket()->setMetadata($mailbox, $data, $options);
+        }
+    }
+
     /* Internal functions */
 
     /**
diff --git a/framework/Imap_Client/lib/Horde/Imap/Client/Mock.php b/framework/Imap_Client/lib/Horde/Imap/Client/Mock.php
new file mode 100644 (file)
index 0000000..01c27d7
--- /dev/null
@@ -0,0 +1,896 @@
+<?php
+/**
+ * A mock IMAP driver for unit testing.
+ *
+ * PHP version 5
+ *
+ * @category Horde
+ * @package  Imap_Client
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Imap_Client
+ */
+
+/**
+ * The mock driver class.
+ *
+ * Copyright 2007-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @category Horde
+ * @package  Imap_Client
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @license  http://www.fsf.org/copyleft/lgpl.html LGPL
+ * @link     http://pear.horde.org/index.php?package=Imap_Client
+ */
+class Horde_Imap_Client_Mock extends Horde_Imap_Client_Base
+{
+    /**
+     * Message flags.
+     */
+    const FLAG_NONE    = 0;
+    const FLAG_DELETED = 1;
+
+    /**
+     * The simulated IMAP storage.
+     *
+     * @var array
+     */
+    static public $storage = array();
+
+    /**
+     * Id of the current user
+     *
+     * @var string
+     */
+    private $_user;
+
+    /**
+     * The data of the mailbox currently opened
+     *
+     * @var array
+     */
+    private $_mbox = null;
+
+    /**
+     * The name of the mailbox currently opened
+     *
+     * @var array
+     */
+    private $_mboxname = null;
+
+    /**
+     * Constructs a new Horde_Imap_Client object.
+     *
+     * @param array $params A hash containing configuration parameters.
+     *
+     * @throws Horde_Imap_Client_Exception
+     */
+    public function __construct($params = array())
+    {
+        parent::__construct($params);
+
+        $this->_user = $params['username'];
+
+        if (!empty($this->params['persistent'])) {
+            register_shutdown_function(array($this, 'shutdown'));
+
+            if (empty(self::$storage) && file_exists($this->params['persistent'])
+                && $data = @unserialize(file_get_contents($this->params['persistent']))) {
+                self::$storage = $data;
+            }
+        }
+
+        if (!is_array(self::$storage)) {
+            /* Simulate an empty IMAP server */
+            self::$storage = array();
+        }
+
+        try {
+            $this->_getMailbox('INBOX');
+        } catch (Horde_Imap_Client_Exception $e) {
+            $this->createMailbox('INBOX');
+        }
+    }
+
+    /**
+     * Store the simulated IMAP store in a file.
+     *
+     * @return NULL
+     */
+    protected function shutdown()
+    {
+        $storage = fopen($this->_params['persistent'], 'a');
+        $data    = @serialize(self::$storage);
+        fwrite($storage, $data);
+        fflush($storage);
+        fclose($storage);
+    }
+
+    /**
+     * Clean the simulated IMAP store.
+     *
+     * @return NULL
+     */
+    static public function clean()
+    {
+        self::$storage = array();
+    }
+
+    /**
+     * Parse the given folder name into a structure that contains the user name.
+     *
+     * @param string $folder The folder name.
+     *
+     * @return string The corrected user name.
+     *
+     * @todo This type of mapping only works for cyrus imap with a specific
+     *       configuration.
+     */
+    function _parseFolder($folder)
+    {
+        if (substr($folder, 0, 5) == 'INBOX') {
+            $user = split('@', $this->_user);
+            return 'user/' . $user[0] . substr($folder, 5);
+        }
+        return $folder;
+    }
+
+    /**
+     * Get CAPABILITY information from the IMAP server.
+     *
+     * @return array  The capability array.
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _capability()
+    {
+        $capabilities = array(
+            'ACL' => true,
+            'METADATA' => true,
+        );
+        return $capabilities;
+    }
+
+    /**
+     * Send a NOOP command.
+     *
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _noop()
+    {
+        throw new Horde_Imap_Client_Exception('not implemented');
+    }
+
+    /**
+     * Get the NAMESPACE information from the IMAP server.
+     *
+     * @return array  An array of namespace information.
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _getNamespaces()
+    {
+        throw new Horde_Imap_Client_Exception('not implemented');
+    }
+
+    /**
+     * Return a list of alerts that MUST be presented to the user (RFC 3501
+     * [7.1]).
+     *
+     * @return array  An array of alert messages.
+     */
+    public function alerts()
+    {
+        throw new Horde_Imap_Client_Exception('not implemented');
+    }
+
+    /**
+     * Login to the IMAP server.
+     *
+     * @return boolean  Return true if global login tasks should be run.
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _login()
+    {
+        /**
+         * We already stored the username on class construction so we have
+         * nothing to do here.
+         */
+        return true;
+    }
+
+    /**
+     * Logout from the IMAP server (see RFC 3501 [6.1.3]).
+     */
+    protected function _logout()
+    {
+        throw new Horde_Imap_Client_Exception('not implemented');
+    }
+
+    /**
+     * Send ID information to the IMAP server (RFC 2971).
+     *
+     * @param array $info The information to send to the server.
+     *
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _sendID($info)
+    {
+        throw new Horde_Imap_Client_Exception('not implemented');
+    }
+
+    /**
+     * Return ID information from the IMAP server (RFC 2971).
+     *
+     * @return array  An array of information returned, with the keys as the
+     *                'field' and the values as the 'value'.
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _getID()
+    {
+        throw new Horde_Imap_Client_Exception('not implemented');
+    }
+
+    /**
+     * Sets the preferred language for server response messages (RFC 5255).
+     *
+     * @param array $langs The preferred list of languages.
+     *
+     * @return string  The language accepted by the server, or null if the
+     *                 default language is used.
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _setLanguage($langs)
+    {
+        throw new Horde_Imap_Client_Exception('not implemented');
+    }
+
+    /**
+     * Gets the preferred language for server response messages (RFC 5255).
+     *
+     * @param array $list If true, return the list of available languages.
+     *
+     * @return mixed  If $list is true, the list of languages available on the
+     *                server (may be empty). If false, the language used by
+     *                the server, or null if the default language is used.
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _getLanguage($list)
+    {
+        throw new Horde_Imap_Client_Exception('not implemented');
+    }
+
+    /**
+     * Check if a mailbox exists.
+     *
+     * @param string $mailbox The mailbox to open (UTF7-IMAP).
+     *
+     * @throws Horde_Imap_Client_Exception
+     */
+    private function _getMailbox($mailbox)
+    {
+        $folder = $this->_parseFolder($mailbox);
+        if (!isset(self::$storage[$folder])) {
+            throw new Horde_Imap_Client_Exception(sprintf("IMAP folder %s does not exist!",
+                                                          $folder));
+        }
+        return $folder;
+    }
+
+    /**
+     * Open a mailbox.
+     *
+     * @param string  $mailbox The mailbox to open (UTF7-IMAP).
+     * @param integer $mode    The access mode.
+     *
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _openMailbox($mailbox, $mode)
+    {
+        $folder          = $this->_getMailbox($mailbox);
+        $this->_mbox     = &self::$storage[$folder];
+        $this->_mboxname = $folder;
+        return true;
+    }
+
+    /**
+     * Create a mailbox.
+     *
+     * @param string $mailbox The mailbox to create (UTF7-IMAP).
+     *
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _createMailbox($mailbox)
+    {
+        $mailbox = $this->_parseFolder($mailbox);
+        if (isset(self::$storage[$mailbox])) {
+            throw new Horde_Imap_Client_Exception(sprintf("IMAP folder %s already exists!",
+                                                          $mailbox));
+        }
+        self::$storage[$mailbox] = array(
+            'status' => array(
+                'uidvalidity' => time(),
+                'uidnext' => 1),
+            'mails' => array(),
+            'permissions' => array(),
+            'annotations' => array(),
+        );
+        return true;
+    }
+
+    /**
+     * Delete a mailbox.
+     *
+     * @param string $mailbox The mailbox to delete (UTF7-IMAP).
+     *
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _deleteMailbox($mailbox)
+    {
+        $folder = $this->_parseFolder($mailbox);
+        if (!isset(self::$storage[$folder])) {
+            throw new Horde_Imap_Client_Exception(sprintf("IMAP folder %s does not exist!",
+                                                          $folder));
+        }
+        unset(self::$storage[$folder]);
+        return true;
+    }
+
+    /**
+     * Rename a mailbox.
+     *
+     * @param string $old The old mailbox name (UTF7-IMAP).
+     * @param string $new The new mailbox name (UTF7-IMAP).
+     *
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _renameMailbox($old, $new)
+    {
+        $old = $this->_parseFolder($old);
+        $new = $this->_parseFolder($new);
+
+        if (!isset(self::$storage[$old])) {
+            throw new Horde_Imap_Client_Exception(sprintf("IMAP folder %s does not exist!",
+                                                          $old));
+        }
+        if (isset(self::$storage[$new])) {
+            throw new Horde_Imap_Client_Exception(sprintf("IMAP folder %s already exists!",
+                                                          $new));
+        }
+        self::$storage[$new] = self::$storage[$old];
+        unset(self::$storage[$old]);
+        return true;
+    }
+
+    /**
+     * Manage subscription status for a mailbox.
+     *
+     * @param string  $mailbox   The mailbox to [un]subscribe to (UTF7-IMAP).
+     * @param boolean $subscribe True to subscribe, false to unsubscribe.
+     *
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _subscribeMailbox($mailbox, $subscribe)
+    {
+        throw new Horde_Imap_Client_Exception('not implemented');
+    }
+
+    /**
+     * Obtain a list of mailboxes matching a pattern.
+     *
+     * @param string  $pattern The mailbox search pattern (UTF7-IMAP).
+     * @param integer $mode    Which mailboxes to return.
+     * @param array   $options Additional options.
+     *
+     * @return array  See self::listMailboxes().
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _listMailboxes($pattern, $mode, $options)
+    {
+        $mboxes  = array_keys(self::$storage);
+        $user    = split('@', $this->_user);
+        $pattern = '#^user/' . $user[0] . '#';
+        $result  = array();
+        foreach ($mboxes as $mbox) {
+            if (preg_match($pattern, $mbox)) {
+                $result[] = preg_replace($pattern, 'INBOX', $mbox);
+            } elseif (!empty(self::$storage[$mbox]['permissions'][$this->_user])
+                      && strpos(self::$storage[$mbox]['permissions'][$this->_user], 'l') !== false) {
+                $result[] = $mbox;
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * Obtain status information for a mailbox.
+     *
+     * @param string $mailbox The mailbox to query (UTF7-IMAP).
+     * @param string $flags   A bitmask of information requested from the
+     *                        server.
+     *
+     * @return array  See self::status().
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _status($mailbox, $flags)
+    {
+        $this->openMailbox($mailbox);
+        return $this->_mbox['status'];
+    }
+
+    /**
+     * Append message(s) to a mailbox.
+     *
+     * @param string $mailbox The mailbox to append the message(s) to
+     *                        (UTF7-IMAP).
+     * @param array  $data    The message data.
+     * @param array  $options Additional options.
+     *
+     * @return mixed  An array of the UIDs of the appended messages (if server
+     *                supports UIDPLUS extension) or true.
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _append($mailbox, $data, $options)
+    {
+        foreach ($data as $element) {
+            $split = strpos($element['data'], "\r\n\r\n");
+            $mail  = array('header' => substr($element['data'], 0, $split + 2),
+                           'body' => substr($element['data'], $split + 3));
+            $this->_appendMessage($mailbox, $mail);
+        }
+    }
+
+    /**
+     * Appends a message to the current folder.
+     *
+     * @param string $mailbox The mailbox to append the message(s) to
+     *                        (UTF7-IMAP).
+     * @param array  $msg     The message to append.
+     *
+     * @return mixed  True or a PEAR error in case of an error.
+     */
+    private function _appendMessage($mailbox, $msg)
+    {
+        $this->openMailbox($mailbox);
+        $mail           = array();
+        $mail['flags']  = self::FLAG_NONE;
+        $mail['header'] = $msg['header'];
+        $mail['body']   = $msg['body'];
+
+        $this->_mbox['mails'][$this->_mbox['status']['uidnext']] = $mail;
+        $this->_mbox['status']['uidnext']++;
+        return true;
+    }
+
+    /**
+     * Request a checkpoint of the currently selected mailbox.
+     *
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _check()
+    {
+        throw new Horde_Imap_Client_Exception('not implemented');
+    }
+
+    /**
+     * Close the connection to the currently selected mailbox, optionally
+     * expunging all deleted messages (RFC 3501 [6.4.2]).
+     *
+     * @param array $options Additional options.
+     *
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _close($options)
+    {
+        throw new Horde_Imap_Client_Exception('not implemented');
+    }
+
+    /**
+     * Expunge all deleted messages from the given mailbox.
+     *
+     * @param array $options Additional options.
+     *
+     * @return array  If 'list' option is true, returns the list of
+     *                expunged messages.
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _expunge($options)
+    {
+        $remaining = array();
+        foreach ($this->_mbox['mails'] as $uid => $mail) {
+            if (!($mail['flags'] & self::FLAG_DELETED)) {
+                $remaining[$uid] = $mail;
+            }
+        }
+        $this->_mbox['mails'] = $remaining;
+        return true;
+    }
+
+    /**
+     * Search a mailbox.
+     *
+     * @param object $query   The search query.
+     * @param array  $options Additional options. The '_query' key contains
+     *                        the value of $query->build(). 'reverse' should
+     *                        be ignored (handled in search()).
+     *
+     * @return array  An array of UIDs (default) or an array of message
+     *                sequence numbers (if 'sequence' is true).
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _search($query, $options)
+    {
+        $uids = array();
+
+        $querystring = $options['_query']['query'];
+        $cmds        = explode(' ', $querystring);
+
+        foreach ($cmds as $cmd) {
+            switch ($cmd) {
+            case 'UNDELETED':
+                foreach ($this->_mbox['mails'] as $uid => $mail) {
+                    if (!($mail['flags'] & self::FLAG_DELETED)) {
+                        $uids[] = $uid;
+                    }
+                }
+                break;
+            default:
+                throw new Horde_Imap_Client_Exception(sprintf('Search command %s not implemented!',
+                                                                $cmd));
+            }
+        }
+        return array('match' => $uids, 'count' => count($uids));
+    }
+
+    /**
+     * Set the comparator to use for searching/sorting (RFC 5255).
+     *
+     * @param string $comparator The comparator string (see RFC 4790 [3.1] -
+     *                           "collation-id" - for format). The reserved
+     *                           string 'default' can be used to select
+     *                           the default comparator.
+     *
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _setComparator($comparator)
+    {
+        throw new Horde_Imap_Client_Exception('not implemented');
+    }
+
+    /**
+     * Get the comparator used for searching/sorting (RFC 5255).
+     *
+     * @return mixed  Null if the default comparator is being used, or an
+     *                array of comparator information (see RFC 5255 [4.8]).
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _getComparator()
+    {
+        throw new Horde_Imap_Client_Exception('not implemented');
+    }
+
+    /**
+     * Thread sort a given list of messages (RFC 5256).
+     *
+     * @param array $options Additional options.
+     *
+     * @return array  An array with the following values, one per message,
+     *                with the key being either the UID (default) or the
+     *                message sequence number (if 'sequence' is true). Values
+     *                of each entry:
+     * <pre>
+     * 'b' (base) - (integer) [OPTIONAL] The ID of the base message. Is not
+     *              set, this is the only message in the thread.
+     *              DEFAULT: Only message in thread
+     * 'l' (level) - (integer) [OPTIONAL] The thread level of this
+     *               message (1 = base).
+     *               DEFAULT: 0
+     * 's' (subthread) - (boolean) [OPTIONAL] Are there more messages in this
+     *                   subthread?
+     *                   DEFAULT: No
+     * </pre>
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _thread($options)
+    {
+        throw new Horde_Imap_Client_Exception('not implemented');
+    }
+
+    /**
+     * Fetch message data.
+     *
+     * @param array $criteria The fetch criteria. Function must not handle
+     *                        'parse' param to FETCH_HEADERTEXT.
+     * @param array $options  Additional options.
+     *
+     * @return array  See self::fetch().
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _fetch($criteria, $options)
+    {
+        $fetch  = array();
+        $result = array();
+
+        reset($criteria);
+        while (list($type, $c_val) = each($criteria)) {
+            if (!is_array($c_val)) {
+                $c_val = array();
+            }
+
+            $uid = $options['ids'][0];
+
+            switch ($type) {
+            case Horde_Imap_Client::FETCH_HEADERTEXT:
+                if (!isset($this->_mbox['mails'][$uid])) {
+                    throw new Horde_Imap_Client_Exception(sprintf("No IMAP message %s!", $uid));
+                }
+                $result['headertext'][$uid] = $this->_mbox['mails'][$uid]['header'];
+                break;
+            case Horde_Imap_Client::FETCH_BODYTEXT:
+                if (!isset($this->_mbox['mails'][$uid])) {
+                    throw new Horde_Imap_Client_Exception(sprintf("No IMAP message %s!", $uid));
+                }
+                $result['bodytext'][$uid] =  $this->_mbox['mails'][$uid]['body'];
+                break;
+
+            case Horde_Imap_Client::FETCH_STRUCTURE:
+            case Horde_Imap_Client::FETCH_FULLMSG:
+            case Horde_Imap_Client::FETCH_MIMEHEADER:
+            case Horde_Imap_Client::FETCH_BODYPART:
+            case Horde_Imap_Client::FETCH_HEADERS:
+            case Horde_Imap_Client::FETCH_BODYPARTSIZE:
+            case Horde_Imap_Client::FETCH_ENVELOPE:
+            case Horde_Imap_Client::FETCH_FLAGS:
+            case Horde_Imap_Client::FETCH_DATE:
+            case Horde_Imap_Client::FETCH_SIZE:
+            case Horde_Imap_Client::FETCH_UID:
+            case Horde_Imap_Client::FETCH_SEQ:
+            case Horde_Imap_Client::FETCH_MODSEQ:
+                throw new Horde_Imap_Client_Exception('Not supported!');
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * Store message flag data.
+     *
+     * @param array $options Additional options.
+     *
+     * @return array  See self::store().
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _store($options)
+    {
+
+        foreach ($options['ids'] as $uid) {
+
+            if (!isset($this->_mbox['mails'][$uid])) {
+                throw new Horde_Imap_Client_Exception(sprintf("No IMAP message %s!", $uid));
+            }
+            foreach ($options['add'] as $flag) {
+                $flag = strtoupper($flag);
+                switch ($flag) {
+                case '\\DELETED':
+                    $this->_mbox['mails'][$uid]['flags'] |= self::FLAG_DELETED;
+                    break;
+                default:
+                    throw new Horde_Imap_Client_Exception(sprintf('Flag %s not implemented!',
+                                                                  $flag));
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Copy messages to another mailbox.
+     *
+     * @param string $dest    The destination mailbox (UTF7-IMAP).
+     * @param array  $options Additional options.
+     *
+     * @return mixed  An array mapping old UIDs (keys) to new UIDs (values) on
+     *                success (if the IMAP server and/or driver support the
+     *                UIDPLUS extension) or true.
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _copy($dest, $options)
+    {
+        $new_folder = $this->_parseFolder($dest);
+
+        foreach ($options['ids'] as $uid) {
+            if (!isset($this->_mbox['mails'][$uid])) {
+                throw new Horde_Imap_Client_Exception(sprintf("No IMAP message %s!", $uid));
+            }
+            $mail = $this->_mbox['mails'][$uid];
+            if (!empty($options['move'])) {
+                unset($this->_mbox['mails'][$uid]);
+            }
+            $this->_appendMessage($new_folder, $mail);
+        }
+        return true;
+    }
+
+    /**
+     * Set quota limits.
+     *
+     * @param string $root    The quota root (UTF7-IMAP).
+     * @param array  $options Additional options.
+     *
+     * @return boolean  True on success.
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _setQuota($root, $options)
+    {
+        throw new Horde_Imap_Client_Exception('not implemented');
+    }
+
+    /**
+     * Get quota limits.
+     *
+     * @param string $root The quota root (UTF7-IMAP).
+     *
+     * @return mixed  An array with these possible keys: 'messages' and
+     *                'storage'; each key holds an array with 2 values:
+     *                'limit' and 'usage'.
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _getQuota($root)
+    {
+        throw new Horde_Imap_Client_Exception('not implemented');
+    }
+
+    /**
+     * Get quota limits for a mailbox.
+     *
+     * @param string $mailbox A mailbox (UTF7-IMAP).
+     *
+     * @return mixed  An array with the keys being the quota roots. Each key
+     *                holds an array with two possible keys: 'messages' and
+     *                'storage'; each of these keys holds an array with 2
+     *                values: 'limit' and 'usage'.
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _getQuotaRoot($mailbox)
+    {
+        throw new Horde_Imap_Client_Exception('not implemented');
+    }
+
+    /**
+     * Get ACL rights for a given mailbox.
+     *
+     * @param string $mailbox A mailbox (UTF7-IMAP).
+     *
+     * @return array  An array with identifiers as the keys and an array of
+     *                rights as the values.
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _getACL($mailbox)
+    {
+        $folder = $this->_getMailbox($mailbox);
+        $acl    = '';
+        if (isset(self::$storage[$folder]['permissions'])) {
+            $acl = self::$storage[$folder]['permissions'];
+        }
+        return $acl;
+    }
+
+    /**
+     * Set ACL rights for a given mailbox/identifier.
+     *
+     * @param string $mailbox    A mailbox (UTF7-IMAP).
+     * @param string $identifier The identifier to alter (UTF7-IMAP).
+     * @param array  $options    Additional options.
+     *
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _setACL($mailbox, $identifier, $options)
+    {
+        $folder = $this->_getMailbox($mailbox);
+        if (empty($options['rights']) && !empty($options['remove'])) {
+            unset(self::$storage[$folder]['permissions'][$identifier]);
+        } else {
+            self::$storage[$folder]['permissions'][$identifier] = $options['rights'];
+        }
+    }
+
+    /**
+     * Get ACL rights for a given mailbox/identifier.
+     *
+     * @param string $mailbox    A mailbox (UTF7-IMAP).
+     * @param string $identifier The identifier to alter (UTF7-IMAP).
+     *
+     * @return array  An array of rights (keys: 'required' and 'optional').
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _listACLRights($mailbox, $identifier)
+    {
+        throw new Horde_Imap_Client_Exception('not implemented');
+    }
+
+    /**
+     * Get the ACL rights for the current user for a given mailbox.
+     *
+     * @param string $mailbox A mailbox (UTF7-IMAP).
+     *
+     * @return array  An array of rights.
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _getMyACLRights($mailbox)
+    {
+        $folder = $this->_getMailbox($mailbox);
+        $acl    = '';
+        if (isset(self::$storage[$folder]['permissions'][$this->_user])) {
+            $acl = self::$storage[$folder]['permissions'][$this->_user];
+        }
+        return $acl;
+    }
+
+    /**
+     * Get metadata for a given mailbox.
+     *
+     * @param string $mailbox A mailbox (UTF7-IMAP).
+     * @param array  $entries The entries to fetch.
+     * @param array  $options Additional options.
+     *
+     * @return array  An array with identifiers as the keys and the
+     *                metadata as the values.
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _getMetadata($mailbox, $entries, $options)
+    {
+        $folder   = $this->_getMailbox($mailbox);
+        $metadata = array();
+        foreach ($entries as $entry) {
+            $result = false;
+            if (isset(self::$storage[$folder]['annotations'])) {
+                $ref  = &self::$storage[$folder]['annotations'];
+                $path = split('/', $entry);
+                foreach ($path as $element) {
+                    if (!isset($ref[$element])) {
+                        $result = false;
+                        break;
+                    } else {
+                        $ref    = &$ref[$element];
+                        $result = true;
+                    }
+                }
+                if ($result && isset($ref['/'])) {
+                    $result = $ref['/'];
+                }
+            }
+            $metadata[$entry] = $result;
+        }
+        return $metadata;
+    }
+
+    /**
+     * Set metadata for a given mailbox/identifier.
+     *
+     * @param string $mailbox A mailbox (UTF7-IMAP).
+     * @param array  $data    A set of data values. The metadata values
+     *                        corresponding to the keys of the array will
+     *                        be set to the values in the array.
+     * @param array  $options Additional options.
+     *
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _setMetadata($mailbox, $data, $options)
+    {
+        $folder = $this->_getMailbox($mailbox);
+        foreach ($data as $key => $value) {
+            $path = split('/', $key);
+            $ref  = &self::$storage[$folder]['annotations'];
+            foreach ($path as $element) {
+                if (!isset($ref[$element])) {
+                    $ref[$element] = array();
+
+                    $ref = &$ref[$element];
+                }
+            }
+            $ref['/'] = $value;
+        }
+        return true;
+    }
+}
\ No newline at end of file
index fd84685..e0a6f33 100644 (file)
@@ -32,6 +32,7 @@
  *   RFC 5255 - LANGUAGE/I18NLEVEL
  *   RFC 5256 - THREAD/SORT
  *   RFC 5267 - ESORT
+ *   RFC 5464 - METADATA
  *
  *   [NO RFC] - XIMAPPROXY
  *       + Requires imapproxy v1.2.7-rc1 or later
@@ -3005,6 +3006,152 @@ class Horde_Imap_Client_Socket extends Horde_Imap_Client_Base
         $this->_temp['myrights'] = $data[1];
     }
 
+    /**
+     * Get metadata for a given mailbox.
+     *
+     * @param string $mailbox A mailbox (UTF7-IMAP).
+     * @param array  $entries The entries to fetch.
+     * @param array  $options Additional options.
+     *
+     * @return array  An array with identifiers as the keys and the
+     *                metadata as the values.
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _getMetadata($mailbox, $entries, $options)
+    {
+        $this->login();
+
+        $this->_temp['metadata'] = array();
+
+        $cmd_options = array();
+        $single_type = '';
+
+        if (!empty($options['annotatemore'])) {
+            if (!empty($options['maxsize']) || !empty($options['depth'])) {
+                throw new Horde_Imap_Client_Exception('ANNOTATEMORE does not support the "depth" and "maxsize" option.');
+            }
+            $cmd = 'GETANNOTATION ';
+
+            $result = array();
+            foreach ($entries as $md_entry) {
+                list($entry, $type) = $this->_getAnnotateMoreEntry($md_entry);
+                if (empty($single_type)) {
+                    $single_type = $type;
+                } else if ($single_type != $type) {
+                    throw new Horde_Imap_Client_Exception('Multiple value types may not be retrieved in one call when using ANNOTATEMORE.');
+                }
+                $result[] = $entry;
+            }
+            $entries = $result;
+        } else {
+            $cmd = 'GETMETADATA ';
+
+            if (!empty($options['maxsize'])) {
+                $cmd_options[] = '(MAXSIZE ' . $options['maxsize'] . ')';
+            }
+            if (!empty($options['depth'])) {
+                $cmd_options[] = '(DEPTH ' . $options['depth'] . ')';
+            }
+        }
+
+        if (count($entries) == 1) {
+            $entry_string = $this->utils->escape($entries[0]) . ' ' . $this->utils->escape($single_type);
+        } else {
+            $entry_string = '(' . join(' ', $entries) . ') ' . $single_type;
+        }
+
+        if (count($cmd_options) == 0) {
+            $option_string = ' ';
+        } else if (count($cmd_options) == 1) {
+            $option_string = ' ' . $cmd_options[0] . ' ';
+        } else {
+            $option_string = ' (' . join(' ', $cmd_options) . ') ';
+        }
+
+        $this->_sendLine($cmd . $this->utils->escape($mailbox) . $option_string . $entry_string);
+        return $this->_temp['metadata'];
+    }
+
+    /**
+     * Set metadata for a given mailbox/identifier.
+     *
+     * @param string $mailbox A mailbox (UTF7-IMAP).
+     * @param array  $data    A set of data values. The metadata values
+     *                        corresponding to the keys of the array will
+     *                        be set to the values in the array.
+     * @param array  $options Additional options.
+     *
+     * @throws Horde_Imap_Client_Exception
+     */
+    protected function _setMetadata($mailbox, $data, $options)
+    {
+        if (!empty($options['annotatemore'])) {
+            $cmd = 'SETANNOTATION ';
+
+            $data_elements = array();
+            foreach ($data as $md_entry => $value) {
+                list($entry, $type) = $this->_getAnnotateMoreEntry($md_entry);
+                $i_value = ($value === null) ? 'NIL' : $this->utils->escape($value);
+                $data_elements[] = $this->utils->escape($entry) . ' (' . $this->utils->escape($type) . ' ' . $i_value . ')';
+            }
+        } else {
+            $cmd = 'SETMETADATA ';
+
+            foreach ($data as $key => $value) {
+                $i_value = ($value === null) ? 'NIL' : $this->utils->escape($value);
+                $data_elements[] = $this->utils->escape($key) . ' ' . $i_value;
+            }
+        }
+
+        if (count($data_elements) == 1) {
+            $data_string = $data_elements[0];
+        } else {
+            $data_string = '(' . join(' ', $data_elements) . ')';
+        }
+
+        /**
+         * Disallow multi-line data for now.
+         * @todo: Support this with sending literal data.
+         */
+        $data_string = str_replace("\n", '', $data_string);
+
+        $this->_sendLine($cmd . $this->utils->escape($mailbox) . ' ' . $data_string);
+    }
+
+    /**
+     * Parse a METADATA response (RFC 5464 [4.4]).
+     *
+     * @param array $data  The server response.
+     */
+    protected function _parseMetadata($data)
+    {
+        switch ($data[0]) {
+        case 'ANNOTATION':
+            $values = $data[3];
+            while (!empty($values)) {
+                $type = array_shift($values);
+                switch ($type) {
+                case 'value.priv':
+                    $this->_temp['metadata'][$data[1]]['/private' . $data[2]] = array_shift($values);
+                    break;
+                case 'value.shared':
+                    $this->_temp['metadata'][$data[1]]['/shared' . $data[2]] = array_shift($values);
+                    break;
+                default:
+                    throw new Horde_Imap_Client_Exception('Invalid METADATA value type ' . $type);
+                }
+            }
+            break;
+        case 'METADATA':
+            $values = $data[2];
+            while (!empty($values)) {
+                $entry = array_shift($values);
+                $this->_temp['metadata'][$data[1]][$entry] = array_shift($values);
+            }
+            break;
+        }
+    }
+
     /* Internal functions. */
 
     /**
@@ -3526,6 +3673,12 @@ class Horde_Imap_Client_Socket extends Horde_Imap_Client_Base
                 $this->_parseVanished(array_slice($ob['token'], 1));
                 break;
 
+            case 'ANNOTATION':
+            case 'METADATA':
+                // Parse a ANNOTATEMORE/METADATA response (RFC 5464).
+                $this->_parseMetadata($ob['token']);
+                break;
+
             default:
                 // Next, look for responses where the keywords occur second.
                 $type = strtoupper($ob['token'][1]);
index 9ecf673..6382b99 100644 (file)
@@ -55,6 +55,7 @@ http://pear.php.net/dtd/package-2.0.xsd">
        <file name="Cclient.php" role="php" />
        <file name="DateTime.php" role="php" />
        <file name="Exception.php" role="php" />
+       <file name="Mock.php" role="php" />
        <file name="Socket.php" role="php" />
        <file name="Sort.php" role="php" />
        <file name="Thread.php" role="php" />
@@ -116,6 +117,7 @@ http://pear.php.net/dtd/package-2.0.xsd">
    <install name="lib/Horde/Imap/Client/Cclient.php" as="Horde/Imap/Client/Cclient.php" />
    <install name="lib/Horde/Imap/Client/DateTime.php" as="Horde/Imap/Client/DateTime.php" />
    <install name="lib/Horde/Imap/Client/Exception.php" as="Horde/Imap/Client/Exception.php" />
+   <install name="lib/Horde/Imap/Client/Mock.php" as="Horde/Imap/Client/Mock.php" />
    <install name="lib/Horde/Imap/Client/Search/Query.php" as="Horde/Imap/Client/Search/Query.php" />
    <install name="lib/Horde/Imap/Client/Socket.php" as="Horde/Imap/Client/Socket.php" />
    <install name="lib/Horde/Imap/Client/Socket/Pop3.php" as="Horde/Imap/Client/Socket/Pop3.php" />
index 4d86aef..8c628d7 100644 (file)
@@ -758,6 +758,30 @@ try {
     print "Fetch: FAILED\n";
 }
 
+print "\nSet METADATA on " . $test_mbox . ".\n";
+try {
+    $imap_client->setMetadata($test_mbox,
+                              array('/shared/comment' => 'test'),
+                              array('annotatemore' => true,
+                                    'nocapability' => true));
+    print "Set Metadata: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Set Metadata: FAILED\n";
+}
+
+print "\nGet METADATA from " . $test_mbox . ".\n";
+try {
+    print_r($imap_client->getMetadata($test_mbox,
+                                      '/shared/comment',
+                                      array('annotatemore' => true,
+                                            'nocapability' => true)));
+    print "Get Metadata: OK\n";
+} catch (Horde_Imap_Client_Exception $e) {
+    print 'ERROR: ' . $e->getMessage() . "\n";
+    print "Get Metadata: FAILED\n";
+}
+
 print "\nRe-open " . $test_mbox . " READ-WRITE.\n";
 try {
     $imap_client->openMailbox($test_mbox, Horde_Imap_Client::OPEN_READWRITE);