[dev] Kolab Webclient: Kolab:: library

Stuart Bingë s.binge at codefusion.co.za
Wed Jan 28 01:29:33 PST 2004


Hello all,

This is the second release of the Kolab Webclient modifications to Horde. It's 
quite a major release, as it's an entirely new file that provides most of the 
common interface code between the various Kolab Horde drivers and the Kolab 
server itself.

It provides functionality that aids in interfacing with a Kolab server, such 
as IMAP abstraction, WebDAV abstraction (specifically to load & store the 
free/busy information on a Kolab server), as well as several smaller 
functions dealing with authentication, newline conversion, etc.

Currently we're placing this file in /lib folder of the 'horde' module, so to 
include it in our drivers we write:

require_once HORDE_BASE . '/lib/Kolab.php';

I don't think that this is a candidate for the 'framework' module, as the code 
relies quite heavily on other Horde code (i.e. it couldn't operate as a 
stand-alone module without quite a bit of rewriting), however I'm open to 
moving this around if the Horde developers have a more suitable location for 
it.

I'm expecting quite a bit of discussion over this before it's submitted into 
CVS - we're more than happy to modify the library to conform with any 
suggestions/guidelines that the developers specify. I've also tried to 
document the library as much as possible, so it should be (relatively) easy 
to see what the purpose is of each function & the arguments it expects.

Regards,

-- 
Stuart Bingë
Code Fusion cc. <http://www.codefusion.co.za/>
Tel: +27 11 391 1412
Mobile: +27 83 298 9727
Email: s.binge at codefusion.co.za
-------------- next part --------------
<?php

/**
 * The Kolab:: utility library provides various functions for dealing with a
 * Kolab server (i.e. functions that relate to Cyrus IMAP, WebDAV, etc.).
 *
 * Copyright 2003 Code Fusion, cc.
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
 *
 * Written by Stuart Bing? <s.binge at codefusion.co.za>
 *
 *
 * NOTE: Requires that the "Net_HTTP_Client" PEAR package be installed in the
 * HORDE_LIBS directory, which is usually the PHP PEAR directory
 * (e.g. /usr/lib/php/, or in the case of Kolab, /kolab/lib/php/).
 *
 * The library can be obtained from the following site:
 * <http://lwest.free.fr/doc/php/lib/index.php3?page=net_http_client&lang=en>.
 *
 * This library also requires a PHP installation with the 'gettext' and 'iconv'
 * extensions.
 */

require_once HORDE_BASE . '/lib/Horde.php';
require_once HORDE_BASE . '/lib/NLS.php';
require_once HORDE_LIBS . 'Net/HTTP/Client.php';
require_once HORDE_LIBS . 'Horde/iCalendar.php';

/**
 * The 'newline' character sequence used by Cyrus IMAP.
 */
define('CYRUS_NL', "\r\n");

/**
 * The Does Not Exist error message returned by the imap_last_error()
 * function if a specified mailbox does not exist on the IMAP server.
 */
define('ERR_MBOX_DNE', 'Mailbox does not exist');

/**
 * The name of an X-Header used in various messages to provide category
 * information for the relevant object (note, task, etc).
 */
define('X_HEAD_CAT', 'X-Horde-Category');

class Kolab {

/* ---------- GENERAL FUNCTIONS ---------- */

    /**
     * Convertes any newlines in the specified text to cyrus format.
     *
     * @access public
     *
     * @param string $text  The text to convert.
     *
     * @return string    $text with all newlines replaced by CRNL.
     */
    function cyrusNewlines($text)
    {
        return preg_replace("/\r\n|\n|\r/s", "\r\n", $text);
    }

    /**
     * Convertes any newlines in the specified text to unix format.
     *
     * @access public
     *
     * @param string $text  The text to convert.
     *
     * @return string    $text with all newlines replaced by NL.
     */
    function unixNewlines($text)
    {
        return preg_replace("/\r\n|\n|\r/s", "\n", $text);
    }

/* ---------- GENERAL KOLAB FUNCTIONS ---------- */

