<?php

/* Constants for mailboxElt attributes. Start at 32 to allow for
   existing LATT_* constants. */
/** @const IMP_TREEELT_HAS_CHILDREN */  define('IMP_TREEELT_HAS_CHILDREN', 32);
/** @const IMP_TREEELT_IS_DISCOVERED */ define('IMP_TREEELT_IS_DISCOVERED', 64);
/** @const IMP_TREEELT_IS_OPEN */       define('IMP_TREEELT_IS_OPEN', 128);

/**
 * The IMP_tree class provides a tree view of the folders in an
 * IMAP repository.  It provides access functions to iterate
 * through this tree and query information about individual
 * mailboxes.
 *
 * $Horde: imp/lib/Tree.php,v 1.73 2003/07/25 08:17:13 slusarz Exp $
 *
 * Copyright 2000-2003 Chuck Hagenbuch <chuck@horde.org>
 * Copyright 2000-2003 Jon Parise <jon@horde.org>
 * Copyright 2000-2003 Anil Madhavapeddy <avsm@horde.org>
 *
 * See the enclosed file COPYING for license information (GPL). If you
 * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
 *
 * @author  Chuck Hagenbuch <chuck@horde.org>
 * @author  Jon Parise <jon@horde.org>
 * @author  Anil Madhavapeddy <avsm@horde.org>
 * @version $Revision: 1.73 $
 * @since   IMP 2.3
 * @package imp
 */
class IMP_Tree {

    /**
     * Associative array containing the mailbox tree.
     *
     * @var array $_tree
     */
    var $_tree;

    /**
     * Key of current element in the tree.
     *
     * @var string $_cur
     */
    var $_cur = '';

    /**
     * Key of first (top?) element in the tree (for traversal).
     *
     * @var string $_first
     */
    var $_first = '';

    /**
     * Hierarchy delimiter.
     *
     * @var string $_delimiter
     */
    var $_delimiter = '/';

    /**
     * Default mailbox listing command.
     *
     * @var string $_listcmd
     */
    var $_listcmd = 'imap_getmailboxes';

    /**
     * Where we start listing folders.
     *
     * @var string $_prefix
     */
    var $_prefix = '';

    /**
     * Show files beginning with a '.'?
     *
     * @var boolean $_dotfiles
     */
    var $_dotfiles = true;

    /**
     * Any additional hierarchies to show, such as UW's '#shared/'.
     *
     * @var array $_hierarchies
     */
    var $_hierarchies = array();

    /**
     * TODO
     *
     * @var string $_server
     */
    var $_server = '';


    /**
     * Constructor.
     *
     * @access public
     *
     * @param optional boolean $dotfiles   Show dotfiles?
     * @param optional array $hierarchies  Any additional hierarchies to show.
     */
    function IMP_Tree($dotfiles = true, $hierarchies = array())
    {
        $this->_dotfiles = $dotfiles;
        if (is_array($hierarchies)) {
            $this->_hierarchies = $hierarchies;
        }
    }

    /**
     * Update the cached folder information such as total and unread messages
     * It acts on the current folder only.
     *
     * @access public
     */
    function updateMessageInfo()
    {
        global $imp;

        if (isset($this->_tree) &&
            is_array($this->_tree) &&
            !empty($this->_cur)) {
            $sts = @imap_status($imp['stream'], $this->_cur, SA_MESSAGES | SA_UNSEEN);
            $this->_tree[$this->_cur]['messages'] = $sts->messages;
            if (isset($this->_tree[$this->_cur]['unseen'])) {
                $this->_tree[$this->_cur]['newmsg'] = $sts->unseen - $this->_tree[$this->_cur]['unseen'];
            }
            $this->_tree[$this->_cur]['unseen'] = isset($sts->unseen) ? $sts->unseen : 0;
        }
    }

    /**
     * Flush the cached folder information such as total and unread messages
     * to reset the cache.  It acts on the current folder only.
     *
     * @access public
     */
    function flushMessageInfo()
    {
        if (isset($this->_tree) &&
            is_array($this->_tree) &&
            !empty($this->_cur)) {
            foreach (array('messages', 'unseen', 'newmsg') as $field) {
                unset($this->_tree[$this->_cur][$field]);
            }
        }
    }

