Passed
Push — master ( e02050...0c04ca )
by
unknown
33:28 queued 20:51
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
			if (isset($recipient[PR_ENTRYID]) && $this->compareABEntryIDs($recipient[PR_ENTRYID], $senderentryid)) {
454
				$found = true;
455
456
				/*
457
				 * If value of attendee_critical_change on meeting response mail is less than PR_RECIPIENT_TRACKSTATUS_TIME
458
				 * on the corresponding recipientRow of meeting then we ignore this response mail.
459
				 */
460
				if (isset($recipient[PR_RECIPIENT_TRACKSTATUS_TIME]) && ($messageprops[$this->proptags['attendee_critical_change']] < $recipient[PR_RECIPIENT_TRACKSTATUS_TIME])) {
461
					continue;
462
				}
463
464
				// The email address matches, update the row
465
				$recipient[PR_RECIPIENT_TRACKSTATUS] = $this->getTrackStatus($messageclass);
466
				if (isset($messageprops[$this->proptags['attendee_critical_change']])) {
467
					$recipient[PR_RECIPIENT_TRACKSTATUS_TIME] = $messageprops[$this->proptags['attendee_critical_change']];
468
				}
469
470
				// If this is a counter proposal, set the proposal properties in the recipient row
471
				if (isset($messageprops[$this->proptags['counter_proposal']]) && $messageprops[$this->proptags['counter_proposal']]) {
472
					$recipient[PR_RECIPIENT_PROPOSEDSTARTTIME] = $messageprops[$this->proptags['proposed_start_whole']];
473
					$recipient[PR_RECIPIENT_PROPOSEDENDTIME] = $messageprops[$this->proptags['proposed_end_whole']];
474
					$recipient[PR_RECIPIENT_PROPOSED] = $messageprops[$this->proptags['counter_proposal']];
475
				}
476
477
				// Update the recipient information
478
				mapi_message_modifyrecipients($calendarItem, MODRECIP_REMOVE, [$recipient]);
479
				mapi_message_modifyrecipients($calendarItem, MODRECIP_ADD, [$recipient]);
480
			}
481
			if (isset($recipient[PR_RECIPIENT_TRACKSTATUS]) && $recipient[PR_RECIPIENT_TRACKSTATUS] == olRecipientTrackStatusAccepted) {
482
				++$acceptedrecips;
483
			}
484
		}
485
486
		// If the recipient was not found in the original calendar item,
487
		// then add the recpient as a new optional recipient
488
		if (!$found) {
489
			$recipient = [];
490
			$recipient[PR_ENTRYID] = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
491
			$recipient[PR_EMAIL_ADDRESS] = $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
492
			$recipient[PR_DISPLAY_NAME] = $messageprops[PR_SENT_REPRESENTING_NAME];
493
			$recipient[PR_ADDRTYPE] = $messageprops[PR_SENT_REPRESENTING_ADDRTYPE];
494
			$recipient[PR_RECIPIENT_TYPE] = MAPI_CC;
495
			$recipient[PR_SEARCH_KEY] = $messageprops[PR_SENT_REPRESENTING_SEARCH_KEY];
496
			$recipient[PR_RECIPIENT_TRACKSTATUS] = $this->getTrackStatus($messageclass);
497
			$recipient[PR_RECIPIENT_TRACKSTATUS_TIME] = $deliverytime;
498
499
			// If this is a counter proposal, set the proposal properties in the recipient row
500
			if (isset($messageprops[$this->proptags['counter_proposal']])) {
501
				$recipient[PR_RECIPIENT_PROPOSEDSTARTTIME] = $messageprops[$this->proptags['proposed_start_whole']];
502
				$recipient[PR_RECIPIENT_PROPOSEDENDTIME] = $messageprops[$this->proptags['proposed_end_whole']];
503
				$recipient[PR_RECIPIENT_PROPOSED] = $messageprops[$this->proptags['counter_proposal']];
504
			}
505
506
			mapi_message_modifyrecipients($calendarItem, MODRECIP_ADD, [$recipient]);
507
			++$totalrecips;
508
			if ($recipient[PR_RECIPIENT_TRACKSTATUS] == olRecipientTrackStatusAccepted) {
509
				++$acceptedrecips;
510
			}
511
		}
512
513
		// TODO: Update counter proposal number property on message
514
		/*
515
		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.
516
		*/
517
		// If this is a counter proposal, set the counter proposal indicator boolean
518
		if (isset($messageprops[$this->proptags['counter_proposal']])) {
519
			$props = [];
520
			if ($messageprops[$this->proptags['counter_proposal']]) {
521
				$props[$this->proptags['counter_proposal']] = true;
522
			}
523
			else {
524
				$props[$this->proptags['counter_proposal']] = false;
525
			}
526
527
			mapi_setprops($calendarItem, $props);
528
		}
529
530
		mapi_savechanges($calendarItem);
531
		if (isset($attach)) {
532
			mapi_savechanges($attach);
533
			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

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

596
				$calendarItemProps = mapi_getprops(/** @scrutinizer ignore-type */ $calendarItem, [$this->proptags['recurring']]);
Loading history...
597
				if ($calendarItemProps[$this->proptags['recurring']] === true) {
598
					$recurr = new Recurrence($store, $calendarItem);
599
600
					// Set message class
601
					$messageProps[PR_MESSAGE_CLASS] = 'IPM.Appointment';
602
603
					if ($recurr->isException($basedate)) {
604
						$recurr->modifyException($messageProps, $basedate);
605
					}
606
					else {
607
						$recurr->createException($messageProps, $basedate);
608
					}
609
				}
610
			}
611
			else {
612
				// set the properties of the cancellation object
613
				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

613
				mapi_setprops(/** @scrutinizer ignore-type */ $calendarItem, $messageProps);
Loading history...
614
			}
615
616
			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

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

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

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

884
								$this->mergeException($calendarItem, /** @scrutinizer ignore-type */ $occurrenceItem, $basedate, $store);
Loading history...
$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

884
								$this->mergeException(/** @scrutinizer ignore-type */ $calendarItem, $occurrenceItem, $basedate, $store);
Loading history...
885
							}
886
						}
887
					}
888
				}
889
890
				if (!isset($props[$this->proptags["recurring_pattern"]])) {
891
					$recurr = new Recurrence($store, $calendarItem);
892
					$recurr->saveRecurrencePattern();
893
				}
894
895
				mapi_savechanges($calendarItem);
896
897
				// After applying update of organiser all local categories of occurrence was removed,
898
				// So if local categories exist then apply it on respective occurrence.
899
				if (!empty($localCategories)) {
900
					$this->applyLocalCategories($calendarItem, $store, $localCategories);
901
				}
902
903
				if ($move) {
904
					// open wastebasket of currently logged in user and move the meeting request to it
905
					// for delegates this will be delegate's wastebasket folder
906
					$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
907
					$sourcefolder = $this->openParentFolder();
908
					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

908
					mapi_folder_copymessages($sourcefolder, [$props[PR_ENTRYID]], /** @scrutinizer ignore-type */ $wastebasket, MESSAGE_MOVE);
Loading history...
909
				}
910
911
				$entryid = $props[PR_ENTRYID];
912
			}
913
			else {
914
				/**
915
				 * This meeting request is not recurring, so can be an exception or normal meeting.
916
				 * If exception then find main recurring item and update exception
917
				 * If main recurring item is not found then put exception into Calendar as normal meeting.
918
				 */
919
				$calendarItem = false;
920
921
				// We found basedate in GlobalID of this meeting request, so this meeting request if for an occurrence.
922
				if ($basedate) {
923
					// Find main recurring item from CleanGlobalID of this meeting request
924
					$items = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder);
925
					if (is_array($items)) {
926
						foreach ($items as $entryid) {
927
							$calendarItem = mapi_msgstore_openentry($store, $entryid);
928
						}
929
					}
930
931
					// Main recurring item is found, so now update exception
932
					if ($calendarItem) {
933
						$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

933
						$this->acceptException(/** @scrutinizer ignore-type */ $calendarItem, $this->message, $basedate, $move, $tentative, $userAction, $store, $isDelegate);
Loading history...
934
						$calendarItemProps = mapi_getprops($calendarItem, [PR_ENTRYID]);
935
						$entryid = $calendarItemProps[PR_ENTRYID];
936
					}
937
				}
938
939
				if (!$calendarItem) {
940
					$items = $this->findCalendarItems($messageprops[$this->proptags['goid']], $calFolder);
941
					if (is_array($items)) {
942
						// Get local categories before deleting MR.
943
						$message = mapi_msgstore_openentry($store, $items[0]);
944
						$localCategories = mapi_getprops($message, [$this->proptags['categories']]);
945
						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

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

1268
					$calendaritem = mapi_msgstore_openentry(/** @scrutinizer ignore-type */ $store, $entryid);
Loading history...
1269
1270
					// Recurring item is found, now delete exception
1271
					if ($calendaritem) {
1272
						$this->doRemoveExceptionFromCalendar($basedate, $calendaritem, $store);
1273
						$result = true;
1274
					}
1275
				}
1276
			}
1277
1278
			if ($this->isMeetingRequest()) {
1279
				$calendaritem = false;
1280
			}
1281
		}
1282
1283
		if (!$calendaritem) {
1284
			$calendar = $this->openDefaultCalendar($store);
1285
1286
			if (!empty($entryids)) {
1287
				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

1287
				mapi_folder_deletemessages(/** @scrutinizer ignore-type */ $calendar, $entryids);
Loading history...
1288
			}
1289
1290
			// All we have to do to decline, is to move the item to the waste basket
1291
			$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
1292
			$sourcefolder = $this->openParentFolder();
1293
1294
			$messageprops = mapi_getprops($this->message, [PR_ENTRYID]);
1295
1296
			// Release the message
1297
			$this->message = null;
1298
1299
			// Move the message to the waste basket
1300
			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

1300
			mapi_folder_copymessages($sourcefolder, [$messageprops[PR_ENTRYID]], /** @scrutinizer ignore-type */ $wastebasket, MESSAGE_MOVE);
Loading history...
1301
1302
			$result = true;
1303
		}
1304
1305
		return $result;
1306
	}
1307
1308
	/**
1309
	 * Removes a meeting request from the calendar when the user presses the
1310
	 * 'remove from calendar' button in response to a meeting cancellation.
1311
	 *
1312
	 * @param mixed $basedate if specified contains starttime of day of an occurrence
1313
	 *
1314
	 * @return null|false
1315
	 */
