<?php
/**
 * Horde_Imsp_Book Class - provides api for dealing with IMSP
 * address books.
 *
 * Copyright 2002-2017 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
 *
 * @author  Michael J Rubinsky <mrubinsk@horde.org>
 * @package Horde_Imsp
 */
class Horde_Imsp_Book
{
    /**
     * Supported ACLs
     *
     */
    const ACL_RIGHTS = 'lrwcda';

    /**
     * Sort order.
     *
     * @var string
     */
    public $sort = 'ascend';

    /**
     * Horde_Imsp_Client object.
     *
     * @var Horde_Imsp_Client_Base
     */
    protected $_imsp;

    /**
     * Parameter list.
     *
     * @var array
     */
    protected $_params;

    /**
     * Constructor function.
     *
     * @param array $params Hash containing IMSP parameters.
     */
    public function __construct(Horde_Imsp_Client_Base $client, array $params)
    {
        $this->_params = $params;
        $this->_imsp = $client;
    }

    /**
     * Returns an array containing the names of all the address books
     * available to the logged in user.
     *
     * @return mixed Array of address book names
     */
    public function getAddressBookList()
    {
        $command_string = 'ADDRESSBOOK *';

        $this->_imsp->send($command_string);

        /* Iterate through the response and populate an array of
         * address book names. */
        $server_response = $this->_imsp->receive();
        $abooks = array();
        while (preg_match("/^\* ADDRESSBOOK/", $server_response)) {
            /* If this is an ADDRESSBOOK response, then this will explode as so:
             * [0] and [1] can be discarded
             * [2] = attributes
             * [3] = delimiter
             * [4] = address book name
             */

            /* First, check for a {} */
            if (preg_match(Horde_Imsp_Client_Socket::OCTET_COUNT, $server_response, $tempArray)) {
                $abooks[] = $this->_imsp->receiveStringLiteral($tempArray[2]);
                /* Get the CRLF at end of ADDRESSBOOK response
                 * that the {} does not include. */
                $this->_imsp->receiveStringLiteral(2);
            } else {
                $parts = explode(' ', $server_response);
                $numParts = count($parts);
                $name = $parts[4];
                $firstChar = substr($name, 0, 1);
                if ($firstChar =="\"") {
                    $name = ltrim($name, "\"");
                    for ($i = 5; $i < $numParts; $i++) {
                        $name .= ' ' . $parts[$i];
                        $lastChar = substr($parts[$i], strlen($parts[$i]) - 1, 1);
                        if ($lastChar == "\"") {
                            $name = rtrim($name, "\"");
                            break;
                        }
                    }
                }
                $abooks[] = $name;
            }
            $server_response = $this->_imsp->receive();
        }

        if ($server_response != 'OK') {
            $this->_imsp->_logger->err('Did not receive expected response frm server.');
            throw new Horde_Imsp_Exception('Did not receive the expected response from the server.');
        }
        $this->_imsp->_logger->debug('ADDRESSBOOK command OK.');

        return $abooks;
    }

