/** * Create a new mailbox on the server, and subscribe to it. * * @param string $name The new mailbox name. * @param string $parent The parent mailbox, if any. * * @return string The new serverid for the mailbox. This is the UTF-8 name * of the mailbox. @since 2.9.0 * @throws Horde_ActiveSync_Exception, Horde_ActiveSync_Exception_FolderExists */ public function createMailbox($name, $parent = null) { if (!empty($parent)) { $ns = $this->_defaultNamespace(); $name = $parent . $ns['delimiter'] . $name; } $mbox = new Horde_Imap_Client_Mailbox($this->_prependNamespace($name)); $imap = $this->_getImapOb(); try { $imap->createMailbox($mbox); $imap->subscribeMailbox($mbox, true); } catch (Horde_Imap_Client_Exception $e) { if ($e->getCode() == Horde_Imap_Client_Exception::ALREADYEXISTS) { $this->_logger->warn(sprintf('[%s] Mailbox %s already exists, subscribing to it.', $this->_procid, $name)); try { $imap->subscribeMailbox($mbox, true); } catch (Horde_Imap_Client_Exception $e) { // Exists, but could not subscribe to it, something is // *really* wrong. throw new Horde_ActiveSync_Exception_FolderExists('Folder Exists!'); } } throw new Horde_ActiveSync_Exception($e); } return $mbox->utf8; }
/** * Set a collection as non-PINGable. * * @param string $collectionid The collection id. */ public function removePingableCollection($id) { if (empty($this->_data['collections'][$id])) { $this->_logger->warn(sprintf('[%s] Collection %s was asked to be removed from PINGABLE but does not exist.', $this->_procid, $id)); return; } $this->_data['collections'][$id]['pingable'] = false; $this->_markCollectionsDirty($id); }
/** * Poll the backend for changes. * * @param integer $heartbeat The heartbeat lifetime to wait for changes. * @param integer $interval The wait interval between poll iterations. * @param array $options An options array containing any of: * - pingable: (boolean) Only poll collections with the pingable flag set. * DEFAULT: false * * @return boolean|integer True if changes were detected in any of the * collections, false if no changes detected * or a status code if failed. */ public function pollForChanges($heartbeat, $interval, array $options = array()) { $dataavailable = false; $started = time(); $until = $started + $heartbeat; $this->_logger->info(sprintf('Waiting for changes for %s seconds', $heartbeat)); // If pinging, make sure we have pingable collections. Note we can't // filter on them here because the collections might change during the // loop below. if (!empty($options['pingable']) && !$this->havePingableCollections()) { $this->_logger->err('No pingable collections.'); return self::COLLECTION_ERR_SERVER; } // Need to update AND SAVE the timestamp for race conditions to be // detected. $this->lasthbsyncstarted = $started; $this->save(); // We only check for remote wipe request once every 5 iterations to // save on DB load since we must reload the device's state each time. $rw_check_countdown = 5; while (($now = time()) < $until) { // Try not to go over the heartbeat interval. if ($until - $now < $interval) { $interval = $until - $now; } // See if another process has altered the sync_cache. if ($this->checkStaleRequest()) { return self::COLLECTION_ERR_STALE; } // Make sure the collections are still there (there might have been // an error in refreshing them from the cache). Ideally this should // NEVER happen. if (!count($this->_collections)) { $this->_logger->err('NO COLLECTIONS! This should not happen!'); return self::COLLECTION_ERR_SERVER; } // Check for WIPE request once every 5 iterations to balance between // performance and speed of catching a remote wipe request. if ($rw_check_countdown-- == 0) { $rw_check_countdown = 5; if ($this->_as->provisioning != Horde_ActiveSync::PROVISIONING_NONE) { $rwstatus = $this->_as->state->getDeviceRWStatus($this->_as->device->id, true); if ($rwstatus == Horde_ActiveSync::RWSTATUS_PENDING || $rwstatus == Horde_ActiveSync::RWSTATUS_WIPED) { return self::COLLECTION_ERR_FOLDERSYNC_REQUIRED; } } } // Check each collection we are interested in. foreach ($this->_collections as $id => $collection) { // Initialize the collection's state data in the state handler. try { $this->initCollectionState($collection, true); } catch (Horde_ActiveSync_Exception_StateGone $e) { $this->_logger->notice(sprintf('[%s] State not found for %s. Continuing.', $this->_procid, $id)); if (!empty($options['pingable'])) { return self::COLLECTION_ERR_PING_NEED_FULL; } $dataavailable = true; $this->setGetChangesFlag($id); continue; } catch (Horde_ActiveSync_Exception_InvalidRequest $e) { // Thrown when state is unable to be initialized because the // collection has not yet been synched, but was requested to // be pinged. $this->_logger->err(sprintf('[%s] Unable to initialize state for %s. Ignoring during pollForChanges: %s.', $this->_procid, $id, $e->getMessage())); continue; } catch (Horde_ActiveSync_Exception_FolderGone $e) { $this->_logger->warn('Folder gone for collection ' . $collection['id']); return self::COLLECTION_ERR_FOLDERSYNC_REQUIRED; } catch (Horde_ActiveSync_Exception $e) { $this->_logger->err('Error loading state: ' . $e->getMessage()); $this->_as->state->loadState(array(), null, Horde_ActiveSync::REQUEST_TYPE_SYNC, $id); $this->setGetChangesFlag($id); $dataavailable = true; continue; } if (!empty($options['pingable']) && !$this->_cache->collectionIsPingable($id)) { $this->_logger->notice(sprintf('[%s] Skipping %s because it is not PINGable.', $this->_procid, $id)); continue; } try { if ($cnt = $this->getCollectionChangeCount(true)) { $dataavailable = true; $this->setGetChangesFlag($id); if (!empty($options['pingable'])) { $this->_cache->setPingChangeFlag($id); } } else { try { $this->_as->state->updateSyncStamp(); } catch (Horde_ActiveSync_Exception $e) { $this->_logger->err($e->getMessage()); } } } catch (Horde_ActiveSync_Exception_StaleState $e) { $this->_logger->notice(sprintf('[%s] SYNC terminating and force-clearing device state: %s', $this->_procid, $e->getMessage())); $this->_as->state->loadState(array(), null, Horde_ActiveSync::REQUEST_TYPE_SYNC, $id); $this->setGetChangesFlag($id); $dataavailable = true; } catch (Horde_ActiveSync_Exception_FolderGone $e) { $this->_logger->notice(sprintf('[%s] SYNC terminating: %s', $this->_procid, $e->getMessage())); // If we are missing a folder, we should clear the PING // cache also, to be sure it picks up any hierarchy changes // since most clients don't seem smart enough to figure this // out on their own. $this->resetPingCache(); return self::COLLECTION_ERR_FOLDERSYNC_REQUIRED; } catch (Horde_Exception_AuthenticationFailure $e) { // We lost authentication for some reason. $this->_logger->err(sprintf('[%s] Authentication lost during PING!!', $this->_procid)); return self::COLLECTION_ERR_AUTHENTICATION; } catch (Horde_ActiveSync_Exception $e) { $this->_logger->err(sprintf('[%s] Sync object cannot be configured, throttling: %s', $this->_procid, $e->getMessage())); $this->_sleep(30); continue; } } if (!empty($dataavailable)) { $this->_logger->info(sprintf('[%s] Found changes!', $this->_procid)); break; } // Wait a bit... $this->_sleep($interval); // Refresh the collections. $this->updateCollectionsFromCache(); } // Check that no other Sync process already started // If so, we exit here and let the other process do the export. if ($this->checkStaleRequest()) { $this->_logger->info('Changes in cache determined during Sync Wait/Heartbeat, exiting here.'); return self::COLLECTION_ERR_STALE; } $this->_logger->info(sprintf('[%s] Looping Sync complete: DataAvailable: %s, DataImported: %s', $this->_procid, $dataavailable, $this->importedChanges)); return $dataavailable; }