1<?php
2/**
3 * @license   http://www.horde.org/licenses/gpl GPLv2
4 *
5 * @copyright 2012-2020 Horde LLC (http://www.horde.org)
6 * @author    Michael J Rubinsky <mrubinsk@horde.org>
7 * @package   ActiveSync
8 */
9
10/**
11 * Handles fetching changes using the HIGHESTMODSEQ value of a
12 * QRESYNC/CONDSTORE enabled IMAP server.
13 *
14 * @license   http://www.horde.org/licenses/gpl GPLv2
15 *
16 * @copyright 2012-2020 Horde LLC (http://www.horde.org)
17 * @author    Michael J Rubinsky <mrubinsk@horde.org>
18 * @package   ActiveSync
19 */
20class Horde_ActiveSync_Imap_Strategy_Modseq extends Horde_ActiveSync_Imap_Strategy_Base
21{
22    /**
23     * Flag to indicate if the HIGHESTMODSEQ value returned in the STATUS call
24     * is to be trusted.
25     *
26     * @var boolean
27     */
28    protected $_modseq_valid = true;
29
30    /**
31     * Const'r
32     *
33     * @param Horde_ActiveSync_Interface_ImapFactory $imap The IMAP factory.
34     * @param array $status                         The IMAP status array.
35     * @param Horde_ActiveSync_Folder_Base $folder  The folder object.
36     * @param Horde_Log_Logger $logger              The logger.
37     */
38    public function __construct(
39        Horde_ActiveSync_Interface_ImapFactory $imap,
40        array $status,
41        Horde_ActiveSync_Folder_Base $folder,
42        $logger)
43    {
44        // If IMAP server reports invalid MODSEQ, this can lead to the client
45        // no longer ever able to detect changes therefore never receiving new
46        // email even if the value is restored at some point in the future.
47        //
48        // This can happen, e.g., if the IMAP server index files are lost or
49        // otherwise corrupted. Normally this would be handled as a loss of
50        // server state and handled by a complete resync, but a majority of
51        // EAS clients do not properly handle the status codes that report this.
52        parent::__construct($imap, $status, $folder, $logger);
53        if ($folder->modseq() > $this->_status[Horde_ActiveSync_Folder_Imap::HIGHESTMODSEQ]) {
54            $this->_logger->err('IMAP Server error: Current HIGHESTMODSEQ is lower than previously reported.');
55            $this->_modseq_valid = false;
56        }
57    }
58
59    /**
60     * Return a folder object containing all IMAP server change information.
61     *
62     * @param array $options  An array of options.
63     *        @see Horde_ActiveSync_Imap_Adapter::getMessageChanges
64     *
65     * @return Horde_ActiveSync_Folder_Base  The populated folder object.
66     */
67    public function getChanges(array $options)
68    {
69        $this->_logger->meta('CONDSTORE and CHANGES');
70        $flags = array();
71        $current_modseq = $this->_status[Horde_ActiveSync_Folder_Imap::HIGHESTMODSEQ];
72        $query = new Horde_Imap_Client_Search_Query();
73
74        // Increment since $imap->search uses >= operator.
75        if ($this->_modseq_valid) {
76            $query->modseq($this->_folder->modseq() + 1);
77        }
78
79        if (!empty($options['sincedate'])) {
80            $query->dateSearch(
81                new Horde_Date($options['sincedate']),
82                Horde_Imap_Client_Search_Query::DATE_SINCE
83            );
84        }
85
86        $search_ret = $this->_imap_ob->search(
87            $this->_mbox,
88            $query,
89            array('results' => array(Horde_Imap_Client::SEARCH_RESULTS_MATCH))
90        );
91
92        $search_uids = $search_ret['count']
93            ? $search_ret['match']->ids
94            : array();
95
96        // Catch changes to FILTERTYPE.
97        if (!empty($options['refreshfilter'])) {
98            $this->_logger->meta('Checking for additional messages within the new FilterType parameters.');
99            $search_ret = $this->_searchQuery($options, false);
100            if ($search_ret['count']) {
101                $this->_logger->meta(sprintf(
102                    'Found %d messages that are now outside FilterType.',
103                    $search_ret['count'])
104                );
105                $search_uids = array_merge($search_uids, $search_ret['match']->ids);
106            }
107        }
108
109        // Protect against very large change sets.
110        $cnt = (count($search_uids) / Horde_ActiveSync_Imap_Adapter::MAX_FETCH) + 1;
111        $query = new Horde_Imap_Client_Fetch_Query();
112        if ($this->_modseq_valid) {
113            $query->modseq();
114        }
115        $query->flags();
116        $changes = array();
117        $categories = array();
118        for ($i = 0; $i <= $cnt; $i++) {
119            $ids = new Horde_Imap_Client_Ids(
120                array_slice(
121                    $search_uids,
122                    $i * Horde_ActiveSync_Imap_Adapter::MAX_FETCH, Horde_ActiveSync_Imap_Adapter::MAX_FETCH
123                )
124            );
125            try {
126                $fetch_ret = $this->_imap_ob->fetch(
127                    $this->_mbox,
128                    $query,
129                    array('ids' => $ids)
130                );
131            } catch (Horde_Imap_Client_Exception $e) {
132                $this->_logger->err($e->getMessage());
133                throw new Horde_ActiveSync_Exception($e);
134            }
135            $this->_buildModSeqChanges(
136                $changes, $flags, $categories, $fetch_ret, $options, $current_modseq
137            );
138        }
139
140        // Set the changes in the folder object.
141        $this->_folder->setChanges(
142            $changes,
143            $flags,
144            $categories,
145            !empty($options['softdelete']) || !empty($options['refreshfilter'])
146        );
147
148        // Check for deleted messages.
149        try {
150            $deleted = $this->_imap_ob->vanished(
151                $this->_mbox,
152                $this->_folder->modseq(),
153                array('ids' => new Horde_Imap_Client_Ids($this->_folder->messages())));
154        } catch (Horde_Imap_Client_Excetion $e) {
155            $this->_logger->err($e->getMessage());
156            throw new Horde_ActiveSync_Exception($e);
157        }
158        $this->_folder->setRemoved($deleted->ids);
159        $this->_logger->meta(sprintf(
160            'Found %d deleted messages.',
161            $deleted->count())
162        );
163
164        // Check for SOFTDELETE messages.
165        if (!empty($options['sincedate']) &&
166            (!empty($options['softdelete']) || !empty($options['refreshfilter']))) {
167            $this->_logger->meta(sprintf(
168                'Polling for SOFTDELETE in %s before %d',
169                $this->_folder->serverid(), $options['sincedate'])
170            );
171            $search_ret = $this->_searchQuery($options, true);
172            if ($search_ret['count']) {
173                $this->_logger->meta(sprintf(
174                    'Found %d messages to SOFTDELETE.',
175                    count($search_ret['match']->ids))
176                );
177                $this->_folder->setSoftDeleted($search_ret['match']->ids);
178            }
179            $this->_folder->setSoftDeleteTimes($options['sincedate'], time());
180        }
181
182        return $this->_folder;
183    }
184
185    /**
186     * Return message UIDs that are now within the cureent FILTERTYPE value.
187     *
188     * @param  array                        $options   Options array.
189     * @param  boolean                      $is_delete If true, return messages
190     *                                                 to SOFTDELETE.
191     *
192     * @return Horde_Imap_Client_Search_Results
193     */
194    protected function _searchQuery($options, $is_delete)
195    {
196        $query = new Horde_Imap_Client_Search_Query();
197        $query->dateSearch(
198            new Horde_Date($options['sincedate']),
199            $is_delete
200                ? Horde_Imap_Client_Search_Query::DATE_BEFORE
201                : Horde_Imap_Client_Search_Query::DATE_SINCE
202        );
203        $query->ids(new Horde_Imap_Client_Ids($this->_folder->messages()), !$is_delete);
204        try {
205            return $this->_imap_ob->search(
206                $this->_mbox,
207                $query,
208                array('results' => array(Horde_Imap_Client::SEARCH_RESULTS_MATCH)));
209        } catch (Horde_Imap_Client_Exception $e) {
210            $this->_logger->err($e->getMessage());
211            throw new Horde_ActiveSync_Exception($e);
212        }
213    }
214
215    /**
216     * Populates the changes, flags, and categories arrays with data from
217     * any messages added/changed on the IMAP server since the last poll.
218     *
219     * @param array &$changes                             Changes array.
220     * @param array &$flags                               Flags array.
221     * @param array &$categories                          Categories array.
222     * @param Horde_Imap_Client_Fetch_Results $fetch_ret  Fetch results.
223     * @param array $options                              Options array.
224     * @param integer $modseq                             Current MODSEQ.
225     */
226    protected function _buildModSeqChanges(
227        &$changes, &$flags, &$categories, $fetch_ret, $options, $modseq)
228    {
229        // Get custom flags to use as categories.
230        $msgFlags = $this->_getMsgFlags();
231
232        // Filter out any changes that we already know about.
233        $fetch_keys = $fetch_ret->ids();
234        $result_set = array_diff($fetch_keys, $changes);
235
236        foreach ($result_set as $uid) {
237            // Ensure no changes after the current modseq have been returned.
238            $data = $fetch_ret[$uid];
239            if ($data->getModSeq() <= $modseq) {
240                $changes[] = $uid;
241                $flags[$uid] = array(
242                    'read' => (array_search(Horde_Imap_Client::FLAG_SEEN, $data->getFlags()) !== false) ? 1 : 0
243                );
244                if (($options['protocolversion']) > Horde_ActiveSync::VERSION_TWOFIVE) {
245                    $flags[$uid]['flagged'] = (array_search(Horde_Imap_Client::FLAG_FLAGGED, $data->getFlags()) !== false) ? 1 : 0;
246                }
247                if ($options['protocolversion'] > Horde_ActiveSync::VERSION_TWELVEONE) {
248                    $categories[$uid] = array();
249                    foreach ($data->getFlags() as $flag) {
250                        if (!empty($msgFlags[Horde_String::lower($flag)])) {
251                            $categories[$uid][] = $msgFlags[Horde_String::lower($flag)];
252                        }
253                    }
254                }
255            }
256        }
257    }
258
259}
260