1<?php
2/**
3 * Copyright 2000-2017 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file COPYING for license information (GPL). If you
6 * did not receive this file, see http://www.horde.org/licenses/gpl.
7 *
8 * @category  Horde
9 * @copyright 2000-2017 Horde LLC
10 * @license   http://www.horde.org/licenses/gpl GPL
11 * @package   IMP
12 */
13
14/**
15 * IMP_Ftree (folder tree) provides a tree view of the mailboxes on a backend
16 * (a/k/a a folder list; in IMP, folders = collection of mailboxes), along
17 * with other display elements (Remote Accounts; Virtual Folders).
18 *
19 * @author    Chuck Hagenbuch <chuck@horde.org>
20 * @author    Anil Madhavapeddy <avsm@horde.org>
21 * @author    Jon Parise <jon@horde.org>
22 * @author    Michael Slusarz <slusarz@horde.org>
23 * @category  Horde
24 * @copyright 2000-2017 Horde LLC
25 * @license   http://www.horde.org/licenses/gpl GPL
26 * @package   IMP
27 *
28 * @property-read boolean $changed  Has the tree changed?
29 * @property-read IMP_Ftree_Eltdiff $eltdiff  Element diff tracker.
30 * @property-read IMP_FTree_Prefs_Expanded $expanded  The expanded folders
31 *                                                    list.
32 * @property-read IMP_Ftree_Prefs_Poll $poll  The poll list.
33 * @property-read boolean $subscriptions  Whether IMAP subscriptions are
34 *                                        enabled.
35 * @property-read boolean $unsubscribed_loaded  True if unsubscribed mailboxes
36 *                                              have been loaded.
37 */
38class IMP_Ftree
39implements ArrayAccess, Countable, IteratorAggregate, Serializable
40{
41    /* Constants for mailboxElt attributes. */
42    const ELT_NOSELECT = 1;
43    const ELT_NAMESPACE_OTHER = 2;
44    const ELT_NAMESPACE_SHARED = 4;
45    const ELT_IS_OPEN = 8;
46    const ELT_IS_SUBSCRIBED = 16;
47    const ELT_NOINFERIORS = 32;
48    const ELT_IS_POLLED = 64;
49    const ELT_NOT_POLLED = 128;
50    const ELT_VFOLDER = 256;
51    const ELT_NONIMAP = 512;
52    const ELT_INVISIBLE = 1024;
53    const ELT_NEED_SORT = 2048;
54    const ELT_REMOTE = 4096;
55    const ELT_REMOTE_AUTH = 8192;
56    const ELT_REMOTE_MBOX = 16384;
57
58    /* The string used to indicate the base of the tree. This must include
59     * null since this is the only 7-bit character not allowed in IMAP
60     * mailboxes (nulls allow us to sort by name but never conflict with an
61     * IMAP mailbox). */
62    const BASE_ELT = "base\0";
63
64    /**
65     * Account sources.
66     *
67     * @var array
68     */
69    protected $_accounts;
70
71    /**
72     * Tree changed flag.  Set when something in the tree has been altered.
73     *
74     * @var boolean
75     */
76    protected $_changed = false;
77
78    /**
79     * Element diff tracking.
80     *
81     * @var IMP_Ftree_Eltdiff
82     */
83    protected $_eltdiff;
84
85    /**
86     * Array containing the mailbox elements.
87     *
88     * @var array
89     */
90    protected $_elts;
91
92    /**
93     * Parent/child list.
94     *
95     * @var array
96     */
97    protected $_parent;
98
99    /**
100     * Temporary data that is not saved across serialization.
101     *
102     * @var array
103     */
104    protected $_temp = array();
105
106    /**
107     * Constructor.
108     */
109    public function __construct()
110    {
111        $this->init();
112    }
113
114    /**
115     */
116    public function __get($name)
117    {
118        global $prefs;
119
120        switch ($name) {
121        case 'changed':
122            return ($this->_changed || $this->eltdiff->changed);
123
124        case 'expanded':
125            if (!isset($this->_temp['expanded'])) {
126                $this->_temp['expanded'] = new IMP_Ftree_Prefs_Expanded();
127            }
128            return $this->_temp['expanded'];
129
130        case 'eltdiff':
131            return $this->_eltdiff;
132
133        case 'poll':
134            if (!isset($this->_temp['poll'])) {
135                $this->_temp['poll'] = new IMP_Ftree_Prefs_Poll($this);
136            }
137            return $this->_temp['poll'];
138
139        case 'subscriptions':
140            return $prefs->getValue('subscribe');
141
142        case 'unsubscribed_loaded':
143            return $this[self::BASE_ELT]->subscribed;
144        }
145    }
146
147    /**
148     * Initialize the tree.
149     */
150    public function init()
151    {
152        global $injector, $session;
153
154        $access_folders = $injector->getInstance('IMP_Factory_Imap')->create()->access(IMP_Imap::ACCESS_FOLDERS);
155
156        /* Reset class variables to the defaults. */
157        $this->_accounts = $this->_elts = $this->_parent = array();
158        $this->_changed = true;
159
160        $old_track = (isset($this->_eltdiff) && $this->_eltdiff->track);
161        $this->_eltdiff = new IMP_Ftree_Eltdiff();
162
163        /* Create a placeholder element to the base of the tree so we can
164         * keep track of whether the base level needs to be sorted. */
165        $this->_elts[self::BASE_ELT] = self::ELT_NEED_SORT | self::ELT_NONIMAP;
166        $this->_parent[self::BASE_ELT] = array();
167
168        $mask = IMP_Ftree_Account::INIT;
169        if (!$access_folders || !$this->subscriptions || $session->get('imp', 'showunsub')) {
170            $mask |= IMP_Ftree_Account::UNSUB;
171            $this->setAttribute('subscribed', self::BASE_ELT, true);
172        }
173
174        /* Add base account. */
175        $ob = $this->_accounts[self::BASE_ELT] = $access_folders
176            ? new IMP_Ftree_Account_Imap()
177            : new IMP_Ftree_Account_Inboxonly();
178        array_map(array($this, '_insertElt'), $ob->getList(null, $mask));
179
180        if ($access_folders) {
181            /* Add remote servers. */
182            $this->insert(iterator_to_array(
183                $injector->getInstance('IMP_Remote')
184            ));
185
186            /* Add virtual folders to the tree. */
187            $this->insert(iterator_to_array(
188                IMP_Search_IteratorFilter::create(
189                    IMP_Search_IteratorFilter::VFOLDER
190                )
191            ));
192        }
193
194        if ($old_track) {
195            $this->eltdiff->track = true;
196        }
197    }
198
199    /**
200     * Insert an element into the tree.
201     *
202     * @param mixed $id  The name of the mailbox (or a list of mailboxes),
203     *                   an IMP_Search_Vfolder object, an IMP_Remote_Account
204     *                   object, or an array containing any mixture of these.
205     */
206    public function insert($id)
207    {
208        foreach ((is_array($id) ? $id : array($id)) as $val) {
209            if (($val instanceof IMP_Search_Vfolder) &&
210                !isset($this->_accounts[strval($val)])) {
211                /* Virtual Folders. */
212                $account = $this->_accounts[strval($val)] = new IMP_Ftree_Account_Vfolder($val);
213            } elseif (($val instanceof IMP_Remote_Account) &&
214                      !isset($this->_accounts[strval($val)])) {
215                /* Remote accounts. */
216                $account = $this->_accounts[strval($val)] = new IMP_Ftree_Account_Remote($val);
217            } else {
218                $account = $this->getAccount($val);
219                $val = $this->_normalize($val);
220            }
221
222            array_map(array($this, '_insertElt'), $account->getList(array($val)));
223        }
224    }
225
226    /**
227     * Expand an element.
228     *
229     * @param mixed $elts         The element (or an array of elements) to
230     *                            expand.
231     * @param boolean $expandall  Expand all subelements?
232     */
233    public function expand($elts, $expandall = false)
234    {
235        foreach ((is_array($elts) ? $elts : array($elts)) as $val) {
236            if (($elt = $this[$val]) && $elt->children) {
237                if (!$elt->open) {
238                    $elt->open = true;
239                }
240
241                /* Expand all children beneath this one. */
242                if ($expandall) {
243                    $this->expand($this->_parent[strval($elt)]);
244                }
245            }
246        }
247    }
248
249    /**
250     * Expand all elements.
251     */
252    public function expandAll()
253    {
254        $this->expand($this->_parent[self::BASE_ELT], true);
255    }
256
257    /**
258     * Collapse an element.
259     *
260     * @param mixed $elts  The element (or an array of elements) to expand.
261     */
262    public function collapse($elts)
263    {
264        foreach ((is_array($elts) ? $elts : array($elts)) as $val) {
265            if ($elt = $this[$val]) {
266                $elt->open = false;
267            }
268        }
269    }
270
271    /**
272     * Collapse all elements.
273     */
274    public function collapseAll()
275    {
276        $this->collapse(
277            array_diff_key(array_keys($this->_elts), array(self::BASE_ELT))
278        );
279    }
280
281    /**
282     * Delete an element from the tree.
283     *
284     * @param mixed $elts  The element (or an array of elements) to delete.
285     */
286    public function delete($id)
287    {
288        if (is_array($id)) {
289            /* We want to delete from the TOP of the tree down to ensure that
290             * parents have an accurate view of what children are left. */
291            $this->sortList($id);
292            $id = array_reverse($id);
293        } else {
294            $id = array($id);
295        }
296
297        foreach (array_filter(array_map(array($this, 'offsetGet'), $id)) as $elt) {
298            $account = $this->getAccount($elt);
299            if (!($mask = $account->delete($elt))) {
300                continue;
301            }
302
303            $this->_changed = true;
304
305            if ($mask & IMP_Ftree_Account::DELETE_RECURSIVE) {
306                foreach (array_map('strval', iterator_to_array(new IMP_Ftree_Iterator($elt), false)) as $val) {
307                    unset(
308                        $this->_elts[$val],
309                        $this->_parent[$val]
310                    );
311                    $this->eltdiff->delete($val);
312                }
313                unset($this->_parent[strval($elt)]);
314            }
315
316            if (strval($account) == strval($elt)) {
317                unset($this->_accounts[strval($elt)]);
318            }
319
320            if ($mask & IMP_Ftree_Account::DELETE_ELEMENT) {
321                /* Do not delete from tree if there are child elements -
322                 * instead, convert to a container element. */
323                if ($elt->children) {
324                    $elt->container = true;
325                    continue;
326                }
327
328                /* Remove the mailbox from the expanded folders list. */
329                unset($this->expanded[$elt]);
330
331                /* Remove the mailbox from the polled list. */
332                $this->poll->removePollList($elt);
333            }
334
335            $parent = strval($elt->parent);
336            $this->eltdiff->delete($elt);
337
338            /* Delete the entry from the parent tree. */
339            unset(
340                $this->_elts[strval($elt)],
341                $this->_parent[$parent][array_search(strval($elt), $this->_parent[$parent], true)]
342            );
343
344            if (empty($this->_parent[$parent])) {
345                /* This mailbox is now completely empty (no children). */
346                unset($this->_parent[$parent]);
347                if ($p_elt = $this[$parent]) {
348                    if ($p_elt->container && !$p_elt->namespace) {
349                        $this->delete($p_elt);
350                    } else {
351                        $p_elt->open = false;
352                        $this->eltdiff->change($p_elt);
353                    }
354                }
355            }
356
357            if (!empty($this->_parent[$parent])) {
358                $this->_parent[$parent] = array_values($this->_parent[$parent]);
359            }
360        }
361    }
362
363    /**
364     * Rename a mailbox.
365     *
366     * @param string $old  The old mailbox name.
367     * @param string $new  The new mailbox name.
368     */
369    public function rename($old, $new)
370    {
371        if (!($old_elt = $this[$old])) {
372            return;
373        }
374
375        $new_list = $polled = array();
376        $old_list = array_merge(
377            array($old),
378            iterator_to_array(new IMP_Ftree_IteratorFilter(new IMP_Ftree_Iterator($old_elt)), false)
379        );
380
381        foreach ($old_list as $val) {
382            $new_list[] = $new_name = substr_replace($val, $new, 0, strlen($old));
383            if ($val->polled) {
384                $polled[] = $new_name;
385            }
386        }
387
388        $this->insert($new_list);
389        $this->poll->addPollList($polled);
390        $this->delete($old_list);
391    }
392
393    /**
394     * Subscribe an element to the tree.
395     *
396     * @param mixed $id  The element name or an array of element names.
397     */
398    public function subscribe($id)
399    {
400        foreach ((is_array($id) ? $id : array($id)) as $val) {
401            $this->setAttribute('subscribed', $val, true);
402            $this->setAttribute('container', $val, false);
403        }
404    }
405
406    /**
407     * Unsubscribe an element from the tree.
408     *
409     * @param mixed $id  The element name or an array of element names.
410     */
411    public function unsubscribe($id)
412    {
413        if (is_array($id)) {
414            /* We want to delete from the TOP of the tree down to ensure that
415             * parents have an accurate view of what children are left. */
416            $this->sortList($id);
417            $id = array_reverse($id);
418        } else {
419            $id = array($id);
420        }
421
422        foreach ($id as $val) {
423            /* INBOX can never be unsubscribed to. */
424            if (($elt = $this[$val]) && !$elt->inbox) {
425                $this->_changed = true;
426
427                /* Do not delete from tree if there are child elements -
428                 * instead, convert to a container element. */
429                if ($elt->children) {
430                    $this->setAttribute('container', $elt, true);
431                }
432
433                /* Set as unsubscribed, add to unsubscribed list, and remove
434                 * from subscribed list. */
435                $this->setAttribute('subscribed', $elt, false);
436            }
437        }
438    }
439
440    /**
441     * Load unsubscribed mailboxes.
442     */
443    public function loadUnsubscribed()
444    {
445        /* If we are switching from unsubscribed to subscribed, no need
446         * to do anything (we just ignore unsubscribed stuff). */
447        if ($this->unsubscribed_loaded) {
448            return;
449        }
450
451        $this->_changed = true;
452
453        /* The BASE_ELT having the SUBSCRIBED mask indicates the unsubscribed
454         * mailboxes have been loaded into the object. */
455        $this->setAttribute('subscribed', self::BASE_ELT, true);
456
457        /* If we are switching from subscribed to unsubscribed, we need
458         * to add all unsubscribed elements that live in currently
459         * discovered items. */
460        $old_track = $this->eltdiff->track;
461        $this->eltdiff->track = false;
462        foreach ($this->_accounts as $val) {
463            array_map(array($this, '_insertElt'), $val->getList(array(), $val::UNSUB));
464        }
465        $this->eltdiff->track = $old_track;
466    }
467
468    /**
469     * Get an attribute value.
470     *
471     * @param string $type  The attribute type.
472     * @param string $name  The element name.
473     *
474     * @return mixed  Boolean attribute result, or null if element or
475     *                attribute doesn't exist
476     */
477    public function getAttribute($type, $name)
478    {
479        if (!($elt = $this[$name])) {
480            return null;
481        }
482        $s_elt = strval($elt);
483
484        switch ($type) {
485        case 'children':
486            return isset($this->_parent[$s_elt]);
487
488        case 'container':
489            $attr = self::ELT_NOSELECT;
490            break;
491
492        case 'invisible':
493            $attr = self::ELT_INVISIBLE;
494            break;
495
496        case 'namespace_other':
497            $attr = self::ELT_NAMESPACE_OTHER;
498            break;
499
500        case 'namespace_shared':
501            $attr = self::ELT_NAMESPACE_SHARED;
502            break;
503
504        case 'needsort':
505            $attr = self::ELT_NEED_SORT;
506            break;
507
508        case 'nochildren':
509            $attr = self::ELT_NOINFERIORS;
510            break;
511
512        case 'nonimap':
513            $attr = self::ELT_NONIMAP;
514            break;
515
516        case 'open':
517            if (!$elt->children) {
518                return false;
519            }
520            $attr = self::ELT_IS_OPEN;
521            break;
522
523        case 'polled':
524            if ($this->_elts[$s_elt] & self::ELT_IS_POLLED) {
525                return true;
526            } elseif ($this->_elts[$s_elt] & self::ELT_NOT_POLLED) {
527                return false;
528            }
529
530            $polled = $this->poll[$elt];
531            $this->setAttribute('polled', $elt, $polled);
532            return $polled;
533
534        case 'remote':
535            $attr = self::ELT_REMOTE;
536            break;
537
538        case 'remote_auth':
539            $attr = self::ELT_REMOTE_AUTH;
540            break;
541
542        case 'remote_mbox':
543            $attr = self::ELT_REMOTE_MBOX;
544            break;
545
546        case 'subscribed':
547            if ($elt->inbox) {
548                return true;
549            }
550            $attr = self::ELT_IS_SUBSCRIBED;
551            break;
552
553        case 'vfolder':
554            $attr = self::ELT_VFOLDER;
555            break;
556
557        default:
558            return null;
559        }
560
561        return (bool)($this->_elts[$s_elt] & $attr);
562    }
563
564    /**
565     * Change an attribute value.
566     *
567     * @param string $type   The attribute type.
568     * @param string $elt    The element name.
569     * @param boolean $bool  The boolean value.
570     */
571    public function setAttribute($type, $elt, $bool)
572    {
573        if (!($elt = $this[$elt])) {
574            return;
575        }
576
577        $attr = null;
578        $s_elt = strval($elt);
579
580        switch ($type) {
581        case 'container':
582            $attr = self::ELT_NOSELECT;
583            $this->eltdiff->change($elt);
584            break;
585
586        case 'invisible':
587            $attr = self::ELT_INVISIBLE;
588            $this->eltdiff->change($elt);
589            break;
590
591        case 'needsort':
592            $attr = self::ELT_NEED_SORT;
593            break;
594
595        case 'open':
596            $attr = self::ELT_IS_OPEN;
597            if ($bool) {
598                $this->expanded[$elt] = true;
599            } else {
600                unset($this->expanded[$elt]);
601            }
602            break;
603
604        case 'polled':
605            if ($bool) {
606                $attr = self::ELT_IS_POLLED;
607                $remove = self::ELT_NOT_POLLED;
608            } else {
609                $attr = self::ELT_NOT_POLLED;
610                $remove = self::ELT_IS_POLLED;
611            }
612            $this->_elts[$s_elt] &= ~$remove;
613            break;
614
615        case 'subscribed':
616            $attr = self::ELT_IS_SUBSCRIBED;
617            $this->eltdiff->change($elt);
618            break;
619
620        default:
621            return;
622        }
623
624        if ($bool) {
625            $this->_elts[$s_elt] |= $attr;
626        } else {
627            $this->_elts[$s_elt] &= ~$attr;
628        }
629
630        $this->_changed = true;
631    }
632
633    /**
634     * Get the account object for a given element ID.
635     *
636     * @param string $id  Element ID.
637     *
638     * @return IMP_Ftree_Account  Account object.
639     */
640    public function getAccount($id)
641    {
642        foreach (array_diff(array_keys($this->_accounts), array(self::BASE_ELT)) as $val) {
643            if (strpos($id, $val) === 0) {
644                return $this->_accounts[$val];
645            }
646        }
647
648        return $this->_accounts[self::BASE_ELT];
649    }
650
651    /**
652     * Return the list of children for a given element ID.
653     *
654     * @param string $id  Element ID.
655     *
656     * @return array  Array of tree elements.
657     */
658    public function getChildren($id)
659    {
660        if (!($elt = $this[$id]) || !isset($this->_parent[strval($elt)])) {
661            return array();
662        }
663
664        $this->_sortLevel($elt);
665        return array_map(
666            array($this, 'offsetGet'), $this->_parent[strval($elt)]
667        );
668    }
669
670    /**
671     * Get the parent element for a given element ID.
672     *
673     * @param string $id  Element ID.
674     *
675     * @return mixed  IMP_Ftree_Element object, or null if no parent.
676     */
677    public function getParent($id)
678    {
679        $id = strval($id);
680
681        if ($id == self::BASE_ELT) {
682            return null;
683        }
684
685        foreach ($this->_parent as $key => $val) {
686            if (in_array($id, $val, true)) {
687                return $this[$key];
688            }
689        }
690
691        return $this[self::BASE_ELT];
692    }
693
694    /**
695     * Sorts a list of mailboxes.
696     *
697     * @param array &$mbox             The list of mailboxes to sort.
698     * @param IMP_Ftree_Element $base  The base element.
699     */
700    public function sortList(&$mbox, $base = false)
701    {
702        if (count($mbox) < 2) {
703            return;
704        }
705
706        if (!$base || (!$base->base_elt && !$base->remote_auth)) {
707            $list_ob = new Horde_Imap_Client_Mailbox_List($mbox);
708            $mbox = $list_ob->sort();
709            return;
710        }
711
712        $prefix = $base->base_elt
713            ? ''
714            : (strval($this->getAccount($base)) . "\0");
715
716        $basesort = $othersort = array();
717        /* INBOX always appears first. */
718        $sorted = array($prefix . 'INBOX');
719
720        foreach ($mbox as $key => $val) {
721            $ob = $this[$val];
722            if ($ob->nonimap) {
723                $othersort[$key] = $ob->mbox_ob->label;
724            } elseif ($val !== ($prefix . 'INBOX')) {
725                $basesort[$key] = $ob->mbox_ob->label;
726            }
727        }
728
729        natcasesort($basesort);
730        natcasesort($othersort);
731        foreach (array_merge(array_keys($basesort), array_keys($othersort)) as $key) {
732            $sorted[] = $mbox[$key];
733        }
734
735        $mbox = $sorted;
736    }
737
738
739    /* Internal methods. */
740
741    /**
742     * Normalize an element ID to the correct, internal name.
743     *
744     * @param string $id  The element ID.
745     *
746     * @return string  The converted name.
747     */
748    protected function _normalize($id)
749    {
750        $id = strval($id);
751
752        return (strcasecmp($id, 'INBOX') === 0)
753            ? 'INBOX'
754            : $id;
755    }
756
757    /**
758     * Insert an element into the tree.
759     *
760     * @param array $elt  Element data. Keys:
761     * <pre>
762     *   - a: (integer) Attributes.
763     *   - p: (string) Parent element ID.
764     *   - v: (string) Mailbox ID.
765     * </pre>
766     */
767    protected function _insertElt($elt)
768    {
769        $name = $this->_normalize($elt['v']);
770
771        $change = false;
772        if (isset($this->_elts[$name])) {
773            if ($elt['a'] & self::ELT_NOSELECT) {
774                return;
775            }
776            $change = true;
777        }
778
779        $p_elt = $this[isset($elt['p']) ? $elt['p'] : self::BASE_ELT];
780        $parent = strval($p_elt);
781
782        $this->_changed = true;
783
784        if (!isset($this->_parent[$parent])) {
785            $this->eltdiff->change($p_elt);
786        }
787        if (!isset($this->_elts[$name])) {
788            $this->_parent[$parent][] = $name;
789        }
790        $this->_elts[$name] = $elt['a'];
791
792        if ($change) {
793            $this->eltdiff->change($name);
794        } else {
795            $this->eltdiff->add($name);
796        }
797
798        /* Check for polled status. */
799        $this->setAttribute('polled', $name, $this->poll[$name]);
800
801        /* Check for expanded status. */
802        $this->setAttribute('open', $name, $this->expanded[$name]);
803
804        if (empty($this->_temp['nohook'])) {
805            try {
806                $this->setAttribute(
807                    'invisible',
808                    $name,
809                    !$GLOBALS['injector']->getInstance('Horde_Core_Hooks')->callHook(
810                        'display_folder',
811                        'imp',
812                        array($name)
813                    )
814                );
815            } catch (Horde_Exception_HookNotSet $e) {
816                $this->_temp['nohook'] = true;
817            }
818        }
819
820        /* Make sure we are sorted correctly. */
821        $this->setAttribute('needsort', $p_elt, true);
822    }
823
824    /**
825     * Sort a level in the tree.
826     *
827     * @param string $id  The parent element whose children need to be sorted.
828     */
829    protected function _sortLevel($id)
830    {
831        if (($elt = $this[$id]) && $elt->needsort) {
832            if (count($this->_parent[strval($elt)]) > 1) {
833                $this->sortList($this->_parent[strval($elt)], $elt);
834            }
835            $this->setAttribute('needsort', $elt, false);
836        }
837    }
838
839    /* ArrayAccess methods. */
840
841    /**
842     */
843    public function offsetExists($offset)
844    {
845        /* Optimization: Only normalize in the rare case it is not found on
846         * the first attempt. */
847        $offset = strval($offset);
848        return (isset($this->_elts[$offset]) ||
849                isset($this->_elts[$this->_normalize($offset)]));
850    }
851
852    /**
853     * @return IMP_Ftree_Element
854     */
855    public function offsetGet($offset)
856    {
857        if ($offset instanceof IMP_Ftree_Element) {
858            return $offset;
859        }
860
861        /* Optimization: Only normalize in the rare case it is not found on
862         * the first attempt. */
863        $offset = strval($offset);
864        if (isset($this->_elts[$offset])) {
865            return new IMP_Ftree_Element($offset, $this);
866        }
867
868        $offset = $this->_normalize($offset);
869        return isset($this->_elts[$offset])
870            ? new IMP_Ftree_Element($offset, $this)
871            : null;
872    }
873
874    /**
875     */
876    public function offsetSet($offset, $value)
877    {
878        $this->insert($offset);
879    }
880
881    /**
882     */
883    public function offsetUnset($offset)
884    {
885        $this->delete($offset);
886    }
887
888    /* Countable methods. */
889
890    /**
891     * Return the number of mailboxes on the server.
892     */
893    public function count()
894    {
895        $this->loadUnsubscribed();
896
897        $iterator = new IMP_Ftree_IteratorFilter($this);
898        $iterator->add($iterator::NONIMAP);
899        $iterator->remove($iterator::UNSUB);
900
901        return iterator_count($iterator);
902    }
903
904    /* Serializable methods. */
905
906    /**
907     */
908    public function serialize()
909    {
910        return $GLOBALS['injector']->getInstance('Horde_Pack')->pack(array(
911            $this->_accounts,
912            $this->_eltdiff,
913            $this->_elts,
914            $this->_parent
915        ), array(
916            'compress' => false,
917            'phpob' => true
918        ));
919    }
920
921    /**
922     * @throws Horde_Pack_Exception
923     */
924    public function unserialize($data)
925    {
926        list(
927            $this->_accounts,
928            $this->_eltdiff,
929            $this->_elts,
930            $this->_parent
931        ) = $GLOBALS['injector']->getInstance('Horde_Pack')->unpack($data);
932    }
933
934    /**
935     * Creates a Horde_Tree representation of the current tree.
936     *
937     * @param string|Horde_Tree $name  Either the tree name, or a Horde_Tree
938     *                                 object to add nodes to.
939     * @param array $opts              Additional options:
940     * <pre>
941     *   - basename: (boolean) Use raw basename instead of abbreviated label?
942     *               DEFAULT: false
943     *   - checkbox: (boolean) Display checkboxes?
944     *               DEFAULT: false
945     *   - editvfolder: (boolean) Display vfolder edit links?
946     *                  DEFAULT: false
947     *   - iterator: (Iterator) Tree iterator to use.
948     *               DEFAULT: Base iterator.
949     *   - open: (boolean) Force child mailboxes to this status.
950     *           DEFAULT: null
951     *   - parent: (string) The parent object of the current level.
952     *             DEFAULT: null (add to base level)
953     *   - poll_info: (boolean) Include poll information in output?
954     *                DEFAULT: false
955     *   - render_params: (array) List of params to pass to renderer if
956     *                    auto-creating.
957     *                    DEFAULT: 'alternate', 'lines', and 'lines_base'
958     *                             are passed in with true values.
959     *   - render_type: (string) The renderer name.
960     *                  DEFAULT: Javascript
961     * </pre>
962     *
963     * @return Horde_Tree  The tree object.
964     */
965    public function createTree($name, array $opts = array())
966    {
967        global $injector, $registry;
968
969        $opts = array_merge(array(
970            'parent' => null,
971            'render_params' => array(),
972            'render_type' => 'Javascript'
973        ), $opts);
974
975        $view = $registry->getView();
976
977        if ($name instanceof Horde_Tree_Renderer_Base) {
978            $tree = $name;
979            $parent = $opts['parent'];
980        } else {
981            $tree = $injector->getInstance('Horde_Core_Factory_Tree')->create($name, $opts['render_type'], array_merge(array(
982                'alternate' => true,
983                'lines' => true,
984                'lines_base' => true,
985                'nosession' => true
986            ), $opts['render_params']));
987            $parent = null;
988        }
989
990        $iterator = empty($opts['iterator'])
991            ? new IMP_Ftree_IteratorFilter($this)
992            : $opts['iterator'];
993
994        foreach ($iterator as $val) {
995            $after = '';
996            $elt_parent = null;
997            $mbox_ob = $val->mbox_ob;
998            $params = array();
999
1000            switch ($opts['render_type']) {
1001            case 'IMP_Tree_Flist':
1002                if ($mbox_ob->vfolder_container) {
1003                    continue 2;
1004                }
1005
1006                $is_open = true;
1007                $label = $params['orig_label'] = empty($opts['basename'])
1008                    ? $mbox_ob->abbrev_label
1009                    : $mbox_ob->basename;
1010                break;
1011
1012            case 'IMP_Tree_Jquerymobile':
1013                $is_open = true;
1014                $label = $mbox_ob->display_html;
1015                $icon = $mbox_ob->icon;
1016                $params['icon'] = $icon->icon;
1017                $params['special'] = $mbox_ob->inbox || $mbox_ob->special;
1018                $params['class'] = 'imp-folder';
1019                $params['urlattributes'] = array(
1020                    'id' => 'imp-mailbox-' . $mbox_ob->form_to
1021                );
1022
1023                /* Force to flat tree so that non-polled parents don't cause
1024                 * polled children to be skipped by renderer (see Bug
1025                 * #11238). */
1026                $elt_parent = $this[self::BASE_ELT];
1027                break;
1028
1029            case 'IMP_Tree_Simplehtml':
1030                $is_open = $val->open;
1031                if ($tree->shouldToggle($mbox_ob->form_to)) {
1032                    if ($is_open) {
1033                        $this->collapse($val);
1034                    } else {
1035                        $this->expand($val);
1036                    }
1037                    $is_open = !$is_open;
1038                }
1039                $label = htmlspecialchars(Horde_String::abbreviate($mbox_ob->abbrev_label, 30 - ($val->level * 2)));
1040                break;
1041
1042            case 'Javascript':
1043                $is_open = $val->open;
1044                $label = empty($opts['basename'])
1045                    ? htmlspecialchars($mbox_ob->abbrev_label)
1046                    : htmlspecialchars($mbox_ob->basename);
1047                $icon = $mbox_ob->icon;
1048                $params['icon'] = $icon->icon;
1049                $params['iconopen'] = $icon->iconopen;
1050                break;
1051            }
1052
1053            if (!empty($opts['poll_info']) && $val->polled) {
1054                $poll_info = $mbox_ob->poll_info;
1055
1056                if ($poll_info->unseen) {
1057                    switch ($opts['render_type']) {
1058                    case 'IMP_Tree_Jquerymobile':
1059                        $after = $poll_info->unseen;
1060                        break;
1061
1062                    default:
1063                        $label = '<strong>' . $label . '</strong>&nbsp;(' .
1064                            $poll_info->unseen . ')';
1065                    }
1066                }
1067            }
1068
1069            if ($val->container) {
1070                $params['container'] = true;
1071            } else {
1072                switch ($view) {
1073                case $registry::VIEW_MINIMAL:
1074                    $params['url'] = IMP_Minimal_Mailbox::url(array('mailbox' => $mbox_ob));
1075                    break;
1076
1077                case $registry::VIEW_SMARTMOBILE:
1078                    $url = new Horde_Core_Smartmobile_Url();
1079                    $url->add('mbox', $mbox_ob->form_to);
1080                    $url->setAnchor('mailbox');
1081                    $params['url'] = strval($url);
1082                    break;
1083
1084                default:
1085                    $params['url'] = $mbox_ob->url('mailbox')->setRaw(true);
1086                    break;
1087                }
1088
1089                if (!$val->subscribed) {
1090                    $params['class'] = 'mboxunsub';
1091                }
1092            }
1093
1094            $checkbox = empty($opts['checkbox'])
1095                ? ''
1096                : '<input type="checkbox" class="checkbox" name="mbox_list[]" value="' . $mbox_ob->form_to . '"';
1097
1098            if ($val->nonimap) {
1099                $checkbox .= ' disabled="disabled"';
1100            }
1101
1102            if ($val->vfolder &&
1103                !empty($opts['editvfolder']) &&
1104                $val->container) {
1105                $after = '&nbsp[' .
1106                    $registry->getServiceLink('prefs', 'imp')->add('group', 'searches')->link(array('title' => _("Edit Virtual Folder"))) . _("Edit") . '</a>'.
1107                    ']';
1108            }
1109
1110            if (is_null($elt_parent)) {
1111                $elt_parent = $val->parent;
1112            }
1113
1114            $tree->addNode(array(
1115                'id' => $mbox_ob->form_to,
1116                'parent' => $elt_parent->base_elt ? $parent : $elt_parent->mbox_ob->form_to,
1117                'label' => $label,
1118                'expanded' => isset($opts['open']) ? $opts['open'] : $is_open,
1119                'params' => $params,
1120                'right' => $after,
1121                'left' => empty($opts['checkbox']) ? null : $checkbox . ' />'
1122            ));
1123        }
1124
1125        return $tree;
1126    }
1127
1128    /* IteratorAggregate methods. */
1129
1130    /**
1131     * This returns a RecursiveIterator - a RecursiveIteratorIterator is
1132     * needed to properly iterate through all elements.
1133     *
1134     * @return IMP_Ftree_Iterator  Iterator object.
1135     */
1136    public function getIterator()
1137    {
1138        return new IMP_Ftree_Iterator($this[self::BASE_ELT]);
1139    }
1140
1141}
1142