/** * Import a folder change from the wbxml stream * * @param string $uid The folder uid * @param string $displayname The folder display name * @param string $parent The parent folder id. * @param integer $type The EAS Folder type. @since 2.9.0 * * @return Horde_ActiveSync_Message_Folder The new folder object. */ public function importFolderChange($uid, $displayname, $parent = Horde_ActiveSync::FOLDER_ROOT, $type = null) { $this->_logger->info(sprintf('[%s] Horde_ActiveSync_Connector_Importer::importFolderChange(%s, %s, %s, %s)', $this->_procid, $uid, $displayname, $parent, $type)); // Convert the uids to serverids. $collections = $this->_as->getCollectionsObject(); $parent_sid = !empty($parent) ? $collections->getBackendIdForFolderUid($parent) : $parent; $folderid = !empty($uid) ? $collections->getBackendIdForFolderUid($uid) : false; // Perform the creation in the backend. try { $results = $this->_as->driver->changeFolder($folderid, $displayname, $parent_sid, $uid, $type); } catch (Horde_ActiveSync_Exception $e) { $this->_logger->err($e->getMessage()); throw $e; } // @todo Horde 6 this should always return an object. if ($results instanceof Horde_ActiveSync_Message_Folder) { $folderid = $results->_serverid; $uid = $results->serverid; } else { // @TODO Remove for 3.0 Need to build a message folder object here // for BC reasons. $serverid = $results; $results = $this->_as->messageFactory('Folder'); $results->serverid = $serverid; $results->_serverid = $folderid; } $change = array(); $change['id'] = $uid; $change['folderid'] = $folderid; $change['mod'] = $displayname; $change['parent'] = $parent; $this->_state->updateState(Horde_ActiveSync::CHANGE_TYPE_CHANGE, $change, Horde_ActiveSync::CHANGE_ORIGIN_PIM); return $results; }
/** * Get an activesync uid for the given backend serverid. If we've seen this * serverid before, return the previously created uid, otherwise return * a new one. * * @param string $id The server's current folder name E.g., INBOX * @param string $type The folder type, a Horde_ActiveSync::FOLDER_TYPE_* * constant. If empty, assumes FOLDER_TYPE_USER_MAIL * @param string $old_id The previous folder name for this folder, if the * folder is being renamed. @since 2.15.0 * @todo This is tempoarary until 3.0 (H6) when we * will have the collection manager take care of ALL * of the folder name <-> UID mapping management. * * @return string A unique identifier for the specified backend folder id. * The first character indicates the foldertype as such: * 'F' - Email * 'C' - Contact * 'A' - Appointment * 'T' - Task * 'N' - Note * @since 2.4.0 */ protected function _getFolderUidForBackendId($id, $type = null, $old_id = null) { // Always use 'RI' for Recipient cache. if ($id == 'RI') { return 'RI'; } $map = $this->_state->getFolderUidToBackendIdMap(); // Rename? if (!empty($old_id) && !empty($map[$old_id])) { $this->_tempMap[$id] = $map[$old_id]; } if (!empty($map[$id])) { return $map[$id]; } elseif (!empty($this->_tempMap[$id])) { return $this->_tempMap[$id]; } // Convert TYPE to CLASS $type = $this->_getClassFromType($type); $rMap = array_flip($this->_typeMap); $prefix = $rMap[$type]; // None found, generate a new UID. $this->_tempMap[$id] = sprintf('%s%04x%04x', $prefix, mt_rand(0, 0xffff), mt_rand(0, 0xffff)); $this->_logger->info(sprintf('[%s] Creating new folder uuid for %s: %s', getmypid(), $id, $this->_tempMap[$id])); return $this->_tempMap[$id]; }
/** * Get all server changes for the specified collection * * @param string $collection The collection type (a Horde interface name - * calendar, contacts, tasks) * @param integer $from_ts Starting timestamp or modification sequence. * @param integer $to_ts Ending timestamp or modification sequence. * @param string $server_id The server id of the collection. If null, uses * multiplexed. * * @return array A hash of add, modify, and delete uids * @throws InvalidArgumentException, Horde_Exception */ public function getChanges($collection, $from_ts, $to_ts, $server_id) { if (!in_array($collection, array('calendar', 'contacts', 'tasks', 'notes'))) { throw new InvalidArgumentException('collection must be one of calendar, contacts, tasks or notes'); } $app = $this->_registry->hasInterface($collection); if (!$app || $this->_registry->isInactive($app)) { throw new Horde_Exception(sprintf('The %s interface is not active in Horde.', $collection)); } // We can use modification sequences. if ($this->hasFeature('modseq', $collection)) { $this->_logger->info(sprintf('[%s] Fetching changes for %s using MODSEQ.', getmypid(), $collection)); try { return $this->_registry->{$collection}->getChangesByModSeq($from_ts, $to_ts, $server_id); } catch (Exception $e) { return array('add' => array(), 'modify' => array(), 'delete' => array()); } } // Older API, use timestamps. $this->_logger->info(sprintf('[%s] Fetching changes for %s using TIMESTAMPS.', getmypid(), $collection)); try { return $this->_registry->{$collection}->getChanges($from_ts, $to_ts, false, $server_id); } catch (Exception $e) { return array('add' => array(), 'modify' => array(), 'delete' => array()); } }
/** * Force reset all collection's PINGABLE flag. Used to force client * to issue a non-empty PING request. * */ public function resetPingCache() { $collections = $this->_cache->getCollections(false); foreach ($collections as $id => $collection) { $this->_logger->info(sprintf('UNSETTING collection %s (%s) PINGABLE flag.', $collection['serverid'], $id)); $this->_cache->removePingableCollection($id); } }
protected function _buildBodyPart(Horde_ActiveSync_Imap_MessageBodyData $mbd, array $options, Horde_ActiveSync_Message_AirSyncBaseBodypart $message) { $this->_logger->info(sprintf('[%s] Preparing BODYPART data.', $this->_procid)); $message->status = Horde_ActiveSync_Message_AirSyncBaseBodypart::STATUS_SUCCESS; if (!empty($options['bodypartprefs']['preview']) && $mbd->plain) { $mbd->plain['body']->rewind(); $message->preview = $mbd->plain['body']->substring(0, $options['bodypartprefs']['preview']); } $message->data = $mbd->bodyPart['body']->stream; $message->truncated = $mbd->bodyPart['truncated']; return $message; }
protected function _reorderChanges(array $ensure) { $changes = array(); foreach ($this->_changes as $change) { if (array_search($change['id'], $ensure) !== false) { $this->_logger->info(sprintf('Placing %s at beginning of changes array.', $change['id'])); array_unshift($changes, $change); } else { $changes[] = $change; } } return $changes; }
/** * Opens a connection to the Kolab server. * * @param boolean $create_missing Create a preferences folder if it is * missing. * * @return Horde_Kolab_Storage_Data The storage backend. * * @throws Horde_Prefs_Exception */ protected function _getStorage($create_missing = false) { $query = $this->_kolab->getList()->getQuery(); if ($folder = $query->getDefault('h-prefs')) { return $this->_kolab->getData($folder); } $folders = $query->listByType('h-prefs'); if (!empty($folders)) { return $this->_kolab->getData($folders[0]); } if (!$create_missing) { throw new Horde_Prefs_Exception('No Kolab storage backend available.'); } $params = $this->getParams(); $folder = $this->_kolab->getList()->getNamespace()->constructFolderName($params['user'], $this->_folder); $this->_kolab->getList()->getListManipulation()->createFolder($folder, 'h-prefs.default'); if ($this->_logger !== null) { $this->_logger->info(sprintf(__CLASS__ . ': Created default Kolab preferences folder "%s".', $this->_folder)); } return $this->_kolab->getData($folder); }
/** * Builds a proper AS mail message object. * * @param Horde_Imap_Client_Mailbox $mbox The IMAP mailbox. * @param Horde_Imap_Client_Data_Fetch $data The fetch results. * @param array $options Additional Options: * - truncation: (integer) Truncate the message body to this length. * DEFAULT: No truncation. * - bodyprefs: (array) Bodyprefs, if sent from device. * DEFAULT: none (No body prefs sent or enforced). * - mimesupport: (integer) Indicates if MIME is supported or not. * Possible values: 0 - Not supported 1 - Only S/MIME or * 2 - All MIME. * DEFAULT: 0 (No MIME support) * - protocolversion: (float) The EAS protocol version to support. * DEFAULT: 2.5 * * @return Horde_ActiveSync_Message_Mail The message object suitable for * streaming to the device. */ protected function _buildMailMessage(Horde_Imap_Client_Mailbox $mbox, Horde_Imap_Client_Data_Fetch $data, $options = array()) { $imap = $this->_getImapOb(); $version = empty($options['protocolversion']) ? Horde_ActiveSync::VERSION_TWOFIVE : $options['protocolversion']; $imap_message = new Horde_ActiveSync_Imap_Message($imap, $mbox, $data); $eas_message = Horde_ActiveSync::messageFactory('Mail'); // Build To: data (POOMMAIL_TO has a max length of 1024). $to = $imap_message->getToAddresses(); $eas_message->to = array_pop($to['to']); foreach ($to['to'] as $to_atom) { if (strlen($eas_message->to) + strlen($to_atom) > 1024) { break; } $eas_message->to .= ',' . $to_atom; } $eas_message->displayto = implode(';', $to['displayto']); if (empty($eas_message->displayto)) { $eas_message->displayto = $eas_message->to; } // Ensure we don't send broken UTF8 data to the client. It makes clients // angry. And we don't like angry clients. $hdr_charset = $imap_message->getStructure()->getHeaderCharset(); // Fill in other header data $eas_message->from = $imap_message->getFromAddress(); $eas_message->subject = Horde_ActiveSync_Utils::ensureUtf8($imap_message->getSubject(), $hdr_charset); $eas_message->threadtopic = $eas_message->subject; $eas_message->datereceived = $imap_message->getDate(); $eas_message->read = $imap_message->getFlag(Horde_Imap_Client::FLAG_SEEN); $eas_message->cc = $imap_message->getCc(); $eas_message->reply_to = $imap_message->getReplyTo(); // Default to IPM.Note - may change below depending on message content. $eas_message->messageclass = 'IPM.Note'; // Codepage id. MS recommends to always set to UTF-8 when possible. // See http://msdn.microsoft.com/en-us/library/windows/desktop/dd317756%28v=vs.85%29.aspx $eas_message->cpid = Horde_ActiveSync_Message_Mail::INTERNET_CPID_UTF8; // Message importance. First try X-Priority, then Importance since // Outlook sends the later. if ($priority = $imap_message->getHeaders()->getValue('X-priority')) { $priorty = preg_replace('/\\D+/', '', $priority); } else { $priority = $imap_message->getHeaders()->getValue('Importance'); } $eas_message->importance = $this->_getEASImportance($priority); // Get the body data and ensure we have something to send. $message_body_data = $this->_validateMessageBodyData($imap_message->getMessageBodyData($options)); if ($version == Horde_ActiveSync::VERSION_TWOFIVE) { $eas_message->body = $message_body_data['plain']['body']->stream; $eas_message->bodysize = $message_body_data['plain']['body']->length(true); $eas_message->bodytruncated = $message_body_data['plain']['truncated']; $eas_message->attachments = $imap_message->getAttachments($version); } else { // Get the message body and determine original type. if (!empty($message_body_data['html'])) { $eas_message->airsyncbasenativebodytype = Horde_ActiveSync::BODYPREF_TYPE_HTML; } else { $eas_message->airsyncbasenativebodytype = Horde_ActiveSync::BODYPREF_TYPE_PLAIN; } $airsync_body = Horde_ActiveSync::messageFactory('AirSyncBaseBody'); if (isset($options['bodyprefs'][Horde_ActiveSync::BODYPREF_TYPE_MIME]) && ($options['mimesupport'] == Horde_ActiveSync::MIME_SUPPORT_ALL || $options['mimesupport'] == Horde_ActiveSync::MIME_SUPPORT_SMIME && $imap_message->isSigned())) { $this->_logger->info(sprintf('[%s] Sending MIME Message.', $this->_procid)); // ActiveSync *REQUIRES* all data sent to be in UTF-8, so we // must convert the body parts to UTF-8. Unfortunately if the // email is signed (or encrypted for that matter) we can't // alter the data in anyway or the signature will not be // verified, so we fetch the entire message and hope for the best. if (!$imap_message->isSigned()) { // Sending a non-signed MIME message, start building the // UTF-8 converted structure. $mime = new Horde_Mime_Part(); $mime->setType('multipart/alternative'); // Populate the text/plain part if we have one. if (!empty($message_body_data['plain'])) { $plain_mime = new Horde_Mime_Part(); $plain_mime->setType('text/plain'); $plain_mime->setContents($message_body_data['plain']['body']->stream, array('usestream' => true)); $plain_mime->setCharset('UTF-8'); $mime->addPart($plain_mime); } // Populate the text/html part if we have one. if (!empty($message_body_data['html'])) { $html_mime = new Horde_Mime_Part(); $html_mime->setType('text/html'); $html_mime->setContents($message_body_data['html']['body']->stream, array('usestream' => true)); $html_mime->setCharset('UTF-8'); $mime->addPart($html_mime); } // If we have attachments, create a multipart/mixed wrapper. if ($imap_message->hasAttachments()) { $base = new Horde_Mime_Part(); $base->setType('multipart/mixed'); $base->addPart($mime); $atc = $imap_message->getAttachmentsMimeParts(); foreach ($atc as $atc_part) { $base->addPart($atc_part); } $eas_message->airsyncbaseattachments = $imap_message->getAttachments($version); } else { $base = $mime; } // Populate the EAS body structure with the MIME data. $airsync_body->data = $base->toString(array('headers' => $imap_message->getHeaders(), 'stream' => true)); $airsync_body->estimateddatasize = $base->getBytes(); } else { // Signed/Encrypted message - can't mess with it at all. $raw = new Horde_ActiveSync_Rfc822($imap_message->getFullMsg(true), false); $airsync_body->estimateddatasize = $raw->getBytes(); $airsync_body->data = $raw->getString(); $eas_message->messageclass = 'IPM.Note.SMIME.MultipartSigned'; // Might not know if we have attachments, but take a best // guess. $eas_message->airsyncbaseattachments = $imap_message->getAttachments($version); } // MIME Truncation $airsync_body->type = Horde_ActiveSync::BODYPREF_TYPE_MIME; $this->_logger->info(sprintf('[%s] Checking MIMETRUNCATION: %s, ServerData: %s', $this->_procid, $options['truncation'], $airsync_body->estimateddatasize)); if (!empty($options['truncation']) && $airsync_body->estimateddatasize > $options['truncation']) { ftruncate($airsync_body->data, $options['truncation']); $airsync_body->truncated = '1'; } else { $airsync_body->truncated = '0'; } $eas_message->airsyncbasebody = $airsync_body; } elseif (isset($options['bodyprefs'][Horde_ActiveSync::BODYPREF_TYPE_HTML]) || isset($options['bodyprefs'][Horde_ActiveSync::BODYPREF_TYPE_RTF])) { // Sending non MIME encoded HTML message text. $this->_logger->info(sprintf('[%s] Sending HTML Message.', $this->_procid)); if (empty($message_body_data['html'])) { $airsync_body->type = Horde_ActiveSync::BODYPREF_TYPE_PLAIN; $message_body_data['html'] = array('body' => $message_body_data['plain']['body'], 'estimated_size' => $message_body_data['plain']['size'], 'truncated' => $message_body_data['plain']['truncated']); } else { $airsync_body->type = Horde_ActiveSync::BODYPREF_TYPE_HTML; } if (!empty($message_body_data['html']['estimated_size'])) { $airsync_body->estimateddatasize = $message_body_data['html']['estimated_size']; $airsync_body->truncated = $message_body_data['html']['truncated']; $airsync_body->data = $message_body_data['html']['body']->stream; $eas_message->airsyncbasebody = $airsync_body; } $eas_message->airsyncbaseattachments = $imap_message->getAttachments($version); } elseif (isset($options['bodyprefs'][Horde_ActiveSync::BODYPREF_TYPE_PLAIN])) { // Non MIME encoded plaintext $this->_logger->info(sprintf('[%s] Sending PLAINTEXT Message.', $this->_procid)); if (!empty($message_body_data['plain']['size'])) { $airsync_body->estimateddatasize = $message_body_data['plain']['size']; $airsync_body->truncated = $message_body_data['plain']['truncated']; $airsync_body->data = $message_body_data['plain']['body']->stream; $airsync_body->type = Horde_ActiveSync::BODYPREF_TYPE_PLAIN; $eas_message->airsyncbasebody = $airsync_body; } $eas_message->airsyncbaseattachments = $imap_message->getAttachments($version); } if ($version > Horde_ActiveSync::VERSION_TWELVEONE) { $flags = array(); $msgFlags = $this->_getMsgFlags(); foreach ($imap_message->getFlags() as $flag) { if (($key = array_search(strtolower($flag), array_map('strtolower', $msgFlags))) !== false) { $flags[] = $msgFlags[$key]; } } $eas_message->categories = $flags; } } // Preview? if ($version >= Horde_ActiveSync::VERSION_FOURTEEN && !empty($options['bodyprefs']['preview'])) { $message_body_data['plain']['body']->rewind(); $eas_message->airsyncbasebody->preview = $message_body_data['plain']['body']->substring(0, $options['bodyprefs']['preview']); } // Check for special message types. $part = $imap_message->getStructure(); if ($part->getType() == 'multipart/report') { $ids = array_keys($imap_message->contentTypeMap()); reset($ids); $part1_id = next($ids); $part2_id = Horde_Mime::mimeIdArithmetic($part1_id, 'next'); $lines = explode(chr(13), $imap_message->getBodyPart($part2_id, array('decode' => true))); switch ($part->getContentTypeParameter('report-type')) { case 'delivery-status': foreach ($lines as $line) { if (strpos(trim($line), 'Action:') === 0) { switch (trim(substr(trim($line), 7))) { case 'failed': $eas_message->messageclass = 'REPORT.IPM.NOTE.NDR'; break 2; case 'delayed': $eas_message->messageclass = 'REPORT.IPM.NOTE.DELAYED'; break 2; case 'delivered': $eas_message->messageclass = 'REPORT.IPM.NOTE.DR'; break 2; } } } break; case 'disposition-notification': foreach ($lines as $line) { if (strpos(trim($line), 'Disposition:') === 0) { if (strpos($line, 'displayed') !== false) { $eas_message->messageclass = 'REPORT.IPM.NOTE.IPNRN'; } elseif (strpos($line, 'deleted') !== false) { $eas_message->messageclass = 'REPORT.IPM.NOTE.IPNNRN'; } break; } } } } // Check for meeting requests and POOMMAIL_FLAG data if ($version >= Horde_ActiveSync::VERSION_TWELVE) { $eas_message->contentclass = 'urn:content-classes:message'; if ($mime_part = $imap_message->hasiCalendar()) { $data = Horde_ActiveSync_Utils::ensureUtf8($mime_part->getContents(), $mime_part->getCharset()); $vCal = new Horde_Icalendar(); if ($vCal->parsevCalendar($data, 'VCALENDAR', $mime_part->getCharset())) { try { $method = $vCal->getAttribute('METHOD'); $eas_message->contentclass = 'urn:content-classes:calendarmessage'; } catch (Horde_Icalendar_Exception $e) { } switch ($method) { case 'REQUEST': case 'PUBLISH': $eas_message->messageclass = 'IPM.Schedule.Meeting.Request'; $mtg = Horde_ActiveSync::messageFactory('MeetingRequest'); $mtg->fromvEvent($vCal); $eas_message->meetingrequest = $mtg; break; case 'REPLY': try { $reply_status = $this->_getiTipStatus($vCal, $eas_message->from); switch ($reply_status) { case 'ACCEPTED': $eas_message->messageclass = 'IPM.Schedule.Meeting.Resp.Pos'; break; case 'DECLINED': $eas_message->messageclass = 'IPM.Schedule.Meeting.Resp.Neg'; break; case 'TENTATIVE': $eas_message->messageclass = 'IPM.Schedule.Meeting.Resp.Tent'; } $mtg = Horde_ActiveSync::messageFactory('MeetingRequest'); $mtg->fromvEvent($vCal); $eas_message->meetingrequest = $mtg; } catch (Horde_ActiveSync_Exception $e) { $this->_logger->err($e->getMessage()); } } } } if ($imap_message->getFlag(Horde_Imap_Client::FLAG_FLAGGED)) { $poommail_flag = Horde_ActiveSync::messageFactory('Flag'); $poommail_flag->subject = $imap_message->getSubject(); $poommail_flag->flagstatus = Horde_ActiveSync_Message_Flag::FLAG_STATUS_ACTIVE; $poommail_flag->flagtype = Horde_Imap_Client::FLAG_FLAGGED; $eas_message->flag = $poommail_flag; } } if ($version >= Horde_ActiveSync::VERSION_FOURTEEN) { $eas_message->messageid = $imap_message->getHeaders()->getValue('Message-ID'); $eas_message->forwarded = $imap_message->getFlag(Horde_Imap_Client::FLAG_FORWARDED); $eas_message->answered = $imap_message->getFlag(Horde_Imap_Client::FLAG_ANSWERED); } return $eas_message; }
/** * Log the message */ protected function _logMessage() { $this->_logger->info("MAIL \n" . $this->_getFullMessage()); }
/** * Load and initialize the sync state * * @param array $collection The collection array for the collection, if * a FOLDERSYNC, pass an empty array. * @param string $syncKey The synckey of the state to load. If empty will * force a reset of the state for the class * specified in $id * @param string $type The type of state a * Horde_ActiveSync::REQUEST_TYPE constant. * @param string $id The folder id this state represents. If empty * assumed to be a foldersync state. * * @throws Horde_ActiveSync_Exception, Horde_ActiveSync_Exception_StateGone */ public function loadState(array $collection, $syncKey, $type = null, $id = null) { // Initialize the local members. $this->_collection = $collection; $this->_changes = null; $this->_type = $type; // If this is a FOLDERSYNC, mock the device id. if ($type == Horde_ActiveSync::REQUEST_TYPE_FOLDERSYNC && empty($id)) { $id = Horde_ActiveSync::REQUEST_TYPE_FOLDERSYNC; } // synckey == 0 is an initial sync or reset. if (empty($syncKey)) { $this->_logger->notice(sprintf('[%s] %s::loadState: clearing folder state.', $this->_procid, __CLASS__)); if ($type == Horde_ActiveSync::REQUEST_TYPE_FOLDERSYNC) { $this->_folder = array(); } else { // Create a new folder object. $this->_folder = $this->_collection['class'] == Horde_ActiveSync::CLASS_EMAIL ? new Horde_ActiveSync_Folder_Imap($this->_collection['serverid'], Horde_ActiveSync::CLASS_EMAIL) : ($this->_collection['serverid'] == 'RI' ? new Horde_ActiveSync_Folder_RI('RI', 'RI') : new Horde_ActiveSync_Folder_Collection($this->_collection['serverid'], $this->_collection['class'])); } $this->_syncKey = '0'; $this->_resetDeviceState($id); return; } $this->_logger->info(sprintf('[%s] Loading state for synckey %s', $this->_procid, $syncKey)); // Check if synckey is allowed if (!preg_match('/^\\{([0-9A-Za-z-]+)\\}([0-9]+)$/', $syncKey, $matches)) { throw new Horde_ActiveSync_Exception('Invalid sync key'); } $this->_syncKey = $syncKey; // Cleanup older syncstates $this->_gc($syncKey); // Load the state $this->_loadState($type); }
/** * The heart of the server. Dispatch a request to the appropriate request * handler. * * @param string $cmd The command we are requesting. * @param string $devId The device id making the request. @deprecated * * @return string|boolean false if failed, true if succeeded and response * content is wbxml, otherwise the * content-type string to send in the response. * @throws Horde_ActiveSync_Exception * @throws Horde_ActiveSync_Exception_InvalidRequest * @throws Horde_ActiveSync_PermissionDenied */ public function handleRequest($cmd, $devId) { $get = $this->getGetVars(); if (empty($cmd)) { $cmd = $get['Cmd']; } if (empty($devId)) { $devId = !empty($get['DeviceId']) ? Horde_String::upper($get['DeviceId']) : null; } else { $devId = Horde_String::upper($devId); } $this->_setLogger($get); // @TODO: Remove is_callable check for H6. // Callback to give the backend the option to limit EAS version based // on user/device/etc... if (is_callable(array($this->_driver, 'versionCallback'))) { $this->_driver->versionCallback($this); } // Autodiscovery handles authentication on it's own. if ($cmd == 'Autodiscover') { $request = new Horde_ActiveSync_Request_Autodiscover($this, new Horde_ActiveSync_Device($this->_state)); if (!empty(self::$_logger)) { $request->setLogger(self::$_logger); } $result = $request->handle($this->_request); $this->_driver->clearAuthentication(); return $result; } if (!$this->authenticate(new Horde_ActiveSync_Credentials($this))) { $this->activeSyncHeader(); $this->versionHeader(); $this->commandsHeader(); throw new Horde_Exception_AuthenticationFailure(); } self::$_logger->info(sprintf('[%s] %s request received for user %s', $this->_procid, Horde_String::upper($cmd), $this->_driver->getUser())); // These are all handled in the same class. if ($cmd == 'FolderDelete' || $cmd == 'FolderUpdate') { $cmd = 'FolderCreate'; } // Device id is REQUIRED if (empty($devId)) { if ($cmd == 'Options') { $this->_doOptionsRequest(); $this->_driver->clearAuthentication(); return true; } $this->_driver->clearAuthentication(); throw new Horde_ActiveSync_Exception_InvalidRequest('Device failed to send device id.'); } // EAS Version $version = $this->getProtocolVersion(); // Device. Even though versions of EAS > 12.1 are supposed to send // EAS status codes back to indicate various errors in allowing a client // to connect, we just throw an exception (thus causing a HTTP error // code to be sent as in versions 12.1 and below). Until we refactor for // Horde 6, we don't know the response type to wrap the status code in // until we load the request handler, which requires we start to parse // the WBXML stream and device information etc... This saves resources // as well as keeps things cleaner until we refactor. $device_result = $this->_handleDevice($devId); // Don't bother with everything else if all we want are Options if ($cmd == 'Options') { $this->_doOptionsRequest(); $this->_driver->clearAuthentication(); return true; } // Set provisioning support now that we are authenticated. $this->setProvisioning($this->_driver->getProvisioning(self::$_device)); // Read the initial Wbxml header $this->_decoder->readWbxmlHeader(); // Support Multipart response for ITEMOPERATIONS requests? $headers = $this->_request->getHeaders(); if (!empty($headers['ms-asacceptmultipart']) && $headers['ms-asacceptmultipart'] == 'T' || !empty($get['AcceptMultiPart'])) { $this->_multipart = true; self::$_logger->info(sprintf('[%s] Requesting multipart data.', $this->_procid)); } // Load the request handler to handle the request // We must send the EAS header here, since some requests may start // output and be large enough to flush the buffer (e.g., GetAttachment) // See Bug: 12486 $this->activeSyncHeader(); if ($cmd != 'GetAttachment') { $this->contentTypeHeader(); } // Should we announce a new version is available to the client? if (!empty($this->_needMsRp)) { self::$_logger->info(sprintf('[%s] Announcing X-MS-RP to client.', $this->_procid)); header("X-MS-RP: " . $this->getSupportedVersions()); } // @TODO: Look at getting rid of having to set the version in the driver // and get it from the device object for H6. $this->_driver->setDevice(self::$_device); $class = 'Horde_ActiveSync_Request_' . basename($cmd); if (class_exists($class)) { $request = new $class($this); $request->setLogger(self::$_logger); $result = $request->handle(); self::$_logger->info(sprintf('[%s] Maximum memory usage for ActiveSync request: %d bytes.', $this->_procid, memory_get_peak_usage(true))); return $result; } $this->_driver->clearAuthentication(); throw new Horde_ActiveSync_Exception_InvalidRequest(basename($cmd) . ' not supported.'); }
/** * The heart of the server. Dispatch a request to the appropriate request * handler. * * @param string $cmd The command we are requesting. * @param string $devId The device id making the request. @deprecated * * @return string|boolean false if failed, true if succeeded and response * content is wbxml, otherwise the * content-type string to send in the response. * @throws Horde_ActiveSync_Exception * @throws Horde_ActiveSync_Exception_InvalidRequest * @throws Horde_ActiveSync_PermissionDenied */ public function handleRequest($cmd, $devId) { $get = $this->getGetVars(); if (empty($cmd)) { $cmd = $get['Cmd']; } if (empty($devId)) { $devId = !empty($get['DeviceId']) ? Horde_String::upper($get['DeviceId']) : null; } else { $devId = Horde_String::upper($devId); } $this->_setLogger($get); // @TODO: Remove is_callable check for H6. // Callback to give the backend the option to limit EAS version based // on user/device/etc... if (is_callable(array($this->_driver, 'versionCallback'))) { $this->_driver->versionCallback($this); } // Autodiscovery handles authentication on it's own. if ($cmd == 'Autodiscover') { $request = new Horde_ActiveSync_Request_Autodiscover($this, new Horde_ActiveSync_Device($this->_state)); if (!empty(self::$_logger)) { $request->setLogger(self::$_logger); } $result = $request->handle($this->_request); $this->_driver->clearAuthentication(); return $result; } if (!$this->authenticate(new Horde_ActiveSync_Credentials($this))) { $this->activeSyncHeader(); $this->versionHeader(); $this->commandsHeader(); throw new Horde_Exception_AuthenticationFailure(); } self::$_logger->info(sprintf('[%s] %s request received for user %s', $this->_procid, Horde_String::upper($cmd), $this->_driver->getUser())); // These are all handled in the same class. if ($cmd == 'FolderDelete' || $cmd == 'FolderUpdate') { $cmd = 'FolderCreate'; } // Device id is REQUIRED if (empty($devId)) { if ($cmd == 'Options') { $this->_doOptionsRequest(); $this->_driver->clearAuthentication(); return true; } $this->_driver->clearAuthentication(); throw new Horde_ActiveSync_Exception_InvalidRequest('Device failed to send device id.'); } // EAS Version $version = $this->getProtocolVersion(); // Does device exist AND does the user have an account on the device? if (!$this->_state->deviceExists($devId, $this->_driver->getUser())) { // Device might exist, but with a new (additional) user account if ($this->_state->deviceExists($devId)) { self::$_device = $this->_state->loadDeviceInfo($devId); } else { self::$_device = new Horde_ActiveSync_Device($this->_state); } self::$_device->policykey = 0; self::$_device->userAgent = $this->_request->getHeader('User-Agent'); self::$_device->deviceType = !empty($get['DeviceType']) ? $get['DeviceType'] : ''; self::$_device->rwstatus = self::RWSTATUS_NA; self::$_device->user = $this->_driver->getUser(); self::$_device->id = $devId; self::$_device->needsVersionUpdate($this->getSupportedVersions()); self::$_device->version = $version; // @TODO: Remove is_callable check for H6. // Combine this with the modifyDevice callback? Allow $device // to be modified here? if (is_callable(array($this->_driver, 'createDeviceCallback'))) { $callback_ret = $this->_driver->createDeviceCallback(self::$_device); if ($callback_ret !== true) { $msg = sprintf('The device %s was disallowed for user %s per policy settings.', self::$_device->id, self::$_device->user); self::$_logger->err($msg); if ($version > self::VERSION_TWELVEONE) { $this->_globalError = $callback_ret; } else { throw new Horde_ActiveSync_Exception($msg); } } else { // Give the driver a chance to modify device properties. if (is_callable(array($this->_driver, 'modifyDeviceCallback'))) { self::$_device = $this->_driver->modifyDeviceCallback(self::$_device); } } } } else { self::$_device = $this->_state->loadDeviceInfo($devId, $this->_driver->getUser()); // If the device state was removed from storage, we may lose the // device properties, so try to repopulate what we can. userAgent // is ALWAYS available, so if it's missing, the state is gone. if (empty(self::$_device->userAgent)) { self::$_device->userAgent = $this->_request->getHeader('User-Agent'); self::$_device->deviceType = !empty($get['DeviceType']) ? $get['DeviceType'] : ''; self::$_device->user = $this->_driver->getUser(); } if (empty(self::$_device->version)) { self::$_device->version = $version; } if (self::$_device->version < $this->_maxVersion && self::$_device->needsVersionUpdate($this->getSupportedVersions())) { $needMsRp = true; } // Give the driver a chance to modify device properties. if (is_callable(array($this->_driver, 'modifyDeviceCallback'))) { self::$_device = $this->_driver->modifyDeviceCallback(self::$_device); } } self::$_device->save(); if (is_callable(array($this->_driver, 'deviceCallback'))) { $callback_ret = $this->_driver->deviceCallback(self::$_device); if ($callback_ret !== true) { $msg = sprintf('The device %s was disallowed for user %s per policy settings.', self::$_device->id, self::$_device->user); self::$_logger->err($msg); if ($version > self::VERSION_TWELVEONE) { $this->_globalError = $callback_ret; } else { throw new Horde_ActiveSync_Exception($msg); } } } // Lastly, check if the device has been set to blocked. if (self::$_device->blocked) { $msg = sprintf('The device %s was blocked.', self::$_device->id); self::$_logger->err($msg); if ($version > self::VERSION_TWELVEONE) { $this->_globalError = Horde_ActiveSync_Status::DEVICE_BLOCKED_FOR_USER; } else { throw new Horde_ActiveSync_Exception($msg); } } // Don't bother with everything else if all we want are Options if ($cmd == 'Options') { $this->_doOptionsRequest(); $this->_driver->clearAuthentication(); return true; } // Set provisioning support now that we are authenticated. $this->setProvisioning($this->_driver->getProvisioning(self::$_device)); // Read the initial Wbxml header $this->_decoder->readWbxmlHeader(); // Support Multipart response for ITEMOPERATIONS requests? $headers = $this->_request->getHeaders(); if (!empty($headers['ms-asacceptmultipart']) && $headers['ms-asacceptmultipart'] == 'T' || !empty($get['AcceptMultiPart'])) { $this->_multipart = true; self::$_logger->info(sprintf('[%s] Requesting multipart data.', $this->_procid)); } // Load the request handler to handle the request // We must send the eas header here, since some requests may start // output and be large enough to flush the buffer (e.g., GetAttachment) // See Bug: 12486 $this->activeSyncHeader(); if ($cmd != 'GetAttachment') { $this->contentTypeHeader(); } // Should we announce a new version is available to the client? if (!empty($needMsRp)) { self::$_logger->info(sprintf('[%s] Announcing X-MS-RP to client.', $this->_procid)); header("X-MS-RP: " . $this->getSupportedVersions()); } // @TODO: Look at getting rid of having to set the version in the driver // and get it from the device object for H6. $this->_driver->setDevice(self::$_device); $class = 'Horde_ActiveSync_Request_' . basename($cmd); if (class_exists($class)) { $request = new $class($this); $request->setLogger(self::$_logger); $result = $request->handle(); self::$_logger->info(sprintf('[%s] Maximum memory usage for ActiveSync request: %d bytes.', $this->_procid, memory_get_peak_usage(true))); return $result; } $this->_driver->clearAuthentication(); throw new Horde_ActiveSync_Exception_InvalidRequest(basename($cmd) . ' not supported.'); }
/** * @param string $text */ public function log($text = '') { if ($this->_logger) { $this->_logger->info($text); } }
/** * Encodes this object (and any sub-objects) as wbxml to the output stream. * Output is ordered according to $_mapping * * @param Horde_ActiveSync_Wbxml_Encoder $encoder The wbxml stream encoder * @throws Horde_ActiveSync_Exception */ public function encodeStream(Horde_ActiveSync_Wbxml_Encoder &$encoder) { if (!$this->_preEncodeValidation()) { $this->_logger->err(sprintf('Pre-encoding validation failed for %s item', get_class($this))); throw new Horde_ActiveSync_Exception(sprintf('Pre-encoding validation failded for %s item', get_class($this))); } foreach ($this->_mapping as $tag => $map) { if (isset($this->{$map[self::KEY_ATTRIBUTE]})) { // Variable is available if (is_object($this->{$map[self::KEY_ATTRIBUTE]}) && !$this->{$map[self::KEY_ATTRIBUTE]} instanceof Horde_Date) { // Objects can do their own encoding $encoder->startTag($tag); $this->{$map[self::KEY_ATTRIBUTE]}->encodeStream($encoder); $encoder->endTag(); } elseif (isset($map[self::KEY_VALUES]) && is_array($this->{$map[self::KEY_ATTRIBUTE]})) { // Array of objects. Note that some array values must be // send as an empty tag if they contain no elements. if (count($this->{$map[self::KEY_ATTRIBUTE]})) { if (!isset($map[self::KEY_PROPERTY]) || $map[self::KEY_PROPERTY] != self::PROPERTY_NO_CONTAINER) { $encoder->startTag($tag); } foreach ($this->{$map[self::KEY_ATTRIBUTE]} as $element) { if (is_object($element)) { // Hanlde multi-typed array containers. if (is_array($map[self::KEY_VALUES])) { $idx = array_search(get_class($element), $map[self::KEY_TYPE]); $tag = $map[self::KEY_VALUES][$idx]; } else { $tag = $map[self::KEY_VALUES]; } // Outputs object container (eg Attachment) $encoder->startTag($tag); $element->encodeStream($encoder); $encoder->endTag(); } else { // Do not ever output empty items here if (strlen($element) > 0) { $encoder->startTag($map[self::KEY_VALUES]); $encoder->content($element); $encoder->endTag(); } } } if (!isset($map[self::KEY_PROPERTY]) || $map[self::KEY_PROPERTY] != self::PROPERTY_NO_CONTAINER) { $encoder->endTag(); } } elseif ($this->_checkSendEmpty($tag)) { $encoder->startTag($tag, null, true); } } else { // Simple type if (!is_resource($this->{$map[self::KEY_ATTRIBUTE]}) && strlen($this->{$map[self::KEY_ATTRIBUTE]}) == 0) { // Do not output empty items except for the following: if ($this->_checkSendEmpty($tag)) { $encoder->startTag($tag, $this->{$map[self::KEY_ATTRIBUTE]}, true); } continue; } elseif ($encoder->multipart && in_array($tag, array(Horde_ActiveSync::SYNC_DATA, Horde_ActiveSync::AIRSYNCBASE_DATA, Horde_ActiveSync_Request_ItemOperations::ITEMOPERATIONS_DATA))) { $this->_logger->info('HANDLING MULTIPART OUTPUT'); $encoder->addPart($this->{$map[self::KEY_ATTRIBUTE]}); $encoder->startTag(Horde_ActiveSync_Request_ItemOperations::ITEMOPERATIONS_PART); $encoder->content((string) (count($encoder->getParts()) - 1)); $encoder->endTag(); continue; } $encoder->startTag($tag); if (isset($map[self::KEY_TYPE]) && in_array($map[self::KEY_TYPE], array(self::TYPE_DATE, self::TYPE_DATE_DASHES, self::TYPE_DATE_LOCAL))) { if (!empty($this->{$map[self::KEY_ATTRIBUTE]})) { // don't output 1-1-1970 $encoder->content($this->_formatDate($this->{$map[self::KEY_ATTRIBUTE]}, $map[self::KEY_TYPE])); } } elseif (isset($map[self::KEY_TYPE]) && $map[self::KEY_TYPE] == self::TYPE_HEX) { $encoder->content(Horde_String::upper(bin2hex($this->{$map[self::KEY_ATTRIBUTE]}))); } elseif (isset($map[self::KEY_TYPE]) && $map[self::KEY_TYPE] == self::TYPE_MAPI_STREAM) { $encoder->content($this->{$map[self::KEY_ATTRIBUTE]}); } else { $encoder->content($this->_checkEncoding($this->{$map[self::KEY_ATTRIBUTE]}, $tag)); } $encoder->endTag(); } } } }
/** * Clean up after initial pairing. Initial pairing can happen either as a * result of either a FOLDERSYNC or PROVISION command, depending on the * device capabilities. */ protected function _cleanUpAfterPairing() { // Android sends a bogus device id of 'validate' during initial // handshake. This data is never used again, and the resulting // FOLDERSYNC response is ignored by the client. Remove the entry, // to avoid having 2 device entries for every android client. if ($this->_device->id == 'validate') { $this->_logger->info(sprintf('[%s] Removing state for bogus VALIDATE device.', $this->_procid)); $this->_state->removeState(array('devId' => 'validate')); } }
/** * Update a collection in the cache. * * @param array $collection The collection data to add/update. * @param array $options Options: * - newsynckey: (boolean) Set the new synckey in the collection. * DEFAULT: false (Do not set the new synckey). * - unsetChanges: (boolean) Unset the GETCHANGES flag in the collection. * DEFAULT: false (Do not unset the GETCHANGES flag). * - unsetPingChangeFlag: (boolean) Unset the PINGCHANGES flag in the collection. * DEFUALT: false (Do not uset the PINGCHANGES flag). * @since 2.3.0 */ public function updateCollection(array $collection, array $options = array()) { $options = array_merge(array('newsynckey' => false, 'unsetChanges' => false, 'unsetPingChangeFlag' => false), $options); if (!empty($collection['id'])) { if ($options['newsynckey']) { $this->_data['collections'][$collection['id']]['synckey'] = $collection['newsynckey']; $this->_markCollectionsDirty($collection['id']); } elseif (isset($collection['synckey'])) { $this->_data['collections'][$collection['id']]['synckey'] = $collection['synckey']; $this->_markCollectionsDirty($collection['id']); } if (isset($collection['class'])) { $this->_data['collections'][$collection['id']]['class'] = $collection['class']; $this->_markCollectionsDirty($collection['id']); } if (isset($collection['windowsize'])) { $this->_data['collections'][$collection['id']]['windowsize'] = $collection['windowsize']; $this->_markCollectionsDirty($collection['id']); } if (isset($collection['deletesasmoves'])) { $this->_data['collections'][$collection['id']]['deletesasmoves'] = $collection['deletesasmoves']; $this->_markCollectionsDirty($collection['id']); } if (isset($collection['filtertype'])) { $this->_data['collections'][$collection['id']]['filtertype'] = $collection['filtertype']; $this->_markCollectionsDirty($collection['id']); } if (isset($collection['truncation'])) { $this->_data['collections'][$collection['id']]['truncation'] = $collection['truncation']; $this->_markCollectionsDirty($collection['id']); } if (isset($collection['rtftruncation'])) { $this->_data['collections'][$collection['id']]['rtftruncation'] = $collection['rtftruncation']; $this->_markCollectionsDirty($collection['id']); } if (isset($collection['mimesupport'])) { $this->_data['collections'][$collection['id']]['mimesupport'] = $collection['mimesupport']; $this->_markCollectionsDirty($collection['id']); } if (isset($collection['mimetruncation'])) { $this->_data['collections'][$collection['id']]['mimetruncation'] = $collection['mimetruncation']; $this->_markCollectionsDirty($collection['id']); } if (isset($collection['conflict'])) { $this->_data['collections'][$collection['id']]['conflict'] = $collection['conflict']; $this->_markCollectionsDirty($collection['id']); } if (isset($collection['bodyprefs'])) { $this->_data['collections'][$collection['id']]['bodyprefs'] = $collection['bodyprefs']; $this->_markCollectionsDirty($collection['id']); } if (isset($collection['bodypartprefs'])) { $this->_data['collections'][$collection['id']]['bodypartprefs'] = $collection['bodypartprefs']; } if (isset($collection['pingable'])) { $this->_data['collections'][$collection['id']]['pingable'] = $collection['pingable']; $this->_markCollectionsDirty($collection['id']); } if (isset($collection['serverid'])) { $this->_data['collections'][$collection['id']]['serverid'] = $collection['serverid']; $this->_markCollectionsDirty($collection['id']); } if ($options['unsetChanges']) { unset($this->_data['collections'][$collection['id']]['getchanges']); $this->_markCollectionsDirty($collection['id']); } if ($options['unsetPingChangeFlag']) { unset($this->_data['collections'][$collection['id']]['pingchange']); $this->_markCollectionsDirty($collection['id']); } } else { $this->_logger->info(sprintf('[%s] Collection without id found: %s', $this->_procid, serialize($collection))); } }
/** * Encodes this object (and any sub-objects) as wbxml to the output stream. * Output is ordered according to $_mapping * * @param Horde_ActiveSync_Wbxml_Encoder $encoder The wbxml stream encoder */ public function encodeStream(Horde_ActiveSync_Wbxml_Encoder &$encoder) { foreach ($this->_mapping as $tag => $map) { if (isset($this->{$map}[self::KEY_ATTRIBUTE])) { // Variable is available if (is_object($this->{$map}[self::KEY_ATTRIBUTE]) && !$this->{$map}[self::KEY_ATTRIBUTE] instanceof Horde_Date) { // Subobjects can do their own encoding $encoder->startTag($tag); $this->{$map}[self::KEY_ATTRIBUTE]->encodeStream($encoder); $encoder->endTag(); } elseif (isset($map[self::KEY_VALUES]) && is_array($this->{$map}[self::KEY_ATTRIBUTE])) { // Array of objects. Note that some array values must be // send as an empty tag if they contain no elements. if (count($this->{$map}[self::KEY_ATTRIBUTE])) { $encoder->startTag($tag); foreach ($this->{$map}[self::KEY_ATTRIBUTE] as $element) { if (is_object($element)) { // Outputs object container (eg Attachment) $encoder->startTag($map[self::KEY_VALUES]); $element->encodeStream($encoder); $encoder->endTag(); } else { // Do not ever output empty items here if (strlen($element) > 0) { $encoder->startTag($map[self::KEY_VALUES]); $encoder->content($element); $encoder->endTag(); } } } $encoder->endTag(); } elseif ($this->_checkSendEmpty($tag)) { $encoder->startTag($tag, null, true); } } else { // Simple type if (!is_resource($this->{$map}[self::KEY_ATTRIBUTE]) && strlen($this->{$map}[self::KEY_ATTRIBUTE]) == 0) { // Do not output empty items except for the following: if ($this->_checkSendEmpty($tag)) { $encoder->startTag($tag, $this->{$map}[self::KEY_ATTRIBUTE], true); } continue; } elseif ($encoder->multipart && in_array($tag, array(Horde_ActiveSync::SYNC_DATA, Horde_ActiveSync::AIRSYNCBASE_DATA, Horde_ActiveSync_Request_ItemOperations::ITEMOPERATIONS_DATA))) { $this->_logger->info('HANDLING MULTIPART OUTPUT'); $encoder->addPart($this->{$map}[self::KEY_ATTRIBUTE]); $encoder->startTag(Horde_ActiveSync_Request_ItemOperations::ITEMOPERATIONS_PART); $encoder->content((string) (count($encoder->getParts()) - 1)); $encoder->endTag(); continue; } $encoder->startTag($tag); if (isset($map[self::KEY_TYPE]) && ($map[self::KEY_TYPE] == self::TYPE_DATE || $map[self::KEY_TYPE] == self::TYPE_DATE_DASHES)) { if (!empty($this->{$map}[self::KEY_ATTRIBUTE])) { // don't output 1-1-1970 $encoder->content($this->_formatDate($this->{$map}[self::KEY_ATTRIBUTE], $map[self::KEY_TYPE])); } } elseif (isset($map[self::KEY_TYPE]) && $map[self::KEY_TYPE] == self::TYPE_HEX) { $encoder->content(Horde_String::upper(bin2hex($this->{$map}[self::KEY_ATTRIBUTE]))); } elseif (isset($map[self::KEY_TYPE]) && $map[self::KEY_TYPE] == self::TYPE_MAPI_STREAM) { $encoder->content($this->{$map}[self::KEY_ATTRIBUTE]); } else { $encoder->content($this->_checkEncoding($this->{$map}[self::KEY_ATTRIBUTE], $tag)); } $encoder->endTag(); } } } }