    /**
     * Strips any superfluos domain suffix from an email address. This was
     * done as Kolab uses 'name at maildomain' user names, whereas horde was
     * returning 'name at maildomain@hostname' addresses for the username.
     *
     * @access public
     *
     * @param string $address  The address to strip the domain from, of the
     *                         form 'a at b@c' where any/all of '@b' and '@c'
     *                         may be missing.
     *
     * @return string  A string of the form 'a at b'.
     */
    function stripKolabUsername($address)
    {
        return preg_replace('/^([^@]*(@[^@]*)?).*$/', "\$1", $address);
    }

    /**
     * Strips any superfluos domain suffix from an email address.
     *
     * @access public
     *
     * @param string $address  The address to strip the domain from, of the
     *                         form 'a at b@c' where any/all of '@b' and '@c'
     *                         may be missing.
     *
     * @return string  A string of the form 'a'.
     */
    function stripBaseUsername($address)
    {
        return preg_replace('/^([^@]*).*$/', "\$1", $address);
    }

    /**
     * Returns the username of the currently logged on Horde user, suitable
     * for use in other Kolab authentication procedures (assuming Horde is
     * using LDAP authentication against the Kolab server).
     *
     * @access public
     *
     * @return string  The current users login name.
     */
    function getUser()
    {
        return Kolab::stripKolabUsername(Auth::getAuth());
    }

    /**
     * Returns the password of the currently logged on Horde user, suitable
     * for use in other Kolab authentication procedures (assuming Horde is
     * using LDAP authentication against the Kolab server).
     *
     * @access public
     *
     * @return string  The current users login password.
     */
    function getPassword()
    {
        return Auth::getCredential('password');
    }

    /**
     * Returns the username and password of the currently logged on Horde user,
     * suitable for use in other Kolab authentication procedures (assuming
     * Horde is using LDAP authentication against the Kolab server).
     *
     * @access public
     *
     * @return array  An array of the form (username, password).
     */
    function getAuthentication()
    {
        return array(Kolab::getUser(), Kolab::getPassword());
    }

/* ---------- CYRUS/IMAP FUNCTIONS ---------- */

    /**
     * Returns an IMAP server address formatted for use in the PHP IMAP
     * functions.
     *
     * @access public
     *
     * @param string $host  The address of the IMAP server, of the form
     *                      'host:port'.
     *
     * @return string  An address string suitable for use in functions such as
     *                 imap_open().
     */
    function imapServerURI($host)
    {
        return '{' . $host . '/imap/notls}';
    }

    /**
     * Returns an address of a Cyrus mailbox, formatted for use in the PHP IMAP
     * functions.
     *
     * @access public
     *
     * @param string $host              The address of the Cyrus server.
     * @param optional string $mailbox  The name of the mailbox (defaults to
     *                                  the Inbox).
     *
     * @return string  A mailbox address string suitable for use in functions
     *                 such as imap_open().
     */
    function cyrusMailboxURI($host, $mailbox = '')
    {
        if (!empty($mailbox)) $mailbox = '/' . imap_utf7_encode($mailbox);
        return Kolab::imapServerURI($host) . "INBOX$mailbox";
    }

    /**
     * Tests if $error was the last IMAP error that was generated.
     *
     * @access public
     *
     * @param string $error  The error to test against.
     *
     * @return boolean  True if $error was the last IMAP error.
     */
    function testIMAPError($error)
    {
        return strcasecmp(imap_last_error(), $error) == 0;
    }

    /**
     * Returns an IMAP stream, connected to a specified Cyrus server, with a
     * specified mailbox opened (optionally creating the mailbox if it does
     * not exist). Raises a fatal Horde error if something goes wrong.
     *
     * @access public
     *
     * @param string  $server           The server to open the IMAP connection
     *                                  to.
     * @param string  $mailbox          The mailbox to open on $server.
     * @param optional boolean $create  True if $mailbox is to be created if it
     *                                  does not exist.
     *
     * @return resource  An open IMAP stream on success.
     */
    function openCyrusConnection($server, $mailbox, $create = true)
    {
        list($user, $pass) = Kolab::getAuthentication();
        $box = Kolab::cyrusMailboxURI($server, $mailbox);

        $imapstream = @imap_open($box, $user, $pass); // Firstly try a straight imap_open()

        if ($create && Kolab::testIMAPError(ERR_MBOX_DNE)) { // Box does not exist, try to create it
            if ($imapstream !== false) @imap_close($imapstream);

            $imapstream = @imap_open(Kolab::imapServerURI($server), $user, $pass, OP_HALFOPEN);
            if ($imapstream === false) return $imapstream;

            if (!@imap_createmailbox($imapstream, $box)) return false;

            $imapstream = @imap_reopen($box); // Successfully created the box, now try to open it
        }

        if ($imapstream === false)
            Horde::fatal(
                PEAR::raiseError(sprintf(_('Unable to open mailbox %s: ' . imap_last_error()), $box)),
                __FILE__,
                __LINE__
            );

        return $imapstream;
    }