    /**
     * Returns an array containing the names that match $search
     * critera in the address book named $abook.
     *
     * @param string $abook  Address book name to search.
     * @param mixed $search  Search criteria either a string (name) or an array
     *                       in the form of 'fieldName' => 'searchTerm'.
     *
     * @return array Array of names of the entries that match.
     * @throws Horde_Imsp_Exception
     */
    public function search($abook, $search)
    {
        //If no field => value pairs, assume we are searching name.
        $criteria = array();
        if (!is_array($search)) {
            $criteria['name'] = $search;
        } else {
            $criteria = $search;
        }

        $this->_imsp->send('SEARCHADDRESS ', true, false);

        // Do we need to send the abook name as {} ?
        if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $abook)) {
            $biBook = sprintf("{%d}", strlen($abook));
            $this->_imsp->send($biBook, false, true, true);
        }

        //Start parsing the search array.
        $this->_imsp->send("$abook", false, false);
        $count = count($criteria);
        $current = 1;
        foreach ($criteria as $search_field => $search) {
            $this->_imsp->send(" $search_field ", false, false);
            // How about the search term as a {}.
            if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $search)) {
                $biSearch = sprintf("{%d}", strlen($search));
                $this->_imsp->send($biSearch, false, true, true);
                $this->_imsp->send($search, false, $current == $count);
                $current++;
            } else {
                // Only send the CrLf if this is the last field/search atom.
                $this->_imsp->send('"' . $search . '"', false, $current == $count);
                $current++;
            }
        }

        // Get the response.
        $server_response = $this->_imsp->receive();
        $abookNames = array();

        while (preg_match("/^\* SEARCHADDRESS/", $server_response)) {
            $chopped_response = preg_replace("/^\* SEARCHADDRESS/", '', $server_response);

            // Remove any lingering white space in front only.
            $chopped_response = ltrim($chopped_response);

            // Get rid of any lingering quotes.
            $temp = preg_replace("/\"/", '', $chopped_response);

            if (preg_match("/({)([0-9]{1,})(\}$)/", $temp, $tempArray)) {
                $dataSize = $tempArray[2];
                $temp = $this->_imsp->receiveStringLiteral($dataSize);

                /* Get the CRLF since {} does not include it. */
                $this->_imsp->receiveStringLiteral(2);
            }

            $abookNames[] = $temp;

            // Get the next response line from the server.
            $server_response = $this->_imsp->receive();
        }

        // Should check for OK or BAD here just to be certain.
        switch ($server_response) {
        case 'BAD':
            $this->_imsp->_logger->err('The IMSP server did not understand your request:' . $command_text);
            throw new Horde_Imsp_Exception('The IMSP server did not understand your request: ' . $command_text);
        case 'NO':
            $this->_imsp->_logger->err('IMSP server is unable to perform your request: ' . $this->_imsp->lastRawError);
            throw new Horde_Imsp_Exception('IMSP server is unable to perform your request: ' . $this->_imsp->lastRawError);
        }

        /* This allows for no results */
        if (count($abookNames) < 1) {
            return $abookNames;
        }

        $this->_imsp->_logger->debug('SEARCHADDRESS command OK');

        // Determine the sort direction and perform the sort.
        switch ($this->sort) {
        case 'ascend':
            sort($abookNames);
            break;

        case 'descend':
            rsort($abookNames);
            break;
        }

        return $abookNames;
    }

    /**
     * Returns an associative array of a single address book entry.
     * Note that there will always be a 'name' field.
     *
     * @param string $abook       Name of the address book to search.
     * @param string $entryName  'name' attribute of the entry to retrieve
     *
     * @return array  Array containing entry.
     * @throws Horde_Imsp_Exception
     * @throws Horde_Exception_NotFound
     */
    public function getEntry($abook, $entryName)
    {
        $this->_imsp->send('FETCHADDRESS ', true, false);
        if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $abook)) {
            $biBook = sprintf("{%d}", strlen($abook));
            $this->_imsp->send($biBook, false, true, true);
        }
        $this->_imsp->send("$abook ", false, false);
        if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $entryName)) {
            $biName = sprintf("{%d}", strlen($entryName));
            $this->_imsp->send($biName, false, true, true);
            $this->_imsp->send($entryName, false, true);
        } else {
            $this->_imsp->send("\"$entryName\"", false, true);
        }

        $server_response = $this->_imsp->receive();
        switch ($server_response) {
        case 'BAD':
            $this->_imsp->_logger->err('The IMSP server did not understand your request.');
            throw new Horde_Imsp_Exception('The IMSP server did not understand your request');
        case 'NO':
            throw new Horde_Exception_NotFound('No entry in this address book matches your query.');
        }

        // Get the data in an associative array.
        $entry = $this->_parseFetchAddressResponse($server_response);

        //Get the next server response -- this *should* be the OK response.
        $server_response = $this->_imsp->receive();
        if ($server_response != 'OK') {
            // Unexpected response throw error but still continue on.
            $this->_imsp->_logger->err('Did not receive the expected response from the server.');
        }
        $this->_imsp->_logger->debug('FETCHADDRESS completed OK');

        return $entry;
    }

    /**
     * Creates a new address book.
     *
     * @param string $abookName FULLY QUALIFIED name such 'jdoe.clients' etc...
     *
     * @throws Horde_Imsp_Exception
     */
    public function createAddressBook($abookName)
    {
        $command_text = 'CREATEADDRESSBOOK ';

        if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $abookName)) {
            $biBook = sprintf("{%d}", strlen($abookName));
            $this->_imsp->send($command_text . $biBook, true, true, true);
            $this->_imsp->send($abookName, false, true);
        } else {
            $this->_imsp->send($command_text . $abookName, true, true);
        }

        $server_response = $this->_imsp->receive();
        switch ($server_response) {
        case 'OK':
            $this->_imsp->_logger->debug('CREATEADDRESSBOOK completed OK');
            break;
        case 'NO':
            // Could not create abook.
            $this->_imsp->_logger->err('IMSP server is unable to perform your request.');
            throw new Horde_Imsp_Exception('IMSP server is unable to perform your request.');
        case 'BAD':
            $this->_imsp->_logger->err('The IMSP server did not understand your request.');
            throw new Horde_Imsp_Exception('The IMSP server did not understand your request.');
        default:
            // Something unexpected.
            $this->_imsp->_logger->err('Did not receive the expected response from the server.');
            throw new Horde_Imsp_Exception('Did not receive the expected response from the server.');
        }
    }

    /**
     * Deletes an address book completely!
     *
     * @param string $abookName Name of address book to delete.
     *
     * @throws Horde_Imsp_Exception
     */
    public function deleteAddressBook($abookName)
    {
        $command_text = 'DELETEADDRESSBOOK ';

        // Check need for {}.
        if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $abookName)) {
            $biBook = sprintf("{%d}", strlen($abookName));
            $this->_imsp->send($command_text . $biBook, true, true, true);
            $this->_imsp->send($abookName, false, true);
        } else {
            $this->_imsp->send($command_text . $abookName, true, true);
        }
        $server_response = $this->_imsp->receive();
        switch ($server_response) {
        case 'OK':
            $this->_imsp->_logger->debug('DELETEADDRESSBOOK completed OK');
            break;
        case 'NO':
            // Could not create abook.
            $this->_imsp->_logger->err('IMSP server is unable to perform your request.');
            throw new Horde_Imsp_Exception('IMSP server is unable to perform your request.');
        case 'BAD':
            $this->_imsp->_logger->err('The IMSP server did not understand your request.');
            throw new Horde_Imsp_Exception('The IMSP server did not understand your request.');
        default:
            // Something unexpected.
            $this->_imsp->_logger->err('Did not receive the expected response from the server.');
            throw new Horde_Imsp_Exception('Did not receive the expected response from the server.');
        }
    }

    /**
     * Renames an address book.
     *
     * @param string $abookOldName Old name.
     * @param string $abookNewName New address book name.
     *
     * @throws Horde_Imsp_Exception
     */
    public function renameAddressBook($abookOldName, $abookNewName)
    {
        $this->_imsp->send('RENAMEADDRESSBOOK ', true, false);
        if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $abookOldName)) {
            $biOldName = sprintf("{%d}", strlen($abookOldName));
            $this->_imsp->send($biOldName, false, true);
            $this->_imsp->receive();
        }

        $this->_imsp->send("$abookOldName ", false, false);
        if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $abookNewName)) {
            $biNewName = sprintf("{%d}", strlen($abookNewName));
            $this->_imsp->send($biNewName, false, true);
            $this->_imsp->receive();
        }
        $this->_imsp->send($abookNewName, false, true);

        // Get server response.
        $server_response = $this->_imsp->receive();
        switch ($server_response) {
        case 'NO':
            // Could not create abook.
            $this->_imsp->_logger->err('IMSP server is unable to perform your request.');
            throw new Horde_Imsp_Exception('IMSP server is unable to perform your request.');
        case 'BAD':
            $this->_imsp->_logger->err('The IMSP server did not understand your request.');
            throw new Horde_Imsp_Exception('The IMSP server did not understand your request.');
        case 'OK':
            $this->_imsp->_logger->debug("Address book $abookOldName successfully changed to $abookNewName");
            break;
        default:
            // Something unexpected.
            $this->_imsp->_logger->err('Did not receive the expected response from the server.');
            throw new Horde_Imsp_Exception('Did not receive the expected response from the server.');
        }
    }

    /**
     * Adds an address book entry to an address book.
     *
     * @param string $abook     Name of address book to add entry to.
     * @param array $entryInfo  Address book entry information -
     *                          there MUST be a field 'name' containing the
     *                          entry name.
     *
     * @throws Horde_Imsp_Exception
     */
    public function addEntry($abook, array $entryInfo)
    {
        $command_text = '';

        // Lock the entry if it already exists.
        $this->lockEntry($abook, $entryInfo['name']);
        $this->_imsp->send('STOREADDRESS ', true, false);

        // Take care of the name.
        $entryName = $entryInfo['name'];

        // {} for book name?
        if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $abook)) {
            $biBook = sprintf("{%d}", strlen($abook));
            $this->_imsp->send($biBook, false, true);
            $this->_imsp->receive();
        }
        $this->_imsp->send("$abook ", false, false);

        // Do we need {} for entry name as well?
        if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $entryName)) {
            $biname = sprintf("{%d}", strlen($entryName));
            $this->_imsp->send($biname, false, true);
            $this->_imsp->receive();
            $this->_imsp->send($entryName, false, false);
        } else {
            $this->_imsp->send("\"$entryName\" ", false, false);
        }

        foreach ($entryInfo as $key => $value) {
            // Do not sent the key name 'name'.
            if ($key != 'name') {
                // Protect from extraneous white space
                $value = trim($value);

                // For some reason, tabs seem to break this.
                $value = preg_replace("/\t/", "\n\r", $value);

                // Check to see if we need {}
                if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $value)) {
                    $command_text .= $key . sprintf(" {%d}", strlen($value));
                    $this->_imsp->send($command_text, false, true);
                    $server_response = $this->_imsp->receive();
                    $command_text = '';
                    if (!preg_match("/^\+/",  $server_response)) {
                        $this->_imsp->_logger->err('Did not receive the expected response from the server.');
                        throw new Horde_Imsp_Exception('Did not receive the expected response from the server.');
                    }
                    $this->_imsp->send($value, false, false);
                } else {
                    // If we are here, then we do not need to send a literal.
                    $value = "\"" . $value . "\"";
                    $command_text .= $key . ' ' . $value . ' ';
                }
            }
        }

        // Send anything that is left of the command.
        $this->_imsp->send($command_text, false, true);
        $server_response = $this->_imsp->receive();

        switch ($server_response) {
        case 'NO':
            // Could not create abook.
            $this->_imsp->_logger->err('IMSP server is unable to perform your request.');
            throw new Horde_Imsp_Exception('IMSP server is unable to perform your request.');
        case 'BAD':
            $this->_imsp->_logger->err('The IMSP server did not understand your request.');
            throw new Horde_Imsp_Exception('The IMSP server did not understand your request.');
        }

        if ($server_response != 'OK') {
            // Cyrus-IMSP server sends a FETCHADDRESS Response here.
            // Do others?     This was not in the RFC.
            $dummy_array = $this->_parseFetchAddressResponse($server_response);
            $server_response = $this->_imsp->receive();
            switch ($server_response) {
            case 'NO':
                // Could not create abook.
                $this->_imsp->_logger->err('IMSP server is unable to perform your request.');
                throw new Horde_Imsp_Exception('IMSP server is unable to perform your request.');
            case 'BAD':
                $this->_imsp->_logger->err('The IMSP server did not understand your request.');
                throw new Horde_Imsp_Exception('The IMSP server did not understand your request.');
            case 'OK':
                $this->_imsp->_logger->debug('STOREADDRESS Completed successfully.');

                //we were successful...so release the lock on the entry
                $this->unlockEntry($abook, $entryInfo['name']);
            }
        }
    }

    /**
     * Deletes an abook entry.
     *
     * @param string $abook     Name of address book containing entry.
     * @param string $bookEntry Name of entry to delete.
     *
     * @throws Horde_Imsp_Exception
     */
    public function deleteEntry($abook, $bookEntry)
    {
        // Start the command.
        $this->_imsp->send('DELETEADDRESS ', true, false);
        // Need {} for book name?
        if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $abook)) {
            $biBook = sprintf("{%d}", strlen($abook));
            $this->_imsp->send($biBook, false, true, true);
        }
        $this->_imsp->send("$abook ", false, false);

        //How bout for the entry name?
        if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $bookEntry)) {
            $biEntry = sprintf("{%d}", strlen($bookEntry));
            $this->_imsp->send($biEntry, false, true, true);
        } else {
            $bookEntry = $this->_imsp->quoteSpacedString($bookEntry);
        }
        $this->_imsp->send($bookEntry, false, true);
        $server_response = $this->_imsp->receive();
        switch ($server_response) {
        case 'NO':
            // Could not create abook.
            $this->_imsp->_logger->err('IMSP server is unable to perform your request.');
            throw new Horde_Imsp_Exception('IMSP server is unable to perform your request.');
        case 'BAD':
            $this->_imsp->_logger->err('The IMSP server did not understand your request.');
            throw new Horde_Imsp_Exception('The IMSP server did not understand your request.');
        case 'OK':
            $this->_imsp->_logger->debug('DELETE Completed successfully.');
        }
    }

    /**
     * Attempts to acquire a semaphore on the address book entry.
     *
     * @param string $abook     Address book name
     * @param string $bookEntry Name of entry to lock
     *
     * @return mixed true or array on success (depends on server in use).
     */
    public function lockEntry($abook, $bookEntry)
    {
        $this->_imsp->send('LOCK ADDRESSBOOK ', true, false);

        // Do we need a string literal?
        if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $abook)) {
            $biBook = sprintf("{%d}", strlen($abook));
            $this->_imsp->send($biBook, false, true, true);
        }
        $this->_imsp->send("$abook ", false, false);
        // What about the entry name?
        if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $bookEntry)) {
            $biEntry = sprintf("{%d}", strlen($bookEntry));
            $this->_imsp->send($biEntry, false, true, true);
            $this->_imsp->send($bookEntry, false, true);
        } else {
            $bookEntry = $this->_imsp->quoteSpacedString($bookEntry);
            $this->_imsp->send("$bookEntry", false, true);
        }

        $server_response = $this->_imsp->receive();
        do {
            switch ($server_response) {
            case 'NO':
                // Could not create abook.
                $this->_imsp->_logger->err('IMSP server is unable to perform your request.');
                throw new Horde_Imsp_Exception('IMSP server is unable to perform your request.');
            case 'BAD':
                $this->_imsp->_logger->err('The IMSP server did not understand your request.');
                throw new Horde_Imsp_Exception('The IMSP server did not understand your request.');
            }

            //Check to see if this is a FETCHADDRESS resonse
            $dummy = $this->_parseFetchAddressResponse($server_response);
            if ($dummy) {
                $server_response = $this->_imsp->receive();
            }
        } while ($server_response != 'OK');

        $this->_imsp->_logger->debug("LOCK ADDRESSBOOK on $abook $bookEntry OK");

        // Return either true or the FETCHADDRESS response if it exists.
        if (!$dummy) {
            return true;
        } else {
            return $dummy;
        }
    }

    /**
     * Unlocks a previously locked address book.
     *
     * @param string $abook     Name of address book containing locked entry.
     * @param string $bookEntry Name of entry to unlock.
     *
     * @throws Horde_Imsp_Exception
     */
    public function unlockEntry($abook, $bookEntry)
    {
        // Start sending command.
        $this->_imsp->send('UNLOCK ADDRESSBOOK ', true, false);

        // {} for book name?
        if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $abook)) {
            $biBook = sprintf("{%d}", strlen($abook));
            $this->_imsp->send($biBook, false, true, true);
        }
        $this->_imsp->send("$abook ", false, false);
        //How bout for entry name?
        if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $bookEntry)) {
            $biEntry=sprintf("{%d}", strlen($bookEntry));
            $this->_imsp->send($biEntry, false, true, true);
            $this->_imsp->send($bookEntry, false, true);
        } else {
            $bookEntry = $this->_imsp->quoteSpacedString($bookEntry);
            $this->_imsp->send("$bookEntry", false, true);
        }
        $response = $this->_imsp->receive();
        switch ($response) {
        case 'NO':
            // Could not create abook.
            $this->_imsp->_logger->err('IMSP server is unable to perform your request.');
            throw new Horde_Imsp_Exception('IMSP server is unable to perform your request.');
        case 'BAD':
            $this->_imsp->_logger->err('The IMSP server did not understand your request.');
            throw new Horde_Imsp_Exception('The IMSP server did not understand your request.');
        case 'OK':
            $this->_imsp->_logger->debug("UNLOCK ADDRESSBOOK on $abook $bookEntry OK");
        }
    }

    /**
     * Access Control List (ACL)  Methods.
     *
     * The following characters are recognized ACL characters: lrwcda
     * l - "lookup"  (see the name and existence of the address book)
     * r - "read"    (search and retrieve addresses from address book)
     * w - "write"   (create/edit new address book entries - not delete)
     * c - "create"  (create new address books under the current address book)
     * d - "delete"  (delete entries or entire book)
     * a - "admin"   (set ACL lists for this address book - usually only
     *               allowed for the owner of the address book)
     *
     * examples:
     *  "lr" would be read only for that user
     *  "lrw" would be read/write
     */

    /**
     * Sets an Access Control List for an abook.
     *
     * @param string $abook Name of address book.
     * @param string $ident Name of user for this acl.
     * @param string $acl   acl for this user/book.
     *
     * @return mixed True on success / PEAR_Error on failure.
     */
    public function setACL($abook, $ident, $acl)
    {
        // Verify that $acl looks good.
        if (preg_match("/[^" . self::ACL_RIGHTS . "]/", $acl)) {
            $this->_imsp->_logger('Bad Argument');
            throw new InvalidArgumentException();
        }

        // Begin sending command.
        $this->_imsp->send('SETACL ADDRESSBOOK ', true, false);
        // {} for book name?
        if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $abook)) {
            $biBook = sprintf("{%d}", strlen($abook));
            $this->_imsp->send($biBook, false, true, true);
        }
        $this->_imsp->send("$abook ", false, false);

        // {} for ident?
        if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $ident)) {
            $biIdent = sprintf("{%d}", strlen($ident));
            $this->_imsp->send($biIdent, false, true, true);
        }
        $this->_imsp->send("$ident ", false, false);

        // Now finish up with the actual ACL.
        $this->_imsp->send($acl, false, true);
        $response = $this->_imsp->receive();
        switch ($response) {
        case 'NO':
            // Could not create abook.
            $this->_imsp->_logger->err('IMSP server is unable to perform your request.');
            throw new Horde_Imsp_Exception('IMSP server is unable to perform your request.');
        case 'BAD':
            $this->_imsp->_logger->err('The IMSP server did not understand your request.');
            throw new Horde_Imsp_Exception('The IMSP server did not understand your request.');
        case 'OK':
            $this->_imsp->_logger->debug("ACL set for $ident on $abook");
            break;
        default:
            // Do not know why we would make it down here.
            $this->_imsp->_logger->err('Did not receive the expected response from the server.');
            throw new Horde_Imsp_Exception('Did not receive the expected response from the server.');
        }
    }

    /**
     * Retrieves an address book's ACL.
     *
     * @param string $abook Name of address book to retrieve acl for.
     *
     * @return mixed array containing acl for every user with access to
     *                     address book or PEAR_Error on failure.
     */
    public function getACL($abook)
    {
        $this->_imsp->send('GETACL ADDRESSBOOK ', true, false);

        // {} for book name?
        if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $abook)) {
            $biName = sprintf("{%d}", strlen($abook));
            $this->_imsp->send($biName, false, true, true);
        }
        $this->_imsp->send($abook, false, true);

        // Get results.
        $response = $this->_imsp->receive();
        switch ($response) {
        case 'NO':
            // Could not create abook.
            $this->_imsp->_logger->err('IMSP server is unable to perform your request.');
            throw new Horde_Imsp_Exception('IMSP server is unable to perform your request.');
        case 'BAD':
            $this->_imsp->_logger->err('The IMSP server did not understand your request.');
            throw new Horde_Imsp_Exception('The IMSP server did not understand your request.');
        }

        // If we are here, we need to receive the * ACL Responses.
        do {
            /* Get an array of responses.
             * The [3] element should be the address book name
             * [4] and [5] will be user/group name and permissions */

            //the book name might be a literal
            if (preg_match(Horde_Imsp_Client_Base::OCTET_COUNT, $response, $tempArray)) {
                $data = $this->_imsp->receiveStringLiteral($tempArray[2]);
                $response = $this->_imsp->receive();
            }

            $parts = explode(' ', $response);

            // Push the array if book was a literal
            if ($data) {
                array_unshift($parts, ' ', ' ', ' ', ' ');
            }
            // Address book name quoted?
            $numParts = count($parts);
            $name = $parts[3];
            $firstACLIdx = 4;
            $firstChar = substr($name, 0, 1);
            if ($firstChar == "\"") {
                for ($i = 4; $i < $numParts; $i++) {
                    $lastChar = substr($parts[$i], strlen($parts[$i]) - 1, 1);
                    $firstACLIdx++;
                    if ($lastChar == "\"") {
                        break;
                    }
                }
            }

            for ($i = $firstACLIdx; $i < count($parts); $i += 2) {
                $results[$parts[$i]] = $parts[$i+1];
            }

            $response = $this->_imsp->receive();

        } while (preg_match("/^\* ACL ADDRESSBOOK/", $response));

        // Hopefully we can receive an OK response here
        if ($response != 'OK') {
            // Some weird problem
            throw new Horde_Imsp_Exception('Did not receive the expected response from the server.');
        }
        $this->_imsp->_logger->debug("GETACL on $abook completed.");

        return $results;
    }

    /**
     * Deletes an ACL entry for an address book.
     *
     * @param string $abook Name of the address book.
     * @param string $ident Name of entry to remove acl for.
     *
     * @throws Horde_Imsp_Exception
     */
    function deleteACL($abook, $ident)
    {
        $this->_imsp->send('DELETEACL ADDRESSBOOK ', true, false);

        // Do we need literal for address book name?
        if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $abook)) {
            $biBook = sprintf("{%d}", strlen($abook));
            $this->_imsp->send($biBook, false, true, true);
        }
        $this->_imsp->send("$abook ", false, false);

        // Literal for ident name?
        if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $ident)) {
            $biIdent = sprintf("{%d}", strlen($ident));
            $this->_imsp->send($biIdent, false, true, true);
            $this->_imsp->send($ident, false, true);
        } else {
            $this->_imsp->send("\"$ident\"", false, true);
        }

        // Get results.
        $server_response = $this->_imsp->receive();
        switch ($server_response) {
        case 'NO':
            // Could not create abook.
            $this->_imsp->_logger->err('IMSP server is unable to perform your request.');
            throw new Horde_Imsp_Exception('IMSP server is unable to perform your request.');
        case 'BAD':
            $this->_imsp->_logger->err('The IMSP server did not understand your request.');
            throw new Horde_Imsp_Exception('The IMSP server did not understand your request.');
        case 'OK':
            $this->_imsp->_logger->debug("DELETED ACL for $ident on $abook");
        default:
            throw new Horde_Imsp_Exception('Did not receive the expected response from the server.');
        }
    }

    /**
     * Returns an ACL string containing the rights for the current user
     *
     * @param string $abook Name of address book to retrieve acl.
     *
     * @return mixed acl of current user.
     */
    public function myRights($abook)
    {
        $data = '';
        $this->_imsp->send('MYRIGHTS ADDRESSBOOK ', true, false);
        if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $abook)) {
            $biBook = sprintf("{%d}", strlen($abook));
            $this->_imsp->send($biBook, false, true, true);
        }
        $this->_imsp->send($abook, false, true);
        $server_response = $this->_imsp->receive();
        switch ($server_response) {
        case 'NO':
            // Could not create abook.
            $this->_imsp->_logger->err('IMSP server is unable to perform your request.');
            throw new Horde_Imsp_Exception('IMSP server is unable to perform your request.');
        case 'BAD':
            $this->_imsp->_logger->err('The IMSP server did not understand your request.');
            throw new Horde_Imsp_Exception('The IMSP server did not understand your request.');
        }

        if (!preg_match("/^\* MYRIGHTS ADDRESSBOOK/", $server_response)) {
            throw new Horde_Imsp_Exception('Did not receive the expected response from the server.');
        }

        // {} for the abook name?
        if (preg_match(Horde_Imsp_Client_Base::OCTET_COUNT, $server_response, $tempArray)) {
            $data = $this->_imsp->receiveStringLiteral($tempArray[2]);
            $server_response = $this->_imsp->receive();
        }

        $parts = explode(' ', $server_response);

        // Push the array if we had a {}
        if ($data) {
            array_unshift($parts, ' ', ' ', ' ', ' ');
        }

        // Quoted address book name?
        $numParts = count($parts);
        $name = $parts[3];
        $firstACLIdx = 4;
        $firstChar = substr($name, 0, 1);
        if ($firstChar == "\"") {
            for ($i = 4; $i < $numParts; $i++) {
                $lastChar = substr($parts[$i], strlen($parts[$i]) - 1, 1);
                $firstACLIdx++;
                if ($lastChar == "\"") {
                    break;
                }
            }
        }

        $acl = $parts[$firstACLIdx];
        $server_response = $this->_imsp->receive();

        if ($server_response != 'OK') {
            throw new Horde_Imsp_Exception('Did not receive the expected response from the server.');
        } else {
            $this->_imsp->_logger->debug("MYRIGHTS on $abook completed.");
            return $acl;
        }
    }

    /**
     * Parses a IMSP fetchaddress response text string into key-value pairs
     *
     * @param  string  $server_response The raw fetchaddress response.
     *
     * @return array   Address book entry information as key=>value pairs.
     */
    protected function _parseFetchAddressResponse($server_response)
    {
        $abook = '';
        if (!preg_match("/^\* FETCHADDRESS /", $server_response)) {
            $this->_imsp->_logger->err('Did not receive a FETCHADDRESS response from server.');
            throw new Horde_Imsp_Exception('Did not receive the expected response from the server.');
        }

        /* NOTES
         * Parse out the server response string
         *
         * After choping off the server command response tags and
         * explode()'ing the server_response string
         * the $parts array contains the chunks of the server returned data.
         *
         * The predifined 'name' field starts in $parts[1].
         * The server should return any single item of data
         * that contains spaces within it as a double quoted string.
         * So we can interpret the existence of a double quote at the beginning
         * of a chunk to mean that the next chunk(s) are part of
         * the same value.  A double quote at the end of a chunk signifies the
         * end of that value and the chunk following that can be interpreted
         * as a key name.
         *
         * We also need to watch for the server returning a {} response for the
         * value of the key as well. */

        // Was the address book name a  {}?
        if (preg_match("/(^\* FETCHADDRESS )({)([0-9]{1,})(\}$)/",
                       $server_response, $tempArray)) {
            $abook = $this->_imsp->receiveStringLiteral($tempArray[3]);
            $chopped_response = trim($this->_imsp->receive());
        } else {
            // Take off the stuff from the beginning of the response
            $chopped_response = trim(preg_replace("/^\* FETCHADDRESS /", '', $server_response));
        }

        $parts = explode(' ', $chopped_response);
        /* If addres book was sent as a {} then we must 'push' a blank
         * value to the start of this array so the rest of the routine
         * will work with the correct indexes. */
         if (!empty($abook)) {
            array_unshift($parts, ' ');
        }

        // Was the address book name quoted?
        $numOfParts = count($parts);
        $name = $parts[0];
        $firstNameIdx = 1;
        $firstChar = substr($name, 0, 1);
        if ($firstChar =="\"") {
            for ($i = 1; $i < $numOfParts; $i++) {
                $lastChar = substr($parts[$i], strlen($parts[$i]) - 1, 1);
                $firstNameIdx++;
                if ($lastChar == "\"") {
                    break;
                }
            }
        }

        // Now start working on the entry name
        $name = $parts[$firstNameIdx];
        $firstChar = substr($name,0,1);

        // Check to see if the first char of the name string is a double quote
        // so we know if we have to extract more of the name.
        if ($firstChar == "\"") {
            $name = ltrim($name, "\"");
            for ($i = $firstNameIdx + 1; $i < $numOfParts; $i++) {
                $name .=  ' ' . $parts[$i];
                $lastChar = substr($parts[$i], strlen($parts[$i]) - 1,1);
                if ($lastChar == "\"") {
                    $name = rtrim($name, "\"");
                    $nextKey = $i + 1;
                    break;
                }
            }

        // Check for {}
        } elseif (preg_match('/\{(\d+)\}/', $name, $matches)) {
            $name = $this->_imsp->receiveStringLiteral($matches[1]);
            $response=$this->_imsp->receive();
            $parts = explode(' ', $response);
            $numOfParts = count($parts);
            $nextKey = 0;
        } else {
            // If only one chunk for 'name' then we just have to point
            // to the next chunk in the array...which will hopefully
            // be '2'
            $nextKey = $firstNameIdx + 1;
        }

        $lastChar = '';
        $entry['name'] = $name;

        // Start parsing the rest of the response.
        for ($i = $nextKey; $i < $numOfParts; $i += 2) {
            $key = $parts[$i];
            /* Check for {} */
            if (@preg_match(Horde_Imsp_Client_Base::OCTET_COUNT, $parts[$i+1], $tempArray)) {
                $server_data = $this->_imsp->receiveStringLiteral($tempArray[2]);
                $entry[$key] = $server_data;

                /* Read any remaining data from the stream and reset
                 * the counter variables so the loop will continue
                 * correctly. Note we set $i  to -2 because it will
                 * be incremented by 2 before the loop will run again */
                $parts = $this->_imsp->getServerResponseChunks();
                $i = -2;
                $numOfParts = count($parts);
            } else {
                // Not a string literal response
                @$entry[$key] = $parts[$i + 1];
                 // Check to see if the value started with a double
                 // quote.  We also need to check if the last char is a
                 // quote to make sure we REALLY have to check the next
                 // elements for a closing quote.
                if ((@substr($parts[$i + 1], 0, 1) == '"') &&
                    (substr($parts[$i + 1],
                     strlen($parts[$i + 1]) - 1, 1) != '"')) {

                    do {
                        $nextElement = $parts[$i+2];

                        // Was this element the last one?
                        $lastChar = substr($nextElement, strlen($nextElement) - 1, 1);
                        $entry[$key] .= ' ' . $nextElement;

                        // NOW, we can check the lastChar.
                        if ($lastChar == '"') {
                            $done = true;
                            $i++;
                        } else {
                            // Check to see if the next element is the
                            // last one. If so, the do loop will terminate.
                            $done = false;
                            $lastChar = substr($parts[$i+3], strlen($parts[$i+3]) - 1,1);
                            $i++;
                        }
                    } while ($lastChar != '"');

                    // Do we need to add the final element, or were
                    // there only two total?
                    if (!$done) {
                        $nextElement = $parts[$i+2];
                        $entry[$key] .= ' ' . $nextElement;
                        $i++;
                    }

                    // Remove the quotes sent back to us from the server.
                    if (substr($entry[$key], 0, 1) == '"') {
                        $entry[$key] = substr($entry[$key], 1, strlen($entry[$key]) - 2);
                    }

                    if (substr($entry[$key], strlen($entry[$key]) - 1, 1) == '"') {
                        $entry[$key] = substr($entry[$key], 0, strlen($entry[$key]) - 2);
                    }
                } elseif ((@substr($parts[$i + 1], 0, 1) == '"') &&
                          (substr($parts[$i + 1], -1, 1) == '"')) {
                    // Remove the quotes sent back to us from the server.
                    if (substr($entry[$key], 0, 1) == '"') {
                        $entry[$key] = substr($entry[$key], 1, strlen($entry[$key]) - 2);
                    }

                    if (substr($entry[$key], -1, 1) == '"') {
                        $entry[$key] = substr($entry[$key], 0, strlen($entry[$key]) - 2);
                    }
                }
            }
        }

        return $entry;
    }

}
