1<?php
2/**
3 * Copyright 2012-2017 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file COPYING for license information (LGPL). If you
6 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
7 *
8 * @category  Horde
9 * @copyright 2012-2017 Horde LLC
10 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
11 * @package   Imap_Client
12 */
13
14/**
15 * An object implementing lookups between UIDs and message sequence numbers.
16 *
17 * @author    Michael Slusarz <slusarz@horde.org>
18 * @category  Horde
19 * @copyright 2012-2017 Horde LLC
20 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
21 * @package   Imap_Client
22 * @since     2.1.0
23 *
24 * @property-read array $map  The raw ID mapping data.
25 * @property-read Horde_Imap_Client_Ids $seq  The sorted sequence values.
26 * @property-read Horde_Imap_Client_Ids $uids  The sorted UIDs.
27 */
28class Horde_Imap_Client_Ids_Map implements Countable, IteratorAggregate, Serializable
29{
30    /**
31     * Sequence -> UID mapping.
32     *
33     * @var array
34     */
35    protected $_ids = array();
36
37    /**
38     * Is the array sorted?
39     *
40     * @var boolean
41     */
42    protected $_sorted = true;
43
44    /**
45     * Constructor.
46     *
47     * @param array $ids  Array of sequence -> UID mapping.
48     */
49    public function __construct(array $ids = array())
50    {
51        $this->update($ids);
52    }
53
54    /**
55     */
56    public function __get($name)
57    {
58        switch ($name) {
59        case 'map':
60            return $this->_ids;
61
62        case 'seq':
63            $this->sort();
64            return new Horde_Imap_Client_Ids(array_keys($this->_ids), true);
65
66        case 'uids':
67            $this->sort();
68            return new Horde_Imap_Client_Ids($this->_ids);
69        }
70    }
71
72    /**
73     * Updates the mapping.
74     *
75     * @param array $ids  Array of sequence -> UID mapping.
76     *
77     * @return boolean  True if the mapping changed.
78     */
79    public function update($ids)
80    {
81        if (empty($ids)) {
82            return false;
83        } elseif (empty($this->_ids)) {
84            $this->_ids = $ids;
85            $change = true;
86        } else {
87            $change = false;
88            foreach ($ids as $k => $v) {
89                if (!isset($this->_ids[$k]) || ($this->_ids[$k] != $v)) {
90                    $this->_ids[$k] = $v;
91                    $change = true;
92                }
93            }
94        }
95
96        if ($change) {
97            $this->_sorted = false;
98        }
99
100        return $change;
101    }
102
103    /**
104     * Create a Sequence <-> UID lookup table.
105     *
106     * @param Horde_Imap_Client_Ids $ids  IDs to lookup.
107     *
108     * @return array  Keys are sequence numbers, values are UIDs.
109     */
110    public function lookup(Horde_Imap_Client_Ids $ids)
111    {
112        if ($ids->all) {
113            return $this->_ids;
114        } elseif ($ids->sequence) {
115            return array_intersect_key($this->_ids, array_flip($ids->ids));
116        }
117
118        return array_intersect($this->_ids, $ids->ids);
119    }
120
121    /**
122     * Removes messages from the ID mapping.
123     *
124     * @param Horde_Imap_Client_Ids $ids  IDs to remove.
125     */
126    public function remove(Horde_Imap_Client_Ids $ids)
127    {
128        /* For sequence numbers, we need to reindex anytime we have an index
129         * that appears equal to or after a previously seen index. If an IMAP
130         * server is smart, it will expunge in reverse order instead. */
131        if ($ids->sequence) {
132            $remove = $ids->ids;
133        } else {
134            $ids->sort();
135            $remove = array_reverse(array_keys($this->lookup($ids)));
136        }
137
138        if (empty($remove)) {
139            return;
140        }
141
142        $this->sort();
143
144        if (count($remove) == count($this->_ids) &&
145            !array_diff($remove, array_keys($this->_ids))) {
146            $this->_ids = array();
147            return;
148        }
149
150        /* Find the minimum sequence number to remove. We know entries before
151         * this are untouched so no need to process them multiple times. */
152        $first = min($remove);
153        $edit = $newids = array();
154        foreach (array_keys($this->_ids) as $i => $seq) {
155            if ($seq >= $first) {
156                $i += (($seq == $first) ? 0 : 1);
157                $newids = array_slice($this->_ids, 0, $i, true);
158                $edit = array_slice($this->_ids, $i + (($seq == $first) ? 0 : 1), null, true);
159                break;
160            }
161        }
162
163        if (!empty($edit)) {
164            foreach ($remove as $val) {
165                $found = false;
166                $tmp = array();
167
168                foreach (array_keys($edit) as $i => $seq) {
169                    if ($found) {
170                        $tmp[$seq - 1] = $edit[$seq];
171                    } elseif ($seq >= $val) {
172                        $tmp = array_slice($edit, 0, ($seq == $val) ? $i : $i + 1, true);
173                        $found = true;
174                    }
175                }
176
177                $edit = $tmp;
178            }
179        }
180
181        $this->_ids = $newids + $edit;
182    }
183
184    /**
185     * Sort the map.
186     */
187    public function sort()
188    {
189        if (!$this->_sorted) {
190            ksort($this->_ids, SORT_NUMERIC);
191            $this->_sorted = true;
192        }
193    }
194
195    /* Countable methods. */
196
197    /**
198     */
199    public function count()
200    {
201        return count($this->_ids);
202    }
203
204    /* IteratorAggregate method. */
205
206    /**
207     */
208    public function getIterator()
209    {
210        return new ArrayIterator($this->_ids);
211    }
212
213    /* Serializable methods. */
214
215    /**
216     */
217    public function serialize()
218    {
219        /* Sort before storing; provides more compressible representation. */
220        $this->sort();
221
222        return json_encode(array(
223            strval(new Horde_Imap_Client_Ids(array_keys($this->_ids))),
224            strval(new Horde_Imap_Client_Ids(array_values($this->_ids)))
225        ));
226    }
227
228    /**
229     */
230    public function unserialize($data)
231    {
232        $data = json_decode($data, true);
233
234        $keys = new Horde_Imap_Client_Ids($data[0]);
235        $vals = new Horde_Imap_Client_Ids($data[1]);
236        $this->_ids = array_combine($keys->ids, $vals->ids);
237
238        /* Guaranteed to be sorted if unserializing. */
239        $this->_sorted = true;
240    }
241
242}
243