    /**
     * Closes a specified IMAP stream, expunging all the messages flagged for
     * deletion.
     *
     * @access public
     *
     * @param resource $imapstream       A reference to an open IMAP stream,
     *                                   connected to the mailbox of interest.
     *                                   This is set to NULL if the connection
     *                                   was terminated successfully.
     * @param optional boolean $expunge  True to expunge the mailbox before
     *                                   closing.
     *
     * @return boolean  True if the stream was closed successfully.
     */
    function closeImapConnection(&$imapstream, $expunge = true)
    {
        $result = true;

        if (isset($imapstream)) {
            $result = @imap_close($imapstream, ($expunge ? CL_EXPUNGE : 0));
            if ($result) $imapstream = NULL;
        }

        return $result;
    }

    /**
     * Ensures that the specified IMAP connection is alive - reconnects
     * if the stream has been disconnected.
     *
     * @access public
     *
     * @param resource $imapstream      A reference to an open IMAP stream,
     *                                  connected to the mailbox of interest.
     * @param string  $server           The server to open the IMAP connection
     *                                  to.
     * @param string  $mailbox          The mailbox to open on $server.
     * @param optional boolean $create  True if $mailbox is to be created if it
     *                                  does not exist.
     *
     */
    function persistentCyrusConnection(&$imapstream, $server, $mailbox, $create = true)
    {
        if (!isset($imapstream) || !imap_ping($imapstream)) {
            if (isset($imapstream)) Kolab::closeImapConnection($imapstream);
            $imapstream = Kolab::openCyrusConnection($server, $mailbox, $create);
        }
    }

    /**
     * Returns a hash of the message headers of a specified message.
     *
     * @access public
     *
     * @param resource $imapstream  An open IMAP stream, connected to the
     *                              mailbox of interest.
     * @param integer $messageid    The message from which to read the headers.
     *
     * @return array  A hash of the headers, where each 'key => value' pair in
     *                the hash corresponds to a 'Name: Value' header line. This
     *                array is empty if an error occurs.
     */
    function getMessageHeaders($imapstream, $messageid)
    {
        $headers = array();
        $charset = NLS::getCharset();

        $headerlines = explode(CYRUS_NL, @imap_fetchheader($imapstream, $messageid, FT_UID));
        foreach ($headerlines as $headerline) {
            if (empty($headerline)) continue;

// // PHP5 only
//             list($hname, $hval) = explode(':', @iconv_mime_decode($headerline, 0, $charset), 2);
            list($hname, $hval) = explode(':', $headerline, 2);
            $headers[trim($hname)] = trim($hval);
        }

        return $headers;
    }

    /**
     * Returns the value of a header attribute in a hash returned by
     * Kolab::getHeaderHash(), or a specified default value if the header
     * attribute does not exist.
     *
     * @access public
     *
     * @param array $headers           The message header hash.
     * @param string $name             The attribute to search for.
     * @param optional mixed $default  The value to return if $name does not
     *                                 exist in $headers.
     *
     * @return mixed    The value of $default.
     */
    function getHeaderValue(&$headers, $name, $default = '')
    {
        return array_key_exists($name, $headers) ? $headers[$name] : $default;
    }

