Meetingrequest::isMeetingConflicting()   F
last analyzed

Complexity

Conditions 23
Paths 312

Size

Total Lines 113
Code Lines 63

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 23
eloc 63
c 3
b 1
f 0
nc 312
nop 3
dl 0
loc 113
rs 1.9333

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 false|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(false|string $messageClass = false): bool {
223
		if ($messageClass === false) {
224
			$messageClass = mapi_getprops($this->message, [PR_MESSAGE_CLASS])[PR_MESSAGE_CLASS] ?? false;
225
		}
226
227
		return $messageClass !== false && stripos($messageClass, 'ipm.schedule.meeting.request') === 0;
228
	}
229
230
	/**
231
	 * Returns TRUE if the message pointed to is a returning meeting request response.
232
	 *
233
	 * @param false|string $messageClass message class to use for checking
234
	 *
235
	 * @return bool returns true if this is a meeting request else false
236
	 */
237
	public function isMeetingRequestResponse(false|string $messageClass = false): bool {
238
		if ($messageClass === false) {
239
			$messageClass = mapi_getprops($this->message, [PR_MESSAGE_CLASS])[PR_MESSAGE_CLASS] ?? false;
240
		}
241
242
		return $messageClass !== false && stripos($messageClass, 'ipm.schedule.meeting.resp') === 0;
243
	}
244
245
	/**
246
	 * Returns TRUE if the message pointed to is a cancellation request.
247
	 *
248
	 * @param false|string $messageClass message class to use for checking
249
	 *
250
	 * @return bool returns true if this is a meeting request else false
251
	 */
252
	public function isMeetingCancellation(false|string $messageClass = false): bool {
253
		if ($messageClass === false) {
254
			$messageClass = mapi_getprops($this->message, [PR_MESSAGE_CLASS])[PR_MESSAGE_CLASS] ?? false;
255
		}
256
257
		return $messageClass !== false && stripos($messageClass, 'ipm.schedule.meeting.canceled') === 0;
258
	}
259
260
	/**
261
	 * Function is used to get the last update counter of meeting request.
262
	 *
263
	 * @return false|int false when last_updatecounter not found else return last_updatecounter
264
	 */
265
	public function getLastUpdateCounter(): false|int {
266
		$calendarItemProps = mapi_getprops($this->message, [$this->proptags['last_updatecounter']]);
267
268
		return $calendarItemProps[$this->proptags['last_updatecounter']] ?? false;
269
	}
270
271
	/**
272
	 * Process an incoming meeting request response. This updates the appointment
273
	 * in your calendar to show whether the user has accepted or declined.
274
	 */
275
	public function processMeetingRequestResponse(): void {
276
		if (!$this->isMeetingRequestResponse()) {
277
			return;
278
		}
279
280
		if (!$this->isLocalOrganiser()) {
281
			return;
282
		}
283
284
		// Get information we need from the response message
285
		$messageprops = mapi_getprops($this->message, [
286
			$this->proptags['goid'],
287
			$this->proptags['goid2'],
288
			PR_OWNER_APPT_ID,
289
			PR_SENT_REPRESENTING_EMAIL_ADDRESS,
290
			PR_SENT_REPRESENTING_NAME,
291
			PR_SENT_REPRESENTING_ADDRTYPE,
292
			PR_SENT_REPRESENTING_ENTRYID,
293
			PR_SENT_REPRESENTING_SEARCH_KEY,
294
			PR_MESSAGE_DELIVERY_TIME,
295
			PR_MESSAGE_CLASS,
296
			PR_PROCESSED,
297
			PR_RCVD_REPRESENTING_ENTRYID,
298
			$this->proptags['proposed_start_whole'],
299
			$this->proptags['proposed_end_whole'],
300
			$this->proptags['proposed_duration'],
301
			$this->proptags['counter_proposal'],
302
			$this->proptags['attendee_critical_change'],
303
		]);
304
305
		$goid2 = $messageprops[$this->proptags['goid2']];
306
307
		if (!isset($goid2) || !isset($messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS])) {
308
			return;
309
		}
310
311
		// Find basedate in GlobalID(0x3), this can be a response for an occurrence
312
		$basedate = $this->getBasedateFromGlobalID($messageprops[$this->proptags['goid']]);
313
314
		$userStore = $this->store;
315
		// check if delegate is processing the response
316
		if (isset($messageprops[PR_RCVD_REPRESENTING_ENTRYID])) {
317
			$delegatorStore = $this->getDelegatorStore($messageprops[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
318
			if (!empty($delegatorStore['store'])) {
319
				$userStore = $delegatorStore['store'];
320
			}
321
		}
322
323
		// check for calendar access
324
		$this->ensureCalendarWriteAccess($userStore);
325
326
		$calendarItem = $this->getCorrespondentCalendarItem(true);
327
328
		// Open the calendar items, and update all the recipients of the calendar item that match
329
		// the email address of the response.
330
		if ($calendarItem !== false) {
331
			$this->processResponse($userStore, $calendarItem, $basedate, $messageprops);
332
		}
333
	}
334
335
	/**
336
	 * Process every incoming MeetingRequest response.This updates the appointment
337
	 * in your calendar to show whether the user has accepted or declined.
338
	 *
339
	 * @param resource  $store        contains the userStore in which the meeting is created
340
	 * @param mixed     $calendarItem resource of the calendar item for which this response has arrived
341
	 * @param false|int $basedate     if present the create an exception
342
	 * @param array     $messageprops contains message properties
343
	 */
344
	public function processResponse(mixed $store, mixed $calendarItem, mixed $basedate, array $messageprops): ?false {
345
		$senderentryid = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
346
		$messageclass = $messageprops[PR_MESSAGE_CLASS];
347
		$deliverytime = $messageprops[PR_MESSAGE_DELIVERY_TIME];
348
		$recurringItem = 0;
349
350
		// Open the calendar item, find the sender in the recipient table and update all the recipients of the calendar item that match
351
		// the email address of the response.
352
		$calendarItemProps = mapi_getprops($calendarItem, [$this->proptags['recurring'], PR_STORE_ENTRYID, PR_PARENT_ENTRYID, PR_ENTRYID, $this->proptags['updatecounter']]);
353
354
		// check if meeting response is already processed
355
		if (isset($messageprops[PR_PROCESSED]) && $messageprops[PR_PROCESSED] === true) {
356
			// meeting is already processed
357
			return null;
358
		}
359
		mapi_setprops($this->message, [PR_PROCESSED => true]);
360
		mapi_savechanges($this->message);
361
362
		// if meeting is updated in organizer's calendar then we don't need to process
363
		// old response
364
		if ($this->isMeetingUpdated($basedate)) {
365
			return null;
366
		}
367
368
		// If basedate is found, then create/modify exception msg and do processing
369
		if ($basedate && !empty($calendarItemProps[$this->proptags['recurring']])) {
370
			$recurr = new Recurrence($store, $calendarItem);
371
372
			// Copy properties from meeting request
373
			$exception_props = mapi_getprops($this->message, [
374
				PR_OWNER_APPT_ID,
375
				$this->proptags['proposed_start_whole'],
376
				$this->proptags['proposed_end_whole'],
377
				$this->proptags['proposed_duration'],
378
				$this->proptags['counter_proposal'],
379
			]);
380
381
			// Create/modify exception
382
			if ($recurr->isException($basedate)) {
383
				$recurr->modifyException($exception_props, $basedate);
384
			}
385
			else {
386
				// When we are creating an exception we need copy recipients from main recurring item
387
				$recips = $this->getMessageRecipients($calendarItem);
388
389
				// Retrieve actual start/due dates from calendar item.
390
				$exception_props[$this->proptags['startdate']] = $recurr->getOccurrenceStart($basedate);
391
				$exception_props[$this->proptags['duedate']] = $recurr->getOccurrenceEnd($basedate);
392
393
				$recurr->createException($exception_props, $basedate, false, $recips);
394
			}
395
396
			mapi_savechanges($calendarItem);
397
398
			$attach = $recurr->getExceptionAttachment($basedate);
399
			if ($attach) {
400
				$recurringItem = $calendarItem;
401
				$calendarItem = mapi_attach_openobj($attach, MAPI_MODIFY);
402
			}
403
			else {
404
				return false;
405
			}
406
		}
407
408
		// Get the recipients of the calendar item
409
		$recipients = $this->getMessageRecipients($calendarItem);
410
411
		// FIXME we should look at the updatecounter property and compare it
412
		// to the counter in the recipient to see if this update is actually
413
		// newer than the status in the calendar item
414
		$found = false;
415
416
		$totalrecips = 0;
417
		$acceptedrecips = 0;
418
		foreach ($recipients as $recipient) {
419
			++$totalrecips;
420
			// external recipients might not have entryid
421
			if (!isset($recipient[PR_ENTRYID]) &&
422
				$recipient[PR_EMAIL_ADDRESS] == $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS]) {
423
				$recipient[PR_ENTRYID] = $senderentryid;
424
			}
425
			if (isset($recipient[PR_ENTRYID]) && $this->compareABEntryIDs($recipient[PR_ENTRYID], $senderentryid)) {
426
				$found = true;
427
428
				/*
429
				 * If value of attendee_critical_change on meeting response mail is less than PR_RECIPIENT_TRACKSTATUS_TIME
430
				 * on the corresponding recipientRow of meeting then we ignore this response mail.
431
				 */
432
				if (isset($recipient[PR_RECIPIENT_TRACKSTATUS_TIME]) && ($messageprops[$this->proptags['attendee_critical_change']] < $recipient[PR_RECIPIENT_TRACKSTATUS_TIME])) {
433
					continue;
434
				}
435
436
				// The email address matches, update the row
437
				$recipient[PR_RECIPIENT_TRACKSTATUS] = $this->getTrackStatus($messageclass);
438
				if (isset($messageprops[$this->proptags['attendee_critical_change']])) {
439
					$recipient[PR_RECIPIENT_TRACKSTATUS_TIME] = $messageprops[$this->proptags['attendee_critical_change']];
440
				}
441
442
				// If this is a counter proposal, set the proposal properties in the recipient row
443
				if (isset($messageprops[$this->proptags['counter_proposal']]) && $messageprops[$this->proptags['counter_proposal']]) {
444
					$recipient[PR_RECIPIENT_PROPOSEDSTARTTIME] = $messageprops[$this->proptags['proposed_start_whole']];
445
					$recipient[PR_RECIPIENT_PROPOSEDENDTIME] = $messageprops[$this->proptags['proposed_end_whole']];
446
					$recipient[PR_RECIPIENT_PROPOSED] = $messageprops[$this->proptags['counter_proposal']];
447
				}
448
449
				// Update the recipient information
450
				mapi_message_modifyrecipients($calendarItem, MODRECIP_REMOVE, [$recipient]);
451
				mapi_message_modifyrecipients($calendarItem, MODRECIP_ADD, [$recipient]);
452
			}
453
			if (isset($recipient[PR_RECIPIENT_TRACKSTATUS]) && $recipient[PR_RECIPIENT_TRACKSTATUS] == olRecipientTrackStatusAccepted) {
454
				++$acceptedrecips;
455
			}
456
		}
457
458
		// If the recipient was not found in the original calendar item,
459
		// then add the recpient as a new optional recipient
460
		if (!$found) {
461
			$recipient = [];
462
			$recipient[PR_ENTRYID] = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
463
			$recipient[PR_EMAIL_ADDRESS] = $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
464
			$recipient[PR_DISPLAY_NAME] = $messageprops[PR_SENT_REPRESENTING_NAME];
465
			$recipient[PR_ADDRTYPE] = $messageprops[PR_SENT_REPRESENTING_ADDRTYPE];
466
			$recipient[PR_RECIPIENT_TYPE] = MAPI_CC;
467
			$recipient[PR_SEARCH_KEY] = $messageprops[PR_SENT_REPRESENTING_SEARCH_KEY];
468
			$recipient[PR_RECIPIENT_TRACKSTATUS] = $this->getTrackStatus($messageclass);
469
			$recipient[PR_RECIPIENT_TRACKSTATUS_TIME] = $deliverytime;
470
471
			// If this is a counter proposal, set the proposal properties in the recipient row
472
			if (isset($messageprops[$this->proptags['counter_proposal']])) {
473
				$recipient[PR_RECIPIENT_PROPOSEDSTARTTIME] = $messageprops[$this->proptags['proposed_start_whole']];
474
				$recipient[PR_RECIPIENT_PROPOSEDENDTIME] = $messageprops[$this->proptags['proposed_end_whole']];
475
				$recipient[PR_RECIPIENT_PROPOSED] = $messageprops[$this->proptags['counter_proposal']];
476
			}
477
478
			mapi_message_modifyrecipients($calendarItem, MODRECIP_ADD, [$recipient]);
479
			++$totalrecips;
480
			if ($recipient[PR_RECIPIENT_TRACKSTATUS] == olRecipientTrackStatusAccepted) {
481
				++$acceptedrecips;
482
			}
483
		}
484
485
		// TODO: Update counter proposal number property on message
486
		/*
487
		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.
488
		*/
489
		// If this is a counter proposal, set the counter proposal indicator boolean
490
		if (isset($messageprops[$this->proptags['counter_proposal']])) {
491
			$props = [$this->proptags['counter_proposal'] => (bool) $messageprops[$this->proptags['counter_proposal']]];
492
			mapi_setprops($calendarItem, $props);
493
		}
494
495
		mapi_savechanges($calendarItem);
496
		if (isset($attach)) {
497
			mapi_savechanges($attach);
498
			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

498
			mapi_savechanges(/** @scrutinizer ignore-type */ $recurringItem);
Loading history...
499
		}
500
501
		return null;
502
	}
503
504
	/**
505
	 * Process an incoming meeting request cancellation. This updates the
506
	 * appointment in your calendar to show that the meeting has been cancelled.
507
	 */
