1<?php
2/**
3 * Copyright 2002-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 2002-2017 Horde LLC
10 * @license   http://www.horde.org/licenses/gpl GPL
11 * @package   IMP
12 */
13
14/**
15 * This class contains code related to generating and handling a mailbox
16 * message list.
17 *
18 * @author    Michael Slusarz <slusarz@horde.org>
19 * @category  Horde
20 * @copyright 2002-2017 Horde LLC
21 * @license   http://www.horde.org/licenses/gpl GPL
22 * @package   IMP
23 */
24class IMP_Mailbox_List
25implements ArrayAccess, Countable, Iterator, Serializable
26{
27    /* The UID count at which the sorted list will undergo UID compression
28     * when being serialized. */
29    const SERIALIZE_LIMIT = 500;
30
31    /**
32     * Has the internal message list changed?
33     *
34     * @var boolean
35     */
36    public $changed = false;
37
38    /**
39     * Max assigned browser-UID.
40     *
41     * @var integer
42     */
43    protected $_buidmax = 0;
44
45    /**
46     * Mapping of browser-UIDs to UIDs.
47     *
48     * @var array
49     */
50    protected $_buids = array();
51
52    /**
53     * The IMAP cache ID of the mailbox.
54     *
55     * @var string
56     */
57    protected $_cacheid = null;
58
59    /**
60     * The location in the sorted array we are at.
61     *
62     * @var integer
63     */
64    protected $_index = null;
65
66    /**
67     * The mailbox to work with.
68     *
69     * @var IMP_Mailbox
70     */
71    protected $_mailbox;
72
73    /**
74     * The array of sorted indices.
75     *
76     * @var array
77     */
78    protected $_sorted = null;
79
80    /**
81     * The thread object representation(s) for the mailbox.
82     *
83     * @var array
84     */
85    protected $_thread = array();
86
87    /**
88     * The thread tree UI cached data.
89     *
90     * @var array
91     */
92    protected $_threadui = array();
93
94    /**
95     * Constructor.
96     *
97     * @param string $mbox  The mailbox to work with.
98     */
99    public function __construct($mbox)
100    {
101        $this->_mailbox = IMP_Mailbox::get($mbox);
102    }
103
104    /**
105     * Build the array of message information.
106     *
107     * @param array $msgnum   An array of index numbers.
108     * @param array $options  Additional options:
109     *   - headers: (boolean) Return info on the non-envelope headers
110     *              'Importance', 'List-Post', and 'X-Priority'.
111     *              DEFAULT: false (only envelope headers returned)
112     *   - preview: (mixed) Include preview information?  If empty, add no
113     *              preview information. If 1, uses value from prefs.
114     *              If 2, forces addition of preview info.
115     *              DEFAULT: No preview information.
116     *   - type: (boolean) Return info on the MIME Content-Type of the base
117     *           message part ('Content-Type' header).
118     *           DEFAULT: false
119     *
120     * @return array  An array with the following keys:
121     *   - overview: (array) The overview information. Contains the following:
122     *   - envelope: (Horde_Imap_Client_Data_Envelope) Envelope information
123     *               returned from the IMAP server.
124     *   - flags: (array) The list of IMAP flags returned from the server.
125     *   - headers: (array) Horde_Mime_Headers objects containing header data
126     *              if either $options['headers'] or $options['type'] are
127     *              true.
128     *   - idx: (integer) Array index of this message.
129     *   - mailbox: (string) The mailbox containing the message.
130     *   - preview: (string) If requested in $options['preview'], the preview
131     *              text.
132     *   - previewcut: (boolean) Has the preview text been cut?
133     *   - size: (integer) The size of the message in bytes.
134     *   - uid: (string) The unique ID of the message.
135     *   - uids: (IMP_Indices) An indices object.
136     */
137    public function getMailboxArray($msgnum, $options = array())
138    {
139        $this->_buildMailbox();
140
141        $headers = $overview = $to_process = $uids = array();
142
143        /* Build the list of mailboxes and messages. */
144        foreach ($msgnum as $i) {
145            /* Make sure that the index is actually in the slice of messages
146               we're looking at. If we're hiding deleted messages, for
147               example, there may be gaps here. */
148            if (isset($this->_sorted[$i - 1])) {
149                $to_process[strval($this->_getMbox($i - 1))][$i] = $this->_sorted[$i - 1];
150            }
151        }
152
153        $fetch_query = new Horde_Imap_Client_Fetch_Query();
154        $fetch_query->envelope();
155        $fetch_query->flags();
156        $fetch_query->size();
157        $fetch_query->uid();
158
159        if (!empty($options['headers'])) {
160            $headers = array_merge($headers, array(
161                'importance',
162                'list-post',
163                'x-priority'
164            ));
165        }
166
167        if (!empty($options['type'])) {
168            $headers[] = 'content-type';
169        }
170
171        if (!empty($headers)) {
172            $fetch_query->headers('imp', $headers, array(
173                'cache' => true,
174                'peek' => true
175            ));
176        }
177
178        if (empty($options['preview'])) {
179            $cache = null;
180            $options['preview'] = 0;
181        } else {
182            $cache = $this->_mailbox->imp_imap->getCache();
183        }
184
185        /* Retrieve information from each mailbox. */
186        foreach ($to_process as $mbox => $ids) {
187            try {
188                $imp_imap = IMP_Mailbox::get($mbox)->imp_imap;
189                $fetch_res = $imp_imap->fetch($mbox, $fetch_query, array(
190                    'ids' => $imp_imap->getIdsOb($ids)
191                ));
192
193                if ($options['preview']) {
194                    $preview_info = $tostore = array();
195                    if ($cache) {
196                        try {
197                            $preview_info = $cache->get($mbox, $ids, array('IMPpreview', 'IMPpreviewc'));
198                        } catch (IMP_Imap_Exception $e) {}
199                    }
200                }
201
202                $mbox_ids = array();
203
204                foreach ($ids as $k => $v) {
205                    if (!isset($fetch_res[$v])) {
206                        continue;
207                    }
208
209                    $f = $fetch_res[$v];
210                    $uid = $f->getUid();
211                    $v = array(
212                        'envelope' => $f->getEnvelope(),
213                        'flags' => $f->getFlags(),
214                        'headers' => $f->getHeaders('imp', Horde_Imap_Client_Data_Fetch::HEADER_PARSE),
215                        'idx' => $k,
216                        'mailbox' => $mbox,
217                        'size' => $f->getSize(),
218                        'uid' => $uid
219                    );
220
221                    if (($options['preview'] === 2) ||
222                        (($options['preview'] === 1) &&
223                         (!$GLOBALS['prefs']->getValue('preview_show_unread') ||
224                          !in_array(Horde_Imap_Client::FLAG_SEEN, $v['flags'])))) {
225                        if (empty($preview_info[$uid])) {
226                            try {
227                                $imp_contents = $GLOBALS['injector']->getInstance('IMP_Factory_Contents')->create(new IMP_Indices($mbox, $uid));
228                                $prev = $imp_contents->generatePreview();
229                                $preview_info[$uid] = array(
230                                    'IMPpreview' => $prev['text'],
231                                    'IMPpreviewc' => $prev['cut']
232                                );
233                                if (!is_null($cache)) {
234                                    $tostore[$uid] = $preview_info[$uid];
235                                }
236                            } catch (Exception $e) {
237                                $preview_info[$uid] = array(
238                                    'IMPpreview' => '',
239                                    'IMPpreviewc' => false
240                                );
241                            }
242                        }
243
244                        $v['preview'] = $preview_info[$uid]['IMPpreview'];
245                        $v['previewcut'] = $preview_info[$uid]['IMPpreviewc'];
246                    }
247
248                    $overview[] = $v;
249                    $mbox_ids[] = $uid;
250                }
251
252                $uids[$mbox] = $mbox_ids;
253
254                if (!is_null($cache) && !empty($tostore)) {
255                    $status = $imp_imap->status($mbox, Horde_Imap_Client::STATUS_UIDVALIDITY);
256                    $cache->set($mbox, $tostore, $status['uidvalidity']);
257                }
258            } catch (IMP_Imap_Exception $e) {}
259        }
260
261        return array(
262            'overview' => $overview,
263            'uids' => new IMP_Indices($uids)
264        );
265    }
266
267    /**
268     * Using the preferences and the current mailbox, determines the messages
269     * to view on the current page (if using a paged view).
270     *
271     * @param integer $page   The page number currently being displayed.
272     * @param integer $start  The starting message number.
273     *
274     * @return array  An array with the following fields:
275     *   - anymsg: (boolean) Are there any messages at all in mailbox? E.g. If
276     *             'msgcount' is 0, there may still be hidden deleted messages.
277     *   - begin: (integer) The beginning message sequence number of the page.
278     *   - end: (integer) The ending message sequence number of the page.
279     *   - msgcount: (integer) The number of viewable messages in the current
280     *               mailbox.
281     *   - page: (integer) The current page number.
282     *   - pagecount: (integer) The number of pages in this mailbox.
283     */
284    public function buildMailboxPage($page = 0, $start = 0)
285    {
286        global $prefs, $session;
287
288        $this->_buildMailbox();
289
290        $ret = array('msgcount' => count($this->_sorted));
291
292        $page_size = max($prefs->getValue('max_msgs'), 1);
293
294        if ($ret['msgcount'] > $page_size) {
295            $ret['pagecount'] = ceil($ret['msgcount'] / $page_size);
296
297            /* Determine which page to display. */
298            if (empty($page) || strcspn($page, '0123456789')) {
299                if (!empty($start)) {
300                    /* Messages set this when returning to a mailbox. */
301                    $page = ceil($start / $page_size);
302                } else {
303                    /* Search for the last visited page first. */
304                    $page = $session->exists('imp', 'mbox_page/' . $this->_mailbox)
305                        ? $session->get('imp', 'mbox_page/' . $this->_mailbox)
306                        : ceil($this->mailboxStart($ret['msgcount']) / $page_size);
307                }
308            }
309
310            /* Make sure we're not past the end or before the beginning, and
311               that we have an integer value. */
312            $ret['page'] = intval($page);
313            if ($ret['page'] > $ret['pagecount']) {
314                $ret['page'] = $ret['pagecount'];
315            } elseif ($ret['page'] < 1) {
316                $ret['page'] = 1;
317            }
318
319            $ret['begin'] = (($ret['page'] - 1) * $page_size) + 1;
320            $ret['end'] = $ret['begin'] + $page_size - 1;
321            if ($ret['end'] > $ret['msgcount']) {
322                $ret['end'] = $ret['msgcount'];
323            }
324        } else {
325            $ret['begin'] = 1;
326            $ret['end'] = $ret['msgcount'];
327            $ret['page'] = 1;
328            $ret['pagecount'] = 1;
329        }
330
331        /* If there are no viewable messages, check for deleted messages in
332           the mailbox. */
333        $ret['anymsg'] = true;
334        if (!$ret['msgcount'] && !$this->_mailbox->search) {
335            try {
336                $status = $this->_mailbox->imp_imap->status($this->_mailbox, Horde_Imap_Client::STATUS_MESSAGES);
337                $ret['anymsg'] = (bool)$status['messages'];
338            } catch (IMP_Imap_Exception $e) {
339                $ret['anymsg'] = false;
340            }
341        }
342
343        /* Store the page value now. */
344        $session->set('imp', 'mbox_page/' . $this->_mailbox, $ret['page']);
345
346        return $ret;
347    }
348
349    /**
350     * Returns true if the mailbox data has been built.
351     *
352     * @return boolean  True if the mailbox has been built.
353     */
354    public function isBuilt()
355    {
356        return !is_null($this->_sorted);
357    }
358
359    /**
360     * Builds the sorted list of messages in the mailbox.
361     */
362    protected function _buildMailbox()
363    {
364        $cacheid = $this->_mailbox->cacheid;
365
366        if ($this->isBuilt() && ($this->_cacheid == $cacheid)) {
367            return;
368        }
369
370        $this->changed = true;
371        $this->_cacheid = $cacheid;
372        $this->_sorted = array();
373
374        $query_ob = $this->_buildMailboxQuery();
375        $sortpref = $this->_mailbox->getSort(true);
376        $thread_sort = ($sortpref->sortby == Horde_Imap_Client::SORT_THREAD);
377
378        if ($this->_mailbox->access_search &&
379            $this->_mailbox->hideDeletedMsgs()) {
380            $delete_query = new Horde_Imap_Client_Search_Query();
381            $delete_query->flag(Horde_Imap_Client::FLAG_DELETED, false);
382
383            if (is_null($query_ob)) {
384                $query_ob = array(strval($this->_mailbox) => $delete_query);
385            } else {
386                foreach ($query_ob as $val) {
387                    $val->andSearch($delete_query);
388                }
389            }
390        }
391
392        if (is_null($query_ob)) {
393            $query_ob = array(strval($this->_mailbox) => null);
394        }
395
396        if ($thread_sort) {
397            $this->_thread = $this->_threadui = array();
398        }
399
400        foreach ($query_ob as $mbox => $val) {
401            if ($thread_sort) {
402                $this->_getThread($mbox, $val ? array('search' => $val) : array());
403                $sorted = $this->_thread[$mbox]->messageList()->ids;
404                if ($sortpref->sortdir) {
405                    $sorted = array_reverse($sorted);
406                }
407            } else {
408                $mbox_ob = IMP_Mailbox::get($mbox);
409                if ($mbox_ob->container) {
410                    continue;
411                }
412
413                $res = $mbox_ob->imp_imap->search($mbox, $val, array(
414                    'sort' => array($sortpref->sortby)
415                ));
416                if ($sortpref->sortdir) {
417                    $res['match']->reverse();
418                }
419                $sorted = $res['match']->ids;
420            }
421
422            $this->_sorted = array_merge($this->_sorted, $sorted);
423            $this->_buildMailboxProcess($mbox, $sorted);
424        }
425    }
426
427    /**
428     */
429    protected function _buildMailboxQuery()
430    {
431        return null;
432    }
433
434    /**
435     */
436    protected function _buildMailboxProcess($mbox, $sorted)
437    {
438    }
439
440    /**
441     * Get the list of unseen messages in the mailbox (IMAP UNSEEN flag, with
442     * UNDELETED if we're hiding deleted messages).
443     *
444     * @param integer $results  A Horde_Imap_Client::SEARCH_RESULTS_* constant
445     *                          that indicates the desired return type.
446     * @param array $opts       Additional options:
447     *   - sort: (array) List of sort criteria to use.
448     *   - uids: (boolean) Return UIDs instead of sequence numbers (for
449     *           $results queries that return message lists).
450     *           DEFAULT: false
451     *
452     * @return mixed  Whatever is requested in $results.
453     */
454    public function unseenMessages($results, array $opts = array())
455    {
456        $count = ($results == Horde_Imap_Client::SEARCH_RESULTS_COUNT);
457
458        if (empty($this->_sorted)) {
459            return $count ? 0 : array();
460        }
461
462        $criteria = new Horde_Imap_Client_Search_Query();
463        $imp_imap = $this->_mailbox->imp_imap;
464
465        if ($this->_mailbox->hideDeletedMsgs()) {
466            $criteria->flag(Horde_Imap_Client::FLAG_DELETED, false);
467        } elseif ($count) {
468            try {
469                $status_res = $imp_imap->status($this->_mailbox, Horde_Imap_Client::STATUS_UNSEEN);
470                return $status_res['unseen'];
471            } catch (IMP_Imap_Exception $e) {
472                return 0;
473            }
474        }
475
476        $criteria->flag(Horde_Imap_Client::FLAG_SEEN, false);
477
478        try {
479            $res = $imp_imap->search($this->_mailbox, $criteria, array(
480                'results' => array($results),
481                'sequence' => empty($opts['uids']),
482                'sort' => empty($opts['sort']) ? null : $opts['sort']
483            ));
484            return $count ? $res['count'] : $res;
485        } catch (IMP_Imap_Exception $e) {
486            return $count ? 0 : array();
487        }
488    }
489
490    /**
491     * Determines the sequence number of the first message to display, based
492     * on the user's preferences.
493     *
494     * @param integer $total  The total number of messages in the mailbox.
495     *
496     * @return integer  The sequence number in the sorted mailbox.
497     */
498    public function mailboxStart($total)
499    {
500        switch ($GLOBALS['prefs']->getValue('mailbox_start')) {
501        case IMP::MAILBOX_START_FIRSTPAGE:
502            return 1;
503
504        case IMP::MAILBOX_START_LASTPAGE:
505            return $total;
506
507        case IMP::MAILBOX_START_FIRSTUNSEEN:
508            if (!$this->_mailbox->access_sort) {
509                return 1;
510            }
511
512            $sortpref = $this->_mailbox->getSort();
513
514            /* Optimization: if sorting by sequence then first unseen
515             * information is returned via a SELECT/EXAMINE call. */
516            if ($sortpref->sortby == Horde_Imap_Client::SORT_SEQUENCE) {
517                try {
518                    $res = $this->_mailbox->imp_imap->status($this->_mailbox, Horde_Imap_Client::STATUS_FIRSTUNSEEN | Horde_Imap_Client::STATUS_MESSAGES);
519                    if (!is_null($res['firstunseen'])) {
520                        return $sortpref->sortdir
521                            ? ($res['messages'] - $res['firstunseen'] + 1)
522                            : $res['firstunseen'];
523                    }
524                } catch (IMP_Imap_Exception $e) {}
525
526                return 1;
527            }
528
529            $unseen_msgs = $this->unseenMessages(Horde_Imap_Client::SEARCH_RESULTS_MIN, array(
530                'sort' => array(Horde_Imap_Client::SORT_DATE),
531                'uids' => true
532            ));
533            return empty($unseen_msgs['min'])
534                ? 1
535                : ($this->getArrayIndex($unseen_msgs['min']) + 1);
536
537        case IMP::MAILBOX_START_LASTUNSEEN:
538            if (!$this->_mailbox->access_sort) {
539                return 1;
540            }
541
542            $unseen_msgs = $this->unseenMessages(Horde_Imap_Client::SEARCH_RESULTS_MAX, array(
543                'sort' => array(Horde_Imap_Client::SORT_DATE),
544                'uids' => true
545            ));
546            return empty($unseen_msgs['max'])
547                ? 1
548                : ($this->getArrayIndex($unseen_msgs['max']) + 1);
549        }
550    }
551
552    /**
553     * Rebuilds/resets the mailbox list.
554     *
555     * @param boolean $reset  If true, resets the list instead of rebuilding.
556     */
557    public function rebuild($reset = false)
558    {
559        $this->_cacheid = $this->_sorted = null;
560
561        if ($reset) {
562            $this->_buidmax = 0;
563            $this->_buids = array();
564            $this->changed = true;
565        } else {
566            $this->_buildMailbox();
567        }
568    }
569
570    /**
571     * Returns the array index of the given message UID.
572     *
573     * @param integer $uid  The message UID.
574     * @param string $mbox  The message mailbox (defaults to the current
575     *                      mailbox).
576     *
577     * @return mixed  The array index of the location of the message UID in
578     *                the current mailbox. Returns null if not found.
579     */
580    public function getArrayIndex($uid, $mbox = null)
581    {
582        $this->_buildMailbox();
583
584        /* array_search() returns false on no result. We will set an
585         * unsuccessful result to NULL. */
586        return (($aindex = array_search($uid, $this->_sorted)) === false)
587            ? null
588            : $aindex;
589    }
590
591    /**
592     * Generate an IMP_Indices object out of the contents of this mailbox.
593     *
594     * @return IMP_Indices  An indices object.
595     */
596    public function getIndicesOb()
597    {
598        $this->_buildMailbox();
599
600        return new IMP_Indices($this->_mailbox, $this->_sorted);
601    }
602
603    /**
604     * Removes messages from the mailbox.
605     *
606     * @param mixed $indices  An IMP_Indices object or true to remove all
607     *                        messages in the mailbox.
608     *
609     * @return boolean  True if the message was removed from the mailbox.
610     */
611    public function removeMsgs($indices)
612    {
613        if ($indices === true) {
614            $this->rebuild();
615            return false;
616        }
617
618        if (!count($indices)) {
619            return false;
620        }
621
622        /* Remove the current entry and recalculate the range. */
623        foreach ($indices as $ob) {
624            foreach ($ob->uids as $uid) {
625                unset($this->_sorted[$this->getArrayIndex($uid, $ob->mbox)]);
626            }
627        }
628
629        $this->changed = true;
630        $this->_sorted = array_values($this->_sorted);
631
632        if (isset($this->_thread[strval($ob->mbox)])) {
633            unset($this->_thread[strval($ob->mbox)], $this->_threadui[strval($ob->mbox)]);
634        }
635
636        if (!is_null($this->_index)) {
637            $this->setIndex(0);
638        }
639
640        return true;
641    }
642
643    /**
644     * Returns the list of UIDs for an entire thread given one message in
645     * that thread.
646     *
647     * @param integer $uid  The message UID.
648     * @param string $mbox  The message mailbox (defaults to the current
649     *                      mailbox).
650     *
651     * @return IMP_Indices  An indices object.
652     */
653    public function getFullThread($uid, $mbox = null)
654    {
655        if (is_null($mbox)) {
656            $mbox = $this->_mailbox;
657        }
658
659        return new IMP_Indices($mbox, array_keys($this->_getThread($mbox)->getThread($uid)));
660    }
661
662    /**
663     * Returns a thread object for a message.
664     *
665     * @param integer $offset  Sequence number of message.
666     *
667     * @return IMP_Mailbox_List_Thread  The thread object.
668     */
669    public function getThreadOb($offset)
670    {
671        $entry = $this[$offset];
672        $mbox = strval($entry['m']);
673        $uid = $entry['u'];
674
675        if (!isset($this->_threadui[$mbox][$uid])) {
676            $thread_level = array();
677            $t_ob = $this->_getThread($mbox);
678
679            foreach ($t_ob->getThread($uid) as $key => $val) {
680                if (is_null($val->base) ||
681                    ($val->last && ($val->base == $key))) {
682                    $this->_threadui[$mbox][$key] = '';
683                    continue;
684                }
685
686                if ($val->last) {
687                    $join = IMP_Mailbox_List_Thread::JOINBOTTOM;
688                } else {
689                    $join = (!$val->level && ($val->base == $key))
690                        ? IMP_Mailbox_List_Thread::JOINBOTTOM_DOWN
691                        : IMP_Mailbox_List_Thread::JOIN;
692                }
693
694                $thread_level[$val->level] = $val->last;
695                $line = '';
696
697                for ($i = 0; $i < $val->level; ++$i) {
698                    if (isset($thread_level[$i])) {
699                        $line .= (isset($thread_level[$i]) && !$thread_level[$i])
700                            ? IMP_Mailbox_List_Thread::LINE
701                            : IMP_Mailbox_List_Thread::BLANK;
702                    }
703                }
704
705                $this->_threadui[$mbox][$key] = $line . $join;
706            }
707        }
708
709        return new IMP_Mailbox_List_Thread($this->_threadui[$mbox][$uid]);
710    }
711
712    /**
713     * Returns the thread object for a mailbox.
714     *
715     * @param string $mbox  The mailbox.
716     * @param array $extra  Extra options to pass to IMAP thread() command.
717     *
718     * @return Horde_Imap_Client_Data_Thread  Thread object.
719     */
720    protected function _getThread($mbox, array $extra = array())
721    {
722        if (!isset($this->_thread[strval($mbox)])) {
723            $imp_imap = IMP_Mailbox::get($mbox)->imp_imap;
724
725            try {
726                $thread = $imp_imap->thread($mbox, array_merge($extra, array(
727                    'criteria' => $imp_imap->thread_algo
728                )));
729            } catch (Horde_Imap_Client_Exception $e) {
730                $thread = new Horde_Imap_Client_Data_Thread(array(), 'uid');
731            }
732
733            $this->_thread[strval($mbox)] = $thread;
734        }
735
736        return $this->_thread[strval($mbox)];
737    }
738
739    /**
740     * Get the mailbox for a sequence ID.
741     *
742     * @param integer $id  Sequence ID.
743     *
744     * @return IMP_Mailbox  The mailbox.
745     */
746    protected function _getMbox($id)
747    {
748        return $this->_mailbox;
749    }
750
751    /* Pseudo-UID related methods. */
752
753    /**
754     * Create a browser-UID from a mail UID.
755     *
756     * @param string $mbox  The mailbox.
757     * @param integer $uid  UID.
758     *
759     * @return integer  Browser-UID.
760     */
761    public function getBuid($mbox, $uid)
762    {
763        return $uid;
764    }
765
766    /**
767     * Resolve a mail UID from a browser-UID.
768     *
769     * @param integer $buid  Browser-UID.
770     *
771     * @return array  Two-element array:
772     *   - m: (IMP_Mailbox) Mailbox of message.
773     *   - u: (string) UID of message.
774     */
775    public function resolveBuid($buid)
776    {
777        return array(
778            'm' => $this->_mailbox,
779            'u' => intval($buid)
780        );
781    }
782
783    /* Tracking related methods. */
784
785    /**
786     * Returns the current message array index. If the array index has
787     * run off the end of the message array, will return the first index.
788     *
789     * @return integer  The message array index.
790     */
791    public function getIndex()
792    {
793        return $this->isValidIndex()
794            ? ($this->_index + 1)
795            : 1;
796    }
797
798    /**
799     * Checks to see if the current index is valid.
800     *
801     * @return boolean  True if index is valid, false if not.
802     */
803    public function isValidIndex()
804    {
805        return !is_null($this->_index);
806    }
807
808    /**
809     * Updates the message array index.
810     *
811     * @param mixed $data  If an integer, the number of messages to increase
812     *                     the array index by. If an indices object, sets
813     *                     array index to the index value.
814     */
815    public function setIndex($data)
816    {
817        if ($data instanceof IMP_Indices) {
818            list($mailbox, $uid) = $data->getSingle();
819            $this->_index = $this->getArrayIndex($uid, $mailbox);
820            if (is_null($this->_index)) {
821                $this->rebuild();
822                $this->_index = $this->getArrayIndex($uid, $mailbox);
823            }
824        } else {
825            $index = $this->_index += $data;
826            if (isset($this->_sorted[$this->_index])) {
827                if (!isset($this->_sorted[$this->_index + 1])) {
828                    $this->rebuild();
829                }
830            } else {
831                $this->rebuild();
832                $this->_index = isset($this->_sorted[$index])
833                    ? $index
834                    : null;
835            }
836        }
837    }
838
839    /* ArrayAccess methods. */
840
841    /**
842     * @param integer $offset  Sequence number of message.
843     */
844    public function offsetExists($offset)
845    {
846        return isset($this->_sorted[$offset - 1]);
847    }
848
849    /**
850     * @param integer $offset  Sequence number of message.
851     *
852     * @return array  Two-element array:
853     *   - m: (IMP_Mailbox) Mailbox of message.
854     *   - u: (string) UID of message.
855     */
856    public function offsetGet($offset)
857    {
858        if (!isset($this->_sorted[$offset - 1])) {
859            return null;
860        }
861
862        $ret = array(
863            'm' => $this->_getMbox($offset - 1),
864            'u' => $this->_sorted[$offset - 1]
865        );
866
867        return $ret;
868    }
869
870    /**
871     * @throws BadMethodCallException
872     */
873    public function offsetSet($offset, $value)
874    {
875        throw new BadMethodCallException('Not supported');
876    }
877
878    /**
879     * @throws BadMethodCallException
880     */
881    public function offsetUnset($offset)
882    {
883        throw new BadMethodCallException('Not supported');
884    }
885
886    /* Countable methods. */
887
888    /**
889     * Returns the current message count of the mailbox.
890     *
891     * @return integer  The mailbox message count.
892     */
893    public function count()
894    {
895        $this->_buildMailbox();
896        return count($this->_sorted);
897    }
898
899    /* Iterator methods. */
900
901    /**
902     * @return array  Two-element array:
903     *   - m: (IMP_Mailbox) Mailbox of message.
904     *   - u: (string) UID of message.
905     */
906    public function current()
907    {
908        return $this[key($this->_sorted) + 1];
909    }
910
911    /**
912     * @return integer  Sequence number of message.
913     */
914    public function key()
915    {
916        return (key($this->_sorted) + 1);
917    }
918
919    /**
920     */
921    public function next()
922    {
923        next($this->_sorted);
924    }
925
926    /**
927     */
928    public function rewind()
929    {
930        reset($this->_sorted);
931    }
932
933    /**
934     */
935    public function valid()
936    {
937        return (key($this->_sorted) !== null);
938    }
939
940    /* Serializable methods. */
941
942    /**
943     * Serialization.
944     *
945     * @return string  Serialized data.
946     */
947    public function serialize()
948    {
949        return $GLOBALS['injector']->getInstance('Horde_Pack')->pack(
950            $this->_serialize(),
951            array(
952                'compression' => false,
953                'phpob' => true
954            )
955        );
956    }
957
958    /**
959     */
960    protected function _serialize()
961    {
962        $data = array(
963            'm' => $this->_mailbox
964        );
965
966        if ($this->_buidmax) {
967            $data['bm'] = $this->_buidmax;
968            if (!empty($this->_buids)) {
969                $data['b'] = $this->_buids;
970            }
971        }
972
973        if (!is_null($this->_cacheid)) {
974            $data['c'] = $this->_cacheid;
975        }
976
977        if (!is_null($this->_sorted)) {
978            /* Store UIDs in sequence string to save on storage space. */
979            if (count($this->_sorted) > self::SERIALIZE_LIMIT) {
980                $ids = $this->_mailbox->imp_imap->getIdsOb();
981                /* Optimization: we know there are no duplicates in sorted. */
982                $ids->duplicates = true;
983                $ids->add($this->_sorted);
984                $data['so'] = $ids;
985            } else {
986                $data['so'] = $this->_sorted;
987            }
988        }
989
990        return $data;
991    }
992
993    /**
994     * Unserialization.
995     *
996     * @param string $data  Serialized data.
997     *
998     * @throws Exception
999     */
1000    public function unserialize($data)
1001    {
1002        $this->_unserialize(
1003            $GLOBALS['injector']->getInstance('Horde_Pack')->unpack($data)
1004        );
1005    }
1006
1007    /**
1008     */
1009    protected function _unserialize($data)
1010    {
1011        $this->_mailbox = $data['m'];
1012
1013        if (isset($data['bm'])) {
1014            $this->_buidmax = $data['bm'];
1015            if (isset($data['b'])) {
1016                $this->_buids = $data['b'];
1017            }
1018        }
1019
1020        if (isset($data['c'])) {
1021            $this->_cacheid = $data['c'];
1022        }
1023
1024        if (isset($data['so'])) {
1025            $this->_sorted = is_object($data['so'])
1026                ? $data['so']->ids
1027                : $data['so'];
1028        }
1029    }
1030
1031}
1032