Passed
Push — master ( fb113c...cd6099 )
by
unknown
28:17 queued 14:28
created

class.meetingrequest.php (91 issues)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1372
					$entryids = mapi_getprops(/** @scrutinizer ignore-type */ $calendarItem, [PR_ENTRYID]);
Loading history...
1373
1374
					$entryids = [$entryids[PR_ENTRYID]];
1375
1376
					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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2855
		$exception_props = mapi_getprops(/** @scrutinizer ignore-type */ $occurrenceItem);
Loading history...
2856
2857
		// Copy recipients list
2858
		$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

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

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

2921
			mapi_folder_copymessages($sourcefolder, [$exception_props[PR_ENTRYID]], /** @scrutinizer ignore-type */ $wastebasket, MESSAGE_MOVE);
Loading history...
2922
		}
2923
2924
		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

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

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

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

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

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

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

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

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

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

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

3203
			mapi_savechanges(/** @scrutinizer ignore-type */ $new);
Loading history...
3204
3205
			// Submit message to non-resource recipients
3206
			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

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

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

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

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

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

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

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

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

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

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