1316
	public function doRemoveFromCalendar($basedate) {
1317
		if ($this->isLocalOrganiser()) {
1318
			return false;
1319
		}
1320
1321
		$messageprops = mapi_getprops($this->message, [PR_ENTRYID, $this->proptags['goid'], PR_RCVD_REPRESENTING_ENTRYID, PR_MESSAGE_CLASS]);
1322
1323
		$goid = $messageprops[$this->proptags['goid']];
1324
1325
		$store = $this->store;
1326
		$calFolder = $this->openDefaultCalendar();
1327
		if (isset($messageprops[PR_RCVD_REPRESENTING_ENTRYID])) {
1328
			$delegatorStore = $this->getDelegatorStore($messageprops[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
1329
			if (!empty($delegatorStore['store'])) {
1330
				$store = $delegatorStore['store'];
1331
			}
1332
			if (!empty($delegatorStore[PR_IPM_APPOINTMENT_ENTRYID])) {
1333
				$calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
1334
			}
1335
		}
1336
1337
		// check for calendar access before deleting the calendar item
1338
		if ($this->checkCalendarWriteAccess($store) !== true) {
1339
			// Throw an exception that we don't have write permissions on calendar folder,
1340
			// allow caller to fill the error message
1341
			throw new MAPIException(null, MAPI_E_NO_ACCESS);
1342
		}
1343
1344
		$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
1345
		// get the source folder of the meeting message
1346
		$sourcefolder = $this->openParentFolder();
1347
1348
		// Check if the message is a meeting request in the inbox or a calendaritem by checking the message class
1349
		if ($this->isMeetingCancellation($messageprops[PR_MESSAGE_CLASS])) {
1350
			// get the basedate to check for exception
1351
			$basedate = $this->getBasedateFromGlobalID($goid);
1352
1353
			$calendarItem = $this->getCorrespondentCalendarItem(true);
1354
1355
			if ($calendarItem !== false) {
1356
				// basedate is provided so open exception
1357
				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...
1358
					$exception = $this->getExceptionItem($calendarItem, $basedate);
1359
1360
					if ($exception !== false) {
0 ignored issues
show
The condition $exception !== false is always true.
Loading history...
1361
						// exception found, remove it from calendar
1362
						$this->doRemoveExceptionFromCalendar($basedate, $calendarItem, $store);
1363
					}
1364
				}
1365
				else {
1366
					// remove normal / recurring series from calendar
1367
					$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

1367
					$entryids = mapi_getprops(/** @scrutinizer ignore-type */ $calendarItem, [PR_ENTRYID]);
Loading history...
1368
1369
					$entryids = [$entryids[PR_ENTRYID]];
1370
1371
					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

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

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

1567
				$recurr = new Recurrence(/** @scrutinizer ignore-type */ $this->openDefaultStore(), $this->message);
Loading history...
1568
1569
				// First send meetingrequest for recurring item
1570
				$this->submitMeetingRequest($this->message, $cancel, $prefix, false, $recurr, false, $modifiedRecips, $deletedRecips);
1571
1572
				// Then send all meeting request for all exceptions
1573
				$exceptions = $recurr->getAllExceptions();
1574
				if ($exceptions) {
1575
					foreach ($exceptions as $exceptionBasedate) {
1576
						$attach = $recurr->getExceptionAttachment($exceptionBasedate);
1577
1578
						if ($attach) {
1579
							$occurrenceItem = mapi_attach_openobj($attach, MAPI_MODIFY);
1580
							$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

1580
							$this->submitMeetingRequest(/** @scrutinizer ignore-type */ $occurrenceItem, $cancel, false, $exceptionBasedate, $recurr, false, $modifiedRecips, $deletedRecips);
Loading history...
1581
							mapi_savechanges($attach);
1582
						}
1583
					}
1584
				}
1585
			}
1586
		}
1587
		else {
1588
			// Basedate found, an exception is to be sent
1589
			if ($basedate) {
1590
				$recurr = new Recurrence($this->openDefaultStore(), $this->message);
1591
1592
				if ($cancel) {
1593
					// @TODO: remove occurrence from Resource's Calendar if resource was booked for whole series
1594
					$this->submitMeetingRequest($this->message, $cancel, $prefix, $basedate, $recurr, false);
1595
				}
1596
				else {
1597
					$attach = $recurr->getExceptionAttachment($basedate);
1598
1599
					if ($attach) {
1600
						$occurrenceItem = mapi_attach_openobj($attach, MAPI_MODIFY);
1601
1602
						// Book resource for this occurrence
1603
						$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

1603
						$resourceRecipData = $this->bookResources(/** @scrutinizer ignore-type */ $occurrenceItem, $cancel, $prefix, $basedate);
Loading history...
1604
1605
						if (!$this->errorSetResource) {
1606
							// Save all previous changes
1607
							mapi_savechanges($this->message);
1608
1609
							$this->submitMeetingRequest($occurrenceItem, $cancel, $prefix, $basedate, $recurr, true, $modifiedRecips, $deletedRecips);
1610
							mapi_savechanges($occurrenceItem);
1611
							mapi_savechanges($attach);
1612
						}
1613
					}
1614
				}
1615
			}
1616
			else {
1617
				// This is normal meeting
1618
				$resourceRecipData = $this->bookResources($this->message, $cancel, $prefix);
1619
1620
				if (!$this->errorSetResource) {
1621
					$this->submitMeetingRequest($this->message, $cancel, $prefix, false, false, false, $modifiedRecips, $deletedRecips);
1622
				}
1623
			}
1624
		}
1625
1626
		if (isset($this->errorSetResource) && $this->errorSetResource) {
1627
			return [
1628
				'error' => $this->errorSetResource,
1629
				'displayname' => $this->recipientDisplayname,
1630
			];
1631
		}
1632
1633
		return true;
1634
	}
1635
1636
	/**
1637
	 * Updates the message after an update has been performed (for example,
1638
	 * changing the time of the meeting). This must be called before re-sending
1639
	 * the meeting request. You can also call this function instead of 'setMeetingRequest()'
1640
	 * as it will automatically call setMeetingRequest on this object if it is the first
1641
	 * call to this function.
1642
	 *
1643
	 * @param mixed $basedate
1644
	 */
1645
	public function updateMeetingRequest($basedate = false): void {
1646
		$messageprops = mapi_getprops($this->message, [$this->proptags['last_updatecounter'], $this->proptags['goid']]);
1647
1648
		if (!isset($messageprops[$this->proptags['goid']])) {
1649
			$this->setMeetingRequest($basedate);
1650
		}
1651
		else {
1652
			$counter = (isset($messageprops[$this->proptags['last_updatecounter']]) ?? 0) + 1;
1653
1654
			// increment value of last_updatecounter, last_updatecounter will be common for recurring series
1655
			// so even if you sending an exception only you need to update the last_updatecounter in the recurring series message
1656
			// this way we can make sure that every time we will be using a uniwue number for every operation
1657
			mapi_setprops($this->message, [$this->proptags['last_updatecounter'] => $counter]);
1658
		}
1659
	}
1660
1661
	/**
1662
	 * Returns TRUE if we are the organiser of the meeting. Can be used with any type of meeting object.
1663
	 */
1664
	public function isLocalOrganiser(): bool {
1665
		$props = mapi_getprops($this->message, [$this->proptags['goid'], PR_MESSAGE_CLASS]);
1666
1667
		if (!$this->isMeetingRequest($props[PR_MESSAGE_CLASS]) && !$this->isMeetingRequestResponse($props[PR_MESSAGE_CLASS]) && !$this->isMeetingCancellation($props[PR_MESSAGE_CLASS])) {
1668
			// we are checking with calendar item
1669
			$calendarItem = $this->message;
1670
		}
1671
		else {
1672
			// we are checking with meeting request / response / cancellation mail
1673
			// get calendar items
1674
			$calendarItem = $this->getCorrespondentCalendarItem(true);
1675
		}
1676
1677
		// even if we have received request/response for exception/occurrence then also
1678
		// we can check recurring series for organizer, no need to check with exception/occurrence
1679
1680
		if ($calendarItem !== false) {
1681
			$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

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

1828
			$folder = mapi_msgstore_openentry($store ? $store : $this->store, /** @scrutinizer ignore-type */ $entryid);
Loading history...
1829
		}
1830
1831
		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...
1832
	}
1833
1834
	/**
1835
	 * Function will return entryid of default folder from store. This method is useful when you want
1836
	 * to get entryid of folder which is stored as store properties
1837
	 * (PR_IPM_FAVORITES_ENTRYID, PR_IPM_OUTBOX_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_IPM_WASTEBASKET_ENTRYID).
1838
	 *
1839
	 * @param int   $prop  proptag of the folder whose entryid we want to get
1840
	 * @param mixed $store {optional} user store from which we need to get entryid of default folder
1841
	 *
1842
	 * @return bool|string entryid of default folder from store
1843
	 */
1844
	public function getBaseEntryID($prop, $store = false) {
1845
		$storeprops = mapi_getprops($store ? $store : $this->store, [$prop]);
1846
		if (!isset($storeprops[$prop])) {
1847
			return false;
1848
		}
1849
1850
		return $storeprops[$prop];
1851
	}
1852
1853
	/**
1854
	 * Function will return resource of any default folder of store.
1855
	 *
1856
	 * @param int   $prop  proptag of the folder that we want to open
1857
	 * @param mixed $store {optional} user store from which we need to open default folder
1858
	 *
1859
	 * @return resource default folder of store
1860
	 */
1861
	public function openBaseFolder($prop, $store = false) {
1862
		$folder = false;
1863
		$entryid = $this->getBaseEntryID($prop, $store);
1864
1865
		if ($entryid !== false) {
1866
			$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

1866
			$folder = mapi_msgstore_openentry($store ? $store : $this->store, /** @scrutinizer ignore-type */ $entryid);
Loading history...
1867
		}
1868
1869
		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...
1870
	}
1871
1872
	/**
1873
	 * Function checks whether user has access over the specified folder or not.
1874
	 *
1875
	 * @param string $entryid entryid The entryid of the folder to check
1876
	 * @param mixed  $store   (optional) store from which folder should be opened
1877
	 *
1878
	 * @return bool true if user has an access over the folder, false if not
1879
	 */
1880
	public function checkFolderWriteAccess($entryid, $store = false) {
1881
		$accessToFolder = false;
1882
1883
		if (!empty($entryid)) {
1884
			if ($store === false) {
1885
				$store = $this->store;
1886
			}
1887
1888
			try {
1889
				$folder = mapi_msgstore_openentry($store, $entryid);
1890
				$folderProps = mapi_getprops($folder, [PR_ACCESS]);
1891
				if (($folderProps[PR_ACCESS] & MAPI_ACCESS_CREATE_CONTENTS) === MAPI_ACCESS_CREATE_CONTENTS) {
1892
					$accessToFolder = true;
1893
				}
1894
			}
1895
			catch (MAPIException $e) {
1896
				// we don't have rights to open folder, so return false
1897
				if ($e->getCode() == MAPI_E_NO_ACCESS) {
1898
					return $accessToFolder;
1899
				}
1900
1901
				// rethrow other errors
1902
				throw $e;
1903
			}
1904
		}
1905
1906
		return $accessToFolder;
1907
	}
1908
1909
	/**
1910
	 * Function checks whether user has access over the specified folder or not.
1911
	 *
1912
	 * @param mixed $store
1913
	 *
1914
	 * @return bool true if user has an access over the folder, false if not
1915
	 */
1916
	public function checkCalendarWriteAccess($store = false) {
1917
		if ($store === false) {
1918
			$messageProps = mapi_getprops($this->message, [PR_RCVD_REPRESENTING_ENTRYID]);
1919
			$store = $this->store;
1920
			// If this meeting request is received by a delegate then open delegator's store.
1921
			if (isset($messageProps[PR_RCVD_REPRESENTING_ENTRYID])) {
1922
				$delegatorStore = $this->getDelegatorStore($messageProps[PR_RCVD_REPRESENTING_ENTRYID]);
1923
				if (!empty($delegatorStore['store'])) {
1924
					$store = $delegatorStore['store'];
1925
				}
1926
			}
1927
		}
1928
1929
		// If the store is a public folder, the calendar folder is the PARENT_ENTRYID of the calendar item
1930
		$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

1930
		$provider = mapi_getprops(/** @scrutinizer ignore-type */ $store, [PR_MDB_PROVIDER]);
Loading history...
1931
		if (isset($provider[PR_MDB_PROVIDER]) && $provider[PR_MDB_PROVIDER] === ZARAFA_STORE_PUBLIC_GUID) {
1932
			$entryid = mapi_getprops($this->message, [PR_PARENT_ENTRYID]);
1933
			$entryid = $entryid[PR_PARENT_ENTRYID];
1934
		}
1935
		else {
1936
			$entryid = $this->getDefaultFolderEntryID(PR_IPM_APPOINTMENT_ENTRYID, $store);
1937
			if ($entryid === false) {
1938
				$entryid = $this->getBaseEntryID(PR_IPM_APPOINTMENT_ENTRYID, $store);
1939
			}
1940
1941
			if ($entryid === false) {
1942
				return false;
1943
			}
1944
		}
1945
1946
		return $this->checkFolderWriteAccess($entryid, $store);
1947
	}
1948
1949
	/**
1950
	 * Function will resolve the user and open its store.
1951
	 *
1952
	 * @param string $ownerentryid the entryid of the user
1953
	 *
1954
	 * @return resource store of the user
1955
	 */
1956
	public function openCustomUserStore($ownerentryid) {
1957
		$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

1957
		$ab = mapi_openaddressbook(/** @scrutinizer ignore-type */ $this->session);
Loading history...
1958
1959
		try {
1960
			$mailuser = mapi_ab_openentry($ab, $ownerentryid);
1961
		}
1962
		catch (MAPIException $e) {
1963
			return;
1964
		}
1965
1966
		$mailuserprops = mapi_getprops($mailuser, [PR_EMAIL_ADDRESS]);
1967
		$storeid = mapi_msgstore_createentryid($this->store, $mailuserprops[PR_EMAIL_ADDRESS]);
1968
1969
		return mapi_openmsgstore($this->session, $storeid);
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

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

2153
		mapi_setprops(/** @scrutinizer ignore-type */ $message, $props);
Loading history...
2154
		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

2154
		mapi_message_modifyrecipients(/** @scrutinizer ignore-type */ $message, MODRECIP_ADD, [$recip]);
Loading history...
2155
		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

2155
		mapi_savechanges(/** @scrutinizer ignore-type */ $message);
Loading history...
2156
		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

2156
		mapi_message_submitmessage(/** @scrutinizer ignore-type */ $message);
Loading history...
2157
	}
2158
2159
	/**
2160
	 * Function which finds items in calendar based on globalId and cleanGlobalId.
2161
	 *
2162
	 * @param string $goid             GlobalID(0x3) of item
2163
	 * @param mixed  $calendar         MAPI_folder of user (optional)
2164
	 * @param bool   $useCleanGlobalId if true then search should be performed on cleanGlobalId(0x23) else globalId(0x3)
2165
	 *
2166
	 * @return mixed
2167
	 */
2168
	public function findCalendarItems($goid, $calendar = false, $useCleanGlobalId = false) {
2169
		if ($calendar === false) {
2170
			// Open the Calendar
2171
			$calendar = $this->openDefaultCalendar();
2172
		}
2173
2174
		// Find the item by restricting all items to the correct ID
2175
		$restrict = [
2176
			RES_PROPERTY,
2177
			[
2178
				RELOP => RELOP_EQ,
2179
				ULPROPTAG => ($useCleanGlobalId === true ? $this->proptags['goid2'] : $this->proptags['goid']),
2180
				VALUE => $goid,
2181
			],
2182
		];
2183
2184
		$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

2184
		$calendarcontents = mapi_folder_getcontentstable(/** @scrutinizer ignore-type */ $calendar);
Loading history...
2185
2186
		$rows = mapi_table_queryallrows($calendarcontents, [PR_ENTRYID], $restrict);
2187
2188
		if (empty($rows)) {
2189
			return;
2190
		}
2191
2192
		$calendaritems = [];
2193
2194
		// In principle, there should only be one row, but we'll handle them all just in case
2195
		foreach ($rows as $row) {
2196
			$calendaritems[] = $row[PR_ENTRYID];
2197
		}
2198
2199
		return $calendaritems;
2200
	}
2201
2202
	// Returns TRUE if both entryid's are equal. Equality is defined by both entryid's pointing at the
2203
	// same SMTP address when converted to SMTP
2204
	public function compareABEntryIDs($entryid1, $entryid2): bool {
2205
		// If the session was not passed, just do a 'normal' compare.
2206
		if (!$this->session) {
2207
			return $entryid1 == $entryid2;
2208
		}
2209
2210
		$smtp1 = $this->getSMTPAddress($entryid1);
2211
		$smtp2 = $this->getSMTPAddress($entryid2);
2212
2213
		if ($smtp1 == $smtp2) {
2214
			return true;
2215
		}
2216
2217
		return false;
2218
	}
2219
2220
	// Gets the SMTP address of the passed addressbook entryid
2221
	public function getSMTPAddress($entryid) {
2222
		if (!$this->session) {
2223
			return false;
2224
		}
2225
2226
		try {
2227
			$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

2227
			$ab = mapi_openaddressbook(/** @scrutinizer ignore-type */ $this->session);
Loading history...
2228
			$abitem = mapi_ab_openentry($ab, $entryid);
2229
2230
			if (!$abitem) {
0 ignored issues
show
$abitem is of type resource, thus it always evaluated to true.
Loading history...
2231
				return '';
2232
			}
2233
		}
2234
		catch (MAPIException $e) {
2235
			return '';
2236
		}
2237
2238
		$props = mapi_getprops($abitem, [PR_ADDRTYPE, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS]);
2239
2240
		if ($props[PR_ADDRTYPE] == 'SMTP') {
2241
			return $props[PR_EMAIL_ADDRESS];
2242
		}
2243
2244
		return $props[PR_SMTP_ADDRESS];
2245
	}
2246
2247
	/**
2248
	 * Gets the properties associated with the owner of the passed store:
2249
	 * PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_ADDRTYPE, PR_ENTRYID, PR_SEARCH_KEY.
2250
	 *
2251
	 * @param mixed $store                  message store
2252
	 * @param bool  $fallbackToLoggedInUser If true then return properties of logged in user instead of mailbox owner.
2253
	 *                                      Not used when passed store is public store.
2254
	 *                                      For public store we are always returning logged in user's info.
2255
	 *
2256
	 * @return array|false properties of logged in user in an array in sequence of display_name, email address, address type, entryid and search key
2257
	 *
2258
	 * @psalm-return false|list{mixed, mixed, mixed, mixed, mixed}
2259
	 */
2260
	public function getOwnerAddress($store, $fallbackToLoggedInUser = true) {
2261
		if (!$this->session) {
2262
			return false;
2263
		}
2264
2265
		$storeProps = mapi_getprops($store, [PR_MAILBOX_OWNER_ENTRYID, PR_USER_ENTRYID]);
2266
2267
		$ownerEntryId = false;
2268
		if (isset($storeProps[PR_USER_ENTRYID]) && $storeProps[PR_USER_ENTRYID]) {
2269
			$ownerEntryId = $storeProps[PR_USER_ENTRYID];
2270
		}
2271
2272
		if (isset($storeProps[PR_MAILBOX_OWNER_ENTRYID]) && $storeProps[PR_MAILBOX_OWNER_ENTRYID] && !$fallbackToLoggedInUser) {
2273
			$ownerEntryId = $storeProps[PR_MAILBOX_OWNER_ENTRYID];
2274
		}
2275
2276
		if ($ownerEntryId) {
2277
			$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

2277
			$ab = mapi_openaddressbook(/** @scrutinizer ignore-type */ $this->session);
Loading history...
2278
2279
			$zarafaUser = mapi_ab_openentry($ab, $ownerEntryId);
2280
			if (!$zarafaUser) {
0 ignored issues
show
$zarafaUser is of type resource, thus it always evaluated to true.
Loading history...
2281
				return false;
2282
			}
2283
2284
			$ownerProps = mapi_getprops($zarafaUser, [PR_ADDRTYPE, PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SEARCH_KEY]);
2285
2286
			$addrType = $ownerProps[PR_ADDRTYPE];
2287
			$name = $ownerProps[PR_DISPLAY_NAME];
2288
			$emailAddr = $ownerProps[PR_EMAIL_ADDRESS];
2289
			$searchKey = $ownerProps[PR_SEARCH_KEY];
2290
			$entryId = $ownerEntryId;
2291
2292
			return [$name, $emailAddr, $addrType, $entryId, $searchKey];
2293
		}
2294
2295
		return false;
2296
	}
2297
2298
	// Opens this session's default message store
2299
	public function openDefaultStore() {
2300
		$entryid = '';
2301
2302
		$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

2302
		$storestable = mapi_getmsgstorestable(/** @scrutinizer ignore-type */ $this->session);
Loading history...
2303
		$rows = mapi_table_queryallrows($storestable, [PR_ENTRYID, PR_DEFAULT_STORE]);
2304
2305
		foreach ($rows as $row) {
2306
			if (isset($row[PR_DEFAULT_STORE]) && $row[PR_DEFAULT_STORE]) {
2307
				$entryid = $row[PR_ENTRYID];
2308
				break;
2309
			}
2310
		}
2311
2312
		if (!$entryid) {
2313
			return false;
2314
		}
2315
2316
		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

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

2504
		$messageprops = mapi_getprops(/** @scrutinizer ignore-type */ $message);
Loading history...
2505
2506
		$calFolder = '';
2507
2508
		if ($basedate) {
2509
			$recurrItemProps = mapi_getprops($this->message, [$this->proptags['goid'], $this->proptags['goid2'], $this->proptags['timezone_data'], $this->proptags['timezone'], PR_OWNER_APPT_ID]);
2510
2511
			$messageprops[$this->proptags['goid']] = $this->setBasedateInGlobalID($recurrItemProps[$this->proptags['goid']], $basedate);
2512
			$messageprops[$this->proptags['goid2']] = $recurrItemProps[$this->proptags['goid2']];
2513
2514
			// Delete properties which are not needed.
2515
			$deleteProps = [$this->proptags['basedate'], PR_DISPLAY_NAME, PR_ATTACHMENT_FLAGS, PR_ATTACHMENT_HIDDEN, PR_ATTACHMENT_LINKID, PR_ATTACH_FLAGS, PR_ATTACH_METHOD];
2516
			foreach ($deleteProps as $propID) {
2517
				if (isset($messageprops[$propID])) {
2518
					unset($messageprops[$propID]);
2519
				}
2520
			}
2521
2522
			if (isset($messageprops[$this->proptags['recurring']])) {
2523
				$messageprops[$this->proptags['recurring']] = false;
2524
			}
2525
2526
			// Set Outlook properties
2527
			$messageprops[$this->proptags['clipstart']] = $messageprops[$this->proptags['startdate']];
2528
			$messageprops[$this->proptags['clipend']] = $messageprops[$this->proptags['duedate']];
2529
			$messageprops[$this->proptags['timezone_data']] = $recurrItemProps[$this->proptags['timezone_data']];
2530
			$messageprops[$this->proptags['timezone']] = $recurrItemProps[$this->proptags['timezone']];
2531
			$messageprops[$this->proptags['attendee_critical_change']] = time();
2532
			$messageprops[$this->proptags['owner_critical_change']] = time();
2533
		}
2534
2535
		// Get resource recipients
2536
		$getResourcesRestriction = [
2537
			RES_PROPERTY,
2538
			[
2539
				RELOP => RELOP_EQ,	// Equals recipient type 3: Resource
2540
				ULPROPTAG => PR_RECIPIENT_TYPE,
2541
				VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
2542
			],
2543
		];
2544
		$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

2544
		$recipienttable = mapi_message_getrecipienttable(/** @scrutinizer ignore-type */ $message);
Loading history...
2545
		$resourceRecipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $getResourcesRestriction);
2546
2547
		$this->errorSetResource = false;
2548
		$resourceRecipData = [];
2549
2550
		// Put appointment into store resource users
2551
		$i = 0;
2552
		$len = count($resourceRecipients);
2553
		while (!$this->errorSetResource && $i < $len) {
2554
			$userStore = $this->openCustomUserStore($resourceRecipients[$i][PR_ENTRYID]);
2555
2556
			// Open root folder
2557
			$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

2557
			$userRoot = mapi_msgstore_openentry(/** @scrutinizer ignore-type */ $userStore, null);
Loading history...
2558
2559
			// Get calendar entryID
2560
			$userRootProps = mapi_getprops($userRoot, [PR_STORE_ENTRYID, PR_IPM_APPOINTMENT_ENTRYID, PR_FREEBUSY_ENTRYIDS]);
2561
2562
			// Open Calendar folder
2563
			$accessToFolder = false;
0 ignored issues
show
The assignment to $accessToFolder is dead and can be removed.
Loading history...
2564
2565
			try {
2566
				// @FIXME this checks delegate has access to resource's calendar folder
2567
				// but it should use boss' credentials
2568
2569
				$accessToFolder = $this->checkCalendarWriteAccess($this->store);
2570
				if ($accessToFolder) {
2571
					$calFolder = mapi_msgstore_openentry($userStore, $userRootProps[PR_IPM_APPOINTMENT_ENTRYID]);
2572
				}
2573
			}
2574
			catch (MAPIException $e) {
2575
				$e->setHandled();
2576
				$this->errorSetResource = 1; // No access
2577
			}
2578
2579
			if ($accessToFolder) {
2580
				/**
2581
				 * Get the LocalFreebusy message that contains the properties that
2582
				 * are set to accept or decline resource meeting requests.
2583
				 */
2584
				$localFreebusyMsg = FreeBusy::getLocalFreeBusyMessage($userStore);
2585
				if ($localFreebusyMsg) {
2586
					$props = mapi_getprops($localFreebusyMsg, [PR_SCHDINFO_AUTO_ACCEPT_APPTS, PR_SCHDINFO_DISALLOW_RECURRING_APPTS, PR_SCHDINFO_DISALLOW_OVERLAPPING_APPTS]);
2587
2588
					$acceptMeetingRequests = isset($props[PR_SCHDINFO_AUTO_ACCEPT_APPTS]) ? $props[PR_SCHDINFO_AUTO_ACCEPT_APPTS] : false;
2589
					$declineRecurringMeetingRequests = isset($props[PR_SCHDINFO_DISALLOW_RECURRING_APPTS]) ? $props[PR_SCHDINFO_DISALLOW_RECURRING_APPTS] : false;
2590
					$declineConflictingMeetingRequests = isset($props[PR_SCHDINFO_DISALLOW_OVERLAPPING_APPTS]) ? $props[PR_SCHDINFO_DISALLOW_OVERLAPPING_APPTS] : false;
2591
2592
					if (!$acceptMeetingRequests) {
2593
						/*
2594
						 * When a resource has not been set to automatically accept meeting requests,
2595
						 * the meeting request has to be sent to him rather than being put directly into
2596
						 * his calendar. No error should be returned.
2597
						 */
2598
						// $errorSetResource = 2;
2599
						$this->nonAcceptingResources[] = $resourceRecipients[$i];
2600
					}
2601
					else {
2602
						if ($declineRecurringMeetingRequests && !$cancel) {
2603
							// Check if appointment is recurring
2604
							if ($messageprops[$this->proptags['recurring']]) {
2605
								$this->errorSetResource = 3;
2606
							}
2607
						}
2608
						if ($declineConflictingMeetingRequests && !$cancel) {
2609
							// Check for conflicting items
2610
							if ($calFolder && $this->isMeetingConflicting($message, $userStore, $calFolder)) {
2611
								$this->errorSetResource = 4; // Conflict
2612
							}
2613
						}
2614
					}
2615
				}
2616
			}
2617
2618
			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...
2619
				/**
2620
				 * First search on GlobalID(0x3)
2621
				 * 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.
2622
				 * If (normal meeting) then GlobalID(0x3) and CleanGlobalID(0x23) are same, so doesn't matter if search is based on GlobalID.
2623
				 */
2624
				$rows = $this->findCalendarItems($messageprops[$this->proptags['goid']], $calFolder);
2625
2626
				/*
2627
				 * If no entry is found then
2628
				 * 1) Resource doesn't have meeting in Calendar. Seriously!!
2629
				 * OR
2630
				 * 2) We were looking for occurrence item but Resource has whole series
2631
				 */
2632
				if (empty($rows)) {
2633
					/**
2634
					 * Now search on CleanGlobalID(0x23) WHY???
2635
					 * Because we are looking recurring item.
2636
					 *
2637
					 * Possible results of this search
2638
					 * 1) If Resource was booked for more than one occurrences then this search will return all those occurrence because search is perform on CleanGlobalID
2639
					 * 2) If Resource was booked for whole series then it should return series.
2640
					 */
2641
					$rows = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder, true);
2642
2643
					$newResourceMsg = false;
2644
					if (!empty($rows)) {
2645
						// Since we are looking for recurring item, open every result and check for 'recurring' property.
2646
						foreach ($rows as $row) {
2647
							$ResourceMsg = mapi_msgstore_openentry($userStore, $row);
2648
							$ResourceMsgProps = mapi_getprops($ResourceMsg, [$this->proptags['recurring']]);
2649
2650
							if (isset($ResourceMsgProps[$this->proptags['recurring']]) && $ResourceMsgProps[$this->proptags['recurring']]) {
2651
								$newResourceMsg = $ResourceMsg;
2652
								break;
2653
							}
2654
						}
2655
					}
2656
2657
					// Still no results found. I giveup, create new message.
2658
					if (!$newResourceMsg) {
2659
						$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

2659
						$newResourceMsg = mapi_folder_createmessage(/** @scrutinizer ignore-type */ $calFolder);
Loading history...
2660
					}
2661
				}
2662
				else {
2663
					$newResourceMsg = mapi_msgstore_openentry($userStore, $rows[0]);
2664
				}
2665
2666
				// Prefix the subject if needed
2667
				if ($prefix && isset($messageprops[PR_SUBJECT])) {
2668
					$messageprops[PR_SUBJECT] = $prefix . $messageprops[PR_SUBJECT];
2669
				}
2670
2671
				// Set status to cancelled if needed
2672
				$messageprops[$this->proptags['busystatus']] = fbBusy; // The default status (Busy)
2673
				if ($cancel) {
2674
					$messageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // The meeting has been canceled
2675
					$messageprops[$this->proptags['busystatus']] = fbFree; // Free
2676
				}
2677
				else {
2678
					$messageprops[$this->proptags['meetingstatus']] = olMeetingReceived; // The recipient is receiving the request
2679
				}
2680
				$messageprops[$this->proptags['responsestatus']] = olResponseAccepted; // The resource automatically accepts the appointment
2681
2682
				$messageprops[PR_MESSAGE_CLASS] = 'IPM.Appointment';
2683
2684
				// Remove the PR_ICON_INDEX as it is not needed in the sent message.
2685
				$messageprops[PR_ICON_INDEX] = null;
2686
				$messageprops[PR_RESPONSE_REQUESTED] = true;
2687
2688
				// get the store of organizer, in case of delegates it will be delegate store
2689
				$defaultStore = $this->openDefaultStore();
2690
2691
				$storeProps = mapi_getprops($this->store, [PR_ENTRYID]);
2692
				$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

2692
				$defaultStoreProps = mapi_getprops(/** @scrutinizer ignore-type */ $defaultStore, [PR_ENTRYID]);
Loading history...
2693
2694
				// @FIXME use entryid comparison functions here
2695
				if ($storeProps[PR_ENTRYID] !== $defaultStoreProps[PR_ENTRYID]) {
2696
					// get delegate information
2697
					$addrInfo = $this->getOwnerAddress($defaultStore, false);
2698
2699
					if (!empty($addrInfo)) {
2700
						list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $addrInfo;
2701
2702
						$messageprops[PR_SENDER_EMAIL_ADDRESS] = $owneremailaddr;
2703
						$messageprops[PR_SENDER_NAME] = $ownername;
2704
						$messageprops[PR_SENDER_ADDRTYPE] = $owneraddrtype;
2705
						$messageprops[PR_SENDER_ENTRYID] = $ownerentryid;
2706
						$messageprops[PR_SENDER_SEARCH_KEY] = $ownersearchkey;
2707
					}
2708
2709
					// get delegator information
2710
					$addrInfo = $this->getOwnerAddress($this->store, false);
2711
2712
					if (!empty($addrInfo)) {
2713
						list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $addrInfo;
2714
2715
						$messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $owneremailaddr;
2716
						$messageprops[PR_SENT_REPRESENTING_NAME] = $ownername;
2717
						$messageprops[PR_SENT_REPRESENTING_ADDRTYPE] = $owneraddrtype;
2718
						$messageprops[PR_SENT_REPRESENTING_ENTRYID] = $ownerentryid;
2719
						$messageprops[PR_SENT_REPRESENTING_SEARCH_KEY] = $ownersearchkey;
2720
					}
2721
				}
2722
				else {
2723
					// get organizer information
2724
					$addrInfo = $this->getOwnerAddress($this->store);
2725
2726
					if (!empty($addrInfo)) {
2727
						list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $addrInfo;
2728
2729
						$messageprops[PR_SENDER_EMAIL_ADDRESS] = $owneremailaddr;
2730
						$messageprops[PR_SENDER_NAME] = $ownername;
2731
						$messageprops[PR_SENDER_ADDRTYPE] = $owneraddrtype;
2732
						$messageprops[PR_SENDER_ENTRYID] = $ownerentryid;
2733
						$messageprops[PR_SENDER_SEARCH_KEY] = $ownersearchkey;
2734
2735
						$messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $owneremailaddr;
2736
						$messageprops[PR_SENT_REPRESENTING_NAME] = $ownername;
2737
						$messageprops[PR_SENT_REPRESENTING_ADDRTYPE] = $owneraddrtype;
2738
						$messageprops[PR_SENT_REPRESENTING_ENTRYID] = $ownerentryid;
2739
						$messageprops[PR_SENT_REPRESENTING_SEARCH_KEY] = $ownersearchkey;
2740
					}
2741
				}
2742
2743
				$messageprops[$this->proptags['replytime']] = time();
2744
2745
				if ($basedate && isset($ResourceMsgProps[$this->proptags['recurring']]) && $ResourceMsgProps[$this->proptags['recurring']]) {
2746
					$recurr = new Recurrence($userStore, $newResourceMsg);
2747
2748
					// Copy recipients list
2749
					$reciptable = mapi_message_getrecipienttable($message);
2750
					$recips = mapi_table_queryallrows($reciptable, $this->recipprops);
2751
2752
					// add owner to recipient table
2753
					$this->addOrganizer($messageprops, $recips, true);
2754
2755
					// Update occurrence
2756
					if ($recurr->isException($basedate)) {
2757
						$recurr->modifyException($messageprops, $basedate, $recips);
2758
					}
2759
					else {
2760
						$recurr->createException($messageprops, $basedate, false, $recips);
2761
					}
2762
				}
2763
				else {
2764
					mapi_setprops($newResourceMsg, $messageprops);
2765
2766
					// Copy attachments
2767
					$this->replaceAttachments($message, $newResourceMsg);
2768
2769
					// Copy all recipients too
2770
					$this->replaceRecipients($message, $newResourceMsg);
2771
2772
					// Now add organizer also to recipient table
2773
					$recips = [];
2774
					$this->addOrganizer($messageprops, $recips);
2775
2776
					mapi_message_modifyrecipients($newResourceMsg, MODRECIP_ADD, $recips);
2777
				}
2778
2779
				mapi_savechanges($newResourceMsg);
2780
2781
				$resourceRecipData[] = [
2782
					'store' => $userStore,
2783
					'folder' => $calFolder,
2784
					'msg' => $newResourceMsg,
2785
				];
2786
				$this->includesResources = true;
2787
			}
2788
			else {
2789
				/*
2790
				 * If no other errors occurred and you have no access to the
2791
				 * folder of the resource, throw an error=1.
2792
				 */
2793
				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...
2794
					$this->errorSetResource = 1;
2795
				}
2796
2797
				for ($j = 0, $len = count($resourceRecipData); $j < $len; ++$j) {
2798
					// Get the EntryID
2799
					$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

2799
					$props = /** @scrutinizer ignore-call */ mapi_message_getprops($resourceRecipData[$j]['msg']);
Loading history...
2800
2801
					mapi_folder_deletemessages($resourceRecipData[$j]['folder'], [$props[PR_ENTRYID]], DELETE_HARD_DELETE);
2802
				}
2803
				$this->recipientDisplayname = $resourceRecipients[$i][PR_DISPLAY_NAME];
2804
			}