    /**
     * Iterate through the folder tree and expand every folder that has
     * children. This function resets the folder pointer, so do not use it
     * in the middle of another iteration of the same IMAP_folder object.
     *
     * @access public
     */
    function expandAllFolders()
    {
        $mailbox = $this->head();
        while (isset($mailbox) && is_array($mailbox)) {
            if ($this->hasChildren($mailbox) && !$this->isOpen($mailbox)) {
                $this->expand($this->_server . $mailbox['value']);
            }
            $mailbox = $this->next();
        }
    }

    /**
     * Iterate through the folder tree and expand every folder that has
     * children and is marked to be expanded in the preferences.
     * This function resets the folder pointer, so do not use it in the
     * middle of another iteration of the same IMAP_folder object
     *
     * @access public
     */
    function expandAsLast()
    {
        global $prefs;

        $expanded = unserialize($prefs->getValue('expanded_folders'));
        $mailbox = $this->head();
        while (isset($mailbox) && is_array($mailbox)) {
            if ($this->hasChildren($mailbox) &&
                !$this->isOpen($mailbox) &&
                isset($expanded[($this->_server . $mailbox['value'])])) {
                $this->expand($this->_server . $mailbox['value']);
            }
            $mailbox = $this->next();
        }
    }

    /**
     * Iterate through and expand every folder below a given starting
     * point.
     *
     * @access public
     *
     * @param string $root  TODO
     */
    function expandMailbox($root)
    {
        if (!isset($this->_tree[$root])) {
            return false;
        }

        $oldcur = $this->_cur;
        $this->_cur = $root;

        $mailbox = $this->_tree[$root];
        $startlevel = $mailbox['level'];
        if (!$this->hasChildren($mailbox)) {
            $this->_cur = $oldcur;
            return;
        }
        if (!$this->isOpen($mailbox)) {
            $this->expand($this->_server . $mailbox['value']);
        }
        $mailbox = $this->next();
        while (isset($mailbox) &&
               is_array($mailbox) &&
               ($mailbox['level'] > $startlevel)) {
            if ($this->hasChildren($mailbox) && !$this->isOpen($mailbox)) {
                $this->expand($this->_server . $mailbox['value']);
            }
            $mailbox = $this->next();
        }

        $this->_cur = $oldcur;
    }

    /**
     * Iterate through the folder tree and collapse every folder that has
     * children.  However, since each folder might not have been discovered,
     * it first expands them all.  This is a hack to get it working; to do
     * it properly it should check for the discovered bit, and if it is not
     * discovered, then ignore that heirarchy of folders.
     *
     * @access public
     */
    function collapseAllFolders()
    {
        // This is a hack to ensure all folders are discovered, and opened.
        // ought to be corrected with proper discovery checking.
        $this->expandAllFolders();

        $mailbox = $this->head();
        while (isset($mailbox) && is_array($mailbox)) {
            if (($this->_prefix != '') && ($mailbox['level'] == 0)) {
                /* We have a folder prefix, don't collapse the first level. */
            } elseif ($this->hasChildren($mailbox)) {
                $this->collapse($this->_server . $mailbox['value']);
            }
            $mailbox = $this->next();
        }
    }

    /**
     * TODO
     *
     * @access public
     *
     * @param string $path  TODO
     *
     * @return array  TODO
     */
    function getList($path)
    {
        global $imp;

        $newboxes = call_user_func($this->_listcmd, $imp['stream'], $this->_server, $path);

        $unique = array();
        if (is_array($newboxes)) {
            $found = array();
            foreach ($newboxes as $box) {
                if (!empty($box->name) && empty($found[$box->name])) {
                    $unique[$box->name] = $box;
                    $found[$box->name] = true;
                }
            }
        }

        return $unique;
    }

    /**
     * Get information about a specific mailbox.
     *
     * @access public
     *
     * @param string $path  The mailbox to query.
     *
     * @return stdClass  See imap_getmailboxes().
     */
    function getMailbox($path)
    {
        global $imp;

        $box = @imap_getmailboxes($imp['stream'], $this->_server, $path);
        return $box[0];
    }

