diff --git a/src/backend/ipcsharedmemory/ipcsharedmemoryprovider.php b/src/backend/ipcsharedmemory/ipcsharedmemoryprovider.php index 35b54534..3cb8b510 100644 --- a/src/backend/ipcsharedmemory/ipcsharedmemoryprovider.php +++ b/src/backend/ipcsharedmemory/ipcsharedmemoryprovider.php @@ -41,7 +41,7 @@ public function __construct($type, $allocate, $class, $serverKey) { $this->type = $type; $this->allocate = $allocate; - if ($this->initSharedMem()) + if ($this->initSharedMem()) ZLog::Write(LOGLEVEL_DEBUG, sprintf("%s(): Initialized.", $class)); } diff --git a/src/backend/kopano/importer.php b/src/backend/kopano/importer.php index 30d1e452..386d8e4e 100644 --- a/src/backend/kopano/importer.php +++ b/src/backend/kopano/importer.php @@ -440,7 +440,7 @@ public function ImportMessageChange($id, $message) { $flags = SYNC_NEW_MESSAGE; if(mapi_importcontentschanges_importmessagechange($this->importer, $props, $flags, $mapimessage)) { - $this->mapiprovider->SetMessage($mapimessage, $message); + $response = $this->mapiprovider->SetMessage($mapimessage, $message); mapi_savechanges($mapimessage); if (mapi_last_hresult()) @@ -448,7 +448,9 @@ public function ImportMessageChange($id, $message) { $sourcekeyprops = mapi_getprops($mapimessage, array (PR_SOURCE_KEY)); - return $this->prefix . bin2hex($sourcekeyprops[PR_SOURCE_KEY]); + $response->serverid = $this->prefix . bin2hex($sourcekeyprops[PR_SOURCE_KEY]); + + return $response; } else throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Error updating object: 0x%X", $id, get_class($message), mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); @@ -480,9 +482,19 @@ public function ImportMessageDeletion($id, $asSoftDelete = false) { return true; } + // check if we need to do actions before deleting this message (e.g. send meeting cancellations to attendees) + $entryid = mapi_msgstore_entryidfromsourcekey($this->store, $this->folderid, hex2bin($sk)); + if ($entryid) { + // open the source message + $mapimessage = mapi_msgstore_openentry($this->store, $entryid); + $this->mapiprovider->PreDeleteMessage($mapimessage); + } + // do a 'soft' delete so people can un-delete if necessary - if(mapi_importcontentschanges_importmessagedeletion($this->importer, 1, array(hex2bin($sk)))) + mapi_importcontentschanges_importmessagedeletion($this->importer, 1, [hex2bin($sk)]); + if (mapi_last_hresult()) { throw new StatusException(sprintf("ImportChangesICS->ImportMessageDeletion('%s'): Error updating object: 0x%X", $sk, mapi_last_hresult()), SYNC_STATUS_OBJECTNOTFOUND); + } return true; } @@ -496,10 +508,11 @@ public function ImportMessageDeletion($id, $asSoftDelete = false) { * @param array $categories * * @access public - * @return boolean + * @return SyncObject * @throws StatusException */ public function ImportMessageReadFlag($id, $flags, $categories = array()) { + $response = new SyncMailResponse(); list($fsk,$sk) = Utils::SplitMessageId($id); // if $fsk is set, we convert it into a backend id. @@ -545,7 +558,7 @@ public function ImportMessageReadFlag($id, $flags, $categories = array()) { $p = mapi_message_setreadflag($realMessage, $flag); ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesICS->ImportMessageReadFlag('%s','%d'): setting readflag on message: 0x%X", $id, $flags, mapi_last_hresult())); } - return true; + return $response; } /** diff --git a/src/backend/kopano/kopano.php b/src/backend/kopano/kopano.php index 66848c26..4febf204 100644 --- a/src/backend/kopano/kopano.php +++ b/src/backend/kopano/kopano.php @@ -805,7 +805,7 @@ public function GetAttachmentData($attname) { if(!strpos($attname, ":")) throw new StatusException(sprintf("KopanoBackend->GetAttachmentData('%s'): Error, attachment requested for non-existing item", $attname), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); - list($id, $attachnum, $parentEntryid) = explode(":", $attname); + list($id, $attachnum, $parentEntryid, $exceptionBasedate) = explode(":", $attname); if (isset($parentEntryid)) { $this->Setup(ZPush::GetAdditionalSyncFolderStore($parentEntryid)); } @@ -820,6 +820,14 @@ public function GetAttachmentData($attname) { if(!$attach) throw new StatusException(sprintf("KopanoBackend->GetAttachmentData('%s'): Error, unable to open attachment number '%s' with: 0x%X", $attname, $attachnum, mapi_last_hresult()), SYNC_ITEMOPERATIONSSTATUS_INVALIDATT); + // attachment of a recurring appointment execption + if(strlen($exceptionBasedate) > 1) { + $recurrence = new Recurrence($this->store, $message); + $exceptionatt = $recurrence->getExceptionAttachment(hex2bin($exceptionBasedate)); + $exceptionobj = mapi_attach_openobj($exceptionatt, 0); + $attach = mapi_message_openattach($exceptionobj, $attachnum); + } + // get necessary attachment props $attprops = mapi_getprops($attach, array(PR_ATTACH_MIME_TAG, PR_ATTACH_MIME_TAG_W, PR_ATTACH_METHOD)); $attachment = new SyncItemOperationsAttachment(); @@ -895,7 +903,9 @@ public function EmptyFolder($folderid, $includeSubfolders = true) { * @return string id of the created/updated calendar obj * @throws StatusException */ - public function MeetingResponse($requestid, $folderid, $response) { + public function MeetingResponse($requestid, $folderid, $request) { + $requestid = $calendarid = $request['requestid']; + $response = $request['response']; // Use standard meeting response code to process meeting request list($fid, $requestid) = Utils::SplitMessageId($requestid); $reqentryid = mapi_msgstore_entryidfromsourcekey($this->store, hex2bin($folderid), hex2bin($requestid)); @@ -908,9 +918,15 @@ public function MeetingResponse($requestid, $folderid, $response) { // ios sends calendar item in MeetingResponse // @see https://jira.z-hub.io/browse/ZP-1524 + $searchForResultCalendarItem = false; $folderClass = ZPush::GetDeviceManager()->GetFolderClassFromCacheByID($fid); // find the corresponding meeting request - if ($folderClass != 'Email') { + if ($folderClass == 'Email') { + // The mobile requested this on a MR, when finishing we need to search for the resulting calendar item! + $searchForResultCalendarItem = true; + } + // we are operating on the calendar item - try searching for the corresponding meeting request first + else { $props = MAPIMapping::GetMeetingRequestProperties(); $props = getPropIdsFromStrings($this->store, $props); @@ -934,18 +950,23 @@ public function MeetingResponse($requestid, $folderid, $response) { $inboxcontents = mapi_folder_getcontentstable($folder); - $rows = mapi_table_queryallrows($inboxcontents, array(PR_ENTRYID), $restrict); - if (empty($rows)) { - throw new StatusException(sprintf("BackendKopano->MeetingResponse('%s','%s', '%s'): Error, meeting request not found in the inbox", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ); + $rows = mapi_table_queryallrows($inboxcontents, [PR_ENTRYID, PR_SOURCE_KEY], $restrict); + // AS 14.0 and older can only respond to a MR in the Inbox! + if (empty($rows) && Request::GetProtocolVersion() <= 14.0) { + throw new StatusException(sprintf("BackendKopano->MeetingResponse('%s','%s', '%s'): Error, meeting request not found in the inbox. Can't proceed, aborting!", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ); + } + if (!empty($rows)) { + ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendKopano->MeetingResponse found meeting request in the inbox with ID: %s", bin2hex($rows[0][PR_SOURCE_KEY]))); + $reqentryid = $rows[0][PR_ENTRYID]; + $mapimessage = mapi_msgstore_openentry($this->store, $reqentryid); + // As we are using an MR from the inbox, when finishing we need to search for the resulting calendar item! + $searchForResultCalendarItem = true; } - ZLog::Write(LOGLEVEL_DEBUG, "BackendKopano->MeetingResponse found meeting request in the inbox"); - $mapimessage = mapi_msgstore_openentry($this->store, $rows[0][PR_ENTRYID]); - $reqentryid = $rows[0][PR_ENTRYID]; } $meetingrequest = new Meetingrequest($this->store, $mapimessage, $this->session); - if(!$meetingrequest->isMeetingRequest()) + if (Request::GetProtocolVersion() <= 14.0 && !$meetingrequest->isMeetingRequest() && !$meetingrequest->isMeetingRequestResponse() && !$meetingrequest->isMeetingCancellation()) { throw new StatusException(sprintf("BackendKopano->MeetingResponse('%s','%s', '%s'): Error, attempt to respond to non-meeting request", $requestid, $folderid, $response), SYNC_MEETRESPSTATUS_INVALIDMEETREQ); if($meetingrequest->isLocalOrganiser()) @@ -956,14 +977,29 @@ public function MeetingResponse($requestid, $folderid, $response) { // anymore for the ios devices since at least version 12.4. Z-Push will send the // accepted email in such a case. // @see https://jira.z-hub.io/browse/ZP-1524 - $sendresponse = false; - $deviceType = strtolower(Request::GetDeviceType()); - if ($deviceType == 'iphone' || $deviceType == 'ipad' || $deviceType == 'ipod') { - $matches = array(); - if (preg_match("/^Apple-.*?\/(\d{4})\./", Request::GetUserAgent(), $matches) && isset($matches[1]) && $matches[1] >= 1607 && $matches[1] <= 1707) { - ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendKopano->MeetingResponse: iOS device %s->%s", Request::GetDeviceType(), Request::GetUserAgent())); - $sendresponse = true; - } + // AS-16.1: did the attendee propose a new time ? + if (!empty($request['proposedstarttime'])) { + $request['proposedstarttime'] = Utils::parseDate($request['proposedstarttime']); + } + else { + $request['proposedstarttime'] = false; + } + if (!empty($request['proposedendtime'])) { + $request['proposedendtime'] = Utils::parseDate($request['proposedendtime']); + } + else { + $request['proposedendtime'] = false; + } + if (!isset($request['body'])) { + $request['body'] = false; + } + // from AS-14.0 we have to take care of sending out meeting request responses + if (Request::GetProtocolVersion() >= 14.0) { + $sendresponse = true; + } + else { + // Old AS versions send MR updates by themselves - so our MR processing doesn't need to do this + $sendresponse = false; } switch($response) { case 1: // accept @@ -971,7 +1007,7 @@ public function MeetingResponse($requestid, $folderid, $response) { $entryid = $meetingrequest->doAccept(false, $sendresponse, false, false, false, false, true); // last true is the $userAction break; case 2: // tentative - $entryid = $meetingrequest->doAccept(true, $sendresponse, false, false, false, false, true); // last true is the $userAction + $entryid = $meetingrequest->doAccept(true, $sendresponse, false, $request['proposedstarttime'], $request['proposedendtime'], $request['body'], true); // last true is the $userAction break; case 3: // decline $meetingrequest->doDecline(false); @@ -980,19 +1016,21 @@ public function MeetingResponse($requestid, $folderid, $response) { // F/B will be updated on logoff - // We have to return the ID of the new calendar item, so do that here - $calendarid = ""; - $calFolderId = ""; - if (isset($entryid)) { - $newitem = mapi_msgstore_openentry($this->store, $entryid); - // new item might be in a delegator's store. ActiveSync does not support accepting them. - if (!$newitem) { - throw new StatusException(sprintf("BackendKopano->MeetingResponse('%s','%s', '%s'): Object with entryid '%s' was not found in user's store (0x%X). It might be in a delegator's store.", $requestid, $folderid, $response, bin2hex($entryid), mapi_last_hresult()), SYNC_MEETRESPSTATUS_SERVERERROR, null, LOGLEVEL_WARN); - } + // We have to return the ID of the new calendar item if it was created from an email + if ($searchForResultCalendarItem) { + $calendarid = ""; + $calFolderId = ""; + if (isset($entryid)) { + $newitem = mapi_msgstore_openentry($this->store, $entryid); + // new item might be in a delegator's store. ActiveSync does not support accepting them. + if (!$newitem) { + throw new StatusException(sprintf("Grommunio->MeetingResponse('%s','%s', '%s'): Object with entryid '%s' was not found in user's store (0x%X). It might be in a delegator's store.", $requestid, $folderid, $response, bin2hex($entryid), mapi_last_hresult()), SYNC_MEETRESPSTATUS_SERVERERROR, null, LOGLEVEL_WARN); + } - $newprops = mapi_getprops($newitem, array(PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY)); - $calendarid = bin2hex($newprops[PR_SOURCE_KEY]); - $calFolderId = bin2hex($newprops[PR_PARENT_SOURCE_KEY]); + $newprops = mapi_getprops($newitem, array(PR_SOURCE_KEY, PR_PARENT_SOURCE_KEY)); + $calendarid = bin2hex($newprops[PR_SOURCE_KEY]); + $calFolderId = bin2hex($newprops[PR_PARENT_SOURCE_KEY]); + } } // on recurring items, the MeetingRequest class responds with a wrong entryid @@ -1499,7 +1537,7 @@ public function TerminateSearch($pid) { } $storeProps = mapi_getprops($this->store, array(PR_STORE_SUPPORT_MASK, PR_FINDER_ENTRYID)); - if (($storeProps[PR_STORE_SUPPORT_MASK] & STORE_SEARCH_OK) != STORE_SEARCH_OK) { + if (isset($storeProps[PR_STORE_SUPPORT_MASK]) && (($storeProps[PR_STORE_SUPPORT_MASK] & STORE_SEARCH_OK) != STORE_SEARCH_OK)) { ZLog::Write(LOGLEVEL_WARN, "Store doesn't support search folders. Public store doesn't have FINDER_ROOT folder"); return false; } diff --git a/src/backend/kopano/mapi/class.meetingrequest.php b/src/backend/kopano/mapi/class.meetingrequest.php index 28eb344c..4c288242 100644 --- a/src/backend/kopano/mapi/class.meetingrequest.php +++ b/src/backend/kopano/mapi/class.meetingrequest.php @@ -612,7 +612,7 @@ function isInCalendar() * @param string $newProposedStartTime contains starttime if user has proposed other time * @param string $newProposedEndTime contains endtime if user has proposed other time * @param string $basedate start of day of occurrence for which user has accepted the recurrent meeting - * @param boolean $isImported true to indicate that MR is imported from .ics or .vcs file else it false. + * @param boolean $isImported true to indicate that MR is imported from .ics or .vcs file else it false. * @return string $entryid entryid of item which created/updated in calendar */ function doAccept($tentative, $sendresponse, $move, $newProposedStartTime=false, $newProposedEndTime=false, $body=false, $userAction = false, $store=false, $basedate = false, $isImported = false) @@ -1830,7 +1830,7 @@ function checkCalendarWriteAccess($store = false) /** * Function will resolve the user and open its store - * @param String $ownerentryid the entryid of the user + * @param String $ownerentryid the entryid of the user * @return MAPIStore store of the user */ function openCustomUserStore($ownerentryid) @@ -3742,7 +3742,7 @@ function getLocalCategories($calendarItem, $store, $calFolder) } return $localCategories; - } + } /** * Helper function which is use to apply local categories on respective occurrences. diff --git a/src/backend/kopano/mapimapping.php b/src/backend/kopano/mapimapping.php index 62012be4..d79b1042 100644 --- a/src/backend/kopano/mapimapping.php +++ b/src/backend/kopano/mapimapping.php @@ -199,6 +199,7 @@ public static function GetEmailProperties() { "rtfinsync" => PR_RTF_IN_SYNC, "processed" => PR_PROCESSED, "messageflags" => PR_MESSAGE_FLAGS, + "clientsubmittime" => PR_CLIENT_SUBMIT_TIME, ); } @@ -341,6 +342,10 @@ public static function GetAppointmentProperties() { "rtfcompressed" => PR_RTF_COMPRESSED, "html" => PR_HTML, "rtfinsync" => PR_RTF_IN_SYNC, + "entryid" => PR_ENTRYID, + "parentsourcekey" => PR_PARENT_SOURCE_KEY, + "location" => "PT_STRING8:PSETID_Appointment:0x8208", + "locations" => "PT_STRING8:PSETID_CustomerLocation:Locations", ); } diff --git a/src/backend/kopano/mapiprovider.php b/src/backend/kopano/mapiprovider.php index f1ce3625..69b0f4b6 100644 --- a/src/backend/kopano/mapiprovider.php +++ b/src/backend/kopano/mapiprovider.php @@ -154,7 +154,7 @@ private function getTask($mapimessage, $contentparameters) { !$messageprops[$taskproperties["deadoccur"]]))) { // Process recurrence $message->recurrence = new SyncTaskRecurrence(); - $this->getRecurrence($mapimessage, $messageprops, $message, $message->recurrence, false); + $this->getRecurrence($mapimessage, $messageprops, $message, $message->recurrence, false, $taskproperties); } // when set the task to complete using the WebAccess, the dateComplete property is not set correctly @@ -234,7 +234,7 @@ private function getAppointment($mapimessage, $contentparameters) { if(isset($messageprops[$appointmentprops["isrecurring"]]) && $messageprops[$appointmentprops["isrecurring"]]) { // Process recurrence $message->recurrence = new SyncRecurrence(); - $this->getRecurrence($mapimessage, $messageprops, $message, $message->recurrence, $tz); + $this->getRecurrence($mapimessage, $messageprops, $message, $message->recurrence, $tz, $appointmentprops); // outlook seems to honour the timezone information contrary to other clients if (empty($message->alldayevent) || Request::IsOutlook()) { @@ -377,6 +377,16 @@ private function getAppointment($mapimessage, $contentparameters) { } } + // Add attachments to message for AS 16.0 and higher + if (Request::GetProtocolVersion() >= 16.0) { + // add attachments + $entryid = bin2hex($messageprops[$appointmentprops["entryid"]]); + $parentSourcekey = bin2hex($messageprops[$appointmentprops["parentsourcekey"]]); + $this->setAttachment($mapimessage, $message, $entryid, $parentSourcekey); + // add location + $message->location2 = new SyncLocation(); + $this->getASlocation($mapimessage, $message->location2, $appointmentprops); + } return $message; } @@ -388,11 +398,12 @@ private function getAppointment($mapimessage, $contentparameters) { * @param SyncObject &$syncMessage the message * @param SyncObject &$syncRecurrence the recurrence message * @param array $tz timezone information + * @param array $appointmentprops property defintions * * @access private * @return */ - private function getRecurrence($mapimessage, $recurprops, &$syncMessage, &$syncRecurrence, $tz) { + private function getRecurrence($mapimessage, $recurprops, &$syncMessage, &$syncRecurrence, $tz, $appointmentprops) { if ($syncRecurrence instanceof SyncTaskRecurrence) $recurrence = new TaskRecurrence($this->store, $mapimessage); else @@ -491,13 +502,22 @@ private function getRecurrence($mapimessage, $recurprops, &$syncMessage, &$syncR if(isset($change["end"])) $exception->endtime = $this->getGMTTimeByTZ($change["end"], $tz); if(isset($change["basedate"])) { - $exception->exceptionstarttime = $this->getGMTTimeByTZ($this->getDayStartOfTimestamp($change["basedate"]) + $recurrence->recur["startocc"] * 60, $tz); + // depending on the AS version the streamer is going to send the correct value + $exception->exceptionstarttime = $exception->instanceid = $this->getGMTTimeByTZ($this->getDayStartOfTimestamp($change["basedate"]) + $recurrence->recur["startocc"] * 60, $tz); //open body because getting only property might not work because of memory limit $exceptionatt = $recurrence->getExceptionAttachment($change["basedate"]); if($exceptionatt) { $exceptionobj = mapi_attach_openobj($exceptionatt, 0); $this->setMessageBodyForType($exceptionobj, SYNC_BODYPREFERENCE_PLAIN, $exception); + if (Request::GetProtocolVersion() >= 16.0) { + // add attachment + $data = mapi_message_getprops($mapimessage, [PR_ENTRYID, PR_PARENT_SOURCE_KEY]); + $this->setAttachment($exceptionobj, $exception, bin2hex($data[PR_ENTRYID]), bin2hex($data[PR_PARENT_SOURCE_KEY]), bin2hex($change["basedate"])); + // add location + $exception->location2 = new SyncLocation(); + $this->getASlocation($exceptionobj, $exception->location2, $appointmentprops); + } } } if(isset($change["subject"])) @@ -545,7 +565,8 @@ private function getRecurrence($mapimessage, $recurprops, &$syncMessage, &$syncR foreach($recurrence->recur["deleted_occurences"] as $deleted) { $exception = new SyncAppointmentException(); - $exception->exceptionstarttime = $this->getGMTTimeByTZ($this->getDayStartOfTimestamp($deleted) + $recurrence->recur["startocc"] * 60, $tz); + // depending on the AS version the streamer is going to send the correct value + $exception->exceptionstarttime = $exception->instanceid = $this->getGMTTimeByTZ($this->getDayStartOfTimestamp($deleted) + $recurrence->recur["startocc"] * 60, $tz); $exception->deleted = "1"; if(!isset($syncMessage->exceptions)) @@ -673,7 +694,7 @@ private function getEmail($mapimessage, $contentparameters) { if(isset($props[$meetingrequestproperties["isrecurringtag"]]) && $props[$meetingrequestproperties["isrecurringtag"]]) { $myrec = new SyncMeetingRequestRecurrence(); // get recurrence -> put $message->meetingrequest as message so the 'alldayevent' is set correctly - $this->getRecurrence($mapimessage, $props, $message->meetingrequest, $myrec, $tz); + $this->getRecurrence($mapimessage, $props, $message->meetingrequest, $myrec, $tz, $meetingrequestproperties); $message->meetingrequest->recurrences = array($myrec); } @@ -771,94 +792,16 @@ private function getEmail($mapimessage, $contentparameters) { } } - // Add attachments - $attachtable = mapi_message_getattachmenttable($mapimessage); - $rows = mapi_table_queryallrows($attachtable, array(PR_ATTACH_NUM)); + // Add attachments to message $entryid = bin2hex($messageprops[$emailproperties["entryid"]]); $parentSourcekey = bin2hex($messageprops[$emailproperties["parentsourcekey"]]); - - foreach($rows as $row) { - if(isset($row[PR_ATTACH_NUM])) { - if (Request::GetProtocolVersion() >= 12.0) { - $attach = new SyncBaseAttachment(); - } - else { - $attach = new SyncAttachment(); - } - - $mapiattach = mapi_message_openattach($mapimessage, $row[PR_ATTACH_NUM]); - $attachprops = mapi_getprops($mapiattach, array(PR_ATTACH_LONG_FILENAME, PR_ATTACH_FILENAME, PR_ATTACHMENT_HIDDEN, PR_ATTACH_CONTENT_ID, PR_ATTACH_CONTENT_ID_W, PR_ATTACH_MIME_TAG, PR_ATTACH_MIME_TAG_W, PR_ATTACH_METHOD, PR_DISPLAY_NAME, PR_DISPLAY_NAME_W, PR_ATTACH_SIZE)); - if ((isset($attachprops[PR_ATTACH_MIME_TAG]) && strpos(strtolower($attachprops[PR_ATTACH_MIME_TAG]), 'signed') !== false) || - (isset($attachprops[PR_ATTACH_MIME_TAG_W]) && strpos(strtolower($attachprops[PR_ATTACH_MIME_TAG_W]), 'signed') !== false)) { - continue; - } - - // the displayname is handled equaly for all AS versions - $attach->displayname = w2u((isset($attachprops[PR_ATTACH_LONG_FILENAME])) ? $attachprops[PR_ATTACH_LONG_FILENAME] : ((isset($attachprops[PR_ATTACH_FILENAME])) ? $attachprops[PR_ATTACH_FILENAME] : ((isset($attachprops[PR_DISPLAY_NAME])) ? $attachprops[PR_DISPLAY_NAME] : "attachment.bin"))); - // fix attachment name in case of inline images - if ($attach->displayname == "inline.txt" && (isset($attachprops[PR_ATTACH_MIME_TAG]) || $attachprops[PR_ATTACH_MIME_TAG_W])) { - $mimetype = (isset($attachprops[PR_ATTACH_MIME_TAG])) ? $attachprops[PR_ATTACH_MIME_TAG]:$attachprops[PR_ATTACH_MIME_TAG_W]; - $mime = explode("/", $mimetype); - - if (count($mime) == 2 && $mime[0] == "image") { - $attach->displayname = "inline." . $mime[1]; - } - } - - // set AS version specific parameters - if (Request::GetProtocolVersion() >= 12.0) { - $attach->filereference = sprintf("%s:%s:%s", $entryid, $row[PR_ATTACH_NUM], $parentSourcekey); - $attach->method = (isset($attachprops[PR_ATTACH_METHOD])) ? $attachprops[PR_ATTACH_METHOD] : ATTACH_BY_VALUE; - - // if displayname does not have the eml extension for embedde messages, android and WP devices won't open it - if ($attach->method == ATTACH_EMBEDDED_MSG) { - if (strtolower(substr($attach->displayname, -4)) != '.eml') - $attach->displayname .= '.eml'; - } - // android devices require attachment size in order to display an attachment properly - if (!isset($attachprops[PR_ATTACH_SIZE])) { - $stream = mapi_openproperty($mapiattach, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0); - // It's not possible to open some (embedded only?) messages, so we need to open the attachment object itself to get the data - if (mapi_last_hresult()) { - $embMessage = mapi_attach_openobj($mapiattach); - $addrbook = $this->getAddressbook(); - $stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $embMessage, array('use_tnef' => -1)); - } - $stat = mapi_stream_stat($stream); - $attach->estimatedDataSize = $stat['cb']; - } - else { - $attach->estimatedDataSize = $attachprops[PR_ATTACH_SIZE]; - } - - if (isset($attachprops[PR_ATTACH_CONTENT_ID]) && $attachprops[PR_ATTACH_CONTENT_ID]) - $attach->contentid = $attachprops[PR_ATTACH_CONTENT_ID]; - - if (!isset($attach->contentid) && isset($attachprops[PR_ATTACH_CONTENT_ID_W]) && $attachprops[PR_ATTACH_CONTENT_ID_W]) - $attach->contentid = $attachprops[PR_ATTACH_CONTENT_ID_W]; - - if (isset($attachprops[PR_ATTACHMENT_HIDDEN]) && $attachprops[PR_ATTACHMENT_HIDDEN]) $attach->isinline = 1; - - if(!isset($message->asattachments)) - $message->asattachments = array(); - - array_push($message->asattachments, $attach); - } - else { - $attach->attsize = $attachprops[PR_ATTACH_SIZE]; - $attach->attname = sprintf("%s:%s:%s", $entryid, $row[PR_ATTACH_NUM], $parentSourcekey); - if(!isset($message->attachments)) - $message->attachments = array(); - - array_push($message->attachments, $attach); - } - } - } + $this->setAttachment($mapimessage, $message, $entryid, $parentSourcekey); // Get To/Cc as SMTP addresses (this is different from displayto and displaycc because we are putting // in the SMTP addresses as well, while displayto and displaycc could just contain the display names $message->to = array(); $message->cc = array(); + $message->bcc = array(); $reciptable = mapi_message_getrecipienttable($mapimessage); $rows = mapi_table_queryallrows($reciptable, array(PR_RECIPIENT_TYPE, PR_DISPLAY_NAME, PR_ADDRTYPE, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS, PR_ENTRYID, PR_SEARCH_KEY)); @@ -899,11 +842,14 @@ private function getEmail($mapimessage, $contentparameters) { array_push($message->to, $fulladdr); } else if($row[PR_RECIPIENT_TYPE] == MAPI_CC) { array_push($message->cc, $fulladdr); + } else if($row[PR_RECIPIENT_TYPE] == MAPI_BCC) { + array_push($message->bcc, $fulladdr); } } if (is_array($message->to) && !empty($message->to)) $message->to = implode(", ", $message->to); if (is_array($message->cc) && !empty($message->cc)) $message->cc = implode(", ", $message->cc); + if (is_array($message->bcc) && !empty($message->bcc)) $message->bcc = implode(", ", $message->bcc); // without importance some mobiles assume "0" (low) - Mantis #439 if (!isset($message->importance)) @@ -1150,6 +1096,30 @@ public function IsMAPIDefaultFolder($entryid) { return false; } + /*---------------------------------------------------------------------------------------------------------- + * PreDeleteMessage + */ + + /** + * Performs any actions before a message is imported for deletion. + * + * @param mixed $mapimessage + */ + public function PreDeleteMessage($mapimessage) { + if ($mapimessage === false) { + return; + } + // Currently this is relevant only for MeetingRequests so cancellation emails can be sent to attendees. + $props = mapi_getprops($mapimessage, [PR_MESSAGE_CLASS]); + $messageClass = isset($props[PR_MESSAGE_CLASS]) ? $props[PR_MESSAGE_CLASS] : false; + + if ($messageClass !== false && stripos($messageClass, 'ipm.appointment') === 0) { + ZLog::Write(LOGLEVEL_DEBUG, "MAPIProvider->PreDeleteMessage(): Appointment message"); + $mr = new Meetingrequest($this->store, $mapimessage, $this->session); + $mr->doCancelInvitation(); + } + } + /**---------------------------------------------------------------------------------------------------------- * SETTER */ @@ -1186,8 +1156,11 @@ public function SetMessage($mapimessage, $message) { * * @param mixed $mapimessage * @param SyncMail $message + * + * @return SyncObject */ private function setEmail($mapimessage, $message) { + $response = new SyncMailResponse(); // update categories if (!isset($message->categories)) $message->categories = array(); $emailmap = MAPIMapping::GetEmailMapping(); @@ -1218,6 +1191,28 @@ private function setEmail($mapimessage, $message) { if (isset($message->asbody->type) && $message->asbody->type == SYNC_BODYPREFERENCE_HTML && isset($message->asbody->data)) { $props[$emailprops["html"]] = stream_get_contents($message->asbody->data); } + + // Android devices send the recipients in to, cc and bcc tags + if (isset($message->to) || isset($message->cc) || isset($message->bcc)) { + $recips = []; + $this->addRecips($message->to, MAPI_TO, $recips); + $this->addRecips($message->cc, MAPI_CC, $recips); + $this->addRecips($message->bcc, MAPI_BCC, $recips); + mapi_message_modifyrecipients($mapimessage, MODRECIP_MODIFY, $recips); + } + + // remove PR_CLIENT_SUBMIT_TIME + mapi_deleteprops( + $mapimessage, + [ + $emailprops["clientsubmittime"], + ] + ); + } + + // save DRAFTs attachments + if (!empty($message->asattachments)) { + $this->editAttachments($mapimessage, $message->asattachments, $response); } // unset message flags if: @@ -1285,6 +1280,8 @@ private function setEmail($mapimessage, $message) { if (!empty($delprops)) { mapi_deleteprops($mapimessage, $delprops); } + + return $response; } /** @@ -1294,15 +1291,94 @@ private function setEmail($mapimessage, $message) { * @param SyncAppointment $message * * @access private - * @return boolean + * @return SyncObject */ private function setAppointment($mapimessage, $appointment) { + $response = new SyncAppointmentResponse(); + // Get timezone info if(isset($appointment->timezone)) $tz = $this->getTZFromSyncBlob(base64_decode($appointment->timezone)); else $tz = false; + $appointmentmapping = MAPIMapping::GetAppointmentMapping(); + $appointmentprops = MAPIMapping::GetAppointmentProperties(); + $appointmentprops = array_merge($this->getPropIdsFromStrings($appointmentmapping), $this->getPropIdsFromStrings($appointmentprops)); + // AS 16: incoming instanceid means we need to create/update an appointment exception + if (Request::GetProtocolVersion() >= 16.0 && isset($appointment->instanceid) && $appointment->instanceid) { + // this property wasn't decoded so use Streamer->parseDate to convert it into a timestamp and get basedate from it + $instanceid = $appointment->parseDate($appointment->instanceid); + $basedate = $this->getDayStartOfTimestamp($instanceid); + // get compatible TZ data + $props = [ $appointmentprops["timezonetag"], $appointmentprops["isrecurring"] ]; + $tzprop = $this->getProps($mapimessage, $props); + $tz = $this->getTZFromMAPIBlob($tzprop[$appointmentprops["timezonetag"]]); + + if ($appointmentprops["isrecurring"] == false) { + ZLog::Write(LOGLEVEL_INFO, sprintf("MAPIProvider->setAppointment(): Cannot modify exception instanceId '%s' as target appointment is not recurring. Ignoring.", $appointment->instanceid)); + return false; + } + // get a recurrence object + $recurrence = new Recurrence($this->store, $mapimessage); + // check if the exception is to be deleted + if (isset($appointment->instanceiddelete) && $appointment->instanceiddelete === true) { + // Delete exception + $recurrence->createException([], $basedate, true); + } + // create or update the exception + else { + $exceptionprops = []; + if (isset($appointment->starttime)) { + $exceptionprops[$appointmentprops["starttime"]] = $appointment->starttime; + } + if (isset($appointment->endtime)) { + $exceptionprops[$appointmentprops["endtime"]] = $appointment->endtime; + } + if (isset($appointment->subject)) { + $exceptionprops[$appointmentprops["subject"]] = u2w($appointment->subject); + } + if (isset($appointment->location)) { + $exceptionprops[$appointmentprops["location"]] = u2w($appointment->location); + } + if (isset($appointment->busystatus)) { + $exceptionprops[$appointmentprops["busystatus"]] = $appointment->busystatus; + } + if (isset($appointment->reminder)) { + $exceptionprops[$appointmentprops["reminderset"]] = 1; + $exceptionprops[$appointmentprops["remindertime"]] = $appointment->reminder; + } + if (isset($appointment->alldayevent)) { + $exceptionprops[$appointmentprops["alldayevent"]] = $mapiexception["alldayevent"] = $appointment->alldayevent; + } + if (isset($appointment->body)) { + $exceptionprops[$appointmentprops["body"]] = u2w($appointment->body); + } + if (isset($appointment->asbody)) { + $this->setASbody($appointment->asbody, $exceptionprops, $appointmentprops); + } + if (isset($appointment->location2)) { + $this->setASlocation($appointment->location2, $exceptionprops, $appointmentprops); + } + // modify if exists else create exception + if ($recurrence->isException($basedate)) { + $recurrence->modifyException($exceptionprops, $basedate); + } + else { + $recurrence->createException($exceptionprops, $basedate); + } + } + // instantiate the MR so we can send a updates to the attendees + $mr = new Meetingrequest($this->store, $mapimessage, $this->session); + $mr->updateMeetingRequest($basedate); + $deleteException = isset($appointment->instanceiddelete) && $appointment->instanceiddelete === true; + $mr->sendMeetingRequest($deleteException, false, $basedate); + return true; + } + + // Save OldProps to later check which data is being changed + $oldProps = $this->getProps($mapimessage, $appointmentprops); + // start and end time may not be set - try to get them from the existing appointment for further calculation - see https://jira.z-hub.io/browse/ZP-983 if (!isset($appointment->starttime) || !isset($appointment->endtime)) { $amapping = MAPIMapping::GetAppointmentMapping(); @@ -1336,16 +1412,31 @@ private function setAppointment($mapimessage, $appointment) { $localend = $localstart + 24 * 60 * 60; } + // use clientUID if set + if ($appointment->clientuid && !$appointment->uid) { + $appointment->uid = $appointment->clientuid; + // Facepalm: iOS sends weird ids (without dashes and a trailing null character) + if (strlen($appointment->uid) == 33) { + $appointment->uid = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split($appointment->uid, 4)); + } + } + // is the transmitted UID OL compatible? // if not, encapsulate the transmitted uid - $appointment->uid = Utils::GetOLUidFromICalUid($appointment->uid); + if ($appointment->uid && substr($appointment->uid, 0, 16) != "040000008200E000") { + // if not, encapsulate the transmitted uid + $appointment->uid = Utils::GetOLUidFromICalUid($appointment->uid); + } + + // if there was a clientuid transport the new UID to the response + if ($appointment->clientuid) { + $response->uid = bin2hex($appointment->uid); + $response->hasResponse = true; + } mapi_setprops($mapimessage, array(PR_MESSAGE_CLASS => "IPM.Appointment")); - $appointmentmapping = MAPIMapping::GetAppointmentMapping(); $this->setPropsInMAPI($mapimessage, $appointment, $appointmentmapping); - $appointmentprops = MAPIMapping::GetAppointmentProperties(); - $appointmentprops = array_merge($this->getPropIdsFromStrings($appointmentmapping), $this->getPropIdsFromStrings($appointmentprops)); //appointment specific properties to be set $props = array(); @@ -1380,6 +1471,9 @@ private function setAppointment($mapimessage, $appointment) { $this->setASbody($appointment->asbody, $props, $appointmentprops); } + if (isset($appointment->location2)) { + $this->setASlocation($appointment->location2, $props, $appointmentprops); + } if ($tz !== false) { $props[$appointmentprops["timezonetag"]] = $this->getMAPIBlobFromTZ($tz); } @@ -1488,6 +1582,8 @@ private function setAppointment($mapimessage, $appointment) { } else { $props[$appointmentprops["isrecurring"]] = false; + // remove recurringstate + mapi_deleteprops($mapimessage, [$appointmentprops["recurringstate"]]); } //always set the PR_SENT_REPRESENTING_* props so that the attendee status update also works with the webaccess @@ -1510,8 +1606,6 @@ private function setAppointment($mapimessage, $appointment) { $props[$appointmentprops["icon"]] = 1026; // the user is the organizer // set these properties to show tracking tab in webapp - - $props[$appointmentprops["mrwassent"]] = true; $props[$appointmentprops["responsestatus"]] = olResponseOrganized; $props[$appointmentprops["meetingstatus"]] = olMeeting; } @@ -1527,7 +1621,41 @@ private function setAppointment($mapimessage, $appointment) { } } - // Do attendees + // when updating a normal appointment to a MR we need to send MR emails + $forceMRUpdateSend = false; + + // For AS-16 get a list of the current attendees (pre update) + if(Request::GetProtocolVersion() >= 16.0 && isset($appointment->meetingstatus) && $appointment->meetingstatus > 0) { + $old_recipienttable = mapi_message_getrecipienttable($mapimessage); + $old_receipstable = mapi_table_queryallrows($old_recipienttable, + [ + PR_ENTRYID, + PR_DISPLAY_NAME, + PR_EMAIL_ADDRESS, + PR_RECIPIENT_ENTRYID, + PR_RECIPIENT_TYPE, + PR_SEND_INTERNET_ENCODING, + PR_SEND_RICH_INFO, + PR_RECIPIENT_DISPLAY_NAME, + PR_ADDRTYPE, + PR_DISPLAY_TYPE, + PR_DISPLAY_TYPE_EX, + PR_RECIPIENT_TRACKSTATUS, + PR_RECIPIENT_TRACKSTATUS_TIME, + PR_RECIPIENT_FLAGS, + PR_ROWID, + PR_OBJECT_TYPE, + PR_SEARCH_KEY, + ]); + $old_receips = []; + foreach($old_receipstable as $oldrec) { + if (isset($oldrec[PR_EMAIL_ADDRESS])) { + $old_receips[$oldrec[PR_EMAIL_ADDRESS]] = $oldrec; + } + } + } + + // Do attendees if(isset($appointment->attendees) && is_array($appointment->attendees)) { $recips = array(); @@ -1543,6 +1671,11 @@ private function setAppointment($mapimessage, $appointment) { array_push($recips, $org); + // remove organizer from old_receips + if(isset($old_receips[$org[PR_EMAIL_ADDRESS]])) { + unset($old_receips[$org[PR_EMAIL_ADDRESS]]); + } + //open addresss book for user resolve $addrbook = $this->getAddressbook(); foreach($appointment->attendees as $attendee) { @@ -1571,12 +1704,41 @@ private function setAppointment($mapimessage, $appointment) { $recip[PR_ENTRYID] = mapi_createoneoff($recip[PR_DISPLAY_NAME], $recip[PR_ADDRTYPE], $recip[PR_EMAIL_ADDRESS]); } + // remove still existing attendees from the list of pre-update attendees - remaining pre-update are considered deleted attendees + if(isset($old_receips[$recip[PR_EMAIL_ADDRESS]])) { + unset($old_receips[$recip[PR_EMAIL_ADDRESS]]); + } + // if there is a new attendee a MR update must be send -> Appointment to MR update + else { + $forceMRUpdateSend = true; + } array_push($recips, $recip); } - mapi_message_modifyrecipients($mapimessage, 0, $recips); + mapi_message_modifyrecipients($mapimessage, ($appointment->clientuid) ? MODRECIP_ADD : MODRECIP_MODIFY, $recips); } mapi_setprops($mapimessage, $props); + + // Since AS 16 we have to take care of MeetingRequest updates + if (Request::GetProtocolVersion() >= 16.0 && isset($appointment->meetingstatus) && $appointment->meetingstatus > 0) { + $mr = new Meetingrequest($this->store, $mapimessage, $this->session); + // Only send updates if this is a new MR or we are the organizer + if ($appointment->clientuid || $mr->isLocalOrganiser() || $forceMRUpdateSend) { + // initialize MR and/or update internal counters + $mr->updateMeetingRequest(); + // when updating, check for significant changes and if needed will clear the existing recipient responses + if (!isset($appointment->clientuid) && !$forceMRUpdateSend) { + $mr->checkSignificantChanges($oldProps, false, false); + } + $mr->sendMeetingRequest(false, false, false, false, array_values($old_receips)); + } + } + + // update attachments send by the mobile + if (!empty($appointment->asattachments)) { + $this->editAttachments($mapimessage, $appointment->asattachments, $response); + } + return $response; } /** @@ -1586,9 +1748,10 @@ private function setAppointment($mapimessage, $appointment) { * @param SyncContact $contact * * @access private - * @return boolean + * @return SyncObject */ private function setContact($mapimessage, $contact) { + $response = new SyncContactResponse(); mapi_setprops($mapimessage, array(PR_MESSAGE_CLASS => "IPM.Contact")); // normalize email addresses @@ -1711,6 +1874,8 @@ private function setContact($mapimessage, $contact) { else ZLog::Write(LOGLEVEL_DEBUG, "FILEAS_ORDER not defined"); mapi_setprops($mapimessage, $props); + + return $response; } /** @@ -1720,9 +1885,10 @@ private function setContact($mapimessage, $contact) { * @param SyncTask $task * * @access private - * @return boolean + * @return SyncObject */ private function setTask($mapimessage, $task) { + $response = new SyncTaskResponse(); mapi_setprops($mapimessage, array(PR_MESSAGE_CLASS => "IPM.Task")); $taskmapping = MAPIMapping::GetTaskMapping(); @@ -1803,7 +1969,7 @@ private function setTask($mapimessage, $task) { } mapi_setprops($mapimessage, $props); - + return $response; } /** @@ -1813,9 +1979,10 @@ private function setTask($mapimessage, $task) { * @param SyncNote $note * * @access private - * @return boolean + * @return SyncObject */ private function setNote($mapimessage, $note) { + $response = new SyncNoteResponse(); // Touchdown does not send categories if all are unset or there is none. // Setting it to an empty array will unset the property in KC as well if (!isset($note->categories)) $note->categories = array(); @@ -1840,6 +2007,8 @@ private function setNote($mapimessage, $note) { $props[$noteprops["internetcpid"]] = INTERNET_CPID_UTF8; mapi_setprops($mapimessage, $props); + + return $response; } /**---------------------------------------------------------------------------------------------------------- @@ -2633,6 +2802,38 @@ private function mapiReadStream($stream, $size) { return mapi_stream_read($stream, $size); } + /** + * Build a filereference key by the clientid. + * + * @param MAPIMessage $mapimessage + * @param SyncObject $message + * @param mixed $clientid + * @param mixed $entryid + * @param mixed $parentSourcekey + * @param mixed $exceptionBasedate + * + * @return bool + */ + private function getFileReferenceForClientId($mapimessage, $clientid, $entryid = 0, $parentSourcekey = 0, $exceptionBasedate = 0) { + if (!$entryid || !$parentSourcekey) { + $props = mapi_getprops($mapimessage, [PR_ENTRYID, PR_PARENT_SOURCE_KEY]); + if (!$entryid && isset($props[PR_ENTRYID])) { + $entryid = bin2hex($props[PR_ENTRYID]); + } + if (!$parentSourcekey && isset($props[PR_PARENT_SOURCE_KEY])) { + $parentSourcekey = bin2hex($props[PR_PARENT_SOURCE_KEY]); + } + } + $attachtable = mapi_message_getattachmenttable($mapimessage); + $rows = mapi_table_queryallrows($attachtable, [PR_EC_WA_ATTACHMENT_ID, PR_ATTACH_NUM]); + foreach ($rows as $row) { + if ($row[PR_EC_WA_ATTACHMENT_ID] == $clientid) { + return sprintf("%s:%s:%s:%s", $entryid, $row[PR_ATTACH_NUM], $parentSourcekey, $exceptionBasedate); + } + } + return false; + } + /** * A wrapper for mapi_inetmapi_imtoinet function. * @@ -2808,6 +3009,277 @@ private function setASbody($asbody, &$props, $appointmentprops) { } } + /** + * Sets attachments from an email message to a SyncObject. + * + * @param mixed $mapimessage + * @param SyncObject $message + * @param string $entryid + * @param string $parentSourcekey + */ + private function setAttachment($mapimessage, &$message, $entryid, $parentSourcekey, $exceptionBasedate = 0) { + // Add attachments + $attachtable = mapi_message_getattachmenttable($mapimessage); + $rows = mapi_table_queryallrows($attachtable, [PR_ATTACH_NUM]); + + foreach ($rows as $row) { + if (isset($row[PR_ATTACH_NUM])) { + if (Request::GetProtocolVersion() >= 12.0) { + $attach = new SyncBaseAttachment(); + } + else { + $attach = new SyncAttachment(); + } + + $mapiattach = mapi_message_openattach($mapimessage, $row[PR_ATTACH_NUM]); + $attachprops = mapi_getprops($mapiattach, [PR_ATTACH_LONG_FILENAME, PR_ATTACH_FILENAME, PR_ATTACHMENT_HIDDEN, PR_ATTACH_CONTENT_ID, PR_ATTACH_CONTENT_ID_A, PR_ATTACH_MIME_TAG, PR_ATTACH_METHOD, PR_DISPLAY_NAME, PR_ATTACH_SIZE, PR_ATTACH_FLAGS]); + if (isset($attachprops[PR_ATTACH_MIME_TAG]) && strpos(strtolower($attachprops[PR_ATTACH_MIME_TAG]), 'signed') !== false) { + continue; + } + + // the displayname is handled equally for all AS versions + $attach->displayname = w2u((isset($attachprops[PR_ATTACH_LONG_FILENAME])) ? $attachprops[PR_ATTACH_LONG_FILENAME] : ((isset($attachprops[PR_ATTACH_FILENAME])) ? $attachprops[PR_ATTACH_FILENAME] : ((isset($attachprops[PR_DISPLAY_NAME])) ? $attachprops[PR_DISPLAY_NAME] : "attachment.bin"))); + // fix attachment name in case of inline images + if (($attach->displayname == "inline.txt" && isset($attachprops[PR_ATTACH_MIME_TAG])) || + (substr_compare($attach->displayname, "attachment", 0, 10, true) === 0 && substr_compare($attach->displayname, ".dat", -4, 4, true) === 0)) { + $mimetype = $attachprops[PR_ATTACH_MIME_TAG] ?? 'application/octet-stream'; + $mime = explode("/", $mimetype); + + if (count($mime) == 2 && $mime[0] == "image") { + $attach->displayname = "inline." . $mime[1]; + } + } + + // set AS version specific parameters + if (Request::GetProtocolVersion() >= 12.0) { + $attach->filereference = sprintf("%s:%s:%s:%s", $entryid, $row[PR_ATTACH_NUM], $parentSourcekey, $exceptionBasedate); + $attach->method = (isset($attachprops[PR_ATTACH_METHOD])) ? $attachprops[PR_ATTACH_METHOD] : ATTACH_BY_VALUE; + + // if displayname does not have the eml extension for embedde messages, android and WP devices won't open it + if ($attach->method == ATTACH_EMBEDDED_MSG) { + if (strtolower(substr($attach->displayname, -4)) != '.eml') { + $attach->displayname .= '.eml'; + } + } + // android devices require attachment size in order to display an attachment properly + if (!isset($attachprops[PR_ATTACH_SIZE])) { + $stream = mapi_openproperty($mapiattach, PR_ATTACH_DATA_BIN, IID_IStream, 0, 0); + // It's not possible to open some (embedded only?) messages, so we need to open the attachment object itself to get the data + if (mapi_last_hresult()) { + $embMessage = mapi_attach_openobj($mapiattach); + $addrbook = $this->getAddressbook(); + $stream = mapi_inetmapi_imtoinet($this->session, $addrbook, $embMessage, ['use_tnef' => -1]); + } + $stat = mapi_stream_stat($stream); + $attach->estimatedDataSize = $stat['cb']; + } + else { + $attach->estimatedDataSize = $attachprops[PR_ATTACH_SIZE]; + } + + if (isset($attachprops[PR_ATTACH_CONTENT_ID]) && $attachprops[PR_ATTACH_CONTENT_ID]) { + $attach->contentid = $attachprops[PR_ATTACH_CONTENT_ID]; + } + + if (!isset($attach->contentid) && isset($attachprops[PR_ATTACH_CONTENT_ID_A]) && $attachprops[PR_ATTACH_CONTENT_ID_A]) { + $attach->contentid = $attachprops[PR_ATTACH_CONTENT_ID_A]; + } + + if (isset($attachprops[PR_ATTACHMENT_HIDDEN]) && $attachprops[PR_ATTACHMENT_HIDDEN]) { + $attach->isinline = 1; + } + + if (isset($attach->contentid, $attachprops[PR_ATTACH_FLAGS]) && $attachprops[PR_ATTACH_FLAGS] & 4) { + $attach->isinline = 1; + } + + if (!isset($message->asattachments)) { + $message->asattachments = []; + } + + array_push($message->asattachments, $attach); + } + else { + $attach->attsize = $attachprops[PR_ATTACH_SIZE]; + $attach->attname = sprintf("%s:%s:%s", $entryid, $row[PR_ATTACH_NUM], $parentSourcekey); + if (!isset($message->attachments)) { + $message->attachments = []; + } + + array_push($message->attachments, $attach); + } + } + } + } + + /** + * Update attachments of a mapimessage based on asattachments received. + * + * @param MAPIMessage $mapimessage + * @param array $asattachments + * @param SyncObject $response + */ + public function editAttachments($mapimessage, $asattachments, &$response) { + foreach ($asattachments as $att) { + // new attachment to be saved + if ($att instanceof SyncBaseAttachmentAdd) { + if (!isset($att->content)) { + ZLog::Write(LOGLEVEL_WARN, sprintf("MAPIProvider->editAttachments(): Ignoring attachment %s to be added as it has no content: %s", $att->clientid, $att->displayname)); + continue; + } + + ZLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->editAttachments(): Saving attachment %s with name: %s", $att->clientid, $att->displayname)); + // only create if the attachment does not already exist + if ($this->getFileReferenceForClientId($mapimessage, $att->clientid, 0, 0) === false) { + // TODO: check: contentlocation + $props = [ + PR_ATTACH_LONG_FILENAME => $att->displayname, + PR_DISPLAY_NAME => $att->displayname, + PR_ATTACH_METHOD => $att->method, // is this correct ?? + PR_ATTACH_DATA_BIN => "", + PR_ATTACHMENT_HIDDEN => false, + PR_ATTACH_EXTENSION => pathinfo($att->displayname, PATHINFO_EXTENSION), + PR_EC_WA_ATTACHMENT_ID => $att->clientid, + ]; + if (!empty($att->contenttype)) { + $props[PR_ATTACH_MIME_TAG] = $att->contenttype; + } + if (!empty($att->contentid)) { + $props[PR_ATTACH_CONTENT_ID] = $att->contentid; + $props[PR_ATTACHMENT_HIDDEN] = true; + } + $attachment = mapi_message_createattach($mapimessage); + mapi_setprops($attachment, $props); + // Stream the file to the PR_ATTACH_DATA_BIN property + $stream = mapi_openproperty($attachment, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY); + mapi_stream_write($stream, stream_get_contents($att->content)); + // Commit the stream and save changes + mapi_stream_commit($stream); + mapi_savechanges($attachment); + } + if (!isset($response->asattachments)) { + $response->asattachments = []; + } + // respond linking the clientid with the newly created filereference + $attResp = new SyncBaseAttachment(); + $attResp->clientid = $att->clientid; + $attResp->filereference = $this->getFileReferenceForClientId($mapimessage, $att->clientid, 0, 0); + $response->asattachments[] = $attResp; + $response->hasResponse = true; + } + // attachment to be removed + elseif ($att instanceof SyncBaseAttachmentDelete) { + list($id, $attachnum, $parentEntryid, $exceptionBasedate) = explode(":", $att->filereference); + ZLog::Write(LOGLEVEL_DEBUG, sprintf("MAPIProvider->editAttachments(): Deleting attachment with num: %s", $attachnum)); + mapi_message_deleteattach($mapimessage, (int) $attachnum); + } + } + } + + /** + * Sets information from SyncLocation type for a MAPI message. + * + * @param SyncBaseBody $aslocation + * @param array $props + * @param array $appointmentprops + */ + private function setASlocation($aslocation, &$props, $appointmentprops) { + $fullAddress = ""; + if ($aslocation->street || $aslocation->city || $aslocation->state || $aslocation->country || $aslocation->postalcode) { + $fullAddress = $aslocation->street .", ". $aslocation->city ."-". $aslocation->state .",". $aslocation->country .",". $aslocation->postalcode; + } + // Determine which data to use as DisplayName. This is also set to the traditional location property for backwards compatibility (this is currently displayed in OL). + $useStreet = false; + if ($aslocation->displayname) { + $props[$appointmentprops["location"]] = $aslocation->displayname; + } + elseif($aslocation->street) { + $useStreet = true; + $props[$appointmentprops["location"]] = $fullAddress; + } + elseif($aslocation->city) { + $props[$appointmentprops["location"]] = $aslocation->city; + } + $loc = []; + $loc["DisplayName"] = ($useStreet) ? $aslocation->street : $props[$appointmentprops["location"]]; + $loc["LocationAnnotation"] = ($aslocation->annotation) ? $aslocation->annotation : ""; + $loc["LocationSource"] = "None"; + $loc["Unresolved"] = ($aslocation->locationuri) ? false : true; + $loc["LocationUri"] = ($aslocation->locationuri) ?? ""; + $loc["Latitude"] = ($aslocation->latitude) ? floatval($aslocation->latitude) : null; + $loc["Longitude"] = ($aslocation->longitude) ? floatval($aslocation->longitude) : null; + $loc["Altitude"] = ($aslocation->altitude) ? floatval($aslocation->altitude) : null; + $loc["Accuracy"] = ($aslocation->accuracy) ? floatval($aslocation->accuracy) : null; + $loc["AltitudeAccuracy"] = ($aslocation->altitudeaccuracy) ? floatval($aslocation->altitudeaccuracy) : null; + $loc["LocationStreet"] = ($aslocation->street) ?? ""; + $loc["LocationCity"] = ($aslocation->city) ?? ""; + $loc["LocationState"] = ($aslocation->state) ?? ""; + $loc["LocationCountry"] = ($aslocation->country) ?? ""; + $loc["LocationPostalCode"] = ($aslocation->postalcode) ?? ""; + $loc["LocationFullAddress"] = $fullAddress; + $props[$appointmentprops["locations"]] = json_encode([$loc], JSON_UNESCAPED_UNICODE); + } + + /** + * Gets information from a MAPI message and applies it to a SyncLocation object. + * + * @param MAPIMessage $mapimessage + * @param SyncObject $aslocation + * @param array $appointmentprops + */ + private function getASlocation($mapimessage, &$aslocation, $appointmentprops) { + $props = mapi_getprops($mapimessage, [ $appointmentprops["locations"], $appointmentprops["location"] ]); + // set the old location as displayname - this is also the "correct" approach if there is more than one location in the "locations" property json + if (isset($props[$appointmentprops["location"]])) { + $aslocation->displayname = $props[$appointmentprops["location"]]; + } + if (isset($props[$appointmentprops["locations"]])) { + $loc = json_decode($props[$appointmentprops["locations"]], true); + if (is_array($loc) && count($loc) == 1) { + $l = $loc[0]; + if (!empty($l['DisplayName'])) { + $aslocation->displayname = $l['DisplayName']; + } + if (!empty($l['LocationAnnotation'])) { + $aslocation->annotation = $l['LocationAnnotation']; + } + if (!empty($l['LocationStreet'])) { + $aslocation->street = $l['LocationStreet']; + } + if (!empty($l['LocationCity'])) { + $aslocation->city = $l['LocationCity']; + } + if (!empty($l['LocationState'])) { + $aslocation->state = $l['LocationState']; + } + if (!empty($l['LocationCountry'])) { + $aslocation->country = $l['LocationCountry']; + } + if (!empty($l['LocationPostalCode'])) { + $aslocation->postalcode = $l['LocationPostalCode']; + } + if (isset($l['Latitude']) && is_numeric($l['Latitude'])) { + $aslocation->latitude = floatval($l['Latitude']); + } + if (isset($l['Longitude']) && is_numeric($l['Longitude'])) { + $aslocation->longitude = floatval($l['Longitude']); + } + if (isset($l['Accuracy']) && is_numeric($l['Accuracy'])) { + $aslocation->accuracy = floatval($l['Accuracy']); + } + if (isset($l['Altitude']) && is_numeric($l['Altitude'])) { + $aslocation->altitude = floatval($l['Altitude']); + } + if (isset($l['AltitudeAccuracy']) && is_numeric($l['AltitudeAccuracy'])) { + $aslocation->altitudeaccuracy = floatval($l['AltitudeAccuracy']); + } + if (!empty($l['LocationUri'])) { + $aslocation->locationuri = $l['LocationUri']; + } + } + } + } + /** * Get MAPI addressbook object. * @@ -2997,4 +3469,65 @@ public function GetMessageCategories($parentsourcekey, $sourcekey) { } return false; } + + /** + * Adds recipients to the recips array. + * + * @param string $recip + * @param int $type + * @param array $recips + * + * @return void + */ + private function addRecips($recip, $type, &$recips) { + if (!empty($recip) && is_array($recip)) { + $emails = $recip; + // Recipients should be comma separated, but android devices separate + // them with semicolon, hence the additional processing + if (count($recip) === 1 && strpos($recip[0], ';') !== false) { + $emails = explode(';', $recip[0]); + } + foreach ($emails as $email) { + $extEmail = $this->extractEmailAddress($email); + if ($extEmail !== false) { + $r = $this->createMapiRecipient($extEmail, $type); + $recips[] = $r; + } + } + } + } + /** + * Creates a MAPI recipient to use with mapi_message_modifyrecipients(). + * + * @param string $email + * @param int $type + * + * @return array + */ + private function createMapiRecipient($email, $type) { + // Open address book for user resolve + $addrbook = $this->getAddressbook(); + $recip = []; + $recip[PR_EMAIL_ADDRESS] = $email; + $recip[PR_SMTP_ADDRESS] = $email; + // lookup information in GAB if possible so we have up-to-date name for given address + $userinfo = [[PR_DISPLAY_NAME => $recip[PR_EMAIL_ADDRESS]]]; + $userinfo = mapi_ab_resolvename($addrbook, $userinfo, EMS_AB_ADDRESS_LOOKUP); + if (mapi_last_hresult() == NOERROR) { + $recip[PR_DISPLAY_NAME] = $userinfo[0][PR_DISPLAY_NAME]; + $recip[PR_EMAIL_ADDRESS] = $userinfo[0][PR_EMAIL_ADDRESS]; + $recip[PR_SEARCH_KEY] = $userinfo[0][PR_SEARCH_KEY]; + $recip[PR_ADDRTYPE] = $userinfo[0][PR_ADDRTYPE]; + $recip[PR_ENTRYID] = $userinfo[0][PR_ENTRYID]; + $recip[PR_RECIPIENT_TYPE] = $type; + } + else { + $recip[PR_DISPLAY_NAME] = $email; + $recip[PR_SEARCH_KEY] = "SMTP:" . $recip[PR_EMAIL_ADDRESS] . "\0"; + $recip[PR_ADDRTYPE] = "SMTP"; + $recip[PR_RECIPIENT_TYPE] = $type; + $recip[PR_ENTRYID] = mapi_createoneoff($recip[PR_DISPLAY_NAME], $recip[PR_ADDRTYPE], $recip[PR_EMAIL_ADDRESS]); + } + return $recip; + } } diff --git a/src/backend/sqlstatemachine/sqlstatemachine.php b/src/backend/sqlstatemachine/sqlstatemachine.php index 8836e32e..d7335e01 100644 --- a/src/backend/sqlstatemachine/sqlstatemachine.php +++ b/src/backend/sqlstatemachine/sqlstatemachine.php @@ -716,10 +716,10 @@ protected function getStateHashStatement(&$params, $key=':key') { */ protected function checkDbAndTables() { try { - if (STATE_SQL_ENGINE == "pgsql") - $sqlStmt = "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema';"; - else - $sqlStmt = sprintf("SHOW TABLES FROM %s", STATE_SQL_DATABASE); + if (STATE_SQL_ENGINE == "pgsql") + $sqlStmt = "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema';"; + else + $sqlStmt = sprintf("SHOW TABLES FROM %s", STATE_SQL_DATABASE); $sth = $this->getDbh(false)->prepare($sqlStmt); $sth->execute(); if ($sth->rowCount() != 3) { diff --git a/src/index.php b/src/index.php index 4dd102d1..70ee15db 100644 --- a/src/index.php +++ b/src/index.php @@ -146,6 +146,7 @@ header(ZPush::GetServerHeader()); header(ZPush::GetSupportedProtocolVersions()); header(ZPush::GetSupportedCommands()); + header("X-AspNet-Version: 4.0.30319"); ZLog::Write(LOGLEVEL_INFO, $nopostex->getMessage()); } else if ($nopostex->getCode() == NoPostRequestException::GET_REQUEST) { diff --git a/src/lib/core/interprocessdata.php b/src/lib/core/interprocessdata.php index 8418f36e..8dfdc3fb 100644 --- a/src/lib/core/interprocessdata.php +++ b/src/lib/core/interprocessdata.php @@ -87,10 +87,10 @@ public function __construct() { $this->ipcProvider = new $this->provider_class($this->type, $this->allocate, get_class($this), $this->type); } } else { - if (!($this instanceof TopCollector) && $this->provider_class !== 'IpcSharedMemoryProvider' ) { + if (!($this instanceof TopCollector) && $this->provider_class !== 'IpcSharedMemoryProvider' ) { $this->type = $type. "-". $this->type; - } - $this->ipcProvider = new $this->provider_class($this->type, $this->allocate, get_class($this), $type); + } + $this->ipcProvider = new $this->provider_class($this->type, $this->allocate, get_class($this), $type); } ZLog::Write(LOGLEVEL_DEBUG, sprintf("%s initialised with IPC provider '%s' with type '%s'", get_class($this), $this->provider_class, $this->type)); diff --git a/src/lib/core/pingtracking.php b/src/lib/core/pingtracking.php index 4b9adc6c..099e4130 100644 --- a/src/lib/core/pingtracking.php +++ b/src/lib/core/pingtracking.php @@ -109,7 +109,7 @@ public function DoForcePingTimeout() { foreach ($pings[self::$devid][self::$user] as $pid=>$starttime) if ($starttime > self::$start) { return true; - } + } elseif ($starttime == self::$start) { // Arbitrary decision to differentiate multiple processes that started at the same second for the same user. Compare PIDs // If the other process has a bigger PID then kill this process diff --git a/src/lib/core/streamer.php b/src/lib/core/streamer.php index f4f4ea83..039add8c 100644 --- a/src/lib/core/streamer.php +++ b/src/lib/core/streamer.php @@ -122,13 +122,30 @@ public function Decode(&$decoder) { if(isset($map[self::STREAMER_ARRAY])) { WBXMLDecoder::ResetInWhile("decodeArray"); while(WBXMLDecoder::InWhile("decodeArray")) { + $streamertype = false; //do not get start tag for an array without a container if (!(isset($map[self::STREAMER_PROP]) && $map[self::STREAMER_PROP] == self::STREAMER_TYPE_NO_CONTAINER)) { - if(!$decoder->getElementStartTag($map[self::STREAMER_ARRAY])) + // are there multiple possibilities for element encapsulation tags? + if (is_array($map[self::STREAMER_ARRAY])) { + $encapTagsTypes = $map[self::STREAMER_ARRAY]; + } + else { + // set $streamertype to null if the element is a single string (e.g. category) + $encapTagsTypes = [$map[self::STREAMER_ARRAY] => isset($map[self::STREAMER_TYPE]) ? $map[self::STREAMER_TYPE] : null]; + } + // Identify the used tag + $streamertype = false; + foreach ($encapTagsTypes as $tag => $type) { + if ($decoder->getElementStartTag($tag)) { + $streamertype = $type; + } + } + if ($streamertype === false) { break; + } } - if(isset($map[self::STREAMER_TYPE])) { - $decoded = new $map[self::STREAMER_TYPE]; + if ($streamertype) { + $decoded = new $streamertype(); $decoded->Decode($decoder); } @@ -167,7 +184,7 @@ public function Decode(&$decoder) { if(isset($map[self::STREAMER_TYPE])) { // Complex type, decode recursively if($map[self::STREAMER_TYPE] == self::STREAMER_TYPE_DATE || $map[self::STREAMER_TYPE] == self::STREAMER_TYPE_DATE_DASHES) { - $decoded = $this->parseDate($decoder->getElementContent()); + $decoded = Utils::parseDate($decoder->getElementContent()); if(!$decoder->getElementEndTag()) return false; } @@ -271,15 +288,20 @@ public function Encode(&$encoder) { foreach ($this->{$map[self::STREAMER_VAR]} as $element) { if(is_object($element)) { - $encoder->startTag($map[self::STREAMER_ARRAY]); // Outputs object container (eg Attachment) + // find corresponding encapsulation tag for element + if (!is_array($map[self::STREAMER_ARRAY])) { + $eltag = $map[self::STREAMER_ARRAY]; + } + else { + $eltag = array_search(get_class($element), $map[self::STREAMER_ARRAY]); + } + $encoder->startTag($eltag); // Outputs object container (eg Attachment) $element->Encode($encoder); $encoder->endTag(); } else { - if(strlen($element) == 0) - // Do not output empty items. Not sure if we should output an empty tag with $encoder->startTag($map[self::STREAMER_ARRAY], false, true); - ; - else { + // Do not output empty items. Not sure if we should output an empty tag with $encoder->startTag($map[self::STREAMER_ARRAY], false, true); + if (strlen($element) > 0) { $encoder->startTag($map[self::STREAMER_ARRAY]); $encoder->content($element); $encoder->endTag(); @@ -466,30 +488,4 @@ private function formatDate($ts, $type) { else if($type == self::STREAMER_TYPE_DATE_DASHES) return Utils::FormatDateUtc($ts,"yyyy-MM-dd'T'HH:mm:SS'.000Z'"); } - - /** - * Transforms an AS timestamp into a unix timestamp - * - * @param string $ts - * - * @access private - * @return long - */ - function parseDate($ts) { - if(preg_match("/(\d{4})[^0-9]*(\d{2})[^0-9]*(\d{2})(T(\d{2})[^0-9]*(\d{2})[^0-9]*(\d{2})(.\d+)?Z){0,1}$/", $ts, $matches)) { - if ($matches[1] >= 2038){ - $matches[1] = 2038; - $matches[2] = 1; - $matches[3] = 18; - $matches[5] = $matches[6] = $matches[7] = 0; - } - - if (!isset($matches[5])) $matches[5] = 0; - if (!isset($matches[6])) $matches[6] = 0; - if (!isset($matches[7])) $matches[7] = 0; - - return gmmktime($matches[5], $matches[6], $matches[7], $matches[2], $matches[3], $matches[1]); - } - return 0; - } } diff --git a/src/lib/core/streamimporter.php b/src/lib/core/streamimporter.php index d4ec4b23..1c3c074a 100644 --- a/src/lib/core/streamimporter.php +++ b/src/lib/core/streamimporter.php @@ -147,6 +147,8 @@ public function ImportMessageChange($id, $message) { if ($message->flags === false || $message->flags === SYNC_NEWMESSAGE) $this->encoder->startTag(SYNC_ADD); else { + $this->encoder->startTag(SYNC_MODIFY); + // on update of an SyncEmail we only export the flags and categories // draft emails are fully exported if($message instanceof SyncMail && (!isset($message->isdraft) || $message->isdraft === false) && ((isset($message->flag) && $message->flag instanceof SyncMailFlags) || isset($message->categories))) { @@ -160,8 +162,6 @@ public function ImportMessageChange($id, $message) { unset($newmessage); ZLog::Write(LOGLEVEL_DEBUG, sprintf("ImportChangesStream->ImportMessageChange('%s'): SyncMail message updated. Message content is striped, only flags/categories are streamed.", $id)); } - - $this->encoder->startTag(SYNC_MODIFY); } // TAG: SYNC_ADD / SYNC_MODIFY diff --git a/src/lib/core/zpushdefs.php b/src/lib/core/zpushdefs.php index 2c1f668b..515a84a9 100644 --- a/src/lib/core/zpushdefs.php +++ b/src/lib/core/zpushdefs.php @@ -815,6 +815,9 @@ define("SYNC_PROVISION_RWSTATUS_PENDING", 2); define("SYNC_PROVISION_RWSTATUS_REQUESTED", 4); define("SYNC_PROVISION_RWSTATUS_WIPED", 8); +define("SYNC_PROVISION_RWSTATUS_PENDING_ACCOUNT_ONLY", 16); +define("SYNC_PROVISION_RWSTATUS_REQUESTED_ACCOUNT_ONLY", 32); +define("SYNC_PROVISION_RWSTATUS_WIPED_ACCOUNT_ONLY", 64); define("SYNC_STATUS_SUCCESS", 1); define("SYNC_STATUS_INVALIDSYNCKEY", 3); diff --git a/src/lib/exceptions/serviceunavailableexception.php b/src/lib/exceptions/serviceunavailableexception.php index c1a3c456..7d753945 100644 --- a/src/lib/exceptions/serviceunavailableexception.php +++ b/src/lib/exceptions/serviceunavailableexception.php @@ -24,16 +24,16 @@ ************************************************/ class ServiceUnavailableException extends HTTPReturnCodeException { - protected $defaultLogLevel = LOGLEVEL_INFO; - protected $httpReturnCode = HTTP_CODE_503; - protected $httpReturnMessage = "Service Unavailable"; - protected $httpHeaders = array(); - protected $showLegal = false; + protected $defaultLogLevel = LOGLEVEL_INFO; + protected $httpReturnCode = HTTP_CODE_503; + protected $httpReturnMessage = "Service Unavailable"; + protected $httpHeaders = array(); + protected $showLegal = false; - public function __construct($message = "", $code = 0, $previous = NULL, $logLevel = false) { - parent::__construct($message, $code, $previous, $logLevel); - if (RETRY_AFTER_DELAY !== false) { - $this->httpHeaders[] = 'Retry-After: ' . RETRY_AFTER_DELAY; - } - } + public function __construct($message = "", $code = 0, $previous = NULL, $logLevel = false) { + parent::__construct($message, $code, $previous, $logLevel); + if (RETRY_AFTER_DELAY !== false) { + $this->httpHeaders[] = 'Retry-After: ' . RETRY_AFTER_DELAY; + } + } } \ No newline at end of file diff --git a/src/lib/interface/iipcprovider.php b/src/lib/interface/iipcprovider.php index 8f25a9c9..d0b1588c 100644 --- a/src/lib/interface/iipcprovider.php +++ b/src/lib/interface/iipcprovider.php @@ -29,11 +29,11 @@ interface IIpcProvider /** * Constructor * - * @param int $type - * @param int $allocate - * @param string $class - * @param string $serverKey - */ + * @param int $type + * @param int $allocate + * @param string $class + * @param string $serverKey + */ public function __construct($type, $allocate, $class, $serverKey); /** diff --git a/src/lib/request/meetingresponse.php b/src/lib/request/meetingresponse.php index c07a6950..08a68fce 100644 --- a/src/lib/request/meetingresponse.php +++ b/src/lib/request/meetingresponse.php @@ -66,6 +66,38 @@ public function Handle($commandCode) { if(!self::$decoder->getElementEndTag()) return false; } + + if (self::$decoder->getElementStartTag(SYNC_MEETINGRESPONSE_SENDRESPONSE)) { + if (self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_BODY)) { + if (self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_TYPE)) { + $req["bodytype"] = self::$decoder->getElementContent(); + if (!self::$decoder->getElementEndTag()) { + return false; + } + } + if (self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_DATA)) { + $req["body"] = self::$decoder->getElementContent(); + if (!self::$decoder->getElementEndTag()) { + return false; + } + } + if (!self::$decoder->getElementEndTag()) { + return false; + } + } // end body + if (self::$decoder->getElementStartTag(SYNC_MEETINGRESPONSE_PROPOSEDSTARTTIME)) { + $req["proposedstarttime"] = self::$decoder->getElementContent(); + if (!self::$decoder->getElementEndTag()) { + return false; + } + } + if (self::$decoder->getElementStartTag(SYNC_MEETINGRESPONSE_PROPOSEDENDTIME)) { + $req["proposedendtime"] = self::$decoder->getElementContent(); + if (!self::$decoder->getElementEndTag()) { + return false; + } + } + } // end send response $e = self::$decoder->peek(); if($e[EN_TYPE] == EN_TYPE_ENDTAG) { diff --git a/src/lib/request/provisioning.php b/src/lib/request/provisioning.php index 8de7cfe4..fab73f67 100644 --- a/src/lib/request/provisioning.php +++ b/src/lib/request/provisioning.php @@ -63,6 +63,9 @@ public function Handle($commandCode) { if (self::$decoder->getElementStartTag(SYNC_PROVISION_REMOTEWIPE)) { $requestName = SYNC_PROVISION_REMOTEWIPE; } + if (self::$decoder->getElementStartTag(SYNC_PROVISION_ACCOUNTONLYREMOTEWIPE)) { + $requestName = SYNC_PROVISION_ACCOUNTONLYREMOTEWIPE; + } if (self::$decoder->getElementStartTag(SYNC_PROVISION_POLICIES)) { $requestName = SYNC_PROVISION_POLICIES; } @@ -76,6 +79,7 @@ public function Handle($commandCode) { //set is available for OOF, device password and device information switch ($requestName) { case SYNC_PROVISION_REMOTEWIPE: + case SYNC_PROVISION_ACCOUNTONLYREMOTEWIPE: if(!self::$decoder->getElementStartTag(SYNC_PROVISION_STATUS)) return false; @@ -238,9 +242,16 @@ public function Handle($commandCode) { //wipe data if a higher RWSTATUS is requested if ($rwstatus > SYNC_PROVISION_RWSTATUS_OK && $policystatus === SYNC_PROVISION_POLICYSTATUS_SUCCESS) { - self::$encoder->startTag(SYNC_PROVISION_REMOTEWIPE, false, true); - self::$deviceManager->SetProvisioningWipeStatus(($rwstatusWiped)?SYNC_PROVISION_RWSTATUS_WIPED:SYNC_PROVISION_RWSTATUS_REQUESTED); - self::$topCollector->AnnounceInformation(sprintf("Remote wipe %s", ($rwstatusWiped)?"executed":"requested"), true); + if ($rwstatus <= SYNC_PROVISION_RWSTATUS_WIPED) { + self::$encoder->startTag(SYNC_PROVISION_REMOTEWIPE, false, true); + self::$deviceManager->SetProvisioningWipeStatus(($rwstatusWiped)?SYNC_PROVISION_RWSTATUS_WIPED:SYNC_PROVISION_RWSTATUS_REQUESTED); + self::$topCollector->AnnounceInformation(sprintf("Remote wipe %s", ($rwstatusWiped)?"executed":"requested"), true); + } + else { + self::$encoder->startTag(SYNC_PROVISION_ACCOUNTONLYREMOTEWIPE, false, true); + self::$deviceManager->SetProvisioningWipeStatus(($rwstatusWiped) ? SYNC_PROVISION_RWSTATUS_WIPED_ACCOUNT_ONLY : SYNC_PROVISION_RWSTATUS_REQUESTED_ACCOUNT_ONLY); + self::$topCollector->AnnounceInformation(sprintf("Account Only Remote wipe %s", ($rwstatusWiped) ? "executed" : "requested"), true); + } } self::$encoder->endTag();//provision diff --git a/src/lib/request/request.php b/src/lib/request/request.php index 47327bf2..0decdfae 100644 --- a/src/lib/request/request.php +++ b/src/lib/request/request.php @@ -760,7 +760,7 @@ static private function filterEvilInput($input, $filter, $replacevalue = '') { * If $input is a valid IPv4 or IPv6 address, returns a valid compact IPv4 or IPv6 address string. * Otherwise, it will strip all characters that are neither numerical or '.' and prefix with "bad-ip". * - * @param string $input The ipv4/ipv6 address + * @param string $input The ipv4/ipv6 address * * @access public * @return string diff --git a/src/lib/request/sync.php b/src/lib/request/sync.php index d837784e..f760a195 100644 --- a/src/lib/request/sync.php +++ b/src/lib/request/sync.php @@ -495,6 +495,16 @@ public function Handle($commandCode) { } } + // get the instanceId if available + $instanceid = false; + if (self::$decoder->getElementStartTag(SYNC_AIRSYNCBASE_INSTANCEID)) { + if (($instanceid = self::$decoder->getElementContent()) !== false) { + if (!self::$decoder->getElementEndTag()) { // end instanceid + return false; + } + } + } + if(self::$decoder->getElementStartTag(SYNC_CLIENTENTRYID)) { $clientid = self::$decoder->getElementContent(); @@ -542,6 +552,20 @@ public function Handle($commandCode) { else $message = false; + // InstanceID sent: do action to a recurrency exception + if ($instanceid) { + // for delete actions we don't have an ASObject + if (!$message) { + $message = GSync::getSyncObjectFromFolderClass($spa->GetContentClass()); + $message->Decode(self::$decoder); + } + $message->instanceid = $instanceid; + if ($element[EN_TAG] == SYNC_REMOVE) { + $message->instanceiddelete = true; + $element[EN_TAG] = SYNC_MODIFY; + } + } + switch($element[EN_TAG]) { case SYNC_FETCH: array_push($actiondata["fetchids"], $serverid); @@ -1080,25 +1104,30 @@ private function syncFolder($sc, $spa, $exporter, $changecount, $streamimporter, self::$encoder->startTag(SYNC_REPLIES); // output result of all new incoming items - foreach($actiondata["clientids"] as $clientid => $serverid) { + foreach ($actiondata["clientids"] as $clientid => $response) { self::$encoder->startTag(SYNC_ADD); self::$encoder->startTag(SYNC_CLIENTENTRYID); self::$encoder->content($clientid); self::$encoder->endTag(); - if ($serverid) { + if (!empty($response->serverid)) { self::$encoder->startTag(SYNC_SERVERENTRYID); - self::$encoder->content($serverid); + self::$encoder->content($response->serverid); self::$encoder->endTag(); } self::$encoder->startTag(SYNC_STATUS); self::$encoder->content((isset($actiondata["statusids"][$clientid])?$actiondata["statusids"][$clientid]:SYNC_STATUS_CLIENTSERVERCONVERSATIONERROR)); self::$encoder->endTag(); + if (!empty($response->hasResponse)) { + self::$encoder->startTag(SYNC_DATA); + $response->Encode(self::$encoder); + self::$encoder->endTag(); + } self::$encoder->endTag(); } // loop through modify operations which were not a success, send status - foreach($actiondata["modifyids"] as $serverid) { - if (isset($actiondata["statusids"][$serverid]) && $actiondata["statusids"][$serverid] !== SYNC_STATUS_SUCCESS) { + foreach($actiondata["modifyids"] as $serverid => $response) { + if (isset($actiondata["statusids"][$serverid]) && ($actiondata["statusids"][$serverid] !== SYNC_STATUS_SUCCESS || !empty($response->hasResponse))) { self::$encoder->startTag(SYNC_MODIFY); self::$encoder->startTag(SYNC_SERVERENTRYID); self::$encoder->content($serverid); @@ -1106,6 +1135,11 @@ private function syncFolder($sc, $spa, $exporter, $changecount, $streamimporter, self::$encoder->startTag(SYNC_STATUS); self::$encoder->content($actiondata["statusids"][$serverid]); self::$encoder->endTag(); + if (!empty($response->hasResponse)) { + self::$encoder->startTag(SYNC_DATA); + $response->Encode(self::$encoder); + self::$encoder->endTag(); + } self::$encoder->endTag(); } } @@ -1483,8 +1517,6 @@ private function importMessage($spa, &$actiondata, $todo, $message, $clientid, $ case SYNC_MODIFY: self::$topCollector->AnnounceInformation(sprintf("Saving modified message %d", $messageCount)); try { - $actiondata["modifyids"][] = $serverid; - // ignore sms messages if ($foldertype == "SMS" || stripos($serverid, self::ZPUSHIGNORESMS) !== false) { ZLog::Write(LOGLEVEL_DEBUG, "SMS sync are not supported. Ignoring message."); @@ -1496,20 +1528,17 @@ private function importMessage($spa, &$actiondata, $todo, $message, $clientid, $ $actiondata["statusids"][$serverid] = SYNC_STATUS_CLIENTSERVERCONVERSATIONERROR; } else { - if(isset($message->read)) { - // Currently, 'read' is only sent by the PDA when it is ONLY setting the read flag. - $this->importer->ImportMessageReadFlag($serverid, $message->read); + // if there is just a read flag change, import it via ImportMessageReadFlag() + if (isset($message->read) && !isset($message->flag) && $message->getCheckedParameters() < 3) { + $response = $this->importer->ImportMessageReadFlag($serverid, $message->read); } - elseif (!isset($message->flag)) { - $this->importer->ImportMessageChange($serverid, $message); - } - - // email todoflags - some devices send todos flags together with read flags, - // so they have to be handled separately - if (isset($message->flag)){ - $this->importer->ImportMessageChange($serverid, $message); + else { + $response = $this->importer->ImportMessageChange($serverid, $message); } + // revert AS16 breaking change + //$response->serverid = $serverid; + $actiondata["modifyids"][$serverid] = $response; $actiondata["statusids"][$serverid] = SYNC_STATUS_SUCCESS; } } diff --git a/src/lib/syncobjects/responsetrait.php b/src/lib/syncobjects/responsetrait.php new file mode 100644 index 00000000..7cc5a1f0 --- /dev/null +++ b/src/lib/syncobjects/responsetrait.php @@ -0,0 +1,15 @@ + array ( self::STREAMER_VAR => "timezone", @@ -221,6 +228,39 @@ function __construct() { self::STREAMER_RONOTIFY => true); } + if (Request::GetProtocolVersion() >= 16.0) { + $mapping[SYNC_AIRSYNCBASE_ATTACHMENTS] = [ + self::STREAMER_VAR => "asattachments", + // Different tags can be used to encapsulate the SyncBaseAttachmentSubtypes depending on its usecase + self::STREAMER_ARRAY => [ + SYNC_AIRSYNCBASE_ATTACHMENT => "SyncBaseAttachment", + SYNC_AIRSYNCBASE_ADD => "SyncBaseAttachmentAdd", + SYNC_AIRSYNCBASE_DELETE => "SyncBaseAttachmentDelete", + ], + ]; + $mapping[SYNC_AIRSYNCBASE_LOCATION] = [ + self::STREAMER_VAR => "location2", + self::STREAMER_TYPE => "SyncLocation", + self::STREAMER_RONOTIFY => true, + ]; + $mapping[SYNC_POOMCAL_CLIENTUID] = [ + self::STREAMER_VAR => "clientuid", + self::STREAMER_RONOTIFY => true, + ]; + // Placeholder for the InstanceId (recurrence exceptions) and its deletion request + $mapping[SYNC_AIRSYNCBASE_INSTANCEID] = [ + self::STREAMER_VAR => "instanceid", + self::STREAMER_TYPE => self::STREAMER_TYPE_IGNORE, + ]; + $mapping[SYNC_AIRSYNCBASE_INSTANCEID_DELETE] = [ + self::STREAMER_VAR => "instanceiddelete", + self::STREAMER_TYPE => self::STREAMER_TYPE_IGNORE, + ]; + + // unset these properties because airsyncbase location will be used instead + unset($mapping[SYNC_POOMCAL_LOCATION]); + } + parent::__construct($mapping); // Indicates that this SyncObject supports the private flag and stripping of private data. @@ -299,3 +339,7 @@ public function Check($logAsDebug = false) { return true; } } + +class SyncAppointmentResponse extends SyncAppointment { + use ResponseTrait; +} \ No newline at end of file diff --git a/src/lib/syncobjects/syncappointmentexception.php b/src/lib/syncobjects/syncappointmentexception.php index a124ef7a..c7df243d 100644 --- a/src/lib/syncobjects/syncappointmentexception.php +++ b/src/lib/syncobjects/syncappointmentexception.php @@ -30,6 +30,7 @@ class SyncAppointmentException extends SyncAppointment { public $deleted; public $exceptionstarttime; + public $instanceid; function __construct() { parent::__construct(); @@ -38,12 +39,28 @@ function __construct() { SYNC_POOMCAL_DELETED => array ( self::STREAMER_VAR => "deleted", self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ZEROORONE => self::STREAMER_CHECK_SETZERO), self::STREAMER_RONOTIFY => true), - - SYNC_POOMCAL_EXCEPTIONSTARTTIME => array ( self::STREAMER_VAR => "exceptionstarttime", + ); + + // pre 16.0 use exceptionstarttime + if (Request::GetProtocolVersion() < 16.0) { + $this->mapping += array( + SYNC_POOMCAL_EXCEPTIONSTARTTIME => array ( self::STREAMER_VAR => "exceptionstarttime", self::STREAMER_TYPE => self::STREAMER_TYPE_DATE, self::STREAMER_CHECKS => array( self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETONE), self::STREAMER_RONOTIFY => true), - ); + ); + } + // AS 16.0+ use instanceid + else { + // overwrite SYNC_AIRSYNCBASE_INSTANCEID definition here + $this->mapping += array( + SYNC_AIRSYNCBASE_INSTANCEID => array ( self::STREAMER_VAR => "instanceid", + self::STREAMER_TYPE => self::STREAMER_TYPE_DATE, + self::STREAMER_CHECKS => [self::STREAMER_CHECK_REQUIRED => self::STREAMER_CHECK_SETONE], + self::STREAMER_RONOTIFY => true, + ); + } + // some parameters are not required in an exception, others are not allowed to be set in SyncAppointmentExceptions $this->mapping[SYNC_POOMCAL_TIMEZONE][self::STREAMER_CHECKS] = array(); diff --git a/src/lib/syncobjects/syncbaseattachment.php b/src/lib/syncobjects/syncbaseattachment.php index d09e30f4..49a63ca6 100644 --- a/src/lib/syncobjects/syncbaseattachment.php +++ b/src/lib/syncobjects/syncbaseattachment.php @@ -46,6 +46,30 @@ function __construct() { SYNC_AIRSYNCBASE_ISINLINE => array (self::STREAMER_VAR => "isinline"), ); + if (Request::GetProtocolVersion() >= 16.0) { + $mapping[SYNC_AIRSYNCBASE_CLIENTID] = [ + self::STREAMER_VAR => "clientid", + self::STREAMER_RONOTIFY => true, + ]; + $mapping[SYNC_AIRSYNCBASE_CONTENTTYPE] = [ + self::STREAMER_VAR => "contenttype", + self::STREAMER_RONOTIFY => true, + ]; + $mapping[SYNC_AIRSYNCBASE_CONTENT] = [ + self::STREAMER_VAR => "content", + self::STREAMER_TYPE => self::STREAMER_TYPE_STREAM_ASPLAIN, + self::STREAMER_RONOTIFY => true, + ]; + } + parent::__construct($mapping); } } + +// Subclasses +class SyncBaseAttachmentAdd extends SyncBaseAttachment { + // overload +} +class SyncBaseAttachmentDelete extends SyncBaseAttachment { + // overload +} diff --git a/src/lib/syncobjects/synccontact.php b/src/lib/syncobjects/synccontact.php index 7ae06937..5dab8908 100644 --- a/src/lib/syncobjects/synccontact.php +++ b/src/lib/syncobjects/synccontact.php @@ -241,3 +241,7 @@ function __construct() { parent::__construct($mapping); } } + +class SyncContactResponse extends SyncContact { + use ResponseTrait; +} \ No newline at end of file diff --git a/src/lib/syncobjects/synclocation.php b/src/lib/syncobjects/synclocation.php new file mode 100644 index 00000000..10ab9bb1 --- /dev/null +++ b/src/lib/syncobjects/synclocation.php @@ -0,0 +1,81 @@ + [ + self::STREAMER_VAR => "displayname", + self::STREAMER_RONOTIFY => true, + ], + SYNC_AIRSYNCBASE_ANNOTATION => [ + self::STREAMER_VAR => "annotation", + self::STREAMER_RONOTIFY => true, + ], + SYNC_AIRSYNCBASE_STREET => [ + self::STREAMER_VAR => "street", + self::STREAMER_RONOTIFY => true, + ], + SYNC_AIRSYNCBASE_CITY => [ + self::STREAMER_VAR => "city", + self::STREAMER_RONOTIFY => true, + ], + SYNC_AIRSYNCBASE_STATE => [ + self::STREAMER_VAR => "state", + self::STREAMER_RONOTIFY => true, + ], + SYNC_AIRSYNCBASE_COUNTRY => [ + self::STREAMER_VAR => "country", + self::STREAMER_RONOTIFY => true, + ], + SYNC_AIRSYNCBASE_POSTALCODE => [ + self::STREAMER_VAR => "postalcode", + self::STREAMER_RONOTIFY => true, + ], + SYNC_AIRSYNCBASE_LATITUDE => [ + self::STREAMER_VAR => "latitude", + self::STREAMER_RONOTIFY => true, + ], + SYNC_AIRSYNCBASE_LONGITUDE => [ + self::STREAMER_VAR => "longitude", + self::STREAMER_RONOTIFY => true, + ], + SYNC_AIRSYNCBASE_ACCURACY => [ + self::STREAMER_VAR => "accuracy", + self::STREAMER_RONOTIFY => true, + ], + SYNC_AIRSYNCBASE_ALTITUDE => [ + self::STREAMER_VAR => "altitude", + self::STREAMER_RONOTIFY => true, + ], + SYNC_AIRSYNCBASE_ALTITUDEACCURACY => [ + self::STREAMER_VAR => "altitudeaccuracy", + self::STREAMER_RONOTIFY => true, + ], + SYNC_AIRSYNCBASE_LOCATIONURI => [ + self::STREAMER_VAR => "locationuri", + self::STREAMER_RONOTIFY => true, + ], + ]; + parent::__construct($mapping); + } +} \ No newline at end of file diff --git a/src/lib/syncobjects/syncmail.php b/src/lib/syncobjects/syncmail.php index 7f889910..bf5bb47d 100644 --- a/src/lib/syncobjects/syncmail.php +++ b/src/lib/syncobjects/syncmail.php @@ -167,7 +167,9 @@ function __construct() { $mapping[SYNC_AIRSYNCBASE_ATTACHMENTS] = array ( self::STREAMER_VAR => "asattachments", self::STREAMER_TYPE => "SyncBaseAttachment", - self::STREAMER_ARRAY => SYNC_AIRSYNCBASE_ATTACHMENT); + self::STREAMER_ARRAY => array( SYNC_AIRSYNCBASE_ATTACHMENT => "SyncBaseAttachment", + SYNC_AIRSYNCBASE_ADD => "SyncBaseAttachmentAdd", + SYNC_AIRSYNCBASE_DELETE => "SyncBaseAttachmentDelete") ); $mapping[SYNC_POOMMAIL_CONTENTCLASS] = array ( self::STREAMER_VAR => "contentclass", self::STREAMER_CHECKS => array( self::STREAMER_CHECK_ONEVALUEOF => array(DEFAULT_EMAIL_CONTENTCLASS, DEFAULT_CALENDAR_CONTENTCLASS) )); @@ -230,3 +232,7 @@ function __construct() { parent::__construct($mapping); } } + +class SyncMailResponse extends SyncMail { + use ResponseTrait; +} \ No newline at end of file diff --git a/src/lib/syncobjects/syncnote.php b/src/lib/syncobjects/syncnote.php index 991e36b3..05f37abf 100644 --- a/src/lib/syncobjects/syncnote.php +++ b/src/lib/syncobjects/syncnote.php @@ -128,3 +128,7 @@ public function SetCategoryFromColor() { } } } + +class SyncNoteResponse extends SyncNote { + use ResponseTrait; +} \ No newline at end of file diff --git a/src/lib/syncobjects/syncobject.php b/src/lib/syncobjects/syncobject.php index c11b3cf6..a88e61b8 100644 --- a/src/lib/syncobjects/syncobject.php +++ b/src/lib/syncobjects/syncobject.php @@ -45,11 +45,13 @@ abstract class SyncObject extends Streamer { protected $unsetVars; protected $supportsPrivateStripping; + protected $checkedParameters; public function __construct($mapping) { $this->unsetVars = array(); $this->supportsPrivateStripping = false; + $this->checkedParameters = false; parent::__construct($mapping); } @@ -397,6 +399,15 @@ public function SupportsPrivateStripping() { return $this->supportsPrivateStripping; } + /** + * Indicates the amount of paramters that were set before Checks were executed and potentially set other parameters. + * + * @return bool/int - returns false if Check() was not executed + */ + public function getCheckedParameters() { + return $this->checkedParameters; + } + /** * Method checks if the object has the minimum of required parameters * and fullfills semantic dependencies @@ -424,6 +435,7 @@ public function Check($logAsDebug = false) { } $defaultLogLevel = LOGLEVEL_WARN; + $this->checkedParameters = 0; // in some cases non-false checks should not provoke a WARN log but only a DEBUG log if ($logAsDebug) @@ -447,6 +459,11 @@ public function Check($logAsDebug = false) { if (isset($v[self::STREAMER_CHECKS])) { foreach ($v[self::STREAMER_CHECKS] as $rule => $condition) { + // count parameter if it's set + if (isset($this->{$v[self::STREAMER_VAR]})) { + ++$this->checkedParameters; + } + // check REQUIRED settings if ($rule === self::STREAMER_CHECK_REQUIRED && (!isset($this->{$v[self::STREAMER_VAR]}) || $this->{$v[self::STREAMER_VAR]} === '' ) ) { // parameter is not set but .. diff --git a/src/lib/syncobjects/synctask.php b/src/lib/syncobjects/synctask.php index f5d4c1b8..628ecc5d 100644 --- a/src/lib/syncobjects/synctask.php +++ b/src/lib/syncobjects/synctask.php @@ -174,3 +174,7 @@ public function Check($logAsDebug = false) { return true; } } + +class SyncTaskResponse extends SyncTask { + use ResponseTrait; +} \ No newline at end of file diff --git a/src/lib/utils/utils.php b/src/lib/utils/utils.php index 837874db..c7505103 100644 --- a/src/lib/utils/utils.php +++ b/src/lib/utils/utils.php @@ -1479,8 +1479,8 @@ public static function SafeGetContents($filename, $functName, $suppressWarnings, * Creates a Compact DateTime from a UTC Timestamp - Formats used for ActiveSync yyyyMMddTHHmmSSZ and yyyy-MM-ddTHH:mm:SS.000Z * * @param timestamp $ts - * - * @param string $format + * + * @param string $format * * @access public * @return string @@ -1498,6 +1498,31 @@ public static function FormatDateUtc($ts,$format) { return datefmt_format($dateFormatUtc, $ts); } + /** + * Transforms an AS timestamp into a unix timestamp + * + * @param string $ts + * + * @access private + * @return long + */ + static function parseDate($ts) { + if(preg_match("/(\d{4})[^0-9]*(\d{2})[^0-9]*(\d{2})(T(\d{2})[^0-9]*(\d{2})[^0-9]*(\d{2})(.\d+)?Z){0,1}$/", $ts, $matches)) { + if ($matches[1] >= 2038){ + $matches[1] = 2038; + $matches[2] = 1; + $matches[3] = 18; + $matches[5] = $matches[6] = $matches[7] = 0; + } + + if (!isset($matches[5])) $matches[5] = 0; + if (!isset($matches[6])) $matches[6] = 0; + if (!isset($matches[7])) $matches[7] = 0; + + return gmmktime($matches[5], $matches[6], $matches[7], $matches[2], $matches[3], $matches[1]); + } + return 0; + } } // TODO Win1252/UTF8 functions are deprecated and will be removed sometime diff --git a/src/lib/wbxml/wbxmlencoder.php b/src/lib/wbxml/wbxmlencoder.php index 51db1a6c..20cd6e5f 100644 --- a/src/lib/wbxml/wbxmlencoder.php +++ b/src/lib/wbxml/wbxmlencoder.php @@ -562,14 +562,14 @@ private function writeLog() { * @return */ private function stringToStream($string) { - if (!is_string($string)) { + if (!is_string($string)) { return $string; } - $stream = fopen('php://memory', 'r+'); - fwrite($stream, $string); - rewind($stream); - return $stream; + $stream = fopen('php://memory', 'r+'); + fwrite($stream, $string); + rewind($stream); + return $stream; } } diff --git a/src/vendor/composer/autoload_classmap.php b/src/vendor/composer/autoload_classmap.php index 4097d686..051ee506 100644 --- a/src/vendor/composer/autoload_classmap.php +++ b/src/vendor/composer/autoload_classmap.php @@ -115,6 +115,7 @@ 'Request' => $baseDir . '/lib/request/request.php', 'RequestProcessor' => $baseDir . '/lib/request/requestprocessor.php', 'ResolveRecipients' => $baseDir . '/lib/request/resolverecipients.php', + 'ResponseTrait' => $baseDir . '/lib/syncobjects/responsetrait.php', 'Search' => $baseDir . '/lib/request/search.php', 'SearchProvider' => $baseDir . '/lib/default/searchprovider.php', 'SendMail' => $baseDir . '/lib/request/sendmail.php', @@ -134,24 +135,31 @@ 'SyncAccount' => $baseDir . '/lib/syncobjects/syncaccount.php', 'SyncAppointment' => $baseDir . '/lib/syncobjects/syncappointment.php', 'SyncAppointmentException' => $baseDir . '/lib/syncobjects/syncappointmentexception.php', + 'SyncAppointmentResponse' => $baseDir . '/lib/syncobjects/syncappointment.php', 'SyncAttachment' => $baseDir . '/lib/syncobjects/syncattachment.php', 'SyncAttendee' => $baseDir . '/lib/syncobjects/syncattendee.php', 'SyncBaseAttachment' => $baseDir . '/lib/syncobjects/syncbaseattachment.php', + 'SyncBaseAttachmentAdd' => $baseDir . '/lib/syncobjects/syncbaseattachment.php', + 'SyncBaseAttachmentDelete' => $baseDir . '/lib/syncobjects/syncbaseattachment.php', 'SyncBaseBody' => $baseDir . '/lib/syncobjects/syncbasebody.php', 'SyncBaseBodyPart' => $baseDir . '/lib/syncobjects/syncbasebodypart.php', 'SyncCollections' => $baseDir . '/lib/core/synccollections.php', 'SyncContact' => $baseDir . '/lib/syncobjects/synccontact.php', + 'SyncContactResponse' => $baseDir . '/lib/syncobjects/synccontact.php', 'SyncDeviceInformation' => $baseDir . '/lib/syncobjects/syncdeviceinformation.php', 'SyncDevicePassword' => $baseDir . '/lib/syncobjects/syncdevicepassword.php', 'SyncEmailAddresses' => $baseDir . '/lib/syncobjects/syncemailaddresses.php', 'SyncFindProperties' => $baseDir . '/lib/syncobjects/syncfindproperties.php', 'SyncFolder' => $baseDir . '/lib/syncobjects/syncfolder.php', 'SyncItemOperationsAttachment' => $baseDir . '/lib/syncobjects/syncitemoperationsattachment.php', + 'SyncLocation' => $baseDir . '/lib/syncobjects/synclocation.php', 'SyncMail' => $baseDir . '/lib/syncobjects/syncmail.php', 'SyncMailFlags' => $baseDir . '/lib/syncobjects/syncmailflags.php', + 'SyncMailResponse' => $baseDir . '/lib/syncobjects/syncmail.php', 'SyncMeetingRequest' => $baseDir . '/lib/syncobjects/syncmeetingrequest.php', 'SyncMeetingRequestRecurrence' => $baseDir . '/lib/syncobjects/syncmeetingrequestrecurrence.php', 'SyncNote' => $baseDir . '/lib/syncobjects/syncnote.php', + 'SyncNoteResponse' => $baseDir . '/lib/syncobjects/syncnote.php', 'SyncOOF' => $baseDir . '/lib/syncobjects/syncoof.php', 'SyncOOFMessage' => $baseDir . '/lib/syncobjects/syncoofmessage.php', 'SyncObject' => $baseDir . '/lib/syncobjects/syncobject.php', @@ -173,6 +181,7 @@ 'SyncSendMailSource' => $baseDir . '/lib/syncobjects/syncsendmailsource.php', 'SyncTask' => $baseDir . '/lib/syncobjects/synctask.php', 'SyncTaskRecurrence' => $baseDir . '/lib/syncobjects/synctaskrecurrence.php', + 'SyncTaskResponse' => $baseDir . '/lib/syncobjects/synctask.php', 'SyncUserInformation' => $baseDir . '/lib/syncobjects/syncuserinformation.php', 'SyncValidateCert' => $baseDir . '/lib/syncobjects/syncvalidatecert.php', 'Syslog' => $baseDir . '/lib/log/syslog.php', diff --git a/src/vendor/composer/autoload_static.php b/src/vendor/composer/autoload_static.php index 0a84ad5c..2d6d7cec 100644 --- a/src/vendor/composer/autoload_static.php +++ b/src/vendor/composer/autoload_static.php @@ -122,6 +122,7 @@ class ComposerStaticInit153a56a781a72686b71399955d98204f 'Request' => __DIR__ . '/../..' . '/lib/request/request.php', 'RequestProcessor' => __DIR__ . '/../..' . '/lib/request/requestprocessor.php', 'ResolveRecipients' => __DIR__ . '/../..' . '/lib/request/resolverecipients.php', + 'ResponseTrait' => __DIR__ . '/../..' . '/lib/syncobjects/responsetrait.php', 'Search' => __DIR__ . '/../..' . '/lib/request/search.php', 'SearchProvider' => __DIR__ . '/../..' . '/lib/default/searchprovider.php', 'SendMail' => __DIR__ . '/../..' . '/lib/request/sendmail.php', @@ -141,24 +142,31 @@ class ComposerStaticInit153a56a781a72686b71399955d98204f 'SyncAccount' => __DIR__ . '/../..' . '/lib/syncobjects/syncaccount.php', 'SyncAppointment' => __DIR__ . '/../..' . '/lib/syncobjects/syncappointment.php', 'SyncAppointmentException' => __DIR__ . '/../..' . '/lib/syncobjects/syncappointmentexception.php', + 'SyncAppointmentResponse' => __DIR__ . '/../..' . '/lib/syncobjects/syncappointment.php', 'SyncAttachment' => __DIR__ . '/../..' . '/lib/syncobjects/syncattachment.php', 'SyncAttendee' => __DIR__ . '/../..' . '/lib/syncobjects/syncattendee.php', 'SyncBaseAttachment' => __DIR__ . '/../..' . '/lib/syncobjects/syncbaseattachment.php', + 'SyncBaseAttachmentAdd' => __DIR__ . '/../..' . '/lib/syncobjects/syncbaseattachment.php', + 'SyncBaseAttachmentDelete' => __DIR__ . '/../..' . '/lib/syncobjects/syncbaseattachment.php', 'SyncBaseBody' => __DIR__ . '/../..' . '/lib/syncobjects/syncbasebody.php', 'SyncBaseBodyPart' => __DIR__ . '/../..' . '/lib/syncobjects/syncbasebodypart.php', 'SyncCollections' => __DIR__ . '/../..' . '/lib/core/synccollections.php', 'SyncContact' => __DIR__ . '/../..' . '/lib/syncobjects/synccontact.php', + 'SyncContactResponse' => __DIR__ . '/../..' . '/lib/syncobjects/synccontact.php', 'SyncDeviceInformation' => __DIR__ . '/../..' . '/lib/syncobjects/syncdeviceinformation.php', 'SyncDevicePassword' => __DIR__ . '/../..' . '/lib/syncobjects/syncdevicepassword.php', 'SyncEmailAddresses' => __DIR__ . '/../..' . '/lib/syncobjects/syncemailaddresses.php', 'SyncFindProperties' => __DIR__ . '/../..' . '/lib/syncobjects/syncfindproperties.php', 'SyncFolder' => __DIR__ . '/../..' . '/lib/syncobjects/syncfolder.php', 'SyncItemOperationsAttachment' => __DIR__ . '/../..' . '/lib/syncobjects/syncitemoperationsattachment.php', + 'SyncLocation' => __DIR__ . '/../..' . '/lib/syncobjects/synclocation.php', 'SyncMail' => __DIR__ . '/../..' . '/lib/syncobjects/syncmail.php', 'SyncMailFlags' => __DIR__ . '/../..' . '/lib/syncobjects/syncmailflags.php', + 'SyncMailResponse' => __DIR__ . '/../..' . '/lib/syncobjects/syncmail.php', 'SyncMeetingRequest' => __DIR__ . '/../..' . '/lib/syncobjects/syncmeetingrequest.php', 'SyncMeetingRequestRecurrence' => __DIR__ . '/../..' . '/lib/syncobjects/syncmeetingrequestrecurrence.php', 'SyncNote' => __DIR__ . '/../..' . '/lib/syncobjects/syncnote.php', + 'SyncNoteResponse' => __DIR__ . '/../..' . '/lib/syncobjects/syncnote.php', 'SyncOOF' => __DIR__ . '/../..' . '/lib/syncobjects/syncoof.php', 'SyncOOFMessage' => __DIR__ . '/../..' . '/lib/syncobjects/syncoofmessage.php', 'SyncObject' => __DIR__ . '/../..' . '/lib/syncobjects/syncobject.php', @@ -180,6 +188,7 @@ class ComposerStaticInit153a56a781a72686b71399955d98204f 'SyncSendMailSource' => __DIR__ . '/../..' . '/lib/syncobjects/syncsendmailsource.php', 'SyncTask' => __DIR__ . '/../..' . '/lib/syncobjects/synctask.php', 'SyncTaskRecurrence' => __DIR__ . '/../..' . '/lib/syncobjects/synctaskrecurrence.php', + 'SyncTaskResponse' => __DIR__ . '/../..' . '/lib/syncobjects/synctask.php', 'SyncUserInformation' => __DIR__ . '/../..' . '/lib/syncobjects/syncuserinformation.php', 'SyncValidateCert' => __DIR__ . '/../..' . '/lib/syncobjects/syncvalidatecert.php', 'Syslog' => __DIR__ . '/../..' . '/lib/log/syslog.php', diff --git a/src/z-push-admin.php b/src/z-push-admin.php index 5de7c9f1..a48e4c64 100755 --- a/src/z-push-admin.php +++ b/src/z-push-admin.php @@ -705,7 +705,7 @@ static public function CommandListDetails() { $data[1], "\t", $data[2], "\t", $data[3], "\t", - ( (isset($device->ignoredmessages) && !empty($device->ignoredmessages)) ? count($device->ignoredmessages) : 0), "\t", + ( (isset($device->ignoredmessages) && !empty($device->ignoredmessages)) ? count($device->ignoredmessages) : 0), "\t", (($device->GetKoeLastAccess() && $device->GetKoeLastAccess() + 25260 < $device->GetLastSyncTime()) ? "KOE inactive":""), "\n"; } }