2805
			++$i;
2806
		}
2807
2808
		/*
2809
		 * Set the BCC-recipients (resources) tackstatus to accepted.
2810
		 */
2811
		// Get resource recipients
2812
		$getResourcesRestriction = [
2813
			RES_PROPERTY,
2814
			[
2815
				RELOP => RELOP_EQ,	// Equals recipient type 3: Resource
2816
				ULPROPTAG => PR_RECIPIENT_TYPE,
2817
				VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
2818
			],
2819
		];
2820
		$recipienttable = mapi_message_getrecipienttable($message);
2821
		$resourceRecipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $getResourcesRestriction);
2822
		if (!empty($resourceRecipients)) {
2823
			// Set Tracking status of resource recipients to olResponseAccepted (3)
2824
			for ($i = 0, $len = count($resourceRecipients); $i < $len; ++$i) {
2825
				$resourceRecipients[$i][PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusAccepted;
2826
				$resourceRecipients[$i][PR_RECIPIENT_TRACKSTATUS_TIME] = time();
2827
			}
2828
			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

2828
			mapi_message_modifyrecipients(/** @scrutinizer ignore-type */ $message, MODRECIP_MODIFY, $resourceRecipients);
Loading history...
2829
		}
2830
2831
		return $resourceRecipData;
2832
	}