508
	public function processMeetingCancellation(): void {
509
		if (!$this->isMeetingCancellation()) {
510
			return;
511
		}
512
513
		if ($this->isLocalOrganiser()) {
514
			return;
515
		}
516
517
		if (!$this->isInCalendar()) {
518
			return;
519
		}
520
521
		$listProperties = $this->proptags;
522
		$listProperties['subject'] = PR_SUBJECT;
523
		$listProperties['sent_representing_name'] = PR_SENT_REPRESENTING_NAME;
524
		$listProperties['sent_representing_address_type'] = PR_SENT_REPRESENTING_ADDRTYPE;
525
		$listProperties['sent_representing_email_address'] = PR_SENT_REPRESENTING_EMAIL_ADDRESS;
526
		$listProperties['sent_representing_entryid'] = PR_SENT_REPRESENTING_ENTRYID;
527
		$listProperties['sent_representing_search_key'] = PR_SENT_REPRESENTING_SEARCH_KEY;
528
		$listProperties['rcvd_representing_name'] = PR_RCVD_REPRESENTING_NAME;
529
		$listProperties['rcvd_representing_address_type'] = PR_RCVD_REPRESENTING_ADDRTYPE;
530
		$listProperties['rcvd_representing_email_address'] = PR_RCVD_REPRESENTING_EMAIL_ADDRESS;
531
		$listProperties['rcvd_representing_entryid'] = PR_RCVD_REPRESENTING_ENTRYID;
532
		$listProperties['rcvd_representing_search_key'] = PR_RCVD_REPRESENTING_SEARCH_KEY;
533
		$messageProps = mapi_getprops($this->message, $listProperties);
534
535
		$goid = $messageProps[$this->proptags['goid']];	// GlobalID (0x3)
536
		if (!isset($goid)) {
537
			return;
538
		}
539
540
		$store = $this->store;
541
		// get delegator store, if delegate is processing this cancellation
542
		if (isset($messageProps[PR_RCVD_REPRESENTING_ENTRYID])) {
543
			$delegatorStore = $this->getDelegatorStore($messageProps[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
544
			if (!empty($delegatorStore['store'])) {
545
				$store = $delegatorStore['store'];
546
			}
547
		}
548
549
		// check for calendar access
550
		$this->ensureCalendarWriteAccess($store);
551
552
		$calendarItem = $this->getCorrespondentCalendarItem(true);
553
		$basedate = $this->getBasedateFromGlobalID($goid);
554
555
		if ($calendarItem !== false) {
556
			// if basedate is provided and we could not find the item then it could be that we are processing
557
			// an exception so get the exception and process it
558
			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...
559
				$calendarItemProps = mapi_getprops($calendarItem, [$this->proptags['recurring']]);
560
				if ($calendarItemProps[$this->proptags['recurring']] === true) {
561
					$recurr = new Recurrence($store, $calendarItem);
562
563
					// Set message class
564
					$messageProps[PR_MESSAGE_CLASS] = 'IPM.Appointment';
565
566
					if ($recurr->isException($basedate)) {
567
						$recurr->modifyException($messageProps, $basedate);
568
					}
569
					else {
570
						$recurr->createException($messageProps, $basedate);
571
					}
572
				}
573
			}
574
			else {
575
				// set the properties of the cancellation object
576
				mapi_setprops($calendarItem, $messageProps);
577
			}
578
579
			mapi_savechanges($calendarItem);
580
		}
581
	}
582
583
	/**
584
	 * Returns true if the corresponding calendar items exists in the celendar folder for this
585
	 * meeting request/response/cancellation.
586
	 */
587
	public function isInCalendar(): bool {
588
		// @TODO check for deleted exceptions
589
		return $this->getCorrespondentCalendarItem(false) !== false;
590
	}
591
592
	/**
593
	 * Accepts the meeting request by moving the item to the calendar
594
	 * and sending a confirmation message back to the sender. If $tentative
595
	 * is TRUE, then the item is accepted tentatively. After accepting, you
596
	 * can't use this class instance any more. The message is closed. If you
597
	 * specify TRUE for 'move', then the item is actually moved (from your
598
	 * inbox probably) to the calendar. If you don't, it is copied into
599
	 * your calendar.
600
	 *
601
	 * @param bool      $tentative            true if user as tentative accepted the meeting
602
	 * @param bool      $sendresponse         true if a response has to be sent to organizer
603
	 * @param bool      $move                 true if the meeting request should be moved to the deleted items after processing
604
	 * @param mixed     $newProposedStartTime contains starttime if user has proposed other time
605
	 * @param mixed     $newProposedEndTime   contains endtime if user has proposed other time
606
	 * @param false|int $basedate             start of day of occurrence for which user has accepted the recurrent meeting
607
	 * @param bool      $isImported           true to indicate that MR is imported from .ics or .vcs file else it false.
608
	 *
609
	 * @return bool|string $entryid entryid of item which created/updated in calendar
610
	 */
611
	public function doAccept(bool $tentative, bool $sendresponse, bool $move, mixed $newProposedStartTime = false, mixed $newProposedEndTime = false, mixed $body = false, mixed $userAction = false, mixed $store = false, mixed $basedate = false, bool $isImported = false): bool|string {
612
		if ($this->isLocalOrganiser()) {
613
			return false;
614
		}
615
616
		// Remove any previous calendar items with this goid and appt id
617
		$messageprops = mapi_getprops($this->message, [PR_ENTRYID, PR_PARENT_ENTRYID,
618
			PR_MESSAGE_CLASS, $this->proptags['goid'], $this->proptags['updatecounter'],
619
			PR_PROCESSED, PR_RCVD_REPRESENTING_ENTRYID, PR_SENDER_ENTRYID,
620
			PR_SENT_REPRESENTING_ENTRYID, PR_RECEIVED_BY_ENTRYID]);
621
622
		// do not process meeting requests in sent items folder
623
		$sentItemsEntryid = $this->getDefaultSentmailEntryID();
624
		if (isset($messageprops[PR_PARENT_ENTRYID]) &&
625
			$sentItemsEntryid !== false &&
626
			$sentItemsEntryid == $messageprops[PR_PARENT_ENTRYID]) {
627
			return false;
628
		}
629
630
		$calFolder = $this->openDefaultCalendar();
631
		$store = $this->store;
632
		// If this meeting request is received by a delegate then open delegator's store.
633
		if (isset($messageprops[PR_RCVD_REPRESENTING_ENTRYID], $messageprops[PR_RECEIVED_BY_ENTRYID]) &&
634
			!compareEntryIds($messageprops[PR_RCVD_REPRESENTING_ENTRYID], $messageprops[PR_RECEIVED_BY_ENTRYID])) {
635
			$delegatorStore = $this->getDelegatorStore($messageprops[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
636
			if (!empty($delegatorStore['store'])) {
637
				$store = $delegatorStore['store'];
638
			}
639
			if (!empty($delegatorStore[PR_IPM_APPOINTMENT_ENTRYID])) {
640
				$calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
641
			}
642
		}
643
644
		// check for calendar access
645
		$this->ensureCalendarWriteAccess($store);
646
647
		// if meeting is out dated then don't process it
648
		if ($this->isMeetingRequest($messageprops[PR_MESSAGE_CLASS]) && $this->isMeetingOutOfDate()) {
649
			return false;
650
		}
651
652
		/*
653
		 *	if this function is called automatically with meeting request object then there will be
654
		 *	two possibilitites
655
		 *	1) meeting request is opened first time, in this case make a tentative appointment in
656
		 *		recipient's calendar
657
		 *	2) after this every subsequent request to open meeting request will not do any processing
658
		 */
659
		if ($this->isMeetingRequest($messageprops[PR_MESSAGE_CLASS]) && $userAction === false) {
660
			if (isset($messageprops[PR_PROCESSED]) && $messageprops[PR_PROCESSED] === true) {
661
				// if meeting request is already processed then don't do anything
662
				return false;
663
			}
664
665
			// if correspondent calendar item is already processed then don't do anything
666
			$calendarItem = $this->getCorrespondentCalendarItem();
667
			if ($calendarItem) {
668
				$calendarItemProps = mapi_getprops($calendarItem, [PR_PROCESSED]);
669
				if (isset($calendarItemProps[PR_PROCESSED]) && $calendarItemProps[PR_PROCESSED] === true) {
670
					// mark meeting-request mail as processed as well
671
					mapi_setprops($this->message, [PR_PROCESSED => true]);
672
					mapi_savechanges($this->message);
673
674
					return false;
675
				}
676
			}
677
		}
678
679
		// Retrieve basedate from globalID, if it is not received as argument
680
		if (!$basedate) {
681
			$basedate = $this->getBasedateFromGlobalID($messageprops[$this->proptags['goid']]);
682
		}
683
684
		// set counter proposal properties in calendar item when proposing new time
685
		$proposeNewTimeProps = [];
686
		if ($newProposedStartTime && $newProposedEndTime) {
687
			$proposeNewTimeProps[$this->proptags['proposed_start_whole']] = $newProposedStartTime;
688
			$proposeNewTimeProps[$this->proptags['proposed_end_whole']] = $newProposedEndTime;
689
			$proposeNewTimeProps[$this->proptags['proposed_duration']] = round($newProposedEndTime - $newProposedStartTime) / 60;
690
			$proposeNewTimeProps[$this->proptags['counter_proposal']] = true;
691
		}
692
693
		// While sender is receiver then we have to process the meeting request as per the intended busy status
694
		// instead of tentative, and accept the same as per the intended busystatus.
695
		$senderEntryId = $messageprops[PR_SENT_REPRESENTING_ENTRYID] ?? $messageprops[PR_SENDER_ENTRYID];
696
		if (isset($messageprops[PR_RECEIVED_BY_ENTRYID]) && compareEntryIds($senderEntryId, $messageprops[PR_RECEIVED_BY_ENTRYID])) {
697
			$entryid = $this->accept(false, $sendresponse, $move, $proposeNewTimeProps, $body, true, $store, $calFolder, $basedate);
698
		}
699
		else {
700
			$entryid = $this->accept($tentative, $sendresponse, $move, $proposeNewTimeProps, $body, $userAction, $store, $calFolder, $basedate);
701
		}
702
703
		// if we have first time processed this meeting then set PR_PROCESSED property
704
		if ($this->isMeetingRequest($messageprops[PR_MESSAGE_CLASS]) && $userAction === false && $isImported === false) {
705
			if (!isset($messageprops[PR_PROCESSED]) || $messageprops[PR_PROCESSED] != true) {
706
				// set processed flag
707
				mapi_setprops($this->message, [PR_PROCESSED => true]);
708
				mapi_savechanges($this->message);
709
			}
710
		}
711
712
		return $entryid;
713
	}
714
715
	/**
716
	 * @param (float|mixed|true)[] $proposeNewTimeProps
717
	 * @param resource             $calFolder
718
	 *
719
	 * @psalm-param array<float|mixed|true> $proposeNewTimeProps
720
	 */
721
	public function accept(bool $tentative, bool $sendresponse, bool $move, array $proposeNewTimeProps, mixed $body, bool $userAction, mixed $store, mixed $calFolder, mixed $basedate = false): mixed {
722
		$messageprops = mapi_getprops($this->message);
723
		$isDelegate = $this->isMessageFromDelegate($messageprops);
724
		$entryid = '';
725
726
		if ($sendresponse) {
727
			$this->createResponse($tentative ? olResponseTentative : olResponseAccepted, $proposeNewTimeProps, $body, $store, $basedate, $calFolder);
728
		}
729
730
		/*
731
		 * Further processing depends on what user is receiving. User can receive recurring item, a single occurrence or a normal meeting.
732
		 * 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.
733
		 * 2) If single occurrence then find occurrence itself using globalID and if item is not found then use cleanGlobalID to find main recurring item
734
		 * 3) Normal meeting req are handled normally as they were handled previously.
735
		 *
736
		 * 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
737
		 * 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.
738
		 * If user is responding from calendar then item is opened and properties are set such as meetingstatus, responsestatus, busystatus etc.
739
		 */
740
		if ($this->isMeetingRequest($messageprops[PR_MESSAGE_CLASS])) {
741
			// This meeting request item is recurring, so find all occurrences and saves them all as exceptions to this meeting request item.
742
			if (!empty($messageprops[$this->proptags['recurring']]) && $basedate === false) {
743
				$calendarItem = false;
744
745
				// Find main recurring item based on GlobalID (0x3)
746
				$items = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder);
747
				if (is_array($items)) {
748
					foreach ($items as $entryid) {
749
						$calendarItem = mapi_msgstore_openentry($store, $entryid);
750
					}
751
				}
752
753
				$processed = false;
754
				if (!$calendarItem) {
755
					// Recurring item not found, so create new meeting in Calendar
756
					$calendarItem = mapi_folder_createmessage($calFolder);
757
				}
758
				else {
759
					// we have found the main recurring item, check if this meeting request is already processed
760
					if (isset($messageprops[PR_PROCESSED]) && $messageprops[PR_PROCESSED] === true) {
761
						// only set required properties, other properties are already copied when processing this meeting request
762
						// for the first time
763
						$processed = true;
764
					}
765
					// While we applying updates of MR then all local categories will be removed,
766
					// So get the local categories of all occurrence before applying update from organiser.
767
					$localCategories = $this->getLocalCategories($calendarItem, $store, $calFolder);
768
				}
769
770
				if (!$processed) {
771
					// get all the properties and copy that to calendar item
772
					$props = mapi_getprops($this->message);
773
					// reset the PidLidMeetingType to Unspecified for outlook display the item
774
					$props[$this->proptags['meetingtype']] = mtgEmpty;
775
					// Correct reminder time (some clients like OL can generate wrong flagdueby time)
776
					$this->correctReminderTime($props);
777
				}
778
				else {
779
					// only get required properties so we will not overwrite existing updated properties from calendar
780
					$props = mapi_getprops($this->message, [PR_ENTRYID]);
781
				}
782
783
				$props[PR_MESSAGE_CLASS] = 'IPM.Appointment';
784
				// When meeting requests are generated by third-party solutions, we might be missing the updatecounter property.
785
				if (!isset($props[$this->proptags['updatecounter']])) {
786
					$props[$this->proptags['updatecounter']] = 0;
787
				}
788
				$props[$this->proptags['meetingstatus']] = olMeetingReceived;
789
				$props[$this->proptags['responsestatus']] = $this->determineResponseStatus($userAction, $tentative);
790
791
				$props[$this->proptags['busystatus']] = $this->calculateBusyStatus($tentative, $props);
792
793
				if ($userAction) {
794
					$this->setReplyTimeAndName($props);
795
				}
796
797
				mapi_setprops($calendarItem, $props);
798
799
				// we have already processed attachments and recipients, so no need to do it again
800
				if (!$processed) {
801
					// Copy attachments too
802
					$this->replaceAttachments($this->message, $calendarItem);
803
					// Copy recipients too
804
					$this->replaceRecipients($this->message, $calendarItem, $isDelegate);
805
				}
806
807
				// Find all occurrences based on CleanGlobalID (0x23)
808
				// there will be no exceptions left if $processed is true, but even if it doesn't hurt to recheck
809
				$items = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder, true);
810
				if (is_array($items)) {
811
					// Save all existing occurrence as exceptions
812
					foreach ($items as $entryid) {
813
						// Open occurrence
814
						$occurrenceItem = mapi_msgstore_openentry($store, $entryid);
815
816
						// Save occurrence into main recurring item as exception
817
						if ($occurrenceItem) {
818
							$occurrenceItemProps = mapi_getprops($occurrenceItem, [$this->proptags['goid'], $this->proptags['recurring']]);
819
820
							// Find basedate of occurrence item
821
							$basedate = $this->getBasedateFromGlobalID($occurrenceItemProps[$this->proptags['goid']]);
822
							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...
823
								$this->mergeException($calendarItem, $occurrenceItem, $basedate, $store);
824
							}
825
						}
826
					}
827
				}
828
829
				if (!isset($props[$this->proptags["recurring_pattern"]])) {
830
					$recurr = new Recurrence($store, $calendarItem);
831
					$recurr->saveRecurrencePattern();
832
				}
833
834
				mapi_savechanges($calendarItem);
835
836
				// After applying update of organiser all local categories of occurrence was removed,
837
				// So if local categories exist then apply it on respective occurrence.
838
				if (!empty($localCategories)) {
839
					$this->applyLocalCategories($calendarItem, $store, $localCategories);
840
				}
841
842
				if ($move) {
843
					// open wastebasket of currently logged in user and move the meeting request to it
844
					// for delegates this will be delegate's wastebasket folder
845
					$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
846
					$sourcefolder = $this->openParentFolder();
847
					mapi_folder_copymessages($sourcefolder, [$props[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
848
				}
849
850
				$entryid = $props[PR_ENTRYID];
851
			}
852
			else {
853
				/**
854
				 * This meeting request is not recurring, so can be an exception or normal meeting.
855
				 * If exception then find main recurring item and update exception
856
				 * If main recurring item is not found then put exception into Calendar as normal meeting.
857
				 */
858
				$calendarItem = false;
859
860
				// We found basedate in GlobalID of this meeting request, so this meeting request if for an occurrence.
861
				if ($basedate) {
862
					// Find main recurring item from CleanGlobalID of this meeting request
863
					$items = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder);
864
					if (is_array($items)) {
865
						foreach ($items as $entryid) {
866
							$calendarItem = mapi_msgstore_openentry($store, $entryid);
867
						}
868
					}
869
870
					// Main recurring item is found, so now update exception
871
					if ($calendarItem) {
872
						$this->acceptException($calendarItem, $this->message, $basedate, $move, $tentative, $userAction, $store, $isDelegate);
873
						$calendarItemProps = mapi_getprops($calendarItem, [PR_ENTRYID]);
874
						$entryid = $calendarItemProps[PR_ENTRYID];
875
					}
876
				}
877
878
				if (!$calendarItem) {
879
					$items = $this->findCalendarItems($messageprops[$this->proptags['goid']], $calFolder);
880
					if (is_array($items)) {
881
						// Get local categories before deleting MR.
882
						$message = mapi_msgstore_openentry($store, $items[0]);
883
						$localCategories = mapi_getprops($message, [$this->proptags['categories']]);
884
						mapi_folder_deletemessages($calFolder, $items);
885
					}
886
887
					if ($move) {
888
						// All we have to do is open the default calendar,
889
						// set the message class correctly to be an appointment item
890
						// and move it to the calendar folder
891
						$sourcefolder = $this->openParentFolder();
892
893
						// create a new calendar message, and copy the message to there,
894
						// since we want to delete (move to wastebasket) the original message
895
						$old_entryid = mapi_getprops($this->message, [PR_ENTRYID]);
896
						$calmsg = mapi_folder_createmessage($calFolder);
897
						mapi_copyto($this->message, [], [], $calmsg); /* includes attachments and recipients */
898
						// reset the PidLidMeetingType to Unspecified for outlook display the item
899
						$tmp_props = [];
900
						$tmp_props[$this->proptags['meetingtype']] = mtgEmpty;
901
						// OL needs this field always being set, or it will not display item
902
						$tmp_props[$this->proptags['recurring']] = false;
903
						mapi_setprops($calmsg, $tmp_props);
904
905
						// After creating new MR, If local categories exist then apply it on new MR.
906
						if (!empty($localCategories)) {
907
							mapi_setprops($calmsg, $localCategories);
908
						}
909
910
						$calItemProps = [];
911
						$calItemProps[PR_MESSAGE_CLASS] = 'IPM.Appointment';
912
913
						/*
914
						 * the client which has sent this meeting request can generate wrong flagdueby
915
						 * time (mainly OL), so regenerate that property so we will always show reminder
916
						 * on right time
917
						 */
918
						if (isset($messageprops[$this->proptags['reminderminutes']])) {
919
							$calItemProps[$this->proptags['flagdueby']] = $messageprops[$this->proptags['startdate']] - ($messageprops[$this->proptags['reminderminutes']] * 60);
920
						}
921
922
						$calItemProps[$this->proptags['busystatus']] = $this->calculateBusyStatus($tentative, $messageprops);
923
						if (isset($messageprops[$this->proptags['intendedbusystatus']])) {
924
							$calItemProps[$this->proptags['intendedbusystatus']] = $messageprops[$this->proptags['intendedbusystatus']];
925
						}
926
927
						$calItemProps[$this->proptags['responsestatus']] = $this->determineResponseStatus($userAction, $tentative);
928
						if ($userAction) {
929
							$this->setReplyTimeAndName($calItemProps);
930
						}
931
932
						$calItemProps[$this->proptags['recurring_pattern']] = '';
933
						$calItemProps[$this->proptags['alldayevent']] = $messageprops[$this->proptags['alldayevent']] ?? false;
934
						$calItemProps[$this->proptags['private']] = $messageprops[$this->proptags['private']] ?? false;
935
						$calItemProps[$this->proptags['meetingstatus']] = $messageprops[$this->proptags['meetingstatus']] ?? olMeetingReceived;
936
						if (isset($messageprops[$this->proptags['startdate']])) {
937
							$calItemProps[$this->proptags['commonstart']] = $calItemProps[$this->proptags['startdate']] = $messageprops[$this->proptags['startdate']];
938
						}
939
						if (isset($messageprops[$this->proptags['duedate']])) {
940
							$calItemProps[$this->proptags['commonend']] = $calItemProps[$this->proptags['duedate']] = $messageprops[$this->proptags['duedate']];
941
						}
942
943
						mapi_setprops($calmsg, $proposeNewTimeProps + $calItemProps);
944
945
						// get properties which stores owner information in meeting request mails
946
						$props = mapi_getprops($calmsg, [
947
							PR_SENT_REPRESENTING_ENTRYID,
948
							PR_SENT_REPRESENTING_NAME,
949
							PR_SENT_REPRESENTING_EMAIL_ADDRESS,
950
							PR_SENT_REPRESENTING_ADDRTYPE,
951
							PR_SENT_REPRESENTING_SEARCH_KEY,
952
							PR_SENT_REPRESENTING_SMTP_ADDRESS,
953
						]);
954
955
						// add owner to recipient table
956
						$recips = [];
957
						$this->addOrganizer($props, $recips);
958
						mapi_message_modifyrecipients($calmsg, MODRECIP_ADD, $recips);
959
						mapi_savechanges($calmsg);
960
961
						// Move the message to the wastebasket
962
						$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
963
						mapi_folder_copymessages($sourcefolder, [$old_entryid[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
964
965
						$messageprops = mapi_getprops($calmsg, [PR_ENTRYID]);
966
						$entryid = $messageprops[PR_ENTRYID];
967
					}
968
					else {
969
						// Create a new appointment with duplicate properties and recipient, but as an IPM.Appointment
970
						$new = mapi_folder_createmessage($calFolder);
971
						$props = mapi_getprops($this->message);
972
973
						$props[$this->proptags['recurring_pattern']] = '';
974
						$props[$this->proptags['alldayevent']] ??= false;
975
						$props[$this->proptags['private']] ??= false;
976
						$props[$this->proptags['meetingstatus']] ??= olMeetingReceived;
977
						if (isset($props[$this->proptags['startdate']])) {
978
							$props[$this->proptags['commonstart']] = $props[$this->proptags['startdate']];
979
						}
980
						if (isset($props[$this->proptags['duedate']])) {
981
							$props[$this->proptags['commonend']] = $props[$this->proptags['duedate']];
982
						}
983
984
						$props[PR_MESSAGE_CLASS] = 'IPM.Appointment';
985
						// reset the PidLidMeetingType to Unspecified for outlook display the item
986
						$props[$this->proptags['meetingtype']] = mtgEmpty;
987
						// OL needs this field always being set, or it will not display item
988
						$props[$this->proptags['recurring']] = false;
989
990
						// After creating new MR, If local categories exist then apply it on new MR.
991
						if (!empty($localCategories)) {
992
							mapi_setprops($new, $localCategories);
993
						}
994
995
						/*
996
						 * the client which has sent this meeting request can generate wrong flagdueby
997
						 * time (mainly OL), so regenerate that property so we will always show reminder
998
						 * on right time
999
						 */
1000
						$this->correctReminderTime($props);
1001
1002
						// When meeting requests are generated by third-party solutions, we might be missing the updatecounter property.
1003
						if (!isset($props[$this->proptags['updatecounter']])) {
1004
							$props[$this->proptags['updatecounter']] = 0;
1005
						}
1006
						$props[$this->proptags['responsestatus']] = $this->determineResponseStatus($userAction, $tentative);
1007
1008
						$props[$this->proptags['busystatus']] = $this->calculateBusyStatus($tentative, $props);
1009
1010
						if ($userAction) {
1011
							$this->setReplyTimeAndName($props);
1012
						}
1013
1014
						mapi_setprops($new, $proposeNewTimeProps + $props);
1015
1016
						// If delegate, then do not add the delegate in recipients
1017
						if ($isDelegate) {
1018
							$delegate = mapi_getprops($this->message, [PR_RECEIVED_BY_EMAIL_ADDRESS]);
1019
							$res = [
1020
								RES_PROPERTY,
1021
								[
1022
									RELOP => RELOP_NE,
1023
									ULPROPTAG => PR_EMAIL_ADDRESS,
1024
									VALUE => [PR_EMAIL_ADDRESS => $delegate[PR_RECEIVED_BY_EMAIL_ADDRESS]],
1025
								],
1026
							];
1027
							$recips = $this->getMessageRecipients($this->message, $res);
1028
						}
1029
						else {
1030
							$recips = $this->getMessageRecipients($this->message);
1031
						}
1032
1033
						$this->addOrganizer($props, $recips);
1034
						mapi_message_modifyrecipients($new, MODRECIP_ADD, $recips);
1035
						mapi_savechanges($new);
1036
1037
						$props = mapi_getprops($new, [PR_ENTRYID]);
1038
						$entryid = $props[PR_ENTRYID];
1039
					}
1040
				}
1041
			}
1042
		}
1043
		else {
1044
			// Here only properties are set on calendaritem, because user is responding from calendar.
1045
			$props = [];
1046
			$props[$this->proptags['responsestatus']] = $tentative ? olResponseTentative : olResponseAccepted;
1047
1048
			$props[$this->proptags['busystatus']] = $this->calculateBusyStatus($tentative, $messageprops);
1049
			if (isset($messageprops[$this->proptags['intendedbusystatus']])) {
1050
				$props[$this->proptags['intendedbusystatus']] = $messageprops[$this->proptags['intendedbusystatus']];
1051
			}
1052
1053
			$props[$this->proptags['meetingstatus']] = olMeetingReceived;
1054
1055
			$this->setReplyTimeAndName($props);
1056
1057
			if ($basedate) {
1058
				$recurr = new Recurrence($store, $this->message);
1059
1060
				// Copy recipients list
1061
				$reciptable = mapi_message_getrecipienttable($this->message);
1062
				$recips = mapi_table_queryallrows($reciptable, $this->recipprops);
1063
1064
				if ($recurr->isException($basedate)) {
1065
					$recurr->modifyException($proposeNewTimeProps + $props, $basedate, $recips);
1066
				}
1067
				else {
1068
					$props[$this->proptags['startdate']] = $recurr->getOccurrenceStart($basedate);
1069
					$props[$this->proptags['duedate']] = $recurr->getOccurrenceEnd($basedate);
1070
1071
					$props[PR_SENT_REPRESENTING_EMAIL_ADDRESS] = $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
1072
					$props[PR_SENT_REPRESENTING_NAME] = $messageprops[PR_SENT_REPRESENTING_NAME];
1073
					$props[PR_SENT_REPRESENTING_ADDRTYPE] = $messageprops[PR_SENT_REPRESENTING_ADDRTYPE];
1074
					$props[PR_SENT_REPRESENTING_ENTRYID] = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
1075
					$props[PR_SENT_REPRESENTING_SEARCH_KEY] = $messageprops[PR_SENT_REPRESENTING_SEARCH_KEY];
1076
1077
					$recurr->createException($proposeNewTimeProps + $props, $basedate, false, $recips);
1078
				}
1079
			}
1080
			else {
1081
				mapi_setprops($this->message, $proposeNewTimeProps + $props);
1082
			}
1083
			mapi_savechanges($this->message);
1084
1085
			$entryid = $messageprops[PR_ENTRYID];
1086
		}
1087
1088
		return $entryid;
1089
	}
1090
1091
	/**
1092
	 * Declines the meeting request by moving the item to the deleted
1093
	 * items folder and sending a decline message. After declining, you
1094
	 * can't use this class instance any more. The message is closed.
1095
	 * When an occurrence is decline then false is returned because that
1096
	 * occurrence is deleted not the recurring item.
1097
	 *
1098
	 * @param bool      $sendresponse true if a response has to be sent to organizer
1099
	 * @param false|int $basedate     if specified contains starttime of day of an occurrence
1100
	 *
1101
	 * @return bool true if item is deleted from Calendar else false
1102
	 */
1103
	public function doDecline(bool $sendresponse, mixed $basedate = false, mixed $body = false): bool {
1104
		if ($this->isLocalOrganiser()) {
1105
			return false;
1106
		}
1107
1108
		$result = false;
1109
		$calendaritem = false;
1110
1111
		// Remove any previous calendar items with this goid and appt id
1112
		$messageprops = mapi_getprops($this->message, [$this->proptags['goid'], $this->proptags['goid2'], PR_RCVD_REPRESENTING_ENTRYID]);
1113
1114
		['store' => $store, 'calFolder' => $calFolder] = $this->resolveDelegateStoreAndCalendar($messageprops);
1115
1116
		// check for calendar access before deleting the calendar item
1117
		$this->ensureCalendarWriteAccess($store);
1118
1119
		$goid = $messageprops[$this->proptags['goid']];
1120
1121
		// First, find the items in the calendar by GlobalObjid (0x3)
1122
		$entryids = $this->findCalendarItems($goid, $calFolder);
1123
1124
		if (!$basedate) {
1125
			$basedate = $this->getBasedateFromGlobalID($goid);
1126
		}
1127
1128
		if ($sendresponse) {
1129
			$this->createResponse(olResponseDeclined, [], $body, $store, $basedate, $calFolder);
1130
		}
1131
1132
		if ($basedate) {
1133
			// use CleanGlobalObjid (0x23)
1134
			$calendaritems = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder);
1135
1136
			if (is_array($calendaritems)) {
1137
				foreach ($calendaritems as $entryid) {
1138
					// Open each calendar item and set the properties of the cancellation object
1139
					$calendaritem = mapi_msgstore_openentry($store, $entryid);
1140
1141
					// Recurring item is found, now delete exception
1142
					if ($calendaritem) {
1143
						$this->doRemoveExceptionFromCalendar($basedate, $calendaritem, $store);
1144
						$result = true;
1145
					}
1146
				}
1147
			}
1148
1149
			if ($this->isMeetingRequest()) {
1150
				$calendaritem = false;
1151
			}
1152
		}
1153
1154
		if (!$calendaritem) {
1155
			$calendar = $this->openDefaultCalendar($store);
1156
1157
			if (!empty($entryids)) {
1158
				mapi_folder_deletemessages($calendar, $entryids);
1159
			}
1160
1161
			// All we have to do to decline, is to move the item to the waste basket
1162
			$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
1163
			$sourcefolder = $this->openParentFolder();
1164
1165
			$messageprops = mapi_getprops($this->message, [PR_ENTRYID]);
1166
1167
			// Release the message
1168
			$this->message = null;
1169
1170
			// Move the message to the waste basket
1171
			mapi_folder_copymessages($sourcefolder, [$messageprops[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
1172
1173
			$result = true;
1174
		}
1175
1176
		return $result;
1177
	}
1178
1179
	/**
1180
	 * Removes a meeting request from the calendar when the user presses the
1181
	 * 'remove from calendar' button in response to a meeting cancellation.
1182
	 *
1183
	 * @param false|int $basedate if specified contains starttime of day of an occurrence
1184
	 */
1185
	public function doRemoveFromCalendar(mixed $basedate): ?false {
1186
		if ($this->isLocalOrganiser()) {
1187
			return false;
1188
		}
1189
1190
		$messageprops = mapi_getprops($this->message, [PR_ENTRYID, $this->proptags['goid'], PR_RCVD_REPRESENTING_ENTRYID, PR_MESSAGE_CLASS]);
1191
1192
		$goid = $messageprops[$this->proptags['goid']];
1193
1194
		$store = $this->store;
0 ignored issues
show
Unused Code introduced by
The assignment to $store is dead and can be removed.
Loading history...
1195
		['store' => $store, 'calFolder' => $calFolder] = $this->resolveDelegateStoreAndCalendar($messageprops);
1196
1197
		// check for calendar access before deleting the calendar item
1198
		$this->ensureCalendarWriteAccess($store);
1199
1200
		$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
1201
		// get the source folder of the meeting message
1202
		$sourcefolder = $this->openParentFolder();
1203
1204
		// Check if the message is a meeting request in the inbox or a calendaritem by checking the message class
1205
		if ($this->isMeetingCancellation($messageprops[PR_MESSAGE_CLASS])) {
1206
			// get the basedate to check for exception
1207
			$basedate = $this->getBasedateFromGlobalID($goid);
1208
1209
			$calendarItem = $this->getCorrespondentCalendarItem(true);
1210
1211
			if ($calendarItem !== false) {
1212
				// basedate is provided so open exception
1213
				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...
1214
					$exception = $this->getExceptionItem($calendarItem, $basedate);
1215
1216
					if ($exception !== false) {
1217
						// exception found, remove it from calendar
1218
						$this->doRemoveExceptionFromCalendar($basedate, $calendarItem, $store);
1219
					}
1220
				}
1221
				else {
1222
					// remove normal / recurring series from calendar
1223
					$entryids = mapi_getprops($calendarItem, [PR_ENTRYID]);
1224
1225
					$entryids = [$entryids[PR_ENTRYID]];
1226
1227
					mapi_folder_copymessages($calFolder, $entryids, $wastebasket, MESSAGE_MOVE);
1228
				}
1229
			}
1230
1231
			// Release the message, because we are going to move it to wastebasket
1232
			$this->message = null;
1233
1234
			// Move the cancellation mail to wastebasket
1235
			mapi_folder_copymessages($sourcefolder, [$messageprops[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
1236
		}
1237
		else {
1238
			// Here only properties are set on calendaritem, because user is responding from calendar.
1239
			if ($basedate) {
1240
				// remove the occurrence
1241
				$this->doRemoveExceptionFromCalendar($basedate, $this->message, $store);
1242
			}
1243
			else {
1244
				// remove normal/recurring meeting item.
1245
				// Move the message to the waste basket
1246
				mapi_folder_copymessages($sourcefolder, [$messageprops[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
1247
			}
1248
		}
1249
1250
		return null;
1251
	}
1252
1253
	/**
1254
	 * Function can be used to cancel any existing meeting and send cancellation mails to attendees.
1255
	 * Should only be called from meeting object from calendar.
1256
	 *
1257
	 * @param false|int $basedate (optional) basedate of occurrence which should be cancelled
1258
	 *
1259
	 * @FIXME cancellation mail is also sent to attendee which has declined the meeting
1260
	 * @FIXME don't send canellation mail when cancelling meeting from past
1261
	 */
1262
	public function doCancelInvitation(false|int $basedate = false): void {
1263
		if (!$this->isLocalOrganiser()) {
1264
			return;
1265
		}
1266
1267
		// check write access for delegate
1268
		if ($this->checkCalendarWriteAccess($this->store) !== true) {
1269
			// Throw an exception that we don't have write permissions on calendar folder,
1270
			// error message will be filled by module
1271
			throw new MAPIException(_("Insufficient permissions"), MAPI_E_NO_ACCESS);
1272
		}
1273
1274
		$messageProps = mapi_getprops($this->message, [PR_ENTRYID, $this->proptags['recurring']]);
1275
1276
		if (!empty($messageProps[$this->proptags['recurring']])) {
1277
			// cancellation of recurring series or one occurrence
1278
			$recurrence = new Recurrence($this->store, $this->message);
1279
1280
			// if basedate is specified then we are cancelling only one occurrence, so create exception for that occurrence
1281
			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...
1282
				$recurrence->createException([], $basedate, true);
1283
			}
1284
1285
			// update the meeting request
1286
			$this->updateMeetingRequest();
1287
1288
			// send cancellation mails
1289
			$this->sendMeetingRequest(true, _('Canceled') . ': ', $basedate);
1290
1291
			// save changes in the message
1292
			mapi_savechanges($this->message);
1293
		}
1294
		else {
1295
			// cancellation of normal meeting request
1296
			// Send the cancellation
1297
			$this->updateMeetingRequest();
1298
			$this->sendMeetingRequest(true, _('Canceled') . ': ');
1299
1300
			// save changes in the message
1301
			mapi_savechanges($this->message);
1302
		}
1303
1304
		// if basedate is specified then we have already created exception of it so nothing should be done now
1305
		// but when cancelling normal / recurring meeting request we need to remove meeting from calendar
1306
		if ($basedate === false) {
1307
			// get the wastebasket folder, for delegate this will give wastebasket of delegate
1308
			$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
1309
1310
			// get the source folder of the meeting message
1311
			$sourcefolder = $this->openParentFolder();
1312
1313
			// Move the message to the deleted items
1314
			mapi_folder_copymessages($sourcefolder, [$messageProps[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
1315
		}
1316
	}
1317
1318
	/**
1319
	 * Convert epoch to MAPI FileTime, number of 100-nanosecond units since
1320
	 * the start of January 1, 1601.
1321
	 * https://msdn.microsoft.com/en-us/library/office/cc765906.aspx.
1322
	 *
1323
	 * @param int $epoch the current epoch
1324
	 *
1325
	 * @return int the MAPI FileTime equalevent to the given epoch time
1326
	 */
1327
	public function epochToMapiFileTime(int $epoch): int {
1328
		$nanoseconds_between_epoch = 116444736000000000;
1329
1330
		return ($epoch * 10000000) + $nanoseconds_between_epoch;
1331
	}
1332
1333
	/**
1334
	 * Sets the properties in the message so that is can be sent
1335
	 * as a meeting request. The caller has to submit the message. This
1336
	 * is only used for new MeetingRequests. Pass the appointment item as $message
1337
	 * in the constructor to do this.
1338
	 */
1339
	public function setMeetingRequest(false|int $basedate = false): void {
1340
		$props = mapi_getprops($this->message, [$this->proptags['updatecounter']]);
1341
1342
		// Create a new global id for this item
1343
		// https://msdn.microsoft.com/en-us/library/ee160198(v=exchg.80).aspx
1344
		$goid = pack('H*', '040000008200E00074C5B7101A82E00800000000');
1345
		/*
1346
		$year = gmdate('Y');
1347
		$month = gmdate('n');
1348
		$day = gmdate('j');
1349
		$goid .= pack('n', $year);
1350
		$goid .= pack('C', $month);
1351
		$goid .= pack('C', $day);
1352
		*/
1353
		// Creation Time
1354
		$time = $this->epochToMapiFileTime(time());
1355
		$goid .= pack('V', $time & 0xFFFFFFFF);
1356
		$goid .= pack('V', $time >> 32);
1357
		// 8 Zeros
1358
		$goid .= pack('H*', '0000000000000000');
1359
		// Length of the random data
1360
		$goid .= pack('V', 16);
1361
		// Random data.
1362
		for ($i = 0; $i < 16; ++$i) {
1363
			$goid .= chr(random_int(0, 255));
1364
		}
1365
1366
		// Create a new appointment id for this item
1367
		$apptid = random_int(0, mt_getrandmax());
1368
1369
		$props[PR_OWNER_APPT_ID] = $apptid;
1370
		$props[PR_ICON_INDEX] = 1026;
1371
		$props[$this->proptags['goid']] = $goid;
1372
		$props[$this->proptags['goid2']] = $goid;
1373
1374
		if (!isset($props[$this->proptags['updatecounter']])) {
1375
			$props[$this->proptags['updatecounter']] = 0;			// OL also starts sequence no with zero.
1376
			$props[$this->proptags['last_updatecounter']] = 0;
1377
		}
1378
1379
		mapi_setprops($this->message, $props);
1380
	}
1381
1382
	/**
1383
	 * Sends a meeting request by copying it to the outbox, converting
1384
	 * the message class, adding some properties that are required only
1385
	 * for sending the message and submitting the message. Set cancel to
1386
	 * true if you wish to completely cancel the meeting request. You can
1387
	 * specify an optional 'prefix' to prefix the sent message, which is normally
1388
	 * 'Canceled: '.
1389
	 *
1390
	 * @return (int|mixed)[]|true
1391
	 *
1392
	 * @psalm-return array{error: 1|3|4, displayname: mixed}|true
1393
	 */
1394
	public function sendMeetingRequest(mixed $cancel, mixed $prefix = false, mixed $basedate = false, mixed $modifiedRecips = false, mixed $deletedRecips = false): array|true {
0 ignored issues
show
Bug introduced by
The type true 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...
1395
		$this->includesResources = false;
1396
		$this->nonAcceptingResources = [];
1397
1398
		// Get the properties of the message
1399
		$messageprops = mapi_getprops($this->message, [$this->proptags['recurring']]);
1400
1401
		/*
1402
		 * Submit message to non-resource recipients
1403
		 */
1404
		// Set BusyStatus to olTentative (1)
1405
		// Set MeetingStatus to olMeetingReceived
1406
		// Set ResponseStatus to olResponseNotResponded
1407
1408
		/*
1409
		 * While sending recurrence meeting exceptions are not sent as attachments
1410
		 * because first all exceptions are sent and then recurrence meeting is sent.
1411
		 */
1412
		if (isset($messageprops[$this->proptags['recurring']]) && $messageprops[$this->proptags['recurring']] && !$basedate) {
1413
			// Book resource
1414
			$this->bookResources($this->message, $cancel, $prefix);
1415
1416
			if (!$this->errorSetResource) {
1417
				$recurr = new Recurrence($this->openDefaultStore(), $this->message);
1418
1419
				// First send meetingrequest for recurring item
1420
				$this->submitMeetingRequest($this->message, $cancel, $prefix, false, $recurr, false, $modifiedRecips, $deletedRecips);
1421
1422
				// Then send all meeting request for all exceptions
1423
				$exceptions = $recurr->getAllExceptions();
1424
				if ($exceptions) {
1425
					foreach ($exceptions as $exceptionBasedate) {
1426
						$attach = $recurr->getExceptionAttachment($exceptionBasedate);
1427
1428
						if ($attach) {
1429
							$occurrenceItem = mapi_attach_openobj($attach, MAPI_MODIFY);
1430
							$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

1430
							$this->submitMeetingRequest(/** @scrutinizer ignore-type */ $occurrenceItem, $cancel, false, $exceptionBasedate, $recurr, false, $modifiedRecips, $deletedRecips);
Loading history...
1431
							mapi_savechanges($attach);
1432
						}
1433
					}
1434
				}
1435
			}
1436
		}
1437
		else {
1438
			// Basedate found, an exception is to be sent
1439
			if ($basedate) {
1440
				$recurr = new Recurrence($this->openDefaultStore(), $this->message);
1441
1442
				if ($cancel) {
1443
					// @TODO: remove occurrence from Resource's Calendar if resource was booked for whole series
1444
					$this->submitMeetingRequest($this->message, $cancel, $prefix, $basedate, $recurr, false);
1445
				}
1446
				else {
1447
					$attach = $recurr->getExceptionAttachment($basedate);
1448
1449
					if ($attach) {
1450
						$occurrenceItem = mapi_attach_openobj($attach, MAPI_MODIFY);
1451
1452
						// Book resource for this occurrence
1453
						$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

1453
						$resourceRecipData = $this->bookResources(/** @scrutinizer ignore-type */ $occurrenceItem, $cancel, $prefix, $basedate);
Loading history...
1454
1455
						if (!$this->errorSetResource) {
1456
							// Save all previous changes
1457
							mapi_savechanges($this->message);
1458
1459
							$this->submitMeetingRequest($occurrenceItem, $cancel, $prefix, $basedate, $recurr, true, $modifiedRecips, $deletedRecips);
1460
							mapi_savechanges($occurrenceItem);
1461
							mapi_savechanges($attach);
1462
						}
1463
					}
1464
				}
1465
			}
1466
			else {
1467
				// This is normal meeting
1468
				$resourceRecipData = $this->bookResources($this->message, $cancel, $prefix);
1469
1470
				if (!$this->errorSetResource) {
1471
					$this->submitMeetingRequest($this->message, $cancel, $prefix, false, false, false, $modifiedRecips, $deletedRecips);
1472
				}
1473
			}
1474
		}
1475
1476
		if (isset($this->errorSetResource) && $this->errorSetResource) {
1477
			return [
1478
				'error' => $this->errorSetResource,
1479
				'displayname' => $this->recipientDisplayname,
1480
			];
1481
		}
1482
1483
		return true;
0 ignored issues
show
Bug Best Practice introduced by
The expression return true returns the type true which is incompatible with the type-hinted return array|true.
Loading history...
1484
	}
1485
1486
	/**
1487
	 * Updates the message after an update has been performed (for example,
1488
	 * changing the time of the meeting). This must be called before re-sending
1489
	 * the meeting request. You can also call this function instead of 'setMeetingRequest()'
1490
	 * as it will automatically call setMeetingRequest on this object if it is the first
1491
	 * call to this function.
1492
	 */
1493
	public function updateMeetingRequest(false|int $basedate = false): void {
1494
		$messageprops = mapi_getprops($this->message, [$this->proptags['last_updatecounter'], $this->proptags['goid']]);
1495
1496
		if (!isset($messageprops[$this->proptags['goid']])) {
1497
			$this->setMeetingRequest($basedate);
1498
		}
1499
		else {
1500
			$counter = ($messageprops[$this->proptags['last_updatecounter']] ?? 0) + 1;
1501
1502
			// increment value of last_updatecounter, last_updatecounter will be common for recurring series
1503
			// so even if you sending an exception only you need to update the last_updatecounter in the recurring series message
1504
			// this way we can make sure that every time we will be using a uniwue number for every operation
1505
			mapi_setprops($this->message, [$this->proptags['last_updatecounter'] => $counter]);
1506
		}
1507
	}
1508
1509
	/**
1510
	 * Returns TRUE if we are the organiser of the meeting. Can be used with any type of meeting object.
1511
	 */
1512
	public function isLocalOrganiser(): bool {
1513
		$props = mapi_getprops($this->message, [$this->proptags['goid'], PR_MESSAGE_CLASS]);
1514
1515
		// Determine which item to check based on message class
1516
		$messageClass = $props[PR_MESSAGE_CLASS] ?? '';
1517
		$isMeetingMessage = $this->isMeetingRequest($messageClass) ||
1518
							$this->isMeetingRequestResponse($messageClass) ||
1519
							$this->isMeetingCancellation($messageClass);
1520
1521
		$calendarItem = $isMeetingMessage ?
1522
			$this->getCorrespondentCalendarItem(true) : // Meeting request/response/cancellation mail
1523
			$this->message;  // Calendar item
1524
1525
		// Even if we have received request/response for exception/occurrence then also
1526
		// we can check recurring series for organizer, no need to check with exception/occurrence
1527
		if ($calendarItem === false) {
1528
			return false;
1529
		}
1530
1531
		$messageProps = mapi_getprops($calendarItem, [$this->proptags['responsestatus']]);
1532
1533
		return isset($messageProps[$this->proptags['responsestatus']]) &&
1534
			   $messageProps[$this->proptags['responsestatus']] === olResponseOrganized;
1535
	}
1536
1537
	/*
1538
	 * Support functions - INTERNAL ONLY
1539
	 ***************************************************************************************************
1540
	 */
1541
1542
	/**
1543
	 * Return the tracking status of a recipient based on the IPM class (passed).
1544
	 *
1545
	 * @return int tracking status constant
1546
	 */
1547
	public function getTrackStatus(string $class): int {
1548
		return match ($class) {
1549
			'IPM.Schedule.Meeting.Resp.Pos' => olRecipientTrackStatusAccepted,
1550
			'IPM.Schedule.Meeting.Resp.Tent' => olRecipientTrackStatusTentative,
1551
			'IPM.Schedule.Meeting.Resp.Neg' => olRecipientTrackStatusDeclined,
1552
			default => olRecipientTrackStatusNone,
1553
		};
1554
	}
1555
1556
	/**
1557
	 * Function returns MAPIFolder resource of the folder that currently holds this meeting/meeting request
1558
	 * object.
1559
	 */
1560
	public function openParentFolder(): mixed {
1561
		$messageprops = mapi_getprops($this->message, [PR_PARENT_ENTRYID]);
1562
1563
		return mapi_msgstore_openentry($this->store, $messageprops[PR_PARENT_ENTRYID]);
1564
	}
1565
1566
	/**
1567
	 * Function will return resource of the default calendar folder of store.
1568
	 *
1569
	 * @param mixed $store {optional} user store whose default calendar should be opened
1570
	 *
1571
	 * @return resource default calendar folder of store
1572
	 */
1573
	public function openDefaultCalendar(mixed $store = false): mixed {
1574
		return $this->openDefaultFolder(PR_IPM_APPOINTMENT_ENTRYID, $store);
1575
	}
1576
1577
	/**
1578
	 * Function will return resource of the default outbox folder of store.
1579
	 *
1580
	 * @param mixed $store {optional} user store whose default outbox should be opened
1581
	 *
1582
	 * @return resource default outbox folder of store
1583
	 */
1584
	public function openDefaultOutbox(mixed $store = false): mixed {
1585
		return $this->openBaseFolder(PR_IPM_OUTBOX_ENTRYID, $store);
1586
	}
1587
1588
	/**
1589
	 * Function will return resource of the default wastebasket folder of store.
1590
	 *
1591
	 * @param mixed $store {optional} user store whose default wastebasket should be opened
1592
	 *
1593
	 * @return resource default wastebasket folder of store
1594
	 */
1595
	public function openDefaultWastebasket(mixed $store = false): mixed {
1596
		return $this->openBaseFolder(PR_IPM_WASTEBASKET_ENTRYID, $store);
1597
	}
1598
1599
	/**
1600
	 * Function will return resource of the default calendar folder of store.
1601
	 *
1602
	 * @param mixed $store {optional} user store whose default calendar should be opened
1603
	 *
1604
	 * @return bool|string default calendar folder of store
1605
	 */
1606
	public function getDefaultWastebasketEntryID(mixed $store = false): bool|string {
1607
		return $this->getBaseEntryID(PR_IPM_WASTEBASKET_ENTRYID, $store);
1608
	}
1609
1610
	/**
1611
	 * Function will return resource of the default sent mail folder of store.
1612
	 *
1613
	 * @param mixed $store {optional} user store whose default sent mail should be opened
1614
	 *
1615
	 * @return bool|string default sent mail folder of store
1616
	 */
1617
	public function getDefaultSentmailEntryID(mixed $store = false): bool|string {
1618
		return $this->getBaseEntryID(PR_IPM_SENTMAIL_ENTRYID, $store);
1619
	}
1620
1621
	/**
1622
	 * Function will return entryid of any default folder of store. This method is useful when you want
1623
	 * to get entryid of folder which is stored as properties of inbox folder
1624
	 * (PR_IPM_APPOINTMENT_ENTRYID, PR_IPM_CONTACT_ENTRYID, PR_IPM_DRAFTS_ENTRYID, PR_IPM_JOURNAL_ENTRYID, PR_IPM_NOTE_ENTRYID, PR_IPM_TASK_ENTRYID).
1625
	 *
1626
	 * @param int   $prop  proptag of the folder for which we want to get entryid
1627
	 * @param mixed $store {optional} user store from which we need to get entryid of default folder
1628
	 *
1629
	 * @return bool|string entryid of folder pointed by $prop
1630
	 */
1631
	public function getDefaultFolderEntryID(int $prop, mixed $store = false): bool|string {
1632
		try {
1633
			$inbox = mapi_msgstore_getreceivefolder($store ?: $this->store);
1634
			$inboxprops = mapi_getprops($inbox, [$prop]);
1635
1636
			return $inboxprops[$prop] ?? false;
1637
		}
1638
		catch (MAPIException $e) {
1639
			// public store doesn't support this method
1640
			if ($e->getCode() == MAPI_E_NO_SUPPORT) {
1641
				// don't propagate this error to parent handlers, if store doesn't support it
1642
				$e->setHandled();
1643
			}
1644
		}
1645
1646
		return false;
1647
	}
1648
1649
	/**
1650
	 * Function will return resource of any default folder of store.
1651
	 *
1652
	 * @param int   $prop  proptag of the folder that we want to open
1653
	 * @param mixed $store {optional} user store from which we need to open default folder
1654
	 *
1655
	 * @return false|resource default folder of store
1656
	 */
1657
	public function openDefaultFolder(int $prop, mixed $store = false): mixed {
1658
		$entryid = $this->getDefaultFolderEntryID($prop, $store);
1659
1660
		return $entryid === false ? false : 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

1660
		return $entryid === false ? false : mapi_msgstore_openentry($store ?: $this->store, /** @scrutinizer ignore-type */ $entryid);
Loading history...
Bug Best Practice introduced by
The expression return $entryid === fals...$this->store, $entryid) also could return the type resource which is incompatible with the documented return type false|resource.
Loading history...
1661
	}
1662
1663
	/**
1664
	 * Function will return entryid of default folder from store. This method is useful when you want
1665
	 * to get entryid of folder which is stored as store properties
1666
	 * (PR_IPM_FAVORITES_ENTRYID, PR_IPM_OUTBOX_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_IPM_WASTEBASKET_ENTRYID).
1667
	 *
1668
	 * @param int   $prop  proptag of the folder whose entryid we want to get
1669
	 * @param mixed $store {optional} user store from which we need to get entryid of default folder
1670
	 *
1671
	 * @return bool|string entryid of default folder from store
1672
	 */
1673
	public function getBaseEntryID(int $prop, mixed $store = false): bool|string {
1674
		$storeprops = mapi_getprops($store ?: $this->store, [$prop]);
1675
1676
		return $storeprops[$prop] ?? false;
1677
	}
1678
1679
	/**
1680
	 * Function will return resource of any default folder of store.
1681
	 *
1682
	 * @param int   $prop  proptag of the folder that we want to open
1683
	 * @param mixed $store {optional} user store from which we need to open default folder
1684
	 *
1685
	 * @return false|resource default folder of store
1686
	 */
1687
	public function openBaseFolder(int $prop, mixed $store = false): mixed {
1688
		$entryid = $this->getBaseEntryID($prop, $store);
1689
1690
		return $entryid === false ? false : 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

1690
		return $entryid === false ? false : mapi_msgstore_openentry($store ?: $this->store, /** @scrutinizer ignore-type */ $entryid);
Loading history...
Bug Best Practice introduced by
The expression return $entryid === fals...$this->store, $entryid) also could return the type resource which is incompatible with the documented return type false|resource.
Loading history...
1691
	}
1692
1693
	/**
1694
	 * Function checks whether user has access over the specified folder or not.
1695
	 *
1696
	 * @param string $entryid entryid The entryid of the folder to check
1697
	 * @param mixed  $store   (optional) store from which folder should be opened
1698
	 *
1699
	 * @return bool true if user has an access over the folder, false if not
1700
	 */
1701
	public function checkFolderWriteAccess(string $entryid, mixed $store = false): bool {
1702
		if (empty($entryid)) {
1703
			return false;
1704
		}
1705
1706
		$store = $store ?: $this->store;
1707
1708
		try {
1709
			$folder = mapi_msgstore_openentry($store, $entryid);
1710
			$folderProps = mapi_getprops($folder, [PR_ACCESS]);
1711
1712
			return ($folderProps[PR_ACCESS] & MAPI_ACCESS_CREATE_CONTENTS) === MAPI_ACCESS_CREATE_CONTENTS;
1713
		}
1714
		catch (MAPIException $e) {
1715
			// We don't have rights to open folder, so return false
1716
			if ($e->getCode() == MAPI_E_NO_ACCESS) {
1717
				return false;
1718
			}
1719
1720
			// Rethrow other errors
1721
			throw $e;
1722
		}
1723
	}
1724
1725
	/**
1726
	 * Function checks whether user has access over the specified folder or not.
1727
	 *
1728
	 * @return bool true if user has an access over the folder, false if not
1729
	 */
1730
	public function checkCalendarWriteAccess(mixed $store = false): bool {
1731
		if ($store === false) {
1732
			$messageProps = mapi_getprops($this->message, [PR_RCVD_REPRESENTING_ENTRYID]);
1733
			$store = $this->store;
1734
			// If this meeting request is received by a delegate then open delegator's store.
1735
			if (isset($messageProps[PR_RCVD_REPRESENTING_ENTRYID])) {
1736
				$delegatorStore = $this->getDelegatorStore($messageProps[PR_RCVD_REPRESENTING_ENTRYID]);
1737
				if (!empty($delegatorStore['store'])) {
1738
					$store = $delegatorStore['store'];
1739
				}
1740
			}
1741
		}
1742
1743
		// If the store is a public folder, the calendar folder is the PARENT_ENTRYID of the calendar item
1744
		$provider = mapi_getprops($store, [PR_MDB_PROVIDER]);
1745
		if (isset($provider[PR_MDB_PROVIDER]) && $provider[PR_MDB_PROVIDER] === ZARAFA_STORE_PUBLIC_GUID) {
1746
			$entryid = mapi_getprops($this->message, [PR_PARENT_ENTRYID]);
1747
			$entryid = $entryid[PR_PARENT_ENTRYID];
1748
		}
1749
		else {
1750
			$entryid = $this->getDefaultFolderEntryID(PR_IPM_APPOINTMENT_ENTRYID, $store);
1751
			if ($entryid === false) {
1752
				$entryid = $this->getBaseEntryID(PR_IPM_APPOINTMENT_ENTRYID, $store);
1753
			}
1754
1755
			if ($entryid === false) {
1756
				return false;
1757
			}
1758
		}
1759
1760
		return $this->checkFolderWriteAccess($entryid, $store);
1761
	}
1762
1763
	/**
1764
	 * Function will resolve the user and open its store.
1765
	 *
1766
	 * @param string $ownerentryid the entryid of the user
1767
	 *
1768
	 * @return resource store of the user
1769
	 */
1770
	public function openCustomUserStore(string $ownerentryid): mixed {
1771
		$ab = mapi_openaddressbook($this->session);
1772
1773
		try {
1774
			$mailuser = mapi_ab_openentry($ab, $ownerentryid);
1775
			if (!$mailuser) {
0 ignored issues
show
introduced by
$mailuser is of type resource, thus it always evaluated to true.
Loading history...
1776
				error_log(sprintf("Unable to open ab entry: 0x%08X", mapi_last_hresult()));
1777
1778
				return null;
1779
			}
1780
		}
1781
		catch (MAPIException) {
1782
			return null;
1783
		}
1784
1785
		$mailuserprops = mapi_getprops($mailuser, [PR_EMAIL_ADDRESS]);
1786
		$storeid = mapi_msgstore_createentryid($this->store, $mailuserprops[PR_EMAIL_ADDRESS]);
1787
1788
		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...
1789
	}
1790
1791
	/**
1792
	 * Function which sends response to organizer when attendee accepts, declines or proposes new time to a received meeting request.
1793
	 *
1794
	 * @param int       $status              response status of attendee
1795
	 * @param array     $proposeNewTimeProps properties of attendee's proposal
1796
	 * @param mixed     $body
1797
	 * @param mixed     $store
1798
	 * @param false|int $basedate            date of occurrence which attendee has responded
1799
	 * @param mixed     $calFolder
1800
	 */
1801
	public function createResponse($status, $proposeNewTimeProps, $body, $store, $basedate, $calFolder): void {
1802
		$messageprops = mapi_getprops($this->message, [
1803
			PR_SENT_REPRESENTING_ENTRYID,
1804
			PR_SENT_REPRESENTING_EMAIL_ADDRESS,
1805
			PR_SENT_REPRESENTING_ADDRTYPE,
1806
			PR_SENT_REPRESENTING_NAME,
1807
			PR_SENT_REPRESENTING_SEARCH_KEY,
1808
			$this->proptags['goid'],
1809
			$this->proptags['goid2'],
1810
			$this->proptags['location'],
1811
			$this->proptags['startdate'],
1812
			$this->proptags['duedate'],
1813
			$this->proptags['recurring'],
1814
			$this->proptags['recurring_pattern'],
1815
			$this->proptags['recurrence_data'],
1816
			$this->proptags['timezone_data'],
1817
			$this->proptags['timezone'],
1818
			$this->proptags['updatecounter'],
1819
			PR_SUBJECT,
1820
			PR_MESSAGE_CLASS,
1821
			PR_OWNER_APPT_ID,
1822
			$this->proptags['is_exception'],
1823
		]);
1824
1825
		$props = [];
1826
1827
		if ($basedate !== false && !$this->isMeetingRequest($messageprops[PR_MESSAGE_CLASS])) {
1828
			// we are creating response from a recurring calendar item object
1829
			// We found basedate,so opened occurrence and get properties.
1830
			$recurr = new Recurrence($store, $this->message);
1831
			$exception = $recurr->getExceptionAttachment($basedate);
1832
1833
			if ($exception) {
1834
				// Exception found, Now retrieve properties
1835
				$imessage = mapi_attach_openobj($exception, 0);
1836
				$imsgprops = mapi_getprops($imessage);
1837
1838
				// If location is provided, copy it to the response
1839
				if (isset($imsgprops[$this->proptags['location']])) {
1840
					$messageprops[$this->proptags['location']] = $imsgprops[$this->proptags['location']];
1841
				}
1842
1843
				// Update $messageprops with timings of occurrence
1844
				$messageprops[$this->proptags['startdate']] = $imsgprops[$this->proptags['startdate']];
1845
				$messageprops[$this->proptags['duedate']] = $imsgprops[$this->proptags['duedate']];
1846
1847
				// Meeting related properties
1848
				$props[$this->proptags['meetingstatus']] = $imsgprops[$this->proptags['meetingstatus']];
1849
				$props[$this->proptags['responsestatus']] = $imsgprops[$this->proptags['responsestatus']];
1850
				$props[PR_SUBJECT] = $imsgprops[PR_SUBJECT];
1851
			}
1852
			else {
1853
				// Exceptions is deleted.
1854
				// Update $messageprops with timings of occurrence
1855
				$messageprops[$this->proptags['startdate']] = $recurr->getOccurrenceStart($basedate);
1856
				$messageprops[$this->proptags['duedate']] = $recurr->getOccurrenceEnd($basedate);
1857
1858
				$props[$this->proptags['meetingstatus']] = olNonMeeting;
1859
				$props[$this->proptags['responsestatus']] = olResponseNone;
1860
			}
1861
1862
			$props[$this->proptags['recurring']] = false;
1863
			$props[$this->proptags['is_exception']] = true;
1864
		}
1865
		else {
1866
			// we are creating a response from meeting request mail (it could be recurring or non-recurring)
1867
			// Send all recurrence info in response, if this is a recurrence meeting.
1868
			$isRecurring = !empty($messageprops[$this->proptags['recurring']]);
1869
			$isException = !empty($messageprops[$this->proptags['is_exception']]);
1870
			if ($isRecurring || $isException) {
1871
				if ($isRecurring) {
1872
					$props[$this->proptags['recurring']] = $messageprops[$this->proptags['recurring']];
1873
				}
1874
				if ($isException) {
1875
					$props[$this->proptags['is_exception']] = $messageprops[$this->proptags['is_exception']];
1876
				}
1877
				$calendaritems = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder);
1878
1879
				$calendaritem = mapi_msgstore_openentry($store, $calendaritems[0]);
1880
				$recurr = new Recurrence($store, $calendaritem);
1881
			}
1882
		}
1883
1884
		// we are sending a response for recurring meeting request (or exception), so set some required properties
1885
		if (isset($recurr) && $recurr) {
1886
			if (!empty($messageprops[$this->proptags['recurring_pattern']])) {
1887
				$props[$this->proptags['recurring_pattern']] = $messageprops[$this->proptags['recurring_pattern']];
1888
			}
1889
1890
			if (!empty($messageprops[$this->proptags['recurrence_data']])) {
1891
				$props[$this->proptags['recurrence_data']] = $messageprops[$this->proptags['recurrence_data']];
1892
			}
1893
1894
			$props[$this->proptags['timezone_data']] = $messageprops[$this->proptags['timezone_data']];
1895
			$props[$this->proptags['timezone']] = $messageprops[$this->proptags['timezone']];
1896
1897
			$this->generateRecurDates($recurr, $messageprops, $props);
1898
		}
1899
1900
		// Create a response message
1901
		$recip = [];
1902
		$recip[PR_ENTRYID] = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
1903
		$recip[PR_EMAIL_ADDRESS] = $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
1904
		$recip[PR_ADDRTYPE] = $messageprops[PR_SENT_REPRESENTING_ADDRTYPE];
1905
		$recip[PR_DISPLAY_NAME] = $messageprops[PR_SENT_REPRESENTING_NAME];
1906
		$recip[PR_RECIPIENT_TYPE] = MAPI_TO;
1907
		$recip[PR_SEARCH_KEY] = $messageprops[PR_SENT_REPRESENTING_SEARCH_KEY];
1908
1909
		$subjectprefix = '';
1910
		$classpostfix = '';
1911
1912
		switch ($status) {
1913
			case olResponseAccepted:
1914
				$classpostfix = 'Pos';
1915
				$subjectprefix = _('Accepted');
1916
				break;
1917
1918
			case olResponseDeclined:
1919
				$classpostfix = 'Neg';
1920
				$subjectprefix = _('Declined');
1921
				break;
1922
1923
			case olResponseTentative:
1924
				$classpostfix = 'Tent';
1925
				$subjectprefix = _('Tentatively accepted');
1926
				break;
1927
		}
1928
1929
		if (!empty($proposeNewTimeProps)) {
1930
			// if attendee has proposed new time then change subject prefix
1931
			$subjectprefix = _('New Time Proposed');
1932
		}
1933
1934
		$props[PR_SUBJECT] = $subjectprefix . ': ' . $messageprops[PR_SUBJECT];
1935
1936
		$props[PR_MESSAGE_CLASS] = 'IPM.Schedule.Meeting.Resp.' . $classpostfix;
1937
		if (isset($messageprops[PR_OWNER_APPT_ID])) {
1938
			$props[PR_OWNER_APPT_ID] = $messageprops[PR_OWNER_APPT_ID];
1939
		}
1940
1941
		// Set GlobalId AND CleanGlobalId, if exception then also set basedate into GlobalId(0x3).
1942
		$props[$this->proptags['goid']] = $this->setBasedateInGlobalID(
1943
			$messageprops[$this->proptags['goid2']],
1944
			$basedate,
1945
			isset($recurr) && $recurr instanceof BaseRecurrence ? $recurr : null
1946
		);
1947
		$props[$this->proptags['goid2']] = $messageprops[$this->proptags['goid2']];
1948
		$props[$this->proptags['updatecounter']] = $messageprops[$this->proptags['updatecounter']] ?? 0;
1949
1950
		if (!empty($proposeNewTimeProps)) {
1951
			// merge proposal properties to message properties which will be sent to organizer
1952
			$props = $proposeNewTimeProps + $props;
1953
		}
1954
1955
		// Set body message in Appointment
1956
		if (isset($body)) {
1957
			$props[PR_BODY] = $this->getMeetingTimeInfo() ?: $body;
1958
		}
1959
1960
		// PR_START_DATE/PR_END_DATE is used in the UI in Outlook on the response message
1961
		$props[PR_START_DATE] = $messageprops[$this->proptags['startdate']];
1962
		$props[PR_END_DATE] = $messageprops[$this->proptags['duedate']];
1963
1964
		// Set startdate and duedate in response mail.
1965
		$props[$this->proptags['startdate']] = $messageprops[$this->proptags['startdate']];
1966
		$props[$this->proptags['duedate']] = $messageprops[$this->proptags['duedate']];
1967
1968
		// responselocation is used in the UI in Outlook on the response message
1969
		if (isset($messageprops[$this->proptags['location']])) {
1970
			$props[$this->proptags['responselocation']] = $messageprops[$this->proptags['location']];
1971
			$props[$this->proptags['location']] = $messageprops[$this->proptags['location']];
1972
		}
1973
1974
		$message = $this->createOutgoingMessage($store);
1975
1976
		mapi_setprops($message, $props);
1977
		mapi_message_modifyrecipients($message, MODRECIP_ADD, [$recip]);
1978
		mapi_savechanges($message);
1979
		mapi_message_submitmessage($message);
1980
	}
1981
1982
	/**
1983
	 * Function which finds items in calendar based on globalId and cleanGlobalId.
1984
	 *
1985
	 * @param string $goid             GlobalID(0x3) of item
1986
	 * @param mixed  $calendar         MAPI_folder of user (optional)
1987
	 * @param bool   $useCleanGlobalId if true then search should be performed on cleanGlobalId(0x23) else globalId(0x3)
1988
	 *
1989
	 * @return array|null
1990
	 */
1991
	public function findCalendarItems(string $goid, mixed $calendar = false, bool $useCleanGlobalId = false): ?array {
1992
		if ($calendar === false) {
1993
			// Open the Calendar
1994
			$calendar = $this->openDefaultCalendar();
1995
		}
1996
1997
		// Find the item by restricting all items to the correct ID
1998
		$restrict = [
1999
			RES_PROPERTY,
2000
			[
2001
				RELOP => RELOP_EQ,
2002
				ULPROPTAG => ($useCleanGlobalId === true ? $this->proptags['goid2'] : $this->proptags['goid']),
2003
				VALUE => $goid,
2004
			],
2005
		];
2006
2007
		$calendarcontents = mapi_folder_getcontentstable($calendar);
2008
2009
		$rows = mapi_table_queryallrows($calendarcontents, [PR_ENTRYID], $restrict);
2010
2011
		if (empty($rows)) {
2012
			return null;
2013
		}
2014
2015
		// In principle, there should only be one row, but we'll handle them all just in case
2016
		return array_column($rows, PR_ENTRYID);
2017
	}
2018
2019
	// Returns TRUE if both entryid's are equal. Equality is defined by both entryid's pointing at the
2020
	// same SMTP address when converted to SMTP
2021
	public function compareABEntryIDs(string $entryid1, string $entryid2): bool {
2022
		// If the session was not passed, just do a 'normal' compare.
2023
		if (!$this->session) {
2024
			return $entryid1 == $entryid2;
2025
		}
2026
2027
		return $this->getSMTPAddress($entryid1) == $this->getSMTPAddress($entryid2);
2028
	}
2029
2030
	// Gets the SMTP address of the passed addressbook entryid
2031
	public function getSMTPAddress(string $entryid): false|string {
2032
		if (!$this->session) {
2033
			return false;
2034
		}
2035
2036
		try {
2037
			$ab = mapi_openaddressbook($this->session);
2038
			$abitem = mapi_ab_openentry($ab, $entryid);
2039
2040
			if (!$abitem) {
0 ignored issues
show
introduced by
$abitem is of type resource, thus it always evaluated to true.
Loading history...
2041
				return '';
2042
			}
2043
		}
2044
		catch (MAPIException) {
2045
			return '';
2046
		}
2047
2048
		$props = mapi_getprops($abitem, [PR_ADDRTYPE, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS]);
2049
2050
		if ($props[PR_ADDRTYPE] == 'SMTP') {
2051
			return $props[PR_EMAIL_ADDRESS];
2052
		}
2053
2054
		return $props[PR_SMTP_ADDRESS];
2055
	}
2056
2057
	/**
2058
	 * Gets the properties associated with the owner of the passed store:
2059
	 * PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_ADDRTYPE, PR_ENTRYID, PR_SEARCH_KEY.
2060
	 *
2061
	 * @param mixed $store                  message store
2062
	 * @param bool  $fallbackToLoggedInUser If true then return properties of logged in user instead of mailbox owner.
2063
	 *                                      Not used when passed store is public store.
2064
	 *                                      For public store we are always returning logged in user's info.
2065
	 *
2066
	 * @return array|false properties of logged in user in an array in sequence of display_name, email address, address type, entryid and search key
2067
	 *
2068
	 * @psalm-return false|list{mixed, mixed, mixed, mixed, mixed}
2069
	 */
2070
	public function getOwnerAddress(mixed $store, bool $fallbackToLoggedInUser = true): array|false {
2071
		if (!$this->session) {
2072
			return false;
2073
		}
2074
2075
		$storeProps = mapi_getprops($store, [PR_MAILBOX_OWNER_ENTRYID, PR_USER_ENTRYID]);
2076
2077
		// Determine owner entry ID: use mailbox owner if not falling back, otherwise use user entry ID
2078
		$ownerEntryId = (!$fallbackToLoggedInUser && !empty($storeProps[PR_MAILBOX_OWNER_ENTRYID])) ?
2079
			$storeProps[PR_MAILBOX_OWNER_ENTRYID] :
2080
			($storeProps[PR_USER_ENTRYID] ?? false);
2081
2082
		if (!$ownerEntryId) {
2083
			return false;
2084
		}
2085
2086
		$ab = mapi_openaddressbook($this->session);
2087
2088
		$zarafaUser = mapi_ab_openentry($ab, $ownerEntryId);
2089
		if (!$zarafaUser) {
0 ignored issues
show
introduced by
$zarafaUser is of type resource, thus it always evaluated to true.
Loading history...
2090
			return false;
2091
		}
2092
2093
		$ownerProps = mapi_getprops($zarafaUser, [PR_ADDRTYPE, PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SEARCH_KEY]);
2094
2095
		return [
2096
			$ownerProps[PR_DISPLAY_NAME],
2097
			$ownerProps[PR_EMAIL_ADDRESS],
2098
			$ownerProps[PR_ADDRTYPE],
2099
			$ownerEntryId,
2100
			$ownerProps[PR_SEARCH_KEY],
2101
		];
2102
	}
2103
2104
	// Opens this session's default message store
2105
	public function openDefaultStore(): mixed {
2106
		$entryid = '';
2107
2108
		$storestable = mapi_getmsgstorestable($this->session);
2109
		$rows = mapi_table_queryallrows($storestable, [PR_ENTRYID, PR_DEFAULT_STORE]);
2110
2111
		foreach ($rows as $row) {
2112
			if (isset($row[PR_DEFAULT_STORE]) && $row[PR_DEFAULT_STORE]) {
2113
				$entryid = $row[PR_ENTRYID];
2114
				break;
2115
			}
2116
		}
2117
2118
		if (!$entryid) {
2119
			return false;
2120
		}
2121
2122
		return mapi_openmsgstore($this->session, $entryid);
2123
	}
2124
2125
	/**
2126
	 * Function which adds organizer to recipient list which is passed.
2127
	 * This function also checks if it has organizer.
2128
	 *
2129
	 * @param array $messageProps message properties
2130
	 * @param array $recipients   recipients list of message
2131
	 * @param bool  $isException  true if we are processing recipient of exception
2132
	 */
2133
	public function addOrganizer(array $messageProps, array &$recipients, bool $isException = false): void {
2134
		$hasOrganizer = false;
2135
		// Check if meeting already has an organizer.
2136
		foreach ($recipients as $key => $recipient) {
2137
			if (isset($recipient[PR_RECIPIENT_FLAGS]) && $recipient[PR_RECIPIENT_FLAGS] == (recipSendable | recipOrganizer)) {
2138
				$hasOrganizer = true;
2139
			}
2140
			elseif ($isException && !isset($recipient[PR_RECIPIENT_FLAGS])) {
2141
				// Recipients for an occurrence
2142
				$recipients[$key][PR_RECIPIENT_FLAGS] = recipSendable | recipExceptionalResponse;
2143
			}
2144
		}
2145
2146
		if (!$hasOrganizer) {
2147
			// Create organizer.
2148
			$organizer = [];
2149
			$organizer[PR_ENTRYID] = $organizer[PR_RECIPIENT_ENTRYID] = $messageProps[PR_SENT_REPRESENTING_ENTRYID];
2150
			$organizer[PR_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
2151
			$organizer[PR_EMAIL_ADDRESS] = $messageProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
2152
			$organizer[PR_RECIPIENT_TYPE] = MAPI_TO;
2153
			$organizer[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_SENT_REPRESENTING_NAME];
2154
			$organizer[PR_ADDRTYPE] = empty($messageProps[PR_SENT_REPRESENTING_ADDRTYPE]) ? 'SMTP' : $messageProps[PR_SENT_REPRESENTING_ADDRTYPE];
2155
			$organizer[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
2156
			$organizer[PR_RECIPIENT_FLAGS] = recipSendable | recipOrganizer;
2157
			$organizer[PR_SEARCH_KEY] = $messageProps[PR_SENT_REPRESENTING_SEARCH_KEY];
2158
			$organizer[PR_SMTP_ADDRESS] = $messageProps[PR_SENT_REPRESENTING_SMTP_ADDRESS] ?? $messageProps[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
2159
2160
			// Add organizer to recipients list.
2161
			array_unshift($recipients, $organizer);
2162
		}
2163
	}
2164
2165
	/**
2166
	 * Function which removes an exception/occurrence from recurrencing meeting
2167
	 * when a meeting cancellation of an occurrence is processed.
2168
	 *
2169
	 * @param false|int $basedate basedate of an occurrence
2170
	 * @param mixed     $message  recurring item from which occurrence has to be deleted
2171
	 * @param resource  $store    MAPI_MSG_Store which contains the item
2172
	 */
2173
	public function doRemoveExceptionFromCalendar(mixed $basedate, mixed $message, mixed $store): void {
2174
		$recurr = new Recurrence($store, $message);
2175
		$recurr->createException([], $basedate, true);
2176
		mapi_savechanges($message);
2177
	}
2178
2179
	/**
2180
	 * Function which returns basedate of an changed occurrence from globalID of meeting request.
2181
	 *
2182
	 * @param string $goid globalID
2183
	 *
2184
	 * @return false|int true if basedate is found else false it not found
2185
	 */
2186
	public function getBasedateFromGlobalID(string $goid): false|int {
2187
		$hexguid = bin2hex($goid);
2188
		$hexbase = substr($hexguid, 32, 8);
2189
		$day = (int) hexdec(substr($hexbase, 6, 2));
2190
		$month = (int) hexdec(substr($hexbase, 4, 2));
2191
		$year = (int) hexdec(substr($hexbase, 0, 4));
2192
2193
		return ($day && $month && $year) ? gmmktime(0, 0, 0, $month, $day, $year) : false;
2194
	}
2195
2196
	/**
2197
	 * Function which sets basedate in globalID of changed occurrence which is to be sent.
2198
	 *
2199
	 * @param string              $goid       globalID
2200
	 * @param false|int           $basedate   of changed occurrence (UTC when $recurrence is provided)
2201
	 * @param null|BaseRecurrence $recurrence recurrence helper for timezone conversion
2202
	 *
2203
	 * @return false|string globalID with basedate in it
2204
	 */
2205
	public function setBasedateInGlobalID(string $goid, false|int $basedate = false, ?BaseRecurrence $recurrence = null): false|string {
2206
		$hexguid = bin2hex($goid);
2207
		$timestamp = $basedate;
2208
2209
		if ($basedate !== false && $recurrence instanceof BaseRecurrence && isset($recurrence->tz)) {
2210
			$timestamp = $recurrence->fromGMT($recurrence->tz, $basedate);
2211
		}
2212
2213
		$year = $timestamp !== false ? sprintf('%04s', dechex((int) gmdate('Y', $timestamp))) : '0000';
2214
		$month = $timestamp !== false ? sprintf('%02s', dechex((int) gmdate('m', $timestamp))) : '00';
2215
		$day = $timestamp !== false ? sprintf('%02s', dechex((int) gmdate('d', $timestamp))) : '00';
2216
2217
		return hex2bin(strtoupper(substr($hexguid, 0, 32) . $year . $month . $day . substr($hexguid, 40)));
2218
	}
2219
2220
	/**
2221
	 * Function which replaces attachments with copy_from in copy_to.
2222
	 *
2223
	 * @param mixed $copyFrom       MAPI_message from which attachments are to be copied
2224
	 * @param mixed $copyTo         MAPI_message to which attachment are to be copied
2225
	 * @param bool  $copyExceptions if true then all exceptions should also be sent as attachments
2226
	 */
2227
	public function replaceAttachments(mixed $copyFrom, mixed $copyTo, bool $copyExceptions = true): void {
2228
		/* remove all old attachments */
2229
		$attachmentTableTo = mapi_message_getattachmenttable($copyTo);
2230
		if ($attachmentTableTo) {
0 ignored issues
show
introduced by
$attachmentTableTo is of type resource, thus it always evaluated to true.
Loading history...
2231
			$attachments = mapi_table_queryallrows($attachmentTableTo, [PR_ATTACH_NUM, PR_ATTACH_METHOD, PR_EXCEPTION_STARTTIME]);
2232
2233
			foreach ($attachments as $attachProps) {
2234
				/* remove exceptions too? */
2235
				if (!$copyExceptions && $attachProps[PR_ATTACH_METHOD] == ATTACH_EMBEDDED_MSG && isset($attachProps[PR_EXCEPTION_STARTTIME])) {
2236
					continue;
2237
				}
2238
				mapi_message_deleteattach($copyTo, $attachProps[PR_ATTACH_NUM]);
2239
			}
2240
		}
2241
2242
		/* copy new attachments */
2243
		$attachmentTableFrom = mapi_message_getattachmenttable($copyFrom);
2244
		if ($attachmentTableFrom) {
0 ignored issues
show
introduced by
$attachmentTableFrom is of type resource, thus it always evaluated to true.
Loading history...
2245
			$attachments = mapi_table_queryallrows($attachmentTableFrom, [PR_ATTACH_NUM, PR_ATTACH_METHOD, PR_EXCEPTION_STARTTIME]);
2246
2247
			foreach ($attachments as $attachProps) {
2248
				if (!$copyExceptions && $attachProps[PR_ATTACH_METHOD] == ATTACH_EMBEDDED_MSG && isset($attachProps[PR_EXCEPTION_STARTTIME])) {
2249
					continue;
2250
				}
2251
2252
				$attachOld = mapi_message_openattach($copyFrom, (int) $attachProps[PR_ATTACH_NUM]);
2253
				$attachNewResourceMsg = mapi_message_createattach($copyTo);
2254
				mapi_copyto($attachOld, [], [], $attachNewResourceMsg, 0);
2255
				mapi_savechanges($attachNewResourceMsg);
2256
			}
2257
		}
2258
	}
2259
2260
	/**
2261
	 * Function which replaces recipients in copyTo with recipients from copyFrom.
2262
	 *
2263
	 * @param mixed $copyFrom   MAPI_message from which recipients are to be copied
2264
	 * @param mixed $copyTo     MAPI_message to which recipients are to be copied
2265
	 * @param bool  $isDelegate indicates whether delegate is processing
2266
	 *                          so don't copy delegate information to recipient table
2267
	 */
2268
	public function replaceRecipients(mixed $copyFrom, mixed $copyTo, bool $isDelegate = false): void {
2269
		// If delegate, then do not add the delegate in recipients
2270
		if ($isDelegate) {
2271
			$delegate = mapi_getprops($copyFrom, [PR_RECEIVED_BY_EMAIL_ADDRESS]);
2272
			$res = [
2273
				RES_PROPERTY,
2274
				[
2275
					RELOP => RELOP_NE,
2276
					ULPROPTAG => PR_EMAIL_ADDRESS,
2277
					VALUE => [PR_EMAIL_ADDRESS => $delegate[PR_RECEIVED_BY_EMAIL_ADDRESS]],
2278
				],
2279
			];
2280
			$recipients = $this->getMessageRecipients($copyFrom, $res);
2281
		}
2282
		else {
2283
			$recipients = $this->getMessageRecipients($copyFrom);
2284
		}
2285
2286
		$copyToRecipientTable = mapi_message_getrecipienttable($copyTo);
2287
		$copyToRecipientRows = mapi_table_queryallrows($copyToRecipientTable, [PR_ROWID]);
2288
2289
		mapi_message_modifyrecipients($copyTo, MODRECIP_REMOVE, $copyToRecipientRows);
2290
		mapi_message_modifyrecipients($copyTo, MODRECIP_ADD, $recipients);
2291
	}
2292
2293
	/**
2294
	 * Function creates meeting item in resource's calendar.
2295
	 *
2296
	 * @param resource  $message  MAPI_message which is to create in resource's calendar
2297
	 * @param bool      $cancel   cancel meeting
2298
	 * @param mixed     $prefix   prefix for subject of meeting
2299
	 * @param false|int $basedate
2300
	 *
2301
	 * @return (mixed|resource)[][]
2302
	 *
2303
	 * @psalm-return list<array{store: resource, folder: mixed, msg: mixed}>
2304
	 */
2305
	public function bookResources($message, $cancel, $prefix, $basedate = false): array {
2306
		if (!$this->enableDirectBooking) {
2307
			return [];
2308
		}
2309
2310
		// Get the properties of the message
2311
		$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

2311
		$messageprops = mapi_getprops(/** @scrutinizer ignore-type */ $message);
Loading history...
2312
2313
		$calFolder = '';
2314
2315
		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...
2316
			$recurrItemProps = mapi_getprops($this->message, [$this->proptags['goid'], $this->proptags['goid2'], $this->proptags['timezone_data'], $this->proptags['timezone'], PR_OWNER_APPT_ID]);
2317
2318
			$recurrenceHelper = new Recurrence($this->openDefaultStore(), $this->message);
2319
			$basedateUtc = $basedate;
2320
			if ($recurrenceHelper instanceof BaseRecurrence && isset($recurrenceHelper->tz)) {
2321
				$basedateUtc = $recurrenceHelper->toGMT($recurrenceHelper->tz, $basedate);
2322
			}
2323
2324
			$messageprops[$this->proptags['goid']] = $this->setBasedateInGlobalID($recurrItemProps[$this->proptags['goid']], $basedateUtc, $recurrenceHelper);
2325
			$messageprops[$this->proptags['goid2']] = $recurrItemProps[$this->proptags['goid2']];
2326
2327
			// Delete properties which are not needed.
2328
			$deleteProps = [$this->proptags['basedate'], PR_DISPLAY_NAME, PR_ATTACHMENT_FLAGS, PR_ATTACHMENT_HIDDEN, PR_ATTACHMENT_LINKID, PR_ATTACH_FLAGS, PR_ATTACH_METHOD];
2329
			foreach ($deleteProps as $propID) {
2330
				if (isset($messageprops[$propID])) {
2331
					unset($messageprops[$propID]);
2332
				}
2333
			}
2334
2335
			if (isset($messageprops[$this->proptags['recurring']])) {
2336
				$messageprops[$this->proptags['recurring']] = false;
2337
			}
2338
2339
			// Set Outlook properties
2340
			$messageprops[$this->proptags['clipstart']] = $messageprops[$this->proptags['startdate']];
2341
			$messageprops[$this->proptags['clipend']] = $messageprops[$this->proptags['duedate']];
2342
			$messageprops[$this->proptags['timezone_data']] = $recurrItemProps[$this->proptags['timezone_data']];
2343
			$messageprops[$this->proptags['timezone']] = $recurrItemProps[$this->proptags['timezone']];
2344
			$messageprops[$this->proptags['attendee_critical_change']] = time();
2345
			$messageprops[$this->proptags['owner_critical_change']] = time();
2346
		}
2347
2348
		// Get resource recipients
2349
		$getResourcesRestriction = [
2350
			RES_PROPERTY,
2351
			[
2352
				RELOP => RELOP_EQ,	// Equals recipient type 3: Resource
2353
				ULPROPTAG => PR_RECIPIENT_TYPE,
2354
				VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
2355
			],
2356
		];
2357
		$resourceRecipients = $this->getMessageRecipients($message, $getResourcesRestriction);
2358
2359
		$this->errorSetResource = false;
2360
		$resourceRecipData = [];
2361
2362
		// Put appointment into store resource users
2363
		$i = 0;
2364
		$len = count($resourceRecipients);
2365
		while (!$this->errorSetResource && $i < $len) {
2366
			$userStore = $this->openCustomUserStore($resourceRecipients[$i][PR_ENTRYID]);
2367
2368
			// Open root folder
2369
			$userRoot = mapi_msgstore_openentry($userStore);
2370
2371
			// Get calendar entryID
2372
			$userRootProps = mapi_getprops($userRoot, [PR_STORE_ENTRYID, PR_IPM_APPOINTMENT_ENTRYID, PR_FREEBUSY_ENTRYIDS]);
2373
2374
			// Open Calendar folder
2375
			$accessToFolder = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $accessToFolder is dead and can be removed.
Loading history...
2376
2377
			try {
2378
				// @FIXME this checks delegate has access to resource's calendar folder
2379
				// but it should use boss' credentials
2380
2381
				$accessToFolder = $this->checkCalendarWriteAccess($this->store);
2382
				if ($accessToFolder) {
2383
					$calFolder = mapi_msgstore_openentry($userStore, $userRootProps[PR_IPM_APPOINTMENT_ENTRYID]);
2384
				}
2385
			}
2386
			catch (MAPIException $e) {
2387
				$e->setHandled();
2388
				$this->errorSetResource = 1; // No access
2389
			}
2390
2391
			if ($accessToFolder) {
2392
				/**
2393
				 * Get the LocalFreebusy message that contains the properties that
2394
				 * are set to accept or decline resource meeting requests.
2395
				 */
2396
				$localFreebusyMsg = FreeBusy::getLocalFreeBusyMessage($userStore);
2397
				if ($localFreebusyMsg) {
2398
					$props = mapi_getprops($localFreebusyMsg, [PR_SCHDINFO_AUTO_ACCEPT_APPTS, PR_SCHDINFO_DISALLOW_RECURRING_APPTS, PR_SCHDINFO_DISALLOW_OVERLAPPING_APPTS]);
2399
2400
					$acceptMeetingRequests = $props[PR_SCHDINFO_AUTO_ACCEPT_APPTS] ?? false;
2401
					$declineRecurringMeetingRequests = $props[PR_SCHDINFO_DISALLOW_RECURRING_APPTS] ?? false;
2402
					$declineConflictingMeetingRequests = $props[PR_SCHDINFO_DISALLOW_OVERLAPPING_APPTS] ?? false;
2403
2404
					if (!$acceptMeetingRequests) {
2405
						/*
2406
						 * When a resource has not been set to automatically accept meeting requests,
2407
						 * the meeting request has to be sent to him rather than being put directly into
2408
						 * his calendar. No error should be returned.
2409
						 */
2410
						// $errorSetResource = 2;
2411
						$this->nonAcceptingResources[] = $resourceRecipients[$i];
2412
					}
2413
					else {
2414
						if ($declineRecurringMeetingRequests && !$cancel) {
2415
							// Check if appointment is recurring
2416
							if ($messageprops[$this->proptags['recurring']]) {
2417
								$this->errorSetResource = 3;
2418
							}
2419
						}
2420
						if ($declineConflictingMeetingRequests && !$cancel) {
2421
							// Check for conflicting items
2422
							if ($calFolder && $this->isMeetingConflicting($message, $userStore, $calFolder)) {
2423
								$this->errorSetResource = 4; // Conflict
2424
							}
2425
						}
2426
					}
2427
				}
2428
			}
2429
2430
			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...
2431
				/**
2432
				 * First search on GlobalID(0x3)
2433
				 * 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.
2434
				 * If (normal meeting) then GlobalID(0x3) and CleanGlobalID(0x23) are same, so doesn't matter if search is based on GlobalID.
2435
				 */
2436
				$rows = $this->findCalendarItems($messageprops[$this->proptags['goid']], $calFolder);
2437
2438
				/*
2439
				 * If no entry is found then
2440
				 * 1) Resource doesn't have meeting in Calendar. Seriously!!
2441
				 * OR
2442
				 * 2) We were looking for occurrence item but Resource has whole series
2443
				 */
2444
				if (empty($rows)) {
2445
					/**
2446
					 * Now search on CleanGlobalID(0x23) WHY???
2447
					 * Because we are looking recurring item.
2448
					 *
2449
					 * Possible results of this search
2450
					 * 1) If Resource was booked for more than one occurrences then this search will return all those occurrence because search is perform on CleanGlobalID
2451
					 * 2) If Resource was booked for whole series then it should return series.
2452
					 */
2453
					$rows = $this->findCalendarItems($messageprops[$this->proptags['goid2']], $calFolder, true);
2454
2455
					$newResourceMsg = false;
2456
					if (!empty($rows)) {
2457
						// Since we are looking for recurring item, open every result and check for 'recurring' property.
2458
						foreach ($rows as $row) {
2459
							$ResourceMsg = mapi_msgstore_openentry($userStore, $row);
2460
							$ResourceMsgProps = mapi_getprops($ResourceMsg, [$this->proptags['recurring']]);
2461
2462
							if (!empty($ResourceMsgProps[$this->proptags['recurring']])) {
2463
								$newResourceMsg = $ResourceMsg;
2464
								break;
2465
							}
2466
						}
2467
					}
2468
2469
					// Still no results found. I giveup, create new message.
2470
					if (!$newResourceMsg) {
2471
						$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

2471
						$newResourceMsg = mapi_folder_createmessage(/** @scrutinizer ignore-type */ $calFolder);
Loading history...
2472
					}
2473
				}
2474
				else {
2475
					$newResourceMsg = mapi_msgstore_openentry($userStore, $rows[0]);
2476
				}
2477
2478
				// Prefix the subject if needed
2479
				if ($prefix && isset($messageprops[PR_SUBJECT])) {
2480
					$messageprops[PR_SUBJECT] = $prefix . $messageprops[PR_SUBJECT];
2481
				}
2482
2483
				// Set status to cancelled if needed
2484
				$messageprops[$this->proptags['busystatus']] = fbBusy; // The default status (Busy)
2485
				if ($cancel) {
2486
					$messageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // The meeting has been canceled
2487
					$messageprops[$this->proptags['busystatus']] = fbFree; // Free
2488
				}
2489
				else {
2490
					$messageprops[$this->proptags['meetingstatus']] = olMeetingReceived; // The recipient is receiving the request
2491
				}
2492
				$messageprops[$this->proptags['responsestatus']] = olResponseAccepted; // The resource automatically accepts the appointment
2493
2494
				$messageprops[PR_MESSAGE_CLASS] = 'IPM.Appointment';
2495
2496
				// Remove the PR_ICON_INDEX as it is not needed in the sent message.
2497
				$messageprops[PR_ICON_INDEX] = null;
2498
				$messageprops[PR_RESPONSE_REQUESTED] = true;
2499
2500
				// get the store of organizer, in case of delegates it will be delegate store
2501
				$defaultStore = $this->openDefaultStore();
2502
2503
				$storeProps = mapi_getprops($this->store, [PR_ENTRYID]);
2504
				$defaultStoreProps = mapi_getprops($defaultStore, [PR_ENTRYID]);
2505
2506
				if (!compareEntryIds($storeProps[PR_ENTRYID], $defaultStoreProps[PR_ENTRYID])) {
2507
					// get delegate information
2508
					$addrInfo = $this->getOwnerAddress($defaultStore, false);
2509
					$this->setAddressProperties($messageprops, $addrInfo, 'SENDER');
0 ignored issues
show
Bug introduced by
It seems like $addrInfo can also be of type false; however, parameter $addrInfo of Meetingrequest::setAddressProperties() does only seem to accept array, 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

2509
					$this->setAddressProperties($messageprops, /** @scrutinizer ignore-type */ $addrInfo, 'SENDER');
Loading history...
2510
2511
					// get delegator information
2512
					$addrInfo = $this->getOwnerAddress($this->store, false);
2513
					$this->setAddressProperties($messageprops, $addrInfo, 'SENT_REPRESENTING');
2514
				}
2515
				else {
2516
					// get organizer information
2517
					$addrInfo = $this->getOwnerAddress($this->store);
2518
					$this->setAddressProperties($messageprops, $addrInfo, 'SENDER');
2519
					$this->setAddressProperties($messageprops, $addrInfo, 'SENT_REPRESENTING');
2520
				}
2521
2522
				$messageprops[$this->proptags['replytime']] = time();
2523
2524
				if ($basedate && isset($ResourceMsgProps[$this->proptags['recurring']]) && $ResourceMsgProps[$this->proptags['recurring']]) {
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...
2525
					$recurr = new Recurrence($userStore, $newResourceMsg);
2526
2527
					// Copy recipients list
2528
					$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

2528
					$reciptable = mapi_message_getrecipienttable(/** @scrutinizer ignore-type */ $message);
Loading history...
2529
					$recips = mapi_table_queryallrows($reciptable, $this->recipprops);
2530
2531
					// add owner to recipient table
2532
					$this->addOrganizer($messageprops, $recips, true);
2533
2534
					// Update occurrence
2535
					if ($recurr->isException($basedate)) {
2536
						$recurr->modifyException($messageprops, $basedate, $recips);
2537
					}
2538
					else {
2539
						$recurr->createException($messageprops, $basedate, false, $recips);
2540
					}
2541
				}
2542
				else {
2543
					mapi_setprops($newResourceMsg, $messageprops);
2544
2545
					// Copy attachments
2546
					$this->replaceAttachments($message, $newResourceMsg);
2547
2548
					// Copy all recipients too
2549
					$this->replaceRecipients($message, $newResourceMsg);
2550
2551
					// Now add organizer also to recipient table
2552
					$recips = [];
2553
					$this->addOrganizer($messageprops, $recips);
2554
2555
					mapi_message_modifyrecipients($newResourceMsg, MODRECIP_ADD, $recips);
2556
				}
2557
2558
				mapi_savechanges($newResourceMsg);
2559
2560
				$resourceRecipData[] = [
2561
					'store' => $userStore,
2562
					'folder' => $calFolder,
2563
					'msg' => $newResourceMsg,
2564
				];
2565
				$this->includesResources = true;
2566
			}
2567
			else {
2568
				/*
2569
				 * If no other errors occurred and you have no access to the
2570
				 * folder of the resource, throw an error=1.
2571
				 */
2572
				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...
2573
					$this->errorSetResource = 1;
2574
				}
2575
2576
				for ($j = 0, $len = count($resourceRecipData); $j < $len; ++$j) {
2577
					// Get the EntryID
2578
					$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

2578
					$props = /** @scrutinizer ignore-call */ mapi_message_getprops($resourceRecipData[$j]['msg']);
Loading history...
2579
2580
					mapi_folder_deletemessages($resourceRecipData[$j]['folder'], [$props[PR_ENTRYID]], DELETE_HARD_DELETE);
2581
				}
2582
				$this->recipientDisplayname = $resourceRecipients[$i][PR_DISPLAY_NAME];
2583
			}
2584
			++$i;
2585
		}
2586
2587
		$resourceRecipients = $this->getMessageRecipients($message);
2588
		if (!empty($resourceRecipients)) {
2589
			// Set Tracking status of resource recipients to olResponseAccepted (3)
2590
			for ($i = 0, $len = count($resourceRecipients); $i < $len; ++$i) {
2591
				if (isset($resourceRecipients[$i][PR_RECIPIENT_TYPE]) && $resourceRecipients[$i][PR_RECIPIENT_TYPE] == MAPI_BCC) {
2592
					$resourceRecipients[$i][PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusAccepted;
2593
					$resourceRecipients[$i][PR_RECIPIENT_TRACKSTATUS_TIME] = time();
2594
				}
2595
			}
2596
			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

2596
			mapi_message_modifyrecipients(/** @scrutinizer ignore-type */ $message, MODRECIP_MODIFY, $resourceRecipients);
Loading history...
2597
		}
2598
2599
		return $resourceRecipData;
2600
	}
2601
2602
	/**
2603
	 * Function which save an exception into recurring item.
2604
	 *
2605
	 * @param resource $recurringItem  reference to MAPI_message of recurring item
2606
	 * @param resource $occurrenceItem reference to MAPI_message of occurrence
2607
	 * @param string   $basedate       basedate of occurrence
2608
	 * @param bool     $move           if true then occurrence item is deleted
2609
	 * @param bool     $tentative      true if user has tentatively accepted it or false if user has accepted it
2610
	 * @param bool     $userAction     true if user has manually responded to meeting request
2611
	 * @param resource $store          user store
2612
	 * @param bool     $isDelegate     true if delegate is processing this meeting request
2613
	 */
2614
	public function acceptException(mixed &$recurringItem, mixed &$occurrenceItem, mixed $basedate, bool $move, bool $tentative, bool $userAction, mixed $store, bool $isDelegate = false): void {
2615
		$recurr = new Recurrence($store, $recurringItem);
2616
2617
		// Copy properties from meeting request
2618
		$exception_props = mapi_getprops($occurrenceItem);
2619
2620
		// Copy recipients list
2621
		// If delegate, then do not add the delegate in recipients
2622
		if ($isDelegate) {
2623
			$delegate = mapi_getprops($this->message, [PR_RECEIVED_BY_EMAIL_ADDRESS]);
2624
			$res = [
2625
				RES_PROPERTY,
2626
				[
2627
					RELOP => RELOP_NE,
2628
					ULPROPTAG => PR_EMAIL_ADDRESS,
2629
					VALUE => [PR_EMAIL_ADDRESS => $delegate[PR_RECEIVED_BY_EMAIL_ADDRESS]],
2630
				],
2631
			];
2632
			$recips = $this->getMessageRecipients($occurrenceItem, $res);
2633
		}
2634
		else {
2635
			$recips = $this->getMessageRecipients($occurrenceItem);
2636
		}
2637
2638
		// add owner to recipient table
2639
		$this->addOrganizer($exception_props, $recips, true);
2640
2641
		// add delegator to meetings
2642
		if ($isDelegate) {
2643
			$this->addDelegator($exception_props, $recips);
2644
		}
2645
2646
		$exception_props[$this->proptags['meetingstatus']] = olMeetingReceived;
2647
		$exception_props[$this->proptags['responsestatus']] = $this->determineResponseStatus($userAction, $tentative);
2648
2649
		if (isset($exception_props[$this->proptags['intendedbusystatus']])) {
2650
			if ($tentative && $exception_props[$this->proptags['intendedbusystatus']] !== fbFree) {
2651
				$exception_props[$this->proptags['busystatus']] = fbTentative;
2652
			}
2653
			else {
2654
				$exception_props[$this->proptags['busystatus']] = $exception_props[$this->proptags['intendedbusystatus']];
2655
			}
2656
			// we already have intendedbusystatus value in $exception_props so no need to copy it
2657
		}
2658
		else {
2659
			$exception_props[$this->proptags['busystatus']] = $tentative ? fbTentative : fbBusy;
2660
		}
2661
2662
		if ($userAction) {
2663
			$this->setReplyTimeAndName($exception_props);
2664
		}
2665
2666
		// In some cases the exception subject is not in the property list,
2667
		// so it's necessary to fetch it.
2668
		if (!isset($exception_props[PR_SUBJECT])) {
2669
			$exSubject = mapi_getprops($occurrenceItem, [PR_SUBJECT]);
2670
			if (!empty($exSubject[PR_SUBJECT])) {
2671
				$exception_props[PR_SUBJECT] = $exSubject[PR_SUBJECT];
2672
			}
2673
		}
2674
		if ($recurr->isException($basedate)) {
2675
			$recurr->modifyException($exception_props, $basedate, $recips, $occurrenceItem);
2676
		}
2677
		else {
2678
			$recurr->createException($exception_props, $basedate, false, $recips, $occurrenceItem);
2679
		}
2680
2681
		// Move the occurrenceItem to the waste basket
2682
		if ($move) {
2683
			$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
2684
			$sourcefolder = mapi_msgstore_openentry($store, $exception_props[PR_PARENT_ENTRYID]);
2685
			mapi_folder_copymessages($sourcefolder, [$exception_props[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
2686
		}
2687
2688
		mapi_savechanges($recurringItem);
2689
	}
2690
2691
	/**
2692
	 * Function which merges an exception mapi message to recurring message.
2693
	 * This will be used when we receive recurring meeting request and we already have an exception message
2694
	 * of same meeting in calendar and we need to remove that exception message and add it to attachment table
2695
	 * of recurring meeting.
2696
	 *
2697
	 * @param resource  $recurringItem  reference to MAPI_message of recurring item
2698
	 * @param resource  $occurrenceItem reference to MAPI_message of occurrence
2699
	 * @param false|int $basedate       basedate of occurrence
2700
	 * @param resource  $store          user store
2701
	 */
2702
	public function mergeException(mixed &$recurringItem, mixed &$occurrenceItem, mixed $basedate, mixed $store): void {
2703
		$recurr = new Recurrence($store, $recurringItem);
2704
2705
		// Copy properties from meeting request
2706
		$exception_props = mapi_getprops($occurrenceItem);
2707
2708
		// Get recipient list from message and add it to exception attachment
2709
		$recips = $this->getMessageRecipients($occurrenceItem);
2710
2711
		if ($recurr->isException($basedate)) {
2712
			$recurr->modifyException($exception_props, $basedate, $recips, $occurrenceItem);
2713
		}
2714
		else {
2715
			$recurr->createException($exception_props, $basedate, false, $recips, $occurrenceItem);
2716
		}
2717
2718
		// Move the occurrenceItem to the waste basket
2719
		$wastebasket = $this->openDefaultWastebasket($this->openDefaultStore());
2720
		$sourcefolder = mapi_msgstore_openentry($store, $exception_props[PR_PARENT_ENTRYID]);
2721
		mapi_folder_copymessages($sourcefolder, [$exception_props[PR_ENTRYID]], $wastebasket, MESSAGE_MOVE);
2722
2723
		mapi_savechanges($recurringItem);
2724
	}
2725
2726
	/**
2727
	 * Function which submits meeting request based on arguments passed to it.
2728
	 *
2729
	 * @param resource  $message        MAPI_message whose meeting request is to be sent
2730
	 * @param bool      $cancel         if true send request, else send cancellation
2731
	 * @param mixed     $prefix         subject prefix
2732
	 * @param false|int $basedate       basedate for an occurrence
2733
	 * @param mixed     $recurObject    recurrence object of mr
2734
	 * @param bool      $copyExceptions When sending update mail for recurring item then we don't send exceptions in attachments
2735
	 * @param mixed     $modifiedRecips
2736
	 * @param mixed     $deletedRecips
2737
	 */
2738
	public function submitMeetingRequest($message, $cancel, $prefix, $basedate = false, $recurObject = false, $copyExceptions = true, $modifiedRecips = false, $deletedRecips = false): void {
2739
		$newmessageprops = $messageprops = mapi_getprops($this->message);
2740
		$new = $this->createOutgoingMessage();
2741
2742
		// Copy the entire message into the new meeting request message
2743
		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...
2744
			// messageprops contains properties of whole recurring series
2745
			// and newmessageprops contains properties of exception item
2746
			$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

2746
			$newmessageprops = mapi_getprops(/** @scrutinizer ignore-type */ $message);
Loading history...
2747
2748
			$basedateUtc = $basedate;
2749
			if ($recurObject instanceof BaseRecurrence && isset($recurObject->tz)) {
2750
				$basedateUtc = $recurObject->toGMT($recurObject->tz, $basedate);
2751
			}
2752
2753
			// Ensure that the correct basedate is set in the new message
2754
			$newmessageprops[$this->proptags['basedate']] = $basedateUtc;
2755
2756
			// Set isRecurring to false, because this is an exception
2757
			$newmessageprops[$this->proptags['recurring']] = false;
2758
2759
			// PidLidIsRecurring indicates a message associated with a recurring series object.
2760
			// It's true both for the series and an exception.
2761
			$newmessageprops[$this->proptags['meetingrecurring']] = true;
2762
2763
			// Recurrence data is not necessary for an exception
2764
			unset($newmessageprops[$this->proptags['recurrence_data']]);
2765
2766
			// set LID_IS_EXCEPTION to true
2767
			$newmessageprops[$this->proptags['is_exception']] = true;
2768
2769
			// Set to high importance
2770
			if ($cancel) {
2771
				$newmessageprops[PR_IMPORTANCE] = IMPORTANCE_HIGH;
2772
			}
2773
2774
			// Set startdate and enddate of exception
2775
			if ($cancel && $recurObject) {
2776
				$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

2776
				/** @scrutinizer ignore-call */ 
2777
    $newmessageprops[$this->proptags['startdate']] = $recurObject->getOccurrenceStart($basedate);
Loading history...
2777
				$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

2777
				/** @scrutinizer ignore-call */ 
2778
    $newmessageprops[$this->proptags['duedate']] = $recurObject->getOccurrenceEnd($basedate);
Loading history...
2778
			}
2779
2780
			// Set basedate in guid (0x3)
2781
			$newmessageprops[$this->proptags['goid']] = $this->setBasedateInGlobalID($messageprops[$this->proptags['goid2']], $basedateUtc, $recurObject instanceof BaseRecurrence ? $recurObject : null);
2782
			$newmessageprops[$this->proptags['goid2']] = $messageprops[$this->proptags['goid2']];
2783
			$newmessageprops[PR_OWNER_APPT_ID] = $messageprops[PR_OWNER_APPT_ID];
2784
2785
			// Get deleted recipiets from exception msg
2786
			$restriction = [
2787
				RES_AND,
2788
				[
2789
					[
2790
						RES_BITMASK,
2791
						[
2792
							ULTYPE => BMR_NEZ,
2793
							ULPROPTAG => PR_RECIPIENT_FLAGS,
2794
							ULMASK => recipExceptionalDeleted,
2795
						],
2796
					],
2797
					[
2798
						RES_BITMASK,
2799
						[
2800
							ULTYPE => BMR_EQZ,
2801
							ULPROPTAG => PR_RECIPIENT_FLAGS,
2802
							ULMASK => recipOrganizer,
2803
						],
2804
					],
2805
				],
2806
			];
2807
2808
			// In direct-booking mode, we don't need to send cancellations to resources
2809
			if ($this->enableDirectBooking) {
2810
				$restriction[1][] = [
2811
					RES_PROPERTY,
2812
					[
2813
						RELOP => RELOP_NE,	// Does not equal recipient type: MAPI_BCC (Resource)
2814
						ULPROPTAG => PR_RECIPIENT_TYPE,
2815
						VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
2816
					],
2817
				];
2818
			}
2819
2820
			$recipients = $this->getMessageRecipients($message, $restriction);
2821
2822
			$deletedRecips = array_merge($deletedRecips ?: [], $recipients);
2823
		}
2824
2825
		// Remove the PR_ICON_INDEX as it is not needed in the sent message.
2826
		$newmessageprops[PR_ICON_INDEX] = null;
2827
		$newmessageprops[PR_RESPONSE_REQUESTED] = true;
2828
2829
		// PR_START_DATE and PR_END_DATE will be used by outlook to show the position in the calendar
2830
		$newmessageprops[PR_START_DATE] = $newmessageprops[$this->proptags['startdate']];
2831
		$newmessageprops[PR_END_DATE] = $newmessageprops[$this->proptags['duedate']];
2832
2833
		// Set updatecounter/AppointmentSequenceNumber
2834
		// get the value of latest updatecounter for the whole series and use it
2835
		$newmessageprops[$this->proptags['updatecounter']] = $messageprops[$this->proptags['last_updatecounter']];
2836
2837
		$meetingTimeInfo = $this->getMeetingTimeInfo();
2838
2839
		if ($meetingTimeInfo) {
2840
			// Needs to unset PR_HTML and PR_RTF_COMPRESSED props
2841
			// because while canceling meeting requests with edit text
2842
			// will override the PR_BODY because body value is not consistent with
2843
			// PR_HTML and PR_RTF_COMPRESSED value so in this case PR_RTF_COMPRESSED will
2844
			// get priority which override the PR_BODY value.
2845
			unset($newmessageprops[PR_HTML], $newmessageprops[PR_RTF_COMPRESSED]);
2846
2847
			$newmessageprops[PR_BODY] = $meetingTimeInfo;
2848
		}
2849
2850
		// Send all recurrence info in mail, if this is a recurrence meeting.
2851
		if (isset($newmessageprops[$this->proptags['recurring']]) && $newmessageprops[$this->proptags['recurring']]) {
2852
			if (!empty($messageprops[$this->proptags['recurring_pattern']])) {
2853
				$newmessageprops[$this->proptags['recurring_pattern']] = $messageprops[$this->proptags['recurring_pattern']];
2854
			}
2855
			$newmessageprops[$this->proptags['recurrence_data']] = $messageprops[$this->proptags['recurrence_data']];
2856
			$newmessageprops[$this->proptags['timezone_data']] = $messageprops[$this->proptags['timezone_data']];
2857
			$newmessageprops[$this->proptags['timezone']] = $messageprops[$this->proptags['timezone']];
2858
2859
			if ($recurObject) {
2860
				$this->generateRecurDates($recurObject, $messageprops, $newmessageprops);
2861
			}
2862
		}
2863
2864
		if (isset($newmessageprops[$this->proptags['counter_proposal']])) {
2865
			unset($newmessageprops[$this->proptags['counter_proposal']]);
2866
		}
2867
2868
		// Prefix the subject if needed
2869
		if ($prefix && isset($newmessageprops[PR_SUBJECT])) {
2870
			$newmessageprops[PR_SUBJECT] = $prefix . $newmessageprops[PR_SUBJECT];
2871
		}
2872
2873
		if (isset($newmessageprops[$this->proptags['categories']]) &&
2874
			!empty($newmessageprops[$this->proptags['categories']])) {
2875
			unset($newmessageprops[$this->proptags['categories']]);
2876
		}
2877
		mapi_setprops($new, $newmessageprops);
2878
2879
		// Copy attachments
2880
		$this->replaceAttachments($message, $new, $copyExceptions);
2881
2882
		// Retrieve only those recipient who should receive this meeting request.
2883
		$stripResourcesRestriction = [
2884
			RES_AND,
2885
			[
2886
				[
2887
					RES_BITMASK,
2888
					[
2889
						ULTYPE => BMR_EQZ,
2890
						ULPROPTAG => PR_RECIPIENT_FLAGS,
2891
						ULMASK => recipExceptionalDeleted,
2892
					],
2893
				],
2894
				[
2895
					RES_BITMASK,
2896
					[
2897
						ULTYPE => BMR_EQZ,
2898
						ULPROPTAG => PR_RECIPIENT_FLAGS,
2899
						ULMASK => recipOrganizer,
2900
					],
2901
				],
2902
			],
2903
		];
2904
2905
		// In direct-booking mode, resources do not receive a meeting request
2906
		if ($this->enableDirectBooking) {
2907
			$stripResourcesRestriction[1][] = [
2908
				RES_PROPERTY,
2909
				[
2910
					RELOP => RELOP_NE,	// Does not equal recipient type: MAPI_BCC (Resource)
2911
					ULPROPTAG => PR_RECIPIENT_TYPE,
2912
					VALUE => [PR_RECIPIENT_TYPE => MAPI_BCC],
2913
				],
2914
			];
2915
		}
2916
2917
		// If no recipients were explicitly provided, we will send the update to all
2918
		// recipients from the meeting.
2919
		if ($modifiedRecips === false) {
2920
			$modifiedRecips = $this->getMessageRecipients($message, $stripResourcesRestriction);
2921
2922
			if ($basedate && empty($modifiedRecips)) {
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...
2923
				// Retrieve full list
2924
				$modifiedRecips = $this->getMessageRecipients($this->message);
2925
2926
				// Save recipients in exceptions
2927
				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

2927
				mapi_message_modifyrecipients(/** @scrutinizer ignore-type */ $message, MODRECIP_ADD, $modifiedRecips);
Loading history...
2928
2929
				// Now retrieve only those recipient who should receive this meeting request.
2930
				$modifiedRecips = $this->getMessageRecipients($this->message, $stripResourcesRestriction);
2931
			}
2932
		}
2933
2934
		// @TODO: handle nonAcceptingResources
2935
		/*
2936
		 * Add resource recipients that did not automatically accept the meeting request.
2937
		 * (note: meaning that they did not decline the meeting request)
2938
		 */ /*
2939
		for($i=0;$i<count($this->nonAcceptingResources);$i++){
2940
			$recipients[] = $this->nonAcceptingResources[$i];
2941
		}*/
2942
2943
		if (!empty($modifiedRecips)) {
2944
			// Strip out the sender/'owner' recipient
2945
			mapi_message_modifyrecipients($new, MODRECIP_ADD, $modifiedRecips);
2946
2947
			// Set some properties that are different in the sent request than
2948
			// in the item in our calendar
2949
2950
			// we should store busystatus value to intendedbusystatus property, because busystatus for outgoing meeting request
2951
			// should always be fbTentative
2952
			$newmessageprops[$this->proptags['intendedbusystatus']] = $newmessageprops[$this->proptags['busystatus']] ?? $messageprops[$this->proptags['busystatus']];
2953
			$newmessageprops[$this->proptags['busystatus']] = fbTentative; // The default status when not accepted
2954
			$newmessageprops[$this->proptags['responsestatus']] = olResponseNotResponded; // The recipient has not responded yet
2955
			$newmessageprops[$this->proptags['attendee_critical_change']] = time();
2956
			$newmessageprops[$this->proptags['owner_critical_change']] = time();
2957
			$newmessageprops[$this->proptags['meetingtype']] = mtgRequest;
2958
2959
			if ($cancel) {
2960
				$newmessageprops[PR_MESSAGE_CLASS] = 'IPM.Schedule.Meeting.Canceled';
2961
				$newmessageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // It's a cancel request
2962
				$newmessageprops[$this->proptags['busystatus']] = fbFree; // set the busy status as free
2963
			}
2964
			else {
2965
				$newmessageprops[PR_MESSAGE_CLASS] = 'IPM.Schedule.Meeting.Request';
2966
				$newmessageprops[$this->proptags['meetingstatus']] = olMeetingReceived; // The recipient is receiving the request
2967
			}
2968
2969
			mapi_setprops($new, $newmessageprops);
2970
			mapi_savechanges($new);
2971
2972
			// Submit message to non-resource recipients
2973
			mapi_message_submitmessage($new);
2974
		}
2975
2976
		// Search through the deleted recipients, and see if any of them is also
2977
		// listed as a recipient to whom we have sent an update. As we don't
2978
		// want to send a cancellation message to recipients who will also receive
2979
		// an meeting update, we have to filter those recipients out.
2980
		if ($deletedRecips) {
2981
			$tmp = [];
2982
2983
			foreach ($deletedRecips as $delRecip) {
2984
				$found = false;
2985
2986
				// Search if the deleted recipient can be found inside
2987
				// the updated recipients as well.
2988
				foreach ($modifiedRecips as $recip) {
2989
					if ($this->compareABEntryIDs($recip[PR_ENTRYID], $delRecip[PR_ENTRYID])) {
2990
						$found = true;
2991
						break;
2992
					}
2993
				}
2994
2995
				// If the recipient was not found, it truly is deleted,
2996
				// and we can safely send a cancellation message
2997
				if (!$found) {
2998
					$tmp[] = $delRecip;
2999
				}
3000
			}
3001
3002
			$deletedRecips = $tmp;
3003
		}
3004
3005
		// Send cancellation to deleted attendees
3006
		if ($deletedRecips) {
3007
			$new = $this->createOutgoingMessage();
3008
3009
			mapi_message_modifyrecipients($new, MODRECIP_ADD, $deletedRecips);
3010
3011
			$newmessageprops[PR_MESSAGE_CLASS] = 'IPM.Schedule.Meeting.Canceled';
3012
			$newmessageprops[$this->proptags['meetingstatus']] = olMeetingCanceled; // It's a cancel request
3013
			$newmessageprops[$this->proptags['busystatus']] = fbFree; // set the busy status as free
3014
			$newmessageprops[PR_IMPORTANCE] = IMPORTANCE_HIGH;	// HIGH Importance
3015
			if (isset($newmessageprops[PR_SUBJECT])) {
3016
				$newmessageprops[PR_SUBJECT] = _('Canceled') . ': ' . $newmessageprops[PR_SUBJECT];
3017
			}
3018
3019
			mapi_setprops($new, $newmessageprops);
3020
			mapi_savechanges($new);
3021
3022
			// Submit message to non-resource recipients
3023
			mapi_message_submitmessage($new);
3024
		}
3025
3026
		// Set properties on meeting object in calendar
3027
		// Set requestsent to 'true' (turns on 'tracking', etc)
3028
		$props = [];
3029
		$props[$this->proptags['meetingstatus']] = olMeeting;
3030
		$props[$this->proptags['responsestatus']] = olResponseOrganized;
3031
		// Only set the 'requestsent' property if it wasn't set previously yet,
3032
		// this ensures we will not accidentally set it from true to false.
3033
		if (!isset($messageprops[$this->proptags['requestsent']]) || $messageprops[$this->proptags['requestsent']] !== true) {
3034
			$props[$this->proptags['requestsent']] = !empty($modifiedRecips) || ($this->includesResources && !$this->errorSetResource);
3035
		}
3036
		$props[$this->proptags['attendee_critical_change']] = time();
3037
		$props[$this->proptags['owner_critical_change']] = time();
3038
		$props[$this->proptags['meetingtype']] = mtgRequest;
3039
		// save the new updatecounter to exception/recurring series/normal meeting
3040
		$props[$this->proptags['updatecounter']] = $newmessageprops[$this->proptags['updatecounter']];
3041
3042
		// PR_START_DATE and PR_END_DATE will be used by outlook to show the position in the calendar
3043
		$props[PR_START_DATE] = $messageprops[$this->proptags['startdate']];
3044
		$props[PR_END_DATE] = $messageprops[$this->proptags['duedate']];
3045
3046
		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

3046
		mapi_setprops(/** @scrutinizer ignore-type */ $message, $props);
Loading history...
3047
3048
		// saving of these properties on calendar item should be handled by caller function
3049
		// based on sending meeting request was successful or not
3050
	}
3051
3052
	/**
3053
	 * OL2007 uses these 4 properties to specify occurrence that should be updated.
3054
	 * ical generates RECURRENCE-ID property based on exception's basedate (PidLidExceptionReplaceTime),
3055
	 * but OL07 doesn't send this property, so ical will generate RECURRENCE-ID property based on date
3056
	 * from GlobalObjId and time from StartRecurTime property, so we are sending basedate property and
3057
	 * also additionally we are sending these properties.
3058
	 * Ref: MS-OXCICAL 2.2.1.20.20 Property: RECURRENCE-ID.
3059
	 *
3060
	 * @param object $recurObject     instance of recurrence class for this message
3061
	 * @param array  $messageprops    properties of meeting object that is going to be sent
3062
	 * @param array  $newmessageprops properties of meeting request/response that is going to be sent
3063
	 */
3064
	public function generateRecurDates(object $recurObject, array $messageprops, array &$newmessageprops): void {
3065
		if ($messageprops[$this->proptags['startdate']] && $messageprops[$this->proptags['duedate']]) {
3066
			$startDate = date('Y:n:j:G:i:s', $recurObject->fromGMT($recurObject->tz, $messageprops[$this->proptags['startdate']]));
3067
			$endDate = date('Y:n:j:G:i:s', $recurObject->fromGMT($recurObject->tz, $messageprops[$this->proptags['duedate']]));
3068
3069
			$startDate = explode(':', $startDate);
3070
			$endDate = explode(':', $endDate);
3071
3072
			// [0] => year, [1] => month, [2] => day, [3] => hour, [4] => minutes, [5] => seconds
3073
			// RecurStartDate = year * 512 + month_number * 32 + day_number
3074
			$newmessageprops[$this->proptags['start_recur_date']] = (((int) $startDate[0]) * 512) + (((int) $startDate[1]) * 32) + ((int) $startDate[2]);
3075
			// RecurStartTime = hour * 4096 + minutes * 64 + seconds
3076
			$newmessageprops[$this->proptags['start_recur_time']] = (((int) $startDate[3]) * 4096) + (((int) $startDate[4]) * 64) + ((int) $startDate[5]);
3077
3078
			$newmessageprops[$this->proptags['end_recur_date']] = (((int) $endDate[0]) * 512) + (((int) $endDate[1]) * 32) + ((int) $endDate[2]);
3079
			$newmessageprops[$this->proptags['end_recur_time']] = (((int) $endDate[3]) * 4096) + (((int) $endDate[4]) * 64) + ((int) $endDate[5]);
3080
		}
3081
	}
3082
3083
	/**
3084
	 * Function will create a new outgoing message that will be used to send meeting mail.
3085
	 *
3086
	 * @param mixed $store (optional) store that is used when creating response, if delegate is creating outgoing mail
3087
	 *                     then this would point to delegate store
3088
	 *
3089
	 * @return resource outgoing mail that is created and can be used for sending it
3090
	 */
3091
	public function createOutgoingMessage(mixed $store = false): mixed {
3092
		// get logged in user's store that will be used to send mail, for delegate this will be
3093
		// delegate store
3094
		$userStore = $this->openDefaultStore();
3095
3096
		$sentprops = [];
3097
		$outbox = $this->openDefaultOutbox($userStore);
3098
3099
		$outgoing = mapi_folder_createmessage($outbox);
3100
3101
		// check if $store is set and it is not equal to $defaultStore (means its the delegation case)
3102
		if ($store !== false) {
3103
			$storeProps = mapi_getprops($store, [PR_ENTRYID]);
3104
			$userStoreProps = mapi_getprops($userStore, [PR_ENTRYID]);
3105
3106
			if (!compareEntryIds($storeProps[PR_ENTRYID], $userStoreProps[PR_ENTRYID])) {
3107
				// get the delegator properties and set it into outgoing mail
3108
				$delegatorDetails = $this->getOwnerAddress($store, false);
3109
				$this->setAddressProperties($sentprops, $delegatorDetails, 'SENT_REPRESENTING');
0 ignored issues
show
Bug introduced by
It seems like $delegatorDetails can also be of type false; however, parameter $addrInfo of Meetingrequest::setAddressProperties() does only seem to accept array, 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

3109
				$this->setAddressProperties($sentprops, /** @scrutinizer ignore-type */ $delegatorDetails, 'SENT_REPRESENTING');
Loading history...
3110
3111
				// get the delegate properties and set it into outgoing mail
3112
				$delegateDetails = $this->getOwnerAddress($userStore, false);
3113
				$this->setAddressProperties($sentprops, $delegateDetails, 'SENDER');
3114
			}
3115
		}
3116
		else {
3117
			// normal user is sending mail, so both set of properties will be same
3118
			$userDetails = $this->getOwnerAddress($userStore);
3119
			$this->setAddressProperties($sentprops, $userDetails, 'SENT_REPRESENTING');
3120
			$this->setAddressProperties($sentprops, $userDetails, 'SENDER');
3121
		}
3122
3123
		$sentprops[PR_SENTMAIL_ENTRYID] = $this->getDefaultSentmailEntryID($userStore);
3124
3125
		mapi_setprops($outgoing, $sentprops);
3126
3127
		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...
3128
	}
3129
3130
	/**
3131
	 * Function which checks that meeting in attendee's calendar is already updated
3132
	 * and we are checking an old meeting request. This function also will update property
3133
	 * meetingtype to indicate that its out of date meeting request.
3134
	 *
3135
	 * @return bool true if meeting request is outofdate else false if it is new
3136
	 */
3137
	public function isMeetingOutOfDate(): bool {
3138
		$result = false;
3139
3140
		$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']]);
3141
3142
		if (!$this->isMeetingRequest($props[PR_MESSAGE_CLASS])) {
3143
			return $result;
3144
		}
3145
3146
		if (isset($props[$this->proptags['meetingtype']]) && ($props[$this->proptags['meetingtype']] & mtgOutOfDate) == mtgOutOfDate) {
3147
			return true;
3148
		}
3149
3150
		// get the basedate to check for exception
3151
		$basedate = $this->getBasedateFromGlobalID($props[$this->proptags['goid']]);
3152
3153
		$calendarItem = $this->getCorrespondentCalendarItem(true);
3154
3155
		// if basedate is provided and we could not find the item then it could be that we are checking
3156
		// an exception so get the exception and check it
3157
		if ($basedate !== false && $calendarItem !== false) {
3158
			$exception = $this->getExceptionItem($calendarItem, $basedate);
3159
3160
			if ($exception !== false) {
3161
				// we are able to find the exception compare with it
3162
				$calendarItem = $exception;
3163
			}
3164
			// we are not able to find exception, could mean that a significant change has occurred on series
3165
			// and it deleted all exceptions, so compare with series
3166
			// $calendarItem already contains reference to series
3167
		}
3168
3169
		if ($calendarItem !== false) {
3170
			$calendarItemProps = mapi_getprops($calendarItem, [
3171
				$this->proptags['owner_critical_change'],
3172
				$this->proptags['updatecounter'],
3173
			]);
3174
3175
			$updateCounter = (isset($calendarItemProps[$this->proptags['updatecounter']]) && $props[$this->proptags['updatecounter']] < $calendarItemProps[$this->proptags['updatecounter']]);
3176
3177
			$criticalChange = (isset($calendarItemProps[$this->proptags['owner_critical_change']]) && $props[$this->proptags['owner_critical_change']] < $calendarItemProps[$this->proptags['owner_critical_change']]);
3178
3179
			if ($updateCounter || $criticalChange) {
3180
				// meeting request is out of date, set properties to indicate this
3181
				mapi_setprops($this->message, [$this->proptags['meetingtype'] => mtgOutOfDate, PR_ICON_INDEX => 1033]);
3182
				mapi_savechanges($this->message);
3183
3184
				$result = true;
3185
			}
3186
		}
3187
3188
		return $result;
3189
	}
3190
3191
	/**
3192
	 * Function which checks that if we have received a meeting response for an updated meeting in organizer's calendar.
3193
	 *
3194
	 * @param false|int $basedate basedate of the exception if we want to compare with exception
3195
	 *
3196
	 * @return bool true if meeting request is updated later
3197
	 */
3198
	public function isMeetingUpdated(false|int $basedate = false): bool {
3199
		$result = false;
3200
3201
		$props = mapi_getprops($this->message, [PR_MESSAGE_CLASS, $this->proptags['updatecounter']]);
3202
3203
		if (!$this->isMeetingRequestResponse($props[PR_MESSAGE_CLASS])) {
3204
			return $result;
3205
		}
3206
3207
		$calendarItem = $this->getCorrespondentCalendarItem(true);
3208
3209
		if ($calendarItem !== false) {
3210
			// basedate is provided so open exception
3211
			if ($basedate !== false) {
3212
				$exception = $this->getExceptionItem($calendarItem, $basedate);
3213
3214
				if ($exception !== false) {
3215
					// we are able to find the exception compare with it
3216
					$calendarItem = $exception;
3217
				}
3218
				// we are not able to find exception, could mean that a significant change has occurred on series
3219
				// and it deleted all exceptions, so compare with series
3220
				// $calendarItem already contains reference to series
3221
			}
3222
3223
			if ($calendarItem !== false) {
3224
				$calendarItemProps = mapi_getprops($calendarItem, [$this->proptags['updatecounter']]);
3225
3226
				/*
3227
				 * if(message_counter < appointment_counter) meeting object is newer then meeting response (meeting is updated)
3228
				 * if(message_counter >= appointment_counter) meeting is not updated, do normal processing
3229
				 */
3230
				if (isset($calendarItemProps[$this->proptags['updatecounter']], $props[$this->proptags['updatecounter']])) {
3231
					$result = $props[$this->proptags['updatecounter']] < $calendarItemProps[$this->proptags['updatecounter']];
3232
				}
3233
			}
3234
		}
3235
3236
		return $result;
3237
	}
3238
3239
	/**
3240
	 * Checks if there has been any significant changes on appointment/meeting item.
3241
	 * Significant changes be:
3242
	 * 1) startdate has been changed
3243
	 * 2) duedate has been changed OR
3244
	 * 3) recurrence pattern has been created, modified or removed.
3245
	 *
3246
	 * @param bool $isRecurrenceChanged for change in recurrence pattern.
3247
	 *                                  true means Recurrence pattern has been changed,
3248
	 *                                  so clear all attendees response
3249
	 */
3250
	public function checkSignificantChanges(array $oldProps, mixed $basedate, bool $isRecurrenceChanged = false): void {
3251
		$message = null;
3252
		$attach = null;
3253
3254
		// If basedate is specified then we need to open exception message to clear recipient responses
3255
		if ($basedate) {
3256
			$recurrence = new Recurrence($this->store, $this->message);
3257
			if ($recurrence->isException($basedate)) {
3258
				$attach = $recurrence->getExceptionAttachment($basedate);
3259
				if ($attach) {
3260
					$message = mapi_attach_openobj($attach, MAPI_MODIFY);
3261
				}
3262
			}
3263
		}
3264
		else {
3265
			// use normal message or recurring series message
3266
			$message = $this->message;
3267
		}
3268
3269
		if (!$message) {
3270
			return;
3271
		}
3272
3273
		$newProps = mapi_getprops($message, [$this->proptags['startdate'], $this->proptags['duedate'], $this->proptags['updatecounter']]);
3274
3275
		// Check whether message is updated or not.
3276
		if (isset($newProps[$this->proptags['updatecounter']]) && $newProps[$this->proptags['updatecounter']] == 0) {
3277
			return;
3278
		}
3279
3280
		if (($newProps[$this->proptags['startdate']] != $oldProps[$this->proptags['startdate']]) ||
3281
				($newProps[$this->proptags['duedate']] != $oldProps[$this->proptags['duedate']]) ||
3282
				$isRecurrenceChanged) {
3283
			$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

3283
			$this->clearRecipientResponse(/** @scrutinizer ignore-type */ $message);
Loading history...
3284
3285
			mapi_setprops($message, [$this->proptags['owner_critical_change'] => time()]);
3286
3287
			mapi_savechanges($message);
3288
			if ($attach) { // Also save attachment Object.
3289
				mapi_savechanges($attach);
3290
			}
3291
		}
3292
	}
3293
3294
	/**
3295
	 * Clear responses of all attendees who have replied in past.
3296
	 *
3297
	 * @param resource $message on which responses should be cleared
3298
	 */
3299
	public function clearRecipientResponse($message): void {
3300
		$recipsRows = $this->getMessageRecipients($message);
3301
		for ($i = 0, $recipsCnt = count($recipsRows); $i < $recipsCnt; ++$i) {
3302
			// Clear track status for everyone in the recipients table
3303
			$recipsRows[$i][PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
3304
		}
3305
		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

3305
		mapi_message_modifyrecipients(/** @scrutinizer ignore-type */ $message, MODRECIP_MODIFY, $recipsRows);
Loading history...
3306
	}
3307
3308
	/**
3309
	 * Function returns correspondent calendar item attached with the meeting request/response/cancellation.
3310
	 * This will only check for actual MAPIMessages in calendar folder, so if a meeting request is
3311
	 * for exception then this function will return recurring series for that meeting request
3312
	 * after that you need to use getExceptionItem function to get exception item that will be
3313
	 * fetched from the attachment table of recurring series MAPIMessage.
3314
	 *
3315
	 * @param bool $open boolean to indicate the function should return entryid or MAPIMessage. Defaults to true.
3316
	 *
3317
	 * @return bool|resource resource of calendar item
3318
	 */
3319
	public function getCorrespondentCalendarItem(bool $open = true): mixed {
3320
		$props = mapi_getprops($this->message, [PR_MESSAGE_CLASS, $this->proptags['goid'], $this->proptags['goid2'], PR_RCVD_REPRESENTING_ENTRYID]);
3321
3322
		if (!$this->isMeetingRequest($props[PR_MESSAGE_CLASS]) && !$this->isMeetingRequestResponse($props[PR_MESSAGE_CLASS]) && !$this->isMeetingCancellation($props[PR_MESSAGE_CLASS])) {
3323
			// can work only with meeting requests/responses/cancellations
3324
			return false;
3325
		}
3326
3327
		// there is no goid - no items can be found - aborting
3328
		if (empty($props[$this->proptags['goid']])) {
3329
			return false;
3330
		}
3331
		$globalId = $props[$this->proptags['goid']];
3332
3333
		['store' => $store, 'calFolder' => $calFolder] = $this->resolveDelegateStoreAndCalendar($props);
3334
3335
		$basedate = $this->getBasedateFromGlobalID($globalId);
3336
3337
		/**
3338
		 * First search for any appointments which correspond to the $globalId,
3339
		 * this can be the entire series (if the Meeting Request refers to the
3340
		 * entire series), or an particular Occurrence (if the meeting Request
3341
		 * contains a basedate).
3342
		 *
3343
		 * If we cannot find a corresponding item, and the $globalId contains
3344
		 * a $basedate, it might imply that a new exception will have to be
3345
		 * created for a series which is present in the calendar, we can look
3346
		 * that one up by searching for the $cleanGlobalId.
3347
		 */
3348
		$entryids = $this->findCalendarItems($globalId, $calFolder);
3349
		if ($basedate !== false && empty($entryids)) {
3350
			// only search if a goid2 is available
3351
			if (!empty($props[$this->proptags['goid2']])) {
3352
				$cleanGlobalId = $props[$this->proptags['goid2']];
3353
				$entryids = $this->findCalendarItems($cleanGlobalId, $calFolder, true);
3354
			}
3355
		}
3356
3357
		// there should be only one item returned
3358
		if (!empty($entryids) && count($entryids) === 1) {
3359
			// return only entryid
3360
			if ($open === false) {
3361
				return $entryids[0];
3362
			}
3363
3364
			// open calendar item and return it
3365
			if ($store) {
3366
				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...
3367
			}
3368
		}
3369
3370
		// no items found in calendar
3371
		return false;
3372
	}
3373
3374
	/**
3375
	 * Function returns exception item based on the basedate passed.
3376
	 *
3377
	 * @param mixed     $recurringMessage Resource of Recurring meeting from calendar
3378
	 * @param false|int $basedate         basedate of exception that needs to be returned
3379
	 * @param mixed     $store            store that contains the recurring calendar item
3380
	 *
3381
	 * @return false|resource resource of exception item
3382
	 */
3383
	public function getExceptionItem(mixed $recurringMessage, mixed $basedate, mixed $store = false): mixed {
3384
		$occurItem = false;
3385
3386
		$props = mapi_getprops($this->message, [PR_RCVD_REPRESENTING_ENTRYID, $this->proptags['recurring']]);
3387
3388
		// check if the passed item is recurring series
3389
		if (isset($props[$this->proptags['recurring']]) && $props[$this->proptags['recurring']] !== false) {
3390
			return false;
3391
		}
3392
3393
		if ($store === false) {
3394
			$store = $this->store;
3395
			// If Delegate is processing Meeting Request/Response for Delegator then retrieve Delegator's store and calendar.
3396
			if (isset($props[PR_RCVD_REPRESENTING_ENTRYID])) {
3397
				$delegatorStore = $this->getDelegatorStore($props[PR_RCVD_REPRESENTING_ENTRYID]);
3398
				if (!empty($delegatorStore['store'])) {
3399
					$store = $delegatorStore['store'];
3400
				}
3401
			}
3402
		}
3403
3404
		$recurr = new Recurrence($store, $recurringMessage);
3405
		$attach = $recurr->getExceptionAttachment($basedate);
3406
		if ($attach) {
3407
			$occurItem = mapi_attach_openobj($attach);
3408
		}
3409
3410
		return $occurItem;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $occurItem also could return the type resource which is incompatible with the documented return type false|resource.
Loading history...
3411
	}
3412
3413
	/**
3414
	 * Function which checks whether received meeting request is either conflicting with other appointments or not.
3415
	 *
3416
	 * @param false|resource $message
3417
	 * @param false|resource $userStore
3418
	 * @param mixed          $calFolder calendar folder for conflict checking
3419
	 *
3420
	 * @psalm-return bool|int<1, max>
3421
	 */
3422
	public function isMeetingConflicting(mixed $message = false, mixed $userStore = false, mixed $calFolder = false): bool|int {
3423
		$returnValue = false;
3424
		$noOfInstances = 0;
3425
3426
		if ($message === false) {
3427
			$message = $this->message;
3428
		}
3429
3430
		$messageProps = mapi_getprops(
3431
			$message,
3432
			[
3433
				PR_MESSAGE_CLASS,
3434
				$this->proptags['goid'],
3435
				$this->proptags['goid2'],
3436
				$this->proptags['startdate'],
3437
				$this->proptags['duedate'],
3438
				$this->proptags['recurring'],
3439
				$this->proptags['clipstart'],
3440
				$this->proptags['clipend'],
3441
				PR_RCVD_REPRESENTING_ENTRYID,
3442
				$this->proptags['basedate'],
3443
				PR_RCVD_REPRESENTING_NAME,
3444
			]
3445
		);
3446
3447
		if ($userStore === false) {
3448
			$userStore = $this->store;
3449
3450
			// check if delegate is processing the response
3451
			if (isset($messageProps[PR_RCVD_REPRESENTING_ENTRYID])) {
3452
				$delegatorStore = $this->getDelegatorStore($messageProps[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
3453
3454
				if (!empty($delegatorStore['store'])) {
3455
					$userStore = $delegatorStore['store'];
3456
				}
3457
				if (!empty($delegatorStore[PR_IPM_APPOINTMENT_ENTRYID])) {
3458
					$calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
3459
				}
3460
			}
3461
		}
3462
3463
		if ($calFolder === false) {
3464
			$calFolder = $this->openDefaultCalendar($userStore);
3465
		}
3466
3467
		if ($calFolder) {
3468
			// Meeting request is recurring, so get all occurrence and check for each occurrence whether it conflicts with other appointments in Calendar.
3469
			if (isset($messageProps[$this->proptags['recurring']]) && $messageProps[$this->proptags['recurring']] === true) {
3470
				// Apply recurrence class and retrieve all occurrences(max: 30 occurrence because recurrence can also be set as 'no end date')
3471
				$recurr = new Recurrence($userStore, $message);
3472
				$items = $recurr->getItems($messageProps[$this->proptags['clipstart']], $messageProps[$this->proptags['clipend']] * (24 * 24 * 60), 30);
3473
3474
				foreach ($items as $item) {
3475
					// Get all items in the timeframe that we want to book, and get the goid and busystatus for each item
3476
					$calendarItems = $recurr->getCalendarItems($userStore, $calFolder, $item[$this->proptags['startdate']], $item[$this->proptags['duedate']], [$this->proptags['goid'], $this->proptags['busystatus']]);
3477
3478
					foreach ($calendarItems as $calendarItem) {
3479
						if ($calendarItem[$this->proptags['busystatus']] !== fbFree) {
3480
							/*
3481
							 * Only meeting requests have globalID, normal appointments do not have globalID
3482
							 * so if any normal appointment if found then it is assumed to be conflict.
3483
							 */
3484
							if (isset($calendarItem[$this->proptags['goid']])) {
3485
								if ($calendarItem[$this->proptags['goid']] !== $messageProps[$this->proptags['goid']]) {
3486
									++$noOfInstances;
3487
									break;
3488
								}
3489
							}
3490
							else {
3491
								++$noOfInstances;
3492
								break;
3493
							}
3494
						}
3495
					}
3496
				}
3497
3498
				if ($noOfInstances > 0) {
3499
					$returnValue = $noOfInstances;
3500
				}
3501
			}
3502
			else {
3503
				// Get all items in the timeframe that we want to book, and get the goid and busystatus for each item
3504
				$items = getCalendarItems($userStore, $calFolder, $messageProps[$this->proptags['startdate']], $messageProps[$this->proptags['duedate']], [$this->proptags['goid'], $this->proptags['busystatus']]);
3505
3506
				if (isset($messageProps[$this->proptags['basedate']]) && !empty($messageProps[$this->proptags['basedate']])) {
3507
					$basedate = $messageProps[$this->proptags['basedate']];
3508
					// Get the goid2 from recurring MR which further used to
3509
					// check the resource conflicts item.
3510
					$recurrItemProps = mapi_getprops($this->message, [$this->proptags['goid2']]);
3511
					$recurrenceHelper = new Recurrence($this->openDefaultStore(), $this->message);
3512
					$messageProps[$this->proptags['goid']] = $this->setBasedateInGlobalID($recurrItemProps[$this->proptags['goid2']], $basedate, $recurrenceHelper);
3513
					$messageProps[$this->proptags['goid2']] = $recurrItemProps[$this->proptags['goid2']];
3514
				}
3515
3516
				foreach ($items as $item) {
3517
					if ($item[$this->proptags['busystatus']] !== fbFree) {
3518
						if (isset($item[$this->proptags['goid']])) {
3519
							if (($item[$this->proptags['goid']] !== $messageProps[$this->proptags['goid']]) &&
3520
								($item[$this->proptags['goid']] !== $messageProps[$this->proptags['goid2']])) {
3521
								$returnValue = true;
3522
								break;
3523
							}
3524
						}
3525
						else {
3526
							$returnValue = true;
3527
							break;
3528
						}
3529
					}
3530
				}
3531
			}
3532
		}
3533
3534
		return $returnValue;
3535
	}
3536
3537
	/**
3538
	 * Function which adds organizer to recipient list which is passed.
3539
	 * This function also checks if it has organizer.
3540
	 *
3541
	 * @param array $messageProps message properties
3542
	 * @param array $recipients   recipients list of message
3543
	 */
3544
	public function addDelegator(array $messageProps, array &$recipients): void {
3545
		$hasDelegator = false;
3546
		// Check if meeting already has an organizer.
3547
		foreach ($recipients as $key => $recipient) {
3548
			if (isset($messageProps[PR_RCVD_REPRESENTING_EMAIL_ADDRESS]) && $recipient[PR_EMAIL_ADDRESS] == $messageProps[PR_RCVD_REPRESENTING_EMAIL_ADDRESS]) {
3549
				$hasDelegator = true;
3550
			}
3551
		}
3552
3553
		if (!$hasDelegator) {
3554
			// Create delegator.
3555
			$delegator = [];
3556
			$delegator[PR_ENTRYID] = $messageProps[PR_RCVD_REPRESENTING_ENTRYID];
3557
			$delegator[PR_DISPLAY_NAME] = $messageProps[PR_RCVD_REPRESENTING_NAME];
3558
			$delegator[PR_EMAIL_ADDRESS] = $messageProps[PR_RCVD_REPRESENTING_EMAIL_ADDRESS];
3559
			$delegator[PR_RECIPIENT_TYPE] = MAPI_TO;
3560
			$delegator[PR_RECIPIENT_DISPLAY_NAME] = $messageProps[PR_RCVD_REPRESENTING_NAME];
3561
			$delegator[PR_ADDRTYPE] = empty($messageProps[PR_RCVD_REPRESENTING_ADDRTYPE]) ? 'SMTP' : $messageProps[PR_RCVD_REPRESENTING_ADDRTYPE];
3562
			$delegator[PR_RECIPIENT_TRACKSTATUS] = olRecipientTrackStatusNone;
3563
			$delegator[PR_RECIPIENT_FLAGS] = recipSendable;
3564
			$delegator[PR_SEARCH_KEY] = $messageProps[PR_RCVD_REPRESENTING_SEARCH_KEY];
3565
3566
			// Add organizer to recipients list.
3567
			array_unshift($recipients, $delegator);
3568
		}
3569
	}
3570
3571
	/**
3572
	 * Function will return delegator's store and calendar folder for processing meetings.
3573
	 *
3574
	 * @param string $receivedRepresentingEntryId entryid of the delegator user
3575
	 * @param array  $foldersToOpen               contains list of folder types that should be returned in result
3576
	 *
3577
	 * @return resource[] contains store of the delegator and resource of folders if $foldersToOpen is not empty
3578
	 *
3579
	 * @psalm-return array<resource>
3580
	 */
3581
	public function getDelegatorStore($receivedRepresentingEntryId, $foldersToOpen = []): array {
3582
		$returnData = [];
3583
3584
		$delegatorStore = $this->openCustomUserStore($receivedRepresentingEntryId);
3585
		$returnData['store'] = $delegatorStore;
3586
3587
		if (!empty($foldersToOpen)) {
3588
			for ($index = 0, $len = count($foldersToOpen); $index < $len; ++$index) {
3589
				$folderType = $foldersToOpen[$index];
3590
3591
				// first try with default folders
3592
				$folder = $this->openDefaultFolder($folderType, $delegatorStore);
3593
3594
				// if folder not found then try with base folders
3595
				if ($folder === false) {
3596
					$folder = $this->openBaseFolder($folderType, $delegatorStore);
3597
				}
3598
3599
				if ($folder === false) {
3600
					// we are still not able to get the folder so give up
3601
					continue;
3602
				}
3603
3604
				$returnData[$folderType] = $folder;
3605
			}
3606
		}
3607
3608
		return $returnData;
3609
	}
3610
3611
	/**
3612
	 * Function returns extra info about meeting timing along with message body
3613
	 * which will be included in body while sending meeting request/response.
3614
	 *
3615
	 * @return false|string $meetingTimeInfo info about meeting timing along with message body
3616
	 */
3617
	public function getMeetingTimeInfo(): false|string {
3618
		return $this->meetingTimeInfo;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->meetingTimeInfo could return the type boolean which is incompatible with the type-hinted return false|string. Consider adding an additional type-check to rule them out.
Loading history...
3619
	}
3620
3621
	/**
3622
	 * Function sets extra info about meeting timing along with message body
3623
	 * which will be included in body while sending meeting request/response.
3624
	 *
3625
	 * @param string $meetingTimeInfo info about meeting timing along with message body
3626
	 */
3627
	public function setMeetingTimeInfo(false|string $meetingTimeInfo): void {
3628
		$this->meetingTimeInfo = $meetingTimeInfo;
3629
	}
3630
3631
	/**
3632
	 * Helper function which is use to get local categories of all occurrence.
3633
	 *
3634
	 * @param mixed $calendarItem meeting request item
3635
	 * @param mixed $store        store containing calendar folder
3636
	 * @param mixed $calFolder    calendar folder
3637
	 *
3638
	 * @return array $localCategories which contain array of basedate along with categories
3639
	 */
3640
	public function getLocalCategories(mixed $calendarItem, mixed $store, mixed $calFolder): array {
3641
		$calendarItemProps = mapi_getprops($calendarItem);
3642
		$recurrence = new Recurrence($store, $calendarItem);
3643
3644
		// Retrieve all occurrences(max: 30 occurrence because recurrence can also be set as 'no end date')
3645
		$items = $recurrence->getItems($calendarItemProps[$this->proptags['clipstart']], $calendarItemProps[$this->proptags['clipend']] * (24 * 24 * 60), 30);
3646
		$localCategories = [];
3647
3648
		foreach ($items as $item) {
3649
			$recurrenceItems = $recurrence->getCalendarItems($store, $calFolder, $item[$this->proptags['startdate']], $item[$this->proptags['duedate']], [$this->proptags['goid'], $this->proptags['busystatus'], $this->proptags['categories']]);
3650
			foreach ($recurrenceItems as $recurrenceItem) {
3651
				// Check if occurrence is exception then get the local categories of that occurrence.
3652
				if (isset($recurrenceItem[$this->proptags['goid']]) && $recurrenceItem[$this->proptags['goid']] == $calendarItemProps[$this->proptags['goid']]) {
3653
					$exceptionAttach = $recurrence->getExceptionAttachment($recurrenceItem['basedate']);
3654
3655
					if ($exceptionAttach) {
3656
						$exception = mapi_attach_openobj($exceptionAttach, 0);
3657
						$exceptionProps = mapi_getprops($exception, [$this->proptags['categories']]);
3658
						if (isset($exceptionProps[$this->proptags['categories']])) {
3659
							$localCategories[$recurrenceItem['basedate']] = $exceptionProps[$this->proptags['categories']];
3660
						}
3661
					}
3662
				}
3663
			}
3664
		}
3665
3666
		return $localCategories;
3667
	}
3668
3669
	/**
3670
	 * Helper function which is use to apply local categories on respective occurrences.
3671
	 *
3672
	 * @param mixed $calendarItem    meeting request item
3673
	 * @param mixed $store           store containing calendar folder
3674
	 * @param array $localCategories array contains basedate and array of categories
3675
	 */
3676
	public function applyLocalCategories(mixed $calendarItem, mixed $store, array $localCategories): void {
3677
		$calendarItemProps = mapi_getprops($calendarItem, [PR_PARENT_ENTRYID, PR_ENTRYID]);
3678
		$message = mapi_msgstore_openentry($store, $calendarItemProps[PR_ENTRYID]);
3679
		$recurrence = new Recurrence($store, $message);
3680
3681
		// Check for all occurrence if it is exception then modify the exception by setting up categories,
3682
		// Otherwise create new exception with categories.
3683
		foreach ($localCategories as $key => $value) {
3684
			if ($recurrence->isException($key)) {
3685
				$recurrence->modifyException([$this->proptags['categories'] => $value], $key);
3686
			}
3687
			else {
3688
				$recurrence->createException([$this->proptags['categories'] => $value], $key, false);
3689
			}
3690
			mapi_savechanges($message);
3691
		}
3692
	}
3693
3694
	/**
3695
	 * Check if a message is from a delegate (received representing someone else).
3696
	 *
3697
	 * @param array $messageprops Message properties
3698
	 *
3699
	 * @return bool True if message is from delegate
3700
	 */
3701
	private function isMessageFromDelegate(array $messageprops): bool {
3702
		return isset($messageprops[PR_RCVD_REPRESENTING_NAME]);
3703
	}
3704
3705
	/**
3706
	 * Calculate the busy status for an accepted meeting based on tentative flag and intended status.
3707
	 *
3708
	 * @param bool  $tentative Whether acceptance is tentative
3709
	 * @param array $props     Message properties containing intended busy status
3710
	 *
3711
	 * @return int The calculated busy status
3712
	 */
3713
	private function calculateBusyStatus(bool $tentative, array $props): int {
3714
		if (isset($props[$this->proptags['intendedbusystatus']])) {
3715
			if ($tentative && $props[$this->proptags['intendedbusystatus']] !== fbFree) {
3716
				return fbTentative;
3717
			}
3718
3719
			return $props[$this->proptags['intendedbusystatus']];
3720
		}
3721
3722
		return $tentative ? fbTentative : fbBusy;
3723
	}
3724
3725
	/**
3726
	 * Set reply time and name properties when user responds to a meeting.
3727
	 *
3728
	 * @param array $props Properties array to update (passed by reference)
3729
	 */
3730
	private function setReplyTimeAndName(array &$props): void {
3731
		$addrInfo = $this->getOwnerAddress($this->store);
3732
3733
		// if user has responded then set replytime and name
3734
		$props[$this->proptags['replytime']] = time();
3735
		if (!empty($addrInfo)) {
3736
			// @FIXME conditionally set this property only for delegation case
3737
			$props[$this->proptags['apptreplyname']] = $addrInfo[0];
3738
		}
3739
	}
3740
3741
	/**
3742
	 * Correct the flagdueby (reminder time) property.
3743
	 * Some clients (mainly OL) can generate wrong flagdueby time, so regenerate it.
3744
	 *
3745
	 * @param array $props Properties array to update (passed by reference)
3746
	 */
3747
	private function correctReminderTime(array &$props): void {
3748
		if (isset($props[$this->proptags['reminderminutes']])) {
3749
			$props[$this->proptags['flagdueby']] = $props[$this->proptags['startdate']] - ($props[$this->proptags['reminderminutes']] * 60);
3750
		}
3751
	}
3752
3753
	/**
3754
	 * Determine the appropriate response status based on user action and tentative flag.
3755
	 *
3756
	 * @param bool $userAction Whether this is a user-initiated action
3757
	 * @param bool $tentative  Whether the response is tentative
3758
	 *
3759
	 * @return int The response status constant
3760
	 */
3761
	private function determineResponseStatus(bool $userAction, bool $tentative): int {
3762
		if (!$userAction) {
3763
			return olResponseNotResponded;
3764
		}
3765
3766
		return $tentative ? olResponseTentative : olResponseAccepted;
3767
	}
3768
3769
	/**
3770
	 * Sets sender or representing address properties in message props.
3771
	 *
3772
	 * @param array  $messageprops reference to message properties array
3773
	 * @param array  $addrInfo     address info array from getOwnerAddress
3774
	 * @param string $prefix       property prefix ('SENDER' or 'SENT_REPRESENTING')
3775
	 */
3776
	private function setAddressProperties(array &$messageprops, array $addrInfo, string $prefix): void {
3777
		if (empty($addrInfo)) {
3778
			return;
3779
		}
3780
3781
		[$ownername, $owneremailaddr, $owneraddrtype, $ownerentryid, $ownersearchkey] = $addrInfo;
3782
3783
		$messageprops[constant("PR_{$prefix}_EMAIL_ADDRESS")] = $owneremailaddr;
3784
		$messageprops[constant("PR_{$prefix}_NAME")] = $ownername;
3785
		$messageprops[constant("PR_{$prefix}_ADDRTYPE")] = $owneraddrtype;
3786
		$messageprops[constant("PR_{$prefix}_ENTRYID")] = $ownerentryid;
3787
		$messageprops[constant("PR_{$prefix}_SEARCH_KEY")] = $ownersearchkey;
3788
	}
3789
3790
	/**
3791
	 * Resolves the appropriate store and calendar folder for delegate scenarios.
3792
	 *
3793
	 * When a meeting request is received by a delegate, this method opens the
3794
	 * delegator's store and calendar folder instead of the delegate's own.
3795
	 *
3796
	 * @param array $messageprops message properties containing delegate information
3797
	 *
3798
	 * @return array{store: mixed, calFolder: mixed} array with 'store' and 'calFolder' keys
3799
	 */
3800
	private function resolveDelegateStoreAndCalendar(array $messageprops): array {
3801
		$store = $this->store;
3802
		$calFolder = $this->openDefaultCalendar();
3803
3804
		// If this meeting request is received by a delegate then open delegator's store
3805
		if (isset($messageprops[PR_RCVD_REPRESENTING_ENTRYID])) {
3806
			$delegatorStore = $this->getDelegatorStore($messageprops[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
3807
			if (!empty($delegatorStore['store'])) {
3808
				$store = $delegatorStore['store'];
3809
			}
3810
			if (!empty($delegatorStore[PR_IPM_APPOINTMENT_ENTRYID])) {
3811
				$calFolder = $delegatorStore[PR_IPM_APPOINTMENT_ENTRYID];
3812
			}
3813
		}
3814
3815
		return ['store' => $store, 'calFolder' => $calFolder];
3816
	}
3817
3818
	/**
3819
	 * Gets all recipients from a message.
3820
	 *
3821
	 * @param resource $message     the message to get recipients from
3822
	 * @param array    $restriction optional restriction to filter recipients
3823
	 *
3824
	 * @return array array of recipient rows
3825
	 */
3826
	private function getMessageRecipients(mixed $message, ?array $restriction = null): array {
3827
		$recipientTable = mapi_message_getrecipienttable($message);
3828
3829
		return empty($restriction) ?
3830
			mapi_table_queryallrows($recipientTable, $this->recipprops) :
3831
			mapi_table_queryallrows($recipientTable, $this->recipprops, $restriction);
3832
	}
3833
3834
	/**
3835
	 * Ensures calendar write access and throws exception if denied.
3836
	 *
3837
	 * @param mixed $store the store to check calendar write access for
3838
	 *
3839
	 * @throws MAPIException with MAPI_E_NO_ACCESS if write access is denied
3840
	 */
3841
	private function ensureCalendarWriteAccess(mixed $store): void {
3842
		if ($this->checkCalendarWriteAccess($store) !== true) {
3843
			throw new MAPIException(_("Insufficient permissions"), MAPI_E_NO_ACCESS);
3844
		}
3845
	}
3846
}
3847