    /**
     * TODO
     *
     * @access public
     *
     * @param string $name                  TODO
     * @param array $attributes             TODO
     * @param string $delimiter             TODO
     * @param optional integer $level       TODO
     * @param optional boolean $children    TODO
     * @param optional boolean $discovered  TODO
     * @param optional boolean $open        TODO
     * @param optional string $next         TODO
     *
     * @return TODO  TODO
     */
    function makeMailboxTreeElt($name, $attributes, $delimiter, $level = 0,
                                $children = false, $discovered = false,
                                $open = false, $next = '')
    {
        $elt['level'] = $level;
        $elt['attributes'] = $attributes;
        $this->setChildren($elt, $children);
        $this->setDiscovered($elt, $discovered);
        $this->setOpen($elt, $open);
        $elt['next'] = $next;

        // computed values
        preg_match('|^{[^}]*}(.*)|', $name, $regs);
        $elt['value'] = $regs[1];
        if (!empty($delimiter)) {
            $tmp = explode($delimiter, $elt['value']);
            $elt['label'] = $tmp[count($tmp) - 1];
        } else {
            $elt['label'] = $elt['value'];
        }
        $elt['label'] = String::convertCharset($elt['label'], 'UTF7-IMAP');

        return $elt;
    }

    /**
     * Initalize the list at the top level of the hierarchy.
     *
     * @access public
     *
     * @param optional boolean $subscribed  TODO
     */
    function init($subscribed = false)
    {
        global $imp;

        // reset all class variables to the defaults
        if ($subscribed) {
            $this->_listcmd = 'imap_getsubscribed';
        } else {
            $this->_listcmd = 'imap_getmailboxes';
        }

        $this->_prefix = $imp['folders'];
        $this->_tree = array();
        $this->_cur = '';
        $this->_first = '';
        $this->_delimiter = '/';
        $this->_server = IMP::serverString();

        // this *should* always get the right delimiter
        $tmp = $this->getMailbox('INBOX');

        // Don't overwrite the default with an empty delimiter; a
        // guess is better than nothing.
        if (!empty($tmp->delimiter)) {
            $this->_delimiter = $tmp->delimiter;
        }

        preg_match('|^({[^}]*})(.*)|', $tmp->name, $regs);
        $this->_server = $regs[1];

        if ($this->_prefix == '') {
            $boxes = $this->getList('%');
            if (!isset($boxes[($this->_server . 'INBOX')])) {
                $boxes[$this->_server . 'INBOX'] = $this->getMailbox('INBOX');
            }

            $nextlevel = $this->getList("%$this->_delimiter%");
        } else {
            $boxes[$this->_server . 'INBOX'] = $this->getMailbox('INBOX');

            // make sure that the name does NOT have a trailing
            // delimiter; otherwise we won't be able to find it when
            // we need to use it as a parent object.
            $tmp = $this->getMailbox($this->noTrailingDelimiter($this->_prefix));
            $tmp->name = $this->noTrailingDelimiter($tmp->name);

            // don't re-insert the INBOX element for Cyrus,
            // Courier-imapd, etc.
            if (!isset($boxes[$tmp->name])) {
                if (empty($tmp->name)) {
                    $tmp = new stdClass;
                    $tmp->name = $this->_server . $this->noTrailingDelimiter($this->_prefix);
                    $tmp->attributes = 0;
                    $tmp->delimiter = '';
                }
                $boxes[$this->_server . $this->_prefix] = $tmp;
            }

            $nextlevel = $this->getList($this->_prefix . '%');
        }

        // If we have any additional hierarchies, add them in.
        foreach ($this->_hierarchies as $hierarchy) {
            $tmp = $this->getMailbox($hierarchy);
            if ($tmp) {
                $tmp->name = $this->noTrailingDelimiter($tmp->name);
                $boxes[$tmp->name] = $tmp;
            }
        }

        $this->feedList($boxes);
        if (is_array($nextlevel) && count($nextlevel) > 0) {
            $this->addLevel($nextlevel);
        }

        // If we have a folder prefix, automatically expand the first
        // level under it.
        if ($this->_prefix != '') {
            $this->expand($this->_server . $this->noTrailingDelimiter($this->_prefix));
        }

        // If there are hierarchies, add the level under them.
        foreach ($this->_hierarchies as $hierarchy) {
            if (isset($this->_tree[$this->_server . $this->noTrailingDelimiter($hierarchy)])) {
                $nextlevel = $this->getList($this->noTrailingDelimiter($hierarchy) . $this->_delimiter . '%');
                if (is_array($nextlevel) && count($nextlevel) > 0) {
                    $this->addLevel($nextlevel);
                }
            }
        }
    }

