Meetingrequest::doAccept()   F
last analyzed

Complexity

Conditions 28
Paths 377

Size

Total Lines 102
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 1 Features 0
Metric Value
cc 28
eloc 51
c 5
b 1
f 0
nc 377
nop 10
dl 0
loc 102
rs 1.1208

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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
	private $mti_html;
116
117
	/**
118
	 * @var null|bool
119
	 */
120
	private $includesResources;
121
	private $nonAcceptingResources;
122
	private $recipientDisplayname;
123
124
	/**
125
	 * Constructor.
126
	 *
127
	 * Takes a store and a message. The message is an appointment item
128
	 * that should be converted into a meeting request or an incoming
129
	 * e-mail message that is a meeting request.
130
	 *
131
	 * The $session variable is optional, but required if the following features
132
	 * are to be used:
133
	 *
134
	 * - Sending meeting requests for meetings that are not in your own store
135
	 * - Sending meeting requests to resources, resource availability checking and resource freebusy updates
136
	 *
137
	 * @param mixed $store
138
	 * @param mixed $message
139
	 * @param mixed $session
140
	 * @param mixed $enableDirectBooking
141
	 */
142
	public function __construct(private $store, public $message, private $session = false, private $enableDirectBooking = true) {
143
		// This variable string saves time information for the MR.
144
		$this->meetingTimeInfo = false;
145
146
		$properties = [];
147
		$properties['goid'] = 'PT_BINARY:PSETID_Meeting:0x3';
148
		$properties['goid2'] = 'PT_BINARY:PSETID_Meeting:0x23';
149
		$properties['type'] = 'PT_STRING8:PSETID_Meeting:0x24';
150
		$properties['meetingrecurring'] = 'PT_BOOLEAN:PSETID_Meeting:0x5';
151
		$properties['unknown2'] = 'PT_BOOLEAN:PSETID_Meeting:0xa';
152
		$properties['attendee_critical_change'] = 'PT_SYSTIME:PSETID_Meeting:0x1';
153
		$properties['owner_critical_change'] = 'PT_SYSTIME:PSETID_Meeting:0x1a';
154
		$properties['meetingstatus'] = 'PT_LONG:PSETID_Appointment:' . PidLidAppointmentStateFlags;
155
		$properties['responsestatus'] = 'PT_LONG:PSETID_Appointment:0x8218';
156
		$properties['unknown6'] = 'PT_LONG:PSETID_Meeting:0x4';
157
		$properties['replytime'] = 'PT_SYSTIME:PSETID_Appointment:0x8220';
158
		$properties['usetnef'] = 'PT_BOOLEAN:PSETID_Common:0x8582';
159
		$properties['recurrence_data'] = 'PT_BINARY:PSETID_Appointment:' . PidLidAppointmentRecur;
160
		$properties['reminderminutes'] = 'PT_LONG:PSETID_Common:' . PidLidReminderDelta;
161
		$properties['reminderset'] = 'PT_BOOLEAN:PSETID_Common:' . PidLidReminderSet;
162
		$properties['sendasical'] = 'PT_BOOLEAN:PSETID_Appointment:0x8200';
163
		$properties['updatecounter'] = 'PT_LONG:PSETID_Appointment:' . PidLidAppointmentSequence;					// AppointmentSequenceNumber
164
		$properties['unknown7'] = 'PT_LONG:PSETID_Appointment:0x8202';
165
		$properties['last_updatecounter'] = 'PT_LONG:PSETID_Appointment:0x8203';			// AppointmentLastSequence
166
		$properties['busystatus'] = 'PT_LONG:PSETID_Appointment:' . PidLidBusyStatus;
167
		$properties['intendedbusystatus'] = 'PT_LONG:PSETID_Appointment:' . PidLidIntendedBusyStatus;
168
		$properties['start'] = 'PT_SYSTIME:PSETID_Appointment:' . PidLidAppointmentStartWhole;
169
		$properties['responselocation'] = 'PT_STRING8:PSETID_Meeting:0x2';
170
		$properties['location'] = 'PT_STRING8:PSETID_Appointment:' . PidLidLocation;
171
		$properties['requestsent'] = 'PT_BOOLEAN:PSETID_Appointment:0x8229';		// PidLidFInvited, MeetingRequestWasSent
172
		$properties['startdate'] = 'PT_SYSTIME:PSETID_Appointment:' . PidLidAppointmentStartWhole;
173
		$properties['duedate'] = 'PT_SYSTIME:PSETID_Appointment:' . PidLidAppointmentEndWhole;
174
		$properties['flagdueby'] = 'PT_SYSTIME:PSETID_Common:' . PidLidReminderSignalTime;
175
		$properties['commonstart'] = 'PT_SYSTIME:PSETID_Common:0x8516';
176
		$properties['commonend'] = 'PT_SYSTIME:PSETID_Common:0x8517';
177
		$properties['recurring'] = 'PT_BOOLEAN:PSETID_Appointment:' . PidLidRecurring;
178
		$properties['clipstart'] = 'PT_SYSTIME:PSETID_Appointment:' . PidLidClipStart;
179
		$properties['clipend'] = 'PT_SYSTIME:PSETID_Appointment:' . PidLidClipEnd;
180
		$properties['start_recur_date'] = 'PT_LONG:PSETID_Meeting:0xD';				// StartRecurTime
181
		$properties['start_recur_time'] = 'PT_LONG:PSETID_Meeting:0xE';				// StartRecurTime
182
		$properties['end_recur_date'] = 'PT_LONG:PSETID_Meeting:0xF';				// EndRecurDate
183
		$properties['end_recur_time'] = 'PT_LONG:PSETID_Meeting:0x10';				// EndRecurTime
184
		$properties['is_exception'] = 'PT_BOOLEAN:PSETID_Meeting:0xA';				// LID_IS_EXCEPTION
185
		$properties['apptreplyname'] = 'PT_STRING8:PSETID_Appointment:0x8230';
186
		// Propose new time properties
187
		$properties['proposed_start_whole'] = 'PT_SYSTIME:PSETID_Appointment:' . PidLidAppointmentProposedStartWhole;
188
		$properties['proposed_end_whole'] = 'PT_SYSTIME:PSETID_Appointment:' . PidLidAppointmentProposedEndWhole;
189
		$properties['proposed_duration'] = 'PT_LONG:PSETID_Appointment:0x8256';
190
		$properties['counter_proposal'] = 'PT_BOOLEAN:PSETID_Appointment:' . PidLidAppointmentCounterProposal;
191
		$properties['recurring_pattern'] = 'PT_STRING8:PSETID_Appointment:' . PidLidRecurrencePattern;
192
		$properties['basedate'] = 'PT_SYSTIME:PSETID_Appointment:' . PidLidExceptionReplaceTime;
193
		$properties['meetingtype'] = 'PT_LONG:PSETID_Meeting:0x26';
194
		$properties['timezone_data'] = 'PT_BINARY:PSETID_Appointment:' . PidLidTimeZoneStruct;
195
		$properties['timezone'] = 'PT_STRING8:PSETID_Appointment:' . PidLidTimeZoneDescription;
196
		$properties['categories'] = 'PT_MV_STRING8:PS_PUBLIC_STRINGS:Keywords';
197
		$properties['private'] = 'PT_BOOLEAN:PSETID_Common:' . PidLidPrivate;
198
		$properties['alldayevent'] = 'PT_BOOLEAN:PSETID_Appointment:' . PidLidAppointmentSubType;
199
		$properties['toattendeesstring'] = 'PT_STRING8:PSETID_Appointment:0x823B';
200
		$properties['ccattendeesstring'] = 'PT_STRING8:PSETID_Appointment:0x823C';
201
202
		$this->proptags = getPropIdsFromStrings($this->store, $properties);
203
	}