2833
2834
	/**
2835
	 * Function which save an exception into recurring item.
2836
	 *
2837
	 * @param resource $recurringItem  reference to MAPI_message of recurring item
2838
	 * @param resource $occurrenceItem reference to MAPI_message of occurrence
2839
	 * @param string   $basedate       basedate of occurrence
2840
	 * @param bool     $move           if true then occurrence item is deleted
2841
	 * @param bool     $tentative      true if user has tentatively accepted it or false if user has accepted it
2842
	 * @param bool     $userAction     true if user has manually responded to meeting request
2843
	 * @param resource $store          user store
2844
	 * @param bool     $isDelegate     true if delegate is processing this meeting request
2845
	 */
2846
	public function acceptException(&$recurringItem, &$occurrenceItem, $basedate, $move, $tentative, $userAction, $store, $isDelegate = false): void {
2847
		$recurr = new Recurrence($store, $recurringItem);
2848
2849
		// Copy properties from meeting request
2850
		$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

2850
		$exception_props = mapi_getprops(/** @scrutinizer ignore-type */ $occurrenceItem);
Loading history...
2851
2852
		// Copy recipients list
2853
		$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

2853
		$reciptable = mapi_message_getrecipienttable(/** @scrutinizer ignore-type */ $occurrenceItem);
Loading history...
2854
		// If delegate, then do not add the delegate in recipients
2855
		if ($isDelegate) {
2856
			$delegate = mapi_getprops($this->message, [PR_RECEIVED_BY_EMAIL_ADDRESS]);
2857
			$res = [
2858
				RES_PROPERTY,
2859
				[
2860
					RELOP => RELOP_NE,
2861
					ULPROPTAG => PR_EMAIL_ADDRESS,
2862
					VALUE => [PR_EMAIL_ADDRESS => $delegate[PR_RECEIVED_BY_EMAIL_ADDRESS]],
2863
				],
2864
			];
2865
			$recips = mapi_table_queryallrows($reciptable, $this->recipprops, $res);
2866
		}
2867
		else {
2868
			$recips = mapi_table_queryallrows($reciptable, $this->recipprops);
2869
		}
2870
2871
		// add owner to recipient table
2872
		$this->addOrganizer($exception_props, $recips, true);
2873
2874
		// add delegator to meetings
2875
		if ($isDelegate) {
2876
			$this->addDelegator($exception_props, $recips);
2877
		}
2878
2879
		$exception_props[$this->proptags['meetingstatus']] = olMeetingReceived;
2880
		$exception_props[$this->proptags['responsestatus']] = $userAction ? ($tentative ? olResponseTentative : olResponseAccepted) : olResponseNotResponded;
2881
2882
		if (isset($exception_props[$this->proptags['intendedbusystatus']])) {
2883
			if ($tentative && $exception_props[$this->proptags['intendedbusystatus']] !== fbFree) {
2884
				$exception_props[$this->proptags['busystatus']] = fbTentative;
2885
			}
2886
			else {
2887
				$exception_props[$this->proptags['busystatus']] = $exception_props[$this->proptags['intendedbusystatus']];
2888
			}
2889
			// we already have intendedbusystatus value in $exception_props so no need to copy it
2890
		}
2891
		else {
2892
			$exception_props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
2893
		}
2894
2895
		if ($userAction) {
2896
			$addrInfo = $this->getOwnerAddress($this->store);
2897
2898
			// if user has responded then set replytime and name
2899
			$exception_props[$this->proptags['replytime']] = time();
2900
			if (!empty($addrInfo)) {
2901
				$exception_props[$this->proptags['apptreplyname']] = $addrInfo[0];
2902
			}
2903
		}
2904
2905
		if ($recurr->isException($basedate)) {
2906
			$recurr->modifyException($exception_props, $basedate, $recips, $occurrenceItem);
2907
		}
2908
		else {
2909
			$recurr->createException($exception_props, $basedate, false, $recips, $occurrenceItem);
2910
		}
2911
2912
		// Move the occurrenceItem to the waste basket
2913
		if ($move) {
2914
			$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
2915
			$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

2915
			$sourcefolder = mapi_msgstore_openentry(/** @scrutinizer ignore-type */ $store, $exception_props[PR_PARENT_ENTRYID]);
Loading history...
2916
			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

2916
			mapi_folder_copymessages($sourcefolder, [$exception_props[PR_ENTRYID]], /** @scrutinizer ignore-type */ $wastebasket, MESSAGE_MOVE);
Loading history...
2917
		}
2918
2919
		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

2919
		mapi_savechanges(/** @scrutinizer ignore-type */ $recurringItem);
Loading history...
2920
	}