    /**
     * TODO
     *
     * @access public
     *
     * @param string $mailbox  TODO
     *
     * @return string  TODO
     */
    function noTrailingDelimiter($mailbox)
    {
        if (substr($mailbox, -1) == $this->_delimiter) {
            $mailbox = String::substr($mailbox, 0, String::length($mailbox) - 1);
        }
        return $mailbox;
    }

    /**
     * Hierarchy sorting function.
     *
     * @access public
     *
     * @param array $list  TODO
     *
     * @return array  TODO
     */
    function hsort($list)
    {
        usort($list, array($this, 'tree_cmp'));
        return $list;
    }

    /**
     * TODO
     *
     * @access public
     *
     * @param TODO $a  TODO
     * @param TODO $b  TODO
     *
     * @return integer  TODO
     */
    function tree_cmp($a, $b)
    {
        $aname = preg_replace('|^{[^}]*}|', '', $a->name);
        $bname = preg_replace('|^{[^}]*}|', '', $b->name);

        // always return INBOX as "smaller"
        if ($aname == 'INBOX') {
            return -1;
        } else if ($bname == 'INBOX') {
            return 1;
        }

        $a_parts = explode($this->_delimiter, $aname);
        $b_parts = explode($this->_delimiter, $bname);

        $iMax = min(count($a_parts), count($b_parts));
        for ($i = 0; $i < $iMax; $i++) {
            if ($a_parts[$i] != $b_parts[$i]) {
                return strnatcasecmp($a_parts[$i], $b_parts[$i]);
            }
        }

        return count($a_parts) - count($b_parts);
    }



    /**
     * Return a serialized version of the tree.
     *
     * @access public
     *
     * @return array  A serialized version of the tree.
     */
    function pickle()
    {
        $pickle = array();
        $pickle['tree'] = $this->_tree;
        $pickle['cur'] = $this->_cur;
        $pickle['first'] = $this->_first;
        $pickle['delimiter'] = $this->_delimiter;
        $pickle['listcmd'] = $this->_listcmd;
        $pickle['prefix'] = $this->_prefix;
        $pickle['server'] = $this->_server;

        return $pickle;
    }

    /**
     * Rebuild oneself from a serialized string.
     *
     * @access public
     *
     * @param array $pickle  A serialized version of the tree.
     */
    function unpickle($pickle)
    {
        $this->_tree = $pickle['tree'];
        $this->_cur = $pickle['cur'];
        $this->_first = $pickle['first'];
        $this->_delimiter = $pickle['delimiter'];
        $this->_listcmd = $pickle['listcmd'];
        $this->_prefix = $pickle['prefix'];
        $this->_server = $pickle['server'];
    }

    /**
     * Expand function. TODO
     *
     * @access public
     *
     * @param string $folder  TODO
     */
    function expand($folder)
    {
        global $prefs;

        if (!isset($this->_tree[$folder])) {
            return;
        }

        $this->setOpen($this->_tree[$folder], true);

        // merge in next level of information if we don't already have it
        if (!$this->isDiscovered($this->_tree[$folder])) {
            $newlevel = $this->getList($folder . $this->_delimiter . '%' . $this->_delimiter . '%');
            if ($newlevel) {
                $this->addLevel($newlevel);
            }
            $this->setDiscovered($this->_tree[$folder], true);
        }

        $serialized = $prefs->getValue('expanded_folders');
        if ($serialized) {
            $expanded = unserialize($serialized);
        } else {
            $expanded = array();
        }
        $expanded[$folder] = true;
        $prefs->setValue('expanded_folders', serialize($expanded));
    }

    /**
     * Collapse function. TODO
     *
     * @access public
     *
     * @param string $folder  TODO
     */
    function collapse($folder)
    {
        global $prefs;

        if (!isset($this->_tree[$folder])) {
            $this->init();
            if (!isset($this->_tree[$folder])) {
                return;
            }
        }
        $this->setOpen($this->_tree[$folder], false);

        $serialized = $prefs->getValue('expanded_folders');
        if ($serialized) {
            $expanded = unserialize($serialized);
        } else {
            $expanded = array();
        }
        unset($expanded[$folder]);
        $prefs->setValue('expanded_folders', serialize($expanded));
    }

    /**
     * Sets the internal array pointer to the next element, and returns the
     * next object.
     *
     * @access public
     *
     * @return mixed  TODO
     */
    function next()
    {
        if (empty($this->_cur)) {
            $this->_cur = $this->_first;
        } else {
            $old = $this->_tree[$this->_cur];
            if (empty($old['next'])) {
                return false;
            }
            $this->_cur = $old['next'];
        }

        return $this->current();
    }

