Meetingrequest::bookResources()   F
last analyzed

Complexity

Conditions 47
Paths > 20000

Size

Total Lines 330
Code Lines 165

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 1 Features 0
Metric Value
cc 47
eloc 165
c 4
b 1
f 0
nc 312971
nop 4
dl 0
loc 330
rs 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

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

611
				mapi_setprops(/** @scrutinizer ignore-type */ $calendarItem, $messageProps);
Loading history...
612
			}
613
614
			mapi_savechanges($calendarItem);
0 ignored issues
show
Bug introduced by
$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

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

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

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

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

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

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

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

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

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

1296
				mapi_folder_deletemessages(/** @scrutinizer ignore-type */ $calendar, $entryids);
Loading history...
1297
			}
1298
1299
			// All we have to do to decline, is to move the item to the waste basket
1300
			$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
1301
			$sourcefolder = $this->openParentFolder();
1302
1303
			$messageprops = mapi_getprops($this->message, [PR_ENTRYID]);
1304
1305
			// Release the message
1306
			$this->message = null;
1307
1308
			// Move the message to the waste basket
1309
			mapi_folder_copymessages($sourcefolder, [$messageprops[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
0 ignored issues
show
Bug introduced by
$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

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

1376
					$entryids = mapi_getprops(/** @scrutinizer ignore-type */ $calendarItem, [PR_ENTRYID]);
Loading history...
1377
1378
					$entryids = [$entryids[PR_ENTRYID]];
1379
1380
					mapi_folder_copymessages($calFolder, $entryids, $wastebasket, MESSAGE_MOVE);
0 ignored issues
show
Bug introduced by
$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

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

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

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

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

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

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

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

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

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

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

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

1930
		$provider = mapi_getprops(/** @scrutinizer ignore-type */ $store, [PR_MDB_PROVIDER]);
Loading history...
1931
		if (isset($provider[PR_MDB_PROVIDER]) && $provider[PR_MDB_PROVIDER] === ZARAFA_STORE_PUBLIC_GUID) {
1932
			$entryid = mapi_getprops($this->message, [PR_PARENT_ENTRYID]);
1933
			$entryid = $entryid[PR_PARENT_ENTRYID];
1934
		}
1935
		else {
1936
			$entryid = $this->getDefaultFolderEntryID(PR_IPM_APPOINTMENT_ENTRYID, $store);
1937
			if ($entryid === false) {
1938
				$entryid = $this->getBaseEntryID(PR_IPM_APPOINTMENT_ENTRYID, $store);
1939
			}
1940
1941
			if ($entryid === false) {
1942
				return false;
1943
			}
1944
		}
1945
1946
		return $this->checkFolderWriteAccess($entryid, $store);
1947
	}
1948
1949
	/**
1950
	 * Function will resolve the user and open its store.
1951
	 *
1952
	 * @param string $ownerentryid the entryid of the user
1953
	 *
1954
	 * @return resource store of the user
1955
	 */
1956
	public function openCustomUserStore($ownerentryid) {
1957
		$ab = mapi_openaddressbook($this->session);
1958
1959
		try {
1960
			$mailuser = mapi_ab_openentry($ab, $ownerentryid);
1961
			if (!$mailuser) {
0 ignored issues
show
introduced by
$mailuser is of type resource, thus it always evaluated to true.
Loading history...
1962
				error_log(sprintf("Unable to open ab entry: 0x%08X", mapi_last_hresult()));
1963
1964
				return;
1965
			}
1966
		}
1967
		catch (MAPIException) {
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
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(
2129
			$messageprops[$this->proptags['goid2']],
2130
			$basedate,
2131
			isset($recurr) && $recurr instanceof BaseRecurrence ? $recurr : null
2132
		);
2133
		$props[$this->proptags['goid2']] = $messageprops[$this->proptags['goid2']];
2134
		$props[$this->proptags['updatecounter']] = $messageprops[$this->proptags['updatecounter']] ?? 0;
2135
2136
		if (!empty($proposeNewTimeProps)) {
2137
			// merge proposal properties to message properties which will be sent to organizer
2138
			$props = $proposeNewTimeProps + $props;
2139
		}
2140
2141
		// Set body message in Appointment
2142
		if (isset($body)) {
2143
			$props[PR_BODY] = $this->getMeetingTimeInfo() ?: $body;
2144
		}
2145
2146
		// PR_START_DATE/PR_END_DATE is used in the UI in Outlook on the response message
2147
		$props[PR_START_DATE] = $messageprops[$this->proptags['startdate']];
2148
		$props[PR_END_DATE] = $messageprops[$this->proptags['duedate']];
2149
2150
		// Set startdate and duedate in response mail.
2151
		$props[$this->proptags['startdate']] = $messageprops[$this->proptags['startdate']];
2152
		$props[$this->proptags['duedate']] = $messageprops[$this->proptags['duedate']];
2153
2154
		// responselocation is used in the UI in Outlook on the response message
2155
		if (isset($messageprops[$this->proptags['location']])) {
2156
			$props[$this->proptags['responselocation']] = $messageprops[$this->proptags['location']];
2157
			$props[$this->proptags['location']] = $messageprops[$this->proptags['location']];
2158
		}
2159
2160
		$message = $this->createOutgoingMessage($store);
2161
2162
		mapi_setprops($message, $props);
0 ignored issues
show
Bug introduced by
$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

2162
		mapi_setprops(/** @scrutinizer ignore-type */ $message, $props);
Loading history...
2163
		mapi_message_modifyrecipients($message, MODRECIP_ADD, [$recip]);
0 ignored issues
show
Bug introduced by
$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

2163
		mapi_message_modifyrecipients(/** @scrutinizer ignore-type */ $message, MODRECIP_ADD, [$recip]);
Loading history...
2164
		mapi_savechanges($message);
0 ignored issues
show
Bug introduced by
$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

2164
		mapi_savechanges(/** @scrutinizer ignore-type */ $message);
Loading history...
2165
		mapi_message_submitmessage($message);
0 ignored issues
show
Bug introduced by
$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

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

2193
		$calendarcontents = mapi_folder_getcontentstable(/** @scrutinizer ignore-type */ $calendar);
Loading history...
2194
2195
		$rows = mapi_table_queryallrows($calendarcontents, [PR_ENTRYID], $restrict);
2196
2197
		if (empty($rows)) {
2198
			return;
2199
		}
2200
2201
		$calendaritems = [];
2202
2203
		// In principle, there should only be one row, but we'll handle them all just in case
2204
		foreach ($rows as $row) {
2205
			$calendaritems[] = $row[PR_ENTRYID];
2206
		}
2207
2208
		return $calendaritems;
2209
	}
2210
2211
	// Returns TRUE if both entryid's are equal. Equality is defined by both entryid's pointing at the
2212
	// same SMTP address when converted to SMTP
2213
	public function compareABEntryIDs($entryid1, $entryid2): bool {
2214
		// If the session was not passed, just do a 'normal' compare.
2215
		if (!$this->session) {
2216
			return $entryid1 == $entryid2;
2217
		}
2218
2219
		$smtp1 = $this->getSMTPAddress($entryid1);
2220
		$smtp2 = $this->getSMTPAddress($entryid2);
2221
2222
		if ($smtp1 == $smtp2) {
2223
			return true;
2224
		}
2225
2226
		return false;
2227
	}
2228
2229
	// Gets the SMTP address of the passed addressbook entryid
2230
	public function getSMTPAddress($entryid) {
2231
		if (!$this->session) {
2232
			return false;
2233
		}
2234
2235
		try {
2236
			$ab = mapi_openaddressbook($this->session);
2237
			$abitem = mapi_ab_openentry($ab, $entryid);
2238
2239
			if (!$abitem) {
0 ignored issues
show
introduced by
$abitem is of type resource, thus it always evaluated to true.
Loading history...
2240
				return '';
2241
			}
2242
		}
2243
		catch (MAPIException) {
2244
			return '';
2245
		}
2246
2247
		$props = mapi_getprops($abitem, [PR_ADDRTYPE, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS]);
2248
2249
		if ($props[PR_ADDRTYPE] == 'SMTP') {
2250
			return $props[PR_EMAIL_ADDRESS];
2251
		}
2252
2253
		return $props[PR_SMTP_ADDRESS];
2254
	}
2255
2256
	/**
2257
	 * Gets the properties associated with the owner of the passed store:
2258
	 * PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_ADDRTYPE, PR_ENTRYID, PR_SEARCH_KEY.
2259
	 *
2260
	 * @param mixed $store                  message store
2261
	 * @param bool  $fallbackToLoggedInUser If true then return properties of logged in user instead of mailbox owner.
2262
	 *                                      Not used when passed store is public store.
2263
	 *                                      For public store we are always returning logged in user's info.
2264
	 *
2265
	 * @return array|false properties of logged in user in an array in sequence of display_name, email address, address type, entryid and search key
2266
	 *
2267
	 * @psalm-return false|list{mixed, mixed, mixed, mixed, mixed}
2268
	 */
2269
	public function getOwnerAddress($store, $fallbackToLoggedInUser = true) {
2270
		if (!$this->session) {
2271
			return false;
2272
		}
2273
2274
		$storeProps = mapi_getprops($store, [PR_MAILBOX_OWNER_ENTRYID, PR_USER_ENTRYID]);
2275
2276
		$ownerEntryId = false;
2277
		if (isset($storeProps[PR_USER_ENTRYID]) && $storeProps[PR_USER_ENTRYID]) {
2278
			$ownerEntryId = $storeProps[PR_USER_ENTRYID];
2279
		}
2280
2281
		if (isset($storeProps[PR_MAILBOX_OWNER_ENTRYID]) && $storeProps[PR_MAILBOX_OWNER_ENTRYID] && !$fallbackToLoggedInUser) {
2282
			$ownerEntryId = $storeProps[PR_MAILBOX_OWNER_ENTRYID];
2283
		}
2284
2285
		if ($ownerEntryId) {
2286
			$ab = mapi_openaddressbook($this->session);
2287
2288
			$zarafaUser = mapi_ab_openentry($ab, $ownerEntryId);
2289
			if (!$zarafaUser) {
0 ignored issues
show
introduced by
$zarafaUser is of type resource, thus it always evaluated to true.
Loading history...
2290
				return false;
2291
			}
2292
2293
			$ownerProps = mapi_getprops($zarafaUser, [PR_ADDRTYPE, PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SEARCH_KEY]);
2294
2295
			$addrType = $ownerProps[PR_ADDRTYPE];
2296
			$name = $ownerProps[PR_DISPLAY_NAME];
2297
			$emailAddr = $ownerProps[PR_EMAIL_ADDRESS];
2298
			$searchKey = $ownerProps[PR_SEARCH_KEY];
2299
			$entryId = $ownerEntryId;
2300
2301
			return [$name, $emailAddr, $addrType, $entryId, $searchKey];
2302
		}
2303
2304
		return false;
2305
	}
2306
2307
	// Opens this session's default message store
2308
	public function openDefaultStore() {
2309
		$entryid = '';
2310
2311
		$storestable = mapi_getmsgstorestable($this->session);
2312
		$rows = mapi_table_queryallrows($storestable, [PR_ENTRYID, PR_DEFAULT_STORE]);
2313
2314
		foreach ($rows as $row) {
2315
			if (isset($row[PR_DEFAULT_STORE]) && $row[PR_DEFAULT_STORE]) {
2316
				$entryid = $row[PR_ENTRYID];
2317
				break;
2318
			}
2319
		}
2320
2321
		if (!$entryid) {
2322
			return false;
2323
		}
2324
2325
		return mapi_openmsgstore($this->session, $entryid);
2326
	}
2327
2328
	/**
2329
	 * Function which adds organizer to recipient list which is passed.
2330
	 * This function also checks if it has organizer.
2331
	 *
2332
	 * @param array $messageProps message properties
2333
	 * @param array $recipients   recipients list of message
2334
	 * @param bool  $isException  true if we are processing recipient of exception
2335
	 */
2336
	public function addOrganizer($messageProps, &$recipients, $isException = false): void {
2337
		$hasOrganizer = false;
2338
		// Check if meeting already has an organizer.
2339
		foreach ($recipients as $key => $recipient) {
2340
			if (isset($recipient[PR_RECIPIENT_FLAGS]) && $recipient[PR_RECIPIENT_FLAGS] == (recipSendable | recipOrganizer)) {
2341
				$hasOrganizer = true;
2342
			}
2343
			elseif ($isException && !isset($recipient[PR_RECIPIENT_FLAGS])) {
2344
				// Recipients for an occurrence
2345
				$recipients[$key][PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalResponse;
2346
			}
2347
		}
2348
2349
		if (!$hasOrganizer) {
2350
			// Create organizer.
2351
			$organizer = [];
2352
			$organizer[PR_ENTRYID] = $organizer[PR_RECIPIENT_ENTRYID] = $messageProps[PR_SENT_REPRESENTING_ENTRYID];
2353
			$organizer[PR_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
2354
			$organizer[PR_EMAIL_ADDRESS] = $messageProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
2355
			$organizer[PR_RECIPIENT_TYPE] = MAPI_TO;
2356
			$organizer[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
2357
			$organizer[PR_ADDRTYPE] = empty($messageProps[PR_SENT_REPRESENTING_ADDRTYPE]) ? 'SMTP' : $messageProps[PR_SENT_REPRESENTING_ADDRTYPE];
2358
			$organizer[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
2359
			$organizer[PR_RECIPIENT_FLAGS] = recipSendable | recipOrganizer;
2360
			$organizer[PR_SEARCH_KEY] = $messageProps[PR_SENT_REPRESENTING_SEARCH_KEY];
2361
			$organizer[PR_SMTP_ADDRESS] = $messageProps[PR_SENT_REPRESENTING_SMTP_ADDRESS] ?? $messageProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
2362
2363
			// Add organizer to recipients list.
2364
			array_unshift($recipients, $organizer);
2365
		}
2366
	}
2367
2368
	/**
2369
	 * Function which removes an exception/occurrence from recurrencing meeting
2370
	 * when a meeting cancellation of an occurrence is processed.
2371
	 *
2372
	 * @param mixed    $basedate basedate of an occurrence
2373
	 * @param mixed    $message  recurring item from which occurrence has to be deleted
2374
	 * @param resource $store    MAPI_MSG_Store which contains the item
2375
	 */
2376
	public function doRemoveExceptionFromCalendar($basedate, $message, $store): void {
2377
		$recurr = new Recurrence($store, $message);
2378
		$recurr->createException([], $basedate, true);
2379
		mapi_savechanges($message);
2380
	}
2381
2382
	/**
2383
	 * Function which returns basedate of an changed occurrence from globalID of meeting request.
2384
	 *
2385
	 * @param string $goid globalID
2386
	 *
2387
	 * @return false|int true if basedate is found else false it not found
2388
	 */
2389
	public function getBasedateFromGlobalID($goid) {
2390
		$hexguid = bin2hex($goid);
2391
		$hexbase = substr($hexguid, 32, 8);
2392
		$day = (int) hexdec(substr($hexbase, 6, 2));
2393
		$month = (int) hexdec(substr($hexbase, 4, 2));
2394
		$year = (int) hexdec(substr($hexbase, 0, 4));
2395
2396
		if ($day && $month && $year) {
2397
			return gmmktime(0, 0, 0, $month, $day, $year);
2398
		}
2399
2400
		return false;
2401
	}
2402
2403
	/**
2404
	 * Function which sets basedate in globalID of changed occurrence which is to be sent.
2405
	 *
2406
	 * @param string               $goid       globalID
2407
	 * @param mixed                $basedate   of changed occurrence (UTC when $recurrence is provided)
2408
	 * @param null|BaseRecurrence $recurrence recurrence helper for timezone conversion
2409
	 *
2410
	 * @return false|string globalID with basedate in it
2411
	 */
2412
	public function setBasedateInGlobalID($goid, $basedate = false, $recurrence = null) {
2413
		$hexguid = bin2hex($goid);
2414
		$timestamp = $basedate;
2415
2416
		if ($basedate !== false && $recurrence instanceof BaseRecurrence && isset($recurrence->tz)) {
2417
			$timestamp = $recurrence->fromGMT($recurrence->tz, $basedate);
2418
		}
2419
2420
		$year = $timestamp !== false ? sprintf('%04s', dechex((int) gmdate('Y', $timestamp))) : '0000';
2421
		$month = $timestamp !== false ? sprintf('%02s', dechex((int) gmdate('m', $timestamp))) : '00';
2422
		$day = $timestamp !== false ? sprintf('%02s', dechex((int) gmdate('d', $timestamp))) : '00';
2423
2424
		return hex2bin(strtoupper(substr($hexguid, 0, 32) . $year . $month . $day . substr($hexguid, 40)));
2425
	}
2426
2427
	/**
2428
	 * Function which replaces attachments with copy_from in copy_to.
2429
	 *
2430
	 * @param mixed $copyFrom       MAPI_message from which attachments are to be copied
2431
	 * @param mixed $copyTo         MAPI_message to which attachment are to be copied
2432
	 * @param bool  $copyExceptions if true then all exceptions should also be sent as attachments
2433
	 */
2434
	public function replaceAttachments($copyFrom, $copyTo, $copyExceptions = true): void {
2435
		/* remove all old attachments */
2436
		$attachmentTableTo = mapi_message_getattachmenttable($copyTo);
2437
		if ($attachmentTableTo) {
0 ignored issues
show
introduced by
$attachmentTableTo is of type resource, thus it always evaluated to true.
Loading history...
2438
			$attachments = mapi_table_queryallrows($attachmentTableTo, [PR_ATTACH_NUM, PR_ATTACH_METHOD, PR_EXCEPTION_STARTTIME]);
2439
2440
			foreach ($attachments as $attachProps) {
2441
				/* remove exceptions too? */
2442
				if (!$copyExceptions && $attachProps[PR_ATTACH_METHOD] == ATTACH_EMBEDDED_MSG && isset($attachProps[PR_EXCEPTION_STARTTIME])) {
2443
					continue;
2444
				}
2445
				mapi_message_deleteattach($copyTo, $attachProps[PR_ATTACH_NUM]);
2446
			}
2447
		}
2448
2449
		/* copy new attachments */
2450
		$attachmentTableFrom = mapi_message_getattachmenttable($copyFrom);
2451
		if ($attachmentTableFrom) {
0 ignored issues
show
introduced by
$attachmentTableFrom is of type resource, thus it always evaluated to true.
Loading history...
2452
			$attachments = mapi_table_queryallrows($attachmentTableFrom, [PR_ATTACH_NUM, PR_ATTACH_METHOD, PR_EXCEPTION_STARTTIME]);
2453
2454
			foreach ($attachments as $attachProps) {
2455
				if (!$copyExceptions && $attachProps[PR_ATTACH_METHOD] == ATTACH_EMBEDDED_MSG && isset($attachProps[PR_EXCEPTION_STARTTIME])) {
2456
					continue;
2457
				}
2458
2459
				$attachOld = mapi_message_openattach($copyFrom, (int) $attachProps[PR_ATTACH_NUM]);
2460
				$attachNewResourceMsg = mapi_message_createattach($copyTo);
2461
				mapi_copyto($attachOld, [], [], $attachNewResourceMsg, 0);
2462
				mapi_savechanges($attachNewResourceMsg);
2463
			}
2464
		}
2465
	}
2466
2467
	/**
2468
	 * Function which replaces recipients in copyTo with recipients from copyFrom.
2469
	 *
2470
	 * @param mixed $copyFrom   MAPI_message from which recipients are to be copied
2471
	 * @param mixed $copyTo     MAPI_message to which recipients are to be copied
2472
	 * @param bool  $isDelegate indicates whether delegate is processing
2473
	 *                          so don't copy delegate information to recipient table
2474
	 */
2475
	public function replaceRecipients($copyFrom, $copyTo, $isDelegate = false): void {
2476
		$recipientTable = mapi_message_getrecipienttable($copyFrom);
2477
2478
		// If delegate, then do not add the delegate in recipients
2479
		if ($isDelegate) {
2480
			$delegate = mapi_getprops($copyFrom, [PR_RECEIVED_BY_EMAIL_ADDRESS]);
2481
			$res = [
2482
				RES_PROPERTY,
2483
				[
2484
					RELOP => RELOP_NE,
2485
					ULPROPTAG => PR_EMAIL_ADDRESS,
2486
					VALUE => [PR_EMAIL_ADDRESS => $delegate[PR_RECEIVED_BY_EMAIL_ADDRESS]],
2487
				],
2488
			];
2489
			$recipients = mapi_table_queryallrows($recipientTable, $this->recipprops, $res);
2490
		}
2491
		else {
2492
			$recipients = mapi_table_queryallrows($recipientTable, $this->recipprops);
2493
		}
2494
2495
		$copyToRecipientTable = mapi_message_getrecipienttable($copyTo);
2496
		$copyToRecipientRows = mapi_table_queryallrows($copyToRecipientTable, [PR_ROWID]);
2497
2498
		mapi_message_modifyrecipients($copyTo, MODRECIP_REMOVE, $copyToRecipientRows);
2499
		mapi_message_modifyrecipients($copyTo, MODRECIP_ADD, $recipients);
2500
	}
2501
2502
	/**
2503
	 * Function creates meeting item in resource's calendar.
2504
	 *
2505
	 * @param resource $message  MAPI_message which is to create in resource's calendar
2506
	 * @param bool     $cancel   cancel meeting
2507
	 * @param mixed    $prefix   prefix for subject of meeting
2508
	 * @param mixed    $basedate
2509
	 *
2510
	 * @return (mixed|resource)[][]
2511
	 *
2512
	 * @psalm-return list<array{store: resource, folder: mixed, msg: mixed}>
2513
	 */
2514
	public function bookResources($message, $cancel, $prefix, $basedate = false): array {
2515
		if (!$this->enableDirectBooking) {
2516
			return [];
2517
		}
2518
2519
		// Get the properties of the message
2520
		$messageprops = mapi_getprops($message);
0 ignored issues
show
Bug introduced by
$message of type resource is incompatible with the type resource expected by parameter $any of mapi_getprops(). ( Ignorable by Annotation )

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

2520
		$messageprops = mapi_getprops(/** @scrutinizer ignore-type */ $message);
Loading history...
2521
2522
		$calFolder = '';
2523
2524
		if ($basedate) {
2525
			$recurrItemProps = mapi_getprops($this->message, [$this->proptags['goid'], $this->proptags['goid2'], $this->proptags['timezone_data'], $this->proptags['timezone'], PR_OWNER_APPT_ID]);
2526
2527
			$recurrenceHelper = new Recurrence($this->openDefaultStore(), $this->message);
0 ignored issues
show
Bug introduced by
$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

2527
			$recurrenceHelper = new Recurrence(/** @scrutinizer ignore-type */ $this->openDefaultStore(), $this->message);
Loading history...
2528
			$basedateUtc = $basedate;
2529
			if ($recurrenceHelper instanceof BaseRecurrence && isset($recurrenceHelper->tz)) {
2530
				$basedateUtc = $recurrenceHelper->toGMT($recurrenceHelper->tz, $basedate);
2531
			}
2532
2533
			$messageprops[$this->proptags['goid']] = $this->setBasedateInGlobalID($recurrItemProps[$this->proptags['goid']], $basedateUtc, $recurrenceHelper);
2534
			$messageprops[$this->proptags['goid2']] = $recurrItemProps[$this->proptags['goid2']];
2535
2536
			// Delete properties which are not needed.
2537
			$deleteProps = [$this->proptags['basedate'], PR_DISPLAY_NAME, PR_ATTACHMENT_FLAGS, PR_ATTACHMENT_HIDDEN, PR_ATTACHMENT_LINKID, PR_ATTACH_FLAGS, PR_ATTACH_METHOD];
2538
			foreach ($deleteProps as $propID) {
2539
				if (isset($messageprops[$propID])) {
2540
					unset($messageprops[$propID]);
2541
				}
2542
			}
2543
2544
			if (isset($messageprops[$this->proptags['recurring']])) {
2545
				$messageprops[$this->proptags['recurring']] = false;
2546
			}
2547
2548
			// Set Outlook properties
2549
			$messageprops[$this->proptags['clipstart']] = $messageprops[$this->proptags['startdate']];
2550
			$messageprops[$this->proptags['clipend']] = $messageprops[$this->proptags['duedate']];
2551
			$messageprops[$this->proptags['timezone_data']] = $recurrItemProps[$this->proptags['timezone_data']];
2552
			$messageprops[$this->proptags['timezone']] = $recurrItemProps[$this->proptags['timezone']];
2553
			$messageprops[$this->proptags['attendee_critical_change']] = time();
2554
			$messageprops[$this->proptags['owner_critical_change']] = time();
2555
		}
2556
2557
		// Get resource recipients
2558
		$getResourcesRestriction = [
2559
			RES_PROPERTY,
2560
			[
2561
				RELOP => RELOP_EQ,	// Equals recipient type 3: Resource
2562
				ULPROPTAG => PR_RECIPIENT_TYPE,
2563
				VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
2564
			],
2565
		];
2566
		$recipienttable = mapi_message_getrecipienttable($message);
0 ignored issues
show
Bug introduced by
$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

2566
		$recipienttable = mapi_message_getrecipienttable(/** @scrutinizer ignore-type */ $message);
Loading history...
2567
		$resourceRecipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $getResourcesRestriction);
2568
2569
		$this->errorSetResource = false;
2570
		$resourceRecipData = [];
2571
2572
		// Put appointment into store resource users
2573
		$i = 0;
2574
		$len = count($resourceRecipients);
2575
		while (!$this->errorSetResource && $i < $len) {
2576
			$userStore = $this->openCustomUserStore($resourceRecipients[$i][PR_ENTRYID]);
2577
2578
			// Open root folder
2579
			$userRoot = mapi_msgstore_openentry($userStore);
0 ignored issues
show
Bug introduced by
$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

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

2681
						$newResourceMsg = mapi_folder_createmessage(/** @scrutinizer ignore-type */ $calFolder);
Loading history...
2682
					}
2683
				}
2684
				else {
2685
					$newResourceMsg = mapi_msgstore_openentry($userStore, $rows[0]);
2686
				}
2687
2688
				// Prefix the subject if needed
2689
				if ($prefix && isset($messageprops[PR_SUBJECT])) {
2690
					$messageprops[PR_SUBJECT] = $prefix . $messageprops[PR_SUBJECT];
2691
				}
2692
2693
				// Set status to cancelled if needed
2694
				$messageprops[$this->proptags['busystatus']] = fbBusy; // The default status (Busy)
2695
				if ($cancel) {
2696
					$messageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // The meeting has been canceled
2697
					$messageprops[$this->proptags['busystatus']] = fbFree; // Free
2698
				}
2699
				else {
2700
					$messageprops[$this->proptags['meetingstatus']] = olMeetingReceived; // The recipient is receiving the request
2701
				}
2702
				$messageprops[$this->proptags['responsestatus']] = olResponseAccepted; // The resource automatically accepts the appointment
2703
2704
				$messageprops[PR_MESSAGE_CLASS] = 'IPM.Appointment';
2705
2706
				// Remove the PR_ICON_INDEX as it is not needed in the sent message.
2707
				$messageprops[PR_ICON_INDEX] = null;
2708
				$messageprops[PR_RESPONSE_REQUESTED] = true;
2709
2710
				// get the store of organizer, in case of delegates it will be delegate store
2711
				$defaultStore = $this->openDefaultStore();
2712
2713
				$storeProps = mapi_getprops($this->store, [PR_ENTRYID]);
2714
				$defaultStoreProps = mapi_getprops($defaultStore, [PR_ENTRYID]);
0 ignored issues
show
Bug introduced by
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

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

2821
					$props = /** @scrutinizer ignore-call */ mapi_message_getprops($resourceRecipData[$j]['msg']);
Loading history...
2822
2823
					mapi_folder_deletemessages($resourceRecipData[$j]['folder'], [$props[PR_ENTRYID]], DELETE_HARD_DELETE);
2824
				}
2825
				$this->recipientDisplayname = $resourceRecipients[$i][PR_DISPLAY_NAME];
2826
			}
2827
			++$i;
2828
		}
2829
2830
		$recipienttable = mapi_message_getrecipienttable($message);
2831
		$resourceRecipients = mapi_table_queryallrows($recipienttable, $this->recipprops);
2832
		if (!empty($resourceRecipients)) {
2833
			// Set Tracking status of resource recipients to olResponseAccepted (3)
2834
			for ($i = 0, $len = count($resourceRecipients); $i < $len; ++$i) {
2835
				if (isset($resourceRecipients[$i][PR_RECIPIENT_TYPE]) && $resourceRecipients[$i][PR_RECIPIENT_TYPE] == MAPI_BCC) {
2836
					$resourceRecipients[$i][PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusAccepted;
2837
					$resourceRecipients[$i][PR_RECIPIENT_TRACKSTATUS_TIME] = time();
2838
				}
2839
			}
2840
			mapi_message_modifyrecipients($message, MODRECIP_MODIFY, $resourceRecipients);
0 ignored issues
show
Bug introduced by
$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

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

2862
		$exception_props = mapi_getprops(/** @scrutinizer ignore-type */ $occurrenceItem);
Loading history...
2863
2864
		// Copy recipients list
2865
		$reciptable = mapi_message_getrecipienttable($occurrenceItem);
0 ignored issues
show
Bug introduced by
$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

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

2935
			$sourcefolder = mapi_msgstore_openentry(/** @scrutinizer ignore-type */ $store, $exception_props[PR_PARENT_ENTRYID]);
Loading history...
2936
			mapi_folder_copymessages($sourcefolder, [$exception_props[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
0 ignored issues
show
Bug introduced by
$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

2936
			mapi_folder_copymessages($sourcefolder, [$exception_props[PR_ENTRYID]], /** @scrutinizer ignore-type */ $wastebasket, MESSAGE_MOVE);
Loading history...
2937
		}
2938
2939
		mapi_savechanges($recurringItem);
0 ignored issues
show
Bug introduced by
$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

2939
		mapi_savechanges(/** @scrutinizer ignore-type */ $recurringItem);
Loading history...
2940
	}
2941
2942
	/**
2943
	 * Function which merges an exception mapi message to recurring message.
2944
	 * This will be used when we receive recurring meeting request and we already have an exception message
2945
	 * of same meeting in calendar and we need to remove that exception message and add it to attachment table
2946
	 * of recurring meeting.
2947
	 *
2948
	 * @param resource $recurringItem  reference to MAPI_message of recurring item
2949
	 * @param resource $occurrenceItem reference to MAPI_message of occurrence
2950
	 * @param mixed    $basedate       basedate of occurrence
2951
	 * @param resource $store          user store
2952
	 */
2953
	public function mergeException(&$recurringItem, &$occurrenceItem, $basedate, $store): void {
2954
		$recurr = new Recurrence($store, $recurringItem);
2955
2956
		// Copy properties from meeting request
2957
		$exception_props = mapi_getprops($occurrenceItem);
0 ignored issues
show
Bug introduced by
$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

2957
		$exception_props = mapi_getprops(/** @scrutinizer ignore-type */ $occurrenceItem);
Loading history...
2958
2959
		// Get recipient list from message and add it to exception attachment
2960
		$reciptable = mapi_message_getrecipienttable($occurrenceItem);
0 ignored issues
show
Bug introduced by
$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

2960
		$reciptable = mapi_message_getrecipienttable(/** @scrutinizer ignore-type */ $occurrenceItem);
Loading history...
2961
		$recips = mapi_table_queryallrows($reciptable, $this->recipprops);
2962
2963
		if ($recurr->isException($basedate)) {
2964
			$recurr->modifyException($exception_props, $basedate, $recips, $occurrenceItem);
2965
		}
2966
		else {
2967
			$recurr->createException($exception_props, $basedate, false, $recips, $occurrenceItem);
2968
		}
2969
2970
		// Move the occurrenceItem to the waste basket
2971
		$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
2972
		$sourcefolder = mapi_msgstore_openentry($store, $exception_props[PR_PARENT_ENTRYID]);
0 ignored issues
show
Bug introduced by
$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

2972
		$sourcefolder = mapi_msgstore_openentry(/** @scrutinizer ignore-type */ $store, $exception_props[PR_PARENT_ENTRYID]);
Loading history...
2973
		mapi_folder_copymessages($sourcefolder, [$exception_props[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
0 ignored issues
show
Bug introduced by
$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

2973
		mapi_folder_copymessages($sourcefolder, [$exception_props[PR_ENTRYID]], /** @scrutinizer ignore-type */ $wastebasket, MESSAGE_MOVE);
Loading history...
2974
2975
		mapi_savechanges($recurringItem);
0 ignored issues
show
Bug introduced by
$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

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

2998
			$newmessageprops = mapi_getprops(/** @scrutinizer ignore-type */ $message);
Loading history...
2999
3000
			$basedateUtc = $basedate;
3001
			if ($recurObject instanceof BaseRecurrence && isset($recurObject->tz)) {
3002
				$basedateUtc = $recurObject->toGMT($recurObject->tz, $basedate);
3003
			}
3004
3005
			// Ensure that the correct basedate is set in the new message
3006
			$newmessageprops[$this->proptags['basedate']] = $basedateUtc;
3007
3008
			// Set isRecurring to false, because this is an exception
3009
			$newmessageprops[$this->proptags['recurring']] = false;
3010
3011
			// PidLidIsRecurring indicates a message associated with a recurring series object.
3012
			// It's true both for the series and an exception.
3013
			$newmessageprops[$this->proptags['meetingrecurring']] = true;
3014
3015
			// Recurrence data is not necessary for an exception
3016
			unset($newmessageprops[$this->proptags['recurrence_data']]);
3017
3018
			// set LID_IS_EXCEPTION to true
3019
			$newmessageprops[$this->proptags['is_exception']] = true;
3020
3021
			// Set to high importance
3022
			if ($cancel) {
3023
				$newmessageprops[PR_IMPORTANCE] = IMPORTANCE_HIGH;
3024
			}
3025
3026
			// Set startdate and enddate of exception
3027
			if ($cancel && $recurObject) {
3028
				$newmessageprops[$this->proptags['startdate']] = $recurObject->getOccurrenceStart($basedate);
0 ignored issues
show
Bug introduced by
The method getOccurrenceStart() does not exist on BaseRecurrence. It seems like you code against a sub-type of BaseRecurrence such as Recurrence. ( Ignorable by Annotation )

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

3028
				/** @scrutinizer ignore-call */ 
3029
    $newmessageprops[$this->proptags['startdate']] = $recurObject->getOccurrenceStart($basedate);
Loading history...
3029
				$newmessageprops[$this->proptags['duedate']] = $recurObject->getOccurrenceEnd($basedate);
0 ignored issues
show
Bug introduced by
The method getOccurrenceEnd() does not exist on BaseRecurrence. It seems like you code against a sub-type of BaseRecurrence such as Recurrence. ( Ignorable by Annotation )

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

3029
				/** @scrutinizer ignore-call */ 
3030
    $newmessageprops[$this->proptags['duedate']] = $recurObject->getOccurrenceEnd($basedate);
Loading history...
3030
			}
3031
3032
			// Set basedate in guid (0x3)
3033
			$newmessageprops[$this->proptags['goid']] = $this->setBasedateInGlobalID($messageprops[$this->proptags['goid2']], $basedateUtc, $recurObject instanceof BaseRecurrence ? $recurObject : null);
3034
			$newmessageprops[$this->proptags['goid2']] = $messageprops[$this->proptags['goid2']];
3035
			$newmessageprops[PR_OWNER_APPT_ID] = $messageprops[PR_OWNER_APPT_ID];
3036
3037
			// Get deleted recipiets from exception msg
3038
			$restriction = [
3039
				RES_AND,
3040
				[
3041
					[
3042
						RES_BITMASK,
3043
						[
3044
							ULTYPE => BMR_NEZ,
3045
							ULPROPTAG => PR_RECIPIENT_FLAGS,
3046
							ULMASK => recipExceptionalDeleted,
3047
						],
3048
					],
3049
					[
3050
						RES_BITMASK,
3051
						[
3052
							ULTYPE => BMR_EQZ,
3053
							ULPROPTAG => PR_RECIPIENT_FLAGS,
3054
							ULMASK => recipOrganizer,
3055
						],
3056
					],
3057
				],
3058
			];
3059
3060
			// In direct-booking mode, we don't need to send cancellations to resources
3061
			if ($this->enableDirectBooking) {
3062
				$restriction[1][] = [
3063
					RES_PROPERTY,
3064
					[
3065
						RELOP => RELOP_NE,	// Does not equal recipient type: MAPI_BCC (Resource)
3066
						ULPROPTAG => PR_RECIPIENT_TYPE,
3067
						VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
3068
					],
3069
				];
3070
			}
3071
3072
			$recipienttable = mapi_message_getrecipienttable($message);
0 ignored issues
show
Bug introduced by
$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

3072
			$recipienttable = mapi_message_getrecipienttable(/** @scrutinizer ignore-type */ $message);
Loading history...
3073
			$recipients = mapi_table_queryallrows($recipienttable, $this->recipprops, $restriction);
3074
3075
			if (!$deletedRecips) {
3076
				$deletedRecips = array_merge([], $recipients);
3077
			}
3078
			else {
3079
				$deletedRecips = array_merge($deletedRecips, $recipients);
3080
			}
3081
		}
3082
3083
		// Remove the PR_ICON_INDEX as it is not needed in the sent message.
3084
		$newmessageprops[PR_ICON_INDEX] = null;
3085
		$newmessageprops[PR_RESPONSE_REQUESTED] = true;
3086
3087
		// PR_START_DATE and PR_END_DATE will be used by outlook to show the position in the calendar
3088
		$newmessageprops[PR_START_DATE] = $newmessageprops[$this->proptags['startdate']];
3089
		$newmessageprops[PR_END_DATE] = $newmessageprops[$this->proptags['duedate']];
3090
3091
		// Set updatecounter/AppointmentSequenceNumber
3092
		// get the value of latest updatecounter for the whole series and use it
3093
		$newmessageprops[$this->proptags['updatecounter']] = $messageprops[$this->proptags['last_updatecounter']];
3094
3095
		$meetingTimeInfo = $this->getMeetingTimeInfo();
3096
3097
		if ($meetingTimeInfo) {
3098
			// Needs to unset PR_HTML and PR_RTF_COMPRESSED props
3099
			// because while canceling meeting requests with edit text
3100
			// will override the PR_BODY because body value is not consistent with
3101
			// PR_HTML and PR_RTF_COMPRESSED value so in this case PR_RTF_COMPRESSED will
3102
			// get priority which override the PR_BODY value.
3103
			unset($newmessageprops[PR_HTML], $newmessageprops[PR_RTF_COMPRESSED]);
3104
3105
			$newmessageprops[PR_BODY] = $meetingTimeInfo;
3106
		}
3107
3108
		// Send all recurrence info in mail, if this is a recurrence meeting.
3109
		if (isset($newmessageprops[$this->proptags['recurring']]) && $newmessageprops[$this->proptags['recurring']]) {
3110
			if (!empty($messageprops[$this->proptags['recurring_pattern']])) {
3111
				$newmessageprops[$this->proptags['recurring_pattern']] = $messageprops[$this->proptags['recurring_pattern']];
3112
			}
3113
			$newmessageprops[$this->proptags['recurrence_data']] = $messageprops[$this->proptags['recurrence_data']];
3114
			$newmessageprops[$this->proptags['timezone_data']] = $messageprops[$this->proptags['timezone_data']];
3115
			$newmessageprops[$this->proptags['timezone']] = $messageprops[$this->proptags['timezone']];
3116
3117
			if ($recurObject) {
3118
				$this->generateRecurDates($recurObject, $messageprops, $newmessageprops);
3119
			}
3120
		}
3121
3122
		if (isset($newmessageprops[$this->proptags['counter_proposal']])) {
3123
			unset($newmessageprops[$this->proptags['counter_proposal']]);
3124
		}
3125
3126
		// Prefix the subject if needed
3127
		if ($prefix && isset($newmessageprops[PR_SUBJECT])) {
3128
			$newmessageprops[PR_SUBJECT] = $prefix . $newmessageprops[PR_SUBJECT];
3129
		}
3130
3131
		if (isset($newmessageprops[$this->proptags['categories']]) &&
3132
			!empty($newmessageprops[$this->proptags['categories']])) {
3133
			unset($newmessageprops[$this->proptags['categories']]);
3134
		}
3135
		mapi_setprops($new, $newmessageprops);
0 ignored issues
show
Bug introduced by
$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

3135
		mapi_setprops(/** @scrutinizer ignore-type */ $new, $newmessageprops);
Loading history...
3136
3137
		// Copy attachments
3138
		$this->replaceAttachments($message, $new, $copyExceptions);
3139
3140
		// Retrieve only those recipient who should receive this meeting request.
3141
		$stripResourcesRestriction = [
3142
			RES_AND,
3143
			[
3144
				[
3145
					RES_BITMASK,
3146
					[
3147
						ULTYPE => BMR_EQZ,
3148
						ULPROPTAG => PR_RECIPIENT_FLAGS,
3149
						ULMASK => recipExceptionalDeleted,
3150
					],
3151
				],
3152
				[
3153
					RES_BITMASK,
3154
					[
3155
						ULTYPE => BMR_EQZ,
3156
						ULPROPTAG => PR_RECIPIENT_FLAGS,
3157
						ULMASK => recipOrganizer,
3158
					],
3159
				],
3160
			],
3161
		];
3162
3163
		// In direct-booking mode, resources do not receive a meeting request
3164
		if ($this->enableDirectBooking) {
3165
			$stripResourcesRestriction[1][] = [
3166
				RES_PROPERTY,
3167
				[
3168
					RELOP => RELOP_NE,	// Does not equal recipient type: MAPI_BCC (Resource)
3169
					ULPROPTAG => PR_RECIPIENT_TYPE,
3170
					VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
3171
				],
3172
			];
3173
		}
3174
3175
		// If no recipients were explicitly provided, we will send the update to all
3176
		// recipients from the meeting.
3177
		if ($modifiedRecips === false) {
3178
			$recipienttable = mapi_message_getrecipienttable($message);
3179
			$modifiedRecips = mapi_table_queryallrows($recipienttable, $this->recipprops, $stripResourcesRestriction);
3180
3181
			if ($basedate && empty($modifiedRecips)) {
3182
				// Retrieve full list
3183
				$recipienttable = mapi_message_getrecipienttable($this->message);
3184
				$modifiedRecips = mapi_table_queryallrows($recipienttable, $this->recipprops);
3185
3186
				// Save recipients in exceptions
3187
				mapi_message_modifyrecipients($message, MODRECIP_ADD, $modifiedRecips);
0 ignored issues
show
Bug introduced by
$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

3187
				mapi_message_modifyrecipients(/** @scrutinizer ignore-type */ $message, MODRECIP_ADD, $modifiedRecips);
Loading history...
3188
3189
				// Now retrieve only those recipient who should receive this meeting request.
3190
				$modifiedRecips = mapi_table_queryallrows($recipienttable, $this->recipprops, $stripResourcesRestriction);
3191
			}
3192
		}
3193
3194
		// @TODO: handle nonAcceptingResources
3195
		/*
3196
		 * Add resource recipients that did not automatically accept the meeting request.
3197
		 * (note: meaning that they did not decline the meeting request)
3198
		 */ /*
3199
		for($i=0;$i<count($this->nonAcceptingResources);$i++){
3200
			$recipients[] = $this->nonAcceptingResources[$i];
3201
		}*/
3202
3203
		if (!empty($modifiedRecips)) {
3204
			// Strip out the sender/'owner' recipient
3205
			mapi_message_modifyrecipients($new, MODRECIP_ADD, $modifiedRecips);
3206
3207
			// Set some properties that are different in the sent request than
3208
			// in the item in our calendar
3209
3210
			// we should store busystatus value to intendedbusystatus property, because busystatus for outgoing meeting request
3211
			// should always be fbTentative
3212
			$newmessageprops[$this->proptags['intendedbusystatus']] = $newmessageprops[$this->proptags['busystatus']] ?? $messageprops[$this->proptags['busystatus']];
3213
			$newmessageprops[$this->proptags['busystatus']] = fbTentative; // The default status when not accepted
3214
			$newmessageprops[$this->proptags['responsestatus']] = olResponseNotResponded; // The recipient has not responded yet
3215
			$newmessageprops[$this->proptags['attendee_critical_change']] = time();
3216
			$newmessageprops[$this->proptags['owner_critical_change']] = time();
3217
			$newmessageprops[$this->proptags['meetingtype']] = mtgRequest;
3218
3219
			if ($cancel) {
3220
				$newmessageprops[PR_MESSAGE_CLASS] = 'IPM.Schedule.Meeting.Canceled';
3221
				$newmessageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // It's a cancel request
3222
				$newmessageprops[$this->proptags['busystatus']] = fbFree; // set the busy status as free
3223
			}
3224
			else {
3225
				$newmessageprops[PR_MESSAGE_CLASS] = 'IPM.Schedule.Meeting.Request';
3226
				$newmessageprops[$this->proptags['meetingstatus']] = olMeetingReceived; // The recipient is receiving the request
3227
			}
3228
3229
			mapi_setprops($new, $newmessageprops);
3230
			mapi_savechanges($new);
0 ignored issues
show
Bug introduced by
$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

3230
			mapi_savechanges(/** @scrutinizer ignore-type */ $new);
Loading history...
3231
3232
			// Submit message to non-resource recipients
3233
			mapi_message_submitmessage($new);
0 ignored issues
show
Bug introduced by
$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

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

3359
		$outgoing = mapi_folder_createmessage(/** @scrutinizer ignore-type */ $outbox);
Loading history...
3360
3361
		// check if $store is set and it is not equal to $defaultStore (means its the delegation case)
3362
		if ($store !== false) {
3363
			$storeProps = mapi_getprops($store, [PR_ENTRYID]);
3364
			$userStoreProps = mapi_getprops($userStore, [PR_ENTRYID]);
0 ignored issues
show
Bug introduced by
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

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

3460
			$calendarItemProps = mapi_getprops(/** @scrutinizer ignore-type */ $calendarItem, [
Loading history...
3461
				$this->proptags['owner_critical_change'],
3462
				$this->proptags['updatecounter'],
3463
			]);
3464
3465
			$updateCounter = (isset($calendarItemProps[$this->proptags['updatecounter']]) && $props[$this->proptags['updatecounter']] < $calendarItemProps[$this->proptags['updatecounter']]);
3466
3467
			$criticalChange = (isset($calendarItemProps[$this->proptags['owner_critical_change']]) && $props[$this->proptags['owner_critical_change']] < $calendarItemProps[$this->proptags['owner_critical_change']]);
3468
3469
			if ($updateCounter || $criticalChange) {
3470
				// meeting request is out of date, set properties to indicate this
3471
				mapi_setprops($this->message, [$this->proptags['meetingtype'] => mtgOutOfDate, PR_ICON_INDEX => 1033]);
3472
				mapi_savechanges($this->message);
3473
3474
				$result = true;
3475
			}
3476
		}
3477
3478
		return $result;
3479
	}
3480
3481
	/**
3482
	 * Function which checks that if we have received a meeting response for an updated meeting in organizer's calendar.
3483
	 *
3484
	 * @param mixed $basedate basedate of the exception if we want to compare with exception
3485
	 *
3486
	 * @return bool true if meeting request is updated later
3487
	 */
3488
	public function isMeetingUpdated($basedate = false) {
3489
		$result = false;
3490
3491
		$props = mapi_getprops($this->message, [PR_MESSAGE_CLASS, $this->proptags['updatecounter']]);
3492
3493
		if (!$this->isMeetingRequestResponse($props[PR_MESSAGE_CLASS])) {
3494
			return $result;
3495
		}
3496
3497
		$calendarItem = $this->getCorrespondentCalendarItem(true);
3498
3499
		if ($calendarItem !== false) {
3500
			// basedate is provided so open exception
3501
			if ($basedate !== false) {
3502
				$exception = $this->getExceptionItem($calendarItem, $basedate);
3503
3504
				if ($exception !== false) {
0 ignored issues
show
introduced by
The condition $exception !== false is always true.
Loading history...
3505
					// we are able to find the exception compare with it
3506
					$calendarItem = $exception;
3507
				}
3508
				// we are not able to find exception, could mean that a significant change has occurred on series
3509
				// and it deleted all exceptions, so compare with series
3510
				// $calendarItem already contains reference to series
3511
			}
3512
3513
			if ($calendarItem !== false) {
0 ignored issues
show
introduced by
The condition $calendarItem !== false is always true.
Loading history...
3514
				$calendarItemProps = mapi_getprops($calendarItem, [$this->proptags['updatecounter']]);
0 ignored issues
show
Bug introduced by
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

3514
				$calendarItemProps = mapi_getprops(/** @scrutinizer ignore-type */ $calendarItem, [$this->proptags['updatecounter']]);
Loading history...
3515
3516
				/*
3517
				 * if(message_counter < appointment_counter) meeting object is newer then meeting response (meeting is updated)
3518
				 * if(message_counter >= appointment_counter) meeting is not updated, do normal processing
3519
				 */
3520
				if (isset($calendarItemProps[$this->proptags['updatecounter']], $props[$this->proptags['updatecounter']])) {
3521
					if ($props[$this->proptags['updatecounter']] < $calendarItemProps[$this->proptags['updatecounter']]) {
3522
						$result = true;
3523
					}
3524
				}
3525
			}
3526
		}
3527
3528
		return $result;
3529
	}
3530
3531
	/**
3532
	 * Checks if there has been any significant changes on appointment/meeting item.
3533
	 * Significant changes be:
3534
	 * 1) startdate has been changed
3535
	 * 2) duedate has been changed OR
3536
	 * 3) recurrence pattern has been created, modified or removed.
3537
	 *
3538
	 * @param mixed $oldProps
3539
	 * @param mixed $basedate
3540
	 * @param mixed $isRecurrenceChanged for change in recurrence pattern.
3541
	 *                                   true means Recurrence pattern has been changed,
3542
	 *                                   so clear all attendees response
3543
	 */
3544
	public function checkSignificantChanges($oldProps, $basedate, $isRecurrenceChanged = false) {
3545
		$message = null;
3546
		$attach = null;
3547
3548
		// If basedate is specified then we need to open exception message to clear recipient responses
3549
		if ($basedate) {
3550
			$recurrence = new Recurrence($this->store, $this->message);
3551
			if ($recurrence->isException($basedate)) {
3552
				$attach = $recurrence->getExceptionAttachment($basedate);
3553
				if ($attach) {
3554
					$message = mapi_attach_openobj($attach, MAPI_MODIFY);
3555
				}
3556
			}
3557
		}
3558
		else {
3559
			// use normal message or recurring series message
3560
			$message = $this->message;
3561
		}
3562
3563
		if (!$message) {
3564
			return;
3565
		}
3566
3567
		$newProps = mapi_getprops($message, [$this->proptags['startdate'], $this->proptags['duedate'], $this->proptags['updatecounter']]);
3568
3569
		// Check whether message is updated or not.
3570
		if (isset($newProps[$this->proptags['updatecounter']]) && $newProps[$this->proptags['updatecounter']] == 0) {
3571
			return;
3572
		}
3573
3574
		if (($newProps[$this->proptags['startdate']] != $oldProps[$this->proptags['startdate']]) ||
3575
				($newProps[$this->proptags['duedate']] != $oldProps[$this->proptags['duedate']]) ||
3576
				$isRecurrenceChanged) {
3577
			$this->clearRecipientResponse($message);
0 ignored issues
show
Bug introduced by
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

3577
			$this->clearRecipientResponse(/** @scrutinizer ignore-type */ $message);
Loading history...
3578
3579
			mapi_setprops($message, [$this->proptags['owner_critical_change'] => time()]);
3580
3581
			mapi_savechanges($message);
3582
			if ($attach) { // Also save attachment Object.
3583
				mapi_savechanges($attach);
3584
			}
3585
		}
3586
	}
3587
3588
	/**
3589
	 * Clear responses of all attendees who have replied in past.
3590
	 *
3591
	 * @param resource $message on which responses should be cleared
3592
	 */
3593
	public function clearRecipientResponse($message): void {
3594
		$recipTable = mapi_message_getrecipienttable($message);
0 ignored issues
show
Bug introduced by
$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

3594
		$recipTable = mapi_message_getrecipienttable(/** @scrutinizer ignore-type */ $message);
Loading history...
3595
		$recipsRows = mapi_table_queryallrows($recipTable, $this->recipprops);
3596
		for ($i = 0, $recipsCnt = mapi_table_getrowcount($recipTable); $i < $recipsCnt; ++$i) {
3597
			// Clear track status for everyone in the recipients table
3598
			$recipsRows[$i][PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
3599
		}
3600
		mapi_message_modifyrecipients($message, MODRECIP_MODIFY, $recipsRows);
0 ignored issues
show
Bug introduced by
$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

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

3672
				return mapi_msgstore_openentry(/** @scrutinizer ignore-type */ $store, $entryids[0]);
Loading history...
3673
			}
3674
		}
3675
3676
		// no items found in calendar
3677
		return false;
3678
	}
3679
3680
	/**
3681
	 * Function returns exception item based on the basedate passed.
3682
	 *
3683
	 * @param mixed $recurringMessage Resource of Recurring meeting from calendar
3684
	 * @param mixed $basedate         basedate of exception that needs to be returned
3685
	 * @param mixed $store            store that contains the recurring calendar item
3686
	 *
3687
	 * @return entryid or MAPIMessage resource of exception item
0 ignored issues
show
Bug introduced by
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...
3688
	 */
3689
	public function getExceptionItem($recurringMessage, $basedate, $store = false) {
3690
		$occurItem = false;
3691
3692
		$props = mapi_getprops($this->message, [PR_RCVD_REPRESENTING_ENTRYID, $this->proptags['recurring']]);
3693
3694
		// check if the passed item is recurring series
3695
		if (isset($props[$this->proptags['recurring']]) && $props[$this->proptags['recurring']] !== false) {
3696
			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...
3697
		}
3698
3699
		if ($store === false) {
3700
			$store = $this->store;
3701
			// If Delegate is processing Meeting Request/Response for Delegator then retrieve Delegator's store and calendar.
3702
			if (isset($props[PR_RCVD_REPRESENTING_ENTRYID])) {
3703
				$delegatorStore = $this->getDelegatorStore($props[PR_RCVD_REPRESENTING_ENTRYID]);
3704
				if (!empty($delegatorStore['store'])) {
3705
					$store = $delegatorStore['store'];
3706
				}
3707
			}
3708
		}
3709
3710
		$recurr = new Recurrence($store, $recurringMessage);
3711
		$attach = $recurr->getExceptionAttachment($basedate);
3712
		if ($attach) {
3713
			$occurItem = mapi_attach_openobj($attach);
3714
		}
3715
3716
		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...
3717
	}
3718
3719
	/**
3720
	 * Function which checks whether received meeting request is either conflicting with other appointments or not.
3721
	 *
3722
	 * @param false|resource $message
3723
	 * @param false|resource $userStore
3724
	 * @param mixed          $calFolder calendar folder for conflict checking
3725
	 *
3726
	 * @return bool|int
3727
	 *
3728
	 * @psalm-return bool|int<1, max>
3729
	 */
3730
	public function isMeetingConflicting($message = false, $userStore = false, $calFolder = false) {
3731
		$returnValue = false;
3732
		$noOfInstances = 0;
3733
3734
		if ($message === false) {
3735
			$message = $this->message;
3736
		}
3737
3738
		$messageProps = mapi_getprops(
3739
			$message,
0 ignored issues
show
Bug introduced by
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

3739
			/** @scrutinizer ignore-type */ $message,
Loading history...
3740
			[
3741
				PR_MESSAGE_CLASS,
3742
				$this->proptags['goid'],
3743
				$this->proptags['goid2'],
3744
				$this->proptags['startdate'],
3745
				$this->proptags['duedate'],
3746
				$this->proptags['recurring'],
3747
				$this->proptags['clipstart'],
3748
				$this->proptags['clipend'],
3749
				PR_RCVD_REPRESENTING_ENTRYID,
3750
				$this->proptags['basedate'],
3751
				PR_RCVD_REPRESENTING_NAME,
3752
			]
3753
		);
3754
3755
		if ($userStore === false) {
3756
			$userStore = $this->store;
3757
3758
			// check if delegate is processing the response
3759
			if (isset($messageProps[PR_RCVD_REPRESENTING_ENTRYID])) {
3760
				$delegatorStore = $this->getDelegatorStore($messageProps[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
3761
3762
				if (!empty($delegatorStore['store'])) {
3763
					$userStore = $delegatorStore['store'];
3764
				}
3765
				if (!empty($delegatorStore[PR_IPM_APPOINTMENT_ENTRYID])) {
3766
					$calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
3767
				}
3768
			}
3769
		}
3770
3771
		if ($calFolder === false) {
3772
			$calFolder = $this->openDefaultCalendar($userStore);
3773
		}
3774
3775
		if ($calFolder) {
3776
			// Meeting request is recurring, so get all occurrence and check for each occurrence whether it conflicts with other appointments in Calendar.
3777
			if (isset($messageProps[$this->proptags['recurring']]) && $messageProps[$this->proptags['recurring']] === true) {
3778
				// Apply recurrence class and retrieve all occurrences(max: 30 occurrence because recurrence can also be set as 'no end date')
3779
				$recurr = new Recurrence($userStore, $message);
3780
				$items = $recurr->getItems($messageProps[$this->proptags['clipstart']], $messageProps[$this->proptags['clipend']] * (24 * 24 * 60), 30);
3781
3782
				foreach ($items as $item) {
3783
					// Get all items in the timeframe that we want to book, and get the goid and busystatus for each item
3784
					$calendarItems = $recurr->getCalendarItems($userStore, $calFolder, $item[$this->proptags['startdate']], $item[$this->proptags['duedate']], [$this->proptags['goid'], $this->proptags['busystatus']]);
3785
3786
					foreach ($calendarItems as $calendarItem) {
3787
						if ($calendarItem[$this->proptags['busystatus']] !== fbFree) {
3788
							/*
3789
							 * Only meeting requests have globalID, normal appointments do not have globalID
3790
							 * so if any normal appointment if found then it is assumed to be conflict.
3791
							 */
3792
							if (isset($calendarItem[$this->proptags['goid']])) {
3793
								if ($calendarItem[$this->proptags['goid']] !== $messageProps[$this->proptags['goid']]) {
3794
									++$noOfInstances;
3795
									break;
3796
								}
3797
							}
3798
							else {
3799
								++$noOfInstances;
3800
								break;
3801
							}
3802
						}
3803
					}
3804
				}
3805
3806
				if ($noOfInstances > 0) {
3807
					$returnValue = $noOfInstances;
3808
				}
3809
			}
3810
			else {
3811
				// Get all items in the timeframe that we want to book, and get the goid and busystatus for each item
3812
				$items = getCalendarItems($userStore, $calFolder, $messageProps[$this->proptags['startdate']], $messageProps[$this->proptags['duedate']], [$this->proptags['goid'], $this->proptags['busystatus']]);
3813
3814
				if (isset($messageProps[$this->proptags['basedate']]) && !empty($messageProps[$this->proptags['basedate']])) {
3815
					$basedate = $messageProps[$this->proptags['basedate']];
3816
					// Get the goid2 from recurring MR which further used to
3817
					// check the resource conflicts item.
3818
					$recurrItemProps = mapi_getprops($this->message, [$this->proptags['goid2']]);
3819
					$recurrenceHelper = new Recurrence($this->openDefaultStore(), $this->message);
0 ignored issues
show
Bug introduced by
$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

3819
					$recurrenceHelper = new Recurrence(/** @scrutinizer ignore-type */ $this->openDefaultStore(), $this->message);
Loading history...
3820
					$messageProps[$this->proptags['goid']] = $this->setBasedateInGlobalID($recurrItemProps[$this->proptags['goid2']], $basedate, $recurrenceHelper);
3821
					$messageProps[$this->proptags['goid2']] = $recurrItemProps[$this->proptags['goid2']];
3822
				}
3823
3824
				foreach ($items as $item) {
3825
					if ($item[$this->proptags['busystatus']] !== fbFree) {
3826
						if (isset($item[$this->proptags['goid']])) {
3827
							if (($item[$this->proptags['goid']] !== $messageProps[$this->proptags['goid']]) &&
3828
								($item[$this->proptags['goid']] !== $messageProps[$this->proptags['goid2']])) {
3829
								$returnValue = true;
3830
								break;
3831
							}
3832
						}
3833
						else {
3834
							$returnValue = true;
3835
							break;
3836
						}
3837
					}
3838
				}
3839
			}
3840
		}
3841
3842
		return $returnValue;
3843
	}
3844
3845
	/**
3846
	 * Function which adds organizer to recipient list which is passed.
3847
	 * This function also checks if it has organizer.
3848
	 *
3849
	 * @param array $messageProps message properties
3850
	 * @param array $recipients   recipients list of message
3851
	 */
3852
	public function addDelegator($messageProps, &$recipients): void {
3853
		$hasDelegator = false;
3854
		// Check if meeting already has an organizer.
3855
		foreach ($recipients as $key => $recipient) {
3856
			if (isset($messageProps[PR_RCVD_REPRESENTING_EMAIL_ADDRESS]) && $recipient[PR_EMAIL_ADDRESS] == $messageProps[PR_RCVD_REPRESENTING_EMAIL_ADDRESS]) {
3857
				$hasDelegator = true;
3858
			}
3859
		}
3860
3861
		if (!$hasDelegator) {
3862
			// Create delegator.
3863
			$delegator = [];
3864
			$delegator[PR_ENTRYID] = $messageProps[PR_RCVD_REPRESENTING_ENTRYID];
3865
			$delegator[PR_DISPLAY_NAME] = $messageProps[PR_RCVD_REPRESENTING_NAME];
3866
			$delegator[PR_EMAIL_ADDRESS] = $messageProps[PR_RCVD_REPRESENTING_EMAIL_ADDRESS];
3867
			$delegator[PR_RECIPIENT_TYPE] = MAPI_TO;
3868
			$delegator[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_RCVD_REPRESENTING_NAME];
3869
			$delegator[PR_ADDRTYPE] = empty($messageProps[PR_RCVD_REPRESENTING_ADDRTYPE]) ? 'SMTP' : $messageProps[PR_RCVD_REPRESENTING_ADDRTYPE];
3870
			$delegator[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
3871
			$delegator[PR_RECIPIENT_FLAGS] = recipSendable;
3872
			$delegator[PR_SEARCH_KEY] = $messageProps[PR_RCVD_REPRESENTING_SEARCH_KEY];
3873
3874
			// Add organizer to recipients list.
3875
			array_unshift($recipients, $delegator);
3876
		}
3877
	}
3878
3879
	/**
3880
	 * Function will return delegator's store and calendar folder for processing meetings.
3881
	 *
3882
	 * @param string $receivedRepresentingEntryId entryid of the delegator user
3883
	 * @param array  $foldersToOpen               contains list of folder types that should be returned in result
3884
	 *
3885
	 * @return resource[] contains store of the delegator and resource of folders if $foldersToOpen is not empty
3886
	 *
3887
	 * @psalm-return array<resource>
3888
	 */
3889
	public function getDelegatorStore($receivedRepresentingEntryId, $foldersToOpen = []): array {
3890
		$returnData = [];
3891
3892
		$delegatorStore = $this->openCustomUserStore($receivedRepresentingEntryId);
3893
		$returnData['store'] = $delegatorStore;
3894
3895
		if (!empty($foldersToOpen)) {
3896
			for ($index = 0, $len = count($foldersToOpen); $index < $len; ++$index) {
3897
				$folderType = $foldersToOpen[$index];
3898
3899
				// first try with default folders
3900
				$folder = $this->openDefaultFolder($folderType, $delegatorStore);
3901
3902
				// if folder not found then try with base folders
3903
				if ($folder === false) {
3904
					$folder = $this->openBaseFolder($folderType, $delegatorStore);
3905
				}
3906
3907
				if ($folder === false) {
3908
					// we are still not able to get the folder so give up
3909
					continue;
3910
				}
3911
3912
				$returnData[$folderType] = $folder;
3913
			}
3914
		}
3915
3916
		return $returnData;
3917
	}
3918
3919
	/**
3920
	 * Function returns extra info about meeting timing along with message body
3921
	 * which will be included in body while sending meeting request/response.
3922
	 *
3923
	 * @return false|string $meetingTimeInfo info about meeting timing along with message body
3924
	 */
3925
	public function getMeetingTimeInfo() {
3926
		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...
3927
	}
3928
3929
	/**
3930
	 * Function sets extra info about meeting timing along with message body
3931
	 * which will be included in body while sending meeting request/response.
3932
	 *
3933
	 * @param string $meetingTimeInfo info about meeting timing along with message body
3934
	 */
3935
	public function setMeetingTimeInfo($meetingTimeInfo): void {
3936
		$this->meetingTimeInfo = $meetingTimeInfo;
3937
	}
3938
3939
	/**
3940
	 * Helper function which is use to get local categories of all occurrence.
3941
	 *
3942
	 * @param mixed $calendarItem meeting request item
3943
	 * @param mixed $store        store containing calendar folder
3944
	 * @param mixed $calFolder    calendar folder
3945
	 *
3946
	 * @return array $localCategories which contain array of basedate along with categories
3947
	 */
3948
	public function getLocalCategories($calendarItem, $store, $calFolder) {
3949
		$calendarItemProps = mapi_getprops($calendarItem);
3950
		$recurrence = new Recurrence($store, $calendarItem);
3951
3952
		// Retrieve all occurrences(max: 30 occurrence because recurrence can also be set as 'no end date')
3953
		$items = $recurrence->getItems($calendarItemProps[$this->proptags['clipstart']], $calendarItemProps[$this->proptags['clipend']] * (24 * 24 * 60), 30);
3954
		$localCategories = [];
3955
3956
		foreach ($items as $item) {
3957
			$recurrenceItems = $recurrence->getCalendarItems($store, $calFolder, $item[$this->proptags['startdate']], $item[$this->proptags['duedate']], [$this->proptags['goid'], $this->proptags['busystatus'], $this->proptags['categories']]);
3958
			foreach ($recurrenceItems as $recurrenceItem) {
3959
				// Check if occurrence is exception then get the local categories of that occurrence.
3960
				if (isset($recurrenceItem[$this->proptags['goid']]) && $recurrenceItem[$this->proptags['goid']] == $calendarItemProps[$this->proptags['goid']]) {
3961
					$exceptionAttach = $recurrence->getExceptionAttachment($recurrenceItem['basedate']);
3962
3963
					if ($exceptionAttach) {
3964
						$exception = mapi_attach_openobj($exceptionAttach, 0);
3965
						$exceptionProps = mapi_getprops($exception, [$this->proptags['categories']]);
3966
						if (isset($exceptionProps[$this->proptags['categories']])) {
3967
							$localCategories[$recurrenceItem['basedate']] = $exceptionProps[$this->proptags['categories']];
3968
						}
3969
					}
3970
				}
3971
			}
3972
		}
3973
3974
		return $localCategories;
3975
	}
3976
3977
	/**
3978
	 * Helper function which is use to apply local categories on respective occurrences.
3979
	 *
3980
	 * @param mixed $calendarItem    meeting request item
3981
	 * @param mixed $store           store containing calendar folder
3982
	 * @param array $localCategories array contains basedate and array of categories
3983
	 */
3984
	public function applyLocalCategories($calendarItem, $store, $localCategories): void {
3985
		$calendarItemProps = mapi_getprops($calendarItem, [PR_PARENT_ENTRYID, PR_ENTRYID]);
3986
		$message = mapi_msgstore_openentry($store, $calendarItemProps[PR_ENTRYID]);
3987
		$recurrence = new Recurrence($store, $message);
3988
3989
		// Check for all occurrence if it is exception then modify the exception by setting up categories,
3990
		// Otherwise create new exception with categories.
3991
		foreach ($localCategories as $key => $value) {
3992
			if ($recurrence->isException($key)) {
3993
				$recurrence->modifyException([$this->proptags['categories'] => $value], $key);
3994
			}
3995
			else {
3996
				$recurrence->createException([$this->proptags['categories'] => $value], $key, false);
3997
			}
3998
			mapi_savechanges($message);
3999
		}
4000
	}
4001
}
4002