    /**
     * Returns the body of a specified mail message.
     *
     * @access public
     *
     * @param resource $imapstream       An open IMAP stream, connected to the
     *                                   mailbox of interest.
     * @param integer $messageid         The message to read.
     * @param optional boolean $convert  True to convert the body from UTF-8 to
     *                                   the current users character set.
     *
     * @return mixed  (string)  The body of mail message $messageid.
     *                (boolean) False on error.
     */
    function getMessageBody($imapstream, $messageid, $convert = false)
    {
        $body = @imap_body($imapstream, $messageid, FT_UID);
        return ($convert ? iconv('UTF-8', NLS::getCharset(), $body) : $body);
    }

    /**
     * Returns a list of messages in the specified mailbox, sorted by date.
     *
     * @access public
     *
     * @param resource $imapstream  An open IMAP stream, connected to the
     *                              mailbox of interest.
     *
     * @return array    A list of the messages in the mailbox opened on
     *                  $imapstream, sorted by date.
     */
    function getMessages($imapstream)
    {
        return @imap_sort($imapstream, SORTDATE, 0, SE_UID);
    }

    /**
     * Finds a set of messages in the specified mailbox that match the specified
     * criteria.
     *
     * @access public
     *
     * @param resource $imapstream  An open IMAP stream, connected to the
     *                              mailbox of interest.
     * @param string $criteria      The search criteria (see imap_search() for
     *                              the format of this string).
     *
     * @return mixed   (array) A list of the messages in the mailbox opened on
     *                         $imapstream that match $criteria.
     *                 (boolean) False on failure.
     */
    function findMessages($imapstream, $criteria)
    {
        return @imap_search($imapstream, $criteria, SE_UID);
    }

    /**
     * Adds a message to the specified mailbox.
     *
     * @access public
     *
     * @param resource $imapstream  An open IMAP stream, connected to the
     *                              mailbox of interest.
     * @param string $mailbox       The cyrusMailboxURI() formatted name of the
     *                              mailbox to add the message to.
     * @param string $conttype      The mime content type of the message.
     * @param string $subject       The message subject.
     * @param string $body          The message body. Newlines are automatically
     *                              converted to the corrent type.
     * @param optional string $ua   The user agent which is adding the message.
     * @param optional array $headers A hash containing any extra headers to
     *                              append. Each 'key => value' pair is written
     *                              as 'key: value' in the message header.
     * @param optional boolean $convert True to convert the message body to
     *                              UTF-8 before adding the message.
     *
     * @return mixed    (boolean) True on success.
     *                  (object)  PEAR_Error on failure.
     */
    function addMessage($imapstream, $mailbox, $conttype, $subject, $body,
                        $ua = '', $headers = array(), $convert = false)
    {
        $user = Kolab::getUser();

// // This block only applies to PHP5, as it is the first version of PHP to
// // support the iconv_mime_encode() function.
//         $iconvprefs = array(
//             'scheme'            => 'Q',
//             'input-charset'     => NLS::getCharset(),
//             'output-charset'    => 'UTF-8',
//             'line-length'       => 76,
//             'line-break-chars'  => CYRUS_NL
//         );
//         $msg = iconv_mime_encode('Content-Type',  $conttype . '; Charset="UTF-8"', $iconvprefs) . CYRUS_NL;
//         $msg .= iconv_mime_encode('From',  $user, $iconvprefs) . CYRUS_NL;
//         $msg .= iconv_mime_encode('Reply-To',  '', $iconvprefs) . CYRUS_NL;
//         $msg .= iconv_mime_encode('To',  $user, $iconvprefs) . CYRUS_NL;
//         $msg .= iconv_mime_encode('User-Agent',  'Horde' . (empty($ua) ? '' : '/' . $ua) . '/Kolab', $iconvprefs) . CYRUS_NL;
//         $msg .= iconv_mime_encode('Date',  date('r'), $iconvprefs) . CYRUS_NL;
//         $msg .= iconv_mime_encode('Subject',  $subject, $iconvprefs) . CYRUS_NL;

        foreach ($headers as $key => $value)
            $msg .= iconv_mime_encode($key, $value, $iconvprefs) . CYRUS_NL;

        $msg  = 'Content-Type: ' . $conttype . ($convert ? '; Charset="UTF-8"' : '') . CYRUS_NL;
        $msg .= 'From: ' .  $user . CYRUS_NL;
        $msg .= 'Reply-To: ' .  '' . CYRUS_NL;
        $msg .= 'To: ' .  $user . CYRUS_NL;
        $msg .= 'User-Agent: ' .  'Horde' . (empty($ua) ? '' : '/' . $ua) . '/Kolab' . CYRUS_NL;
        $msg .= 'Date: ' .  date('r') . CYRUS_NL;
        $msg .= 'Subject: ' .  $subject . CYRUS_NL;

        foreach ($headers as $key => $value)
            $msg .= $key . ': ' . $value . CYRUS_NL;

        if ($convert) {
            $body = iconv(NLS::getCharset(), 'UTF-8', $body);
            if ($body === false)
                return PEAR::raiseError(
                    sprintf(
                        _('Unable to add message from %s to mailbox %s: An error was encountered while encoding the message body'),
                        $user,
                        $mailbox
                    )
                );
        }

        $msg .= CYRUS_NL . Kolab::cyrusNewlines($body);

        if (!@imap_append($imapstream, $mailbox, $msg))
            return PEAR::raiseError(
                sprintf(
                    _('Unable to add message from %s to mailbox %s: ' . imap_last_error()),
                    $user,
                    $mailbox
                )
            );

        return true;
    }