204
205
	/**
206
	 * Sets the direct booking property. This is an alternative to the setting of the direct booking
207
	 * property through the constructor. However, setting it in the constructor is preferred.
208
	 *
209
	 * @param bool $directBookingSetting
210
	 */
211
	public function setDirectBooking($directBookingSetting): void {
212
		$this->enableDirectBooking = $directBookingSetting;
213
	}
214
215
	/**
216
	 * Returns TRUE if the message pointed to is an incoming meeting request and should
217
	 * therefore be replied to with doAccept or doDecline().
218
	 *
219
	 * @param false|string $messageClass message class to use for checking
220
	 *
221
	 * @return bool returns true if this is a meeting request else false
222
	 */
223
	public function isMeetingRequest(false|string $messageClass = false): bool {
224
		if ($messageClass === false) {
225
			$messageClass = mapi_getprops($this->message, [PR_MESSAGE_CLASS])[PR_MESSAGE_CLASS] ?? false;
226
		}
227
228
		return $messageClass !== false && stripos($messageClass, 'ipm.schedule.meeting.request') === 0;
229
	}
230
231
	/**
232
	 * Returns TRUE if the message pointed to is a returning meeting request response.
233
	 *
234
	 * @param false|string $messageClass message class to use for checking
235
	 *
236
	 * @return bool returns true if this is a meeting request else false
237
	 */