2921
2922
	/**
2923
	 * Function which merges an exception mapi message to recurring message.
2924
	 * This will be used when we receive recurring meeting request and we already have an exception message
2925
	 * of same meeting in calendar and we need to remove that exception message and add it to attachment table
2926
	 * of recurring meeting.
2927
	 *
2928
	 * @param resource $recurringItem  reference to MAPI_message of recurring item
2929
	 * @param resource $occurrenceItem reference to MAPI_message of occurrence
2930
	 * @param mixed    $basedate       basedate of occurrence
2931
	 * @param resource $store          user store
2932
	 */
2933
	public function mergeException(&$recurringItem, &$occurrenceItem, $basedate, $store): void {
2934
		$recurr = new Recurrence($store, $recurringItem);
2935
2936
		// Copy properties from meeting request
2937
		$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

2937
		$exception_props = mapi_getprops(/** @scrutinizer ignore-type */ $occurrenceItem);
Loading history...
2938
2939
		// Get recipient list from message and add it to exception attachment
2940
		$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

2940
		$reciptable = mapi_message_getrecipienttable(/** @scrutinizer ignore-type */ $occurrenceItem);
Loading history...
2941
		$recips = mapi_table_queryallrows($reciptable, $this->recipprops);
2942
2943
		if ($recurr->isException($basedate)) {
2944
			$recurr->modifyException($exception_props, $basedate, $recips, $occurrenceItem);
2945
		}
2946
		else {
2947
			$recurr->createException($exception_props, $basedate, false, $recips, $occurrenceItem);
2948
		}
2949
2950
		// Move the occurrenceItem to the waste basket
2951
		$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
2952
		$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

2952
		$sourcefolder = mapi_msgstore_openentry(/** @scrutinizer ignore-type */ $store, $exception_props[PR_PARENT_ENTRYID]);
Loading history...
2953
		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

2953
		mapi_folder_copymessages($sourcefolder, [$exception_props[PR_ENTRYID]], /** @scrutinizer ignore-type */ $wastebasket, MESSAGE_MOVE);
Loading history...
2954
2955
		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

2955
		mapi_savechanges(/** @scrutinizer ignore-type */ $recurringItem);
Loading history...
2956
	}
