Автор: Michael J Rubinsky (mrubinsk@horde.org)
Пример #1
0
 /**
  * Const'r
  *
  * @param Horde_ActiveSync_Interface_ImapFactory $imap The IMAP factory.
  * @param array $status                         The IMAP status array.
  * @param Horde_ActiveSync_Folder_Base $folder  The folder object.
  * @param Horde_Log_Logger $logger              The logger.
  */
 public function __construct(Horde_ActiveSync_Interface_ImapFactory $imap, array $status, Horde_ActiveSync_Folder_Base $folder, $logger)
 {
     $this->_imap = $imap;
     $this->_imap_ob = $imap->getImapOb();
     $this->_status = $status;
     $this->_folder = $folder;
     $this->_logger = $logger;
     $this->_procid = getmypid();
     $this->_mbox = new Horde_Imap_Client_Mailbox($folder->serverid());
 }
Пример #2
0
 /**
  * Get a list of server changes that occured during the specified time
  * period.
  *
  * @param Horde_ActiveSync_Folder_Base $folder
  *      The ActiveSync folder object to request changes for.
  * @param integer $from_ts     The starting timestamp
  * @param integer $to_ts       The ending timestamp
  * @param integer $cutoffdate  The earliest date to retrieve back to.
  *
  * @param boolean $ping        If true, returned changeset may
  *                             not contain the full changeset, but rather
  *                             only a single change, designed only to
  *                             indicate *some* change has taken place. The
  *                             value should not be used to determine *what*
  *                             change has taken place.
  * @param boolean $ignoreFirstSync  If true, will not trigger an initial sync
  *                                  if $from_ts is 0. Needed to avoid race
  *                                  conditions when we don't have any history
  *                                  data. @since 2.6.0
  *                                  @todo If we can pass the synckey (
  *                                  perhaps as part of $folder), we can
  *                                  just look for synckey 0 to know when
  *                                  we CAN trigger an initial sync without
  *                                  this flag.
  * @param integer $maxitems         Maximum number of recipients for a RI
  *                                  collection. @since 2.12.0
  * @param boolean $refreshFilter    Force a SOFTDELETE operation and check
  *                                  for new items within the (newly changed)
  *                                  current FilterType interval.
  *                                  @since 2.19.0
  *
  * @return array  An array of hashes that contain the ids of items that have
  *                changed in the specified collection along with a 'type'
  *                flag that indicates the type of change.
  * @todo H6 - Clean up method parameters, update parent class etc...
  *          - Return a new ids object.
  *          - Refactor to use a Repository pattern for each supported
  *            collection and move the bulk of the logic in the switch
  *            structure below to the various classes - and refactor out most
  *            of the stuff in the registry connector since the Repositories
  *            will handle the basic CRUD operations and change detection on
  *            each collection.
  */
 public function getServerChanges($folder, $from_ts, $to_ts, $cutoffdate, $ping, $ignoreFirstSync = false, $maxitems = 100, $refreshFilter = false)
 {
     $this->_logger->info(sprintf("[%s] Horde_Core_ActiveSync_Driver::getServerChanges(%s, %u, %u, %u, %d, %s, %u, %s)", $this->_pid, $folder->serverid(), $from_ts, $to_ts, $cutoffdate, $ping, $ignoreFirstSync, $maxitems, $refreshFilter));
     $changes = array('add' => array(), 'delete' => array(), 'modify' => array(), 'soft' => array());
     ob_start();
     switch ($folder->collectionClass()) {
         case Horde_ActiveSync::CLASS_CALENDAR:
             if ($folder->serverid() == self::APPOINTMENTS_FOLDER_UID) {
                 $server_id = null;
             } else {
                 $parts = $this->_parseFolderId($folder->serverid());
                 $server_id = $parts[self::FOLDER_PART_ID];
             }
             if ($from_ts == 0 && !$ignoreFirstSync) {
                 // Can't use History if it's a first sync
                 $startstamp = (int) $cutoffdate;
                 $endstamp = time() + 32140800;
                 //60 * 60 * 24 * 31 * 12 == one year
                 try {
                     $changes['add'] = $this->_connector->calendar_listUids($startstamp, $endstamp, $server_id);
                 } catch (Horde_Exception_AuthenticationFailure $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception $e) {
                     $this->_logger->err($e->getMessage());
                     $this->_endBuffer();
                     return array();
                 }
                 $folder->setSoftDeleteTimes($cutoffdate, time());
             } else {
                 try {
                     $changes = $this->_connector->getChanges('calendar', $from_ts, $to_ts, $server_id);
                 } catch (Horde_Exception_AuthenticationFailure $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception $e) {
                     $this->_logger->err($e->getMessage());
                     $this->_endBuffer();
                     return array();
                 }
                 // Softdelete. We check this once per close-to 24 hour period,or
                 // if the FILTERTYPE range changes. We introduce an element of
                 // randomness in the time to help avoid a large number of
                 // clients performing a softdelete at once. It's 23 hours + some
                 // random number of minutes < 60.
                 //
                 // @todo We need to populate additional events if the FILTERTYPE
                 // is expanded, but we need to persist the previous FILTERTYPE
                 // so we know exactly which events we already have and which
                 // we don't (since we don't track the UIDs themselves).
                 if (!$ping) {
                     if (!$refreshFilter) {
                         $sd = $folder->getSoftDeleteTimes();
                         if ($sd[1] + 82800 + mt_rand(0, 3600) < time()) {
                             $from = $sd[2];
                         }
                     } else {
                         // Force refresh the FILTERTYPE.
                         $from = 0;
                     }
                     if (isset($from)) {
                         $this->_logger->info(sprintf('[%s] Polling for SOFTDELETE items in calendar collection', getmypid()));
                         // Gets the list of ids contained between the last softdelete
                         // run and the current cutoffdate.
                         $changes['soft'] = $this->_connector->softDelete('calendar', $from, $cutoffdate, $server_id);
                         $folder->setSoftDeleteTimes($cutoffdate, time());
                     }
                 }
             }
             break;
         case Horde_ActiveSync::CLASS_CONTACTS:
             // Multiplexed or multiple?
             if ($folder->serverid() == self::CONTACTS_FOLDER_UID) {
                 $server_id = null;
             } else {
                 $parts = $this->_parseFolderId($folder->serverid());
                 $server_id = $parts[self::FOLDER_PART_ID];
             }
             // Can't use History for first sync
             if ($from_ts == 0 && !$ignoreFirstSync) {
                 try {
                     $changes['add'] = $this->_connector->contacts_listUids($server_id);
                 } catch (Horde_Exception_AuthenticationFailure $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception $e) {
                     $this->_logger->err($e->getMessage());
                     $this->_endBuffer();
                     return array();
                 }
             } else {
                 try {
                     $changes = $this->_connector->getChanges('contacts', $from_ts, $to_ts, $server_id);
                 } catch (Horde_Exception_AuthenticationFailure $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception $e) {
                     $this->_logger->err($e->getMessage());
                     $this->_endBuffer();
                     return array();
                 }
             }
             break;
         case Horde_ActiveSync::CLASS_TASKS:
             // Multiplexed or multiple?
             if ($folder->serverid() == self::TASKS_FOLDER_UID) {
                 $server_id = null;
             } else {
                 $parts = $this->_parseFolderId($folder->serverid());
                 $server_id = $parts[self::FOLDER_PART_ID];
             }
             // Can't use History for first sync
             if ($from_ts == 0 && !$ignoreFirstSync) {
                 try {
                     $changes['add'] = $this->_connector->tasks_listUids($server_id);
                 } catch (Horde_Exception_AuthenticationFailure $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception $e) {
                     $this->_logger->err($e->getMessage());
                     $this->_endBuffer();
                     return array();
                 }
             } else {
                 try {
                     $changes = $this->_connector->getChanges('tasks', $from_ts, $to_ts, $server_id);
                 } catch (Horde_Exception_AuthenticationFailure $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception $e) {
                     $this->_logger->err($e->getMessage());
                     $this->_endBuffer();
                     return array();
                 }
             }
             break;
         case Horde_ActiveSync::CLASS_NOTES:
             // Multiplexed or multiple?
             if ($folder->serverid() == self::NOTES_FOLDER_UID) {
                 $server_id = null;
             } else {
                 $parts = $this->_parseFolderId($folder->serverid());
                 $server_id = $parts[self::FOLDER_PART_ID];
             }
             // Can't use History for first sync
             if ($from_ts == 0 && !$ignoreFirstSync) {
                 try {
                     $changes['add'] = $this->_connector->notes_listUids($server_id);
                 } catch (Horde_Exception_AuthenticationFailure $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception $e) {
                     $this->_logger->err($e->getMessage());
                     $this->_endBuffer();
                     return array();
                 }
             } else {
                 try {
                     $changes = $this->_connector->getChanges('notes', $from_ts, $to_ts, $server_id);
                 } catch (Horde_Exception_AuthenticationFailure $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception $e) {
                     $this->_logger->err($e->getMessage());
                     $this->_endBuffer();
                     return array();
                 }
             }
             break;
         case 'RI':
             $folder->setChanges($this->_connector->getRecipientCache($maxitems));
             $changes = array('add' => $folder->added(), 'delete' => $folder->removed(), 'modify' => array(), 'soft' => array());
             break;
         case Horde_ActiveSync::CLASS_EMAIL:
             if (empty($this->_imap)) {
                 $this->_endBuffer();
                 return array();
             }
             $this->_logger->info(sprintf('[%s] %s IMAP PREVIOUS MODSEQ: %d', $this->_pid, $folder->serverid(), $folder->modseq()));
             if ($ping) {
                 try {
                     $ping_res = $this->_imap->ping($folder);
                     if ($ping_res) {
                         $changes['add'] = array(1);
                     }
                 } catch (Horde_ActiveSync_Exeption_StaleState $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_ActiveSync_Exception_FolderGone $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception_AuthenticationFailure $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception $e) {
                     $this->_logger->err($e->getMessage());
                     $this->_endBuffer();
                     return array();
                 }
             } else {
                 // SOFTDELETE, but only if we aren't refreshing FILTERTYPE.
                 $soft = false;
                 if (!$refreshFilter) {
                     $sd = $folder->getSoftDeleteTimes();
                     if (empty($sd[0]) && empty($sd[1]) && !empty($cutoffdate)) {
                         // No SOFTDELETE performed, this is likely the first
                         // sync so we must prime the SOFTDELETE values.
                         $folder->setSoftDeleteTimes((int) $cutoffdate, time());
                     } else {
                         if ($sd[1] + 82800 + mt_rand(0, 3600) < time()) {
                             $soft = true;
                         } else {
                             $soft = false;
                         }
                     }
                 }
                 try {
                     $folder = $this->_imap->getMessageChanges($folder, array('sincedate' => (int) $cutoffdate, 'protocolversion' => $this->_version, 'softdelete' => $soft, 'refreshfilter' => $refreshFilter));
                     // Poll the maillog for reply/forward state changes.
                     if (empty($GLOBALS['conf']['activesync']['no_maillogsync'])) {
                         $folder = $this->_getMaillogChanges($folder, $from_ts);
                     }
                 } catch (Horde_ActiveSync_Exception_StaleState $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_ActiveSync_Exception_FolderGone $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception_AuthenticationFailure $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception $e) {
                     $this->_logger->err($e->getMessage());
                     $this->_endBuffer();
                     return array();
                 }
                 $changes['add'] = $folder->added();
                 $changes['delete'] = $folder->removed();
                 $changes['modify'] = $folder->changed();
                 $changes['soft'] = $folder->getSoftDeleted();
             }
     }
     $results = array();
     if (!$folder->haveInitialSync) {
         $this->_logger->info(sprintf('[%s] Initial sync, only sending UID.', $this->_pid));
         $this->_endBuffer();
         $add = $changes['add'];
         $changes = null;
         return $add;
     } else {
         foreach ($changes['add'] as $add) {
             $results[] = array('id' => $add, 'type' => Horde_ActiveSync::CHANGE_TYPE_CHANGE, 'flags' => Horde_ActiveSync::FLAG_NEWMESSAGE);
         }
     }
     // For CLASS_EMAIL, all changes are a change in flags, categories or
     // softdelete.
     if ($folder->collectionClass() == Horde_ActiveSync::CLASS_EMAIL) {
         $flags = $folder->flags();
         $categories = $folder->categories();
         foreach ($changes['modify'] as $uid) {
             $results[] = array('id' => $uid, 'type' => Horde_ActiveSync::CHANGE_TYPE_FLAGS, 'flags' => $flags[$uid], 'categories' => $categories[$uid]);
         }
     } else {
         foreach ($changes['modify'] as $change) {
             $results[] = array('id' => $change, 'type' => Horde_ActiveSync::CHANGE_TYPE_CHANGE);
         }
     }
     // Server Deletions and Softdeletions
     foreach ($changes['delete'] as $deleted) {
         $results[] = array('id' => $deleted, 'type' => Horde_ActiveSync::CHANGE_TYPE_DELETE);
     }
     if (!empty($changes['soft'])) {
         foreach ($changes['soft'] as $deleted) {
             $results[] = array('id' => $deleted, 'type' => Horde_ActiveSync::CHANGE_TYPE_SOFTDELETE);
         }
     }
     $this->_endBuffer();
     return $results;
 }