238
	public function isMeetingRequestResponse(false|string $messageClass = false): bool {
239
		if ($messageClass === false) {
240
			$messageClass = mapi_getprops($this->message, [PR_MESSAGE_CLASS])[PR_MESSAGE_CLASS] ?? false;
241
		}
242
243
		return $messageClass !== false && stripos($messageClass, 'ipm.schedule.meeting.resp') === 0;
244
	}
245
246
	/**
247
	 * Returns TRUE if the message pointed to is a cancellation request.
248
	 *
249
	 * @param false|string $messageClass message class to use for checking
250
	 *
251
	 * @return bool returns true if this is a meeting request else false
252
	 */
253
	public function isMeetingCancellation(false|string $messageClass = false): bool {
254
		if ($messageClass === false) {
255
			$messageClass = mapi_getprops($this->message, [PR_MESSAGE_CLASS])[PR_MESSAGE_CLASS] ?? false;
256
		}
257
258
		return $messageClass !== false && stripos($messageClass, 'ipm.schedule.meeting.canceled') === 0;
259
	}
260
261
	/**
262
	 * Function is used to get the last update counter of meeting request.
263
	 *
264
	 * @return false|int false when last_updatecounter not found else return last_updatecounter
265
	 */
266
	public function getLastUpdateCounter(): false|int {
267
		$calendarItemProps = mapi_getprops($this->message, [$this->proptags['last_updatecounter']]);
268
269
		return $calendarItemProps[$this->proptags['last_updatecounter']] ?? false;
270
	}
271
272
	/**
273
	 * Process an incoming meeting request response. This updates the appointment
274
	 * in your calendar to show whether the user has accepted or declined.
275
	 */
276
	public function processMeetingRequestResponse(): void {
277
		if (!$this->isMeetingRequestResponse()) {
278
			return;
279
		}
280
281
		if (!$this->isLocalOrganiser()) {
282
			return;
283
		}
284
285
		// Get information we need from the response message
286
		$messageprops = mapi_getprops($this->message, [
287
			$this->proptags['goid'],
288
			$this->proptags['goid2'],
289
			PR_OWNER_APPT_ID,
290
			PR_SENT_REPRESENTING_EMAIL_ADDRESS,
291
			PR_SENT_REPRESENTING_NAME,
292
			PR_SENT_REPRESENTING_ADDRTYPE,
293
			PR_SENT_REPRESENTING_ENTRYID,
294
			PR_SENT_REPRESENTING_SEARCH_KEY,
295
			PR_MESSAGE_DELIVERY_TIME,
296
			PR_MESSAGE_CLASS,
297
			PR_PROCESSED,
298
			PR_RCVD_REPRESENTING_ENTRYID,
299
			$this->proptags['proposed_start_whole'],
300
			$this->proptags['proposed_end_whole'],
301
			$this->proptags['proposed_duration'],
302
			$this->proptags['counter_proposal'],
303
			$this->proptags['attendee_critical_change'],
304
		]);
305
306
		$goid2 = $messageprops[$this->proptags['goid2']];
307
308
		if (!isset($goid2) || !isset($messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS])) {
309
			return;
310
		}
311
312
		// Find basedate in GlobalID(0x3), this can be a response for an occurrence
313
		$basedate = $this->getBasedateFromGlobalID($messageprops[$this->proptags['goid']]);
314
315
		$userStore = $this->store;
316
		// check if delegate is processing the response
317
		if (isset($messageprops[PR_RCVD_REPRESENTING_ENTRYID])) {
318
			$delegatorStore = $this->getDelegatorStore($messageprops[PR_RCVD_REPRESENTING_ENTRYID], [PR_IPM_APPOINTMENT_ENTRYID]);
319
			if (!empty($delegatorStore['store'])) {
320
				$userStore = $delegatorStore['store'];
321
			}
322
		}
323
324
		// check for calendar access
325
		$this->ensureCalendarWriteAccess($userStore);
326
327
		$calendarItem = $this->getCorrespondentCalendarItem(true);
328
329
		// Open the calendar items, and update all the recipients of the calendar item that match
330
		// the email address of the response.
331
		if ($calendarItem !== false) {
332
			$this->processResponse($userStore, $calendarItem, $basedate, $messageprops);
333
		}