2957
2958
	/**
2959
	 * Function which submits meeting request based on arguments passed to it.
2960
	 *
2961
	 * @param resource $message        MAPI_message whose meeting request is to be sent
2962
	 * @param bool     $cancel         if true send request, else send cancellation
2963
	 * @param mixed    $prefix         subject prefix
2964
	 * @param mixed    $basedate       basedate for an occurrence
2965
	 * @param mixed    $recurObject    recurrence object of mr
2966
	 * @param bool     $copyExceptions When sending update mail for recurring item then we don't send exceptions in attachments
2967
	 * @param mixed    $modifiedRecips
2968
	 * @param mixed    $deletedRecips
2969
	 */
2970
	public function submitMeetingRequest($message, $cancel, $prefix, $basedate = false, $recurObject = false, $copyExceptions = true, $modifiedRecips = false, $deletedRecips = false): void {
2971
		$newmessageprops = $messageprops = mapi_getprops($this->message);
2972
		$new = $this->createOutgoingMessage();
2973
2974
		// Copy the entire message into the new meeting request message
2975
		if ($basedate) {
2976
			// messageprops contains properties of whole recurring series
2977
			// and newmessageprops contains properties of exception item
2978
			$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

2978
			$newmessageprops = mapi_getprops(/** @scrutinizer ignore-type */ $message);
Loading history...
2979
2980
			// Ensure that the correct basedate is set in the new message
2981
			$newmessageprops[$this->proptags['basedate']] = $basedate;
2982
2983
			// Set isRecurring to false, because this is an exception
2984
			$newmessageprops[$this->proptags['recurring']] = false;
2985
2986
			// set LID_IS_EXCEPTION to true
2987
			$newmessageprops[$this->proptags['is_exception']] = true;
2988
2989
			// Set to high importance
2990
			if ($cancel) {
2991
				$newmessageprops[PR_IMPORTANCE] = IMPORTANCE_HIGH;
2992
			}
2993
2994
			// Set startdate and enddate of exception
2995
			if ($cancel && $recurObject) {
2996
				$newmessageprops[$this->proptags['startdate']] = $recurObject->getOccurrenceStart($basedate);
2997
				$newmessageprops[$this->proptags['duedate']] = $recurObject->getOccurrenceEnd($basedate);
2998
			}
2999
3000
			// Set basedate in guid (0x3)
3001
			$newmessageprops[$this->proptags['goid']] = $this->setBasedateInGlobalID($messageprops[$this->proptags['goid2']], $basedate);
3002
			$newmessageprops[$this->proptags['goid2']] = $messageprops[$this->proptags['goid2']];
3003
			$newmessageprops[PR_OWNER_APPT_ID] = $messageprops[PR_OWNER_APPT_ID];
3004
3005
			// Get deleted recipiets from exception msg
3006
			$restriction = [
3007
				RES_AND,
3008
				[
3009
					[
3010
						RES_BITMASK,
3011
						[
3012
							ULTYPE => BMR_NEZ,
3013
							ULPROPTAG => PR_RECIPIENT_FLAGS,
3014
							ULMASK => recipExceptionalDeleted,
3015
						],
3016
					],
3017
					[
3018
						RES_BITMASK,
3019
						[
3020
							ULTYPE => BMR_EQZ,
3021
							ULPROPTAG => PR_RECIPIENT_FLAGS,
3022
							ULMASK => recipOrganizer,
3023
						],
3024
					],
3025
				],
3026
			];
3027
3028
			// In direct-booking mode, we don't need to send cancellations to resources
3029
			if ($this->enableDirectBooking) {
3030
				$restriction[1][] = [
3031
					RES_PROPERTY,
3032
					[
3033
						RELOP => RELOP_NE,	// Does not equal recipient type: MAPI_BCC (Resource)
3034
						ULPROPTAG => PR_RECIPIENT_TYPE,
3035
						VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
3036
					],
3037
				];
3038
			}
3039
3040
			$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

3040
			$recipienttable = mapi_message_getrecipienttable(/** @scrutinizer ignore-type */ $message);
Loading history...
3041
			$recipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $restriction);
3042
3043
			if (!$deletedRecips) {
3044
				$deletedRecips = array_merge([], $recipients);
3045
			}
3046
			else {
3047
				$deletedRecips = array_merge($deletedRecips, $recipients);
3048
			}
3049
		}
3050
3051
		// Remove the PR_ICON_INDEX as it is not needed in the sent message.
3052
		$newmessageprops[PR_ICON_INDEX] = null;
3053
		$newmessageprops[PR_RESPONSE_REQUESTED] = true;
3054
3055
		// PR_START_DATE and PR_END_DATE will be used by outlook to show the position in the calendar
3056
		$newmessageprops[PR_START_DATE] = $newmessageprops[$this->proptags['startdate']];
3057
		$newmessageprops[PR_END_DATE] = $newmessageprops[$this->proptags['duedate']];
3058
3059
		// Set updatecounter/AppointmentSequenceNumber
3060
		// get the value of latest updatecounter for the whole series and use it
3061
		$newmessageprops[$this->proptags['updatecounter']] = $messageprops[$this->proptags['last_updatecounter']];
3062
3063
		$meetingTimeInfo = $this->getMeetingTimeInfo();
3064
3065
		if ($meetingTimeInfo) {
3066
			// Needs to unset PR_HTML and PR_RTF_COMPRESSED props
3067
			// because while canceling meeting requests with edit text
3068
			// will override the PR_BODY because body value is not consistent with
3069
			// PR_HTML and PR_RTF_COMPRESSED value so in this case PR_RTF_COMPRESSED will
3070
			// get priority which override the PR_BODY value.
3071
			unset($newmessageprops[PR_HTML], $newmessageprops[PR_RTF_COMPRESSED]);
3072
3073
			$newmessageprops[PR_BODY] = $meetingTimeInfo;
3074
		}
3075
3076
		// Send all recurrence info in mail, if this is a recurrence meeting.
3077
		if (isset($messageprops[$this->proptags['recurring']]) && $messageprops[$this->proptags['recurring']]) {
3078
			if (!empty($messageprops[$this->proptags['recurring_pattern']])) {
3079
				$newmessageprops[$this->proptags['recurring_pattern']] = $messageprops[$this->proptags['recurring_pattern']];
3080
			}
3081
			$newmessageprops[$this->proptags['recurrence_data']] = $messageprops[$this->proptags['recurrence_data']];
3082
			$newmessageprops[$this->proptags['timezone_data']] = $messageprops[$this->proptags['timezone_data']];
3083
			$newmessageprops[$this->proptags['timezone']] = $messageprops[$this->proptags['timezone']];
3084
3085
			if ($recurObject) {
3086
				$this->generateRecurDates($recurObject, $messageprops, $newmessageprops);
3087
			}
3088
		}
3089
3090
		if (isset($newmessageprops[$this->proptags['counter_proposal']])) {
3091
			unset($newmessageprops[$this->proptags['counter_proposal']]);
3092
		}
3093
3094
		// Prefix the subject if needed
3095
		if ($prefix && isset($newmessageprops[PR_SUBJECT])) {
3096
			$newmessageprops[PR_SUBJECT] = $prefix . $newmessageprops[PR_SUBJECT];
3097
		}
3098
3099
		if (isset($newmessageprops[$this->proptags['categories']]) &&
3100
			!empty($newmessageprops[$this->proptags['categories']])) {
3101
			unset($newmessageprops[$this->proptags['categories']]);
3102
		}
3103
		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

3103
		mapi_setprops(/** @scrutinizer ignore-type */ $new, $newmessageprops);
Loading history...
3104
3105
		// Copy attachments
3106
		$this->replaceAttachments($message, $new, $copyExceptions);
3107
3108
		// Retrieve only those recipient who should receive this meeting request.
3109
		$stripResourcesRestriction = [
3110
			RES_AND,
3111
			[
3112
				[
3113
					RES_BITMASK,
3114
					[
3115
						ULTYPE => BMR_EQZ,
3116
						ULPROPTAG => PR_RECIPIENT_FLAGS,
3117
						ULMASK => recipExceptionalDeleted,
3118
					],
3119
				],
3120
				[
3121
					RES_BITMASK,
3122
					[
3123
						ULTYPE => BMR_EQZ,
3124
						ULPROPTAG => PR_RECIPIENT_FLAGS,
3125
						ULMASK => recipOrganizer,
3126
					],
3127
				],
3128
			],
3129
		];
3130
3131
		// In direct-booking mode, resources do not receive a meeting request
3132
		if ($this->enableDirectBooking) {
3133
			$stripResourcesRestriction[1][] = [
3134
				RES_PROPERTY,
3135
				[
3136
					RELOP => RELOP_NE,	// Does not equal recipient type: MAPI_BCC (Resource)
3137
					ULPROPTAG => PR_RECIPIENT_TYPE,
3138
					VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
3139
				],
3140
			];
3141
		}
3142
3143
		// If no recipients were explicitly provided, we will send the update to all
3144
		// recipients from the meeting.