    /**
     * TODO
     *
     * @access public
     *
     * @return mixed  TODO
     */
    function head()
    {
        $this->_cur = $this->_first;
        if (isset($this->_tree[$this->_cur])) {
            return $this->_tree[$this->_cur];
        } else {
            return false;
        }
    }

    /**
     * TODO
     *
     * @access public
     */
    function reset()
    {
        $this->_cur = $this->_first;
    }

    /**
     * Return the next element after $this->_cur that is on the save level as
     * $this->_cur OR a higher level (0 == highest). So you don't fall off a
     * cliff if you should be dropping back up to the next level.
     *
     * @access public
     *
     * @return mixed  TODO
     */
    function nextOnLevel()
    {
        $elt = $this->_tree[$this->_cur];
        $level = $elt['level'];
        $next = $this->next();
        if (!is_array($next)) {
            return false;
        }
        while ($next['level'] > $level) {
            $next = $this->next();
            if (!is_array($next)) {
                return false;
            }
        }
        return $this->current();
    }

    /**
     * Return the current object.
     *
     * @access public
     *
     * @return mixed  TODO
     */
    function current()
    {
        if (isset($this->_tree[$this->_cur]) &&
            is_array($this->_tree[$this->_cur])) {
            return $this->_tree[$this->_cur];
        } else {
            return false;
        }
    }

    /**
     * Insert an element, and return the key it was inserted as.
     * NB: This does not take care of updating any next pointers!
     * Use insertInto() or update them manually.
     * ALSO NB: $elt must be an object that has a "name" field.
     *
     * @access public
     *
     * @param TODO $elt  TODO
     * @param TODO $id   TODO
     *
     * @return TODO  TODO
     */
    function insert($elt, $id)
    {
        $this->_tree[$id] = $elt;
        return $id;
    }

    /**
     * TODO
     *
     * @access public
     *
     * @param TODO $id  TODO
     *
     * @return boolean  TODO
     */
    function delete($id)
    {
        if (!isset($this->_tree[$id])) {
            return false;
        }
        $elt = $this->_tree[$id];
        $tmp = explode($this->_delimiter, $elt['value']);
        if (count($tmp) == 1) {
            $pkey = $this->_first;
        } else {
            $pkey = $this->_server . String::substr($elt['value'], 0, String::length($elt['value']) - String::length($tmp[count($tmp) - 1]) - 1);
        }

        // find the element before $elt
        $cid = $pkey;

        // Make sure the folder we are deleting exists in the tree
        if (!isset($this->_tree[$cid])) {
            // raise message here?
            return;
        }

        $cur = $this->_tree[$cid];
        $level = $cur['level'];
        while ($cur['next'] != $id && $cid != '' && $cur['level'] >= $level) {
            $cid = $cur['next'];
            $cur = $this->_tree[$cid];
        }

        if ($cur['next'] == $id) {
            $cur['next'] = $elt['next'];
            $this->_tree[$cid] = $cur;
            unset($this->_tree[$id]);
            return true;
        } else {
            return false;
        }
    }

    /**
     * TODO
     *
     * @access public
     *
     * @param TODO $elt  TODO
     * @param TODO $id   TODO
     */
    function insertInto($elt, $id)
    {
        $tmp = explode($this->_delimiter, $elt['value']);
        if (count($tmp) == 1) {
            $pkey = $this->_first;
        } else {
            $pkey = $this->_server . String::substr($elt['value'], 0, String::length($elt['value']) - String::length($tmp[count($tmp) - 1]) - 1);
        }

        $parent = $this->_tree[$pkey];
        $cid = $parent['next'];
        $cur = $this->_tree[$cid];
        if ($this->tree_cmp($elt, $cur) == -1) {
            $cid = $pkey;
            $cur = $parent;
            $elt['level'] = $cur['level'] + 1;
        } else {
            $level = $cur['level'];
            while ($cur['level'] == $level && $this->tree_cmp($elt, $cur) > 0) {
                $prev = $cid;
                $cid = $cur['next'];
                $cur = $this->_tree[$cid];
            }
            $cid = $prev;
            $cur = $this->_tree[$cid];
            /* Insert on the same level. */
            $elt['level'] = $cur['level'];
        }

        /* Save the next; this will be the next of the inserted element. */
        $tmp = $cur['next'];

        /* Stick the new element into the tree. */
        $nid = $this->insert($elt, $id);

        /* Have the element we found point to it. */
        $cur['next'] = $nid;

        /* Make sure that element is current in the tree. */
        $this->_tree[$cid] = $cur;

        /* Set the new elements next to cur's old next, closing the loop. */
        $elt['next'] = $tmp;

        /* Update the new element in the tree. */
        $this->_tree[$nid] = $elt;
    }