    /**
     * Deletes a message in the specified mailbox.
     *
     * @access public
     *
     * @param resource $imapstream       An open IMAP stream, connected to the
     *                                   mailbox of interest.
     * @param integer $messageid         The message to delete.
     * @param optional boolean $expunge  True to expunge the mailbox after
     *                                   deletion.
     */
    function deleteMessage($imapstream, $messageid, $expunge = false)
    {
        @imap_delete($imapstream, $messageid, FT_UID);
        if ($expunge) @imap_expunge($imapstream);
    }

/* ---------- WEBDAV FUNCTIONS ---------- */

    /**
     * Retrieves the contents of a specified users VFB file, stored on a
     * specified WebDAV server.
     *
     * @access public
     *
     * @param string $server         The address of the WebDAV server, of the
     *                               form host:port.
     * @param string $folder         The folder on $server where the VFB file is
     *                               stored.
     * @param optional string $user  The name of the user whose VFB file is to
     *                               be retrieved. Defaults to the current user.
     *
     * @return mixed  (string) The contents of the users VFB file, suitable for
     *                    parsing by a Horde_iCalendar object.
     *                (object) PEAR_Error on failure.
     */
    function retrieveFreeBusy($server, $folder, $user = '')
    {
        list($uname, $pass) = Kolab::getAuthentication();
        if (empty($user)) $user = $uname;

        $http = new Net_HTTP_Client();
        list($host, $port) = explode(':', $server);
        if (empty($port)) $port = 80;
        if (!$http->Connect($host, $port))
            return PEAR::raiseError(sprintf(
                _('Unable to retrieve free/busy information for user %s on server %s: '
                . $http->getStatusMessage()),
                $user,
                $server
            ));
        $http->setCredentials($uname, $pass);

        $status = $http->Get($folder . '/' . $user . '.vfb');
        if ($status != 200) {
            // Try `user' instead of `user at domain', for backward compatibility
            $status = $http->Get($folder . '/' . Kolab::stripBaseUsername($user) . '.vfb');
            if ($status != 200)
                return PEAR::raiseError(sprintf(
                    _('Unable to retrieve free/busy information for user %s on server %s: '
                    . $http->getStatusMessage()),
                    $user,
                    $server
                ));
        }
        $vfb = $http->getBody();

        $http->Disconnect();

        return $vfb;
    }