3145
		if ($modifiedRecips === false) {
3146
			$recipienttable = mapi_message_getrecipienttable($message);
3147
			$modifiedRecips = mapi_table_queryallrows($recipienttable, $this->recipprops, $stripResourcesRestriction);
3148
3149
			if ($basedate && empty($modifiedRecips)) {
3150
				// Retrieve full list
3151
				$recipienttable = mapi_message_getrecipienttable($this->message);
3152
				$modifiedRecips = mapi_table_queryallrows($recipienttable, $this->recipprops);
3153
3154
				// Save recipients in exceptions
3155
				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

3155
				mapi_message_modifyrecipients(/** @scrutinizer ignore-type */ $message, MODRECIP_ADD, $modifiedRecips);
Loading history...
3156
3157
				// Now retrieve only those recipient who should receive this meeting request.
3158
				$modifiedRecips = mapi_table_queryallrows($recipienttable, $this->recipprops, $stripResourcesRestriction);
3159
			}
3160
		}
3161
3162
		// @TODO: handle nonAcceptingResources
3163
		/*
3164
		 * Add resource recipients that did not automatically accept the meeting request.
3165
		 * (note: meaning that they did not decline the meeting request)
3166
		 */ /*
3167
		for($i=0;$i<count($this->nonAcceptingResources);$i++){
3168
			$recipients[] = $this->nonAcceptingResources[$i];
3169
		}*/
3170
3171
		if (!empty($modifiedRecips)) {
3172
			// Strip out the sender/'owner' recipient
3173
			mapi_message_modifyrecipients($new, MODRECIP_ADD, $modifiedRecips);
3174
3175
			// Set some properties that are different in the sent request than
3176
			// in the item in our calendar
3177
3178
			// we should store busystatus value to intendedbusystatus property, because busystatus for outgoing meeting request
3179
			// should always be fbTentative
3180
			$newmessageprops[$this->proptags['intendedbusystatus']] = isset($newmessageprops[$this->proptags['busystatus']]) ? $newmessageprops[$this->proptags['busystatus']] : $messageprops[$this->proptags['busystatus']];
3181
			$newmessageprops[$this->proptags['busystatus']] = fbTentative; // The default status when not accepted
3182
			$newmessageprops[$this->proptags['responsestatus']] = olResponseNotResponded; // The recipient has not responded yet
3183
			$newmessageprops[$this->proptags['attendee_critical_change']] = time();
3184
			$newmessageprops[$this->proptags['owner_critical_change']] = time();
3185
			$newmessageprops[$this->proptags['meetingtype']] = mtgRequest;
3186
3187
			if ($cancel) {
3188
				$newmessageprops[PR_MESSAGE_CLASS] = 'IPM.Schedule.Meeting.Canceled';
3189
				$newmessageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // It's a cancel request
3190
				$newmessageprops[$this->proptags['busystatus']] = fbFree; // set the busy status as free
3191
			}
3192
			else {
3193
				$newmessageprops[PR_MESSAGE_CLASS] = 'IPM.Schedule.Meeting.Request';
3194
				$newmessageprops[$this->proptags['meetingstatus']] = olMeetingReceived; // The recipient is receiving the request
3195
			}
3196
3197
			mapi_setprops($new, $newmessageprops);
3198
			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

3198
			mapi_savechanges(/** @scrutinizer ignore-type */ $new);
Loading history...
3199
3200
			// Submit message to non-resource recipients
3201
			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

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

3327
		$outgoing = mapi_folder_createmessage(/** @scrutinizer ignore-type */ $outbox);
Loading history...
3328
3329
		// check if $store is set and it is not equal to $defaultStore (means its the delegation case)
3330
		if ($store !== false) {
3331
			$storeProps = mapi_getprops($store, [PR_ENTRYID]);
3332
			$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

3332
			$userStoreProps = mapi_getprops(/** @scrutinizer ignore-type */ $userStore, [PR_ENTRYID]);
Loading history...
3333
3334
			// @FIXME use entryid comparison functions here
3335
			if ($storeProps[PR_ENTRYID] !== $userStoreProps[PR_ENTRYID]) {
3336
				// get the delegator properties and set it into outgoing mail
3337
				$delegatorDetails = $this->getOwnerAddress($store, false);
3338
3339
				if (!empty($delegatorDetails)) {
3340
					list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $delegatorDetails;
3341
					$sentprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $owneremailaddr;
3342
					$sentprops[PR_SENT_REPRESENTING_NAME] = $ownername;
3343
					$sentprops[PR_SENT_REPRESENTING_ADDRTYPE] = $owneraddrtype;
3344
					$sentprops[PR_SENT_REPRESENTING_ENTRYID] = $ownerentryid;
3345
					$sentprops[PR_SENT_REPRESENTING_SEARCH_KEY] = $ownersearchkey;
3346
				}
3347
3348
				// get the delegate properties and set it into outgoing mail
3349
				$delegateDetails = $this->getOwnerAddress($userStore, false);
3350
3351
				if (!empty($delegateDetails)) {
3352
					list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $delegateDetails;
3353
					$sentprops[PR_SENDER_EMAIL_ADDRESS] = $owneremailaddr;
3354
					$sentprops[PR_SENDER_NAME] = $ownername;
3355
					$sentprops[PR_SENDER_ADDRTYPE] = $owneraddrtype;
3356
					$sentprops[PR_SENDER_ENTRYID] = $ownerentryid;
3357
					$sentprops[PR_SENDER_SEARCH_KEY] = $ownersearchkey;
3358
				}
3359
			}
3360
		}
3361
		else {
3362
			// normal user is sending mail, so both set of properties will be same
3363
			$userDetails = $this->getOwnerAddress($userStore);
3364
3365
			if (!empty($userDetails)) {
3366
				list($ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey) = $userDetails;
3367
				$sentprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $owneremailaddr;
3368
				$sentprops[PR_SENT_REPRESENTING_NAME] = $ownername;
3369
				$sentprops[PR_SENT_REPRESENTING_ADDRTYPE] = $owneraddrtype;
3370
				$sentprops[PR_SENT_REPRESENTING_ENTRYID] = $ownerentryid;
3371
				$sentprops[PR_SENT_REPRESENTING_SEARCH_KEY] = $ownersearchkey;
3372
3373
				$sentprops[PR_SENDER_EMAIL_ADDRESS] = $owneremailaddr;
3374
				$sentprops[PR_SENDER_NAME] = $ownername;
3375
				$sentprops[PR_SENDER_ADDRTYPE] = $owneraddrtype;
3376
				$sentprops[PR_SENDER_ENTRYID] = $ownerentryid;
3377
				$sentprops[PR_SENDER_SEARCH_KEY] = $ownersearchkey;
3378
			}
3379
		}
3380
3381
		$sentprops[PR_SENTMAIL_ENTRYID] = $this->getDefaultSentmailEntryID($userStore);
3382
3383
		mapi_setprops($outgoing, $sentprops);
3384
3385
		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...
3386
	}
3387
3388
	/**
3389
	 * Function which checks that meeting in attendee's calendar is already updated
3390
	 * and we are checking an old meeting request. This function also will update property
3391
	 * meetingtype to indicate that its out of date meeting request.
3392
	 *
3393
	 * @return bool true if meeting request is outofdate else false if it is new
3394
	 */
3395
	public function isMeetingOutOfDate() {
3396
		$result = false;
3397
3398
		$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']]);
3399
3400
		if (!$this->isMeetingRequest($props[PR_MESSAGE_CLASS])) {
3401
			return $result;
3402
		}
3403
3404
		if (isset($props[$this->proptags['meetingtype']]) && ($props[$this->proptags['meetingtype']] & mtgOutOfDate) == mtgOutOfDate) {
3405
			return true;
3406
		}
3407
3408
		// get the basedate to check for exception
3409
		$basedate = $this->getBasedateFromGlobalID($props[$this->proptags['goid']]);
3410
3411
		$calendarItem = $this->getCorrespondentCalendarItem(true);
3412
3413
		// if basedate is provided and we could not find the item then it could be that we are checking
3414
		// an exception so get the exception and check it
3415
		if ($basedate !== false && $calendarItem !== false) {
3416
			$exception = $this->getExceptionItem($calendarItem, $basedate);
3417
3418
			if ($exception !== false) {
0 ignored issues
show
The condition $exception !== false is always true.
Loading history...
3419
				// we are able to find the exception compare with it
3420
				$calendarItem = $exception;
3421
			}
3422
			// we are not able to find exception, could mean that a significant change has occurred on series
3423
			// and it deleted all exceptions, so compare with series
3424
			// $calendarItem already contains reference to series
3425
		}
3426
3427
		if ($calendarItem !== false) {
3428
			$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

3428
			$calendarItemProps = mapi_getprops(/** @scrutinizer ignore-type */ $calendarItem, [
Loading history...
3429
				$this->proptags['owner_critical_change'],
3430
				$this->proptags['updatecounter'],
3431
			]);
3432
3433
			$updateCounter = (isset($calendarItemProps[$this->proptags['updatecounter']]) && $props[$this->proptags['updatecounter']] < $calendarItemProps[$this->proptags['updatecounter']]);
3434
3435
			$criticalChange = (isset($calendarItemProps[$this->proptags['owner_critical_change']]) && $props[$this->proptags['owner_critical_change']] < $calendarItemProps[$this->proptags['owner_critical_change']]);
3436
3437
			if ($updateCounter || $criticalChange) {
3438
				// meeting request is out of date, set properties to indicate this
3439
				mapi_setprops($this->message, [$this->proptags['meetingtype'] => mtgOutOfDate, PR_ICON_INDEX => 1033]);
3440
				mapi_savechanges($this->message);
3441
3442
				$result = true;
3443
			}
3444
		}
3445
3446
		return $result;
3447
	}
3448
3449
	/**
3450
	 * Function which checks that if we have received a meeting response for an updated meeting in organizer's calendar.
3451
	 *
3452
	 * @param mixed $basedate basedate of the exception if we want to compare with exception
3453
	 *
3454
	 * @return bool true if meeting request is updated later
3455
	 */
