Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More AS16.1 Changes #116

Merged
merged 27 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
489674b
AS16.1 - Fixed Android Nine App
matidau Oct 4, 2024
b9b86da
AS16.1 - Handle creation/update/deletion of exceptions from the mobile
matidau Oct 5, 2024
abb2cd0
AS16.1 - Implemented AS 16.0 Drafts sync
matidau Oct 5, 2024
6135138
AS16.1 - Implement Account Only Remote Wipe
matidau Oct 5, 2024
bfc96f3
AS16.1 - Implement Appointment Attachment save+remove
matidau Oct 29, 2024
121de01
AS16.1 - Propose New Time from mobile implemented.
matidau Oct 31, 2024
84ddc02
AS16.1 - fix: undefined array key error
matidau Oct 31, 2024
1bfee47
AS16.1 - Added appointment exception InstanceID
matidau Oct 31, 2024
884cccf
AS16.1 - Fix warning if store has no PR_STORE_SUPPORT_MASK
matidau Oct 31, 2024
22b3bf9
AS16.1 - Added support to attach files to appointments and draft emails.
matidau Nov 1, 2024
8b76595
AS16.1 - Send meeting requests to attendees on MR-Appointment creatio…
matidau Nov 1, 2024
a71ab81
AS16.1 - Fixed showing "this message was not yet send" in web
matidau Nov 1, 2024
1e8781e
AS16.1 - Basic attachment in calendars support
matidau Nov 1, 2024
4b44975
AS16.1 - Add location handling for appointments and recurrency except…
matidau Nov 1, 2024
d268396
AS16.1 - Handle creation/update/deletion of exceptions from the mobile
matidau Nov 1, 2024
60bfb44
AS16.1 - Implement bcc email field
matidau Nov 1, 2024
a1db684
AS16.1 - Save recipients in drafts for android devices
matidau Nov 1, 2024
c1f265d
AS16.1 - Use ClientUID if transmitted
matidau Nov 1, 2024
7ad9578
AS16.1 - Send MR when upgrading a normal appointment to a MR (adding …
matidau Nov 1, 2024
5fbc03d
AS16.1 - Ignore added attachments when no content is sent (iOS 16.2 b…
matidau Nov 1, 2024
131e74f
AS16.1 - location altitude, accuracy and altitudeaccuracy should also…
matidau Nov 1, 2024
f55e00b
AS16.1 - Fix warnings when importing a read flag change from the mobile
matidau Nov 1, 2024
e4e7ada
AS16.1 - Fix ZLog
matidau Nov 7, 2024
07f60c7
AS16.1 - fix indentation from tabs
matidau Nov 7, 2024
e8831da
AS16.1 - revert AS16 breaking change 1
matidau Nov 7, 2024
df119ed
AS16.1 - make Utils::parseDate static
matidau Nov 7, 2024
b143956
AS16.1 - make Utils::parseDate static
matidau Nov 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/backend/ipcsharedmemory/ipcsharedmemoryprovider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down
23 changes: 18 additions & 5 deletions src/backend/kopano/importer.php
Original file line number Diff line number Diff line change
Expand Up @@ -440,15 +440,17 @@ 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())
throw new StatusException(sprintf("ImportChangesICS->ImportMessageChange('%s','%s'): Error, mapi_savechanges() failed: 0x%X", $id, get_class($message), mapi_last_hresult()), SYNC_STATUS_SYNCCANNOTBECOMPLETED);

$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);
Expand Down Expand Up @@ -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;
}
Expand All @@ -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.
Expand Down Expand Up @@ -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;
}

/**
Expand Down
102 changes: 70 additions & 32 deletions src/backend/kopano/kopano.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand All @@ -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();
Expand Down Expand Up @@ -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));
Expand All @@ -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);

Expand All @@ -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())
Expand All @@ -956,22 +977,37 @@ 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
default:
$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);
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down
6 changes: 3 additions & 3 deletions src/backend/kopano/mapi/class.meetingrequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -3742,7 +3742,7 @@ function getLocalCategories($calendarItem, $store, $calFolder)
}

return $localCategories;
}
}

/**
* Helper function which is use to apply local categories on respective occurrences.
Expand Down
5 changes: 5 additions & 0 deletions src/backend/kopano/mapimapping.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}

Expand Down Expand Up @@ -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",
);
}

Expand Down
Loading
Loading