    /**
     * Insert a level of hierarchy.
     * NB: $list is assumed to be in the form returned by
     * imap_getmailboxes()/imap_getsubscribed().
     *
     * @access public
     *
     * @param array $list  TODO
     * @param TODO $id     TODO
     */
    function insertMailboxListAt($list, $id)
    {
        /* First get the element we're inserting after and save its next
           attribute. */
        $old = $this->_tree[$id];
        $tmp = $old['next'];
        $level = $old['level'] + 1;
        $oid = $id;

        /* Now loop through the list inserting all of the elements in order. */
        foreach ($list as $value) {
            $elt = $this->makeMailboxTreeElt($value->name, $value->attributes, $value->delimiter, $level);
            if ($this->_dotfiles || ($elt['label'][0] != '.')) {
                $nid = $this->insert($elt, $value->name);
                $old['next'] = $nid;
                $this->_tree[$oid] = $old;
                $oid = $nid;
                $old = $this->_tree[$oid];
            }
        }

        /* Close the loop, making the next of the last elt in the list the old
           next of the original element. */
        $old['next'] = $tmp;
        $this->_tree[$oid] = $old;
    }

    /**
     * Start the list. Calling this with an existing list will simply fail,
     * since that would create two lists in the same array with no links to
     * each other.
     *
     * @access public
     *
     * @param array $list  TODO
     */
    function feedList($list)
    {
        $level = 0;

        /* Make sure we are sorted correctly. */
        $list = $this->hsort($list);

        foreach ($list as $value) {
            $elt = $this->makeMailboxTreeElt($value->name, $value->attributes, $value->delimiter, $level);
            if ($this->_dotfiles ||
                ($elt['label'][0] != '.') ||
                ($this->noTrailingDelimiter($this->_prefix) == $elt['label'])) {
                $nid = $this->insert($elt, $value->name);
                if ($this->_first == '') {
                    $this->_first = $nid;
                } else {
                    $old['next'] = $nid;
                    $this->_tree[$oid] = $old;
                }
                $oid = $nid;
                $old = $this->_tree[$oid];
            }
        }
    }

    /**
     * Add another level of hierarchy to the tree.
     *
     * @access public
     *
     * @param array $list  TODO
     */
    function addLevel($list)
    {
        /* First, since we're really adding one level below what we're
           displaying, we need to get the information we need from
           what we're displaying: namely, which nodes in the level we
           *are* displaying actually have children. */
        $prefixes = $this->filter($list);
        foreach ($prefixes as $key => $junk) {
            if (isset($this->_tree[$key])) {
                $this->setChildren($this->_tree[$key], true);
            }
        }

        /* Sort the list. */
        $list = $this->hsort($list);

        /* Toss them all in. Stir well. */
        $lastpid = '';
        foreach ($list as $key => $value) {
            $elt = $this->makeMailboxTreeElt($value->name, $value->attributes, $value->delimiter);

            /* Don't include the parent directories that UW passes back in
               %/% lists; also filter out dotfiles if requested. */
            if (($elt['label'] != '') &&
                ($this->_dotfiles || ($elt['label'][0] != '.'))) {
                $tmp = explode($this->_delimiter, $elt['value']);
                if (count($tmp) == 1) {
                    /* This case is broken. Why? */
                    $pkey = $value->name;
                } else {
                    $pkey = $this->_server . String::substr($elt['value'], 0, String::length($elt['value']) - String::length($tmp[count($tmp) - 1]) - 1);
                }
                if ($pkey == $lastpid) {
                    /* The element we want to put this one right after. */
                    $prev = $this->_tree[$lastid];

                    /* Store what *was* its next element. */
                    $next = $prev['next'];

                    /* We're on the same level. */
                    $elt['level'] = $prev['level'];

                    /* Make the new elements next element what was the old
                       elements next. */
                    $elt['next'] = $next;

                    /* Actually insert the new element into the tree. */
                    $nid = $this->insert($elt, $value->name);

                    /* Make the old elements next element the element we just
                       inserted. */
                    $prev['next'] = $nid;

                    /* Update the value inside the tree array. */
                    $this->_tree[$lastid] = $prev;

                    /* Update the last inserted id. */
                    $lastid = $nid;
                } elseif (isset($this->_tree[$pkey])) {
                    /* The parent element is what we want to insert after. */
                    $parent = $this->_tree[$pkey];

                    /* Save what was its next element. */
                    $next = $parent['next'];

                    /* We're going one level deeper. */
                    $elt['level'] = $parent['level'] + 1;

                    /* Make the new elts next pointer what the parent's next
                       pointer was. */
                    $elt['next'] = $next;

                    /* Put the new element into the tree. */
                    $nid = $this->insert($elt, $value->name);

                    /* Close the gap by setting the parent's next pointer to
                       the new element. */
                    $parent['next'] = $nid;

                    /* Update the parent value in the tree array. */
                    $this->_tree[$pkey] = $parent;

                    /* Update the last parent id. */
                    $lastpid = $pkey;

                    /* Update the last inserted id. */
                    $lastid = $nid;
                }
            }
        }
    }

