Passed
Push — master ( 8eb077...db15bd )
by
unknown
13:17
created

class.meetingrequest.php (91 issues)

1
<?php
2
/*
3
 * SPDX-License-Identifier: AGPL-3.0-only
4
 * SPDX-FileCopyrightText: Copyright 2005-2016 Zarafa Deutschland GmbH
5
 * SPDX-FileCopyrightText: Copyright 2020-2024 grommunio GmbH
6
 */
7
8
class Meetingrequest {
9
	/*
10
	 * NOTE
11
	 *
12
	 * This class is designed to modify and update meeting request properties
13
	 * and to search for linked appointments in the calendar. It does not
14
	 * - set standard properties like subject or location
15
	 * - commit property changes through savechanges() (except in accept() and decline())
16
	 *
17
	 * To set all the other properties, just handle the item as any other appointment
18
	 * item. You aren't even required to set those properties before or after using
19
	 * this class. If you update properties before REsending a meeting request (ie with
20
	 * a time change) you MUST first call updateMeetingRequest() so the internal counters
21
	 * can be updated. You can then submit the message any way you like.
22
	 *
23
	 */
24
25
	/*
26
	 * How to use
27
	 * ----------
28
	 *
29
	 * Sending a meeting request:
30
	 * - Create appointment item as normal, but as 'tentative'
31
	 *   (this is the state of the item when the receiving user has received but
32
	 *    not accepted the item)
33
	 * - Set recipients as normally in e-mails
34
	 * - Create Meetingrequest class instance
35
	 * - Call checkCalendarWriteAccess(), to check for write permissions on calendar folder
36
	 * - Call setMeetingRequest(), this turns on all the meeting request properties in the
37
	 *   calendar item
38
	 * - Call sendMeetingRequest(), this sends a copy of the item with some extra properties
39
	 *
40
	 * Updating a meeting request:
41
	 * - Create Meetingrequest class instance
42
	 * - Call checkCalendarWriteAccess(), to check for write permissions on calendar folder
43
	 * - Call updateMeetingRequest(), this updates the counters
44
	 * - Call checkSignificantChanges(), this will check for significant changes and if needed will clear the
45
	 *   existing recipient responses
46
	 * - Call sendMeetingRequest()
47
	 *
48
	 * Clicking on a an e-mail:
49
	 * - Create Meetingrequest class instance
50
	 * - Check isMeetingRequest(), if true:
51
	 *   - Check isLocalOrganiser(), if true then ignore the message
52
	 *   - Check isInCalendar(), if not call doAccept(true, false, false). This adds the item in your
53
	 *     calendar as tentative without sending a response
54
	 *   - Show Accept, Tentative, Decline buttons
55
	 *   - When the user presses Accept, Tentative or Decline, call doAccept(false, true, true),
56
	 *     doAccept(true, true, true) or doDecline(true) respectively to really accept or decline and
57
	 *     send the response. This will remove the request from your inbox.
58
	 * - Check isMeetingRequestResponse, if true:
59
	 *   - Check isLocalOrganiser(), if not true then ignore the message
60
	 *   - Call processMeetingRequestResponse()
61
	 *     This will update the trackstatus of all recipients, and set the item to 'busy'
62
	 *     when all the recipients have accepted.
63
	 * - Check isMeetingCancellation(), if true:
64
	 *   - Check isLocalOrganiser(), if true then ignore the message
65
	 *   - Check isInCalendar(), if not, then ignore
66
	 *     Call processMeetingCancellation()
67
	 *   - Show 'Remove From Calendar' button to user
68
	 *   - When userpresses button, call doRemoveFromCalendar(), which removes the item from your
69
	 *     calendar and deletes the message
70
	 *
71
	 * Cancelling a meeting request:
72
	 *   - Call doCancelInvitation, which will send cancellation mails to attendees and will remove
73
	 *     meeting object from calendar
74
	 */
75
76
	// All properties for a recipient that are interesting
77
	public $recipprops = [
78
		PR_ENTRYID,
79
		PR_DISPLAY_NAME,
80
		PR_EMAIL_ADDRESS,
81
		PR_RECIPIENT_ENTRYID,
82
		PR_RECIPIENT_TYPE,
83
		PR_SEND_INTERNET_ENCODING,
84
		PR_SEND_RICH_INFO,
85
		PR_RECIPIENT_DISPLAY_NAME,
86
		PR_ADDRTYPE,
87
		PR_DISPLAY_TYPE,
88
		PR_DISPLAY_TYPE_EX,
89
		PR_RECIPIENT_TRACKSTATUS,
90
		PR_RECIPIENT_TRACKSTATUS_TIME,
91
		PR_RECIPIENT_FLAGS,
92
		PR_ROWID,
93
		PR_OBJECT_TYPE,
94
		PR_SEARCH_KEY,
95
		PR_SMTP_ADDRESS,
96
	];
97
98
	/**
99
	 * Indication whether the setting of resources in a Meeting Request is success (false) or if it
100
	 * has failed (integer).
101
	 *
102
	 * @var null|false|int
103
	 *
104
	 * @psalm-var 1|3|4|false|null
105
	 */
106
	public $errorSetResource;
107
108
	public $proptags;
109
	private $store;
110
	public $message;
111
	private $session;
112
113
	/**
114
	 * @var false|string
115
	 */
116
	private $meetingTimeInfo;
117
	private $enableDirectBooking;
118
119
	/**
120
	 * @var null|bool
121
	 */
122
	private $includesResources;
123
	private $nonAcceptingResources;
124
	private $recipientDisplayname;
125
126
	/**
127
	 * Constructor.
128
	 *
129
	 * Takes a store and a message. The message is an appointment item
130
	 * that should be converted into a meeting request or an incoming
131
	 * e-mail message that is a meeting request.
132
	 *
133
	 * The $session variable is optional, but required if the following features
134
	 * are to be used:
135
	 *
136
	 * - Sending meeting requests for meetings that are not in your own store
137
	 * - Sending meeting requests to resources, resource availability checking and resource freebusy updates
138
	 *
139
	 * @param mixed $store
140
	 * @param mixed $message
141
	 * @param mixed $session
142
	 * @param mixed $enableDirectBooking
143
	 */
144
	public function __construct($store, $message, $session = false, $enableDirectBooking = true) {
145
		$this->store = $store;
146
		$this->message = $message;
147
		$this->session = $session;
148
		// This variable string saves time information for the MR.
149
		$this->meetingTimeInfo = false;
150
		$this->enableDirectBooking = $enableDirectBooking;
151
152
		$properties = [];
153
		$properties['goid'] = 'PT_BINARY:PSETID_Meeting:0x3';
154
		$properties['goid2'] = 'PT_BINARY:PSETID_Meeting:0x23';
155
		$properties['type'] = 'PT_STRING8:PSETID_Meeting:0x24';
156
		$properties['meetingrecurring'] = 'PT_BOOLEAN:PSETID_Meeting:0x5';
157
		$properties['unknown2'] = 'PT_BOOLEAN:PSETID_Meeting:0xa';
158
		$properties['attendee_critical_change'] = 'PT_SYSTIME:PSETID_Meeting:0x1';
159
		$properties['owner_critical_change'] = 'PT_SYSTIME:PSETID_Meeting:0x1a';
160
		$properties['meetingstatus'] = 'PT_LONG:PSETID_Appointment:' . PidLidAppointmentStateFlags;
161
		$properties['responsestatus'] = 'PT_LONG:PSETID_Appointment:0x8218';
162
		$properties['unknown6'] = 'PT_LONG:PSETID_Meeting:0x4';
163
		$properties['replytime'] = 'PT_SYSTIME:PSETID_Appointment:0x8220';
164
		$properties['usetnef'] = 'PT_BOOLEAN:PSETID_Common:0x8582';
165
		$properties['recurrence_data'] = 'PT_BINARY:PSETID_Appointment:' . PidLidAppointmentRecur;
166
		$properties['reminderminutes'] = 'PT_LONG:PSETID_Common:' . PidLidReminderDelta;
167
		$properties['reminderset'] = 'PT_BOOLEAN:PSETID_Common:' . PidLidReminderSet;
168
		$properties['sendasical'] = 'PT_BOOLEAN:PSETID_Appointment:0x8200';
169
		$properties['updatecounter'] = 'PT_LONG:PSETID_Appointment:' . PidLidAppointmentSequence;					// AppointmentSequenceNumber
170
		$properties['unknown7'] = 'PT_LONG:PSETID_Appointment:0x8202';
171
		$properties['last_updatecounter'] = 'PT_LONG:PSETID_Appointment:0x8203';			// AppointmentLastSequence
172
		$properties['busystatus'] = 'PT_LONG:PSETID_Appointment:' . PidLidBusyStatus;
173
		$properties['intendedbusystatus'] = 'PT_LONG:PSETID_Appointment:' . PidLidIntendedBusyStatus;
174
		$properties['start'] = 'PT_SYSTIME:PSETID_Appointment:' . PidLidAppointmentStartWhole;
175
		$properties['responselocation'] = 'PT_STRING8:PSETID_Meeting:0x2';
176
		$properties['location'] = 'PT_STRING8:PSETID_Appointment:' . PidLidLocation;
177
		$properties['requestsent'] = 'PT_BOOLEAN:PSETID_Appointment:0x8229';		// PidLidFInvited, MeetingRequestWasSent
178
		$properties['startdate'] = 'PT_SYSTIME:PSETID_Appointment:' . PidLidAppointmentStartWhole;
179
		$properties['duedate'] = 'PT_SYSTIME:PSETID_Appointment:' . PidLidAppointmentEndWhole;
180
		$properties['flagdueby'] = 'PT_SYSTIME:PSETID_Common:' . PidLidReminderSignalTime;
181
		$properties['commonstart'] = 'PT_SYSTIME:PSETID_Common:0x8516';
182
		$properties['commonend'] = 'PT_SYSTIME:PSETID_Common:0x8517';
183
		$properties['recurring'] = 'PT_BOOLEAN:PSETID_Appointment:' . PidLidRecurring;
184
		$properties['clipstart'] = 'PT_SYSTIME:PSETID_Appointment:' . PidLidClipStart;
185
		$properties['clipend'] = 'PT_SYSTIME:PSETID_Appointment:' . PidLidClipEnd;
186
		$properties['start_recur_date'] = 'PT_LONG:PSETID_Meeting:0xD';				// StartRecurTime
187
		$properties['start_recur_time'] = 'PT_LONG:PSETID_Meeting:0xE';				// StartRecurTime
188
		$properties['end_recur_date'] = 'PT_LONG:PSETID_Meeting:0xF';				// EndRecurDate
189
		$properties['end_recur_time'] = 'PT_LONG:PSETID_Meeting:0x10';				// EndRecurTime
190
		$properties['is_exception'] = 'PT_BOOLEAN:PSETID_Meeting:0xA';				// LID_IS_EXCEPTION
191
		$properties['apptreplyname'] = 'PT_STRING8:PSETID_Appointment:0x8230';
192
		// Propose new time properties
193
		$properties['proposed_start_whole'] = 'PT_SYSTIME:PSETID_Appointment:' . PidLidAppointmentProposedStartWhole;
194
		$properties['proposed_end_whole'] = 'PT_SYSTIME:PSETID_Appointment:' . PidLidAppointmentProposedEndWhole;
195
		$properties['proposed_duration'] = 'PT_LONG:PSETID_Appointment:0x8256';
196
		$properties['counter_proposal'] = 'PT_BOOLEAN:PSETID_Appointment:' . PidLidAppointmentCounterProposal;
197
		$properties['recurring_pattern'] = 'PT_STRING8:PSETID_Appointment:' . PidLidRecurrencePattern;
198
		$properties['basedate'] = 'PT_SYSTIME:PSETID_Appointment:' . PidLidExceptionReplaceTime;
199
		$properties['meetingtype'] = 'PT_LONG:PSETID_Meeting:0x26';
200
		$properties['timezone_data'] = 'PT_BINARY:PSETID_Appointment:' . PidLidTimeZoneStruct;
201
		$properties['timezone'] = 'PT_STRING8:PSETID_Appointment:' . PidLidTimeZoneDescription;
202
		$properties['categories'] = 'PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords';
203
		$properties['private'] = 'PT_BOOLEAN:PSETID_Common:' . PidLidPrivate;
204
		$properties['alldayevent'] = 'PT_BOOLEAN:PSETID_Appointment:' . PidLidAppointmentSubType;
205
		$properties['toattendeesstring'] = 'PT_STRING8:PSETID_Appointment:0x823B';
206
		$properties['ccattendeesstring'] = 'PT_STRING8:PSETID_Appointment:0x823C';
207
208
		$this->proptags = getPropIdsFromStrings($store, $properties);
209
	}
210
211
	/**
212
	 * Sets the direct booking property. This is an alternative to the setting of the direct booking
213
	 * property through the constructor. However, setting it in the constructor is preferred.
214
	 *
215
	 * @param bool $directBookingSetting
216
	 */
217
	public function setDirectBooking($directBookingSetting): void {
218
		$this->enableDirectBooking = $directBookingSetting;
219
	}
220
221
	/**
222
	 * Returns TRUE if the message pointed to is an incoming meeting request and should
223
	 * therefore be replied to with doAccept or doDecline().
224
	 *
225
	 * @param string $messageClass message class to use for checking
226
	 *
227
	 * @return bool returns true if this is a meeting request else false
228
	 */
229
	public function isMeetingRequest($messageClass = false) {
230
		if ($messageClass === false) {
231
			$props = mapi_getprops($this->message, [PR_MESSAGE_CLASS]);
232
			$messageClass = isset($props[PR_MESSAGE_CLASS]) ? $props[PR_MESSAGE_CLASS] : false;
233
		}
234
235
		if ($messageClass !== false && stripos($messageClass, 'ipm.schedule.meeting.request') === 0) {
236
			return true;
237
		}
238
239
		return false;
240
	}
241
242
	/**
243
	 * Returns TRUE if the message pointed to is a returning meeting request response.
244
	 *
245
	 * @param string $messageClass message class to use for checking
246
	 *
247
	 * @return bool returns true if this is a meeting request else false
248
	 */
249
	public function isMeetingRequestResponse($messageClass = false) {
250
		if ($messageClass === false) {
251
			$props = mapi_getprops($this->message, [PR_MESSAGE_CLASS]);
252
			$messageClass = isset($props[PR_MESSAGE_CLASS]) ? $props[PR_MESSAGE_CLASS] : false;
253
		}
254
255
		if ($messageClass !== false && stripos($messageClass, 'ipm.schedule.meeting.resp') === 0) {
256
			return true;
257
		}
258
259
		return false;
260
	}
261
262
	/**
263
	 * Returns TRUE if the message pointed to is a cancellation request.
264
	 *
265
	 * @param string $messageClass message class to use for checking
266
	 *
267
	 * @return bool returns true if this is a meeting request else false
268
	 */
269
	public function isMeetingCancellation($messageClass = false) {
270
		if ($messageClass === false) {
271
			$props = mapi_getprops($this->message, [PR_MESSAGE_CLASS]);
272
			$messageClass = isset($props[PR_MESSAGE_CLASS]) ? $props[PR_MESSAGE_CLASS] : false;
273
		}
274
275
		if ($messageClass !== false && stripos($messageClass, 'ipm.schedule.meeting.canceled') === 0) {
276
			return true;
277
		}
278
279
		return false;
280
	}
281
282
	/**
283
	 * Function is used to get the last update counter of meeting request.
284
	 *
285
	 * @return bool|int false when last_updatecounter not found else return last_updatecounter
286
	 */
287
	public function getLastUpdateCounter() {
288
		$calendarItemProps = mapi_getprops($this->message, [$this->proptags['last_updatecounter']]);
289
		if (isset($calendarItemProps) && !empty($calendarItemProps)) {
290
			return $calendarItemProps[$this->proptags['last_updatecounter']];
291
		}
292
293
		return false;
294
	}
295
296
	/**
297
	 * Process an incoming meeting request response. This updates the appointment
298
	 * in your calendar to show whether the user has accepted or declined.
299
	 */
300
	public function processMeetingRequestResponse() {
301
		if (!$this->isMeetingRequestResponse()) {
302
			return;
303
		}
304
305
		if (!$this->isLocalOrganiser()) {
306
			return;
307
		}
308
309
		// Get information we need from the response message
310
		$messageprops = mapi_getprops($this->message, [
311
			$this->proptags['goid'],
312
			$this->proptags['goid2'],
313
			PR_OWNER_APPT_ID,
314
			PR_SENT_REPRESENTING_EMAIL_ADDRESS,
315
			PR_SENT_REPRESENTING_NAME,
316
			PR_SENT_REPRESENTING_ADDRTYPE,
317
			PR_SENT_REPRESENTING_ENTRYID,
318
			PR_SENT_REPRESENTING_SEARCH_KEY,
319
			PR_MESSAGE_DELIVERY_TIME,
320
			PR_MESSAGE_CLASS,
321
			PR_PROCESSED,
322
			PR_RCVD_REPRESENTING_ENTRYID,
323
			$this->proptags['proposed_start_whole'],
324
			$this->proptags['proposed_end_whole'],
325
			$this->proptags['proposed_duration'],
326
			$this->proptags['counter_proposal'],
327
			$this->proptags['attendee_critical_change'],
328
		]);
329
330
		$goid2 = $messageprops[$this->proptags['goid2']];
331
332
		if (!isset($goid2) || !isset($messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS])) {
333
			return;
334
		}
335
336
		// Find basedate in GlobalID(0x3), this can be a response for an occurrence
337
		$basedate = $this->getBasedateFromGlobalID($messageprops[$this->proptags['goid']]);
338
339
		$userStore = $this->store;
340
		// check if delegate is processing the response
341
		if (isset($messageprops[PR_RCVD_REPRESENTING_ENTRYID])) {
342
			$delegatorStore = $this->getDelegatorStore($messageprops[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
343
			if (!empty($delegatorStore['store'])) {
344
				$userStore = $delegatorStore['store'];
345
			}
346
		}
347
348
		// check for calendar access
349
		if ($this->checkCalendarWriteAccess($userStore) !== true) {
350
			// Throw an exception that we don't have write permissions on calendar folder,
351
			// allow caller to fill the error message
352
			throw new MAPIException(null, MAPI_E_NO_ACCESS);
353
		}
354
355
		$calendarItem = $this->getCorrespondentCalendarItem(true);
356
357
		// Open the calendar items, and update all the recipients of the calendar item that match
358
		// the email address of the response.
359
		if ($calendarItem !== false) {
360
			$this->processResponse($userStore, $calendarItem, $basedate, $messageprops);
361
		}
362
	}
363
364
	/**
365
	 * Process every incoming MeetingRequest response.This updates the appointment
366
	 * in your calendar to show whether the user has accepted or declined.
367
	 *
368
	 * @param resource $store        contains the userStore in which the meeting is created
369
	 * @param mixed    $calendarItem resource of the calendar item for which this response has arrived
370
	 * @param mixed    $basedate     if present the create an exception
371
	 * @param array    $messageprops contains message properties
372
	 *
373
	 * @return null|false
374
	 */
375
	public function processResponse($store, $calendarItem, $basedate, $messageprops) {
376
		$senderentryid = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
377
		$messageclass = $messageprops[PR_MESSAGE_CLASS];
378
		$deliverytime = $messageprops[PR_MESSAGE_DELIVERY_TIME];
379
		$recurringItem = 0;
380
381
		// Open the calendar item, find the sender in the recipient table and update all the recipients of the calendar item that match
382
		// the email address of the response.
383
		$calendarItemProps = mapi_getprops($calendarItem, [$this->proptags['recurring'], PR_STORE_ENTRYID, PR_PARENT_ENTRYID, PR_ENTRYID, $this->proptags['updatecounter']]);
384
385
		// check if meeting response is already processed
386
		if (isset($messageprops[PR_PROCESSED]) && $messageprops[PR_PROCESSED] == true) {
387
			// meeting is already processed
388
			return;
389
		}
390
		mapi_setprops($this->message, [PR_PROCESSED => true]);
391
		mapi_savechanges($this->message);
392
393
		// if meeting is updated in organizer's calendar then we don't need to process
394
		// old response
395
		if ($this->isMeetingUpdated($basedate)) {
396
			return;
397
		}
398
399
		// If basedate is found, then create/modify exception msg and do processing
400
		if ($basedate && isset($calendarItemProps[$this->proptags['recurring']]) && $calendarItemProps[$this->proptags['recurring']] === true) {
401
			$recurr = new Recurrence($store, $calendarItem);
402
403
			// Copy properties from meeting request
404
			$exception_props = mapi_getprops($this->message, [
405
				PR_OWNER_APPT_ID,
406
				$this->proptags['proposed_start_whole'],
407
				$this->proptags['proposed_end_whole'],
408
				$this->proptags['proposed_duration'],
409
				$this->proptags['counter_proposal'],
410
			]);
411
412
			// Create/modify exception
413
			if ($recurr->isException($basedate)) {
414
				$recurr->modifyException($exception_props, $basedate);
415
			}
416
			else {
417
				// When we are creating an exception we need copy recipients from main recurring item
418
				$recipTable = mapi_message_getrecipienttable($calendarItem);
419
				$recips = mapi_table_queryallrows($recipTable, $this->recipprops);
420
421
				// Retrieve actual start/due dates from calendar item.
422
				$exception_props[$this->proptags['startdate']] = $recurr->getOccurrenceStart($basedate);
423
				$exception_props[$this->proptags['duedate']] = $recurr->getOccurrenceEnd($basedate);
424
425
				$recurr->createException($exception_props, $basedate, false, $recips);
426
			}
427
428
			mapi_savechanges($calendarItem);
429
430
			$attach = $recurr->getExceptionAttachment($basedate);
431
			if ($attach) {
432
				$recurringItem = $calendarItem;
433
				$calendarItem = mapi_attach_openobj($attach, MAPI_MODIFY);
434
			}
435
			else {
436
				return false;
437
			}
438
		}
439
440
		// Get the recipients of the calendar item
441
		$reciptable = mapi_message_getrecipienttable($calendarItem);
442
		$recipients = mapi_table_queryallrows($reciptable, $this->recipprops);
443
444
		// FIXME we should look at the updatecounter property and compare it
445
		// to the counter in the recipient to see if this update is actually
446
		// newer than the status in the calendar item
447
		$found = false;
448
449
		$totalrecips = 0;
450
		$acceptedrecips = 0;
451
		foreach ($recipients as $recipient) {
452
			++$totalrecips;
453
			// external recipients might not have entryid
454
			if (!isset($recipient[PR_ENTRYID]) &&
455
				$recipient[PR_EMAIL_ADDRESS] == $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS]) {
456
				$recipient[PR_ENTRYID] = $senderentryid;
457
			}
458
			if (isset($recipient[PR_ENTRYID]) && $this->compareABEntryIDs($recipient[PR_ENTRYID], $senderentryid)) {
459
				$found = true;
460
461
				/*
462
				 * If value of attendee_critical_change on meeting response mail is less than PR_RECIPIENT_TRACKSTATUS_TIME
463
				 * on the corresponding recipientRow of meeting then we ignore this response mail.
464
				 */
465
				if (isset($recipient[PR_RECIPIENT_TRACKSTATUS_TIME]) && ($messageprops[$this->proptags['attendee_critical_change']] < $recipient[PR_RECIPIENT_TRACKSTATUS_TIME])) {
466
					continue;
467
				}
468
469
				// The email address matches, update the row
470
				$recipient[PR_RECIPIENT_TRACKSTATUS] = $this->getTrackStatus($messageclass);
471
				if (isset($messageprops[$this->proptags['attendee_critical_change']])) {
472
					$recipient[PR_RECIPIENT_TRACKSTATUS_TIME] = $messageprops[$this->proptags['attendee_critical_change']];
473
				}
474
475
				// If this is a counter proposal, set the proposal properties in the recipient row
476
				if (isset($messageprops[$this->proptags['counter_proposal']]) && $messageprops[$this->proptags['counter_proposal']]) {
477
					$recipient[PR_RECIPIENT_PROPOSEDSTARTTIME] = $messageprops[$this->proptags['proposed_start_whole']];
478
					$recipient[PR_RECIPIENT_PROPOSEDENDTIME] = $messageprops[$this->proptags['proposed_end_whole']];
479
					$recipient[PR_RECIPIENT_PROPOSED] = $messageprops[$this->proptags['counter_proposal']];
480
				}
481
482
				// Update the recipient information
483
				mapi_message_modifyrecipients($calendarItem, MODRECIP_REMOVE, [$recipient]);
484
				mapi_message_modifyrecipients($calendarItem, MODRECIP_ADD, [$recipient]);
485
			}
486
			if (isset($recipient[PR_RECIPIENT_TRACKSTATUS]) && $recipient[PR_RECIPIENT_TRACKSTATUS] == olRecipientTrackStatusAccepted) {
487
				++$acceptedrecips;
488
			}
489
		}
490
491
		// If the recipient was not found in the original calendar item,
492
		// then add the recpient as a new optional recipient
493
		if (!$found) {
494
			$recipient = [];
495
			$recipient[PR_ENTRYID] = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
496
			$recipient[PR_EMAIL_ADDRESS] = $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
497
			$recipient[PR_DISPLAY_NAME] = $messageprops[PR_SENT_REPRESENTING_NAME];
498
			$recipient[PR_ADDRTYPE] = $messageprops[PR_SENT_REPRESENTING_ADDRTYPE];
499
			$recipient[PR_RECIPIENT_TYPE] = MAPI_CC;
500
			$recipient[PR_SEARCH_KEY] = $messageprops[PR_SENT_REPRESENTING_SEARCH_KEY];
501
			$recipient[PR_RECIPIENT_TRACKSTATUS] = $this->getTrackStatus($messageclass);
502
			$recipient[PR_RECIPIENT_TRACKSTATUS_TIME] = $deliverytime;
503
504
			// If this is a counter proposal, set the proposal properties in the recipient row
505
			if (isset($messageprops[$this->proptags['counter_proposal']])) {
506
				$recipient[PR_RECIPIENT_PROPOSEDSTARTTIME] = $messageprops[$this->proptags['proposed_start_whole']];
507
				$recipient[PR_RECIPIENT_PROPOSEDENDTIME] = $messageprops[$this->proptags['proposed_end_whole']];
508
				$recipient[PR_RECIPIENT_PROPOSED] = $messageprops[$this->proptags['counter_proposal']];
509
			}
510
511
			mapi_message_modifyrecipients($calendarItem, MODRECIP_ADD, [$recipient]);
512
			++$totalrecips;
513
			if ($recipient[PR_RECIPIENT_TRACKSTATUS] == olRecipientTrackStatusAccepted) {
514
				++$acceptedrecips;
515
			}
516
		}
517
518
		// TODO: Update counter proposal number property on message
519
		/*
520
		If it is the first time this attendee has proposed a new date/time, increment the value of the PidLidAppointmentProposalNumber property on the organizer's meeting object, by 0x00000001. If this property did not previously exist on the organizer's meeting object, it MUST be set with a value of 0x00000001.
521
		*/
522
		// If this is a counter proposal, set the counter proposal indicator boolean
523
		if (isset($messageprops[$this->proptags['counter_proposal']])) {
524
			$props = [];
525
			if ($messageprops[$this->proptags['counter_proposal']]) {
526
				$props[$this->proptags['counter_proposal']] = true;
527
			}
528
			else {
529
				$props[$this->proptags['counter_proposal']] = false;
530
			}
531
532
			mapi_setprops($calendarItem, $props);
533
		}
534
535
		mapi_savechanges($calendarItem);
536
		if (isset($attach)) {
537
			mapi_savechanges($attach);
538
			mapi_savechanges($recurringItem);
0 ignored issues
show
It seems like $recurringItem can also be of type integer; however, parameter $any of mapi_savechanges() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

538
			mapi_savechanges(/** @scrutinizer ignore-type */ $recurringItem);
Loading history...
539
		}
540
	}
541
542
	/**
543
	 * Process an incoming meeting request cancellation. This updates the
544
	 * appointment in your calendar to show that the meeting has been cancelled.
545
	 */
546
	public function processMeetingCancellation() {
547
		if (!$this->isMeetingCancellation()) {
548
			return;
549
		}
550
551
		if ($this->isLocalOrganiser()) {
552
			return;
553
		}
554
555
		if (!$this->isInCalendar()) {
556
			return;
557
		}
558
559
		$listProperties = $this->proptags;
560
		$listProperties['subject'] = PR_SUBJECT;
561
		$listProperties['sent_representing_name'] = PR_SENT_REPRESENTING_NAME;
562
		$listProperties['sent_representing_address_type'] = PR_SENT_REPRESENTING_ADDRTYPE;
563
		$listProperties['sent_representing_email_address'] = PR_SENT_REPRESENTING_EMAIL_ADDRESS;
564
		$listProperties['sent_representing_entryid'] = PR_SENT_REPRESENTING_ENTRYID;
565
		$listProperties['sent_representing_search_key'] = PR_SENT_REPRESENTING_SEARCH_KEY;
566
		$listProperties['rcvd_representing_name'] = PR_RCVD_REPRESENTING_NAME;
567
		$listProperties['rcvd_representing_address_type'] = PR_RCVD_REPRESENTING_ADDRTYPE;
568
		$listProperties['rcvd_representing_email_address'] = PR_RCVD_REPRESENTING_EMAIL_ADDRESS;
569
		$listProperties['rcvd_representing_entryid'] = PR_RCVD_REPRESENTING_ENTRYID;
570
		$listProperties['rcvd_representing_search_key'] = PR_RCVD_REPRESENTING_SEARCH_KEY;
571
		$messageProps = mapi_getprops($this->message, $listProperties);
572
573
		$goid = $messageProps[$this->proptags['goid']];	// GlobalID (0x3)
574
		if (!isset($goid)) {
575
			return;
576
		}
577
578
		$store = $this->store;
579
		// get delegator store, if delegate is processing this cancellation
580
		if (isset($messageProps[PR_RCVD_REPRESENTING_ENTRYID])) {
581
			$delegatorStore = $this->getDelegatorStore($messageProps[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
582
			if (!empty($delegatorStore['store'])) {
583
				$store = $delegatorStore['store'];
584
			}
585
		}
586
587
		// check for calendar access
588
		if ($this->checkCalendarWriteAccess($store) !== true) {
589
			// Throw an exception that we don't have write permissions on calendar folder,
590
			// allow caller to fill the error message
591
			throw new MAPIException(null, MAPI_E_NO_ACCESS);
592
		}
593
594
		$calendarItem = $this->getCorrespondentCalendarItem(true);
595
		$basedate = $this->getBasedateFromGlobalID($goid);
596
597
		if ($calendarItem !== false) {
598
			// if basedate is provided and we could not find the item then it could be that we are processing
599
			// an exception so get the exception and process it
600
			if ($basedate) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $basedate of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
601
				$calendarItemProps = mapi_getprops($calendarItem, [$this->proptags['recurring']]);
0 ignored issues
show
$calendarItem of type resource|true is incompatible with the type resource expected by parameter $any of mapi_getprops(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

601
				$calendarItemProps = mapi_getprops(/** @scrutinizer ignore-type */ $calendarItem, [$this->proptags['recurring']]);
Loading history...
602
				if ($calendarItemProps[$this->proptags['recurring']] === true) {
603
					$recurr = new Recurrence($store, $calendarItem);
604
605
					// Set message class
606
					$messageProps[PR_MESSAGE_CLASS] = 'IPM.Appointment';
607
608
					if ($recurr->isException($basedate)) {
609
						$recurr->modifyException($messageProps, $basedate);
610
					}
611
					else {
612
						$recurr->createException($messageProps, $basedate);
613
					}
614
				}
615
			}
616
			else {
617
				// set the properties of the cancellation object
618
				mapi_setprops($calendarItem, $messageProps);
0 ignored issues
show
$calendarItem of type resource|true is incompatible with the type resource expected by parameter $any of mapi_setprops(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

618
				mapi_setprops(/** @scrutinizer ignore-type */ $calendarItem, $messageProps);
Loading history...
619
			}
620
621
			mapi_savechanges($calendarItem);
0 ignored issues
show
$calendarItem of type resource|true is incompatible with the type resource expected by parameter $any of mapi_savechanges(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

621
			mapi_savechanges(/** @scrutinizer ignore-type */ $calendarItem);
Loading history...
622
		}
623
	}
624
625
	/**
626
	 * Returns true if the corresponding calendar items exists in the celendar folder for this
627
	 * meeting request/response/cancellation.
628
	 */
629
	public function isInCalendar(): bool {
630
		// @TODO check for deleted exceptions
631
		return $this->getCorrespondentCalendarItem(false) !== false;
632
	}
633
634
	/**
635
	 * Accepts the meeting request by moving the item to the calendar
636
	 * and sending a confirmation message back to the sender. If $tentative
637
	 * is TRUE, then the item is accepted tentatively. After accepting, you
638
	 * can't use this class instance any more. The message is closed. If you
639
	 * specify TRUE for 'move', then the item is actually moved (from your
640
	 * inbox probably) to the calendar. If you don't, it is copied into
641
	 * your calendar.
642
	 *
643
	 * @param bool  $tentative            true if user as tentative accepted the meeting
644
	 * @param bool  $sendresponse         true if a response has to be sent to organizer
645
	 * @param bool  $move                 true if the meeting request should be moved to the deleted items after processing
646
	 * @param mixed $newProposedStartTime contains starttime if user has proposed other time
647
	 * @param mixed $newProposedEndTime   contains endtime if user has proposed other time
648
	 * @param mixed $body
649
	 * @param mixed $userAction
650
	 * @param mixed $store
651
	 * @param mixed $basedate             start of day of occurrence for which user has accepted the recurrent meeting
652
	 * @param bool  $isImported           true to indicate that MR is imported from .ics or .vcs file else it false.
653
	 *
654
	 * @return bool|string $entryid entryid of item which created/updated in calendar
655
	 */
656
	public function doAccept($tentative, $sendresponse, $move, $newProposedStartTime = false, $newProposedEndTime = false, $body = false, $userAction = false, $store = false, $basedate = false, $isImported = false) {
657
		if ($this->isLocalOrganiser()) {
658
			return false;
659
		}
660
661
		// Remove any previous calendar items with this goid and appt id
662
		$messageprops = mapi_getprops($this->message, [PR_ENTRYID, PR_PARENT_ENTRYID,
663
			PR_MESSAGE_CLASS, $this->proptags['goid'], $this->proptags['updatecounter'],
664
			PR_PROCESSED, PR_RCVD_REPRESENTING_ENTRYID, PR_SENDER_ENTRYID,
665
			PR_SENT_REPRESENTING_ENTRYID, PR_RECEIVED_BY_ENTRYID]);
666
667
		// do not process meeting requests in sent items folder
668
		$sentItemsEntryid = $this->getDefaultSentmailEntryID();
669
		if (isset($messageprops[PR_PARENT_ENTRYID]) &&
670
			$sentItemsEntryid !== false &&
671
			$sentItemsEntryid == $messageprops[PR_PARENT_ENTRYID]) {
672
			return false;
673
		}
674
675
		$calFolder = $this->openDefaultCalendar();
676
		$store = $this->store;
677
		// If this meeting request is received by a delegate then open delegator's store.
678
		if (isset($messageprops[PR_RCVD_REPRESENTING_ENTRYID], $messageprops[PR_RECEIVED_BY_ENTRYID]) &&
679
			!compareEntryIds($messageprops[PR_RCVD_REPRESENTING_ENTRYID], $messageprops[PR_RECEIVED_BY_ENTRYID])) {
680
			$delegatorStore = $this->getDelegatorStore($messageprops[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
681
			if (!empty($delegatorStore['store'])) {
682
				$store = $delegatorStore['store'];
683
			}
684
			if (!empty($delegatorStore[PR_IPM_APPOINTMENT_ENTRYID])) {
685
				$calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
686
			}
687
		}
688
689
		// check for calendar access
690
		if ($this->checkCalendarWriteAccess($store) !== true) {
691
			// Throw an exception that we don't have write permissions on calendar folder,
692
			// allow caller to fill the error message
693
			throw new MAPIException(null, MAPI_E_NO_ACCESS);
694
		}
695
696
		// if meeting is out dated then don't process it
697
		if ($this->isMeetingRequest($messageprops[PR_MESSAGE_CLASS]) && $this->isMeetingOutOfDate()) {
698
			return false;
699
		}
700
701
		/*
702
		 *	if this function is called automatically with meeting request object then there will be
703
		 *	two possibilitites
704
		 *	1) meeting request is opened first time, in this case make a tentative appointment in
705
		 *		recipient's calendar
706
		 *	2) after this every subsequent request to open meeting request will not do any processing
707
		 */
708
		if ($this->isMeetingRequest($messageprops[PR_MESSAGE_CLASS]) && $userAction == false) {
709
			if (isset($messageprops[PR_PROCESSED]) && $messageprops[PR_PROCESSED] == true) {
710
				// if meeting request is already processed then don't do anything
711
				return false;
712
			}
713
714
			// if correspondent calendar item is already processed then don't do anything
715
			$calendarItem = $this->getCorrespondentCalendarItem();
716
			if ($calendarItem) {
717
				$calendarItemProps = mapi_getprops($calendarItem, [PR_PROCESSED]);
0 ignored issues
show
$calendarItem of type resource|true is incompatible with the type resource expected by parameter $any of mapi_getprops(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

717
				$calendarItemProps = mapi_getprops(/** @scrutinizer ignore-type */ $calendarItem, [PR_PROCESSED]);
Loading history...
718
				if (isset($calendarItemProps[PR_PROCESSED]) && $calendarItemProps[PR_PROCESSED] == true) {
719
					// mark meeting-request mail as processed as well
720
					mapi_setprops($this->message, [PR_PROCESSED => true]);
721
					mapi_savechanges($this->message);
722
723
					return false;
724
				}
725
			}
726
		}
727
728
		// Retrieve basedate from globalID, if it is not received as argument
729
		if (!$basedate) {
730
			$basedate = $this->getBasedateFromGlobalID($messageprops[$this->proptags['goid']]);
731
		}
732
733
		// set counter proposal properties in calendar item when proposing new time
734
		$proposeNewTimeProps = [];
735
		if ($newProposedStartTime && $newProposedEndTime) {
736
			$proposeNewTimeProps[$this->proptags['proposed_start_whole']] = $newProposedStartTime;
737
			$proposeNewTimeProps[$this->proptags['proposed_end_whole']] = $newProposedEndTime;
738
			$proposeNewTimeProps[$this->proptags['proposed_duration']] = round($newProposedEndTime - $newProposedStartTime) / 60;
739
			$proposeNewTimeProps[$this->proptags['counter_proposal']] = true;
740
		}
741
742
		// While sender is receiver then we have to process the meeting request as per the intended busy status
743
		// instead of tentative, and accept the same as per the intended busystatus.
744
		$senderEntryId = isset($messageprops[PR_SENT_REPRESENTING_ENTRYID]) ? $messageprops[PR_SENT_REPRESENTING_ENTRYID] : $messageprops[PR_SENDER_ENTRYID];
745
		if (isset($messageprops[PR_RECEIVED_BY_ENTRYID]) && compareEntryIds($senderEntryId, $messageprops[PR_RECEIVED_BY_ENTRYID])) {
746
			$entryid = $this->accept(false, $sendresponse, $move, $proposeNewTimeProps, $body, true, $store, $calFolder, $basedate);
747
		}
748
		else {
749
			$entryid = $this->accept($tentative, $sendresponse, $move, $proposeNewTimeProps, $body, $userAction, $store, $calFolder, $basedate);
750
		}
751
752
		// if we have first time processed this meeting then set PR_PROCESSED property
753
		if ($this->isMeetingRequest($messageprops[PR_MESSAGE_CLASS]) && $userAction === false && $isImported === false) {
754
			if (!isset($messageprops[PR_PROCESSED]) || $messageprops[PR_PROCESSED] != true) {
755
				// set processed flag
756
				mapi_setprops($this->message, [PR_PROCESSED => true]);
757
				mapi_savechanges($this->message);
758
			}
759
		}
760
761
		return $entryid;
762
	}
763
764
	/**
765
	 * @param (float|mixed|true)[] $proposeNewTimeProps
766
	 * @param resource             $calFolder
767
	 * @param mixed                $body
768
	 * @param mixed                $store
769
	 * @param mixed                $basedate
770
	 *
771
	 * @psalm-param array<float|mixed|true> $proposeNewTimeProps
772
	 */
773
	public function accept(bool $tentative, bool $sendresponse, bool $move, array $proposeNewTimeProps, $body, bool $userAction, $store, $calFolder, $basedate = false) {
774
		$messageprops = mapi_getprops($this->message);
775
		$isDelegate = isset($messageprops[PR_RCVD_REPRESENTING_NAME]);
776
		$entryid = '';
777
778
		if ($sendresponse) {
779
			$this->createResponse($tentative ? olResponseTentative : olResponseAccepted, $proposeNewTimeProps, $body, $store, $basedate, $calFolder);
780
		}
781
782
		/*
783
		 * Further processing depends on what user is receiving. User can receive recurring item, a single occurrence or a normal meeting.
784
		 * 1) If meeting req is of recurrence then we find all the occurrence in calendar because in past user might have received one or few occurrences.
785
		 * 2) If single occurrence then find occurrence itself using globalID and if item is not found then use cleanGlobalID to find main recurring item
786
		 * 3) Normal meeting req are handled normally as they were handled previously.
787
		 *
788
		 * Also user can respond(accept/decline) to item either from previewpane or from calendar by opening the item. If user is responding the meeting from previewpane
789
		 * and that item is not found in calendar then item is move else item is opened and all properties, attachments and recipient are copied from meeting request.
790
		 * If user is responding from calendar then item is opened and properties are set such as meetingstatus, responsestatus, busystatus etc.
791
		 */
792
		if ($this->isMeetingRequest($messageprops[PR_MESSAGE_CLASS])) {
793
			// This meeting request item is recurring, so find all occurrences and saves them all as exceptions to this meeting request item.
794
			if (isset($messageprops[$this->proptags['recurring']]) && $messageprops[$this->proptags['recurring']] == true && $basedate == false) {
795
				$calendarItem = false;
796
797
				// Find main recurring item based on GlobalID (0x3)
798
				$items = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder);
799
				if (is_array($items)) {
800
					foreach ($items as $entryid) {
801
						$calendarItem = mapi_msgstore_openentry($store, $entryid);
802
					}
803
				}
804
805
				$processed = false;
806
				if (!$calendarItem) {
807
					// Recurring item not found, so create new meeting in Calendar
808
					$calendarItem = mapi_folder_createmessage($calFolder);
0 ignored issues
show
$calFolder of type resource is incompatible with the type resource expected by parameter $fld of mapi_folder_createmessage(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

808
					$calendarItem = mapi_folder_createmessage(/** @scrutinizer ignore-type */ $calFolder);
Loading history...
809
				}
810
				else {
811
					// we have found the main recurring item, check if this meeting request is already processed
812
					if (isset($messageprops[PR_PROCESSED]) && $messageprops[PR_PROCESSED] == true) {
813
						// only set required properties, other properties are already copied when processing this meeting request
814
						// for the first time
815
						$processed = true;
816
					}
817
					// While we applying updates of MR then all local categories will be removed,
818
					// So get the local categories of all occurrence before applying update from organiser.
819
					$localCategories = $this->getLocalCategories($calendarItem, $store, $calFolder);
820
				}
821
822
				if (!$processed) {
823
					// get all the properties and copy that to calendar item
824
					$props = mapi_getprops($this->message);
825
					// reset the PidLidMeetingType to Unspecified for outlook display the item
826
					$props[$this->proptags['meetingtype']] = mtgEmpty;
827
					/*
828
					 * the client which has sent this meeting request can generate wrong flagdueby
829
					 * time (mainly OL), so regenerate that property so we will always show reminder
830
					 * on right time
831
					 */
832
					if (isset($props[$this->proptags['reminderminutes']])) {
833
						$props[$this->proptags['flagdueby']] = $props[$this->proptags['startdate']] - ($props[$this->proptags['reminderminutes']] * 60);
834
					}
835
				}
836
				else {
837
					// only get required properties so we will not overwrite existing updated properties from calendar
838
					$props = mapi_getprops($this->message, [PR_ENTRYID]);
839
				}
840
841
				$props[PR_MESSAGE_CLASS] = 'IPM.Appointment';
842
				// When meeting requests are generated by third-party solutions, we might be missing the updatecounter property.
843
				if (!isset($props[$this->proptags['updatecounter']])) {
844
					$props[$this->proptags['updatecounter']] = 0;
845
				}
846
				$props[$this->proptags['meetingstatus']] = olMeetingReceived;
847
				// when we are automatically processing the meeting request set responsestatus to olResponseNotResponded
848
				$props[$this->proptags['responsestatus']] = $userAction ? ($tentative ? olResponseTentative : olResponseAccepted) : olResponseNotResponded;
849
850
				if (isset($props[$this->proptags['intendedbusystatus']])) {
851
					if ($tentative && $props[$this->proptags['intendedbusystatus']] !== fbFree) {
852
						$props[$this->proptags['busystatus']] = fbTentative;
853
					}
854
					else {
855
						$props[$this->proptags['busystatus']] = $props[$this->proptags['intendedbusystatus']];
856
					}
857
					// we already have intendedbusystatus value in $props so no need to copy it
858
				}
859
				else {
860
					$props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
861
				}
862
863
				if ($userAction) {
864
					$addrInfo = $this->getOwnerAddress($this->store);
865
866
					// if user has responded then set replytime and name
867
					$props[$this->proptags['replytime']] = time();
868
					if (!empty($addrInfo)) {
869
						// @FIXME conditionally set this property only for delegation case
870
						$props[$this->proptags['apptreplyname']] = $addrInfo[0];
871
					}
872
				}
873
874
				mapi_setprops($calendarItem, $props);
875
876
				// we have already processed attachments and recipients, so no need to do it again
877
				if (!$processed) {
878
					// Copy attachments too
879
					$this->replaceAttachments($this->message, $calendarItem);
880
					// Copy recipients too
881
					$this->replaceRecipients($this->message, $calendarItem, $isDelegate);
882
				}
883
884
				// Find all occurrences based on CleanGlobalID (0x23)
885
				// there will be no exceptions left if $processed is true, but even if it doesn't hurt to recheck
886
				$items = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder, true);
887
				if (is_array($items)) {
888
					// Save all existing occurrence as exceptions
889
					foreach ($items as $entryid) {
890
						// Open occurrence
891
						$occurrenceItem = mapi_msgstore_openentry($store, $entryid);
892
893
						// Save occurrence into main recurring item as exception
894
						if ($occurrenceItem) {
895
							$occurrenceItemProps = mapi_getprops($occurrenceItem, [$this->proptags['goid'], $this->proptags['recurring']]);
896
897
							// Find basedate of occurrence item
898
							$basedate = $this->getBasedateFromGlobalID($occurrenceItemProps[$this->proptags['goid']]);
899
							if ($basedate && $occurrenceItemProps[$this->proptags['recurring']] != true) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $basedate of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
900
								$this->mergeException($calendarItem, $occurrenceItem, $basedate, $store);
0 ignored issues
show
$calendarItem of type resource is incompatible with the type resource expected by parameter $recurringItem of Meetingrequest::mergeException(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

900
								$this->mergeException(/** @scrutinizer ignore-type */ $calendarItem, $occurrenceItem, $basedate, $store);
Loading history...
$occurrenceItem of type resource is incompatible with the type resource expected by parameter $occurrenceItem of Meetingrequest::mergeException(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

900
								$this->mergeException($calendarItem, /** @scrutinizer ignore-type */ $occurrenceItem, $basedate, $store);
Loading history...
901
							}
902
						}
903
					}
904
				}
905
906
				if (!isset($props[$this->proptags["recurring_pattern"]])) {
907
					$recurr = new Recurrence($store, $calendarItem);
908
					$recurr->saveRecurrencePattern();
909
				}
910
911
				mapi_savechanges($calendarItem);
912
913
				// After applying update of organiser all local categories of occurrence was removed,
914
				// So if local categories exist then apply it on respective occurrence.
915
				if (!empty($localCategories)) {
916
					$this->applyLocalCategories($calendarItem, $store, $localCategories);
917
				}
918
919
				if ($move) {
920
					// open wastebasket of currently logged in user and move the meeting request to it
921
					// for delegates this will be delegate's wastebasket folder
922
					$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
923
					$sourcefolder = $this->openParentFolder();
924
					mapi_folder_copymessages($sourcefolder, [$props[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
0 ignored issues
show
$wastebasket of type resource is incompatible with the type resource expected by parameter $dstfld of mapi_folder_copymessages(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

924
					mapi_folder_copymessages($sourcefolder, [$props[PR_ENTRYID]], /** @scrutinizer ignore-type */ $wastebasket, MESSAGE_MOVE);
Loading history...
925
				}
926
927
				$entryid = $props[PR_ENTRYID];
928
			}
929
			else {
930
				/**
931
				 * This meeting request is not recurring, so can be an exception or normal meeting.
932
				 * If exception then find main recurring item and update exception
933
				 * If main recurring item is not found then put exception into Calendar as normal meeting.
934
				 */
935
				$calendarItem = false;
936
937
				// We found basedate in GlobalID of this meeting request, so this meeting request if for an occurrence.
938
				if ($basedate) {
939
					// Find main recurring item from CleanGlobalID of this meeting request
940
					$items = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder);
941
					if (is_array($items)) {
942
						foreach ($items as $entryid) {
943
							$calendarItem = mapi_msgstore_openentry($store, $entryid);
944
						}
945
					}
946
947
					// Main recurring item is found, so now update exception
948
					if ($calendarItem) {
949
						$this->acceptException($calendarItem, $this->message, $basedate, $move, $tentative, $userAction, $store, $isDelegate);
0 ignored issues
show
$calendarItem of type resource is incompatible with the type resource expected by parameter $recurringItem of Meetingrequest::acceptException(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

949
						$this->acceptException(/** @scrutinizer ignore-type */ $calendarItem, $this->message, $basedate, $move, $tentative, $userAction, $store, $isDelegate);
Loading history...
950
						$calendarItemProps = mapi_getprops($calendarItem, [PR_ENTRYID]);
951
						$entryid = $calendarItemProps[PR_ENTRYID];
952
					}
953
				}
954
955
				if (!$calendarItem) {
956
					$items = $this->findCalendarItems($messageprops[$this->proptags['goid']], $calFolder);
957
					if (is_array($items)) {
958
						// Get local categories before deleting MR.
959
						$message = mapi_msgstore_openentry($store, $items[0]);
960
						$localCategories = mapi_getprops($message, [$this->proptags['categories']]);
961
						mapi_folder_deletemessages($calFolder, $items);
0 ignored issues
show
$calFolder of type resource is incompatible with the type resource expected by parameter $fld of mapi_folder_deletemessages(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

961
						mapi_folder_deletemessages(/** @scrutinizer ignore-type */ $calFolder, $items);
Loading history...
962
					}
963
964
					if ($move) {
965
						// All we have to do is open the default calendar,
966
						// set the message class correctly to be an appointment item
967
						// and move it to the calendar folder
968
						$sourcefolder = $this->openParentFolder();
969
970
						// create a new calendar message, and copy the message to there,
971
						// since we want to delete (move to wastebasket) the original message
972
						$old_entryid = mapi_getprops($this->message, [PR_ENTRYID]);
973
						$calmsg = mapi_folder_createmessage($calFolder);
974
						mapi_copyto($this->message, [], [], $calmsg); /* includes attachments and recipients */
975
						// reset the PidLidMeetingType to Unspecified for outlook display the item
976
						$tmp_props = [];
977
						$tmp_props[$this->proptags['meetingtype']] = mtgEmpty;
978
						// OL needs this field always being set, or it will not display item
979
						$tmp_props[$this->proptags['recurring']] = false;
980
						mapi_setprops($calmsg, $tmp_props);
981
982
						// After creating new MR, If local categories exist then apply it on new MR.
983
						if (!empty($localCategories)) {
984
							mapi_setprops($calmsg, $localCategories);
985
						}
986
987
						$calItemProps = [];
988
						$calItemProps[PR_MESSAGE_CLASS] = 'IPM.Appointment';
989
990
						/*
991
						 * the client which has sent this meeting request can generate wrong flagdueby
992
						 * time (mainly OL), so regenerate that property so we will always show reminder
993
						 * on right time
994
						 */
995
						if (isset($messageprops[$this->proptags['reminderminutes']])) {
996
							$calItemProps[$this->proptags['flagdueby']] = $messageprops[$this->proptags['startdate']] - ($messageprops[$this->proptags['reminderminutes']] * 60);
997
						}
998
999
						if (isset($messageprops[$this->proptags['intendedbusystatus']])) {
1000
							if ($tentative && $messageprops[$this->proptags['intendedbusystatus']] !== fbFree) {
1001
								$calItemProps[$this->proptags['busystatus']] = fbTentative;
1002
							}
1003
							else {
1004
								$calItemProps[$this->proptags['busystatus']] = $messageprops[$this->proptags['intendedbusystatus']];
1005
							}
1006
							$calItemProps[$this->proptags['intendedbusystatus']] = $messageprops[$this->proptags['intendedbusystatus']];
1007
						}
1008
						else {
1009
							$calItemProps[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
1010
						}
1011
1012
						// when we are automatically processing the meeting request set responsestatus to olResponseNotResponded
1013
						$calItemProps[$this->proptags['responsestatus']] = $userAction ? ($tentative ? olResponseTentative : olResponseAccepted) : olResponseNotResponded;
1014
						if ($userAction) {
1015
							$addrInfo = $this->getOwnerAddress($this->store);
1016
1017
							// if user has responded then set replytime and name
1018
							$calItemProps[$this->proptags['replytime']] = time();
1019
							if (!empty($addrInfo)) {
1020
								$calItemProps[$this->proptags['apptreplyname']] = $addrInfo[0];
1021
							}
1022
						}
1023
1024
						$calItemProps[$this->proptags['recurring_pattern']] = '';
1025
						$calItemProps[$this->proptags['alldayevent']] = $messageprops[$this->proptags['alldayevent']] ?? false;
1026
						$calItemProps[$this->proptags['private']] = $messageprops[$this->proptags['private']] ?? false;
1027
						$calItemProps[$this->proptags['meetingstatus']] = $messageprops[$this->proptags['meetingstatus']] ?? olMeetingReceived;
1028
						if (isset($messageprops[$this->proptags['startdate']])) {
1029
							$calItemProps[$this->proptags['commonstart']] = $calItemProps[$this->proptags['startdate']] = $messageprops[$this->proptags['startdate']];
1030
						}
1031
						if (isset($messageprops[$this->proptags['duedate']])) {
1032
							$calItemProps[$this->proptags['commonend']] = $calItemProps[$this->proptags['duedate']] = $messageprops[$this->proptags['duedate']];
1033
						}
1034
1035
						mapi_setprops($calmsg, $proposeNewTimeProps + $calItemProps);
1036
1037
						// get properties which stores owner information in meeting request mails
1038
						$props = mapi_getprops($calmsg, [
1039
							PR_SENT_REPRESENTING_ENTRYID,
1040
							PR_SENT_REPRESENTING_NAME,
1041
							PR_SENT_REPRESENTING_EMAIL_ADDRESS,
1042
							PR_SENT_REPRESENTING_ADDRTYPE,
1043
							PR_SENT_REPRESENTING_SEARCH_KEY,
1044
							PR_SENT_REPRESENTING_SMTP_ADDRESS,
1045
						]);
1046
1047
						// add owner to recipient table
1048
						$recips = [];
1049
						$this->addOrganizer($props, $recips);
1050
						mapi_message_modifyrecipients($calmsg, MODRECIP_ADD, $recips);
1051
						mapi_savechanges($calmsg);
1052
1053
						// Move the message to the wastebasket
1054
						$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
1055
						mapi_folder_copymessages($sourcefolder, [$old_entryid[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
1056
1057
						$messageprops = mapi_getprops($calmsg, [PR_ENTRYID]);
1058
						$entryid = $messageprops[PR_ENTRYID];
1059
					}
1060
					else {
1061
						// Create a new appointment with duplicate properties and recipient, but as an IPM.Appointment
1062
						$new = mapi_folder_createmessage($calFolder);
1063
						$props = mapi_getprops($this->message);
1064
1065
						$props[$this->proptags['recurring_pattern']] = '';
1066
						$props[$this->proptags['alldayevent']] = $props[$this->proptags['alldayevent']] ?? false;
1067
						$props[$this->proptags['private']] = $props[$this->proptags['private']] ?? false;
1068
						$props[$this->proptags['meetingstatus']] = $props[$this->proptags['meetingstatus']] ?? olMeetingReceived;
1069
						if (isset($props[$this->proptags['startdate']])) {
1070
							$props[$this->proptags['commonstart']] = $props[$this->proptags['startdate']];
1071
						}
1072
						if (isset($props[$this->proptags['duedate']])) {
1073
							$props[$this->proptags['commonend']] = $props[$this->proptags['duedate']];
1074
						}
1075
1076
						$props[PR_MESSAGE_CLASS] = 'IPM.Appointment';
1077
						// reset the PidLidMeetingType to Unspecified for outlook display the item
1078
						$props[$this->proptags['meetingtype']] = mtgEmpty;
1079
						// OL needs this field always being set, or it will not display item
1080
						$props[$this->proptags['recurring']] = false;
1081
1082
						// After creating new MR, If local categories exist then apply it on new MR.
1083
						if (!empty($localCategories)) {
1084
							mapi_setprops($new, $localCategories);
1085
						}
1086
1087
						/*
1088
						 * the client which has sent this meeting request can generate wrong flagdueby
1089
						 * time (mainly OL), so regenerate that property so we will always show reminder
1090
						 * on right time
1091
						 */
1092
						if (isset($props[$this->proptags['reminderminutes']])) {
1093
							$props[$this->proptags['flagdueby']] = $props[$this->proptags['startdate']] - ($props[$this->proptags['reminderminutes']] * 60);
1094
						}
1095
1096
						// When meeting requests are generated by third-party solutions, we might be missing the updatecounter property.
1097
						if (!isset($props[$this->proptags['updatecounter']])) {
1098
							$props[$this->proptags['updatecounter']] = 0;
1099
						}
1100
						// when we are automatically processing the meeting request set responsestatus to olResponseNotResponded
1101
						$props[$this->proptags['responsestatus']] = $userAction ? ($tentative ? olResponseTentative : olResponseAccepted) : olResponseNotResponded;
1102
1103
						if (isset($props[$this->proptags['intendedbusystatus']])) {
1104
							if ($tentative && $props[$this->proptags['intendedbusystatus']] !== fbFree) {
1105
								$props[$this->proptags['busystatus']] = fbTentative;
1106
							}
1107
							else {
1108
								$props[$this->proptags['busystatus']] = $props[$this->proptags['intendedbusystatus']];
1109
							}
1110
							// we already have intendedbusystatus value in $props so no need to copy it
1111
						}
1112
						else {
1113
							$props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
1114
						}
1115
1116
						if ($userAction) {
1117
							$addrInfo = $this->getOwnerAddress($this->store);
1118
1119
							// if user has responded then set replytime and name
1120
							$props[$this->proptags['replytime']] = time();
1121
							if (!empty($addrInfo)) {
1122
								$props[$this->proptags['apptreplyname']] = $addrInfo[0];
1123
							}
1124
						}
1125
1126
						mapi_setprops($new, $proposeNewTimeProps + $props);
1127
1128
						$reciptable = mapi_message_getrecipienttable($this->message);
1129
1130
						$recips = [];
1131
						// If delegate, then do not add the delegate in recipients
1132
						if ($isDelegate) {
1133
							$delegate = mapi_getprops($this->message, [PR_RECEIVED_BY_EMAIL_ADDRESS]);
1134
							$res = [
1135
								RES_PROPERTY,
1136
								[
1137
									RELOP => RELOP_NE,
1138
									ULPROPTAG => PR_EMAIL_ADDRESS,
1139
									VALUE => [PR_EMAIL_ADDRESS => $delegate[PR_RECEIVED_BY_EMAIL_ADDRESS]],
1140
								],
1141
							];
1142
							$recips = mapi_table_queryallrows($reciptable, $this->recipprops, $res);
1143
						}
1144
						else {
1145
							$recips = mapi_table_queryallrows($reciptable, $this->recipprops);
1146
						}
1147
1148
						$this->addOrganizer($props, $recips);
1149
						mapi_message_modifyrecipients($new, MODRECIP_ADD, $recips);
1150
						mapi_savechanges($new);
1151
1152
						$props = mapi_getprops($new, [PR_ENTRYID]);
1153
						$entryid = $props[PR_ENTRYID];
1154
					}
1155
				}
1156
			}
1157
		}
1158
		else {
1159
			// Here only properties are set on calendaritem, because user is responding from calendar.
1160
			$props = [];
1161
			$props[$this->proptags['responsestatus']] = $tentative ? olResponseTentative : olResponseAccepted;
1162
1163
			if (isset($messageprops[$this->proptags['intendedbusystatus']])) {
1164
				if ($tentative && $messageprops[$this->proptags['intendedbusystatus']] !== fbFree) {
1165
					$props[$this->proptags['busystatus']] = fbTentative;
1166
				}
1167
				else {
1168
					$props[$this->proptags['busystatus']] = $messageprops[$this->proptags['intendedbusystatus']];
1169
				}
1170
				$props[$this->proptags['intendedbusystatus']] = $messageprops[$this->proptags['intendedbusystatus']];
1171
			}
1172
			else {
1173
				$props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
1174
			}
1175
1176
			$props[$this->proptags['meetingstatus']] = olMeetingReceived;
1177
1178
			$addrInfo = $this->getOwnerAddress($this->store);
1179
1180
			// if user has responded then set replytime and name
1181
			$props[$this->proptags['replytime']] = time();
1182
			if (!empty($addrInfo)) {
1183
				$props[$this->proptags['apptreplyname']] = $addrInfo[0];
1184
			}
1185
1186
			if ($basedate) {
1187
				$recurr = new Recurrence($store, $this->message);
1188
1189
				// Copy recipients list
1190
				$reciptable = mapi_message_getrecipienttable($this->message);
1191
				$recips = mapi_table_queryallrows($reciptable, $this->recipprops);
1192
1193
				if ($recurr->isException($basedate)) {
1194
					$recurr->modifyException($proposeNewTimeProps + $props, $basedate, $recips);
1195
				}
1196
				else {
1197
					$props[$this->proptags['startdate']] = $recurr->getOccurrenceStart($basedate);
1198
					$props[$this->proptags['duedate']] = $recurr->getOccurrenceEnd($basedate);
1199
1200
					$props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
1201
					$props[PR_SENT_REPRESENTING_NAME] = $messageprops[PR_SENT_REPRESENTING_NAME];
1202
					$props[PR_SENT_REPRESENTING_ADDRTYPE] = $messageprops[PR_SENT_REPRESENTING_ADDRTYPE];
1203
					$props[PR_SENT_REPRESENTING_ENTRYID] = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
1204
					$props[PR_SENT_REPRESENTING_SEARCH_KEY] = $messageprops[PR_SENT_REPRESENTING_SEARCH_KEY];
1205
1206
					$recurr->createException($proposeNewTimeProps + $props, $basedate, false, $recips);
1207
				}
1208
			}
1209
			else {
1210
				mapi_setprops($this->message, $proposeNewTimeProps + $props);
1211
			}
1212
			mapi_savechanges($this->message);
1213
1214
			$entryid = $messageprops[PR_ENTRYID];
1215
		}
1216
1217
		return $entryid;
1218
	}
1219
1220
	/**
1221
	 * Declines the meeting request by moving the item to the deleted
1222
	 * items folder and sending a decline message. After declining, you
1223
	 * can't use this class instance any more. The message is closed.
1224
	 * When an occurrence is decline then false is returned because that
1225
	 * occurrence is deleted not the recurring item.
1226
	 *
1227
	 * @param bool  $sendresponse true if a response has to be sent to organizer
1228
	 * @param mixed $basedate     if specified contains starttime of day of an occurrence
1229
	 * @param mixed $body
1230
	 *
1231
	 * @return bool true if item is deleted from Calendar else false
1232
	 */
1233
	public function doDecline($sendresponse, $basedate = false, $body = false) {
1234
		if ($this->isLocalOrganiser()) {
1235
			return false;
1236
		}
1237
1238
		$result = false;
1239
		$calendaritem = false;
1240
1241
		// Remove any previous calendar items with this goid and appt id
1242
		$messageprops = mapi_getprops($this->message, [$this->proptags['goid'], $this->proptags['goid2'], PR_RCVD_REPRESENTING_ENTRYID]);
1243
1244
		$store = $this->store;
1245
		$calFolder = $this->openDefaultCalendar();
1246
		// If this meeting request is received by a delegate then open delegator's store.
1247
		if (isset($messageprops[PR_RCVD_REPRESENTING_ENTRYID])) {
1248
			$delegatorStore = $this->getDelegatorStore($messageprops[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
1249
			if (!empty($delegatorStore['store'])) {
1250
				$store = $delegatorStore['store'];
1251
			}
1252
			if (!empty($delegatorStore[PR_IPM_APPOINTMENT_ENTRYID])) {
1253
				$calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
1254
			}
1255
		}
1256
1257
		// check for calendar access before deleting the calendar item
1258
		if ($this->checkCalendarWriteAccess($store) !== true) {
1259
			// Throw an exception that we don't have write permissions on calendar folder,
1260
			// allow caller to fill the error message
1261
			throw new MAPIException(null, MAPI_E_NO_ACCESS);
1262
		}
1263
1264
		$goid = $messageprops[$this->proptags['goid']];
1265
1266
		// First, find the items in the calendar by GlobalObjid (0x3)
1267
		$entryids = $this->findCalendarItems($goid, $calFolder);
1268
1269
		if (!$basedate) {
1270
			$basedate = $this->getBasedateFromGlobalID($goid);
1271
		}
1272
1273
		if ($sendresponse) {
1274
			$this->createResponse(olResponseDeclined, [], $body, $store, $basedate, $calFolder);
1275
		}
1276
1277
		if ($basedate) {
1278
			// use CleanGlobalObjid (0x23)
1279
			$calendaritems = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder);
1280
1281
			if (is_array($calendaritems)) {
1282
				foreach ($calendaritems as $entryid) {
1283
					// Open each calendar item and set the properties of the cancellation object
1284
					$calendaritem = mapi_msgstore_openentry($store, $entryid);
0 ignored issues
show
It seems like $store can also be of type resource; however, parameter $store of mapi_msgstore_openentry() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1284
					$calendaritem = mapi_msgstore_openentry(/** @scrutinizer ignore-type */ $store, $entryid);
Loading history...
1285
1286
					// Recurring item is found, now delete exception
1287
					if ($calendaritem) {
1288
						$this->doRemoveExceptionFromCalendar($basedate, $calendaritem, $store);
1289
						$result = true;
1290
					}
1291
				}
1292
			}
1293
1294
			if ($this->isMeetingRequest()) {
1295
				$calendaritem = false;
1296
			}
1297
		}
1298
1299
		if (!$calendaritem) {
1300
			$calendar = $this->openDefaultCalendar($store);
1301
1302
			if (!empty($entryids)) {
1303
				mapi_folder_deletemessages($calendar, $entryids);
0 ignored issues
show
$calendar of type resource is incompatible with the type resource expected by parameter $fld of mapi_folder_deletemessages(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1303
				mapi_folder_deletemessages(/** @scrutinizer ignore-type */ $calendar, $entryids);
Loading history...
1304
			}
1305
1306
			// All we have to do to decline, is to move the item to the waste basket
1307
			$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
1308
			$sourcefolder = $this->openParentFolder();
1309
1310
			$messageprops = mapi_getprops($this->message, [PR_ENTRYID]);
1311
1312
			// Release the message
1313
			$this->message = null;
1314
1315
			// Move the message to the waste basket
1316
			mapi_folder_copymessages($sourcefolder, [$messageprops[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
0 ignored issues
show
$wastebasket of type resource is incompatible with the type resource expected by parameter $dstfld of mapi_folder_copymessages(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1316
			mapi_folder_copymessages($sourcefolder, [$messageprops[PR_ENTRYID]], /** @scrutinizer ignore-type */ $wastebasket, MESSAGE_MOVE);
Loading history...
1317
1318
			$result = true;
1319
		}
1320
1321
		return $result;
1322
	}
1323
1324
	/**
1325
	 * Removes a meeting request from the calendar when the user presses the
1326
	 * 'remove from calendar' button in response to a meeting cancellation.
1327
	 *
1328
	 * @param mixed $basedate if specified contains starttime of day of an occurrence
1329
	 *
1330
	 * @return null|false
1331
	 */
1332
	public function doRemoveFromCalendar($basedate) {
1333
		if ($this->isLocalOrganiser()) {
1334
			return false;
1335
		}
1336
1337
		$messageprops = mapi_getprops($this->message, [PR_ENTRYID, $this->proptags['goid'], PR_RCVD_REPRESENTING_ENTRYID, PR_MESSAGE_CLASS]);
1338
1339
		$goid = $messageprops[$this->proptags['goid']];
1340
1341
		$store = $this->store;
1342
		$calFolder = $this->openDefaultCalendar();
1343
		if (isset($messageprops[PR_RCVD_REPRESENTING_ENTRYID])) {
1344
			$delegatorStore = $this->getDelegatorStore($messageprops[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
1345
			if (!empty($delegatorStore['store'])) {
1346
				$store = $delegatorStore['store'];
1347
			}
1348
			if (!empty($delegatorStore[PR_IPM_APPOINTMENT_ENTRYID])) {
1349
				$calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
1350
			}
1351
		}
1352
1353
		// check for calendar access before deleting the calendar item
1354
		if ($this->checkCalendarWriteAccess($store) !== true) {
1355
			// Throw an exception that we don't have write permissions on calendar folder,
1356
			// allow caller to fill the error message
1357
			throw new MAPIException(null, MAPI_E_NO_ACCESS);
1358
		}
1359
1360
		$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
1361
		// get the source folder of the meeting message
1362
		$sourcefolder = $this->openParentFolder();
1363
1364
		// Check if the message is a meeting request in the inbox or a calendaritem by checking the message class
1365
		if ($this->isMeetingCancellation($messageprops[PR_MESSAGE_CLASS])) {
1366
			// get the basedate to check for exception
1367
			$basedate = $this->getBasedateFromGlobalID($goid);
1368
1369
			$calendarItem = $this->getCorrespondentCalendarItem(true);
1370
1371
			if ($calendarItem !== false) {
1372
				// basedate is provided so open exception
1373
				if ($basedate) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $basedate of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1374
					$exception = $this->getExceptionItem($calendarItem, $basedate);
1375
1376
					if ($exception !== false) {
0 ignored issues
show
The condition $exception !== false is always true.
Loading history...
1377
						// exception found, remove it from calendar
1378
						$this->doRemoveExceptionFromCalendar($basedate, $calendarItem, $store);
1379
					}
1380
				}
1381
				else {
1382
					// remove normal / recurring series from calendar
1383
					$entryids = mapi_getprops($calendarItem, [PR_ENTRYID]);
0 ignored issues
show
$calendarItem of type resource|true is incompatible with the type resource expected by parameter $any of mapi_getprops(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1383
					$entryids = mapi_getprops(/** @scrutinizer ignore-type */ $calendarItem, [PR_ENTRYID]);
Loading history...
1384
1385
					$entryids = [$entryids[PR_ENTRYID]];
1386
1387
					mapi_folder_copymessages($calFolder, $entryids, $wastebasket, MESSAGE_MOVE);
0 ignored issues
show
$wastebasket of type resource is incompatible with the type resource expected by parameter $dstfld of mapi_folder_copymessages(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1387
					mapi_folder_copymessages($calFolder, $entryids, /** @scrutinizer ignore-type */ $wastebasket, MESSAGE_MOVE);
Loading history...
1388
				}
1389
			}
1390
1391
			// Release the message, because we are going to move it to wastebasket
1392
			$this->message = null;
1393
1394
			// Move the cancellation mail to wastebasket
1395
			mapi_folder_copymessages($sourcefolder, [$messageprops[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
1396
		}
1397
		else {
1398
			// Here only properties are set on calendaritem, because user is responding from calendar.
1399
			if ($basedate) {
1400
				// remove the occurrence
1401
				$this->doRemoveExceptionFromCalendar($basedate, $this->message, $store);
1402
			}
1403
			else {
1404
				// remove normal/recurring meeting item.
1405
				// Move the message to the waste basket
1406
				mapi_folder_copymessages($sourcefolder, [$messageprops[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
1407
			}
1408
		}
1409
	}
1410
1411
	/**
1412
	 * Function can be used to cancel any existing meeting and send cancellation mails to attendees.
1413
	 * Should only be called from meeting object from calendar.
1414
	 *
1415
	 * @param mixed $basedate (optional) basedate of occurrence which should be cancelled
1416
	 *
1417
	 * @FIXME cancellation mail is also sent to attendee which has declined the meeting
1418
	 * @FIXME don't send canellation mail when cancelling meeting from past
1419
	 */
1420
	public function doCancelInvitation($basedate = false) {
1421
		if (!$this->isLocalOrganiser()) {
1422
			return;
1423
		}
1424
1425
		// check write access for delegate
1426
		if ($this->checkCalendarWriteAccess($this->store) !== true) {
1427
			// Throw an exception that we don't have write permissions on calendar folder,
1428
			// error message will be filled by module
1429
			throw new MAPIException(null, MAPI_E_NO_ACCESS);
1430
		}
1431
1432
		$messageProps = mapi_getprops($this->message, [PR_ENTRYID, $this->proptags['recurring']]);
1433
1434
		if (isset($messageProps[$this->proptags['recurring']]) && $messageProps[$this->proptags['recurring']] === true) {
1435
			// cancellation of recurring series or one occurrence
1436
			$recurrence = new Recurrence($this->store, $this->message);
1437
1438
			// if basedate is specified then we are cancelling only one occurrence, so create exception for that occurrence
1439
			if ($basedate) {
1440
				$recurrence->createException([], $basedate, true);
1441
			}
1442
1443
			// update the meeting request
1444
			$this->updateMeetingRequest();
1445
1446
			// send cancellation mails
1447
			$this->sendMeetingRequest(true, dgettext('zarafa', 'Canceled') . ': ', $basedate);
1448
1449
			// save changes in the message
1450
			mapi_savechanges($this->message);
1451
		}
1452
		else {
1453
			// cancellation of normal meeting request
1454
			// Send the cancellation
1455
			$this->updateMeetingRequest();
1456
			$this->sendMeetingRequest(true, dgettext('zarafa', 'Canceled') . ': ');
1457
1458
			// save changes in the message
1459
			mapi_savechanges($this->message);
1460
		}
1461
1462
		// if basedate is specified then we have already created exception of it so nothing should be done now
1463
		// but when cancelling normal / recurring meeting request we need to remove meeting from calendar
1464
		if ($basedate === false) {
1465
			// get the wastebasket folder, for delegate this will give wastebasket of delegate
1466
			$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
1467
1468
			// get the source folder of the meeting message
1469
			$sourcefolder = $this->openParentFolder();
1470
1471
			// Move the message to the deleted items
1472
			mapi_folder_copymessages($sourcefolder, [$messageProps[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
0 ignored issues
show
$wastebasket of type resource is incompatible with the type resource expected by parameter $dstfld of mapi_folder_copymessages(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1472
			mapi_folder_copymessages($sourcefolder, [$messageProps[PR_ENTRYID]], /** @scrutinizer ignore-type */ $wastebasket, MESSAGE_MOVE);
Loading history...
1473
		}
1474
	}
1475
1476
	/**
1477
	 * Convert epoch to MAPI FileTime, number of 100-nanosecond units since
1478
	 * the start of January 1, 1601.
1479
	 * https://msdn.microsoft.com/en-us/library/office/cc765906.aspx.
1480
	 *
1481
	 * @param int $epoch the current epoch
1482
	 *
1483
	 * @return int the MAPI FileTime equalevent to the given epoch time
1484
	 */
1485
	public function epochToMapiFileTime($epoch) {
1486
		$nanoseconds_between_epoch = 116444736000000000;
1487
1488
		return ($epoch * 10000000) + $nanoseconds_between_epoch;
1489
	}
1490
1491
	/**
1492
	 * Sets the properties in the message so that is can be sent
1493
	 * as a meeting request. The caller has to submit the message. This
1494
	 * is only used for new MeetingRequests. Pass the appointment item as $message
1495
	 * in the constructor to do this.
1496
	 *
1497
	 * @param mixed $basedate
1498
	 */
1499
	public function setMeetingRequest($basedate = false): void {
1500
		$props = mapi_getprops($this->message, [$this->proptags['updatecounter']]);
1501
1502
		// Create a new global id for this item
1503
		// https://msdn.microsoft.com/en-us/library/ee160198(v=exchg.80).aspx
1504
		$goid = pack('H*', '040000008200E00074C5B7101A82E00800000000');
1505
		/*
1506
		$year = gmdate('Y');
1507
		$month = gmdate('n');
1508
		$day = gmdate('j');
1509
		$goid .= pack('n', $year);
1510
		$goid .= pack('C', $month);
1511
		$goid .= pack('C', $day);
1512
		*/
1513
		// Creation Time
1514
		$time = $this->epochToMapiFileTime(time());
1515
		$goid .= pack('V', $time & 0xFFFFFFFF);
1516
		$goid .= pack('V', $time >> 32);
1517
		// 8 Zeros
1518
		$goid .= pack('H*', '0000000000000000');
1519
		// Length of the random data
1520
		$goid .= pack('V', 16);
1521
		// Random data.
1522
		for ($i = 0; $i < 16; ++$i) {
1523
			$goid .= chr(rand(0, 255));
1524
		}
1525
1526
		// Create a new appointment id for this item
1527
		$apptid = rand();
1528
1529
		$props[PR_OWNER_APPT_ID] = $apptid;
1530
		$props[PR_ICON_INDEX] = 1026;
1531
		$props[$this->proptags['goid']] = $goid;
1532
		$props[$this->proptags['goid2']] = $goid;
1533
1534
		if (!isset($props[$this->proptags['updatecounter']])) {
1535
			$props[$this->proptags['updatecounter']] = 0;			// OL also starts sequence no with zero.
1536
			$props[$this->proptags['last_updatecounter']] = 0;
1537
		}
1538
1539
		mapi_setprops($this->message, $props);
1540
	}
1541
1542
	/**
1543
	 * Sends a meeting request by copying it to the outbox, converting
1544
	 * the message class, adding some properties that are required only
1545
	 * for sending the message and submitting the message. Set cancel to
1546
	 * true if you wish to completely cancel the meeting request. You can
1547
	 * specify an optional 'prefix' to prefix the sent message, which is normally
1548
	 * 'Canceled: '.
1549
	 *
1550
	 * @param mixed $cancel
1551
	 * @param mixed $prefix
1552
	 * @param mixed $basedate
1553
	 * @param mixed $modifiedRecips
1554
	 * @param mixed $deletedRecips
1555
	 *
1556
	 * @return (int|mixed)[]|true
1557
	 *
1558
	 * @psalm-return array{error: 1|3|4, displayname: mixed}|true
1559
	 */
1560
	public function sendMeetingRequest($cancel, $prefix = false, $basedate = false, $modifiedRecips = false, $deletedRecips = false) {
1561
		$this->includesResources = false;
1562
		$this->nonAcceptingResources = [];
1563
1564
		// Get the properties of the message
1565
		$messageprops = mapi_getprops($this->message, [$this->proptags['recurring']]);
1566
1567
		/*
1568
		 * Submit message to non-resource recipients
1569
		 */
1570
		// Set BusyStatus to olTentative (1)
1571
		// Set MeetingStatus to olMeetingReceived
1572
		// Set ResponseStatus to olResponseNotResponded
1573
1574
		/*
1575
		 * While sending recurrence meeting exceptions are not sent as attachments
1576
		 * because first all exceptions are sent and then recurrence meeting is sent.
1577
		 */
1578
		if (isset($messageprops[$this->proptags['recurring']]) && $messageprops[$this->proptags['recurring']] && !$basedate) {
1579
			// Book resource
1580
			$this->bookResources($this->message, $cancel, $prefix);
1581
1582
			if (!$this->errorSetResource) {
1583
				$recurr = new Recurrence($this->openDefaultStore(), $this->message);
0 ignored issues
show
$this->openDefaultStore() of type false|resource is incompatible with the type resource expected by parameter $store of Recurrence::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1583
				$recurr = new Recurrence(/** @scrutinizer ignore-type */ $this->openDefaultStore(), $this->message);
Loading history...
1584
1585
				// First send meetingrequest for recurring item
1586
				$this->submitMeetingRequest($this->message, $cancel, $prefix, false, $recurr, false, $modifiedRecips, $deletedRecips);
1587
1588
				// Then send all meeting request for all exceptions
1589
				$exceptions = $recurr->getAllExceptions();
1590
				if ($exceptions) {
1591
					foreach ($exceptions as $exceptionBasedate) {
1592
						$attach = $recurr->getExceptionAttachment($exceptionBasedate);
1593
1594
						if ($attach) {
1595
							$occurrenceItem = mapi_attach_openobj($attach, MAPI_MODIFY);
1596
							$this->submitMeetingRequest($occurrenceItem, $cancel, false, $exceptionBasedate, $recurr, false, $modifiedRecips, $deletedRecips);
0 ignored issues
show
$occurrenceItem of type resource is incompatible with the type resource expected by parameter $message of Meetingrequest::submitMeetingRequest(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1596
							$this->submitMeetingRequest(/** @scrutinizer ignore-type */ $occurrenceItem, $cancel, false, $exceptionBasedate, $recurr, false, $modifiedRecips, $deletedRecips);
Loading history...
1597
							mapi_savechanges($attach);
1598
						}
1599
					}
1600
				}
1601
			}
1602
		}
1603
		else {
1604
			// Basedate found, an exception is to be sent
1605
			if ($basedate) {
1606
				$recurr = new Recurrence($this->openDefaultStore(), $this->message);
1607
1608
				if ($cancel) {
1609
					// @TODO: remove occurrence from Resource's Calendar if resource was booked for whole series
1610
					$this->submitMeetingRequest($this->message, $cancel, $prefix, $basedate, $recurr, false);
1611
				}
1612
				else {
1613
					$attach = $recurr->getExceptionAttachment($basedate);
1614
1615
					if ($attach) {
1616
						$occurrenceItem = mapi_attach_openobj($attach, MAPI_MODIFY);
1617
1618
						// Book resource for this occurrence
1619
						$resourceRecipData = $this->bookResources($occurrenceItem, $cancel, $prefix, $basedate);
0 ignored issues
show
The assignment to $resourceRecipData is dead and can be removed.
Loading history...
$occurrenceItem of type resource is incompatible with the type resource expected by parameter $message of Meetingrequest::bookResources(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1619
						$resourceRecipData = $this->bookResources(/** @scrutinizer ignore-type */ $occurrenceItem, $cancel, $prefix, $basedate);
Loading history...
1620
1621
						if (!$this->errorSetResource) {
1622
							// Save all previous changes
1623
							mapi_savechanges($this->message);
1624
1625
							$this->submitMeetingRequest($occurrenceItem, $cancel, $prefix, $basedate, $recurr, true, $modifiedRecips, $deletedRecips);
1626
							mapi_savechanges($occurrenceItem);
1627
							mapi_savechanges($attach);
1628
						}
1629
					}
1630
				}
1631
			}
1632
			else {
1633
				// This is normal meeting
1634
				$resourceRecipData = $this->bookResources($this->message, $cancel, $prefix);
1635
1636
				if (!$this->errorSetResource) {
1637
					$this->submitMeetingRequest($this->message, $cancel, $prefix, false, false, false, $modifiedRecips, $deletedRecips);
1638
				}
1639
			}
1640
		}
1641
1642
		if (isset($this->errorSetResource) && $this->errorSetResource) {
1643
			return [
1644
				'error' => $this->errorSetResource,
1645
				'displayname' => $this->recipientDisplayname,
1646
			];
1647
		}
1648
1649
		return true;
1650
	}
1651
1652
	/**
1653
	 * Updates the message after an update has been performed (for example,
1654
	 * changing the time of the meeting). This must be called before re-sending
1655
	 * the meeting request. You can also call this function instead of 'setMeetingRequest()'
1656
	 * as it will automatically call setMeetingRequest on this object if it is the first
1657
	 * call to this function.
1658
	 *
1659
	 * @param mixed $basedate
1660
	 */
1661
	public function updateMeetingRequest($basedate = false): void {
1662
		$messageprops = mapi_getprops($this->message, [$this->proptags['last_updatecounter'], $this->proptags['goid']]);
1663
1664
		if (!isset($messageprops[$this->proptags['goid']])) {
1665
			$this->setMeetingRequest($basedate);
1666
		}
1667
		else {
1668
			$counter = (isset($messageprops[$this->proptags['last_updatecounter']]) ?? 0) + 1;
1669
1670
			// increment value of last_updatecounter, last_updatecounter will be common for recurring series
1671
			// so even if you sending an exception only you need to update the last_updatecounter in the recurring series message
1672
			// this way we can make sure that every time we will be using a uniwue number for every operation
1673
			mapi_setprops($this->message, [$this->proptags['last_updatecounter'] => $counter]);
1674
		}
1675
	}
1676
1677
	/**
1678
	 * Returns TRUE if we are the organiser of the meeting. Can be used with any type of meeting object.
1679
	 */
1680
	public function isLocalOrganiser(): bool {
1681
		$props = mapi_getprops($this->message, [$this->proptags['goid'], PR_MESSAGE_CLASS]);
1682
1683
		if (!$this->isMeetingRequest($props[PR_MESSAGE_CLASS]) && !$this->isMeetingRequestResponse($props[PR_MESSAGE_CLASS]) && !$this->isMeetingCancellation($props[PR_MESSAGE_CLASS])) {
1684
			// we are checking with calendar item
1685
			$calendarItem = $this->message;
1686
		}
1687
		else {
1688
			// we are checking with meeting request / response / cancellation mail
1689
			// get calendar items
1690
			$calendarItem = $this->getCorrespondentCalendarItem(true);
1691
		}
1692
1693
		// even if we have received request/response for exception/occurrence then also
1694
		// we can check recurring series for organizer, no need to check with exception/occurrence
1695
1696
		if ($calendarItem !== false) {
1697
			$messageProps = mapi_getprops($calendarItem, [$this->proptags['responsestatus']]);
0 ignored issues
show
It seems like $calendarItem can also be of type resource and true; however, parameter $any of mapi_getprops() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1697
			$messageProps = mapi_getprops(/** @scrutinizer ignore-type */ $calendarItem, [$this->proptags['responsestatus']]);
Loading history...
1698
1699
			if (isset($messageProps[$this->proptags['responsestatus']]) && $messageProps[$this->proptags['responsestatus']] === olResponseOrganized) {
1700
				return true;
1701
			}
1702
		}
1703
1704
		return false;
1705
	}
1706
1707
	/*
1708
	 * Support functions - INTERNAL ONLY
1709
	 ***************************************************************************************************
1710
	 */
1711
1712
	/**
1713
	 * Return the tracking status of a recipient based on the IPM class (passed).
1714
	 *
1715
	 * @param mixed $class
1716
	 */
1717
	public function getTrackStatus($class) {
1718
		$status = olRecipientTrackStatusNone;
1719
1720
		switch ($class) {
1721
			case 'IPM.Schedule.Meeting.Resp.Pos':
1722
				$status = olRecipientTrackStatusAccepted;
1723
				break;
1724
1725
			case 'IPM.Schedule.Meeting.Resp.Tent':
1726
				$status = olRecipientTrackStatusTentative;
1727
				break;
1728
1729
			case 'IPM.Schedule.Meeting.Resp.Neg':
1730
				$status = olRecipientTrackStatusDeclined;
1731
				break;
1732
		}
1733
1734
		return $status;
1735
	}
1736
1737
	/**
1738
	 * Function returns MAPIFolder resource of the folder that currently holds this meeting/meeting request
1739
	 * object.
1740
	 */
1741
	public function openParentFolder() {
1742
		$messageprops = mapi_getprops($this->message, [PR_PARENT_ENTRYID]);
1743
1744
		return mapi_msgstore_openentry($this->store, $messageprops[PR_PARENT_ENTRYID]);
1745
	}
1746
1747
	/**
1748
	 * Function will return resource of the default calendar folder of store.
1749
	 *
1750
	 * @param mixed $store {optional} user store whose default calendar should be opened
1751
	 *
1752
	 * @return resource default calendar folder of store
1753
	 */
1754
	public function openDefaultCalendar($store = false) {
1755
		return $this->openDefaultFolder(PR_IPM_APPOINTMENT_ENTRYID, $store);
1756
	}
1757
1758
	/**
1759
	 * Function will return resource of the default outbox folder of store.
1760
	 *
1761
	 * @param mixed $store {optional} user store whose default outbox should be opened
1762
	 *
1763
	 * @return resource default outbox folder of store
1764
	 */
1765
	public function openDefaultOutbox($store = false) {
1766
		return $this->openBaseFolder(PR_IPM_OUTBOX_ENTRYID, $store);
1767
	}
1768
1769
	/**
1770
	 * Function will return resource of the default wastebasket folder of store.
1771
	 *
1772
	 * @param mixed $store {optional} user store whose default wastebasket should be opened
1773
	 *
1774
	 * @return resource default wastebasket folder of store
1775
	 */
1776
	public function openDefaultWastebasket($store = false) {
1777
		return $this->openBaseFolder(PR_IPM_WASTEBASKET_ENTRYID, $store);
1778
	}
1779
1780
	/**
1781
	 * Function will return resource of the default calendar folder of store.
1782
	 *
1783
	 * @param mixed $store {optional} user store whose default calendar should be opened
1784
	 *
1785
	 * @return bool|string default calendar folder of store
1786
	 */
1787
	public function getDefaultWastebasketEntryID($store = false) {
1788
		return $this->getBaseEntryID(PR_IPM_WASTEBASKET_ENTRYID, $store);
1789
	}
1790
1791
	/**
1792
	 * Function will return resource of the default sent mail folder of store.
1793
	 *
1794
	 * @param mixed $store {optional} user store whose default sent mail should be opened
1795
	 *
1796
	 * @return bool|string default sent mail folder of store
1797
	 */
1798
	public function getDefaultSentmailEntryID($store = false) {
1799
		return $this->getBaseEntryID(PR_IPM_SENTMAIL_ENTRYID, $store);
1800
	}
1801
1802
	/**
1803
	 * Function will return entryid of any default folder of store. This method is useful when you want
1804
	 * to get entryid of folder which is stored as properties of inbox folder
1805
	 * (PR_IPM_APPOINTMENT_ENTRYID, PR_IPM_CONTACT_ENTRYID, PR_IPM_DRAFTS_ENTRYID, PR_IPM_JOURNAL_ENTRYID, PR_IPM_NOTE_ENTRYID, PR_IPM_TASK_ENTRYID).
1806
	 *
1807
	 * @param int   $prop  proptag of the folder for which we want to get entryid
1808
	 * @param mixed $store {optional} user store from which we need to get entryid of default folder
1809
	 *
1810
	 * @return bool|string entryid of folder pointed by $prop
1811
	 */
1812
	public function getDefaultFolderEntryID($prop, $store = false) {
1813
		try {
1814
			$inbox = mapi_msgstore_getreceivefolder($store ? $store : $this->store);
1815
			$inboxprops = mapi_getprops($inbox, [$prop]);
1816
			if (isset($inboxprops[$prop])) {
1817
				return $inboxprops[$prop];
1818
			}
1819
		}
1820
		catch (MAPIException $e) {
1821
			// public store doesn't support this method
1822
			if ($e->getCode() == MAPI_E_NO_SUPPORT) {
1823
				// don't propagate this error to parent handlers, if store doesn't support it
1824
				$e->setHandled();
1825
			}
1826
		}
1827
1828
		return false;
1829
	}
1830
1831
	/**
1832
	 * Function will return resource of any default folder of store.
1833
	 *
1834
	 * @param int   $prop  proptag of the folder that we want to open
1835
	 * @param mixed $store {optional} user store from which we need to open default folder
1836
	 *
1837
	 * @return resource default folder of store
1838
	 */
1839
	public function openDefaultFolder($prop, $store = false) {
1840
		$folder = false;
1841
		$entryid = $this->getDefaultFolderEntryID($prop, $store);
1842
1843
		if ($entryid !== false) {
1844
			$folder = mapi_msgstore_openentry($store ? $store : $this->store, $entryid);
0 ignored issues
show
It seems like $entryid can also be of type true; however, parameter $entryid of mapi_msgstore_openentry() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1844
			$folder = mapi_msgstore_openentry($store ? $store : $this->store, /** @scrutinizer ignore-type */ $entryid);
Loading history...
1845
		}
1846
1847
		return $folder;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $folder returns the type false|resource which is incompatible with the documented return type resource.
Loading history...
1848
	}
1849
1850
	/**
1851
	 * Function will return entryid of default folder from store. This method is useful when you want
1852
	 * to get entryid of folder which is stored as store properties
1853
	 * (PR_IPM_FAVORITES_ENTRYID, PR_IPM_OUTBOX_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_IPM_WASTEBASKET_ENTRYID).
1854
	 *
1855
	 * @param int   $prop  proptag of the folder whose entryid we want to get
1856
	 * @param mixed $store {optional} user store from which we need to get entryid of default folder
1857
	 *
1858
	 * @return bool|string entryid of default folder from store
1859
	 */
1860
	public function getBaseEntryID($prop, $store = false) {
1861
		$storeprops = mapi_getprops($store ? $store : $this->store, [$prop]);
1862
		if (!isset($storeprops[$prop])) {
1863
			return false;
1864
		}
1865
1866
		return $storeprops[$prop];
1867
	}
1868
1869
	/**
1870
	 * Function will return resource of any default folder of store.
1871
	 *
1872
	 * @param int   $prop  proptag of the folder that we want to open
1873
	 * @param mixed $store {optional} user store from which we need to open default folder
1874
	 *
1875
	 * @return resource default folder of store
1876
	 */
1877
	public function openBaseFolder($prop, $store = false) {
1878
		$folder = false;
1879
		$entryid = $this->getBaseEntryID($prop, $store);
1880
1881
		if ($entryid !== false) {
1882
			$folder = mapi_msgstore_openentry($store ? $store : $this->store, $entryid);
0 ignored issues
show
It seems like $entryid can also be of type true; however, parameter $entryid of mapi_msgstore_openentry() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1882
			$folder = mapi_msgstore_openentry($store ? $store : $this->store, /** @scrutinizer ignore-type */ $entryid);
Loading history...
1883
		}
1884
1885
		return $folder;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $folder returns the type false|resource which is incompatible with the documented return type resource.
Loading history...
1886
	}
1887
1888
	/**
1889
	 * Function checks whether user has access over the specified folder or not.
1890
	 *
1891
	 * @param string $entryid entryid The entryid of the folder to check
1892
	 * @param mixed  $store   (optional) store from which folder should be opened
1893
	 *
1894
	 * @return bool true if user has an access over the folder, false if not
1895
	 */
1896
	public function checkFolderWriteAccess($entryid, $store = false) {
1897
		$accessToFolder = false;
1898
1899
		if (!empty($entryid)) {
1900
			if ($store === false) {
1901
				$store = $this->store;
1902
			}
1903
1904
			try {
1905
				$folder = mapi_msgstore_openentry($store, $entryid);
1906
				$folderProps = mapi_getprops($folder, [PR_ACCESS]);
1907
				if (($folderProps[PR_ACCESS] & MAPI_ACCESS_CREATE_CONTENTS) === MAPI_ACCESS_CREATE_CONTENTS) {
1908
					$accessToFolder = true;
1909
				}
1910
			}
1911
			catch (MAPIException $e) {
1912
				// we don't have rights to open folder, so return false
1913
				if ($e->getCode() == MAPI_E_NO_ACCESS) {
1914
					return $accessToFolder;
1915
				}
1916
1917
				// rethrow other errors
1918
				throw $e;
1919
			}
1920
		}
1921
1922
		return $accessToFolder;
1923
	}
1924
1925
	/**
1926
	 * Function checks whether user has access over the specified folder or not.
1927
	 *
1928
	 * @param mixed $store
1929
	 *
1930
	 * @return bool true if user has an access over the folder, false if not
1931
	 */
1932
	public function checkCalendarWriteAccess($store = false) {
1933
		if ($store === false) {
1934
			$messageProps = mapi_getprops($this->message, [PR_RCVD_REPRESENTING_ENTRYID]);
1935
			$store = $this->store;
1936
			// If this meeting request is received by a delegate then open delegator's store.
1937
			if (isset($messageProps[PR_RCVD_REPRESENTING_ENTRYID])) {
1938
				$delegatorStore = $this->getDelegatorStore($messageProps[PR_RCVD_REPRESENTING_ENTRYID]);
1939
				if (!empty($delegatorStore['store'])) {
1940
					$store = $delegatorStore['store'];
1941
				}
1942
			}
1943
		}
1944
1945
		// If the store is a public folder, the calendar folder is the PARENT_ENTRYID of the calendar item
1946
		$provider = mapi_getprops($store, [PR_MDB_PROVIDER]);
0 ignored issues
show
It seems like $store can also be of type resource; however, parameter $any of mapi_getprops() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1946
		$provider = mapi_getprops(/** @scrutinizer ignore-type */ $store, [PR_MDB_PROVIDER]);
Loading history...
1947
		if (isset($provider[PR_MDB_PROVIDER]) && $provider[PR_MDB_PROVIDER] === ZARAFA_STORE_PUBLIC_GUID) {
1948
			$entryid = mapi_getprops($this->message, [PR_PARENT_ENTRYID]);
1949
			$entryid = $entryid[PR_PARENT_ENTRYID];
1950
		}
1951
		else {
1952
			$entryid = $this->getDefaultFolderEntryID(PR_IPM_APPOINTMENT_ENTRYID, $store);
1953
			if ($entryid === false) {
1954
				$entryid = $this->getBaseEntryID(PR_IPM_APPOINTMENT_ENTRYID, $store);
1955
			}
1956
1957
			if ($entryid === false) {
1958
				return false;
1959
			}
1960
		}
1961
1962
		return $this->checkFolderWriteAccess($entryid, $store);
1963
	}
1964
1965
	/**
1966
	 * Function will resolve the user and open its store.
1967
	 *
1968
	 * @param string $ownerentryid the entryid of the user
1969
	 *
1970
	 * @return resource store of the user
1971
	 */
1972
	public function openCustomUserStore($ownerentryid) {
1973
		$ab = mapi_openaddressbook($this->session);
0 ignored issues
show
It seems like $this->session can also be of type boolean; however, parameter $session of mapi_openaddressbook() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1973
		$ab = mapi_openaddressbook(/** @scrutinizer ignore-type */ $this->session);
Loading history...
1974
1975
		try {
1976
			$mailuser = mapi_ab_openentry($ab, $ownerentryid);
1977
		}
1978
		catch (MAPIException $e) {
1979
			return;
1980
		}
1981
1982
		$mailuserprops = mapi_getprops($mailuser, [PR_EMAIL_ADDRESS]);
1983
		$storeid = mapi_msgstore_createentryid($this->store, $mailuserprops[PR_EMAIL_ADDRESS]);
1984
1985
		return mapi_openmsgstore($this->session, $storeid);
0 ignored issues
show
Bug Best Practice introduced by
The expression return mapi_openmsgstore...his->session, $storeid) returns the type resource which is incompatible with the documented return type resource.
Loading history...
It seems like $this->session can also be of type boolean; however, parameter $ses of mapi_openmsgstore() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1985
		return mapi_openmsgstore(/** @scrutinizer ignore-type */ $this->session, $storeid);
Loading history...
1986
	}
1987
1988
	/**
1989
	 * Function which sends response to organizer when attendee accepts, declines or proposes new time to a received meeting request.
1990
	 *
1991
	 * @param int   $status              response status of attendee
1992
	 * @param array $proposeNewTimeProps properties of attendee's proposal
1993
	 * @param mixed $body
1994
	 * @param mixed $store
1995
	 * @param mixed $basedate            date of occurrence which attendee has responded
1996
	 * @param mixed $calFolder
1997
	 */
1998
	public function createResponse($status, $proposeNewTimeProps, $body, $store, $basedate, $calFolder): void {
1999
		$messageprops = mapi_getprops($this->message, [
2000
			PR_SENT_REPRESENTING_ENTRYID,
2001
			PR_SENT_REPRESENTING_EMAIL_ADDRESS,
2002
			PR_SENT_REPRESENTING_ADDRTYPE,
2003
			PR_SENT_REPRESENTING_NAME,
2004
			PR_SENT_REPRESENTING_SEARCH_KEY,
2005
			$this->proptags['goid'],
2006
			$this->proptags['goid2'],
2007
			$this->proptags['location'],
2008
			$this->proptags['startdate'],
2009
			$this->proptags['duedate'],
2010
			$this->proptags['recurring'],
2011
			$this->proptags['recurring_pattern'],
2012
			$this->proptags['recurrence_data'],
2013
			$this->proptags['timezone_data'],
2014
			$this->proptags['timezone'],
2015
			$this->proptags['updatecounter'],
2016
			PR_SUBJECT,
2017
			PR_MESSAGE_CLASS,
2018
			PR_OWNER_APPT_ID,
2019
			$this->proptags['is_exception'],
2020
		]);
2021
2022
		$props = [];
2023
2024
		if ($basedate !== false && !$this->isMeetingRequest($messageprops[PR_MESSAGE_CLASS])) {
2025
			// we are creating response from a recurring calendar item object
2026
			// We found basedate,so opened occurrence and get properties.
2027
			$recurr = new Recurrence($store, $this->message);
2028
			$exception = $recurr->getExceptionAttachment($basedate);
2029
2030
			if ($exception) {
2031
				// Exception found, Now retrieve properties
2032
				$imessage = mapi_attach_openobj($exception, 0);
2033
				$imsgprops = mapi_getprops($imessage);
2034
2035
				// If location is provided, copy it to the response
2036
				if (isset($imsgprops[$this->proptags['location']])) {
2037
					$messageprops[$this->proptags['location']] = $imsgprops[$this->proptags['location']];
2038
				}
2039
2040
				// Update $messageprops with timings of occurrence
2041
				$messageprops[$this->proptags['startdate']] = $imsgprops[$this->proptags['startdate']];
2042
				$messageprops[$this->proptags['duedate']] = $imsgprops[$this->proptags['duedate']];
2043
2044
				// Meeting related properties
2045
				$props[$this->proptags['meetingstatus']] = $imsgprops[$this->proptags['meetingstatus']];
2046
				$props[$this->proptags['responsestatus']] = $imsgprops[$this->proptags['responsestatus']];
2047
				$props[PR_SUBJECT] = $imsgprops[PR_SUBJECT];
2048
			}
2049
			else {
2050
				// Exceptions is deleted.
2051
				// Update $messageprops with timings of occurrence
2052
				$messageprops[$this->proptags['startdate']] = $recurr->getOccurrenceStart($basedate);
2053
				$messageprops[$this->proptags['duedate']] = $recurr->getOccurrenceEnd($basedate);
2054
2055
				$props[$this->proptags['meetingstatus']] = olNonMeeting;
2056
				$props[$this->proptags['responsestatus']] = olResponseNone;
2057
			}
2058
2059
			$props[$this->proptags['recurring']] = false;
2060
			$props[$this->proptags['is_exception']] = true;
2061
		}
2062
		else {
2063
			// we are creating a response from meeting request mail (it could be recurring or non-recurring)
2064
			// Send all recurrence info in response, if this is a recurrence meeting.
2065
			$isRecurring = isset($messageprops[$this->proptags['recurring']]) && $messageprops[$this->proptags['recurring']];
2066
			$isException = isset($messageprops[$this->proptags['is_exception']]) && $messageprops[$this->proptags['is_exception']];
2067
			if ($isRecurring || $isException) {
2068
				if ($isRecurring) {
2069
					$props[$this->proptags['recurring']] = $messageprops[$this->proptags['recurring']];
2070
				}
2071
				if ($isException) {
2072
					$props[$this->proptags['is_exception']] = $messageprops[$this->proptags['is_exception']];
2073
				}
2074
				$calendaritems = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder);
2075
2076
				$calendaritem = mapi_msgstore_openentry($store, $calendaritems[0]);
2077
				$recurr = new Recurrence($store, $calendaritem);
2078
			}
2079
		}
2080
2081
		// we are sending a response for recurring meeting request (or exception), so set some required properties
2082
		if (isset($recurr) && $recurr) {
2083
			if (!empty($messageprops[$this->proptags['recurring_pattern']])) {
2084
				$props[$this->proptags['recurring_pattern']] = $messageprops[$this->proptags['recurring_pattern']];
2085
			}
2086
2087
			if (!empty($messageprops[$this->proptags['recurrence_data']])) {
2088
				$props[$this->proptags['recurrence_data']] = $messageprops[$this->proptags['recurrence_data']];
2089
			}
2090
2091
			$props[$this->proptags['timezone_data']] = $messageprops[$this->proptags['timezone_data']];
2092
			$props[$this->proptags['timezone']] = $messageprops[$this->proptags['timezone']];
2093
2094
			$this->generateRecurDates($recurr, $messageprops, $props);
2095
		}
2096
2097
		// Create a response message
2098
		$recip = [];
2099
		$recip[PR_ENTRYID] = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
2100
		$recip[PR_EMAIL_ADDRESS] = $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
2101
		$recip[PR_ADDRTYPE] = $messageprops[PR_SENT_REPRESENTING_ADDRTYPE];
2102
		$recip[PR_DISPLAY_NAME] = $messageprops[PR_SENT_REPRESENTING_NAME];
2103
		$recip[PR_RECIPIENT_TYPE] = MAPI_TO;
2104
		$recip[PR_SEARCH_KEY] = $messageprops[PR_SENT_REPRESENTING_SEARCH_KEY];
2105
2106
		$subjectprefix = '';
2107
		$classpostfix = '';
2108
2109
		switch ($status) {
2110
			case olResponseAccepted:
2111
				$classpostfix = 'Pos';
2112
				$subjectprefix = dgettext('zarafa', 'Accepted');
2113
				break;
2114
2115
			case olResponseDeclined:
2116
				$classpostfix = 'Neg';
2117
				$subjectprefix = dgettext('zarafa', 'Declined');
2118
				break;
2119
2120
			case olResponseTentative:
2121
				$classpostfix = 'Tent';
2122
				$subjectprefix = dgettext('zarafa', 'Tentatively accepted');
2123
				break;
2124
		}
2125
2126
		if (!empty($proposeNewTimeProps)) {
2127
			// if attendee has proposed new time then change subject prefix
2128
			$subjectprefix = dgettext('zarafa', 'New Time Proposed');
2129
		}
2130
2131
		$props[PR_SUBJECT] = $subjectprefix . ': ' . $messageprops[PR_SUBJECT];
2132
2133
		$props[PR_MESSAGE_CLASS] = 'IPM.Schedule.Meeting.Resp.' . $classpostfix;
2134
		if (isset($messageprops[PR_OWNER_APPT_ID])) {
2135
			$props[PR_OWNER_APPT_ID] = $messageprops[PR_OWNER_APPT_ID];
2136
		}
2137
2138
		// Set GlobalId AND CleanGlobalId, if exception then also set basedate into GlobalId(0x3).
2139
		$props[$this->proptags['goid']] = $this->setBasedateInGlobalID($messageprops[$this->proptags['goid2']], $basedate);
2140
		$props[$this->proptags['goid2']] = $messageprops[$this->proptags['goid2']];
2141
		$props[$this->proptags['updatecounter']] = isset($messageprops[$this->proptags['updatecounter']]) ? $messageprops[$this->proptags['updatecounter']] : 0;
2142
2143
		if (!empty($proposeNewTimeProps)) {
2144
			// merge proposal properties to message properties which will be sent to organizer
2145
			$props = $proposeNewTimeProps + $props;
2146
		}
2147
2148
		// Set body message in Appointment
2149
		if (isset($body)) {
2150
			$props[PR_BODY] = $this->getMeetingTimeInfo() ? $this->getMeetingTimeInfo() : $body;
2151
		}
2152
2153
		// PR_START_DATE/PR_END_DATE is used in the UI in Outlook on the response message
2154
		$props[PR_START_DATE] = $messageprops[$this->proptags['startdate']];
2155
		$props[PR_END_DATE] = $messageprops[$this->proptags['duedate']];
2156
2157
		// Set startdate and duedate in response mail.
2158
		$props[$this->proptags['startdate']] = $messageprops[$this->proptags['startdate']];
2159
		$props[$this->proptags['duedate']] = $messageprops[$this->proptags['duedate']];
2160
2161
		// responselocation is used in the UI in Outlook on the response message
2162
		if (isset($messageprops[$this->proptags['location']])) {
2163
			$props[$this->proptags['responselocation']] = $messageprops[$this->proptags['location']];
2164
			$props[$this->proptags['location']] = $messageprops[$this->proptags['location']];
2165
		}
2166
2167
		$message = $this->createOutgoingMessage($store);
2168
2169
		mapi_setprops($message, $props);
0 ignored issues
show
$message of type resource is incompatible with the type resource expected by parameter $any of mapi_setprops(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2169
		mapi_setprops(/** @scrutinizer ignore-type */ $message, $props);
Loading history...
2170
		mapi_message_modifyrecipients($message, MODRECIP_ADD, [$recip]);
0 ignored issues
show
$message of type resource is incompatible with the type resource expected by parameter $msg of mapi_message_modifyrecipients(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2170
		mapi_message_modifyrecipients(/** @scrutinizer ignore-type */ $message, MODRECIP_ADD, [$recip]);
Loading history...
2171
		mapi_savechanges($message);
0 ignored issues
show
$message of type resource is incompatible with the type resource expected by parameter $any of mapi_savechanges(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2171
		mapi_savechanges(/** @scrutinizer ignore-type */ $message);
Loading history...
2172
		mapi_message_submitmessage($message);
0 ignored issues
show
$message of type resource is incompatible with the type resource expected by parameter $msg of mapi_message_submitmessage(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2172
		mapi_message_submitmessage(/** @scrutinizer ignore-type */ $message);
Loading history...
2173
	}
2174
2175
	/**
2176
	 * Function which finds items in calendar based on globalId and cleanGlobalId.
2177
	 *
2178
	 * @param string $goid             GlobalID(0x3) of item
2179
	 * @param mixed  $calendar         MAPI_folder of user (optional)
2180
	 * @param bool   $useCleanGlobalId if true then search should be performed on cleanGlobalId(0x23) else globalId(0x3)
2181
	 *
2182
	 * @return mixed
2183
	 */
2184
	public function findCalendarItems($goid, $calendar = false, $useCleanGlobalId = false) {
2185
		if ($calendar === false) {
2186
			// Open the Calendar
2187
			$calendar = $this->openDefaultCalendar();
2188
		}
2189
2190
		// Find the item by restricting all items to the correct ID
2191
		$restrict = [
2192
			RES_PROPERTY,
2193
			[
2194
				RELOP => RELOP_EQ,
2195
				ULPROPTAG => ($useCleanGlobalId === true ? $this->proptags['goid2'] : $this->proptags['goid']),
2196
				VALUE => $goid,
2197
			],
2198
		];
2199
2200
		$calendarcontents = mapi_folder_getcontentstable($calendar);
0 ignored issues
show
It seems like $calendar can also be of type resource; however, parameter $fld of mapi_folder_getcontentstable() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2200
		$calendarcontents = mapi_folder_getcontentstable(/** @scrutinizer ignore-type */ $calendar);
Loading history...
2201
2202
		$rows = mapi_table_queryallrows($calendarcontents, [PR_ENTRYID], $restrict);
2203
2204
		if (empty($rows)) {
2205
			return;
2206
		}
2207
2208
		$calendaritems = [];
2209
2210
		// In principle, there should only be one row, but we'll handle them all just in case
2211
		foreach ($rows as $row) {
2212
			$calendaritems[] = $row[PR_ENTRYID];
2213
		}
2214
2215
		return $calendaritems;
2216
	}
2217
2218
	// Returns TRUE if both entryid's are equal. Equality is defined by both entryid's pointing at the
2219
	// same SMTP address when converted to SMTP
2220
	public function compareABEntryIDs($entryid1, $entryid2): bool {
2221
		// If the session was not passed, just do a 'normal' compare.
2222
		if (!$this->session) {
2223
			return $entryid1 == $entryid2;
2224
		}
2225
2226
		$smtp1 = $this->getSMTPAddress($entryid1);
2227
		$smtp2 = $this->getSMTPAddress($entryid2);
2228
2229
		if ($smtp1 == $smtp2) {
2230
			return true;
2231
		}
2232
2233
		return false;
2234
	}
2235
2236
	// Gets the SMTP address of the passed addressbook entryid
2237
	public function getSMTPAddress($entryid) {
2238
		if (!$this->session) {
2239
			return false;
2240
		}
2241
2242
		try {
2243
			$ab = mapi_openaddressbook($this->session);
0 ignored issues
show
It seems like $this->session can also be of type true; however, parameter $session of mapi_openaddressbook() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2243
			$ab = mapi_openaddressbook(/** @scrutinizer ignore-type */ $this->session);
Loading history...
2244
			$abitem = mapi_ab_openentry($ab, $entryid);
2245
2246
			if (!$abitem) {
0 ignored issues
show
$abitem is of type resource, thus it always evaluated to true.
Loading history...
2247
				return '';
2248
			}
2249
		}
2250
		catch (MAPIException $e) {
2251
			return '';
2252
		}
2253
2254
		$props = mapi_getprops($abitem, [PR_ADDRTYPE, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS]);
2255
2256
		if ($props[PR_ADDRTYPE] == 'SMTP') {
2257
			return $props[PR_EMAIL_ADDRESS];
2258
		}
2259
2260
		return $props[PR_SMTP_ADDRESS];
2261
	}
2262
2263
	/**
2264
	 * Gets the properties associated with the owner of the passed store:
2265
	 * PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_ADDRTYPE, PR_ENTRYID, PR_SEARCH_KEY.
2266
	 *
2267
	 * @param mixed $store                  message store
2268
	 * @param bool  $fallbackToLoggedInUser If true then return properties of logged in user instead of mailbox owner.
2269
	 *                                      Not used when passed store is public store.
2270
	 *                                      For public store we are always returning logged in user's info.
2271
	 *
2272
	 * @return array|false properties of logged in user in an array in sequence of display_name, email address, address type, entryid and search key
2273
	 *
2274
	 * @psalm-return false|list{mixed, mixed, mixed, mixed, mixed}
2275
	 */
2276
	public function getOwnerAddress($store, $fallbackToLoggedInUser = true) {
2277
		if (!$this->session) {
2278
			return false;
2279
		}
2280
2281
		$storeProps = mapi_getprops($store, [PR_MAILBOX_OWNER_ENTRYID, PR_USER_ENTRYID]);
2282
2283
		$ownerEntryId = false;
2284
		if (isset($storeProps[PR_USER_ENTRYID]) && $storeProps[PR_USER_ENTRYID]) {
2285
			$ownerEntryId = $storeProps[PR_USER_ENTRYID];
2286
		}
2287
2288
		if (isset($storeProps[PR_MAILBOX_OWNER_ENTRYID]) && $storeProps[PR_MAILBOX_OWNER_ENTRYID] && !$fallbackToLoggedInUser) {
2289
			$ownerEntryId = $storeProps[PR_MAILBOX_OWNER_ENTRYID];
2290
		}
2291
2292
		if ($ownerEntryId) {
2293
			$ab = mapi_openaddressbook($this->session);
0 ignored issues
show
It seems like $this->session can also be of type true; however, parameter $session of mapi_openaddressbook() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2293
			$ab = mapi_openaddressbook(/** @scrutinizer ignore-type */ $this->session);
Loading history...
2294
2295
			$zarafaUser = mapi_ab_openentry($ab, $ownerEntryId);
2296
			if (!$zarafaUser) {
0 ignored issues
show
$zarafaUser is of type resource, thus it always evaluated to true.
Loading history...
2297
				return false;
2298
			}
2299
2300
			$ownerProps = mapi_getprops($zarafaUser, [PR_ADDRTYPE, PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SEARCH_KEY]);
2301
2302
			$addrType = $ownerProps[PR_ADDRTYPE];
2303
			$name = $ownerProps[PR_DISPLAY_NAME];
2304
			$emailAddr = $ownerProps[PR_EMAIL_ADDRESS];
2305
			$searchKey = $ownerProps[PR_SEARCH_KEY];
2306
			$entryId = $ownerEntryId;
2307
2308
			return [$name, $emailAddr, $addrType, $entryId, $searchKey];
2309
		}
2310
2311
		return false;
2312
	}
2313
2314
	// Opens this session's default message store
2315
	public function openDefaultStore() {
2316
		$entryid = '';
2317
2318
		$storestable = mapi_getmsgstorestable($this->session);
0 ignored issues
show
It seems like $this->session can also be of type boolean; however, parameter $session of mapi_getmsgstorestable() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2318
		$storestable = mapi_getmsgstorestable(/** @scrutinizer ignore-type */ $this->session);
Loading history...
2319
		$rows = mapi_table_queryallrows($storestable, [PR_ENTRYID, PR_DEFAULT_STORE]);
2320
2321
		foreach ($rows as $row) {
2322
			if (isset($row[PR_DEFAULT_STORE]) && $row[PR_DEFAULT_STORE]) {
2323
				$entryid = $row[PR_ENTRYID];
2324
				break;
2325
			}
2326
		}
2327
2328
		if (!$entryid) {
2329
			return false;
2330
		}
2331
2332
		return mapi_openmsgstore($this->session, $entryid);
0 ignored issues
show
It seems like $this->session can also be of type boolean; however, parameter $ses of mapi_openmsgstore() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2332
		return mapi_openmsgstore(/** @scrutinizer ignore-type */ $this->session, $entryid);
Loading history...
2333
	}
2334
2335
	/**
2336
	 * Function which adds organizer to recipient list which is passed.
2337
	 * This function also checks if it has organizer.
2338
	 *
2339
	 * @param array $messageProps message properties
2340
	 * @param array $recipients   recipients list of message
2341
	 * @param bool  $isException  true if we are processing recipient of exception
2342
	 */
2343
	public function addOrganizer($messageProps, &$recipients, $isException = false): void {
2344
		$hasOrganizer = false;
2345
		// Check if meeting already has an organizer.
2346
		foreach ($recipients as $key => $recipient) {
2347
			if (isset($recipient[PR_RECIPIENT_FLAGS]) && $recipient[PR_RECIPIENT_FLAGS] == (recipSendable | recipOrganizer)) {
2348
				$hasOrganizer = true;
2349
			}
2350
			elseif ($isException && !isset($recipient[PR_RECIPIENT_FLAGS])) {
2351
				// Recipients for an occurrence
2352
				$recipients[$key][PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalResponse;
2353
			}
2354
		}
2355
2356
		if (!$hasOrganizer) {
2357
			// Create organizer.
2358
			$organizer = [];
2359
			$organizer[PR_ENTRYID] = $organizer[PR_RECIPIENT_ENTRYID] = $messageProps[PR_SENT_REPRESENTING_ENTRYID];
2360
			$organizer[PR_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
2361
			$organizer[PR_EMAIL_ADDRESS] = $messageProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
2362
			$organizer[PR_RECIPIENT_TYPE] = MAPI_TO;
2363
			$organizer[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
2364
			$organizer[PR_ADDRTYPE] = empty($messageProps[PR_SENT_REPRESENTING_ADDRTYPE]) ? 'SMTP' : $messageProps[PR_SENT_REPRESENTING_ADDRTYPE];
2365
			$organizer[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
2366
			$organizer[PR_RECIPIENT_FLAGS] = recipSendable | recipOrganizer;
2367
			$organizer[PR_SEARCH_KEY] = $messageProps[PR_SENT_REPRESENTING_SEARCH_KEY];
2368
			$organizer[PR_SMTP_ADDRESS] = $messageProps[PR_SENT_REPRESENTING_SMTP_ADDRESS] ?? $messageProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
2369
2370
			// Add organizer to recipients list.
2371
			array_unshift($recipients, $organizer);
2372
		}
2373
	}
2374
2375
	/**
2376
	 * Function which removes an exception/occurrence from recurrencing meeting
2377
	 * when a meeting cancellation of an occurrence is processed.
2378
	 *
2379
	 * @param mixed    $basedate basedate of an occurrence
2380
	 * @param mixed    $message  recurring item from which occurrence has to be deleted
2381
	 * @param resource $store    MAPI_MSG_Store which contains the item
2382
	 */
2383
	public function doRemoveExceptionFromCalendar($basedate, $message, $store): void {
2384
		$recurr = new Recurrence($store, $message);
2385
		$recurr->createException([], $basedate, true);
2386
		mapi_savechanges($message);
2387
	}
2388
2389
	/**
2390
	 * Function which returns basedate of an changed occurrence from globalID of meeting request.
2391
	 *
2392
	 * @param string $goid globalID
2393
	 *
2394
	 * @return false|int true if basedate is found else false it not found
2395
	 */
2396
	public function getBasedateFromGlobalID($goid) {
2397
		$hexguid = bin2hex($goid);
2398
		$hexbase = substr($hexguid, 32, 8);
2399
		$day = (int) hexdec(substr($hexbase, 6, 2));
2400
		$month = (int) hexdec(substr($hexbase, 4, 2));
2401
		$year = (int) hexdec(substr($hexbase, 0, 4));
2402
2403
		if ($day && $month && $year) {
2404
			return gmmktime(0, 0, 0, $month, $day, $year);
2405
		}
2406
2407
		return false;
2408
	}
2409
2410
	/**
2411
	 * Function which sets basedate in globalID of changed occurrence which is to be sent.
2412
	 *
2413
	 * @param string $goid     globalID
2414
	 * @param mixed  $basedate of changed occurrence
2415
	 *
2416
	 * @return false|string globalID with basedate in it
2417
	 */
2418
	public function setBasedateInGlobalID($goid, $basedate = false) {
2419
		$hexguid = bin2hex($goid);
2420
		$year = $basedate ? sprintf('%04s', dechex((int) gmdate('Y', $basedate))) : '0000';
2421
		$month = $basedate ? sprintf('%02s', dechex((int) gmdate('m', $basedate))) : '00';
2422
		$day = $basedate ? sprintf('%02s', dechex((int) gmdate('d', $basedate))) : '00';
2423
2424
		return hex2bin(strtoupper(substr($hexguid, 0, 32) . $year . $month . $day . substr($hexguid, 40)));
2425
	}
2426
2427
	/**
2428
	 * Function which replaces attachments with copy_from in copy_to.
2429
	 *
2430
	 * @param mixed $copyFrom       MAPI_message from which attachments are to be copied
2431
	 * @param mixed $copyTo         MAPI_message to which attachment are to be copied
2432
	 * @param bool  $copyExceptions if true then all exceptions should also be sent as attachments
2433
	 */
2434
	public function replaceAttachments($copyFrom, $copyTo, $copyExceptions = true): void {
2435
		/* remove all old attachments */
2436
		$attachmentTableTo = mapi_message_getattachmenttable($copyTo);
2437
		if ($attachmentTableTo) {
0 ignored issues
show
$attachmentTableTo is of type resource, thus it always evaluated to true.
Loading history...
2438
			$attachments = mapi_table_queryallrows($attachmentTableTo, [PR_ATTACH_NUM, PR_ATTACH_METHOD, PR_EXCEPTION_STARTTIME]);
2439
2440
			foreach ($attachments as $attachProps) {
2441
				/* remove exceptions too? */
2442
				if (!$copyExceptions && $attachProps[PR_ATTACH_METHOD] == ATTACH_EMBEDDED_MSG && isset($attachProps[PR_EXCEPTION_STARTTIME])) {
2443
					continue;
2444
				}
2445
				mapi_message_deleteattach($copyTo, $attachProps[PR_ATTACH_NUM]);
2446
			}
2447
		}
2448
2449
		/* copy new attachments */
2450
		$attachmentTableFrom = mapi_message_getattachmenttable($copyFrom);
2451
		if ($attachmentTableFrom) {
0 ignored issues
show
$attachmentTableFrom is of type resource, thus it always evaluated to true.
Loading history...
2452
			$attachments = mapi_table_queryallrows($attachmentTableFrom, [PR_ATTACH_NUM, PR_ATTACH_METHOD, PR_EXCEPTION_STARTTIME]);
2453
2454
			foreach ($attachments as $attachProps) {
2455
				if (!$copyExceptions && $attachProps[PR_ATTACH_METHOD] == ATTACH_EMBEDDED_MSG && isset($attachProps[PR_EXCEPTION_STARTTIME])) {
2456
					continue;
2457
				}
2458
2459
				$attachOld = mapi_message_openattach($copyFrom, (int) $attachProps[PR_ATTACH_NUM]);
2460
				$attachNewResourceMsg = mapi_message_createattach($copyTo);
2461
				mapi_copyto($attachOld, [], [], $attachNewResourceMsg, 0);
2462
				mapi_savechanges($attachNewResourceMsg);
2463
			}
2464
		}
2465
	}
2466
2467
	/**
2468
	 * Function which replaces recipients in copyTo with recipients from copyFrom.
2469
	 *
2470
	 * @param mixed $copyFrom   MAPI_message from which recipients are to be copied
2471
	 * @param mixed $copyTo     MAPI_message to which recipients are to be copied
2472
	 * @param bool  $isDelegate indicates whether delegate is processing
2473
	 *                          so don't copy delegate information to recipient table
2474
	 */
2475
	public function replaceRecipients($copyFrom, $copyTo, $isDelegate = false): void {
2476
		$recipientTable = mapi_message_getrecipienttable($copyFrom);
2477
2478
		// If delegate, then do not add the delegate in recipients
2479
		if ($isDelegate) {
2480
			$delegate = mapi_getprops($copyFrom, [PR_RECEIVED_BY_EMAIL_ADDRESS]);
2481
			$res = [
2482
				RES_PROPERTY,
2483
				[
2484
					RELOP => RELOP_NE,
2485
					ULPROPTAG => PR_EMAIL_ADDRESS,
2486
					VALUE => [PR_EMAIL_ADDRESS => $delegate[PR_RECEIVED_BY_EMAIL_ADDRESS]],
2487
				],
2488
			];
2489
			$recipients = mapi_table_queryallrows($recipientTable, $this->recipprops, $res);
2490
		}
2491
		else {
2492
			$recipients = mapi_table_queryallrows($recipientTable, $this->recipprops);
2493
		}
2494
2495
		$copyToRecipientTable = mapi_message_getrecipienttable($copyTo);
2496
		$copyToRecipientRows = mapi_table_queryallrows($copyToRecipientTable, [PR_ROWID]);
2497
2498
		mapi_message_modifyrecipients($copyTo, MODRECIP_REMOVE, $copyToRecipientRows);
2499
		mapi_message_modifyrecipients($copyTo, MODRECIP_ADD, $recipients);
2500
	}
2501
2502
	/**
2503
	 * Function creates meeting item in resource's calendar.
2504
	 *
2505
	 * @param resource $message  MAPI_message which is to create in resource's calendar
2506
	 * @param bool     $cancel   cancel meeting
2507
	 * @param mixed    $prefix   prefix for subject of meeting
2508
	 * @param mixed    $basedate
2509
	 *
2510
	 * @return (mixed|resource)[][]
2511
	 *
2512
	 * @psalm-return list<array{store: resource, folder: mixed, msg: mixed}>
2513
	 */
2514
	public function bookResources($message, $cancel, $prefix, $basedate = false): array {
2515
		if (!$this->enableDirectBooking) {
2516
			return [];
2517
		}
2518
2519
		// Get the properties of the message
2520
		$messageprops = mapi_getprops($message);
0 ignored issues
show
$message of type resource is incompatible with the type resource expected by parameter $any of mapi_getprops(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2520
		$messageprops = mapi_getprops(/** @scrutinizer ignore-type */ $message);
Loading history...
2521
2522
		$calFolder = '';
2523
2524
		if ($basedate) {
2525
			$recurrItemProps = mapi_getprops($this->message, [$this->proptags['goid'], $this->proptags['goid2'], $this->proptags['timezone_data'], $this->proptags['timezone'], PR_OWNER_APPT_ID]);
2526
2527
			$messageprops[$this->proptags['goid']] = $this->setBasedateInGlobalID($recurrItemProps[$this->proptags['goid']], $basedate);
2528
			$messageprops[$this->proptags['goid2']] = $recurrItemProps[$this->proptags['goid2']];
2529
2530
			// Delete properties which are not needed.
2531
			$deleteProps = [$this->proptags['basedate'], PR_DISPLAY_NAME, PR_ATTACHMENT_FLAGS, PR_ATTACHMENT_HIDDEN, PR_ATTACHMENT_LINKID, PR_ATTACH_FLAGS, PR_ATTACH_METHOD];
2532
			foreach ($deleteProps as $propID) {
2533
				if (isset($messageprops[$propID])) {
2534
					unset($messageprops[$propID]);
2535
				}
2536
			}
2537
2538
			if (isset($messageprops[$this->proptags['recurring']])) {
2539
				$messageprops[$this->proptags['recurring']] = false;
2540
			}
2541
2542
			// Set Outlook properties
2543
			$messageprops[$this->proptags['clipstart']] = $messageprops[$this->proptags['startdate']];
2544
			$messageprops[$this->proptags['clipend']] = $messageprops[$this->proptags['duedate']];
2545
			$messageprops[$this->proptags['timezone_data']] = $recurrItemProps[$this->proptags['timezone_data']];
2546
			$messageprops[$this->proptags['timezone']] = $recurrItemProps[$this->proptags['timezone']];
2547
			$messageprops[$this->proptags['attendee_critical_change']] = time();
2548
			$messageprops[$this->proptags['owner_critical_change']] = time();
2549
		}
2550
2551
		// Get resource recipients
2552
		$getResourcesRestriction = [
2553
			RES_PROPERTY,
2554
			[
2555
				RELOP => RELOP_EQ,	// Equals recipient type 3: Resource
2556
				ULPROPTAG => PR_RECIPIENT_TYPE,
2557
				VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
2558
			],
2559
		];
2560
		$recipienttable = mapi_message_getrecipienttable($message);
0 ignored issues
show
$message of type resource is incompatible with the type resource expected by parameter $msg of mapi_message_getrecipienttable(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2560
		$recipienttable = mapi_message_getrecipienttable(/** @scrutinizer ignore-type */ $message);
Loading history...
2561
		$resourceRecipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $getResourcesRestriction);
2562
2563
		$this->errorSetResource = false;
2564
		$resourceRecipData = [];
2565
2566
		// Put appointment into store resource users
2567
		$i = 0;
2568
		$len = count($resourceRecipients);
2569
		while (!$this->errorSetResource && $i < $len) {
2570
			$userStore = $this->openCustomUserStore($resourceRecipients[$i][PR_ENTRYID]);
2571
2572
			// Open root folder
2573
			$userRoot = mapi_msgstore_openentry($userStore, null);
0 ignored issues
show
$userStore of type resource is incompatible with the type resource expected by parameter $store of mapi_msgstore_openentry(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2573
			$userRoot = mapi_msgstore_openentry(/** @scrutinizer ignore-type */ $userStore, null);
Loading history...
2574
2575
			// Get calendar entryID
2576
			$userRootProps = mapi_getprops($userRoot, [PR_STORE_ENTRYID, PR_IPM_APPOINTMENT_ENTRYID, PR_FREEBUSY_ENTRYIDS]);
2577
2578
			// Open Calendar folder
2579
			$accessToFolder = false;
0 ignored issues
show
The assignment to $accessToFolder is dead and can be removed.
Loading history...
2580
2581
			try {
2582
				// @FIXME this checks delegate has access to resource's calendar folder
2583
				// but it should use boss' credentials
2584
2585
				$accessToFolder = $this->checkCalendarWriteAccess($this->store);
2586
				if ($accessToFolder) {
2587
					$calFolder = mapi_msgstore_openentry($userStore, $userRootProps[PR_IPM_APPOINTMENT_ENTRYID]);
2588
				}
2589
			}
2590
			catch (MAPIException $e) {
2591
				$e->setHandled();
2592
				$this->errorSetResource = 1; // No access
2593
			}
2594
2595
			if ($accessToFolder) {
2596
				/**
2597
				 * Get the LocalFreebusy message that contains the properties that
2598
				 * are set to accept or decline resource meeting requests.
2599
				 */
2600
				$localFreebusyMsg = FreeBusy::getLocalFreeBusyMessage($userStore);
2601
				if ($localFreebusyMsg) {
2602
					$props = mapi_getprops($localFreebusyMsg, [PR_SCHDINFO_AUTO_ACCEPT_APPTS, PR_SCHDINFO_DISALLOW_RECURRING_APPTS, PR_SCHDINFO_DISALLOW_OVERLAPPING_APPTS]);
2603
2604
					$acceptMeetingRequests = isset($props[PR_SCHDINFO_AUTO_ACCEPT_APPTS]) ? $props[PR_SCHDINFO_AUTO_ACCEPT_APPTS] : false;
2605
					$declineRecurringMeetingRequests = isset($props[PR_SCHDINFO_DISALLOW_RECURRING_APPTS]) ? $props[PR_SCHDINFO_DISALLOW_RECURRING_APPTS] : false;
2606
					$declineConflictingMeetingRequests = isset($props[PR_SCHDINFO_DISALLOW_OVERLAPPING_APPTS]) ? $props[PR_SCHDINFO_DISALLOW_OVERLAPPING_APPTS] : false;
2607
2608
					if (!$acceptMeetingRequests) {
2609
						/*
2610
						 * When a resource has not been set to automatically accept meeting requests,
2611
						 * the meeting request has to be sent to him rather than being put directly into
2612
						 * his calendar. No error should be returned.
2613
						 */
2614
						// $errorSetResource = 2;
2615
						$this->nonAcceptingResources[] = $resourceRecipients[$i];
2616
					}
2617
					else {
2618
						if ($declineRecurringMeetingRequests && !$cancel) {
2619
							// Check if appointment is recurring
2620
							if ($messageprops[$this->proptags['recurring']]) {
2621
								$this->errorSetResource = 3;
2622
							}
2623
						}
2624
						if ($declineConflictingMeetingRequests && !$cancel) {
2625
							// Check for conflicting items
2626
							if ($calFolder && $this->isMeetingConflicting($message, $userStore, $calFolder)) {
2627
								$this->errorSetResource = 4; // Conflict
2628
							}
2629
						}
2630
					}
2631
				}
2632
			}
2633
2634
			if (!$this->errorSetResource && $accessToFolder) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->errorSetResource of type false|integer is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
2635
				/**
2636
				 * First search on GlobalID(0x3)
2637
				 * If (recurring and occurrence) If Resource was booked for only this occurrence then Resource should have only this occurrence in Calendar and not whole series.
2638
				 * If (normal meeting) then GlobalID(0x3) and CleanGlobalID(0x23) are same, so doesn't matter if search is based on GlobalID.
2639
				 */
2640
				$rows = $this->findCalendarItems($messageprops[$this->proptags['goid']], $calFolder);
2641
2642
				/*
2643
				 * If no entry is found then
2644
				 * 1) Resource doesn't have meeting in Calendar. Seriously!!
2645
				 * OR
2646
				 * 2) We were looking for occurrence item but Resource has whole series
2647
				 */
2648
				if (empty($rows)) {
2649
					/**
2650
					 * Now search on CleanGlobalID(0x23) WHY???
2651
					 * Because we are looking recurring item.
2652
					 *
2653
					 * Possible results of this search
2654
					 * 1) If Resource was booked for more than one occurrences then this search will return all those occurrence because search is perform on CleanGlobalID
2655
					 * 2) If Resource was booked for whole series then it should return series.
2656
					 */
2657
					$rows = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder, true);
2658
2659
					$newResourceMsg = false;
2660
					if (!empty($rows)) {
2661
						// Since we are looking for recurring item, open every result and check for 'recurring' property.
2662
						foreach ($rows as $row) {
2663
							$ResourceMsg = mapi_msgstore_openentry($userStore, $row);
2664
							$ResourceMsgProps = mapi_getprops($ResourceMsg, [$this->proptags['recurring']]);
2665
2666
							if (isset($ResourceMsgProps[$this->proptags['recurring']]) && $ResourceMsgProps[$this->proptags['recurring']]) {
2667
								$newResourceMsg = $ResourceMsg;
2668
								break;
2669
							}
2670
						}
2671
					}
2672
2673
					// Still no results found. I giveup, create new message.
2674
					if (!$newResourceMsg) {
2675
						$newResourceMsg = mapi_folder_createmessage($calFolder);
0 ignored issues
show
It seems like $calFolder can also be of type string; however, parameter $fld of mapi_folder_createmessage() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2675
						$newResourceMsg = mapi_folder_createmessage(/** @scrutinizer ignore-type */ $calFolder);
Loading history...
2676
					}
2677
				}
2678
				else {
2679
					$newResourceMsg = mapi_msgstore_openentry($userStore, $rows[0]);
2680
				}
2681
2682
				// Prefix the subject if needed
2683
				if ($prefix && isset($messageprops[PR_SUBJECT])) {
2684
					$messageprops[PR_SUBJECT] = $prefix . $messageprops[PR_SUBJECT];
2685
				}
2686
2687
				// Set status to cancelled if needed
2688
				$messageprops[$this->proptags['busystatus']] = fbBusy; // The default status (Busy)
2689
				if ($cancel) {
2690
					$messageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // The meeting has been canceled
2691
					$messageprops[$this->proptags['busystatus']] = fbFree; // Free
2692
				}
2693
				else {
2694
					$messageprops[$this->proptags['meetingstatus']] = olMeetingReceived; // The recipient is receiving the request
2695
				}
2696
				$messageprops[$this->proptags['responsestatus']] = olResponseAccepted; // The resource automatically accepts the appointment
2697
2698
				$messageprops[PR_MESSAGE_CLASS] = 'IPM.Appointment';
2699
2700
				// Remove the PR_ICON_INDEX as it is not needed in the sent message.
2701
				$messageprops[PR_ICON_INDEX] = null;
2702
				$messageprops[PR_RESPONSE_REQUESTED] = true;
2703
2704
				// get the store of organizer, in case of delegates it will be delegate store
2705
				$defaultStore = $this->openDefaultStore();
2706
2707
				$storeProps = mapi_getprops($this->store, [PR_ENTRYID]);
2708
				$defaultStoreProps = mapi_getprops($defaultStore, [PR_ENTRYID]);
0 ignored issues
show
It seems like $defaultStore can also be of type false; however, parameter $any of mapi_getprops() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2708
				$defaultStoreProps = mapi_getprops(/** @scrutinizer ignore-type */ $defaultStore, [PR_ENTRYID]);
Loading history...
2709
2710
				// @FIXME use entryid comparison functions here
2711
				if ($storeProps[PR_ENTRYID] !== $defaultStoreProps[PR_ENTRYID]) {
2712
					// get delegate information
2713
					$addrInfo = $this->getOwnerAddress($defaultStore, false);
2714
2715
					if (!empty($addrInfo)) {
2716
						list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $addrInfo;
2717
2718
						$messageprops[PR_SENDER_EMAIL_ADDRESS] = $owneremailaddr;
2719
						$messageprops[PR_SENDER_NAME] = $ownername;
2720
						$messageprops[PR_SENDER_ADDRTYPE] = $owneraddrtype;
2721
						$messageprops[PR_SENDER_ENTRYID] = $ownerentryid;
2722
						$messageprops[PR_SENDER_SEARCH_KEY] = $ownersearchkey;
2723
					}
2724
2725
					// get delegator information
2726
					$addrInfo = $this->getOwnerAddress($this->store, false);
2727
2728
					if (!empty($addrInfo)) {
2729
						list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $addrInfo;
2730
2731
						$messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $owneremailaddr;
2732
						$messageprops[PR_SENT_REPRESENTING_NAME] = $ownername;
2733
						$messageprops[PR_SENT_REPRESENTING_ADDRTYPE] = $owneraddrtype;
2734
						$messageprops[PR_SENT_REPRESENTING_ENTRYID] = $ownerentryid;
2735
						$messageprops[PR_SENT_REPRESENTING_SEARCH_KEY] = $ownersearchkey;
2736
					}
2737
				}
2738
				else {
2739
					// get organizer information
2740
					$addrInfo = $this->getOwnerAddress($this->store);
2741
2742
					if (!empty($addrInfo)) {
2743
						list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $addrInfo;
2744
2745
						$messageprops[PR_SENDER_EMAIL_ADDRESS] = $owneremailaddr;
2746
						$messageprops[PR_SENDER_NAME] = $ownername;
2747
						$messageprops[PR_SENDER_ADDRTYPE] = $owneraddrtype;
2748
						$messageprops[PR_SENDER_ENTRYID] = $ownerentryid;
2749
						$messageprops[PR_SENDER_SEARCH_KEY] = $ownersearchkey;
2750
2751
						$messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $owneremailaddr;
2752
						$messageprops[PR_SENT_REPRESENTING_NAME] = $ownername;
2753
						$messageprops[PR_SENT_REPRESENTING_ADDRTYPE] = $owneraddrtype;
2754
						$messageprops[PR_SENT_REPRESENTING_ENTRYID] = $ownerentryid;
2755
						$messageprops[PR_SENT_REPRESENTING_SEARCH_KEY] = $ownersearchkey;
2756
					}
2757
				}
2758
2759
				$messageprops[$this->proptags['replytime']] = time();
2760
2761
				if ($basedate && isset($ResourceMsgProps[$this->proptags['recurring']]) && $ResourceMsgProps[$this->proptags['recurring']]) {
2762
					$recurr = new Recurrence($userStore, $newResourceMsg);
2763
2764
					// Copy recipients list
2765
					$reciptable = mapi_message_getrecipienttable($message);
2766
					$recips = mapi_table_queryallrows($reciptable, $this->recipprops);
2767
2768
					// add owner to recipient table
2769
					$this->addOrganizer($messageprops, $recips, true);
2770
2771
					// Update occurrence
2772
					if ($recurr->isException($basedate)) {
2773
						$recurr->modifyException($messageprops, $basedate, $recips);
2774
					}
2775
					else {
2776
						$recurr->createException($messageprops, $basedate, false, $recips);
2777
					}
2778
				}
2779
				else {
2780
					mapi_setprops($newResourceMsg, $messageprops);
2781
2782
					// Copy attachments
2783
					$this->replaceAttachments($message, $newResourceMsg);
2784
2785
					// Copy all recipients too
2786
					$this->replaceRecipients($message, $newResourceMsg);
2787
2788
					// Now add organizer also to recipient table
2789
					$recips = [];
2790
					$this->addOrganizer($messageprops, $recips);
2791
2792
					mapi_message_modifyrecipients($newResourceMsg, MODRECIP_ADD, $recips);
2793
				}
2794
2795
				mapi_savechanges($newResourceMsg);
2796
2797
				$resourceRecipData[] = [
2798
					'store' => $userStore,
2799
					'folder' => $calFolder,
2800
					'msg' => $newResourceMsg,
2801
				];
2802
				$this->includesResources = true;
2803
			}
2804
			else {
2805
				/*
2806
				 * If no other errors occurred and you have no access to the
2807
				 * folder of the resource, throw an error=1.
2808
				 */
2809
				if (!$this->errorSetResource) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->errorSetResource of type false|integer is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
2810
					$this->errorSetResource = 1;
2811
				}
2812
2813
				for ($j = 0, $len = count($resourceRecipData); $j < $len; ++$j) {
2814
					// Get the EntryID
2815
					$props = mapi_message_getprops($resourceRecipData[$j]['msg']);
0 ignored issues
show
The function mapi_message_getprops was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2815
					$props = /** @scrutinizer ignore-call */ mapi_message_getprops($resourceRecipData[$j]['msg']);
Loading history...
2816
2817
					mapi_folder_deletemessages($resourceRecipData[$j]['folder'], [$props[PR_ENTRYID]], DELETE_HARD_DELETE);
2818
				}
2819
				$this->recipientDisplayname = $resourceRecipients[$i][PR_DISPLAY_NAME];
2820
			}
2821
			++$i;
2822
		}
2823
2824
		/*
2825
		 * Set the BCC-recipients (resources) tackstatus to accepted.
2826
		 */
2827
		// Get resource recipients
2828
		$getResourcesRestriction = [
2829
			RES_PROPERTY,
2830
			[
2831
				RELOP => RELOP_EQ,	// Equals recipient type 3: Resource
2832
				ULPROPTAG => PR_RECIPIENT_TYPE,
2833
				VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
2834
			],
2835
		];
2836
		$recipienttable = mapi_message_getrecipienttable($message);
2837
		$resourceRecipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $getResourcesRestriction);
2838
		if (!empty($resourceRecipients)) {
2839
			// Set Tracking status of resource recipients to olResponseAccepted (3)
2840
			for ($i = 0, $len = count($resourceRecipients); $i < $len; ++$i) {
2841
				$resourceRecipients[$i][PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusAccepted;
2842
				$resourceRecipients[$i][PR_RECIPIENT_TRACKSTATUS_TIME] = time();
2843
			}
2844
			mapi_message_modifyrecipients($message, MODRECIP_MODIFY, $resourceRecipients);
0 ignored issues
show
$message of type resource is incompatible with the type resource expected by parameter $msg of mapi_message_modifyrecipients(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2844
			mapi_message_modifyrecipients(/** @scrutinizer ignore-type */ $message, MODRECIP_MODIFY, $resourceRecipients);
Loading history...
2845
		}
2846
2847
		return $resourceRecipData;
2848
	}
2849
2850
	/**
2851
	 * Function which save an exception into recurring item.
2852
	 *
2853
	 * @param resource $recurringItem  reference to MAPI_message of recurring item
2854
	 * @param resource $occurrenceItem reference to MAPI_message of occurrence
2855
	 * @param string   $basedate       basedate of occurrence
2856
	 * @param bool     $move           if true then occurrence item is deleted
2857
	 * @param bool     $tentative      true if user has tentatively accepted it or false if user has accepted it
2858
	 * @param bool     $userAction     true if user has manually responded to meeting request
2859
	 * @param resource $store          user store
2860
	 * @param bool     $isDelegate     true if delegate is processing this meeting request
2861
	 */
2862
	public function acceptException(&$recurringItem, &$occurrenceItem, $basedate, $move, $tentative, $userAction, $store, $isDelegate = false): void {
2863
		$recurr = new Recurrence($store, $recurringItem);
2864
2865
		// Copy properties from meeting request
2866
		$exception_props = mapi_getprops($occurrenceItem);
0 ignored issues
show
$occurrenceItem of type resource is incompatible with the type resource expected by parameter $any of mapi_getprops(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2866
		$exception_props = mapi_getprops(/** @scrutinizer ignore-type */ $occurrenceItem);
Loading history...
2867
2868
		// Copy recipients list
2869
		$reciptable = mapi_message_getrecipienttable($occurrenceItem);
0 ignored issues
show
$occurrenceItem of type resource is incompatible with the type resource expected by parameter $msg of mapi_message_getrecipienttable(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2869
		$reciptable = mapi_message_getrecipienttable(/** @scrutinizer ignore-type */ $occurrenceItem);
Loading history...
2870
		// If delegate, then do not add the delegate in recipients
2871
		if ($isDelegate) {
2872
			$delegate = mapi_getprops($this->message, [PR_RECEIVED_BY_EMAIL_ADDRESS]);
2873
			$res = [
2874
				RES_PROPERTY,
2875
				[
2876
					RELOP => RELOP_NE,
2877
					ULPROPTAG => PR_EMAIL_ADDRESS,
2878
					VALUE => [PR_EMAIL_ADDRESS => $delegate[PR_RECEIVED_BY_EMAIL_ADDRESS]],
2879
				],
2880
			];
2881
			$recips = mapi_table_queryallrows($reciptable, $this->recipprops, $res);
2882
		}
2883
		else {
2884
			$recips = mapi_table_queryallrows($reciptable, $this->recipprops);
2885
		}
2886
2887
		// add owner to recipient table
2888
		$this->addOrganizer($exception_props, $recips, true);
2889
2890
		// add delegator to meetings
2891
		if ($isDelegate) {
2892
			$this->addDelegator($exception_props, $recips);
2893
		}
2894
2895
		$exception_props[$this->proptags['meetingstatus']] = olMeetingReceived;
2896
		$exception_props[$this->proptags['responsestatus']] = $userAction ? ($tentative ? olResponseTentative : olResponseAccepted) : olResponseNotResponded;
2897
2898
		if (isset($exception_props[$this->proptags['intendedbusystatus']])) {
2899
			if ($tentative && $exception_props[$this->proptags['intendedbusystatus']] !== fbFree) {
2900
				$exception_props[$this->proptags['busystatus']] = fbTentative;
2901
			}
2902
			else {
2903
				$exception_props[$this->proptags['busystatus']] = $exception_props[$this->proptags['intendedbusystatus']];
2904
			}
2905
			// we already have intendedbusystatus value in $exception_props so no need to copy it
2906
		}
2907
		else {
2908
			$exception_props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
2909
		}
2910
2911
		if ($userAction) {
2912
			$addrInfo = $this->getOwnerAddress($this->store);
2913
2914
			// if user has responded then set replytime and name
2915
			$exception_props[$this->proptags['replytime']] = time();
2916
			if (!empty($addrInfo)) {
2917
				$exception_props[$this->proptags['apptreplyname']] = $addrInfo[0];
2918
			}
2919
		}
2920
2921
		if ($recurr->isException($basedate)) {
2922
			$recurr->modifyException($exception_props, $basedate, $recips, $occurrenceItem);
2923
		}
2924
		else {
2925
			$recurr->createException($exception_props, $basedate, false, $recips, $occurrenceItem);
2926
		}
2927
2928
		// Move the occurrenceItem to the waste basket
2929
		if ($move) {
2930
			$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
2931
			$sourcefolder = mapi_msgstore_openentry($store, $exception_props[PR_PARENT_ENTRYID]);
0 ignored issues
show
$store of type resource is incompatible with the type resource expected by parameter $store of mapi_msgstore_openentry(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2931
			$sourcefolder = mapi_msgstore_openentry(/** @scrutinizer ignore-type */ $store, $exception_props[PR_PARENT_ENTRYID]);
Loading history...
2932
			mapi_folder_copymessages($sourcefolder, [$exception_props[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
0 ignored issues
show
$wastebasket of type resource is incompatible with the type resource expected by parameter $dstfld of mapi_folder_copymessages(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2932
			mapi_folder_copymessages($sourcefolder, [$exception_props[PR_ENTRYID]], /** @scrutinizer ignore-type */ $wastebasket, MESSAGE_MOVE);
Loading history...
2933
		}
2934
2935
		mapi_savechanges($recurringItem);
0 ignored issues
show
$recurringItem of type resource is incompatible with the type resource expected by parameter $any of mapi_savechanges(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2935
		mapi_savechanges(/** @scrutinizer ignore-type */ $recurringItem);
Loading history...
2936
	}
2937
2938
	/**
2939
	 * Function which merges an exception mapi message to recurring message.
2940
	 * This will be used when we receive recurring meeting request and we already have an exception message
2941
	 * of same meeting in calendar and we need to remove that exception message and add it to attachment table
2942
	 * of recurring meeting.
2943
	 *
2944
	 * @param resource $recurringItem  reference to MAPI_message of recurring item
2945
	 * @param resource $occurrenceItem reference to MAPI_message of occurrence
2946
	 * @param mixed    $basedate       basedate of occurrence
2947
	 * @param resource $store          user store
2948
	 */
2949
	public function mergeException(&$recurringItem, &$occurrenceItem, $basedate, $store): void {
2950
		$recurr = new Recurrence($store, $recurringItem);
2951
2952
		// Copy properties from meeting request
2953
		$exception_props = mapi_getprops($occurrenceItem);
0 ignored issues
show
$occurrenceItem of type resource is incompatible with the type resource expected by parameter $any of mapi_getprops(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2953
		$exception_props = mapi_getprops(/** @scrutinizer ignore-type */ $occurrenceItem);
Loading history...
2954
2955
		// Get recipient list from message and add it to exception attachment
2956
		$reciptable = mapi_message_getrecipienttable($occurrenceItem);
0 ignored issues
show
$occurrenceItem of type resource is incompatible with the type resource expected by parameter $msg of mapi_message_getrecipienttable(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2956
		$reciptable = mapi_message_getrecipienttable(/** @scrutinizer ignore-type */ $occurrenceItem);
Loading history...
2957
		$recips = mapi_table_queryallrows($reciptable, $this->recipprops);
2958
2959
		if ($recurr->isException($basedate)) {
2960
			$recurr->modifyException($exception_props, $basedate, $recips, $occurrenceItem);
2961
		}
2962
		else {
2963
			$recurr->createException($exception_props, $basedate, false, $recips, $occurrenceItem);
2964
		}
2965
2966
		// Move the occurrenceItem to the waste basket
2967
		$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
2968
		$sourcefolder = mapi_msgstore_openentry($store, $exception_props[PR_PARENT_ENTRYID]);
0 ignored issues
show
$store of type resource is incompatible with the type resource expected by parameter $store of mapi_msgstore_openentry(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2968
		$sourcefolder = mapi_msgstore_openentry(/** @scrutinizer ignore-type */ $store, $exception_props[PR_PARENT_ENTRYID]);
Loading history...
2969
		mapi_folder_copymessages($sourcefolder, [$exception_props[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
0 ignored issues
show
$wastebasket of type resource is incompatible with the type resource expected by parameter $dstfld of mapi_folder_copymessages(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2969
		mapi_folder_copymessages($sourcefolder, [$exception_props[PR_ENTRYID]], /** @scrutinizer ignore-type */ $wastebasket, MESSAGE_MOVE);
Loading history...
2970
2971
		mapi_savechanges($recurringItem);
0 ignored issues
show
$recurringItem of type resource is incompatible with the type resource expected by parameter $any of mapi_savechanges(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2971
		mapi_savechanges(/** @scrutinizer ignore-type */ $recurringItem);
Loading history...
2972
	}
2973
2974
	/**
2975
	 * Function which submits meeting request based on arguments passed to it.
2976
	 *
2977
	 * @param resource $message        MAPI_message whose meeting request is to be sent
2978
	 * @param bool     $cancel         if true send request, else send cancellation
2979
	 * @param mixed    $prefix         subject prefix
2980
	 * @param mixed    $basedate       basedate for an occurrence
2981
	 * @param mixed    $recurObject    recurrence object of mr
2982
	 * @param bool     $copyExceptions When sending update mail for recurring item then we don't send exceptions in attachments
2983
	 * @param mixed    $modifiedRecips
2984
	 * @param mixed    $deletedRecips
2985
	 */
2986
	public function submitMeetingRequest($message, $cancel, $prefix, $basedate = false, $recurObject = false, $copyExceptions = true, $modifiedRecips = false, $deletedRecips = false): void {
2987
		$newmessageprops = $messageprops = mapi_getprops($this->message);
2988
		$new = $this->createOutgoingMessage();
2989
2990
		// Copy the entire message into the new meeting request message
2991
		if ($basedate) {
2992
			// messageprops contains properties of whole recurring series
2993
			// and newmessageprops contains properties of exception item
2994
			$newmessageprops = mapi_getprops($message);
0 ignored issues
show
$message of type resource is incompatible with the type resource expected by parameter $any of mapi_getprops(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2994
			$newmessageprops = mapi_getprops(/** @scrutinizer ignore-type */ $message);
Loading history...
2995
2996
			// Ensure that the correct basedate is set in the new message
2997
			$newmessageprops[$this->proptags['basedate']] = $basedate;
2998
2999
			// Set isRecurring to false, because this is an exception
3000
			$newmessageprops[$this->proptags['recurring']] = false;
3001
3002
			// set LID_IS_EXCEPTION to true
3003
			$newmessageprops[$this->proptags['is_exception']] = true;
3004
3005
			// Set to high importance
3006
			if ($cancel) {
3007
				$newmessageprops[PR_IMPORTANCE] = IMPORTANCE_HIGH;
3008
			}
3009
3010
			// Set startdate and enddate of exception
3011
			if ($cancel && $recurObject) {
3012
				$newmessageprops[$this->proptags['startdate']] = $recurObject->getOccurrenceStart($basedate);
3013
				$newmessageprops[$this->proptags['duedate']] = $recurObject->getOccurrenceEnd($basedate);
3014
			}
3015
3016
			// Set basedate in guid (0x3)
3017
			$newmessageprops[$this->proptags['goid']] = $this->setBasedateInGlobalID($messageprops[$this->proptags['goid2']], $basedate);
3018
			$newmessageprops[$this->proptags['goid2']] = $messageprops[$this->proptags['goid2']];
3019
			$newmessageprops[PR_OWNER_APPT_ID] = $messageprops[PR_OWNER_APPT_ID];
3020
3021
			// Get deleted recipiets from exception msg
3022
			$restriction = [
3023
				RES_AND,
3024
				[
3025
					[
3026
						RES_BITMASK,
3027
						[
3028
							ULTYPE => BMR_NEZ,
3029
							ULPROPTAG => PR_RECIPIENT_FLAGS,
3030
							ULMASK => recipExceptionalDeleted,
3031
						],
3032
					],
3033
					[
3034
						RES_BITMASK,
3035
						[
3036
							ULTYPE => BMR_EQZ,
3037
							ULPROPTAG => PR_RECIPIENT_FLAGS,
3038
							ULMASK => recipOrganizer,
3039
						],
3040
					],
3041
				],
3042
			];
3043
3044
			// In direct-booking mode, we don't need to send cancellations to resources
3045
			if ($this->enableDirectBooking) {
3046
				$restriction[1][] = [
3047
					RES_PROPERTY,
3048
					[
3049
						RELOP => RELOP_NE,	// Does not equal recipient type: MAPI_BCC (Resource)
3050
						ULPROPTAG => PR_RECIPIENT_TYPE,
3051
						VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
3052
					],
3053
				];
3054
			}
3055
3056
			$recipienttable = mapi_message_getrecipienttable($message);
0 ignored issues
show
$message of type resource is incompatible with the type resource expected by parameter $msg of mapi_message_getrecipienttable(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3056
			$recipienttable = mapi_message_getrecipienttable(/** @scrutinizer ignore-type */ $message);
Loading history...
3057
			$recipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $restriction);
3058
3059
			if (!$deletedRecips) {
3060
				$deletedRecips = array_merge([], $recipients);
3061
			}
3062
			else {
3063
				$deletedRecips = array_merge($deletedRecips, $recipients);
3064
			}
3065
		}
3066
3067
		// Remove the PR_ICON_INDEX as it is not needed in the sent message.
3068
		$newmessageprops[PR_ICON_INDEX] = null;
3069
		$newmessageprops[PR_RESPONSE_REQUESTED] = true;
3070
3071
		// PR_START_DATE and PR_END_DATE will be used by outlook to show the position in the calendar
3072
		$newmessageprops[PR_START_DATE] = $newmessageprops[$this->proptags['startdate']];
3073
		$newmessageprops[PR_END_DATE] = $newmessageprops[$this->proptags['duedate']];
3074
3075
		// Set updatecounter/AppointmentSequenceNumber
3076
		// get the value of latest updatecounter for the whole series and use it
3077
		$newmessageprops[$this->proptags['updatecounter']] = $messageprops[$this->proptags['last_updatecounter']];
3078
3079
		$meetingTimeInfo = $this->getMeetingTimeInfo();
3080
3081
		if ($meetingTimeInfo) {
3082
			// Needs to unset PR_HTML and PR_RTF_COMPRESSED props
3083
			// because while canceling meeting requests with edit text
3084
			// will override the PR_BODY because body value is not consistent with
3085
			// PR_HTML and PR_RTF_COMPRESSED value so in this case PR_RTF_COMPRESSED will
3086
			// get priority which override the PR_BODY value.
3087
			unset($newmessageprops[PR_HTML], $newmessageprops[PR_RTF_COMPRESSED]);
3088
3089
			$newmessageprops[PR_BODY] = $meetingTimeInfo;
3090
		}
3091
3092
		// Send all recurrence info in mail, if this is a recurrence meeting.
3093
		if (isset($messageprops[$this->proptags['recurring']]) && $messageprops[$this->proptags['recurring']]) {
3094
			if (!empty($messageprops[$this->proptags['recurring_pattern']])) {
3095
				$newmessageprops[$this->proptags['recurring_pattern']] = $messageprops[$this->proptags['recurring_pattern']];
3096
			}
3097
			$newmessageprops[$this->proptags['recurrence_data']] = $messageprops[$this->proptags['recurrence_data']];
3098
			$newmessageprops[$this->proptags['timezone_data']] = $messageprops[$this->proptags['timezone_data']];
3099
			$newmessageprops[$this->proptags['timezone']] = $messageprops[$this->proptags['timezone']];
3100
3101
			if ($recurObject) {
3102
				$this->generateRecurDates($recurObject, $messageprops, $newmessageprops);
3103
			}
3104
		}
3105
3106
		if (isset($newmessageprops[$this->proptags['counter_proposal']])) {
3107
			unset($newmessageprops[$this->proptags['counter_proposal']]);
3108
		}
3109
3110
		// Prefix the subject if needed
3111
		if ($prefix && isset($newmessageprops[PR_SUBJECT])) {
3112
			$newmessageprops[PR_SUBJECT] = $prefix . $newmessageprops[PR_SUBJECT];
3113
		}
3114
3115
		if (isset($newmessageprops[$this->proptags['categories']]) &&
3116
			!empty($newmessageprops[$this->proptags['categories']])) {
3117
			unset($newmessageprops[$this->proptags['categories']]);
3118
		}
3119
		mapi_setprops($new, $newmessageprops);
0 ignored issues
show
$new of type resource is incompatible with the type resource expected by parameter $any of mapi_setprops(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3119
		mapi_setprops(/** @scrutinizer ignore-type */ $new, $newmessageprops);
Loading history...
3120
3121
		// Copy attachments
3122
		$this->replaceAttachments($message, $new, $copyExceptions);
3123
3124
		// Retrieve only those recipient who should receive this meeting request.
3125
		$stripResourcesRestriction = [
3126
			RES_AND,
3127
			[
3128
				[
3129
					RES_BITMASK,
3130
					[
3131
						ULTYPE => BMR_EQZ,
3132
						ULPROPTAG => PR_RECIPIENT_FLAGS,
3133
						ULMASK => recipExceptionalDeleted,
3134
					],
3135
				],
3136
				[
3137
					RES_BITMASK,
3138
					[
3139
						ULTYPE => BMR_EQZ,
3140
						ULPROPTAG => PR_RECIPIENT_FLAGS,
3141
						ULMASK => recipOrganizer,
3142
					],
3143
				],
3144
			],
3145
		];
3146
3147
		// In direct-booking mode, resources do not receive a meeting request
3148
		if ($this->enableDirectBooking) {
3149
			$stripResourcesRestriction[1][] = [
3150
				RES_PROPERTY,
3151
				[
3152
					RELOP => RELOP_NE,	// Does not equal recipient type: MAPI_BCC (Resource)
3153
					ULPROPTAG => PR_RECIPIENT_TYPE,
3154
					VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
3155
				],
3156
			];
3157
		}
3158
3159
		// If no recipients were explicitly provided, we will send the update to all
3160
		// recipients from the meeting.
3161
		if ($modifiedRecips === false) {
3162
			$recipienttable = mapi_message_getrecipienttable($message);
3163
			$modifiedRecips = mapi_table_queryallrows($recipienttable, $this->recipprops, $stripResourcesRestriction);
3164
3165
			if ($basedate && empty($modifiedRecips)) {
3166
				// Retrieve full list
3167
				$recipienttable = mapi_message_getrecipienttable($this->message);
3168
				$modifiedRecips = mapi_table_queryallrows($recipienttable, $this->recipprops);
3169
3170
				// Save recipients in exceptions
3171
				mapi_message_modifyrecipients($message, MODRECIP_ADD, $modifiedRecips);
0 ignored issues
show
$message of type resource is incompatible with the type resource expected by parameter $msg of mapi_message_modifyrecipients(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3171
				mapi_message_modifyrecipients(/** @scrutinizer ignore-type */ $message, MODRECIP_ADD, $modifiedRecips);
Loading history...
3172
3173
				// Now retrieve only those recipient who should receive this meeting request.
3174
				$modifiedRecips = mapi_table_queryallrows($recipienttable, $this->recipprops, $stripResourcesRestriction);
3175
			}
3176
		}
3177
3178
		// @TODO: handle nonAcceptingResources
3179
		/*
3180
		 * Add resource recipients that did not automatically accept the meeting request.
3181
		 * (note: meaning that they did not decline the meeting request)
3182
		 */ /*
3183
		for($i=0;$i<count($this->nonAcceptingResources);$i++){
3184
			$recipients[] = $this->nonAcceptingResources[$i];
3185
		}*/
3186
3187
		if (!empty($modifiedRecips)) {
3188
			// Strip out the sender/'owner' recipient
3189
			mapi_message_modifyrecipients($new, MODRECIP_ADD, $modifiedRecips);
3190
3191
			// Set some properties that are different in the sent request than
3192
			// in the item in our calendar
3193
3194
			// we should store busystatus value to intendedbusystatus property, because busystatus for outgoing meeting request
3195
			// should always be fbTentative
3196
			$newmessageprops[$this->proptags['intendedbusystatus']] = isset($newmessageprops[$this->proptags['busystatus']]) ? $newmessageprops[$this->proptags['busystatus']] : $messageprops[$this->proptags['busystatus']];
3197
			$newmessageprops[$this->proptags['busystatus']] = fbTentative; // The default status when not accepted
3198
			$newmessageprops[$this->proptags['responsestatus']] = olResponseNotResponded; // The recipient has not responded yet
3199
			$newmessageprops[$this->proptags['attendee_critical_change']] = time();
3200
			$newmessageprops[$this->proptags['owner_critical_change']] = time();
3201
			$newmessageprops[$this->proptags['meetingtype']] = mtgRequest;
3202
3203
			if ($cancel) {
3204
				$newmessageprops[PR_MESSAGE_CLASS] = 'IPM.Schedule.Meeting.Canceled';
3205
				$newmessageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // It's a cancel request
3206
				$newmessageprops[$this->proptags['busystatus']] = fbFree; // set the busy status as free
3207
			}
3208
			else {
3209
				$newmessageprops[PR_MESSAGE_CLASS] = 'IPM.Schedule.Meeting.Request';
3210
				$newmessageprops[$this->proptags['meetingstatus']] = olMeetingReceived; // The recipient is receiving the request
3211
			}
3212
3213
			mapi_setprops($new, $newmessageprops);
3214
			mapi_savechanges($new);
0 ignored issues
show
$new of type resource is incompatible with the type resource expected by parameter $any of mapi_savechanges(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3214
			mapi_savechanges(/** @scrutinizer ignore-type */ $new);
Loading history...
3215
3216
			// Submit message to non-resource recipients
3217
			mapi_message_submitmessage($new);
0 ignored issues
show
$new of type resource is incompatible with the type resource expected by parameter $msg of mapi_message_submitmessage(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3217
			mapi_message_submitmessage(/** @scrutinizer ignore-type */ $new);
Loading history...
3218
		}
3219
3220
		// Search through the deleted recipients, and see if any of them is also
3221
		// listed as a recipient to whom we have sent an update. As we don't
3222
		// want to send a cancellation message to recipients who will also receive
3223
		// an meeting update, we have to filter those recipients out.
3224
		if ($deletedRecips) {
3225
			$tmp = [];
3226
3227
			foreach ($deletedRecips as $delRecip) {
3228
				$found = false;
3229
3230
				// Search if the deleted recipient can be found inside
3231
				// the updated recipients as well.
3232
				foreach ($modifiedRecips as $recip) {
3233
					if ($this->compareABEntryIDs($recip[PR_ENTRYID], $delRecip[PR_ENTRYID])) {
3234
						$found = true;
3235
						break;
3236
					}
3237
				}
3238
3239
				// If the recipient was not found, it truly is deleted,
3240
				// and we can safely send a cancellation message
3241
				if (!$found) {
3242
					$tmp[] = $delRecip;
3243
				}
3244
			}
3245
3246
			$deletedRecips = $tmp;
3247
		}
3248
3249
		// Send cancellation to deleted attendees
3250
		if ($deletedRecips) {
3251
			$new = $this->createOutgoingMessage();
3252
3253
			mapi_message_modifyrecipients($new, MODRECIP_ADD, $deletedRecips);
3254
3255
			$newmessageprops[PR_MESSAGE_CLASS] = 'IPM.Schedule.Meeting.Canceled';
3256
			$newmessageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // It's a cancel request
3257
			$newmessageprops[$this->proptags['busystatus']] = fbFree; // set the busy status as free
3258
			$newmessageprops[PR_IMPORTANCE] = IMPORTANCE_HIGH;	// HIGH Importance
3259
			if (isset($newmessageprops[PR_SUBJECT])) {
3260
				$newmessageprops[PR_SUBJECT] = dgettext('zarafa', 'Canceled') . ': ' . $newmessageprops[PR_SUBJECT];
3261
			}
3262
3263
			mapi_setprops($new, $newmessageprops);
3264
			mapi_savechanges($new);
3265
3266
			// Submit message to non-resource recipients
3267
			mapi_message_submitmessage($new);
3268
		}
3269
3270
		// Set properties on meeting object in calendar
3271
		// Set requestsent to 'true' (turns on 'tracking', etc)
3272
		$props = [];
3273
		$props[$this->proptags['meetingstatus']] = olMeeting;
3274
		$props[$this->proptags['responsestatus']] = olResponseOrganized;
3275
		// Only set the 'requestsent' property if it wasn't set previously yet,
3276
		// this ensures we will not accidentally set it from true to false.
3277
		if (!isset($messageprops[$this->proptags['requestsent']]) || $messageprops[$this->proptags['requestsent']] !== true) {
3278
			$props[$this->proptags['requestsent']] = !empty($modifiedRecips) || ($this->includesResources && !$this->errorSetResource);
3279
		}
3280
		$props[$this->proptags['attendee_critical_change']] = time();
3281
		$props[$this->proptags['owner_critical_change']] = time();
3282
		$props[$this->proptags['meetingtype']] = mtgRequest;
3283
		// save the new updatecounter to exception/recurring series/normal meeting
3284
		$props[$this->proptags['updatecounter']] = $newmessageprops[$this->proptags['updatecounter']];
3285
3286
		// PR_START_DATE and PR_END_DATE will be used by outlook to show the position in the calendar
3287
		$props[PR_START_DATE] = $messageprops[$this->proptags['startdate']];
3288
		$props[PR_END_DATE] = $messageprops[$this->proptags['duedate']];
3289
3290
		mapi_setprops($message, $props);
3291
3292
		// saving of these properties on calendar item should be handled by caller function
3293
		// based on sending meeting request was successful or not
3294
	}
3295
3296
	/**
3297
	 * OL2007 uses these 4 properties to specify occurrence that should be updated.
3298
	 * ical generates RECURRENCE-ID property based on exception's basedate (PidLidExceptionReplaceTime),
3299
	 * but OL07 doesn't send this property, so ical will generate RECURRENCE-ID property based on date
3300
	 * from GlobalObjId and time from StartRecurTime property, so we are sending basedate property and
3301
	 * also additionally we are sending these properties.
3302
	 * Ref: MS-OXCICAL 2.2.1.20.20 Property: RECURRENCE-ID.
3303
	 *
3304
	 * @param object $recurObject     instance of recurrence class for this message
3305
	 * @param array  $messageprops    properties of meeting object that is going to be sent
3306
	 * @param array  $newmessageprops properties of meeting request/response that is going to be sent
3307
	 */
3308
	public function generateRecurDates($recurObject, $messageprops, &$newmessageprops): void {
3309
		if ($messageprops[$this->proptags['startdate']] && $messageprops[$this->proptags['duedate']]) {
3310
			$startDate = date('Y:n:j:G:i:s', $recurObject->fromGMT($recurObject->tz, $messageprops[$this->proptags['startdate']]));
3311
			$endDate = date('Y:n:j:G:i:s', $recurObject->fromGMT($recurObject->tz, $messageprops[$this->proptags['duedate']]));
3312
3313
			$startDate = explode(':', $startDate);
3314
			$endDate = explode(':', $endDate);
3315
3316
			// [0] => year, [1] => month, [2] => day, [3] => hour, [4] => minutes, [5] => seconds
3317
			// RecurStartDate = year * 512 + month_number * 32 + day_number
3318
			$newmessageprops[$this->proptags['start_recur_date']] = (((int) $startDate[0]) * 512) + (((int) $startDate[1]) * 32) + ((int) $startDate[2]);
3319
			// RecurStartTime = hour * 4096 + minutes * 64 + seconds
3320
			$newmessageprops[$this->proptags['start_recur_time']] = (((int) $startDate[3]) * 4096) + (((int) $startDate[4]) * 64) + ((int) $startDate[5]);
3321
3322
			$newmessageprops[$this->proptags['end_recur_date']] = (((int) $endDate[0]) * 512) + (((int) $endDate[1]) * 32) + ((int) $endDate[2]);
3323
			$newmessageprops[$this->proptags['end_recur_time']] = (((int) $endDate[3]) * 4096) + (((int) $endDate[4]) * 64) + ((int) $endDate[5]);
3324
		}
3325
	}
3326
3327
	/**
3328
	 * Function will create a new outgoing message that will be used to send meeting mail.
3329
	 *
3330
	 * @param mixed $store (optional) store that is used when creating response, if delegate is creating outgoing mail
3331
	 *                     then this would point to delegate store
3332
	 *
3333
	 * @return resource outgoing mail that is created and can be used for sending it
3334
	 */
3335
	public function createOutgoingMessage($store = false) {
3336
		// get logged in user's store that will be used to send mail, for delegate this will be
3337
		// delegate store
3338
		$userStore = $this->openDefaultStore();
3339
3340
		$sentprops = [];
3341
		$outbox = $this->openDefaultOutbox($userStore);
3342
3343
		$outgoing = mapi_folder_createmessage($outbox);
0 ignored issues
show
$outbox of type resource is incompatible with the type resource expected by parameter $fld of mapi_folder_createmessage(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3343
		$outgoing = mapi_folder_createmessage(/** @scrutinizer ignore-type */ $outbox);
Loading history...
3344
3345
		// check if $store is set and it is not equal to $defaultStore (means its the delegation case)
3346
		if ($store !== false) {
3347
			$storeProps = mapi_getprops($store, [PR_ENTRYID]);
3348
			$userStoreProps = mapi_getprops($userStore, [PR_ENTRYID]);
0 ignored issues
show
It seems like $userStore can also be of type false; however, parameter $any of mapi_getprops() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3348
			$userStoreProps = mapi_getprops(/** @scrutinizer ignore-type */ $userStore, [PR_ENTRYID]);
Loading history...
3349
3350
			// @FIXME use entryid comparison functions here
3351
			if ($storeProps[PR_ENTRYID] !== $userStoreProps[PR_ENTRYID]) {
3352
				// get the delegator properties and set it into outgoing mail
3353
				$delegatorDetails = $this->getOwnerAddress($store, false);
3354
3355
				if (!empty($delegatorDetails)) {
3356
					list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $delegatorDetails;
3357
					$sentprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $owneremailaddr;
3358
					$sentprops[PR_SENT_REPRESENTING_NAME] = $ownername;
3359
					$sentprops[PR_SENT_REPRESENTING_ADDRTYPE] = $owneraddrtype;
3360
					$sentprops[PR_SENT_REPRESENTING_ENTRYID] = $ownerentryid;
3361
					$sentprops[PR_SENT_REPRESENTING_SEARCH_KEY] = $ownersearchkey;
3362
				}
3363
3364
				// get the delegate properties and set it into outgoing mail
3365
				$delegateDetails = $this->getOwnerAddress($userStore, false);
3366
3367
				if (!empty($delegateDetails)) {
3368
					list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $delegateDetails;
3369
					$sentprops[PR_SENDER_EMAIL_ADDRESS] = $owneremailaddr;
3370
					$sentprops[PR_SENDER_NAME] = $ownername;
3371
					$sentprops[PR_SENDER_ADDRTYPE] = $owneraddrtype;
3372
					$sentprops[PR_SENDER_ENTRYID] = $ownerentryid;
3373
					$sentprops[PR_SENDER_SEARCH_KEY] = $ownersearchkey;
3374
				}
3375
			}
3376
		}
3377
		else {
3378
			// normal user is sending mail, so both set of properties will be same
3379
			$userDetails = $this->getOwnerAddress($userStore);
3380
3381
			if (!empty($userDetails)) {
3382
				list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $userDetails;
3383
				$sentprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $owneremailaddr;
3384
				$sentprops[PR_SENT_REPRESENTING_NAME] = $ownername;
3385
				$sentprops[PR_SENT_REPRESENTING_ADDRTYPE] = $owneraddrtype;
3386
				$sentprops[PR_SENT_REPRESENTING_ENTRYID] = $ownerentryid;
3387
				$sentprops[PR_SENT_REPRESENTING_SEARCH_KEY] = $ownersearchkey;
3388
3389
				$sentprops[PR_SENDER_EMAIL_ADDRESS] = $owneremailaddr;
3390
				$sentprops[PR_SENDER_NAME] = $ownername;
3391
				$sentprops[PR_SENDER_ADDRTYPE] = $owneraddrtype;
3392
				$sentprops[PR_SENDER_ENTRYID] = $ownerentryid;
3393
				$sentprops[PR_SENDER_SEARCH_KEY] = $ownersearchkey;
3394
			}
3395
		}
3396
3397
		$sentprops[PR_SENTMAIL_ENTRYID] = $this->getDefaultSentmailEntryID($userStore);
3398
3399
		mapi_setprops($outgoing, $sentprops);
3400
3401
		return $outgoing;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $outgoing returns the type resource which is incompatible with the documented return type resource.
Loading history...
3402
	}
3403
3404
	/**
3405
	 * Function which checks that meeting in attendee's calendar is already updated
3406
	 * and we are checking an old meeting request. This function also will update property
3407
	 * meetingtype to indicate that its out of date meeting request.
3408
	 *
3409
	 * @return bool true if meeting request is outofdate else false if it is new
3410
	 */
3411
	public function isMeetingOutOfDate() {
3412
		$result = false;
3413
3414
		$props = mapi_getprops($this->message, [PR_MESSAGE_CLASS, $this->proptags['goid'], $this->proptags['goid2'], $this->proptags['updatecounter'], $this->proptags['meetingtype'], $this->proptags['owner_critical_change']]);
3415
3416
		if (!$this->isMeetingRequest($props[PR_MESSAGE_CLASS])) {
3417
			return $result;
3418
		}
3419
3420
		if (isset($props[$this->proptags['meetingtype']]) && ($props[$this->proptags['meetingtype']] & mtgOutOfDate) == mtgOutOfDate) {
3421
			return true;
3422
		}
3423
3424
		// get the basedate to check for exception
3425
		$basedate = $this->getBasedateFromGlobalID($props[$this->proptags['goid']]);
3426
3427
		$calendarItem = $this->getCorrespondentCalendarItem(true);
3428
3429
		// if basedate is provided and we could not find the item then it could be that we are checking
3430
		// an exception so get the exception and check it
3431
		if ($basedate !== false && $calendarItem !== false) {
3432
			$exception = $this->getExceptionItem($calendarItem, $basedate);
3433
3434
			if ($exception !== false) {
0 ignored issues
show
The condition $exception !== false is always true.
Loading history...
3435
				// we are able to find the exception compare with it
3436
				$calendarItem = $exception;
3437
			}
3438
			// we are not able to find exception, could mean that a significant change has occurred on series
3439
			// and it deleted all exceptions, so compare with series
3440
			// $calendarItem already contains reference to series
3441
		}
3442
3443
		if ($calendarItem !== false) {
3444
			$calendarItemProps = mapi_getprops($calendarItem, [
0 ignored issues
show
It seems like $calendarItem can also be of type resource and true; however, parameter $any of mapi_getprops() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3444
			$calendarItemProps = mapi_getprops(/** @scrutinizer ignore-type */ $calendarItem, [
Loading history...
3445
				$this->proptags['owner_critical_change'],
3446
				$this->proptags['updatecounter'],
3447
			]);
3448
3449
			$updateCounter = (isset($calendarItemProps[$this->proptags['updatecounter']]) && $props[$this->proptags['updatecounter']] < $calendarItemProps[$this->proptags['updatecounter']]);
3450
3451
			$criticalChange = (isset($calendarItemProps[$this->proptags['owner_critical_change']]) && $props[$this->proptags['owner_critical_change']] < $calendarItemProps[$this->proptags['owner_critical_change']]);
3452
3453
			if ($updateCounter || $criticalChange) {
3454
				// meeting request is out of date, set properties to indicate this
3455
				mapi_setprops($this->message, [$this->proptags['meetingtype'] => mtgOutOfDate, PR_ICON_INDEX => 1033]);
3456
				mapi_savechanges($this->message);
3457
3458
				$result = true;
3459
			}
3460
		}
3461
3462
		return $result;
3463
	}
3464
3465
	/**
3466
	 * Function which checks that if we have received a meeting response for an updated meeting in organizer's calendar.
3467
	 *
3468
	 * @param mixed $basedate basedate of the exception if we want to compare with exception
3469
	 *
3470
	 * @return bool true if meeting request is updated later
3471
	 */
3472
	public function isMeetingUpdated($basedate = false) {
3473
		$result = false;
3474
3475
		$props = mapi_getprops($this->message, [PR_MESSAGE_CLASS, $this->proptags['updatecounter']]);
3476
3477
		if (!$this->isMeetingRequestResponse($props[PR_MESSAGE_CLASS])) {
3478
			return $result;
3479
		}
3480
3481
		$calendarItem = $this->getCorrespondentCalendarItem(true);
3482
3483
		if ($calendarItem !== false) {
3484
			// basedate is provided so open exception
3485
			if ($basedate !== false) {
3486
				$exception = $this->getExceptionItem($calendarItem, $basedate);
3487
3488
				if ($exception !== false) {
0 ignored issues
show
The condition $exception !== false is always true.
Loading history...
3489
					// we are able to find the exception compare with it
3490
					$calendarItem = $exception;
3491
				}
3492
				// we are not able to find exception, could mean that a significant change has occurred on series
3493
				// and it deleted all exceptions, so compare with series
3494
				// $calendarItem already contains reference to series
3495
			}
3496
3497
			if ($calendarItem !== false) {
0 ignored issues
show
The condition $calendarItem !== false is always true.
Loading history...
3498
				$calendarItemProps = mapi_getprops($calendarItem, [$this->proptags['updatecounter']]);
0 ignored issues
show
It seems like $calendarItem can also be of type resource and true; however, parameter $any of mapi_getprops() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3498
				$calendarItemProps = mapi_getprops(/** @scrutinizer ignore-type */ $calendarItem, [$this->proptags['updatecounter']]);
Loading history...
3499
3500
				/*
3501
				 * if(message_counter < appointment_counter) meeting object is newer then meeting response (meeting is updated)
3502
				 * if(message_counter >= appointment_counter) meeting is not updated, do normal processing
3503
				 */
3504
				if (isset($calendarItemProps[$this->proptags['updatecounter']], $props[$this->proptags['updatecounter']])) {
3505
					if ($props[$this->proptags['updatecounter']] < $calendarItemProps[$this->proptags['updatecounter']]) {
3506
						$result = true;
3507
					}
3508
				}
3509
			}
3510
		}
3511
3512
		return $result;
3513
	}
3514
3515
	/**
3516
	 * Checks if there has been any significant changes on appointment/meeting item.
3517
	 * Significant changes be:
3518
	 * 1) startdate has been changed
3519
	 * 2) duedate has been changed OR
3520
	 * 3) recurrence pattern has been created, modified or removed.
3521
	 *
3522
	 * @param mixed $oldProps
3523
	 * @param mixed $basedate
3524
	 * @param mixed $isRecurrenceChanged for change in recurrence pattern.
3525
	 *                                   true means Recurrence pattern has been changed,
3526
	 *                                   so clear all attendees response
3527
	 */
3528
	public function checkSignificantChanges($oldProps, $basedate, $isRecurrenceChanged = false) {
3529
		$message = null;
3530
		$attach = null;
3531
3532
		// If basedate is specified then we need to open exception message to clear recipient responses
3533
		if ($basedate) {
3534
			$recurrence = new Recurrence($this->store, $this->message);
3535
			if ($recurrence->isException($basedate)) {
3536
				$attach = $recurrence->getExceptionAttachment($basedate);
3537
				if ($attach) {
3538
					$message = mapi_attach_openobj($attach, MAPI_MODIFY);
3539
				}
3540
			}
3541
		}
3542
		else {
3543
			// use normal message or recurring series message
3544
			$message = $this->message;
3545
		}
3546
3547
		if (!$message) {
3548
			return;
3549
		}
3550
3551
		$newProps = mapi_getprops($message, [$this->proptags['startdate'], $this->proptags['duedate'], $this->proptags['updatecounter']]);
3552
3553
		// Check whether message is updated or not.
3554
		if (isset($newProps[$this->proptags['updatecounter']]) && $newProps[$this->proptags['updatecounter']] == 0) {
3555
			return;
3556
		}
3557
3558
		if (($newProps[$this->proptags['startdate']] != $oldProps[$this->proptags['startdate']]) ||
3559
				($newProps[$this->proptags['duedate']] != $oldProps[$this->proptags['duedate']]) ||
3560
				$isRecurrenceChanged) {
3561
			$this->clearRecipientResponse($message);
0 ignored issues
show
It seems like $message can also be of type resource; however, parameter $message of Meetingrequest::clearRecipientResponse() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3561
			$this->clearRecipientResponse(/** @scrutinizer ignore-type */ $message);
Loading history...
3562
3563
			mapi_setprops($message, [$this->proptags['owner_critical_change'] => time()]);
3564
3565
			mapi_savechanges($message);
3566
			if ($attach) { // Also save attachment Object.
3567
				mapi_savechanges($attach);
3568
			}
3569
		}
3570
	}
3571
3572
	/**
3573
	 * Clear responses of all attendees who have replied in past.
3574
	 *
3575
	 * @param resource $message on which responses should be cleared
3576
	 */
3577
	public function clearRecipientResponse($message): void {
3578
		$recipTable = mapi_message_getrecipienttable($message);
0 ignored issues
show
$message of type resource is incompatible with the type resource expected by parameter $msg of mapi_message_getrecipienttable(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3578
		$recipTable = mapi_message_getrecipienttable(/** @scrutinizer ignore-type */ $message);
Loading history...
3579
		$recipsRows = mapi_table_queryallrows($recipTable, $this->recipprops);
3580
3581
		foreach ($recipsRows as $recipient) {
3582
			if (($recipient[PR_RECIPIENT_FLAGS] & recipOrganizer) != recipOrganizer) {
3583
				// Recipient is attendee, set the trackstatus to 'Not Responded'
3584
				$recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
3585
			}
3586
			else {
3587
				// Recipient is organizer, this is not possible, but for safety
3588
				// it is best to clear the trackstatus for him as well by setting
3589
				// the trackstatus to 'Organized'.
3590
				$recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
3591
			}
3592
			mapi_message_modifyrecipients($message, MODRECIP_MODIFY, [$recipient]);
0 ignored issues
show
$message of type resource is incompatible with the type resource expected by parameter $msg of mapi_message_modifyrecipients(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3592
			mapi_message_modifyrecipients(/** @scrutinizer ignore-type */ $message, MODRECIP_MODIFY, [$recipient]);
Loading history...
3593
		}
3594
	}
3595
3596
	/**
3597
	 * Function returns correspondent calendar item attached with the meeting request/response/cancellation.
3598
	 * This will only check for actual MAPIMessages in calendar folder, so if a meeting request is
3599
	 * for exception then this function will return recurring series for that meeting request
3600
	 * after that you need to use getExceptionItem function to get exception item that will be
3601
	 * fetched from the attachment table of recurring series MAPIMessage.
3602
	 *
3603
	 * @param bool $open boolean to indicate the function should return entryid or MAPIMessage. Defaults to true.
3604
	 *
3605
	 * @return bool|resource resource of calendar item
3606
	 */
3607
	public function getCorrespondentCalendarItem($open = true) {
3608
		$props = mapi_getprops($this->message, [PR_MESSAGE_CLASS, $this->proptags['goid'], $this->proptags['goid2'], PR_RCVD_REPRESENTING_ENTRYID]);
3609
3610
		if (!$this->isMeetingRequest($props[PR_MESSAGE_CLASS]) && !$this->isMeetingRequestResponse($props[PR_MESSAGE_CLASS]) && !$this->isMeetingCancellation($props[PR_MESSAGE_CLASS])) {
3611
			// can work only with meeting requests/responses/cancellations
3612
			return false;
3613
		}
3614
3615
		// there is no goid - no items can be found - aborting
3616
		if (empty($props[$this->proptags['goid']])) {
3617
			return false;
3618
		}
3619
		$globalId = $props[$this->proptags['goid']];
3620
3621
		$store = $this->store;
3622
		$calFolder = $this->openDefaultCalendar();
3623
		// If Delegate is processing Meeting Request/Response for Delegator then retrieve Delegator's store and calendar.
3624
		if (isset($props[PR_RCVD_REPRESENTING_ENTRYID])) {
3625
			$delegatorStore = $this->getDelegatorStore($props[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
3626
			if (!empty($delegatorStore['store'])) {
3627
				$store = $delegatorStore['store'];
3628
			}
3629
			if (!empty($delegatorStore[PR_IPM_APPOINTMENT_ENTRYID])) {
3630
				$calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
3631
			}
3632
		}
3633
3634
		$basedate = $this->getBasedateFromGlobalID($globalId);
3635
3636
		/**
3637
		 * First search for any appointments which correspond to the $globalId,
3638
		 * this can be the entire series (if the Meeting Request refers to the
3639
		 * entire series), or an particular Occurrence (if the meeting Request
3640
		 * contains a basedate).
3641
		 *
3642
		 * If we cannot find a corresponding item, and the $globalId contains
3643
		 * a $basedate, it might imply that a new exception will have to be
3644
		 * created for a series which is present in the calendar, we can look
3645
		 * that one up by searching for the $cleanGlobalId.
3646
		 */
3647
		$entryids = $this->findCalendarItems($globalId, $calFolder);
3648
		if ($basedate !== false && empty($entryids)) {
3649
			// only search if a goid2 is available
3650
			if (!empty($props[$this->proptags['goid2']])) {
3651
				$cleanGlobalId = $props[$this->proptags['goid2']];
3652
				$entryids = $this->findCalendarItems($cleanGlobalId, $calFolder, true);
3653
			}
3654
		}
3655
3656
		// there should be only one item returned
3657
		if (!empty($entryids) && count($entryids) === 1) {
3658
			// return only entryid
3659
			if ($open === false) {
3660
				return $entryids[0];
3661
			}
3662
3663
			// open calendar item and return it
3664
			if ($store) {
3665
				return mapi_msgstore_openentry($store, $entryids[0]);
0 ignored issues
show
Bug Best Practice introduced by
The expression return mapi_msgstore_ope...y($store, $entryids[0]) returns the type resource which is incompatible with the documented return type boolean|resource.
Loading history...
It seems like $store can also be of type resource; however, parameter $store of mapi_msgstore_openentry() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3665
				return mapi_msgstore_openentry(/** @scrutinizer ignore-type */ $store, $entryids[0]);
Loading history...
3666
			}
3667
		}
3668
3669
		// no items found in calendar
3670
		return false;
3671
	}
3672
3673
	/**
3674
	 * Function returns exception item based on the basedate passed.
3675
	 *
3676
	 * @param mixed $recurringMessage Resource of Recurring meeting from calendar
3677
	 * @param mixed $basedate         basedate of exception that needs to be returned
3678
	 * @param mixed $store            store that contains the recurring calendar item
3679
	 *
3680
	 * @return entryid or MAPIMessage resource of exception item
0 ignored issues
show
The type entryid was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
3681
	 */
3682
	public function getExceptionItem($recurringMessage, $basedate, $store = false) {
3683
		$occurItem = false;
3684
3685
		$props = mapi_getprops($this->message, [PR_RCVD_REPRESENTING_ENTRYID, $this->proptags['recurring']]);
3686
3687
		// check if the passed item is recurring series
3688
		if (isset($props[$this->proptags['recurring']]) && $props[$this->proptags['recurring']] !== false) {
3689
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type entryid.
Loading history...
3690
		}
3691
3692
		if ($store === false) {
3693
			$store = $this->store;
3694
			// If Delegate is processing Meeting Request/Response for Delegator then retrieve Delegator's store and calendar.
3695
			if (isset($props[PR_RCVD_REPRESENTING_ENTRYID])) {
3696
				$delegatorStore = $this->getDelegatorStore($props[PR_RCVD_REPRESENTING_ENTRYID]);
3697
				if (!empty($delegatorStore['store'])) {
3698
					$store = $delegatorStore['store'];
3699
				}
3700
			}
3701
		}
3702
3703
		$recurr = new Recurrence($store, $recurringMessage);
3704
		$attach = $recurr->getExceptionAttachment($basedate);
3705
		if ($attach) {
3706
			$occurItem = mapi_attach_openobj($attach);
3707
		}
3708
3709
		return $occurItem;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $occurItem returns the type false|resource which is incompatible with the documented return type entryid.
Loading history...
3710
	}
3711
3712
	/**
3713
	 * Function which checks whether received meeting request is either conflicting with other appointments or not.
3714
	 *
3715
	 * @param false|resource $message
3716
	 * @param false|resource $userStore
3717
	 * @param mixed          $calFolder calendar folder for conflict checking
3718
	 *
3719
	 * @return bool|int
3720
	 *
3721
	 * @psalm-return bool|int<1, max>
3722
	 */
3723
	public function isMeetingConflicting($message = false, $userStore = false, $calFolder = false) {
3724
		$returnValue = false;
3725
		$noOfInstances = 0;
3726
3727
		if ($message === false) {
3728
			$message = $this->message;
3729
		}
3730
3731
		$messageProps = mapi_getprops(
3732
			$message,
0 ignored issues
show
It seems like $message can also be of type resource; however, parameter $any of mapi_getprops() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3732
			/** @scrutinizer ignore-type */ $message,
Loading history...
3733
			[
3734
				PR_MESSAGE_CLASS,
3735
				$this->proptags['goid'],
3736
				$this->proptags['goid2'],
3737
				$this->proptags['startdate'],
3738
				$this->proptags['duedate'],
3739
				$this->proptags['recurring'],
3740
				$this->proptags['clipstart'],
3741
				$this->proptags['clipend'],
3742
				PR_RCVD_REPRESENTING_ENTRYID,
3743
				$this->proptags['basedate'],
3744
				PR_RCVD_REPRESENTING_NAME,
3745
			]
3746
		);
3747
3748
		if ($userStore === false) {
3749
			$userStore = $this->store;
3750
3751
			// check if delegate is processing the response
3752
			if (isset($messageProps[PR_RCVD_REPRESENTING_ENTRYID])) {
3753
				$delegatorStore = $this->getDelegatorStore($messageProps[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
3754
3755
				if (!empty($delegatorStore['store'])) {
3756
					$userStore = $delegatorStore['store'];
3757
				}
3758
				if (!empty($delegatorStore[PR_IPM_APPOINTMENT_ENTRYID])) {
3759
					$calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
3760
				}
3761
			}
3762
		}
3763
3764
		if ($calFolder === false) {
3765
			$calFolder = $this->openDefaultCalendar($userStore);
3766
		}
3767
3768
		if ($calFolder) {
3769
			// Meeting request is recurring, so get all occurrence and check for each occurrence whether it conflicts with other appointments in Calendar.
3770
			if (isset($messageProps[$this->proptags['recurring']]) && $messageProps[$this->proptags['recurring']] === true) {
3771
				// Apply recurrence class and retrieve all occurrences(max: 30 occurrence because recurrence can also be set as 'no end date')
3772
				$recurr = new Recurrence($userStore, $message);
3773
				$items = $recurr->getItems($messageProps[$this->proptags['clipstart']], $messageProps[$this->proptags['clipend']] * (24 * 24 * 60), 30);
3774
3775
				foreach ($items as $item) {
3776
					// Get all items in the timeframe that we want to book, and get the goid and busystatus for each item
3777
					$calendarItems = $recurr->getCalendarItems($userStore, $calFolder, $item[$this->proptags['startdate']], $item[$this->proptags['duedate']], [$this->proptags['goid'], $this->proptags['busystatus']]);
3778
3779
					foreach ($calendarItems as $calendarItem) {
3780
						if ($calendarItem[$this->proptags['busystatus']] !== fbFree) {
3781
							/*
3782
							 * Only meeting requests have globalID, normal appointments do not have globalID
3783
							 * so if any normal appointment if found then it is assumed to be conflict.
3784
							 */
3785
							if (isset($calendarItem[$this->proptags['goid']])) {
3786
								if ($calendarItem[$this->proptags['goid']] !== $messageProps[$this->proptags['goid']]) {
3787
									++$noOfInstances;
3788
									break;
3789
								}
3790
							}
3791
							else {
3792
								++$noOfInstances;
3793
								break;
3794
							}
3795
						}
3796
					}
3797
				}
3798
3799
				if ($noOfInstances > 0) {
3800
					$returnValue = $noOfInstances;
3801
				}
3802
			}
3803
			else {
3804
				// Get all items in the timeframe that we want to book, and get the goid and busystatus for each item
3805
				$items = getCalendarItems($userStore, $calFolder, $messageProps[$this->proptags['startdate']], $messageProps[$this->proptags['duedate']], [$this->proptags['goid'], $this->proptags['busystatus']]);
3806
3807
				if (isset($messageProps[$this->proptags['basedate']]) && !empty($messageProps[$this->proptags['basedate']])) {
3808
					$basedate = $messageProps[$this->proptags['basedate']];
3809
					// Get the goid2 from recurring MR which further used to
3810
					// check the resource conflicts item.
3811
					$recurrItemProps = mapi_getprops($this->message, [$this->proptags['goid2']]);
3812
					$messageProps[$this->proptags['goid']] = $this->setBasedateInGlobalID($recurrItemProps[$this->proptags['goid2']], $basedate);
3813
					$messageProps[$this->proptags['goid2']] = $recurrItemProps[$this->proptags['goid2']];
3814
				}
3815
3816
				foreach ($items as $item) {
3817
					if ($item[$this->proptags['busystatus']] !== fbFree) {
3818
						if (isset($item[$this->proptags['goid']])) {
3819
							if (($item[$this->proptags['goid']] !== $messageProps[$this->proptags['goid']]) &&
3820
								($item[$this->proptags['goid']] !== $messageProps[$this->proptags['goid2']])) {
3821
								$returnValue = true;
3822
								break;
3823
							}
3824
						}
3825
						else {
3826
							$returnValue = true;
3827
							break;
3828
						}
3829
					}
3830
				}
3831
			}
3832
		}
3833
3834
		return $returnValue;
3835
	}
3836
3837
	/**
3838
	 * Function which adds organizer to recipient list which is passed.
3839
	 * This function also checks if it has organizer.
3840
	 *
3841
	 * @param array $messageProps message properties
3842
	 * @param array $recipients   recipients list of message
3843
	 */
3844
	public function addDelegator($messageProps, &$recipients): void {
3845
		$hasDelegator = false;
3846
		// Check if meeting already has an organizer.
3847
		foreach ($recipients as $key => $recipient) {
3848
			if (isset($messageProps[PR_RCVD_REPRESENTING_EMAIL_ADDRESS]) && $recipient[PR_EMAIL_ADDRESS] == $messageProps[PR_RCVD_REPRESENTING_EMAIL_ADDRESS]) {
3849
				$hasDelegator = true;
3850
			}
3851
		}
3852
3853
		if (!$hasDelegator) {
3854
			// Create delegator.
3855
			$delegator = [];
3856
			$delegator[PR_ENTRYID] = $messageProps[PR_RCVD_REPRESENTING_ENTRYID];
3857
			$delegator[PR_DISPLAY_NAME] = $messageProps[PR_RCVD_REPRESENTING_NAME];
3858
			$delegator[PR_EMAIL_ADDRESS] = $messageProps[PR_RCVD_REPRESENTING_EMAIL_ADDRESS];
3859
			$delegator[PR_RECIPIENT_TYPE] = MAPI_TO;
3860
			$delegator[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_RCVD_REPRESENTING_NAME];
3861
			$delegator[PR_ADDRTYPE] = empty($messageProps[PR_RCVD_REPRESENTING_ADDRTYPE]) ? 'SMTP' : $messageProps[PR_RCVD_REPRESENTING_ADDRTYPE];
3862
			$delegator[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
3863
			$delegator[PR_RECIPIENT_FLAGS] = recipSendable;
3864
			$delegator[PR_SEARCH_KEY] = $messageProps[PR_RCVD_REPRESENTING_SEARCH_KEY];
3865
3866
			// Add organizer to recipients list.
3867
			array_unshift($recipients, $delegator);
3868
		}
3869
	}
3870
3871
	/**
3872
	 * Function will return delegator's store and calendar folder for processing meetings.
3873
	 *
3874
	 * @param string $receivedRepresentingEntryId entryid of the delegator user
3875
	 * @param array  $foldersToOpen               contains list of folder types that should be returned in result
3876
	 *
3877
	 * @return resource[] contains store of the delegator and resource of folders if $foldersToOpen is not empty
3878
	 *
3879
	 * @psalm-return array<resource>
3880
	 */
3881
	public function getDelegatorStore($receivedRepresentingEntryId, $foldersToOpen = []): array {
3882
		$returnData = [];
3883
3884
		$delegatorStore = $this->openCustomUserStore($receivedRepresentingEntryId);
3885
		$returnData['store'] = $delegatorStore;
3886
3887
		if (!empty($foldersToOpen)) {
3888
			for ($index = 0, $len = count($foldersToOpen); $index < $len; ++$index) {
3889
				$folderType = $foldersToOpen[$index];
3890
3891
				// first try with default folders
3892
				$folder = $this->openDefaultFolder($folderType, $delegatorStore);
3893
3894
				// if folder not found then try with base folders
3895
				if ($folder === false) {
3896
					$folder = $this->openBaseFolder($folderType, $delegatorStore);
3897
				}
3898
3899
				if ($folder === false) {
3900
					// we are still not able to get the folder so give up
3901
					continue;
3902
				}
3903
3904
				$returnData[$folderType] = $folder;
3905
			}
3906
		}
3907
3908
		return $returnData;
3909
	}
3910
3911
	/**
3912
	 * Function returns extra info about meeting timing along with message body
3913
	 * which will be included in body while sending meeting request/response.
3914
	 *
3915
	 * @return false|string $meetingTimeInfo info about meeting timing along with message body
3916
	 */
3917
	public function getMeetingTimeInfo() {
3918
		return $this->meetingTimeInfo;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->meetingTimeInfo also could return the type boolean which is incompatible with the documented return type false|string.
Loading history...
3919
	}
3920
3921
	/**
3922
	 * Function sets extra info about meeting timing along with message body
3923
	 * which will be included in body while sending meeting request/response.
3924
	 *
3925
	 * @param string $meetingTimeInfo info about meeting timing along with message body
3926
	 */
3927
	public function setMeetingTimeInfo($meetingTimeInfo): void {
3928
		$this->meetingTimeInfo = $meetingTimeInfo;
3929
	}
3930
3931
	/**
3932
	 * Helper function which is use to get local categories of all occurrence.
3933
	 *
3934
	 * @param mixed $calendarItem meeting request item
3935
	 * @param mixed $store        store containing calendar folder
3936
	 * @param mixed $calFolder    calendar folder
3937
	 *
3938
	 * @return array $localCategories which contain array of basedate along with categories
3939
	 */
3940
	public function getLocalCategories($calendarItem, $store, $calFolder) {
3941
		$calendarItemProps = mapi_getprops($calendarItem);
3942
		$recurrence = new Recurrence($store, $calendarItem);
3943
3944
		// Retrieve all occurrences(max: 30 occurrence because recurrence can also be set as 'no end date')
3945
		$items = $recurrence->getItems($calendarItemProps[$this->proptags['clipstart']], $calendarItemProps[$this->proptags['clipend']] * (24 * 24 * 60), 30);
3946
		$localCategories = [];
3947
3948
		foreach ($items as $item) {
3949
			$recurrenceItems = $recurrence->getCalendarItems($store, $calFolder, $item[$this->proptags['startdate']], $item[$this->proptags['duedate']], [$this->proptags['goid'], $this->proptags['busystatus'], $this->proptags['categories']]);
3950
			foreach ($recurrenceItems as $recurrenceItem) {
3951
				// Check if occurrence is exception then get the local categories of that occurrence.
3952
				if (isset($recurrenceItem[$this->proptags['goid']]) && $recurrenceItem[$this->proptags['goid']] == $calendarItemProps[$this->proptags['goid']]) {
3953
					$exceptionAttach = $recurrence->getExceptionAttachment($recurrenceItem['basedate']);
3954
3955
					if ($exceptionAttach) {
3956
						$exception = mapi_attach_openobj($exceptionAttach, 0);
3957
						$exceptionProps = mapi_getprops($exception, [$this->proptags['categories']]);
3958
						if (isset($exceptionProps[$this->proptags['categories']])) {
3959
							$localCategories[$recurrenceItem['basedate']] = $exceptionProps[$this->proptags['categories']];
3960
						}
3961
					}
3962
				}
3963
			}
3964
		}
3965
3966
		return $localCategories;
3967
	}
3968
3969
	/**
3970
	 * Helper function which is use to apply local categories on respective occurrences.
3971
	 *
3972
	 * @param mixed $calendarItem    meeting request item
3973
	 * @param mixed $store           store containing calendar folder
3974
	 * @param array $localCategories array contains basedate and array of categories
3975
	 */
3976
	public function applyLocalCategories($calendarItem, $store, $localCategories): void {
3977
		$calendarItemProps = mapi_getprops($calendarItem, [PR_PARENT_ENTRYID, PR_ENTRYID]);
3978
		$message = mapi_msgstore_openentry($store, $calendarItemProps[PR_ENTRYID]);
3979
		$recurrence = new Recurrence($store, $message);
3980
3981
		// Check for all occurrence if it is exception then modify the exception by setting up categories,
3982
		// Otherwise create new exception with categories.
3983
		foreach ($localCategories as $key => $value) {
3984
			if ($recurrence->isException($key)) {
3985
				$recurrence->modifyException([$this->proptags['categories'] => $value], $key);
3986
			}
3987
			else {
3988
				$recurrence->createException([$this->proptags['categories'] => $value], $key, false);
3989
			}
3990
			mapi_savechanges($message);
3991
		}
3992
	}
3993
}
3994