    /**
     * Stores the specified VFB data in the current users VFB file on the
     * specified WebDAV server.
     *
     * @access public
     *
     * @param string $server  The address of the WebDAV server, of the form
     *                        host:port.
     * @param string $folder  The folder on $server where the VFB file is stored.
     * @param string $vfb     The new VFB data to store.
     *
     * @return mixed  (boolean) True on success.
     *                (object)  PEAR_Error on failure.
     */
    function storeFreeBusy($server, $folder, $vfb)
    {
        list($user, $pass) = Kolab::getAuthentication();

        $http = new Net_HTTP_Client();
        if (strpos($server, ':') !== false) {
            list($host, $port) = explode(':', $server);
        } else {
            $host = $server;
            $port = 80;
        }
        if (!$http->Connect($host, $port))
            return PEAR::raiseError(sprintf(
                _('Unable to store free/busy information for user %s on server %s: '
                . $http->getStatusMessage()),
                $user,
                $server
            ));

        $http->setCredentials($user, $pass);

        $status = $http->Put($folder . '/' . $user . '.vfb', $vfb);
        if ($status != 200)
            return PEAR::raiseError(sprintf(
                _('Unable to store free/busy information for user %s on server %s: '
                . $http->getStatusMessage()),
                $user,
                $server
            ));

        $http->Disconnect();

        return true;
    }

    /**
     * Retrieves the value of a specified attribute in a Horde_iCalendar object,
     * or a default value if the attribute does not exist.
     *
     * @access private
     *
     * @param Horde_iCalendar* $component  The component of interest.
     * @param string $attribute            The attribute of interest.
     * @param mixed $default               The value to return if $attribute
     *                                     does not exist.
     *
     * @return mixed  (string) The value of $attribute.
     *                (mixed) $default if $attribute does not exist.
     */
    function _getAttr(&$component, $attribute, $default = '')
    {
        $tmp = $component->getAttribute($attribute);
        if (is_a($tmp, 'PEAR_Error')) return $default;
        return $tmp;
    }

    /**
     * Compiles and uploads a free/busy VFB file for the current user. The busy
     * times are obtained from reading iCalendar events in the users calendar
     * folder. By default the VFB file spans 8 weeks, starting at the time when
     * the function is called.
     *
     * @access private
     *
     * @param optional string $calserver  The server address where the calendar
     *                                    folder is located.
     * @param optional string $calfolder  The name of the calendar folder.
     * @param optional string $vfbserver  The server address where the free/busy
     *                                    folder is located.
     * @param optional string $vfbfolder  The name of the free/busy folder.
     * @param optional integer $period    The period (in seconds) from which
     *                                    busy information should be drawn from.
     *                                    This period begins when the function
     *                                    is called.
     *
     * @return mixed  (boolean) True on success.
     *                (object)  PEAR_Error on failure.
     */
    function compileFreeBusy($calserver = 'localhost', $calfolder = 'Calendar',
                             $vfbserver = 'localhost', $vfbfolder = '/freebusy/',
                             $period = 4838400)
    {
        $imap = Kolab::openCyrusConnection($calserver, $calfolder);

        $vfbcont = new Horde_iCalendar;
        $vfb =& $vfbcont->newComponent('VFREEBUSY', $vfbcont);

        $cal = new Horde_iCalendar;
        $cal->setAttribute('ORGANIZER', 'MAILTO:' . Kolab::getUser());

        $vfbstart = time();
        $vfbend = $vfbstart + $period;

        $vfb->setAttribute('DTSTART', $vfbstart);
        $vfb->setAttribute('DTEND', $vfbend);

        $msgs = Kolab::getMessages($imap);
        foreach ($msgs as $msg) {
            $cal->parsevCalendar(Kolab::getMessageBody($imap, $msg));

            $components = $cal->getComponents();
            $ispresent = false;

            foreach ($components as $c)
            {
                if ($ispresent) break;
                if (!is_a($c, 'Horde_iCalendar_vevent')) continue;
                $ispresent = true;

                $start = Kolab::_getAttr($c, 'DTSTART', 0);
                $end = Kolab::_getAttr($c, 'DTEND', 0);

                if ($start > $vfbend || $end < $vfbstart) continue;

                $start = max($start, $vfbstart);
                $end = min($end, $vfbend);

                $vfb->addBusyPeriod('BUSY', $start, $end);
            }
        }

        Kolab::closeImapConnection($imap);

        $vfb->simplify();
        $vfbcont->addComponent($vfb);
        $vfbfile = $vfbcont->exportvCalendar();

        return Kolab::storeFreeBusy($vfbserver, $vfbfolder, $vfbfile);
    }

}


More information about the dev mailing list