Skip to content

Commit

Permalink
Break out strategies for each type of IMAP change detection.
Browse files Browse the repository at this point in the history
  • Loading branch information
mrubinsk committed Dec 4, 2016
1 parent 8dc6a7e commit 84af3f8
Show file tree
Hide file tree
Showing 6 changed files with 621 additions and 292 deletions.
302 changes: 12 additions & 290 deletions framework/ActiveSync/lib/Horde/ActiveSync/Imap/Adapter.php
Expand Up @@ -354,227 +354,25 @@ public function getMessageChanges(
$folder->checkValidity($status);
}

// If IMAP server reports invalid MODSEQ, this can lead to the client
// no longer ever able to detect changes therefore never receiving new
// email even if the value is restored at some point in the future.
//
// This can happen, e.g., if the IMAP server index files are lost or
// otherwise corrupted. Normally this would be handled as a loss of
// server state and handled by a complete resync, but a majority of
// EAS clients do not properly handle the status codes that report this.
$current_modseq = $status[Horde_ActiveSync_Folder_Imap::HIGHESTMODSEQ];
if ($modseq_corrupted = $folder->modseq() > $current_modseq) {
$this->_logger->err(sprintf(
'[%s] IMAP Server error: Current HIGHESTMODSEQ is lower than previously reported.',
$this->_procid)
);
}

$modseq_corrupted = $folder->modseq() > $current_modseq;
if ($modseq_corrupted || (($current_modseq && $folder->modseq() > 0) &&
(($folder->modseq() < $current_modseq) ||
!empty($options['softdelete']) || !empty($options['refreshfilter'])))) {

$query = new Horde_Imap_Client_Search_Query();

if (!$modseq_corrupted) {
$this->_logger->info(sprintf(
'[%s] CONDSTORE and CHANGES', $this->_procid));
// Catch all *changes* since the provided MODSEQ value.
// $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))
$strategy = new Horde_ActiveSync_Imap_Strategy_Modseq(
$this->_imap, $status, $folder, $this->_logger
);

$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();
if (!$modseq_corrupted) {
$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());
}

$folder = $strategy->getChanges($options);
} 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);
}
$strategy = new Horde_ActiveSync_Imap_Strategy_Initial(
$this->_imap, $status, $folder, $this->_logger
);
$folder = $strategy->getChanges($options);
} 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']));
$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);
$strategy = new Horde_ActiveSync_Imap_Strategy_Plain(
$this->_imap, $status, $folder, $this->_logger
);
$folder = $strategy->getChanges($options);
} elseif ($current_modseq > 0 && $folder->modseq() == 0) {
throw new Horde_ActiveSync_Exception_StaleState('Transition to MODSEQ enabled server');
}
Expand All @@ -583,82 +381,6 @@ public function getMessageChanges(
return $folder;
}

/**
* Return message UIDs that are now within the cureent FILTERTYPE value.
*
* @param Horde_ActiveSync_Folder_Imap $folder The IMAP folder object.
* @param array $options Options array.
* @param Horde_Imap_Client_Mailbox $mbox The current mailbox.
* @param boolean $is_delete If true, return messages
* to SOFTDELETE.
*
* @return Horde_Imap_Client_Search_Results
*/
protected function _buildSearchQuery($folder, $options, $mbox, $is_delete)
{
$query = new Horde_Imap_Client_Search_Query();
$query->dateSearch(
new Horde_Date($options['sincedate']),
$is_delete
? Horde_Imap_Client_Search_Query::DATE_BEFORE
: Horde_Imap_Client_Search_Query::DATE_SINCE
);
$query->ids(new Horde_Imap_Client_Ids($folder->messages()), !$is_delete);
try {
return $this->_getImapOb()->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);
}
}

/**
* Populates the changes, flags, and categories arrays with data from
* any messages added/changed on the IMAP server since the last poll.
*
* @param array &$changes Changes array.
* @param array &$flags Flags array.
* @param array &$categories Categories array.
* @param Horde_Imap_Client_Fetch_Results $fetch_ret Fetch results.
* @param array $options Options array.
* @param integer $modseq Current MODSEQ.
*/
protected function _buildModSeqChanges(
&$changes, &$flags, &$categories, $fetch_ret, $options, $modseq)
{
// Get custom flags to use as categories.
$msgFlags = $this->_getMsgFlags();

// Filter out any changes that we already know about.
$fetch_keys = $fetch_ret->ids();
$result_set = array_diff($fetch_keys, $changes);
foreach ($result_set as $uid) {
$data = $fetch_ret[$uid];
// Ensure no changes after the current modseq as reported by the
// server status have been returned.
if ($data->getModSeq() <= $modseq) {
$changes[] = $uid;
$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 ($options['protocolversion'] > Horde_ActiveSync::VERSION_TWELVEONE) {
$categories[$uid] = array();
foreach ($data->getFlags() as $flag) {
if (!empty($msgFlags[Horde_String::lower($flag)])) {
$categories[$uid][] = $msgFlags[Horde_String::lower($flag)];
}
}
}
}
}
}

/**
* Return AS mail messages, from the given IMAP UIDs.
*
Expand Down

0 comments on commit 84af3f8

Please sign in to comment.