334
	}
335
336
	/**
337
	 * Process every incoming MeetingRequest response.This updates the appointment
338
	 * in your calendar to show whether the user has accepted or declined.
339
	 *
340
	 * @param resource  $store        contains the userStore in which the meeting is created
341
	 * @param mixed     $calendarItem resource of the calendar item for which this response has arrived
342
	 * @param false|int $basedate     if present the create an exception
343
	 * @param array     $messageprops contains message properties
344
	 */
345
	public function processResponse(mixed $store, mixed $calendarItem, mixed $basedate, array $messageprops): ?false {
346
		$senderentryid = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
347
		$messageclass = $messageprops[PR_MESSAGE_CLASS];
348
		$deliverytime = $messageprops[PR_MESSAGE_DELIVERY_TIME];
349
		$recurringItem = 0;
350
351
		// Open the calendar item, find the sender in the recipient table and update all the recipients of the calendar item that match
352
		// the email address of the response.
353
		$calendarItemProps = mapi_getprops($calendarItem, [$this->proptags['recurring'], PR_STORE_ENTRYID, PR_PARENT_ENTRYID, PR_ENTRYID, $this->proptags['updatecounter']]);
354
355
		// check if meeting response is already processed
356
		if (isset($messageprops[PR_PROCESSED]) && $messageprops[PR_PROCESSED] === true) {
357
			// meeting is already processed
358
			return null;
359
		}
360
		mapi_setprops($this->message, [PR_PROCESSED => true]);
361
		mapi_savechanges($this->message);
362
363
		// if meeting is updated in organizer's calendar then we don't need to process
364
		// old response
365
		if ($this->isMeetingUpdated($basedate)) {
366
			return null;
367
		}
368
369
		// If basedate is found, then create/modify exception msg and do processing
370
		if ($basedate && !empty($calendarItemProps[$this->proptags['recurring']])) {
371
			$recurr = new Recurrence($store, $calendarItem);
372
373
			// Copy properties from meeting request
374
			$exception_props = mapi_getprops($this->message, [
375
				PR_OWNER_APPT_ID,
376
				$this->proptags['proposed_start_whole'],
377
				$this->proptags['proposed_end_whole'],
378
				$this->proptags['proposed_duration'],
379
				$this->proptags['counter_proposal'],
380
			]);
381
382
			// Create/modify exception
383
			if ($recurr->isException($basedate)) {
384
				$recurr->modifyException($exception_props, $basedate);
385
			}
386
			else {
387
				// When we are creating an exception we need copy recipients from main recurring item
388
				$recips = $this->getMessageRecipients($calendarItem);
389
390
				// Retrieve actual start/due dates from calendar item.
391
				$exception_props[$this->proptags['startdate']] = $recurr->getOccurrenceStart($basedate);
392
				$exception_props[$this->proptags['duedate']] = $recurr->getOccurrenceEnd($basedate);
393
394
				$recurr->createException($exception_props, $basedate, false, $recips);
395
			}
396
397
			mapi_savechanges($calendarItem);
398
399
			$attach = $recurr->getExceptionAttachment($basedate);
400
			if ($attach) {
401
				$recurringItem = $calendarItem;
402
				$calendarItem = mapi_attach_openobj($attach, MAPI_MODIFY);
403
			}
404
			else {
405
				return false;
406
			}
407
		}
408
409
		// Get the recipients of the calendar item
410
		$recipients = $this->getMessageRecipients($calendarItem);
411
412
		// FIXME we should look at the updatecounter property and compare it
413
		// to the counter in the recipient to see if this update is actually
414
		// newer than the status in the calendar item
415
		$found = false;
416
417
		$totalrecips = 0;
418
		$acceptedrecips = 0;
419
		foreach ($recipients as $recipient) {
420
			++$totalrecips;
421
			// external recipients might not have entryid
422
			if (!isset($recipient[PR_ENTRYID]) &&
423
				$recipient[PR_EMAIL_ADDRESS] == $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS]) {
424
				$recipient[PR_ENTRYID] = $senderentryid;
425
			}
426
			if (isset($recipient[PR_ENTRYID]) && $this->compareABEntryIDs($recipient[PR_ENTRYID], $senderentryid)) {
427
				$found = true;
428
429
				/*
430
				 * If value of attendee_critical_change on meeting response mail is less than PR_RECIPIENT_TRACKSTATUS_TIME
431
				 * on the corresponding recipientRow of meeting then we ignore this response mail.
432
				 */
433
				if (isset($recipient[PR_RECIPIENT_TRACKSTATUS_TIME]) && ($messageprops[$this->proptags['attendee_critical_change']] < $recipient[PR_RECIPIENT_TRACKSTATUS_TIME])) {
434
					continue;
435
				}
436
437
				// The email address matches, update the row
438
				$recipient[PR_RECIPIENT_TRACKSTATUS] = $this->getTrackStatus($messageclass);
439
				if (isset($messageprops[$this->proptags['attendee_critical_change']])) {
440
					$recipient[PR_RECIPIENT_TRACKSTATUS_TIME] = $messageprops[$this->proptags['attendee_critical_change']];
441
				}
442
443
				// If this is a counter proposal, set the proposal properties in the recipient row
444
				if (isset($messageprops[$this->proptags['counter_proposal']]) && $messageprops[$this->proptags['counter_proposal']]) {
445
					$recipient[PR_RECIPIENT_PROPOSEDSTARTTIME] = $messageprops[$this->proptags['proposed_start_whole']];
446
					$recipient[PR_RECIPIENT_PROPOSEDENDTIME] = $messageprops[$this->proptags['proposed_end_whole']];
447
					$recipient[PR_RECIPIENT_PROPOSED] = $messageprops[$this->proptags['counter_proposal']];
448
				}
449
450
				// Update the recipient information
451
				mapi_message_modifyrecipients($calendarItem, MODRECIP_REMOVE, [$recipient]);
452
				mapi_message_modifyrecipients($calendarItem, MODRECIP_ADD, [$recipient]);
453
			}
454
			if (isset($recipient[PR_RECIPIENT_TRACKSTATUS]) && $recipient[PR_RECIPIENT_TRACKSTATUS] == olRecipientTrackStatusAccepted) {
455
				++$acceptedrecips;
456
			}
457
		}
