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