3456
	public function isMeetingUpdated($basedate = false) {
3457
		$result = false;
3458
3459
		$props = mapi_getprops($this->message, [PR_MESSAGE_CLASS, $this->proptags['updatecounter']]);
3460
3461
		if (!$this->isMeetingRequestResponse($props[PR_MESSAGE_CLASS])) {
3462
			return $result;
3463
		}
3464
3465
		$calendarItem = $this->getCorrespondentCalendarItem(true);
3466
3467
		if ($calendarItem !== false) {
3468
			// basedate is provided so open exception
3469
			if ($basedate !== false) {
3470
				$exception = $this->getExceptionItem($calendarItem, $basedate);
3471
3472
				if ($exception !== false) {
0 ignored issues
show
The condition $exception !== false is always true.
Loading history...
3473
					// we are able to find the exception compare with it
3474
					$calendarItem = $exception;
3475
				}
3476
				// we are not able to find exception, could mean that a significant change has occurred on series
3477
				// and it deleted all exceptions, so compare with series
3478
				// $calendarItem already contains reference to series
3479
			}
3480
3481
			if ($calendarItem !== false) {
0 ignored issues
show
The condition $calendarItem !== false is always true.
Loading history...
3482
				$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

3482
				$calendarItemProps = mapi_getprops(/** @scrutinizer ignore-type */ $calendarItem, [$this->proptags['updatecounter']]);
Loading history...
3483
3484
				/*
3485
				 * if(message_counter < appointment_counter) meeting object is newer then meeting response (meeting is updated)
3486
				 * if(message_counter >= appointment_counter) meeting is not updated, do normal processing
3487
				 */
3488
				if (isset($calendarItemProps[$this->proptags['updatecounter']], $props[$this->proptags['updatecounter']])) {
3489
					if ($props[$this->proptags['updatecounter']] < $calendarItemProps[$this->proptags['updatecounter']]) {
3490
						$result = true;
3491
					}
3492
				}
3493
			}
3494
		}
3495
3496
		return $result;
3497
	}
3498
3499
	/**
3500
	 * Checks if there has been any significant changes on appointment/meeting item.
3501
	 * Significant changes be:
3502
	 * 1) startdate has been changed
3503
	 * 2) duedate has been changed OR
3504
	 * 3) recurrence pattern has been created, modified or removed.
3505
	 *
3506
	 * @param mixed $oldProps
3507
	 * @param mixed $basedate
3508
	 * @param mixed $isRecurrenceChanged for change in recurrence pattern.
3509
	 *                                   true means Recurrence pattern has been changed,
3510
	 *                                   so clear all attendees response
3511
	 */
3512
	public function checkSignificantChanges($oldProps, $basedate, $isRecurrenceChanged = false) {
3513
		$message = null;
3514
		$attach = null;
3515
3516
		// If basedate is specified then we need to open exception message to clear recipient responses
3517
		if ($basedate) {
3518
			$recurrence = new Recurrence($this->store, $this->message);
3519
			if ($recurrence->isException($basedate)) {
3520
				$attach = $recurrence->getExceptionAttachment($basedate);
3521
				if ($attach) {
3522
					$message = mapi_attach_openobj($attach, MAPI_MODIFY);
3523
				}
3524
			}
3525
		}
3526
		else {
3527
			// use normal message or recurring series message
3528
			$message = $this->message;
3529
		}
3530
3531
		if (!$message) {
3532
			return;
3533
		}
3534
3535
		$newProps = mapi_getprops($message, [$this->proptags['startdate'], $this->proptags['duedate'], $this->proptags['updatecounter']]);
3536
3537
		// Check whether message is updated or not.
3538
		if (isset($newProps[$this->proptags['updatecounter']]) && $newProps[$this->proptags['updatecounter']] == 0) {
3539
			return;
3540
		}
3541
3542
		if (($newProps[$this->proptags['startdate']] != $oldProps[$this->proptags['startdate']]) ||
3543
				($newProps[$this->proptags['duedate']] != $oldProps[$this->proptags['duedate']]) ||
3544
				$isRecurrenceChanged) {
3545
			$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

3545
			$this->clearRecipientResponse(/** @scrutinizer ignore-type */ $message);
Loading history...
3546
3547
			mapi_setprops($message, [$this->proptags['owner_critical_change'] => time()]);
3548
3549
			mapi_savechanges($message);
3550
			if ($attach) { // Also save attachment Object.
3551
				mapi_savechanges($attach);
3552
			}
3553
		}
3554
	}
3555
3556
	/**
3557
	 * Clear responses of all attendees who have replied in past.
3558
	 *
3559
	 * @param resource $message on which responses should be cleared
3560
	 */
3561
	public function clearRecipientResponse($message): void {
3562
		$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

3562
		$recipTable = mapi_message_getrecipienttable(/** @scrutinizer ignore-type */ $message);
Loading history...
3563
		$recipsRows = mapi_table_queryallrows($recipTable, $this->recipprops);
3564
3565
		foreach ($recipsRows as $recipient) {
3566
			if (($recipient[PR_RECIPIENT_FLAGS] & recipOrganizer) != recipOrganizer) {
3567
				// Recipient is attendee, set the trackstatus to 'Not Responded'
3568
				$recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
3569
			}
3570
			else {
3571
				// Recipient is organizer, this is not possible, but for safety
3572
				// it is best to clear the trackstatus for him as well by setting
3573
				// the trackstatus to 'Organized'.
3574
				$recipient[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
3575
			}
3576
			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

3576
			mapi_message_modifyrecipients(/** @scrutinizer ignore-type */ $message, MODRECIP_MODIFY, [$recipient]);
Loading history...
3577
		}
3578
	}
3579
3580
	/**
3581
	 * Function returns correspondent calendar item attached with the meeting request/response/cancellation.
3582
	 * This will only check for actual MAPIMessages in calendar folder, so if a meeting request is
3583
	 * for exception then this function will return recurring series for that meeting request
3584
	 * after that you need to use getExceptionItem function to get exception item that will be
3585
	 * fetched from the attachment table of recurring series MAPIMessage.
3586
	 *
3587
	 * @param bool $open boolean to indicate the function should return entryid or MAPIMessage. Defaults to true.
3588
	 *
3589
	 * @return bool|resource resource of calendar item
3590
	 */
3591
	public function getCorrespondentCalendarItem($open = true) {
3592
		$props = mapi_getprops($this->message, [PR_MESSAGE_CLASS, $this->proptags['goid'], $this->proptags['goid2'], PR_RCVD_REPRESENTING_ENTRYID]);
3593
3594
		if (!$this->isMeetingRequest($props[PR_MESSAGE_CLASS]) && !$this->isMeetingRequestResponse($props[PR_MESSAGE_CLASS]) && !$this->isMeetingCancellation($props[PR_MESSAGE_CLASS])) {
3595
			// can work only with meeting requests/responses/cancellations
3596
			return false;
3597
		}
3598
3599
		// there is no goid - no items can be found - aborting
3600
		if (empty($props[$this->proptags['goid']])) {
3601
			return false;
3602
		}
3603
		$globalId = $props[$this->proptags['goid']];
3604
3605
		$store = $this->store;
3606
		$calFolder = $this->openDefaultCalendar();
3607
		// If Delegate is processing Meeting Request/Response for Delegator then retrieve Delegator's store and calendar.
3608
		if (isset($props[PR_RCVD_REPRESENTING_ENTRYID])) {
3609
			$delegatorStore = $this->getDelegatorStore($props[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
3610
			if (!empty($delegatorStore['store'])) {
3611
				$store = $delegatorStore['store'];
3612
			}
3613
			if (!empty($delegatorStore[PR_IPM_APPOINTMENT_ENTRYID])) {
3614
				$calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
3615
			}
3616
		}
3617
3618
		$basedate = $this->getBasedateFromGlobalID($globalId);
3619
3620
		/**
3621
		 * First search for any appointments which correspond to the $globalId,
3622
		 * this can be the entire series (if the Meeting Request refers to the
3623
		 * entire series), or an particular Occurrence (if the meeting Request
3624
		 * contains a basedate).
3625
		 *
3626
		 * If we cannot find a corresponding item, and the $globalId contains
3627
		 * a $basedate, it might imply that a new exception will have to be
3628
		 * created for a series which is present in the calendar, we can look
3629
		 * that one up by searching for the $cleanGlobalId.
3630
		 */
3631
		$entryids = $this->findCalendarItems($globalId, $calFolder);
3632
		if ($basedate !== false && empty($entryids)) {
3633
			// only search if a goid2 is available
3634
			if (!empty($props[$this->proptags['goid2']])) {
3635
				$cleanGlobalId = $props[$this->proptags['goid2']];
3636
				$entryids = $this->findCalendarItems($cleanGlobalId, $calFolder, true);
3637
			}
3638
		}
3639
3640
		// there should be only one item returned
3641
		if (!empty($entryids) && count($entryids) === 1) {
3642
			// return only entryid
3643
			if ($open === false) {
3644
				return $entryids[0];
3645
			}
3646
3647
			// open calendar item and return it
3648
			if ($store) {
3649
				return mapi_msgstore_openentry($store, $entryids[0]);
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

3649
				return mapi_msgstore_openentry(/** @scrutinizer ignore-type */ $store, $entryids[0]);
Loading history...
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...
3650
			}
3651
		}
3652
3653
		// no items found in calendar
3654
		return false;
3655
	}
3656
3657
	/**
3658
	 * Function returns exception item based on the basedate passed.
3659
	 *
3660
	 * @param mixed $recurringMessage Resource of Recurring meeting from calendar
3661
	 * @param mixed $basedate         basedate of exception that needs to be returned
3662
	 * @param mixed $store            store that contains the recurring calendar item
3663
	 *
3664
	 * @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...
3665
	 */
3666
	public function getExceptionItem($recurringMessage, $basedate, $store = false) {
3667
		$occurItem = false;
3668
3669
		$props = mapi_getprops($this->message, [PR_RCVD_REPRESENTING_ENTRYID, $this->proptags['recurring']]);
3670
3671
		// check if the passed item is recurring series
3672
		if (isset($props[$this->proptags['recurring']]) && $props[$this->proptags['recurring']] !== false) {
3673
			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...
3674
		}
3675
3676
		if ($store === false) {
3677
			$store = $this->store;
3678
			// If Delegate is processing Meeting Request/Response for Delegator then retrieve Delegator's store and calendar.
3679
			if (isset($props[PR_RCVD_REPRESENTING_ENTRYID])) {
3680
				$delegatorStore = $this->getDelegatorStore($props[PR_RCVD_REPRESENTING_ENTRYID]);
3681
				if (!empty($delegatorStore['store'])) {
3682
					$store = $delegatorStore['store'];
3683
				}
3684
			}
3685
		}
3686
3687
		$recurr = new Recurrence($store, $recurringMessage);
3688
		$attach = $recurr->getExceptionAttachment($basedate);
3689
		if ($attach) {
3690
			$occurItem = mapi_attach_openobj($attach);
3691
		}
3692
3693
		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...
3694
	}
3695
3696
	/**
3697
	 * Function which checks whether received meeting request is either conflicting with other appointments or not.
3698
	 *
3699
	 * @param false|resource $message
3700
	 * @param false|resource $userStore
3701
	 * @param mixed          $calFolder calendar folder for conflict checking
3702
	 *
3703
	 * @return bool|int
3704
	 *
3705
	 * @psalm-return bool|int<1, max>
3706
	 */
3707
	public function isMeetingConflicting($message = false, $userStore = false, $calFolder = false) {
3708
		$returnValue = false;
3709
		$noOfInstances = 0;
3710
3711
		if ($message === false) {
3712
			$message = $this->message;
3713
		}
3714
3715
		$messageProps = mapi_getprops(
3716
			$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

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