458
459
		// If the recipient was not found in the original calendar item,
460
		// then add the recpient as a new optional recipient
461
		if (!$found) {
462
			$recipient = [];
463
			$recipient[PR_ENTRYID] = $messageprops[PR_SENT_REPRESENTING_ENTRYID];
464
			$recipient[PR_EMAIL_ADDRESS] = $messageprops[PR_SENT_REPRESENTING_EMAIL_ADDRESS];
465
			$recipient[PR_DISPLAY_NAME] = $messageprops[PR_SENT_REPRESENTING_NAME];
466
			$recipient[PR_ADDRTYPE] = $messageprops[PR_SENT_REPRESENTING_ADDRTYPE];
467
			$recipient[PR_RECIPIENT_TYPE] = MAPI_CC;
468
			$recipient[PR_SEARCH_KEY] = $messageprops[PR_SENT_REPRESENTING_SEARCH_KEY];
469
			$recipient[PR_RECIPIENT_TRACKSTATUS] = $this->getTrackStatus($messageclass);
470
			$recipient[PR_RECIPIENT_TRACKSTATUS_TIME] = $deliverytime;
471
472
			// If this is a counter proposal, set the proposal properties in the recipient row
473
			if (isset($messageprops[$this->proptags['counter_proposal']])) {
474
				$recipient[PR_RECIPIENT_PROPOSEDSTARTTIME] = $messageprops[$this->proptags['proposed_start_whole']];
475
				$recipient[PR_RECIPIENT_PROPOSEDENDTIME] = $messageprops[$this->proptags['proposed_end_whole']];
476
				$recipient[PR_RECIPIENT_PROPOSED] = $messageprops[$this->proptags['counter_proposal']];
477
			}
478
479
			mapi_message_modifyrecipients($calendarItem, MODRECIP_ADD, [$recipient]);
480
			++$totalrecips;
481
			if ($recipient[PR_RECIPIENT_TRACKSTATUS] == olRecipientTrackStatusAccepted) {
482
				++$acceptedrecips;
483
			}
484
		}
485
486
		// TODO: Update counter proposal number property on message
487
		/*
488
		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.
489
		*/
490
		// If this is a counter proposal, set the counter proposal indicator boolean
491
		if (isset($messageprops[$this->proptags['counter_proposal']])) {
492
			$props = [$this->proptags['counter_proposal'] => (bool) $messageprops[$this->proptags['counter_proposal']]];
493
			mapi_setprops($calendarItem, $props);
494
		}
495
496
		mapi_savechanges($calendarItem);
497
		if (isset($attach)) {
498
			mapi_savechanges($attach);
499
			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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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