    /**
     * TODO
     *
     * @access public
     *
     * @param array $mboxlist  TODO
     *
     * @return mixed  TODO
     */
    function filter($mboxlist)
    {
        if ($mboxlist) {
            foreach ($mboxlist as $box) {
                $prefixes[String::substr($box->name, 0, String::length($box->name) - String::length(strrchr($box->name, $this->_delimiter)))] = true;
            }
            return $prefixes;
        }
        return false;
    }

    /**
     * TODO
     *
     * @access public
     *
     * @param TODO $elt  TODO
     *
     * @return integer  TODO
     */
    function hasChildren($elt)
    {
        return $elt['attributes'] & IMP_TREEELT_HAS_CHILDREN;
    }

    /**
     * TODO
     *
     * @access public
     *
     * @param TODO &$elt     TODO
     * @param boolean $bool  TODO
     */
    function setChildren(&$elt, $bool)
    {
        if ($bool != $this->hasChildren($elt)) {
            if ($bool) {
                $elt['attributes'] |= IMP_TREEELT_HAS_CHILDREN;
            } else {
                $elt['attributes'] &= ~IMP_TREEELT_HAS_CHILDREN;
            }
        }
    }

    /**
     * TODO
     *
     * @access public
     *
     * @param TODO $elt  TODO
     *
     * @return integer  TODO
     */
    function isDiscovered($elt)
    {
        return $elt['attributes'] & IMP_TREEELT_IS_DISCOVERED;
    }

    /**
     * TODO
     *
     * @access public
     *
     * @param TODO &$elt     TODO
     * @param boolean $bool  TODO
     */
    function setDiscovered(&$elt, $bool)
    {
        if ($bool != $this->isDiscovered($elt)) {
            if ($bool) {
                $elt['attributes'] |= IMP_TREEELT_IS_DISCOVERED;
            } else {
                $elt['attributes'] &= ~IMP_TREEELT_IS_DISCOVERED;
            }
        }
    }

    /**
     * TODO
     *
     * @access public
     *
     * @param TODO $elt  TODO
     *
     * @return integer  TODO
     */
    function isOpen($elt)
    {
        return $elt['attributes'] & IMP_TREEELT_IS_OPEN;
    }

    /**
     * TODO
     *
     * @access public
     *
     * @param TODO &$elt     TODO
     * @param boolean $bool  TODO
     */
    function setOpen(&$elt, $bool)
    {
        if ($bool != $this->isOpen($elt)) {
            if ($bool) {
                $elt['attributes'] |= IMP_TREEELT_IS_OPEN;
            } else {
                $elt['attributes'] &= ~IMP_TREEELT_IS_OPEN;
            }
        }
    }

    /**
     * Return the prefix.
     *
     * @access public
     *
     * @return string  The prefix where folders are begin to be listed.
     */
    function getPrefix()
    {
        return $this->_prefix;
    }

    /**
     * Return the delimiter.
     *
     * @access public
     *
     * @return string  The hierarchy delimiter.
     */
    function getDelimiter()
    {
        return $this->_delimiter;
    }

    /**
     * Return the server string.
     *
     * @access public
     *
     * @return string  The server string.
     */
    function getServer()
    {
        return $this->_server;
    }

}