Пример #3
0
 /**
  * Get a list of server changes that occured during the specified time
  * period.
  *
  * @param Horde_ActiveSync_Folder_Base $folder
  *      The ActiveSync folder object to request changes for.
  * @param integer $from_ts     The starting timestamp
  * @param integer $to_ts       The ending timestamp
  * @param integer $cutoffdate  The earliest date to retrieve back to.
  *
  * @param boolean $ping        If true, returned changeset may
  *                             not contain the full changeset, but rather
  *                             only a single change, designed only to
  *                             indicate *some* change has taken place. The
  *                             value should not be used to determine *what*
  *                             change has taken place.
  * @param boolean $ignoreFirstSync  If true, will not trigger an initial sync
  *                                  if $from_ts is 0. Needed to avoid race
  *                                  conditions when we don't have any history
  *                                  data. @since 2.6.0
  * @param integer $maxitems         Maximum number of recipients for a RI
  *                                  collection. @since 2.12.0
  *
  * @return array  An array of hashes that contain the ids of items that have
  *                changed in the specified collection along with a 'type'
  *                flag that indicates the type of change.
  * @todo H6 - clean up method parameters, update parent class etc...
  *            return a new ids object.
  */
 public function getServerChanges($folder, $from_ts, $to_ts, $cutoffdate, $ping, $ignoreFirstSync = false, $maxitems = 100)
 {
     $this->_logger->info(sprintf("[%s] Horde_Core_ActiveSync_Driver::getServerChanges(%s, %u, %u, %u, %d)", $this->_pid, $folder->serverid(), $from_ts, $to_ts, $cutoffdate, $ping));
     $changes = array('add' => array(), 'delete' => array(), 'modify' => array(), 'soft' => array());
     ob_start();
     switch ($folder->collectionClass()) {
         case Horde_ActiveSync::CLASS_CALENDAR:
             if ($folder->serverid() == self::APPOINTMENTS_FOLDER_UID) {
                 $server_id = null;
             } else {
                 $parts = $this->_parseFolderId($folder->serverid());
                 $server_id = $parts[self::FOLDER_PART_ID];
             }
             if ($from_ts == 0 && !$ignoreFirstSync) {
                 // Can't use History if it's a first sync
                 $startstamp = (int) $cutoffdate;
                 $endstamp = time() + 32140800;
                 //60 * 60 * 24 * 31 * 12 == one year
                 try {
                     $changes['add'] = $this->_connector->calendar_listUids($startstamp, $endstamp, $server_id);
                 } catch (Horde_Exception_AuthenticationFailure $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception $e) {
                     $this->_logger->err($e->getMessage());
                     $this->_endBuffer();
                     return array();
                 }
                 $folder->setSoftDeleteTimes($cutoffdate, time());
             } else {
                 try {
                     $changes = $this->_connector->getChanges('calendar', $from_ts, $to_ts, $server_id);
                 } catch (Horde_Exception_AuthenticationFailure $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception $e) {
                     $this->_logger->err($e->getMessage());
                     $this->_endBuffer();
                     return array();
                 }
                 // Softdelete. We check this once per close-to 24 hour period.
                 // We introduce an element of randomness in the time to help
                 // avoid a large number of clients performing a softdelete at
                 // once. It's 23 hours + some random number of minutes < 60.
                 if (!$ping) {
                     $sd = $folder->getSoftDeleteTimes();
                     if ($sd[1] + 82800 + mt_rand(0, 3600) < time()) {
                         $this->_logger->info(sprintf('[%s] Polling for SOFTDELETE items in calendar collection', getmypid()));
                         // Gets the list of ids contained between the last softdelete
                         // run and the current cutoffdate.
                         $changes['soft'] = $this->_connector->softDelete('calendar', $sd[0], $cutoffdate, $server_id);
                         $folder->setSoftDeleteTimes($cutoffdate, time());
                     }
                 }
             }
             break;
         case Horde_ActiveSync::CLASS_CONTACTS:
             // Multiplexed or multiple?
             if ($folder->serverid() == self::CONTACTS_FOLDER_UID) {
                 $server_id = null;
             } else {
                 $parts = $this->_parseFolderId($folder->serverid());
                 $server_id = $parts[self::FOLDER_PART_ID];
             }
             // Can't use History for first sync
             if ($from_ts == 0 && !$ignoreFirstSync) {
                 try {
                     $changes['add'] = $this->_connector->contacts_listUids($server_id);
                 } catch (Horde_Exception_AuthenticationFailure $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception $e) {
                     $this->_logger->err($e->getMessage());
                     $this->_endBuffer();
                     return array();
                 }
             } else {
                 try {
                     $changes = $this->_connector->getChanges('contacts', $from_ts, $to_ts, $server_id);
                 } catch (Horde_Exception_AuthenticationFailure $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception $e) {
                     $this->_logger->err($e->getMessage());
                     $this->_endBuffer();
                     return array();
                 }
             }
             break;
         case Horde_ActiveSync::CLASS_TASKS:
             // Multiplexed or multiple?
             if ($folder->serverid() == self::TASKS_FOLDER_UID) {
                 $server_id = null;
             } else {
                 $parts = $this->_parseFolderId($folder->serverid());
                 $server_id = $parts[self::FOLDER_PART_ID];
             }
             // Can't use History for first sync
             if ($from_ts == 0 && !$ignoreFirstSync) {
                 try {
                     $changes['add'] = $this->_connector->tasks_listUids($server_id);
                 } catch (Horde_Exception_AuthenticationFailure $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception $e) {
                     $this->_logger->err($e->getMessage());
                     $this->_endBuffer();
                     return array();
                 }
             } else {
                 try {
                     $changes = $this->_connector->getChanges('tasks', $from_ts, $to_ts, $server_id);
                 } catch (Horde_Exception_AuthenticationFailure $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception $e) {
                     $this->_logger->err($e->getMessage());
                     $this->_endBuffer();
                     return array();
                 }
             }
             break;
         case Horde_ActiveSync::CLASS_NOTES:
             // Multiplexed or multiple?
             if ($folder->serverid() == self::NOTES_FOLDER_UID) {
                 $server_id = null;
             } else {
                 $parts = $this->_parseFolderId($folder->serverid());
                 $server_id = $parts[self::FOLDER_PART_ID];
             }
             // Can't use History for first sync
             if ($from_ts == 0 && !$ignoreFirstSync) {
                 try {
                     $changes['add'] = $this->_connector->notes_listUids($server_id);
                 } catch (Horde_Exception_AuthenticationFailure $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception $e) {
                     $this->_logger->err($e->getMessage());
                     $this->_endBuffer();
                     return array();
                 }
             } else {
                 try {
                     $changes = $this->_connector->getChanges('notes', $from_ts, $to_ts, $server_id);
                 } catch (Horde_Exception_AuthenticationFailure $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception $e) {
                     $this->_logger->err($e->getMessage());
                     $this->_endBuffer();
                     return array();
                 }
             }
             break;
         case 'RI':
             $folder->setChanges($this->_connector->getRecipientCache($maxitems));
             $changes = array('add' => $folder->added(), 'delete' => $folder->removed(), 'modify' => array(), 'soft' => array());
             break;
         case Horde_ActiveSync::CLASS_EMAIL:
             if (empty($this->_imap)) {
                 $this->_endBuffer();
                 return array();
             }
             $this->_logger->info(sprintf('[%s] %s IMAP MODSEQ: %d', $this->_pid, $folder->serverid(), $folder->modseq()));
             if ($ping) {
                 try {
                     $ping_res = $this->_imap->ping($folder);
                     if ($ping_res) {
                         $changes['add'] = array(1);
                     }
                 } catch (Horde_ActiveSync_Exeption_StaleState $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_ActiveSync_Exception_FolderGone $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception_AuthenticationFailure $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception $e) {
                     $this->_logger->err($e->getMessage());
                     $this->_endBuffer();
                     return array();
                 }
             } else {
                 // Calculate SOFTDELETE if needed?
                 $sd = $folder->getSoftDeleteTimes();
                 if ($sd[1] + 82800 + mt_rand(0, 3600) < time()) {
                     $soft = true;
                 } else {
                     $soft = false;
                 }
                 try {
                     $folder = $this->_imap->getMessageChanges($folder, array('sincedate' => (int) $cutoffdate, 'protocolversion' => $this->_version, 'softdelete' => $soft));
                     // Poll the maillog for reply/forward state changes.
                     $folder = $this->_getMaillogChanges($folder, $from_ts);
                 } catch (Horde_ActiveSync_Exception_StaleState $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_ActiveSync_Exception_FolderGone $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception_AuthenticationFailure $e) {
                     $this->_endBuffer();
                     throw $e;
                 } catch (Horde_Exception $e) {
                     $this->_logger->err($e->getMessage());
                     $this->_endBuffer();
                     return array();
                 }
                 $changes['add'] = $folder->added();
                 $changes['delete'] = $folder->removed();
                 $changes['modify'] = $folder->changed();
                 $changes['soft'] = $folder->getSoftDeleted();
             }
     }
     $results = array();
     foreach ($changes['add'] as $add) {
         $results[] = array('id' => $add, 'type' => Horde_ActiveSync::CHANGE_TYPE_CHANGE, 'flags' => Horde_ActiveSync::FLAG_NEWMESSAGE);
     }
     // For CLASS_EMAIL, all changes are a change in flags, categories or
     // softdelete.
     if ($folder->collectionClass() == Horde_ActiveSync::CLASS_EMAIL) {
         $flags = $folder->flags();
         $categories = $folder->categories();
         foreach ($changes['modify'] as $uid) {
             $results[] = array('id' => $uid, 'type' => Horde_ActiveSync::CHANGE_TYPE_FLAGS, 'flags' => $flags[$uid], 'categories' => $categories[$uid]);
         }
     } else {
         foreach ($changes['modify'] as $change) {
             $results[] = array('id' => $change, 'type' => Horde_ActiveSync::CHANGE_TYPE_CHANGE);
         }
     }
     // Server Deletions and Softdeletions
     foreach ($changes['delete'] as $deleted) {
         $results[] = array('id' => $deleted, 'type' => Horde_ActiveSync::CHANGE_TYPE_DELETE);
     }
     if (!empty($changes['soft'])) {
         foreach ($changes['soft'] as $deleted) {
             $results[] = array('id' => $deleted, 'type' => Horde_ActiveSync::CHANGE_TYPE_SOFTDELETE);
         }
     }
     $this->_endBuffer();
     return $results;
 }