/**
* Return message changes from the specified mailbox.
*
* @param Horde_ActiveSync_Folder_Imap $folder The folder object.
* @param array $options Additional options:
* - sincedate: (integer) Timestamp of earliest message to retrieve.
* DEFAULT: 0 (Don't filter).
* - protocolversion: (float) EAS protocol version to support.
* DEFAULT: none REQUIRED
* - softdelete: (boolean) If true, calculate SOFTDELETE data.
* @since 2.8.0
* - refreshfilter: (boolean) If true, force refresh the query to reflect
* changes in FILTERTYPE (using the sincedate)
* @since 2.28.0
*
* @return Horde_ActiveSync_Folder_Imap The folder object, containing any
* change instructions for the device.
*
* @throws Horde_ActiveSync_Exception_FolderGone,
* Horde_ActiveSync_Exception, Horde_ActiveSync_Exception_StaleState
*/
public function getMessageChanges(Horde_ActiveSync_Folder_Imap $folder, array $options = array())
{
$imap = $this->_getImapOb();
$mbox = new Horde_Imap_Client_Mailbox($folder->serverid());
$flags = array();
$search_uids = array();
// Note: non-CONDSTORE servers will return a highestmodseq of 0
$status_flags = Horde_Imap_Client::STATUS_HIGHESTMODSEQ | Horde_Imap_Client::STATUS_UIDVALIDITY | Horde_Imap_Client::STATUS_UIDNEXT_FORCE | Horde_Imap_Client::STATUS_MESSAGES | Horde_Imap_Client::STATUS_FORCE_REFRESH;
try {
$status = $imap->status($mbox, $status_flags);
} catch (Horde_Imap_Client_Exception $e) {
// If we can't status the mailbox, assume it's gone.
throw new Horde_ActiveSync_Exception_FolderGone($e);
}
$this->_logger->info(sprintf('[%s] IMAP status: %s', $this->_procid, serialize($status)));
$current_modseq = $status[Horde_ActiveSync_Folder_Imap::HIGHESTMODSEQ];
if ($current_modseq && $folder->modseq() > 0 && ($folder->modseq() < $current_modseq || !empty($options['softdelete']) || !empty($options['refreshfilter']))) {
$this->_logger->info(sprintf('[%s] CONDSTORE and CHANGES', $this->_procid));
$folder->checkValidity($status);
// Catch all *changes* since the provided MODSEQ value.
$query = new Horde_Imap_Client_Search_Query();
// $imap->search uses a >= comparison for MODSEQ, so we must
// increment by one so we don't continuously receive the same change
// set.
$query->modseq($folder->modseq() + 1);
if (!empty($options['sincedate'])) {
$query->dateSearch(new Horde_Date($options['sincedate']), Horde_Imap_Client_Search_Query::DATE_SINCE);
}
$search_ret = $imap->search($mbox, $query, array('results' => array(Horde_Imap_Client::SEARCH_RESULTS_MATCH)));
$search_uids = $search_ret['count'] ? $search_ret['match']->ids : array();
// Catch changes to FILTERTYPE.
if (!empty($options['refreshfilter'])) {
$this->_logger->info(sprintf('[%s] Checking for additional messages within the new FilterType parameters.', $this->_procid));
$search_ret = $this->_buildSearchQuery($folder, $options, $mbox, false);
if ($search_ret['count']) {
$this->_logger->info(sprintf('[%s] Found %d messages that are now outside FilterType.', $this->_procid, $search_ret['count']));
$search_uids = array_merge($search_uids, $search_ret['match']->ids);
} else {
$this->_logger->info(sprintf('[%s] Found NO additional messages.', $this->_procid));
}
}
// Protect against very large change sets like might occur if
// the FILTERTYPE is changed from some short interval like one week
// to no filter at all.
$cnt = count($search_uids) / self::MAX_FETCH + 1;
$query = new Horde_Imap_Client_Fetch_Query();
$query->modseq();
$query->flags();
$changes = array();
$categories = array();
for ($i = 0; $i <= $cnt; $i++) {
$ids = new Horde_Imap_Client_Ids(array_slice($search_uids, $i * self::MAX_FETCH, self::MAX_FETCH));
try {
$fetch_ret = $imap->fetch($mbox, $query, array('ids' => $ids));
} catch (Horde_Imap_Client_Exception $e) {
$this->_logger->err($e->getMessage());
throw new Horde_ActiveSync_Exception($e);
}
$this->_buildModSeqChanges($changes, $flags, $categories, $fetch_ret, $options, $current_modseq);
}
// Set the changes in the folder object.
$folder->setChanges($changes, $flags, $categories, !empty($options['softdelete']) || !empty($options['refreshfilter']));
// Check for deleted messages.
try {
$deleted = $imap->vanished($mbox, $folder->modseq(), array('ids' => new Horde_Imap_Client_Ids($folder->messages())));
} catch (Horde_Imap_Client_Excetion $e) {
$this->_logger->err($e->getMessage());
throw new Horde_ActiveSync_Exception($e);
}
$folder->setRemoved($deleted->ids);
$this->_logger->info(sprintf('[%s] Found %d deleted messages.', $this->_procid, $deleted->count()));
// Check for SOFTDELETE messages.
if ((!empty($options['softdelete']) || !empty($options['refreshfilter'])) && !empty($options['sincedate'])) {
$this->_logger->info(sprintf('[%s] Polling for SOFTDELETE in %s before %d', $this->_procid, $folder->serverid(), $options['sincedate']));
$search_ret = $this->_buildSearchQuery($folder, $options, $mbox, true);
if ($search_ret['count']) {
$this->_logger->info(sprintf('[%s] Found %d messages to SOFTDELETE.', $this->_procid, count($search_ret['match']->ids)));
$folder->setSoftDeleted($search_ret['match']->ids);
} else {
$this->_logger->info(sprintf('[%s] Found NO messages to SOFTDELETE.', $this->_procid));
}
$folder->setSoftDeleteTimes($options['sincedate'], time());
}
} elseif ($folder->uidnext() == 0) {
$this->_logger->info(sprintf('[%s] INITIAL SYNC', $this->_procid));
$query = new Horde_Imap_Client_Search_Query();
if (!empty($options['sincedate'])) {
$query->dateSearch(new Horde_Date($options['sincedate']), Horde_Imap_Client_Search_Query::DATE_SINCE);
}
$search_ret = $imap->search($mbox, $query, array('results' => array(Horde_Imap_Client::SEARCH_RESULTS_MATCH)));
if ($current_modseq && !$folder->haveInitialSync) {
$this->_logger->info(sprintf('[%s] Priming IMAP folder object.', $this->_procid));
$folder->primeFolder($search_ret['match']->ids);
} elseif (count($search_ret['match']->ids)) {
// No modseq.
$query = new Horde_Imap_Client_Fetch_Query();
$query->flags();
$cnt = $search_ret['count'] / self::MAX_FETCH + 1;
for ($i = 0; $i <= $cnt; $i++) {
$ids = new Horde_Imap_Client_Ids(array_slice($search_ret['match']->ids, $i * self::MAX_FETCH, self::MAX_FETCH));
$fetch_ret = $imap->fetch($mbox, $query, array('ids' => $ids));
foreach ($fetch_ret as $uid => $data) {
$flags[$uid] = array('read' => array_search(Horde_Imap_Client::FLAG_SEEN, $data->getFlags()) !== false ? 1 : 0);
if ($options['protocolversion'] > Horde_ActiveSync::VERSION_TWOFIVE) {
$flags[$uid]['flagged'] = array_search(Horde_Imap_Client::FLAG_FLAGGED, $data->getFlags()) !== false ? 1 : 0;
}
}
}
$folder->setChanges($search_ret['match']->ids, $flags);
}
} elseif ($current_modseq == 0) {
$this->_logger->info(sprintf('[%s] NO CONDSTORE or per mailbox MODSEQ. minuid: %s, total_messages: %s', $this->_procid, $folder->minuid(), $status['messages']));
$folder->checkValidity($status);
$query = new Horde_Imap_Client_Search_Query();
if (!empty($options['sincedate'])) {
$query->dateSearch(new Horde_Date($options['sincedate']), Horde_Imap_Client_Search_Query::DATE_SINCE);
}
try {
$search_ret = $imap->search($mbox, $query, array('results' => array(Horde_Imap_Client::SEARCH_RESULTS_MATCH)));
} catch (Horde_Imap_Client_Exception $e) {
$this->_logger->err($e->getMessage());
throw new Horde_ActiveSync_Exception($e);
}
$cnt = $search_ret['count'] / self::MAX_FETCH + 1;
$query = new Horde_Imap_Client_Fetch_Query();
$query->flags();
for ($i = 0; $i <= $cnt; $i++) {
$ids = new Horde_Imap_Client_Ids(array_slice($search_ret['match']->ids, $i * self::MAX_FETCH, self::MAX_FETCH));
try {
$fetch_ret = $imap->fetch($mbox, $query, array('ids' => $ids));
} catch (Horde_Imap_Client_Exception $e) {
$this->_logger->err($e->getMessage());
throw new Horde_ActiveSync_Exception($e);
}
foreach ($fetch_ret as $uid => $data) {
$flags[$uid] = array('read' => array_search(Horde_Imap_Client::FLAG_SEEN, $data->getFlags()) !== false ? 1 : 0);
if ($options['protocolversion'] > Horde_ActiveSync::VERSION_TWOFIVE) {
$flags[$uid]['flagged'] = array_search(Horde_Imap_Client::FLAG_FLAGGED, $data->getFlags()) !== false ? 1 : 0;
}
}
}
if (!empty($flags)) {
$folder->setChanges($search_ret['match']->ids, $flags);
}
$folder->setRemoved($imap->vanished($mbox, null, array('ids' => new Horde_Imap_Client_Ids($folder->messages())))->ids);
} elseif ($current_modseq > 0 && $folder->modseq() == 0) {
throw new Horde_ActiveSync_Exception_StaleState('Transition to MODSEQ enabled server');
}
$folder->setStatus($status);
return $folder;
}